@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -90,7 +90,7 @@ import {
90
90
  EXPORT_CLIENTS,
91
91
  EXPORT_CLIENT_IDS,
92
92
  OPENCODE_PROVIDER_ID,
93
- buildClientConfig,
93
+ buildClientConfigText,
94
94
  isExportClientId,
95
95
  opencodeProxyBaseUrl,
96
96
  } from "../../clients/config-export";
@@ -104,93 +104,9 @@ import type {
104
104
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
105
105
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
106
106
  import type { ManagementContext } from "./context";
107
+ import { listManagementModelRows, loadExportModels } from "./model-rows";
107
108
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
108
109
 
109
- /**
110
- * One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
111
- * that model plus the identity/visibility fields this boundary computes for every row
112
- * regardless of source. `disabled` is always present; the rest vary by row origin.
113
- */
114
- type ManagementModelRow = Partial<CatalogModel> & {
115
- provider: string;
116
- id: string;
117
- namespaced: string;
118
- disabled: boolean;
119
- native?: boolean;
120
- custom?: boolean;
121
- customId?: string;
122
- };
123
-
124
- /**
125
- * The exact row list `/api/models` returns. Extracted so `/api/client-config` exports the
126
- * models the GUI's Models tab shows — including this function's `disabled` computation,
127
- * which the export core (src/clients/config-export.ts) deliberately does not perform.
128
- */
129
- async function listManagementModelRows(config: OcxConfig): Promise<ManagementModelRow[]> {
130
- const models = await fetchAllModels(config);
131
- const disabled = new Set(config.disabledModels ?? []);
132
- // Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
133
- // from the static supported set so a disabled model stays listed and re-enableable.
134
- const native: ManagementModelRow[] = nativeModelRows(config).map(row => ({
135
- provider: "openai",
136
- id: row.slug,
137
- namespaced: row.slug,
138
- disabled: row.disabled,
139
- native: true,
140
- ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
141
- }));
142
- const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
143
- const namespaced = routedSlug(cm.provider, cm.modelId);
144
- return {
145
- provider: cm.provider,
146
- id: cm.modelId,
147
- namespaced,
148
- disabled: [...disabled].some(stored => slugEquals(stored, cm.provider, cm.modelId)),
149
- custom: true,
150
- customId: cm.id,
151
- displayName: cm.displayName,
152
- ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
153
- ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
154
- };
155
- });
156
- const publicModels = uniqueCatalogModelsForPublicList(models);
157
- const comboNamespaced = new Set(
158
- publicModels.filter(model => model.provider === "combo").map(catalogModelSlug),
159
- );
160
- const visibleCustomModels = customModels.filter(model => !comboNamespaced.has(model.namespaced));
161
- // Custom metadata wins when a physical live/static row resolves to the same Codex-facing
162
- // slug, while a combo keeps the same precedence it has in routing and /v1/models.
163
- const customNamespaced = new Set(visibleCustomModels.map(c => c.namespaced));
164
- const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
165
- // Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
166
- const namespaced = catalogModelSlug(m);
167
- if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
168
- const contextCap = providerContextCap(config, m.provider);
169
- return {
170
- ...m,
171
- namespaced,
172
- disabled: [...disabled].some(stored => (
173
- stored === namespaced || slugEquals(stored, m.provider, m.id)
174
- )),
175
- ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
176
- };
177
- }).filter((row): row is ManagementModelRow => row !== null);
178
- return [...native, ...dedupedRouted, ...visibleCustomModels];
179
- }
180
-
181
- /** `/api/models` row → the narrower input the client-config serializers accept. */
182
- function toExportModel(row: ManagementModelRow): ExportModel {
183
- return {
184
- namespaced: row.namespaced,
185
- provider: row.provider,
186
- id: row.id,
187
- ...(row.native ? { native: true } : {}),
188
- ...(row.displayName ? { displayName: row.displayName } : {}),
189
- ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
190
- ...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
191
- };
192
- }
193
-
194
110
  /**
195
111
  * Counts read back off the SERIALIZED document rather than recomputed from the input rows.
196
112
  * `modelsWithoutLimits` drives a GUI line claiming "these models ship without limits", so it
@@ -198,17 +114,14 @@ function toExportModel(row: ManagementModelRow): ExportModel {
198
114
  * core's "authoritative context window" rule would be free to drift from it silently.
199
115
  */
