@bitkyc08/opencodex 2.7.22 → 2.7.24-preview.20260718

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.
Files changed (55) hide show
  1. package/README.ko.md +37 -7
  2. package/README.md +52 -11
  3. package/README.zh-CN.md +36 -7
  4. package/bin/ocx.mjs +5 -3
  5. package/gui/dist/assets/index-DDs2IUqv.js +40 -0
  6. package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/opencode.svg +1 -1
  9. package/package.json +5 -2
  10. package/src/adapters/anthropic-image-normalize.ts +70 -29
  11. package/src/adapters/cursor/transport-retry.ts +20 -1
  12. package/src/adapters/run-turn-queue.ts +40 -0
  13. package/src/codex/auth-api.ts +10 -1
  14. package/src/codex/auth-context.ts +33 -7
  15. package/src/codex/catalog.ts +357 -23
  16. package/src/codex/routing.ts +10 -4
  17. package/src/combos/failover.ts +102 -0
  18. package/src/combos/index.ts +37 -0
  19. package/src/combos/request.ts +31 -0
  20. package/src/combos/resolve.ts +171 -0
  21. package/src/combos/types.ts +203 -0
  22. package/src/config.ts +280 -11
  23. package/src/lib/errors.ts +86 -24
  24. package/src/lib/upstream-retry.ts +8 -4
  25. package/src/oauth/index.ts +7 -1
  26. package/src/oauth/key-providers.ts +2 -32
  27. package/src/oauth/login-cli.ts +4 -3
  28. package/src/oauth/token-guardian.ts +38 -3
  29. package/src/providers/derive.ts +27 -2
  30. package/src/providers/kiro-models.ts +8 -3
  31. package/src/providers/label.ts +3 -1
  32. package/src/providers/openai-sidecar.ts +94 -0
  33. package/src/providers/openai-tier-startup.ts +27 -0
  34. package/src/providers/openai-tiers.ts +283 -0
  35. package/src/providers/openai-virtual-models.ts +82 -0
  36. package/src/providers/quota.ts +344 -24
  37. package/src/providers/registry.ts +148 -31
  38. package/src/reasoning-effort.ts +12 -11
  39. package/src/router.ts +80 -36
  40. package/src/server/auth-cors.ts +85 -9
  41. package/src/server/images.ts +31 -75
  42. package/src/server/index.ts +45 -86
  43. package/src/server/management-api.ts +273 -21
  44. package/src/server/request-log.ts +221 -20
  45. package/src/server/responses.ts +594 -75
  46. package/src/server/search.ts +22 -37
  47. package/src/types.ts +49 -1
  48. package/src/update/index.ts +50 -6
  49. package/src/update/job.ts +21 -4
  50. package/src/usage/log.ts +124 -1
  51. package/src/usage/summary.ts +147 -56
  52. package/src/vision/index.ts +20 -19
  53. package/src/web-search/index.ts +15 -17
  54. package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
  55. package/gui/dist/assets/index-BqbMn1sv.js +0 -40
@@ -1,4 +1,4 @@
1
- import type { OcxProviderConfig } from "../types";
1
+ import type { CodexAccountMode, OcxProviderConfig } from "../types";
2
2
  import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models";
3
3
  import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS } from "./antigravity-models";
