@gajae-code/ai 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2644 -0
- package/README.md +1181 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +66 -0
- package/dist/types/auth-broker/index.d.ts +5 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +96 -0
- package/dist/types/auth-broker/server.d.ts +32 -0
- package/dist/types/auth-broker/types.d.ts +105 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
- package/dist/types/auth-gateway/http.d.ts +39 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +17 -0
- package/dist/types/auth-gateway/types.d.ts +115 -0
- package/dist/types/auth-storage.d.ts +641 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/index.d.ts +49 -0
- package/dist/types/model-cache.d.ts +17 -0
- package/dist/types/model-manager.d.ts +62 -0
- package/dist/types/model-thinking.d.ts +71 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/provider-models/bundled-references.d.ts +4 -0
- package/dist/types/provider-models/descriptors.d.ts +48 -0
- package/dist/types/provider-models/google.d.ts +20 -0
- package/dist/types/provider-models/index.d.ts +5 -0
- package/dist/types/provider-models/ollama.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +237 -0
- package/dist/types/provider-models/special.d.ts +16 -0
- package/dist/types/providers/amazon-bedrock.d.ts +36 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic.d.ts +188 -0
- package/dist/types/providers/aws-credentials.d.ts +43 -0
- package/dist/types/providers/aws-eventstream.d.ts +38 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +15 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
- package/dist/types/providers/cursor.d.ts +42 -0
- package/dist/types/providers/error-message.d.ts +27 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +72 -0
- package/dist/types/providers/google-gemini-headers.d.ts +18 -0
- package/dist/types/providers/google-shared.d.ts +163 -0
- package/dist/types/providers/google-types.d.ts +138 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +175 -0
- package/dist/types/providers/ollama.d.ts +6 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-codex/constants.d.ts +26 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
- package/dist/types/providers/openai-codex-responses.d.ts +67 -0
- package/dist/types/providers/openai-completions-compat.d.ts +25 -0
- package/dist/types/providers/openai-completions.d.ts +33 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-shared.d.ts +89 -0
- package/dist/types/providers/openai-responses.d.ts +32 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +68 -0
- package/dist/types/providers/register-builtins.d.ts +31 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +12 -0
- package/dist/types/providers/vision-guard.d.ts +8 -0
- package/dist/types/rate-limit-utils.d.ts +19 -0
- package/dist/types/stream.d.ts +24 -0
- package/dist/types/types.d.ts +746 -0
- package/dist/types/usage/claude.d.ts +3 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +2 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +2 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +258 -0
- package/dist/types/utils/abort.d.ts +19 -0
- package/dist/types/utils/anthropic-auth.d.ts +31 -0
- package/dist/types/utils/discovery/antigravity.d.ts +61 -0
- package/dist/types/utils/discovery/codex.d.ts +38 -0
- package/dist/types/utils/discovery/cursor.d.ts +23 -0
- package/dist/types/utils/discovery/gemini.d.ts +25 -0
- package/dist/types/utils/discovery/index.d.ts +4 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
- package/dist/types/utils/event-stream.d.ts +28 -0
- package/dist/types/utils/fireworks-model-id.d.ts +10 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/h2-fetch.d.ts +22 -0
- package/dist/types/utils/http-inspector.d.ts +31 -0
- package/dist/types/utils/idle-iterator.d.ts +67 -0
- package/dist/types/utils/json-parse.d.ts +10 -0
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
- package/dist/types/utils/oauth/anthropic.d.ts +22 -0
- package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
- package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
- package/dist/types/utils/oauth/callback-server.d.ts +57 -0
- package/dist/types/utils/oauth/cerebras.d.ts +1 -0
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/cursor.d.ts +15 -0
- package/dist/types/utils/oauth/deepseek.d.ts +10 -0
- package/dist/types/utils/oauth/firepass.d.ts +1 -0
- package/dist/types/utils/oauth/fireworks.d.ts +1 -0
- package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
- package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
- package/dist/types/utils/oauth/huggingface.d.ts +19 -0
- package/dist/types/utils/oauth/index.d.ts +38 -0
- package/dist/types/utils/oauth/kagi.d.ts +17 -0
- package/dist/types/utils/oauth/kilo.d.ts +5 -0
- package/dist/types/utils/oauth/kimi.d.ts +21 -0
- package/dist/types/utils/oauth/litellm.d.ts +18 -0
- package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
- package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
- package/dist/types/utils/oauth/moonshot.d.ts +1 -0
- package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
- package/dist/types/utils/oauth/nvidia.d.ts +18 -0
- package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
- package/dist/types/utils/oauth/ollama.d.ts +18 -0
- package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
- package/dist/types/utils/oauth/opencode.d.ts +18 -0
- package/dist/types/utils/oauth/parallel.d.ts +17 -0
- package/dist/types/utils/oauth/perplexity.d.ts +9 -0
- package/dist/types/utils/oauth/pkce.d.ts +8 -0
- package/dist/types/utils/oauth/qianfan.d.ts +17 -0
- package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
- package/dist/types/utils/oauth/synthetic.d.ts +1 -0
- package/dist/types/utils/oauth/tavily.d.ts +17 -0
- package/dist/types/utils/oauth/together.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +44 -0
- package/dist/types/utils/oauth/venice.d.ts +18 -0
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/vllm.d.ts +16 -0
- package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
- package/dist/types/utils/oauth/zai.d.ts +18 -0
- package/dist/types/utils/oauth/zenmux.d.ts +1 -0
- package/dist/types/utils/overflow.d.ts +54 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/retry-after.d.ts +3 -0
- package/dist/types/utils/retry.d.ts +26 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +49 -0
- package/dist/types/utils/schema/index.d.ts +13 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +93 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +25 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/wire.d.ts +54 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/dist/types/utils/sse-debug.d.ts +10 -0
- package/dist/types/utils/tool-call-healing.d.ts +71 -0
- package/dist/types/utils/tool-choice.d.ts +50 -0
- package/dist/types/utils/validation.d.ts +17 -0
- package/dist/types/utils.d.ts +28 -0
- package/package.json +146 -0
- package/src/api-registry.ts +96 -0
- package/src/auth-broker/client.ts +358 -0
- package/src/auth-broker/index.ts +5 -0
- package/src/auth-broker/refresher.ts +127 -0
- package/src/auth-broker/remote-store.ts +623 -0
- package/src/auth-broker/server.ts +644 -0
- package/src/auth-broker/types.ts +127 -0
- package/src/auth-broker/wire-schemas.ts +200 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +717 -0
- package/src/auth-gateway/types.ts +134 -0
- package/src/auth-storage.ts +4104 -0
- package/src/cli.ts +262 -0
- package/src/index.ts +54 -0
- package/src/model-cache.ts +129 -0
- package/src/model-manager.ts +450 -0
- package/src/model-thinking.ts +691 -0
- package/src/models.json +73853 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +90 -0
- package/src/provider-models/bundled-references.ts +38 -0
- package/src/provider-models/descriptors.ts +308 -0
- package/src/provider-models/google.ts +91 -0
- package/src/provider-models/index.ts +5 -0
- package/src/provider-models/ollama.ts +153 -0
- package/src/provider-models/openai-compat.ts +2275 -0
- package/src/provider-models/special.ts +67 -0
- package/src/providers/amazon-bedrock.ts +849 -0
- package/src/providers/anthropic-messages-server-schema.ts +229 -0
- package/src/providers/anthropic-messages-server.ts +677 -0
- package/src/providers/anthropic.ts +2696 -0
- package/src/providers/aws-credentials.ts +501 -0
- package/src/providers/aws-eventstream.ts +185 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +337 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2561 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +372 -0
- package/src/providers/google-auth.ts +252 -0
- package/src/providers/google-gemini-cli.ts +795 -0
- package/src/providers/google-gemini-headers.ts +41 -0
- package/src/providers/google-shared.ts +902 -0
- package/src/providers/google-types.ts +167 -0
- package/src/providers/google-vertex.ts +88 -0
- package/src/providers/google.ts +41 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +500 -0
- package/src/providers/ollama.ts +544 -0
- package/src/providers/openai-anthropic-shim.ts +138 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +628 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +161 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2598 -0
- package/src/providers/openai-completions-compat.ts +279 -0
- package/src/providers/openai-completions.ts +1853 -0
- package/src/providers/openai-responses-server-schema.ts +290 -0
- package/src/providers/openai-responses-server.ts +1183 -0
- package/src/providers/openai-responses-shared.ts +800 -0
- package/src/providers/openai-responses.ts +621 -0
- package/src/providers/pi-native-client.ts +228 -0
- package/src/providers/pi-native-server.ts +210 -0
- package/src/providers/register-builtins.ts +412 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +309 -0
- package/src/providers/vision-guard.ts +31 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +895 -0
- package/src/types.ts +884 -0
- package/src/usage/claude.ts +431 -0
- package/src/usage/gemini.ts +250 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +201 -0
- package/src/usage/kimi.ts +271 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +503 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +183 -0
- package/src/utils/abort.ts +51 -0
- package/src/utils/anthropic-auth.ts +87 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +4 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +142 -0
- package/src/utils/fireworks-model-id.ts +30 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/h2-fetch.ts +60 -0
- package/src/utils/http-inspector.ts +176 -0
- package/src/utils/idle-iterator.ts +250 -0
- package/src/utils/json-parse.ts +148 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +200 -0
- package/src/utils/oauth/api-key-login.ts +87 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +16 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/deepseek.ts +53 -0
- package/src/utils/oauth/firepass.ts +24 -0
- package/src/utils/oauth/fireworks.ts +15 -0
- package/src/utils/oauth/github-copilot.ts +362 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +200 -0
- package/src/utils/oauth/google-gemini-cli.ts +256 -0
- package/src/utils/oauth/google-oauth-shared.ts +110 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +444 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +254 -0
- package/src/utils/oauth/litellm.ts +47 -0
- package/src/utils/oauth/lm-studio.ts +38 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +16 -0
- package/src/utils/oauth/nanogpt.ts +15 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama-cloud.ts +28 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +299 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +206 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +16 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +16 -0
- package/src/utils/oauth/types.ts +94 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +137 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +15 -0
- package/src/utils/overflow.ts +137 -0
- package/src/utils/parse-bind.ts +54 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +54 -0
- package/src/utils/schema/CONSTRAINTS.md +164 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +190 -0
- package/src/utils/schema/index.ts +13 -0
- package/src/utils/schema/json-schema-validator.ts +577 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +1588 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +97 -0
- package/src/utils/schema/types.ts +11 -0
- package/src/utils/schema/wire.ts +213 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -0
- package/src/utils/sse-debug.ts +289 -0
- package/src/utils/tool-call-healing.ts +271 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1019 -0
- package/src/utils.ts +166 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaomi MiMo login flow.
|
|
3
|
+
*
|
|
4
|
+
* Xiaomi MiMo provides OpenAI-compatible models via
|
|
5
|
+
* https://api.xiaomimimo.com/v1.
|
|
6
|
+
*
|
|
7
|
+
* This is not OAuth - it's a simple API key flow:
|
|
8
|
+
* 1. Open browser to Xiaomi MiMo API key console
|
|
9
|
+
* 2. User copies their API key
|
|
10
|
+
* 3. User pastes the API key into the CLI
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { OAuthController } from "./types";
|
|
14
|
+
|
|
15
|
+
const PROVIDER_ID = "xiaomi";
|
|
16
|
+
const PROVIDER_NAME = "Xiaomi MiMo";
|
|
17
|
+
const STANDARD_AUTH_URL = "https://platform.xiaomimimo.com/#/console/api-keys";
|
|
18
|
+
const STANDARD_API_BASE_URL = "https://api.xiaomimimo.com/v1";
|
|
19
|
+
const TOKEN_PLAN_SGP_API_BASE_URL = "https://token-plan-sgp.xiaomimimo.com/v1";
|
|
20
|
+
const TOKEN_PLAN_AMS_API_BASE_URL = "https://token-plan-ams.xiaomimimo.com/v1";
|
|
21
|
+
const TOKEN_PLAN_KEY_PREFIX = "tp-";
|
|
22
|
+
const STANDARD_VALIDATION_MODEL = "mimo-v2-flash";
|
|
23
|
+
const TOKEN_PLAN_VALIDATION_MODEL = "mimo-v2.5";
|
|
24
|
+
|
|
25
|
+
function isTokenPlanKey(apiKey: string): boolean {
|
|
26
|
+
return apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const VALIDATION_TIMEOUT_MS = 15_000;
|
|
30
|
+
|
|
31
|
+
async function validateXiaomiApiKey(apiKey: string, signal?: AbortSignal): Promise<void> {
|
|
32
|
+
// For token-plan keys try SGP first, then AMS as fallback.
|
|
33
|
+
// Standard sk- keys only hit the one endpoint.
|
|
34
|
+
const endpoints = isTokenPlanKey(apiKey)
|
|
35
|
+
? [
|
|
36
|
+
{ baseUrl: TOKEN_PLAN_SGP_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
|
|
37
|
+
{ baseUrl: TOKEN_PLAN_AMS_API_BASE_URL, model: TOKEN_PLAN_VALIDATION_MODEL },
|
|
38
|
+
]
|
|
39
|
+
: [{ baseUrl: STANDARD_API_BASE_URL, model: STANDARD_VALIDATION_MODEL }];
|
|
40
|
+
|
|
41
|
+
let lastError: Error | null = null;
|
|
42
|
+
|
|
43
|
+
for (const ep of endpoints) {
|
|
44
|
+
// Fresh timeout per endpoint so SGP→AMS fallback works after a regional
|
|
45
|
+
// timeout: a shared AbortSignal.timeout would stay aborted and instantly
|
|
46
|
+
// abort the AMS fetch.
|
|
47
|
+
const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
|
|
48
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(`${ep.baseUrl}/chat/completions`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"x-api-key": apiKey,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model: ep.model,
|
|
58
|
+
max_tokens: 1,
|
|
59
|
+
messages: [{ role: "user", content: "ping" }],
|
|
60
|
+
}),
|
|
61
|
+
signal: requestSignal,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (response.ok) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 401 means this endpoint didn't accept the key; try the next one
|
|
69
|
+
if (response.status === 401) {
|
|
70
|
+
let details = "";
|
|
71
|
+
try {
|
|
72
|
+
details = (await response.text()).trim();
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore body parse errors, status is enough
|
|
75
|
+
}
|
|
76
|
+
lastError = new Error(
|
|
77
|
+
details
|
|
78
|
+
? `${PROVIDER_NAME} API key validation failed (${response.status}): ${details}`
|
|
79
|
+
: `${PROVIDER_NAME} API key validation failed (${response.status})`,
|
|
80
|
+
);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Non-auth errors are real failures
|
|
85
|
+
let details = "";
|
|
86
|
+
try {
|
|
87
|
+
details = (await response.text()).trim();
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore body parse errors, status is enough
|
|
90
|
+
}
|
|
91
|
+
const message = details
|
|
92
|
+
? `${PROVIDER_NAME} API key validation failed (${response.status}): ${details}`
|
|
93
|
+
: `${PROVIDER_NAME} API key validation failed (${response.status})`;
|
|
94
|
+
throw new Error(message);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// Only re-throw AbortError when the caller explicitly cancelled.
|
|
97
|
+
// Timeout aborts (from AbortSignal.timeout) should fall through to
|
|
98
|
+
// the next endpoint so SGP→AMS fallback works during regional outages.
|
|
99
|
+
if (e instanceof DOMException && e.name === "AbortError" && signal?.aborted) {
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw lastError ?? new Error(`${PROVIDER_NAME} API key validation failed`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Login to Xiaomi MiMo.
|
|
110
|
+
*
|
|
111
|
+
* Opens browser to API keys page, prompts user to paste their API key.
|
|
112
|
+
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
113
|
+
*/
|
|
114
|
+
export async function loginXiaomi(options: OAuthController): Promise<string> {
|
|
115
|
+
if (!options.onPrompt) {
|
|
116
|
+
throw new Error(`${PROVIDER_NAME} login requires onPrompt callback`);
|
|
117
|
+
}
|
|
118
|
+
options.onAuth?.({
|
|
119
|
+
url: STANDARD_AUTH_URL,
|
|
120
|
+
instructions: "Copy your API key from the Xiaomi MiMo console",
|
|
121
|
+
});
|
|
122
|
+
const apiKey = await options.onPrompt({
|
|
123
|
+
message: "Paste your Xiaomi API key (sk-... or token-plan tp-...)",
|
|
124
|
+
placeholder: "sk-... or tp-...",
|
|
125
|
+
});
|
|
126
|
+
if (options.signal?.aborted) {
|
|
127
|
+
throw new Error("Login cancelled");
|
|
128
|
+
}
|
|
129
|
+
const trimmed = apiKey.trim();
|
|
130
|
+
if (!trimmed) {
|
|
131
|
+
throw new Error("API key is required");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
options.onProgress?.(`Validating ${PROVIDER_ID} API key...`);
|
|
135
|
+
await validateXiaomiApiKey(trimmed, options.signal);
|
|
136
|
+
return trimmed;
|
|
137
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Z.AI login flow.
|
|
3
|
+
*
|
|
4
|
+
* Z.AI is a platform that provides access to GLM models through an OpenAI-compatible API.
|
|
5
|
+
* API docs: https://docs.z.ai/guides/overview/quick-start
|
|
6
|
+
*
|
|
7
|
+
* This is not OAuth - it's a simple API key flow:
|
|
8
|
+
* 1. User gets their API key from https://z.ai/settings/api-keys
|
|
9
|
+
* 2. User pastes the API key into the CLI
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { validateOpenAICompatibleApiKey } from "./api-key-validation";
|
|
13
|
+
import type { OAuthController } from "./types";
|
|
14
|
+
|
|
15
|
+
const AUTH_URL = "https://z.ai/manage-apikey/apikey-list";
|
|
16
|
+
const API_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
|
|
17
|
+
const VALIDATION_MODEL = "glm-4.7";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Login to Z.AI.
|
|
21
|
+
*
|
|
22
|
+
* Opens browser to API keys page, prompts user to paste their API key.
|
|
23
|
+
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
24
|
+
*/
|
|
25
|
+
export async function loginZai(options: OAuthController): Promise<string> {
|
|
26
|
+
if (!options.onPrompt) {
|
|
27
|
+
throw new Error("Z.AI login requires onPrompt callback");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Open browser to API keys page
|
|
31
|
+
options.onAuth?.({
|
|
32
|
+
url: AUTH_URL,
|
|
33
|
+
instructions: "Copy your API key from the dashboard",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Prompt user to paste their API key
|
|
37
|
+
const apiKey = await options.onPrompt({
|
|
38
|
+
message: "Paste your Z.AI API key",
|
|
39
|
+
placeholder: "sk-...",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (options.signal?.aborted) {
|
|
43
|
+
throw new Error("Login cancelled");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const trimmed = apiKey.trim();
|
|
47
|
+
if (!trimmed) {
|
|
48
|
+
throw new Error("API key is required");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
options.onProgress?.("Validating API key...");
|
|
52
|
+
await validateOpenAICompatibleApiKey({
|
|
53
|
+
provider: "Z.AI",
|
|
54
|
+
apiKey: trimmed,
|
|
55
|
+
baseUrl: API_BASE_URL,
|
|
56
|
+
model: VALIDATION_MODEL,
|
|
57
|
+
signal: options.signal,
|
|
58
|
+
});
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** ZenMux login flow (API key paste, validated via /models). */
|
|
2
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
3
|
+
|
|
4
|
+
export const loginZenMux = createApiKeyLogin({
|
|
5
|
+
providerLabel: "ZenMux",
|
|
6
|
+
authUrl: "https://zenmux.ai/settings/keys",
|
|
7
|
+
instructions: "Create or copy your ZenMux API key",
|
|
8
|
+
promptMessage: "Paste your ZenMux API key",
|
|
9
|
+
placeholder: "sk-...",
|
|
10
|
+
validation: {
|
|
11
|
+
kind: "models-endpoint",
|
|
12
|
+
provider: "ZenMux",
|
|
13
|
+
modelsUrl: "https://zenmux.ai/api/v1/models",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { AssistantMessage } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regex patterns to detect context overflow errors from different providers.
|
|
5
|
+
*
|
|
6
|
+
* These patterns match error messages returned when the input exceeds
|
|
7
|
+
* the model's context window.
|
|
8
|
+
*
|
|
9
|
+
* Provider-specific patterns (with example error messages):
|
|
10
|
+
*
|
|
11
|
+
* - Anthropic: "prompt is too long: 213462 tokens > 200000 maximum"
|
|
12
|
+
* - OpenAI: "Your input exceeds the context window of this model"
|
|
13
|
+
* - Google: "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)"
|
|
14
|
+
* - xAI: "This model's maximum prompt length is 131072 but the request contains 537812 tokens"
|
|
15
|
+
* - Groq: "Please reduce the length of the messages or completion"
|
|
16
|
+
* - OpenRouter: "This endpoint's maximum context length is X tokens. However, you requested about Y tokens"
|
|
17
|
+
* - llama.cpp: "the request exceeds the available context size, try increasing it"
|
|
18
|
+
* - LM Studio: "tokens to keep from the initial prompt is greater than the context length"
|
|
19
|
+
* - GitHub Copilot: "prompt token count of X exceeds the limit of Y"
|
|
20
|
+
* - MiniMax: "invalid params, context window exceeds limit"
|
|
21
|
+
* - Kimi For Coding: "Your request exceeded model token limit: X (requested: Y)"
|
|
22
|
+
* - Anthropic 413: "request_too_large" / "Request exceeds the maximum size" (payload too large)
|
|
23
|
+
* - HTTP 413 variants: "Payload Too Large" / "Request Entity Too Large"
|
|
24
|
+
* - z.ai / GLM: Returns finish_reason: "model_context_window_exceeded" mapped to error message
|
|
25
|
+
* - z.ai: Does NOT error, accepts overflow silently - handled via usage.input > contextWindow
|
|
26
|
+
* - Ollama: Silently truncates input - not detectable via error message
|
|
27
|
+
*/
|
|
28
|
+
const OVERFLOW_PATTERNS = [
|
|
29
|
+
/prompt is too long/i, // Anthropic
|
|
30
|
+
/input is too long for requested model/i, // Amazon Bedrock
|
|
31
|
+
/exceeds the context window/i, // OpenAI (Completions & Responses API)
|
|
32
|
+
/input token count.*exceeds the maximum/i, // Google (Gemini)
|
|
33
|
+
/maximum prompt length is \d+/i, // xAI (Grok)
|
|
34
|
+
/reduce the length of the messages/i, // Groq
|
|
35
|
+
/maximum context length is \d+ tokens/i, // OpenRouter (all backends)
|
|
36
|
+
/exceeds the limit of \d+/i, // GitHub Copilot
|
|
37
|
+
/exceeds the available context size/i, // llama.cpp server
|
|
38
|
+
/requested tokens?.*exceed.*context (window|length|size)/i, // llama.cpp / OpenAI-compatible local servers
|
|
39
|
+
/context (window|length|size).*(exceeded|overflow|too small)/i, // Generic local server variants
|
|
40
|
+
/(prompt|input).*(too long|too large).*(context|n_ctx)/i, // llama.cpp phrasing variants
|
|
41
|
+
/requested tokens?.*(exceeds?|greater than).*(n_ctx|context)/i, // llama.cpp n_ctx variants
|
|
42
|
+
/greater than the context length/i, // LM Studio
|
|
43
|
+
/context window exceeds limit/i, // MiniMax
|
|
44
|
+
/exceeded model token limit/i, // Kimi For Coding
|
|
45
|
+
/context[_ ]length[_ ]exceeded/i, // Generic fallback
|
|
46
|
+
/too many tokens/i, // Generic fallback
|
|
47
|
+
/token limit exceeded/i, // Generic fallback
|
|
48
|
+
/request_too_large/i, // Anthropic 413 (request body too large)
|
|
49
|
+
/request exceeds the maximum size/i, // Anthropic 413 variant
|
|
50
|
+
/payload too large/i, // Generic HTTP 413 variant
|
|
51
|
+
/entity too large/i, // Generic HTTP 413 variant
|
|
52
|
+
/\b413\b.*\b(request|payload|entity)\b.*\btoo large\b/i, // "413 Request Entity Too Large" variants
|
|
53
|
+
/model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Check if an assistant message represents a context overflow error.
|
|
57
|
+
*
|
|
58
|
+
* This handles two cases:
|
|
59
|
+
* 1. Error-based overflow: Most providers return stopReason "error" with a
|
|
60
|
+
* specific error message pattern.
|
|
61
|
+
* 2. Silent overflow: Some providers accept overflow requests and return
|
|
62
|
+
* successfully. For these, we check if usage.input exceeds the context window.
|
|
63
|
+
*
|
|
64
|
+
* ## Reliability by Provider
|
|
65
|
+
*
|
|
66
|
+
* **Reliable detection (returns error with detectable message):**
|
|
67
|
+
* - Anthropic: "prompt is too long: X tokens > Y maximum"
|
|
68
|
+
* - OpenAI (Completions & Responses): "exceeds the context window"
|
|
69
|
+
* - Google Gemini: "input token count exceeds the maximum"
|
|
70
|
+
* - xAI (Grok): "maximum prompt length is X but request contains Y"
|
|
71
|
+
* - Groq: "reduce the length of the messages"
|
|
72
|
+
* - Cerebras: 400/413 status code (no body)
|
|
73
|
+
* - Mistral: 400/413 status code (no body)
|
|
74
|
+
* - HTTP 413 payload/entity-too-large variants
|
|
75
|
+
* - OpenRouter (all backends): "maximum context length is X tokens"
|
|
76
|
+
* - llama.cpp: "exceeds the available context size"
|
|
77
|
+
* - LM Studio: "greater than the context length"
|
|
78
|
+
* - Kimi For Coding: "exceeded model token limit: X (requested: Y)"
|
|
79
|
+
* - Anthropic 413: "request_too_large" (request body exceeds size limit)
|
|
80
|
+
* - HTTP 413: "Payload Too Large" / "Request Entity Too Large"
|
|
81
|
+
*
|
|
82
|
+
* **Unreliable detection:**
|
|
83
|
+
* - z.ai: Sometimes accepts overflow silently (detectable via usage.input > contextWindow),
|
|
84
|
+
* sometimes returns rate limit errors. Pass contextWindow param to detect silent overflow.
|
|
85
|
+
* - Ollama: Silently truncates input without error. Cannot be detected via this function.
|
|
86
|
+
* The response will have usage.input < expected, but we don't know the expected value.
|
|
87
|
+
*
|
|
88
|
+
* ## Custom Providers
|
|
89
|
+
*
|
|
90
|
+
* If you've added custom models via settings.json, this function may not detect
|
|
91
|
+
* overflow errors from those providers. To add support:
|
|
92
|
+
*
|
|
93
|
+
* 1. Send a request that exceeds the model's context window
|
|
94
|
+
* 2. Check the errorMessage in the response
|
|
95
|
+
* 3. Create a regex pattern that matches the error
|
|
96
|
+
* 4. The pattern should be added to OVERFLOW_PATTERNS in this file, or
|
|
97
|
+
* check the errorMessage yourself before calling this function
|
|
98
|
+
*
|
|
99
|
+
* @param message - The assistant message to check
|
|
100
|
+
* @param contextWindow - Optional context window size for detecting silent overflow (z.ai)
|
|
101
|
+
* @returns true if the message indicates a context overflow
|
|
102
|
+
*/
|
|
103
|
+
export function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean {
|
|
104
|
+
// Case 1: Check error message patterns
|
|
105
|
+
if (message.stopReason === "error" && message.errorMessage) {
|
|
106
|
+
// Check known patterns
|
|
107
|
+
if (OVERFLOW_PATTERNS.some(p => p.test(message.errorMessage!))) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Cerebras and Mistral return 400/413 with no body for context overflow.
|
|
112
|
+
// Proxy providers (e.g. api.synthetic.new) wrap upstream 400/413 no-body
|
|
113
|
+
// responses in a JSON envelope, so the status code phrase may appear
|
|
114
|
+
// anywhere in the message rather than at its start.
|
|
115
|
+
// Note: 429 is rate limiting (requests/tokens per time), NOT context overflow
|
|
116
|
+
if (/\b4(00|13)\s*(status code)?\s*\(no body\)/i.test(message.errorMessage)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Case 2: Usage-based overflow (silent or provider-specific)
|
|
122
|
+
if (contextWindow) {
|
|
123
|
+
const inputTokens = message.usage.input + message.usage.cacheRead + message.usage.cacheWrite;
|
|
124
|
+
if (inputTokens > contextWindow) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the overflow patterns for testing purposes.
|
|
134
|
+
*/
|
|
135
|
+
export function getOverflowPatterns(): RegExp[] {
|
|
136
|
+
return [...OVERFLOW_PATTERNS];
|
|
137
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `host:port` parser used by the auth-broker and auth-gateway boot
|
|
3
|
+
* paths. Centralized so the two servers can't drift on what they accept (the
|
|
4
|
+
* gateway used to silently allow empty hostnames; this fixes it).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ParsedBind {
|
|
8
|
+
hostname: string;
|
|
9
|
+
port: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parsePort(raw: string, bind: string): number {
|
|
13
|
+
if (!/^\d+$/.test(raw)) {
|
|
14
|
+
throw new Error(`Invalid bind '${bind}'; port must be an integer.`);
|
|
15
|
+
}
|
|
16
|
+
const port = Number.parseInt(raw, 10);
|
|
17
|
+
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
18
|
+
throw new Error(`Invalid bind '${bind}'; port out of range.`);
|
|
19
|
+
}
|
|
20
|
+
return port;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a `host:port` (or bare `port`, which assumes loopback) string.
|
|
25
|
+
*
|
|
26
|
+
* Accepts:
|
|
27
|
+
* - `"4000"` → `127.0.0.1:4000`
|
|
28
|
+
* - `"0.0.0.0:4000"` → as written
|
|
29
|
+
* - `"[::1]:4000"` → as written (brackets retained, Bun handles them)
|
|
30
|
+
*
|
|
31
|
+
* Rejects:
|
|
32
|
+
* - empty input
|
|
33
|
+
* - empty hostname (`":4000"`)
|
|
34
|
+
* - non-integer / out-of-range port
|
|
35
|
+
*/
|
|
36
|
+
export function parseBind(raw: string): ParsedBind {
|
|
37
|
+
const trimmed = raw.trim();
|
|
38
|
+
if (trimmed.length === 0) {
|
|
39
|
+
throw new Error("Invalid bind; expected 'host:port' or 'port'.");
|
|
40
|
+
}
|
|
41
|
+
if (/^\d+$/.test(trimmed)) {
|
|
42
|
+
return { hostname: "127.0.0.1", port: parsePort(trimmed, raw) };
|
|
43
|
+
}
|
|
44
|
+
const lastColon = trimmed.lastIndexOf(":");
|
|
45
|
+
if (lastColon < 0) {
|
|
46
|
+
throw new Error(`Invalid bind '${raw}'; expected 'host:port' or 'port'.`);
|
|
47
|
+
}
|
|
48
|
+
const hostPart = trimmed.slice(0, lastColon);
|
|
49
|
+
const portPart = trimmed.slice(lastColon + 1);
|
|
50
|
+
if (hostPart.length === 0) {
|
|
51
|
+
throw new Error(`Invalid bind '${raw}'; host must not be empty.`);
|
|
52
|
+
}
|
|
53
|
+
return { hostname: hostPart, port: parsePort(portPart, raw) };
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Api, Model, ProviderResponseMetadata, StreamOptions } from "../types";
|
|
2
|
+
|
|
3
|
+
export function normalizeProviderResponse(
|
|
4
|
+
response: Response,
|
|
5
|
+
requestId?: string | null,
|
|
6
|
+
metadata?: Record<string, unknown>,
|
|
7
|
+
): ProviderResponseMetadata {
|
|
8
|
+
const headers: Record<string, string> = {};
|
|
9
|
+
response.headers.forEach((value, key) => {
|
|
10
|
+
headers[key.toLowerCase()] = value;
|
|
11
|
+
});
|
|
12
|
+
const providerResponse: ProviderResponseMetadata = {
|
|
13
|
+
status: response.status,
|
|
14
|
+
headers,
|
|
15
|
+
};
|
|
16
|
+
if (requestId !== undefined) providerResponse.requestId = requestId;
|
|
17
|
+
if (metadata !== undefined) providerResponse.metadata = metadata;
|
|
18
|
+
return providerResponse;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function notifyProviderResponse(
|
|
22
|
+
options: Pick<StreamOptions, "onResponse"> | undefined,
|
|
23
|
+
response: Response,
|
|
24
|
+
model?: Model<Api>,
|
|
25
|
+
requestId?: string | null,
|
|
26
|
+
metadata?: Record<string, unknown>,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
if (!options?.onResponse) return;
|
|
29
|
+
await options.onResponse(normalizeProviderResponse(response, requestId, metadata), model);
|
|
30
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export type HeadersLike = Headers | Record<string, string | undefined> | undefined | null;
|
|
2
|
+
|
|
3
|
+
const RETRY_AFTER_HINT = "retry-after-ms=";
|
|
4
|
+
|
|
5
|
+
export function formatErrorMessageWithRetryAfter(error: unknown, headers?: HeadersLike): string {
|
|
6
|
+
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
|
7
|
+
if (message.includes(RETRY_AFTER_HINT)) {
|
|
8
|
+
return message;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const retryAfterMs = getRetryAfterMsFromHeaders(headers ?? getHeadersFromError(error));
|
|
12
|
+
if (retryAfterMs === undefined) {
|
|
13
|
+
return message;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `${message} ${RETRY_AFTER_HINT}${retryAfterMs}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getRetryAfterMsFromHeaders(headers: HeadersLike): number | undefined {
|
|
20
|
+
if (!headers) return undefined;
|
|
21
|
+
|
|
22
|
+
const retryAfter = parseRetryAfterHeader(getHeaderValue(headers, "retry-after"));
|
|
23
|
+
const resetMs = parseResetHeader(getHeaderValue(headers, "x-ratelimit-reset-ms"), "ms");
|
|
24
|
+
const resetSeconds = parseResetHeader(getHeaderValue(headers, "x-ratelimit-reset"), "s");
|
|
25
|
+
|
|
26
|
+
const candidates = [retryAfter, resetMs, resetSeconds].filter((value): value is number => value !== undefined);
|
|
27
|
+
if (candidates.length === 0) return undefined;
|
|
28
|
+
return Math.max(...candidates);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getHeadersFromError(error: unknown): HeadersLike {
|
|
32
|
+
if (!error || typeof error !== "object") return undefined;
|
|
33
|
+
const record = error as { headers?: unknown; response?: { headers?: unknown }; cause?: unknown };
|
|
34
|
+
const direct = extractHeaders(record.headers) ?? extractHeaders(record.response?.headers);
|
|
35
|
+
if (direct) return direct;
|
|
36
|
+
if (record.cause) return getHeadersFromError(record.cause);
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractHeaders(value: unknown): HeadersLike {
|
|
41
|
+
if (!value) return undefined;
|
|
42
|
+
if (value instanceof Headers) return value;
|
|
43
|
+
if (typeof value === "object") return value as Record<string, string | undefined>;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getHeaderValue(headers: Headers | Record<string, string | undefined>, name: string): string | undefined {
|
|
48
|
+
if (headers instanceof Headers) {
|
|
49
|
+
const value = headers.get(name);
|
|
50
|
+
return value ?? undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const target = name.toLowerCase();
|
|
54
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
55
|
+
if (key.toLowerCase() === target && typeof value === "string") {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseRetryAfterHeader(value: string | undefined): number | undefined {
|
|
63
|
+
if (!value) return undefined;
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
if (!trimmed) return undefined;
|
|
66
|
+
|
|
67
|
+
const numeric = Number(trimmed);
|
|
68
|
+
if (Number.isFinite(numeric)) {
|
|
69
|
+
if (numeric <= 0) return undefined;
|
|
70
|
+
return Math.ceil(numeric * 1000);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const dateMs = Date.parse(trimmed);
|
|
74
|
+
if (!Number.isNaN(dateMs)) {
|
|
75
|
+
const delay = dateMs - Date.now();
|
|
76
|
+
return delay > 0 ? Math.ceil(delay) : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseResetHeader(value: string | undefined, unit: "ms" | "s"): number | undefined {
|
|
83
|
+
if (!value) return undefined;
|
|
84
|
+
const numeric = Number(value);
|
|
85
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return undefined;
|
|
86
|
+
|
|
87
|
+
const nowMs = Date.now();
|
|
88
|
+
let targetMs: number | undefined;
|
|
89
|
+
|
|
90
|
+
if (unit === "ms") {
|
|
91
|
+
if (numeric > 1e12) {
|
|
92
|
+
targetMs = numeric;
|
|
93
|
+
} else if (numeric > 1e9) {
|
|
94
|
+
targetMs = numeric * 1000;
|
|
95
|
+
} else {
|
|
96
|
+
return Math.ceil(numeric);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if (numeric > 1e12) {
|
|
100
|
+
targetMs = numeric;
|
|
101
|
+
} else if (numeric > 1e9) {
|
|
102
|
+
targetMs = numeric * 1000;
|
|
103
|
+
} else {
|
|
104
|
+
return Math.ceil(numeric * 1000);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (targetMs <= nowMs) return undefined;
|
|
109
|
+
return Math.ceil(targetMs - nowMs);
|
|
110
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { scheduler } from "node:timers/promises";
|
|
2
|
+
import { extractHttpStatusFromError, isRetryableError } from "@gajae-code/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GitHub Copilot intermittently rejects preview models (gpt-5.3-OpenAI code backend,
|
|
6
|
+
* gpt-5.4, gpt-5.4-mini, ...) with HTTP 400 `model_not_supported`, even
|
|
7
|
+
* though the model is listed as enabled on the user's account via `/models`.
|
|
8
|
+
*
|
|
9
|
+
* Root cause: Copilot's request-routing backend is rolled out per OAuth
|
|
10
|
+
* client. Our OAuth client id is shared with opencode; VS Code uses its own
|
|
11
|
+
* client and sees full availability, so the same account may succeed in VS
|
|
12
|
+
* Code and flap between 200/400 here. See opencode#13313 and copilot-cli#2597.
|
|
13
|
+
*
|
|
14
|
+
* Retrying the identical request 2-3 times almost always lands on a backend
|
|
15
|
+
* that has the model, so we wrap the initial request with a short retry loop.
|
|
16
|
+
*/
|
|
17
|
+
export function isCopilotTransientModelError(error: unknown): boolean {
|
|
18
|
+
if (extractHttpStatusFromError(error) !== 400) return false;
|
|
19
|
+
if (!error || typeof error !== "object") return false;
|
|
20
|
+
const info = error as { code?: unknown; error?: { code?: unknown } | null };
|
|
21
|
+
const code = typeof info.code === "string" ? info.code : info.error?.code;
|
|
22
|
+
return code === "model_not_supported";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const COPILOT_MODEL_RETRY_MAX_ATTEMPTS = 3;
|
|
26
|
+
const COPILOT_MODEL_RETRY_BASE_DELAY_MS = 400;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap an initial Copilot request so transient `model_not_supported` 400s are
|
|
30
|
+
* retried a small number of times. No-op for non-Copilot providers.
|
|
31
|
+
*
|
|
32
|
+
* The callback **MUST** create a fresh in-flight request each invocation — a
|
|
33
|
+
* once-consumed AsyncIterable cannot be re-iterated.
|
|
34
|
+
*/
|
|
35
|
+
export async function callWithCopilotModelRetry<T>(
|
|
36
|
+
fn: () => Promise<T>,
|
|
37
|
+
options: { provider: string; signal?: AbortSignal; retryBaseDelayMs?: number },
|
|
38
|
+
): Promise<T> {
|
|
39
|
+
if (options.provider !== "github-copilot") return fn();
|
|
40
|
+
|
|
41
|
+
let lastError: unknown;
|
|
42
|
+
const retryBaseDelayMs = options.retryBaseDelayMs ?? COPILOT_MODEL_RETRY_BASE_DELAY_MS;
|
|
43
|
+
for (let attempt = 0; attempt < COPILOT_MODEL_RETRY_MAX_ATTEMPTS; attempt++) {
|
|
44
|
+
try {
|
|
45
|
+
return await fn();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
lastError = error;
|
|
48
|
+
if (!isCopilotTransientModelError(error) && !isRetryableError(error)) throw error;
|
|
49
|
+
if (attempt === COPILOT_MODEL_RETRY_MAX_ATTEMPTS - 1) break;
|
|
50
|
+
await scheduler.wait(retryBaseDelayMs * (attempt + 1), { signal: options.signal });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw lastError;
|
|
54
|
+
}
|