@bitkyc08/opencodex 2.7.40 → 2.7.41
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/README.md +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/assets/index-BmvM6wRb.js +65 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
- package/gui/dist/assets/index-cydcmbzC.js +0 -52
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export type ProviderAccessGroup =
|
|
2
|
+
| "recurring-or-keyless"
|
|
3
|
+
| "recurring-uncapped"
|
|
4
|
+
| "recurring-credit"
|
|
5
|
+
| "signup-credit";
|
|
6
|
+
|
|
7
|
+
export const FREE_PROVIDER_ACCESS_GROUPS = {
|
|
8
|
+
"recurring-or-keyless": [
|
|
9
|
+
"agy", "aihorde", "api-airforce", "arcee-ai", "bazaarlink", "blackbox", "bluesminds", "cerebras",
|
|
10
|
+
"cloudflare-ai", "cohere", "coze", "duckduckgo-web", "felo-web", "friendliai", "gemini", "github-models",
|
|
11
|
+
"groq", "hackclub", "huggingchat", "huggingface", "iflytek", "inference-net", "kiro", "liquid", "llm7",
|
|
12
|
+
"mistral", "morph", "muse-spark-web", "nara", "navy", "nlpcloud", "ollama-cloud", "opencode", "openrouter",
|
|
13
|
+
"ovhcloud", "pollinations", "puter", "qwen-web", "reka", "sambanova", "sparkdesk", "t3-web", "uncloseai",
|
|
14
|
+
],
|
|
15
|
+
"recurring-uncapped": [
|
|
16
|
+
"agnes", "ainative", "aion", "baidu", "glm", "glm-cn", "kilo-gateway", "opencode-zen", "requesty",
|
|
17
|
+
"routeway", "sealion", "siliconflow", "tencent",
|
|
18
|
+
],
|
|
19
|
+
"recurring-credit": ["bytez", "nous-research"],
|
|
20
|
+
"signup-credit": [
|
|
21
|
+
"agentrouter", "ai21", "baichuan", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
|
|
22
|
+
"hyperbolic", "longcat", "monsterapi", "nebius", "novita", "nscale", "nvidia", "predibase", "publicai", "qoder",
|
|
23
|
+
"scaleway", "sensenova", "stepfun", "together", "vertex",
|
|
24
|
+
],
|
|
25
|
+
} as const satisfies Record<ProviderAccessGroup, readonly string[]>;
|
|
26
|
+
|
|
27
|
+
export interface FreeDirectoryProvider {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
adapter: string;
|
|
31
|
+
baseUrl: string;
|
|
32
|
+
authKind: "oauth" | "key";
|
|
33
|
+
accessGroups: readonly ProviderAccessGroup[];
|
|
34
|
+
supportLevel: "supported" | "experimental" | "reference";
|
|
35
|
+
verification: "official" | "primary" | "unverified";
|
|
36
|
+
documentationUrl?: string;
|
|
37
|
+
modelsUrl?: string;
|
|
38
|
+
discovery: "live" | "static" | "hybrid" | "unsupported";
|
|
39
|
+
/**
|
|
40
|
+
* Date the endpoint was checked against provider documentation. Only rows that were actually
|
|
41
|
+
* verified carry it — a uniform date on an `unverified` row is a claim nobody made.
|
|
42
|
+
*/
|
|
43
|
+
lastVerified?: string;
|
|
44
|
+
dashboardUrl?: string;
|
|
45
|
+
keyOptional?: boolean;
|
|
46
|
+
models?: string[];
|
|
47
|
+
liveModels: boolean;
|
|
48
|
+
note?: string;
|
|
49
|
+
googleMode?: "ai-studio" | "vertex";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ConnectableOverride = Partial<Omit<FreeDirectoryProvider, "id" | "accessGroups">>;
|
|
53
|
+
|
|
54
|
+
const LAST_VERIFIED = "2026-07-23";
|
|
55
|
+
/**
|
|
56
|
+
* Connectable rows carry `lastVerified` because their endpoint was checked against provider
|
|
57
|
+
* documentation on that date. Reference rows built by the generator below deliberately omit it:
|
|
58
|
+
* stamping a verification date on a row marked `unverified` asserts provenance nobody produced.
|
|
59
|
+
*/
|
|
60
|
+
const openAi = (baseUrl: string, dashboardUrl: string, extra: Partial<ConnectableOverride> = {}): ConnectableOverride => ({
|
|
61
|
+
baseUrl,
|
|
62
|
+
dashboardUrl,
|
|
63
|
+
adapter: "openai-chat",
|
|
64
|
+
authKind: "key",
|
|
65
|
+
supportLevel: "experimental",
|
|
66
|
+
verification: "primary",
|
|
67
|
+
discovery: "live",
|
|
68
|
+
liveModels: true,
|
|
69
|
+
lastVerified: LAST_VERIFIED,
|
|
70
|
+
...extra,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// API roots are limited to documented or primary-source integrations. Consumer-web/session
|
|
74
|
+
// providers remain reference-only: this directory never asks users to paste cookies or bypass WAFs.
|
|
75
|
+
const CONNECTABLE: Record<string, ConnectableOverride> = {
|
|
76
|
+
kiro: { baseUrl: "https://runtime.us-east-1.kiro.dev", dashboardUrl: "https://kiro.dev", adapter: "kiro", authKind: "oauth", supportLevel: "supported", verification: "official", documentationUrl: "https://kiro.dev/docs/cli/authentication/", lastVerified: LAST_VERIFIED, discovery: "static", liveModels: false },
|
|
77
|
+
aihorde: openAi("https://oai.aihorde.net/v1", "https://aihorde.net", { modelsUrl: "https://oai.aihorde.net/v1/models" }),
|
|
78
|
+
"arcee-ai": openAi("https://conductor.arcee.ai/v1", "https://conductor.arcee.ai", { verification: "official", documentationUrl: "https://docs.arcee.ai/" }),
|
|
79
|
+
cerebras: openAi("https://api.cerebras.ai/v1", "https://cloud.cerebras.ai/platform/apikeys", { supportLevel: "supported", verification: "official", documentationUrl: "https://inference-docs.cerebras.ai/api-reference/models/list" }),
|
|
80
|
+
"cloudflare-ai": openAi("https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1", "https://dash.cloudflare.com/?to=/:account/ai/workers-ai", { supportLevel: "supported", verification: "official", documentationUrl: "https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/", discovery: "static", liveModels: false, models: ["@cf/meta/llama-3.3-70b-instruct-fp8-fast", "@cf/qwen/qwq-32b"] }),
|
|
81
|
+
cohere: openAi("https://api.cohere.com/compatibility/v1", "https://dashboard.cohere.com/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.cohere.com/reference/list-models", modelsUrl: "https://api.cohere.com/compatibility/v1/models" }),
|
|
82
|
+
friendliai: openAi("https://api.friendli.ai/serverless/v1", "https://suite.friendli.ai", { modelsUrl: "https://api.friendli.ai/serverless/v1/models" }),
|
|
83
|
+
gemini: { baseUrl: "https://generativelanguage.googleapis.com", dashboardUrl: "https://aistudio.google.com/apikey", adapter: "google", authKind: "key", supportLevel: "supported", verification: "official", documentationUrl: "https://ai.google.dev/api/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true, googleMode: "ai-studio", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"] },
|
|
84
|
+
"github-models": openAi("https://models.github.ai/inference", "https://github.com/settings/tokens", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.github.com/en/github-models/prototyping-with-ai-models", discovery: "static", liveModels: false, models: ["openai/gpt-4.1", "meta/llama-4-scout-17b-16e-instruct"] }),
|
|
85
|
+
groq: openAi("https://api.groq.com/openai/v1", "https://console.groq.com/keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://console.groq.com/docs/api-reference#models" }),
|
|
86
|
+
hackclub: openAi("https://ai.hackclub.com/proxy/v1", "https://ai.hackclub.com", { modelsUrl: "https://ai.hackclub.com/proxy/v1/models" }),
|
|
87
|
+
huggingface: openAi("https://router.huggingface.co/v1", "https://huggingface.co/settings/tokens", { supportLevel: "supported", verification: "official", documentationUrl: "https://huggingface.co/docs/inference-providers/index", modelsUrl: "https://router.huggingface.co/v1/models" }),
|
|
88
|
+
// The OpenAI-compatible base URL is documented in the HTTP guide; Web.html covers the
|
|
89
|
+
// WebSocket protocol and does not describe this endpoint.
|
|
90
|
+
iflytek: openAi("https://spark-api-open.xf-yun.com/v1", "https://console.xfyun.cn", { verification: "official", documentationUrl: "https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html" }),
|
|
91
|
+
liquid: openAi("https://inference.liquid.ai/v1", "https://playground.liquid.ai", { modelsUrl: "https://inference.liquid.ai/v1/models" }),
|
|
92
|
+
mistral: openAi("https://api.mistral.ai/v1", "https://console.mistral.ai/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.mistral.ai/api/endpoint/models" }),
|
|
93
|
+
morph: openAi("https://api.morphllm.com/v1", "https://morphllm.com", { verification: "official", documentationUrl: "https://docs.morphllm.com/api-reference/introduction" }),
|
|
94
|
+
"ollama-cloud": openAi("https://ollama.com/v1", "https://ollama.com/settings/keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.ollama.com/cloud", modelsUrl: "https://ollama.com/api/tags" }),
|
|
95
|
+
opencode: openAi("https://opencode.ai/zen/v1", "https://opencode.ai/auth", { supportLevel: "supported", verification: "official", documentationUrl: "https://opencode.ai/docs/zen/", modelsUrl: "https://opencode.ai/zen/v1/models" }),
|
|
96
|
+
openrouter: openAi("https://openrouter.ai/api/v1", "https://openrouter.ai/keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://openrouter.ai/docs/api/api-reference/models/get-models" }),
|
|
97
|
+
ovhcloud: openAi("https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", "https://console.ovhcloud.com", { keyOptional: true, modelsUrl: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/models" }),
|
|
98
|
+
pollinations: openAi("https://gen.pollinations.ai/v1", "https://pollinations.ai", { keyOptional: true }),
|
|
99
|
+
reka: openAi("https://api.reka.ai/v1", "https://platform.reka.ai"),
|
|
100
|
+
sambanova: openAi("https://api.sambanova.ai/v1", "https://cloud.sambanova.ai/apis", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.sambanova.ai/cloud/docs/api-reference/models" }),
|
|
101
|
+
sparkdesk: openAi("https://spark-api-open.xf-yun.com/v1", "https://console.xfyun.cn", { verification: "official", documentationUrl: "https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html" }),
|
|
102
|
+
agnes: openAi("https://apihub.agnes-ai.com/v1", "https://agnes-ai.com"),
|
|
103
|
+
ainative: openAi("https://api.ainative.studio/api/v1", "https://ainative.studio", { modelsUrl: "https://api.ainative.studio/api/v1/models" }),
|
|
104
|
+
aion: openAi("https://api.aionlabs.ai/v1", "https://aionlabs.ai", { modelsUrl: "https://api.aionlabs.ai/v1/models" }),
|
|
105
|
+
baidu: openAi("https://qianfan.baidubce.com/v2", "https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application", { verification: "official", documentationUrl: "https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu", discovery: "static", liveModels: false, models: ["ernie-5.1", "ernie-5.0", "ernie-4.5-turbo-128k"] }),
|
|
106
|
+
glm: openAi("https://api.z.ai/api/coding/paas/v4", "https://z.ai/manage-apikey/apikey-list", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.z.ai/guides/overview/quick-start", discovery: "static", liveModels: false, models: ["glm-5"] }),
|
|
107
|
+
"glm-cn": openAi("https://open.bigmodel.cn/api/coding/paas/v4", "https://open.bigmodel.cn/usercenter/apikeys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.bigmodel.cn/cn/api/introduction", discovery: "static", liveModels: false, models: ["glm-4.5-flash"] }),
|
|
108
|
+
"kilo-gateway": openAi("https://api.kilo.ai/api/gateway", "https://kilo.ai", { modelsUrl: "https://api.kilo.ai/api/gateway/models" }),
|
|
109
|
+
"opencode-zen": openAi("https://opencode.ai/zen/v1", "https://opencode.ai/auth", { supportLevel: "supported", verification: "official", documentationUrl: "https://opencode.ai/docs/zen/" }),
|
|
110
|
+
requesty: openAi("https://router.requesty.ai/v1", "https://app.requesty.ai", { modelsUrl: "https://router.requesty.ai/v1/models" }),
|
|
111
|
+
routeway: openAi("https://api.routeway.ai/v1", "https://routeway.ai", { modelsUrl: "https://api.routeway.ai/v1/models" }),
|
|
112
|
+
sealion: openAi("https://api.sea-lion.ai/v1", "https://sea-lion.ai", { discovery: "static", liveModels: false, models: ["aisingapore/Llama-SEA-LION-v3.5-70B-R", "aisingapore/Llama-SEA-LION-v3-70B-IT", "aisingapore/Gemma-SEA-LION-v4-27B-IT", "aisingapore/Qwen-SEA-LION-v4.5-27B-IT", "aisingapore/Qwen-SEA-LION-v4-32B-IT"] }),
|
|
113
|
+
siliconflow: openAi("https://api.siliconflow.cn/v1", "https://cloud.siliconflow.cn/account/ak", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.siliconflow.cn/en/api-reference/models/get-model-list" }),
|
|
114
|
+
tencent: openAi("https://api.hunyuan.cloud.tencent.com/v1", "https://console.cloud.tencent.com/hunyuan/api-key", { verification: "official", documentationUrl: "https://cloud.tencent.com/document/product/1729/111007", discovery: "static", liveModels: false, models: ["hunyuan-turbos-latest", "hunyuan-t1-latest", "hunyuan-pro", "hunyuan-lite"] }),
|
|
115
|
+
// Endpoint is live, but the recurring-credit terms could not be confirmed, so this row stays
|
|
116
|
+
// `unverified` and drops the shared verification date rather than borrowing it.
|
|
117
|
+
bytez: openAi("https://api.bytez.com/models/v2/openai/v1", "https://bytez.com", { verification: "unverified", lastVerified: undefined, documentationUrl: "https://docs.bytez.com/", discovery: "static", liveModels: false, models: ["meta-llama/Llama-3.3-70B-Instruct", "mistralai/Mistral-7B-Instruct-v0.3", "Qwen/Qwen2.5-72B-Instruct"], note: "The recurring-credit classification is retained from the requested catalog, but the current reset terms could not be independently verified." }),
|
|
118
|
+
"nous-research": openAi("https://inference-api.nousresearch.com/v1", "https://portal.nousresearch.com", { discovery: "static", liveModels: false, models: ["Hermes-4-405B", "Hermes-4-70B"] }),
|
|
119
|
+
agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true },
|
|
120
|
+
ai21: openAi("https://api.ai21.com/studio/v1", "https://studio.ai21.com/account/api-key", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.ai21.com/reference/models" }),
|
|
121
|
+
baichuan: openAi("https://api.baichuan-ai.com/v1", "https://platform.baichuan-ai.com/console/apikey", { verification: "official" }),
|
|
122
|
+
deepinfra: openAi("https://api.deepinfra.com/v1/openai", "https://deepinfra.com/dash/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://deepinfra.com/docs/openai_api" }),
|
|
123
|
+
deepseek: openAi("https://api.deepseek.com", "https://platform.deepseek.com/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://api-docs.deepseek.com/api/list-models" }),
|
|
124
|
+
doubao: openAi("https://ark.cn-beijing.volces.com/api/v3", "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey", { verification: "official" }),
|
|
125
|
+
fireworks: openAi("https://api.fireworks.ai/inference/v1", "https://fireworks.ai/account/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.fireworks.ai/api-reference/list-models", modelsUrl: "https://api.fireworks.ai/v1/accounts/fireworks/models?filter=supports_serverless=true" }),
|
|
126
|
+
"freemodel-dev": openAi("https://api.freemodel.dev/v1", "https://freemodel.dev", { modelsUrl: "https://api.freemodel.dev/v1/models" }),
|
|
127
|
+
hyperbolic: openAi("https://api.hyperbolic.xyz/v1", "https://app.hyperbolic.xyz/settings", { verification: "official" }),
|
|
128
|
+
longcat: openAi("https://api.longcat.chat/openai/v1", "https://longcat.chat", { verification: "official", discovery: "static", liveModels: false, models: ["LongCat-2.0"] }),
|
|
129
|
+
monsterapi: openAi("https://api.monsterapi.ai/v1", "https://monsterapi.ai", { verification: "official" }),
|
|
130
|
+
nebius: openAi("https://api.tokenfactory.nebius.com/v1", "https://studio.nebius.com", { verification: "official" }),
|
|
131
|
+
novita: openAi("https://api.novita.ai/openai/v1", "https://novita.ai/settings/key-management", { supportLevel: "supported", verification: "official", modelsUrl: "https://api.novita.ai/openai/v1/models" }),
|
|
132
|
+
nscale: openAi("https://inference.api.nscale.com/v1", "https://console.nscale.com", { verification: "official" }),
|
|
133
|
+
nvidia: openAi("https://integrate.api.nvidia.com/v1", "https://build.nvidia.com", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.api.nvidia.com/nim/reference/llm-apis" }),
|
|
134
|
+
publicai: openAi("https://api.publicai.co/v1", "https://publicai.co"),
|
|
135
|
+
scaleway: openAi("https://api.scaleway.ai/v1", "https://console.scaleway.com/generative-api", { verification: "official" }),
|
|
136
|
+
sensenova: openAi("https://token.sensenova.cn/v1", "https://console.sensenova.cn", { verification: "official" }),
|
|
137
|
+
stepfun: openAi("https://api.stepfun.com/v1", "https://platform.stepfun.com", { verification: "official" }),
|
|
138
|
+
together: openAi("https://api.together.xyz/v1", "https://api.together.xyz/settings/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.together.ai/reference/models-1" }),
|
|
139
|
+
vertex: { baseUrl: "https://aiplatform.googleapis.com", dashboardUrl: "https://console.cloud.google.com/vertex-ai", adapter: "google", authKind: "key", supportLevel: "experimental", verification: "official", documentationUrl: "https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference", lastVerified: LAST_VERIFIED, discovery: "static", liveModels: false, googleMode: "vertex", models: ["gemini-3.1-pro-preview", "gemini-3.1-flash-lite", "gemini-3-flash-preview"], note: "Vertex accepts an express-mode API key here. ADC users can instead configure project and location through the existing Google Vertex provider." },
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const LABELS: Record<string, string> = {
|
|
143
|
+
agy: "AGY", aihorde: "AI Horde", "api-airforce": "API Airforce", "arcee-ai": "Arcee AI",
|
|
144
|
+
bazaarlink: "BazaarLink", blackbox: "Blackbox", bluesminds: "Bluesminds", "cloudflare-ai": "Cloudflare AI",
|
|
145
|
+
coze: "Coze", "duckduckgo-web": "DuckDuckGo Web", "felo-web": "Felo Web", friendliai: "FriendliAI",
|
|
146
|
+
gemini: "Google Gemini", "github-models": "GitHub Models", huggingchat: "HuggingChat", huggingface: "Hugging Face",
|
|
147
|
+
iflytek: "iFlytek", "inference-net": "Inference.net", llm7: "LLM7", "muse-spark-web": "Muse Spark Web",
|
|
148
|
+
nlpcloud: "NLP Cloud", "ollama-cloud": "Ollama Cloud", ovhcloud: "OVHcloud", "qwen-web": "Qwen Web",
|
|
149
|
+
"t3-web": "T3 Web", uncloseai: "UncloseAI", ainative: "AI Native", baidu: "Baidu Qianfan",
|
|
150
|
+
glm: "Z.AI GLM", "glm-cn": "BigModel GLM (CN)", "kilo-gateway": "Kilo Gateway", "opencode-zen": "OpenCode Zen",
|
|
151
|
+
sealion: "SEA-LION", bytez: "Bytez", "nous-research": "Nous Research", agentrouter: "AgentRouter",
|
|
152
|
+
ai21: "AI21", baichuan: "Baichuan", deepinfra: "DeepInfra", deepseek: "DeepSeek", doubao: "Doubao",
|
|
153
|
+
"freemodel-dev": "FreeModel.dev", nebius: "Nebius", novita: "Novita", nscale: "Nscale", nvidia: "NVIDIA NIM",
|
|
154
|
+
publicai: "PublicAI", qoder: "Qoder", sensenova: "SenseNova", stepfun: "StepFun", vertex: "Google Vertex AI",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const referenceNote = "Reference entry only: no safe documented API integration is enabled. Configure it manually only with provider documentation; consumer-web cookies and anti-bot bypasses are intentionally unsupported.";
|
|
158
|
+
|
|
159
|
+
export const FREE_PROVIDER_DIRECTORY: readonly FreeDirectoryProvider[] = Object.values(FREE_PROVIDER_ACCESS_GROUPS)
|
|
160
|
+
.flat()
|
|
161
|
+
.filter((id, index, ids) => ids.indexOf(id) === index)
|
|
162
|
+
.map(id => {
|
|
163
|
+
const accessGroups = (Object.entries(FREE_PROVIDER_ACCESS_GROUPS) as [ProviderAccessGroup, readonly string[]][])
|
|
164
|
+
.filter(([, ids]) => ids.includes(id))
|
|
165
|
+
.map(([group]) => group);
|
|
166
|
+
const override = CONNECTABLE[id];
|
|
167
|
+
return {
|
|
168
|
+
id,
|
|
169
|
+
label: LABELS[id] ?? id.split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" "),
|
|
170
|
+
adapter: "openai-chat",
|
|
171
|
+
baseUrl: "",
|
|
172
|
+
authKind: "key",
|
|
173
|
+
accessGroups,
|
|
174
|
+
supportLevel: "reference",
|
|
175
|
+
verification: "unverified",
|
|
176
|
+
discovery: "unsupported",
|
|
177
|
+
liveModels: false,
|
|
178
|
+
...(!override ? { note: referenceNote } : {}),
|
|
179
|
+
...override,
|
|
180
|
+
} satisfies FreeDirectoryProvider;
|
|
181
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { saveConfigPreservingClaudeCode } from "../config";
|
|
12
12
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
13
13
|
import { resolveProviderTransport } from "./xai-transport";
|
|
14
14
|
|
|
@@ -112,7 +112,7 @@ export function rotateKeyOn429(
|
|
|
112
112
|
if (!isKeyInCooldown(providerName, candidate.id, now)) {
|
|
113
113
|
// Swap active key
|
|
114
114
|
provider.apiKey = candidate.key;
|
|
115
|
-
|
|
115
|
+
saveConfigPreservingClaudeCode(config);
|
|
116
116
|
console.warn(
|
|
117
117
|
// Log ids only — labels are user-supplied free text and could carry secret material.
|
|
118
118
|
`[key-failover] ${providerName}: 429 on key ${currentEntry?.id ?? "?"}; rotating to key ${candidate.id}`,
|
|
@@ -47,8 +47,9 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
47
47
|
|
|
48
48
|
const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
49
49
|
|
|
50
|
-
// gpt-5.6-sol
|
|
51
|
-
//
|
|
50
|
+
// gpt-5.6-sol and claude-opus-5 send these values through Kiro's verified native effort fields
|
|
51
|
+
// (`reasoning.effort` and `output_config.effort` respectively). Other models map them to bounded
|
|
52
|
+
// thinking instructions until their native effort support is verified.
|
|
52
53
|
export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
|
|
53
54
|
KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
|
|
54
55
|
);
|
|
@@ -6,7 +6,7 @@ export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi";
|
|
|
6
6
|
export const OPENAI_API_PROVIDER_ID = "openai-apikey";
|
|
7
7
|
export const LEGACY_CHATGPT_PROVIDER_ID = "chatgpt";
|
|
8
8
|
|
|
9
|
-
const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
9
|
+
export const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
10
10
|
const LEGACY_OPENAI_MULTI_PREFIX = `${LEGACY_OPENAI_MULTI_PROVIDER_ID}/`;
|
|
11
11
|
|
|
12
12
|
function canonicalCodexForwardProvider(mode: CodexAccountMode): OcxProviderConfig {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export interface ProviderRewriteResult {
|
|
4
|
+
/** Number of references re-pointed. */
|
|
5
|
+
changed: number;
|
|
6
|
+
/**
|
|
7
|
+
* Sites where the destination key already held a value, left untouched. A
|
|
8
|
+
* non-empty list means the caller must not treat the rewrite as complete —
|
|
9
|
+
* merging two settings is not a decision this function can make. The function
|
|
10
|
+
* is NOT transactional: earlier sites are already rewritten by the time a
|
|
11
|
+
* collision is found, so a caller that receives collisions must discard the
|
|
12
|
+
* config it passed in.
|
|
13
|
+
*/
|
|
14
|
+
collisions: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Re-point every config reference from one provider id to another.
|
|
19
|
+
*
|
|
20
|
+
* Three shapes exist and the difference matters: routed model strings
|
|
21
|
+
* (`"<provider>/<model>"`), bare provider ids (`customModels[].provider`,
|
|
22
|
+
* `combos[*].targets[].provider`), and keys that ARE provider ids or routes
|
|
23
|
+
* (`providerContextCaps`, `claudeCode.desktopProfile.assignments`). A rewrite
|
|
24
|
+
* that handles only the first leaves an orphaned context cap and — worse — a
|
|
25
|
+
* combo target naming a provider that no longer exists, which fails validation
|
|
26
|
+
* in `src/combos/types.ts` and makes `loadConfig` discard the whole config.
|
|
27
|
+
*
|
|
28
|
+
* `providers[*].selectedModels` is deliberately NOT rewritten: those are
|
|
29
|
+
* per-provider native model ids, and upstream ids may themselves contain a
|
|
30
|
+
* slash, so a prefix rewrite could mangle an unrelated provider's allowlist. A
|
|
31
|
+
* caller that moves a provider row handles its own allowlist, where the
|
|
32
|
+
* destination catalog is known.
|
|
33
|
+
*/
|
|
34
|
+
export function rewriteProviderReferences(config: OcxConfig, from: string, to: string): ProviderRewriteResult {
|
|
35
|
+
const prefix = `${from}/`;
|
|
36
|
+
const collisions: string[] = [];
|
|
37
|
+
let changed = 0;
|
|
38
|
+
|
|
39
|
+
/** Routed string: rewrite only an exact `<from>/…` prefix, never a longer id. */
|
|
40
|
+
const route = (value: unknown): string | undefined => {
|
|
41
|
+
if (typeof value !== "string" || !value.startsWith(prefix)) return undefined;
|
|
42
|
+
changed += 1;
|
|
43
|
+
return `${to}/${value.slice(prefix.length)}`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rewrite a routed-string list in place. Assigning the result unconditionally
|
|
48
|
+
* would add an own property with value `undefined` where the field was absent,
|
|
49
|
+
* which breaks the no-op contract. The key type is an explicit union rather
|
|
50
|
+
* than `keyof OcxConfig`: the latter also admits `customModels` and friends, so
|
|
51
|
+
* `map` would infer a union array that is not assignable back.
|
|
52
|
+
*/
|
|
53
|
+
type RoutedListKey = "disabledModels" | "subagentModels" | "subagentModelFallback";
|
|
54
|
+
const routeListAt = (key: RoutedListKey): void => {
|
|
55
|
+
const list = config[key];
|
|
56
|
+
if (!list) return;
|
|
57
|
+
config[key] = list.map(id => route(id) ?? id);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const routeRecordValues = (record: Record<string, string> | undefined): void => {
|
|
61
|
+
if (!record) return;
|
|
62
|
+
for (const [key, value] of Object.entries(record)) {
|
|
63
|
+
const next = route(value);
|
|
64
|
+
if (next) record[key] = next;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (config.defaultProvider === from) {
|
|
69
|
+
config.defaultProvider = to;
|
|
70
|
+
changed += 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
routeListAt("disabledModels");
|
|
74
|
+
routeListAt("subagentModels");
|
|
75
|
+
routeListAt("subagentModelFallback");
|
|
76
|
+
|
|
77
|
+
const scalarOwners: Array<[Record<string, unknown> | undefined, string]> = [
|
|
78
|
+
[config as unknown as Record<string, unknown>, "injectionModel"],
|
|
79
|
+
[config.shadowCallIntercept as Record<string, unknown> | undefined, "model"],
|
|
80
|
+
[config.webSearchSidecar as Record<string, unknown> | undefined, "model"],
|
|
81
|
+
[config.visionSidecar as Record<string, unknown> | undefined, "model"],
|
|
82
|
+
[config.claudeCode as unknown as Record<string, unknown> | undefined, "model"],
|
|
83
|
+
[config.claudeCode as unknown as Record<string, unknown> | undefined, "smallFastModel"],
|
|
84
|
+
[config.claudeCode?.webSearchSidecar as Record<string, unknown> | undefined, "model"],
|
|
85
|
+
[config.claudeCode?.visionSidecar as Record<string, unknown> | undefined, "model"],
|
|
86
|
+
];
|
|
87
|
+
for (const [owner, key] of scalarOwners) {
|
|
88
|
+
if (!owner) continue;
|
|
89
|
+
const next = route(owner[key]);
|
|
90
|
+
if (next) owner[key] = next;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
routeRecordValues(config.claudeCode?.tierModels as Record<string, string> | undefined);
|
|
94
|
+
routeRecordValues(config.claudeCode?.modelMap as Record<string, string> | undefined);
|
|
95
|
+
|
|
96
|
+
// Bare provider ids.
|
|
97
|
+
for (const model of config.customModels ?? []) {
|
|
98
|
+
if (model.provider === from) {
|
|
99
|
+
model.provider = to;
|
|
100
|
+
changed += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const combo of Object.values(config.combos ?? {})) {
|
|
104
|
+
for (const target of combo.targets ?? []) {
|
|
105
|
+
if (target.provider === from) {
|
|
106
|
+
target.provider = to;
|
|
107
|
+
changed += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Keys. `providerContextCaps` is KEYED by provider id — a prefix rewrite would
|
|
113
|
+
// silently orphan the cap — and a destination key may already be occupied.
|
|
114
|
+
const caps = config.providerContextCaps;
|
|
115
|
+
if (caps && Object.hasOwn(caps, from)) {
|
|
116
|
+
if (Object.hasOwn(caps, to)) {
|
|
117
|
+
collisions.push(`providerContextCaps.${to}`);
|
|
118
|
+
} else {
|
|
119
|
+
caps[to] = caps[from]!;
|
|
120
|
+
delete caps[from];
|
|
121
|
+
changed += 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// `desktopProfile` is nested and asymmetric: `assignments` is KEYED by route
|
|
126
|
+
// while `defaults` holds routes as VALUES.
|
|
127
|
+
const profile = config.claudeCode?.desktopProfile;
|
|
128
|
+
if (profile) {
|
|
129
|
+
for (const key of Object.keys(profile.assignments ?? {})) {
|
|
130
|
+
const next = route(key);
|
|
131
|
+
if (!next) continue;
|
|
132
|
+
if (Object.hasOwn(profile.assignments, next)) {
|
|
133
|
+
changed -= 1; // `route` counted it; the move did not happen.
|
|
134
|
+
collisions.push(`claudeCode.desktopProfile.assignments.${next}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
profile.assignments[next] = profile.assignments[key]!;
|
|
138
|
+
delete profile.assignments[key];
|
|
139
|
+
}
|
|
140
|
+
const defaults = profile.defaults as Record<string, string | null> | undefined;
|
|
141
|
+
if (defaults) {
|
|
142
|
+
for (const [family, value] of Object.entries(defaults)) {
|
|
143
|
+
const next = route(value);
|
|
144
|
+
if (next) defaults[family] = next;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { changed, collisions };
|
|
150
|
+
}
|
|
@@ -71,6 +71,7 @@ export interface ProviderRegistryEntry {
|
|
|
71
71
|
promptCacheKey?: boolean;
|
|
72
72
|
autoToolChoiceOnlyModels?: string[];
|
|
73
73
|
preserveReasoningContentModels?: string[];
|
|
74
|
+
reasoningSplitModels?: string[];
|
|
74
75
|
thinkingToggleModels?: string[];
|
|
75
76
|
thinkingBudgetModels?: string[];
|
|
76
77
|
escapeBuiltinToolNames?: boolean;
|
|
@@ -92,7 +93,7 @@ export type ProviderConfigSeed = Pick<
|
|
|
92
93
|
| "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
|
|
93
94
|
| "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
|
|
94
95
|
| "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
|
|
95
|
-
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
|
|
96
|
+
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "reasoningSplitModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
|
|
96
97
|
| "googleMode" | "project" | "location" | "headers"
|
|
97
98
|
>;
|
|
98
99
|
|
|
@@ -117,6 +118,16 @@ const MINIMAX_MODELS = [
|
|
|
117
118
|
const MINIMAX_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
|
|
118
119
|
MINIMAX_MODELS.map(id => [id, id === "MiniMax-M3" ? 1_000_000 : 204_800]),
|
|
119
120
|
);
|
|
121
|
+
const MINIMAX_M3_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
122
|
+
const MINIMAX_M3_REASONING_EFFORT_MAP: Record<string, string> = {
|
|
123
|
+
none: "disabled",
|
|
124
|
+
minimal: "disabled",
|
|
125
|
+
low: "disabled",
|
|
126
|
+
medium: "adaptive",
|
|
127
|
+
high: "adaptive",
|
|
128
|
+
xhigh: "adaptive",
|
|
129
|
+
max: "adaptive",
|
|
130
|
+
};
|
|
120
131
|
const OPENAI_GPT56_MODELS = ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
|
|
121
132
|
const OPENAI_GPT56_PRO_MODELS = ["gpt-5.6-sol-pro", "gpt-5.6-terra-pro", "gpt-5.6-luna-pro"];
|
|
122
133
|
const OPENAI_API_GPT56_CONTEXT_WINDOW = 1_050_000;
|
|
@@ -390,6 +401,18 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
390
401
|
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
|
|
391
402
|
models: ["grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
392
403
|
defaultModel: "grok-4.5",
|
|
404
|
+
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
|
|
405
|
+
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
|
|
406
|
+
// inputModalities undefined, and deriveComboCatalogModel defaults an undefined member to
|
|
407
|
+
// ["text"] — so any combo containing an xAI target is advertised to Codex as text-only and
|
|
408
|
+
// the app blocks attachments client-side. grok-build-0.1 / grok-composer-2.5-fast stay out
|
|
409
|
+
// (they are already listed in noVisionModels below).
|
|
410
|
+
modelInputModalities: {
|
|
411
|
+
"grok-4.5": ["text", "image"],
|
|
412
|
+
"grok-4.3": ["text", "image"],
|
|
413
|
+
"grok-4.20-0309-reasoning": ["text", "image"],
|
|
414
|
+
"grok-4.20-0309-non-reasoning": ["text", "image"],
|
|
415
|
+
},
|
|
393
416
|
noReasoningModels: ["grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
394
417
|
// Replay assistant reasoning_content for grok reasoning models: xAI documents dropped
|
|
395
418
|
// reasoning_content as the top cause of prompt-cache misses on multi-turn conversations
|
|
@@ -470,7 +493,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
470
493
|
baseUrl: "https://runtime.us-east-1.kiro.dev",
|
|
471
494
|
authKind: "oauth",
|
|
472
495
|
oauthId: "kiro",
|
|
473
|
-
note: "Import-first: reuses your installed kiro-cli
|
|
496
|
+
note: "Import-first: reuses your installed Kiro CLI login — requires kiro-cli installed and signed in (`kiro-cli login`). Experimental third-party harness — see Kiro ToS.",
|
|
474
497
|
models: KIRO_MODELS,
|
|
475
498
|
defaultModel: "kiro-auto",
|
|
476
499
|
// Kiro speaks CodeWhisperer wire, not OpenAI-style GET /models. Keep the static
|
|
@@ -644,6 +667,18 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
644
667
|
preserveReasoningContentModels: ["deepseek/deepseek-v4-pro"],
|
|
645
668
|
note: "OpenAI-compatible adaptive router. Default is a tool-capable model; orcarouter/auto (adaptive routing) is also selectable. Full catalog: https://www.orcarouter.ai/models",
|
|
646
669
|
},
|
|
670
|
+
{
|
|
671
|
+
// BizRouter: Korean enterprise LLM gateway (api.bizrouter.ai). Model ids are
|
|
672
|
+
// vendor-namespaced (`<vendor>/<model>`) and pass through to the upstream as-is.
|
|
673
|
+
// Live-verified 2026-07-24: /v1/chat/completions accepts the `tools` field and
|
|
674
|
+
// streams, and GET /v1/models returns the per-API-key allowed catalog in the
|
|
675
|
+
// OpenAI list shape, so live model discovery narrows to what the key can use.
|
|
676
|
+
id: "bizrouter", label: "BizRouter", adapter: "openai-chat", baseUrl: "https://api.bizrouter.ai/v1",
|
|
677
|
+
authKind: "key", dashboardUrl: "https://bizrouter.ai/settings/keys",
|
|
678
|
+
defaultModel: "openai/gpt-5.6-sol",
|
|
679
|
+
models: ["openai/gpt-5.6-sol", "anthropic/claude-sonnet-5", "google/gemini-3.5-flash"],
|
|
680
|
+
note: "Korean enterprise LLM gateway. Per-key allowed models are discovered live from /v1/models. Full catalog: https://bizrouter.ai/models",
|
|
681
|
+
},
|
|
647
682
|
{ id: "groq", label: "Groq", adapter: "openai-chat", baseUrl: "https://api.groq.com/openai/v1", authKind: "key", featured: true, dashboardUrl: "https://console.groq.com/keys" },
|
|
648
683
|
// 2026-07-10 Gemini API refresh: Tier-2 ai.google.dev evidence recorded in
|
|
649
684
|
// devlog/_plan/260710_provider_hardening/001_research_frontier.md.
|
|
@@ -908,12 +943,24 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
908
943
|
id: "minimax", label: "MiniMax — Coding Plan", baseUrl: "https://api.minimax.io/v1", adapter: "openai-chat", authKind: "key",
|
|
909
944
|
dashboardUrl: "https://platform.minimax.io", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
|
|
910
945
|
modelContextWindows: MINIMAX_MODEL_CONTEXT_WINDOWS,
|
|
946
|
+
modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS },
|
|
947
|
+
modelDefaultReasoningEfforts: { "MiniMax-M3": "medium" },
|
|
948
|
+
modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
|
|
949
|
+
preserveReasoningContentModels: MINIMAX_MODELS,
|
|
950
|
+
reasoningSplitModels: MINIMAX_MODELS,
|
|
951
|
+
thinkingToggleModels: ["MiniMax-M3"],
|
|
911
952
|
jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "Subscription Key or API Key",
|
|
912
953
|
},
|
|
913
954
|
{
|
|
914
955
|
id: "minimax-cn", label: "MiniMax — Coding Plan (CN)", baseUrl: "https://api.minimaxi.com/v1", adapter: "openai-chat", authKind: "key",
|
|
915
956
|
dashboardUrl: "https://platform.minimaxi.com", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
|
|
916
957
|
modelContextWindows: MINIMAX_MODEL_CONTEXT_WINDOWS,
|
|
958
|
+
modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS },
|
|
959
|
+
modelDefaultReasoningEfforts: { "MiniMax-M3": "medium" },
|
|
960
|
+
modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
|
|
961
|
+
preserveReasoningContentModels: MINIMAX_MODELS,
|
|
962
|
+
reasoningSplitModels: MINIMAX_MODELS,
|
|
963
|
+
thinkingToggleModels: ["MiniMax-M3"],
|
|
917
964
|
jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "中国区 Subscription Key",
|
|
918
965
|
},
|
|
919
966
|
{
|
package/src/responses/parser.ts
CHANGED
|
@@ -25,28 +25,54 @@ type InputBlock =
|
|
|
25
25
|
| { type: "input_text"; text: string }
|
|
26
26
|
| { type: "text"; text: string }
|
|
27
27
|
| { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
|
|
28
|
-
| { type: "input_file"; file_id?: string; filename?: string };
|
|
28
|
+
| { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
/** A usable reference string, or undefined. Empty strings and non-strings are not references. */
|
|
31
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
32
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
31
36
|
if (typeof blocks === "string") return blocks;
|
|
32
|
-
|
|
37
|
+
// The catch-all can also hand back a non-array `content` (an object, a number), which would
|
|
38
|
+
// throw at the loop below before any per-block guard runs.
|
|
39
|
+
if (!Array.isArray(blocks)) return [];
|
|
33
40
|
const parts: OcxContentPart[] = [];
|
|
34
41
|
for (const raw of blocks) {
|
|
42
|
+
// A malformed message item fails its strict schema and falls through to inputItemSchema's
|
|
43
|
+
// permissive catch-all, so blocks reaching here are NOT guaranteed to match the declared
|
|
44
|
+
// shape. Validate each field before use, as outputToToolResultContent already does.
|
|
45
|
+
if (!isObj(raw)) continue;
|
|
35
46
|
const block = raw as InputBlock;
|
|
36
47
|
if (block.type === "input_text" || block.type === "text") {
|
|
37
|
-
parts.push({ type: "text", text:
|
|
48
|
+
if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text });
|
|
38
49
|
} else if (block.type === "input_image") {
|
|
39
50
|
const b = block as { image_url?: string; file_id?: string; detail?: string };
|
|
40
|
-
|
|
51
|
+
const imageUrl = nonEmptyString(b.image_url);
|
|
52
|
+
const fileId = nonEmptyString(b.file_id);
|
|
53
|
+
const detail = nonEmptyString(b.detail);
|
|
54
|
+
if (imageUrl) {
|
|
41
55
|
// Preserve the image as a structured part — adapters send it as a native image block.
|
|
42
56
|
// NEVER inline the (often base64 data-URL) image_url as text: that explodes the token count.
|
|
43
|
-
parts.push({ type: "image", imageUrl
|
|
44
|
-
} else {
|
|
45
|
-
parts.push({ type: "text", text: `[image: ${
|
|
57
|
+
parts.push({ type: "image", imageUrl, ...(detail ? { detail: normalizeImageDetail(detail) } : {}) });
|
|
58
|
+
} else if (fileId) {
|
|
59
|
+
parts.push({ type: "text", text: `[image: ${fileId}]` }); // file_id ref → no inline data
|
|
46
60
|
}
|
|
61
|
+
// No usable reference: omit the block. A "[image: ?]" marker would claim an attachment
|
|
62
|
+
// the request never carried, which is worse than dropping malformed input.
|
|
47
63
|
} else if (block.type === "input_file") {
|
|
48
|
-
const
|
|
49
|
-
|
|
64
|
+
const b = block as { file_id?: string; filename?: string; file_data?: string };
|
|
65
|
+
const fileId = nonEmptyString(b.file_id);
|
|
66
|
+
const fileData = nonEmptyString(b.file_data);
|
|
67
|
+
const filename = nonEmptyString(b.filename);
|
|
68
|
+
if (fileId) {
|
|
69
|
+
parts.push({ type: "text", text: `[file: ${fileId}]` });
|
|
70
|
+
} else if (fileData) {
|
|
71
|
+
// Inline file_data is often large base64. Preserve only its presence and name, never bytes.
|
|
72
|
+
parts.push({ type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" });
|
|
73
|
+
}
|
|
74
|
+
// A bare filename is not a file resource in the Responses schema, so omit it rather than
|
|
75
|
+
// fabricating a "[file: ...]" marker for an attachment that was never sent.
|
|
50
76
|
}
|
|
51
77
|
}
|
|
52
78
|
// Collapse to a plain string only for a single TEXT part; images must stay structured.
|
|
@@ -56,14 +82,19 @@ function inputContentParts(blocks: unknown[] | string | undefined): string | Ocx
|
|
|
56
82
|
|
|
57
83
|
type OutputBlock = { type: "output_text"; text: string } | { type: "text"; text: string } | { type: "refusal"; refusal: string };
|
|
58
84
|
|
|
59
|
-
function outputTextOf(blocks: unknown
|
|
85
|
+
function outputTextOf(blocks: unknown): OcxTextContent[] {
|
|
60
86
|
if (typeof blocks === "string") return blocks.length > 0 ? [{ type: "text", text: blocks }] : [];
|
|
61
|
-
if (!blocks) return [];
|
|
87
|
+
if (!Array.isArray(blocks)) return [];
|
|
62
88
|
const out: OcxTextContent[] = [];
|
|
63
89
|
for (const raw of blocks) {
|
|
90
|
+
// Same catch-all caveat as inputContentParts: validate before use.
|
|
91
|
+
if (!isObj(raw)) continue;
|
|
64
92
|
const b = raw as OutputBlock;
|
|
65
|
-
if (b.type === "output_text" || b.type === "text")
|
|
66
|
-
|
|
93
|
+
if (b.type === "output_text" || b.type === "text") {
|
|
94
|
+
if (typeof raw.text === "string") out.push({ type: "text", text: raw.text });
|
|
95
|
+
} else if (b.type === "refusal") {
|
|
96
|
+
if (typeof raw.refusal === "string") out.push({ type: "text", text: `[refusal: ${raw.refusal}]` });
|
|
97
|
+
}
|
|
67
98
|
}
|
|
68
99
|
return out;
|
|
69
100
|
}
|
|
@@ -311,9 +342,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
311
342
|
content?: unknown;
|
|
312
343
|
};
|
|
313
344
|
|
|
314
|
-
const content = inputContentParts(
|
|
315
|
-
agentMessage.content as unknown[] | string | undefined,
|
|
316
|
-
);
|
|
345
|
+
const content = inputContentParts(agentMessage.content);
|
|
317
346
|
|
|
318
347
|
const hasContent =
|
|
319
348
|
typeof content === "string"
|
|
@@ -338,7 +367,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
338
367
|
switch (msg.role) {
|
|
339
368
|
case "system": {
|
|
340
369
|
pendingReasoning.length = 0;
|
|
341
|
-
const text = inputContentParts(msg.content
|
|
370
|
+
const text = inputContentParts(msg.content);
|
|
342
371
|
const flat = typeof text === "string" ? text : text.map(p => (p.type === "text" ? p.text : "")).join("");
|
|
343
372
|
if (flat.length > 0) systemPrompt.push(flat);
|
|
344
373
|
break;
|
|
@@ -346,12 +375,12 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
346
375
|
case "user":
|
|
347
376
|
case "developer": {
|
|
348
377
|
pendingReasoning.length = 0;
|
|
349
|
-
const content = inputContentParts(msg.content
|
|
378
|
+
const content = inputContentParts(msg.content);
|
|
350
379
|
messages.push({ role: msg.role, content, timestamp: now });
|
|
351
380
|
break;
|
|
352
381
|
}
|
|
353
382
|
case "assistant": {
|
|
354
|
-
const parts = outputTextOf(msg.content
|
|
383
|
+
const parts = outputTextOf(msg.content);
|
|
355
384
|
messages.push({
|
|
356
385
|
role: "assistant",
|
|
357
386
|
content: pendingReasoning.length > 0
|