200
116
  function summarizeExportedModels(client: ExportClientId, document: unknown): { modelCount: number; modelsWithoutLimits: number } {
201
- if (client === "opencode") {
202
- const models = (document as OpencodeGeneratedConfig).provider[OPENCODE_PROVIDER_ID].models;
203
- const entries = Object.values(models);
204
- return { modelCount: entries.length, modelsWithoutLimits: entries.filter(entry => entry.limit === undefined).length };
205
- }
206
- const models = (document as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models;
207
- return { modelCount: models.length, modelsWithoutLimits: models.filter(entry => entry.contextWindow === undefined).length };
117
+ // Each client counts its own document shape. The previous branch assumed
118
+ // "anything that is not OpenCode must be Pi", which silently misread the
119
+ // moment a third client existed.
120
+ return EXPORT_CLIENTS[client].summarize(document);
208
121
  }
209
122
 
210
123
  export async function handleModelRoutes(ctx: ManagementContext): Promise<Response | null> {
211
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
124
+ const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
212
125
  // A handler persists the exact config object passed in. Production defaults to
213
126
  // the real store; tests that pass an in-memory fixture inject a no-op/spy. Do not
214
127
  // bypass this seam with a dynamic config import — doing so replaced a user's
@@ -249,9 +162,12 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
249
162
  );
250
163
  }
251
164
  const spec = EXPORT_CLIENTS[requested];
252
- let rows: ManagementModelRow[];
165
+ let models: ExportModel[];
253
166
  try {
254
- rows = await listManagementModelRows(config);
167
+ // The ONE loader every export surface uses. It carries the visibility
168
+ // filter with it, so this route and the integration routes cannot end up
169
+ // telling a client about different models.
170
+ models = await loadExportModels(config);
255
171
  } catch (error) {
256
172
  // A partial or empty `models` block reads as a valid config while offering nothing,
257
173
  // so a catalog failure is surfaced as unavailable rather than serialized. The
@@ -264,21 +180,24 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
264
180
  config,
265
181
  );
266
182
  }
267
- // The export core does not filter visibility — it serializes what it is given. A model the
268
- // user disabled in the Models tab is absent from /v1/models, so exporting it would hand the
269
- // client a selector the proxy refuses to route.
270
- const models = rows.filter(row => !row.disabled).map(toExportModel);
271
- const document = buildClientConfig(requested, {
183
+ const built = buildClientConfigText(requested, {
272
184
  baseUrl: opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname),
273
185
  models,
274
186
  config,
275
187
  });
188
+ const document = built.document;
276
189
  return jsonResponse({
277
190
  client: spec.id,
278
191
  filename: spec.filename,
279
192
  destination: spec.destination(process.env),
280
193
  apiKeyEnv: spec.apiKeyEnv,
281
194
  exportHint: spec.exportHint,
195
+ // The client's own format and the exact bytes for it. The GUI previously
196
+ // re-serialized `config` as JSON, which is wrong for four of the six
197
+ // clients; `mediaType` also drives the download blob.
198
+ format: built.format,
199
+ mediaType: built.mediaType,
200
+ text: built.text,
282
201
  ...summarizeExportedModels(requested, document),
283
202
  config: document,
284
203
  }, 200, req, config);
@@ -292,8 +211,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
292
211
  const disabled = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string") : [];
293
212
  config.disabledModels = disabled;
294
213
  persistConfig(config);
295
- await refreshCodexCatalogBestEffort();
296
- return jsonResponse({ ok: true, disabled });
214
+ const catalogRefresh = await convergeCodexCatalog();
215
+ return jsonResponse({ ok: true, disabled, catalogRefresh });
297
216
  }
