@bitkyc08/opencodex 2.7.23 → 2.7.24

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-BzhyTAco.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 +112 -20
  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-DQjt6Hly.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.
@@ -161,8 +187,23 @@ const KIMI_CODING_MODELS = [...KIMI_CODING_K3_MODELS, ...KIMI_LEGACY_API_MODELS,
161
187
  const KIMI_THINKING_MODELS = KIMI_CODING_MODELS;
162
188
  const KIMI_CODING_NO_REASONING_MODELS = KIMI_CODING_MODELS.filter(id => !KIMI_CODING_K3_MODELS.includes(id));
163
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
+ };
164
199
  const KIMI_CODING_REASONING_EFFORTS = Object.fromEntries(
165
- KIMI_CODING_MODELS.map(id => [id, KIMI_CODING_K3_MODELS.includes(id) ? ["max"] : []]),
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]),
166
207
  );
167
208
  const KIMI_API_REASONING_EFFORTS = Object.fromEntries(
168
209
  KIMI_API_MODELS.map(id => [id, id === "kimi-k3" ? ["max"] : []]),
@@ -221,12 +262,13 @@ const UMANS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntrie
221
262
  export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
222
263
  {
223
264
  id: "openai",
224
- label: "OpenAI (ChatGPT login)",
265
+ label: "OpenAI (Codex login)",
225
266
  adapter: "openai-responses",
226
267
  baseUrl: "https://chatgpt.com/backend-api/codex",
227
268
  authKind: "forward",
269
+ codexAccountMode: "pool",
228
270
  featured: true,
229
- note: "Uses your codex login no API key",
271
+ note: "Codex login account pool (default) or Direct main-account mode via codexAccountMode",
230
272
  },
231
273
  {
232
274
  id: "cursor",
@@ -270,20 +312,21 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
270
312
  // 260709 refresh: lineup + metadata from official docs.x.ai (grok-4.5 announced 07-08);
271
313
  // grok-composer-2.5-fast kept as account-verified (absent from public docs). Evidence:
272
314
  // devlog/model_update/260709_model_refresh/001_xai_lineup.md.
273
- 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"],
274
318
  defaultModel: "grok-4.5",
275
319
  noReasoningModels: ["grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
276
320
  // Replay assistant reasoning_content for grok reasoning models: xAI documents dropped
277
321
  // reasoning_content as the top cause of prompt-cache misses on multi-turn conversations
278
322
  // (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13 — devlog/_plan/260713_grok_caching).
279
323
  // Models that never emit reasoning simply have no thinking parts to replay (no-op).
280
- 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"],
281
325
  // grok-4.5 reasoning is always-on with low/medium/high control (no off tier upstream).
282
326
  modelReasoningEfforts: { "grok-4.5": ["low", "medium", "high"] },
283
327
  modelContextWindows: {
284
328
  "grok-4.5": 500_000,
285
329
  "grok-4.3": 1_000_000,
286
- "grok-4.20-multi-agent-0309": 1_000_000,
287
330
  "grok-4.20-0309-reasoning": 1_000_000,
288
331
  "grok-4.20-0309-non-reasoning": 1_000_000,
289
332
  "grok-build-0.1": 256_000,
@@ -335,9 +378,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
335
378
  defaultModel: "kimi-k2.7-code",
336
379
  modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
337
380
  modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
338
- // K3 accepts reasoning_effort=max; older Kimi models use Kimi's private thinking control.
381
+ // K3 accepts low/high/max; Codex aliases are normalized by the model-scoped wire map.
339
382
  noReasoningModels: KIMI_CODING_NO_REASONING_MODELS,
340
383
  modelReasoningEfforts: KIMI_CODING_REASONING_EFFORTS,
384
+ modelDefaultReasoningEfforts: KIMI_CODING_DEFAULT_REASONING_EFFORTS,
385
+ modelReasoningEffortMap: KIMI_CODING_REASONING_EFFORT_MAPS,
341
386
  noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
342
387
  noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
343
388
  noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
@@ -354,11 +399,35 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
354
399
  note: "Import-first: reuses your installed kiro-cli login (no browser). Experimental third-party harness — see Kiro ToS.",
355
400
  models: KIRO_MODELS,
356
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,
357
406
  // Per-model context metadata is maintained next to the Kiro model list.
358
407
  modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
359
408
  modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
360
409
  },
361
- { 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
+ },
362
431
  {
363
432
  id: "umans",
364
433
  label: "Umans AI Coding Plan",
@@ -387,17 +456,22 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
387
456
  id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
388
457
  authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
389
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"] },
390
461
  modelReasoningEfforts: {
391
462
  "glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
463
+ "kimi-k3": KIMI_CODING_K3_REASONING_EFFORTS,
392
464
  "kimi-k2.7-code": [],
393
465
  "kimi-k2.7-code-highspeed": [],
394
466
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS])),
395
467
  ...Object.fromEntries(OPENCODE_GO_THINKING_BUDGET_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
396
468
  ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
397
469
  },
470
+ modelDefaultReasoningEfforts: { "kimi-k3": "max" },
398
471
  // glm-5.2 uses identity labels now that `max` is a native Codex level (no alias map);
399
472
  // the thinking-toggle map is a REAL wire alias (effort -> enabled/disabled) and stays.
400
473
  modelReasoningEffortMap: {
474
+ "kimi-k3": KIMI_CODING_K3_REASONING_EFFORT_MAP,
401
475
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
402
476
  ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
403
477
  },
@@ -414,12 +488,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
414
488
  "minimax-m2.5", "minimax-m2.7",
415
489
  "qwen3.7-max",
416
490
  ],
417
- noTemperatureModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
418
- noTopPModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
419
- 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"],
420
494
  autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
421
495
  // Issue #78: DeepSeek V4 thinking mode requires reasoning_content replay on tool-call turns.
422
- 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],
423
497
  },
