From d2c7e4e96a7115cc065b6349dbc40a04f5a9e926 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 1 Apr 2026 03:08:20 +0800 Subject: [PATCH] refactor(runtime): move executor utilities to `helps` package and update references --- .../runtime/executor/aistudio_executor.go | 71 +++++---- .../runtime/executor/antigravity_executor.go | 129 +++++++-------- internal/runtime/executor/claude_executor.go | 141 +++++++++-------- .../runtime/executor/claude_executor_test.go | 17 +- internal/runtime/executor/codex_executor.go | 101 ++++++------ .../executor/codex_websockets_executor.go | 92 +++++------ .../runtime/executor/gemini_cli_executor.go | 83 +++++----- internal/runtime/executor/gemini_executor.go | 83 +++++----- .../executor/gemini_vertex_executor.go | 149 +++++++++--------- .../executor/{ => helps}/cache_helpers.go | 16 +- .../{ => helps}/claude_device_profile.go | 68 ++++---- .../executor/{ => helps}/cloak_obfuscate.go | 10 +- .../executor/{ => helps}/cloak_utils.go | 14 +- .../executor/{ => helps}/logging_helpers.go | 28 ++-- .../executor/{ => helps}/payload_helpers.go | 8 +- .../executor/{ => helps}/proxy_helpers.go | 6 +- .../{ => helps}/proxy_helpers_test.go | 4 +- .../{ => helps}/thinking_providers.go | 2 +- .../executor/{ => helps}/token_helpers.go | 14 +- .../executor/{ => helps}/usage_helpers.go | 54 ++++--- .../{ => helps}/usage_helpers_test.go | 8 +- .../executor/{ => helps}/user_id_cache.go | 4 +- .../{ => helps}/user_id_cache_test.go | 18 +-- internal/runtime/executor/iflow_executor.go | 69 ++++---- internal/runtime/executor/kimi_executor.go | 59 +++---- .../executor/openai_compat_executor.go | 69 ++++---- internal/runtime/executor/qwen_executor.go | 71 +++++---- 27 files changed, 712 insertions(+), 676 deletions(-) rename internal/runtime/executor/{ => helps}/cache_helpers.go (81%) rename internal/runtime/executor/{ => helps}/claude_device_profile.go (84%) rename internal/runtime/executor/{ => helps}/cloak_obfuscate.go (93%) rename internal/runtime/executor/{ => helps}/cloak_utils.go (83%) rename internal/runtime/executor/{ => helps}/logging_helpers.go (92%) rename internal/runtime/executor/{ => helps}/payload_helpers.go (97%) rename internal/runtime/executor/{ => helps}/proxy_helpers.go (94%) rename internal/runtime/executor/{ => helps}/proxy_helpers_test.go (93%) rename internal/runtime/executor/{ => helps}/thinking_providers.go (97%) rename internal/runtime/executor/{ => helps}/token_helpers.go (94%) rename internal/runtime/executor/{ => helps}/usage_helpers.go (91%) rename internal/runtime/executor/{ => helps}/usage_helpers_test.go (94%) rename internal/runtime/executor/{ => helps}/user_id_cache.go (96%) rename internal/runtime/executor/{ => helps}/user_id_cache_test.go (83%) diff --git a/internal/runtime/executor/aistudio_executor.go b/internal/runtime/executor/aistudio_executor.go index db56a183..01c4e06e 100644 --- a/internal/runtime/executor/aistudio_executor.go +++ b/internal/runtime/executor/aistudio_executor.go @@ -14,6 +14,7 @@ import ( "strings" "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/wsrelay" 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"} } baseModel := thinking.ParseSuffix(req.Model).ModelName - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) translatedReq, body, err := e.translateRequest(req, opts, false) if err != nil { @@ -137,7 +138,7 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: endpoint, Method: http.MethodPost, 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) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { - appendAPIResponseChunk(ctx, e.cfg, wsResp.Body) + helps.AppendAPIResponseChunk(ctx, e.cfg, wsResp.Body) } if wsResp.Status < 200 || wsResp.Status >= 300 { 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 out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, wsResp.Body, ¶m) 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"} } baseModel := thinking.ParseSuffix(req.Model).ModelName - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) translatedReq, body, err := e.translateRequest(req, opts, true) if err != nil { @@ -195,7 +196,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: endpoint, Method: http.MethodPost, 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) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return nil, err } firstEvent, ok := <-wsStream if !ok { err = fmt.Errorf("wsrelay: stream closed before start") - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return nil, err } if firstEvent.Status > 0 && firstEvent.Status != http.StatusOK { metadataLogged := false 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 } var body bytes.Buffer if len(firstEvent.Payload) > 0 { - appendAPIResponseChunk(ctx, e.cfg, firstEvent.Payload) + helps.AppendAPIResponseChunk(ctx, e.cfg, firstEvent.Payload) body.Write(firstEvent.Payload) } if firstEvent.Type == wsrelay.MessageTypeStreamEnd { @@ -233,18 +234,18 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth } for event := range wsStream { if event.Err != nil { - recordAPIResponseError(ctx, e.cfg, event.Err) + helps.RecordAPIResponseError(ctx, e.cfg, event.Err) if body.Len() == 0 { body.WriteString(event.Err.Error()) } break } 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 } if len(event.Payload) > 0 { - appendAPIResponseChunk(ctx, e.cfg, event.Payload) + helps.AppendAPIResponseChunk(ctx, e.cfg, event.Payload) body.Write(event.Payload) } if event.Type == wsrelay.MessageTypeStreamEnd { @@ -260,23 +261,23 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth metadataLogged := false processEvent := func(event wsrelay.StreamEvent) bool { if event.Err != nil { - recordAPIResponseError(ctx, e.cfg, event.Err) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, event.Err) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)} return false } switch event.Type { case wsrelay.MessageTypeStreamStart: 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 } case wsrelay.MessageTypeStreamChunk: if len(event.Payload) > 0 { - appendAPIResponseChunk(ctx, e.cfg, event.Payload) - filtered := FilterSSEUsageMetadata(event.Payload) - if detail, ok := parseGeminiStreamUsage(filtered); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, event.Payload) + filtered := helps.FilterSSEUsageMetadata(event.Payload) + if detail, ok := helps.ParseGeminiStreamUsage(filtered); ok { + reporter.Publish(ctx, detail) } lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, filtered, ¶m) for i := range lines { @@ -288,21 +289,21 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth return false case wsrelay.MessageTypeHTTPResp: 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 } 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, ¶m) for i := range lines { out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON(lines[i])} } - reporter.publish(ctx, parseGeminiUsage(event.Payload)) + reporter.Publish(ctx, helps.ParseGeminiUsage(event.Payload)) return false case wsrelay.MessageTypeError: - recordAPIResponseError(ctx, e.cfg, event.Err) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, event.Err) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)} return false } @@ -345,7 +346,7 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: endpoint, Method: http.MethodPost, 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) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { - appendAPIResponseChunk(ctx, e.cfg, resp.Body) + helps.AppendAPIResponseChunk(ctx, e.cfg, resp.Body) } if resp.Status < 200 || resp.Status >= 300 { 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 } payload = fixGeminiImageAspectRatio(baseModel, payload) - requestedModel := payloadRequestedModel(opts, req.Model) - payload = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + payload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel) payload, _ = sjson.DeleteBytes(payload, "generationConfig.maxOutputTokens") payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseMimeType") payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseJsonSchema") diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 6ee972a7..d72dc035 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -24,6 +24,7 @@ import ( "github.com/google/uuid" "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/util" 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 { 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 client.Transport == nil { client.Transport = antigravityTransport @@ -405,12 +406,12 @@ func (e *AntigravityExecutor) attemptCreditsFallback( httpReq, errReq := e.buildRequest(ctx, auth, token, modelName, creditsPayload, stream, alt, baseURL) if errReq != nil { - recordAPIResponseError(ctx, e.cfg, errReq) + helps.RecordAPIResponseError(ctx, e.cfg, errReq) return nil, true } httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, errDo) return nil, true } if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices { @@ -420,16 +421,16 @@ func (e *AntigravityExecutor) attemptCreditsFallback( 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) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("antigravity executor: close credits fallback response body error: %v", errClose) } if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) return nil, true } - appendAPIResponseChunk(ctx, e.cfg, bodyBytes) + helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes) if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) { clearAntigravityPreferCredits(auth, modelName) markAntigravityCreditsExhausted(auth, now) @@ -457,8 +458,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au auth = updatedAuth } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("antigravity") @@ -476,8 +477,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au return resp, err } - requestedModel := payloadRequestedModel(opts, req.Model) - translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) baseURLs := antigravityBaseURLFallbackOrder(auth) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) @@ -507,7 +508,7 @@ attemptLoop: httpResp, errDo := httpClient.Do(httpReq) 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) { return resp, errDo } @@ -522,17 +523,17 @@ attemptLoop: 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) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("antigravity executor: close response body error: %v", errClose) } if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) err = errRead return resp, err } - appendAPIResponseChunk(ctx, e.cfg, bodyBytes) + helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes) if httpResp.StatusCode == http.StatusTooManyRequests { if usedCreditsDirect { @@ -543,29 +544,29 @@ attemptLoop: } else { creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, false, opts.Alt, baseURL, bodyBytes) 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) if errClose := creditsResp.Body.Close(); errClose != nil { log.Errorf("antigravity executor: close credits success response body error: %v", errClose) } if errCreditsRead != nil { - recordAPIResponseError(ctx, e.cfg, errCreditsRead) + helps.RecordAPIResponseError(ctx, e.cfg, errCreditsRead) err = errCreditsRead return resp, err } - appendAPIResponseChunk(ctx, e.cfg, creditsBody) - reporter.publish(ctx, parseAntigravityUsage(creditsBody)) + helps.AppendAPIResponseChunk(ctx, e.cfg, creditsBody) + reporter.Publish(ctx, helps.ParseAntigravityUsage(creditsBody)) var param any converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, creditsBody, ¶m) resp = cliproxyexecutor.Response{Payload: converted, Headers: creditsResp.Header.Clone()} - reporter.ensurePublished(ctx) + reporter.EnsurePublished(ctx) return resp, nil } } } 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 lastBody = append([]byte(nil), bodyBytes...) lastErr = nil @@ -591,11 +592,11 @@ attemptLoop: return resp, err } - reporter.publish(ctx, parseAntigravityUsage(bodyBytes)) + reporter.Publish(ctx, helps.ParseAntigravityUsage(bodyBytes)) var param any converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, ¶m) resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()} - reporter.ensurePublished(ctx) + reporter.EnsurePublished(ctx) return resp, nil } @@ -625,8 +626,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth * auth = updatedAuth } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("antigravity") @@ -644,8 +645,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth * return resp, err } - requestedModel := payloadRequestedModel(opts, req.Model) - translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) baseURLs := antigravityBaseURLFallbackOrder(auth) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) @@ -675,7 +676,7 @@ attemptLoop: httpResp, errDo := httpClient.Do(httpReq) 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) { return resp, errDo } @@ -689,14 +690,14 @@ attemptLoop: err = errDo 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 { bodyBytes, errRead := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("antigravity executor: close response body error: %v", errClose) } 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) { err = errRead return resp, err @@ -715,7 +716,7 @@ attemptLoop: err = errRead return resp, err } - appendAPIResponseChunk(ctx, e.cfg, bodyBytes) + helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes) if httpResp.StatusCode == http.StatusTooManyRequests { if usedCreditsDirect { 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) if creditsResp != nil { 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) for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) // Filter usage metadata for all models // 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 { continue } - if detail, ok := parseAntigravityStreamUsage(payload); ok { - reporter.publish(ctx, detail) + if detail, ok := helps.ParseAntigravityStreamUsage(payload); ok { + reporter.Publish(ctx, detail) } out <- cliproxyexecutor.StreamChunk{Payload: payload} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } else { - reporter.ensurePublished(ctx) + reporter.EnsurePublished(ctx) } }(httpResp) @@ -809,11 +810,11 @@ attemptLoop: } resp = cliproxyexecutor.Response{Payload: e.convertStreamToNonStream(buffer.Bytes())} - reporter.publish(ctx, parseAntigravityUsage(resp.Payload)) + reporter.Publish(ctx, helps.ParseAntigravityUsage(resp.Payload)) var param any converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, resp.Payload, ¶m) resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()} - reporter.ensurePublished(ctx) + reporter.EnsurePublished(ctx) return resp, nil } @@ -1042,8 +1043,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya auth = updatedAuth } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("antigravity") @@ -1061,8 +1062,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya return nil, err } - requestedModel := payloadRequestedModel(opts, req.Model) - translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) baseURLs := antigravityBaseURLFallbackOrder(auth) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) @@ -1091,7 +1092,7 @@ attemptLoop: } httpResp, errDo := httpClient.Do(httpReq) 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) { return nil, errDo } @@ -1105,14 +1106,14 @@ attemptLoop: err = errDo 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 { bodyBytes, errRead := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("antigravity executor: close response body error: %v", errClose) } 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) { err = errRead return nil, err @@ -1131,7 +1132,7 @@ attemptLoop: err = errRead return nil, err } - appendAPIResponseChunk(ctx, e.cfg, bodyBytes) + helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes) if httpResp.StatusCode == http.StatusTooManyRequests { if usedCreditsDirect { 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) if creditsResp != nil { 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 for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) // Filter usage metadata for all models // 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 { continue } - if detail, ok := parseAntigravityStreamUsage(payload); ok { - reporter.publish(ctx, detail) + if detail, ok := helps.ParseAntigravityStreamUsage(payload); ok { + reporter.Publish(ctx, detail) } chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), ¶m) @@ -1213,11 +1214,11 @@ attemptLoop: out <- cliproxyexecutor.StreamChunk{Payload: tail[i]} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } else { - reporter.ensurePublished(ctx) + reporter.EnsurePublished(ctx) } }(httpResp) 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 } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: requestURL.String(), Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -1334,7 +1335,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut httpResp, errDo := httpClient.Do(httpReq) 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) { return cliproxyexecutor.Response{}, errDo } @@ -1348,16 +1349,16 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut 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) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("antigravity executor: close response body error: %v", errClose) } if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { 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 { payloadLog = []byte(payloadStr) } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: requestURL.String(), Method: http.MethodPost, Headers: httpReq.Header.Clone(), diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index cc88dd77..c417bc33 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -23,6 +23,7 @@ import ( "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/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/util" 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 { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -106,8 +107,8 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r baseURL = "https://api.anthropic.com" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("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. body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = ensureModelMaxTokens(body, baseModel) // 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 authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -184,33 +185,33 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { // Decompress error responses — pass the Content-Encoding value (may be empty) // and let decodeResponseBody handle both header-declared and magic-byte-detected // compression. This keeps error-path behaviour consistent with the success path. errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding")) 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) - logWithRequestID(ctx).Warn(msg) + helps.LogWithRequestID(ctx).Warn(msg) return resp, statusErr{code: httpResp.StatusCode, msg: msg} } b, readErr := io.ReadAll(errBody) 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) - logWithRequestID(ctx).Warn(msg) + helps.LogWithRequestID(ctx).Warn(msg) b = []byte(msg) } - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} if errClose := errBody.Close(); errClose != nil { 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")) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) if errClose := httpResp.Body.Close(); errClose != nil { 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) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) if stream { lines := bytes.Split(data, []byte("\n")) for _, line := range lines { - if detail, ok := parseClaudeStreamUsage(line); ok { - reporter.publish(ctx, detail) + if detail, ok := helps.ParseClaudeStreamUsage(line); ok { + reporter.Publish(ctx, detail) } } } else { - reporter.publish(ctx, parseClaudeUsage(data)) + reporter.Publish(ctx, helps.ParseClaudeUsage(data)) } if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { data = stripClaudeToolPrefixFromResponse(data, claudeToolPrefix) @@ -275,8 +276,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A baseURL = "https://api.anthropic.com" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("claude") originalPayloadSource := req.Payload @@ -297,8 +298,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A // based on client type and configuration. body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body = ensureModelMaxTokens(body, baseModel) // 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 authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -348,33 +349,33 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { // Decompress error responses — pass the Content-Encoding value (may be empty) // and let decodeResponseBody handle both header-declared and magic-byte-detected // compression. This keeps error-path behaviour consistent with the success path. errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding")) 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) - logWithRequestID(ctx).Warn(msg) + helps.LogWithRequestID(ctx).Warn(msg) return nil, statusErr{code: httpResp.StatusCode, msg: msg} } b, readErr := io.ReadAll(errBody) 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) - logWithRequestID(ctx).Warn(msg) + helps.LogWithRequestID(ctx).Warn(msg) b = []byte(msg) } - 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.AppendAPIResponseChunk(ctx, e.cfg, 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 { 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")) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) if errClose := httpResp.Body.Close(); errClose != nil { 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 for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseClaudeStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseClaudeStreamUsage(line); ok { + reporter.Publish(ctx, detail) } if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix) @@ -418,8 +419,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A out <- cliproxyexecutor.StreamChunk{Payload: cloned} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } return @@ -431,9 +432,9 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A var param any for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseClaudeStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseClaudeStreamUsage(line); ok { + reporter.Publish(ctx, detail) } if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() { line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix) @@ -453,8 +454,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } }() @@ -503,7 +504,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -515,32 +516,32 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { // Decompress error responses — pass the Content-Encoding value (may be empty) // and let decodeResponseBody handle both header-declared and magic-byte-detected // compression. This keeps error-path behaviour consistent with the success path. errBody, decErr := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding")) 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) - logWithRequestID(ctx).Warn(msg) + helps.LogWithRequestID(ctx).Warn(msg) return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: msg} } b, readErr := io.ReadAll(errBody) 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) - logWithRequestID(ctx).Warn(msg) + helps.LogWithRequestID(ctx).Warn(msg) b = []byte(msg) } - appendAPIResponseChunk(ctx, e.cfg, b) + helps.AppendAPIResponseChunk(ctx, e.cfg, b) if errClose := errBody.Close(); errClose != nil { 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")) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) if errClose := resp.Body.Close(); errClose != nil { 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) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) count := gjson.GetBytes(data, "input_tokens").Int() out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) 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 { ginHeaders = ginCtx.Request.Header } - stabilizeDeviceProfile := claudeDeviceProfileStabilizationEnabled(cfg) - var deviceProfile claudeDeviceProfile + stabilizeDeviceProfile := helps.ClaudeDeviceProfileStabilizationEnabled(cfg) + var deviceProfile helps.ClaudeDeviceProfile 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" @@ -871,9 +872,9 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, } util.ApplyCustomHeadersFromAttrs(r, attrs) if stabilizeDeviceProfile { - applyClaudeDeviceProfileHeaders(r, deviceProfile) + helps.ApplyClaudeDeviceProfileHeaders(r, deviceProfile) } else { - applyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg) + helps.ApplyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg) } // Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which // 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 == "" { return line } - payload := jsonPayload(line) + payload := helps.JSONPayload(line) if len(payload) == 0 || !gjson.ValidBytes(payload) { return line } @@ -1156,9 +1157,9 @@ func resolveClaudeKeyCloakConfig(cfg *config.Config, auth *cliproxyauth.Auth) *c func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte { generateID := func() string { if useCache { - return cachedUserID(apiKey) + return helps.CachedUserID(apiKey) } - return generateFakeUserID() + return helps.GenerateFakeUserID() } 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() - if existingUserID == "" || !isValidUserID(existingUserID) { + if existingUserID == "" || !helps.IsValidUserID(existingUserID) { payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateID()) } return payload @@ -1292,7 +1293,7 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A } // Determine if cloaking should be applied - if !shouldCloak(cloakMode, clientUserAgent) { + if !helps.ShouldCloak(cloakMode, clientUserAgent) { return payload } @@ -1306,8 +1307,8 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A // Apply sensitive word obfuscation if len(sensitiveWords) > 0 { - matcher := buildSensitiveWordMatcher(sensitiveWords) - payload = obfuscateSensitiveWords(payload, matcher) + matcher := helps.BuildSensitiveWordMatcher(sensitiveWords) + payload = helps.ObfuscateSensitiveWords(payload, matcher) } return payload diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index ee8e9025..b6acdda4 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -16,6 +16,7 @@ import ( "github.com/klauspost/compress/zstd" "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/runtime/executor/helps" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" @@ -24,9 +25,7 @@ import ( ) func resetClaudeDeviceProfileCache() { - claudeDeviceProfileCacheMu.Lock() - claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry) - claudeDeviceProfileCacheMu.Unlock() + helps.ResetClaudeDeviceProfileCache() } func newClaudeHeaderTestRequest(t *testing.T, incoming http.Header) *http.Request { @@ -339,7 +338,7 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi var pauseOnce 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)" { return } @@ -347,13 +346,13 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi <-releaseLow } t.Cleanup(func() { - claudeDeviceProfileBeforeCandidateStore = nil + helps.ClaudeDeviceProfileBeforeCandidateStore = nil releaseOnce.Do(func() { close(releaseLow) }) }) - lowResultCh := make(chan claudeDeviceProfile, 1) + lowResultCh := make(chan helps.ClaudeDeviceProfile, 1) 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)"}, "X-Stainless-Package-Version": []string{"0.74.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") } - 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)"}, "X-Stainless-Package-Version": []string{"0.75.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") } - cached := resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{ + cached := helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{ "User-Agent": []string{"curl/8.7.1"}, }, cfg) if cached.UserAgent != "claude-cli/2.1.63 (external, cli)" { diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 9eafb6be..d404302a 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -13,6 +13,7 @@ import ( 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/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/util" 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 { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) 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" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("codex") @@ -106,8 +107,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re return resp, err } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "stream", true) 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 authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -139,10 +140,10 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re AuthType: authType, AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } 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) } }() - 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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) return resp, err } data, err := io.ReadAll(httpResp.Body) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) lines := bytes.Split(data, []byte("\n")) for _, line := range lines { @@ -176,8 +177,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re continue } - if detail, ok := parseCodexUsage(line); ok { - reporter.publish(ctx, detail) + if detail, ok := helps.ParseCodexUsage(line); ok { + reporter.Publish(ctx, detail) } var param any @@ -197,8 +198,8 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A baseURL = "https://chatgpt.com/backend-api/codex" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("openai-response") @@ -215,8 +216,8 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A return resp, err } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.DeleteBytes(body, "stream") body = normalizeCodexInstructions(body) @@ -233,7 +234,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -244,10 +245,10 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A AuthType: authType, AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } 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) } }() - 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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) return resp, err } data, err := io.ReadAll(httpResp.Body) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - appendAPIResponseChunk(ctx, e.cfg, data) - reporter.publish(ctx, parseOpenAIUsage(data)) - reporter.ensurePublished(ctx) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + reporter.Publish(ctx, helps.ParseOpenAIUsage(data)) + reporter.EnsurePublished(ctx) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, data, ¶m) 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" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("codex") @@ -306,8 +307,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au return nil, err } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.DeleteBytes(body, "previous_response_id") body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") body, _ = sjson.DeleteBytes(body, "safety_identifier") @@ -327,7 +328,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -339,24 +340,24 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { data, readErr := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } if readErr != nil { - recordAPIResponseError(ctx, e.cfg, readErr) + helps.RecordAPIResponseError(ctx, e.cfg, readErr) return nil, readErr } - 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.AppendAPIResponseChunk(ctx, e.cfg, 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) return nil, err } @@ -373,13 +374,13 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au var param any for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) if bytes.HasPrefix(line, dataTag) { data := bytes.TrimSpace(line[5:]) if gjson.GetBytes(data, "type").String() == "response.completed" { - if detail, ok := parseCodexUsage(data); ok { - reporter.publish(ctx, detail) + if detail, ok := helps.ParseCodexUsage(data); ok { + reporter.Publish(ctx, detail) } } } @@ -390,8 +391,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) 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) { - var cache codexCache + var cache helps.CodexCache if from == "claude" { userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id") if userIDResult.Exists() { key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String()) var ok bool - if cache, ok = getCodexCache(key); !ok { - cache = codexCache{ + if cache, ok = helps.GetCodexCache(key); !ok { + cache = helps.CodexCache{ ID: uuid.New().String(), Expire: time.Now().Add(1 * time.Hour), } - setCodexCache(key, cache) + helps.SetCodexCache(key, cache) } } } else if from == "openai-response" { @@ -615,7 +616,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form cache.ID = promptCacheKey.String() } } 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() } } diff --git a/internal/runtime/executor/codex_websockets_executor.go b/internal/runtime/executor/codex_websockets_executor.go index fca82fe7..fdfccd9a 100644 --- a/internal/runtime/executor/codex_websockets_executor.go +++ b/internal/runtime/executor/codex_websockets_executor.go @@ -15,10 +15,12 @@ import ( "sync" "time" + "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/gorilla/websocket" "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/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" 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" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("codex") @@ -173,8 +175,8 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut return resp, err } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) body, _ = sjson.SetBytes(body, "stream", true) body, _ = sjson.DeleteBytes(body, "previous_response_id") @@ -209,7 +211,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut } wsReqBody := buildCodexWebsocketRequestBody(body) - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", 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) 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 { bodyErr := websocketHandshakeBody(respHS) if len(bodyErr) > 0 { - appendAPIResponseChunk(ctx, e.cfg, bodyErr) + helps.AppendAPIResponseChunk(ctx, e.cfg, bodyErr) } if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired { 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 { return resp, statusErr{code: respHS.StatusCode, msg: string(bodyErr)} } - recordAPIResponseError(ctx, e.cfg, errDial) + helps.RecordAPIResponseError(ctx, e.cfg, errDial) return resp, errDial } 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) if errDialRetry == nil && connRetry != nil { wsReqBodyRetry := buildCodexWebsocketRequestBody(body) - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", Headers: wsHeaders.Clone(), @@ -287,15 +289,15 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut wsReqBody = wsReqBodyRetry } else { e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry) - recordAPIResponseError(ctx, e.cfg, errSendRetry) + helps.RecordAPIResponseError(ctx, e.cfg, errSendRetry) return resp, errSendRetry } } else { - recordAPIResponseError(ctx, e.cfg, errDialRetry) + helps.RecordAPIResponseError(ctx, e.cfg, errDialRetry) return resp, errDialRetry } } else { - recordAPIResponseError(ctx, e.cfg, errSend) + helps.RecordAPIResponseError(ctx, e.cfg, 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) if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } if msgType != websocket.TextMessage { @@ -315,7 +317,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut if sess != nil { e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err) } - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } continue @@ -325,21 +327,21 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut if len(payload) == 0 { continue } - appendAPIResponseChunk(ctx, e.cfg, payload) + helps.AppendAPIResponseChunk(ctx, e.cfg, payload) if wsErr, ok := parseCodexWebsocketError(payload); ok { if sess != nil { e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr) } - recordAPIResponseError(ctx, e.cfg, wsErr) + helps.RecordAPIResponseError(ctx, e.cfg, wsErr) return resp, wsErr } payload = normalizeCodexWebsocketCompletion(payload) eventType := gjson.GetBytes(payload, "type").String() if eventType == "response.completed" { - if detail, ok := parseCodexUsage(payload); ok { - reporter.publish(ctx, detail) + if detail, ok := helps.ParseCodexUsage(payload); ok { + reporter.Publish(ctx, detail) } var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, payload, ¶m) @@ -364,8 +366,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr baseURL = "https://chatgpt.com/backend-api/codex" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("codex") @@ -376,8 +378,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr return nil, err } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel) httpURL := strings.TrimSuffix(baseURL, "/") + "/responses" wsURL, err := buildCodexResponsesWebsocketURL(httpURL) @@ -403,7 +405,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr } wsReqBody := buildCodexWebsocketRequestBody(body) - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", Headers: wsHeaders.Clone(), @@ -419,12 +421,12 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr var upstreamHeaders http.Header if respHS != nil { 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 { bodyErr := websocketHandshakeBody(respHS) if len(bodyErr) > 0 { - appendAPIResponseChunk(ctx, e.cfg, bodyErr) + helps.AppendAPIResponseChunk(ctx, e.cfg, bodyErr) } if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired { 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 { return nil, statusErr{code: respHS.StatusCode, msg: string(bodyErr)} } - recordAPIResponseError(ctx, e.cfg, errDial) + helps.RecordAPIResponseError(ctx, e.cfg, errDial) if sess != nil { sess.reqMu.Unlock() } @@ -451,20 +453,20 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr } if errSend := writeCodexWebsocketMessage(sess, conn, wsReqBody); errSend != nil { - recordAPIResponseError(ctx, e.cfg, errSend) + helps.RecordAPIResponseError(ctx, e.cfg, errSend) if sess != nil { e.invalidateUpstreamConn(sess, conn, "send_error", errSend) // Retry once with a new websocket connection for the same execution session. connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders) if errDialRetry != nil || connRetry == nil { - recordAPIResponseError(ctx, e.cfg, errDialRetry) + helps.RecordAPIResponseError(ctx, e.cfg, errDialRetry) sess.clearActive(readCh) sess.reqMu.Unlock() return nil, errDialRetry } wsReqBodyRetry := buildCodexWebsocketRequestBody(body) - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", Headers: wsHeaders.Clone(), @@ -476,7 +478,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr AuthValue: authValue, }) 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) sess.clearActive(readCh) sess.reqMu.Unlock() @@ -542,8 +544,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr } terminateReason = "read_error" terminateErr = errRead - recordAPIResponseError(ctx, e.cfg, errRead) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) + reporter.PublishFailure(ctx) _ = send(cliproxyexecutor.StreamChunk{Err: errRead}) return } @@ -552,8 +554,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr err = fmt.Errorf("codex websockets executor: unexpected binary message") terminateReason = "unexpected_binary" terminateErr = err - recordAPIResponseError(ctx, e.cfg, err) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, err) + reporter.PublishFailure(ctx) if sess != nil { e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err) } @@ -567,13 +569,13 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr if len(payload) == 0 { continue } - appendAPIResponseChunk(ctx, e.cfg, payload) + helps.AppendAPIResponseChunk(ctx, e.cfg, payload) if wsErr, ok := parseCodexWebsocketError(payload); ok { terminateReason = "upstream_error" terminateErr = wsErr - recordAPIResponseError(ctx, e.cfg, wsErr) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, wsErr) + reporter.PublishFailure(ctx) if sess != nil { e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr) } @@ -584,8 +586,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr payload = normalizeCodexWebsocketCompletion(payload) eventType := gjson.GetBytes(payload, "type").String() if eventType == "response.completed" || eventType == "response.done" { - if detail, ok := parseCodexUsage(payload); ok { - reporter.publish(ctx, detail) + if detail, ok := helps.ParseCodexUsage(payload); ok { + reporter.Publish(ctx, detail) } } @@ -767,19 +769,19 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto return rawJSON, headers } - var cache codexCache + var cache helps.CodexCache if from == "claude" { userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id") if userIDResult.Exists() { key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String()) - if cached, ok := getCodexCache(key); ok { + if cached, ok := helps.GetCodexCache(key); ok { cache = cached } else { - cache = codexCache{ + cache = helps.CodexCache{ ID: uuid.New().String(), Expire: time.Now().Add(1 * time.Hour), } - setCodexCache(key, cache) + helps.SetCodexCache(key, cache) } } } else if from == "openai-response" { @@ -806,8 +808,8 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth * } var ginHeaders http.Header - if ginCtx := ginContextFrom(ctx); ginCtx != nil && ginCtx.Request != nil { - ginHeaders = ginCtx.Request.Header + if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { + ginHeaders = ginCtx.Request.Header.Clone() } cfgUserAgent, cfgBetaFeatures := codexHeaderDefaults(cfg, auth) diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index 7d2d2a9b..b2b656ee 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -18,6 +18,7 @@ import ( "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/runtime/executor/helps" "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/util" @@ -112,8 +113,8 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth return resp, err } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("gemini-cli") @@ -132,8 +133,8 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth } basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload) - requestedModel := payloadRequestedModel(opts, req.Model) - basePayload = applyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + basePayload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel) action := "generateContent" 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) applyGeminiCLIHeaders(reqHTTP, attemptModel) reqHTTP.Header.Set("Accept", "application/json") - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: reqHTTP.Header.Clone(), @@ -204,7 +205,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth httpResp, errDo := httpClient.Do(reqHTTP) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, errDo) err = errDo return resp, err } @@ -213,15 +214,15 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth if errClose := httpResp.Body.Close(); errClose != nil { 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 { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) err = errRead return resp, err } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 { - reporter.publish(ctx, parseGeminiCLIUsage(data)) + reporter.Publish(ctx, helps.ParseGeminiCLIUsage(data)) var param any out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, opts.OriginalRequest, payload, data, ¶m) 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 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 idx+1 < len(models) { 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 { - appendAPIResponseChunk(ctx, e.cfg, lastBody) + helps.AppendAPIResponseChunk(ctx, e.cfg, lastBody) } if lastStatus == 0 { lastStatus = 429 @@ -266,8 +267,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut return nil, err } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("gemini-cli") @@ -286,8 +287,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut } basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload) - requestedModel := payloadRequestedModel(opts, req.Model) - basePayload = applyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + basePayload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel) projectID := resolveGeminiProjectID(auth) @@ -335,7 +336,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) applyGeminiCLIHeaders(reqHTTP, attemptModel) reqHTTP.Header.Set("Accept", "text/event-stream") - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: reqHTTP.Header.Clone(), @@ -349,25 +350,25 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut httpResp, errDo := httpClient.Do(reqHTTP) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, errDo) err = errDo 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 { data, errRead := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("gemini cli executor: close response body error: %v", errClose) } if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) err = errRead return nil, err } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) lastStatus = httpResp.StatusCode 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 idx+1 < len(models) { 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 for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseGeminiCLIStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseGeminiCLIStreamUsage(line); ok { + reporter.Publish(ctx, detail) } if bytes.HasPrefix(line, dataTag) { segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), ¶m) @@ -411,8 +412,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut out <- cliproxyexecutor.StreamChunk{Payload: segments[i]} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } return @@ -420,13 +421,13 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut data, errRead := io.ReadAll(resp.Body) if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errRead} return } - appendAPIResponseChunk(ctx, e.cfg, data) - reporter.publish(ctx, parseGeminiCLIUsage(data)) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + reporter.Publish(ctx, helps.ParseGeminiCLIUsage(data)) var param any segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, ¶m) for i := range segments { @@ -443,7 +444,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut } if len(lastBody) > 0 { - appendAPIResponseChunk(ctx, e.cfg, lastBody) + helps.AppendAPIResponseChunk(ctx, e.cfg, lastBody) } if lastStatus == 0 { lastStatus = 429 @@ -516,7 +517,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) applyGeminiCLIHeaders(reqHTTP, baseModel) reqHTTP.Header.Set("Accept", "application/json") - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: reqHTTP.Header.Clone(), @@ -530,17 +531,19 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. resp, errDo := httpClient.Do(reqHTTP) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, errDo) return cliproxyexecutor.Response{}, errDo } data, errRead := io.ReadAll(resp.Body) - _ = resp.Body.Close() - 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()) if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) return cliproxyexecutor.Response{}, errRead } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) if resp.StatusCode >= 200 && resp.StatusCode < 300 { count := gjson.GetBytes(data, "totalTokens").Int() translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data) @@ -611,7 +614,7 @@ func prepareGeminiCLITokenSource(ctx context.Context, cfg *config.Config, auth * } 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) } @@ -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 { - return newProxyAwareHTTPClient(ctx, cfg, auth, timeout) + return helps.NewProxyAwareHTTPClient(ctx, cfg, auth, timeout) } func cloneMap(in map[string]any) map[string]any { diff --git a/internal/runtime/executor/gemini_executor.go b/internal/runtime/executor/gemini_executor.go index 35b95da4..fb4fbfda 100644 --- a/internal/runtime/executor/gemini_executor.go +++ b/internal/runtime/executor/gemini_executor.go @@ -13,6 +13,7 @@ import ( "strings" "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/util" 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 { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -110,8 +111,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r apiKey, bearer := geminiCreds(auth) - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) // Official Gemini API via API key or OAuth bearer from := opts.SourceFormat @@ -130,8 +131,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r } body = fixGeminiImageAspectRatio(baseModel, body) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) action := "generateContent" @@ -165,7 +166,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -177,10 +178,10 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } 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) } }() - 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} return resp, err } data, err := io.ReadAll(httpResp.Body) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - appendAPIResponseChunk(ctx, e.cfg, data) - reporter.publish(ctx, parseGeminiUsage(data)) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + reporter.Publish(ctx, helps.ParseGeminiUsage(data)) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m) 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) - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("gemini") @@ -237,8 +238,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } body = fixGeminiImageAspectRatio(baseModel, body) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) baseURL := resolveGeminiBaseURL(auth) @@ -268,7 +269,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -280,17 +281,17 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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 { 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 for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - filtered := FilterSSEUsageMetadata(line) - payload := jsonPayload(filtered) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + filtered := helps.FilterSSEUsageMetadata(line) + payload := helps.JSONPayload(filtered) if len(payload) == 0 { continue } - if detail, ok := parseGeminiStreamUsage(payload); ok { - reporter.publish(ctx, detail) + if detail, ok := helps.ParseGeminiStreamUsage(payload); ok { + reporter.Publish(ctx, detail) } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), ¶m) for i := range lines { @@ -329,8 +330,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } }() @@ -381,7 +382,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -393,23 +394,27 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } - defer func() { _ = resp.Body.Close() }() - recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) + defer func() { + 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) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) 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)} } diff --git a/internal/runtime/executor/gemini_vertex_executor.go b/internal/runtime/executor/gemini_vertex_executor.go index 13a2b65c..83152e13 100644 --- a/internal/runtime/executor/gemini_vertex_executor.go +++ b/internal/runtime/executor/gemini_vertex_executor.go @@ -16,6 +16,7 @@ import ( 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/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 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 { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) 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) { baseModel := thinking.ParseSuffix(req.Model).ModelName - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) var body []byte @@ -332,8 +333,8 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au } body = fixGeminiImageAspectRatio(baseModel, body) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) } @@ -369,7 +370,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -381,10 +382,10 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, errDo) return resp, errDo } defer func() { @@ -392,21 +393,21 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} return resp, err } data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } - appendAPIResponseChunk(ctx, e.cfg, data) - reporter.publish(ctx, parseGeminiUsage(data)) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + reporter.Publish(ctx, helps.ParseGeminiUsage(data)) // For Imagen models, convert response to Gemini format before translation // 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) { baseModel := thinking.ParseSuffix(req.Model).ModelName - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("gemini") @@ -447,8 +448,8 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip } body = fixGeminiImageAspectRatio(baseModel, body) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) action := getVertexAction(baseModel, false) @@ -484,7 +485,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -496,10 +497,10 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, errDo) return resp, errDo } 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) } }() - 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} return resp, err } data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } - appendAPIResponseChunk(ctx, e.cfg, data) - reporter.publish(ctx, parseGeminiUsage(data)) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + reporter.Publish(ctx, helps.ParseGeminiUsage(data)) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m) 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) { baseModel := thinking.ParseSuffix(req.Model).ModelName - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("gemini") @@ -552,8 +553,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte } body = fixGeminiImageAspectRatio(baseModel, body) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) action := getVertexAction(baseModel, true) @@ -588,7 +589,7 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -600,17 +601,17 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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 { log.Errorf("vertex executor: close response body error: %v", errClose) } @@ -630,9 +631,9 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte var param any for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseGeminiStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseGeminiStreamUsage(line); ok { + reporter.Publish(ctx, detail) } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range lines { @@ -644,8 +645,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) 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) { baseModel := thinking.ParseSuffix(req.Model).ModelName - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("gemini") @@ -676,8 +677,8 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth } body = fixGeminiImageAspectRatio(baseModel, body) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "model", baseModel) action := getVertexAction(baseModel, true) @@ -712,7 +713,7 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -724,17 +725,17 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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 { 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 for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseGeminiStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseGeminiStreamUsage(line); ok { + reporter.Publish(ctx, detail) } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range lines { @@ -768,8 +769,8 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } }() @@ -819,7 +820,7 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -831,10 +832,10 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, errDo) return cliproxyexecutor.Response{}, errDo } defer func() { @@ -842,19 +843,19 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} } data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) return cliproxyexecutor.Response{}, errRead } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) count := gjson.GetBytes(data, "totalTokens").Int() out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) 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 authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -915,10 +916,10 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth * AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { - recordAPIResponseError(ctx, e.cfg, errDo) + helps.RecordAPIResponseError(ctx, e.cfg, errDo) return cliproxyexecutor.Response{}, errDo } defer func() { @@ -926,19 +927,19 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth * 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} } data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { - recordAPIResponseError(ctx, e.cfg, errRead) + helps.RecordAPIResponseError(ctx, e.cfg, errRead) return cliproxyexecutor.Response{}, errRead } - appendAPIResponseChunk(ctx, e.cfg, data) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) count := gjson.GetBytes(data, "totalTokens").Int() out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) 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) { - 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) } // Use cloud-platform scope for Vertex AI. diff --git a/internal/runtime/executor/cache_helpers.go b/internal/runtime/executor/helps/cache_helpers.go similarity index 81% rename from internal/runtime/executor/cache_helpers.go rename to internal/runtime/executor/helps/cache_helpers.go index b6de886d..ec063384 100644 --- a/internal/runtime/executor/cache_helpers.go +++ b/internal/runtime/executor/helps/cache_helpers.go @@ -1,11 +1,11 @@ -package executor +package helps import ( "sync" "time" ) -type codexCache struct { +type CodexCache struct { ID string Expire time.Time } @@ -13,7 +13,7 @@ type codexCache struct { // codexCacheMap stores prompt cache IDs keyed by model+user_id. // Protected by codexCacheMu. Entries expire after 1 hour. var ( - codexCacheMap = make(map[string]codexCache) + codexCacheMap = make(map[string]CodexCache) codexCacheMu sync.RWMutex ) @@ -47,20 +47,20 @@ func purgeExpiredCodexCache() { } } -// getCodexCache retrieves a cached entry, returning ok=false if not found or expired. -func getCodexCache(key string) (codexCache, bool) { +// GetCodexCache retrieves a cached entry, returning ok=false if not found or expired. +func GetCodexCache(key string) (CodexCache, bool) { codexCacheCleanupOnce.Do(startCodexCacheCleanup) codexCacheMu.RLock() cache, ok := codexCacheMap[key] codexCacheMu.RUnlock() if !ok || cache.Expire.Before(time.Now()) { - return codexCache{}, false + return CodexCache{}, false } return cache, true } -// setCodexCache stores a cache entry. -func setCodexCache(key string, cache codexCache) { +// SetCodexCache stores a cache entry. +func SetCodexCache(key string, cache CodexCache) { codexCacheCleanupOnce.Do(startCodexCacheCleanup) codexCacheMu.Lock() codexCacheMap[key] = cache diff --git a/internal/runtime/executor/claude_device_profile.go b/internal/runtime/executor/helps/claude_device_profile.go similarity index 84% rename from internal/runtime/executor/claude_device_profile.go rename to internal/runtime/executor/helps/claude_device_profile.go index 374720b8..2cf4d917 100644 --- a/internal/runtime/executor/claude_device_profile.go +++ b/internal/runtime/executor/helps/claude_device_profile.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "crypto/sha256" @@ -32,7 +32,7 @@ var ( claudeDeviceProfileCacheMu sync.RWMutex claudeDeviceProfileCacheCleanupOnce sync.Once - claudeDeviceProfileBeforeCandidateStore func(claudeDeviceProfile) + ClaudeDeviceProfileBeforeCandidateStore func(ClaudeDeviceProfile) ) type claudeCLIVersion struct { @@ -63,29 +63,35 @@ func (v claudeCLIVersion) Compare(other claudeCLIVersion) int { } } -type claudeDeviceProfile struct { +type ClaudeDeviceProfile struct { UserAgent string PackageVersion string RuntimeVersion string OS string Arch string - Version claudeCLIVersion - HasVersion bool + version claudeCLIVersion + hasVersion bool } type claudeDeviceProfileCacheEntry struct { - profile claudeDeviceProfile + profile ClaudeDeviceProfile expire time.Time } -func claudeDeviceProfileStabilizationEnabled(cfg *config.Config) bool { +func ClaudeDeviceProfileStabilizationEnabled(cfg *config.Config) bool { if cfg == nil || cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile == nil { return false } 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 { if strings.TrimSpace(cfgVal) != "" { return strings.TrimSpace(cfgVal) @@ -98,7 +104,7 @@ func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile { hd = cfg.ClaudeHeaderDefaults } - profile := claudeDeviceProfile{ + profile := ClaudeDeviceProfile{ UserAgent: hdrDefault(hd.UserAgent, defaultClaudeFingerprintUserAgent), PackageVersion: hdrDefault(hd.PackageVersion, defaultClaudeFingerprintPackageVersion), RuntimeVersion: hdrDefault(hd.RuntimeVersion, defaultClaudeFingerprintRuntimeVersion), @@ -106,8 +112,8 @@ func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile { Arch: hdrDefault(hd.Arch, defaultClaudeFingerprintArch), } if version, ok := parseClaudeCLIVersion(profile.UserAgent); ok { - profile.Version = version - profile.HasVersion = true + profile.version = version + profile.hasVersion = true } return profile } @@ -162,17 +168,17 @@ func parseClaudeCLIVersion(userAgent string) (claudeCLIVersion, bool) { return claudeCLIVersion{major: major, minor: minor, patch: patch}, true } -func shouldUpgradeClaudeDeviceProfile(candidate, current claudeDeviceProfile) bool { - if candidate.UserAgent == "" || !candidate.HasVersion { +func shouldUpgradeClaudeDeviceProfile(candidate, current ClaudeDeviceProfile) bool { + if candidate.UserAgent == "" || !candidate.hasVersion { return false } - if current.UserAgent == "" || !current.HasVersion { + if current.UserAgent == "" || !current.hasVersion { 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.Arch = baseline.Arch return profile @@ -180,38 +186,38 @@ func pinClaudeDeviceProfilePlatform(profile, baseline claudeDeviceProfile) claud // normalizeClaudeDeviceProfile keeps stabilized profiles pinned to the current // 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) - if profile.UserAgent == "" || !profile.HasVersion || shouldUpgradeClaudeDeviceProfile(baseline, profile) { + if profile.UserAgent == "" || !profile.hasVersion || shouldUpgradeClaudeDeviceProfile(baseline, profile) { profile.UserAgent = baseline.UserAgent profile.PackageVersion = baseline.PackageVersion profile.RuntimeVersion = baseline.RuntimeVersion - profile.Version = baseline.Version - profile.HasVersion = baseline.HasVersion + profile.version = baseline.version + profile.hasVersion = baseline.hasVersion } 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 { - return claudeDeviceProfile{}, false + return ClaudeDeviceProfile{}, false } userAgent := strings.TrimSpace(headers.Get("User-Agent")) version, ok := parseClaudeCLIVersion(userAgent) if !ok { - return claudeDeviceProfile{}, false + return ClaudeDeviceProfile{}, false } baseline := defaultClaudeDeviceProfile(cfg) - profile := claudeDeviceProfile{ + profile := ClaudeDeviceProfile{ UserAgent: userAgent, PackageVersion: firstNonEmptyHeader(headers, "X-Stainless-Package-Version", baseline.PackageVersion), RuntimeVersion: firstNonEmptyHeader(headers, "X-Stainless-Runtime-Version", baseline.RuntimeVersion), OS: firstNonEmptyHeader(headers, "X-Stainless-Os", baseline.OS), Arch: firstNonEmptyHeader(headers, "X-Stainless-Arch", baseline.Arch), - Version: version, - HasVersion: true, + version: version, + hasVersion: true, } return profile, true } @@ -263,7 +269,7 @@ func purgeExpiredClaudeDeviceProfiles() { 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) cacheKey := claudeDeviceProfileCacheKey(auth, apiKey) @@ -283,8 +289,8 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers claudeDeviceProfileCacheMu.RUnlock() if hasCandidate { - if claudeDeviceProfileBeforeCandidateStore != nil { - claudeDeviceProfileBeforeCandidateStore(candidate) + if ClaudeDeviceProfileBeforeCandidateStore != nil { + ClaudeDeviceProfileBeforeCandidateStore(candidate) } claudeDeviceProfileCacheMu.Lock() @@ -324,7 +330,7 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers return baseline } -func applyClaudeDeviceProfileHeaders(r *http.Request, profile claudeDeviceProfile) { +func ApplyClaudeDeviceProfileHeaders(r *http.Request, profile ClaudeDeviceProfile) { if r == nil { return } @@ -344,7 +350,7 @@ func applyClaudeDeviceProfileHeaders(r *http.Request, profile claudeDeviceProfil 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 { return } diff --git a/internal/runtime/executor/cloak_obfuscate.go b/internal/runtime/executor/helps/cloak_obfuscate.go similarity index 93% rename from internal/runtime/executor/cloak_obfuscate.go rename to internal/runtime/executor/helps/cloak_obfuscate.go index 81781802..dce724af 100644 --- a/internal/runtime/executor/cloak_obfuscate.go +++ b/internal/runtime/executor/helps/cloak_obfuscate.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "regexp" @@ -18,9 +18,9 @@ type SensitiveWordMatcher struct { 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. -func buildSensitiveWordMatcher(words []string) *SensitiveWordMatcher { +func BuildSensitiveWordMatcher(words []string) *SensitiveWordMatcher { if len(words) == 0 { return nil } @@ -81,9 +81,9 @@ func (m *SensitiveWordMatcher) obfuscateText(text string) string { 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. -func obfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []byte { +func ObfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []byte { if matcher == nil || matcher.regex == nil { return payload } diff --git a/internal/runtime/executor/cloak_utils.go b/internal/runtime/executor/helps/cloak_utils.go similarity index 83% rename from internal/runtime/executor/cloak_utils.go rename to internal/runtime/executor/helps/cloak_utils.go index 2a3433ac..11ace545 100644 --- a/internal/runtime/executor/cloak_utils.go +++ b/internal/runtime/executor/helps/cloak_utils.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "crypto/rand" @@ -28,9 +28,17 @@ func isValidUserID(userID string) bool { 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. -func shouldCloak(cloakMode string, userAgent string) bool { +func ShouldCloak(cloakMode string, userAgent string) bool { switch strings.ToLower(cloakMode) { case "always": return true diff --git a/internal/runtime/executor/logging_helpers.go b/internal/runtime/executor/helps/logging_helpers.go similarity index 92% rename from internal/runtime/executor/logging_helpers.go rename to internal/runtime/executor/helps/logging_helpers.go index ae2aee3f..f9389edd 100644 --- a/internal/runtime/executor/logging_helpers.go +++ b/internal/runtime/executor/helps/logging_helpers.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "bytes" @@ -24,8 +24,8 @@ const ( apiResponseKey = "API_RESPONSE" ) -// upstreamRequestLog captures the outbound upstream request details for logging. -type upstreamRequestLog struct { +// UpstreamRequestLog captures the outbound upstream request details for logging. +type UpstreamRequestLog struct { URL string Method string Headers http.Header @@ -49,8 +49,8 @@ type upstreamAttempt struct { errorWritten bool } -// recordAPIRequest stores the upstream request metadata in Gin context for request logging. -func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequestLog) { +// RecordAPIRequest stores the upstream request metadata in Gin context for request logging. +func RecordAPIRequest(ctx context.Context, cfg *config.Config, info UpstreamRequestLog) { if cfg == nil || !cfg.RequestLog { return } @@ -96,8 +96,8 @@ func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequ updateAggregatedRequest(ginCtx, attempts) } -// recordAPIResponseMetadata captures upstream response status/header information for the latest attempt. -func recordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status int, headers http.Header) { +// RecordAPIResponseMetadata captures upstream response status/header information for the latest attempt. +func RecordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status int, headers http.Header) { if cfg == nil || !cfg.RequestLog { return } @@ -122,8 +122,8 @@ func recordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status i updateAggregatedResponse(ginCtx, attempts) } -// 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) { +// 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) { if cfg == nil || !cfg.RequestLog || err == nil { return } @@ -147,8 +147,8 @@ func recordAPIResponseError(ctx context.Context, cfg *config.Config, err error) updateAggregatedResponse(ginCtx, attempts) } -// appendAPIResponseChunk appends an upstream response chunk to Gin context for request logging. -func appendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byte) { +// AppendAPIResponseChunk appends an upstream response chunk to Gin context for request logging. +func AppendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byte) { if cfg == nil || !cfg.RequestLog { 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 if trimmed := strings.TrimSpace(info.Provider); trimmed != "" { parts = append(parts, fmt.Sprintf("provider=%s", trimmed)) @@ -321,7 +321,7 @@ func formatAuthInfo(info upstreamRequestLog) string { 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") if !isHTML { 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. // 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 { return log.NewEntry(log.StandardLogger()) } diff --git a/internal/runtime/executor/payload_helpers.go b/internal/runtime/executor/helps/payload_helpers.go similarity index 97% rename from internal/runtime/executor/payload_helpers.go rename to internal/runtime/executor/helps/payload_helpers.go index 271e2c5b..73514c2d 100644 --- a/internal/runtime/executor/payload_helpers.go +++ b/internal/runtime/executor/helps/payload_helpers.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "encoding/json" @@ -11,12 +11,12 @@ import ( "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) // and restricts matches to the given protocol when supplied. Defaults are checked // against the original payload when provided. requestedModel carries the client-visible // 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 { 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) if len(opts.Metadata) == 0 { return fallback diff --git a/internal/runtime/executor/proxy_helpers.go b/internal/runtime/executor/helps/proxy_helpers.go similarity index 94% rename from internal/runtime/executor/proxy_helpers.go rename to internal/runtime/executor/helps/proxy_helpers.go index 5511497b..022bc65c 100644 --- a/internal/runtime/executor/proxy_helpers.go +++ b/internal/runtime/executor/helps/proxy_helpers.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "context" @@ -12,7 +12,7 @@ import ( 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) // 2. Use cfg.ProxyURL if auth proxy is not configured // 3. Use RoundTripper from context if neither are configured @@ -25,7 +25,7 @@ import ( // // Returns: // - *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{} if timeout > 0 { httpClient.Timeout = timeout diff --git a/internal/runtime/executor/proxy_helpers_test.go b/internal/runtime/executor/helps/proxy_helpers_test.go similarity index 93% rename from internal/runtime/executor/proxy_helpers_test.go rename to internal/runtime/executor/helps/proxy_helpers_test.go index 4ae5c937..33117167 100644 --- a/internal/runtime/executor/proxy_helpers_test.go +++ b/internal/runtime/executor/helps/proxy_helpers_test.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "context" @@ -13,7 +13,7 @@ import ( func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) { t.Parallel() - client := newProxyAwareHTTPClient( + client := NewProxyAwareHTTPClient( context.Background(), &config.Config{SDKConfig: sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}}, &cliproxyauth.Auth{ProxyURL: "direct"}, diff --git a/internal/runtime/executor/thinking_providers.go b/internal/runtime/executor/helps/thinking_providers.go similarity index 97% rename from internal/runtime/executor/thinking_providers.go rename to internal/runtime/executor/helps/thinking_providers.go index b961db90..36b63c90 100644 --- a/internal/runtime/executor/thinking_providers.go +++ b/internal/runtime/executor/helps/thinking_providers.go @@ -1,4 +1,4 @@ -package executor +package helps import ( _ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/antigravity" diff --git a/internal/runtime/executor/token_helpers.go b/internal/runtime/executor/helps/token_helpers.go similarity index 94% rename from internal/runtime/executor/token_helpers.go rename to internal/runtime/executor/helps/token_helpers.go index f4236f9b..92b8ba8d 100644 --- a/internal/runtime/executor/token_helpers.go +++ b/internal/runtime/executor/helps/token_helpers.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "fmt" @@ -8,8 +8,8 @@ import ( "github.com/tiktoken-go/tokenizer" ) -// tokenizerForModel returns a tokenizer codec suitable for an OpenAI-style model id. -func tokenizerForModel(model string) (tokenizer.Codec, error) { +// TokenizerForModel returns a tokenizer codec suitable for an OpenAI-style model id. +func TokenizerForModel(model string) (tokenizer.Codec, error) { sanitized := strings.ToLower(strings.TrimSpace(model)) switch { case sanitized == "": @@ -37,8 +37,8 @@ func tokenizerForModel(model string) (tokenizer.Codec, error) { } } -// countOpenAIChatTokens approximates prompt tokens for OpenAI chat completions payloads. -func countOpenAIChatTokens(enc tokenizer.Codec, payload []byte) (int64, error) { +// CountOpenAIChatTokens approximates prompt tokens for OpenAI chat completions payloads. +func CountOpenAIChatTokens(enc tokenizer.Codec, payload []byte) (int64, error) { if enc == 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 } -// buildOpenAIUsageJSON returns a minimal usage structure understood by downstream translators. -func buildOpenAIUsageJSON(count int64) []byte { +// BuildOpenAIUsageJSON returns a minimal usage structure understood by downstream translators. +func BuildOpenAIUsageJSON(count int64) []byte { return []byte(fmt.Sprintf(`{"usage":{"prompt_tokens":%d,"completion_tokens":0,"total_tokens":%d}}`, count, count)) } diff --git a/internal/runtime/executor/usage_helpers.go b/internal/runtime/executor/helps/usage_helpers.go similarity index 91% rename from internal/runtime/executor/usage_helpers.go rename to internal/runtime/executor/helps/usage_helpers.go index de2f2e52..23040984 100644 --- a/internal/runtime/executor/usage_helpers.go +++ b/internal/runtime/executor/helps/usage_helpers.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "bytes" @@ -15,7 +15,7 @@ import ( "github.com/tidwall/sjson" ) -type usageReporter struct { +type UsageReporter struct { provider string model string authID string @@ -26,9 +26,9 @@ type usageReporter struct { once sync.Once } -func newUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *usageReporter { - apiKey := apiKeyFromContext(ctx) - reporter := &usageReporter{ +func NewUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *UsageReporter { + apiKey := APIKeyFromContext(ctx) + reporter := &UsageReporter{ provider: provider, model: model, requestedAt: time.Now(), @@ -42,24 +42,24 @@ func newUsageReporter(ctx context.Context, provider, model string, auth *cliprox 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) } -func (r *usageReporter) publishFailure(ctx context.Context) { +func (r *UsageReporter) PublishFailure(ctx context.Context) { 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 { return } 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 { 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. // This is used to ensure request counting even when upstream responses do not // 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 { 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 { 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() { return 0 } @@ -119,7 +119,7 @@ func (r *usageReporter) latency() time.Duration { return latency } -func apiKeyFromContext(ctx context.Context) string { +func APIKeyFromContext(ctx context.Context) string { if ctx == nil { return "" } @@ -184,7 +184,7 @@ func resolveUsageSource(auth *cliproxyauth.Auth, ctxAPIKey string) string { return "" } -func parseCodexUsage(data []byte) (usage.Detail, bool) { +func ParseCodexUsage(data []byte) (usage.Detail, bool) { usageNode := gjson.ParseBytes(data).Get("response.usage") if !usageNode.Exists() { return usage.Detail{}, false @@ -203,7 +203,7 @@ func parseCodexUsage(data []byte) (usage.Detail, bool) { return detail, true } -func parseOpenAIUsage(data []byte) usage.Detail { +func ParseOpenAIUsage(data []byte) usage.Detail { usageNode := gjson.ParseBytes(data).Get("usage") if !usageNode.Exists() { return usage.Detail{} @@ -238,7 +238,7 @@ func parseOpenAIUsage(data []byte) usage.Detail { return detail } -func parseOpenAIStreamUsage(line []byte) (usage.Detail, bool) { +func ParseOpenAIStreamUsage(line []byte) (usage.Detail, bool) { payload := jsonPayload(line) if len(payload) == 0 || !gjson.ValidBytes(payload) { return usage.Detail{}, false @@ -261,7 +261,7 @@ func parseOpenAIStreamUsage(line []byte) (usage.Detail, bool) { return detail, true } -func parseClaudeUsage(data []byte) usage.Detail { +func ParseClaudeUsage(data []byte) usage.Detail { usageNode := gjson.ParseBytes(data).Get("usage") if !usageNode.Exists() { return usage.Detail{} @@ -279,7 +279,7 @@ func parseClaudeUsage(data []byte) usage.Detail { return detail } -func parseClaudeStreamUsage(line []byte) (usage.Detail, bool) { +func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) { payload := jsonPayload(line) if len(payload) == 0 || !gjson.ValidBytes(payload) { return usage.Detail{}, false @@ -314,7 +314,7 @@ func parseGeminiFamilyUsageDetail(node gjson.Result) usage.Detail { return detail } -func parseGeminiCLIUsage(data []byte) usage.Detail { +func ParseGeminiCLIUsage(data []byte) usage.Detail { usageNode := gjson.ParseBytes(data) node := usageNode.Get("response.usageMetadata") if !node.Exists() { @@ -326,7 +326,7 @@ func parseGeminiCLIUsage(data []byte) usage.Detail { return parseGeminiFamilyUsageDetail(node) } -func parseGeminiUsage(data []byte) usage.Detail { +func ParseGeminiUsage(data []byte) usage.Detail { usageNode := gjson.ParseBytes(data) node := usageNode.Get("usageMetadata") if !node.Exists() { @@ -338,7 +338,7 @@ func parseGeminiUsage(data []byte) usage.Detail { return parseGeminiFamilyUsageDetail(node) } -func parseGeminiStreamUsage(line []byte) (usage.Detail, bool) { +func ParseGeminiStreamUsage(line []byte) (usage.Detail, bool) { payload := jsonPayload(line) if len(payload) == 0 || !gjson.ValidBytes(payload) { return usage.Detail{}, false @@ -353,7 +353,7 @@ func parseGeminiStreamUsage(line []byte) (usage.Detail, bool) { return parseGeminiFamilyUsageDetail(node), true } -func parseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) { +func ParseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) { payload := jsonPayload(line) if len(payload) == 0 || !gjson.ValidBytes(payload) { return usage.Detail{}, false @@ -368,7 +368,7 @@ func parseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) { return parseGeminiFamilyUsageDetail(node), true } -func parseAntigravityUsage(data []byte) usage.Detail { +func ParseAntigravityUsage(data []byte) usage.Detail { usageNode := gjson.ParseBytes(data) node := usageNode.Get("response.usageMetadata") if !node.Exists() { @@ -383,7 +383,7 @@ func parseAntigravityUsage(data []byte) usage.Detail { return parseGeminiFamilyUsageDetail(node) } -func parseAntigravityStreamUsage(line []byte) (usage.Detail, bool) { +func ParseAntigravityStreamUsage(line []byte) (usage.Detail, bool) { payload := jsonPayload(line) if len(payload) == 0 || !gjson.ValidBytes(payload) { return usage.Detail{}, false @@ -552,6 +552,10 @@ func isStopChunkWithoutUsage(jsonBytes []byte) bool { return !hasUsageMetadata(jsonBytes) } +func JSONPayload(line []byte) []byte { + return jsonPayload(line) +} + func jsonPayload(line []byte) []byte { trimmed := bytes.TrimSpace(line) if len(trimmed) == 0 { diff --git a/internal/runtime/executor/usage_helpers_test.go b/internal/runtime/executor/helps/usage_helpers_test.go similarity index 94% rename from internal/runtime/executor/usage_helpers_test.go rename to internal/runtime/executor/helps/usage_helpers_test.go index 785f72b4..1a5648e8 100644 --- a/internal/runtime/executor/usage_helpers_test.go +++ b/internal/runtime/executor/helps/usage_helpers_test.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "testing" @@ -9,7 +9,7 @@ import ( 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}}}`) - detail := parseOpenAIUsage(data) + detail := ParseOpenAIUsage(data) if 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) { 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 { t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10) } @@ -48,7 +48,7 @@ func TestParseOpenAIUsageResponses(t *testing.T) { } func TestUsageReporterBuildRecordIncludesLatency(t *testing.T) { - reporter := &usageReporter{ + reporter := &UsageReporter{ provider: "openai", model: "gpt-5.4", requestedAt: time.Now().Add(-1500 * time.Millisecond), diff --git a/internal/runtime/executor/user_id_cache.go b/internal/runtime/executor/helps/user_id_cache.go similarity index 96% rename from internal/runtime/executor/user_id_cache.go rename to internal/runtime/executor/helps/user_id_cache.go index ff8efd9d..ad41fd9a 100644 --- a/internal/runtime/executor/user_id_cache.go +++ b/internal/runtime/executor/helps/user_id_cache.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "crypto/sha256" @@ -49,7 +49,7 @@ func userIDCacheKey(apiKey string) string { return hex.EncodeToString(sum[:]) } -func cachedUserID(apiKey string) string { +func CachedUserID(apiKey string) string { if apiKey == "" { return generateFakeUserID() } diff --git a/internal/runtime/executor/user_id_cache_test.go b/internal/runtime/executor/helps/user_id_cache_test.go similarity index 83% rename from internal/runtime/executor/user_id_cache_test.go rename to internal/runtime/executor/helps/user_id_cache_test.go index 420a3cad..b166576c 100644 --- a/internal/runtime/executor/user_id_cache_test.go +++ b/internal/runtime/executor/helps/user_id_cache_test.go @@ -1,4 +1,4 @@ -package executor +package helps import ( "testing" @@ -14,8 +14,8 @@ func resetUserIDCache() { func TestCachedUserID_ReusesWithinTTL(t *testing.T) { resetUserIDCache() - first := cachedUserID("api-key-1") - second := cachedUserID("api-key-1") + first := CachedUserID("api-key-1") + second := CachedUserID("api-key-1") if first == "" { 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) { resetUserIDCache() - expiredID := cachedUserID("api-key-expired") + expiredID := CachedUserID("api-key-expired") cacheKey := userIDCacheKey("api-key-expired") userIDCacheMu.Lock() userIDCache[cacheKey] = userIDCacheEntry{ @@ -37,7 +37,7 @@ func TestCachedUserID_ExpiresAfterTTL(t *testing.T) { } userIDCacheMu.Unlock() - newID := cachedUserID("api-key-expired") + newID := CachedUserID("api-key-expired") if newID == expiredID { 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) { resetUserIDCache() - first := cachedUserID("api-key-1") - second := cachedUserID("api-key-2") + first := CachedUserID("api-key-1") + second := CachedUserID("api-key-2") if first == second { 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() key := "api-key-renew" - id := cachedUserID(key) + id := CachedUserID(key) cacheKey := userIDCacheKey(key) soon := time.Now() @@ -72,7 +72,7 @@ func TestCachedUserID_RenewsTTLOnHit(t *testing.T) { } 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) } diff --git a/internal/runtime/executor/iflow_executor.go b/internal/runtime/executor/iflow_executor.go index cc5cc33d..3e9e17fb 100644 --- a/internal/runtime/executor/iflow_executor.go +++ b/internal/runtime/executor/iflow_executor.go @@ -16,6 +16,7 @@ import ( "github.com/google/uuid" 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/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" 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 { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -86,8 +87,8 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re baseURL = iflowauth.DefaultAPIBaseURL } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("openai") @@ -106,8 +107,8 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re } body = preserveReasoningContentInMessages(body) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint @@ -122,7 +123,7 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: endpoint, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -134,10 +135,10 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } 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) } }() - 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} return resp, err } data, err := io.ReadAll(httpResp.Body) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - appendAPIResponseChunk(ctx, e.cfg, data) - reporter.publish(ctx, parseOpenAIUsage(data)) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + reporter.Publish(ctx, helps.ParseOpenAIUsage(data)) // Ensure usage is recorded even if upstream omits usage metadata. - reporter.ensurePublished(ctx) + reporter.EnsurePublished(ctx) var param any // 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 } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat 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 { body = ensureToolsArray(body) } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint @@ -230,7 +231,7 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: endpoint, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -242,21 +243,21 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { data, _ := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("iflow executor: close response body error: %v", errClose) } - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} return nil, err } @@ -275,9 +276,9 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au var param any for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseOpenAIStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseOpenAIStreamUsage(line); ok { + reporter.Publish(ctx, detail) } chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range chunks { @@ -285,12 +286,12 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } // 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 @@ -303,17 +304,17 @@ func (e *IFlowExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth to := sdktranslator.FromString("openai") body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false) - enc, err := tokenizerForModel(baseModel) + enc, err := helps.TokenizerForModel(baseModel) if err != nil { 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 { 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) return cliproxyexecutor.Response{Payload: translated}, nil } diff --git a/internal/runtime/executor/kimi_executor.go b/internal/runtime/executor/kimi_executor.go index e7052ee2..ce7d2ddc 100644 --- a/internal/runtime/executor/kimi_executor.go +++ b/internal/runtime/executor/kimi_executor.go @@ -15,6 +15,7 @@ import ( 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/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 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 { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -76,8 +77,8 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req token := kimiCreds(auth) - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) to := sdktranslator.FromString("openai") originalPayloadSource := req.Payload @@ -100,8 +101,8 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req return resp, err } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, err = normalizeKimiToolMessageLinks(body) if err != nil { return resp, err @@ -119,7 +120,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -131,10 +132,10 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } 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) } }() - 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} return resp, err } data, err := io.ReadAll(httpResp.Body) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - appendAPIResponseChunk(ctx, e.cfg, data) - reporter.publish(ctx, parseOpenAIUsage(data)) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + reporter.Publish(ctx, helps.ParseOpenAIUsage(data)) var param any // Note: TranslateNonStream uses req.Model (original with suffix) to preserve // 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 token := kimiCreds(auth) - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) to := sdktranslator.FromString("openai") originalPayloadSource := req.Payload @@ -204,8 +205,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut if err != nil { return nil, fmt.Errorf("kimi executor: failed to set stream_options in payload: %w", err) } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) body, err = normalizeKimiToolMessageLinks(body) if err != nil { return nil, err @@ -223,7 +224,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -235,17 +236,17 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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 { 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 for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseOpenAIStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseOpenAIStreamUsage(line); ok { + reporter.Publish(ctx, detail) } chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range chunks { @@ -279,8 +280,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } }() diff --git a/internal/runtime/executor/openai_compat_executor.go b/internal/runtime/executor/openai_compat_executor.go index 3bb6e012..a03e4987 100644 --- a/internal/runtime/executor/openai_compat_executor.go +++ b/internal/runtime/executor/openai_compat_executor.go @@ -11,6 +11,7 @@ import ( "time" "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/util" 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 { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) 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) { baseModel := thinking.ParseSuffix(req.Model).ModelName - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) baseURL, apiKey := e.resolveCredentials(auth) if baseURL == "" { @@ -95,8 +96,8 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A originalPayload := originalPayloadSource originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream) translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, opts.Stream) - requestedModel := payloadRequestedModel(opts, req.Model) - translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel) if opts.Alt == "responses/compact" { if updated, errDelete := sjson.DeleteBytes(translated, "stream"); errDelete == nil { translated = updated @@ -129,7 +130,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -141,10 +142,10 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } 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) } }() - 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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)} return resp, err } body, err := io.ReadAll(httpResp.Body) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - appendAPIResponseChunk(ctx, e.cfg, body) - reporter.publish(ctx, parseOpenAIUsage(body)) + helps.AppendAPIResponseChunk(ctx, e.cfg, body) + reporter.Publish(ctx, helps.ParseOpenAIUsage(body)) // 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 var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, body, ¶m) @@ -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) { baseModel := thinking.ParseSuffix(req.Model).ModelName - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) baseURL, apiKey := e.resolveCredentials(auth) if baseURL == "" { @@ -197,8 +198,8 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy originalPayload := originalPayloadSource originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true) translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true) - requestedModel := payloadRequestedModel(opts, req.Model) - translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel) translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier()) if err != nil { @@ -232,7 +233,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy authLabel = auth.Label authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -244,17 +245,17 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { b, _ := io.ReadAll(httpResp.Body) - 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.AppendAPIResponseChunk(ctx, e.cfg, 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 { 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 for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseOpenAIStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseOpenAIStreamUsage(line); ok { + reporter.Publish(ctx, detail) } if len(line) == 0 { continue @@ -294,12 +295,12 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy } } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } // 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 } @@ -318,17 +319,17 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau return cliproxyexecutor.Response{}, err } - enc, err := tokenizerForModel(modelForCounting) + enc, err := helps.TokenizerForModel(modelForCounting) if err != nil { 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 { 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) return cliproxyexecutor.Response{Payload: translatedUsage}, nil } diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index ff19dcb5..24f6c558 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -13,6 +13,7 @@ import ( 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/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" 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 cooldown := timeUntilNextDay() 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 } @@ -202,7 +203,7 @@ func (e *QwenExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, if err := e.PrepareRequest(httpReq, auth); err != nil { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -217,7 +218,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req authID = auth.ID } 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 } @@ -228,8 +229,8 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req baseURL = "https://portal.qwen.ai/v1" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat to := sdktranslator.FromString("openai") @@ -247,8 +248,8 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req return resp, err } - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" 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 authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -273,10 +274,10 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } 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) } }() - 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 { b, _ := io.ReadAll(httpResp.Body) - appendAPIResponseChunk(ctx, e.cfg, b) + helps.AppendAPIResponseChunk(ctx, e.cfg, 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} return resp, err } data, err := io.ReadAll(httpResp.Body) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - appendAPIResponseChunk(ctx, e.cfg, data) - reporter.publish(ctx, parseOpenAIUsage(data)) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + reporter.Publish(ctx, helps.ParseOpenAIUsage(data)) var param any // Note: TranslateNonStream uses req.Model (original with suffix) to preserve // 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 } 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 } @@ -331,8 +332,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut baseURL = "https://portal.qwen.ai/v1" } - reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth) - defer reporter.trackFailure(ctx, &err) + reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth) + defer reporter.TrackFailure(ctx, &err) from := opts.SourceFormat 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.SetBytes(body, "stream_options.include_usage", true) - requestedModel := payloadRequestedModel(opts, req.Model) - body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) + requestedModel := helps.PayloadRequestedModel(opts, req.Model) + body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" 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 authType, authValue = auth.AccountInfo() } - recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: httpReq.Header.Clone(), @@ -383,19 +384,19 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut AuthValue: authValue, }) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, err := httpClient.Do(httpReq) if err != nil { - recordAPIResponseError(ctx, e.cfg, err) + helps.RecordAPIResponseError(ctx, e.cfg, 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 { b, _ := io.ReadAll(httpResp.Body) - appendAPIResponseChunk(ctx, e.cfg, b) + helps.AppendAPIResponseChunk(ctx, e.cfg, 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 { 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 for scanner.Scan() { line := scanner.Bytes() - appendAPIResponseChunk(ctx, e.cfg, line) - if detail, ok := parseOpenAIStreamUsage(line); ok { - reporter.publish(ctx, detail) + helps.AppendAPIResponseChunk(ctx, e.cfg, line) + if detail, ok := helps.ParseOpenAIStreamUsage(line); ok { + reporter.Publish(ctx, detail) } chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range chunks { @@ -429,8 +430,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]} } if errScan := scanner.Err(); errScan != nil { - recordAPIResponseError(ctx, e.cfg, errScan) - reporter.publishFailure(ctx) + helps.RecordAPIResponseError(ctx, e.cfg, errScan) + reporter.PublishFailure(ctx) out <- cliproxyexecutor.StreamChunk{Err: errScan} } }() @@ -449,17 +450,17 @@ func (e *QwenExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, modelName = baseModel } - enc, err := tokenizerForModel(modelName) + enc, err := helps.TokenizerForModel(modelName) if err != nil { 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 { 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) return cliproxyexecutor.Response{Payload: translated}, nil }