@bitkyc08/opencodex 2.10.1 → 2.10.2

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 (57) hide show
  1. package/bin/ocx.mjs +18 -9
  2. package/gui/dist/assets/index-BKVqyYqT.js +70 -0
  3. package/gui/dist/assets/{index-Cd6_PBKn.css → index-Ca_3269W.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  6. package/gui/dist/provider-icons/openai.svg +1 -1
  7. package/package.json +1 -1
  8. package/src/adapters/command-code.ts +453 -0
  9. package/src/adapters/google.ts +3 -0
  10. package/src/cli/claude.ts +37 -22
  11. package/src/cli/index.ts +16 -3
  12. package/src/cli/launcher-context.ts +77 -0
  13. package/src/codex/admission.ts +1 -1
  14. package/src/codex/app-server-processes.ts +44 -1
  15. package/src/codex/catalog/sync.ts +22 -3
  16. package/src/codex/catalog-write-serialization.ts +1 -1
  17. package/src/codex/codex-write-lock.ts +1 -1
  18. package/src/codex/convergence-types.ts +1 -1
  19. package/src/codex/desired-state.ts +27 -7
  20. package/src/codex/history-job.ts +1 -1
  21. package/src/codex/history-lock.ts +1 -1
  22. package/src/codex/history-worker.ts +1 -1
  23. package/src/codex/internal/history-writer.ts +1 -1
  24. package/src/codex/transition-state.ts +1 -1
  25. package/src/codex/user-identity.ts +1 -1
  26. package/src/config.ts +6 -1
  27. package/src/integrations/config-io.ts +1 -1
  28. package/src/integrations/journal.ts +1 -1
  29. package/src/integrations/merge.ts +1 -1
  30. package/src/integrations/ownership.ts +1 -1
  31. package/src/integrations/registry.ts +1 -1
  32. package/src/integrations/serialize.ts +1 -1
  33. package/src/integrations/state.ts +1 -1
  34. package/src/integrations/store.ts +1 -1
  35. package/src/integrations/writer.ts +1 -1
  36. package/src/lib/bounded-body.ts +3 -1
  37. package/src/lib/bun-runtime.ts +21 -17
  38. package/src/lib/bun-stream-caps.ts +1 -1
  39. package/src/lib/local-management-attestation.ts +51 -0
  40. package/src/lib/shadow-call.ts +4 -4
  41. package/src/oauth/command-code.ts +239 -0
  42. package/src/oauth/health.ts +46 -2
  43. package/src/oauth/index.ts +38 -3
  44. package/src/providers/command-code-efforts.ts +85 -0
  45. package/src/providers/google-vertex-location.ts +14 -0
  46. package/src/providers/registry.ts +138 -26
  47. package/src/routing/capability.ts +1 -0
  48. package/src/server/adapter-resolve.ts +3 -0
  49. package/src/server/auth-cors.ts +6 -1
  50. package/src/server/index.ts +44 -2
  51. package/src/server/management/integration-routes.ts +1 -1
  52. package/src/server/management/native-integration-routes.ts +2 -2
  53. package/src/server/responses/core.ts +13 -9
  54. package/src/storage/scanner.ts +1 -1
  55. package/src/types.ts +6 -0
  56. package/src/usage/log.ts +1 -1
  57. package/gui/dist/assets/index-ChZQsmBY.js +0 -70
@@ -14,6 +14,7 @@ import {
14
14
  cursorModelInputModalities,
15
15
  cursorModelReasoningEfforts,
16
16
  } from "../adapters/cursor/discovery";
17
+ import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "./command-code-efforts";
17
18
 
18
19
  export type ProviderAuthKind = "forward" | "oauth" | "key" | "local";
19
20
  export type MetadataModelIdNormalize = "case-insensitive";
@@ -348,16 +349,89 @@ const THINKING_BUDGET_MODELS = [
348
349
  const OPENCODE_GO_THINKING_BUDGET_MODELS = ["qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"];
349
350
  const DEEPSEEK_THINKING_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"];
350
351
  const OPENCODE_FREE_DEEPSEEK_MODELS = ["deepseek-v4-flash-free"];
351
- // "max" is advertised too: the wire map routes xhigh->max and max->max, so the picker
352
- // should surface the max tier instead of hiding it behind xhigh.
353
- const DEEPSEEK_THINKING_EFFORTS = ["high", "xhigh", "max"];
354
- const DEEPSEEK_THINKING_REASONING_MAP: Record<string, string> = {
352
+ /*
353
+ * Zen free models that reject `image_url` upstream (#1043, and the reproducible
354
+ * half of #1024).
355
+ *
356
+ * Zen publishes NO modality metadata — its `/v1/models` returns only id, object,
357
+ * created, owned_by — so this list is measured, not derived. Each id was probed
358
+ * once against https://opencode.ai/zen/v1 on 2026-08-05 with a text control first
359
+ * and then a 1x1 PNG; the six below failed the image request, four of them with
360
+ * `[404] No endpoints found that support image input` and `big-pickle` with the
361
+ * exact deserialize error quoted in #1043.
362
+ *
363
+ * `mimo-v2.5-free` and `longcat-2.0-free` ACCEPT images and are deliberately
364
+ * absent. Adding them would silently replace a working image with a caption,
365
+ * which is worse than the loud 400 this list exists to prevent — see the negative
366
+ * assertion in tests/provider-registry-parity.test.ts.
367
+ *
368
+ * Zen's roster is discovered live while this list is static, so it is a dated
369
+ * exception list, not a capability model. Re-probe before extending it.
370
+ * Evidence: devlog/_plan/260805_bug_fix_stack/002_zen_modality_probe.md
371
+ */
372
+ const OPENCODE_ZEN_TEXT_ONLY_MODELS = [
373
+ "big-pickle",
374
+ "nemotron-3-ultra-free",
375
+ "ling-3.0-flash-free",
376
+ "north-mini-code-free",
377
+ "laguna-s-2.1-free",
378
+ "deepseek-v4-flash-free",
379
+ ];
380
+ /*
381
+ * DeepSeek's Codex ladder is low/high/max, and the two V4 models resolve it
382
+ * DIFFERENTLY. From the official thinking-mode table (api-docs.deepseek.com,
383
+ * EN and zh-cn agree, re-verified 2026-08-06):
384
+ *
385
+ * requested | v4-flash | v4-pro
386
+ * low | low | high
387
+ * high | high | high
388
+ * xhigh | high | max
389
+ * max | max | max
390
+ *
391
+ * Two consequences (#1057):
392
+ *
393
+ * - `xhigh` is a COMPATIBILITY ALIAS, not a native tier. It stays in the wire maps
394
+ * so existing requests and saved configs keep working, but it is not advertised.
395
+ * - Pro does NOT honor `low` — the vendor silently upgrades it to `high`. So Pro
396
+ * advertises only the two levels it actually distinguishes. Advertising `low`
397
+ * there would put a tier in the picker that costs `high`, which is the same
398
+ * defect this fixes wearing a different value.
399
+ *
400
+ * The vendor page footnotes that Pro's mapping updates in early August 2026; as of
401
+ * the re-verification above it had not changed. When it does, Pro gains `low` here.
402
+ *
403
+ * `medium` has no row in the vendor table — mapping it to `high` is OUR
404
+ * compatibility choice for clients that only speak the OpenAI ladder.
405
+ */
406
+ const DEEPSEEK_FLASH_THINKING_EFFORTS = ["low", "high", "max"];
407
+ const DEEPSEEK_PRO_THINKING_EFFORTS = ["high", "max"];
408
+ const DEEPSEEK_PRO_REASONING_MAP: Record<string, string> = {
355
409
  low: "high",
356
410
  medium: "high",
357
411
  high: "high",
358
412
  xhigh: "max",
359
413
  max: "max",
360
414
  };
415
+ const DEEPSEEK_FLASH_REASONING_MAP: Record<string, string> = {
416
+ low: "low",
417
+ medium: "high",
418
+ high: "high",
419
+ xhigh: "high",
420
+ max: "max",
421
+ };
422
+ /**
423
+ * Flash-versus-Pro classification for DeepSeek V4 model ids, including prefixed
424
+ * (`deepseek/deepseek-v4-pro`) and suffixed (`deepseek-v4-flash-free`) forms.
425
+ * `tests/provider-registry-parity.test.ts` enumerates every id the registry
426
+ * actually passes here, so a future id this substring test would misread cannot
427
+ * land silently.
428
+ */
429
+ const isDeepseekFlashModel = (modelId: string): boolean =>
430
+ modelId.toLowerCase().includes("flash");
431
+ const deepseekThinkingEffortsFor = (modelId: string): string[] =>
432
+ isDeepseekFlashModel(modelId) ? DEEPSEEK_FLASH_THINKING_EFFORTS : DEEPSEEK_PRO_THINKING_EFFORTS;
433
+ const deepseekReasoningMapFor = (modelId: string): Record<string, string> =>
434
+ isDeepseekFlashModel(modelId) ? DEEPSEEK_FLASH_REASONING_MAP : DEEPSEEK_PRO_REASONING_MAP;
361
435
  // 260719 Alibaba Token Plan Personal Edition (China/Beijing). Keep it distinct from
362
436
  // Coding Plan: the products use different exact allowlists and different base URLs.
363
437
  // Evidence: https://help.aliyun.com/en/model-studio/token-plan-personal-overview
@@ -412,7 +486,7 @@ const TENCENT_CODING_PLAN_MODELS = ["tc-code-latest", "glm-5", "kimi-k2.5", "min
412
486
  // is documented as grounds for suspension — see the `note` on both Plan entries.
413
487
  // Report a break by opening an issue tagging the owner; the three things that rot first are the
414
488
  // static catalogs (liveModels:false cannot self-heal), the base URLs, and those Plan terms.
415
- // Full evidence ledger: devlog/_plan/260801_pr611_volcengine_evidence/000_evidence_ledger.md
489
+ // Full evidence ledger: devlog/_fin/260801_pr611_volcengine_evidence/000_evidence_ledger.md
416
490
  const VOLCENGINE_ARK_MODELS = [
417
491
  "doubao-seed-2-1-pro-260628",
418
492
  "doubao-seed-2-1-turbo-260628",
@@ -537,7 +611,7 @@ const NVIDIA_NIM_KIMI_MODELS = [
537
611
  *
538
612
  * Two lists, both verified per-model against NVIDIA documentation on 2026-08-04
539
613
  * (build.nvidia.com model pages and docs.api.nvidia.com/nim/reference/*). Evidence and
540
- * the per-id audit: devlog/_plan/260804_stack7_service_vision/011_nim_id_audit.md.
614
+ * the per-id audit: devlog/_fin/260804_stack7_service_vision/011_nim_id_audit.md.
541
615
  *
542
616
  * Read `noVisionModels` carefully — it lists models that CANNOT see images, which is
543
617
  * what routes them through the proxy's vision sidecar (src/vision/index.ts) and makes the
@@ -799,6 +873,32 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
799
873
  },
800
874
  noVisionModels: ["grok-build-0.1", "grok-composer-2.5-fast"],
801
875
  },
876
+ {
877
+ id: "command-code",
878
+ label: "Command Code - Auth",
879
+ adapter: "command-code",
880
+ baseUrl: "https://api.commandcode.ai",
881
+ authKind: "oauth",
882
+ oauthId: "command-code",
883
+ featured: true,
884
+ note: "Log in with your Command Code account",
885
+ // OAuth needs one initial selection, but the exposed catalog is always discovered from the
886
+ // signed-in account. Do not add a static model list here.
887
+ defaultModel: "deepseek/deepseek-v4-flash",
888
+ liveModels: true,
889
+ modelDiscovery: {
890
+ url: "https://api.commandcode.ai/provider/v1/models",
891
+ maxResponseBytes: 262_144,
892
+ maxModels: 256,
893
+ },
894
+ // These are capability facts from official Command Code model profiles, not seeded models.
895
+ // Unknown/new live models deliberately do not advertise a reasoning picker.
896
+ reasoningEfforts: [],
897
+ modelReasoningEfforts: COMMAND_CODE_MODEL_REASONING_EFFORTS,
898
+ defaultMaxOutputTokens: 64_000,
899
+ // The proprietary generate wire has no verified per-request serialization flag.
900
+ parallelToolCalls: false,
901
+ },
802
902
  {
803
903
  id: "anthropic",
804
904
  label: "Anthropic Claude",
@@ -938,7 +1038,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
938
1038
  "kimi-k2.7-code-highspeed": [],
939
1039
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS])),
940
1040
  ...Object.fromEntries(OPENCODE_GO_THINKING_BUDGET_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
941
- ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
1041
+ ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
942
1042
  },
943
1043
  modelDefaultReasoningEfforts: { "kimi-k3": "max" },
944
1044
  // glm-5.2 uses identity labels now that `max` is a native Codex level (no alias map);
@@ -946,7 +1046,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
946
1046
  modelReasoningEffortMap: {
947
1047
  "kimi-k3": KIMI_CODING_K3_REASONING_EFFORT_MAP,
948
1048
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
949
- ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
1049
+ ...Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
950
1050
  },
951
1051
  thinkingToggleModels: OPENCODE_GO_THINKING_TOGGLE_MODELS,
952
1052
  thinkingBudgetModels: THINKING_BUDGET_MODELS,
@@ -1087,9 +1187,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1087
1187
  // returned 200), so no noTemperatureModels entry is warranted here.
1088
1188
  modelReasoningEfforts: {
1089
1189
  "openai/gpt-5.5": ["low", "medium", "high", "xhigh"],
1090
- "deepseek/deepseek-v4-pro": DEEPSEEK_THINKING_EFFORTS,
1190
+ "deepseek/deepseek-v4-pro": deepseekThinkingEffortsFor("deepseek/deepseek-v4-pro"),
1091
1191
  },
1092
- modelReasoningEffortMap: { "deepseek/deepseek-v4-pro": DEEPSEEK_THINKING_REASONING_MAP },
1192
+ modelReasoningEffortMap: { "deepseek/deepseek-v4-pro": deepseekReasoningMapFor("deepseek/deepseek-v4-pro") },
1093
1193
  preserveReasoningContentModels: ["deepseek/deepseek-v4-pro"],
1094
1194
  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",
1095
1195
  },
@@ -1182,8 +1282,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1182
1282
  - 대안 분석: Globally preserve reasoning_content for all OpenAI-compatible models; preserve it for legacy deepseek-reasoner too; mark only V4 thinking models in registry metadata.
1183
1283
  - 선택 근거: DeepSeek V4 thinking mode requires history replay, while older DeepSeek reasoner has different compatibility rules. A model-scoped registry flag fixes built-in and stale saved configs without broad provider regressions.
1184
1284
  */
1185
- modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
1186
- modelReasoningEffortMap: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
1285
+ modelReasoningEfforts: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
1286
+ modelReasoningEffortMap: Object.fromEntries(DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
1187
1287
  preserveReasoningContentModels: DEEPSEEK_THINKING_MODELS,
1188
1288
  // Issue #88: every DeepSeek API model is text-only input (no image support upstream) — the
1189
1289
  // vision sidecar describes attached images for them, and the catalog advertises image input
@@ -1256,7 +1356,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1256
1356
  },
1257
1357
  {
1258
1358
  id: "commandcode",
1259
- label: "Command Code",
1359
+ label: "Command Code - API",
1260
1360
  adapter: "openai-chat",
1261
1361
  baseUrl: "https://api.commandcode.ai/provider/v1",
1262
1362
  authKind: "key",
@@ -1278,7 +1378,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1278
1378
  },
1279
1379
  // Verified 2026-08-03: public /provider/v1/models returns 51 rows; /chat/completions returns
1280
1380
  // 401 UNAUTHORIZED without a Bearer key. Primary source: https://commandcode.ai/docs/provider.
1281
- note: "Command Code Provider API (OpenAI-compatible); API access requires the Provider plan. CLI auth bridging for Go/Pro subscriptions is not yet available. Docs: https://commandcode.ai/docs/provider.",
1381
+ note: "Command Code Provider API (OpenAI-compatible); API access requires the Provider plan. Use `ocx login command-code` for OAuth account login (imports an existing local Command Code CLI credential when present). Docs: https://commandcode.ai/docs/provider.",
1282
1382
  },
1283
1383
  // FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
1284
1384
  { id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" },
@@ -1470,10 +1570,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1470
1570
  modelInputModalities: VOLCENGINE_PLAN_INPUT_MODALITIES,
1471
1571
  noVisionModels: VOLCENGINE_PLAN_TEXT_ONLY_MODELS,
1472
1572
  modelReasoningEfforts: Object.fromEntries(
1473
- DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS]),
1573
+ DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)]),
1474
1574
  ),
1475
1575
  modelReasoningEffortMap: Object.fromEntries(
1476
- DEEPSEEK_THINKING_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP]),
1576
+ DEEPSEEK_THINKING_MODELS.map(id => [id, deepseekReasoningMapFor(id)]),
1477
1577
  ),
