@bitkyc08/opencodex 2.20.0 → 2.22.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 (64) hide show
  1. package/AGENTS_INSTALL.md +32 -0
  2. package/README.md +1 -1
  3. package/gui/dist/assets/{index-DSK3S5HY.js → index-ClEcVlFO.js} +43 -17
  4. package/gui/dist/assets/index-DQsMZzI5.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic.ts +2 -28
  8. package/src/adapters/google.ts +31 -3
  9. package/src/adapters/openai-chat.ts +25 -8
  10. package/src/adapters/responses-tool-schema.ts +67 -0
  11. package/src/bridge.ts +15 -2
  12. package/src/claude/agents-inject.ts +2 -2
  13. package/src/claude/gateway-cache.ts +41 -4
  14. package/src/cli/claude.ts +1 -1
  15. package/src/cli/codex-log-guard-doctor.ts +103 -0
  16. package/src/cli/dispatch.ts +7 -1
  17. package/src/cli/help.ts +1 -1
  18. package/src/cli/models.ts +16 -6
  19. package/src/cli/observe.ts +38 -2
  20. package/src/cli/registry.ts +2 -1
  21. package/src/cli/v2.ts +34 -1
  22. package/src/codex/app-server-processes.ts +46 -26
  23. package/src/codex/catalog/effort.ts +49 -1
  24. package/src/codex/catalog/parsing.ts +64 -4
  25. package/src/codex/catalog/provider-fetch.ts +12 -0
  26. package/src/codex/catalog/sync.ts +14 -1
  27. package/src/codex/convergence.ts +2 -0
  28. package/src/codex/inject.ts +3 -3
  29. package/src/codex/log-guard/inspect.ts +506 -0
  30. package/src/codex/log-guard/lock.ts +150 -0
  31. package/src/codex/log-guard/maintenance.ts +403 -0
  32. package/src/codex/log-guard/path-safety.ts +39 -0
  33. package/src/codex/log-guard/policy.ts +44 -0
  34. package/src/codex/log-guard/processes.ts +205 -0
  35. package/src/codex/log-guard/protection.ts +489 -0
  36. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  37. package/src/codex/paths.ts +5 -0
  38. package/src/codex/plugins-doctor.ts +1 -1
  39. package/src/codex/project-config-warnings.ts +2 -2
  40. package/src/generated/compatibility-version.json +93 -45
  41. package/src/images/loop.ts +15 -5
  42. package/src/providers/antigravity-models.ts +11 -1
  43. package/src/providers/model-discovery.ts +94 -6
  44. package/src/providers/quota.ts +159 -0
  45. package/src/providers/registry.ts +20 -1
  46. package/src/providers/slug-codec.ts +29 -0
  47. package/src/responses/custom-tool-compat.ts +4 -1
  48. package/src/responses/parser.ts +7 -1
  49. package/src/responses/provider-opaque-metadata.ts +73 -0
  50. package/src/responses/schema.ts +6 -0
  51. package/src/router.ts +12 -4
  52. package/src/routing/capability.ts +32 -17
  53. package/src/server/auth-cors.ts +42 -6
  54. package/src/server/index.ts +1 -0
  55. package/src/server/management/agent-settings-routes.ts +20 -2
  56. package/src/server/management/context.ts +15 -0
  57. package/src/server/management/model-routes.ts +12 -3
  58. package/src/server/management/storage-log-guard-routes.ts +186 -0
  59. package/src/server/management-api.ts +3 -1
  60. package/src/server/responses/core.ts +13 -0
  61. package/src/server/system-env.ts +1 -1
  62. package/src/types.ts +24 -2
  63. package/src/web-search/loop.ts +21 -5
  64. package/gui/dist/assets/index-DF_UFrGS.css +0 -1
