From 3dd0844b9874b48b8bfc82065cb70e5143d6ca89 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 17 Oct 2025 04:12:38 +0800 Subject: [PATCH] Enhance logging for API requests and responses across executors - Added detailed logging of upstream request metadata including URL, method, headers, and body for Codex, Gemini, IFlow, OpenAI Compat, and Qwen executors. - Implemented error logging for API response failures to capture errors during HTTP requests. - Introduced structured logging for authentication details (AuthID, AuthLabel, AuthType, AuthValue) to improve traceability. - Updated response logging to include status codes and headers for better debugging. - Ensured that all executors consistently log API interactions to facilitate monitoring and troubleshooting. --- internal/logging/request_logger.go | 32 +- internal/runtime/executor/claude_executor.go | 66 +++- internal/runtime/executor/codex_executor.go | 48 ++- .../runtime/executor/gemini_cli_executor.go | 88 ++++- internal/runtime/executor/gemini_executor.go | 63 +++- internal/runtime/executor/iflow_executor.go | 42 ++- internal/runtime/executor/logging_helpers.go | 323 +++++++++++++++++- .../executor/openai_compat_executor.go | 42 ++- internal/runtime/executor/qwen_executor.go | 42 ++- 9 files changed, 699 insertions(+), 47 deletions(-) diff --git a/internal/logging/request_logger.go b/internal/logging/request_logger.go index 6c143d89..47f701e8 100644 --- a/internal/logging/request_logger.go +++ b/internal/logging/request_logger.go @@ -328,9 +328,19 @@ func (l *FileRequestLogger) formatLogContent(url, method string, headers map[str // Request info content.WriteString(l.formatRequestInfo(url, method, headers, body)) - content.WriteString("=== API REQUEST ===\n") - content.Write(apiRequest) - content.WriteString("\n\n") + if len(apiRequest) > 0 { + if bytes.HasPrefix(apiRequest, []byte("=== API REQUEST")) { + content.Write(apiRequest) + if !bytes.HasSuffix(apiRequest, []byte("\n")) { + content.WriteString("\n") + } + } else { + content.WriteString("=== API REQUEST ===\n") + content.Write(apiRequest) + content.WriteString("\n") + } + content.WriteString("\n") + } for i := 0; i < len(apiResponseErrors); i++ { content.WriteString("=== API ERROR RESPONSE ===\n") @@ -339,9 +349,19 @@ func (l *FileRequestLogger) formatLogContent(url, method string, headers map[str content.WriteString("\n\n") } - content.WriteString("=== API RESPONSE ===\n") - content.Write(apiResponse) - content.WriteString("\n\n") + if len(apiResponse) > 0 { + if bytes.HasPrefix(apiResponse, []byte("=== API RESPONSE")) { + content.Write(apiResponse) + if !bytes.HasSuffix(apiResponse, []byte("\n")) { + content.WriteString("\n") + } + } else { + content.WriteString("=== API RESPONSE ===\n") + content.Write(apiResponse) + content.WriteString("\n") + } + content.WriteString("\n") + } // Response section content.WriteString("=== RESPONSE ===\n") diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 0af426fe..2939df1e 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -54,16 +54,33 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r } url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err } applyClaudeHeaders(httpReq, apiKey, false) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } defer func() { @@ -71,6 +88,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r log.Errorf("response body close error: %v", errClose) } }() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) @@ -82,6 +100,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r if hasZSTDEcoding(resp.Header.Get("Content-Encoding")) { decoder, err = zstd.NewReader(resp.Body) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, fmt.Errorf("failed to initialize zstd decoder: %w", err) } reader = decoder @@ -89,6 +108,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r } data, err := io.ReadAll(reader) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) @@ -120,18 +140,36 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A body, _ = sjson.SetRawBytes(body, "system", []byte(misc.ClaudeCodeInstructions)) url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err } applyClaudeHeaders(httpReq, apiKey, true) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return nil, err } + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) @@ -162,6 +200,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A out <- cliproxyexecutor.StreamChunk{Payload: cloned} } if err = scanner.Err(); err != nil { + recordAPIResponseError(ctx, e.cfg, err) out <- cliproxyexecutor.StreamChunk{Err: err} } return @@ -184,6 +223,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } } if err = scanner.Err(); err != nil { + recordAPIResponseError(ctx, e.cfg, err) out <- cliproxyexecutor.StreamChunk{Err: err} } }() @@ -208,16 +248,33 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut } url := fmt.Sprintf("%s/v1/messages/count_tokens?beta=true", baseURL) - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err } applyClaudeHeaders(httpReq, apiKey, false) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } defer func() { @@ -225,6 +282,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut log.Errorf("response body close error: %v", errClose) } }() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) @@ -235,6 +293,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut if hasZSTDEcoding(resp.Header.Get("Content-Encoding")) { decoder, err = zstd.NewReader(resp.Body) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, fmt.Errorf("failed to initialize zstd decoder: %w", err) } reader = decoder @@ -242,6 +301,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut } data, err := io.ReadAll(reader) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 38c0507d..7681fc33 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -79,19 +79,37 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re body, _ = sjson.DeleteBytes(body, "previous_response_id") url := strings.TrimSuffix(baseURL, "/") + "/responses" - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err } applyCodexHeaders(httpReq, auth, apiKey) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } defer func() { _ = resp.Body.Close() }() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) @@ -100,6 +118,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re } data, err := io.ReadAll(resp.Body) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) @@ -165,21 +184,43 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au body, _ = sjson.DeleteBytes(body, "previous_response_id") url := strings.TrimSuffix(baseURL, "/") + "/responses" - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err } applyCodexHeaders(httpReq, auth, apiKey) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return nil, err } + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() - b, _ := io.ReadAll(resp.Body) + b, readErr := io.ReadAll(resp.Body) + if readErr != nil { + recordAPIResponseError(ctx, e.cfg, readErr) + return nil, readErr + } appendAPIResponseChunk(ctx, e.cfg, b) log.Debugf("request error, error status: %d, error body: %s", resp.StatusCode, string(b)) return nil, statusErr{code: resp.StatusCode, msg: string(b)} @@ -211,6 +252,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } } if err = scanner.Err(); err != nil { + recordAPIResponseError(ctx, e.cfg, err) out <- cliproxyexecutor.StreamChunk{Err: err} } }() diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index b4a8fc64..3ab0b4cf 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -83,6 +83,11 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth httpClient := newHTTPClient(ctx, e.cfg, auth, 0) respCtx := context.WithValue(ctx, "alt", opts.Alt) + var authID, authLabel, authType, authValue string + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + var lastStatus int var lastBody []byte @@ -108,7 +113,6 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth url = url + fmt.Sprintf("?$alt=%s", opts.Alt) } - recordAPIRequest(ctx, e.cfg, payload) reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) if errReq != nil { return cliproxyexecutor.Response{}, errReq @@ -117,13 +121,30 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) applyGeminiCLIHeaders(reqHTTP) reqHTTP.Header.Set("Accept", "application/json") + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: reqHTTP.Header.Clone(), + Body: payload, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) resp, errDo := httpClient.Do(reqHTTP) if errDo != nil { + recordAPIResponseError(ctx, e.cfg, errDo) return cliproxyexecutor.Response{}, errDo } - data, _ := io.ReadAll(resp.Body) + data, errRead := io.ReadAll(resp.Body) _ = resp.Body.Close() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) + if errRead != nil { + recordAPIResponseError(ctx, e.cfg, errRead) + return cliproxyexecutor.Response{}, errRead + } appendAPIResponseChunk(ctx, e.cfg, data) if resp.StatusCode >= 200 && resp.StatusCode < 300 { reporter.publish(ctx, parseGeminiCLIUsage(data)) @@ -132,7 +153,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth return cliproxyexecutor.Response{Payload: []byte(out)}, nil } lastStatus = resp.StatusCode - lastBody = data + lastBody = append([]byte(nil), data...) if resp.StatusCode != 429 { break } @@ -170,6 +191,11 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut httpClient := newHTTPClient(ctx, e.cfg, auth, 0) respCtx := context.WithValue(ctx, "alt", opts.Alt) + var authID, authLabel, authType, authValue string + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + var lastStatus int var lastBody []byte @@ -192,7 +218,6 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut url = url + fmt.Sprintf("?$alt=%s", opts.Alt) } - recordAPIRequest(ctx, e.cfg, payload) reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) if errReq != nil { return nil, errReq @@ -201,17 +226,34 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) applyGeminiCLIHeaders(reqHTTP) reqHTTP.Header.Set("Accept", "text/event-stream") + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: reqHTTP.Header.Clone(), + Body: payload, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) resp, errDo := httpClient.Do(reqHTTP) if errDo != nil { + recordAPIResponseError(ctx, e.cfg, errDo) return nil, errDo } + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { - data, _ := io.ReadAll(resp.Body) + data, errRead := io.ReadAll(resp.Body) _ = resp.Body.Close() + if errRead != nil { + recordAPIResponseError(ctx, e.cfg, errRead) + return nil, errRead + } appendAPIResponseChunk(ctx, e.cfg, data) lastStatus = resp.StatusCode - lastBody = data + lastBody = append([]byte(nil), data...) log.Debugf("request error, error status: %d, error body: %s", resp.StatusCode, string(data)) if resp.StatusCode == 429 { continue @@ -247,6 +289,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])} } if errScan := scanner.Err(); errScan != nil { + recordAPIResponseError(ctx, e.cfg, errScan) out <- cliproxyexecutor.StreamChunk{Err: errScan} } return @@ -254,6 +297,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut data, errRead := io.ReadAll(resp.Body) if errRead != nil { + recordAPIResponseError(ctx, e.cfg, errRead) out <- cliproxyexecutor.StreamChunk{Err: errRead} return } @@ -297,6 +341,13 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. httpClient := newHTTPClient(ctx, e.cfg, auth, 0) respCtx := context.WithValue(ctx, "alt", opts.Alt) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + var lastStatus int var lastBody []byte @@ -322,7 +373,6 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. url = url + fmt.Sprintf("?$alt=%s", opts.Alt) } - recordAPIRequest(ctx, e.cfg, payload) reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) if errReq != nil { return cliproxyexecutor.Response{}, errReq @@ -331,13 +381,30 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) applyGeminiCLIHeaders(reqHTTP) reqHTTP.Header.Set("Accept", "application/json") + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: reqHTTP.Header.Clone(), + Body: payload, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) resp, errDo := httpClient.Do(reqHTTP) if errDo != nil { + recordAPIResponseError(ctx, e.cfg, errDo) return cliproxyexecutor.Response{}, errDo } - data, _ := io.ReadAll(resp.Body) + data, errRead := io.ReadAll(resp.Body) _ = resp.Body.Close() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) + if errRead != nil { + recordAPIResponseError(ctx, e.cfg, errRead) + return cliproxyexecutor.Response{}, errRead + } appendAPIResponseChunk(ctx, e.cfg, data) if resp.StatusCode >= 200 && resp.StatusCode < 300 { count := gjson.GetBytes(data, "totalTokens").Int() @@ -345,16 +412,13 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. return cliproxyexecutor.Response{Payload: []byte(translated)}, nil } lastStatus = resp.StatusCode - lastBody = data + lastBody = append([]byte(nil), data...) if resp.StatusCode == 429 { continue } break } - if len(lastBody) > 0 { - appendAPIResponseChunk(ctx, e.cfg, lastBody) - } if lastStatus == 0 { lastStatus = 429 } diff --git a/internal/runtime/executor/gemini_executor.go b/internal/runtime/executor/gemini_executor.go index 59a8ae9e..ae01eb37 100644 --- a/internal/runtime/executor/gemini_executor.go +++ b/internal/runtime/executor/gemini_executor.go @@ -96,7 +96,6 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r body, _ = sjson.DeleteBytes(body, "session_id") - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err @@ -107,13 +106,32 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r } else if bearer != "" { httpReq.Header.Set("Authorization", "Bearer "+bearer) } + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } defer func() { _ = resp.Body.Close() }() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) @@ -122,6 +140,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r } data, err := io.ReadAll(resp.Body) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) @@ -154,7 +173,6 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A body, _ = sjson.DeleteBytes(body, "session_id") - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err @@ -165,12 +183,31 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } else { httpReq.Header.Set("Authorization", "Bearer "+bearer) } + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return nil, err } + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) @@ -202,6 +239,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} } if err = scanner.Err(); err != nil { + recordAPIResponseError(ctx, e.cfg, err) out <- cliproxyexecutor.StreamChunk{Err: err} } }() @@ -224,7 +262,6 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig") url := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, req.Model, "countTokens") - recordAPIRequest(ctx, e.cfg, translatedReq) requestBody := bytes.NewReader(translatedReq) @@ -238,16 +275,36 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut } else { httpReq.Header.Set("Authorization", "Bearer "+bearer) } + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: translatedReq, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } defer func() { _ = resp.Body.Close() }() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) data, err := io.ReadAll(resp.Body) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) diff --git a/internal/runtime/executor/iflow_executor.go b/internal/runtime/executor/iflow_executor.go index c8d23366..b63e82b2 100644 --- a/internal/runtime/executor/iflow_executor.go +++ b/internal/runtime/executor/iflow_executor.go @@ -56,20 +56,38 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err } applyIFlowHeaders(httpReq, apiKey, false) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: endpoint, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } defer func() { _ = resp.Body.Close() }() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) @@ -80,6 +98,7 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re data, err := io.ReadAll(resp.Body) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) @@ -113,20 +132,38 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return nil, err } applyIFlowHeaders(httpReq, apiKey, true) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: endpoint, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return nil, err } + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) @@ -156,6 +193,7 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } } if err := scanner.Err(); err != nil { + recordAPIResponseError(ctx, e.cfg, err) out <- cliproxyexecutor.StreamChunk{Err: err} } }() diff --git a/internal/runtime/executor/logging_helpers.go b/internal/runtime/executor/logging_helpers.go index 79f4590f..da0dcd40 100644 --- a/internal/runtime/executor/logging_helpers.go +++ b/internal/runtime/executor/logging_helpers.go @@ -3,19 +3,144 @@ package executor import ( "bytes" "context" + "fmt" + "net/http" + "sort" + "strings" + "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" ) -// recordAPIRequest stores the upstream request payload in Gin context for request logging. -func recordAPIRequest(ctx context.Context, cfg *config.Config, payload []byte) { - if cfg == nil || !cfg.RequestLog || len(payload) == 0 { +const ( + apiAttemptsKey = "API_UPSTREAM_ATTEMPTS" + apiRequestKey = "API_REQUEST" + apiResponseKey = "API_RESPONSE" +) + +// upstreamRequestLog captures the outbound upstream request details for logging. +type upstreamRequestLog struct { + URL string + Method string + Headers http.Header + Body []byte + Provider string + AuthID string + AuthLabel string + AuthType string + AuthValue string +} + +type upstreamAttempt struct { + index int + request string + response *strings.Builder + responseIntroWritten bool + statusWritten bool + headersWritten bool + bodyStarted bool + bodyHasContent bool + errorWritten bool +} + +// 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 } - if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { - ginCtx.Set("API_REQUEST", bytes.Clone(payload)) + ginCtx := ginContextFrom(ctx) + if ginCtx == nil { + return } + + attempts := getAttempts(ginCtx) + index := len(attempts) + 1 + + builder := &strings.Builder{} + builder.WriteString(fmt.Sprintf("=== API REQUEST %d ===\n", index)) + builder.WriteString(fmt.Sprintf("Timestamp: %s\n", time.Now().Format(time.RFC3339Nano))) + if info.URL != "" { + builder.WriteString(fmt.Sprintf("Upstream URL: %s\n", info.URL)) + } else { + builder.WriteString("Upstream URL: \n") + } + if info.Method != "" { + builder.WriteString(fmt.Sprintf("HTTP Method: %s\n", info.Method)) + } + if auth := formatAuthInfo(info); auth != "" { + builder.WriteString(fmt.Sprintf("Auth: %s\n", auth)) + } + builder.WriteString("\nHeaders:\n") + writeHeaders(builder, info.Headers) + builder.WriteString("\nBody:\n") + if len(info.Body) > 0 { + builder.WriteString(string(bytes.Clone(info.Body))) + } else { + builder.WriteString("") + } + builder.WriteString("\n\n") + + attempt := &upstreamAttempt{ + index: index, + request: builder.String(), + response: &strings.Builder{}, + } + attempts = append(attempts, attempt) + ginCtx.Set(apiAttemptsKey, attempts) + 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) { + if cfg == nil || !cfg.RequestLog { + return + } + ginCtx := ginContextFrom(ctx) + if ginCtx == nil { + return + } + attempts, attempt := ensureAttempt(ginCtx) + ensureResponseIntro(attempt) + + if status > 0 && !attempt.statusWritten { + attempt.response.WriteString(fmt.Sprintf("Status: %d\n", status)) + attempt.statusWritten = true + } + if !attempt.headersWritten { + attempt.response.WriteString("Headers:\n") + writeHeaders(attempt.response, headers) + attempt.headersWritten = true + attempt.response.WriteString("\n") + } + + 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) { + if cfg == nil || !cfg.RequestLog || err == nil { + return + } + ginCtx := ginContextFrom(ctx) + if ginCtx == nil { + return + } + attempts, attempt := ensureAttempt(ginCtx) + ensureResponseIntro(attempt) + + if attempt.bodyStarted && !attempt.bodyHasContent { + // Ensure body does not stay empty marker if error arrives first. + attempt.bodyStarted = false + } + if attempt.errorWritten { + attempt.response.WriteString("\n") + } + attempt.response.WriteString(fmt.Sprintf("Error: %s\n", err.Error())) + attempt.errorWritten = true + + updateAggregatedResponse(ginCtx, attempts) } // appendAPIResponseChunk appends an upstream response chunk to Gin context for request logging. @@ -27,15 +152,185 @@ func appendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byt if len(data) == 0 { return } - if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { - if existing, exists := ginCtx.Get("API_RESPONSE"); exists { - if prev, okBytes := existing.([]byte); okBytes { - prev = append(prev, data...) - prev = append(prev, []byte("\n\n")...) - ginCtx.Set("API_RESPONSE", prev) - return - } + ginCtx := ginContextFrom(ctx) + if ginCtx == nil { + return + } + attempts, attempt := ensureAttempt(ginCtx) + ensureResponseIntro(attempt) + + if !attempt.headersWritten { + attempt.response.WriteString("Headers:\n") + writeHeaders(attempt.response, nil) + attempt.headersWritten = true + attempt.response.WriteString("\n") + } + if !attempt.bodyStarted { + attempt.response.WriteString("Body:\n") + attempt.bodyStarted = true + } + if attempt.bodyHasContent { + attempt.response.WriteString("\n\n") + } + attempt.response.WriteString(string(data)) + attempt.bodyHasContent = true + + updateAggregatedResponse(ginCtx, attempts) +} + +func ginContextFrom(ctx context.Context) *gin.Context { + ginCtx, _ := ctx.Value("gin").(*gin.Context) + return ginCtx +} + +func getAttempts(ginCtx *gin.Context) []*upstreamAttempt { + if ginCtx == nil { + return nil + } + if value, exists := ginCtx.Get(apiAttemptsKey); exists { + if attempts, ok := value.([]*upstreamAttempt); ok { + return attempts + } + } + return nil +} + +func ensureAttempt(ginCtx *gin.Context) ([]*upstreamAttempt, *upstreamAttempt) { + attempts := getAttempts(ginCtx) + if len(attempts) == 0 { + attempt := &upstreamAttempt{ + index: 1, + request: "=== API REQUEST 1 ===\n\n\n", + response: &strings.Builder{}, + } + attempts = []*upstreamAttempt{attempt} + ginCtx.Set(apiAttemptsKey, attempts) + updateAggregatedRequest(ginCtx, attempts) + } + return attempts, attempts[len(attempts)-1] +} + +func ensureResponseIntro(attempt *upstreamAttempt) { + if attempt == nil || attempt.response == nil || attempt.responseIntroWritten { + return + } + attempt.response.WriteString(fmt.Sprintf("=== API RESPONSE %d ===\n", attempt.index)) + attempt.response.WriteString(fmt.Sprintf("Timestamp: %s\n", time.Now().Format(time.RFC3339Nano))) + attempt.response.WriteString("\n") + attempt.responseIntroWritten = true +} + +func updateAggregatedRequest(ginCtx *gin.Context, attempts []*upstreamAttempt) { + if ginCtx == nil { + return + } + var builder strings.Builder + for _, attempt := range attempts { + builder.WriteString(attempt.request) + } + ginCtx.Set(apiRequestKey, []byte(builder.String())) +} + +func updateAggregatedResponse(ginCtx *gin.Context, attempts []*upstreamAttempt) { + if ginCtx == nil { + return + } + var builder strings.Builder + for idx, attempt := range attempts { + if attempt == nil || attempt.response == nil { + continue + } + responseText := attempt.response.String() + if responseText == "" { + continue + } + builder.WriteString(responseText) + if !strings.HasSuffix(responseText, "\n") { + builder.WriteString("\n") + } + if idx < len(attempts)-1 { + builder.WriteString("\n") + } + } + ginCtx.Set(apiResponseKey, []byte(builder.String())) +} + +func writeHeaders(builder *strings.Builder, headers http.Header) { + if builder == nil { + return + } + if len(headers) == 0 { + builder.WriteString("\n") + return + } + keys := make([]string, 0, len(headers)) + for key := range headers { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + values := headers[key] + if len(values) == 0 { + builder.WriteString(fmt.Sprintf("%s:\n", key)) + continue + } + for _, value := range values { + builder.WriteString(fmt.Sprintf("%s: %s\n", key, sanitizeHeaderValue(key, value))) } - ginCtx.Set("API_RESPONSE", data) + } +} + +func formatAuthInfo(info upstreamRequestLog) string { + var parts []string + if trimmed := strings.TrimSpace(info.Provider); trimmed != "" { + parts = append(parts, fmt.Sprintf("provider=%s", trimmed)) + } + if trimmed := strings.TrimSpace(info.AuthID); trimmed != "" { + parts = append(parts, fmt.Sprintf("auth_id=%s", trimmed)) + } + if trimmed := strings.TrimSpace(info.AuthLabel); trimmed != "" { + parts = append(parts, fmt.Sprintf("label=%s", trimmed)) + } + + authType := strings.ToLower(strings.TrimSpace(info.AuthType)) + authValue := strings.TrimSpace(info.AuthValue) + switch authType { + case "api_key": + if authValue != "" { + parts = append(parts, fmt.Sprintf("type=api_key value=%s", util.HideAPIKey(authValue))) + } else { + parts = append(parts, "type=api_key") + } + case "oauth": + if authValue != "" { + parts = append(parts, fmt.Sprintf("type=oauth account=%s", authValue)) + } else { + parts = append(parts, "type=oauth") + } + default: + if authType != "" { + if authValue != "" { + parts = append(parts, fmt.Sprintf("type=%s value=%s", authType, authValue)) + } else { + parts = append(parts, fmt.Sprintf("type=%s", authType)) + } + } + } + + return strings.Join(parts, ", ") +} + +func sanitizeHeaderValue(key, value string) string { + trimmedValue := strings.TrimSpace(value) + lowerKey := strings.ToLower(strings.TrimSpace(key)) + switch { + case strings.Contains(lowerKey, "authorization"), + strings.Contains(lowerKey, "api-key"), + strings.Contains(lowerKey, "apikey"), + strings.Contains(lowerKey, "token"), + strings.Contains(lowerKey, "secret"): + return util.HideAPIKey(trimmedValue) + default: + return trimmedValue } } diff --git a/internal/runtime/executor/openai_compat_executor.go b/internal/runtime/executor/openai_compat_executor.go index 7d409f5d..e5d0aeec 100644 --- a/internal/runtime/executor/openai_compat_executor.go +++ b/internal/runtime/executor/openai_compat_executor.go @@ -54,7 +54,6 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A } url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" - recordAPIRequest(ctx, e.cfg, translated) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) if err != nil { return cliproxyexecutor.Response{}, err @@ -64,13 +63,32 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A httpReq.Header.Set("Authorization", "Bearer "+apiKey) } httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat") + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: translated, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } defer func() { _ = resp.Body.Close() }() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) @@ -79,6 +97,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A } body, err := io.ReadAll(resp.Body) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, body) @@ -103,7 +122,6 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy } url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" - recordAPIRequest(ctx, e.cfg, translated) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) if err != nil { return nil, err @@ -115,12 +133,31 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy httpReq.Header.Set("User-Agent", "cli-proxy-openai-compat") httpReq.Header.Set("Accept", "text/event-stream") httpReq.Header.Set("Cache-Control", "no-cache") + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: translated, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return nil, err } + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) @@ -153,6 +190,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy } } if err = scanner.Err(); err != nil { + recordAPIResponseError(ctx, e.cfg, err) out <- cliproxyexecutor.StreamChunk{Err: err} } }() diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index dcc02474..e631e615 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -51,19 +51,37 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err } applyQwenHeaders(httpReq, token, false) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } defer func() { _ = resp.Body.Close() }() + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) @@ -72,6 +90,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req } data, err := io.ReadAll(resp.Body) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) @@ -102,18 +121,36 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut body, _ = sjson.SetBytes(body, "stream_options.include_usage", true) url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" - recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err } applyQwenHeaders(httpReq, token, true) + var authID, authLabel, authType, authValue string + if auth != nil { + authID = auth.ID + authLabel = auth.Label + authType, authValue = auth.AccountInfo() + } + recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ + URL: url, + Method: http.MethodPost, + Headers: httpReq.Header.Clone(), + Body: body, + Provider: e.Identifier(), + AuthID: authID, + AuthLabel: authLabel, + AuthType: authType, + AuthValue: authValue, + }) httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) resp, err := httpClient.Do(httpReq) if err != nil { + recordAPIResponseError(ctx, e.cfg, err) return nil, err } + recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) @@ -141,6 +178,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut } } if err = scanner.Err(); err != nil { + recordAPIResponseError(ctx, e.cfg, err) out <- cliproxyexecutor.StreamChunk{Err: err} } }()