@bitkyc08/opencodex 2.7.33 → 2.7.34
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/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +21 -10
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/gui/dist/assets/index-BkmJJgg6.js +52 -0
- package/gui/dist/assets/index-Sg-7L_oZ.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +13 -6
- package/src/adapters/cursor/discovery.ts +39 -4
- package/src/adapters/cursor/exec-policy.ts +11 -13
- package/src/adapters/cursor/live-transport.ts +22 -4
- package/src/adapters/cursor/protobuf-events.ts +140 -8
- package/src/adapters/cursor/protobuf-request.ts +15 -0
- package/src/adapters/cursor/request-builder.ts +10 -5
- package/src/adapters/cursor/transport.ts +3 -2
- package/src/adapters/cursor/types.ts +14 -0
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro-errors.ts +111 -2
- package/src/adapters/kiro-events.ts +154 -35
- package/src/adapters/kiro-retry.ts +116 -32
- package/src/adapters/kiro-tools.ts +30 -20
- package/src/adapters/kiro-wire.ts +47 -6
- package/src/adapters/kiro.ts +891 -228
- package/src/adapters/openai-chat.ts +12 -5
- package/src/adapters/openai-responses.ts +7 -2
- package/src/bridge.ts +109 -26
- package/src/claude/outbound.ts +27 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog.ts +375 -33
- package/src/combos/index.ts +3 -0
- package/src/combos/request.ts +4 -4
- package/src/combos/resolve.ts +2 -2
- package/src/combos/types.ts +104 -2
- package/src/config.ts +70 -1
- package/src/lib/eventstream-decoder.ts +9 -0
- package/src/oauth/index.ts +3 -1
- package/src/oauth/kiro-credentials.ts +48 -20
- package/src/oauth/login-cli.ts +2 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/kiro-models.ts +2 -2
- package/src/providers/openai-sidecar.ts +28 -1
- package/src/providers/registry.ts +39 -2
- package/src/responses/parser.ts +22 -10
- package/src/responses/schema.ts +1 -0
- package/src/responses/state.ts +50 -10
- package/src/router.ts +15 -3
- package/src/server/auth-cors.ts +7 -0
- package/src/server/claude-messages.ts +6 -0
- package/src/server/index.ts +6 -3
- package/src/server/management-api.ts +187 -43
- package/src/server/ports.ts +4 -2
- package/src/server/request-log.ts +3 -2
- package/src/server/responses-item-id-repair.ts +281 -0
- package/src/server/responses.ts +274 -73
- package/src/types.ts +109 -16
- package/src/update/job.ts +81 -1
- package/src/vision/describe.ts +2 -1
- package/src/web-search/executor.ts +2 -1
- package/src/web-search/loop.ts +9 -1
- package/src/web-search/progress-stream.ts +12 -10
- package/gui/dist/assets/index-D6Fcl4yM.css +0 -1
- package/gui/dist/assets/index-d63HMU0x.js +0 -52
package/src/codex/catalog.ts
CHANGED
|
@@ -13,11 +13,11 @@ import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJa
|
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../providers/derive";
|
|
14
14
|
import { getProviderRegistryEntry } from "../providers/registry";
|
|
15
15
|
import { applyProviderContextCap, providerContextCap } from "../providers/context-cap";
|
|
16
|
-
import {
|
|
16
|
+
import { routedSlug, slugEquals, slugsEquivalent } from "../providers/slug-codec";
|
|
17
17
|
import { CODEX_GPT5_IDENTITY_LINE } from "../adapters/identity";
|
|
18
18
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../adapters/cursor/discovery";
|
|
19
19
|
import { fetchCursorUsableModels } from "../adapters/cursor/live-models";
|
|
20
|
-
import { OPENAI_API_PROVIDER_ID } from "../providers/openai-tiers";
|
|
20
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
21
21
|
import {
|
|
22
22
|
COMBO_NAMESPACE,
|
|
23
23
|
comboModelId,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
targetKey,
|
|
27
27
|
} from "../combos";
|
|
28
28
|
import type { NormalizedComboConfig } from "../combos/types";
|
|
29
|
+
import { providerDestinationResolvedError } from "../lib/destination-policy";
|
|
29
30
|
import { redactSecretString } from "../lib/redact";
|
|
30
31
|
import upstreamModelsSnapshot from "./data/upstream-models.json";
|
|
31
32
|
|
|
@@ -138,6 +139,54 @@ export function nativeOpenAiContextWindow(slug: string): number | undefined {
|
|
|
138
139
|
: undefined);
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
/* ── Native-slug capability helpers for combo member resolution (issue #268) ── */
|
|
143
|
+
|
|
144
|
+
/** Input modalities for a native OpenAI slug, defaulting to the GPT family baseline. */
|
|
145
|
+
function nativeInputModalities(slug: string): string[] {
|
|
146
|
+
const upstream = UPSTREAM_NATIVE_ENTRIES.get(slug);
|
|
147
|
+
if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
|
|
148
|
+
return [...upstream!.input_modalities as string[]];
|
|
149
|
+
}
|
|
150
|
+
// gpt-5.3-codex-spark is not in the upstream snapshot; all supported natives are
|
|
151
|
+
// text+image capable, so default to the family baseline rather than text-only.
|
|
152
|
+
return ["text", "image"];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Reasoning effort ladder for a native OpenAI slug, mirroring the catalog emission path. */
|
|
156
|
+
function nativeReasoningEfforts(slug: string): string[] {
|
|
157
|
+
const upstream = UPSTREAM_NATIVE_ENTRIES.get(slug);
|
|
158
|
+
const levels = Array.isArray(upstream?.supported_reasoning_levels)
|
|
159
|
+
? upstream!.supported_reasoning_levels as Array<{ effort?: string }>
|
|
160
|
+
: [];
|
|
161
|
+
if (levels.length > 0) {
|
|
162
|
+
const efforts = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
|
|
163
|
+
// gpt-5.6 natives get max+ultra restored (ensureGpt56ReasoningLevels catalog path does
|
|
164
|
+
// the same); older natives (gpt-5.5/5.4/5.4-mini/5.3-codex-spark) stop at xhigh per
|
|
165
|
+
// upstream snapshot.
|
|
166
|
+
if (isGpt56NativeSlug(slug)) {
|
|
167
|
+
const set = new Set(efforts);
|
|
168
|
+
for (const e of ["max", "ultra"]) set.add(e);
|
|
169
|
+
return [...set];
|
|
170
|
+
}
|
|
171
|
+
return efforts;
|
|
172
|
+
}
|
|
173
|
+
// gpt-5.3-codex-spark is not in upstream snapshot — use the standard old-ladder default.
|
|
174
|
+
return ["low", "medium", "high", "xhigh"];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Whether a native OpenAI slug supports parallel tool calls (per upstream snapshot). */
|
|
178
|
+
function nativeParallelToolCalls(slug: string): boolean {
|
|
179
|
+
return UPSTREAM_NATIVE_ENTRIES.get(slug)?.supports_parallel_tool_calls === true
|
|
180
|
+
|| false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Quick check whether the config has any combo targets at all. */
|
|
184
|
+
function hasComboTargets(config: { combos?: Record<string, { targets?: unknown[] }> }): boolean {
|
|
185
|
+
const combos = config.combos;
|
|
186
|
+
if (!combos) return false;
|
|
187
|
+
return Object.values(combos).some(c => Array.isArray(c?.targets) && c!.targets!.length > 0);
|
|
188
|
+
}
|
|
189
|
+
|
|
141
190
|
/**
|
|
142
191
|
* Bare (slash-free) entries of `disabledModels` — the native GPT half of the single
|
|
143
192
|
* enable/disable choke point. Routed ids are always namespaced `provider/id`, so bare
|
|
@@ -252,6 +301,22 @@ export function nativeEffortClamp(slug: string, effort: string | undefined): str
|
|
|
252
301
|
return highest ?? null;
|
|
253
302
|
}
|
|
254
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Request-time guard for `nativeEffortClamp`: only the canonical built-in OpenAI/Codex
|
|
306
|
+
* forward provider should ever use the native mock-max repair. Bare third-party
|
|
307
|
+
* `defaultModel` selectors are still routed models whose adapters own effort mapping,
|
|
308
|
+
* and explicit `provider/model` requests are routed by construction.
|
|
309
|
+
*/
|
|
310
|
+
export function shouldApplyNativeEffortClamp(
|
|
311
|
+
providerName: string,
|
|
312
|
+
provider: OcxProviderConfig,
|
|
313
|
+
requestedModelId: string,
|
|
314
|
+
): boolean {
|
|
315
|
+
return !requestedModelId.includes("/")
|
|
316
|
+
&& providerName === OPENAI_CODEX_PROVIDER_ID
|
|
317
|
+
&& isCanonicalOpenAiForwardProvider(provider);
|
|
318
|
+
}
|
|
319
|
+
|
|
255
320
|
/**
|
|
256
321
|
* True when a preserved catalog entry for a snapshot-covered slug should be UPGRADED to the
|
|
257
322
|
* pinned upstream entry. Discriminator: `display_name === slug` — both ocx synthesis and the
|
|
@@ -292,6 +357,100 @@ export function catalogModelEfforts(slugs: readonly string[]): Map<string, strin
|
|
|
292
357
|
return out;
|
|
293
358
|
}
|
|
294
359
|
|
|
360
|
+
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
361
|
+
|
|
362
|
+
export type SpawnAgentSurface = "v1" | "v2";
|
|
363
|
+
export type SubagentRosterExclusionReason =
|
|
364
|
+
| "missing_catalog_entry"
|
|
365
|
+
| "picker_hidden"
|
|
366
|
+
| "surface_incompatible"
|
|
367
|
+
| "outside_display_limit";
|
|
368
|
+
|
|
369
|
+
export interface EffectiveSubagentModel {
|
|
370
|
+
model: string;
|
|
371
|
+
efforts: string[];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export interface SubagentRosterExclusion {
|
|
375
|
+
configured: string;
|
|
376
|
+
reason: SubagentRosterExclusionReason;
|
|
377
|
+
catalogModel?: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface EffectiveSubagentRoster {
|
|
381
|
+
candidates: EffectiveSubagentModel[];
|
|
382
|
+
advertised: EffectiveSubagentModel[];
|
|
383
|
+
excluded: SubagentRosterExclusion[];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function catalogEntryEfforts(entry: RawEntry): string[] {
|
|
387
|
+
const levels = Array.isArray(entry.supported_reasoning_levels)
|
|
388
|
+
? entry.supported_reasoning_levels as Array<{ effort?: string }>
|
|
389
|
+
: [];
|
|
390
|
+
return levels.flatMap(level => typeof level.effort === "string" ? [level.effort] : []);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function configuredCatalogEntry(entries: RawEntry[], configured: string): RawEntry | undefined {
|
|
394
|
+
return entries.find(entry => entry.slug === configured)
|
|
395
|
+
?? entries.find(entry => typeof entry.slug === "string" && slugsEquivalent(configured, entry.slug));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The effective sub-agent roster for a collaboration surface: Codex's picker-visible
|
|
400
|
+
* (`visibility === "list"`), surface-compatible, priority-sorted first five catalog
|
|
401
|
+
* entries (candidates), intersected with the configured subagentModels (advertised),
|
|
402
|
+
* plus a reason for every configured entry that did not make the set (excluded).
|
|
403
|
+
* Canonical `entry.slug` is returned; legacy raw aliases are matching inputs only.
|
|
404
|
+
*/
|
|
405
|
+
export function effectiveSubagentRoster(
|
|
406
|
+
configuredModels: readonly string[],
|
|
407
|
+
surface: SpawnAgentSurface,
|
|
408
|
+
): EffectiveSubagentRoster {
|
|
409
|
+
const configured = configuredModels
|
|
410
|
+
.filter(model => model.trim().length > 0)
|
|
411
|
+
.filter((model, index, all) =>
|
|
412
|
+
!all.slice(0, index).some(previous => slugsEquivalent(previous, model))
|
|
413
|
+
);
|
|
414
|
+
const entries = readCatalog(readCodexCatalogPath())?.models ?? [];
|
|
415
|
+
const ordered = entries
|
|
416
|
+
.map((entry, index) => ({ entry, index }))
|
|
417
|
+
.filter(({ entry }) => typeof entry.slug === "string")
|
|
418
|
+
.filter(({ entry }) => entry.visibility === "list")
|
|
419
|
+
.filter(({ entry }) => surface !== "v2" || entry.multi_agent_version === "v2")
|
|
420
|
+
.sort((left, right) => {
|
|
421
|
+
const leftPriority = typeof left.entry.priority === "number" && Number.isFinite(left.entry.priority)
|
|
422
|
+
? left.entry.priority : Number.MAX_SAFE_INTEGER;
|
|
423
|
+
const rightPriority = typeof right.entry.priority === "number" && Number.isFinite(right.entry.priority)
|
|
424
|
+
? right.entry.priority : Number.MAX_SAFE_INTEGER;
|
|
425
|
+
return leftPriority - rightPriority || left.index - right.index;
|
|
426
|
+
})
|
|
427
|
+
.slice(0, MAX_SPAWN_AGENT_MODEL_OVERRIDES);
|
|
428
|
+
|
|
429
|
+
const candidates = ordered.map(({ entry }) => ({
|
|
430
|
+
model: entry.slug as string,
|
|
431
|
+
efforts: catalogEntryEfforts(entry),
|
|
432
|
+
}));
|
|
433
|
+
const advertised = candidates.filter(candidate =>
|
|
434
|
+
configured.some(model => slugsEquivalent(model, candidate.model))
|
|
435
|
+
);
|
|
436
|
+
const excluded = configured.flatMap((model): SubagentRosterExclusion[] => {
|
|
437
|
+
const entry = configuredCatalogEntry(entries, model);
|
|
438
|
+
if (!entry) return [{ configured: model, reason: "missing_catalog_entry" }];
|
|
439
|
+
const catalogModel = entry.slug as string;
|
|
440
|
+
if (entry.visibility !== "list") {
|
|
441
|
+
return [{ configured: model, catalogModel, reason: "picker_hidden" }];
|
|
442
|
+
}
|
|
443
|
+
if (surface === "v2" && entry.multi_agent_version !== "v2") {
|
|
444
|
+
return [{ configured: model, catalogModel, reason: "surface_incompatible" }];
|
|
445
|
+
}
|
|
446
|
+
if (!candidates.some(candidate => candidate.model === catalogModel)) {
|
|
447
|
+
return [{ configured: model, catalogModel, reason: "outside_display_limit" }];
|
|
448
|
+
}
|
|
449
|
+
return [];
|
|
450
|
+
});
|
|
451
|
+
return { candidates, advertised, excluded };
|
|
452
|
+
}
|
|
453
|
+
|
|
295
454
|
/**
|
|
296
455
|
* The native (passthrough) OpenAI slugs to advertise — the LIVE Codex catalog's own bare slugs when
|
|
297
456
|
* available, with documented Codex-native additions layered in, else the static fallback above.
|
|
@@ -305,6 +464,8 @@ export function nativeOpenAiSlugs(): string[] {
|
|
|
305
464
|
export interface CatalogModel {
|
|
306
465
|
id: string;
|
|
307
466
|
provider: string;
|
|
467
|
+
/** Public Codex-facing slug override (used by combo aliases). */
|
|
468
|
+
alias?: string;
|
|
308
469
|
owned_by?: string;
|
|
309
470
|
reasoningEfforts?: string[];
|
|
310
471
|
defaultReasoningEffort?: string;
|
|
@@ -315,6 +476,8 @@ export interface CatalogModel {
|
|
|
315
476
|
inputModalities?: string[];
|
|
316
477
|
/** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */
|
|
317
478
|
parallelToolCalls?: boolean;
|
|
479
|
+
/** Whether Codex may send Responses text.verbosity for this routed model. */
|
|
480
|
+
supportsVerbosity?: boolean;
|
|
318
481
|
}
|
|
319
482
|
|
|
320
483
|
type RawEntry = Record<string, unknown>;
|
|
@@ -443,7 +606,7 @@ function applyNativeOpenAiContextOverride(entry: RawEntry): void {
|
|
|
443
606
|
|
|
444
607
|
function ensureStrictCatalogFields(
|
|
445
608
|
entry: RawEntry,
|
|
446
|
-
options: { preserveExactInputModalities?: boolean } = {},
|
|
609
|
+
options: { preserveExactInputModalities?: boolean; isRouted?: boolean } = {},
|
|
447
610
|
): RawEntry {
|
|
448
611
|
if (typeof entry.supports_reasoning_summaries !== "boolean") entry.supports_reasoning_summaries = true;
|
|
449
612
|
if (typeof entry.default_reasoning_summary !== "string") entry.default_reasoning_summary = "none";
|
|
@@ -464,7 +627,7 @@ function ensureStrictCatalogFields(
|
|
|
464
627
|
if (
|
|
465
628
|
typeof entry.max_context_window !== "number"
|
|
466
629
|
|| entry.max_context_window <= 0
|
|
467
|
-
|| (!isNativeOpenAiEntry(entry) && entry.max_context_window > contextWindow)
|
|
630
|
+
|| ((options.isRouted === true || !isNativeOpenAiEntry(entry)) && entry.max_context_window > contextWindow)
|
|
468
631
|
) {
|
|
469
632
|
entry.max_context_window = contextWindow;
|
|
470
633
|
}
|
|
@@ -534,7 +697,7 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls =
|
|
|
534
697
|
// openai-chat adapter stops forcing parallel_tool_calls:false and the buffered stream parser
|
|
535
698
|
// assembles multi-call turns (devlog/_plan/260709_parallel_tool_calls).
|
|
536
699
|
entry.supports_parallel_tool_calls = isCursorEntry || parallelToolCalls === true;
|
|
537
|
-
return ensureStrictCatalogFields(entry);
|
|
700
|
+
return ensureStrictCatalogFields(entry, { isRouted: true });
|
|
538
701
|
}
|
|
539
702
|
|
|
540
703
|
// provider + NATIVE model id are passed separately: the Codex-facing slug may carry an
|
|
@@ -732,6 +895,9 @@ const ROUTED_REASONING_LEVELS = [...CODEX_REASONING_LEVELS];
|
|
|
732
895
|
|
|
733
896
|
function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void {
|
|
734
897
|
if (!model) return;
|
|
898
|
+
// This marker survives strict catalog normalization and lets sync distinguish a stale
|
|
899
|
+
// bare combo alias from a genuine native model row.
|
|
900
|
+
if (model.provider === COMBO_NAMESPACE) entry.owned_by = model.owned_by ?? COMBO_NAMESPACE;
|
|
735
901
|
if (typeof model.contextWindow === "number" && model.contextWindow > 0) {
|
|
736
902
|
entry.context_window = model.contextWindow;
|
|
737
903
|
entry.max_context_window = model.contextWindow;
|
|
@@ -743,6 +909,9 @@ function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void
|
|
|
743
909
|
if (Array.isArray(model.inputModalities) && model.inputModalities.length > 0) {
|
|
744
910
|
entry.input_modalities = model.inputModalities;
|
|
745
911
|
}
|
|
912
|
+
if (typeof model.supportsVerbosity === "boolean") {
|
|
913
|
+
entry.support_verbosity = model.supportsVerbosity;
|
|
914
|
+
}
|
|
746
915
|
}
|
|
747
916
|
|
|
748
917
|
function applyReasoningLevels(
|
|
@@ -910,7 +1079,11 @@ function isExactComboCatalogModel(
|
|
|
910
1079
|
model: CatalogModel | undefined,
|
|
911
1080
|
exactComboSlugs: ReadonlySet<string>,
|
|
912
1081
|
): boolean {
|
|
913
|
-
return model !== undefined && exactComboSlugs.has(
|
|
1082
|
+
return model !== undefined && exactComboSlugs.has(catalogModelSlug(model));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export function catalogModelSlug(model: CatalogModel): string {
|
|
1086
|
+
return model.alias ?? routedSlug(model.provider, model.id);
|
|
914
1087
|
}
|
|
915
1088
|
|
|
916
1089
|
function deriveEntry(
|
|
@@ -922,7 +1095,8 @@ function deriveEntry(
|
|
|
922
1095
|
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
923
1096
|
): RawEntry {
|
|
924
1097
|
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
|
|
925
|
-
|
|
1098
|
+
const isRouted = model !== undefined;
|
|
1099
|
+
if (!isRouted && !slug.includes("/")) {
|
|
926
1100
|
// Supported native slug covered by the upstream snapshot: use the REAL entry (exact
|
|
927
1101
|
// reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
|
|
928
1102
|
// instead of cloning an older template.
|
|
@@ -940,7 +1114,7 @@ function deriveEntry(
|
|
|
940
1114
|
delete e.availability_nux; // don't replay another model's "now available" NUX
|
|
941
1115
|
// Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
|
|
942
1116
|
// and advertise the reasoning ladder Codex accepts.
|
|
943
|
-
if (
|
|
1117
|
+
if (isRouted) {
|
|
944
1118
|
// Native id for identity text + metadata lookups — the slug may be an encoded
|
|
945
1119
|
// alias (`provider/vendor-model`); the model object carries the native id.
|
|
946
1120
|
const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
|
|
@@ -974,6 +1148,7 @@ function deriveEntry(
|
|
|
974
1148
|
}
|
|
975
1149
|
return ensureStrictCatalogFields(normalizeServiceTiers(e), {
|
|
976
1150
|
preserveExactInputModalities: preserveExact,
|
|
1151
|
+
isRouted,
|
|
977
1152
|
});
|
|
978
1153
|
}
|
|
979
1154
|
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
@@ -981,20 +1156,21 @@ function deriveEntry(
|
|
|
981
1156
|
slug, display_name: slug, description: desc,
|
|
982
1157
|
shell_type: "shell_command", visibility: "list", supported_in_api: true,
|
|
983
1158
|
priority, base_instructions: "You are a helpful coding assistant.",
|
|
984
|
-
...(
|
|
1159
|
+
...(isRouted ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
|
|
985
1160
|
};
|
|
986
|
-
if (
|
|
1161
|
+
if (isRouted) {
|
|
987
1162
|
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
|
|
988
1163
|
}
|
|
989
1164
|
else {
|
|
990
1165
|
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
|
|
991
1166
|
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
|
|
992
1167
|
}
|
|
993
|
-
if (model &&
|
|
1168
|
+
if (model && isRouted) applyJawcodeCatalogMetadata(entry, model.provider, model.id, model.contextCap);
|
|
994
1169
|
applyCatalogModelMetadata(entry, model);
|
|
995
|
-
applyNativeOpenAiContextOverride(entry);
|
|
1170
|
+
if (!isRouted) applyNativeOpenAiContextOverride(entry);
|
|
996
1171
|
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
|
|
997
1172
|
preserveExactInputModalities: preserveExact,
|
|
1173
|
+
isRouted,
|
|
998
1174
|
});
|
|
999
1175
|
}
|
|
1000
1176
|
|
|
@@ -1019,6 +1195,9 @@ export function buildCatalogEntries(
|
|
|
1019
1195
|
const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
|
|
1020
1196
|
const out: RawEntry[] = [];
|
|
1021
1197
|
const collisionSkipped = resolveSlugAliasCollisions(goModels);
|
|
1198
|
+
const comboPublicSlugs = new Set(goModels
|
|
1199
|
+
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
1200
|
+
.map(catalogModelSlug));
|
|
1022
1201
|
for (const slug of gptSlugs) {
|
|
1023
1202
|
const e = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9);
|
|
1024
1203
|
if (rank.has(slug)) e.priority = rank.get(slug)!;
|
|
@@ -1026,9 +1205,13 @@ export function buildCatalogEntries(
|
|
|
1026
1205
|
}
|
|
1027
1206
|
for (const m of goModels) {
|
|
1028
1207
|
if (collisionSkipped.has(m)) continue;
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1208
|
+
const slug = catalogModelSlug(m);
|
|
1209
|
+
if (m.provider !== COMBO_NAMESPACE && comboPublicSlugs.has(slug)) {
|
|
1210
|
+
warnComboMasqueradeCollisionOnce(slug);
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
// Provider rows use the one-slash slug codec; combo aliases intentionally override that
|
|
1214
|
+
// public slug and may be bare.
|
|
1032
1215
|
const e = deriveEntry(
|
|
1033
1216
|
template,
|
|
1034
1217
|
slug,
|
|
@@ -1193,6 +1376,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
1193
1376
|
}
|
|
1194
1377
|
: {}),
|
|
1195
1378
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
1379
|
+
...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
|
|
1196
1380
|
// Default-on for openai-chat providers (explicit false opts out); other adapters
|
|
1197
1381
|
// advertise only on explicit opt-in.
|
|
1198
1382
|
...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
|
|
@@ -1358,28 +1542,64 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
1358
1542
|
const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
|
|
1359
1543
|
? "vertex-aiplatform"
|
|
1360
1544
|
: "provider-models";
|
|
1545
|
+
const failedDiscoveryFallback = (): { models: CatalogModel[]; fallback: "stale" | "configured" } => {
|
|
1546
|
+
markModelsFetchFailure(name);
|
|
1547
|
+
const stale = getStaleCached(name);
|
|
1548
|
+
return {
|
|
1549
|
+
models: stale
|
|
1550
|
+
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
|
|
1551
|
+
: configured,
|
|
1552
|
+
fallback: stale ? "stale" : "configured",
|
|
1553
|
+
};
|
|
1554
|
+
};
|
|
1361
1555
|
try {
|
|
1556
|
+
const destinationError = await providerDestinationResolvedError(name, {
|
|
1557
|
+
baseUrl: url,
|
|
1558
|
+
allowPrivateNetwork: prov.allowPrivateNetwork,
|
|
1559
|
+
});
|
|
1560
|
+
if (destinationError) {
|
|
1561
|
+
const { models, fallback } = failedDiscoveryFallback();
|
|
1562
|
+
console.warn(
|
|
1563
|
+
`[opencodex] Provider model discovery for "${name}" was blocked by destination policy: ${destinationError} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1564
|
+
);
|
|
1565
|
+
return models;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1362
1568
|
const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
|
|
1363
1569
|
if (!res.ok) {
|
|
1364
|
-
|
|
1365
|
-
const stale = getStaleCached(name);
|
|
1366
|
-
const fallback = stale ? "stale" : "configured";
|
|
1570
|
+
const { models, fallback } = failedDiscoveryFallback();
|
|
1367
1571
|
console.warn(
|
|
1368
1572
|
`[opencodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1369
1573
|
);
|
|
1370
|
-
return
|
|
1574
|
+
return models;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const contentType = (
|
|
1578
|
+
res.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() || "missing"
|
|
1579
|
+
).slice(0, 80);
|
|
1580
|
+
const body = await res.text();
|
|
1581
|
+
let json: unknown;
|
|
1582
|
+
try {
|
|
1583
|
+
json = JSON.parse(body) as unknown;
|
|
1584
|
+
} catch {
|
|
1585
|
+
const { models, fallback } = failedDiscoveryFallback();
|
|
1586
|
+
const diagnostic = contentType === "application/json" || contentType.endsWith("+json")
|
|
1587
|
+
? "returned invalid JSON in a 2xx response"
|
|
1588
|
+
: "returned a non-JSON 2xx response";
|
|
1589
|
+
console.warn(
|
|
1590
|
+
`[opencodex] Provider model discovery for "${name}" ${diagnostic} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1591
|
+
);
|
|
1592
|
+
return models;
|
|
1371
1593
|
}
|
|
1372
|
-
const json = await res.json() as unknown;
|
|
1373
1594
|
const data = json !== null && typeof json === "object" && !Array.isArray(json)
|
|
1374
1595
|
? (json as { data?: unknown }).data
|
|
1375
1596
|
: undefined;
|
|
1376
1597
|
if (!isProviderModelsApiItems(data)) {
|
|
1377
|
-
|
|
1598
|
+
const { models, fallback } = failedDiscoveryFallback();
|
|
1378
1599
|
console.warn(
|
|
1379
|
-
`[opencodex] Provider model discovery for "${name}" returned malformed 2xx data
|
|
1600
|
+
`[opencodex] Provider model discovery for "${name}" returned malformed 2xx data [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1380
1601
|
);
|
|
1381
|
-
|
|
1382
|
-
return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : configured;
|
|
1602
|
+
return models;
|
|
1383
1603
|
}
|
|
1384
1604
|
const items = data;
|
|
1385
1605
|
const live = items.map(m => applyProviderConfigHints(name, prov, {
|
|
@@ -1420,13 +1640,11 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
1420
1640
|
setCached(name, live);
|
|
1421
1641
|
return live;
|
|
1422
1642
|
} catch (error) {
|
|
1423
|
-
|
|
1424
|
-
const stale = getStaleCached(name);
|
|
1425
|
-
const fallback = stale ? "stale" : "configured";
|
|
1643
|
+
const { models, fallback } = failedDiscoveryFallback();
|
|
1426
1644
|
console.warn(
|
|
1427
1645
|
`[opencodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1428
1646
|
);
|
|
1429
|
-
return
|
|
1647
|
+
return models;
|
|
1430
1648
|
}
|
|
1431
1649
|
}
|
|
1432
1650
|
|
|
@@ -1461,6 +1679,9 @@ export function filterCatalogVisibleModels(
|
|
|
1461
1679
|
return models.filter(m => {
|
|
1462
1680
|
// disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
|
|
1463
1681
|
for (const stored of disabled) {
|
|
1682
|
+
// Combo management stores the public alias, while canonical `combo/<id>` references
|
|
1683
|
+
// remain valid for backward compatibility through slugEquals below.
|
|
1684
|
+
if (m.alias !== undefined && stored === catalogModelSlug(m)) return false;
|
|
1464
1685
|
if (slugEquals(stored, m.provider, m.id)) return false;
|
|
1465
1686
|
}
|
|
1466
1687
|
const allow = allowByProvider.get(m.provider);
|
|
@@ -1499,6 +1720,51 @@ export async function gatherRoutedModels(config: OcxConfig): Promise<CatalogMode
|
|
|
1499
1720
|
// exposure decision goes through shouldExposeRoutedModel (single choke point).
|
|
1500
1721
|
.filter(shouldExposeRoutedModel);
|
|
1501
1722
|
const memberByKey = new Map(all.map(model => [`${model.provider}/${model.id}`, model]));
|
|
1723
|
+
// [Decision Log]
|
|
1724
|
+
// - 목적과 의도: 콤보 타겟에 native OpenAI(Codex login) 모델이 포함될 때 카탈로그에서
|
|
1725
|
+
// 누락되는 버그(issue #268)를 수정. "openai" provider는 forward-auth(Codex login
|
|
1726
|
+
// passthrough)이므로 fetchProviderModels가 항상 []를 반환하고, native slugs는
|
|
1727
|
+
// 별도 정적 경로(nativeOpenAiSlugs)로만 노출됨. 따라서 memberByKey에
|
|
1728
|
+
// openai/<slug> 키가 존재하지 않아 콤보가 조용히 drop됨.
|
|
1729
|
+
// - 기존 구현 및 제약 조건: memberByKey는 routed provider /models fetch 결과로만 구성.
|
|
1730
|
+
// - 검토한 주요 대안: (A) native slugs를 all 배열에 직접 push — /v1/models와 온디스크
|
|
1731
|
+
// 카탈로그에서 native 모델이 중복 노출되는 부작용 발생. (B) memberByKey에만 synthetic
|
|
1732
|
+
// CatalogModel을 주입 — 콤보 멤버 해석에만 사용하고 all에는 추가하지 않으므로 기존
|
|
1733
|
+
// 노출 경로에 영향 없음.
|
|
1734
|
+
// - 선택한 방식: (B) — synthetic entries를 memberByKey에만 주입.
|
|
1735
|
+
// - 다른 대안 대신 이 방식을 선택한 이유: 기존 native 모델 노출 경로(/v1/models, 온디스크
|
|
1736
|
+
// 카탈로그 sync, management API)를 전혀 변경하지 않고 콤보 resolution만 수선하기 때문.
|
|
1737
|
+
// - 장점, 단점 및 영향: 장점 — 최소 수정, 기존 경로 무변경. 단점 — synthetic entries의
|
|
1738
|
+
// capability 데이터가 static/upstream snapshot 기반이므로, 사용자가 커스텀 config
|
|
1739
|
+
// 힌트(modelContextWindows 등)로 native 모델의 context window를 오버라이드한 경우
|
|
1740
|
+
// 반영되지 않음. 하지만 nativeOpenAiContextWindow가 이미 config 오버라이드를
|
|
1741
|
+
// 우선시하므로 실제 충돌 가능성은 낮음.
|
|
1742
|
+
if (!hasComboTargets(config)) {
|
|
1743
|
+
// Skip the native slug injection entirely when no combos are configured — avoids
|
|
1744
|
+
// calling nativeOpenAiSlugs() (which reads the live Codex catalog from disk) for
|
|
1745
|
+
// configs that will never need it.
|
|
1746
|
+
} else {
|
|
1747
|
+
const disabled = disabledNativeSlugs(config);
|
|
1748
|
+
for (const slug of nativeOpenAiSlugs()) {
|
|
1749
|
+
if (disabled.has(slug)) continue;
|
|
1750
|
+
const contextWindow = nativeOpenAiContextWindow(slug);
|
|
1751
|
+
if (contextWindow === undefined) continue;
|
|
1752
|
+
const synthetic: CatalogModel = {
|
|
1753
|
+
provider: "openai",
|
|
1754
|
+
id: slug,
|
|
1755
|
+
owned_by: "openai",
|
|
1756
|
+
contextWindow,
|
|
1757
|
+
maxInputTokens: contextWindow,
|
|
1758
|
+
inputModalities: nativeInputModalities(slug),
|
|
1759
|
+
reasoningEfforts: nativeReasoningEfforts(slug),
|
|
1760
|
+
...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
|
|
1761
|
+
};
|
|
1762
|
+
const key = `openai/${slug}`;
|
|
1763
|
+
// Only inject when not already present from a routed provider (an API-key
|
|
1764
|
+
// "openai" provider could shadow the native one).
|
|
1765
|
+
if (!memberByKey.has(key)) memberByKey.set(key, synthetic);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1502
1768
|
for (const id of listComboIds(config)) {
|
|
1503
1769
|
const combo = getCombo(config, id);
|
|
1504
1770
|
if (!combo) continue;
|
|
@@ -1585,6 +1851,7 @@ export function deriveComboCatalogModel(
|
|
|
1585
1851
|
maxInputTokens,
|
|
1586
1852
|
inputModalities,
|
|
1587
1853
|
reasoningEfforts,
|
|
1854
|
+
...(combo.alias ? { alias: combo.alias } : {}),
|
|
1588
1855
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
1589
1856
|
...(members.every(member => member.parallelToolCalls === true)
|
|
1590
1857
|
? { parallelToolCalls: true }
|
|
@@ -1636,8 +1903,18 @@ function warnUncataloguedComboOnce(
|
|
|
1636
1903
|
);
|
|
1637
1904
|
}
|
|
1638
1905
|
|
|
1639
|
-
export function exactComboCatalogSlugs(
|
|
1640
|
-
|
|
1906
|
+
export function exactComboCatalogSlugs(
|
|
1907
|
+
config: Pick<OcxConfig, "combos" | "disabledModels">,
|
|
1908
|
+
): Set<string> {
|
|
1909
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
1910
|
+
return new Set(listComboIds(config).flatMap(id => {
|
|
1911
|
+
const alias = typeof config.combos?.[id]?.alias === "string"
|
|
1912
|
+
? config.combos[id]!.alias!.trim()
|
|
1913
|
+
: "";
|
|
1914
|
+
const canonical = comboModelId(id);
|
|
1915
|
+
const publicSlug = alias || canonical;
|
|
1916
|
+
return disabled.has(publicSlug) || disabled.has(canonical) ? [] : [publicSlug];
|
|
1917
|
+
}));
|
|
1641
1918
|
}
|
|
1642
1919
|
|
|
1643
1920
|
function normalizedOpenAiApiSignature(model: CatalogModel): string {
|
|
@@ -1665,12 +1942,23 @@ export function resetOpenAiApiCatalogWarningStateForTests(): void {
|
|
|
1665
1942
|
* callable via its raw full-slash selector — and we warn once per provider+alias.
|
|
1666
1943
|
*/
|
|
1667
1944
|
const slugAliasCollisionWarnings = new Set<string>();
|
|
1945
|
+
const comboMasqueradeCollisionWarnings = new Set<string>();
|
|
1946
|
+
|
|
1947
|
+
function warnComboMasqueradeCollisionOnce(slug: string): void {
|
|
1948
|
+
if (comboMasqueradeCollisionWarnings.has(slug)) return;
|
|
1949
|
+
comboMasqueradeCollisionWarnings.add(slug);
|
|
1950
|
+
console.warn(
|
|
1951
|
+
`[opencodex] combo alias collision on "${safeCatalogWarningLabel(slug)}": the combo wins and the shadowed provider model is omitted from the catalog.`,
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1668
1954
|
|
|
1669
1955
|
function resolveSlugAliasCollisions(goModels: CatalogModel[]): Set<CatalogModel> {
|
|
1670
1956
|
const skipped = new Set<CatalogModel>();
|
|
1671
1957
|
const winnerByAlias = new Map<string, CatalogModel>();
|
|
1672
1958
|
for (const m of goModels) {
|
|
1673
|
-
|
|
1959
|
+
// Combo aliases have their own collision policy below: they always shadow provider rows.
|
|
1960
|
+
if (m.provider === COMBO_NAMESPACE) continue;
|
|
1961
|
+
const key = catalogModelSlug(m);
|
|
1674
1962
|
const winner = winnerByAlias.get(key);
|
|
1675
1963
|
if (!winner) {
|
|
1676
1964
|
winnerByAlias.set(key, m);
|
|
@@ -1695,6 +1983,53 @@ function resolveSlugAliasCollisions(goModels: CatalogModel[]): Set<CatalogModel>
|
|
|
1695
1983
|
return skipped;
|
|
1696
1984
|
}
|
|
1697
1985
|
|
|
1986
|
+
/** Apply Codex-facing encoded-slug collision policy to management and picker rows. */
|
|
1987
|
+
export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): CatalogModel[] {
|
|
1988
|
+
const collisionSkipped = resolveSlugAliasCollisions(goModels);
|
|
1989
|
+
const comboPublicSlugs = new Set(goModels
|
|
1990
|
+
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
1991
|
+
.map(catalogModelSlug));
|
|
1992
|
+
const seen = new Set<string>();
|
|
1993
|
+
const out: CatalogModel[] = [];
|
|
1994
|
+
for (const model of goModels) {
|
|
1995
|
+
if (collisionSkipped.has(model)) continue;
|
|
1996
|
+
const slug = catalogModelSlug(model);
|
|
1997
|
+
if (model.provider !== COMBO_NAMESPACE && comboPublicSlugs.has(slug)) {
|
|
1998
|
+
warnComboMasqueradeCollisionOnce(slug);
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
if (seen.has(slug)) continue;
|
|
2002
|
+
seen.add(slug);
|
|
2003
|
+
out.push(model);
|
|
2004
|
+
}
|
|
2005
|
+
return out;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Deduplicate the ordinary OpenAI `/v1/models` list by the exact id emitted by that
|
|
2010
|
+
* endpoint. Raw selectors that merely encode to the same Codex slug remain independently
|
|
2011
|
+
* callable and visible; an exact combo alias collision still resolves in favor of the combo.
|
|
2012
|
+
*/
|
|
2013
|
+
export function uniqueCatalogModelsForRawPublicList(goModels: CatalogModel[]): CatalogModel[] {
|
|
2014
|
+
const publicId = (model: CatalogModel): string => model.alias ?? `${model.provider}/${model.id}`;
|
|
2015
|
+
const comboPublicIds = new Set(goModels
|
|
2016
|
+
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
2017
|
+
.map(publicId));
|
|
2018
|
+
const seen = new Set<string>();
|
|
2019
|
+
const out: CatalogModel[] = [];
|
|
2020
|
+
for (const model of goModels) {
|
|
2021
|
+
const id = publicId(model);
|
|
2022
|
+
if (model.provider !== COMBO_NAMESPACE && comboPublicIds.has(id)) {
|
|
2023
|
+
warnComboMasqueradeCollisionOnce(id);
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
if (seen.has(id)) continue;
|
|
2027
|
+
seen.add(id);
|
|
2028
|
+
out.push(model);
|
|
2029
|
+
}
|
|
2030
|
+
return out;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
1698
2033
|
/** Test-only reset for every process-global catalog cache/warning owner. */
|
|
1699
2034
|
export function resetCatalogRuntimeStateForTests(): void {
|
|
1700
2035
|
bundledCatalogCache = null;
|
|
@@ -1702,6 +2037,7 @@ export function resetCatalogRuntimeStateForTests(): void {
|
|
|
1702
2037
|
openAiApiCollisionWarnings.clear();
|
|
1703
2038
|
comboCatalogWarningSignatures.clear();
|
|
1704
2039
|
slugAliasCollisionWarnings.clear();
|
|
2040
|
+
comboMasqueradeCollisionWarnings.clear();
|
|
1705
2041
|
clearModelCache();
|
|
1706
2042
|
}
|
|
1707
2043
|
|
|
@@ -1803,7 +2139,10 @@ export function orderForSubagents(goModels: CatalogModel[], featured?: string[])
|
|
|
1803
2139
|
const rank = new Map(featured.map((id, i) => [id, i]));
|
|
1804
2140
|
// Featured picks may be stored raw (legacy) or encoded — match both forms.
|
|
1805
2141
|
const rankOf = (m: CatalogModel) =>
|
|
1806
|
-
|
|
2142
|
+
(m.alias ? rank.get(m.alias) : undefined)
|
|
2143
|
+
?? rank.get(`${m.provider}/${m.id}`)
|
|
2144
|
+
?? rank.get(routedSlug(m.provider, m.id))
|
|
2145
|
+
?? Number.MAX_SAFE_INTEGER;
|
|
1807
2146
|
return [...goModels].sort((a, b) => {
|
|
1808
2147
|
return rankOf(a) - rankOf(b);
|
|
1809
2148
|
});
|
|
@@ -1831,6 +2170,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
1831
2170
|
const native = catalogModels
|
|
1832
2171
|
.filter(m => typeof m.slug === "string"
|
|
1833
2172
|
&& !(m.slug as string).includes("/")
|
|
2173
|
+
&& m.owned_by !== COMBO_NAMESPACE
|
|
1834
2174
|
&& !goIds.has(m.slug as string)
|
|
1835
2175
|
&& !isUnsupportedOpenAiNativeSlug(m.slug as string))
|
|
1836
2176
|
.map(m => {
|
|
@@ -1898,7 +2238,8 @@ export function mergeCatalogEntriesForSync(
|
|
|
1898
2238
|
if (!hasPhysicalComboProvider) {
|
|
1899
2239
|
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1900
2240
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1901
|
-
|
|
2241
|
+
const comboOwned = slug.startsWith(`${COMBO_NAMESPACE}/`) || entry.owned_by === COMBO_NAMESPACE;
|
|
2242
|
+
return !comboOwned || freshSlugs.has(slug);
|
|
1902
2243
|
});
|
|
1903
2244
|
}
|
|
1904
2245
|
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
@@ -1921,6 +2262,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
1921
2262
|
const exactCombo = typeof m.slug === "string" && exactComboSlugs.has(m.slug);
|
|
1922
2263
|
const e = ensureStrictCatalogFields(normalized, {
|
|
1923
2264
|
preserveExactInputModalities: exactCombo,
|
|
2265
|
+
isRouted: finalRoutedEntries.includes(m),
|
|
1924
2266
|
});
|
|
1925
2267
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
1926
2268
|
// the max rung — ensure it here so subagent max spawns validate on every
|
package/src/combos/index.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
export {
|
|
2
2
|
COMBO_NAMESPACE,
|
|
3
|
+
comboAliasIssues,
|
|
3
4
|
comboConfigError,
|
|
4
5
|
comboConfigIssues,
|
|
5
6
|
comboDefaultEffort,
|
|
6
7
|
comboModelId,
|
|
8
|
+
comboPublicModelId,
|
|
7
9
|
getCombo,
|
|
8
10
|
isValidComboId,
|
|
9
11
|
listComboIds,
|
|
10
12
|
normalizeComboConfig,
|
|
11
13
|
parseComboModelId,
|
|
14
|
+
resolveComboId,
|
|
12
15
|
targetKey,
|
|
13
16
|
} from "./types";
|
|
14
17
|
export {
|