1478
1578
  preserveReasoningContentModels: DEEPSEEK_THINKING_MODELS,
1479
1579
  note: "Coding tools only. Volcengine restricts Coding Plan quota to supported AI coding tools and warns that using this key for general API calls may suspend the subscription or ban the account. Use the plan key issued by the Ark console.",
@@ -1519,9 +1619,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1519
1619
  modelReasoningEfforts: {
1520
1620
  ...Object.fromEntries(ALIBABA_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
1521
1621
  "glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
1522
- "deepseek-v4-pro": DEEPSEEK_THINKING_EFFORTS,
1622
+ "deepseek-v4-pro": deepseekThinkingEffortsFor("deepseek-v4-pro"),
1523
1623
  },
1524
- modelReasoningEffortMap: { "deepseek-v4-pro": DEEPSEEK_THINKING_REASONING_MAP },
1624
+ modelReasoningEffortMap: { "deepseek-v4-pro": deepseekReasoningMapFor("deepseek-v4-pro") },
1525
1625
  thinkingBudgetModels: ALIBABA_TOKEN_PLAN_QWEN_MODELS,
1526
1626
  preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash"],
1527
1627
  noVisionModels: ["glm-5.2", "deepseek-v4-pro"],
@@ -1553,12 +1653,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1553
1653
  ...Object.fromEntries(ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
1554
1654
  "qwen3.8-max": ["low", "high", "xhigh"],
1555
1655
  "glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
1556
- "deepseek-v4-pro": DEEPSEEK_THINKING_EFFORTS,
1557
- "deepseek-v4-flash": DEEPSEEK_THINKING_EFFORTS,
1656
+ "deepseek-v4-pro": deepseekThinkingEffortsFor("deepseek-v4-pro"),
1657
+ "deepseek-v4-flash": deepseekThinkingEffortsFor("deepseek-v4-flash"),
1558
1658
  },
1559
1659
  modelReasoningEffortMap: {
1560
- "deepseek-v4-pro": DEEPSEEK_THINKING_REASONING_MAP,
1561
- "deepseek-v4-flash": DEEPSEEK_THINKING_REASONING_MAP,
1660
+ "deepseek-v4-pro": deepseekReasoningMapFor("deepseek-v4-pro"),
1661
+ "deepseek-v4-flash": deepseekReasoningMapFor("deepseek-v4-flash"),
1562
1662
  },
1563
1663
  thinkingBudgetModels: ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS,
1564
1664
  preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "deepseek-v4-flash", "qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash"],
