mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-08 08:55:22 +00:00
- HIGH: GetByKeyForAuth 的 WithGroup 显式 Select 补 web_search_price_per_call 列, 否则 auth 快照里分组覆盖价恒为 nil,覆盖价/免费配置在实际计费中静默失效 - MEDIUM: 按次搜索倍率改用不含高峰因子的基础倍率(与 image/video 按次不变式 及分组表单价格预览承诺一致),calculateOpenAIRecordUsageCost 新增独立倍率参数 - LOW: 计费门槛从 <400 收紧为严格 2xx,1xx/3xx 透传不计费
166 lines
5.3 KiB
Go
166 lines
5.3 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/tidwall/gjson"
|
||
)
|
||
|
||
const (
|
||
chatgptCodexAlphaSearchURL = "https://chatgpt.com/backend-api/codex/alpha/search"
|
||
openAIPlatformAlphaSearchURL = "https://api.openai.com/v1/alpha/search"
|
||
)
|
||
|
||
// ForwardAlphaSearch proxies Codex standalone web search without binding the
|
||
// evolving alpha request or response schema.
|
||
//
|
||
// 返回值约定:仅当上游返回 2xx(一次真实成功的搜索)时返回非 nil 的
|
||
// *OpenAIForwardResult(WebSearchCalls=1,供按次计费);上游错误被原样透传
|
||
// 给客户端时返回 (nil, nil),不产生计费。
|
||
func (s *OpenAIGatewayService) ForwardAlphaSearch(ctx context.Context, c *gin.Context, account *Account, body []byte) (*OpenAIForwardResult, error) {
|
||
if s == nil || c == nil || account == nil {
|
||
return nil, fmt.Errorf("service, context, and account are required")
|
||
}
|
||
modelResult := gjson.GetBytes(body, "model")
|
||
requestedModel := strings.TrimSpace(modelResult.String())
|
||
if modelResult.Type != gjson.String || requestedModel == "" {
|
||
return nil, fmt.Errorf("model is required")
|
||
}
|
||
|
||
upstreamModel := normalizeOpenAIModelForUpstream(account, account.GetMappedModel(requestedModel))
|
||
if upstreamModel != "" && upstreamModel != requestedModel {
|
||
body = ReplaceModelInBody(body, upstreamModel)
|
||
}
|
||
|
||
token, _, err := s.GetAccessToken(ctx, account)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req, err := s.buildOpenAIAlphaSearchRequest(ctx, c, account, body, token)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
proxyURL := ""
|
||
if account.ProxyID != nil && account.Proxy != nil {
|
||
proxyURL = account.Proxy.URL()
|
||
}
|
||
upstreamStart := time.Now()
|
||
resp, err := s.httpUpstream.Do(req, proxyURL, account.ID, account.Concurrency)
|
||
SetOpsLatencyMs(c, OpsUpstreamLatencyMsKey, time.Since(upstreamStart).Milliseconds())
|
||
if err != nil {
|
||
return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, true)
|
||
}
|
||
defer func() { _ = resp.Body.Close() }()
|
||
|
||
respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read alpha search response: %w", err)
|
||
}
|
||
|
||
if resp.StatusCode >= http.StatusBadRequest {
|
||
upstreamMessage := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(respBody)))
|
||
if s.shouldFailoverOpenAIUpstreamResponse(resp.StatusCode, upstreamMessage, respBody) {
|
||
resp.Body = io.NopCloser(bytes.NewReader(respBody))
|
||
s.handleFailoverSideEffects(ctx, resp, account, respBody, upstreamModel)
|
||
return nil, &UpstreamFailoverError{
|
||
StatusCode: resp.StatusCode,
|
||
ResponseBody: respBody,
|
||
RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
|
||
}
|
||
}
|
||
}
|
||
|
||
if !account.IsShadow() {
|
||
s.UpdateCodexUsageSnapshotFromHeaders(ctx, account.ID, resp.Header)
|
||
}
|
||
writeOpenAIPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
|
||
contentType := resp.Header.Get("Content-Type")
|
||
if contentType == "" {
|
||
contentType = "application/json"
|
||
}
|
||
c.Data(resp.StatusCode, contentType, respBody)
|
||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||
// 非 2xx(错误/重定向)已原样透传给客户端:不是一次成功的搜索,不计费。
|
||
return nil, nil
|
||
}
|
||
return &OpenAIForwardResult{
|
||
RequestID: strings.TrimSpace(resp.Header.Get("x-request-id")),
|
||
Model: requestedModel,
|
||
UpstreamModel: upstreamModel,
|
||
Duration: time.Since(upstreamStart),
|
||
WebSearchCalls: 1,
|
||
}, nil
|
||
}
|
||
|
||
func (s *OpenAIGatewayService) buildOpenAIAlphaSearchRequest(ctx context.Context, c *gin.Context, account *Account, body []byte, token string) (*http.Request, error) {
|
||
clientBeta := ""
|
||
if c != nil {
|
||
clientBeta = c.GetHeader("OpenAI-Beta")
|
||
}
|
||
req, err := s.buildUpstreamRequestOpenAIPassthrough(ctx, c, account, body, token)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
targetURL, err := s.openAIAlphaSearchURL(account)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
parsedURL, err := url.Parse(targetURL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse alpha search URL: %w", err)
|
||
}
|
||
if c != nil && c.Request != nil && c.Request.URL != nil {
|
||
query := parsedURL.Query()
|
||
for key, values := range c.Request.URL.Query() {
|
||
for _, value := range values {
|
||
query.Add(key, value)
|
||
}
|
||
}
|
||
parsedURL.RawQuery = query.Encode()
|
||
}
|
||
req.URL = parsedURL
|
||
req.Header.Set("Accept", "application/json")
|
||
if clientBeta == "" {
|
||
req.Header.Del("OpenAI-Beta")
|
||
}
|
||
if version := strings.TrimSpace(c.GetHeader("Version")); version != "" {
|
||
req.Header.Set("Version", version)
|
||
} else if account.Type == AccountTypeOAuth {
|
||
req.Header.Set("Version", codexCLIVersion)
|
||
}
|
||
return req, nil
|
||
}
|
||
|
||
func (s *OpenAIGatewayService) openAIAlphaSearchURL(account *Account) (string, error) {
|
||
if account == nil {
|
||
return "", fmt.Errorf("account is required")
|
||
}
|
||
switch account.Type {
|
||
case AccountTypeOAuth:
|
||
return chatgptCodexAlphaSearchURL, nil
|
||
case AccountTypeAPIKey:
|
||
baseURL := account.GetOpenAIBaseURL()
|
||
if baseURL == "" {
|
||
return openAIPlatformAlphaSearchURL, nil
|
||
}
|
||
validatedURL, err := s.validateUpstreamBaseURL(baseURL)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return buildOpenAIEndpointURL(validatedURL, "/v1/alpha/search"), nil
|
||
default:
|
||
return "", fmt.Errorf("unsupported OpenAI account type: %s", account.Type)
|
||
}
|
||
}
|