refactor(runtime): move executor utilities to helps package and update references

This commit is contained in:
Luis Pater
2026-04-01 03:08:20 +08:00
parent 51fd58d74f
commit d2c7e4e96a
27 changed files with 712 additions and 676 deletions
+36 -35
View File
@@ -14,6 +14,7 @@ import (
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay" "github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -115,8 +116,8 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"} return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
} }
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
translatedReq, body, err := e.translateRequest(req, opts, false) translatedReq, body, err := e.translateRequest(req, opts, false)
if err != nil { if err != nil {
@@ -137,7 +138,7 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: endpoint, URL: endpoint,
Method: http.MethodPost, Method: http.MethodPost,
Headers: wsReq.Headers.Clone(), Headers: wsReq.Headers.Clone(),
@@ -151,17 +152,17 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
wsResp, err := e.relay.NonStream(ctx, authID, wsReq) wsResp, err := e.relay.NonStream(ctx, authID, wsReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
recordAPIResponseMetadata(ctx, e.cfg, wsResp.Status, wsResp.Headers.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, wsResp.Status, wsResp.Headers.Clone())
if len(wsResp.Body) > 0 { if len(wsResp.Body) > 0 {
appendAPIResponseChunk(ctx, e.cfg, wsResp.Body) helps.AppendAPIResponseChunk(ctx, e.cfg, wsResp.Body)
} }
if wsResp.Status < 200 || wsResp.Status >= 300 { if wsResp.Status < 200 || wsResp.Status >= 300 {
return resp, statusErr{code: wsResp.Status, msg: string(wsResp.Body)} return resp, statusErr{code: wsResp.Status, msg: string(wsResp.Body)}
} }
reporter.publish(ctx, parseGeminiUsage(wsResp.Body)) reporter.Publish(ctx, helps.ParseGeminiUsage(wsResp.Body))
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, wsResp.Body, &param) out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, wsResp.Body, &param)
resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON(out), Headers: wsResp.Headers.Clone()} resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON(out), Headers: wsResp.Headers.Clone()}
@@ -174,8 +175,8 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"} return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
} }
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
translatedReq, body, err := e.translateRequest(req, opts, true) translatedReq, body, err := e.translateRequest(req, opts, true)
if err != nil { if err != nil {
@@ -195,7 +196,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: endpoint, URL: endpoint,
Method: http.MethodPost, Method: http.MethodPost,
Headers: wsReq.Headers.Clone(), Headers: wsReq.Headers.Clone(),
@@ -208,24 +209,24 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
}) })
wsStream, err := e.relay.Stream(ctx, authID, wsReq) wsStream, err := e.relay.Stream(ctx, authID, wsReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
firstEvent, ok := <-wsStream firstEvent, ok := <-wsStream
if !ok { if !ok {
err = fmt.Errorf("wsrelay: stream closed before start") err = fmt.Errorf("wsrelay: stream closed before start")
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
if firstEvent.Status > 0 && firstEvent.Status != http.StatusOK { if firstEvent.Status > 0 && firstEvent.Status != http.StatusOK {
metadataLogged := false metadataLogged := false
if firstEvent.Status > 0 { if firstEvent.Status > 0 {
recordAPIResponseMetadata(ctx, e.cfg, firstEvent.Status, firstEvent.Headers.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, firstEvent.Status, firstEvent.Headers.Clone())
metadataLogged = true metadataLogged = true
} }
var body bytes.Buffer var body bytes.Buffer
if len(firstEvent.Payload) > 0 { if len(firstEvent.Payload) > 0 {
appendAPIResponseChunk(ctx, e.cfg, firstEvent.Payload) helps.AppendAPIResponseChunk(ctx, e.cfg, firstEvent.Payload)
body.Write(firstEvent.Payload) body.Write(firstEvent.Payload)
} }
if firstEvent.Type == wsrelay.MessageTypeStreamEnd { if firstEvent.Type == wsrelay.MessageTypeStreamEnd {
@@ -233,18 +234,18 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
} }
for event := range wsStream { for event := range wsStream {
if event.Err != nil { if event.Err != nil {
recordAPIResponseError(ctx, e.cfg, event.Err) helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
if body.Len() == 0 { if body.Len() == 0 {
body.WriteString(event.Err.Error()) body.WriteString(event.Err.Error())
} }
break break
} }
if !metadataLogged && event.Status > 0 { if !metadataLogged && event.Status > 0 {
recordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
metadataLogged = true metadataLogged = true
} }
if len(event.Payload) > 0 { if len(event.Payload) > 0 {
appendAPIResponseChunk(ctx, e.cfg, event.Payload) helps.AppendAPIResponseChunk(ctx, e.cfg, event.Payload)
body.Write(event.Payload) body.Write(event.Payload)
} }
if event.Type == wsrelay.MessageTypeStreamEnd { if event.Type == wsrelay.MessageTypeStreamEnd {
@@ -260,23 +261,23 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
metadataLogged := false metadataLogged := false
processEvent := func(event wsrelay.StreamEvent) bool { processEvent := func(event wsrelay.StreamEvent) bool {
if event.Err != nil { if event.Err != nil {
recordAPIResponseError(ctx, e.cfg, event.Err) helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)} out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}
return false return false
} }
switch event.Type { switch event.Type {
case wsrelay.MessageTypeStreamStart: case wsrelay.MessageTypeStreamStart:
if !metadataLogged && event.Status > 0 { if !metadataLogged && event.Status > 0 {
recordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
metadataLogged = true metadataLogged = true
} }
case wsrelay.MessageTypeStreamChunk: case wsrelay.MessageTypeStreamChunk:
if len(event.Payload) > 0 { if len(event.Payload) > 0 {
appendAPIResponseChunk(ctx, e.cfg, event.Payload) helps.AppendAPIResponseChunk(ctx, e.cfg, event.Payload)
filtered := FilterSSEUsageMetadata(event.Payload) filtered := helps.FilterSSEUsageMetadata(event.Payload)
if detail, ok := parseGeminiStreamUsage(filtered); ok { if detail, ok := helps.ParseGeminiStreamUsage(filtered); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, filtered, &param) lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, filtered, &param)
for i := range lines { for i := range lines {
@@ -288,21 +289,21 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
return false return false
case wsrelay.MessageTypeHTTPResp: case wsrelay.MessageTypeHTTPResp:
if !metadataLogged && event.Status > 0 { if !metadataLogged && event.Status > 0 {
recordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
metadataLogged = true metadataLogged = true
} }
if len(event.Payload) > 0 { if len(event.Payload) > 0 {
appendAPIResponseChunk(ctx, e.cfg, event.Payload) helps.AppendAPIResponseChunk(ctx, e.cfg, event.Payload)
} }
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, event.Payload, &param) lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, event.Payload, &param)
for i := range lines { for i := range lines {
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON(lines[i])} out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON(lines[i])}
} }
reporter.publish(ctx, parseGeminiUsage(event.Payload)) reporter.Publish(ctx, helps.ParseGeminiUsage(event.Payload))
return false return false
case wsrelay.MessageTypeError: case wsrelay.MessageTypeError:
recordAPIResponseError(ctx, e.cfg, event.Err) helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)} out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}
return false return false
} }
@@ -345,7 +346,7 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: endpoint, URL: endpoint,
Method: http.MethodPost, Method: http.MethodPost,
Headers: wsReq.Headers.Clone(), Headers: wsReq.Headers.Clone(),
@@ -358,12 +359,12 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
}) })
resp, err := e.relay.NonStream(ctx, authID, wsReq) resp, err := e.relay.NonStream(ctx, authID, wsReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
recordAPIResponseMetadata(ctx, e.cfg, resp.Status, resp.Headers.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, resp.Status, resp.Headers.Clone())
if len(resp.Body) > 0 { if len(resp.Body) > 0 {
appendAPIResponseChunk(ctx, e.cfg, resp.Body) helps.AppendAPIResponseChunk(ctx, e.cfg, resp.Body)
} }
if resp.Status < 200 || resp.Status >= 300 { if resp.Status < 200 || resp.Status >= 300 {
return cliproxyexecutor.Response{}, statusErr{code: resp.Status, msg: string(resp.Body)} return cliproxyexecutor.Response{}, statusErr{code: resp.Status, msg: string(resp.Body)}
@@ -404,8 +405,8 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
return nil, translatedPayload{}, err return nil, translatedPayload{}, err
} }
payload = fixGeminiImageAspectRatio(baseModel, payload) payload = fixGeminiImageAspectRatio(baseModel, payload)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
payload = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel) payload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel)
payload, _ = sjson.DeleteBytes(payload, "generationConfig.maxOutputTokens") payload, _ = sjson.DeleteBytes(payload, "generationConfig.maxOutputTokens")
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseMimeType") payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseMimeType")
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseJsonSchema") payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseJsonSchema")
@@ -24,6 +24,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
@@ -142,7 +143,7 @@ func initAntigravityTransport() {
func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client { func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
antigravityTransportOnce.Do(initAntigravityTransport) antigravityTransportOnce.Do(initAntigravityTransport)
client := newProxyAwareHTTPClient(ctx, cfg, auth, timeout) client := helps.NewProxyAwareHTTPClient(ctx, cfg, auth, timeout)
// If no transport is set, use the shared HTTP/1.1 transport. // If no transport is set, use the shared HTTP/1.1 transport.
if client.Transport == nil { if client.Transport == nil {
client.Transport = antigravityTransport client.Transport = antigravityTransport
@@ -405,12 +406,12 @@ func (e *AntigravityExecutor) attemptCreditsFallback(
httpReq, errReq := e.buildRequest(ctx, auth, token, modelName, creditsPayload, stream, alt, baseURL) httpReq, errReq := e.buildRequest(ctx, auth, token, modelName, creditsPayload, stream, alt, baseURL)
if errReq != nil { if errReq != nil {
recordAPIResponseError(ctx, e.cfg, errReq) helps.RecordAPIResponseError(ctx, e.cfg, errReq)
return nil, true return nil, true
} }
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
return nil, true return nil, true
} }
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices { if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
@@ -420,16 +421,16 @@ func (e *AntigravityExecutor) attemptCreditsFallback(
return httpResp, true return httpResp, true
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
bodyBytes, errRead := io.ReadAll(httpResp.Body) bodyBytes, errRead := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("antigravity executor: close credits fallback response body error: %v", errClose) log.Errorf("antigravity executor: close credits fallback response body error: %v", errClose)
} }
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
return nil, true return nil, true
} }
appendAPIResponseChunk(ctx, e.cfg, bodyBytes) helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) { if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
clearAntigravityPreferCredits(auth, modelName) clearAntigravityPreferCredits(auth, modelName)
markAntigravityCreditsExhausted(auth, now) markAntigravityCreditsExhausted(auth, now)
@@ -457,8 +458,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
auth = updatedAuth auth = updatedAuth
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("antigravity") to := sdktranslator.FromString("antigravity")
@@ -476,8 +477,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
return resp, err return resp, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
baseURLs := antigravityBaseURLFallbackOrder(auth) baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
@@ -507,7 +508,7 @@ attemptLoop:
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) { if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
return resp, errDo return resp, errDo
} }
@@ -522,17 +523,17 @@ attemptLoop:
return resp, err return resp, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
bodyBytes, errRead := io.ReadAll(httpResp.Body) bodyBytes, errRead := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("antigravity executor: close response body error: %v", errClose) log.Errorf("antigravity executor: close response body error: %v", errClose)
} }
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
err = errRead err = errRead
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, bodyBytes) helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
if httpResp.StatusCode == http.StatusTooManyRequests { if httpResp.StatusCode == http.StatusTooManyRequests {
if usedCreditsDirect { if usedCreditsDirect {
@@ -543,29 +544,29 @@ attemptLoop:
} else { } else {
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, false, opts.Alt, baseURL, bodyBytes) creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, false, opts.Alt, baseURL, bodyBytes)
if creditsResp != nil { if creditsResp != nil {
recordAPIResponseMetadata(ctx, e.cfg, creditsResp.StatusCode, creditsResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, creditsResp.StatusCode, creditsResp.Header.Clone())
creditsBody, errCreditsRead := io.ReadAll(creditsResp.Body) creditsBody, errCreditsRead := io.ReadAll(creditsResp.Body)
if errClose := creditsResp.Body.Close(); errClose != nil { if errClose := creditsResp.Body.Close(); errClose != nil {
log.Errorf("antigravity executor: close credits success response body error: %v", errClose) log.Errorf("antigravity executor: close credits success response body error: %v", errClose)
} }
if errCreditsRead != nil { if errCreditsRead != nil {
recordAPIResponseError(ctx, e.cfg, errCreditsRead) helps.RecordAPIResponseError(ctx, e.cfg, errCreditsRead)
err = errCreditsRead err = errCreditsRead
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, creditsBody) helps.AppendAPIResponseChunk(ctx, e.cfg, creditsBody)
reporter.publish(ctx, parseAntigravityUsage(creditsBody)) reporter.Publish(ctx, helps.ParseAntigravityUsage(creditsBody))
var param any var param any
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, creditsBody, &param) converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, creditsBody, &param)
resp = cliproxyexecutor.Response{Payload: converted, Headers: creditsResp.Header.Clone()} resp = cliproxyexecutor.Response{Payload: converted, Headers: creditsResp.Header.Clone()}
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
return resp, nil return resp, nil
} }
} }
} }
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices { if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
log.Debugf("antigravity executor: upstream error status: %d, body: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), bodyBytes)) log.Debugf("antigravity executor: upstream error status: %d, body: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), bodyBytes))
lastStatus = httpResp.StatusCode lastStatus = httpResp.StatusCode
lastBody = append([]byte(nil), bodyBytes...) lastBody = append([]byte(nil), bodyBytes...)
lastErr = nil lastErr = nil
@@ -591,11 +592,11 @@ attemptLoop:
return resp, err return resp, err
} }
reporter.publish(ctx, parseAntigravityUsage(bodyBytes)) reporter.Publish(ctx, helps.ParseAntigravityUsage(bodyBytes))
var param any var param any
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, &param) converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, &param)
resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()} resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()}
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
return resp, nil return resp, nil
} }
@@ -625,8 +626,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
auth = updatedAuth auth = updatedAuth
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("antigravity") to := sdktranslator.FromString("antigravity")
@@ -644,8 +645,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
return resp, err return resp, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
baseURLs := antigravityBaseURLFallbackOrder(auth) baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
@@ -675,7 +676,7 @@ attemptLoop:
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) { if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
return resp, errDo return resp, errDo
} }
@@ -689,14 +690,14 @@ attemptLoop:
err = errDo err = errDo
return resp, err return resp, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices { if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
bodyBytes, errRead := io.ReadAll(httpResp.Body) bodyBytes, errRead := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("antigravity executor: close response body error: %v", errClose) log.Errorf("antigravity executor: close response body error: %v", errClose)
} }
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
if errors.Is(errRead, context.Canceled) || errors.Is(errRead, context.DeadlineExceeded) { if errors.Is(errRead, context.Canceled) || errors.Is(errRead, context.DeadlineExceeded) {
err = errRead err = errRead
return resp, err return resp, err
@@ -715,7 +716,7 @@ attemptLoop:
err = errRead err = errRead
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, bodyBytes) helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
if httpResp.StatusCode == http.StatusTooManyRequests { if httpResp.StatusCode == http.StatusTooManyRequests {
if usedCreditsDirect { if usedCreditsDirect {
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) { if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
@@ -726,7 +727,7 @@ attemptLoop:
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes) creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes)
if creditsResp != nil { if creditsResp != nil {
httpResp = creditsResp httpResp = creditsResp
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
} }
} }
} }
@@ -771,29 +772,29 @@ attemptLoop:
scanner.Buffer(nil, streamScannerBuffer) scanner.Buffer(nil, streamScannerBuffer)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
// Filter usage metadata for all models // Filter usage metadata for all models
// Only retain usage statistics in the terminal chunk // Only retain usage statistics in the terminal chunk
line = FilterSSEUsageMetadata(line) line = helps.FilterSSEUsageMetadata(line)
payload := jsonPayload(line) payload := helps.JSONPayload(line)
if payload == nil { if payload == nil {
continue continue
} }
if detail, ok := parseAntigravityStreamUsage(payload); ok { if detail, ok := helps.ParseAntigravityStreamUsage(payload); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
out <- cliproxyexecutor.StreamChunk{Payload: payload} out <- cliproxyexecutor.StreamChunk{Payload: payload}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} else { } else {
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
} }
}(httpResp) }(httpResp)
@@ -809,11 +810,11 @@ attemptLoop:
} }
resp = cliproxyexecutor.Response{Payload: e.convertStreamToNonStream(buffer.Bytes())} resp = cliproxyexecutor.Response{Payload: e.convertStreamToNonStream(buffer.Bytes())}
reporter.publish(ctx, parseAntigravityUsage(resp.Payload)) reporter.Publish(ctx, helps.ParseAntigravityUsage(resp.Payload))
var param any var param any
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, resp.Payload, &param) converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, resp.Payload, &param)
resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()} resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()}
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
return resp, nil return resp, nil
} }
@@ -1042,8 +1043,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
auth = updatedAuth auth = updatedAuth
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("antigravity") to := sdktranslator.FromString("antigravity")
@@ -1061,8 +1062,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
return nil, err return nil, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
baseURLs := antigravityBaseURLFallbackOrder(auth) baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
@@ -1091,7 +1092,7 @@ attemptLoop:
} }
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) { if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
return nil, errDo return nil, errDo
} }
@@ -1105,14 +1106,14 @@ attemptLoop:
err = errDo err = errDo
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices { if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
bodyBytes, errRead := io.ReadAll(httpResp.Body) bodyBytes, errRead := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("antigravity executor: close response body error: %v", errClose) log.Errorf("antigravity executor: close response body error: %v", errClose)
} }
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
if errors.Is(errRead, context.Canceled) || errors.Is(errRead, context.DeadlineExceeded) { if errors.Is(errRead, context.Canceled) || errors.Is(errRead, context.DeadlineExceeded) {
err = errRead err = errRead
return nil, err return nil, err
@@ -1131,7 +1132,7 @@ attemptLoop:
err = errRead err = errRead
return nil, err return nil, err
} }
appendAPIResponseChunk(ctx, e.cfg, bodyBytes) helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
if httpResp.StatusCode == http.StatusTooManyRequests { if httpResp.StatusCode == http.StatusTooManyRequests {
if usedCreditsDirect { if usedCreditsDirect {
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) { if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
@@ -1142,7 +1143,7 @@ attemptLoop:
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes) creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes)
if creditsResp != nil { if creditsResp != nil {
httpResp = creditsResp httpResp = creditsResp
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
} }
} }
} }
@@ -1188,19 +1189,19 @@ attemptLoop:
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
// Filter usage metadata for all models // Filter usage metadata for all models
// Only retain usage statistics in the terminal chunk // Only retain usage statistics in the terminal chunk
line = FilterSSEUsageMetadata(line) line = helps.FilterSSEUsageMetadata(line)
payload := jsonPayload(line) payload := helps.JSONPayload(line)
if payload == nil { if payload == nil {
continue continue
} }
if detail, ok := parseAntigravityStreamUsage(payload); ok { if detail, ok := helps.ParseAntigravityStreamUsage(payload); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), &param) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), &param)
@@ -1213,11 +1214,11 @@ attemptLoop:
out <- cliproxyexecutor.StreamChunk{Payload: tail[i]} out <- cliproxyexecutor.StreamChunk{Payload: tail[i]}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} else { } else {
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
} }
}(httpResp) }(httpResp)
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
@@ -1320,7 +1321,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
httpReq.Host = host httpReq.Host = host
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: requestURL.String(), URL: requestURL.String(),
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -1334,7 +1335,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) { if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
return cliproxyexecutor.Response{}, errDo return cliproxyexecutor.Response{}, errDo
} }
@@ -1348,16 +1349,16 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
return cliproxyexecutor.Response{}, errDo return cliproxyexecutor.Response{}, errDo
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
bodyBytes, errRead := io.ReadAll(httpResp.Body) bodyBytes, errRead := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("antigravity executor: close response body error: %v", errClose) log.Errorf("antigravity executor: close response body error: %v", errClose)
} }
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
return cliproxyexecutor.Response{}, errRead return cliproxyexecutor.Response{}, errRead
} }
appendAPIResponseChunk(ctx, e.cfg, bodyBytes) helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices { if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
count := gjson.GetBytes(bodyBytes, "totalTokens").Int() count := gjson.GetBytes(bodyBytes, "totalTokens").Int()
@@ -1624,7 +1625,7 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
if e.cfg != nil && e.cfg.RequestLog { if e.cfg != nil && e.cfg.RequestLog {
payloadLog = []byte(payloadStr) payloadLog = []byte(payloadStr)
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: requestURL.String(), URL: requestURL.String(),
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
+71 -70
View File
@@ -23,6 +23,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -91,7 +92,7 @@ func (e *ClaudeExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Aut
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
@@ -106,8 +107,8 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
baseURL = "https://api.anthropic.com" baseURL = "https://api.anthropic.com"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("claude") to := sdktranslator.FromString("claude")
// Use streaming translation to preserve function calling, except for claude. // Use streaming translation to preserve function calling, except for claude.
@@ -130,8 +131,8 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
// based on client type and configuration. // based on client type and configuration.
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey) body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body = ensureModelMaxTokens(body, baseModel) body = ensureModelMaxTokens(body, baseModel)
// Disable thinking if tool_choice forces tool use (Anthropic API constraint) // Disable thinking if tool_choice forces tool use (Anthropic API constraint)
@@ -172,7 +173,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -184,33 +185,33 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
// Decompress error responses — pass the Content-Encoding value (may be empty) // Decompress error responses — pass the Content-Encoding value (may be empty)
// and let decodeResponseBody handle both header-declared and magic-byte-detected // and let decodeResponseBody handle both header-declared and magic-byte-detected
// compression. This keeps error-path behaviour consistent with the success path. // compression. This keeps error-path behaviour consistent with the success path.
errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding")) errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
if decErr != nil { if decErr != nil {
recordAPIResponseError(ctx, e.cfg, decErr) helps.RecordAPIResponseError(ctx, e.cfg, decErr)
msg := fmt.Sprintf("failed to decode error response body: %v", decErr) msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
logWithRequestID(ctx).Warn(msg) helps.LogWithRequestID(ctx).Warn(msg)
return resp, statusErr{code: httpResp.StatusCode, msg: msg} return resp, statusErr{code: httpResp.StatusCode, msg: msg}
} }
b, readErr := io.ReadAll(errBody) b, readErr := io.ReadAll(errBody)
if readErr != nil { if readErr != nil {
recordAPIResponseError(ctx, e.cfg, readErr) helps.RecordAPIResponseError(ctx, e.cfg, readErr)
msg := fmt.Sprintf("failed to read error response body: %v", readErr) msg := fmt.Sprintf("failed to read error response body: %v", readErr)
logWithRequestID(ctx).Warn(msg) helps.LogWithRequestID(ctx).Warn(msg)
b = []byte(msg) b = []byte(msg)
} }
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = statusErr{code: httpResp.StatusCode, msg: string(b)} err = statusErr{code: httpResp.StatusCode, msg: string(b)}
if errClose := errBody.Close(); errClose != nil { if errClose := errBody.Close(); errClose != nil {
log.Errorf("response body close error: %v", errClose) log.Errorf("response body close error: %v", errClose)
@@ -219,7 +220,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
} }
decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding")) decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("response body close error: %v", errClose) log.Errorf("response body close error: %v", errClose)
} }
@@ -232,19 +233,19 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
}() }()
data, err := io.ReadAll(decodedBody) data, err := io.ReadAll(decodedBody)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
if stream { if stream {
lines := bytes.Split(data, []byte("\n")) lines := bytes.Split(data, []byte("\n"))
for _, line := range lines { for _, line := range lines {
if detail, ok := parseClaudeStreamUsage(line); ok { if detail, ok := helps.ParseClaudeStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
} }
} else { } else {
reporter.publish(ctx, parseClaudeUsage(data)) reporter.Publish(ctx, helps.ParseClaudeUsage(data))
} }
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
data = stripClaudeToolPrefixFromResponse(data, claudeToolPrefix) data = stripClaudeToolPrefixFromResponse(data, claudeToolPrefix)
@@ -275,8 +276,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
baseURL = "https://api.anthropic.com" baseURL = "https://api.anthropic.com"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("claude") to := sdktranslator.FromString("claude")
originalPayloadSource := req.Payload originalPayloadSource := req.Payload
@@ -297,8 +298,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
// based on client type and configuration. // based on client type and configuration.
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey) body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body = ensureModelMaxTokens(body, baseModel) body = ensureModelMaxTokens(body, baseModel)
// Disable thinking if tool_choice forces tool use (Anthropic API constraint) // Disable thinking if tool_choice forces tool use (Anthropic API constraint)
@@ -336,7 +337,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -348,33 +349,33 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
// Decompress error responses — pass the Content-Encoding value (may be empty) // Decompress error responses — pass the Content-Encoding value (may be empty)
// and let decodeResponseBody handle both header-declared and magic-byte-detected // and let decodeResponseBody handle both header-declared and magic-byte-detected
// compression. This keeps error-path behaviour consistent with the success path. // compression. This keeps error-path behaviour consistent with the success path.
errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding")) errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
if decErr != nil { if decErr != nil {
recordAPIResponseError(ctx, e.cfg, decErr) helps.RecordAPIResponseError(ctx, e.cfg, decErr)
msg := fmt.Sprintf("failed to decode error response body: %v", decErr) msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
logWithRequestID(ctx).Warn(msg) helps.LogWithRequestID(ctx).Warn(msg)
return nil, statusErr{code: httpResp.StatusCode, msg: msg} return nil, statusErr{code: httpResp.StatusCode, msg: msg}
} }
b, readErr := io.ReadAll(errBody) b, readErr := io.ReadAll(errBody)
if readErr != nil { if readErr != nil {
recordAPIResponseError(ctx, e.cfg, readErr) helps.RecordAPIResponseError(ctx, e.cfg, readErr)
msg := fmt.Sprintf("failed to read error response body: %v", readErr) msg := fmt.Sprintf("failed to read error response body: %v", readErr)
logWithRequestID(ctx).Warn(msg) helps.LogWithRequestID(ctx).Warn(msg)
b = []byte(msg) b = []byte(msg)
} }
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
if errClose := errBody.Close(); errClose != nil { if errClose := errBody.Close(); errClose != nil {
log.Errorf("response body close error: %v", errClose) log.Errorf("response body close error: %v", errClose)
} }
@@ -383,7 +384,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
} }
decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding")) decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("response body close error: %v", errClose) log.Errorf("response body close error: %v", errClose)
} }
@@ -404,9 +405,9 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
scanner.Buffer(nil, 52_428_800) // 50MB scanner.Buffer(nil, 52_428_800) // 50MB
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseClaudeStreamUsage(line); ok { if detail, ok := helps.ParseClaudeStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix) line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix)
@@ -418,8 +419,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
out <- cliproxyexecutor.StreamChunk{Payload: cloned} out <- cliproxyexecutor.StreamChunk{Payload: cloned}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
return return
@@ -431,9 +432,9 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseClaudeStreamUsage(line); ok { if detail, ok := helps.ParseClaudeStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix) line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix)
@@ -453,8 +454,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
} }
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
}() }()
@@ -503,7 +504,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -515,32 +516,32 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
resp, err := httpClient.Do(httpReq) resp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
// Decompress error responses — pass the Content-Encoding value (may be empty) // Decompress error responses — pass the Content-Encoding value (may be empty)
// and let decodeResponseBody handle both header-declared and magic-byte-detected // and let decodeResponseBody handle both header-declared and magic-byte-detected
// compression. This keeps error-path behaviour consistent with the success path. // compression. This keeps error-path behaviour consistent with the success path.
errBody, decErr := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding")) errBody, decErr := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding"))
if decErr != nil { if decErr != nil {
recordAPIResponseError(ctx, e.cfg, decErr) helps.RecordAPIResponseError(ctx, e.cfg, decErr)
msg := fmt.Sprintf("failed to decode error response body: %v", decErr) msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
logWithRequestID(ctx).Warn(msg) helps.LogWithRequestID(ctx).Warn(msg)
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: msg} return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: msg}
} }
b, readErr := io.ReadAll(errBody) b, readErr := io.ReadAll(errBody)
if readErr != nil { if readErr != nil {
recordAPIResponseError(ctx, e.cfg, readErr) helps.RecordAPIResponseError(ctx, e.cfg, readErr)
msg := fmt.Sprintf("failed to read error response body: %v", readErr) msg := fmt.Sprintf("failed to read error response body: %v", readErr)
logWithRequestID(ctx).Warn(msg) helps.LogWithRequestID(ctx).Warn(msg)
b = []byte(msg) b = []byte(msg)
} }
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
if errClose := errBody.Close(); errClose != nil { if errClose := errBody.Close(); errClose != nil {
log.Errorf("response body close error: %v", errClose) log.Errorf("response body close error: %v", errClose)
} }
@@ -548,7 +549,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
} }
decodedBody, err := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding")) decodedBody, err := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding"))
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
if errClose := resp.Body.Close(); errClose != nil { if errClose := resp.Body.Close(); errClose != nil {
log.Errorf("response body close error: %v", errClose) log.Errorf("response body close error: %v", errClose)
} }
@@ -561,10 +562,10 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
}() }()
data, err := io.ReadAll(decodedBody) data, err := io.ReadAll(decodedBody)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
count := gjson.GetBytes(data, "input_tokens").Int() count := gjson.GetBytes(data, "input_tokens").Int()
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
return cliproxyexecutor.Response{Payload: out, Headers: resp.Header.Clone()}, nil return cliproxyexecutor.Response{Payload: out, Headers: resp.Header.Clone()}, nil
@@ -800,10 +801,10 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
ginHeaders = ginCtx.Request.Header ginHeaders = ginCtx.Request.Header
} }
stabilizeDeviceProfile := claudeDeviceProfileStabilizationEnabled(cfg) stabilizeDeviceProfile := helps.ClaudeDeviceProfileStabilizationEnabled(cfg)
var deviceProfile claudeDeviceProfile var deviceProfile helps.ClaudeDeviceProfile
if stabilizeDeviceProfile { if stabilizeDeviceProfile {
deviceProfile = resolveClaudeDeviceProfile(auth, apiKey, ginHeaders, cfg) deviceProfile = helps.ResolveClaudeDeviceProfile(auth, apiKey, ginHeaders, cfg)
} }
baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05" baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05"
@@ -871,9 +872,9 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
} }
util.ApplyCustomHeadersFromAttrs(r, attrs) util.ApplyCustomHeadersFromAttrs(r, attrs)
if stabilizeDeviceProfile { if stabilizeDeviceProfile {
applyClaudeDeviceProfileHeaders(r, deviceProfile) helps.ApplyClaudeDeviceProfileHeaders(r, deviceProfile)
} else { } else {
applyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg) helps.ApplyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg)
} }
// Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which // Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which
// may override it with a user-configured value. Compressed SSE breaks the line // may override it with a user-configured value. Compressed SSE breaks the line
@@ -1044,7 +1045,7 @@ func stripClaudeToolPrefixFromStreamLine(line []byte, prefix string) []byte {
if prefix == "" { if prefix == "" {
return line return line
} }
payload := jsonPayload(line) payload := helps.JSONPayload(line)
if len(payload) == 0 || !gjson.ValidBytes(payload) { if len(payload) == 0 || !gjson.ValidBytes(payload) {
return line return line
} }
@@ -1156,9 +1157,9 @@ func resolveClaudeKeyCloakConfig(cfg *config.Config, auth *cliproxyauth.Auth) *c
func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte { func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte {
generateID := func() string { generateID := func() string {
if useCache { if useCache {
return cachedUserID(apiKey) return helps.CachedUserID(apiKey)
} }
return generateFakeUserID() return helps.GenerateFakeUserID()
} }
metadata := gjson.GetBytes(payload, "metadata") metadata := gjson.GetBytes(payload, "metadata")
@@ -1168,7 +1169,7 @@ func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte {
} }
existingUserID := gjson.GetBytes(payload, "metadata.user_id").String() existingUserID := gjson.GetBytes(payload, "metadata.user_id").String()
if existingUserID == "" || !isValidUserID(existingUserID) { if existingUserID == "" || !helps.IsValidUserID(existingUserID) {
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateID()) payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateID())
} }
return payload return payload
@@ -1292,7 +1293,7 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A
} }
// Determine if cloaking should be applied // Determine if cloaking should be applied
if !shouldCloak(cloakMode, clientUserAgent) { if !helps.ShouldCloak(cloakMode, clientUserAgent) {
return payload return payload
} }
@@ -1306,8 +1307,8 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A
// Apply sensitive word obfuscation // Apply sensitive word obfuscation
if len(sensitiveWords) > 0 { if len(sensitiveWords) > 0 {
matcher := buildSensitiveWordMatcher(sensitiveWords) matcher := helps.BuildSensitiveWordMatcher(sensitiveWords)
payload = obfuscateSensitiveWords(payload, matcher) payload = helps.ObfuscateSensitiveWords(payload, matcher)
} }
return payload return payload
@@ -16,6 +16,7 @@ import (
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
@@ -24,9 +25,7 @@ import (
) )
func resetClaudeDeviceProfileCache() { func resetClaudeDeviceProfileCache() {
claudeDeviceProfileCacheMu.Lock() helps.ResetClaudeDeviceProfileCache()
claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry)
claudeDeviceProfileCacheMu.Unlock()
} }
func newClaudeHeaderTestRequest(t *testing.T, incoming http.Header) *http.Request { func newClaudeHeaderTestRequest(t *testing.T, incoming http.Header) *http.Request {
@@ -339,7 +338,7 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi
var pauseOnce sync.Once var pauseOnce sync.Once
var releaseOnce sync.Once var releaseOnce sync.Once
claudeDeviceProfileBeforeCandidateStore = func(candidate claudeDeviceProfile) { helps.ClaudeDeviceProfileBeforeCandidateStore = func(candidate helps.ClaudeDeviceProfile) {
if candidate.UserAgent != "claude-cli/2.1.62 (external, cli)" { if candidate.UserAgent != "claude-cli/2.1.62 (external, cli)" {
return return
} }
@@ -347,13 +346,13 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi
<-releaseLow <-releaseLow
} }
t.Cleanup(func() { t.Cleanup(func() {
claudeDeviceProfileBeforeCandidateStore = nil helps.ClaudeDeviceProfileBeforeCandidateStore = nil
releaseOnce.Do(func() { close(releaseLow) }) releaseOnce.Do(func() { close(releaseLow) })
}) })
lowResultCh := make(chan claudeDeviceProfile, 1) lowResultCh := make(chan helps.ClaudeDeviceProfile, 1)
go func() { go func() {
lowResultCh <- resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{ lowResultCh <- helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
"User-Agent": []string{"claude-cli/2.1.62 (external, cli)"}, "User-Agent": []string{"claude-cli/2.1.62 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.74.0"}, "X-Stainless-Package-Version": []string{"0.74.0"},
"X-Stainless-Runtime-Version": []string{"v24.3.0"}, "X-Stainless-Runtime-Version": []string{"v24.3.0"},
@@ -368,7 +367,7 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi
t.Fatal("timed out waiting for lower candidate to pause before storing") t.Fatal("timed out waiting for lower candidate to pause before storing")
} }
highResult := resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{ highResult := helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
"User-Agent": []string{"claude-cli/2.1.63 (external, cli)"}, "User-Agent": []string{"claude-cli/2.1.63 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.75.0"}, "X-Stainless-Package-Version": []string{"0.75.0"},
"X-Stainless-Runtime-Version": []string{"v24.4.0"}, "X-Stainless-Runtime-Version": []string{"v24.4.0"},
@@ -399,7 +398,7 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi
t.Fatalf("highResult platform = %s/%s, want %s/%s", highResult.OS, highResult.Arch, "MacOS", "arm64") t.Fatalf("highResult platform = %s/%s, want %s/%s", highResult.OS, highResult.Arch, "MacOS", "arm64")
} }
cached := resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{ cached := helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
"User-Agent": []string{"curl/8.7.1"}, "User-Agent": []string{"curl/8.7.1"},
}, cfg) }, cfg)
if cached.UserAgent != "claude-cli/2.1.63 (external, cli)" { if cached.UserAgent != "claude-cli/2.1.63 (external, cli)" {
+51 -50
View File
@@ -13,6 +13,7 @@ import (
codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -73,7 +74,7 @@ func (e *CodexExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
@@ -88,8 +89,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
baseURL = "https://chatgpt.com/backend-api/codex" baseURL = "https://chatgpt.com/backend-api/codex"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("codex") to := sdktranslator.FromString("codex")
@@ -106,8 +107,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
return resp, err return resp, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
body, _ = sjson.SetBytes(body, "stream", true) body, _ = sjson.SetBytes(body, "stream", true)
body, _ = sjson.DeleteBytes(body, "previous_response_id") body, _ = sjson.DeleteBytes(body, "previous_response_id")
@@ -128,7 +129,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -139,10 +140,10 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
AuthType: authType, AuthType: authType,
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
defer func() { defer func() {
@@ -150,20 +151,20 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
log.Errorf("codex executor: close response body error: %v", errClose) log.Errorf("codex executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = newCodexStatusErr(httpResp.StatusCode, b) err = newCodexStatusErr(httpResp.StatusCode, b)
return resp, err return resp, err
} }
data, err := io.ReadAll(httpResp.Body) data, err := io.ReadAll(httpResp.Body)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
lines := bytes.Split(data, []byte("\n")) lines := bytes.Split(data, []byte("\n"))
for _, line := range lines { for _, line := range lines {
@@ -176,8 +177,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
continue continue
} }
if detail, ok := parseCodexUsage(line); ok { if detail, ok := helps.ParseCodexUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
var param any var param any
@@ -197,8 +198,8 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
baseURL = "https://chatgpt.com/backend-api/codex" baseURL = "https://chatgpt.com/backend-api/codex"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("openai-response") to := sdktranslator.FromString("openai-response")
@@ -215,8 +216,8 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
return resp, err return resp, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
body, _ = sjson.DeleteBytes(body, "stream") body, _ = sjson.DeleteBytes(body, "stream")
body = normalizeCodexInstructions(body) body = normalizeCodexInstructions(body)
@@ -233,7 +234,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -244,10 +245,10 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
AuthType: authType, AuthType: authType,
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
defer func() { defer func() {
@@ -255,22 +256,22 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
log.Errorf("codex executor: close response body error: %v", errClose) log.Errorf("codex executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = newCodexStatusErr(httpResp.StatusCode, b) err = newCodexStatusErr(httpResp.StatusCode, b)
return resp, err return resp, err
} }
data, err := io.ReadAll(httpResp.Body) data, err := io.ReadAll(httpResp.Body)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseOpenAIUsage(data)) reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, data, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, data, &param)
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
@@ -288,8 +289,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
baseURL = "https://chatgpt.com/backend-api/codex" baseURL = "https://chatgpt.com/backend-api/codex"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("codex") to := sdktranslator.FromString("codex")
@@ -306,8 +307,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
return nil, err return nil, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.DeleteBytes(body, "previous_response_id") body, _ = sjson.DeleteBytes(body, "previous_response_id")
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
body, _ = sjson.DeleteBytes(body, "safety_identifier") body, _ = sjson.DeleteBytes(body, "safety_identifier")
@@ -327,7 +328,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -339,24 +340,24 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
data, readErr := io.ReadAll(httpResp.Body) data, readErr := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("codex executor: close response body error: %v", errClose) log.Errorf("codex executor: close response body error: %v", errClose)
} }
if readErr != nil { if readErr != nil {
recordAPIResponseError(ctx, e.cfg, readErr) helps.RecordAPIResponseError(ctx, e.cfg, readErr)
return nil, readErr return nil, readErr
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
err = newCodexStatusErr(httpResp.StatusCode, data) err = newCodexStatusErr(httpResp.StatusCode, data)
return nil, err return nil, err
} }
@@ -373,13 +374,13 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if bytes.HasPrefix(line, dataTag) { if bytes.HasPrefix(line, dataTag) {
data := bytes.TrimSpace(line[5:]) data := bytes.TrimSpace(line[5:])
if gjson.GetBytes(data, "type").String() == "response.completed" { if gjson.GetBytes(data, "type").String() == "response.completed" {
if detail, ok := parseCodexUsage(data); ok { if detail, ok := helps.ParseCodexUsage(data); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
} }
} }
@@ -390,8 +391,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
} }
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
}() }()
@@ -595,18 +596,18 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*
} }
func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte) (*http.Request, error) { func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte) (*http.Request, error) {
var cache codexCache var cache helps.CodexCache
if from == "claude" { if from == "claude" {
userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id") userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id")
if userIDResult.Exists() { if userIDResult.Exists() {
key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String()) key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String())
var ok bool var ok bool
if cache, ok = getCodexCache(key); !ok { if cache, ok = helps.GetCodexCache(key); !ok {
cache = codexCache{ cache = helps.CodexCache{
ID: uuid.New().String(), ID: uuid.New().String(),
Expire: time.Now().Add(1 * time.Hour), Expire: time.Now().Add(1 * time.Hour),
} }
setCodexCache(key, cache) helps.SetCodexCache(key, cache)
} }
} }
} else if from == "openai-response" { } else if from == "openai-response" {
@@ -615,7 +616,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form
cache.ID = promptCacheKey.String() cache.ID = promptCacheKey.String()
} }
} else if from == "openai" { } else if from == "openai" {
if apiKey := strings.TrimSpace(apiKeyFromContext(ctx)); apiKey != "" { if apiKey := strings.TrimSpace(helps.APIKeyFromContext(ctx)); apiKey != "" {
cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String() cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String()
} }
} }
@@ -15,10 +15,12 @@ import (
"sync" "sync"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -155,8 +157,8 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
baseURL = "https://chatgpt.com/backend-api/codex" baseURL = "https://chatgpt.com/backend-api/codex"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("codex") to := sdktranslator.FromString("codex")
@@ -173,8 +175,8 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
return resp, err return resp, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
body, _ = sjson.SetBytes(body, "stream", true) body, _ = sjson.SetBytes(body, "stream", true)
body, _ = sjson.DeleteBytes(body, "previous_response_id") body, _ = sjson.DeleteBytes(body, "previous_response_id")
@@ -209,7 +211,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
} }
wsReqBody := buildCodexWebsocketRequestBody(body) wsReqBody := buildCodexWebsocketRequestBody(body)
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: wsURL, URL: wsURL,
Method: "WEBSOCKET", Method: "WEBSOCKET",
Headers: wsHeaders.Clone(), Headers: wsHeaders.Clone(),
@@ -223,12 +225,12 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
conn, respHS, errDial := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders) conn, respHS, errDial := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
if respHS != nil { if respHS != nil {
recordAPIResponseMetadata(ctx, e.cfg, respHS.StatusCode, respHS.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, respHS.StatusCode, respHS.Header.Clone())
} }
if errDial != nil { if errDial != nil {
bodyErr := websocketHandshakeBody(respHS) bodyErr := websocketHandshakeBody(respHS)
if len(bodyErr) > 0 { if len(bodyErr) > 0 {
appendAPIResponseChunk(ctx, e.cfg, bodyErr) helps.AppendAPIResponseChunk(ctx, e.cfg, bodyErr)
} }
if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired { if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired {
return e.CodexExecutor.Execute(ctx, auth, req, opts) return e.CodexExecutor.Execute(ctx, auth, req, opts)
@@ -236,7 +238,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
if respHS != nil && respHS.StatusCode > 0 { if respHS != nil && respHS.StatusCode > 0 {
return resp, statusErr{code: respHS.StatusCode, msg: string(bodyErr)} return resp, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
} }
recordAPIResponseError(ctx, e.cfg, errDial) helps.RecordAPIResponseError(ctx, e.cfg, errDial)
return resp, errDial return resp, errDial
} }
closeHTTPResponseBody(respHS, "codex websockets executor: close handshake response body error") closeHTTPResponseBody(respHS, "codex websockets executor: close handshake response body error")
@@ -271,7 +273,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders) connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
if errDialRetry == nil && connRetry != nil { if errDialRetry == nil && connRetry != nil {
wsReqBodyRetry := buildCodexWebsocketRequestBody(body) wsReqBodyRetry := buildCodexWebsocketRequestBody(body)
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: wsURL, URL: wsURL,
Method: "WEBSOCKET", Method: "WEBSOCKET",
Headers: wsHeaders.Clone(), Headers: wsHeaders.Clone(),
@@ -287,15 +289,15 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
wsReqBody = wsReqBodyRetry wsReqBody = wsReqBodyRetry
} else { } else {
e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry) e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry)
recordAPIResponseError(ctx, e.cfg, errSendRetry) helps.RecordAPIResponseError(ctx, e.cfg, errSendRetry)
return resp, errSendRetry return resp, errSendRetry
} }
} else { } else {
recordAPIResponseError(ctx, e.cfg, errDialRetry) helps.RecordAPIResponseError(ctx, e.cfg, errDialRetry)
return resp, errDialRetry return resp, errDialRetry
} }
} else { } else {
recordAPIResponseError(ctx, e.cfg, errSend) helps.RecordAPIResponseError(ctx, e.cfg, errSend)
return resp, errSend return resp, errSend
} }
} }
@@ -306,7 +308,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
} }
msgType, payload, errRead := readCodexWebsocketMessage(ctx, sess, conn, readCh) msgType, payload, errRead := readCodexWebsocketMessage(ctx, sess, conn, readCh)
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
return resp, errRead return resp, errRead
} }
if msgType != websocket.TextMessage { if msgType != websocket.TextMessage {
@@ -315,7 +317,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
if sess != nil { if sess != nil {
e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err) e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err)
} }
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
continue continue
@@ -325,21 +327,21 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
if len(payload) == 0 { if len(payload) == 0 {
continue continue
} }
appendAPIResponseChunk(ctx, e.cfg, payload) helps.AppendAPIResponseChunk(ctx, e.cfg, payload)
if wsErr, ok := parseCodexWebsocketError(payload); ok { if wsErr, ok := parseCodexWebsocketError(payload); ok {
if sess != nil { if sess != nil {
e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr) e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr)
} }
recordAPIResponseError(ctx, e.cfg, wsErr) helps.RecordAPIResponseError(ctx, e.cfg, wsErr)
return resp, wsErr return resp, wsErr
} }
payload = normalizeCodexWebsocketCompletion(payload) payload = normalizeCodexWebsocketCompletion(payload)
eventType := gjson.GetBytes(payload, "type").String() eventType := gjson.GetBytes(payload, "type").String()
if eventType == "response.completed" { if eventType == "response.completed" {
if detail, ok := parseCodexUsage(payload); ok { if detail, ok := helps.ParseCodexUsage(payload); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, payload, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, payload, &param)
@@ -364,8 +366,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
baseURL = "https://chatgpt.com/backend-api/codex" baseURL = "https://chatgpt.com/backend-api/codex"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("codex") to := sdktranslator.FromString("codex")
@@ -376,8 +378,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
return nil, err return nil, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel)
httpURL := strings.TrimSuffix(baseURL, "/") + "/responses" httpURL := strings.TrimSuffix(baseURL, "/") + "/responses"
wsURL, err := buildCodexResponsesWebsocketURL(httpURL) wsURL, err := buildCodexResponsesWebsocketURL(httpURL)
@@ -403,7 +405,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
} }
wsReqBody := buildCodexWebsocketRequestBody(body) wsReqBody := buildCodexWebsocketRequestBody(body)
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: wsURL, URL: wsURL,
Method: "WEBSOCKET", Method: "WEBSOCKET",
Headers: wsHeaders.Clone(), Headers: wsHeaders.Clone(),
@@ -419,12 +421,12 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
var upstreamHeaders http.Header var upstreamHeaders http.Header
if respHS != nil { if respHS != nil {
upstreamHeaders = respHS.Header.Clone() upstreamHeaders = respHS.Header.Clone()
recordAPIResponseMetadata(ctx, e.cfg, respHS.StatusCode, respHS.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, respHS.StatusCode, respHS.Header.Clone())
} }
if errDial != nil { if errDial != nil {
bodyErr := websocketHandshakeBody(respHS) bodyErr := websocketHandshakeBody(respHS)
if len(bodyErr) > 0 { if len(bodyErr) > 0 {
appendAPIResponseChunk(ctx, e.cfg, bodyErr) helps.AppendAPIResponseChunk(ctx, e.cfg, bodyErr)
} }
if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired { if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired {
return e.CodexExecutor.ExecuteStream(ctx, auth, req, opts) return e.CodexExecutor.ExecuteStream(ctx, auth, req, opts)
@@ -432,7 +434,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
if respHS != nil && respHS.StatusCode > 0 { if respHS != nil && respHS.StatusCode > 0 {
return nil, statusErr{code: respHS.StatusCode, msg: string(bodyErr)} return nil, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
} }
recordAPIResponseError(ctx, e.cfg, errDial) helps.RecordAPIResponseError(ctx, e.cfg, errDial)
if sess != nil { if sess != nil {
sess.reqMu.Unlock() sess.reqMu.Unlock()
} }
@@ -451,20 +453,20 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
} }
if errSend := writeCodexWebsocketMessage(sess, conn, wsReqBody); errSend != nil { if errSend := writeCodexWebsocketMessage(sess, conn, wsReqBody); errSend != nil {
recordAPIResponseError(ctx, e.cfg, errSend) helps.RecordAPIResponseError(ctx, e.cfg, errSend)
if sess != nil { if sess != nil {
e.invalidateUpstreamConn(sess, conn, "send_error", errSend) e.invalidateUpstreamConn(sess, conn, "send_error", errSend)
// Retry once with a new websocket connection for the same execution session. // Retry once with a new websocket connection for the same execution session.
connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders) connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
if errDialRetry != nil || connRetry == nil { if errDialRetry != nil || connRetry == nil {
recordAPIResponseError(ctx, e.cfg, errDialRetry) helps.RecordAPIResponseError(ctx, e.cfg, errDialRetry)
sess.clearActive(readCh) sess.clearActive(readCh)
sess.reqMu.Unlock() sess.reqMu.Unlock()
return nil, errDialRetry return nil, errDialRetry
} }
wsReqBodyRetry := buildCodexWebsocketRequestBody(body) wsReqBodyRetry := buildCodexWebsocketRequestBody(body)
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: wsURL, URL: wsURL,
Method: "WEBSOCKET", Method: "WEBSOCKET",
Headers: wsHeaders.Clone(), Headers: wsHeaders.Clone(),
@@ -476,7 +478,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
AuthValue: authValue, AuthValue: authValue,
}) })
if errSendRetry := writeCodexWebsocketMessage(sess, connRetry, wsReqBodyRetry); errSendRetry != nil { if errSendRetry := writeCodexWebsocketMessage(sess, connRetry, wsReqBodyRetry); errSendRetry != nil {
recordAPIResponseError(ctx, e.cfg, errSendRetry) helps.RecordAPIResponseError(ctx, e.cfg, errSendRetry)
e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry) e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry)
sess.clearActive(readCh) sess.clearActive(readCh)
sess.reqMu.Unlock() sess.reqMu.Unlock()
@@ -542,8 +544,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
} }
terminateReason = "read_error" terminateReason = "read_error"
terminateErr = errRead terminateErr = errRead
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
_ = send(cliproxyexecutor.StreamChunk{Err: errRead}) _ = send(cliproxyexecutor.StreamChunk{Err: errRead})
return return
} }
@@ -552,8 +554,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
err = fmt.Errorf("codex websockets executor: unexpected binary message") err = fmt.Errorf("codex websockets executor: unexpected binary message")
terminateReason = "unexpected_binary" terminateReason = "unexpected_binary"
terminateErr = err terminateErr = err
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
if sess != nil { if sess != nil {
e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err) e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err)
} }
@@ -567,13 +569,13 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
if len(payload) == 0 { if len(payload) == 0 {
continue continue
} }
appendAPIResponseChunk(ctx, e.cfg, payload) helps.AppendAPIResponseChunk(ctx, e.cfg, payload)
if wsErr, ok := parseCodexWebsocketError(payload); ok { if wsErr, ok := parseCodexWebsocketError(payload); ok {
terminateReason = "upstream_error" terminateReason = "upstream_error"
terminateErr = wsErr terminateErr = wsErr
recordAPIResponseError(ctx, e.cfg, wsErr) helps.RecordAPIResponseError(ctx, e.cfg, wsErr)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
if sess != nil { if sess != nil {
e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr) e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr)
} }
@@ -584,8 +586,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
payload = normalizeCodexWebsocketCompletion(payload) payload = normalizeCodexWebsocketCompletion(payload)
eventType := gjson.GetBytes(payload, "type").String() eventType := gjson.GetBytes(payload, "type").String()
if eventType == "response.completed" || eventType == "response.done" { if eventType == "response.completed" || eventType == "response.done" {
if detail, ok := parseCodexUsage(payload); ok { if detail, ok := helps.ParseCodexUsage(payload); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
} }
@@ -767,19 +769,19 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto
return rawJSON, headers return rawJSON, headers
} }
var cache codexCache var cache helps.CodexCache
if from == "claude" { if from == "claude" {
userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id") userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id")
if userIDResult.Exists() { if userIDResult.Exists() {
key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String()) key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String())
if cached, ok := getCodexCache(key); ok { if cached, ok := helps.GetCodexCache(key); ok {
cache = cached cache = cached
} else { } else {
cache = codexCache{ cache = helps.CodexCache{
ID: uuid.New().String(), ID: uuid.New().String(),
Expire: time.Now().Add(1 * time.Hour), Expire: time.Now().Add(1 * time.Hour),
} }
setCodexCache(key, cache) helps.SetCodexCache(key, cache)
} }
} }
} else if from == "openai-response" { } else if from == "openai-response" {
@@ -806,8 +808,8 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
} }
var ginHeaders http.Header var ginHeaders http.Header
if ginCtx := ginContextFrom(ctx); ginCtx != nil && ginCtx.Request != nil { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
ginHeaders = ginCtx.Request.Header ginHeaders = ginCtx.Request.Header.Clone()
} }
cfgUserAgent, cfgBetaFeatures := codexHeaderDefaults(cfg, auth) cfgUserAgent, cfgBetaFeatures := codexHeaderDefaults(cfg, auth)
@@ -18,6 +18,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
@@ -112,8 +113,8 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
return resp, err return resp, err
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("gemini-cli") to := sdktranslator.FromString("gemini-cli")
@@ -132,8 +133,8 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
} }
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload) basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
basePayload = applyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel) basePayload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel)
action := "generateContent" action := "generateContent"
if req.Metadata != nil { if req.Metadata != nil {
@@ -190,7 +191,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
applyGeminiCLIHeaders(reqHTTP, attemptModel) applyGeminiCLIHeaders(reqHTTP, attemptModel)
reqHTTP.Header.Set("Accept", "application/json") reqHTTP.Header.Set("Accept", "application/json")
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: reqHTTP.Header.Clone(), Headers: reqHTTP.Header.Clone(),
@@ -204,7 +205,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
httpResp, errDo := httpClient.Do(reqHTTP) httpResp, errDo := httpClient.Do(reqHTTP)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
err = errDo err = errDo
return resp, err return resp, err
} }
@@ -213,15 +214,15 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("gemini cli executor: close response body error: %v", errClose) log.Errorf("gemini cli executor: close response body error: %v", errClose)
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
err = errRead err = errRead
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
if httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 { if httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 {
reporter.publish(ctx, parseGeminiCLIUsage(data)) reporter.Publish(ctx, helps.ParseGeminiCLIUsage(data))
var param any var param any
out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, opts.OriginalRequest, payload, data, &param) out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, opts.OriginalRequest, payload, data, &param)
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
@@ -230,7 +231,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
lastStatus = httpResp.StatusCode lastStatus = httpResp.StatusCode
lastBody = append([]byte(nil), data...) lastBody = append([]byte(nil), data...)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
if httpResp.StatusCode == 429 { if httpResp.StatusCode == 429 {
if idx+1 < len(models) { if idx+1 < len(models) {
log.Debugf("gemini cli executor: rate limited, retrying with next model: %s", models[idx+1]) log.Debugf("gemini cli executor: rate limited, retrying with next model: %s", models[idx+1])
@@ -245,7 +246,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
} }
if len(lastBody) > 0 { if len(lastBody) > 0 {
appendAPIResponseChunk(ctx, e.cfg, lastBody) helps.AppendAPIResponseChunk(ctx, e.cfg, lastBody)
} }
if lastStatus == 0 { if lastStatus == 0 {
lastStatus = 429 lastStatus = 429
@@ -266,8 +267,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
return nil, err return nil, err
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("gemini-cli") to := sdktranslator.FromString("gemini-cli")
@@ -286,8 +287,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
} }
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload) basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
basePayload = applyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel) basePayload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel)
projectID := resolveGeminiProjectID(auth) projectID := resolveGeminiProjectID(auth)
@@ -335,7 +336,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
applyGeminiCLIHeaders(reqHTTP, attemptModel) applyGeminiCLIHeaders(reqHTTP, attemptModel)
reqHTTP.Header.Set("Accept", "text/event-stream") reqHTTP.Header.Set("Accept", "text/event-stream")
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: reqHTTP.Header.Clone(), Headers: reqHTTP.Header.Clone(),
@@ -349,25 +350,25 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
httpResp, errDo := httpClient.Do(reqHTTP) httpResp, errDo := httpClient.Do(reqHTTP)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
err = errDo err = errDo
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
data, errRead := io.ReadAll(httpResp.Body) data, errRead := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("gemini cli executor: close response body error: %v", errClose) log.Errorf("gemini cli executor: close response body error: %v", errClose)
} }
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
err = errRead err = errRead
return nil, err return nil, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
lastStatus = httpResp.StatusCode lastStatus = httpResp.StatusCode
lastBody = append([]byte(nil), data...) lastBody = append([]byte(nil), data...)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
if httpResp.StatusCode == 429 { if httpResp.StatusCode == 429 {
if idx+1 < len(models) { if idx+1 < len(models) {
log.Debugf("gemini cli executor: rate limited, retrying with next model: %s", models[idx+1]) log.Debugf("gemini cli executor: rate limited, retrying with next model: %s", models[idx+1])
@@ -394,9 +395,9 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseGeminiCLIStreamUsage(line); ok { if detail, ok := helps.ParseGeminiCLIStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
if bytes.HasPrefix(line, dataTag) { if bytes.HasPrefix(line, dataTag) {
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), &param) segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), &param)
@@ -411,8 +412,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
out <- cliproxyexecutor.StreamChunk{Payload: segments[i]} out <- cliproxyexecutor.StreamChunk{Payload: segments[i]}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
return return
@@ -420,13 +421,13 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
data, errRead := io.ReadAll(resp.Body) data, errRead := io.ReadAll(resp.Body)
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errRead} out <- cliproxyexecutor.StreamChunk{Err: errRead}
return return
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseGeminiCLIUsage(data)) reporter.Publish(ctx, helps.ParseGeminiCLIUsage(data))
var param any var param any
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, &param) segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, &param)
for i := range segments { for i := range segments {
@@ -443,7 +444,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
} }
if len(lastBody) > 0 { if len(lastBody) > 0 {
appendAPIResponseChunk(ctx, e.cfg, lastBody) helps.AppendAPIResponseChunk(ctx, e.cfg, lastBody)
} }
if lastStatus == 0 { if lastStatus == 0 {
lastStatus = 429 lastStatus = 429
@@ -516,7 +517,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
applyGeminiCLIHeaders(reqHTTP, baseModel) applyGeminiCLIHeaders(reqHTTP, baseModel)
reqHTTP.Header.Set("Accept", "application/json") reqHTTP.Header.Set("Accept", "application/json")
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: reqHTTP.Header.Clone(), Headers: reqHTTP.Header.Clone(),
@@ -530,17 +531,19 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
resp, errDo := httpClient.Do(reqHTTP) resp, errDo := httpClient.Do(reqHTTP)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
return cliproxyexecutor.Response{}, errDo return cliproxyexecutor.Response{}, errDo
} }
data, errRead := io.ReadAll(resp.Body) data, errRead := io.ReadAll(resp.Body)
_ = resp.Body.Close() if errClose := resp.Body.Close(); errClose != nil {
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) helps.LogWithRequestID(ctx).Errorf("response body close error: %v", errClose)
}
helps.RecordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
return cliproxyexecutor.Response{}, errRead return cliproxyexecutor.Response{}, errRead
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
if resp.StatusCode >= 200 && resp.StatusCode < 300 { if resp.StatusCode >= 200 && resp.StatusCode < 300 {
count := gjson.GetBytes(data, "totalTokens").Int() count := gjson.GetBytes(data, "totalTokens").Int()
translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data) translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data)
@@ -611,7 +614,7 @@ func prepareGeminiCLITokenSource(ctx context.Context, cfg *config.Config, auth *
} }
ctxToken := ctx ctxToken := ctx
if httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0); httpClient != nil { if httpClient := helps.NewProxyAwareHTTPClient(ctx, cfg, auth, 0); httpClient != nil {
ctxToken = context.WithValue(ctxToken, oauth2.HTTPClient, httpClient) ctxToken = context.WithValue(ctxToken, oauth2.HTTPClient, httpClient)
} }
@@ -707,7 +710,7 @@ func geminiOAuthMetadata(auth *cliproxyauth.Auth) map[string]any {
} }
func newHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client { func newHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
return newProxyAwareHTTPClient(ctx, cfg, auth, timeout) return helps.NewProxyAwareHTTPClient(ctx, cfg, auth, timeout)
} }
func cloneMap(in map[string]any) map[string]any { func cloneMap(in map[string]any) map[string]any {
+44 -39
View File
@@ -13,6 +13,7 @@ import (
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -85,7 +86,7 @@ func (e *GeminiExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Aut
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
@@ -110,8 +111,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
apiKey, bearer := geminiCreds(auth) apiKey, bearer := geminiCreds(auth)
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
// Official Gemini API via API key or OAuth bearer // Official Gemini API via API key or OAuth bearer
from := opts.SourceFormat from := opts.SourceFormat
@@ -130,8 +131,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
} }
body = fixGeminiImageAspectRatio(baseModel, body) body = fixGeminiImageAspectRatio(baseModel, body)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
action := "generateContent" action := "generateContent"
@@ -165,7 +166,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -177,10 +178,10 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
defer func() { defer func() {
@@ -188,21 +189,21 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
log.Errorf("gemini executor: close response body error: %v", errClose) log.Errorf("gemini executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = statusErr{code: httpResp.StatusCode, msg: string(b)} err = statusErr{code: httpResp.StatusCode, msg: string(b)}
return resp, err return resp, err
} }
data, err := io.ReadAll(httpResp.Body) data, err := io.ReadAll(httpResp.Body)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseGeminiUsage(data)) reporter.Publish(ctx, helps.ParseGeminiUsage(data))
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, &param)
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
@@ -218,8 +219,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
apiKey, bearer := geminiCreds(auth) apiKey, bearer := geminiCreds(auth)
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("gemini") to := sdktranslator.FromString("gemini")
@@ -237,8 +238,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
} }
body = fixGeminiImageAspectRatio(baseModel, body) body = fixGeminiImageAspectRatio(baseModel, body)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
baseURL := resolveGeminiBaseURL(auth) baseURL := resolveGeminiBaseURL(auth)
@@ -268,7 +269,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -280,17 +281,17 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("gemini executor: close response body error: %v", errClose) log.Errorf("gemini executor: close response body error: %v", errClose)
} }
@@ -310,14 +311,14 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
filtered := FilterSSEUsageMetadata(line) filtered := helps.FilterSSEUsageMetadata(line)
payload := jsonPayload(filtered) payload := helps.JSONPayload(filtered)
if len(payload) == 0 { if len(payload) == 0 {
continue continue
} }
if detail, ok := parseGeminiStreamUsage(payload); ok { if detail, ok := helps.ParseGeminiStreamUsage(payload); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), &param) lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), &param)
for i := range lines { for i := range lines {
@@ -329,8 +330,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
}() }()
@@ -381,7 +382,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -393,23 +394,27 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
resp, err := httpClient.Do(httpReq) resp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
defer func() { _ = resp.Body.Close() }() defer func() {
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if errClose := resp.Body.Close(); errClose != nil {
helps.LogWithRequestID(ctx).Errorf("response body close error: %v", errClose)
}
}()
helps.RecordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", resp.StatusCode, summarizeErrorBody(resp.Header.Get("Content-Type"), data)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", resp.StatusCode, helps.SummarizeErrorBody(resp.Header.Get("Content-Type"), data))
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(data)} return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(data)}
} }
@@ -16,6 +16,7 @@ import (
vertexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex" vertexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
@@ -227,7 +228,7 @@ func (e *GeminiVertexExecutor) HttpRequest(ctx context.Context, auth *cliproxyau
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
@@ -301,8 +302,8 @@ func (e *GeminiVertexExecutor) Refresh(_ context.Context, auth *cliproxyauth.Aut
func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (resp cliproxyexecutor.Response, err error) { func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (resp cliproxyexecutor.Response, err error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
var body []byte var body []byte
@@ -332,8 +333,8 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
} }
body = fixGeminiImageAspectRatio(baseModel, body) body = fixGeminiImageAspectRatio(baseModel, body)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
} }
@@ -369,7 +370,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -381,10 +382,10 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
return resp, errDo return resp, errDo
} }
defer func() { defer func() {
@@ -392,21 +393,21 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
log.Errorf("vertex executor: close response body error: %v", errClose) log.Errorf("vertex executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = statusErr{code: httpResp.StatusCode, msg: string(b)} err = statusErr{code: httpResp.StatusCode, msg: string(b)}
return resp, err return resp, err
} }
data, errRead := io.ReadAll(httpResp.Body) data, errRead := io.ReadAll(httpResp.Body)
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
return resp, errRead return resp, errRead
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseGeminiUsage(data)) reporter.Publish(ctx, helps.ParseGeminiUsage(data))
// For Imagen models, convert response to Gemini format before translation // For Imagen models, convert response to Gemini format before translation
// This ensures Imagen responses use the same format as gemini-3-pro-image-preview // This ensures Imagen responses use the same format as gemini-3-pro-image-preview
@@ -427,8 +428,8 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (resp cliproxyexecutor.Response, err error) { func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (resp cliproxyexecutor.Response, err error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("gemini") to := sdktranslator.FromString("gemini")
@@ -447,8 +448,8 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
} }
body = fixGeminiImageAspectRatio(baseModel, body) body = fixGeminiImageAspectRatio(baseModel, body)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
action := getVertexAction(baseModel, false) action := getVertexAction(baseModel, false)
@@ -484,7 +485,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -496,10 +497,10 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
return resp, errDo return resp, errDo
} }
defer func() { defer func() {
@@ -507,21 +508,21 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
log.Errorf("vertex executor: close response body error: %v", errClose) log.Errorf("vertex executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = statusErr{code: httpResp.StatusCode, msg: string(b)} err = statusErr{code: httpResp.StatusCode, msg: string(b)}
return resp, err return resp, err
} }
data, errRead := io.ReadAll(httpResp.Body) data, errRead := io.ReadAll(httpResp.Body)
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
return resp, errRead return resp, errRead
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseGeminiUsage(data)) reporter.Publish(ctx, helps.ParseGeminiUsage(data))
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, &param)
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
@@ -532,8 +533,8 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (_ *cliproxyexecutor.StreamResult, err error) { func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (_ *cliproxyexecutor.StreamResult, err error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("gemini") to := sdktranslator.FromString("gemini")
@@ -552,8 +553,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
} }
body = fixGeminiImageAspectRatio(baseModel, body) body = fixGeminiImageAspectRatio(baseModel, body)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
action := getVertexAction(baseModel, true) action := getVertexAction(baseModel, true)
@@ -588,7 +589,7 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -600,17 +601,17 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
return nil, errDo return nil, errDo
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("vertex executor: close response body error: %v", errClose) log.Errorf("vertex executor: close response body error: %v", errClose)
} }
@@ -630,9 +631,9 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseGeminiStreamUsage(line); ok { if detail, ok := helps.ParseGeminiStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param) lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param)
for i := range lines { for i := range lines {
@@ -644,8 +645,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
}() }()
@@ -656,8 +657,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (_ *cliproxyexecutor.StreamResult, err error) { func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (_ *cliproxyexecutor.StreamResult, err error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("gemini") to := sdktranslator.FromString("gemini")
@@ -676,8 +677,8 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
} }
body = fixGeminiImageAspectRatio(baseModel, body) body = fixGeminiImageAspectRatio(baseModel, body)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "model", baseModel)
action := getVertexAction(baseModel, true) action := getVertexAction(baseModel, true)
@@ -712,7 +713,7 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -724,17 +725,17 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
return nil, errDo return nil, errDo
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("vertex executor: close response body error: %v", errClose) log.Errorf("vertex executor: close response body error: %v", errClose)
} }
@@ -754,9 +755,9 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseGeminiStreamUsage(line); ok { if detail, ok := helps.ParseGeminiStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param) lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param)
for i := range lines { for i := range lines {
@@ -768,8 +769,8 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
}() }()
@@ -819,7 +820,7 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -831,10 +832,10 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
return cliproxyexecutor.Response{}, errDo return cliproxyexecutor.Response{}, errDo
} }
defer func() { defer func() {
@@ -842,19 +843,19 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
log.Errorf("vertex executor: close response body error: %v", errClose) log.Errorf("vertex executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(b)} return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(b)}
} }
data, errRead := io.ReadAll(httpResp.Body) data, errRead := io.ReadAll(httpResp.Body)
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
return cliproxyexecutor.Response{}, errRead return cliproxyexecutor.Response{}, errRead
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
count := gjson.GetBytes(data, "totalTokens").Int() count := gjson.GetBytes(data, "totalTokens").Int()
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil
@@ -903,7 +904,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -915,10 +916,10 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, errDo := httpClient.Do(httpReq) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
recordAPIResponseError(ctx, e.cfg, errDo) helps.RecordAPIResponseError(ctx, e.cfg, errDo)
return cliproxyexecutor.Response{}, errDo return cliproxyexecutor.Response{}, errDo
} }
defer func() { defer func() {
@@ -926,19 +927,19 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
log.Errorf("vertex executor: close response body error: %v", errClose) log.Errorf("vertex executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(b)} return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(b)}
} }
data, errRead := io.ReadAll(httpResp.Body) data, errRead := io.ReadAll(httpResp.Body)
if errRead != nil { if errRead != nil {
recordAPIResponseError(ctx, e.cfg, errRead) helps.RecordAPIResponseError(ctx, e.cfg, errRead)
return cliproxyexecutor.Response{}, errRead return cliproxyexecutor.Response{}, errRead
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
count := gjson.GetBytes(data, "totalTokens").Int() count := gjson.GetBytes(data, "totalTokens").Int()
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil
@@ -1012,7 +1013,7 @@ func vertexBaseURL(location string) string {
} }
func vertexAccessToken(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, saJSON []byte) (string, error) { func vertexAccessToken(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, saJSON []byte) (string, error) {
if httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0); httpClient != nil { if httpClient := helps.NewProxyAwareHTTPClient(ctx, cfg, auth, 0); httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
} }
// Use cloud-platform scope for Vertex AI. // Use cloud-platform scope for Vertex AI.
@@ -1,11 +1,11 @@
package executor package helps
import ( import (
"sync" "sync"
"time" "time"
) )
type codexCache struct { type CodexCache struct {
ID string ID string
Expire time.Time Expire time.Time
} }
@@ -13,7 +13,7 @@ type codexCache struct {
// codexCacheMap stores prompt cache IDs keyed by model+user_id. // codexCacheMap stores prompt cache IDs keyed by model+user_id.
// Protected by codexCacheMu. Entries expire after 1 hour. // Protected by codexCacheMu. Entries expire after 1 hour.
var ( var (
codexCacheMap = make(map[string]codexCache) codexCacheMap = make(map[string]CodexCache)
codexCacheMu sync.RWMutex codexCacheMu sync.RWMutex
) )
@@ -47,20 +47,20 @@ func purgeExpiredCodexCache() {
} }
} }
// getCodexCache retrieves a cached entry, returning ok=false if not found or expired. // GetCodexCache retrieves a cached entry, returning ok=false if not found or expired.
func getCodexCache(key string) (codexCache, bool) { func GetCodexCache(key string) (CodexCache, bool) {
codexCacheCleanupOnce.Do(startCodexCacheCleanup) codexCacheCleanupOnce.Do(startCodexCacheCleanup)
codexCacheMu.RLock() codexCacheMu.RLock()
cache, ok := codexCacheMap[key] cache, ok := codexCacheMap[key]
codexCacheMu.RUnlock() codexCacheMu.RUnlock()
if !ok || cache.Expire.Before(time.Now()) { if !ok || cache.Expire.Before(time.Now()) {
return codexCache{}, false return CodexCache{}, false
} }
return cache, true return cache, true
} }
// setCodexCache stores a cache entry. // SetCodexCache stores a cache entry.
func setCodexCache(key string, cache codexCache) { func SetCodexCache(key string, cache CodexCache) {
codexCacheCleanupOnce.Do(startCodexCacheCleanup) codexCacheCleanupOnce.Do(startCodexCacheCleanup)
codexCacheMu.Lock() codexCacheMu.Lock()
codexCacheMap[key] = cache codexCacheMap[key] = cache
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"crypto/sha256" "crypto/sha256"
@@ -32,7 +32,7 @@ var (
claudeDeviceProfileCacheMu sync.RWMutex claudeDeviceProfileCacheMu sync.RWMutex
claudeDeviceProfileCacheCleanupOnce sync.Once claudeDeviceProfileCacheCleanupOnce sync.Once
claudeDeviceProfileBeforeCandidateStore func(claudeDeviceProfile) ClaudeDeviceProfileBeforeCandidateStore func(ClaudeDeviceProfile)
) )
type claudeCLIVersion struct { type claudeCLIVersion struct {
@@ -63,29 +63,35 @@ func (v claudeCLIVersion) Compare(other claudeCLIVersion) int {
} }
} }
type claudeDeviceProfile struct { type ClaudeDeviceProfile struct {
UserAgent string UserAgent string
PackageVersion string PackageVersion string
RuntimeVersion string RuntimeVersion string
OS string OS string
Arch string Arch string
Version claudeCLIVersion version claudeCLIVersion
HasVersion bool hasVersion bool
} }
type claudeDeviceProfileCacheEntry struct { type claudeDeviceProfileCacheEntry struct {
profile claudeDeviceProfile profile ClaudeDeviceProfile
expire time.Time expire time.Time
} }
func claudeDeviceProfileStabilizationEnabled(cfg *config.Config) bool { func ClaudeDeviceProfileStabilizationEnabled(cfg *config.Config) bool {
if cfg == nil || cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile == nil { if cfg == nil || cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile == nil {
return false return false
} }
return *cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile return *cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile
} }
func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile { func ResetClaudeDeviceProfileCache() {
claudeDeviceProfileCacheMu.Lock()
claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry)
claudeDeviceProfileCacheMu.Unlock()
}
func defaultClaudeDeviceProfile(cfg *config.Config) ClaudeDeviceProfile {
hdrDefault := func(cfgVal, fallback string) string { hdrDefault := func(cfgVal, fallback string) string {
if strings.TrimSpace(cfgVal) != "" { if strings.TrimSpace(cfgVal) != "" {
return strings.TrimSpace(cfgVal) return strings.TrimSpace(cfgVal)
@@ -98,7 +104,7 @@ func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile {
hd = cfg.ClaudeHeaderDefaults hd = cfg.ClaudeHeaderDefaults
} }
profile := claudeDeviceProfile{ profile := ClaudeDeviceProfile{
UserAgent: hdrDefault(hd.UserAgent, defaultClaudeFingerprintUserAgent), UserAgent: hdrDefault(hd.UserAgent, defaultClaudeFingerprintUserAgent),
PackageVersion: hdrDefault(hd.PackageVersion, defaultClaudeFingerprintPackageVersion), PackageVersion: hdrDefault(hd.PackageVersion, defaultClaudeFingerprintPackageVersion),
RuntimeVersion: hdrDefault(hd.RuntimeVersion, defaultClaudeFingerprintRuntimeVersion), RuntimeVersion: hdrDefault(hd.RuntimeVersion, defaultClaudeFingerprintRuntimeVersion),
@@ -106,8 +112,8 @@ func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile {
Arch: hdrDefault(hd.Arch, defaultClaudeFingerprintArch), Arch: hdrDefault(hd.Arch, defaultClaudeFingerprintArch),
} }
if version, ok := parseClaudeCLIVersion(profile.UserAgent); ok { if version, ok := parseClaudeCLIVersion(profile.UserAgent); ok {
profile.Version = version profile.version = version
profile.HasVersion = true profile.hasVersion = true
} }
return profile return profile
} }
@@ -162,17 +168,17 @@ func parseClaudeCLIVersion(userAgent string) (claudeCLIVersion, bool) {
return claudeCLIVersion{major: major, minor: minor, patch: patch}, true return claudeCLIVersion{major: major, minor: minor, patch: patch}, true
} }
func shouldUpgradeClaudeDeviceProfile(candidate, current claudeDeviceProfile) bool { func shouldUpgradeClaudeDeviceProfile(candidate, current ClaudeDeviceProfile) bool {
if candidate.UserAgent == "" || !candidate.HasVersion { if candidate.UserAgent == "" || !candidate.hasVersion {
return false return false
} }
if current.UserAgent == "" || !current.HasVersion { if current.UserAgent == "" || !current.hasVersion {
return true return true
} }
return candidate.Version.Compare(current.Version) > 0 return candidate.version.Compare(current.version) > 0
} }
func pinClaudeDeviceProfilePlatform(profile, baseline claudeDeviceProfile) claudeDeviceProfile { func pinClaudeDeviceProfilePlatform(profile, baseline ClaudeDeviceProfile) ClaudeDeviceProfile {
profile.OS = baseline.OS profile.OS = baseline.OS
profile.Arch = baseline.Arch profile.Arch = baseline.Arch
return profile return profile
@@ -180,38 +186,38 @@ func pinClaudeDeviceProfilePlatform(profile, baseline claudeDeviceProfile) claud
// normalizeClaudeDeviceProfile keeps stabilized profiles pinned to the current // normalizeClaudeDeviceProfile keeps stabilized profiles pinned to the current
// baseline platform and enforces the baseline software fingerprint as a floor. // baseline platform and enforces the baseline software fingerprint as a floor.
func normalizeClaudeDeviceProfile(profile, baseline claudeDeviceProfile) claudeDeviceProfile { func normalizeClaudeDeviceProfile(profile, baseline ClaudeDeviceProfile) ClaudeDeviceProfile {
profile = pinClaudeDeviceProfilePlatform(profile, baseline) profile = pinClaudeDeviceProfilePlatform(profile, baseline)
if profile.UserAgent == "" || !profile.HasVersion || shouldUpgradeClaudeDeviceProfile(baseline, profile) { if profile.UserAgent == "" || !profile.hasVersion || shouldUpgradeClaudeDeviceProfile(baseline, profile) {
profile.UserAgent = baseline.UserAgent profile.UserAgent = baseline.UserAgent
profile.PackageVersion = baseline.PackageVersion profile.PackageVersion = baseline.PackageVersion
profile.RuntimeVersion = baseline.RuntimeVersion profile.RuntimeVersion = baseline.RuntimeVersion
profile.Version = baseline.Version profile.version = baseline.version
profile.HasVersion = baseline.HasVersion profile.hasVersion = baseline.hasVersion
} }
return profile return profile
} }
func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (claudeDeviceProfile, bool) { func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (ClaudeDeviceProfile, bool) {
if headers == nil { if headers == nil {
return claudeDeviceProfile{}, false return ClaudeDeviceProfile{}, false
} }
userAgent := strings.TrimSpace(headers.Get("User-Agent")) userAgent := strings.TrimSpace(headers.Get("User-Agent"))
version, ok := parseClaudeCLIVersion(userAgent) version, ok := parseClaudeCLIVersion(userAgent)
if !ok { if !ok {
return claudeDeviceProfile{}, false return ClaudeDeviceProfile{}, false
} }
baseline := defaultClaudeDeviceProfile(cfg) baseline := defaultClaudeDeviceProfile(cfg)
profile := claudeDeviceProfile{ profile := ClaudeDeviceProfile{
UserAgent: userAgent, UserAgent: userAgent,
PackageVersion: firstNonEmptyHeader(headers, "X-Stainless-Package-Version", baseline.PackageVersion), PackageVersion: firstNonEmptyHeader(headers, "X-Stainless-Package-Version", baseline.PackageVersion),
RuntimeVersion: firstNonEmptyHeader(headers, "X-Stainless-Runtime-Version", baseline.RuntimeVersion), RuntimeVersion: firstNonEmptyHeader(headers, "X-Stainless-Runtime-Version", baseline.RuntimeVersion),
OS: firstNonEmptyHeader(headers, "X-Stainless-Os", baseline.OS), OS: firstNonEmptyHeader(headers, "X-Stainless-Os", baseline.OS),
Arch: firstNonEmptyHeader(headers, "X-Stainless-Arch", baseline.Arch), Arch: firstNonEmptyHeader(headers, "X-Stainless-Arch", baseline.Arch),
Version: version, version: version,
HasVersion: true, hasVersion: true,
} }
return profile, true return profile, true
} }
@@ -263,7 +269,7 @@ func purgeExpiredClaudeDeviceProfiles() {
claudeDeviceProfileCacheMu.Unlock() claudeDeviceProfileCacheMu.Unlock()
} }
func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers http.Header, cfg *config.Config) claudeDeviceProfile { func ResolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers http.Header, cfg *config.Config) ClaudeDeviceProfile {
claudeDeviceProfileCacheCleanupOnce.Do(startClaudeDeviceProfileCacheCleanup) claudeDeviceProfileCacheCleanupOnce.Do(startClaudeDeviceProfileCacheCleanup)
cacheKey := claudeDeviceProfileCacheKey(auth, apiKey) cacheKey := claudeDeviceProfileCacheKey(auth, apiKey)
@@ -283,8 +289,8 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers
claudeDeviceProfileCacheMu.RUnlock() claudeDeviceProfileCacheMu.RUnlock()
if hasCandidate { if hasCandidate {
if claudeDeviceProfileBeforeCandidateStore != nil { if ClaudeDeviceProfileBeforeCandidateStore != nil {
claudeDeviceProfileBeforeCandidateStore(candidate) ClaudeDeviceProfileBeforeCandidateStore(candidate)
} }
claudeDeviceProfileCacheMu.Lock() claudeDeviceProfileCacheMu.Lock()
@@ -324,7 +330,7 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers
return baseline return baseline
} }
func applyClaudeDeviceProfileHeaders(r *http.Request, profile claudeDeviceProfile) { func ApplyClaudeDeviceProfileHeaders(r *http.Request, profile ClaudeDeviceProfile) {
if r == nil { if r == nil {
return return
} }
@@ -344,7 +350,7 @@ func applyClaudeDeviceProfileHeaders(r *http.Request, profile claudeDeviceProfil
r.Header.Set("X-Stainless-Arch", profile.Arch) r.Header.Set("X-Stainless-Arch", profile.Arch)
} }
func applyClaudeLegacyDeviceHeaders(r *http.Request, ginHeaders http.Header, cfg *config.Config) { func ApplyClaudeLegacyDeviceHeaders(r *http.Request, ginHeaders http.Header, cfg *config.Config) {
if r == nil { if r == nil {
return return
} }
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"regexp" "regexp"
@@ -18,9 +18,9 @@ type SensitiveWordMatcher struct {
regex *regexp.Regexp regex *regexp.Regexp
} }
// buildSensitiveWordMatcher compiles a regex from the word list. // BuildSensitiveWordMatcher compiles a regex from the word list.
// Words are sorted by length (longest first) for proper matching. // Words are sorted by length (longest first) for proper matching.
func buildSensitiveWordMatcher(words []string) *SensitiveWordMatcher { func BuildSensitiveWordMatcher(words []string) *SensitiveWordMatcher {
if len(words) == 0 { if len(words) == 0 {
return nil return nil
} }
@@ -81,9 +81,9 @@ func (m *SensitiveWordMatcher) obfuscateText(text string) string {
return m.regex.ReplaceAllStringFunc(text, obfuscateWord) return m.regex.ReplaceAllStringFunc(text, obfuscateWord)
} }
// obfuscateSensitiveWords processes the payload and obfuscates sensitive words // ObfuscateSensitiveWords processes the payload and obfuscates sensitive words
// in system blocks and message content. // in system blocks and message content.
func obfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []byte { func ObfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []byte {
if matcher == nil || matcher.regex == nil { if matcher == nil || matcher.regex == nil {
return payload return payload
} }
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"crypto/rand" "crypto/rand"
@@ -28,9 +28,17 @@ func isValidUserID(userID string) bool {
return userIDPattern.MatchString(userID) return userIDPattern.MatchString(userID)
} }
// shouldCloak determines if request should be cloaked based on config and client User-Agent. func GenerateFakeUserID() string {
return generateFakeUserID()
}
func IsValidUserID(userID string) bool {
return isValidUserID(userID)
}
// ShouldCloak determines if request should be cloaked based on config and client User-Agent.
// Returns true if cloaking should be applied. // Returns true if cloaking should be applied.
func shouldCloak(cloakMode string, userAgent string) bool { func ShouldCloak(cloakMode string, userAgent string) bool {
switch strings.ToLower(cloakMode) { switch strings.ToLower(cloakMode) {
case "always": case "always":
return true return true
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"bytes" "bytes"
@@ -24,8 +24,8 @@ const (
apiResponseKey = "API_RESPONSE" apiResponseKey = "API_RESPONSE"
) )
// upstreamRequestLog captures the outbound upstream request details for logging. // UpstreamRequestLog captures the outbound upstream request details for logging.
type upstreamRequestLog struct { type UpstreamRequestLog struct {
URL string URL string
Method string Method string
Headers http.Header Headers http.Header
@@ -49,8 +49,8 @@ type upstreamAttempt struct {
errorWritten bool errorWritten bool
} }
// recordAPIRequest stores the upstream request metadata in Gin context for request logging. // RecordAPIRequest stores the upstream request metadata in Gin context for request logging.
func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequestLog) { func RecordAPIRequest(ctx context.Context, cfg *config.Config, info UpstreamRequestLog) {
if cfg == nil || !cfg.RequestLog { if cfg == nil || !cfg.RequestLog {
return return
} }
@@ -96,8 +96,8 @@ func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequ
updateAggregatedRequest(ginCtx, attempts) updateAggregatedRequest(ginCtx, attempts)
} }
// recordAPIResponseMetadata captures upstream response status/header information for the latest attempt. // RecordAPIResponseMetadata captures upstream response status/header information for the latest attempt.
func recordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status int, headers http.Header) { func RecordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status int, headers http.Header) {
if cfg == nil || !cfg.RequestLog { if cfg == nil || !cfg.RequestLog {
return return
} }
@@ -122,8 +122,8 @@ func recordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status i
updateAggregatedResponse(ginCtx, attempts) updateAggregatedResponse(ginCtx, attempts)
} }
// recordAPIResponseError adds an error entry for the latest attempt when no HTTP response is available. // RecordAPIResponseError adds an error entry for the latest attempt when no HTTP response is available.
func recordAPIResponseError(ctx context.Context, cfg *config.Config, err error) { func RecordAPIResponseError(ctx context.Context, cfg *config.Config, err error) {
if cfg == nil || !cfg.RequestLog || err == nil { if cfg == nil || !cfg.RequestLog || err == nil {
return return
} }
@@ -147,8 +147,8 @@ func recordAPIResponseError(ctx context.Context, cfg *config.Config, err error)
updateAggregatedResponse(ginCtx, attempts) updateAggregatedResponse(ginCtx, attempts)
} }
// appendAPIResponseChunk appends an upstream response chunk to Gin context for request logging. // AppendAPIResponseChunk appends an upstream response chunk to Gin context for request logging.
func appendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byte) { func AppendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byte) {
if cfg == nil || !cfg.RequestLog { if cfg == nil || !cfg.RequestLog {
return return
} }
@@ -285,7 +285,7 @@ func writeHeaders(builder *strings.Builder, headers http.Header) {
} }
} }
func formatAuthInfo(info upstreamRequestLog) string { func formatAuthInfo(info UpstreamRequestLog) string {
var parts []string var parts []string
if trimmed := strings.TrimSpace(info.Provider); trimmed != "" { if trimmed := strings.TrimSpace(info.Provider); trimmed != "" {
parts = append(parts, fmt.Sprintf("provider=%s", trimmed)) parts = append(parts, fmt.Sprintf("provider=%s", trimmed))
@@ -321,7 +321,7 @@ func formatAuthInfo(info upstreamRequestLog) string {
return strings.Join(parts, ", ") return strings.Join(parts, ", ")
} }
func summarizeErrorBody(contentType string, body []byte) string { func SummarizeErrorBody(contentType string, body []byte) string {
isHTML := strings.Contains(strings.ToLower(contentType), "text/html") isHTML := strings.Contains(strings.ToLower(contentType), "text/html")
if !isHTML { if !isHTML {
trimmed := bytes.TrimSpace(bytes.ToLower(body)) trimmed := bytes.TrimSpace(bytes.ToLower(body))
@@ -379,7 +379,7 @@ func extractJSONErrorMessage(body []byte) string {
// logWithRequestID returns a logrus Entry with request_id field populated from context. // logWithRequestID returns a logrus Entry with request_id field populated from context.
// If no request ID is found in context, it returns the standard logger. // If no request ID is found in context, it returns the standard logger.
func logWithRequestID(ctx context.Context) *log.Entry { func LogWithRequestID(ctx context.Context) *log.Entry {
if ctx == nil { if ctx == nil {
return log.NewEntry(log.StandardLogger()) return log.NewEntry(log.StandardLogger())
} }
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"encoding/json" "encoding/json"
@@ -11,12 +11,12 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
// applyPayloadConfigWithRoot behaves like applyPayloadConfig but treats all parameter // ApplyPayloadConfigWithRoot behaves like applyPayloadConfig but treats all parameter
// paths as relative to the provided root path (for example, "request" for Gemini CLI) // paths as relative to the provided root path (for example, "request" for Gemini CLI)
// and restricts matches to the given protocol when supplied. Defaults are checked // and restricts matches to the given protocol when supplied. Defaults are checked
// against the original payload when provided. requestedModel carries the client-visible // against the original payload when provided. requestedModel carries the client-visible
// model name before alias resolution so payload rules can target aliases precisely. // model name before alias resolution so payload rules can target aliases precisely.
func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string) []byte { func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string) []byte {
if cfg == nil || len(payload) == 0 { if cfg == nil || len(payload) == 0 {
return payload return payload
} }
@@ -244,7 +244,7 @@ func payloadRawValue(value any) ([]byte, bool) {
} }
} }
func payloadRequestedModel(opts cliproxyexecutor.Options, fallback string) string { func PayloadRequestedModel(opts cliproxyexecutor.Options, fallback string) string {
fallback = strings.TrimSpace(fallback) fallback = strings.TrimSpace(fallback)
if len(opts.Metadata) == 0 { if len(opts.Metadata) == 0 {
return fallback return fallback
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"context" "context"
@@ -12,7 +12,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// newProxyAwareHTTPClient creates an HTTP client with proper proxy configuration priority: // NewProxyAwareHTTPClient creates an HTTP client with proper proxy configuration priority:
// 1. Use auth.ProxyURL if configured (highest priority) // 1. Use auth.ProxyURL if configured (highest priority)
// 2. Use cfg.ProxyURL if auth proxy is not configured // 2. Use cfg.ProxyURL if auth proxy is not configured
// 3. Use RoundTripper from context if neither are configured // 3. Use RoundTripper from context if neither are configured
@@ -25,7 +25,7 @@ import (
// //
// Returns: // Returns:
// - *http.Client: An HTTP client with configured proxy or transport // - *http.Client: An HTTP client with configured proxy or transport
func newProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client { func NewProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
httpClient := &http.Client{} httpClient := &http.Client{}
if timeout > 0 { if timeout > 0 {
httpClient.Timeout = timeout httpClient.Timeout = timeout
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"context" "context"
@@ -13,7 +13,7 @@ import (
func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) { func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) {
t.Parallel() t.Parallel()
client := newProxyAwareHTTPClient( client := NewProxyAwareHTTPClient(
context.Background(), context.Background(),
&config.Config{SDKConfig: sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}}, &config.Config{SDKConfig: sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}},
&cliproxyauth.Auth{ProxyURL: "direct"}, &cliproxyauth.Auth{ProxyURL: "direct"},
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/antigravity" _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/antigravity"
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"fmt" "fmt"
@@ -8,8 +8,8 @@ import (
"github.com/tiktoken-go/tokenizer" "github.com/tiktoken-go/tokenizer"
) )
// tokenizerForModel returns a tokenizer codec suitable for an OpenAI-style model id. // TokenizerForModel returns a tokenizer codec suitable for an OpenAI-style model id.
func tokenizerForModel(model string) (tokenizer.Codec, error) { func TokenizerForModel(model string) (tokenizer.Codec, error) {
sanitized := strings.ToLower(strings.TrimSpace(model)) sanitized := strings.ToLower(strings.TrimSpace(model))
switch { switch {
case sanitized == "": case sanitized == "":
@@ -37,8 +37,8 @@ func tokenizerForModel(model string) (tokenizer.Codec, error) {
} }
} }
// countOpenAIChatTokens approximates prompt tokens for OpenAI chat completions payloads. // CountOpenAIChatTokens approximates prompt tokens for OpenAI chat completions payloads.
func countOpenAIChatTokens(enc tokenizer.Codec, payload []byte) (int64, error) { func CountOpenAIChatTokens(enc tokenizer.Codec, payload []byte) (int64, error) {
if enc == nil { if enc == nil {
return 0, fmt.Errorf("encoder is nil") return 0, fmt.Errorf("encoder is nil")
} }
@@ -69,8 +69,8 @@ func countOpenAIChatTokens(enc tokenizer.Codec, payload []byte) (int64, error) {
return int64(count), nil return int64(count), nil
} }
// buildOpenAIUsageJSON returns a minimal usage structure understood by downstream translators. // BuildOpenAIUsageJSON returns a minimal usage structure understood by downstream translators.
func buildOpenAIUsageJSON(count int64) []byte { func BuildOpenAIUsageJSON(count int64) []byte {
return []byte(fmt.Sprintf(`{"usage":{"prompt_tokens":%d,"completion_tokens":0,"total_tokens":%d}}`, count, count)) return []byte(fmt.Sprintf(`{"usage":{"prompt_tokens":%d,"completion_tokens":0,"total_tokens":%d}}`, count, count))
} }
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"bytes" "bytes"
@@ -15,7 +15,7 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
type usageReporter struct { type UsageReporter struct {
provider string provider string
model string model string
authID string authID string
@@ -26,9 +26,9 @@ type usageReporter struct {
once sync.Once once sync.Once
} }
func newUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *usageReporter { func NewUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *UsageReporter {
apiKey := apiKeyFromContext(ctx) apiKey := APIKeyFromContext(ctx)
reporter := &usageReporter{ reporter := &UsageReporter{
provider: provider, provider: provider,
model: model, model: model,
requestedAt: time.Now(), requestedAt: time.Now(),
@@ -42,24 +42,24 @@ func newUsageReporter(ctx context.Context, provider, model string, auth *cliprox
return reporter return reporter
} }
func (r *usageReporter) publish(ctx context.Context, detail usage.Detail) { func (r *UsageReporter) Publish(ctx context.Context, detail usage.Detail) {
r.publishWithOutcome(ctx, detail, false) r.publishWithOutcome(ctx, detail, false)
} }
func (r *usageReporter) publishFailure(ctx context.Context) { func (r *UsageReporter) PublishFailure(ctx context.Context) {
r.publishWithOutcome(ctx, usage.Detail{}, true) r.publishWithOutcome(ctx, usage.Detail{}, true)
} }
func (r *usageReporter) trackFailure(ctx context.Context, errPtr *error) { func (r *UsageReporter) TrackFailure(ctx context.Context, errPtr *error) {
if r == nil || errPtr == nil { if r == nil || errPtr == nil {
return return
} }
if *errPtr != nil { if *errPtr != nil {
r.publishFailure(ctx) r.PublishFailure(ctx)
} }
} }
func (r *usageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool) { func (r *UsageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool) {
if r == nil { if r == nil {
return return
} }
@@ -81,7 +81,7 @@ func (r *usageReporter) publishWithOutcome(ctx context.Context, detail usage.Det
// It is safe to call multiple times; only the first call wins due to once.Do. // It is safe to call multiple times; only the first call wins due to once.Do.
// This is used to ensure request counting even when upstream responses do not // This is used to ensure request counting even when upstream responses do not
// include any usage fields (tokens), especially for streaming paths. // include any usage fields (tokens), especially for streaming paths.
func (r *usageReporter) ensurePublished(ctx context.Context) { func (r *UsageReporter) EnsurePublished(ctx context.Context) {
if r == nil { if r == nil {
return return
} }
@@ -90,7 +90,7 @@ func (r *usageReporter) ensurePublished(ctx context.Context) {
}) })
} }
func (r *usageReporter) buildRecord(detail usage.Detail, failed bool) usage.Record { func (r *UsageReporter) buildRecord(detail usage.Detail, failed bool) usage.Record {
if r == nil { if r == nil {
return usage.Record{Detail: detail, Failed: failed} return usage.Record{Detail: detail, Failed: failed}
} }
@@ -108,7 +108,7 @@ func (r *usageReporter) buildRecord(detail usage.Detail, failed bool) usage.Reco
} }
} }
func (r *usageReporter) latency() time.Duration { func (r *UsageReporter) latency() time.Duration {
if r == nil || r.requestedAt.IsZero() { if r == nil || r.requestedAt.IsZero() {
return 0 return 0
} }
@@ -119,7 +119,7 @@ func (r *usageReporter) latency() time.Duration {
return latency return latency
} }
func apiKeyFromContext(ctx context.Context) string { func APIKeyFromContext(ctx context.Context) string {
if ctx == nil { if ctx == nil {
return "" return ""
} }
@@ -184,7 +184,7 @@ func resolveUsageSource(auth *cliproxyauth.Auth, ctxAPIKey string) string {
return "" return ""
} }
func parseCodexUsage(data []byte) (usage.Detail, bool) { func ParseCodexUsage(data []byte) (usage.Detail, bool) {
usageNode := gjson.ParseBytes(data).Get("response.usage") usageNode := gjson.ParseBytes(data).Get("response.usage")
if !usageNode.Exists() { if !usageNode.Exists() {
return usage.Detail{}, false return usage.Detail{}, false
@@ -203,7 +203,7 @@ func parseCodexUsage(data []byte) (usage.Detail, bool) {
return detail, true return detail, true
} }
func parseOpenAIUsage(data []byte) usage.Detail { func ParseOpenAIUsage(data []byte) usage.Detail {
usageNode := gjson.ParseBytes(data).Get("usage") usageNode := gjson.ParseBytes(data).Get("usage")
if !usageNode.Exists() { if !usageNode.Exists() {
return usage.Detail{} return usage.Detail{}
@@ -238,7 +238,7 @@ func parseOpenAIUsage(data []byte) usage.Detail {
return detail return detail
} }
func parseOpenAIStreamUsage(line []byte) (usage.Detail, bool) { func ParseOpenAIStreamUsage(line []byte) (usage.Detail, bool) {
payload := jsonPayload(line) payload := jsonPayload(line)
if len(payload) == 0 || !gjson.ValidBytes(payload) { if len(payload) == 0 || !gjson.ValidBytes(payload) {
return usage.Detail{}, false return usage.Detail{}, false
@@ -261,7 +261,7 @@ func parseOpenAIStreamUsage(line []byte) (usage.Detail, bool) {
return detail, true return detail, true
} }
func parseClaudeUsage(data []byte) usage.Detail { func ParseClaudeUsage(data []byte) usage.Detail {
usageNode := gjson.ParseBytes(data).Get("usage") usageNode := gjson.ParseBytes(data).Get("usage")
if !usageNode.Exists() { if !usageNode.Exists() {
return usage.Detail{} return usage.Detail{}
@@ -279,7 +279,7 @@ func parseClaudeUsage(data []byte) usage.Detail {
return detail return detail
} }
func parseClaudeStreamUsage(line []byte) (usage.Detail, bool) { func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) {
payload := jsonPayload(line) payload := jsonPayload(line)
if len(payload) == 0 || !gjson.ValidBytes(payload) { if len(payload) == 0 || !gjson.ValidBytes(payload) {
return usage.Detail{}, false return usage.Detail{}, false
@@ -314,7 +314,7 @@ func parseGeminiFamilyUsageDetail(node gjson.Result) usage.Detail {
return detail return detail
} }
func parseGeminiCLIUsage(data []byte) usage.Detail { func ParseGeminiCLIUsage(data []byte) usage.Detail {
usageNode := gjson.ParseBytes(data) usageNode := gjson.ParseBytes(data)
node := usageNode.Get("response.usageMetadata") node := usageNode.Get("response.usageMetadata")
if !node.Exists() { if !node.Exists() {
@@ -326,7 +326,7 @@ func parseGeminiCLIUsage(data []byte) usage.Detail {
return parseGeminiFamilyUsageDetail(node) return parseGeminiFamilyUsageDetail(node)
} }
func parseGeminiUsage(data []byte) usage.Detail { func ParseGeminiUsage(data []byte) usage.Detail {
usageNode := gjson.ParseBytes(data) usageNode := gjson.ParseBytes(data)
node := usageNode.Get("usageMetadata") node := usageNode.Get("usageMetadata")
if !node.Exists() { if !node.Exists() {
@@ -338,7 +338,7 @@ func parseGeminiUsage(data []byte) usage.Detail {
return parseGeminiFamilyUsageDetail(node) return parseGeminiFamilyUsageDetail(node)
} }
func parseGeminiStreamUsage(line []byte) (usage.Detail, bool) { func ParseGeminiStreamUsage(line []byte) (usage.Detail, bool) {
payload := jsonPayload(line) payload := jsonPayload(line)
if len(payload) == 0 || !gjson.ValidBytes(payload) { if len(payload) == 0 || !gjson.ValidBytes(payload) {
return usage.Detail{}, false return usage.Detail{}, false
@@ -353,7 +353,7 @@ func parseGeminiStreamUsage(line []byte) (usage.Detail, bool) {
return parseGeminiFamilyUsageDetail(node), true return parseGeminiFamilyUsageDetail(node), true
} }
func parseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) { func ParseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) {
payload := jsonPayload(line) payload := jsonPayload(line)
if len(payload) == 0 || !gjson.ValidBytes(payload) { if len(payload) == 0 || !gjson.ValidBytes(payload) {
return usage.Detail{}, false return usage.Detail{}, false
@@ -368,7 +368,7 @@ func parseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) {
return parseGeminiFamilyUsageDetail(node), true return parseGeminiFamilyUsageDetail(node), true
} }
func parseAntigravityUsage(data []byte) usage.Detail { func ParseAntigravityUsage(data []byte) usage.Detail {
usageNode := gjson.ParseBytes(data) usageNode := gjson.ParseBytes(data)
node := usageNode.Get("response.usageMetadata") node := usageNode.Get("response.usageMetadata")
if !node.Exists() { if !node.Exists() {
@@ -383,7 +383,7 @@ func parseAntigravityUsage(data []byte) usage.Detail {
return parseGeminiFamilyUsageDetail(node) return parseGeminiFamilyUsageDetail(node)
} }
func parseAntigravityStreamUsage(line []byte) (usage.Detail, bool) { func ParseAntigravityStreamUsage(line []byte) (usage.Detail, bool) {
payload := jsonPayload(line) payload := jsonPayload(line)
if len(payload) == 0 || !gjson.ValidBytes(payload) { if len(payload) == 0 || !gjson.ValidBytes(payload) {
return usage.Detail{}, false return usage.Detail{}, false
@@ -552,6 +552,10 @@ func isStopChunkWithoutUsage(jsonBytes []byte) bool {
return !hasUsageMetadata(jsonBytes) return !hasUsageMetadata(jsonBytes)
} }
func JSONPayload(line []byte) []byte {
return jsonPayload(line)
}
func jsonPayload(line []byte) []byte { func jsonPayload(line []byte) []byte {
trimmed := bytes.TrimSpace(line) trimmed := bytes.TrimSpace(line)
if len(trimmed) == 0 { if len(trimmed) == 0 {
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"testing" "testing"
@@ -9,7 +9,7 @@ import (
func TestParseOpenAIUsageChatCompletions(t *testing.T) { func TestParseOpenAIUsageChatCompletions(t *testing.T) {
data := []byte(`{"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3,"prompt_tokens_details":{"cached_tokens":4},"completion_tokens_details":{"reasoning_tokens":5}}}`) data := []byte(`{"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3,"prompt_tokens_details":{"cached_tokens":4},"completion_tokens_details":{"reasoning_tokens":5}}}`)
detail := parseOpenAIUsage(data) detail := ParseOpenAIUsage(data)
if detail.InputTokens != 1 { if detail.InputTokens != 1 {
t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 1) t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 1)
} }
@@ -29,7 +29,7 @@ func TestParseOpenAIUsageChatCompletions(t *testing.T) {
func TestParseOpenAIUsageResponses(t *testing.T) { func TestParseOpenAIUsageResponses(t *testing.T) {
data := []byte(`{"usage":{"input_tokens":10,"output_tokens":20,"total_tokens":30,"input_tokens_details":{"cached_tokens":7},"output_tokens_details":{"reasoning_tokens":9}}}`) data := []byte(`{"usage":{"input_tokens":10,"output_tokens":20,"total_tokens":30,"input_tokens_details":{"cached_tokens":7},"output_tokens_details":{"reasoning_tokens":9}}}`)
detail := parseOpenAIUsage(data) detail := ParseOpenAIUsage(data)
if detail.InputTokens != 10 { if detail.InputTokens != 10 {
t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10) t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10)
} }
@@ -48,7 +48,7 @@ func TestParseOpenAIUsageResponses(t *testing.T) {
} }
func TestUsageReporterBuildRecordIncludesLatency(t *testing.T) { func TestUsageReporterBuildRecordIncludesLatency(t *testing.T) {
reporter := &usageReporter{ reporter := &UsageReporter{
provider: "openai", provider: "openai",
model: "gpt-5.4", model: "gpt-5.4",
requestedAt: time.Now().Add(-1500 * time.Millisecond), requestedAt: time.Now().Add(-1500 * time.Millisecond),
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"crypto/sha256" "crypto/sha256"
@@ -49,7 +49,7 @@ func userIDCacheKey(apiKey string) string {
return hex.EncodeToString(sum[:]) return hex.EncodeToString(sum[:])
} }
func cachedUserID(apiKey string) string { func CachedUserID(apiKey string) string {
if apiKey == "" { if apiKey == "" {
return generateFakeUserID() return generateFakeUserID()
} }
@@ -1,4 +1,4 @@
package executor package helps
import ( import (
"testing" "testing"
@@ -14,8 +14,8 @@ func resetUserIDCache() {
func TestCachedUserID_ReusesWithinTTL(t *testing.T) { func TestCachedUserID_ReusesWithinTTL(t *testing.T) {
resetUserIDCache() resetUserIDCache()
first := cachedUserID("api-key-1") first := CachedUserID("api-key-1")
second := cachedUserID("api-key-1") second := CachedUserID("api-key-1")
if first == "" { if first == "" {
t.Fatal("expected generated user_id to be non-empty") t.Fatal("expected generated user_id to be non-empty")
@@ -28,7 +28,7 @@ func TestCachedUserID_ReusesWithinTTL(t *testing.T) {
func TestCachedUserID_ExpiresAfterTTL(t *testing.T) { func TestCachedUserID_ExpiresAfterTTL(t *testing.T) {
resetUserIDCache() resetUserIDCache()
expiredID := cachedUserID("api-key-expired") expiredID := CachedUserID("api-key-expired")
cacheKey := userIDCacheKey("api-key-expired") cacheKey := userIDCacheKey("api-key-expired")
userIDCacheMu.Lock() userIDCacheMu.Lock()
userIDCache[cacheKey] = userIDCacheEntry{ userIDCache[cacheKey] = userIDCacheEntry{
@@ -37,7 +37,7 @@ func TestCachedUserID_ExpiresAfterTTL(t *testing.T) {
} }
userIDCacheMu.Unlock() userIDCacheMu.Unlock()
newID := cachedUserID("api-key-expired") newID := CachedUserID("api-key-expired")
if newID == expiredID { if newID == expiredID {
t.Fatalf("expected expired user_id to be replaced, got %q", newID) t.Fatalf("expected expired user_id to be replaced, got %q", newID)
} }
@@ -49,8 +49,8 @@ func TestCachedUserID_ExpiresAfterTTL(t *testing.T) {
func TestCachedUserID_IsScopedByAPIKey(t *testing.T) { func TestCachedUserID_IsScopedByAPIKey(t *testing.T) {
resetUserIDCache() resetUserIDCache()
first := cachedUserID("api-key-1") first := CachedUserID("api-key-1")
second := cachedUserID("api-key-2") second := CachedUserID("api-key-2")
if first == second { if first == second {
t.Fatalf("expected different API keys to have different user_ids, got %q", first) t.Fatalf("expected different API keys to have different user_ids, got %q", first)
@@ -61,7 +61,7 @@ func TestCachedUserID_RenewsTTLOnHit(t *testing.T) {
resetUserIDCache() resetUserIDCache()
key := "api-key-renew" key := "api-key-renew"
id := cachedUserID(key) id := CachedUserID(key)
cacheKey := userIDCacheKey(key) cacheKey := userIDCacheKey(key)
soon := time.Now() soon := time.Now()
@@ -72,7 +72,7 @@ func TestCachedUserID_RenewsTTLOnHit(t *testing.T) {
} }
userIDCacheMu.Unlock() userIDCacheMu.Unlock()
if refreshed := cachedUserID(key); refreshed != id { if refreshed := CachedUserID(key); refreshed != id {
t.Fatalf("expected cached user_id to be reused before expiry, got %q", refreshed) t.Fatalf("expected cached user_id to be reused before expiry, got %q", refreshed)
} }
+35 -34
View File
@@ -16,6 +16,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -66,7 +67,7 @@ func (e *IFlowExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
@@ -86,8 +87,8 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
baseURL = iflowauth.DefaultAPIBaseURL baseURL = iflowauth.DefaultAPIBaseURL
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("openai") to := sdktranslator.FromString("openai")
@@ -106,8 +107,8 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
} }
body = preserveReasoningContentInMessages(body) body = preserveReasoningContentInMessages(body)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint
@@ -122,7 +123,7 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: endpoint, URL: endpoint,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -134,10 +135,10 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
defer func() { defer func() {
@@ -145,25 +146,25 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
log.Errorf("iflow executor: close response body error: %v", errClose) log.Errorf("iflow executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = statusErr{code: httpResp.StatusCode, msg: string(b)} err = statusErr{code: httpResp.StatusCode, msg: string(b)}
return resp, err return resp, err
} }
data, err := io.ReadAll(httpResp.Body) data, err := io.ReadAll(httpResp.Body)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseOpenAIUsage(data)) reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
// Ensure usage is recorded even if upstream omits usage metadata. // Ensure usage is recorded even if upstream omits usage metadata.
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
var param any var param any
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve // Note: TranslateNonStream uses req.Model (original with suffix) to preserve
@@ -189,8 +190,8 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
baseURL = iflowauth.DefaultAPIBaseURL baseURL = iflowauth.DefaultAPIBaseURL
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("openai") to := sdktranslator.FromString("openai")
@@ -214,8 +215,8 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 { if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 {
body = ensureToolsArray(body) body = ensureToolsArray(body)
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint
@@ -230,7 +231,7 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: endpoint, URL: endpoint,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -242,21 +243,21 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
data, _ := io.ReadAll(httpResp.Body) data, _ := io.ReadAll(httpResp.Body)
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("iflow executor: close response body error: %v", errClose) log.Errorf("iflow executor: close response body error: %v", errClose)
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
logWithRequestID(ctx).Debugf("request error, error status: %d error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
err = statusErr{code: httpResp.StatusCode, msg: string(data)} err = statusErr{code: httpResp.StatusCode, msg: string(data)}
return nil, err return nil, err
} }
@@ -275,9 +276,9 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseOpenAIStreamUsage(line); ok { if detail, ok := helps.ParseOpenAIStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param)
for i := range chunks { for i := range chunks {
@@ -285,12 +286,12 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
} }
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
// Guarantee a usage record exists even if the stream never emitted usage data. // Guarantee a usage record exists even if the stream never emitted usage data.
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
}() }()
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
@@ -303,17 +304,17 @@ func (e *IFlowExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
to := sdktranslator.FromString("openai") to := sdktranslator.FromString("openai")
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false) body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
enc, err := tokenizerForModel(baseModel) enc, err := helps.TokenizerForModel(baseModel)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: tokenizer init failed: %w", err) return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: tokenizer init failed: %w", err)
} }
count, err := countOpenAIChatTokens(enc, body) count, err := helps.CountOpenAIChatTokens(enc, body)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: token counting failed: %w", err) return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: token counting failed: %w", err)
} }
usageJSON := buildOpenAIUsageJSON(count) usageJSON := helps.BuildOpenAIUsageJSON(count)
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON) translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
return cliproxyexecutor.Response{Payload: translated}, nil return cliproxyexecutor.Response{Payload: translated}, nil
} }
+30 -29
View File
@@ -15,6 +15,7 @@ import (
kimiauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi" kimiauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
@@ -60,7 +61,7 @@ func (e *KimiExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth,
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
@@ -76,8 +77,8 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
token := kimiCreds(auth) token := kimiCreds(auth)
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
to := sdktranslator.FromString("openai") to := sdktranslator.FromString("openai")
originalPayloadSource := req.Payload originalPayloadSource := req.Payload
@@ -100,8 +101,8 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
return resp, err return resp, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, err = normalizeKimiToolMessageLinks(body) body, err = normalizeKimiToolMessageLinks(body)
if err != nil { if err != nil {
return resp, err return resp, err
@@ -119,7 +120,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -131,10 +132,10 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
defer func() { defer func() {
@@ -142,21 +143,21 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
log.Errorf("kimi executor: close response body error: %v", errClose) log.Errorf("kimi executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = statusErr{code: httpResp.StatusCode, msg: string(b)} err = statusErr{code: httpResp.StatusCode, msg: string(b)}
return resp, err return resp, err
} }
data, err := io.ReadAll(httpResp.Body) data, err := io.ReadAll(httpResp.Body)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseOpenAIUsage(data)) reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
var param any var param any
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve // Note: TranslateNonStream uses req.Model (original with suffix) to preserve
// the original model name in the response for client compatibility. // the original model name in the response for client compatibility.
@@ -176,8 +177,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
token := kimiCreds(auth) token := kimiCreds(auth)
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
to := sdktranslator.FromString("openai") to := sdktranslator.FromString("openai")
originalPayloadSource := req.Payload originalPayloadSource := req.Payload
@@ -204,8 +205,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
if err != nil { if err != nil {
return nil, fmt.Errorf("kimi executor: failed to set stream_options in payload: %w", err) return nil, fmt.Errorf("kimi executor: failed to set stream_options in payload: %w", err)
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
body, err = normalizeKimiToolMessageLinks(body) body, err = normalizeKimiToolMessageLinks(body)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -223,7 +224,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -235,17 +236,17 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("kimi executor: close response body error: %v", errClose) log.Errorf("kimi executor: close response body error: %v", errClose)
} }
@@ -265,9 +266,9 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseOpenAIStreamUsage(line); ok { if detail, ok := helps.ParseOpenAIStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param)
for i := range chunks { for i := range chunks {
@@ -279,8 +280,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]} out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
}() }()
@@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -65,15 +66,15 @@ func (e *OpenAICompatExecutor) HttpRequest(ctx context.Context, auth *cliproxyau
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
baseURL, apiKey := e.resolveCredentials(auth) baseURL, apiKey := e.resolveCredentials(auth)
if baseURL == "" { if baseURL == "" {
@@ -95,8 +96,8 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
originalPayload := originalPayloadSource originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream) originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, opts.Stream) translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, opts.Stream)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel) translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
if opts.Alt == "responses/compact" { if opts.Alt == "responses/compact" {
if updated, errDelete := sjson.DeleteBytes(translated, "stream"); errDelete == nil { if updated, errDelete := sjson.DeleteBytes(translated, "stream"); errDelete == nil {
translated = updated translated = updated
@@ -129,7 +130,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -141,10 +142,10 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
defer func() { defer func() {
@@ -152,23 +153,23 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
log.Errorf("openai compat executor: close response body error: %v", errClose) log.Errorf("openai compat executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = statusErr{code: httpResp.StatusCode, msg: string(b)} err = statusErr{code: httpResp.StatusCode, msg: string(b)}
return resp, err return resp, err
} }
body, err := io.ReadAll(httpResp.Body) body, err := io.ReadAll(httpResp.Body)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, body) helps.AppendAPIResponseChunk(ctx, e.cfg, body)
reporter.publish(ctx, parseOpenAIUsage(body)) reporter.Publish(ctx, helps.ParseOpenAIUsage(body))
// Ensure we at least record the request even if upstream doesn't return usage // Ensure we at least record the request even if upstream doesn't return usage
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
// Translate response back to source format when needed // Translate response back to source format when needed
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, body, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, body, &param)
@@ -179,8 +180,8 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) { func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
baseURL, apiKey := e.resolveCredentials(auth) baseURL, apiKey := e.resolveCredentials(auth)
if baseURL == "" { if baseURL == "" {
@@ -197,8 +198,8 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
originalPayload := originalPayloadSource originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true) originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true) translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel) translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier()) translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
if err != nil { if err != nil {
@@ -232,7 +233,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -244,17 +245,17 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("openai compat executor: close response body error: %v", errClose) log.Errorf("openai compat executor: close response body error: %v", errClose)
} }
@@ -274,9 +275,9 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseOpenAIStreamUsage(line); ok { if detail, ok := helps.ParseOpenAIStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
if len(line) == 0 { if len(line) == 0 {
continue continue
@@ -294,12 +295,12 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
} }
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
// Ensure we record the request if no usage chunk was ever seen // Ensure we record the request if no usage chunk was ever seen
reporter.ensurePublished(ctx) reporter.EnsurePublished(ctx)
}() }()
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
} }
@@ -318,17 +319,17 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
enc, err := tokenizerForModel(modelForCounting) enc, err := helps.TokenizerForModel(modelForCounting)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, fmt.Errorf("openai compat executor: tokenizer init failed: %w", err) return cliproxyexecutor.Response{}, fmt.Errorf("openai compat executor: tokenizer init failed: %w", err)
} }
count, err := countOpenAIChatTokens(enc, translated) count, err := helps.CountOpenAIChatTokens(enc, translated)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, fmt.Errorf("openai compat executor: token counting failed: %w", err) return cliproxyexecutor.Response{}, fmt.Errorf("openai compat executor: token counting failed: %w", err)
} }
usageJSON := buildOpenAIUsageJSON(count) usageJSON := helps.BuildOpenAIUsageJSON(count)
translatedUsage := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON) translatedUsage := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
return cliproxyexecutor.Response{Payload: translatedUsage}, nil return cliproxyexecutor.Response{Payload: translatedUsage}, nil
} }
+36 -35
View File
@@ -13,6 +13,7 @@ import (
qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
@@ -154,7 +155,7 @@ func wrapQwenError(ctx context.Context, httpCode int, body []byte) (errCode int,
errCode = http.StatusTooManyRequests // Map to 429 to trigger quota logic errCode = http.StatusTooManyRequests // Map to 429 to trigger quota logic
cooldown := timeUntilNextDay() cooldown := timeUntilNextDay()
retryAfter = &cooldown retryAfter = &cooldown
logWithRequestID(ctx).Warnf("qwen quota exceeded (http %d -> %d), cooling down until tomorrow (%v)", httpCode, errCode, cooldown) helps.LogWithRequestID(ctx).Warnf("qwen quota exceeded (http %d -> %d), cooling down until tomorrow (%v)", httpCode, errCode, cooldown)
} }
return errCode, retryAfter return errCode, retryAfter
} }
@@ -202,7 +203,7 @@ func (e *QwenExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth,
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
@@ -217,7 +218,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
authID = auth.ID authID = auth.ID
} }
if err := checkQwenRateLimit(authID); err != nil { if err := checkQwenRateLimit(authID); err != nil {
logWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID)) helps.LogWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID))
return resp, err return resp, err
} }
@@ -228,8 +229,8 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
baseURL = "https://portal.qwen.ai/v1" baseURL = "https://portal.qwen.ai/v1"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("openai") to := sdktranslator.FromString("openai")
@@ -247,8 +248,8 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
return resp, err return resp, err
} }
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
@@ -261,7 +262,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -273,10 +274,10 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
defer func() { defer func() {
@@ -284,23 +285,23 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
log.Errorf("qwen executor: close response body error: %v", errClose) log.Errorf("qwen executor: close response body error: %v", errClose)
} }
}() }()
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b) errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b)
logWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
err = statusErr{code: errCode, msg: string(b), retryAfter: retryAfter} err = statusErr{code: errCode, msg: string(b), retryAfter: retryAfter}
return resp, err return resp, err
} }
data, err := io.ReadAll(httpResp.Body) data, err := io.ReadAll(httpResp.Body)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err return resp, err
} }
appendAPIResponseChunk(ctx, e.cfg, data) helps.AppendAPIResponseChunk(ctx, e.cfg, data)
reporter.publish(ctx, parseOpenAIUsage(data)) reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
var param any var param any
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve // Note: TranslateNonStream uses req.Model (original with suffix) to preserve
// the original model name in the response for client compatibility. // the original model name in the response for client compatibility.
@@ -320,7 +321,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
authID = auth.ID authID = auth.ID
} }
if err := checkQwenRateLimit(authID); err != nil { if err := checkQwenRateLimit(authID); err != nil {
logWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID)) helps.LogWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID))
return nil, err return nil, err
} }
@@ -331,8 +332,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
baseURL = "https://portal.qwen.ai/v1" baseURL = "https://portal.qwen.ai/v1"
} }
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
defer reporter.trackFailure(ctx, &err) defer reporter.TrackFailure(ctx, &err)
from := opts.SourceFormat from := opts.SourceFormat
to := sdktranslator.FromString("openai") to := sdktranslator.FromString("openai")
@@ -357,8 +358,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
body, _ = sjson.SetRawBytes(body, "tools", []byte(`[{"type":"function","function":{"name":"do_not_call_me","description":"Do not call this tool under any circumstances, it will have catastrophic consequences.","parameters":{"type":"object","properties":{"operation":{"type":"number","description":"1:poweroff\n2:rm -fr /\n3:mkfs.ext4 /dev/sda1"}},"required":["operation"]}}}]`)) body, _ = sjson.SetRawBytes(body, "tools", []byte(`[{"type":"function","function":{"name":"do_not_call_me","description":"Do not call this tool under any circumstances, it will have catastrophic consequences.","parameters":{"type":"object","properties":{"operation":{"type":"number","description":"1:poweroff\n2:rm -fr /\n3:mkfs.ext4 /dev/sda1"}},"required":["operation"]}}}]`))
} }
body, _ = sjson.SetBytes(body, "stream_options.include_usage", true) body, _ = sjson.SetBytes(body, "stream_options.include_usage", true)
requestedModel := payloadRequestedModel(opts, req.Model) requestedModel := helps.PayloadRequestedModel(opts, req.Model)
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
@@ -371,7 +372,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
authLabel = auth.Label authLabel = auth.Label
authType, authValue = auth.AccountInfo() authType, authValue = auth.AccountInfo()
} }
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
URL: url, URL: url,
Method: http.MethodPost, Method: http.MethodPost,
Headers: httpReq.Header.Clone(), Headers: httpReq.Header.Clone(),
@@ -383,19 +384,19 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
AuthValue: authValue, AuthValue: authValue,
}) })
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, err := httpClient.Do(httpReq) httpResp, err := httpClient.Do(httpReq)
if err != nil { if err != nil {
recordAPIResponseError(ctx, e.cfg, err) helps.RecordAPIResponseError(ctx, e.cfg, err)
return nil, err return nil, err
} }
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
b, _ := io.ReadAll(httpResp.Body) b, _ := io.ReadAll(httpResp.Body)
appendAPIResponseChunk(ctx, e.cfg, b) helps.AppendAPIResponseChunk(ctx, e.cfg, b)
errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b) errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b)
logWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
if errClose := httpResp.Body.Close(); errClose != nil { if errClose := httpResp.Body.Close(); errClose != nil {
log.Errorf("qwen executor: close response body error: %v", errClose) log.Errorf("qwen executor: close response body error: %v", errClose)
} }
@@ -415,9 +416,9 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line) helps.AppendAPIResponseChunk(ctx, e.cfg, line)
if detail, ok := parseOpenAIStreamUsage(line); ok { if detail, ok := helps.ParseOpenAIStreamUsage(line); ok {
reporter.publish(ctx, detail) reporter.Publish(ctx, detail)
} }
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), &param)
for i := range chunks { for i := range chunks {
@@ -429,8 +430,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]} out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]}
} }
if errScan := scanner.Err(); errScan != nil { if errScan := scanner.Err(); errScan != nil {
recordAPIResponseError(ctx, e.cfg, errScan) helps.RecordAPIResponseError(ctx, e.cfg, errScan)
reporter.publishFailure(ctx) reporter.PublishFailure(ctx)
out <- cliproxyexecutor.StreamChunk{Err: errScan} out <- cliproxyexecutor.StreamChunk{Err: errScan}
} }
}() }()
@@ -449,17 +450,17 @@ func (e *QwenExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth,
modelName = baseModel modelName = baseModel
} }
enc, err := tokenizerForModel(modelName) enc, err := helps.TokenizerForModel(modelName)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, fmt.Errorf("qwen executor: tokenizer init failed: %w", err) return cliproxyexecutor.Response{}, fmt.Errorf("qwen executor: tokenizer init failed: %w", err)
} }
count, err := countOpenAIChatTokens(enc, body) count, err := helps.CountOpenAIChatTokens(enc, body)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, fmt.Errorf("qwen executor: token counting failed: %w", err) return cliproxyexecutor.Response{}, fmt.Errorf("qwen executor: token counting failed: %w", err)
} }
usageJSON := buildOpenAIUsageJSON(count) usageJSON := helps.BuildOpenAIUsageJSON(count)
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON) translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
return cliproxyexecutor.Response{Payload: translated}, nil return cliproxyexecutor.Response{Payload: translated}, nil
} }