@@ -1649,7 +1749,17 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1649
1749
  autoToolChoiceOnlyModels: KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS,
1650
1750
  preserveReasoningContentModels: KIMI_THINKING_MODELS,
1651
1751
  },
1652
- { id: "opencode-zen", label: "opencode zen", baseUrl: "https://opencode.ai/zen/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://opencode.ai/auth" },
1752
+ {
1753
+ id: "opencode-zen",
1754
+ label: "opencode zen",
1755
+ baseUrl: "https://opencode.ai/zen/v1",
1756
+ adapter: "openai-chat",
1757
+ authKind: "key",
1758
+ dashboardUrl: "https://opencode.ai/auth",
1759
+ // #1043: without this the proxy forwards image parts to text-only Zen models and
1760
+ // the upstream rejects the whole request with a 400.
1761
+ noVisionModels: OPENCODE_ZEN_TEXT_ONLY_MODELS,
1762
+ },
1653
1763
  { id: "vercel-ai-gateway", label: "Vercel AI Gateway", baseUrl: "https://ai-gateway.vercel.sh/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://vercel.com/dashboard" },
1654
1764
  {
1655
1765
  id: "opencode-free",
@@ -1665,10 +1775,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1665
1775
  staticHeaders: {
1666
1776
  "x-opencode-client": "desktop",
1667
1777
  },
1668
- modelReasoningEfforts: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, DEEPSEEK_THINKING_EFFORTS])),
1669
- modelReasoningEffortMap: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, DEEPSEEK_THINKING_REASONING_MAP])),
1778
+ modelReasoningEfforts: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
1779
+ modelReasoningEffortMap: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
1670
1780
  preserveReasoningContentModels: OPENCODE_FREE_DEEPSEEK_MODELS,
