Files
new-api/controller/codex_usage.go
CaIon c6741c36a9 feat(channel): add a per-channel switch to skip upstream TLS verification
Until now only the global TLS_INSECURE_SKIP_VERIFY setting could accept a
self-signed upstream certificate. A channel's tls_insecure_skip_verify
setting now does it for that channel's API requests: relay and the Responses
and Realtime WebSocket dialer, task submission and polling, plugin fetches,
balance, usage and model-list queries, and media proxied through the channel
proxy. Credential exchanges (Codex OAuth refresh, Vertex and plugin
service-account tokens) keep verifying certificates. The channel drawer shows
the switch under the HTTP protocol settings.
2026-10-10 14:02:06 +08:00

167 lines
4.6 KiB
Go

package controller
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/relay/channel/codex"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
)
func GetCodexChannelUsage(c *gin.Context) {
fetchCodexChannelWhamData(
c,
service.FetchCodexWhamUsage,
common.LogText("failed to fetch codex usage"),
"Failed to fetch usage. Please try again later",
)
}
func GetCodexChannelRateLimitResetCredits(c *gin.Context) {
fetchCodexChannelWhamData(
c,
service.FetchCodexWhamRateLimitResetCredits,
common.LogText("failed to fetch codex reset credits"),
"Failed to fetch reset credit details. Please try again later",
)
}
func ResetCodexChannelUsage(c *gin.Context) {
fetchCodexChannelWhamData(
c,
service.ConsumeCodexWhamRateLimitResetCredit,
common.LogText("failed to reset codex usage"),
"Failed to reset usage. Please try again later",
)
}
type codexWhamFetchFunc func(
ctx context.Context,
client *http.Client,
baseURL string,
accessToken string,
accountID string,
) (statusCode int, body []byte, err error)
func fetchCodexChannelWhamData(
c *gin.Context,
fetch codexWhamFetchFunc,
logPrefix string,
userMessage string,
) {
channelId, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, fmt.Errorf("invalid channel id: %w", err))
return
}
ch, err := model.GetChannelById(channelId, true)
if err != nil {
common.ApiError(c, err)
return
}
if ch == nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "channel not found"})
return
}
if ch.Type != constant.ChannelTypeCodex {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "channel type is not Codex"})
return
}
if ch.ChannelInfo.IsMultiKey {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "multi-key channel is not supported"})
return
}
oauthKey, err := codex.ParseOAuthKey(strings.TrimSpace(ch.Key))
if err != nil {
common.SysError(common.LogText("failed to parse oauth key: %s", err.Error()))
common.ApiErrorT(c, "Failed to parse credentials. Please check the channel configuration")
return
}
accessToken := strings.TrimSpace(oauthKey.AccessToken)
accountID := strings.TrimSpace(oauthKey.AccountID)
if accessToken == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "codex channel: access_token is required"})
return
}
if accountID == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "codex channel: account_id is required"})
return
}
setting := ch.GetSetting()
client, err := service.GetHttpClientWithProxySettings(setting.Proxy, setting)
if err != nil {
common.ApiError(c, err)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
statusCode, body, err := fetch(ctx, client, ch.GetBaseURL(), accessToken, accountID)
if err != nil {
common.SysError(logPrefix + ": " + err.Error())
common.ApiErrorT(c, userMessage)
return
}
if (statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden) && strings.TrimSpace(oauthKey.RefreshToken) != "" {
refreshCtx, refreshCancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
defer refreshCancel()
res, refreshErr := service.RefreshCodexOAuthTokenWithProxy(refreshCtx, oauthKey.RefreshToken, setting.Proxy)
if refreshErr == nil {
oauthKey.AccessToken = res.AccessToken
oauthKey.RefreshToken = res.RefreshToken
oauthKey.LastRefresh = time.Now().Format(time.RFC3339)
oauthKey.Expired = res.ExpiresAt.Format(time.RFC3339)
if strings.TrimSpace(oauthKey.Type) == "" {
oauthKey.Type = "codex"
}
encoded, encErr := common.Marshal(oauthKey)
if encErr == nil {
_ = model.DB.Model(&model.Channel{}).Where("id = ?", ch.Id).Update("key", string(encoded)).Error
model.InitChannelCache()
}
ctx2, cancel2 := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel2()
statusCode, body, err = fetch(ctx2, client, ch.GetBaseURL(), oauthKey.AccessToken, accountID)
if err != nil {
common.SysError(common.LogText("%s after refresh: %s", logPrefix, err.Error()))
common.ApiErrorT(c, userMessage)
return
}
}
}
var payload any
if common.Unmarshal(body, &payload) != nil {
payload = string(body)
}
ok := statusCode >= 200 && statusCode < 300
resp := gin.H{
"success": ok,
"message": "",
"upstream_status": statusCode,
"data": payload,
}
if !ok {
resp["message"] = fmt.Sprintf("upstream status: %d", statusCode)
}
c.JSON(http.StatusOK, resp)
}