@@ -75,6 +75,7 @@ export interface CodexAppServerProcess {
75
75
  export interface ProcessSnapshot {
76
76
  pid: number;
77
77
  commandLine: string;
78
+ executable?: string;
78
79
  uid?: number;
79
80
  owner?: string;
80
81
  startedAtMs?: number;
@@ -223,13 +224,20 @@ export function codexAppServerProcessIdentity(proc: Pick<CodexAppServerProcess,
223
224
  }
224
225
 
225
226
  /** True when the command line is a Codex app-server (or code-mode host) worth restarting. */
226
- export function isCodexAppServerCommandLine(commandLine: string): boolean {
227
- const tokens = tokenizeCommandLine(commandLine.trim());
227
+ export function isCodexAppServerCommandLine(commandLine: string, executable?: string): boolean {
228
+ const trimmed = commandLine.trim();
229
+ let tokens = tokenizeCommandLine(trimmed);
230
+ if (executable) {
231
+ if (isCodeModeHostToken(executable)) return true;
232
+ if (isCodexExecutableToken(executable)) {
233
+ let remainder = trimmed;
234
+ if (remainder.startsWith(executable)) remainder = remainder.slice(executable.length).trimStart();
235
+ else if (remainder.startsWith(`"${executable}"`)) remainder = remainder.slice(executable.length + 2).trimStart();
236
+ tokens = [executable, ...tokenizeCommandLine(remainder)];
237
+ }
238
+ }
228
239
  if (tokens.length === 0) return false;
229
240
  if (isCodeModeHostProcess(tokens)) return true;
230
-
231
- // Require Codex as argv0 so later-argument occurrences stay unmatched
232
- // (e.g. `node worker.js codex app-server`).
233
241
  if (!isCodexExecutableToken(tokens[0]!)) return false;
234
242
 
235
243
  let i = 1;
@@ -239,7 +247,6 @@ export function isCodexAppServerCommandLine(commandLine: string): boolean {
239
247
  i = advancePastCodexGlobalOption(tokens, i);
240
248
  continue;
241
249
  }
242
- // First non-option after globals is the Codex subcommand.
243
250
  return token.toLowerCase() === "app-server";
244
251
  }
245
252
  return false;
@@ -265,12 +272,13 @@ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
265
272
  const status = readFileSync(`/proc/${pid}/status`, "utf8");
266
273
  const processUid = parseUnixProcStatusUid(status);
267
274
  if (uid !== undefined && processUid !== undefined && processUid !== uid) continue;
268
- const commandLine = readFileSync(`/proc/${pid}/cmdline`)
275
+ const argv = readFileSync(`/proc/${pid}/cmdline`)
269
276
  .toString("utf8")
270
- .replace(/\0/g, " ")
271
- .trim();
277
+ .split("\0")
278
+ .filter(Boolean);
279
+ const commandLine = argv.join(" ").trim();
272
280
  if (!commandLine) continue;
273
- out.push({ pid, commandLine, uid: processUid });
281
+ out.push({ pid, commandLine, executable: argv[0], uid: processUid });
274
282
  } catch {
275
283
  /* process exited mid-scan */
276
284
  }
@@ -280,20 +288,29 @@ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
280
288
 
281
289
  function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
282
290
  const out: ProcessSnapshot[] = [];
283
- // Top-level exec failure propagates: callers decide their own safe default
284
- // (restart flow → treat as none; staleness check → unknown, never "fresh").
285
- const output = uid !== undefined
291
+ const commandOutput = uid !== undefined
286
292
  ? execFileSync("/bin/ps", ["-u", String(uid), "-o", "pid=,command="], {
287
- encoding: "utf-8",
288
- stdio: ["ignore", "pipe", "ignore"],
289
- timeout: 5_000,
293
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
290
294
  })
291
295
  : execFileSync("/bin/ps", ["-axo", "pid=,uid=,command="], {
292
- encoding: "utf-8",
293
- stdio: ["ignore", "pipe", "ignore"],
294
- timeout: 5_000,
296
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
295
297
  });
296
- for (const raw of output.split(/\r?\n/)) {
298
+ const executableOutput = uid !== undefined
299
+ ? execFileSync("/bin/ps", ["-u", String(uid), "-o", "pid=,comm="], {
300
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
301
+ })
302
+ : execFileSync("/bin/ps", ["-axo", "pid=,comm="], {
303
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
304
+ });
305
+ const executableByPid = new Map<number, string>();
306
+ for (const raw of executableOutput.split(/\r?\n/)) {
307
+ const match = /^\s*(\d+)\s+(.+)$/.exec(raw);
308
+ if (!match) continue;
309
+ const pid = Number(match[1]);
310
+ const executable = match[2]?.trim() ?? "";
311
+ if (Number.isSafeInteger(pid) && pid > 0 && executable) executableByPid.set(pid, executable);
312
+ }
313
+ for (const raw of commandOutput.split(/\r?\n/)) {
297
314
  const line = raw.trim();
298
315
  if (!line) continue;
299
316
  if (uid !== undefined) {
@@ -301,8 +318,8 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
301
318
  if (!match) continue;
302
319
  const pid = Number(match[1]);
303
320
  const commandLine = match[2]?.trim() ?? "";
304
- if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine) continue;
305
- out.push({ pid, commandLine, uid });
321
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !commandLine) continue;
322
+ out.push({ pid, commandLine, executable: executableByPid.get(pid), uid });
306
323
  continue;
307
324
  }
308
325
  const match = /^(\d+)\s+(\d+)\s+(.*)$/.exec(line);
@@ -310,8 +327,11 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
310
327
  const pid = Number(match[1]);
311
328
  const processUid = Number(match[2]);
312
329
  const commandLine = match[3]?.trim() ?? "";
313
- if (!Number.isSafeInteger(pid) || pid <= 1 || !commandLine) continue;
314
- out.push({ pid, commandLine, uid: Number.isSafeInteger(processUid) ? processUid : undefined });
330
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !commandLine) continue;
331
+ out.push({
332
+ pid, commandLine, executable: executableByPid.get(pid),
333
+ uid: Number.isSafeInteger(processUid) ? processUid : undefined,
334
+ });
315
335
  }