1671
- noVisionModels: OPENCODE_FREE_DEEPSEEK_MODELS,
1781
+ // Same Zen roster behind the same base URL, so it carries the same measured
1782
+ // text-only list rather than only its DeepSeek member (#1043).
1783
+ noVisionModels: OPENCODE_ZEN_TEXT_ONLY_MODELS,
1672
1784
  },
1673
1785
  { id: "xiaomi", label: "Xiaomi MiMo", baseUrl: "https://api.xiaomimimo.com/anthropic", adapter: "anthropic", authKind: "key", dashboardUrl: "https://xiaomimimo.com", defaultModel: "mimo-v2.5-pro" },
1674
1786
  { id: "kilo", label: "Kilo", baseUrl: "https://api.kilo.ai/api/gateway", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://kilo.ai" },
@@ -111,6 +111,7 @@ const TOOL_CAPABLE_ADAPTERS = new Set([
111
111
  "azure",
112
112
  "kiro",
113
113
  "mimo-free",
114
+ "command-code",
114
115
  ]);
115
116
 
116
117
  function localRemoteEvidence(baseUrl: string | undefined): Pick<RouteCapabilityEvidence, "localOnly" | "remoteAllowed"> {
@@ -5,6 +5,7 @@ import { createGoogleAdapter } from "../adapters/google";
5
5
  import { createKiroAdapter } from "../adapters/kiro";
6
6
  import { createMimoFreeAdapter } from "../adapters/mimo-free";
7
7
  import { createOpenAIChatAdapter } from "../adapters/openai-chat";
8
+ import { createCommandCodeAdapter } from "../adapters/command-code";
8
9
  import { createResponsesPassthroughAdapter } from "../adapters/openai-responses";
9
10
  import type { OcxProviderConfig } from "../types";
10
11
  import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
@@ -57,6 +58,8 @@ export function resolveWireProtocolOverride(
57
58
  /** Build the provider adapter for a resolved provider config. */
58
59
  export function resolveAdapter(providerConfig: OcxProviderConfig, cacheRetention?: "none" | "short" | "long") {
59
60
  switch (providerConfig.adapter) {
61
+ case "command-code":
62
+ return createCommandCodeAdapter(providerConfig);
60
63
  case "openai-chat":
61
64
  return createOpenAIChatAdapter(providerConfig);
62
65
  case "anthropic":
@@ -15,10 +15,11 @@ import {
15
15
  } from "../config";
16
16
  import { providerDestinationConfigError } from "../lib/destination-policy";
17
17
  import { redactSecretString } from "../lib/redact";
18
- import { getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
18
+ import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
19
19
  import { providerConfigSeed } from "../providers/derive";
20
20
  import type { OcxConfig, OcxProviderConfig } from "../types";
21
21
  import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
22
+ import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
22
23
 
23
24
  let _corsOrigin = "http://localhost:10100";
24
25
  export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
@@ -425,6 +426,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
425
426
  const typed = provider as unknown as OcxProviderConfig;
426
427
  const baseUrlError = providerBaseUrlConfigError(typed.baseUrl);
427
428
  if (baseUrlError) return `provider ${name} ${baseUrlError}`;
429
+ if (effectiveGoogleMode(name, typed) === "vertex" && typed.location !== undefined) {
430
+ const locationError = googleVertexLocationConfigError(typed.location);
431
+ if (locationError) return `provider ${name} ${locationError}`;
432
+ }
428
433
  const destinationError = providerDestinationConfigError(name, typed);
429
434
  if (destinationError) return `provider ${name} ${destinationError}`;
430
435
  const headersError = providerHeadersConfigError(typed.headers);
@@ -174,6 +174,12 @@ import {
174
174
  requireManagementAuth,
175
175
  type ManagementAuthState,
176
176
  } from "./management-auth";
177
+ import {
178
+ LOCAL_ATTESTATION_CHALLENGE_HEADER,
179
+ LOCAL_ATTESTATION_PROOF_HEADER,
180
+ createLocalAttestationProof,
181
+ createLocalAttestationSecret,
182
+ } from "../lib/local-management-attestation";
177
183
 
178
184
  const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
179
185
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
@@ -357,9 +363,33 @@ export interface StartServerDeps {
357
363
  nativeMainStartup?: NativeMainStartupGateDeps;
358
364
  /** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
359
365
  liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
366
+ /** Test-only seam; production derives a fresh local-attestation secret per process. */
367
+ localAttestationSecret?: string;
368
+ }
369
+
370
+ /*
371
+ * #1046. `startServer` rewrites the Codex models cache during boot, and an
372
+ * app-server that started earlier keeps its own in-memory model list. The stale
373
+ * warning is not emitted here: `handleStart` runs a catalog sync moments later,
374
+ * so warning now would read an mtime that write is about to move, and both sites
375
+ * calling the helper independently would warn twice. This records the fact; the
376
+ * CLI start path owns the single decision.
377
+ *
378
+ * A caller that starts a server without `handleStart` (tests, embedded use)
379
+ * deliberately gets no warning — lifecycle diagnostics belong to whoever owns
380
+ * the lifecycle.
381
+ */
382
+ let startupCacheInvalidationWrote = false;
383
+
384
+ /** #1046: did this process's startup cache invalidation actually write? */
385
+ export function consumeStartupCacheInvalidationWrite(): boolean {
386
+ const wrote = startupCacheInvalidationWrote;
387
+ startupCacheInvalidationWrote = false;
388
+ return wrote;
360
389
  }
361
390
 
362
391
  export function startServer(port?: number, deps: StartServerDeps = {}) {
392
+ const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
363
393
  const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
364
394
  setLiveStateStoreConfig(config);
365
395
  applyProxyEnv(config);
@@ -407,8 +437,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}) {
407
437
  // otherwise turn "no Codex installed" into "proxy will not start".
408
438
  try {
409
439
  const startupCodexHome = getCodexHome();
410
- withCatalogWriteSerialization(startupCodexHome, permit =>
440
+ // #1046: record whether this actually rewrote the cache. `handleStart` ORs this
441
+ // with the later startup sync and warns ONCE about stale app-servers; warning
442
+ // here instead would read a catalog mtime the sync is about to move.
443
+ const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
411
444
  invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
445
+ // A refused permit is not a write; only a completed run that returned true is.
446
+ startupCacheInvalidationWrote = outcome.kind === "completed" && outcome.value === true;
412
447
  } catch { /* no readable Codex home: nothing to invalidate */ }
413
448
  // Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
414
449
  // the server can serve a request, and AFTER the startup migrations above — those run
@@ -545,7 +580,14 @@ export function startServer(port?: number, deps: StartServerDeps = {}) {
545
580
 
546
581
  if (url.pathname === "/healthz" && req.method === "GET") {
547
582
  // service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
548
- return jsonResponse({ status: "ok", service: "opencodex", version: VERSION, uptime: process.uptime(), pid: process.pid, port: listenPort }, 200, req, config);
583
+ const healthPort = server.port ?? listenPort;
584
+ const response = jsonResponse({ status: "ok", service: "opencodex", version: VERSION, uptime: process.uptime(), pid: process.pid, port: healthPort }, 200, req, config);
585
+ const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
586
+ if (challenge) {
587
+ const proof = createLocalAttestationProof(localAttestationSecret, challenge, process.pid, healthPort);
588
+ if (proof) response.headers.set(LOCAL_ATTESTATION_PROOF_HEADER, proof);
589
+ }
590
+ return response;
549
591
  }
550
592
 
551
593
  if (url.pathname.startsWith("/api/")) {
@@ -6,7 +6,7 @@
6
6
  * belongs to src/integrations/writer.ts. Duplicating any of it here is how the
7
7
  * API and the writer would start disagreeing about what happened to a file.
8
8
  *
9
- * Design of record: devlog/_plan/260802_client_toggle_api/040_wp4_management_api.md.
9
+ * Design of record: devlog/_fin/260802_client_toggle_api/040_wp4_management_api.md.
10
10
  */
11
11
  import { readFileSync } from "node:fs";
12
12
  import type { IntegrationIO } from "../../integrations/config-io";
@@ -9,12 +9,12 @@
9
9
  * route — turning them back on is the undo.
10
10
  *
11
11
  * That conclusion cost eleven audit rounds; the reasoning is in
12
- * devlog/_plan/260803_integrations_toggle_all/, and 007 records why Codex and
12
+ * devlog/_fin/260803_integrations_toggle_all/, and 007 records why Codex and
13
13
  * Claude Desktop are NOT here: their state spans several artifacts and a live
14
14
  * database, so they need a durable operation record this module deliberately
15
15
  * does not have.
16
16
  *
17
- * Design of record: devlog/_plan/260803_integrations_toggle_all/030 (routes),
17
+ * Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
18
18
  * 011 (Claude Code), 012 (Grok).
19
19
  */
20
20
  import { readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
@@ -747,10 +747,18 @@ const UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE =
747
747
  // branch of the passthrough return path). 32 MiB matches the continuation snapshot read
748
748
  // bound and is far above any legitimate non-streaming completion, including base64 image
749
749
  // payloads. The stall deadlines only govern the body transfer — generation time before
750
- // the response headers is untouched.
750
+ // the response headers is untouched. Generation after early/chunked headers but before
751
+ // the first body byte previously used the 30-second inactivity deadline; this call site
752
+ // gives it the full body deadline instead.
751
753
  const MAX_UPSTREAM_JSON_BODY_BYTES = 32 * 1024 * 1024;
752
754
  const UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS = 180_000;
753
755
  const UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS = 30_000;
756
+ export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
757
+ maxBytes: MAX_UPSTREAM_JSON_BODY_BYTES,
758
+ totalTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
759
+ inactivityTimeoutMs: UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS,
760
+ firstByteTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
761
+ };
754
762
 
755
763
  function unreadableEncryptedAgentTaskResponse(): Response {
756
764
  return new Response(
@@ -1375,8 +1383,8 @@ async function handleResponsesInner(
1375
1383
  logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
1376
1384
  logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
1377
1385
 
1378
- // Shadow call intercept: rewrite Codex's hard-coded helper calls
1379
- // (gpt-5.4-mini on older clients, gpt-5.6-luna on 0.145.0+)
1386
+ // Shadow call intercept: rewrite Codex 0.145.0+ helper calls (gpt-5.6-luna).
1387
+ // Ancient clients using gpt-5.4-mini remain configurable via sourceModels.
1380
1388
  const _sci = config.shadowCallIntercept;
1381
1389
  if (_sci?.enabled && _sci.model && shouldInterceptShadowCall(
1382
1390
  parsed.modelId,
@@ -2039,7 +2047,7 @@ async function handleResponsesInner(
2039
2047
  // `streamMode: "eager-relay"` opt-in. Darwin `auto` always stays tee. The
2040
2048
  // eager shape skips tee and uses one bounded reader with inline inspection
2041
2049
  // (src/server/relay-eager.ts; policy:
2042
- // devlog/_plan/260731_macos_rss_retention/100_darwin_eager_optin.md).
2050
+ // devlog/_fin/260731_macos_rss_retention/100_darwin_eager_optin.md).
2043
2051
  // The bundled known-bad runtime remains on tee by default on both platforms.
2044
2052
  if (isEventStream && upstreamResponse.body) {
2045
2053
  const repairConfig = route.provider.responsesItemIdRepair;
@@ -2226,11 +2234,7 @@ async function handleResponsesInner(
2226
2234
  // without limit. This path is no longer rare — WebSocket turns for models whose
2227
2235
  // streaming terminal event is unreliable are deliberately answered with bounded JSON.
2228
2236
  // Oversize and stall deadlines both fail closed; a partial body is never parsed.
2229
- const bounded = await readBoundedResponseBody(upstreamResponse, {
2230
- maxBytes: MAX_UPSTREAM_JSON_BODY_BYTES,
2231
- totalTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
2232
- inactivityTimeoutMs: UPSTREAM_JSON_BODY_INACTIVITY_TIMEOUT_MS,
2233
- });
2237
+ const bounded = await readBoundedResponseBody(upstreamResponse, UPSTREAM_JSON_BODY_READ_OPTIONS);
2234
2238
  if (bounded.oversized) {
2235
2239
  return formatErrorResponse(502, "upstream_error", "upstream JSON response exceeded the safe body limit");
2236
2240
  }
@@ -16,7 +16,7 @@ const IMMUTABLE_READONLY_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLI
16
16
 
17
17
  /**
18
18
  * Read-only CODEX_HOME storage scanner — Phase 1 of the Storage page epic
19
- * (devlog/_plan/500_storage-page-session-cleanup). Pure measurement: sizes via
19
+ * (devlog/_fin/500_storage-page-session-cleanup). Pure measurement: sizes via
20
20
  * fs.stat walks, DB row counts via immutable readonly opens that degrade to
21
21
  * null on lock/corruption. Performs zero writes under CODEX_HOME.
22
22
  */
package/src/types.ts CHANGED
@@ -1066,6 +1066,12 @@ export interface OcxProviderConfig {
1066
1066
  * the legacy `/v1/responses` construction.
1067
1067
  */
1068
1068
  responsesPath?: string;
1069
+ /**
1070
+ * Command Code protocol version sent as `x-command-code-version` on /alpha/generate requests.
1071
+ * The internal endpoint's schema drifts with the CLI version; operators can pin a known-good
1072
+ * version here instead of waiting for a code change. Absent uses the adapter's current default.
1073
+ */
1074
+ commandCodeVersion?: string;
1069
1075
  /**
1070
1076
  * Responses upstream that stores nothing server-side (DeepSeek documents "the API
1071
1077
  * is stateless"). Stateful request parameters are dropped, `store` is pinned false,
package/src/usage/log.ts CHANGED
@@ -421,7 +421,7 @@ export type UsageLogRevision = {
421
421
  let usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
422
422
  const MANAGEMENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
423
423
  const MANAGEMENT_USAGE_READ_CHUNK_BYTES = 1024 * 1024;
424
- const MANAGEMENT_USAGE_MAX_ENTRIES = 200_000;
424
+ const MANAGEMENT_USAGE_MAX_ENTRIES = 500_000;
425
425
  const MANAGEMENT_USAGE_FLIGHT_STALE_MS = 30_000;
426
426
  export interface ManagementUsageSnapshot {
427
427
  entries: PersistedUsageEntry[];