4
4
  import {
@@ -18,10 +18,16 @@ export interface ProviderRegistryEntry {
18
18
  adapter: string;
19
19
  baseUrl: string;
20
20
  authKind: ProviderAuthKind;
21
+ codexAccountMode?: CodexAccountMode;
21
22
  /** OAuth preset may explicitly honor a persisted API-key billing mode. */
22
23
  allowKeyAuthOverride?: boolean;
23
24
  allowPrivateNetworkByDefault?: boolean;
24
25
  keyOptional?: boolean;
26
+ /**
27
+ * Free-tier pricing (no paid subscription required). Distinct from `keyOptional`:
28
+ * free tiers may still require an API key (e.g. NVIDIA NIM free credits).
29
+ */
30
+ freeTier?: boolean;
25
31
  allowBaseUrlOverride?: boolean;
26
32
  /** Static headers merged into every upstream request for this provider. */
27
33
  staticHeaders?: Record<string, string>;
@@ -38,6 +44,7 @@ export interface ProviderRegistryEntry {
38
44
  modelInputModalities?: Record<string, string[]>;
39
45
  reasoningEfforts?: string[];
40
46
  modelReasoningEfforts?: Record<string, string[]>;
47
+ modelDefaultReasoningEfforts?: Record<string, string>;
41
48
  reasoningEffortMap?: Record<string, string>;
42
49
  modelReasoningEffortMap?: Record<string, Record<string, string>>;
43
50
  noVisionModels?: string[];
@@ -53,6 +60,8 @@ export interface ProviderRegistryEntry {
53
60
  thinkingBudgetModels?: string[];
54
61
  escapeBuiltinToolNames?: boolean;
55
62
  oauthId?: string;
63
+ virtualModels?: Record<string, { wireModelId: string; reasoningMode: "pro" }>;
64
+ modelMaxInputTokens?: Record<string, number>;
56
65
  jawcodeBundle?: string;
57
66
  extraMetadataAliases?: string[];
58
67
  metadataModelIdNormalize?: MetadataModelIdNormalize;
@@ -63,9 +72,10 @@ export interface ProviderRegistryEntry {
63
72
 
64
73
  export type ProviderConfigSeed = Pick<
65
74
  OcxProviderConfig,
66
- "adapter" | "baseUrl" | "authMode" | "keyOptional" | "modelSuffixBracketStrip" | "defaultModel" | "models"
75
+ "adapter" | "baseUrl" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
67
76
  | "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
68
- | "reasoningEfforts" | "modelReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
77
+ | "modelMaxInputTokens"
78
+ | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
69
79
  | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
70
80
  | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
71
81
  | "googleMode" | "project" | "location" | "headers"
@@ -92,13 +102,29 @@ const MINIMAX_MODELS = [
92
102
  const MINIMAX_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
93
103
  MINIMAX_MODELS.map(id => [id, id === "MiniMax-M3" ? 1_000_000 : 204_800]),
94
104
  );
95
- const OPENAI_GPT56_MODELS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
96
- const OPENAI_GPT56_CONTEXT_WINDOW = 372_000;
105
+ const OPENAI_GPT56_MODELS = ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
106
+ const OPENAI_GPT56_PRO_MODELS = ["gpt-5.6-sol-pro", "gpt-5.6-terra-pro", "gpt-5.6-luna-pro"];
107
+ const OPENAI_API_GPT56_CONTEXT_WINDOW = 1_050_000;
108
+ const OPENAI_CODEX_GPT56_CONTEXT_WINDOW = 372_000;
97
109
  const OPENAI_GPT56_CONTEXT_WINDOWS = {
98
- "gpt-5.6-sol": OPENAI_GPT56_CONTEXT_WINDOW,
99
- "gpt-5.6-terra": OPENAI_GPT56_CONTEXT_WINDOW,
100
- "gpt-5.6-luna": OPENAI_GPT56_CONTEXT_WINDOW,
110
+ "gpt-5.6-sol": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
111
+ "gpt-5.6-terra": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
112
+ "gpt-5.6-luna": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
101
113
  };
114
+ const OPENAI_API_GPT56_CONTEXT_WINDOWS: Record<string, number> = {
115
+ ...Object.fromEntries([...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_CONTEXT_WINDOW])),
116
+ "gpt-5.5": OPENAI_API_GPT56_CONTEXT_WINDOW,
117
+ };
118
+ const OPENAI_API_GPT56_MAX_INPUT_TOKENS: Record<string, number> = {
119
+ ...Object.fromEntries([...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, 922_000])),
120
+ "gpt-5.5": 922_000,
121
+ };
122
+ const OPENAI_API_GPT56_VIRTUAL_MODELS: Record<string, { wireModelId: string; reasoningMode: "pro" }> = {
123
+ "gpt-5.6-sol-pro": { wireModelId: "gpt-5.6-sol", reasoningMode: "pro" },
124
+ "gpt-5.6-terra-pro": { wireModelId: "gpt-5.6-terra", reasoningMode: "pro" },
125
+ "gpt-5.6-luna-pro": { wireModelId: "gpt-5.6-luna", reasoningMode: "pro" },
126
+ };
127
+ const OPENAI_API_GPT56_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
102
128
  const OPENROUTER_GPT56_MODELS = OPENAI_GPT56_MODELS.map(id => `openai/${id}`);
103
129
  // OpenRouter's live /endpoints routes report 1,050,000; keep this separate from the
104
130
  // unverified OpenAI API-key seed. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
@@ -146,16 +172,48 @@ const DEEPSEEK_THINKING_REASONING_MAP: Record<string, string> = {
146
172
  xhigh: "max",
147
173
  max: "max",
148
174
  };
149
- // 260710 Kimi model aliases and context windows: Tier-2 evidence in
150
- // devlog/_plan/260710_provider_hardening/002_research_cn.md.
151
- const KIMI_API_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
152
- const KIMI_CODING_MODELS = [...KIMI_API_MODELS, "kimi-for-coding"];
175
+ // 260717 Kimi K3: the subscription endpoint uses one upstream id (`k3`) for both
176
+ // entitlement tiers. Bare `k3` advertises the Moderato 256K ceiling; the local `[1m]`
177
+ // alias advertises Allegretto's 1M ceiling and is stripped before the upstream request.
178
+ // The separately billed Moonshot API uses `kimi-k3`.
179
+ // Evidence: https://www.kimi.com/code/docs/en/kimi-code/models.html
180
+ // https://www.kimi.com/code/docs/en/kimi-code/error-reference.html
181
+ const KIMI_K3_STANDARD_CONTEXT_WINDOW = 262_144;
182
+ const KIMI_K3_1M_CONTEXT_WINDOW = 1_048_576;
183
+ const KIMI_CODING_K3_MODELS = ["k3", "k3[1m]"];
184
+ const KIMI_LEGACY_API_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
185
+ const KIMI_API_MODELS = ["kimi-k3", ...KIMI_LEGACY_API_MODELS];
186
+ const KIMI_CODING_MODELS = [...KIMI_CODING_K3_MODELS, ...KIMI_LEGACY_API_MODELS, "kimi-for-coding"];
153
187
  const KIMI_THINKING_MODELS = KIMI_CODING_MODELS;
188
+ const KIMI_CODING_NO_REASONING_MODELS = KIMI_CODING_MODELS.filter(id => !KIMI_CODING_K3_MODELS.includes(id));
189
+ const KIMI_API_NO_REASONING_MODELS = KIMI_API_MODELS.filter(id => id !== "kimi-k3");
190
+ const KIMI_CODING_K3_REASONING_EFFORTS = ["low", "high", "max"];
191
+ const KIMI_CODING_K3_REASONING_EFFORT_MAP: Record<string, string> = {
192
+ none: "none",
193
+ low: "low",
194
+ medium: "high",
195
+ high: "high",
196
+ xhigh: "max",
197
+ max: "max",
198
+ };
199
+ const KIMI_CODING_REASONING_EFFORTS = Object.fromEntries(
200
+ KIMI_CODING_MODELS.map(id => [id, KIMI_CODING_K3_MODELS.includes(id) ? KIMI_CODING_K3_REASONING_EFFORTS : []]),
201
+ );
202
+ const KIMI_CODING_DEFAULT_REASONING_EFFORTS = Object.fromEntries(
203
+ KIMI_CODING_K3_MODELS.map(id => [id, "max"]),
204
+ );
205
+ const KIMI_CODING_REASONING_EFFORT_MAPS = Object.fromEntries(
206
+ KIMI_CODING_K3_MODELS.map(id => [id, KIMI_CODING_K3_REASONING_EFFORT_MAP]),
207
+ );
208
+ const KIMI_API_REASONING_EFFORTS = Object.fromEntries(
209
+ KIMI_API_MODELS.map(id => [id, id === "kimi-k3" ? ["max"] : []]),
210
+ );
154
211
  const KIMI_LOCKED_PARAMETER_MODELS = KIMI_CODING_MODELS;
155
212
  const KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-for-coding"];
156
213
  const KIMI_API_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
157
- KIMI_API_MODELS.map(id => [id, 262_144]),
214
+ KIMI_API_MODELS.map(id => [id, id === "kimi-k3" ? KIMI_K3_1M_CONTEXT_WINDOW : 262_144]),
158
215
  );
216
+ const KIMI_API_MODEL_INPUT_MODALITIES = { "kimi-k3": ["text", "image"] };
159
217
 
160
218
  // 260715 NVIDIA NIM kimi family (issue #126): documented served ids on integrate
161
219
  // chat/completions per docs.api.nvidia.com/nim/reference/llm-apis; live /v1/models
@@ -168,7 +226,10 @@ const NVIDIA_NIM_KIMI_MODELS = [
168
226
  "moonshotai/kimi-k2-instruct", "moonshotai/kimi-k2-instruct-0905",
169
227
  ];
170
228
  const KIMI_CODING_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
171
- KIMI_CODING_MODELS.map(id => [id, 262_144]),
229
+ KIMI_CODING_MODELS.map(id => [id, id === "k3[1m]" ? KIMI_K3_1M_CONTEXT_WINDOW : KIMI_K3_STANDARD_CONTEXT_WINDOW]),
230
+ );
231
+ const KIMI_CODING_MODEL_INPUT_MODALITIES = Object.fromEntries(
232
+ KIMI_CODING_K3_MODELS.map(id => [id, ["text", "image"]]),
172
233
  );
173
234
  const NEURALWATT_REASONING_HISTORY_MODELS = [
174
235
  "glm-5.2", "glm-5.2-short",
@@ -201,12 +262,13 @@ const UMANS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntrie
201
262
  export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
202
263
  {
203
264
  id: "openai",
204
- label: "OpenAI (ChatGPT login)",
265
+ label: "OpenAI (Codex login)",
205
266
  adapter: "openai-responses",
206
267
  baseUrl: "https://chatgpt.com/backend-api/codex",
207
268
  authKind: "forward",
269
+ codexAccountMode: "pool",
208
270
  featured: true,
209
- note: "Uses your codex login no API key",
271
+ note: "Codex login account pool (default) or Direct main-account mode via codexAccountMode",
210
272
  },
211
273
  {
212
274
  id: "cursor",
@@ -250,20 +312,21 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
250
312
  // 260709 refresh: lineup + metadata from official docs.x.ai (grok-4.5 announced 07-08);
251
313
  // grok-composer-2.5-fast kept as account-verified (absent from public docs). Evidence:
252
314
  // devlog/model_update/260709_model_refresh/001_xai_lineup.md.
253
- models: ["grok-4.5", "grok-4.3", "grok-4.20-multi-agent-0309", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
315
+ // grok-4.20-multi-agent-0309 is intentionally absent: the OAuth chat-completions
316
+ // transport returns 400 ("Multi Agent requests are not allowed on chat completions").
317
+ 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"],
254
318
  defaultModel: "grok-4.5",
255
319
  noReasoningModels: ["grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
256
320
  // Replay assistant reasoning_content for grok reasoning models: xAI documents dropped
257
321
  // reasoning_content as the top cause of prompt-cache misses on multi-turn conversations
258
322
  // (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13 — devlog/_plan/260713_grok_caching).
259
323
  // Models that never emit reasoning simply have no thinking parts to replay (no-op).
260
- preserveReasoningContentModels: ["grok-4.5", "grok-4.3", "grok-4.20-multi-agent-0309", "grok-4.20-0309-reasoning"],
324
+ preserveReasoningContentModels: ["grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
261
325
  // grok-4.5 reasoning is always-on with low/medium/high control (no off tier upstream).
262
326
  modelReasoningEfforts: { "grok-4.5": ["low", "medium", "high"] },
263
327
  modelContextWindows: {
264
328
  "grok-4.5": 500_000,
265
329
  "grok-4.3": 1_000_000,
266
- "grok-4.20-multi-agent-0309": 1_000_000,
267
330
  "grok-4.20-0309-reasoning": 1_000_000,
268
331
  "grok-4.20-0309-non-reasoning": 1_000_000,
269
332
  "grok-build-0.1": 256_000,
@@ -306,6 +369,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
306
369
  adapter: "openai-chat",
307
370
  baseUrl: "https://api.kimi.com/coding/v1",
308
371
  authKind: "oauth",
372
+ modelSuffixBracketStrip: true,
309
373
  featured: true,
310
374
  oauthId: "kimi",
311
375
  jawcodeBundle: "moonshot",
@@ -313,9 +377,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
313
377
  models: KIMI_CODING_MODELS,
314
378
  defaultModel: "kimi-k2.7-code",
315
379
  modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
316
- // Kimi thinking is controlled by Kimi's `thinking` extension, not OpenAI `reasoning_effort`.
317
- noReasoningModels: KIMI_THINKING_MODELS,
318
- modelReasoningEfforts: Object.fromEntries(KIMI_THINKING_MODELS.map(id => [id, []])),
380
+ modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
381
+ // K3 accepts low/high/max; Codex aliases are normalized by the model-scoped wire map.
382
+ noReasoningModels: KIMI_CODING_NO_REASONING_MODELS,
383
+ modelReasoningEfforts: KIMI_CODING_REASONING_EFFORTS,
384
+ modelDefaultReasoningEfforts: KIMI_CODING_DEFAULT_REASONING_EFFORTS,
385
+ modelReasoningEffortMap: KIMI_CODING_REASONING_EFFORT_MAPS,
319
386
  noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
320
387
  noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
321
388
  noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
@@ -332,11 +399,35 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
332
399
  note: "Import-first: reuses your installed kiro-cli login (no browser). Experimental third-party harness — see Kiro ToS.",
333
400
  models: KIRO_MODELS,
334
401
  defaultModel: "kiro-auto",
402
+ // Kiro speaks CodeWhisperer wire, not OpenAI-style GET /models. Keep the static
403
+ // catalog authoritative so a spurious 2xx from runtime.../models cannot drop seeded ids
404
+ // (e.g. newly listed GPT-5.6 tiers) via live-discovery reconciliation.
405
+ liveModels: false,
335
406
  // Per-model context metadata is maintained next to the Kiro model list.
336
407
  modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
337
408
  modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
338
409
  },
339
- { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5", models: ["gpt-5.5", ...OPENAI_GPT56_MODELS], liveModels: true, modelContextWindows: OPENAI_GPT56_CONTEXT_WINDOWS },
410
+ {
411
+ id: "openai-apikey",
412
+ label: "OpenAI API",
413
+ adapter: "openai-responses",
414
+ baseUrl: "https://api.openai.com/v1",
415
+ authKind: "key",
416
+ featured: true,
417
+ dashboardUrl: "https://platform.openai.com/api-keys",
418
+ defaultModel: "gpt-5.5",
419
+ models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS],
420
+ liveModels: true,
421
+ modelContextWindows: OPENAI_API_GPT56_CONTEXT_WINDOWS,
422
+ modelMaxInputTokens: OPENAI_API_GPT56_MAX_INPUT_TOKENS,
423
+ modelInputModalities: Object.fromEntries(
424
+ ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, ["text", "image"]]),
425
+ ),
426
+ modelReasoningEfforts: Object.fromEntries(
427
+ [...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS]),
428
+ ),
429
+ virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS,
430
+ },
340
431
  {
341
432
  id: "umans",
342
433
  label: "Umans AI Coding Plan",
@@ -365,17 +456,22 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
365
456
  id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
366
457
  authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
367
458
  jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
459
+ modelContextWindows: { "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW },
460
+ modelInputModalities: { "kimi-k3": ["text", "image"] },
368
461
  modelReasoningEfforts: {
369
462
  "glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
463
+ "kimi-k3": KIMI_CODING_K3_REASONING_EFFORTS,
370
464
  "kimi-k2.7-code": [],
371
465
  "kimi-k2.7-code-highspeed": [],
372
466
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS])),
373
467
  ...Object.fromEntries(OPENCODE_GO_THINKING_BUDGET_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
374
468
  ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
375
469
  },
470
+ modelDefaultReasoningEfforts: { "kimi-k3": "max" },
376
471
  // glm-5.2 uses identity labels now that `max` is a native Codex level (no alias map);
377
472
  // the thinking-toggle map is a REAL wire alias (effort -> enabled/disabled) and stays.
378
473
  modelReasoningEffortMap: {
474
+ "kimi-k3": KIMI_CODING_K3_REASONING_EFFORT_MAP,
379
475
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
380
476
  ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
381
477
  },
@@ -392,12 +488,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
392
488
  "minimax-m2.5", "minimax-m2.7",
393
489
  "qwen3.7-max",
394
490
  ],
395
- noTemperatureModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
396
- noTopPModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
397
- noPenaltyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
491
+ noTemperatureModels: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
492
+ noTopPModels: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
493
+ noPenaltyModels: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
398
494
  autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
399
495
  // Issue #78: DeepSeek V4 thinking mode requires reasoning_content replay on tool-call turns.
400
- preserveReasoningContentModels: ["glm-5.2", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", ...DEEPSEEK_THINKING_MODELS],
496
+ preserveReasoningContentModels: ["glm-5.2", "kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", ...DEEPSEEK_THINKING_MODELS],
401
497
  },
402
498
  {
403
499
  id: "neuralwatt",
@@ -502,8 +598,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
502
598
  dashboardUrl: "https://platform.moonshot.ai/console/api-keys", defaultModel: "kimi-k2.7-code", jawcodeBundle: "moonshot",
503
599
  models: KIMI_API_MODELS,
504
600
  modelContextWindows: KIMI_API_MODEL_CONTEXT_WINDOWS,
505
- noReasoningModels: KIMI_API_MODELS,
506
- modelReasoningEfforts: Object.fromEntries(KIMI_API_MODELS.map(id => [id, []])),
601
+ modelInputModalities: KIMI_API_MODEL_INPUT_MODALITIES,
602
+ noReasoningModels: KIMI_API_NO_REASONING_MODELS,
603
+ modelReasoningEfforts: KIMI_API_REASONING_EFFORTS,
507
604
  noTemperatureModels: KIMI_API_MODELS,
508
605
  noTopPModels: KIMI_API_MODELS,
509
606
  noPenaltyModels: KIMI_API_MODELS,
@@ -521,10 +618,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
521
618
  // its working reasoning_effort. Future kimi ids must be appended individually.
522
619
  {
523
620
  id: "nvidia", label: "NVIDIA NIM", baseUrl: "https://integrate.api.nvidia.com/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://build.nvidia.com",
621
+ // Free pricing, but an API key is still required (free key from build.nvidia.com).
622
+ freeTier: true,
524
623
  parallelToolCalls: false,
525
624
  noReasoningModels: NVIDIA_NIM_KIMI_MODELS,
526
625
  modelReasoningEfforts: Object.fromEntries(NVIDIA_NIM_KIMI_MODELS.map(id => [id, []])),
527
626
  preserveReasoningContentModels: NVIDIA_NIM_KIMI_THINKING_MODELS,
627
+ note: "Free tier on NVIDIA NIM — API key still required (get a free key at build.nvidia.com).",
528
628
  },
529
629
  { id: "venice", label: "Venice", baseUrl: "https://api.venice.ai/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://venice.ai/settings/api" },
530
630
  // 260710 GLM-5.2 context and path-specific ids: Tier-2 evidence in
@@ -599,10 +699,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
599
699
  {
600
700
  id: "kimi-code", label: "Kimi (coding)", baseUrl: "https://api.kimi.com/coding/v1", adapter: "openai-chat", authKind: "key",
601
701
  dashboardUrl: "https://platform.moonshot.cn/console/api-keys", defaultModel: "kimi-k2.7-code",
702
+ modelSuffixBracketStrip: true,
602
703
  models: KIMI_CODING_MODELS,
603
704
  modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
604
- noReasoningModels: KIMI_THINKING_MODELS,
605
- modelReasoningEfforts: Object.fromEntries(KIMI_THINKING_MODELS.map(id => [id, []])),
705
+ modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
706
+ noReasoningModels: KIMI_CODING_NO_REASONING_MODELS,
707
+ modelReasoningEfforts: KIMI_CODING_REASONING_EFFORTS,
708
+ modelDefaultReasoningEfforts: KIMI_CODING_DEFAULT_REASONING_EFFORTS,
709
+ modelReasoningEffortMap: KIMI_CODING_REASONING_EFFORT_MAPS,
606
710
  noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
607
711
  noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
608
712
  noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
@@ -657,6 +761,19 @@ export function getProviderRegistryEntry(id: string): ProviderRegistryEntry | un
657
761
  return PROVIDER_REGISTRY.find(entry => entry.id === id);
658
762
  }
659
763
 
764
+ /**
765
+ * Effective Codex account mode for a provider. For canonical `openai`, a valid persisted
766
+ * `codexAccountMode` on the provider config wins and a missing/invalid value defaults to
767
+ * `"pool"`. Other providers keep registry-only metadata (there is no mode for `openai-apikey`).
768
+ */
769
+ export function providerCodexAccountMode(id: string, provider?: OcxProviderConfig): CodexAccountMode | undefined {
770
+ const registryMode = getProviderRegistryEntry(id)?.codexAccountMode;
771
+ if (id !== "openai") return registryMode;
772
+ const persisted = provider?.codexAccountMode;
773
+ if (persisted === "pool" || persisted === "direct") return persisted;
774
+ return registryMode ?? "pool";
775
+ }
776
+
660
777
  /**
661
778
  * Effective Google wire mode for a provider: config value, else registry backfill (a saved
662
779
  * key-login config may omit `googleMode` — mirrors the router's backfill), else "ai-studio"
@@ -58,23 +58,24 @@ export function sanitizeCodexReasoningEfforts(efforts: readonly string[] | undef
58
58
  export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId: string): string[] | undefined {
59
59
  if (modelInList(provider.noReasoningModels, modelId)) return [];
60
60
  const modelEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId);
61
- if (modelEfforts !== undefined) return healMaxTier(provider, modelId, sanitizeCodexReasoningEfforts(modelEfforts) ?? []);
62
- if (provider.reasoningEfforts !== undefined) return healMaxTier(provider, modelId, sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? []);
61
+ if (modelEfforts !== undefined) return healMappedTiers(provider, modelId, sanitizeCodexReasoningEfforts(modelEfforts) ?? []);
62
+ if (provider.reasoningEfforts !== undefined) return healMappedTiers(provider, modelId, sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? []);
63
63
  return undefined;
64
64
  }
65
65
 
66
66
  /**
67
- * Stale-ladder self-heal: saved configs seeded before `max` became a native Codex level can
68
- * advertise a ladder that stops at `xhigh` while the wire map already routes xhigh -> max
69
- * (e.g. opencode-go glm-5.2, deepseek thinking models). When the map proves the provider
70
- * accepts wire `max`, append `max` so the picker actually shows the top tier. Thinking-toggle
71
- * maps (xhigh -> "enabled") never match, so binary-toggle models stay two-step.
67
+ * Stale-ladder self-heal: a registry wire map is authoritative evidence of the upstream tiers
68
+ * it can emit. Merge Codex-native map values into an older persisted ladder so newly documented
69
+ * tiers appear without rewriting the user's config. Non-Codex values such as enabled/disabled
70
+ * and Kimi's none sentinel are ignored here; they remain request-only wire aliases.
72
71
  */
73
- function healMaxTier(provider: OcxProviderConfig, modelId: string, efforts: string[]): string[] {
74
- if (efforts.includes("max") || !efforts.includes("xhigh")) return efforts;
72
+ function healMappedTiers(provider: OcxProviderConfig, modelId: string, efforts: string[]): string[] {
73
+ if (efforts.length === 0) return efforts;
75
74
  const wireMap = reasoningEffortMapFor(provider, modelId);
76
- if (wireMap?.xhigh !== "max" && wireMap?.max !== "max") return efforts;
77
- return sanitizeCodexReasoningEfforts([...efforts, "max"]) ?? efforts;
75
+ if (!wireMap) return efforts;
76
+ const mappedTiers = Object.values(wireMap).filter(isCodexReasoningEffort);
77
+ if (mappedTiers.length === 0) return efforts;
78
+ return sanitizeCodexReasoningEfforts([...efforts, ...mappedTiers]) ?? efforts;
78
79
  }
79
80
 
80
81
  function requestToCodexEffort(requested: string): string | undefined {
package/src/router.ts CHANGED
@@ -1,12 +1,16 @@
1
- import type { OcxConfig, OcxProviderConfig } from "./types";
1
+ import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
2
+ import { COMBO_NAMESPACE, tryPickComboModel, type ComboPick } from "./combos";
2
3
  import { hasOwnProvider, resolveEnvValue } from "./config";
3
4
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
4
- import { PROVIDER_REGISTRY } from "./providers/registry";
5
+ import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
6
+ import { LEGACY_CHATGPT_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
5
7
 
6
- interface RouteResult {
8
+ export interface RouteResult {
7
9
  providerName: string;
8
10
  provider: OcxProviderConfig;
9
11
  modelId: string;
12
+ codexAccountMode?: CodexAccountMode;
13
+ combo?: ComboPick;
10
14
  }
11
15
 
12
16
  const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string[] }> = [
@@ -16,12 +20,6 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
16
20
  "claude-", "claude-sonnet-", "claude-opus-", "claude-haiku-",
17
21
  ],
18
22
  },
19
- {
20
- providerNames: ["openai", "chatgpt", "openai-apikey"],
21
- prefixes: [
22
- "gpt-", "o1-", "o3-", "o4-",
23
- ],
24
- },
25
23
  {
26
24
  providerNames: ["groq"],
27
25
  prefixes: [
@@ -67,6 +65,18 @@ function mergeRecordFill<T>(
67
65
  return { ...(seed ?? {}), ...(user ?? {}) };
68
66
  }
69
67
 
68
+ function mergePositiveNumberCaps(
69
+ seed: Record<string, number> | undefined,
70
+ user: Record<string, number> | undefined,
71
+ ): Record<string, number> | undefined {
72
+ if (!seed && !user) return undefined;
73
+ const out = { ...(seed ?? {}) };
74
+ for (const [key, value] of Object.entries(user ?? {})) {
75
+ out[key] = typeof out[key] === "number" ? Math.min(out[key]!, value) : value;
76
+ }
77
+ return out;
78
+ }
79
+
70
80
  function mergeStringArrayRecord(
71
81
  seed: Record<string, string[]> | undefined,
72
82
  user: Record<string, string[]> | undefined,
@@ -95,8 +105,14 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
95
105
  const reasoningEffortMap = mergeRecord(registryEntry.reasoningEffortMap, provider.reasoningEffortMap);
96
106
  const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
97
107
  const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
98
- const modelContextWindows = mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows);
108
+ const modelDefaultReasoningEfforts = mergeRecordFill(registryEntry.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts);
109
+ const modelContextWindows = providerName === OPENAI_API_PROVIDER_ID
110
+ ? mergePositiveNumberCaps(registryEntry.modelContextWindows, provider.modelContextWindows)
111
+ : mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows);
99
112
  const modelInputModalities = mergeRecordFill(registryEntry.modelInputModalities, provider.modelInputModalities);
113
+ const modelMaxInputTokens = providerName === OPENAI_API_PROVIDER_ID
114
+ ? mergePositiveNumberCaps(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens)
115
+ : mergeRecordFill(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens);
100
116
  const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
101
117
  const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
102
118
  const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
@@ -140,7 +156,9 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
140
156
  ...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
141
157
  ...(modelContextWindows ? { modelContextWindows } : {}),
142
158
  ...(modelInputModalities ? { modelInputModalities } : {}),
159
+ ...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
143
160
  ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
161
+ ...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
144
162
  ...(reasoningEffortMap ? { reasoningEffortMap } : {}),
145
163
  ...(modelReasoningEffortMap ? { modelReasoningEffortMap } : {}),
146
164
  ...(noVisionModels ? { noVisionModels } : {}),
@@ -156,10 +174,44 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
156
174
  }
157
175
 
158
176
  function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][] {
159
- return Object.entries(config.providers).filter(([, provider]) => provider.disabled !== true);
177
+ return Object.entries(config.providers)
178
+ .filter(([name, provider]) => name !== LEGACY_CHATGPT_PROVIDER_ID && provider.disabled !== true);
179
+ }
180
+
181
+ export class NoEnabledOpenAiProviderError extends Error {
182
+ constructor(modelId: string) {
183
+ super(`No enabled canonical OpenAI provider for model: ${modelId}`);
184
+ this.name = "NoEnabledOpenAiProviderError";
185
+ }
186
+ }
187
+
188
+ function isBareOpenAiFamilyModel(modelId: string): boolean {
189
+ return !modelId.includes("/") && /^(?:gpt-|o1-|o3-|o4-)/.test(modelId);
190
+ }
191
+
192
+ function routeResult(providerName: string, provider: OcxProviderConfig, modelId: string): RouteResult {
193
+ const codexAccountMode = providerCodexAccountMode(providerName, provider);
194
+ return {
195
+ providerName,
196
+ provider: routedProviderConfig(providerName, provider),
197
+ modelId,
198
+ ...(codexAccountMode ? { codexAccountMode } : {}),
199
+ };
160
200
  }
161
201
 
162
202
  export function routeModel(config: OcxConfig, modelId: string): RouteResult {
203
+ const preservePhysicalComboProvider =
204
+ hasOwnProvider(config.providers, COMBO_NAMESPACE)
205
+ && Object.keys(config.combos ?? {}).length === 0;
206
+ if (!preservePhysicalComboProvider) {
207
+ const combo = tryPickComboModel(config, modelId);
208
+ if (combo) {
209
+ const concrete = `${combo.target.provider}/${combo.target.model}`;
210
+ const routed = routeModel(config, concrete);
211
+ return { ...routed, combo };
212
+ }
213
+ }
214
+
163
215
  // 0. Explicit "<provider>/<model>" namespace (e.g. "opencode-go/deepseek-v4-pro").
164
216
  // Only triggers when the prefix matches a CONFIGURED provider, so genuine
165
217
  // slash-containing model ids (e.g. "anthropic/claude-...") fall through when
@@ -167,24 +219,25 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
167
219
  const slash = modelId.indexOf("/");
168
220
  if (slash > 0) {
169
221
  const provName = modelId.slice(0, slash);
222
+ if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
223
+ throw new Error(`No provider configured for model: ${modelId}`);
224
+ }
170
225
  if (hasOwnProvider(config.providers, provName)) {
171
226
  const prov = config.providers[provName];
172
227
  if (prov.disabled === true) throw new Error(`Provider is disabled: ${provName}`);
173
- return {
174
- providerName: provName,
175
- provider: routedProviderConfig(provName, prov),
176
- modelId: modelId.slice(slash + 1),
177
- };
228
+ return routeResult(provName, prov, modelId.slice(slash + 1));
178
229
  }
179
230
  }
180
231
 
232
+ if (isBareOpenAiFamilyModel(modelId)) {
233
+ const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
234
+ if (provider && provider.disabled !== true) return routeResult(OPENAI_CODEX_PROVIDER_ID, provider, modelId);
235
+ throw new NoEnabledOpenAiProviderError(modelId);
236
+ }
237
+
181
238
  for (const [provName, prov] of activeProviderEntries(config)) {
182
239
  if (prov.defaultModel === modelId) {
183
- return {
184
- providerName: provName,
185
- provider: routedProviderConfig(provName, prov),
186
- modelId,
187
- };
240
+ return routeResult(provName, prov, modelId);
188
241
  }
189
242
  }
190
243
 
@@ -193,22 +246,17 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
193
246
 
194
247
  for (const [provName, prov] of activeProviderEntries(config)) {
195
248
  if (prov.models && Array.isArray(prov.models) && (prov.models as string[]).includes(modelId)) {
196
- return {
197
- providerName: provName,
198
- provider: routedProviderConfig(provName, prov),
199
- modelId,
200
- };
249
+ return routeResult(provName, prov, modelId);
201
250
  }
202
251
  }
203
252
 
253
+ if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
254
+ throw new Error(`No provider configured for model: ${modelId}`);
255
+ }
204
256
  if (hasOwnProvider(config.providers, config.defaultProvider)) {
205
257
  const defaultProv = config.providers[config.defaultProvider];
206
258
  if (defaultProv.disabled === true) throw new Error(`Default provider is disabled: ${config.defaultProvider}`);
207
- return {
208
- providerName: config.defaultProvider,
209
- provider: routedProviderConfig(config.defaultProvider, defaultProv),
210
- modelId,
211
- };
259
+ return routeResult(config.defaultProvider, defaultProv, modelId);
212
260
  }
213
261
 
214
262
  throw new Error(`No provider configured for model: ${modelId}`);
@@ -222,11 +270,7 @@ function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResu
222
270
  );
223
271
  if (matchingProvider) {
224
272
  const [provName, prov] = matchingProvider;
225
- return {
226
- providerName: provName,
227
- provider: routedProviderConfig(provName, prov),
228
- modelId,
229
- };
273
+ return routeResult(provName, prov, modelId);
230
274
  }
231
275
  }
232
276
  }