@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.
- package/AGENTS_INSTALL.md +32 -0
- package/README.md +1 -1
- package/gui/dist/assets/{index-DSK3S5HY.js → index-ClEcVlFO.js} +43 -17
- package/gui/dist/assets/index-DQsMZzI5.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/openai-chat.ts +25 -8
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/claude/agents-inject.ts +2 -2
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +1 -1
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/dispatch.ts +7 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/models.ts +16 -6
- package/src/cli/observe.ts +38 -2
- package/src/cli/registry.ts +2 -1
- package/src/cli/v2.ts +34 -1
- package/src/codex/app-server-processes.ts +46 -26
- package/src/codex/catalog/effort.ts +49 -1
- package/src/codex/catalog/parsing.ts +64 -4
- package/src/codex/catalog/provider-fetch.ts +12 -0
- package/src/codex/catalog/sync.ts +14 -1
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +3 -3
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +39 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/paths.ts +5 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/project-config-warnings.ts +2 -2
- package/src/generated/compatibility-version.json +93 -45
- package/src/images/loop.ts +15 -5
- package/src/providers/antigravity-models.ts +11 -1
- package/src/providers/model-discovery.ts +94 -6
- package/src/providers/quota.ts +159 -0
- package/src/providers/registry.ts +20 -1
- package/src/providers/slug-codec.ts +29 -0
- package/src/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- package/src/router.ts +12 -4
- package/src/routing/capability.ts +32 -17
- package/src/server/auth-cors.ts +42 -6
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +20 -2
- package/src/server/management/context.ts +15 -0
- package/src/server/management/model-routes.ts +12 -3
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/responses/core.ts +13 -0
- package/src/server/system-env.ts +1 -1
- package/src/types.ts +24 -2
- package/src/web-search/loop.ts +21 -5
- 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
|
|
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
|
|
275
|
+
const argv = readFileSync(`/proc/${pid}/cmdline`)
|
|
269
276
|
.toString("utf8")
|
|
270
|
-
.
|
|
271
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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 <=
|
|
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 <=
|
|
314
|
-
out.push({
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/src/codex/convergence.ts
CHANGED
|
@@ -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,
|
package/src/codex/inject.ts
CHANGED
|
@@ -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
|
})
|