- Added `LoggerPlugin` to log usage metrics for observability. - Introduced a new `Manager` to handle usage record queuing and plugin registration. - Integrated new usage reporter and detailed metrics parsing into executors, covering providers like OpenAI, Codex, Claude, and Gemini. - Improved token usage breakdown across streaming and non-streaming responses.
Showing the changes to internal/runtime/executor/codex_executor.go (1 of 13 files). Show all
Unified · Split
| @@ -18,12 +18,15 @@ import ( · 17 unmodified lines | |||
| 18 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" | 18 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" |
| 19 | sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" | 19 | sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" |
| 20 | log "github.com/sirupsen/logrus" | 20 | log "github.com/sirupsen/logrus" |
| 21 | "github.com/tidwall/gjson" | ||
| 21 | "github.com/tidwall/sjson" | 22 | "github.com/tidwall/sjson" |
| 22 | 23 | ||
| 23 | "github.com/gin-gonic/gin" | 24 | "github.com/gin-gonic/gin" |
| 24 | "github.com/google/uuid" | 25 | "github.com/google/uuid" |
| 25 | ) | 26 | ) |
| 26 | 27 | ||
| 28 | var dataTag = []byte("data:") | ||
| 29 | |||
| 27 | // CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint). | 30 | // CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint). |
| 28 | // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter. | 31 | // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter. |
| 29 | type CodexExecutor struct { | 32 | type CodexExecutor struct { |
| @@ -44,6 +47,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 14 unmodified lines | |||
| 44 | if baseURL == "" { | 47 | if baseURL == "" { |
| 45 | baseURL = "https://chatgpt.com/backend-api/codex" | 48 | baseURL = "https://chatgpt.com/backend-api/codex" |
| 46 | } | 49 | } |
| 50 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | ||
| 47 | 51 | ||
| 48 | from := opts.SourceFormat | 52 | from := opts.SourceFormat |
| 49 | to := sdktranslator.FromString("codex") | 53 | to := sdktranslator.FromString("codex") |
| @@ -75,6 +79,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 25 unmodified lines | |||
| 75 | } | 79 | } |
| 76 | } | 80 | } |
| 77 | 81 | ||
| 82 | body, _ = sjson.SetBytes(body, "stream", true) | ||
| 83 | |||
| 78 | url := strings.TrimSuffix(baseURL, "/") + "/responses" | 84 | url := strings.TrimSuffix(baseURL, "/") + "/responses" |
| 79 | recordAPIRequest(ctx, e.cfg, body) | 85 | recordAPIRequest(ctx, e.cfg, body) |
| 80 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) | 86 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) |
| @@ -103,9 +109,27 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 22 unmodified lines | |||
| 103 | return cliproxyexecutor.Response{}, err | 109 | return cliproxyexecutor.Response{}, err |
| 104 | } | 110 | } |
| 105 | appendAPIResponseChunk(ctx, e.cfg, data) | 111 | appendAPIResponseChunk(ctx, e.cfg, data) |
| 106 | var param any | 112 | |
| 107 | out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) | 113 | lines := bytes.Split(data, []byte("\n")) |
| 108 | return cliproxyexecutor.Response{Payload: []byte(out)}, nil | 114 | for _, line := range lines { |
| 115 | if !bytes.HasPrefix(line, dataTag) { | ||
| 116 | continue | ||
| 117 | } | ||
| 118 | |||
| 119 | line = bytes.TrimSpace(line[5:]) | ||
| 120 | if gjson.GetBytes(line, "type").String() != "response.completed" { | ||
| 121 | continue | ||
| 122 | } | ||
| 123 | |||
| 124 | if detail, ok := parseCodexUsage(line); ok { | ||
| 125 | reporter.publish(ctx, detail) | ||
| 126 | } | ||
| 127 | |||
| 128 | var param any | ||
| 129 | out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, line, ¶m) | ||
| 130 | return cliproxyexecutor.Response{Payload: []byte(out)}, nil | ||
| 131 | } | ||
| 132 | return cliproxyexecutor.Response{}, statusErr{code: 408, msg: "stream error: stream disconnected before completion: stream closed before response.completed"} | ||
| 109 | } | 133 | } |
| 110 | 134 | ||
| 111 | func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { | 135 | func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { |
| @@ -116,6 +140,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 4 unmodified lines | |||
| 116 | if baseURL == "" { | 140 | if baseURL == "" { |
| 117 | baseURL = "https://chatgpt.com/backend-api/codex" | 141 | baseURL = "https://chatgpt.com/backend-api/codex" |
| 118 | } | 142 | } |
| 143 | reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | ||
| 119 | 144 | ||
| 120 | from := opts.SourceFormat | 145 | from := opts.SourceFormat |
| 121 | to := sdktranslator.FromString("codex") | 146 | to := sdktranslator.FromString("codex") |
| @@ -181,6 +206,16 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 59 unmodified lines | |||
| 181 | for scanner.Scan() { | 206 | for scanner.Scan() { |
| 182 | line := scanner.Bytes() | 207 | line := scanner.Bytes() |
| 183 | appendAPIResponseChunk(ctx, e.cfg, line) | 208 | appendAPIResponseChunk(ctx, e.cfg, line) |
| 209 | |||
| 210 | if bytes.HasPrefix(line, dataTag) { | ||
| 211 | data := bytes.TrimSpace(line[5:]) | ||
| 212 | if gjson.GetBytes(data, "type").String() == "response.completed" { | ||
| 213 | if detail, ok := parseCodexUsage(data); ok { | ||
| 214 | reporter.publish(ctx, detail) | ||
| 215 | } | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 184 | chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) | 219 | chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) |
| 185 | for i := range chunks { | 220 | for i := range chunks { |
| 186 | out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} | 221 | out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} |