316
336
  return out;
317
337
  }
@@ -414,7 +434,7 @@ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): C
414
434
  const matched: CodexAppServerProcess[] = [];
415
435
  for (const snapshot of snapshots) {
416
436
  if (seen.has(snapshot.pid)) continue;
417
- if (!isCodexAppServerCommandLine(snapshot.commandLine)) continue;
437
+ if (!isCodexAppServerCommandLine(snapshot.commandLine, snapshot.executable)) continue;
418
438
  seen.add(snapshot.pid);
419
439
  matched.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
420
440
  }
@@ -626,7 +646,7 @@ export function collectCodexAppServerCatalogState(
626
646
  const seen = new Set<number>();
627
647
  for (const snapshot of snapshots) {
628
648
  if (seen.has(snapshot.pid)) continue;
629
- if (!isCodexAppServerCommandLine(snapshot.commandLine)) continue;
649
+ if (!isCodexAppServerCommandLine(snapshot.commandLine, snapshot.executable)) continue;
630
650
  seen.add(snapshot.pid);
631
651
  processes.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
632
652
  }
@@ -31,7 +31,7 @@ import { redactSecretString, redactUserPath } from "../../lib/redact";
31
31
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
32
 
33
33
 
34
- import { readCatalog, readCodexCatalogPath } from "./parsing";
34
+ import { generatedModelMetadata, readCatalog, readCodexCatalogPath } from "./parsing";
35
35
  import type { CatalogModel, RawEntry } from "./parsing";
36
36
  import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
37
37
  import { nativeOpenAiCapabilitySourceSlug } from "./native-models";
@@ -148,6 +148,54 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
148
148
  }];
149
149
  entry.additional_speed_tiers = ["fast"];
150
150
  }