298
217
 
299
218
  // One user-facing visibility switch spans two persisted filters: a provider allowlist and the
@@ -391,8 +310,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
391
310
 
392
311
  config.disabledModels = disabled;
393
312
  persistConfig(config);
394
- await refreshCodexCatalogBestEffort();
395
- return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled });
313
+ const catalogRefresh = await convergeCodexCatalog();
314
+ return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled, catalogRefresh });
396
315
  }
397
316
 
398
317
  if (url.pathname === "/api/custom-models" && req.method === "GET") {
@@ -430,8 +349,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
430
349
  };
431
350
  config.customModels = [...existing, entry];
432
351
  persistConfig(config);
433
- await refreshCodexCatalogBestEffort();
434
- return jsonResponse(entry, 201);
352
+ const catalogRefresh = await convergeCodexCatalog();
353
+ return jsonResponse({ ...entry, catalogRefresh }, 201);
435
354
  }
436
355
 
437
356
  const customPutMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
@@ -468,8 +387,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
468
387
  list[idx] = cm;
469
388
  config.customModels = list;
470
389
  persistConfig(config);
471
- await refreshCodexCatalogBestEffort();
472
- return jsonResponse(cm);
390
+ const catalogRefresh = await convergeCodexCatalog();
391
+ return jsonResponse({ ...cm, catalogRefresh });
473
392
  }
474
393
 
475
394
  const customDelMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
@@ -482,8 +401,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
482
401
  list.splice(idx, 1);
483
402
  config.customModels = list.length > 0 ? list : undefined;
484
403
  persistConfig(config);
485
- await refreshCodexCatalogBestEffort();
486
- return jsonResponse({ ok: true });
404
+ const catalogRefresh = await convergeCodexCatalog();
405
+ return jsonResponse({ ok: true, catalogRefresh });
487
406
  }
488
407
 
489
408
  // Per-provider catalog allowlist (issue #52): when a provider has a non-empty selectedModels list,
@@ -518,8 +437,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
518
437
  if (models.length > 0) config.providers[provider].selectedModels = models;
519
438
  else delete config.providers[provider].selectedModels;
520
439
  persistConfig(config);
521
- await refreshCodexCatalogBestEffort();
522
- return jsonResponse({ ok: true, provider, selected: models });
440
+ const catalogRefresh = await convergeCodexCatalog();
441
+ return jsonResponse({ ok: true, provider, selected: models, catalogRefresh });
523
442
  }
524
443
  return null;
525
444
  }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The `/api/models` row list and its projection into export models.
