History

feat(usage): implement usage tracking infrastructure across executors

- 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.

Luis Pater committed 1 year ago

Commit 3ade03f1 parent 5090d9813 files · +706 −133Browse files

Showing the changes to internal/runtime/executor/codex_executor.go (1 of 13 files). Show all

Unified · Split

internal/runtime/executor/codex_executor.go+38−3
@@ -18,12 +18,15 @@ import ( · 17 unmodified lines
1818 cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
1919 sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
2020 log "github.com/sirupsen/logrus"
21+ "github.com/tidwall/gjson"
2122 "github.com/tidwall/sjson"
2223
2324 "github.com/gin-gonic/gin"
2425 "github.com/google/uuid"
2526 )
2627
28+var dataTag = []byte("data:")
29+
2730 // CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint).
2831 // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter.
2932 type CodexExecutor struct {
@@ -44,6 +47,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 14 unmodified lines
4447 if baseURL == "" {
4548 baseURL = "https://chatgpt.com/backend-api/codex"
4649 }
50+ reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
4751
4852 from := opts.SourceFormat
4953 to := sdktranslator.FromString("codex")
@@ -75,6 +79,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re · 25 unmodified lines
7579 }
7680 }
7781
82+ body, _ = sjson.SetBytes(body, "stream", true)
83+
7884 url := strings.TrimSuffix(baseURL, "/") + "/responses"
7985 recordAPIRequest(ctx, e.cfg, body)
8086 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
103109 return cliproxyexecutor.Response{}, err
104110 }
105111 appendAPIResponseChunk(ctx, e.cfg, data)
106− var param any
107− out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param)
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, &param)
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"}
109133 }
110134
111135 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
116140 if baseURL == "" {
117141 baseURL = "https://chatgpt.com/backend-api/codex"
118142 }
143+ reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
119144
120145 from := opts.SourceFormat
121146 to := sdktranslator.FromString("codex")
@@ -181,6 +206,16 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au · 59 unmodified lines
181206 for scanner.Scan() {
182207 line := scanner.Bytes()
183208 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+
184219 chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), &param)
185220 for i := range chunks {
186221 out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t