424
498
  {
425
499
  id: "neuralwatt",
@@ -544,10 +618,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
544
618
  // its working reasoning_effort. Future kimi ids must be appended individually.
545
619
  {
546
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,
547
623
  parallelToolCalls: false,
548
624
  noReasoningModels: NVIDIA_NIM_KIMI_MODELS,
549
625
  modelReasoningEfforts: Object.fromEntries(NVIDIA_NIM_KIMI_MODELS.map(id => [id, []])),
550
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).",
551
628
  },
552
629
  { id: "venice", label: "Venice", baseUrl: "https://api.venice.ai/api/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://venice.ai/settings/api" },
553
630
  // 260710 GLM-5.2 context and path-specific ids: Tier-2 evidence in
@@ -628,6 +705,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
628
705
  modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
629
706
  noReasoningModels: KIMI_CODING_NO_REASONING_MODELS,
630
707
  modelReasoningEfforts: KIMI_CODING_REASONING_EFFORTS,
708
+ modelDefaultReasoningEfforts: KIMI_CODING_DEFAULT_REASONING_EFFORTS,
709
+ modelReasoningEffortMap: KIMI_CODING_REASONING_EFFORT_MAPS,
631
710
  noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
632
711
  noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
633
712
  noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
@@ -682,6 +761,19 @@ export function getProviderRegistryEntry(id: string): ProviderRegistryEntry | un
682
761
  return PROVIDER_REGISTRY.find(entry => entry.id === id);
683
762
  }
684
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
+
685
777
  /**
686
778
  * Effective Google wire mode for a provider: config value, else registry backfill (a saved
687
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
  }
@@ -2,11 +2,13 @@ import { timingSafeEqual } from "node:crypto";
2
2
  import { formatErrorResponse } from "../bridge";
3
3
  import {
4
4
  codexAutoStartEnabled,
5
+ positiveIntegerRecordConfigError,
5
6
  providerBaseUrlConfigError,
6
7
  providerHeadersConfigError,
7
8
  } from "../config";
8
9
  import { providerDestinationConfigError } from "../lib/destination-policy";
9
- import { getProviderRegistryEntry } from "../providers/registry";
10
+ import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers/registry";
11
+ import { providerConfigSeed } from "../providers/derive";
10
12
  import type { OcxConfig, OcxProviderConfig } from "../types";
11
13
 
12
14
  let _corsOrigin = "http://localhost:10100";
@@ -139,6 +141,18 @@ export function isProxyAdmissionSecret(token: string, config: OcxConfig): boolea
139
141
  return false;
140
142
  }
141
143
 
144
+ export class ForwardAdmissionCredentialError extends Error {
145
+ constructor() {
146
+ super("OpenCodex admission credentials cannot be forwarded upstream");
147
+ this.name = "ForwardAdmissionCredentialError";
148
+ }
149
+ }
150
+
151
+ export function validateForwardAdmissionCredential(headers: Headers, config: OcxConfig): void {
152
+ const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
153
+ if (bearer && isProxyAdmissionSecret(bearer, config)) throw new ForwardAdmissionCredentialError();
154
+ }
155
+
142
156
  export function hasValidApiAuth(req: Request, config: OcxConfig): boolean {
143
157
  if (!isApiAuthRequired(config)) return true;
144
158
  const actual = req.headers.get("x-opencodex-api-key")?.trim()
@@ -155,18 +169,77 @@ export function requireApiAuth(req: Request, config: OcxConfig, kind: "managemen
155
169
  return formatErrorResponse(401, "authentication_error", "opencodex API key required");
156
170
  }
157
171
 
158
- export function providerManagementConfigError(name: string, provider: OcxProviderConfig): string | null {
159
- const baseUrlError = providerBaseUrlConfigError(provider.baseUrl);
172
+ /**
173
+ * Admission for OpenAI Responses transports whose Authorization header belongs to
174
+ * Codex Direct. Remote binds must use the dedicated proxy header so the two bearer
175
+ * domains can never be confused.
176
+ */
177
+ export function requireResponsesApiAuth(req: Request, config: OcxConfig): Response | null {
178
+ if (!isApiAuthRequired(config)) return null;
179
+ const actual = req.headers.get("x-opencodex-api-key")?.trim();
180
+ if (actual && isProxyAdmissionSecret(actual, config)) return null;
181
+ return formatErrorResponse(401, "authentication_error", "opencodex API key required");
182
+ }
183
+
184
+ const FORBIDDEN_PROVIDER_RUNTIME_FIELDS = [
185
+ "virtualModels", "codexAuthContext", "selectedForwardHeaders",
186
+ "sidecarOutcomeRecorder", "_codexAccountOverride", "_codexAccountRequired",
187
+ ] as const;
188
+
189
+ function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: OcxProviderConfig): boolean {
190
+ const actualKeys = Object.keys(actual).sort();
191
+ const expectedKeys = Object.keys(expected).sort();
192
+ if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, i) => key !== expectedKeys[i])) return false;
193
+ return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
194
+ }
195
+
196
+ export function providerManagementConfigError(name: unknown, provider: unknown): string | null {
197
+ if (typeof name !== "string" || !provider || typeof provider !== "object" || Array.isArray(provider)) {
198
+ return "provider must be a plain object";
199
+ }
200
+ const raw = provider as Record<string, unknown>;
201
+ for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) {
202
+ if (Object.hasOwn(raw, field)) return `provider ${name} must not include runtime field "${field}"`;
203
+ }
204
+ if (name === "chatgpt") return "provider chatgpt is reserved for internal credential compatibility";
205
+ if (name === "openai-multi") return "provider openai-multi is reserved for legacy config migration";
206
+ if (name === "openai") {
207
+ const entry = getProviderRegistryEntry(name);
208
+ const seed = entry ? providerConfigSeed(entry) : undefined;
209
+ if (!Object.hasOwn(raw, "codexAccountMode") || (raw.codexAccountMode !== "pool" && raw.codexAccountMode !== "direct")) {
210
+ return "provider openai codexAccountMode must be pool or direct";
211
+ }
212
+ if (seed) seed.codexAccountMode = raw.codexAccountMode;
213
+ const canonical = seed && sameCanonicalProviderSeed(raw, seed);
214
+ if (!canonical) {
215
+ return `provider ${name} must equal the canonical built-in provider seed`;
216
+ }
217
+ } else if (Object.hasOwn(raw, "codexAccountMode")) {
218
+ return `provider ${name} must not include codexAccountMode`;
219
+ }
220
+ const typed = provider as unknown as OcxProviderConfig;
221
+ const baseUrlError = providerBaseUrlConfigError(typed.baseUrl);
160
222
  if (baseUrlError) return `provider ${name} ${baseUrlError}`;