3
+ *
4
+ * Extracted from model-routes.ts so `/api/client-config` and the integration
5
+ * routes read the SAME visible-model list. Two callers computing "which models
6
+ * does this user actually have" independently is how the export and the toggle
7
+ * would quietly disagree about what a client was told.
8
+ *
9
+ * Bodies are unchanged from their previous home; only `export` was added.
10
+ */
11
+ import type { CatalogModel } from "../../codex/catalog";
12
+ import { catalogModelSlug, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
13
+ import type { ExportModel } from "../../clients/config-export";
14
+ import { providerContextCap } from "../../providers/context-cap";
15
+ import { routedSlug, slugEquals } from "../../providers/slug-codec";
16
+ import type { OcxConfig } from "../../types";
17
+ import { fetchAllModels } from "./shared";
18
+
19
+ /**
20
+ * One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
21
+ * that model plus the identity/visibility fields this boundary computes for every row
22
+ * regardless of source. `disabled` is always present; the rest vary by row origin.
23
+ */
24
+ export type ManagementModelRow = Partial<CatalogModel> & {
25
+ provider: string;
26
+ id: string;
27
+ namespaced: string;
28
+ disabled: boolean;
29
+ native?: boolean;
30
+ custom?: boolean;
31
+ customId?: string;
32
+ };
33
+
34
+ /**
35
+ * The exact row list `/api/models` returns. Extracted so `/api/client-config` exports the
36
+ * models the GUI's Models tab shows — including this function's `disabled` computation,
37
+ * which the export core (src/clients/config-export.ts) deliberately does not perform.
38
+ */
39
+ export async function listManagementModelRows(config: OcxConfig): Promise<ManagementModelRow[]> {
40
+ const models = await fetchAllModels(config);
41
+ const disabled = new Set(config.disabledModels ?? []);
42
+ // Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
43
+ // from the static supported set so a disabled model stays listed and re-enableable.
44
+ const native: ManagementModelRow[] = nativeModelRows(config).map(row => ({
45
+ provider: "openai",
46
+ id: row.slug,
47
+ namespaced: row.slug,
48
+ disabled: row.disabled,
49
+ native: true,
50
+ ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
51
+ }));
52
+ const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
53
+ const namespaced = routedSlug(cm.provider, cm.modelId);
54
+ return {
55
+ provider: cm.provider,
56
+ id: cm.modelId,
57
+ namespaced,
58
+ disabled: [...disabled].some(stored => slugEquals(stored, cm.provider, cm.modelId)),
59
+ custom: true,
60
+ customId: cm.id,
61
+ displayName: cm.displayName,
62
+ ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
63
+ ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
64
+ };
65
+ });
66
+ const publicModels = uniqueCatalogModelsForPublicList(models);
67
+ const comboNamespaced = new Set(
68
+ publicModels.filter(model => model.provider === "combo").map(catalogModelSlug),
69
+ );
70
+ const visibleCustomModels = customModels.filter(model => !comboNamespaced.has(model.namespaced));
71
+ // Custom metadata wins when a physical live/static row resolves to the same Codex-facing
72
+ // slug, while a combo keeps the same precedence it has in routing and /v1/models.
73
+ const customNamespaced = new Set(visibleCustomModels.map(c => c.namespaced));
74
+ const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
75
+ // Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
76
+ const namespaced = catalogModelSlug(m);
77
+ if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
78
+ const contextCap = providerContextCap(config, m.provider);
79
+ return {
80
+ ...m,
81
+ namespaced,
82
+ disabled: [...disabled].some(stored => (
83
+ stored === namespaced || slugEquals(stored, m.provider, m.id)
84
+ )),
85
+ ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
86
+ };
87
+ }).filter((row): row is ManagementModelRow => row !== null);
88
+ return [...native, ...dedupedRouted, ...visibleCustomModels];
89
+ }
90
+
91
+ /** `/api/models` row → the narrower input the client-config serializers accept. */
92
+ export function toExportModel(row: ManagementModelRow): ExportModel {
93
+ return {
94
+ namespaced: row.namespaced,
95
+ provider: row.provider,
96
+ id: row.id,
97
+ ...(row.native ? { native: true } : {}),
98
+ ...(row.displayName ? { displayName: row.displayName } : {}),
99
+ ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
100
+ ...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Visible (non-disabled) rows as export models — the ONE loader both
106
+ * `/api/client-config` and the integration routes use, so the two can never
107
+ * disagree about which models a client is told about.
108
+ *
109
+ * The visibility filter lives HERE rather than at each call site: the export
110
+ * core serializes what it is given, so a model the user disabled in the Models
111
+ * tab is absent from `/v1/models` and exporting it would hand the client a
112
+ * selector the proxy refuses to route.
113
+ */
114
+ export async function loadExportModels(config: OcxConfig): Promise<ExportModel[]> {
115
+ const rows = await listManagementModelRows(config);
116
+ return rows.filter(row => !row.disabled).map(toExportModel);
117
+ }