151
+ stampCapabilityProvenance(entry, model);
152
+ }
153
+
154
+ /**
155
+ * Record which capability values a real source actually asserted (#1796).
156
+ *
157
+ * `ensureStrictCatalogFields` fills `context_window` and `input_modalities` with
158
+ * compatibility defaults so Codex's strict parser accepts the file, which means
159
+ * an entry ALWAYS carries both and their presence proves nothing. Routing has to
160
+ * tell "the provider said text-only" apart from "nobody said anything", so it
161
+ * reads this block and never the entry itself ("unknown is not zero",
162
+ * src/routing/capability.ts).
163
+ *
164
+ * Two real sources exist and both are consulted here, in the same precedence the
165
+ * writers use (`applyCatalogMetadata` runs first, the model's own fields
166
+ * overwrite it): the `CatalogModel` and the generated jawcode metadata table.
167
+ * Reading only the model would silently drop every provider whose capabilities
168
+ * live in that table.
169
+ */
170
+ function stampCapabilityProvenance(entry: RawEntry, model: CatalogModel): void {
171
+ // Virtual combo rows are synthesized from last-resort defaults (a generic 128k
172
+ // context and a `["text"]` modality), so their values are placeholders rather
173
+ // than assertions. Stamping them would reintroduce the exact false-evidence
174
+ // defect this block exists to prevent.
175
+ if (model.provider === COMBO_NAMESPACE) return;
176
+
177
+ const meta = generatedModelMetadata(model.provider, model.id);
178
+ const metaContext = typeof meta?.contextWindow === "number" && meta.contextWindow > 0
179
+ // The generated context is capped before it reaches the entry, so provenance
180
+ // must apply the same cap or routing would advertise a window the cap refused.
181
+ ? applyProviderContextCap(meta.contextWindow, model.contextCap) ?? meta.contextWindow
182
+ : undefined;
183
+ const contextWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
184
+ ? model.contextWindow
185
+ : metaContext;
186
+ const inputModalities = Array.isArray(model.inputModalities) && model.inputModalities.length > 0
187
+ ? model.inputModalities
188
+ : (Array.isArray(meta?.input) && meta.input.length > 0 ? meta.input : undefined);
189
+
190
+ entry.opencodex_capability_provenance = {
191
+ provider: model.provider,
192
+ model_id: model.id,
193
+ ...(contextWindow !== undefined ? { context_window: contextWindow } : {}),
194
+ ...(inputModalities !== undefined ? { input_modalities: [...inputModalities] } : {}),
195
+ ...(Array.isArray(model.capabilities) && model.capabilities.length > 0
196
+ ? { capabilities: [...model.capabilities] }
197
+ : {}),
198
+ };
151
199
  }
152
200
 
