@bitkyc08/opencodex 2.19.0 → 2.20.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/gui/dist/assets/index-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +13 -7
- package/src/cli/claude.ts +2 -1
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +224 -76
- package/src/images/loop.ts +11 -1
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +7 -3
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +74 -36
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +257 -46
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/types.ts +66 -4
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/gui/dist/assets/index-CQ7bIKee.css +0 -1
- package/gui/dist/assets/index-D_JUZLEC.js +0 -76
|
@@ -32,6 +32,11 @@ import { modelInList } from "../../types";
|
|
|
32
32
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
33
33
|
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
34
34
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
35
|
+
import {
|
|
36
|
+
captureServiceTierAdapterAuthority,
|
|
37
|
+
serviceTierSupportForModel,
|
|
38
|
+
type CapturedServiceTierAdapterAuthority,
|
|
39
|
+
} from "../../providers/service-tier";
|
|
35
40
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
36
41
|
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
|
|
37
42
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
@@ -150,6 +155,7 @@ interface CapturedProviderGather {
|
|
|
150
155
|
readonly discovery: ResolvedProviderModelDiscovery;
|
|
151
156
|
readonly policy: CatalogProviderDiscoveryPolicySnapshot;
|
|
152
157
|
readonly request: CapturedModelsRequest;
|
|
158
|
+
readonly serviceTierAdapterAuthority: CapturedServiceTierAdapterAuthority;
|
|
153
159
|
readonly observedAuth?: ModelsAuthResolution;
|
|
154
160
|
/**
|
|
155
161
|
* Configured model ids this provider must keep even when live discovery omits
|
|
@@ -401,6 +407,12 @@ function captureProviderGather(
|
|
|
401
407
|
): CapturedProviderGather {
|
|
402
408
|
const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
|
|
403
409
|
enrichProviderFromRegistry(name, enriched);
|
|
410
|
+
const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
|
|
411
|
+
const serviceTierAdapterAuthority = captureServiceTierAdapterAuthority(
|
|
412
|
+
name,
|
|
413
|
+
enriched,
|
|
414
|
+
registryTransportMatch,
|
|
415
|
+
);
|
|
404
416
|
const provider = recursivelyFreeze(enriched);
|
|
405
417
|
const observedAuth = authResolver.kind === "observed"
|
|
406
418
|
&& provider.authMode !== "forward"
|
|
@@ -414,7 +426,6 @@ function captureProviderGather(
|
|
|
414
426
|
maxResponseBytes: resolved.maxResponseBytes,
|
|
415
427
|
maxModels: resolved.maxModels,
|
|
416
428
|
});
|
|
417
|
-
const registryTransportMatch = providerMatchesRegistryTransport(name, provider);
|
|
418
429
|
const trustedOpenAiApi = captureTrustedOpenAiApiPolicy(name, registryTransportMatch);
|
|
419
430
|
const policy = detachedFrozen({
|
|
420
431
|
provider: name,
|
|
@@ -438,6 +449,7 @@ function captureProviderGather(
|
|
|
438
449
|
discovery,
|
|
439
450
|
policy,
|
|
440
451
|
request,
|
|
452
|
+
serviceTierAdapterAuthority,
|
|
441
453
|
...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
|
|
442
454
|
...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
|
|
443
455
|
? { retainConfiguredModelIds }
|
|
@@ -506,6 +518,7 @@ function captureGatherFlight(
|
|
|
506
518
|
// It is the one member of a provider row that is legitimately a function,
|
|
507
519
|
// so it is dropped here rather than allowed to break every encode.
|
|
508
520
|
provider: omitProviderTransportExecutor(provider.provider),
|
|
521
|
+
serviceTierAdapterAuthority: provider.serviceTierAdapterAuthority,
|
|
509
522
|
// Combo retention is capture-time state, not a provider-row field. Two
|
|
510
523
|
// gathers that share providers but differ in combo targets must not join.
|
|
511
524
|
retainConfiguredModelIds: [...(provider.retainConfiguredModelIds ?? [])].sort(),
|
|
@@ -561,6 +574,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
|
|
|
561
574
|
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
562
575
|
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
563
576
|
rsDel: prov.modelReasoningSummaryDelivery ?? null,
|
|
577
|
+
serviceTier: prov.modelSupportsServiceTier ?? null,
|
|
564
578
|
noVis: [...(prov.noVisionModels ?? [])].sort(),
|
|
565
579
|
ptc: prov.parallelToolCalls ?? null,
|
|
566
580
|
gMode: prov.googleMode ?? null,
|
|
@@ -632,8 +646,10 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
632
646
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
633
647
|
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
|
|
634
648
|
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
|
|
649
|
+
const supportsServiceTier = serviceTierSupportForModel(prov, model.id, name);
|
|
650
|
+
const { supportsServiceTier: _staleServiceTier, ...modelWithoutServiceTier } = model;
|
|
635
651
|
const hinted = {
|
|
636
|
-
...
|
|
652
|
+
...modelWithoutServiceTier,
|
|
637
653
|
...(configuredCap !== undefined
|
|
638
654
|
? {
|
|
639
655
|
contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
|
|
@@ -652,6 +668,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
652
668
|
: {}),
|
|
653
669
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
654
670
|
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
671
|
+
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
655
672
|
...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
|
|
656
673
|
// Default-on for openai-chat providers (explicit false opts out); other adapters
|
|
657
674
|
// advertise only on explicit opt-in.
|
|
@@ -1740,6 +1757,7 @@ async function gatherRoutedModelsUncached(
|
|
|
1740
1757
|
const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
|
|
1741
1758
|
const customModels = (config.customModels ?? []).map(cm => {
|
|
1742
1759
|
const rawProvider = config.providers[cm.provider];
|
|
1760
|
+
const effectiveProvider = enrichedByName.get(cm.provider) ?? rawProvider;
|
|
1743
1761
|
// Registry routing backfills an omitted authMode on the built-in OpenAI provider to
|
|
1744
1762
|
// forward. Keep the catalog projection on the same contract while still failing closed
|
|
1745
1763
|
// for every explicit non-forward mode and every non-canonical endpoint.
|
|
@@ -1762,6 +1780,9 @@ async function gatherRoutedModelsUncached(
|
|
|
1762
1780
|
? nativeDefaultReasoningEffort(cm.modelId)
|
|
1763
1781
|
: undefined;
|
|
1764
1782
|
const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
|
|
1783
|
+
const supportsServiceTier = effectiveProvider
|
|
1784
|
+
? serviceTierSupportForModel(effectiveProvider, cm.modelId, cm.provider)
|
|
1785
|
+
: undefined;
|
|
1765
1786
|
const base: CatalogModel = {
|
|
1766
1787
|
id: cm.modelId,
|
|
1767
1788
|
provider: cm.provider,
|
|
@@ -1775,14 +1796,27 @@ async function gatherRoutedModelsUncached(
|
|
|
1775
1796
|
? { inputModalities: cm.inputModalities }
|
|
1776
1797
|
: codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
|
|
1777
1798
|
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
1799
|
+
// Native-alias defaults apply only where the custom row declares nothing: the explicit
|
|
1800
|
+
// spreads below must win (later in object order), so a stored `[]` stays empty and a
|
|
1801
|
+
// declared ladder is never replaced by the alias's native ladder.
|
|
1778
1802
|
...(codexForwardNativeCapabilityAlias
|
|
1779
1803
|
? {
|
|
1780
1804
|
codexForwardNativeCapabilityAlias: true,
|
|
1781
|
-
reasoningEfforts: nativeReasoningEfforts(cm.modelId),
|
|
1782
1805
|
parallelToolCalls: nativeParallelToolCalls(cm.modelId),
|
|
1783
|
-
...(
|
|
1806
|
+
...(Array.isArray(cm.reasoningEfforts)
|
|
1807
|
+
? {}
|
|
1808
|
+
: {
|
|
1809
|
+
reasoningEfforts: nativeReasoningEfforts(cm.modelId),
|
|
1810
|
+
...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}),
|
|
1811
|
+
}),
|
|
1784
1812
|
}
|
|
1785
1813
|
: {}),
|
|
1814
|
+
// Explicit custom-row ladder wins over the inherited provider row below: the merge only
|
|
1815
|
+
// gap-fills, so a stored `[]` (explicit "no reasoning") or a declared ladder is kept
|
|
1816
|
+
// verbatim instead of being replaced by the replaced row's metadata.
|
|
1817
|
+
...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
|
|
1818
|
+
...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
|
|
1819
|
+
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
1786
1820
|
};
|
|
1787
1821
|
// #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that
|
|
1788
1822
|
// row's provider capability metadata (reasoning ladder, default effort, parallel tool calls,
|
|
@@ -1791,13 +1825,19 @@ async function gatherRoutedModelsUncached(
|
|
|
1791
1825
|
// noReasoningModels model loses its empty ladder and the catalog synthesizes the generic one,
|
|
1792
1826
|
// which Codex then rejects for spawn_agent with effort "none".
|
|
1793
1827
|
const replaced = replacedByRoutedSlug.get(routedSlug(cm.provider, cm.modelId));
|
|
1828
|
+
// The final ladder is what the catalog will advertise; the inherited default only rides
|
|
1829
|
+
// along when it is actually a member — otherwise a provider default like "xhigh" would
|
|
1830
|
+
// re-apply onto a narrower custom ladder and override the fallback in applyReasoningLevels.
|
|
1831
|
+
const effectiveLadder = base.reasoningEfforts ?? replaced?.reasoningEfforts;
|
|
1794
1832
|
const merged: CatalogModel = replaced ? {
|
|
1795
1833
|
...base,
|
|
1796
1834
|
...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
|
|
1797
1835
|
...(base.maxInputTokens === undefined && replaced.maxInputTokens !== undefined ? { maxInputTokens: replaced.maxInputTokens } : {}),
|
|
1798
1836
|
...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
|
|
1799
1837
|
...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
|
|
1800
|
-
...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined
|
|
1838
|
+
...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined
|
|
1839
|
+
&& Array.isArray(effectiveLadder) && effectiveLadder.includes(replaced.defaultReasoningEffort)
|
|
1840
|
+
? { defaultReasoningEffort: replaced.defaultReasoningEffort } : {}),
|
|
1801
1841
|
...(base.parallelToolCalls === undefined && replaced.parallelToolCalls !== undefined ? { parallelToolCalls: replaced.parallelToolCalls } : {}),
|
|
1802
1842
|
...(base.supportsVerbosity === undefined && replaced.supportsVerbosity !== undefined ? { supportsVerbosity: replaced.supportsVerbosity } : {}),
|
|
1803
1843
|
...(base.supportsReasoningSummaries === undefined && replaced.supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: replaced.supportsReasoningSummaries } : {}),
|
|
@@ -67,6 +67,18 @@ import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, truste
|
|
|
67
67
|
|
|
68
68
|
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
69
69
|
|
|
70
|
+
// Base for config.modelPickerOrder display priorities (#1649). modelPickerOrder is a DISPLAY-ONLY
|
|
71
|
+
// reordering of the Codex model picker: it rewrites a row's Codex-visible `priority` but never the
|
|
72
|
+
// spawn_agent candidate window. The window is derived from SPAWN_PRIORITY_FIELD (the natural
|
|
73
|
+
// priority captured before the override), so display order and spawn candidates are decoupled.
|
|
74
|
+
export const PICKER_ORDER_PRIORITY_BASE = 1_000;
|
|
75
|
+
|
|
76
|
+
// OpenCodex-private catalog field: the spawn_agent candidate priority a row would have WITHOUT
|
|
77
|
+
// modelPickerOrder. Codex ignores unknown catalog fields (same as opencodex_catalog_kind), so this
|
|
78
|
+
// is invisible to Codex; effectiveSubagentRoster reads it so a display reorder cannot change which
|
|
79
|
+
// rows are spawn_agent candidates. Absent on rows modelPickerOrder did not move.
|
|
80
|
+
export const SPAWN_PRIORITY_FIELD = "opencodex_spawn_priority";
|
|
81
|
+
|
|
70
82
|
export type SpawnAgentSurface = "v1" | "v2";
|
|
71
83
|
|
|
72
84
|
export type SubagentRosterExclusionReason =
|
|
@@ -144,10 +156,17 @@ export function effectiveSubagentRoster(
|
|
|
144
156
|
.filter(({ entry }) => entry.visibility === "list")
|
|
145
157
|
.filter(({ entry }) => surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
146
158
|
.sort((left, right) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
// Spawn candidates rank by the natural priority (SPAWN_PRIORITY_FIELD when present), so a
|
|
160
|
+
// modelPickerOrder display reorder (#1649) can never change candidate membership. Rows the
|
|
161
|
+
// override did not move fall back to their Codex-visible `priority`.
|
|
162
|
+
const spawnPriorityOf = (entry: RawEntry): number => {
|
|
163
|
+
const spawn = entry[SPAWN_PRIORITY_FIELD];
|
|
164
|
+
if (typeof spawn === "number" && Number.isFinite(spawn)) return spawn;
|
|
165
|
+
return typeof entry.priority === "number" && Number.isFinite(entry.priority)
|
|
166
|
+
? entry.priority : Number.MAX_SAFE_INTEGER;
|
|
167
|
+
};
|
|
168
|
+
const leftPriority = spawnPriorityOf(left.entry);
|
|
169
|
+
const rightPriority = spawnPriorityOf(right.entry);
|
|
151
170
|
return leftPriority - rightPriority || left.index - right.index;
|
|
152
171
|
})
|
|
153
172
|
.slice(0, MAX_SPAWN_AGENT_MODEL_OVERRIDES);
|
|
@@ -362,6 +381,8 @@ export interface ObservedCatalogEntryBuildInput {
|
|
|
362
381
|
readonly gptSlugs: readonly string[];
|
|
363
382
|
readonly goModels: readonly CatalogModel[];
|
|
364
383
|
readonly featured?: readonly string[];
|
|
384
|
+
/** Optional full picker ordering (config.modelPickerOrder); orders non-featured rows. */
|
|
385
|
+
readonly modelPickerOrder?: readonly string[];
|
|
365
386
|
readonly wsEnabled: boolean;
|
|
366
387
|
readonly multiAgentMode: MultiAgentMode;
|
|
367
388
|
readonly exactComboSlugs: ReadonlySet<string>;
|
|
@@ -416,6 +437,7 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
416
437
|
gptSlugs,
|
|
417
438
|
goModels,
|
|
418
439
|
featured,
|
|
440
|
+
modelPickerOrder,
|
|
419
441
|
wsEnabled,
|
|
420
442
|
multiAgentMode,
|
|
421
443
|
exactComboSlugs,
|
|
@@ -433,6 +455,39 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
433
455
|
// it sorts to the front. This works for native gpt slugs AND routed slugs alike.
|
|
434
456
|
const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
|
|
435
457
|
const priorityStride = Math.max(accountSelectors.length, 1);
|
|
458
|
+
// Optional full picker order (#1649). Independent of the 5-slot spawn_agent cap: it only
|
|
459
|
+
// rewrites the Codex-visible display `priority` of listed non-featured routed rows so a >5
|
|
460
|
+
// catalog stays put across rebuilds. Featured rows keep their existing 0..N-1 band; when
|
|
461
|
+
// modelPickerOrder is unset the helper is a no-op and every priority below is byte-identical to
|
|
462
|
+
// before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so
|
|
463
|
+
// this display reorder cannot change which rows are spawn candidates.
|
|
464
|
+
const pickerOrder = Array.isArray(modelPickerOrder)
|
|
465
|
+
? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.length > 0)
|
|
466
|
+
: [];
|
|
467
|
+
const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const));
|
|
468
|
+
const pickerOrderActive = pickerOrder.length > 0;
|
|
469
|
+
// The display band reuses the existing high priority tier (>= PICKER_ORDER_PRIORITY_BASE, the
|
|
470
|
+
// same 1_000+ neighborhood account rows occupy), keeping listed rows visually after the featured
|
|
471
|
+
// band. Candidate membership does not depend on this — see SPAWN_PRIORITY_FIELD.
|
|
472
|
+
/**
|
|
473
|
+
* Priority for a non-featured routed row that is explicitly LISTED in modelPickerOrder. Listed
|
|
474
|
+
* slugs sort in declared order within the high picker-order display tier
|
|
475
|
+
* (>= PICKER_ORDER_PRIORITY_BASE). This sets the Codex-visible `priority` only; the caller records
|
|
476
|
+
* the row's natural priority in SPAWN_PRIORITY_FIELD so the spawn_agent candidate window is
|
|
477
|
+
* unchanged. Returns undefined when the feature is off or the row is not listed, so those rows
|
|
478
|
+
* keep their original assignment (default 5 / account 1_000+) untouched.
|
|
479
|
+
*
|
|
480
|
+
* Scope: only the generic routed `<provider>/<model>` rows call this (see the goModels loop
|
|
481
|
+
* below). Native passthrough rows and account-qualified native rows keep their own priority
|
|
482
|
+
* logic and are intentionally not reordered here — this matches the documented contract on
|
|
483
|
+
* OcxConfig.modelPickerOrder (route native ordering through subagentModels instead).
|
|
484
|
+
*/
|
|
485
|
+
const pickerOrderPriority = (slug: string, altSlug?: string): number | undefined => {
|
|
486
|
+
if (!pickerOrderActive) return undefined;
|
|
487
|
+
const hit = pickerOrderRank.get(slug) ?? (altSlug !== undefined ? pickerOrderRank.get(altSlug) : undefined);
|
|
488
|
+
if (hit === undefined) return undefined;
|
|
489
|
+
return PICKER_ORDER_PRIORITY_BASE + hit * priorityStride;
|
|
490
|
+
};
|
|
436
491
|
const out: RawEntry[] = [];
|
|
437
492
|
const nativeEntries: RawEntry[] = [];
|
|
438
493
|
const collisionSkipped = resolveSlugAliasCollisions([...goModels]);
|
|
@@ -537,11 +592,24 @@ export function buildCatalogEntriesFromObservedState({
|
|
|
537
592
|
}
|
|
538
593
|
// Featured picks may be stored raw (legacy) or encoded — honor both.
|
|
539
594
|
const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);
|
|
595
|
+
// Natural priority: what the row would get WITHOUT modelPickerOrder. This is the value the
|
|
596
|
+
// spawn_agent candidate window is derived from (see effectiveSubagentRoster), so it must never
|
|
597
|
+
// move when modelPickerOrder reorders the picker.
|
|
540
598
|
if (rankHit !== undefined) e.priority = rankHit * priorityStride;
|
|
541
599
|
else if (accountSelectors.length > 0) {
|
|
542
600
|
// Keep the generated account rows together in Codex's priority-sorted flat picker.
|
|
543
601
|
e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
|
|
544
602
|
}
|
|
603
|
+
// #1649: modelPickerOrder is a DISPLAY-ONLY override. Record the natural priority spawn_agent
|
|
604
|
+
// must keep using, then let modelPickerOrder move only the Codex-visible `priority`. Featured
|
|
605
|
+
// rows are never overridden (their rank is authoritative for both display and spawn).
|
|
606
|
+
if (rankHit === undefined) {
|
|
607
|
+
const pickerPriority = pickerOrderPriority(slug, `${m.provider}/${m.id}`);
|
|
608
|
+
if (pickerPriority !== undefined) {
|
|
609
|
+
e[SPAWN_PRIORITY_FIELD] = typeof e.priority === "number" ? e.priority : 5;
|
|
610
|
+
e.priority = pickerPriority;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
545
613
|
out.push(e);
|
|
546
614
|
}
|
|
547
615
|
// Central capability override (phase 120.4): the advertised flag must match the implemented WS
|
|
@@ -1324,6 +1392,7 @@ function writeRetainedCatalogSync({
|
|
|
1324
1392
|
const enabledGo = filterCatalogVisibleModels(goModels, config);
|
|
1325
1393
|
const featured = config.subagentModels ?? [];
|
|
1326
1394
|
const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
|
|
1395
|
+
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
1327
1396
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
1328
1397
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
1329
1398
|
const suppressedBareNativeSlugs = desktopAllowlistSuppressedNativeSlugs(config);
|
|
@@ -1355,6 +1424,7 @@ function writeRetainedCatalogSync({
|
|
|
1355
1424
|
gptSlugs: [],
|
|
1356
1425
|
goModels: orderedGoModels,
|
|
1357
1426
|
featured,
|
|
1427
|
+
modelPickerOrder,
|
|
1358
1428
|
wsEnabled,
|
|
1359
1429
|
multiAgentMode,
|
|
1360
1430
|
exactComboSlugs,
|
package/src/codex/convergence.ts
CHANGED
|
@@ -225,6 +225,7 @@ function prepareCatalog(
|
|
|
225
225
|
const enabled = filterCatalogVisibleModels(routedModels, config);
|
|
226
226
|
const featured = config.subagentModels ?? [];
|
|
227
227
|
const ordered = orderForSubagents(enabled, featured);
|
|
228
|
+
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
228
229
|
const multiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2"
|
|
229
230
|
? config.multiAgentMode : "default";
|
|
230
231
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
@@ -255,6 +256,7 @@ function prepareCatalog(
|
|
|
255
256
|
gptSlugs: [],
|
|
256
257
|
goModels: ordered,
|
|
257
258
|
featured,
|
|
259
|
+
modelPickerOrder,
|
|
258
260
|
wsEnabled: websocketsEnabled(config),
|
|
259
261
|
multiAgentMode,
|
|
260
262
|
exactComboSlugs,
|
package/src/combos/index.ts
CHANGED
package/src/combos/request.ts
CHANGED
|
@@ -23,6 +23,36 @@ export function comboIdFromRawBody(body: unknown, config: OcxConfig): string | n
|
|
|
23
23
|
return resolveComboId(config, model);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Detect image-bearing Responses *input* only.
|
|
28
|
+
*
|
|
29
|
+
* Must not walk the full request body: tool JSON schemas, metadata, or extension
|
|
30
|
+
* payloads can legally contain `{ "type": "input_image" }` without any image
|
|
31
|
+
* being dispatched. After previous_response_id expansion, scan the materialised
|
|
32
|
+
* `input` tree (message content and function_call_output.output).
|
|
33
|
+
*/
|
|
34
|
+
export function comboRequestHasImageInput(body: unknown): boolean {
|
|
35
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
36
|
+
return responsesInputHasImage((body as { input?: unknown }).input);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function responsesInputHasImage(input: unknown): boolean {
|
|
40
|
+
if (typeof input === "string" || input == null) return false;
|
|
41
|
+
if (!Array.isArray(input)) return false;
|
|
42
|
+
return input.some(responsesInputNodeHasImage);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function responsesInputNodeHasImage(value: unknown): boolean {
|
|
46
|
+
if (!value || typeof value !== "object") return false;
|
|
47
|
+
if (Array.isArray(value)) return value.some(responsesInputNodeHasImage);
|
|
48
|
+
const record = value as Record<string, unknown>;
|
|
49
|
+
if (record.type === "input_image") return true;
|
|
50
|
+
// Message content parts and nested function_call_output content/output arrays.
|
|
51
|
+
if (record.content !== undefined && responsesInputNodeHasImage(record.content)) return true;
|
|
52
|
+
if (record.output !== undefined && responsesInputNodeHasImage(record.output)) return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
export function concreteComboRequestBody(
|
|
27
57
|
body: unknown,
|
|
28
58
|
target: Pick<OcxComboTarget, "provider" | "model">,
|
package/src/combos/types.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface NormalizedComboConfig {
|
|
|
37
37
|
strategy: OcxComboStrategy;
|
|
38
38
|
stickyLimit: number;
|
|
39
39
|
defaultEffort: OcxComboDefaultEffort | null;
|
|
40
|
+
/** Disable image input; `auto` preserves the intersection derived from all targets. */
|
|
41
|
+
imageInput: "auto" | "disabled";
|
|
40
42
|
/** Trimmed public alias, or null when the combo keeps the default `combo/<id>` slug. */
|
|
41
43
|
alias: string | null;
|
|
42
44
|
/** Explicit native-family alias opt-in. */
|
|
@@ -220,6 +222,9 @@ export function comboConfigIssues(
|
|
|
220
222
|
message: "defaultEffort must be one of: low, medium, high, xhigh, max, ultra",
|
|
221
223
|
});
|
|
222
224
|
}
|
|
225
|
+
if (body.imageInput !== undefined && body.imageInput !== "auto" && body.imageInput !== "disabled") {
|
|
226
|
+
issues.push({ path: ["imageInput"], message: 'imageInput must be "auto" or "disabled"' });
|
|
227
|
+
}
|
|
223
228
|
|
|
224
229
|
if (body.alias !== undefined) {
|
|
225
230
|
if (typeof body.alias !== "string") {
|
|
@@ -339,6 +344,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
339
344
|
strategy: raw.strategy ?? "failover",
|
|
340
345
|
stickyLimit: raw.stickyLimit ?? 1,
|
|
341
346
|
defaultEffort: raw.defaultEffort ?? null,
|
|
347
|
+
imageInput: raw.imageInput === "disabled" ? "disabled" : "auto",
|
|
342
348
|
alias: alias || null,
|
|
343
349
|
nativeAlias: raw.nativeAlias === true,
|
|
344
350
|
displayName: displayName || null,
|
package/src/config.ts
CHANGED
|
@@ -688,6 +688,33 @@ const retryOn429PolicySchema = z.object({
|
|
|
688
688
|
respectRetryAfter: z.boolean().optional(),
|
|
689
689
|
}).strict();
|
|
690
690
|
|
|
691
|
+
const requestPacingRuleSchema = z.object({
|
|
692
|
+
// Keep the RPM-derived timer within the same one-hour bound as minIntervalMs.
|
|
693
|
+
requestsPerMinute: z.number().min(1 / 60).max(60_000).optional(),
|
|
694
|
+
minIntervalMs: z.number().int().min(1).max(3_600_000).optional(),
|
|
695
|
+
}).strict().refine(value => value.requestsPerMinute !== undefined || value.minIntervalMs !== undefined, {
|
|
696
|
+
message: "request pacing rules need requestsPerMinute or minIntervalMs",
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const requestPacingSchema = z.object({
|
|
700
|
+
enabled: z.boolean(),
|
|
701
|
+
requestsPerMinute: z.number().min(1 / 60).max(60_000).optional(),
|
|
702
|
+
minIntervalMs: z.number().int().min(1).max(3_600_000).optional(),
|
|
703
|
+
models: z.record(z.string().trim().min(1), requestPacingRuleSchema).optional(),
|
|
704
|
+
}).strict().refine(value => value.enabled === false
|
|
705
|
+
|| value.requestsPerMinute !== undefined
|
|
706
|
+
|| value.minIntervalMs !== undefined
|
|
707
|
+
|| (value.models !== undefined && Object.keys(value.models).length > 0), {
|
|
708
|
+
message: "enabled request pacing needs a provider rule or model override",
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
export function requestPacingConfigError(value: unknown): string | null {
|
|
712
|
+
if (value === undefined) return null;
|
|
713
|
+
const parsed = requestPacingSchema.safeParse(value);
|
|
714
|
+
if (parsed.success) return null;
|
|
715
|
+
return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs provider rule or model overrides";
|
|
716
|
+
}
|
|
717
|
+
|
|
691
718
|
/**
|
|
692
719
|
* Zod schema for one provider entry: known fields are validated strictly while unknown
|
|
693
720
|
* fields pass through (preserved for runtime extensions).
|
|
@@ -695,6 +722,7 @@ const retryOn429PolicySchema = z.object({
|
|
|
695
722
|
const providerConfigSchema = z.object({
|
|
696
723
|
adapter: z.string().min(1),
|
|
697
724
|
baseUrl: z.string().min(1),
|
|
725
|
+
requestPacing: requestPacingSchema.optional().catch(undefined),
|
|
698
726
|
mcpMaxTools: z.number().int().positive().optional(),
|
|
699
727
|
mcpMaxSchemaBytes: z.number().int().positive().optional(),
|
|
700
728
|
mcpMaxResultBytes: z.number().int().positive().optional(),
|
|
@@ -703,6 +731,7 @@ const providerConfigSchema = z.object({
|
|
|
703
731
|
statelessResponses: z.boolean().optional(),
|
|
704
732
|
requiresAdjacentResponsesToolResults: z.boolean().optional(),
|
|
705
733
|
supportsServiceTier: z.boolean().optional(),
|
|
734
|
+
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
|
|
706
735
|
preserveResponsesReasoningContent: z.boolean().optional(),
|
|
707
736
|
allowPrivateNetwork: z.boolean().optional(),
|
|
708
737
|
noStructuredOutputModels: z.array(z.string().min(1))
|
|
@@ -1244,6 +1273,8 @@ const configSchema = z.object({
|
|
|
1244
1273
|
]).optional().catch(undefined),
|
|
1245
1274
|
providers: z.record(z.string(), providerConfigSchema),
|
|
1246
1275
|
defaultProvider: z.string().min(1).default("openai"),
|
|
1276
|
+
// A retry can be billable, so absence and malformed hand edits both stay off.
|
|
1277
|
+
emptyCompletionRetry: z.boolean().optional().catch(false),
|
|
1247
1278
|
openaiProviderTierVersion: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
1248
1279
|
// Invalid hand edits must not discard an otherwise usable config.
|
|
1249
1280
|
googleAntigravityStaticCatalogVersion: z.union([z.literal(1), z.literal(2)]).optional().catch(undefined),
|
|
@@ -1483,6 +1514,17 @@ const configSchema = z.object({
|
|
|
1483
1514
|
message: reasoningSummariesError,
|
|
1484
1515
|
});
|
|
1485
1516
|
}
|
|
1517
|
+
const serviceTierModelsError = booleanRecordConfigError(
|
|
1518
|
+
(provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
|
|
1519
|
+
"modelSupportsServiceTier",
|
|
1520
|
+
);
|
|
1521
|
+
if (serviceTierModelsError) {
|
|
1522
|
+
ctx.addIssue({
|
|
1523
|
+
code: "custom",
|
|
1524
|
+
path: ["providers", redactSecretString(name), "modelSupportsServiceTier"],
|
|
1525
|
+
message: serviceTierModelsError,
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1486
1528
|
const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
|
|
1487
1529
|
(provider as { modelReasoningSummaryDelivery?: unknown }).modelReasoningSummaryDelivery,
|
|
1488
1530
|
(provider as { modelSupportsReasoningSummaries?: unknown }).modelSupportsReasoningSummaries,
|
|
@@ -2331,6 +2373,14 @@ function codexAccountPickerEnabledError(value: unknown): string | null {
|
|
|
2331
2373
|
return "schema_invalid: codexAccountPickerEnabled: must be a boolean or omitted";
|
|
2332
2374
|
}
|
|
2333
2375
|
|
|
2376
|
+
function emptyCompletionRetryError(value: unknown): string | null {
|
|
2377
|
+
const raw = rawConfigRecord(value);
|
|
2378
|
+
if (!raw || !Object.hasOwn(raw, "emptyCompletionRetry")) return null;
|
|
2379
|
+
const enabled = raw.emptyCompletionRetry;
|
|
2380
|
+
if (enabled === undefined || typeof enabled === "boolean") return null;
|
|
2381
|
+
return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted";
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2334
2384
|
/** Validate an in-memory config candidate without touching disk. Used by headless CLI import/set. */
|
|
2335
2385
|
/**
|
|
2336
2386
|
* Reject a loopback-listener port that collides with the proxy port (#1102).
|
|
@@ -2379,6 +2429,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
2379
2429
|
?? googleAntigravityStaticCatalogVersionError(value)
|
|
2380
2430
|
?? codexAccountPrioritiesError(value)
|
|
2381
2431
|
?? codexAccountPickerEnabledError(value)
|
|
2432
|
+
?? emptyCompletionRetryError(value)
|
|
2382
2433
|
?? loopbackListenerPortError(value);
|
|
2383
2434
|
if (boundaryError) return { ok: false, error: boundaryError };
|
|
2384
2435
|
const result = configSchema.safeParse(value);
|
|
@@ -3229,6 +3280,7 @@ export function getDefaultConfig(): OcxConfig {
|
|
|
3229
3280
|
// Adding extra providers (e.g. opencode-go) and switching defaultProvider is a user/runtime choice.
|
|
3230
3281
|
return {
|
|
3231
3282
|
port: 10100,
|
|
3283
|
+
emptyCompletionRetry: false,
|
|
3232
3284
|
managementUsageMaxReadBytes: 64 * 1024 * 1024,
|
|
3233
3285
|
appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024),
|
|
3234
3286
|
// Fresh/re-initialized configs are already written in the current three-tier
|