- 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 | 18 | cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" |
| 19 | 19 | sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" |
| 20 | 20 | log "github.com/sirupsen/logrus" |
| 21 | + "github.com/tidwall/gjson" | |
| 21 | 22 | "github.com/tidwall/sjson" |
| 22 | 23 | |
| 23 | 24 | "github.com/gin-gonic/gin" |
| 24 | 25 | "github.com/google/uuid" |
| 25 | 26 | ) |
| 26 | 27 | |
| 28 | +var dataTag = []byte("data:") | |
| 29 | + | |
| 27 | 30 | // CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint). |
| 28 | 31 | // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter. |
| 29 | 32 | type CodexExecutor struct { |
| @@ -44,6 +47,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 14 unmodified lines | ||
| 44 | 47 | if baseURL == "" { |
| 45 | 48 | baseURL = "https://chatgpt.com/backend-api/codex" |
| 46 | 49 | } |
| 50 | + reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | |
| 47 | 51 | |
| 48 | 52 | from := opts.SourceFormat |
| 49 | 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 | 84 | url := strings.TrimSuffix(baseURL, "/") + "/responses" |
| 79 | 85 | recordAPIRequest(ctx, e.cfg, body) |
| 80 | 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 | 109 | return cliproxyexecutor.Response{}, err |
| 104 | 110 | } |
| 105 | 111 | appendAPIResponseChunk(ctx, e.cfg, data) |
| 106 | − var param any | |
| 107 | − out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) | |
| 108 | − return cliproxyexecutor.Response{Payload: []byte(out)}, nil | |
| 112 | + | |
| 113 | + lines := bytes.Split(data, []byte("\n")) | |
| 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 | 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 | 140 | if baseURL == "" { |
| 117 | 141 | baseURL = "https://chatgpt.com/backend-api/codex" |
| 118 | 142 | } |
| 143 | + reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) | |
| 119 | 144 | |
| 120 | 145 | from := opts.SourceFormat |
| 121 | 146 | to := sdktranslator.FromString("codex") |
| @@ -181,6 +206,16 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 59 unmodified lines | ||
| 181 | 206 | for scanner.Scan() { |
| 182 | 207 | line := scanner.Bytes() |
| 183 | 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 | 219 | chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) |
| 185 | 220 | for i := range chunks { |
| 186 | 221 | out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} |