diff --git a/internal/runtime/executor/github_copilot_executor.go b/internal/runtime/executor/github_copilot_executor.go index 64bca39a..f29af146 100644 --- a/internal/runtime/executor/github_copilot_executor.go +++ b/internal/runtime/executor/github_copilot_executor.go @@ -63,10 +63,38 @@ func NewGitHubCopilotExecutor(cfg *config.Config) *GitHubCopilotExecutor { func (e *GitHubCopilotExecutor) Identifier() string { return githubCopilotAuthType } // PrepareRequest implements ProviderExecutor. -func (e *GitHubCopilotExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) error { +func (e *GitHubCopilotExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error { + if req == nil { + return nil + } + ctx := req.Context() + if ctx == nil { + ctx = context.Background() + } + apiToken, errToken := e.ensureAPIToken(ctx, auth) + if errToken != nil { + return errToken + } + e.applyHeaders(req, apiToken) return nil } +// HttpRequest injects GitHub Copilot credentials into the request and executes it. +func (e *GitHubCopilotExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) { + if req == nil { + return nil, fmt.Errorf("github-copilot executor: request is nil") + } + if ctx == nil { + ctx = req.Context() + } + httpReq := req.WithContext(ctx) + if errPrepare := e.PrepareRequest(httpReq, auth); errPrepare != nil { + return nil, errPrepare + } + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + return httpClient.Do(httpReq) +} + // Execute handles non-streaming requests to GitHub Copilot. func (e *GitHubCopilotExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { apiToken, errToken := e.ensureAPIToken(ctx, auth) diff --git a/internal/runtime/executor/kiro_executor.go b/internal/runtime/executor/kiro_executor.go index 1e882888..4d3c9749 100644 --- a/internal/runtime/executor/kiro_executor.go +++ b/internal/runtime/executor/kiro_executor.go @@ -28,7 +28,6 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" log "github.com/sirupsen/logrus" - ) const ( @@ -218,7 +217,48 @@ func NewKiroExecutor(cfg *config.Config) *KiroExecutor { func (e *KiroExecutor) Identifier() string { return "kiro" } // PrepareRequest prepares the HTTP request before execution. -func (e *KiroExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) error { return nil } +func (e *KiroExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error { + if req == nil { + return nil + } + accessToken, _ := kiroCredentials(auth) + if strings.TrimSpace(accessToken) == "" { + return statusErr{code: http.StatusUnauthorized, msg: "missing access token"} + } + if isIDCAuth(auth) { + req.Header.Set("User-Agent", kiroIDEUserAgent) + req.Header.Set("X-Amz-User-Agent", kiroIDEAmzUserAgent) + req.Header.Set("x-amzn-kiro-agent-mode", kiroIDEAgentModeSpec) + } else { + req.Header.Set("User-Agent", kiroUserAgent) + req.Header.Set("X-Amz-User-Agent", kiroFullUserAgent) + } + req.Header.Set("Amz-Sdk-Request", "attempt=1; max=3") + req.Header.Set("Amz-Sdk-Invocation-Id", uuid.New().String()) + req.Header.Set("Authorization", "Bearer "+accessToken) + var attrs map[string]string + if auth != nil { + attrs = auth.Attributes + } + util.ApplyCustomHeadersFromAttrs(req, attrs) + return nil +} + +// HttpRequest injects Kiro credentials into the request and executes it. +func (e *KiroExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) { + if req == nil { + return nil, fmt.Errorf("kiro executor: request is nil") + } + if ctx == nil { + ctx = req.Context() + } + httpReq := req.WithContext(ctx) + if errPrepare := e.PrepareRequest(httpReq, auth); errPrepare != nil { + return nil, errPrepare + } + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + return httpClient.Do(httpReq) +} // Execute sends the request to Kiro API and returns the response. // Supports automatic token refresh on 401/403 errors. @@ -1004,7 +1044,7 @@ func findRealThinkingEndTag(content string, alreadyInCodeBlock, alreadyInInlineC discussionPatterns := []string{ "标签", "返回", "输出", "包含", "使用", "解析", "转换", "生成", // Chinese "tag", "return", "output", "contain", "use", "parse", "emit", "convert", "generate", // English - "", // discussing both tags together + "", // discussing both tags together "``", // explicitly in inline code } isDiscussion := false @@ -1852,7 +1892,6 @@ func (e *KiroExecutor) extractEventTypeFromBytes(headers []byte) string { return "" } - // NOTE: Response building functions moved to internal/translator/kiro/claude/kiro_claude_response.go // The executor now uses kiroclaude.BuildClaudeResponse() and kiroclaude.ExtractThinkingFromContent() instead @@ -1889,18 +1928,18 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out var lastReportedOutputTokens int64 // Last reported output token count // Upstream usage tracking - Kiro API returns credit usage and context percentage - var upstreamCreditUsage float64 // Credit usage from upstream (e.g., 1.458) - var upstreamContextPercentage float64 // Context usage percentage from upstream (e.g., 78.56) - var hasUpstreamUsage bool // Whether we received usage from upstream + var upstreamCreditUsage float64 // Credit usage from upstream (e.g., 1.458) + var upstreamContextPercentage float64 // Context usage percentage from upstream (e.g., 78.56) + var hasUpstreamUsage bool // Whether we received usage from upstream // Translator param for maintaining tool call state across streaming events // IMPORTANT: This must persist across all TranslateStream calls var translatorParam any // Thinking mode state tracking - tag-based parsing for tags in content - inThinkBlock := false // Whether we're currently inside a block - isThinkingBlockOpen := false // Track if thinking content block SSE event is open - thinkingBlockIndex := -1 // Index of the thinking content block + inThinkBlock := false // Whether we're currently inside a block + isThinkingBlockOpen := false // Track if thinking content block SSE event is open + thinkingBlockIndex := -1 // Index of the thinking content block var accumulatedThinkingContent strings.Builder // Accumulate thinking content for token counting // Buffer for handling partial tag matches at chunk boundaries @@ -2319,16 +2358,16 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out lastUsageUpdateLen = accumulatedContent.Len() lastUsageUpdateTime = time.Now() - } + } - // TAG-BASED THINKING PARSING: Parse tags from content - // Combine pending content with new content for processing - pendingContent.WriteString(contentDelta) - processContent := pendingContent.String() - pendingContent.Reset() + // TAG-BASED THINKING PARSING: Parse tags from content + // Combine pending content with new content for processing + pendingContent.WriteString(contentDelta) + processContent := pendingContent.String() + pendingContent.Reset() - // Process content looking for thinking tags - for len(processContent) > 0 { + // Process content looking for thinking tags + for len(processContent) > 0 { if inThinkBlock { // We're inside a thinking block, look for endIdx := strings.Index(processContent, kirocommon.ThinkingEndTag) @@ -2503,7 +2542,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out processContent = "" } } - } + } } // Handle tool uses in response (with deduplication) @@ -2927,7 +2966,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Calculate input tokens from context percentage // Using 200k as the base since that's what Kiro reports against calculatedInputTokens := int64(upstreamContextPercentage * 200000 / 100) - + // Only use calculated value if it's significantly different from local estimate // This provides more accurate token counts based on upstream data if calculatedInputTokens > 0 {