From 8d5e470e1fc0661a9434ca85c34eda90a5631e8c Mon Sep 17 00:00:00 2001 From: rensumo Date: Sat, 4 Apr 2026 14:52:59 +0800 Subject: [PATCH 1/5] feat: dynamically fetch antigravity UA version from releases API Fetch the latest version from the antigravity auto-updater releases endpoint and cache it for 6 hours. Falls back to 1.21.9 if the API is unreachable or returns unexpected data. --- cmd/fetch_antigravity_models/main.go | 3 +- internal/misc/antigravity_version.go | 97 +++++++++++++++++++ .../runtime/executor/antigravity_executor.go | 5 +- 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 internal/misc/antigravity_version.go diff --git a/cmd/fetch_antigravity_models/main.go b/cmd/fetch_antigravity_models/main.go index 54ec16ca..d4328eb3 100644 --- a/cmd/fetch_antigravity_models/main.go +++ b/cmd/fetch_antigravity_models/main.go @@ -26,6 +26,7 @@ import ( "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" + "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" sdkauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" @@ -188,7 +189,7 @@ func fetchModels(ctx context.Context, auth *coreauth.Auth) []modelEntry { httpReq.Close = true httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+accessToken) - httpReq.Header.Set("User-Agent", "antigravity/1.21.9 darwin/arm64") + httpReq.Header.Set("User-Agent", misc.AntigravityUserAgent()) httpClient := &http.Client{Timeout: 30 * time.Second} if transport, _, errProxy := proxyutil.BuildHTTPTransport(auth.ProxyURL); errProxy == nil && transport != nil { diff --git a/internal/misc/antigravity_version.go b/internal/misc/antigravity_version.go new file mode 100644 index 00000000..c882269f --- /dev/null +++ b/internal/misc/antigravity_version.go @@ -0,0 +1,97 @@ +// Package misc provides miscellaneous utility functions for the CLI Proxy API server. +package misc + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + antigravityReleasesURL = "https://antigravity-auto-updater-974169037036.us-central1.run.app/releases" + antigravityFallbackVersion = "1.21.9" + antigravityVersionCacheTTL = 6 * time.Hour + antigravityFetchTimeout = 10 * time.Second +) + +type antigravityRelease struct { + Version string `json:"version"` + ExecutionID string `json:"execution_id"` +} + +var ( + cachedAntigravityVersion string + antigravityVersionMu sync.RWMutex + antigravityVersionExpiry time.Time +) + +// AntigravityLatestVersion returns the latest antigravity version from the releases API. +// It caches the result for antigravityVersionCacheTTL and falls back to antigravityFallbackVersion +// if the fetch fails. +func AntigravityLatestVersion() string { + antigravityVersionMu.RLock() + if cachedAntigravityVersion != "" && time.Now().Before(antigravityVersionExpiry) { + v := cachedAntigravityVersion + antigravityVersionMu.RUnlock() + return v + } + antigravityVersionMu.RUnlock() + + antigravityVersionMu.Lock() + defer antigravityVersionMu.Unlock() + + // Double-check after acquiring write lock. + if cachedAntigravityVersion != "" && time.Now().Before(antigravityVersionExpiry) { + return cachedAntigravityVersion + } + + version := fetchAntigravityLatestVersion() + cachedAntigravityVersion = version + antigravityVersionExpiry = time.Now().Add(antigravityVersionCacheTTL) + return version +} + +// AntigravityUserAgent returns the User-Agent string for antigravity requests +// using the latest version fetched from the releases API. +func AntigravityUserAgent() string { + return fmt.Sprintf("antigravity/%s darwin/arm64", AntigravityLatestVersion()) +} + +func fetchAntigravityLatestVersion() string { + client := &http.Client{Timeout: antigravityFetchTimeout} + resp, err := client.Get(antigravityReleasesURL) + if err != nil { + log.WithError(err).Warn("failed to fetch antigravity releases, using fallback version") + return antigravityFallbackVersion + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.WithField("status", resp.StatusCode).Warn("antigravity releases API returned non-200, using fallback version") + return antigravityFallbackVersion + } + + var releases []antigravityRelease + if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { + log.WithError(err).Warn("failed to decode antigravity releases response, using fallback version") + return antigravityFallbackVersion + } + + if len(releases) == 0 { + log.Warn("antigravity releases API returned empty list, using fallback version") + return antigravityFallbackVersion + } + + version := releases[0].Version + if version == "" { + log.Warn("antigravity releases API returned empty version, using fallback version") + return antigravityFallbackVersion + } + + log.WithField("version", version).Info("fetched latest antigravity version") + return version +} diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index b9bf4842..ecab3c87 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -24,6 +24,7 @@ import ( "github.com/google/uuid" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" @@ -45,7 +46,7 @@ const ( antigravityGeneratePath = "/v1internal:generateContent" antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" antigravityClientSecret = "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" - defaultAntigravityAgent = "antigravity/1.21.9 darwin/arm64" + defaultAntigravityAgent = "antigravity/1.21.9 darwin/arm64" // fallback only; overridden at runtime by misc.AntigravityUserAgent() antigravityAuthType = "antigravity" refreshSkew = 3000 * time.Second antigravityCreditsRetryTTL = 5 * time.Hour @@ -1739,7 +1740,7 @@ func resolveUserAgent(auth *cliproxyauth.Auth) string { } } } - return defaultAntigravityAgent + return misc.AntigravityUserAgent() } func antigravityRetryAttempts(auth *cliproxyauth.Auth, cfg *config.Config) int { From c2d4137fb970250b07139d721725c329eba3a2c7 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sat, 4 Apr 2026 21:51:02 +0800 Subject: [PATCH 2/5] feat(executor): enhance Qwen system message handling with strict injection and merging rules Closes: #2537 --- internal/runtime/executor/qwen_executor.go | 109 +++++++++++++--- .../runtime/executor/qwen_executor_test.go | 121 ++++++++++++++++++ 2 files changed, 210 insertions(+), 20 deletions(-) diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index f771099c..d8eec537 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -172,32 +172,101 @@ func timeUntilNextDay() time.Duration { return tomorrow.Sub(now) } -// ensureQwenSystemMessage prepends a default system message if none exists in "messages". +// ensureQwenSystemMessage ensures the request has a single system message at the beginning. +// It always injects the default system prompt and merges any user-provided system messages +// into the injected system message content to satisfy Qwen's strict message ordering rules. func ensureQwenSystemMessage(payload []byte) ([]byte, error) { - messages := gjson.GetBytes(payload, "messages") - if messages.Exists() && messages.IsArray() { - var buf bytes.Buffer - buf.WriteByte('[') - buf.Write(qwenDefaultSystemMessage) - for _, msg := range messages.Array() { - buf.WriteByte(',') - buf.WriteString(msg.Raw) + isInjectedSystemPart := func(part gjson.Result) bool { + if !part.Exists() || !part.IsObject() { + return false } - buf.WriteByte(']') - updated, errSet := sjson.SetRawBytes(payload, "messages", buf.Bytes()) - if errSet != nil { - return nil, fmt.Errorf("qwen executor: set default system message failed: %w", errSet) + if !strings.EqualFold(part.Get("type").String(), "text") { + return false } - return updated, nil + if !strings.EqualFold(part.Get("cache_control.type").String(), "ephemeral") { + return false + } + text := part.Get("text").String() + return text == "" || text == "You are Qwen Code." } - var buf bytes.Buffer - buf.WriteByte('[') - buf.Write(qwenDefaultSystemMessage) - buf.WriteByte(']') - updated, errSet := sjson.SetRawBytes(payload, "messages", buf.Bytes()) + defaultParts := gjson.ParseBytes(qwenDefaultSystemMessage).Get("content") + var systemParts []any + if defaultParts.Exists() && defaultParts.IsArray() { + for _, part := range defaultParts.Array() { + systemParts = append(systemParts, part.Value()) + } + } + if len(systemParts) == 0 { + systemParts = append(systemParts, map[string]any{ + "type": "text", + "text": "You are Qwen Code.", + "cache_control": map[string]any{ + "type": "ephemeral", + }, + }) + } + + appendSystemContent := func(content gjson.Result) { + makeTextPart := func(text string) map[string]any { + return map[string]any{ + "type": "text", + "text": text, + } + } + + if !content.Exists() || content.Type == gjson.Null { + return + } + if content.IsArray() { + for _, part := range content.Array() { + if part.Type == gjson.String { + systemParts = append(systemParts, makeTextPart(part.String())) + continue + } + if isInjectedSystemPart(part) { + continue + } + systemParts = append(systemParts, part.Value()) + } + return + } + if content.Type == gjson.String { + systemParts = append(systemParts, makeTextPart(content.String())) + return + } + if content.IsObject() { + if isInjectedSystemPart(content) { + return + } + systemParts = append(systemParts, content.Value()) + return + } + systemParts = append(systemParts, makeTextPart(content.String())) + } + + messages := gjson.GetBytes(payload, "messages") + var nonSystemMessages []any + if messages.Exists() && messages.IsArray() { + for _, msg := range messages.Array() { + if strings.EqualFold(msg.Get("role").String(), "system") { + appendSystemContent(msg.Get("content")) + continue + } + nonSystemMessages = append(nonSystemMessages, msg.Value()) + } + } + + newMessages := make([]any, 0, 1+len(nonSystemMessages)) + newMessages = append(newMessages, map[string]any{ + "role": "system", + "content": systemParts, + }) + newMessages = append(newMessages, nonSystemMessages...) + + updated, errSet := sjson.SetBytes(payload, "messages", newMessages) if errSet != nil { - return nil, fmt.Errorf("qwen executor: set default system message failed: %w", errSet) + return nil, fmt.Errorf("qwen executor: set system message failed: %w", errSet) } return updated, nil } diff --git a/internal/runtime/executor/qwen_executor_test.go b/internal/runtime/executor/qwen_executor_test.go index 6a777c53..627cf453 100644 --- a/internal/runtime/executor/qwen_executor_test.go +++ b/internal/runtime/executor/qwen_executor_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + "github.com/tidwall/gjson" ) func TestQwenExecutorParseSuffix(t *testing.T) { @@ -28,3 +29,123 @@ func TestQwenExecutorParseSuffix(t *testing.T) { }) } } + +func TestEnsureQwenSystemMessage_MergeStringSystem(t *testing.T) { + payload := []byte(`{ + "model": "qwen3.6-plus", + "stream": true, + "messages": [ + { "role": "system", "content": "ABCDEFG" }, + { "role": "user", "content": [ { "type": "text", "text": "你好" } ] } + ] + }`) + + out, err := ensureQwenSystemMessage(payload) + if err != nil { + t.Fatalf("ensureQwenSystemMessage() error = %v", err) + } + + msgs := gjson.GetBytes(out, "messages").Array() + if len(msgs) != 2 { + t.Fatalf("messages length = %d, want 2", len(msgs)) + } + if msgs[0].Get("role").String() != "system" { + t.Fatalf("messages[0].role = %q, want %q", msgs[0].Get("role").String(), "system") + } + parts := msgs[0].Get("content").Array() + if len(parts) != 2 { + t.Fatalf("messages[0].content length = %d, want 2", len(parts)) + } + if parts[0].Get("text").String() != "You are Qwen Code." || parts[0].Get("cache_control.type").String() != "ephemeral" { + t.Fatalf("messages[0].content[0] = %s, want injected system part", parts[0].Raw) + } + if parts[1].Get("type").String() != "text" || parts[1].Get("text").String() != "ABCDEFG" { + t.Fatalf("messages[0].content[1] = %s, want text part with ABCDEFG", parts[1].Raw) + } + if msgs[1].Get("role").String() != "user" { + t.Fatalf("messages[1].role = %q, want %q", msgs[1].Get("role").String(), "user") + } +} + +func TestEnsureQwenSystemMessage_MergeObjectSystem(t *testing.T) { + payload := []byte(`{ + "messages": [ + { "role": "system", "content": { "type": "text", "text": "ABCDEFG" } }, + { "role": "user", "content": [ { "type": "text", "text": "你好" } ] } + ] + }`) + + out, err := ensureQwenSystemMessage(payload) + if err != nil { + t.Fatalf("ensureQwenSystemMessage() error = %v", err) + } + + msgs := gjson.GetBytes(out, "messages").Array() + if len(msgs) != 2 { + t.Fatalf("messages length = %d, want 2", len(msgs)) + } + parts := msgs[0].Get("content").Array() + if len(parts) != 2 { + t.Fatalf("messages[0].content length = %d, want 2", len(parts)) + } + if parts[1].Get("text").String() != "ABCDEFG" { + t.Fatalf("messages[0].content[1].text = %q, want %q", parts[1].Get("text").String(), "ABCDEFG") + } +} + +func TestEnsureQwenSystemMessage_PrependsWhenMissing(t *testing.T) { + payload := []byte(`{ + "messages": [ + { "role": "user", "content": [ { "type": "text", "text": "你好" } ] } + ] + }`) + + out, err := ensureQwenSystemMessage(payload) + if err != nil { + t.Fatalf("ensureQwenSystemMessage() error = %v", err) + } + + msgs := gjson.GetBytes(out, "messages").Array() + if len(msgs) != 2 { + t.Fatalf("messages length = %d, want 2", len(msgs)) + } + if msgs[0].Get("role").String() != "system" { + t.Fatalf("messages[0].role = %q, want %q", msgs[0].Get("role").String(), "system") + } + if !msgs[0].Get("content").IsArray() || len(msgs[0].Get("content").Array()) == 0 { + t.Fatalf("messages[0].content = %s, want non-empty array", msgs[0].Get("content").Raw) + } + if msgs[1].Get("role").String() != "user" { + t.Fatalf("messages[1].role = %q, want %q", msgs[1].Get("role").String(), "user") + } +} + +func TestEnsureQwenSystemMessage_MergesMultipleSystemMessages(t *testing.T) { + payload := []byte(`{ + "messages": [ + { "role": "system", "content": "A" }, + { "role": "user", "content": [ { "type": "text", "text": "hi" } ] }, + { "role": "system", "content": "B" } + ] + }`) + + out, err := ensureQwenSystemMessage(payload) + if err != nil { + t.Fatalf("ensureQwenSystemMessage() error = %v", err) + } + + msgs := gjson.GetBytes(out, "messages").Array() + if len(msgs) != 2 { + t.Fatalf("messages length = %d, want 2", len(msgs)) + } + parts := msgs[0].Get("content").Array() + if len(parts) != 3 { + t.Fatalf("messages[0].content length = %d, want 3", len(parts)) + } + if parts[1].Get("text").String() != "A" { + t.Fatalf("messages[0].content[1].text = %q, want %q", parts[1].Get("text").String(), "A") + } + if parts[2].Get("text").String() != "B" { + t.Fatalf("messages[0].content[2].text = %q, want %q", parts[2].Get("text").String(), "B") + } +} From 3774b56e9f9eda24bce4b80ae058364733d87178 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sat, 4 Apr 2026 22:09:11 +0800 Subject: [PATCH 3/5] feat(misc): add background updater for Antigravity version caching Introduce `StartAntigravityVersionUpdater` to periodically refresh the cached Antigravity version using a non-blocking background process. Updated main server flow to initialize the updater. --- cmd/server/main.go | 2 + internal/misc/antigravity_version.go | 122 +++++++++++++++++++-------- 2 files changed, 90 insertions(+), 34 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 8bc41c78..d63cfbd3 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -502,6 +502,7 @@ func main() { if standalone { // Standalone mode: start an embedded local server and connect TUI client to it. managementasset.StartAutoUpdater(context.Background(), configFilePath) + misc.StartAntigravityVersionUpdater(context.Background()) if !localModel { registry.StartModelsUpdater(context.Background()) } @@ -577,6 +578,7 @@ func main() { } else { // Start the main proxy service managementasset.StartAutoUpdater(context.Background(), configFilePath) + misc.StartAntigravityVersionUpdater(context.Background()) if !localModel { registry.StartModelsUpdater(context.Background()) } diff --git a/internal/misc/antigravity_version.go b/internal/misc/antigravity_version.go index c882269f..595cfefd 100644 --- a/internal/misc/antigravity_version.go +++ b/internal/misc/antigravity_version.go @@ -2,7 +2,9 @@ package misc import ( + "context" "encoding/json" + "errors" "fmt" "net/http" "sync" @@ -24,14 +26,69 @@ type antigravityRelease struct { } var ( - cachedAntigravityVersion string + cachedAntigravityVersion = antigravityFallbackVersion antigravityVersionMu sync.RWMutex antigravityVersionExpiry time.Time + antigravityUpdaterOnce sync.Once ) -// AntigravityLatestVersion returns the latest antigravity version from the releases API. -// It caches the result for antigravityVersionCacheTTL and falls back to antigravityFallbackVersion -// if the fetch fails. +// StartAntigravityVersionUpdater starts a background goroutine that periodically refreshes the cached antigravity version. +// This is intentionally decoupled from request execution to avoid blocking executors on version lookups. +func StartAntigravityVersionUpdater(ctx context.Context) { + antigravityUpdaterOnce.Do(func() { + go runAntigravityVersionUpdater(ctx) + }) +} + +func runAntigravityVersionUpdater(ctx context.Context) { + if ctx == nil { + ctx = context.Background() + } + + ticker := time.NewTicker(antigravityVersionCacheTTL / 2) + defer ticker.Stop() + + log.Infof("periodic antigravity version refresh started (interval=%s)", antigravityVersionCacheTTL/2) + + refreshAntigravityVersion(ctx) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + refreshAntigravityVersion(ctx) + } + } +} + +func refreshAntigravityVersion(ctx context.Context) { + version, errFetch := fetchAntigravityLatestVersion(ctx) + + antigravityVersionMu.Lock() + defer antigravityVersionMu.Unlock() + + now := time.Now() + + if errFetch == nil { + cachedAntigravityVersion = version + antigravityVersionExpiry = now.Add(antigravityVersionCacheTTL) + log.WithField("version", version).Info("fetched latest antigravity version") + return + } + + if cachedAntigravityVersion == "" || now.After(antigravityVersionExpiry) { + cachedAntigravityVersion = antigravityFallbackVersion + antigravityVersionExpiry = now.Add(antigravityVersionCacheTTL) + log.WithError(errFetch).Warn("failed to refresh antigravity version, using fallback version") + return + } + + log.WithError(errFetch).Debug("failed to refresh antigravity version, keeping cached value") +} + +// AntigravityLatestVersion returns the cached antigravity version refreshed by StartAntigravityVersionUpdater. +// It falls back to antigravityFallbackVersion if the cache is empty or stale. func AntigravityLatestVersion() string { antigravityVersionMu.RLock() if cachedAntigravityVersion != "" && time.Now().Before(antigravityVersionExpiry) { @@ -41,18 +98,7 @@ func AntigravityLatestVersion() string { } antigravityVersionMu.RUnlock() - antigravityVersionMu.Lock() - defer antigravityVersionMu.Unlock() - - // Double-check after acquiring write lock. - if cachedAntigravityVersion != "" && time.Now().Before(antigravityVersionExpiry) { - return cachedAntigravityVersion - } - - version := fetchAntigravityLatestVersion() - cachedAntigravityVersion = version - antigravityVersionExpiry = time.Now().Add(antigravityVersionCacheTTL) - return version + return antigravityFallbackVersion } // AntigravityUserAgent returns the User-Agent string for antigravity requests @@ -61,37 +107,45 @@ func AntigravityUserAgent() string { return fmt.Sprintf("antigravity/%s darwin/arm64", AntigravityLatestVersion()) } -func fetchAntigravityLatestVersion() string { - client := &http.Client{Timeout: antigravityFetchTimeout} - resp, err := client.Get(antigravityReleasesURL) - if err != nil { - log.WithError(err).Warn("failed to fetch antigravity releases, using fallback version") - return antigravityFallbackVersion +func fetchAntigravityLatestVersion(ctx context.Context) (string, error) { + if ctx == nil { + ctx = context.Background() } - defer resp.Body.Close() + + client := &http.Client{Timeout: antigravityFetchTimeout} + + httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodGet, antigravityReleasesURL, nil) + if errReq != nil { + return "", fmt.Errorf("build antigravity releases request: %w", errReq) + } + + resp, errDo := client.Do(httpReq) + if errDo != nil { + return "", fmt.Errorf("fetch antigravity releases: %w", errDo) + } + defer func() { + if errClose := resp.Body.Close(); errClose != nil { + log.WithError(errClose).Warn("antigravity releases response body close error") + } + }() if resp.StatusCode != http.StatusOK { - log.WithField("status", resp.StatusCode).Warn("antigravity releases API returned non-200, using fallback version") - return antigravityFallbackVersion + return "", fmt.Errorf("antigravity releases API returned status %d", resp.StatusCode) } var releases []antigravityRelease - if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { - log.WithError(err).Warn("failed to decode antigravity releases response, using fallback version") - return antigravityFallbackVersion + if errDecode := json.NewDecoder(resp.Body).Decode(&releases); errDecode != nil { + return "", fmt.Errorf("decode antigravity releases response: %w", errDecode) } if len(releases) == 0 { - log.Warn("antigravity releases API returned empty list, using fallback version") - return antigravityFallbackVersion + return "", errors.New("antigravity releases API returned empty list") } version := releases[0].Version if version == "" { - log.Warn("antigravity releases API returned empty version, using fallback version") - return antigravityFallbackVersion + return "", errors.New("antigravity releases API returned empty version") } - log.WithField("version", version).Info("fetched latest antigravity version") - return version + return version, nil } From 4ba10531dac636b3993832503272865bc0db45ef Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sun, 5 Apr 2026 01:20:50 +0800 Subject: [PATCH 4/5] feat(docs): add Poixe AI sponsorship details to README files Added Poixe AI sponsorship information, including referral bonuses and platform capabilities, to README files in English, Japanese, and Chinese. Updated assets to include Poixe AI logo. --- README.md | 4 ++++ README_CN.md | 4 ++++ README_JA.md | 4 ++++ assets/poixeai.png | Bin 0 -> 39549 bytes 4 files changed, 12 insertions(+) create mode 100644 assets/poixeai.png diff --git a/README.md b/README.md index e8a4460f..c027be19 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB LingtrueAPI Thanks to LingtrueAPI for its sponsorship of this project! LingtrueAPI is a global large - model API intermediary service platform that provides API calling services for various top - notch models such as Claude Code, Codex, and Gemini. It is committed to enabling users to connect to global AI capabilities at low cost and with high stability. LingtrueAPI offers special discounts to users of this software: register using this link, and enter the promo code "LingtrueAPI" when making the first recharge to enjoy a 10% discount. + +PoixeAI +Thanks to Poixe AI for sponsoring this project! Poixe AI provides reliable LLM API services. You can leverage the platform's API endpoints to seamlessly build AI-powered products. Additionally, you can become a vendor by providing AI API resources to the platform and earn revenue. Register through the exclusive CLIProxyAPI referral link and receive a bonus of $5 USD on your first top-up. + diff --git a/README_CN.md b/README_CN.md index 572cedfb..3e71528d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -38,6 +38,10 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元 LingtrueAPI 感谢 LingtrueAPI 对本项目的赞助!LingtrueAPI 是一家全球大模型API中转服务平台,提供Claude Code、Codex、Gemini 等多种顶级模型API调用服务,致力于让用户以低成本、高稳定性链接全球AI能力。LingtrueAPI为本软件用户提供了特别优惠:使用此链接注册,并在首次充值时输入 "LingtrueAPI" 优惠码即可享受9折优惠。 + +PoixeAI +感谢 Poixe AI 对本项目的赞助!Poixe AI 提供可靠的 AI 模型接口服务,您可以使用平台提供的 LLM API 接口轻松构建 AI 产品,同时也可以成为供应商,为平台提供大模型资源以赚取收益。通过 CLIProxyAPI 专属链接注册,充值额外赠送 $5 美金 + diff --git a/README_JA.md b/README_JA.md index 5d9f6e31..d3f06949 100644 --- a/README_JA.md +++ b/README_JA.md @@ -38,6 +38,10 @@ GLM CODING PLANを10%割引で取得:https://z.ai/subscribe?ic=8JVLJQFSKB LingtrueAPI LingtrueAPIのスポンサーシップに感謝します!LingtrueAPIはグローバルな大規模モデルAPIリレーサービスプラットフォームで、Claude Code、Codex、GeminiなどのトップモデルAPI呼び出しサービスを提供し、ユーザーが低コストかつ高い安定性で世界中のAI能力に接続できるよう支援しています。LingtrueAPIは本ソフトウェアのユーザーに特別割引を提供しています:こちらのリンクから登録し、初回チャージ時にプロモーションコード「LingtrueAPI」を入力すると10%割引になります。 + +PoixeAI +Poixe AIのスポンサーシップに感謝します!Poixe AIは信頼できるAIモデルAPIサービスを提供しており、プラットフォームが提供するLLM APIを使って簡単にAI製品を構築できます。また、サプライヤーとしてプラットフォームに大規模モデルのリソースを提供し、収益を得ることも可能です。CLIProxyAPIの専用リンクから登録すると、チャージ時に追加で$5が付与されます。 + diff --git a/assets/poixeai.png b/assets/poixeai.png new file mode 100644 index 0000000000000000000000000000000000000000..6732d2a0ce4b23be803b259325b4b5c1a6fddc18 GIT binary patch literal 39549 zcmeFZc{J7U{yw}bgfb*k8Fwj_S;#z-3K_~&h>T^5Z3rRrR4GN13=xu{WNa{pB10Nz zG8CJVA!DZBwe>mYd(LnD{&?2&{PC=3t;ag&lfCWtblb=^D0Kz~02{Z@Jc zfxvJ;N7INvpuz8J{?M+)JLZHCD*Qv|p=0hvATZWb{=ep$5F;0XK&|S0#EfL7rz?Nb z-Br}a&fV5t)X&uepC%9#Rs1|`PM)?W@!Q%vI=d+ejyFMl2a&~v)r<~Kq*4>+=Bq&JF|Iew_NF+~Z+kclXt7~7v&eew5|i}UK0JZwC@?2nxFuvZc^viEZL z_B?67dIrit|C~g_)82+;Z>KCNE-51-E`|Roiv4eYqFm#D4$!nA*(e9>Jmc+7Qm}JB z`S%z3zyD~e>1azkNEvz>d;NVw`0qd7UiO~<^}(ZRS}yxdG`)@d|2e@wAGGzfakKlc zPpVTsc~ZvQ%v|s9`{(|Bf2()$-#4e_M>=4qY%jIX-bP}dq=?PQlY2#^q{YQWY^6@_ z6S0@s>mY4oZ)Yp#@IO!a_mKZSjkYb$A}uK`DI+Z>AtkX-PFhy{-qS$|&|Np`Bc>Kqbqb!Qml}}j?_yZnh?>`TNSMqA! zBqw)IWi>lz8+S#@-~aUv*5K-3|KA!=B#D2I=zraSpN+?VtVeuIesvwWdmeFjcTv`` zar3d^-(_U)W@qnd@41^F>qO~4)&@UiJt+Oh8sJ}zd!_$~bw#oN{iOfL2l@MKNK!~w zV*gHRc=7MFh7(-NrGY$Z_Rw>9{`@*1vx|E|4e z7h&D%`^e2}H~;fHHLiZ0vYL)7{v#wmZ(o%I&@eyMG-P3PPt$AcUR4diL3^ZQa=h`DBB_+VKc2UjW-(Q^b@yAiIrOg=~q}sq z56jKX4K9~hTK4etH2OnLS)bKU-<;#U**CjT=F+{*k|@SX@A~EGSrX}pJe7gWt=6&G zq3_=hM72EekX=mXGTy&`|Di*1q}`OOKFyrF9>p$Z_36!yG&J<|^j1&(h&&VkNDyll;2AG_>Nin4FcGd!#F30q1Gx3# zZWkOW8rRU-)ALeBYC9|aSw-GbBIS@XBFclrtga6q%nS`XK76=Nihhcpix)3``(`)H z%-K;Jv4LyrR{9MavNRr7f89YjB!=ilIb?iXd?hQjk0k%Gr-xm{0wa1-;)bqi!SUUq zqDhP$wZ)EYio8sR>Y%*losXk#r%pXG6ULn-^z`&R zdL%haFA`qYH~O)RY2!wnD9WX(xuc6QgKs~7wzRV1y`Y_?@$KxhFOiWn#ofX@Yc*ug z{UIGadW{spp3&jt?Cfl9ol07rxRwj$$Ju$617)S8j#ycV@@#AC^Z54d+wmtpBJ6Po z%J?tyDERy=Zc|PyXjd0zUm)MT6sY{KLvNJdj=E~Jw6xUc@!`kx!KkKV#SVHVCMFsh zVI*3XcbB*CzsRQ|YMCCV5yklS-MfRPt2cT)_C0Qtjg~4&)Uq;Y$sc1sKR@sOeBR;o z={y`~aP;Wg`QJlXLZpfB$<}vd&!|_fPT8Otcbmq++@z$WpFe+=oPLk%c~u-|kNZ9| z`mvL!vx|+5&D-011G{hnV~4t{>uAT_{aHekE1vk(Fsqn(^^}VX>B;9;$zsWjm&@#5 zC8Y*0S3G%gOrDA^qr7}>^RvU9J8)o@_m6kpK0ZAzGQ-Rwi*o~)Zppa%PkxK}XCm*`-@t6(`Iu-Z zi>#A4iKTi+DZs+QB2$Cek(;scS->oA3Fj17RQ&1lWWTodKbH;K(S^%eR0S=4e&TB# zFe;5XF*Y{dv17;R=x8S~n#<>j66eNy+1ZDpoNNLEKYjO%psX3n;9mFLIUFMAv$DLz zq_SvGslI_dE-N>;dDQ1n6u)w2cDAp#_d|x(>({S;sPq>4xOzxf^n$PGLUBz^jgCP7 zgkPq{H<9Dz9ykLJ50A>yR3d4^3CETs-p8>Ub>f}a-4=-?xB z4$=@U__6Ij_ZvDI1wG)eeCm4-{F)5RO{YpaY zV-!tIO&uKWk!YuEADRU!^D*VzoSq*_wSMYn_%}eUO2VI2xV-3Zh#UJ$||U4@u%+lXC?$Gu07j4IVR5aMs&zU^eA9L2wVVu+Y^L|gj>W)>4l$#d)1 zZ=k2gposFUJuvh-B`-JkazVS6mzS5hxw+KVYmBRx+Y+<*Gu<2!t)-=f|8^2Bnp#^& z$z&aUeO&GOo9x5I46P%-eu;{SIdV~!!U?;`>j3v_%ZoF&)5fYfaDYzmMWm6jvDdFpa>MkN z$i2z0q?MI%yObxYjrW{xkN@)J%j}ncOHXth{QjN7LQOO8-yiweUEIl5$;rhfv}cd) z>YWj?uJ+wKzyA_%gnBwg;b4?g&3`4Z6G7LmU9+qtHQPzP+qU>sF<|Critm@RQ{>Bk zmyJ`62W6t8_=2Lf@A6*SFR!`b^t%VmFJB^ETxG0Lob-RDoO(&9!6CfP=2GMD-kNoP zf3*>QiX;P#rhxUbW;Sz|wa=R`bfOTOYf-or}9OHArXX#`_AZmM#7U z=HruON@@7;p*&#r*N#I;IvAvZiTF#GW@ly&Mx9`^b8tZ1b#)yXX13%;y8qUk_&31P z?oxNInM@KkpIn;#Dt4??S6-6?(T5o9WSiUn(IU_yzwv=VD9 z2?`KBt(!Qs(l}AdFQg}T)KH;8=kYFHkLdOKxmP>swHUcxU44DgUAz3od)Ihx zAc+bJIu5qn=wf@|w>bAuJv}EkH~nwe5_&hf=zW$uubPmW%E!ld826{(`zt&m;&b_# zFnKEZm#<&H8z$jEM7qf?RuuBCzmH8g9-5!4@c!MqBtTUpj1guNoh&2F@3n)u8oj{} zj+HSMr#o64JC=Cmium7jK=3Gs zY46^|%1~mlLn~tDZUH3_2!vkVPfAmg7+*ACr}p<>uzrMSPI+;K7kS*G%%R$!G5CbduVCk^78i?ZdE@m#wWS z``mfGDRdb>Q8tQQN7K{Oah!U3f_ykzwtk9H?vd8k)-K|+xZ|Q?yLX>Cb0(e9M$5*= zMqOPU#mxKfeG;Pj`W_YB&&koz(E;wmMb*s5mw$NST#PC2w(#~{+*&>b8bD}4VWSHnLdB!m!F6JnWqr^2Hz)FMA< z@*Ca1t~X?(ICUy${(JJrGIszauj#%>7)UqundsMYbabpZu8*lZGw|Zs{NOEWYU(Z`dtkh;Zr$3o2;xnwtOD$DhYlXh(cn%vvPTI?Dorg6f3SII zR$EsG=%)6!>=1fo#d$~W0k7)blyXMy#Ldw%Fu1zA@4KOk3u|q=d|S@z#ryZk7<-ev zqq#?{Dm)|P{}RaR^b?9*ddOF(e(|xfTNVApy*CK4(rW=h+1u~HGc(6E(1h8vT$kt~ zZj6bE*)1lP{ZGhwNCl1%+y=hv;z`G>%ACxpm{d=zFsWM^k*X~}~lJ-xiZ?{bC8^>(qcv$5I8Ol2U!v_$@m zA%EHz8ijf3>BMc@YF%V7FY5fvrK~J0 zMuvuJmNg0Y?%msMeDBE7qlQr)b(E`!9@oL!clTe?za?|wZ-FCJPuPqRkI4tjxajKY zo<1$)O)I3RsJM|POn2v9#=el27TuOeE7J1f{P3G><5e0$AWYG7GIg7sIWqyCl`F)# z?XnJ*`=v`v-n4HdB_%uGyuq7BmX-kOsmkXL{u3zeLb8VIWz&GN%SuXiv(jVbmseEm z*tv5Vz!K}YGD2&3c$jOSTV}xt9gqn#GqaN?wn|i)cxz7q$F)T)yBmJc zMMu+a+&DEo%_C_SotO8c_^Ye4^E|4w`)@qntUZq45{vxVU!R|N4PESZd46tT28B3iVYE!}@EwE3wWKJcTwOiA3vqG3 zCnrbx`Q{hKs$RW(IfDZNX5BuSQ4vl!LKYAb6}|UIv;E5#ULKwccf;##C&1@&gpixv zJ2JGQ7Pd3N^toPVpcmk zI<7N3tp5D@Ty@BrF1D*ZA3yRb`i=i6tTHh*wXX2|4pP_1&jx;#8QoK46iJTmh6 z2;Was!Gl>yrdzgb0V2VVU&%VchgPup^^Dv)oJA*ULcwej#6zedVO;jDLIU>i4wM=- z-*y?<+XdMi!zp)m(hiXaT;ol<?%doQ$E?>H{`5D)qQ}cm-9` z)V<~VYuksrSb;(OaczN}JAZW-XZtFfvT<`CKXT+-f4@3UZ32_>9KV>@rOLHw zc~ooFR8_0@^PM9h^8%rcetz;kGPPw*Mzs#nNW`urWVs>`l)(bK#7n9#S zDlRrHw9>O&b7GxFxe|+jCp}|RS?$M9opJKqNeAT&OG z_AFO})5ydm>FU++stvt4FD~+6SdSSS7yC`v{?uS9EkSO`$=MZ9u55D(xL4j^@C{|B*a&gIITvH^ZoJxsg%%bqI(v4g! zYGtn7k0NG%zCtFneMv_c|3fKsDeZC&b=At_JFm5%N-@x^^QP9gost5I59F4s;i4hH zaxE#T|LPA8X&IS4tn|g*x;$%D>t4P3@k*?Uf+=KNIK4M`pfc@PVF8NiopB_Dy#LnM zH}P6w%)pc+x_ftXU0qn^+NKkVn)gadq(w!4EzbW&M%zRm0r`hf+z7uQKdGsy;mwB@ z`3JKem6j&SQ-zoTQ3Bo+k8L<_E@bi`O}}@-@9B-bj{OB@xP*lJ7ezF#M`B+?W7}K4 zo8<$;3yrIwVHWu>)(>*TG}zwlXc}HeTB%?-D4J}HIj*RXlaVRCe?J!Ird#?m{pQEC z^|@&_M(Hron=-CF;5oUJ0)Zg5M^IBBHMfl8YwnToWE({$f7TZo^}_6NU>${p0Ee{M zWnMl_dRMKCj~zP(&CA>z<4@3_T3Z7KrML4)R_o}|qY&Y}y!ImUAk8=1^}igP zM}6D9dv_O`3qYP_nHvun#t*32jJ%3D6y6tNxY5mlgM~#s1=vIhI`M-G3ko~f}+7GBKxn;&q25Xsp_SND(nU>VuJiL5PBZ{B^~%o`!~-UOhP> zYW+O0^`f!~x2HVHClOQ!(#++YpBCCIDc_p>5l@H^y(#ulrf($l>6t-VmmeJ>04sUsKopsPl`~;*xQhoB8OD*@2Wk<^N2z z`kjyr){2l&TxF;Tw2^~Zou^8SaMsR{HR~pS|DO23sQ9KTIQZT2*ur|NECRuDXmpeZ zJlJyLRR6$0g|{yAQ)TwA9)wGchYlb9`Yd3dH}x@qo8n@Gz@0azN}l+P_Wz!zpf-Rg z!2Q37GU@`Sge}Tr7h5&`zJ2>f!e5`U^*xGKOiQZD*SBx4RrbG))}%0i5&9w_jIlIy zg8qJ6JbGqUmc4_6k?FXf;vMa~HH!sd(9f{QKR-R2h}HL-g@r|VY07wn{)9T9Z_Z}< zp>N+bDG{-S`}6RPyWQ=soJ)7 z^B`ip1-WCi#avQS2%GBI#6V*gT}qusJ;p%_JOW(;BU4gTRaKQm{=EXiLXBUvvq_%x z&Yd)T6$(vFP5g>l%CfR^s1Qf;igR&M35iX2bqQQ7^bv5w2=n#O&VbJ0CL4c#q9=q% z10rPWla=)w>+)*-qP4j1_w{4>`3cJ0c^%+%b)+7s-Yl$DuzTlP$7<=W~F zz{}oEd&{A4fL-JW$)EZ0PGkfmk1^q3)|-2JcMHhR%gbf4_$}A?gibK8M|Vt9Q}cSO z!w7Tfx52?vB^Ch%diSC@ex zf#4CMpFJ>T`bs^Wc^~sMFDKI(jdSOop+Gn}r7_w>tMY#+cDw|^rOgw4Jv+&>etsVu zR-dhCRd{6Hd<*Z?0oN*1F(<4VmpprBCjXj&q*=)tptih$ac7XUgCqEyQ zb(Lpt1Z9)=pUOEdIyM{;$;fj`YU{IS&jS33Zru-0Hom+jlq*EwJ`M@N$!V82wE={K zG<`A|wWo^?30E8)!5g~TmJ^kBlIAWh!rs&$*1ga;?m0hcf9+sIvAX=OJ$ptvexJ)U z%B?xUDdl9)CQ_q?B`cqD5#u{SM->_$zj1^uSmx&M=f`>B1>&%0=FxFew0}4_ATt<`Fo#&#HDB4$MW>2lZY-$iJM*&}P=dlA zi}bVKRFsz+n0|P{<^J^HiJG+|^yD*`!YfvVjd#R&bXMU7kW$`;*oJ zdi!?wwSzK;&1pPNK(T>lj3u#CL1CdyWhJv`J(2w;SO1`9W@pAtml*MP!dVw~1$bNN#Qpr?Wb1PBm88+ZxtC%h8O+@pwib4W z3J!W^)$cN=*Sh5u6nuSy^tZ0FujfAv$)&!xF1Yt=Na@DSKevY}+_{+!+y^v{h>TP> zP0-TN7yvPO?84vL=K-)P&?$03paY8v>l#)0G?_U(lkryT%=C0bf$9mX=YjLVE3?7v zY^+*_6%`e8c3aeGm%0FAr>3UTgved_$M${t7C!NzeNE0ZyRcbWem+DMCLZ+!x6hT9 zPkdTiT7I_P+Lv#oX=SwxdI^x6E)luXmvunC_j&# z-)JM8_4M-5Swu$^&9W4E?<gnz$UY7~=N7n!Gun<* zCI>3fkFhW}XJchmjpE;Jbo8h<94JH%!=p!~rKNck{fcsPhmrKb%blIqL9YAW+Y2&H z;0Zl}RW@ir{IB>b$YxbUg3Vlil|7dryASAw#5tPvl9HQZs^6In)n4!lajp7!7 z`}=6#wnPpPV`i6^0-&C9_sdAwH6;LXluX@So~>Gulb4Up*q@=7A}_sT2X*CI;#Oom z8Y-&mw{D^H4N76v?-pHLTIx_dQEHtl19*#Zj9IC~6Hm^}fMv3;WNlK{c~oR3%)!Ay zrZ+s}?QQ?iBeL3T&h!by;xZNB8evU#Wfs!04XtjkT8NHK`jlHX53lIoaDEwX|g5QBR85Ecl|n zp7xQ%DEQM*TdG*3K(nOpuj7j8G!h^C>Y~;&Z2d+i&yhQ}liAP5^C+;(uUT_QPw%^D z1wz2~iS3CKz2FNCK62sASDfO5pWT>P3GLk2sL^uy+BJK3cWYDA^*rkA`}a43A4#t$ zg8&?V9$4Y$FVF;D1EFMt<@p5J`i6!rTwH~gPfANm*(<*b!oiWYnfWPP9OadjL3873 z8{fmuU{OiRyW4{Y4>mF|AgFbE{h|%8BqpY$q@)W)4*+g3Gmqa5 zu7sGOcS|Owc>gx2ADA#+OyWdtUf#m*HWd(J+Nujp_4SzmQSEHBUr?!`YJPosc6;Ta zalfZ;^GwC8o|=)8RcVQlH^r?i{79*Xr+g8H||CnrM-_%EiWrdCz0 z1b_WCILOY;ExgQI+tAo3W>!SN{8B66aifMEm=k6@fn$7)g)O(!6DBRrKTeoga(_jt-xWrtiVgd6^o&(Qqr;FxCctFqLktdPC-NUW( zi;CKTj$XXDS5#y$_m){*=f0nxpQ>Oqlbb=S&fF7;lOk46Io&VJ7b3^$-Ind>?35H0 zWvq;l1=hs#0N%F~X=_mPJbiqE7G^|9O}0b#*t6o~1malK&JZ1ZPsjTs*H*C6aqPTeO2LudO5#(4_Tj_loqyTanCok{ofrwpCtO{vDIZu4YSmwl&Z`|0@yx_Rq za|(#l8Y-RNv7!b<2yyk4CoLuQp}QM6aS@KIc9CcUV0=t~ESEa@ ziAllnv&)NT$+7qRr^x3Ahytsopr?vtdgOOmv@x7-F&MJG%{s&(D45;Uq(f%b+uZK)3*KE$|?sN*q$ zy)j#KLkEb?m`pbt8$$~V)K%I?q~T7OnQ*5%9Gf;x48IX1$7%!8BGKTgnue^>m=^X! z2ZQq+9-5V_Nhj(I2RC*OqiV9Vvxonzh^QWgD@I(L8H<|Ez{)CUW%(S=(ACxT&lz^_ z*&{C}r)H{(03Gy@MS$_j@?H^z1`Ur8CMJYYts4Bt^nuwLLPt8vT0`^_wI*}A3rE4h-6gLsr~l-`=%0w z5_nElCre<|q3w>y_U-E|*BsW?rrDR{K!c+TAzudv2b1Xql+h5u(<71M>2)SCSj^KU z+hJQt3BipYKwZspesfRFG{F`MJ{sKZY&vEbv{P%=tO0+*E8U-?qmX~F=B^;g9Brqw ze%S3oroz4J+=tZSOKx9N8On+*lmH`adbE2QzyY1IG@)Iw*ALUO&|Hkl)qsYPTh^(apG+T{k%N|Vd%MBvuQkZ=+_H!bI#k%`Q%+7rWo2zIUxqTO zZ94;vje$!BlPWL~-Sca`qXzDQISo3cs-OX@7Mx<*pjDch7#!6yyfOdh4`wI5*N@ha zRqLp!=`jE>c6RkB{+plyPbw=hDfLvYJ&(`p&)t9ioL7Dg&UVEJI)(8)-SY)$yJ9PW zy`W{Bh0pBwQ2Mc5u^E6w_cJn3|1FprHOSTwuS<)H_(z<01u|j3fluY>D+1Ai*_*eq z@e*J9>VnuU4Rqo&+M{NQTcTv~-+YiAYz`@d_@+F0qClcPrNqsYUWiE(77_Vc70kpF z>NLJM?}g?Gx+y?uoa*2f@_j;!+7~d_YD7DxL1e2G#j}&YQ_;9P6Lv~K5ogebk_l)B zx6yi@&~QjsF|(mW&Fvy0BI;Ixi$9?-w$ow25~Q}uxR}8;m5wRP=#Ytq$Mb|-DUUfZKi_k7(z;xK7Ck;I|yg)TtF8;!W^vzq(Qi=y9(+FG$ z3=B-$jMMokTKW0SmATU~F|A07glaWq)0V8g7}f3zHNUc=Ld`Vo-5feh7cS@_$WamE zJF^e-Vf~GNQKdxUB0>`lp^cVmhOF11aV00ujvj*&f{tjq|8r?HOEP$y_lMR^F#mZ@ zCX->t!x@wMe$S&T4&05pCefN>csFhHR;WK=`T!i^nHQs?_J1sOU47b&yLYJr@6P4s z(f?nwq-aul~G!i&cf9M(GUzg_KM4sz-~WJ^*i@I(3RtR}2j17MG&h zhJu#|p)M|<*0y~O1T{%NxoJ0g4l2ZX#F48l8t8K{C!~`sC5`86fi=kbEZ$lV^ry}V0;Dx51P3kO#z&`L8qU4K9 zN{;lD$Rwyrey1JCakfDD^ z(fsJq>91u7&Q!&-SHcy1(G^zk{MH0stPz!#RD&6%uWv52x-HwhhS*lbf5{{cRL!mWfx9BIUq- zhvW~s5K%$D?JGb;`sQUe)VsUEth~JXkccvNfde5)b5r2Q8sziB;}y1WMDc_^AA^S! zN??6h!6r+Pf52~UGW{L}g(v8qw6k4JN;0vrNe>NOOKLJjw+>Olz`)Q>)Egcd>4AIk z)-BwE8N3*K29R9QQa}#Euv4;;x3}~WI(3~E^bEm6BoQ`HA7fV*d|)kx_)AegfhwM6 zT|O)C^KQ2 z-QDw;Gy~JdCsFNfz?c{UQ4|&oM!QH(E+7u1o_-fv;wJN|ASJ_MVr&N9i@wRgfV2j0 z5U>rda^>>*;O+w^CZ%9pXeUohPcO|)oCMQ`n1VFn?7W+Fvj{>FxNdrZmAQE{Orh3} z2uw6an1guyu~f#OR~wk}iJz!OG1ZxyoPS4IKZRBdmc#@YQ(s?~sH>?-2lfov@)_jk zUH(U484zNmsC#plxm@n$Kdv=wDgJXzH zrnOYNas4__x9)9O!^4M}D!&f_zeBBoosoryXX*3ORsnSr6ZT#XEpzi5k&%((KS<>E zKG1;7CD{ppO`=95D?5AaN+*g9#Kj`73oG{@Jm8jb`GD$|rg5{b&jVB2H#jJRRLmY% zKX4s>8d1RdEnCh4lJZ_!pAr^M@$D@tDfAnfnr0Uku0RQr`mK!nz(N#wT7K=;tZz?&H~PhNl?4eCI8;XyVLSV?TcgJ%IwJ=y)HPO?unpIQ zQsb_$l#f02d#KqGxhobt3akLO<%OZtV3e;Y1}+*)foHJW5A^p>P7LfPCr|)fMn;C} z^DlL^wK}&LCj1nwt*x&(YQO;t6oL$a^Yg$keuEM?vfLNL!s@^j`BZ|QA~ZYovI6~w|2H1YT6}-*d~0$*4{p1L6*7ylFG8I z|I{uLwR$`o3yUNLQ^byypZ&eF$s5ZG#0zUUEG!KCTeBxjd@bxDXS`5c4}p9Gknfl& zJLvGLrR8|3%NCNBF*p-KD(|`&#S>z|+kMy6!$bU`$_c0}lx}odT6J67r6i@X1`Siw zSq!-_&@%%A^{1N*B{$K8A=Or445-eQ;XE;wnySjm%I$PH)Z^LPx+GCHz&15q1BfY< z{Elt{KoRHZ;m)jYi^6mHC=}4#C9JVgKo)sPOw7HnHX{D)bO4&$hK7bfzt5jPr~0z} zg&(|HYnQKV*N@{6yCda%<%6i>sZ)R6TL;ICdalKY!Bp`zP3ZYuBzo|ipz+=sc#wJ? zJFiQ5vlT@JhEg}^hdMe`8n2bP)pHa&Ju-@E4rB>O-`7kRi||dez#xG;m2pmYb2p_(#s&-Cce8 z9w-6z;^empKM155zcz(qA}ED}nuYaZl)>rxe}n|}gGvE2Eg%CF#79M>>GzNYUxgB| zbEY{L7dxiN(b?WkyI}+DC+HvEgf!wOO5adjeFBgPQwWVC!tL@lWJh?P!d^sa@h@`WzZm z`}s4E{P~Psv28<9QBfO~m%(=VzkLr>S3a-93;>qTiWSYwOt2(+qlyDmS{oX!fsVBf zG7tJ@ZMXI@$!E#|4zf+}FcW?^`TZ3PH0bOfke$_ZfoYsl@$2g~I~m{F79k-5D>F}S zXyK@fV)_b&Q%9^1O3lc~p3Xy$Yk?ibj+YZGCyt_T1e+VYk*F06pSSJ7CK%Yy;4Vr^ zJ9jm+73uOqeEe!*m)yJe7^%@7*+g4AlI%}K(bdp71z}+ZB7OIJc2>o2oSzgyGJou3 z=;I@UMXG!E{{2cYeQ0ktZr)TclgdW7fA3!7%w)8s>XGQn+`oQ#elFm$RkRsOLetBa z78Ra5+;jwZ!Avn#?QD7?=8x)YYSc92(4lvSS$&(u)3?xn%CISf99sw!UHs^gL+j0k zxhEXQ-r+dOT{wsSU^fHUt4EK!bj1ZwZ3!HW9)*x48sZb%jB z$1-anLNrwM`Tw@Fx1WV8HZSkc!pqb$Zde}fowK)L<={vx&L-Y!#Un06Y~VooqWQc{ z8L|-_9UYzyn5A8WECfXo%ygA9JPTfV&Nh$^xC^`!laZZ;T`V37J~1H>4On?Ep~VLI znTn@25a^5QOZ3HrZ$LafsMKJd``o{Lf`mZkKF=-Zsi`Rh{Jw~RieneCUEDAo$|dV@ z!r%W-ESpf8#`VJV^comF0Os0>7qIRj+TLu)nBV^$%&e358sIP1)!F&U9VA*U!>gsG zKAxVF2)C^9fK71?H*qE44^4?=&HRCZ0ga|Z`w=nuA9CTz@x^Z$War(CQDOU4b*Q+R zh&E70WMySDGc#4A#Jdq!SPa%jk5co5Ce**_=rAw$usL;V1f?)Z|5-S@Mw!5JgJC%Zj`&g-)SyI59rXKUyukckf>ReZ$Iwd5C5%iM=+8ab8 zJX|=VjWCqE#l=7hlf4G0lbHAhxDrMk7-8Y`iDcr_U(j2Vu`~8ql)UdRY>*p9?+mk0zr&fsymMSEx zyAUjR)*eP@1^5OscssH4bxqCrghXh#^uT(bDla|r{QeS)d3e;BXWhDWpH9RDmXB`B zXvCPhxVXR)df{ z6ae*%vcNu;idfn_hTMk^$SRelr=>M}Xd8hx9*K50oZ3u0^5`a)xPFu&vk!kC9;Qnf zFNCN>R0%}+l`5fac=IMPA?kDcC9wAnY+>pA^a)*$&+YBZl@W3nbPi!Ng5`N;0)X9` zS>vnNoCtAdMw<<*2)HxrObyf!%8o>kk{8l9^wiXdQc{nRCK3ABI@B%~FO4n(?46)d zcm%Z9?q+1Pw6;df2QbmmK~`hBaI_Y9B_%Fy!<^R-VRqO;Z{NP~u$d?%^aO|##uDbq zxMz>y9z<4JeZu>Ng&dPPWJE3eRnUv%ewfYT$+j<>>t^LS!?YFnyGFbe9d&0bmV*JwoOEPP|ON7sl-nY5b$pqo}s>lfCI z`fFpDwR%Pz-;j5}L&tSwGbP9&JShQrq0k}7L&04)Pd*k`r!|1O4FW*6aK$zXn6e#b zRP{0u$^c~Hw~of?V*?9Z4uDLQtrI_9m&wS;AO&!YDyah|Qr3iPuZJvZqo;>QNGtzW zTpk?@I!mIZnJ~LtfJ(bbyW)J!My*alk=4JY*I9zWRG8o6-`rE#qoXhDLf3Cxw=vLhPCO2G0%`y`Ez9Ep#K zA&%-lLunT^e?(P!zOGL|KmfF=&Os*@GE%P+N57PFr+_-|84)ot71$-me*h#TU%N(E zbrH?Jsh#?_X)VtO(Kc%62)KQDDz_@=)6D?0<{D>T@u?SsUh(>l(ygSVos05_kUe{1 zt2Ih?94WMVhHbT2Gie%}Fy9F7+s9FvaT)wgQ(L=~>^+u{zzo%O`m9HbaNUYVZO&{!vOa;HVY?_sT#2&Dc!*@shS zC|#3;Bgld07Uv{TTWQT)UGw3H82Tz;(T>p{(40dz^?Nh3~#`VK6ky6@Z{nqMk z2bc24$32u&55GTEi>})XW8)agO{v2P<=3Mw1ce~~Z5<01EP+sChbSWMUYnyY%j7}}2S91)rMwYfjWoHXDVl5A_mYr^!^mZT6}In#xjpCtkq$l?n*s6JuLY!A7b0DhN;f= zv^0K-*!dJGphI%^mm-^WUtaA-mt*RHjt=6qo!G*Ap~2UbmG!n|A)(@X z`}bGL?;n`2`zVf_3CXEOMn?+Up{thvY^V%v{@@}bcgv`;zMlE{i99qy>uamn1UvzV zdc}?&NJ4XmA`pj6%rBzYn}B9lTr4acyMHv#>43V5q-{NIAW@Tqy}hL0gL;2u^qXJ9 zcgf0@yMKM&ulpw#vr_rw$!|Z!Y8vQ)!0u~l#KSIKY6C+~7vek(wu^3f1Vya853fpm ze5P+Jyf5%Q`oDKcL4Eow-kn>2F)Be;R!*+NZ_nOh5X7SB8ODD+Zsp#YGS8 zx%}bB=&1LZGuS+_Pg**{jc5sCPEjpCz&}ui`qLcOaz{x4$~&Jw%T0tNgr^I)@7mN; zOu8N(9{T!>7qr9Nwykvo{xHJo91{W9I9!4Q!WB zVYHOkCL*Gzn*6!QCXByeF$1DK79X+?9Z%?;kdGQVI={YF9nOA(K0tKhNF&Vx4I_~X zlk{g{;qatFUDW<-Figc+A9Xq|wRJSF@OdSuwX3TLDUMSfmZ?U*;FjdD#W^i)?VZxn zZ#z35q0IO3L1tA!7m9*=>+XQ%{caXat(N(ToR3sn#BeDOX$j^(XcZu(Zf?A@ZPw); z#IPDD^JT@w+wI!Uq2~%l2#ViMv<1(LPf#LPR#pNmsU5FKOY4O@1tLgj`P_!|4kx5G zjAJ{IOgHDTg$4T;*zw!}?tA+{ba)m0C{;Za06}^_`wYcBn4uhhfL2<%1l(Bt;?qqk z*Z?LlT8Er7Gf<}nvm1w))y5CMtpU_}4_5-S0*KejC{BSlV%zbX;i(F*AviLDlE)wI z(=_ZtdBav=V;~=39=V-62T|KX^?8yXKb`_0PZO$-#Tt;?AIPiVqwB#TICOw|XJSm1 z#DOr3ea==cuF;=Ak!jG0&$)Almy%I3Ap$JIgqir1Hbwu^#fyW7nEValr}+59SBwFz zUj?|!!NH-|I0z)=g39Rp4t{=uyp1N5OL)_I3%<6ui{H>QTHZFqr{GUp7R4Nb6t(K* zU;&7~5FT}YlX|TwTB~r|H}fex!&xwKlRN12;!)CpL^!kW+-=?^B=ivwgVeAYW=3cz z53uhUql)i(a45Fn0^qIYb45@1!L@5|ar9i!cjV~P(<5&ogU~$PbP|ZVcO|@`4iY_f z&{AWqDJfZQOzQ1GI)*m4-txS5UjPTy-KS4Kb$4&64AsQbMWBg5_YqrSv8!2mzVQ$| z^;3hl+$nd4t$_^+V^7yv5=*iDkkeJI5y$ z3@Jnxo#5cA-XzYC2)pzoc0tt3@Snrj5|je*r9BUZ!i@ZUz4pBEQ0=?mbLeguYA!m4 z2pFRa4F&{H1`@2K*Pt#~NWwVfn>pY)4cu~aqGLZF8g6>a@=lWwBPPz;)X>q>*KY2= zGXxz5DimB0GA9w1h@Fr%a8d}fpPnqUyC@_r?F~5vU$+rzMZPikI_1!7Rr^f1%rZIAL{vTYqusbMT%QweucPMXyKk2SElt12 zSz%!56W&)^U0sdjhJLYOxl*{!IGo$gPEObkzga$P)x07$WkFZj=JHbiXRl|5iJSf7Uvw;jzh(Yqn3l@<|56cpru zHTcYcHktWhgF`XyuX)~SJ;k+;?LVR<>a6?s=@qEp1{lGEs9TYci2y??D=p?OHR~KY z6criS-&+v_Zcc2v_hm19!1QP3)JvnV>agL!4@exHN~UC@h~C zBw>6d0gbsosv-Z6*z=GuP{39-0K^6Ht6E}G(u=}{WWfUm5AG2VpsHMJ$m+&KghZm* zYZ-CrO?$g6(g>yqEz0QBQ>S4Kbwv+y-cJvO!_@lRwM!h|o}iFn^DEvK6D!14o-}9UIF+zM+{Yh#wXO-;BJnvgBsFFHd}7Ei?-bR*7q1Sr*y5LiyT>07gT! z*n3C#g66RGD|`I?+c$4cigVUln|AHS7U_Th4b%9hSFbitx<7_`47-3$fovqg3SBld z!awn<3(zq#?sSY*rbAN}+TS?$w(xos!55IMz&Yx_zC!u?JsY~{jVXd})lL$YoaV+? zo)w=Ct1LmYml6?S7-`|RdhoY-0{7AbI7p=@w~~cIFmQ<0YJYEX3-)l8xuN2q z$D)Rq!PgAYqtf4e@a~<-&cxxNq4ymfHD85b2b5P(_&s}bQIbNqZ@a)=XJoW3ZO6@9 zls>eN(%5wb$tHZY9~pNbiaF2KYIs4nE&7bR{l0}#LgFAzw*he zEdp1AlEWpZ3U()#nmgE*%B1jp7P}hPA74tpfB!z3|8&8h;Y^}f zicVxrt05(?P?NqlJGr`^8!dSsK7X&cpa3#;!Q;nAoP;Lb(APo!$GGA7n=qXMG)cb= z;l2Wom%E$1cHKJc8dg63xTIuh@pqd+o=QPc5wqQAK&G$NW11vs`t_#@9MXFrbmuYJrb2$|2Xk5`c{yR+S#^4s`7vVKj$q1eZG!KMKyn! zRZL6_b4aF-iTtP$ZGb$FExsSjS@^+po9rahG1~*!DGkh47!z~A*0z5G&nM+IX!5Up zwoC;k|G@Ie4BB@5dHCWWG#l2Y8ug~rxldINCPwHrBoP<9eDTej(-iJQ` z5P@ zLqI;HvQ&dt7h z1`$sMCs^azp$Y50zMT>U@IcWbKNg|xb3ZTk=u;#6&w0cbZ^_+$MLmQFuq~-LD1AwvfW%=86N%G zd#2)e=Zhgec~Z{Pr`(mcSK)(z*N7C6IfuPdkS)`MsJ5ZH?cKY%vfmkTGbhbUN69xS zs+E|rP!-yk7lXsYF??QWy=`X%wtjG#L4i*fqP4ZNyNa)v5)y)yFd>ut^CxZ&N+dNkku9)l`w92!6Z$zuJ5Aznb$m{=ZUGv=>=gP9l^@ zhNQG2TZKjpDKXY0IVhr1IkKh@lcok-f!?v}i-8Mk;$m-}|fP{h9yZd;9$G z@q@9{aXlW7YyQ!+v@MrLC>Hhb4rzWovclf8=>-e%>)*3iIC$FJIs5xV zNOZIcqNA0>a&@47{tC(I+{de!(i)&hJK8kc4x{nBQIF}N%jxPc& z+zCfoN9TgLZW`O8bA*OLdygP-+*IwO85wR*_Ur(Jr`0dry67v~X9)IG&kye2?QHFE z*6-!Dp7rXs4)}r~IM8_$X8Cfr<^8d@i5#&XPKHmu+-RuT^!uhw=OpSG+~mvP+C=Pa zWb{WEXlTH`A6x10#2qkdD>++`90b-fw z++YatJNC1jgc{MyVLGD*qGEm=$43?DuQa<(Oi_h3v2~dtW z_ng2nKD@?6l9{;@?XXxOIbi<@MT$V)Y}K6x(eN=4)j6CbjVD3bVQ^75lO@~x(n>5V zFNZLQIgp&DebTNm-=J#NVIIJ7*F4?L4Sub@Sm8=uOFA^$)DtI^vV6^Gdzvg*53c1x zm+_G#B4;LQ-wPL3J3GsZ^KxF-*2b+es|>ALGRygvpJ#H zhgO%axEbZDJDx7k+O;Fa3an?k(&(C!O^pj?uejbXtHN>Q@Zo+X=YIY~&+W%b1b8D7RAN8ogWlJPt*a_LyK3ziM|<1r zT95_Vr;E$nb#?uu@^r621gOZQTUUzLt_7ub@Z3Ih`0z;A7-t$kPy+~R`Jy3o&RLY1 z;84~J%z~~ePhI%rpi-j!0D(m+|It^mqGyjulo^L6d3s(-O??$y)CXxu7lRPPSE={F zPzvfke*e(cTDRv`ruTS7tNMWIuVLn&o)5^0WH75ir!i z`7=Xrk!JNZGn>44LK_?c-o>+`qE5y3+O$DqV>KPe3^$6DACL85%&4n3TiU?bxoh)7gRKjE}mq@1*W1k_ll1((6MW$yPv32W2 zyu^k?3E70KT-ph+h*6w`@}uamGoeWr-K0?+k1=eym`Ahm-|N4Y8n||%xf8n5k;58Y%TM6 z8d6^EMNF;j*&UIabA73$?LD`XIM8P}e(s!>^@0!pa=_SpoyFR&-aj*_tGObn@^B4W z+3iGNO)#N+t&V7Z>h$U0Ho*r5+THIEvo8O+a-~(D-DA$|Ym|!DEnL{YvM!L9Z&a(Q ztzqr_9?T+N3B48-b-q$;I;26kFb`ilO1AeiGII3re|*OwFLkPiN6y+Xw_C~2j8weG z%)5P9PpZTLsC8Kf$CexLd}(Z{Pq2XptNu34uP`V$7-*DACQ^P=%nApp%)FajYcw2P zPv8D-=Hd=g2#vz4c@Bs(2xGp^wc8U$%KtI!cV)4hLmxf8c7kDk4p8o{TR!%lF3LRR zBM^(T7YsA`{G6UOD)-w@`j>$kB|Prj&2Wmldp`M&knpuqn{^R5mw%O(c1>)O6R=0& zx9AGGCWfWaSpdRx^t6fRh(lK>R>%l2*<>&D=auAdy}0Wu6PpLz3y;%-|0Tr?!0B z(Kb;_(RVYgH$1OgtPc<~Ei*B4@ zyB44GPL$&od2ne(MTHu-!)e>j$PMVPXvTpEs=51eRX>p^-1RsWCHhbYlceKSpY!`t z)dUUMs~Zy^f3NqJ$W9eU3^9?~vFqsqC}8YkS^WfjENf{zlDO&E_d;1or%)(WB!-b+ z7y9_fKwoiQzFk$+DM2x~!8q>hS!nwg1@K1E?bYkomcxb#Oh2c)4taM?*OgUP8j9rx zj_@y2Obq+l$$Mf|c7tfbA`}gAad#?)SW;)h-H^Fy2r0#PsBTJN*;#eE$g~nV-+M1I z9-(+;`S|lUV*4#vza{3m>o5>fuF}EtS{McbfmVG4wqs0YD>-w%l61+}Rd14?&s|SL zM3G=XsIs_kS(DS*Uv0v0OUrWtI$ai4PwRPfzpU%mv3u1DbX8FzMW=b$biyk#r<3UL zJy@1Tjw97!3=16dmrG?$%*~$^6_IL1!P7i`>=+eW6nlV&sWB|tU=D0Cv0SxDh(naj zC5dr7WWtpzGb>-3pPPA#-npW*`r2jMR1vp;Lpu8z(kgeQBo-OOs5{0L{PhIq=h&4GUN10ydt(ql3{Td|*W!?dUYTTCStpfV zZWldl48-lRpddV5ebr@K<76s2*i7;p9?V303-%z@oDaq44|>}hGd^uOaNq?`FTh9S znDUOoE!O8PB7`%Ra=_M|$zoZG=i@U8%BgYXo2=5Ncr@$u_}f({~NOs(kge)oi17-pm?YEiIn4Hn4w_^B*MuXcE?)u)ZsIU{L+>QsUKF8#6EJ5|qE~$}G+x3{0 zMPR-?tNA9U{VcN?>(*I&$Sff*0m}ct#ut`$f6@{q>v@sbOAu}Zyf|@a%7qI$)()FU zL}(H24*l9=2zr1ARRNr%O0Ax0{B6kk#Q_0LbO_FEvy1?a!wKTdHy6$9L)+vk)|k2nziuSqVyk$*b>G08TpA;U>sG z@|F7fSKLrZG<5j4MeB8IB%Kw?w@x-aKL>+Yu8_)4q+mnLWsJhcT2^VIySuu0lU3h$ zJ-TW3L3a;i1wpH7$mz7SnRDiJ8+}XM_VWjcTbOKWOVb^-AFZwJ*at2)_IdZWbBY*Q zY+U^M!Ct=+r?2>>P!nXyyBGCsqUmhI#M9{1+$+in6Pq^LP&SNkS--O!-o zWwDb=u3Z7>E1dGlTE=pTu05VNMj8=v937KP54n4LQ|eU3IZJo59AM}{J{urd;Qr{Q zZNlmwxTf)?9`#tMa6AJSjPA21*O#2JQ)l(4aYs;51X2qNwRVp+pa#TydbjyXHPqMK zGTpX{;$lTF(a&z(y79_5(2||x`(Y*UYGG6Dm5o2F#d3n&Nh0IbJ^{cqc1{ULoWB=!NfF~LcqNq+S*%k&T$F%el*`R;A;hk zW7ys#KOb8531}2sg%k;$jOoCz27P-B8^sq zwYl%mp_kyHsA6DW@L7c-|V@9M-cB9{{B0KwS&aw){7UTsP~3e!dn3t4hnhHmqUzX zg*Ky*a?T|IUb%R&VwP_tyxIe+j9~IXMF2k89S|24REo!e!0wES!6_}Z{TP;MTRWSj zchL2Olh%CHw3U_g5+lQJ`BWCEZK8Kt{Rkx_oedC6>sLLYY*C(f(#!q$~Njl9r1Dr%}%&xV`iL$Pk_-mSRP0q_guzd}4RbM1f&xM9ml>2hT zUu?s6J-e~Z#hYY(Qh9kZU{oTlr{TP$g2kMT>O2KMn43Micjq?guz7MIzg-@F$-kUL z@NHe4te@cn3OjhMKYttR;(>mfGw2zepLVfZv%H>OV%ztdZ4v3ZsGZhepTO?AfSvA$ zgu&rAZ_{cKq-&;Iioa1K+*@(Q_yBQV_2|9<)CUV*b`}-#{WbvW!m)EmZ0H5MgfL zthRT38fdQkNySKQpr# z9-YtHab))T3>F1~`6 z$Y#~#0b8*n0!4*E(x4mqI9-f<8*C9 ziGpA!x}De#E9%30lbZeokqYwi_9ol$PWXYnA82Iy<;%snxe8gn&TzQ=)tbBFOQA}I z^qIeQ^lXw`y(*|kRCTeNsO$iQDfQuqHNB`(Ott{^A=ZL2MzHE=$id)DGO6y_tJfif z3qUyw-l6G&5-6q??OIqE#u}6(wAW&ewmk>Z4sCheBj$TZ{NUO95);c` zAN5v?rVQKSl(Kd<{Q9?v8$$8voF3Re)ppRBF*gMJh-w{55t|{u&Twft7nybU!2|l> znpuvYNvMGHy6L*lp1sW9-&|W6(8BSy@bWH=!qh-~`znVit96ELv}$F%t_v;o6xE}ylP7z`9GA=WZLieFUInYEnJONJ zuD-M7FTeZ%t$qDT^Bz63Vz-DW$13zN@L-i$bwFn+KYQ6LUT3|GdT$U73Qr)9us3v> zI5BYT+D7EKJ?2I}URYXO411FX?h1lDW;45kkNXMZ9V%`bWI1l!j!uq9f4(sUOX;>x z8KL}@ljPccupV~q&(&WVO2@aQ;C#8i6$G!Bz6)`^j(T9h)vGEJ=f=ucudr&B^(x@7 zzZjYTu~Hhj1{*W0Z_>% zPM&PMsLK#X=`Y6~8L~!b@iW%Mz;>MnH9(&}Z|SuyT}ruP{iB)egVY{K8-69t{q;Dd zhPHk8hBQFZ^T)%h{msk-a!r<_Yk<{35F5GBe;C)n(-Ks8t26JGo_X;W76<`0|C&FA z30TqNZ9j!{0IN1$_?P@8i(&GjT7;8u7`un{)t^?nJR8WnxLU8+;GKHZH?f>0zr)uaI@0(!v6kJH7 z!cWaTbNRAvlUgpnmsz1aecCkO!Ay=7<)8%Oo2lu^__h|%fBLmi4K3bs;n~lgorf3s zWGog$^pIwmX;v&gfAJz@GtfJXtJk*838iqRg@vjVSD^dGjT`WjHRQ0fvYP2|X#Tnc zF4p_{y1fU7oD&+Pv?{VT&7fqc{L<+L9flF}Zb$NMs~e7^ZfWEu&Q7WC1@f5Px9iFt zG9S9>+gO>JPSRNDq&*g}Zp`7fi*9@CaN@0I3`5EIO<`n!*wG1ylb*}bLYUi5p5jT= zcyzz$Rbh8$x%64uygQpWyuWYqD3P;tb!E0}&nKI;%gDr)4ysV2Q?TFonSnHyrkTRq zx;@kpbdnaoZ23+(K{PA4QvH4Uj2Wywdg-{z%$q#<5ho0yz|TMJye3Sz52dBN{0DBU zY-JuOaT8jR~l<`!PqsAA2kPuRMi~0jYQ}WkO z^YrA~e?5QlJMulAwX>0iPegqD^JFK9OeYss`yXJNsMPxgd{JKoX0}nJQ`W|-;nZ~VlD=Xke z;A`dSs@rswqke~Y@awOR$?nFQ*MpiJ2kkxk!VeT*P_#CbdeT76Pja9=L<;Bp<1qbv z>wwBIxp&6(a6b8{TUqprU+B|8iObiKAsG`o0$$A%q3S>ty!!c&##*)OIhAU!wPVm@ zz@$%kjL34>@ZpmuPCR@5{M(=UmF8VlRsUxE(G~f~$>fT(w|3HKCW*Q59Y-bnd8U-y zy1w;3UHpsZL{ZRjx@&7Y3o_0DD^@g)6jlem^%c3fuQ4P}O;oLGXaJjBSDm9&GUFNo z1~%BumN&giX1rBpW5`L?HO|kA5s@Cuy5ygjq7sc?4sVFX@$KUhsk|=lx&I=YoT`9> ziHV`D&Euu&o$p8<>N?GuHOj(b<@;AyJq$%YK{wK;H}po1oq4_UZuw@CWL%zOP9Gz! zoHMz-M;D$JZ<1i|nHRyE1oF_jFstS$qB@wjr;6I5pVn#x{Z!nRE8xSAuXY8FpaZqm z+S6m^5Q#HVi+s^X)SD5mi%m46L9G?MWNXOu1o^a1!9j2uz9OXoUu=x% z8fuN~#d&nna>h(@vm23+7!%{ObSYLQ>PsP#GpZ@sRGV|A1_>ptX*HlH3Ms|1BJ7>M zzN@IFd9L|&+;vVqy24a3=T8sNjHWx7I&I)`(?Ms+UfISDp7{$3QzuXea3K`!O2OvQ z8#!i34*3H?tBuq^s@xd`za=)(sGsaTvrEq}5wz_-VIG4nK%{M|oG^f;L@(D?Jl@PP ztt&w)YfZy%Hd72@AdL%UI+w?2B-hGgbtIx33Yjxo-x<)|#dKiGy4aT$h?-Cl)s^j7 zU3FWbEO^Q;s^E6b(jTYhT+y^n?$k8Cj}Mp!zl;zp4@`#Y?v&_eKYt#7K2%9guI(rt zQ-;6@;&&g*yaECY#8aHZ1Vn5u^nyolhUC4HleoinN26Eakg$#%=_}4nz%CgGdj#@~oR>o!Y|p{Jomb3CDTi-|mZGbd z?7&f@uADuq$seVR-A3ux$?KMfmA(;K@uYZsBWGL{y}rfGygu4qquyptqm)14}m zU4>V?^+X^2DK;;MM{Q*P2AxcwTwGl2S8+oxD@BV~XNCG|{m)9;*gTKe?C~dA=TaMs zEO1&-pslMG4EtfMc^|6BycJ)M#+^L5jDs`Tv;-F_t*`eEE<}KkuhalU+$^7B&Z{hd z`nF|spf~&G06?K=(NYgjPwb3gSb*j1G6z-Nw0FOy!Kdpt+YOQj9%hx+zrp3z{dwiSz25FHbE(L7vG)ZOHc5q zyGZQh6E{3>LtGrZF#}q8o~?af1_RTh6?=IrDgAz!>J=F&)3;LGN3XWMu*^l zCAn6|Tku+cGZS_f3G{(4Vy%S7%u^uC7{1(;9tutou@_%!k6*a(4JkiXe)FM)tgNQF zyryGt+2-^x$}GY9kw1#-Kc&5yqtLfX8t#t6`y^M_{!K?L)yZARPH2Kb{yY$q?`dDLOky%RYw zk-jaM``46@Zq@sScpr+7AJ2oVBs%a2l|L=eT5?jfK@v(f4z$e`M-<8*qVG^(8P zc~~Zb5=~d#R&RRypMa;cT8p9_lY!hS=B^oclkA}2%8~JA-5J001F|UxO9etu+FIn5 zcpV<*i5HiYIC`EJY}Wc386}yzX!Ptk;n~gCSR&d#>-p0NT-~|#kzGNUOX=zDT|Cyx?vyANGR5V8hggh}xw$`FzkHoWQ??N3 z12k3eUJCY2>Gpz65N&L7c-X`alDxf`C9v~C;!8IW=KOjLp(i0iKUi^ad@_ak+k~IZ84BFg0qC+;pj?b3AGZy$e z(IvgDHns+#Sd}Vo#sy_yv7aEUMKZu@!^53p;K1tICpc@;^h+{TMC9)DCQeRqU=krY z@XDMrCu385?&p|)A@|M~Z9o3!pBytMX}asZ?;q`4P2UdZlRM9$T%Nyl$)xGwG2Q~V zBK~QPZCFiY+A@IjcF<5JW=$;S@0+?Uy7%6peQrtTtQ2;EU!J5!WCTb6r}62*QLvJ= zVTIc0yBY1amxSzRmuB4fR?q&-7s)Gi>jv;h+go^)NaSHT-DDm)qV&fB+19vvN=$)mpsC zBh#faYDeE`FL`Ps42ua$ZS2BR(_l>r*rS)jRy<$2cI~>`MP9FPET|GqpU8B>JTN%H z!%M-D{AePBwTg??H@>v1B9{6uTehuM<$exmRE^MqD2PTH4nOR3{2IoSY;-GeSQZAK7(j}3PL<+n)*X$Ivb>yBeHzc0Crg{#q2n-y^XfHc4yWkf zB_+z`w})y*zXBa<{`hf0*_#=Hoy4ay#bcvMsn@NG*t}~O!1lOZ<^{j7p0S!vT9r#3 zMbT8V{7WNc#YM;iaGVCb5u85wg}sSb9lBehy?Noz62pdVk^n1xxr?ZY*;7uQF-#xB z&H&FiM~V13D43v)bri9Wfg}KWKjG2}MIwX9x`Dv?b>_YAsDyshpUjY6lbds6hrX>f z{9O=S$+Z*7$@4JsvHkHX$z#y!z`$?#HmnoxO;=J>g!QDKXv)G6>?z<1i_ISZ{0)Y8 zDfniH1UI%LuyDKz(rK+7Hd`;Df=?OaOmAvGybrO$e8j=A5fP}O6vYYyjWv~a-D5(? zX4Ty)44nJ8-2LoP=&!XOIGd7%i)A4J`Dt8GIyjU5;Rp*0w0~i$yR|xX?gnL&R+gbS zFM$LP%QB~@mjsIkDpcRh{hKe0$z#fdwuXkq(q{v#du(X&DiXMmbM|O(A_zzE2dB_f z-bDEq zki!?|GG-zr`__ljp+lq(mFW1YshKk|3b+Q}UvB(DcW4Xbpn-rKQgs({Eq3kZbg7{b z>B7aFB%58OVfSiO^ldm0dj{CZ&nKnl>sed*qSTn@rd2)HDa?LaP*j9z%CA$WhEs=J z#{^Ky;f*8S@jE{=pbg<623NS|`>#DqP~5afY-^a8n3hlZkV1Sm`Y=$3lRw7tEr7@( z`~(@)(^lxbd*V?{FZNr^@BD5W4-pzxMEYV+Z*RAfw-NRnRUke50j+jMBFK8(>c%=v z%E&m(;G5dNk|Vn+Dk`d~ax(*&A$#i$o!F2>g^l+6*hv$@2S1VG6#-XA9N|(;!nsAj3PdD5G zbSoc%ptsT)QY zLeU-nw3Z{d58;Qbko7CS(ejzoMgMZvyfWPcYTElh?pj3dr*FT6$bwzbH=3z5^xdR= z{@9XRO(g{SAA)sWVm%=ro*p3d|Ar+0gtHQD#uyXNySPTACj4;fGTQV5u;Dx|Y~gjgv+2TfcGR zC-Zf*8zyuwAx|ST$y3M>W(59udeH9>4hf<8I*Ganoiw;Ft~I!el=o+IOgeTT=?Y1l;86|7R#fXo4evfkSjn$04aF8$tCuk4_S*wT`M92 z+LjI_`a!{GIfsTll|P-4u{I#Uuzu77B2cTKuhL1}kB*D`m+h1~t&U1_Ok3+J+UUvd ziZhK&DOng1@|8Kl^g<+~yN^}~eD~LA0X;>pm#;JVJuE9)`6LteB@=3eYuBSk94tio zff$92V83xLIZ3j~*4{J+QNl;5ilRUXLqe`aMG2O)($lo~F@;@6qJ@r!w_snJXAec; z(xopr@1r*Avf40n(|&4VYNI4mYkV@OkOcXGG)7h5ZsS{Ew>KMYfiMvU6f?O(TU|W~ zJ6mcN4#C@Z#ZwS)qWvaJ3}s{@AXBxEba{}?q4acf@z^e$Z~01(oJxlLK^cN^wOFAb zz~)&E)(0-VH+It8mJ7Tf7cOHhAM0XFKJOnKJPH8~gBIHTn$;Z}3!gosxd*zCithvU zS4^~9Fon@8%xo}#0#$j7LtInq^Ft$m{b2G!xK(7qQ!%A_o%%5Cf4V~JF9>Mj0zSEM z#!wE9`To`t|=v4g(B?0r}<_@?+GOl6DYk4pW})D-3_wKwa|FknZ|p-+j2u$95E zW%b>E_*?1M3l@pY6``j&kc65nopqG#R7u;Kw5FpzPGRkkPxT$QT~N3ExK5&;j^t9B zpG@?7-MmsDF-je1;Gm@#SnZUpVT?C_<<>z&*ejPiZP3won2J7Jfz+l_ivS#smw*eXT(ICROgT(qbbZ`STzLw9=9<*=JT962jQL)_L({LtZu(z@*$rE z3(nIH0nXXc)3WpDQuOxqU}&`4EEheg^j{gN9#Q!!8p=eGA{n*lD}$22+JGnRpd!2js0ljdQQAIepyInd=R!N!AZ< zY^hN1)obh1;2SEgnicLdXIjm_+bwgZKtZoJhgE`)7=Gt7+}xNtKaXf7V+Fpr$L`K(>! zE8)QtNA~Ue+wN!cE_TUh5N~W3fOsL>%x|;XngXWfY(!m)!x2~>nS@}p!sKlC0U<4S zyS4r1pt$-Lq+@y}tHt8Zbmx4sWe}5s5{I-l-@}pm$gVL|%>*3HXa*Hs!Pp-dl!S(W zBK+X&X4|KwW&d+m-SIsxYr~G6I5CT+AKWuN$E`?3gap3gNa!+p9PGk~bVHY*pv&0i zbM^}6w~mSf?Sy97u@uFJHGLqpp3LIQrh}z+@*EfYS#x%});7 z;cnaU&5rLew;z`6n@O}GQ>EM$X+|R!0PatA(w#tbMhH(`KmrQFA&hb&xfJsF_HTCa zAILV0VBsEAx!!-b3)S6|%~_Mpy7PwG1d5U<)qVPOvQ@Hpq4nQ!wO68QlHZkJeAaFZ zH}JnCT=5)KYI^g>fgZ^)$SEkp`lxmx6YqFXue5ebvU1TAw3B?~AuTSSGY1ilOHRdVW|z9Mn=z3nPdus7K8x* zJ)F+Fj=f8P69Ba1=&5czXwZZE_m#37RgueM6YV6>%p9Q;iQ;@oNlVMaDdNhNp4QI(OkyFjGyjLhq}#J6tq~(m-Fgj&eyPaE$-GGf2_gTz-n(~-Cclh_;g9?k!Wv6zs<-zFKfhMe1P93g z_3qoqROjPt!z0g}>9=(0Z!jSL&m(`dxku^E9%M@xA+x9`y0OmKaZdCHfq*3YeM<;8 z4G)!8->!BGv45AwewBMpjksQPQ|tWG6j~qREUEHXV_{9s6|^ym1EF&re%dAwzW=v; zx7^}RzYyg~C52@{!%t}YD(Cjoj3&-VW?!0%5!!u9z6wru#ees@h0Y<{k0Z%Rdg$s3 z3>q4<^chi(FQE+8(cxzvxDhoA%r+s({okU=SP@Hx``~SRD-OdK)>rKt$d+X>#olAZWapuO?>aMU3oeb4y zGP20oi08tb2T~h>1=O)YKMZi{qW&N+Z$Q6(aQcL`N=*Qtogqa8dnTSVCE=yXxVs5T zdBXu%L}p(_1NBU?Q_dUyGKrKfa}+(-v$nxVC~$_{zLv_ z)rS$}c3(+av6;Q$UvBN1Ws4(x5!AV7&x||9ylO-QOEW+&e?tZv8*8MOvXy-1h2KSF zVN4VErXZ>c2+G~6Qz-hA`RAmg*NI#mgUFpbze#SJHzMK3#x~BOqfQ>^NaG>J>($qH zjdBDT$H0D{>eQ$^RXjpCSaIzmvcnX!Z9Z;coLEZ;2NwS<#G9#qoEssdO5Me%B0;_y{`n_Pb?VqPYd#}&6m^U$9#r@)_w$H^2C_QZlHB!WOxs|G z3MI(|iBHbQ&!6XB4oGH~-Tcr6<}fMywYeD`?>cSlkq`N}kT80R8dG@JB_-bqCk<&{ zxKCOHnoDZv?rz8vNqF2{RpOK!)A6wZ&f7l3{I&E`O9+WvEO!pLOpMi0l^k^%Itbs3 zQg$x1l8KTuU%H)~0I+v={QH%Tt3Jf!`r-kefT^i?%OH5kX6$x}x)4$_Bf&mPrVy67&~!ZJN->VZJ= z+T2E1rGqR8)N*rjLcn-OBuwFRw3I<1J_gwoigc&*FVZPv&E^7yv19o8X!{gs z`~5TCB!h32G_0vpj$)+9Yt*c<>De>C6AQuhii(Q|uq)XVba^N5AL`KY?v0oD>cd}S zj@>zNlp_)<=$?jNvORew51&4*eVHyH^_dnSK;>crmO0GTe;RQoJx!P&FJ z>XcowHhf=0D#rfj8B_CBuDN>DRCp5h+vi&Qh?zg24<9GXQABAA>>ZyTrg>@x=57%$ z{Mg;OP(uWt>W(EQm(t?<9rFKQuLFB_D+qP0_ofZ-96UJHG-)f2x;S<)XKet5P}W!_ zwm*ji8D+;l4a@9);X{7cA=d)9a#XT)>c3(;epaPw2rV>?Zk=gJt)B6$3*`bi{dp+=#rAV@Sdf-Mh5U!p<&^zFp)*nW-W# zN5RATnmX{AMXP%;r%zV+GZ$w85W_o$qqBR%pid3#g zk4c_}!=C;9_itwV6uNeIkqi(q5|jV~^WymN@mUJOC&zhtTwkAtItf&%Q0Gr8nwo6Y z!Cv7m$U&eVyb)Wo6IX($)E-*D)6!Y^Ov>Cnb37SE>1E!dG~Wr~?}Fa9Ye)_UnVG#> z_(gJPUH!ma_u>v7H0yYJM@%QaNd|pg^{OhF9vq{J?X%=GmIMUk`(N_T5(~kE(ge6w zm{Qeoeoxg_QHx(r(ZPBy)R`z4&*Kw-JH{ZrO?eAPBw&ca@Ke?j6HH;vbneonW1;(a zIJeq(%$d_lnl!;P%@=a4dHkd-xdLHa5C=V`FMON-rS){X3YmE-BQtc6(R%%}r49a< zN`1D(j(>G12hn2|fIkImox1ZbL^t+^L+RA=P_dez!}jk>&f8YGtpSO5E_#LQW6t(S z*WkFKh%Xhw-lnubs0W9iG92?5=}2AoenhW(OdYh*?>LQh=k2yuqh_4J*vxzPCQ0%} zgH8fYu?#tR27F{+G7JuvurSMYYG$*}rV=DkV0Y}xanqdjuQNP0+@3nYMWWbSS2qZR z5%}1I&$13Z=FWXG;d&QJZ+e`)l%qO!r%#Ua%UauQxzoJ8+a6gy<%2X^@Ml!+98oCB zj|9uuw|6g3&8Uwm<-$!YdjI{pagnxj77jZA_67oZ)G3nDXNwR~gAMVyt?mQ%z#KXE zf|cnrych5#c;usBzqBtDr^v@n1U*_-g*vgcRL}BYA;>jd(VqY4pHENz_{FsS_)}t= zJOx~#p!)Yx<(&&{K7xTV;BJ^zPG z9@4Si+y?o+wEajIQ5fEyN8kkO0$Am{FUmNYV zP}}gaKp2S;m#(|)X4}v8$k0)Yhu{xW*S%6B?!E^~Th#b^DUl#pcxO_%&gytTn}-a0 zlT2Hwtrd*0y5i>7FDr)j+ELiDOPHU)X_S@q4Z0DhX516mpW(T_OZ@x*5((0dK+hlu zcdE{GgrjcL7d&!u^M`hLM}I#(O}wVJkAo?|5Ab%a@k3AHEk_i=sEMC3)u{a-c#`z} z`aOKA?}gtwg@@((?&hBzpv^3^y6o(*fB#tt$G--{r(1n2hxOV)E=z3Q^{Iy70VSYf z!OT3W7R$E~w%8z;6-e20>S=0t3|@OZ+iDp%-LKeQ3`s z{;&I^lF)Xxazk;raG7C-q5n9TZ~rJgxoyuqboyyUdRw$z=W}`&gqUmoBltDo%sR) literal 0 HcmV?d00001 From ada8e2905efb0b9e30494e092f32171830031ac7 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sun, 5 Apr 2026 01:56:34 +0800 Subject: [PATCH 5/5] feat(api): enhance proxy resolution for API key-based auth Added comprehensive support for resolving proxy URLs from configuration based on API key and provider attributes. Introduced new helper functions and extended the test suite to validate fallback mechanisms and compatibility cases. --- internal/api/handlers/management/api_tools.go | 123 ++++++++++++++++++ .../api/handlers/management/api_tools_test.go | 99 ++++++++++++++ 2 files changed, 222 insertions(+) diff --git a/internal/api/handlers/management/api_tools.go b/internal/api/handlers/management/api_tools.go index de546ea8..cb4805e9 100644 --- a/internal/api/handlers/management/api_tools.go +++ b/internal/api/handlers/management/api_tools.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" @@ -636,6 +637,11 @@ func (h *Handler) apiCallTransport(auth *coreauth.Auth) http.RoundTripper { if proxyStr := strings.TrimSpace(auth.ProxyURL); proxyStr != "" { proxyCandidates = append(proxyCandidates, proxyStr) } + if h != nil && h.cfg != nil { + if proxyStr := strings.TrimSpace(proxyURLFromAPIKeyConfig(h.cfg, auth)); proxyStr != "" { + proxyCandidates = append(proxyCandidates, proxyStr) + } + } } if h != nil && h.cfg != nil { if proxyStr := strings.TrimSpace(h.cfg.ProxyURL); proxyStr != "" { @@ -658,6 +664,123 @@ func (h *Handler) apiCallTransport(auth *coreauth.Auth) http.RoundTripper { return clone } +type apiKeyConfigEntry interface { + GetAPIKey() string + GetBaseURL() string +} + +func resolveAPIKeyConfig[T apiKeyConfigEntry](entries []T, auth *coreauth.Auth) *T { + if auth == nil || len(entries) == 0 { + return nil + } + attrKey, attrBase := "", "" + if auth.Attributes != nil { + attrKey = strings.TrimSpace(auth.Attributes["api_key"]) + attrBase = strings.TrimSpace(auth.Attributes["base_url"]) + } + for i := range entries { + entry := &entries[i] + cfgKey := strings.TrimSpace((*entry).GetAPIKey()) + cfgBase := strings.TrimSpace((*entry).GetBaseURL()) + if attrKey != "" && attrBase != "" { + if strings.EqualFold(cfgKey, attrKey) && strings.EqualFold(cfgBase, attrBase) { + return entry + } + continue + } + if attrKey != "" && strings.EqualFold(cfgKey, attrKey) { + if cfgBase == "" || strings.EqualFold(cfgBase, attrBase) { + return entry + } + } + if attrKey == "" && attrBase != "" && strings.EqualFold(cfgBase, attrBase) { + return entry + } + } + if attrKey != "" { + for i := range entries { + entry := &entries[i] + if strings.EqualFold(strings.TrimSpace((*entry).GetAPIKey()), attrKey) { + return entry + } + } + } + return nil +} + +func proxyURLFromAPIKeyConfig(cfg *config.Config, auth *coreauth.Auth) string { + if cfg == nil || auth == nil { + return "" + } + authKind, authAccount := auth.AccountInfo() + if !strings.EqualFold(strings.TrimSpace(authKind), "api_key") { + return "" + } + + attrs := auth.Attributes + compatName := "" + providerKey := "" + if len(attrs) > 0 { + compatName = strings.TrimSpace(attrs["compat_name"]) + providerKey = strings.TrimSpace(attrs["provider_key"]) + } + if compatName != "" || strings.EqualFold(strings.TrimSpace(auth.Provider), "openai-compatibility") { + return resolveOpenAICompatAPIKeyProxyURL(cfg, auth, strings.TrimSpace(authAccount), providerKey, compatName) + } + + switch strings.ToLower(strings.TrimSpace(auth.Provider)) { + case "gemini": + if entry := resolveAPIKeyConfig(cfg.GeminiKey, auth); entry != nil { + return strings.TrimSpace(entry.ProxyURL) + } + case "claude": + if entry := resolveAPIKeyConfig(cfg.ClaudeKey, auth); entry != nil { + return strings.TrimSpace(entry.ProxyURL) + } + case "codex": + if entry := resolveAPIKeyConfig(cfg.CodexKey, auth); entry != nil { + return strings.TrimSpace(entry.ProxyURL) + } + } + return "" +} + +func resolveOpenAICompatAPIKeyProxyURL(cfg *config.Config, auth *coreauth.Auth, apiKey, providerKey, compatName string) string { + if cfg == nil || auth == nil { + return "" + } + apiKey = strings.TrimSpace(apiKey) + if apiKey == "" { + return "" + } + candidates := make([]string, 0, 3) + if v := strings.TrimSpace(compatName); v != "" { + candidates = append(candidates, v) + } + if v := strings.TrimSpace(providerKey); v != "" { + candidates = append(candidates, v) + } + if v := strings.TrimSpace(auth.Provider); v != "" { + candidates = append(candidates, v) + } + + for i := range cfg.OpenAICompatibility { + compat := &cfg.OpenAICompatibility[i] + for _, candidate := range candidates { + if candidate != "" && strings.EqualFold(strings.TrimSpace(candidate), compat.Name) { + for j := range compat.APIKeyEntries { + entry := &compat.APIKeyEntries[j] + if strings.EqualFold(strings.TrimSpace(entry.APIKey), apiKey) { + return strings.TrimSpace(entry.ProxyURL) + } + } + return "" + } + } + } + return "" +} + func buildProxyTransport(proxyStr string) *http.Transport { transport, _, errBuild := proxyutil.BuildHTTPTransport(proxyStr) if errBuild != nil { diff --git a/internal/api/handlers/management/api_tools_test.go b/internal/api/handlers/management/api_tools_test.go index 6ed98c6e..b27fe639 100644 --- a/internal/api/handlers/management/api_tools_test.go +++ b/internal/api/handlers/management/api_tools_test.go @@ -58,6 +58,105 @@ func TestAPICallTransportInvalidAuthFallsBackToGlobalProxy(t *testing.T) { } } +func TestAPICallTransportAPIKeyAuthFallsBackToConfigProxyURL(t *testing.T) { + t.Parallel() + + h := &Handler{ + cfg: &config.Config{ + SDKConfig: sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}, + GeminiKey: []config.GeminiKey{{ + APIKey: "gemini-key", + ProxyURL: "http://gemini-proxy.example.com:8080", + }}, + ClaudeKey: []config.ClaudeKey{{ + APIKey: "claude-key", + ProxyURL: "http://claude-proxy.example.com:8080", + }}, + CodexKey: []config.CodexKey{{ + APIKey: "codex-key", + ProxyURL: "http://codex-proxy.example.com:8080", + }}, + OpenAICompatibility: []config.OpenAICompatibility{{ + Name: "bohe", + BaseURL: "https://bohe.example.com", + APIKeyEntries: []config.OpenAICompatibilityAPIKey{{ + APIKey: "compat-key", + ProxyURL: "http://compat-proxy.example.com:8080", + }}, + }}, + }, + } + + cases := []struct { + name string + auth *coreauth.Auth + wantProxy string + }{ + { + name: "gemini", + auth: &coreauth.Auth{ + Provider: "gemini", + Attributes: map[string]string{"api_key": "gemini-key"}, + }, + wantProxy: "http://gemini-proxy.example.com:8080", + }, + { + name: "claude", + auth: &coreauth.Auth{ + Provider: "claude", + Attributes: map[string]string{"api_key": "claude-key"}, + }, + wantProxy: "http://claude-proxy.example.com:8080", + }, + { + name: "codex", + auth: &coreauth.Auth{ + Provider: "codex", + Attributes: map[string]string{"api_key": "codex-key"}, + }, + wantProxy: "http://codex-proxy.example.com:8080", + }, + { + name: "openai-compatibility", + auth: &coreauth.Auth{ + Provider: "bohe", + Attributes: map[string]string{ + "api_key": "compat-key", + "compat_name": "bohe", + "provider_key": "bohe", + }, + }, + wantProxy: "http://compat-proxy.example.com:8080", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + transport := h.apiCallTransport(tc.auth) + httpTransport, ok := transport.(*http.Transport) + if !ok { + t.Fatalf("transport type = %T, want *http.Transport", transport) + } + + req, errRequest := http.NewRequest(http.MethodGet, "https://example.com", nil) + if errRequest != nil { + t.Fatalf("http.NewRequest returned error: %v", errRequest) + } + + proxyURL, errProxy := httpTransport.Proxy(req) + if errProxy != nil { + t.Fatalf("httpTransport.Proxy returned error: %v", errProxy) + } + if proxyURL == nil || proxyURL.String() != tc.wantProxy { + t.Fatalf("proxy URL = %v, want %s", proxyURL, tc.wantProxy) + } + }) + } +} + func TestAuthByIndexDistinguishesSharedAPIKeysAcrossProviders(t *testing.T) { t.Parallel()