@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.0

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 (173) hide show
  1. package/README.md +8 -1
  2. package/bin/ocx.mjs +47 -22
  3. package/gui/dist/assets/index-BDjpkcRN.js +67 -0
  4. package/gui/dist/assets/index-BHsKRFh9.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/discovery.ts +4 -1
  10. package/src/adapters/cursor/effort-map.ts +3 -0
  11. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  12. package/src/adapters/cursor/protobuf-events.ts +24 -2
  13. package/src/adapters/cursor/protobuf-request.ts +1 -2
  14. package/src/adapters/cursor/tool-definitions.ts +68 -29
  15. package/src/adapters/google-wire-compiler.ts +4 -0
  16. package/src/adapters/google.ts +128 -2
  17. package/src/adapters/identity.ts +12 -2
  18. package/src/adapters/kiro.ts +64 -7
  19. package/src/adapters/mimo-free.ts +2 -0
  20. package/src/adapters/openai-responses.ts +246 -59
  21. package/src/claude/agents-inject.ts +5 -0
  22. package/src/claude/alias.ts +94 -14
  23. package/src/claude/inbound.ts +26 -9
  24. package/src/claude/outbound.ts +6 -3
  25. package/src/cli/account-auth.ts +1 -1
  26. package/src/cli/agent-driven.ts +37 -0
  27. package/src/cli/catalog-prewarm.ts +24 -0
  28. package/src/cli/claude.ts +35 -10
  29. package/src/cli/doctor.ts +71 -19
  30. package/src/cli/help.ts +42 -6
  31. package/src/cli/index.ts +93 -19
  32. package/src/cli/interactive-confirm.ts +133 -0
  33. package/src/cli/opencode.ts +701 -0
  34. package/src/cli/provider-runtime.ts +3 -0
  35. package/src/cli/provider.ts +31 -10
  36. package/src/cli/star-prompt.ts +79 -18
  37. package/src/cli/status.ts +47 -13
  38. package/src/cli/v2.ts +10 -1
  39. package/src/codex/account-id.ts +34 -0
  40. package/src/codex/account-lifecycle.ts +4 -1
  41. package/src/codex/account-namespace-match.ts +63 -0
  42. package/src/codex/account-namespaces.ts +149 -0
  43. package/src/codex/account-pause.ts +20 -0
  44. package/src/codex/account-store.ts +2 -0
  45. package/src/codex/account-usability.ts +6 -1
  46. package/src/codex/app-server-processes.ts +511 -0
  47. package/src/codex/auth-api.ts +293 -34
  48. package/src/codex/auth-collision.ts +2 -1
  49. package/src/codex/auth-context.ts +60 -17
  50. package/src/codex/catalog/bundled.ts +9 -2
  51. package/src/codex/catalog/parsing.ts +42 -2
  52. package/src/codex/catalog/provider-fetch.ts +264 -70
  53. package/src/codex/catalog/sync.ts +45 -8
  54. package/src/codex/catalog.ts +2 -2
  55. package/src/codex/features.ts +524 -5
  56. package/src/codex/history-provider.ts +145 -1
  57. package/src/codex/inject.ts +114 -14
  58. package/src/codex/main-account.ts +2 -8
  59. package/src/codex/pool-rotation.ts +186 -0
  60. package/src/codex/quota.ts +92 -2
  61. package/src/codex/routing.ts +695 -106
  62. package/src/codex/runtime.ts +10 -1
  63. package/src/codex/shim.ts +4 -1
  64. package/src/codex/subagent-defaults.ts +550 -0
  65. package/src/codex/subagent-model-fallback.ts +2 -0
  66. package/src/codex/sync.ts +3 -0
  67. package/src/config.ts +574 -25
  68. package/src/generated/jawcode-model-metadata.ts +12 -12
  69. package/src/github/star-state.ts +191 -0
  70. package/src/images/artifacts.ts +516 -0
  71. package/src/images/fulfill-video.ts +163 -0
  72. package/src/images/fulfill.ts +111 -0
  73. package/src/images/index.ts +4 -0
  74. package/src/images/loop.ts +789 -0
  75. package/src/images/plan.ts +133 -0
  76. package/src/images/synthetic-tool.ts +133 -0
  77. package/src/images/types.ts +41 -0
  78. package/src/images/xai-client.ts +141 -0
  79. package/src/images/xai-video-client.ts +163 -0
  80. package/src/lib/admin-secrets.ts +25 -0
  81. package/src/lib/bun-binary-validator.d.mts +3 -0
  82. package/src/lib/bun-binary-validator.mjs +18 -0
  83. package/src/lib/bun-runtime.ts +6 -20
  84. package/src/lib/config-ownership.ts +327 -0
  85. package/src/lib/crash-guard.ts +2 -0
  86. package/src/lib/destination-policy.ts +132 -7
  87. package/src/lib/pinned-http.ts +151 -0
  88. package/src/lib/process-control.ts +2 -2
  89. package/src/lib/provider-outbound.ts +167 -0
  90. package/src/lib/provider-url.ts +14 -0
  91. package/src/lib/proxy-env.ts +18 -0
  92. package/src/lib/shadow-call.ts +30 -0
  93. package/src/lib/test-home-guard.ts +90 -0
  94. package/src/lib/win-exec.ts +12 -2
  95. package/src/lib/windows-elevation.ts +81 -3
  96. package/src/lib/windows-secret-acl.ts +189 -12
  97. package/src/lib/winsw.ts +2 -0
  98. package/src/oauth/anthropic-routing.ts +570 -0
  99. package/src/oauth/health.ts +6 -0
  100. package/src/oauth/index.ts +310 -75
  101. package/src/oauth/key-providers.ts +38 -8
  102. package/src/oauth/kimi.ts +2 -0
  103. package/src/oauth/kiro-credentials.ts +373 -12
  104. package/src/oauth/kiro.ts +424 -43
  105. package/src/oauth/login-cli.ts +33 -6
  106. package/src/oauth/store.ts +56 -4
  107. package/src/oauth/types.ts +11 -0
  108. package/src/providers/alibaba-region-migration.ts +16 -3
  109. package/src/providers/antigravity-models.ts +3 -0
  110. package/src/providers/api-keys.ts +13 -6
  111. package/src/providers/derive.ts +8 -2
  112. package/src/providers/key-failover.ts +24 -4
  113. package/src/providers/model-discovery.ts +356 -0
  114. package/src/providers/quota.ts +233 -29
  115. package/src/providers/registry.ts +125 -3
  116. package/src/responses/parser.ts +11 -0
  117. package/src/responses/state.ts +22 -8
  118. package/src/responses/tool-groups.ts +19 -0
  119. package/src/router.ts +19 -7
  120. package/src/server/auth-cors.ts +114 -24
  121. package/src/server/claude-messages.ts +8 -1
  122. package/src/server/gui-static.ts +30 -6
  123. package/src/server/images.ts +303 -9
  124. package/src/server/index.ts +77 -9
  125. package/src/server/lifecycle.ts +25 -1
  126. package/src/server/live.ts +75 -25
  127. package/src/server/management/agent-settings-routes.ts +106 -8
  128. package/src/server/management/combo-routes.ts +7 -0
  129. package/src/server/management/config-routes.ts +22 -7
  130. package/src/server/management/context.ts +11 -1
  131. package/src/server/management/logs-usage-routes.ts +167 -3
  132. package/src/server/management/model-routes.ts +46 -13
  133. package/src/server/management/oauth-account-routes.ts +163 -17
  134. package/src/server/management/provider-routes.ts +73 -10
  135. package/src/server/management/shared.ts +2 -2
  136. package/src/server/management/sidebar-routes.ts +39 -0
  137. package/src/server/management/system-restart.ts +172 -0
  138. package/src/server/management/system-routes.ts +33 -10
  139. package/src/server/management-api.ts +5 -3
  140. package/src/server/management-auth.ts +216 -0
  141. package/src/server/proxy-liveness.ts +14 -3
  142. package/src/server/responses/compact.ts +21 -13
  143. package/src/server/responses/core.ts +614 -172
  144. package/src/server/responses/upstream-error.ts +48 -0
  145. package/src/server/responses-image-gen-repair.ts +118 -0
  146. package/src/server/responses-item-id-repair.ts +10 -85
  147. package/src/server/sse-payload-rewrite.ts +116 -0
  148. package/src/server/startup-action-control.ts +30 -14
  149. package/src/server/system-env.ts +28 -10
  150. package/src/service.ts +284 -19
  151. package/src/storage/cleanup-job.ts +57 -0
  152. package/src/storage/cleanup.ts +1504 -28
  153. package/src/storage/policy-job.ts +387 -0
  154. package/src/storage/policy-scheduler.ts +40 -0
  155. package/src/storage/policy-worker.ts +53 -0
  156. package/src/storage/policy.ts +522 -0
  157. package/src/storage/restore-job.ts +253 -0
  158. package/src/storage/restore-worker.ts +52 -0
  159. package/src/storage/storage-mutation-coordinator.ts +109 -0
  160. package/src/storage/worker-lifecycle.ts +81 -0
  161. package/src/tray/windows.ts +34 -4
  162. package/src/types.ts +107 -1
  163. package/src/update/badge.ts +72 -0
  164. package/src/update/index.ts +36 -18
  165. package/src/update/job.ts +111 -16
  166. package/src/update/npm-invocation.d.mts +23 -0
  167. package/src/update/npm-invocation.mjs +94 -0
  168. package/src/usage/debug.ts +2 -0
  169. package/src/usage/expected-prices.ts +6 -5
  170. package/src/usage/log.ts +12 -0
  171. package/src/web-search/loop.ts +57 -16
  172. package/gui/dist/assets/index-CjKFJHSC.js +0 -65
  173. package/gui/dist/assets/index-DfVGuN88.css +0 -1
