From d693d7993b576e9b639c9ca95904f92afcbf0b70 Mon Sep 17 00:00:00 2001 From: ciberponk Date: Sat, 21 Feb 2026 12:56:10 +0800 Subject: [PATCH 01/10] feat: support responses compaction payload compatibility for codex translator --- .../codex_openai-responses_request.go | 40 +++++++++++++++++++ .../codex_openai-responses_request_test.go | 38 ++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request.go b/internal/translator/codex/openai/responses/codex_openai-responses_request.go index f0407149e0..3762f15293 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request.go @@ -2,6 +2,7 @@ package responses import ( "fmt" + "strings" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -26,6 +27,8 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, rawJSON, _ = sjson.DeleteBytes(rawJSON, "temperature") rawJSON, _ = sjson.DeleteBytes(rawJSON, "top_p") rawJSON, _ = sjson.DeleteBytes(rawJSON, "service_tier") + rawJSON, _ = sjson.DeleteBytes(rawJSON, "truncation") + rawJSON = applyResponsesCompactionCompatibility(rawJSON) // Delete the user field as it is not supported by the Codex upstream. rawJSON, _ = sjson.DeleteBytes(rawJSON, "user") @@ -36,6 +39,43 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, return rawJSON } +// applyResponsesCompactionCompatibility handles OpenAI Responses context_management.compaction +// for Codex upstream compatibility. +// +// Codex /responses currently rejects context_management with: +// {"detail":"Unsupported parameter: context_management"}. +// +// Compatibility strategy: +// 1) Remove context_management before forwarding to Codex upstream. +// 2) Remove truncation as Codex upstream currently rejects it as unsupported. +func applyResponsesCompactionCompatibility(rawJSON []byte) []byte { + contextManagement := gjson.GetBytes(rawJSON, "context_management") + if !contextManagement.Exists() { + return rawJSON + } + + hasCompactionRule := false + switch { + case contextManagement.IsArray(): + for _, item := range contextManagement.Array() { + if strings.EqualFold(item.Get("type").String(), "compaction") { + hasCompactionRule = true + break + } + } + case contextManagement.IsObject(): + hasCompactionRule = strings.EqualFold(contextManagement.Get("type").String(), "compaction") + } + + if hasCompactionRule { + // no-op marker: compaction hint detected and consumed for compatibility. + } + + rawJSON, _ = sjson.DeleteBytes(rawJSON, "context_management") + rawJSON, _ = sjson.DeleteBytes(rawJSON, "truncation") + return rawJSON +} + // convertSystemRoleToDeveloper traverses the input array and converts any message items // with role "system" to role "developer". This is necessary because Codex API does not // accept "system" role in the input array. diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go b/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go index 4f5624869f..65732c3ffa 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go @@ -280,3 +280,41 @@ func TestUserFieldDeletion(t *testing.T) { t.Errorf("user field should be deleted, but it was found with value: %s", userField.Raw) } } + +func TestContextManagementCompactionCompatibility(t *testing.T) { + inputJSON := []byte(`{ + "model": "gpt-5.2", + "context_management": [ + { + "type": "compaction", + "compact_threshold": 12000 + } + ], + "input": [{"role":"user","content":"hello"}] + }`) + + output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false) + outputStr := string(output) + + if gjson.Get(outputStr, "context_management").Exists() { + t.Fatalf("context_management should be removed for Codex compatibility") + } + if gjson.Get(outputStr, "truncation").Exists() { + t.Fatalf("truncation should be removed for Codex compatibility") + } +} + +func TestTruncationRemovedForCodexCompatibility(t *testing.T) { + inputJSON := []byte(`{ + "model": "gpt-5.2", + "truncation": "disabled", + "input": [{"role":"user","content":"hello"}] + }`) + + output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false) + outputStr := string(output) + + if gjson.Get(outputStr, "truncation").Exists() { + t.Fatalf("truncation should be removed for Codex compatibility") + } +} From afc8a0f9be7f261c4df6322dfe156913558934d0 Mon Sep 17 00:00:00 2001 From: fan Date: Sat, 21 Feb 2026 22:20:48 +0800 Subject: [PATCH 02/10] refactor: simplify context_management compatibility handling --- .../codex_openai-responses_request.go | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request.go b/internal/translator/codex/openai/responses/codex_openai-responses_request.go index 3762f15293..1161c515a0 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request.go @@ -2,7 +2,6 @@ package responses import ( "fmt" - "strings" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -47,32 +46,12 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, // // Compatibility strategy: // 1) Remove context_management before forwarding to Codex upstream. -// 2) Remove truncation as Codex upstream currently rejects it as unsupported. func applyResponsesCompactionCompatibility(rawJSON []byte) []byte { - contextManagement := gjson.GetBytes(rawJSON, "context_management") - if !contextManagement.Exists() { + if !gjson.GetBytes(rawJSON, "context_management").Exists() { return rawJSON } - hasCompactionRule := false - switch { - case contextManagement.IsArray(): - for _, item := range contextManagement.Array() { - if strings.EqualFold(item.Get("type").String(), "compaction") { - hasCompactionRule = true - break - } - } - case contextManagement.IsObject(): - hasCompactionRule = strings.EqualFold(contextManagement.Get("type").String(), "compaction") - } - - if hasCompactionRule { - // no-op marker: compaction hint detected and consumed for compatibility. - } - rawJSON, _ = sjson.DeleteBytes(rawJSON, "context_management") - rawJSON, _ = sjson.DeleteBytes(rawJSON, "truncation") return rawJSON } From 3b421c8181c93393ac715d8281cefd06c68d2e03 Mon Sep 17 00:00:00 2001 From: piexian <64474352+piexian@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:38:46 +0800 Subject: [PATCH 03/10] feat(qwen): add rate limiting and quota error handling - Add 60 requests/minute rate limiting per credential using sliding window - Detect insufficient_quota errors and set cooldown until next day (Beijing time) - Map quota errors (HTTP 403/429) to 429 with retryAfter for conductor integration - Cache Beijing timezone at package level to avoid repeated syscalls - Add redactAuthID function to protect credentials in logs - Extract wrapQwenError helper to consolidate error handling --- internal/runtime/executor/qwen_executor.go | 185 ++++++++++++++++++++- 1 file changed, 176 insertions(+), 9 deletions(-) diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index bcc4a057ae..e7957d2918 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "strings" + "sync" "time" qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" @@ -22,9 +23,151 @@ import ( ) const ( - qwenUserAgent = "QwenCode/0.10.3 (darwin; arm64)" + qwenUserAgent = "QwenCode/0.10.3 (darwin; arm64)" + qwenRateLimitPerMin = 60 // 60 requests per minute per credential + qwenRateLimitWindow = time.Minute // sliding window duration ) +// qwenBeijingLoc caches the Beijing timezone to avoid repeated LoadLocation syscalls. +var qwenBeijingLoc = func() *time.Location { + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil || loc == nil { + log.Warnf("qwen: failed to load Asia/Shanghai timezone: %v, using fixed UTC+8", err) + return time.FixedZone("CST", 8*3600) + } + return loc +}() + +// qwenQuotaCodes is a package-level set of error codes that indicate quota exhaustion. +var qwenQuotaCodes = map[string]struct{}{ + "insufficient_quota": {}, + "quota_exceeded": {}, +} + +// qwenRateLimiter tracks request timestamps per credential for rate limiting. +// Qwen has a limit of 60 requests per minute per account. +var qwenRateLimiter = struct { + sync.Mutex + requests map[string][]time.Time // authID -> request timestamps +}{ + requests: make(map[string][]time.Time), +} + +// redactAuthID returns a redacted version of the auth ID for safe logging. +// Keeps a small prefix/suffix to allow correlation across events. +func redactAuthID(id string) string { + if id == "" { + return "" + } + if len(id) <= 8 { + return id + } + return id[:4] + "..." + id[len(id)-4:] +} + +// checkQwenRateLimit checks if the credential has exceeded the rate limit. +// Returns nil if allowed, or a statusErr with retryAfter if rate limited. +func checkQwenRateLimit(authID string) error { + if authID == "" { + // Empty authID should not bypass rate limiting in production + // Use debug level to avoid log spam for certain auth flows + log.Debug("qwen rate limit check: empty authID, skipping rate limit") + return nil + } + + now := time.Now() + windowStart := now.Add(-qwenRateLimitWindow) + + qwenRateLimiter.Lock() + defer qwenRateLimiter.Unlock() + + // Get and filter timestamps within the window + timestamps := qwenRateLimiter.requests[authID] + var validTimestamps []time.Time + for _, ts := range timestamps { + if ts.After(windowStart) { + validTimestamps = append(validTimestamps, ts) + } + } + + // Always prune expired entries to prevent memory leak + // Delete empty entries, otherwise update with pruned slice + if len(validTimestamps) == 0 { + delete(qwenRateLimiter.requests, authID) + } + + // Check if rate limit exceeded + if len(validTimestamps) >= qwenRateLimitPerMin { + // Calculate when the oldest request will expire + oldestInWindow := validTimestamps[0] + retryAfter := oldestInWindow.Add(qwenRateLimitWindow).Sub(now) + if retryAfter < time.Second { + retryAfter = time.Second + } + retryAfterSec := int(retryAfter.Seconds()) + return statusErr{ + code: http.StatusTooManyRequests, + msg: fmt.Sprintf(`{"error":{"code":"rate_limit_exceeded","message":"Qwen rate limit: %d requests/minute exceeded, retry after %ds","type":"rate_limit_exceeded"}}`, qwenRateLimitPerMin, retryAfterSec), + retryAfter: &retryAfter, + } + } + + // Record this request and update the map with pruned timestamps + validTimestamps = append(validTimestamps, now) + qwenRateLimiter.requests[authID] = validTimestamps + + return nil +} + +// isQwenQuotaError checks if the error response indicates a quota exceeded error. +// Qwen returns HTTP 403 with error.code="insufficient_quota" when daily quota is exhausted. +func isQwenQuotaError(body []byte) bool { + code := strings.ToLower(gjson.GetBytes(body, "error.code").String()) + errType := strings.ToLower(gjson.GetBytes(body, "error.type").String()) + + // Primary check: exact match on error.code or error.type (most reliable) + if _, ok := qwenQuotaCodes[code]; ok { + return true + } + if _, ok := qwenQuotaCodes[errType]; ok { + return true + } + + // Fallback: check message only if code/type don't match (less reliable) + msg := strings.ToLower(gjson.GetBytes(body, "error.message").String()) + if strings.Contains(msg, "insufficient_quota") || strings.Contains(msg, "quota exceeded") || + strings.Contains(msg, "free allocated quota exceeded") { + return true + } + + return false +} + +// wrapQwenError wraps an HTTP error response, detecting quota errors and mapping them to 429. +// Returns the appropriate status code and retryAfter duration for statusErr. +// Only checks for quota errors when httpCode is 403 or 429 to avoid false positives. +func wrapQwenError(ctx context.Context, httpCode int, body []byte) (errCode int, retryAfter *time.Duration) { + errCode = httpCode + // Only check quota errors for expected status codes to avoid false positives + // Qwen returns 403 for quota errors, 429 for rate limits + if (httpCode == http.StatusForbidden || httpCode == http.StatusTooManyRequests) && isQwenQuotaError(body) { + errCode = http.StatusTooManyRequests // Map to 429 to trigger quota logic + cooldown := timeUntilNextDay() + retryAfter = &cooldown + logWithRequestID(ctx).Warnf("qwen quota exceeded (http %d -> %d), cooling down until tomorrow (%v)", httpCode, errCode, cooldown) + } + return errCode, retryAfter +} + +// timeUntilNextDay returns duration until midnight Beijing time (UTC+8). +// Qwen's daily quota resets at 00:00 Beijing time. +func timeUntilNextDay() time.Duration { + now := time.Now() + nowLocal := now.In(qwenBeijingLoc) + tomorrow := time.Date(nowLocal.Year(), nowLocal.Month(), nowLocal.Day()+1, 0, 0, 0, 0, qwenBeijingLoc) + return tomorrow.Sub(now) +} + // QwenExecutor is a stateless executor for Qwen Code using OpenAI-compatible chat completions. // If access token is unavailable, it falls back to legacy via ClientAdapter. type QwenExecutor struct { @@ -67,6 +210,17 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req if opts.Alt == "responses/compact" { return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"} } + + // Check rate limit before proceeding + var authID string + if auth != nil { + authID = auth.ID + } + if err := checkQwenRateLimit(authID); err != nil { + logWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID)) + return resp, err + } + baseModel := thinking.ParseSuffix(req.Model).ModelName token, baseURL := qwenCreds(auth) @@ -102,9 +256,8 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req return resp, err } applyQwenHeaders(httpReq, token, false) - var authID, authLabel, authType, authValue string + var authLabel, authType, authValue string if auth != nil { - authID = auth.ID authLabel = auth.Label authType, authValue = auth.AccountInfo() } @@ -135,8 +288,10 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { b, _ := io.ReadAll(httpResp.Body) appendAPIResponseChunk(ctx, e.cfg, b) - logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) - err = statusErr{code: httpResp.StatusCode, msg: string(b)} + + errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b) + logWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) + err = statusErr{code: errCode, msg: string(b), retryAfter: retryAfter} return resp, err } data, err := io.ReadAll(httpResp.Body) @@ -158,6 +313,17 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut if opts.Alt == "responses/compact" { return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"} } + + // Check rate limit before proceeding + var authID string + if auth != nil { + authID = auth.ID + } + if err := checkQwenRateLimit(authID); err != nil { + logWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID)) + return nil, err + } + baseModel := thinking.ParseSuffix(req.Model).ModelName token, baseURL := qwenCreds(auth) @@ -200,9 +366,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut return nil, err } applyQwenHeaders(httpReq, token, true) - var authID, authLabel, authType, authValue string + var authLabel, authType, authValue string if auth != nil { - authID = auth.ID authLabel = auth.Label authType, authValue = auth.AccountInfo() } @@ -228,11 +393,13 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { b, _ := io.ReadAll(httpResp.Body) appendAPIResponseChunk(ctx, e.cfg, b) - logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) + + errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b) + logWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("qwen executor: close response body error: %v", errClose) } - err = statusErr{code: httpResp.StatusCode, msg: string(b)} + err = statusErr{code: errCode, msg: string(b), retryAfter: retryAfter} return nil, err } out := make(chan cliproxyexecutor.StreamChunk) From 450d1227bdab7c2a41007b2dae9d8e7f6ab04a90 Mon Sep 17 00:00:00 2001 From: lyd123qw2008 <326643467@qq.com> Date: Mon, 23 Feb 2026 22:07:50 +0800 Subject: [PATCH 04/10] fix(auth): respect configured auto-refresh interval --- sdk/cliproxy/auth/conductor.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index cd447e68d4..028b70c153 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -1828,9 +1828,7 @@ func (m *Manager) persist(ctx context.Context, auth *Auth) error { // every few seconds and triggers refresh operations when required. // Only one loop is kept alive; starting a new one cancels the previous run. func (m *Manager) StartAutoRefresh(parent context.Context, interval time.Duration) { - if interval <= 0 || interval > refreshCheckInterval { - interval = refreshCheckInterval - } else { + if interval <= 0 { interval = refreshCheckInterval } if m.refreshCancel != nil { From 7acd428507a413850ccda7a029e815650f0c94cf Mon Sep 17 00:00:00 2001 From: lyd123qw2008 <326643467@qq.com> Date: Mon, 23 Feb 2026 22:31:30 +0800 Subject: [PATCH 05/10] fix(codex): stop retrying refresh_token_reused errors --- internal/auth/codex/openai_auth.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/auth/codex/openai_auth.go b/internal/auth/codex/openai_auth.go index 89deeadb6e..b3620b8ac9 100644 --- a/internal/auth/codex/openai_auth.go +++ b/internal/auth/codex/openai_auth.go @@ -266,6 +266,9 @@ func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken str if err == nil { return tokenData, nil } + if isNonRetryableRefreshErr(err) { + return nil, err + } lastErr = err log.Warnf("Token refresh attempt %d failed: %v", attempt+1, err) @@ -274,6 +277,14 @@ func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken str return nil, fmt.Errorf("token refresh failed after %d attempts: %w", maxRetries, lastErr) } +func isNonRetryableRefreshErr(err error) bool { + if err == nil { + return false + } + raw := strings.ToLower(err.Error()) + return strings.Contains(raw, "refresh_token_reused") +} + // UpdateTokenStorage updates an existing CodexTokenStorage with new token data. // This is typically called after a successful token refresh to persist the new credentials. func (o *CodexAuth) UpdateTokenStorage(storage *CodexTokenStorage, tokenData *CodexTokenData) { From 3b3e0d1141c1f9e8d3813181bf47f225175d347b Mon Sep 17 00:00:00 2001 From: lyd123qw2008 <326643467@qq.com> Date: Mon, 23 Feb 2026 22:41:33 +0800 Subject: [PATCH 06/10] test(codex): log non-retryable refresh error and cover single-attempt behavior --- internal/auth/codex/openai_auth.go | 1 + internal/auth/codex/openai_auth_test.go | 44 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 internal/auth/codex/openai_auth_test.go diff --git a/internal/auth/codex/openai_auth.go b/internal/auth/codex/openai_auth.go index b3620b8ac9..8c32f3eb25 100644 --- a/internal/auth/codex/openai_auth.go +++ b/internal/auth/codex/openai_auth.go @@ -267,6 +267,7 @@ func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken str return tokenData, nil } if isNonRetryableRefreshErr(err) { + log.Warnf("Token refresh attempt %d failed with non-retryable error: %v", attempt+1, err) return nil, err } diff --git a/internal/auth/codex/openai_auth_test.go b/internal/auth/codex/openai_auth_test.go new file mode 100644 index 0000000000..3327eb4ab5 --- /dev/null +++ b/internal/auth/codex/openai_auth_test.go @@ -0,0 +1,44 @@ +package codex + +import ( + "context" + "io" + "net/http" + "strings" + "sync/atomic" + "testing" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestRefreshTokensWithRetry_NonRetryableOnlyAttemptsOnce(t *testing.T) { + var calls int32 + auth := &CodexAuth{ + httpClient: &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + atomic.AddInt32(&calls, 1) + return &http.Response{ + StatusCode: http.StatusBadRequest, + Body: io.NopCloser(strings.NewReader(`{"error":"invalid_grant","code":"refresh_token_reused"}`)), + Header: make(http.Header), + Request: req, + }, nil + }), + }, + } + + _, err := auth.RefreshTokensWithRetry(context.Background(), "dummy_refresh_token", 3) + if err == nil { + t.Fatalf("expected error for non-retryable refresh failure") + } + if !strings.Contains(strings.ToLower(err.Error()), "refresh_token_reused") { + t.Fatalf("expected refresh_token_reused in error, got: %v", err) + } + if got := atomic.LoadInt32(&calls); got != 1 { + t.Fatalf("expected 1 refresh attempt, got %d", got) + } +} From 43e531a3b6b7481a132d5c8c9e39952b4027e576 Mon Sep 17 00:00:00 2001 From: Howard Dong Date: Wed, 25 Feb 2026 17:09:40 +0800 Subject: [PATCH 07/10] feat(copilot): fetch and persist user email and display name on login - Expand OAuth scope to include read:user for full profile access - Add GitHubUserInfo struct with Login, Email, Name fields - Update FetchUserInfo to return complete user profile - Add Email and Name fields to CopilotTokenStorage and CopilotAuthBundle - Fix provider string bug: 'github' -> 'github-copilot' in auth_files.go - Fix semantic bug: email field was storing username - Update Label to prefer email over username in both CLI and Web API paths - Add 9 unit tests covering new functionality --- .../api/handlers/management/auth_files.go | 24 +- internal/auth/copilot/copilot_auth.go | 13 +- internal/auth/copilot/oauth.go | 42 ++-- internal/auth/copilot/oauth_test.go | 213 ++++++++++++++++++ internal/auth/copilot/token.go | 8 + sdk/auth/github_copilot.go | 9 +- 6 files changed, 283 insertions(+), 26 deletions(-) create mode 100644 internal/auth/copilot/oauth_test.go diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index d090049282..342868bcd6 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -1929,8 +1929,6 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) { state := fmt.Sprintf("gh-%d", time.Now().UnixNano()) // Initialize Copilot auth service - // We need to import "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot" first if not present - // Assuming copilot package is imported as "copilot" deviceClient := copilot.NewDeviceFlowClient(h.cfg) // Initiate device flow @@ -1944,7 +1942,7 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) { authURL := deviceCode.VerificationURI userCode := deviceCode.UserCode - RegisterOAuthSession(state, "github") + RegisterOAuthSession(state, "github-copilot") go func() { fmt.Printf("Please visit %s and enter code: %s\n", authURL, userCode) @@ -1956,9 +1954,13 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) { return } - username, errUser := deviceClient.FetchUserInfo(ctx, tokenData.AccessToken) + userInfo, errUser := deviceClient.FetchUserInfo(ctx, tokenData.AccessToken) if errUser != nil { log.Warnf("Failed to fetch user info: %v", errUser) + } + + username := userInfo.Login + if username == "" { username = "github-user" } @@ -1967,18 +1969,26 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) { TokenType: tokenData.TokenType, Scope: tokenData.Scope, Username: username, + Email: userInfo.Email, + Name: userInfo.Name, Type: "github-copilot", } fileName := fmt.Sprintf("github-%s.json", username) + label := userInfo.Email + if label == "" { + label = username + } record := &coreauth.Auth{ ID: fileName, - Provider: "github", + Provider: "github-copilot", + Label: label, FileName: fileName, Storage: tokenStorage, Metadata: map[string]any{ - "email": username, + "email": userInfo.Email, "username": username, + "name": userInfo.Name, }, } @@ -1992,7 +2002,7 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) { fmt.Printf("Authentication successful! Token saved to %s\n", savedPath) fmt.Println("You can now use GitHub Copilot services through this CLI") CompleteOAuthSession(state) - CompleteOAuthSessionsByProvider("github") + CompleteOAuthSessionsByProvider("github-copilot") }() c.JSON(200, gin.H{ diff --git a/internal/auth/copilot/copilot_auth.go b/internal/auth/copilot/copilot_auth.go index c40e7082b8..d702583ee9 100644 --- a/internal/auth/copilot/copilot_auth.go +++ b/internal/auth/copilot/copilot_auth.go @@ -82,15 +82,16 @@ func (c *CopilotAuth) WaitForAuthorization(ctx context.Context, deviceCode *Devi } // Fetch the GitHub username - username, err := c.deviceClient.FetchUserInfo(ctx, tokenData.AccessToken) + userInfo, err := c.deviceClient.FetchUserInfo(ctx, tokenData.AccessToken) if err != nil { log.Warnf("copilot: failed to fetch user info: %v", err) - username = "unknown" } return &CopilotAuthBundle{ TokenData: tokenData, - Username: username, + Username: userInfo.Login, + Email: userInfo.Email, + Name: userInfo.Name, }, nil } @@ -150,12 +151,12 @@ func (c *CopilotAuth) ValidateToken(ctx context.Context, accessToken string) (bo return false, "", nil } - username, err := c.deviceClient.FetchUserInfo(ctx, accessToken) + userInfo, err := c.deviceClient.FetchUserInfo(ctx, accessToken) if err != nil { return false, "", err } - return true, username, nil + return true, userInfo.Login, nil } // CreateTokenStorage creates a new CopilotTokenStorage from auth bundle. @@ -165,6 +166,8 @@ func (c *CopilotAuth) CreateTokenStorage(bundle *CopilotAuthBundle) *CopilotToke TokenType: bundle.TokenData.TokenType, Scope: bundle.TokenData.Scope, Username: bundle.Username, + Email: bundle.Email, + Name: bundle.Name, Type: "github-copilot", } } diff --git a/internal/auth/copilot/oauth.go b/internal/auth/copilot/oauth.go index d3f46aaa10..c2fe52cb2f 100644 --- a/internal/auth/copilot/oauth.go +++ b/internal/auth/copilot/oauth.go @@ -53,7 +53,7 @@ func NewDeviceFlowClient(cfg *config.Config) *DeviceFlowClient { func (c *DeviceFlowClient) RequestDeviceCode(ctx context.Context) (*DeviceCodeResponse, error) { data := url.Values{} data.Set("client_id", copilotClientID) - data.Set("scope", "user:email") + data.Set("scope", "read:user user:email") req, err := http.NewRequestWithContext(ctx, http.MethodPost, copilotDeviceCodeURL, strings.NewReader(data.Encode())) if err != nil { @@ -211,15 +211,25 @@ func (c *DeviceFlowClient) exchangeDeviceCode(ctx context.Context, deviceCode st }, nil } -// FetchUserInfo retrieves the GitHub username for the authenticated user. -func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string) (string, error) { +// GitHubUserInfo holds GitHub user profile information. +type GitHubUserInfo struct { + // Login is the GitHub username. + Login string + // Email is the primary email address (may be empty if not public). + Email string + // Name is the display name. + Name string +} + +// FetchUserInfo retrieves the GitHub user profile for the authenticated user. +func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string) (GitHubUserInfo, error) { if accessToken == "" { - return "", NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("access token is empty")) + return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("access token is empty")) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, copilotUserInfoURL, nil) if err != nil { - return "", NewAuthenticationError(ErrUserInfoFailed, err) + return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, err) } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Accept", "application/json") @@ -227,7 +237,7 @@ func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string resp, err := c.httpClient.Do(req) if err != nil { - return "", NewAuthenticationError(ErrUserInfoFailed, err) + return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, err) } defer func() { if errClose := resp.Body.Close(); errClose != nil { @@ -237,19 +247,25 @@ func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string if !isHTTPSuccess(resp.StatusCode) { bodyBytes, _ := io.ReadAll(resp.Body) - return "", NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("status %d: %s", resp.StatusCode, string(bodyBytes))) + return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("status %d: %s", resp.StatusCode, string(bodyBytes))) } - var userInfo struct { + var raw struct { Login string `json:"login"` + Email string `json:"email"` + Name string `json:"name"` } - if err = json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { - return "", NewAuthenticationError(ErrUserInfoFailed, err) + if err = json.NewDecoder(resp.Body).Decode(&raw); err != nil { + return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, err) } - if userInfo.Login == "" { - return "", NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("empty username")) + if raw.Login == "" { + return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("empty username")) } - return userInfo.Login, nil + return GitHubUserInfo{ + Login: raw.Login, + Email: raw.Email, + Name: raw.Name, + }, nil } diff --git a/internal/auth/copilot/oauth_test.go b/internal/auth/copilot/oauth_test.go new file mode 100644 index 0000000000..3311b4f850 --- /dev/null +++ b/internal/auth/copilot/oauth_test.go @@ -0,0 +1,213 @@ +package copilot + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// roundTripFunc lets us inject a custom transport for testing. +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +// newTestClient returns an *http.Client whose requests are redirected to the given test server, +// regardless of the original URL host. +func newTestClient(srv *httptest.Server) *http.Client { + return &http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + req2 := req.Clone(req.Context()) + req2.URL.Scheme = "http" + req2.URL.Host = strings.TrimPrefix(srv.URL, "http://") + return srv.Client().Transport.RoundTrip(req2) + }), + } +} + +// TestFetchUserInfo_FullProfile verifies that FetchUserInfo returns login, email, and name. +func TestFetchUserInfo_FullProfile(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{ + "login": "octocat", + "email": "octocat@github.com", + "name": "The Octocat", + }) + })) + defer srv.Close() + + client := &DeviceFlowClient{httpClient: newTestClient(srv)} + info, err := client.FetchUserInfo(context.Background(), "test-token") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if info.Login != "octocat" { + t.Errorf("Login: got %q, want %q", info.Login, "octocat") + } + if info.Email != "octocat@github.com" { + t.Errorf("Email: got %q, want %q", info.Email, "octocat@github.com") + } + if info.Name != "The Octocat" { + t.Errorf("Name: got %q, want %q", info.Name, "The Octocat") + } +} + +// TestFetchUserInfo_EmptyEmail verifies graceful handling when email is absent (private account). +func TestFetchUserInfo_EmptyEmail(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + // GitHub returns null for private emails. + _, _ = w.Write([]byte(`{"login":"privateuser","email":null,"name":"Private User"}`)) + })) + defer srv.Close() + + client := &DeviceFlowClient{httpClient: newTestClient(srv)} + info, err := client.FetchUserInfo(context.Background(), "test-token") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if info.Login != "privateuser" { + t.Errorf("Login: got %q, want %q", info.Login, "privateuser") + } + if info.Email != "" { + t.Errorf("Email: got %q, want empty string", info.Email) + } + if info.Name != "Private User" { + t.Errorf("Name: got %q, want %q", info.Name, "Private User") + } +} + +// TestFetchUserInfo_EmptyToken verifies error is returned for empty access token. +func TestFetchUserInfo_EmptyToken(t *testing.T) { + client := &DeviceFlowClient{httpClient: http.DefaultClient} + _, err := client.FetchUserInfo(context.Background(), "") + if err == nil { + t.Fatal("expected error for empty token, got nil") + } +} + +// TestFetchUserInfo_EmptyLogin verifies error is returned when API returns no login. +func TestFetchUserInfo_EmptyLogin(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"email":"someone@example.com","name":"No Login"}`)) + })) + defer srv.Close() + + client := &DeviceFlowClient{httpClient: newTestClient(srv)} + _, err := client.FetchUserInfo(context.Background(), "test-token") + if err == nil { + t.Fatal("expected error for empty login, got nil") + } +} + +// TestFetchUserInfo_HTTPError verifies error is returned on non-2xx response. +func TestFetchUserInfo_HTTPError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message":"Bad credentials"}`)) + })) + defer srv.Close() + + client := &DeviceFlowClient{httpClient: newTestClient(srv)} + _, err := client.FetchUserInfo(context.Background(), "bad-token") + if err == nil { + t.Fatal("expected error for 401 response, got nil") + } +} + +// TestCopilotTokenStorage_EmailNameFields verifies Email and Name serialise correctly. +func TestCopilotTokenStorage_EmailNameFields(t *testing.T) { + ts := &CopilotTokenStorage{ + AccessToken: "ghu_abc", + TokenType: "bearer", + Scope: "read:user user:email", + Username: "octocat", + Email: "octocat@github.com", + Name: "The Octocat", + Type: "github-copilot", + } + + data, err := json.Marshal(ts) + if err != nil { + t.Fatalf("marshal error: %v", err) + } + + var out map[string]any + if err = json.Unmarshal(data, &out); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + + for _, key := range []string{"access_token", "username", "email", "name", "type"} { + if _, ok := out[key]; !ok { + t.Errorf("expected key %q in JSON output, not found", key) + } + } + if out["email"] != "octocat@github.com" { + t.Errorf("email: got %v, want %q", out["email"], "octocat@github.com") + } + if out["name"] != "The Octocat" { + t.Errorf("name: got %v, want %q", out["name"], "The Octocat") + } +} + +// TestCopilotTokenStorage_OmitEmptyEmailName verifies email/name are omitted when empty (omitempty). +func TestCopilotTokenStorage_OmitEmptyEmailName(t *testing.T) { + ts := &CopilotTokenStorage{ + AccessToken: "ghu_abc", + Username: "octocat", + Type: "github-copilot", + } + + data, err := json.Marshal(ts) + if err != nil { + t.Fatalf("marshal error: %v", err) + } + + var out map[string]any + if err = json.Unmarshal(data, &out); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + + if _, ok := out["email"]; ok { + t.Error("email key should be omitted when empty (omitempty), but was present") + } + if _, ok := out["name"]; ok { + t.Error("name key should be omitted when empty (omitempty), but was present") + } +} + +// TestCopilotAuthBundle_EmailNameFields verifies bundle carries email and name through the pipeline. +func TestCopilotAuthBundle_EmailNameFields(t *testing.T) { + bundle := &CopilotAuthBundle{ + TokenData: &CopilotTokenData{AccessToken: "ghu_abc"}, + Username: "octocat", + Email: "octocat@github.com", + Name: "The Octocat", + } + if bundle.Email != "octocat@github.com" { + t.Errorf("bundle.Email: got %q, want %q", bundle.Email, "octocat@github.com") + } + if bundle.Name != "The Octocat" { + t.Errorf("bundle.Name: got %q, want %q", bundle.Name, "The Octocat") + } +} + +// TestGitHubUserInfo_Struct verifies the exported GitHubUserInfo struct fields are accessible. +func TestGitHubUserInfo_Struct(t *testing.T) { + info := GitHubUserInfo{ + Login: "octocat", + Email: "octocat@github.com", + Name: "The Octocat", + } + if info.Login == "" || info.Email == "" || info.Name == "" { + t.Error("GitHubUserInfo fields should not be empty") + } +} diff --git a/internal/auth/copilot/token.go b/internal/auth/copilot/token.go index 4e5eed6c45..aa7ea94907 100644 --- a/internal/auth/copilot/token.go +++ b/internal/auth/copilot/token.go @@ -26,6 +26,10 @@ type CopilotTokenStorage struct { ExpiresAt string `json:"expires_at,omitempty"` // Username is the GitHub username associated with this token. Username string `json:"username"` + // Email is the GitHub email address associated with this token. + Email string `json:"email,omitempty"` + // Name is the GitHub display name associated with this token. + Name string `json:"name,omitempty"` // Type indicates the authentication provider type, always "github-copilot" for this storage. Type string `json:"type"` } @@ -46,6 +50,10 @@ type CopilotAuthBundle struct { TokenData *CopilotTokenData // Username is the GitHub username. Username string + // Email is the GitHub email address. + Email string + // Name is the GitHub display name. + Name string } // DeviceCodeResponse represents GitHub's device code response. diff --git a/sdk/auth/github_copilot.go b/sdk/auth/github_copilot.go index 1d14ac4751..c2d2f14e2c 100644 --- a/sdk/auth/github_copilot.go +++ b/sdk/auth/github_copilot.go @@ -86,6 +86,8 @@ func (a GitHubCopilotAuthenticator) Login(ctx context.Context, cfg *config.Confi metadata := map[string]any{ "type": "github-copilot", "username": authBundle.Username, + "email": authBundle.Email, + "name": authBundle.Name, "access_token": authBundle.TokenData.AccessToken, "token_type": authBundle.TokenData.TokenType, "scope": authBundle.TokenData.Scope, @@ -98,13 +100,18 @@ func (a GitHubCopilotAuthenticator) Login(ctx context.Context, cfg *config.Confi fileName := fmt.Sprintf("github-copilot-%s.json", authBundle.Username) + label := authBundle.Email + if label == "" { + label = authBundle.Username + } + fmt.Printf("\nGitHub Copilot authentication successful for user: %s\n", authBundle.Username) return &coreauth.Auth{ ID: fileName, Provider: a.Provider(), FileName: fileName, - Label: authBundle.Username, + Label: label, Storage: tokenStorage, Metadata: metadata, }, nil From fc346f4537070feba80615a3f7bb39147b7ce29c Mon Sep 17 00:00:00 2001 From: Howard Dong Date: Wed, 25 Feb 2026 17:17:51 +0800 Subject: [PATCH 08/10] fix(copilot): add username fallback and consistent file name prefix - Add 'github-user' fallback in WaitForAuthorization when FetchUserInfo returns empty Login (fixes malformed 'github-copilot-.json' filenames) - Standardize Web API file name to 'github-copilot-.json' to match CLI path convention (was 'github-.json') Addresses Gemini Code Assist review comments on PR #291. --- internal/api/handlers/management/auth_files.go | 2 +- internal/auth/copilot/copilot_auth.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 342868bcd6..3794793c58 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -1974,7 +1974,7 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) { Type: "github-copilot", } - fileName := fmt.Sprintf("github-%s.json", username) + fileName := fmt.Sprintf("github-copilot-%s.json", username) label := userInfo.Email if label == "" { label = username diff --git a/internal/auth/copilot/copilot_auth.go b/internal/auth/copilot/copilot_auth.go index d702583ee9..5776648c52 100644 --- a/internal/auth/copilot/copilot_auth.go +++ b/internal/auth/copilot/copilot_auth.go @@ -87,9 +87,14 @@ func (c *CopilotAuth) WaitForAuthorization(ctx context.Context, deviceCode *Devi log.Warnf("copilot: failed to fetch user info: %v", err) } + username := userInfo.Login + if username == "" { + username = "github-user" + } + return &CopilotAuthBundle{ TokenData: tokenData, - Username: userInfo.Login, + Username: username, Email: userInfo.Email, Name: userInfo.Name, }, nil From ae4484af040156a97b5cb9b9cc1e888ab6c481b2 Mon Sep 17 00:00:00 2001 From: Koosha Paridehpour Date: Wed, 25 Feb 2026 06:24:49 -0700 Subject: [PATCH 09/10] Strip empty messages on translation from openai to claude Cherry-picked from merge/1698-strip-empty-messages-openai-to-claude into aligned base --- .../openai/chat-completions/claude_openai_request.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go index f94825b2a0..1cde776629 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -156,8 +156,12 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream } else if contentResult.Exists() && contentResult.IsArray() { contentResult.ForEach(func(_, part gjson.Result) bool { if part.Get("type").String() == "text" { + textContent := part.Get("text").String() + if textContent == "" { + return true + } textPart := `{"type":"text","text":""}` - textPart, _ = sjson.Set(textPart, "text", part.Get("text").String()) + textPart, _ = sjson.Set(textPart, "text", textContent) out, _ = sjson.SetRaw(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart) } return true @@ -178,8 +182,12 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream switch partType { case "text": + textContent := part.Get("text").String() + if textContent == "" { + return true + } textPart := `{"type":"text","text":""}` - textPart, _ = sjson.Set(textPart, "text", part.Get("text").String()) + textPart, _ = sjson.Set(textPart, "text", textContent) msg, _ = sjson.SetRaw(msg, "content.-1", textPart) case "image_url": From 62780706e5c2bf0c492862be3e8c9d161814fd35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:04:28 +0000 Subject: [PATCH 10/10] chore(deps): bump github.com/cloudflare/circl Bumps the go_modules group with 1 update in the / directory: [github.com/cloudflare/circl](https://github.com/cloudflare/circl). Updates `github.com/cloudflare/circl` from 1.6.1 to 1.6.3 - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.6.1...v1.6.3) --- updated-dependencies: - dependency-name: github.com/cloudflare/circl dependency-version: 1.6.3 dependency-type: indirect dependency-group: go_modules ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 461d5517d7..4fb76c08c7 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect @@ -91,8 +91,8 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect diff --git a/go.sum b/go.sum index 8a4a967d9a..52812f7387 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -201,10 +201,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=