153
201
  export function applyReasoningLevels(
@@ -341,6 +341,37 @@ export function ensureStrictCatalogFields(
341
341
 
342
342
  export type MultiAgentMode = "v1" | "default" | "v2";
343
343
 
344
+ export interface MultiAgentModeOptions {
345
+ /**
346
+ * When the catalog is in v2 mode, stamp ChatGPT-native rows as v1 instead.
347
+ * Routed parents get v2 (plaintext child tasks). Native Sol/Terra stay on v1
348
+ * so they can still spawn Grok/Claude — ChatGPT encrypts v2 NEW_TASK bodies.
349
+ */
350
+ keepNativeChatGptOnV1?: boolean;
351
+ }
352
+
353
+ /** Catalog rows that run on the ChatGPT backend (encrypt v2 child tasks). */
354
+ export function catalogEntryIsNativeChatGpt(entry: RawEntry): boolean {
355
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
356
+ // combo-native-alias-v1 occupies a bare native slug but is routed through
357
+ // OpenCodex. Keep those on v2 unless the row still carries the ChatGPT-forward
358
+ // contract (`use_responses_lite`).
359
+ if (entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND) {
360
+ return entry.use_responses_lite === true;
361
+ }
362
+ if (trustedAccountBoundNativeCatalogSlug(entry)) return true;
363
+ const routedNativeSlug = slug.startsWith(`${OPENAI_CODEX_PROVIDER_ID}/`)
364
+ ? slug.slice(OPENAI_CODEX_PROVIDER_ID.length + 1)
365
+ : "";
366
+ if (
367
+ entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
368
+ && entry.use_responses_lite === true
369
+ && isNativeOpenAiCapabilityAliasModel(routedNativeSlug)
370
+ ) return true;
371
+ if (UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
372
+ return false;
373
+ }
374
+
344
375
  export const ROUTED_CODEX_TOOL_MODE = "code_mode_only";
345
376
 
346
377
  export function applyRoutedCodexToolMode(entry: RawEntry): RawEntry {
@@ -358,8 +389,23 @@ export function applyRoutedCodexToolMode(entry: RawEntry): RawEntry {
358
389
  * 260730_codex_rs_upstream_v2_live_handoff/060). Upstream pins are always
359
390
  * preserved: a genuine "v1" pin is a real capability statement and stays excluded.
360
391
  * With the feature off the output is byte-identical to the historical behavior.
392
+ *
393
+ * `keepNativeChatGptOnV1` only applies when `mode === "v2"`. It leaves Sol/Terra
394
+ * (and other ChatGPT-native rows) on v1 so a native parent can still spawn a
395
+ * routed child. See issue #92.
361
396
  */
362
- export function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode, v2FeatureEnabled = false): RawEntry[] {
397
+ export function applyMultiAgentMode(
398
+ entries: RawEntry[],
399
+ mode: MultiAgentMode,
400
+ v2FeatureEnabled = false,
401
+ options: MultiAgentModeOptions = {},
402
+ ): RawEntry[] {
403
+ if (mode === "v2" && options.keepNativeChatGptOnV1 === true) {
404
+ for (const entry of entries) {
405
+ entry.multi_agent_version = catalogEntryIsNativeChatGpt(entry) ? "v1" : "v2";
406
+ }
407
+ return entries;
408
+ }
363
409
  if (mode === "default") {
364
410
  // Restore upstream defaults: clear any stale forced multi_agent_version and
365
411
  // re-apply upstream pins from the snapshot for native entries that have one.
@@ -455,11 +501,25 @@ export function catalogModelSupportsReasoningSummaries(modelId: string): boolean
455
501
  return values.size === 1 ? values.values().next().value : undefined;
456
502
  }
457
503
 
458
- export function applyCatalogMetadata(entry: RawEntry, provider: string, modelId: string, contextCap?: number): void {
504
+ /**
505
+ * Resolve the generated jawcode metadata row for a provider/model pair.
506
+ *
507
+ * Exported because it is the SECOND source of real capability assertions:
508
+ * `applyCatalogMetadata` writes context/modalities from it without ever
509
+ * touching a `CatalogModel`, so the routing-evidence provenance stamp in
510
+ * `applyCatalogModelMetadata` has to consult the same table. Both callers share
511
+ * this one lookup rather than duplicating the resolve/case-fold rules, which is
512
+ * what keeps the serialized entry and its provenance from drifting apart.
513
+ */
514
+ export function generatedModelMetadata(provider: string, modelId: string) {
459
515
  const jawcodeProvider = resolveMetadataProvider(provider);
460
- if (!jawcodeProvider) return;
461
- const meta = getModelMetadata(jawcodeProvider, modelId)
516
+ if (!jawcodeProvider) return undefined;
517
+ return getModelMetadata(jawcodeProvider, modelId)
462
518
  ?? (shouldCaseFoldMetadataModelId(provider) ? getModelMetadataCaseInsensitive(jawcodeProvider, modelId) : undefined);
519
+ }
520
+
521
+ export function applyCatalogMetadata(entry: RawEntry, provider: string, modelId: string, contextCap?: number): void {
522
+ const meta = generatedModelMetadata(provider, modelId);
463
523
  if (!meta) return;
464
524
  if (typeof meta.contextWindow === "number" && meta.contextWindow > 0) {
465
525
  const contextWindow = applyProviderContextCap(meta.contextWindow, contextCap) ?? meta.contextWindow;
@@ -990,6 +990,11 @@ function modelInputModalities(
990
990
  if (capabilityRecord?.vision === false) return ["text"];
991
991
  if (capabilityRecord?.vision === true || capabilities?.some(value => (
992
992
  value === "vision" || value === "image-input" || value === "image_input"
993
+ // llama.cpp and Ollama-compatible servers report vision as "multimodal" —
994
+ // it is the only image signal those servers emit (#1797). Mapped to the
995
+ // closed `text|image` enum rather than passed through: an out-of-enum
996
+ // modality makes Codex reject the entire catalog file.
997
+ || value === "multimodal"
993
998
  ))) {
994
999
  return ["text", "image"];
995
1000
  }
@@ -1008,6 +1013,13 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
1008
1013
  item.context_size,
1009
1014
  item.max_model_len,
1010
1015
  item.max_context_length,
1016
+ // llama.cpp reports the served context under `meta`: `n_ctx` is what the
1017
+ // server was actually started with, `n_ctx_train` the model's trained
1018
+ // maximum. Prefer the served value — routing must not promise a window the
1019
+ // running server will refuse. Both come LAST so no provider already
1020
+ // supplying a recognized field changes behavior (#1797).
1021
+ plainRecord(item.meta)?.n_ctx,
1022
+ plainRecord(item.meta)?.n_ctx_train,
1011
1023
  );
1012
1024
  const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
1013
1025
  // Some OpenAI-compatible catalogs expose the selectable ladder under
@@ -390,6 +390,7 @@ export interface ObservedCatalogEntryBuildInput {
390
390
  readonly suppressedBareNativeSlugs: ReadonlySet<string>;
391
391
  readonly disabledNativeAccountSlugs: ReadonlySet<string>;
392
392
  readonly multiAgentV2Enabled: boolean;
393
+ readonly keepNativeChatGptOnV1?: boolean;
393
394
  readonly openaiContextCap?: number;
394
395
  /** Additional native ids to clone under account selectors, without creating bare rows. */
395
396
  readonly accountNativeSlugs?: readonly string[];
@@ -412,6 +413,7 @@ export function buildCatalogEntries(
412
413
  contextCap?: number,
413
414
  accountNativeSlugs?: readonly string[],
414
415
  accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>,
416
+ keepNativeChatGptOnV1 = false,
415
417
  ): RawEntry[] {
416
418
  return buildCatalogEntriesFromObservedState({
417
419
  template,
@@ -425,6 +427,7 @@ export function buildCatalogEntries(
425
427
  suppressedBareNativeSlugs,
426
428
  disabledNativeAccountSlugs,
427
429
  multiAgentV2Enabled: isMultiAgentV2Enabled(),
430
+ keepNativeChatGptOnV1,
428
431
  openaiContextCap: contextCap,
429
432
  accountNativeSlugs,
430
433
  accountNativeSlugsBySelector,
@@ -445,6 +448,7 @@ export function buildCatalogEntriesFromObservedState({
445
448
  suppressedBareNativeSlugs,
446
449
  disabledNativeAccountSlugs,
447
450
  multiAgentV2Enabled,
451
+ keepNativeChatGptOnV1,
448
452
  openaiContextCap,
449
453
  accountNativeSlugs,
450
454
  accountNativeSlugsBySelector,
@@ -624,7 +628,9 @@ export function buildCatalogEntriesFromObservedState({
624
628
  delete entry.prefer_websockets;
625
629
  }
626
630
  }
627
- return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled);
631
+ return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
632
+ keepNativeChatGptOnV1,
633
+ });
628
634
  }
629
635
 
630
636
  export function resetCatalogRuntimeStateForTests(): void {
@@ -733,6 +739,7 @@ export interface ObservedCatalogMergeInput {
733
739
  readonly legacyCustomModelSlugs: ReadonlySet<string>;
734
740
  readonly multiAgentMode: MultiAgentMode;
735
741
  readonly multiAgentV2Enabled: boolean;
742
+ readonly keepNativeChatGptOnV1?: boolean;
736
743
  readonly exactComboSlugs: ReadonlySet<string>;
737
744
  readonly hasPhysicalComboProvider: boolean;
738
745
  readonly includeNativeOpenAi: boolean;
@@ -763,6 +770,7 @@ export function mergeCatalogEntriesFromObservedState({
763
770
  legacyCustomModelSlugs,
764
771
  multiAgentMode,
765
772
  multiAgentV2Enabled,
773
+ keepNativeChatGptOnV1,
766
774
  exactComboSlugs,
767
775
  hasPhysicalComboProvider,
768
776
  includeNativeOpenAi,
@@ -1088,6 +1096,7 @@ export function mergeCatalogEntriesFromObservedState({
1088
1096
  applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
1089
1097
  multiAgentMode,
1090
1098
  multiAgentV2Enabled,
1099
+ { keepNativeChatGptOnV1 },
1091
1100
  );
1092
1101
  for (const entry of versionedEntries) {
1093
1102
  const kind = entry.opencodex_catalog_kind;
@@ -1125,6 +1134,7 @@ export function mergeCatalogEntriesForSync(
1125
1134
  )),
1126
1135
  ),
1127
1136
  openaiContextCap?: number,
1137
+ keepNativeChatGptOnV1 = false,
1128
1138
  ): RawEntry[] {
1129
1139
  // Retained for source compatibility with the original helper contract. Raw provider ids must
1130
1140
  // not suppress same-named native rows; actual admitted combo entries own that decision now.
@@ -1154,6 +1164,7 @@ export function mergeCatalogEntriesForSync(
1154
1164
  legacyCustomModelSlugs,
1155
1165
  multiAgentMode,
1156
1166
  multiAgentV2Enabled: isMultiAgentV2Enabled(),
1167
+ keepNativeChatGptOnV1,
1157
1168
  exactComboSlugs,
1158
1169
  hasPhysicalComboProvider,
1159
1170
  includeNativeOpenAi,
@@ -1477,6 +1488,7 @@ function writeRetainedCatalogSync({
1477
1488
  suppressedBareNativeSlugs,
1478
1489
  disabledNativeAccountSlugs: new Set([...disabledNativeSlugs(config)].filter(slug => suppressedBareNativeSlugs.has(slug))),
1479
1490
  multiAgentV2Enabled,
1491
+ keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
1480
1492
  openaiContextCap,
1481
1493
  accountNativeSlugs,
1482
1494
  accountNativeSlugsBySelector,
@@ -1497,6 +1509,7 @@ function writeRetainedCatalogSync({
1497
1509
  legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
1498
1510
  multiAgentMode,
1499
1511
  multiAgentV2Enabled,
1512
+ keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
1500
1513
  exactComboSlugs,
1501
1514
  hasPhysicalComboProvider,
1502
1515
  includeNativeOpenAi,
@@ -279,6 +279,7 @@ function prepareCatalog(
279
279
  suppressedBareNativeSlugs,
280
280
  disabledNativeAccountSlugs: new Set([...disabledNative].filter(slug => suppressedBareNativeSlugs.has(slug))),
281
281
  multiAgentV2Enabled,
282
+ keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
282
283
  accountNativeSlugs,
283
284
  accountNativeSlugsBySelector,
284
285
  }).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined);
@@ -305,6 +306,7 @@ function prepareCatalog(
305
306
  legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
306
307
  multiAgentMode,
307
308
  multiAgentV2Enabled,
309
+ keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
308
310
  exactComboSlugs,
309
311
  hasPhysicalComboProvider,
310
312
  includeNativeOpenAi,
@@ -449,7 +449,7 @@ function stripRootRoutedModel(content: string): string {
449
449
  .filter((line, i) => {
450
450
  const isRoot = firstTable === -1 || i < firstTable;
451
451
  if (!isRoot) return true;
452
- const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
452
+ const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/);
453
453
  if (!m) return true;
454
454
  const model = parseTomlString(m[1]);
455
455
  return !model?.includes("/");
@@ -485,7 +485,7 @@ function setRootModelCatalogPath(content: string, catalogPath: string): string {
485
485
  const rootEnd = firstTable === -1 ? lines.length : firstTable;
486
486
  for (let i = 0; i < rootEnd; i++) {
487
487
  const m = lines[i].match(
488
- /^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/,
488
+ /^\s*model_catalog_json\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/,
489
489
  );
490
490
  if (!m) continue;
491
491
  const existing = parseTomlString(m[1]);
@@ -580,7 +580,7 @@ function stripOpencodexCatalogPath(content: string): string {
580
580
  .split("\n")
581
581
  .filter((line) => {
582
582
  const m = line.match(
583
- /^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/,
583
+ /^\s*model_catalog_json\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/,
584
584
  );
585
585
  return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
586
586
  })