@@ -17,11 +17,79 @@ import {
17
17
  export type ProviderAuthKind = "forward" | "oauth" | "key" | "local";
18
18
  export type MetadataModelIdNormalize = "case-insensitive";
19
19
 
20
+ export type ProviderModelDiscoveryScalar = string | number | boolean;
21
+
22
+ export type ProviderModelDiscoveryPredicate =
23
+ | {
24
+ path: readonly string[];
25
+ equalsAny: readonly ProviderModelDiscoveryScalar[];
26
+ caseInsensitive?: boolean;
27
+ }
28
+ | {
29
+ path: readonly string[];
30
+ /**
31
+ * A string-valued upstream target uses substring matching; an array-valued target uses
32
+ * exact element matching. Use `equalsAny` when the string must match in full.
33
+ */
34
+ containsAny: readonly ProviderModelDiscoveryScalar[];
35
+ caseInsensitive?: boolean;
36
+ }
37
+ | {
38
+ path: readonly string[];
39
+ /** Uses the same string-substring and array-element semantics as `containsAny`. */
40
+ containsAll: readonly ProviderModelDiscoveryScalar[];
41
+ caseInsensitive?: boolean;
42
+ };
43
+
44
+ export interface ProviderModelDiscoveryFilter {
45
+ /** Every predicate must match. */
46
+ allOf?: readonly ProviderModelDiscoveryPredicate[];
47
+ /** At least one predicate must match. */
48
+ anyOf?: readonly ProviderModelDiscoveryPredicate[];
49
+ /** No predicate may match. */
50
+ noneOf?: readonly ProviderModelDiscoveryPredicate[];
51
+ }
52
+
53
+ interface ProviderModelDiscoverySharedSpec {
54
+ /** Query parameters applied to the resolved discovery URL. */
55
+ query?: Readonly<Record<string, string>>;
56
+ /** Declarative eligibility rules evaluated against each untrusted model row. */
57
+ filter?: ProviderModelDiscoveryFilter;
58
+ /** Optional lower byte ceiling; the process-wide hard ceiling still wins. */
59
+ maxResponseBytes?: number;
60
+ /** Optional lower raw-row ceiling; the process-wide hard ceiling still wins. */
61
+ maxModels?: number;
62
+ }
63
+
64
+ type ProviderModelDiscoveryLocation =
65
+ | {
66
+ /** Registry-owned absolute endpoint. Mutually exclusive with `path`. */
67
+ url: string;
68
+ path?: never;
69
+ }
70
+ | {
71
+ /** Resource path relative to baseUrl; query strings and fragments are disallowed. */
72
+ path: string;
73
+ url?: never;
74
+ }
75
+ | {
76
+ /** Keep the adapter-derived default discovery endpoint. */
77
+ url?: never;
78
+ path?: never;
79
+ };
80
+
81
+ /**
82
+ * Trusted live-model discovery policy. This metadata is registry-only: it must never be copied
83
+ * into config.json, where a same-named custom provider could otherwise redirect a stored key.
84
+ */
85
+ export type ProviderModelDiscoverySpec = ProviderModelDiscoverySharedSpec & ProviderModelDiscoveryLocation;
86
+
20
87
  export interface ProviderRegistryEntry {
21
88
  id: string;
22
89
  label: string;
23
90
  adapter: string;
24
91
  baseUrl: string;
92
+ apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
25
93
  authKind: ProviderAuthKind;
26
94
  codexAccountMode?: CodexAccountMode;
27
95
  /** OAuth preset may explicitly honor a persisted API-key billing mode. */
@@ -34,6 +102,11 @@ export interface ProviderRegistryEntry {
34
102
  */
35
103
  freeTier?: boolean;
36
104
  allowBaseUrlOverride?: boolean;
105
+ /**
106
+ * Do not claim an existing same-named key provider whose fixed destination differs from this
107
+ * preset. Enable for newly promoted ids so an older custom key cannot be silently retargeted.
108
+ */
109
+ preserveCustomDestination?: boolean;
37
110
  /**
38
111
  * Optional endpoint picker for providers with multiple official hosts
39
112
  * (e.g. Qwen Cloud token plan vs pay-as-you-go). Requires `allowBaseUrlOverride`
@@ -50,6 +123,7 @@ export interface ProviderRegistryEntry {
50
123
  defaultModel?: string;
51
124
  models?: string[];
52
125
  liveModels?: boolean;
126
+ modelDiscovery?: ProviderModelDiscoverySpec;
53
127
  contextWindow?: number;
54
128
  modelContextWindows?: Record<string, number>;
55
129
  modelInputModalities?: Record<string, string[]>;
@@ -88,7 +162,7 @@ export interface ProviderRegistryEntry {
88
162
 
89
163
  export type ProviderConfigSeed = Pick<
90
164
  OcxProviderConfig,
91
- "adapter" | "baseUrl" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
165
+ "adapter" | "baseUrl" | "apiKeyTransport" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
92
166
  | "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
93
167
  | "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
94
168
  | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
@@ -383,6 +457,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
383
457
  modelContextWindows: cursorModelContextWindows(CURSOR_STATIC_MODELS),
384
458
  modelInputModalities: cursorModelInputModalities(CURSOR_STATIC_MODELS),
385
459
  modelReasoningEfforts: cursorModelReasoningEfforts(CURSOR_STATIC_MODELS),
460
+ // Kimi K3 documents `max` as its API default, and its Cursor ladder has no `medium`
461
+ // rung — so applyReasoningLevels' medium->high->first fallback would settle the catalog
462
+ // default on `high`, the picker would send `high` explicitly, and the request builder's
463
+ // no-effort fallback to `kimi-k3-max` would never be reached. Mirrors the other K3
464
+ // routes (kimi, kimi-code, opencode-go).
465
+ modelDefaultReasoningEfforts: { "kimi-k3": "max" },
386
466
  // Cursor's wire protocol never forwards image parts (request-builder emits an unsupported-
387
467
  // content marker), so the vision sidecar covers ALL cursor models regardless of what the
388
468
  // upstream model could natively do. Live-discovered models outside the static list fall back
@@ -480,6 +560,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
480
560
  baseUrl: "https://api.kimi.com/coding/v1",
481
561
  authKind: "oauth",
482
562
  modelSuffixBracketStrip: true,
563
+ // Kimi Code Plan documents a stable session/task prompt_cache_key as required to improve
564
+ // cache hit rates.
565
+ // The chat adapter only forwards a key already on the internal request (Codex's session key,
566
+ // or the one the Claude /v1/messages inbound derives); the adapter itself never invents one.
567
+ // Evidence: https://platform.kimi.com/docs/api/chat
568
+ promptCacheKey: true,
483
569
  featured: true,
484
570
  oauthId: "kimi",
485
571
  jawcodeBundle: "moonshot",
@@ -506,7 +592,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
506
592
  baseUrl: "https://runtime.us-east-1.kiro.dev",
507
593
  authKind: "oauth",
508
594
  oauthId: "kiro",
509
- note: "Import-first: reuses your installed Kiro CLI login requires kiro-cli installed and signed in (`kiro-cli login`). Experimental third-party harness — see Kiro ToS.",
595
+ note: "Import-first: reuses your installed and signed-in Kiro CLI session (requires `kiro-cli login`). Add account logs `kiro-cli` out, switches it through a fresh browser login, stores the account by profile ARN, and restores the previous CLI session on cancellation or failure. Experimental third-party harness — see Kiro ToS.",
510
596
  models: KIRO_MODELS,
511
597
  defaultModel: "kiro-auto",
512
598
  // Kiro speaks CodeWhisperer wire, not OpenAI-style GET /models. Keep the static
@@ -978,7 +1064,6 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
978
1064
  authKind: "key",
979
1065
  dashboardUrl: "https://ollama.com/settings/keys",
980
1066
  // Live IDs verified 2026-07-10; qwen3-coder:480b retires 2026-07-15.
981
- // Evidence: .codexclaw/evidence/260710_wp9_ollama_cloud_model_ids.md.
982
1067
  models: ["glm-5.2", "deepseek-v4-pro", "qwen3-coder:480b", "gpt-oss:120b", "kimi-k2.6", "minimax-m3", "qwen3.5:397b", "gemma4:31b"],
983
1068
  defaultModel: "glm-5.2",
984
1069
  noVisionModels: [
@@ -1019,6 +1104,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1019
1104
  id: "kimi-code", label: "Kimi (coding)", baseUrl: "https://api.kimi.com/coding/v1", adapter: "openai-chat", authKind: "key",
1020
1105
  dashboardUrl: "https://platform.moonshot.cn/console/api-keys", defaultModel: "kimi-k2.7-code",
1021
1106
  modelSuffixBracketStrip: true,
1107
+ // API-key form of the same Kimi Code Plan transport; keep cache affinity identical to OAuth.
1108
+ promptCacheKey: true,
1022
1109
  models: KIMI_CODING_MODELS,
1023
1110
  modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
1024
1111
  modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
@@ -1113,6 +1200,41 @@ export function getProviderRegistryEntry(id: string): ProviderRegistryEntry | un
1113
1200
  return PROVIDER_REGISTRY.find(entry => entry.id === id);
1114
1201
  }
1115
1202
 
1203
+ function normalizedProviderEndpoint(value: string): string {
1204
+ const trimmed = value.trim();
1205
+ try {
1206
+ const parsed = new URL(trimmed);
1207
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "") || "/";
1208
+ return parsed.toString().replace(/\/$/, "");
1209
+ } catch {
1210
+ return trimmed.replace(/\/+$/, "");
1211
+ }
1212
+ }
1213
+
1214
+ /**
1215
+ * Whether registry transport defaults own this configured row.
1216
+ *
1217
+ * OAuth/forward providers stay pinned because their credentials must never be sent to an
1218
+ * arbitrary same-named host. Existing key presets keep their historical pinning behavior; a new
1219
+ * preset can opt into collision preservation, in which case its fixed endpoint owns only rows
1220
+ * that still match that destination.
1221
+ */
1222
+ export function providerMatchesRegistryTransport(
1223
+ id: string,
1224
+ provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
1225
+ ): boolean {
1226
+ const entry = getProviderRegistryEntry(id);
1227
+ if (!entry) return false;
1228
+ if (entry.authKind !== "key" || entry.preserveCustomDestination !== true) return true;
1229
+ // The opt-in is intentionally limited to fixed key destinations. Fail closed if a future
1230
+ // registry edit combines it with an override/template despite the registry parity tests.
1231
+ if (entry.allowBaseUrlOverride || /\{[^}]*\}/.test(entry.baseUrl)) return false;
1232
+ if (typeof provider.baseUrl !== "string") return false;
1233
+ if (provider.adapter !== entry.adapter) return false;
1234
+ if (provider.authMode !== undefined && provider.authMode !== "key") return false;
1235
+ return normalizedProviderEndpoint(provider.baseUrl) === normalizedProviderEndpoint(entry.baseUrl);
1236
+ }
1237
+
1116
1238
  /**
1117
1239
  * Effective Codex account mode for a provider. For canonical `openai`, a valid persisted
1118
1240
  * `codexAccountMode` on the provider config wins and a missing/invalid value defaults to
@@ -16,6 +16,7 @@ import { compactionItemToText } from "./compaction";
16
16
  import { previousResponseReplayPrefixLength } from "./state";
17
17
  import { decodeReasoningEnvelope } from "./reasoning-envelope";
18
18
  import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
19
+ import { extractHostedImageGeneration, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool";
19
20
 
20
21
  function isObj(v: unknown): v is Record<string, unknown> {
21
22
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -107,6 +108,10 @@ function mapToolChoice(value: unknown): OcxRequestOptions["toolChoice"] {
107
108
  if ((t === "function" || t === "custom") && "name" in value) {
108
109
  return { name: (value as { name: string }).name };
109
110
  }
111
+ // Hosted image tool types (with or without a name) map to the synthetic image_gen wire name.
112
+ if (t === "image_generation" || t === "image_gen") {
113
+ return { name: IMAGE_GEN_TOOL_NAME };
114
+ }
110
115
  if (t === "allowed_tools" && Array.isArray(value.tools)) {
111
116
  const names = value.tools
112
117
  .map(allowedToolName)
@@ -124,6 +129,7 @@ function allowedToolName(tool: unknown): string | undefined {
124
129
  if (!isObj(tool)) return undefined;
125
130
  if (typeof tool.name === "string" && tool.name.length > 0) return tool.name;
126
131
  if (tool.type === "web_search" || tool.type === "web_search_preview") return WEB_SEARCH_TOOL_NAME;
132
+ if (tool.type === "image_generation" || tool.type === "image_gen") return IMAGE_GEN_TOOL_NAME;
127
133
  if (tool.type === "tool_search") return "tool_search";
128
134
  return undefined;
129
135
  }
@@ -612,6 +618,10 @@ export function parseRequest(body: unknown): OcxParsedRequest {
612
618
  // gpt-mini sidecar for routed providers. buildTools still drops the hosted tool; the sidecar path
613
619
  // re-injects a synthetic function tool only when it will actually handle the call.
614
620
  const webSearch = extractHostedWebSearch(data.tools as unknown[] | undefined);
621
+ const imageGen = extractHostedImageGeneration([
622
+ ...(data.tools as unknown[] ?? []),
623
+ ...loadedToolSpecs,
624
+ ]);
615
625
  // Detect structured-output mode (Responses `text.format`) so the web-search sidecar can render its
616
626
  // tool_result as JSON rather than prose that could corrupt the model's schema-constrained answer.
617
627
  const structuredOutput = detectStructuredOutput(data.text);
@@ -625,6 +635,7 @@ export function parseRequest(body: unknown): OcxParsedRequest {
625
635
  _rawBody: body,
626
636
  ...(replayedInputPrefixLength > 0 ? { _replayPrefixLen: replayedInputPrefixLength } : {}),
627
637
  ...(webSearch ? { _webSearch: webSearch } : {}),
638
+ ...(imageGen ? { _imageGeneration: imageGen } : {}),
628
639
  ...(structuredOutput ? { _structuredOutput: true } : {}),
629
640
  ...(compactionRequest ? { _compactionRequest: true } : {}),
630
641
  ...(contextCompactionBoundary ? { _contextCompactionBoundary: true } : {}),
@@ -1,6 +1,6 @@
1
1
  import { chmodSync, existsSync, lstatSync, mkdirSync, opendirSync, readFileSync, unlinkSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { atomicWriteFile, getConfigDir } from "../config";
3
+ import { atomicWriteFileAsync, getConfigDir } from "../config";
4
4
  import type { OcxProviderContinuationState } from "../types";
5
5
 
6
6
  const MAX_STORED_RESPONSES = 1_000;
@@ -83,6 +83,8 @@ const replayedInputPrefixLengths = new WeakMap<object, number>();
83
83
  let loaded = false;
84
84
  let persistTimer: ReturnType<typeof setTimeout> | null = null;
85
85
  let pendingPersistPath: string | null = null;
86
+ /** Single-flight gate: overlapping response-state writes serialize (#612). */
87
+ let persistGate: Promise<void> = Promise.resolve();
86
88
 
87
89
  function now(): number {
88
90
  return Date.now();
@@ -247,12 +249,18 @@ function ensureLoaded(): void {
247
249
  }
248
250
  }
249
251
 
250
- function persistNow(path: string): void {
252
+ async function persistNow(path: string): Promise<void> {
251
253
  if (persistTimer) {
252
254
  clearTimeout(persistTimer);
253
255
  persistTimer = null;
254
256
  }
255
257
  pendingPersistPath = null;
258
+
259
+ // Serialize writers so concurrent flush + debounce cannot race on temps / ACL (#612).
260
+ const previous = persistGate;
261
+ let release!: () => void;
262
+ persistGate = new Promise<void>(resolve => { release = resolve; });
263
+ await previous;
256
264
  try {
257
265
  const entries: [string, StoredResponseState][] = [];
258
266
  let total = 0;
@@ -273,9 +281,11 @@ function persistNow(path: string): void {
273
281
  // mkdirSync's mode only applies on creation — re-harden an existing config dir so the
274
282
  // conversation-content snapshot never lands in a group/world-readable directory.
275
283
  try { chmodSync(dirname(path), 0o700); } catch { /* best-effort (e.g. Windows) */ }
276
- atomicWriteFile(path, JSON.stringify({ version: 2, states: entries }));
284
+ await atomicWriteFileAsync(path, JSON.stringify({ version: 2, states: entries }));
277
285
  } catch {
278
286
  /* best-effort: disk trouble must never affect request handling */
287
+ } finally {
288
+ release();
279
289
  }
280
290
  }
281
291
 
@@ -285,15 +295,18 @@ function schedulePersist(): void {
285
295
  // debounce fires, and a late write must land in the home that owned the recorded state.
286
296
  pendingPersistPath = snapshotPath();
287
297
  const path = pendingPersistPath;
288
- persistTimer = setTimeout(() => persistNow(path), SNAPSHOT_DEBOUNCE_MS);
298
+ persistTimer = setTimeout(() => { void persistNow(path); }, SNAPSHOT_DEBOUNCE_MS);
289
299
  (persistTimer as { unref?: () => void }).unref?.();
290
300
  }
291
301
 
292
302
  /** Flush any pending debounced snapshot write (graceful shutdown / deterministic tests). */
293
- export function flushResponseState(): void {
294
- if (!persistTimer) return;
295
- // Use the path captured when the write was scheduled — OPENCODEX_HOME may have moved since.
296
- persistNow(pendingPersistPath ?? snapshotPath());
303
+ export async function flushResponseState(): Promise<void> {
304
+ if (persistTimer) {
305
+ await persistNow(pendingPersistPath ?? snapshotPath());
306
+ return;
307
+ }
308
+ // No pending timer: still await any in-flight write so shutdown does not race (#612).
309
+ await persistGate;
297
310
  }
298
311
 
299
312
  function inputItems(input: unknown): unknown[] {
@@ -448,6 +461,7 @@ export function clearResponseStateMemoryForTests(): void {
448
461
  clearTimeout(persistTimer);
449
462
  persistTimer = null;
450
463
  }
464
+ pendingPersistPath = null;
451
465
  states.clear();
452
466
  storedResponseBytes = 0;
453
467
  loaded = false;
@@ -0,0 +1,19 @@
1
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
2
+ return !!value && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+
5
+ /** Collect top-level and Responses Lite tool containers without altering their order. */
6
+ export function collectResponsesToolGroups(body: unknown): unknown[][] {
7
+ if (!isPlainObject(body)) return [];
8
+
9
+ const groups: unknown[][] = [];
10
+ if (Array.isArray(body.tools)) groups.push(body.tools);
11
+ if (!Array.isArray(body.input)) return groups;
12
+
13
+ for (const item of body.input) {
14
+ if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
15
+ groups.push(item.tools);
16
+ }
17
+ }
18
+ return groups;
19
+ }
package/src/router.ts CHANGED
@@ -3,7 +3,7 @@ import { preservesPhysicalComboProvider, tryPickComboModel, type ComboPick } fro
3
3
  import { hasOwnProvider, resolveEnvValue } from "./config";
4
4
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
5
5
  import { redactSecretString, redactUrlForLog } from "./lib/redact";
6
- import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
6
+ import { PROVIDER_REGISTRY, providerCodexAccountMode, providerMatchesRegistryTransport } from "./providers/registry";
7
7
  import { LEGACY_CHATGPT_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
8
8
  import { decodeRoutedModelId, encodeRoutedModelId } from "./providers/slug-codec";
9
9
  import { getStaleCached } from "./codex/model-cache";
@@ -40,7 +40,9 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
40
40
  export function knownModelIdsForProvider(provName: string, prov: OcxProviderConfig): string[] {
41
41
  const ids = new Set<string>();
42
42
  for (const id of prov.models ?? []) ids.add(id);
43
- const registry = PROVIDER_REGISTRY.find(entry => entry.id === provName);
43
+ const registry = providerMatchesRegistryTransport(provName, prov)
44
+ ? PROVIDER_REGISTRY.find(entry => entry.id === provName)
45
+ : undefined;
44
46
  for (const id of registry?.models ?? []) ids.add(id);
45
47
  // Registry model-keyed hint maps double as known native ids (e.g. NVIDIA carries no
46
48
  // static models list but names `moonshotai/kimi-k2.6` in its effort/window maps).
@@ -185,15 +187,22 @@ function warnIfBaseUrlDiscarded(providerName: string, userBaseUrl: string, effec
185
187
  );
186
188
  }
187
189
 
190
+ function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
191
+ const resolved = resolveEnvValue(apiKey);
192
+ return typeof resolved === "string" && resolved.trim().length > 0 ? resolved : undefined;
193
+ }
194
+
188
195
  function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
189
196
  const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
190
- if (!registryEntry) {
197
+ if (!registryEntry || !providerMatchesRegistryTransport(providerName, provider)) {
191
198
  assertProviderDestinationAllowed(providerName, provider);
192
- return { ...provider, apiKey: resolveEnvValue(provider.apiKey) };
199
+ return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey) };
193
200
  }
201
+ const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
194
202
  const explicitKeyOverride = registryEntry.authKind === "oauth"
195
203
  && registryEntry.allowKeyAuthOverride === true
196
- && provider.authMode === "key";
204
+ && provider.authMode === "key"
205
+ && resolvedApiKey !== undefined;
197
206
  const canonicalAuthMode = explicitKeyOverride
198
207
  ? "key"
199
208
  : registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
@@ -239,7 +248,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
239
248
  adapter: registryEntry.adapter,
240
249
  baseUrl,
241
250
  authMode: canonicalAuthMode,
242
- apiKey: resolveEnvValue(provider.apiKey),
251
+ apiKey: resolvedApiKey,
243
252
  // Backfill the Google wire mode + Vertex project/location from the registry when the user
244
253
  // config omits them, so a minimal `google-vertex`/`google-antigravity` entry still routes
245
254
  // through the correct branch (CCA/Vertex) instead of falling back to AI Studio.
@@ -286,7 +295,10 @@ function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][]
286
295
 
287
296
  export class NoEnabledOpenAiProviderError extends Error {
288
297
  constructor(modelId: string) {
289
- super(`No enabled OpenAI provider for model: ${modelId}. Run 'ocx init' to configure a provider, or check that your config has an enabled 'openai' provider.`);
298
+ super(
299
+ `Model ${modelId} requires the canonical openai provider. `
300
+ + `Run: ocx provider add openai && ocx sync && ocx restart`,
301
+ );
290
302
  this.name = "NoEnabledOpenAiProviderError";
291
303
  }
292
304
  }
@@ -1,6 +1,7 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
2
  import { formatErrorResponse } from "../bridge";
3
3
  import {
4
+ apiKeyTransportConfigError,
4
5
  booleanRecordConfigError,
5
6
  modelAdapterRecordConfigError,
6
7
  codexAutoStartEnabled,
@@ -11,13 +12,14 @@ import {
11
12
  reasoningSummaryDeliveryRecordConfigError,
12
13
  } from "../config";
13
14
  import { providerDestinationConfigError } from "../lib/destination-policy";
14
- import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers/registry";
15
+ import { getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport } from "../providers/registry";
15
16
  import { providerConfigSeed } from "../providers/derive";
16
17
  import type { OcxConfig, OcxProviderConfig } from "../types";
17
18
  import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
18
19
 
19
20
  let _corsOrigin = "http://localhost:10100";
20
21
  export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
22
+ /** The proxy's own listening port. No admission check uses it: both loopback predicates key on hostname alone. */
21
23
  export function configuredPort(): string {
22
24
  try { return new URL(_corsOrigin).port; } catch { return "10100"; }
23
25
  }
@@ -35,8 +37,18 @@ export function parseHttpHost(value: string | null): { hostname: string; port: s
35
37
  export function isLoopbackRequestHost(value: string | null): boolean {
36
38
  const parsed = parseHttpHost(value);
37
39
  if (!parsed) return true;
38
- if (!isLoopbackHostname(parsed.hostname)) return false;
39
- return parsed.port === "" || parsed.port === configuredPort();
40
+ // Loopback is a trust boundary by hostname, not by port. `ssh -L 20100:localhost:10100`
41
+ // legitimately arrives as `Host: localhost:20100`, and refusing it took the whole /v1/*
42
+ // data plane down with it, not just CORS. The sibling isLoopbackOriginValue() dropped its
43
+ // own port check for the same reason in e4e06125b ("same-trust-boundary"). Port equality
44
+ // was never the rebinding defense: a rebinding browser connects to the real port and sends
45
+ // it verbatim, so the hostname check below is what rejected it then and now.
46
+ //
47
+ // Scope of that guarantee: it holds for Hosts `parseHttpHost` can parse. An unparseable
48
+ // Host still returns true above — pre-existing behavior, not browser-reachable (a browser
49
+ // composes Host from its own connection), and pinned by a characterization test in
50
+ // tests/server-loopback-host-gate.test.ts. Tightening it is separate work.
51
+ return isLoopbackHostname(parsed.hostname);
40
52
  }
41
53
 
42
54
  export function isLoopbackOriginValue(value: string): boolean {
@@ -76,6 +88,34 @@ export function isAllowedRequestOrigin(req: Request, config: OcxConfig): boolean
76
88
  return !origin || isLoopbackOriginValue(origin) || isSameOriginAsRequest(req, origin) || isExtraAllowedOrigin(origin, config);
77
89
  }
78
90
 
91
+ export function managementRequestOrigin(req: Request, config: OcxConfig): string | null {
92
+ const host = req.headers.get("Host");
93
+ const parsedHost = parseHttpHost(host);
94
+ if (!host || !parsedHost) return null;
95
+ if (!isApiAuthRequired(config) && !isLoopbackHostname(parsedHost.hostname)) return null;
96
+ try {
97
+ const protocol = new URL(req.url).protocol;
98
+ if (protocol !== "http:" && protocol !== "https:") return null;
99
+ return new URL(`${protocol}//${host}`).origin;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ export function isAllowedManagementOrigin(req: Request, config: OcxConfig): boolean {
106
+ const requestOrigin = managementRequestOrigin(req, config);
107
+ if (!requestOrigin) return false;
108
+ const origin = req.headers.get("Origin");
109
+ return !origin || origin === requestOrigin;
110
+ }
111
+
112
+ export function browserSecurityHeaders(): Record<string, string> {
113
+ return {
114
+ "X-Frame-Options": "DENY",
115
+ "Content-Security-Policy": "frame-ancestors 'none'",
116
+ };
117
+ }
118
+
79
119
  export function corsHeaders(req?: Request, config?: OcxConfig): Record<string, string> {
80
120
  const origin = req?.headers.get("Origin");
81
121
  const allowOrigin = origin && req && config && isAllowedRequestOrigin(req, config) ? origin : _corsOrigin;
@@ -87,9 +127,19 @@ export function corsHeaders(req?: Request, config?: OcxConfig): Record<string, s
87
127
  // block covers GPT-Live voice protocol headers relayed by the /v1/live call-create path.
88
128
  "Access-Control-Allow-Headers": "Content-Type, Authorization, X-OpenCodex-API-Key, X-Api-Key, Anthropic-Version, Anthropic-Beta, ChatGPT-Account-Id, OpenAI-Alpha, X-Session-Id, Session-Id, Thread-Id, Originator, X-OAI-Attestation",
89
129
  "Vary": "Origin",
130
+ ...browserSecurityHeaders(),
90
131
  };
91
132
  }
92
133
 
134
+ export function managementCorsHeaders(req?: Request, config?: OcxConfig): Record<string, string> {
135
+ const headers = corsHeaders();
136
+ const origin = req?.headers.get("Origin");
137
+ if (origin && req && config && isAllowedManagementOrigin(req, config)) {
138
+ headers["Access-Control-Allow-Origin"] = origin;
139
+ }
140
+ return headers;
141
+ }
142
+
93
143
  export function withCors(response: Response, req: Request, config: OcxConfig): Response {
94
144
  const headers = new Headers(response.headers);
95
145
  for (const [name, value] of Object.entries(corsHeaders(req, config))) {
@@ -102,6 +152,18 @@ export function withCors(response: Response, req: Request, config: OcxConfig): R
102
152
  });
103
153
  }
104
154
 
155
+ export function withManagementCors(response: Response, req: Request, config: OcxConfig): Response {
156
+ const headers = new Headers(response.headers);
157
+ for (const [name, value] of Object.entries(managementCorsHeaders(req, config))) {
158
+ headers.set(name, value);
159
+ }
160
+ return new Response(response.body, {
161
+ status: response.status,
162
+ statusText: response.statusText,
163
+ headers,
164
+ });
165
+ }
166
+
105
167
  export function jsonResponse(data: unknown, status = 200, req?: Request, config?: OcxConfig): Response {
106
168
  return new Response(JSON.stringify(data), {
107
169
  status,
@@ -114,8 +176,15 @@ export function configuredApiAuthToken(_config: OcxConfig): string | undefined {
114
176
  return token || undefined;
115
177
  }
116
178
 
179
+ export function configuredAdminAuthToken(): string | undefined {
180
+ const token = process.env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim();
181
+ return token || undefined;
182
+ }
183
+
117
184
  export function isLoopbackHostname(hostname: string | undefined): boolean {
118
- const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase();
185
+ // A fully-qualified "localhost." is the same host as "localhost": curl and some clients
186
+ // send the trailing dot verbatim, and refusing it 403s a legitimate loopback caller.
187
+ const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase().replace(/\.$/, "");
119
188
  return normalized === "" || normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
120
189
  }
121
190
 
@@ -124,31 +193,48 @@ export function isApiAuthRequired(config: OcxConfig): boolean {
124
193
  }
125
194
 
126
195
  export function assertServerAuthConfig(config: OcxConfig): void {
127
- if (isApiAuthRequired(config) && !configuredApiAuthToken(config)) {
128
- throw new Error("OPENCODEX_API_AUTH_TOKEN is required when binding opencodex to a non-loopback hostname");
196
+ const hasConfiguredDataCredential = !!configuredApiAuthToken(config)
197
+ || (config.apiKeys ?? []).some(entry => !!entry.key.trim());
198
+ if (isApiAuthRequired(config) && !hasConfiguredDataCredential) {
199
+ throw new Error(
200
+ "A data-plane credential (OPENCODEX_API_AUTH_TOKEN or config.apiKeys) is required when binding opencodex to a non-loopback hostname",
201
+ );
129
202
  }
130
203
  }
131
204
 
132
- /** Whether `token` is one of the proxy's own admission secrets (env token or config API keys). */
133
- export function isProxyAdmissionSecret(token: string, config: OcxConfig): boolean {
134
- const actual = token.trim();
135
- if (!actual) return false;
205
+ function secretEquals(actual: string, expected: string | undefined): boolean {
206
+ if (!expected) return false;
136
207
  const enc = new TextEncoder();
137
208
  const actualBytes = enc.encode(actual);
138
- // Check env-based token
139
- const expected = configuredApiAuthToken(config);
140
- if (expected) {
141
- const expectedBytes = enc.encode(expected);
142
- if (expectedBytes.length === actualBytes.length && timingSafeEqual(actualBytes, expectedBytes)) return true;
143
- }
144
- // Check config-based API keys
209
+ const expectedBytes = enc.encode(expected);
210
+ return expectedBytes.length === actualBytes.length && timingSafeEqual(actualBytes, expectedBytes);
211
+ }
212
+
213
+ /** Whether `token` is a data-plane admission secret. */
214
+ export function isDataPlaneAdmissionSecret(token: string, config: OcxConfig): boolean {
215
+ const actual = token.trim();
216
+ if (!actual) return false;
217
+ if (secretEquals(actual, configuredApiAuthToken(config))) return true;
145
218
  for (const k of config.apiKeys ?? []) {
146
- const keyBytes = enc.encode(k.key);
147
- if (keyBytes.length === actualBytes.length && timingSafeEqual(actualBytes, keyBytes)) return true;
219
+ if (secretEquals(actual, k.key)) return true;
148
220
  }
149
221
  return false;
150
222
  }
151
223
 
224
+ /** Whether `token` is the environment-provided management secret. */
225
+ export function isManagementAdmissionSecret(token: string): boolean {
226
+ const actual = token.trim();
227
+ return !!actual && secretEquals(actual, configuredAdminAuthToken());
228
+ }
229
+
230
+ /** Whether `token` is one of the proxy's own admission secrets and must never reach an upstream. */
231
+ export function isProxyAdmissionSecret(token: string, config: OcxConfig): boolean {
232
+ const actual = token.trim();
233
+ if (!actual) return false;
234
+ if (/^ocx_(?:data|admin|session)_/.test(actual) || /^ocx_[0-9a-f]{40}$/.test(actual)) return true;
235
+ return isDataPlaneAdmissionSecret(actual, config) || isManagementAdmissionSecret(actual);
236
+ }
237
+
152
238
  export class ForwardAdmissionCredentialError extends Error {
153
239
  constructor() {
154
240
  super("OpenCodex admission credentials cannot be forwarded upstream");
@@ -168,12 +254,11 @@ export function hasValidApiAuth(req: Request, config: OcxConfig): boolean {
168
254
  // Anthropic-SDK clients (Claude Code with ANTHROPIC_API_KEY) authenticate via x-api-key.
169
255
  || req.headers.get("x-api-key")?.trim();
170
256
  if (!actual) return false;
171
- return isProxyAdmissionSecret(actual, config);
257
+ return isDataPlaneAdmissionSecret(actual, config);
172
258
  }
173
259
 
174
- export function requireApiAuth(req: Request, config: OcxConfig, kind: "management" | "data-plane"): Response | null {
260
+ export function requireApiAuth(req: Request, config: OcxConfig, _kind: "data-plane"): Response | null {
175
261
  if (hasValidApiAuth(req, config)) return null;
176
- if (kind === "management") return jsonResponse({ error: "opencodex API key required" }, 401);
177
262
  return formatErrorResponse(401, "authentication_error", "opencodex API key required");
178
263
  }
179
264
 
@@ -185,7 +270,7 @@ export function requireApiAuth(req: Request, config: OcxConfig, kind: "managemen
185
270
  export function requireResponsesApiAuth(req: Request, config: OcxConfig): Response | null {
186
271
  if (!isApiAuthRequired(config)) return null;
187
272
  const actual = req.headers.get("x-opencodex-api-key")?.trim();
188
- if (actual && isProxyAdmissionSecret(actual, config)) return null;
273
+ if (actual && isDataPlaneAdmissionSecret(actual, config)) return null;
189
274
  return formatErrorResponse(401, "authentication_error", "opencodex API key required");
190
275
  }
191
276
 
@@ -232,6 +317,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
232
317
  if (destinationError) return `provider ${name} ${destinationError}`;
233
318
  const headersError = providerHeadersConfigError(typed.headers);
234
319
  if (headersError) return `provider ${name} ${headersError}`;
320
+ const apiKeyTransportError = apiKeyTransportConfigError(typed);
321
+ if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
235
322
  const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
236
323
  if (maxInputError) return `provider ${name} ${maxInputError}`;
237
324
  const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
@@ -307,6 +394,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
307
394
  "disabled",
308
395
  "allowPrivateNetwork",
309
396
  "authMode",
397
+ "apiKeyTransport",
310
398
  "keyOptional",
311
399
  "freeTier",
312
400
  "liveModels",
@@ -330,7 +418,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
330
418
  ] as const) {
331
419
  copyIfDefined(dto, provider, key);
332
420
  }
333
- const registryNote = getProviderRegistryEntry(name)?.note;
421
+ const registryNote = providerMatchesRegistryTransport(name, provider)
422
+ ? getProviderRegistryEntry(name)?.note
423
+ : undefined;
334
424
  if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
335
425
  const codexAccountMode = providerCodexAccountMode(name, provider);
336
426
  if (codexAccountMode) dto.codexAccountMode = codexAccountMode;