161
- const destinationError = providerDestinationConfigError(name, provider);
223
+ const destinationError = providerDestinationConfigError(name, typed);
162
224
  if (destinationError) return `provider ${name} ${destinationError}`;
163
- const headersError = providerHeadersConfigError(provider.headers);
225
+ const headersError = providerHeadersConfigError(typed.headers);
164
226
  if (headersError) return `provider ${name} ${headersError}`;
165
- if (provider.authMode === "forward") {
227
+ const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
228
+ if (maxInputError) return `provider ${name} ${maxInputError}`;
229
+ if (typed.authMode === "local") {
230
+ // "local" bypasses key-requirement enforcement (api-keys/key-failover treat non-oauth/
231
+ // forward as key auth; openai-chat skips credential checks for local). Only providers
232
+ // whose registry entry is genuinely local (Ollama/vLLM/LM Studio) may claim it.
233
+ const entry = getProviderRegistryEntry(name);
234
+ if (entry && entry.authKind !== "local") {
235
+ return `provider ${name} cannot use authMode "local" — its registry entry requires ${entry.authKind} auth`;
236
+ }
237
+ }
238
+ if (typed.authMode === "forward") {
166
239
  const normalizedName = name.trim().toLowerCase();
167
- const base = provider.baseUrl.replace(/\/+$/, "");
168
- const isBuiltInChatGptForward = (normalizedName === "openai" || normalizedName === "chatgpt")
169
- && provider.adapter === "openai-responses"
240
+ const base = typed.baseUrl.replace(/\/+$/, "");
241
+ const isBuiltInChatGptForward = normalizedName === "openai"
242
+ && typed.adapter === "openai-responses"
170
243
  && base === "https://chatgpt.com/backend-api/codex";
171
244
  if (isBuiltInChatGptForward) return null;
172
245
  return `provider ${name} uses reserved authMode "forward"; configure ChatGPT passthrough via the built-in provider`;
@@ -212,6 +285,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
212
285
  "allowPrivateNetwork",
213
286
  "authMode",
214
287
  "keyOptional",
288
+ "freeTier",
215
289
  "liveModels",
216
290
  "models",
217
291
  "contextWindow",
@@ -231,6 +305,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
231
305
  }
232
306
  const registryNote = getProviderRegistryEntry(name)?.note;
233
307
  if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
308
+ const codexAccountMode = providerCodexAccountMode(name, provider);
309
+ if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
234
310
  providers[name] = dto;
235
311
  }
236
312
  return {