Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions docs-site/src/content/docs/fr/guides/model-ordering.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ priorités `i * N + j`, où `j` est la position du sélecteur en base zéro ; un
sont déplacées hors de ces groupes de sélecteurs. Codex continue de n’annoncer que les cinq premières
lignes visibles dans le sélecteur.

Les priorités sans sélecteur pertinentes sont :
Sans ordre global du sélecteur, les priorités sans sélecteur pertinentes sont :

| Entrée du catalogue | Priorité | Source |
| --- | --- : | --- |
Expand Down Expand Up @@ -134,8 +134,31 @@ au-delà de ce bloc mis en avant :
Les lignes routées indiquées apparaissent dans l’ordre configuré. Une ligne absente du tableau conserve sa
priorité normale et reste donc devant la bande d’affichage de `modelPickerOrder` ; indiquez toutes les
lignes routées dont vous souhaitez contrôler l’ordre relatif. Une ligne également présente dans
`subagentModels` conserve sa priorité de mise en avant. `modelPickerOrder` ne réorganise ni les lignes
natives non qualifiées ni celles qualifiées par un compte ; utilisez `subagentModels` pour celles-ci.
`subagentModels` conserve sa priorité de mise en avant. Une liste contenant uniquement des identifiants
routés conserve la position normale des lignes natives.

Pour ordonner tout le sélecteur, incluez un identifiant natif non qualifié :

```json
{
"modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"]
}
```

Les lignes indiquées apparaissent d’abord dans l’ordre du tableau, puis les lignes absentes
selon leur priorité naturelle. La correspondance est exacte : `gpt-5.6-sol` et
`openai/gpt-5.6-sol` désignent deux lignes distinctes. Pour une ligne qualifiée par un compte,
indiquez son identifiant complet, sélecteur inclus. Les formes brute et encodée du même
identifiant routé sont acceptées, avec priorité aux correspondances exactes. Les entrées
vides sont ignorées.

### Migration : identifiants natifs dans les listes existantes

Auparavant, les identifiants natifs dans `modelPickerOrder` étaient ignorés. Une liste
existante contenant un identifiant natif non qualifié ordonne désormais tout le sélecteur,
y compris les lignes mises en avant. Supprimez ces identifiants pour conserver l’ancien
comportement limité aux lignes routées. Les listes absentes, vides ou uniquement routées
conservent leur comportement ; les priorités des candidats sous-agents ne changent pas.

`modelPickerOrder` ne modifie jamais l’ensemble des candidats de `spawn_agent`. Il change uniquement la
priorité visible par Codex dans le sélecteur, tandis qu’OpenCodex conserve la priorité naturelle de chaque
Expand Down
26 changes: 23 additions & 3 deletions docs-site/src/content/docs/guides/model-ordering.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ priorities `i * N + j`, where `j` is the selector's zero-based position; a route
rows are moved outside those selector groups. Codex still advertises only the first five
picker-visible rows.

The relevant no-selector priorities are:
Without complete-picker ordering, the relevant no-selector priorities are:

| Catalog entry | Priority | Source |
| --- | ---: | --- |
Expand Down Expand Up @@ -133,8 +133,28 @@ featured block:
Listed routed rows appear in the configured order. A routed row omitted from the array keeps its
normal priority, so it remains ahead of the `modelPickerOrder` display band; list every routed row
whose relative position you want to control. A row also present in `subagentModels` keeps its
featured priority. Bare native and account-qualified native rows are not reordered by
`modelPickerOrder`; use `subagentModels` for those rows.
featured priority. With a routed-only list, native rows keep their normal positions.

To order the complete picker, include a bare native id:

```json
{
"modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"]
}
```

Listed rows appear first in array order, followed by unlisted rows in natural priority
order. Matching uses exact catalog ids: `gpt-5.6-sol` and `openai/gpt-5.6-sol` are separate
rows. Raw and encoded spellings of the same routed id are also accepted, with exact
matches taking precedence. Empty entries are ignored. Account-qualified rows need
their selector-qualified id in the list.

### Migration note: native ids in existing orders

Previously, native ids in `modelPickerOrder` were ignored. An existing list containing
a bare native id now activates complete-picker ordering, including featured rows.
Remove bare native ids to keep the previous routed-only behavior. Unset, empty and
routed-only lists retain their behavior; subagent candidate priorities are unchanged.

`modelPickerOrder` never changes the `spawn_agent` candidate set. It changes only the
Codex-visible picker priority while opencodex retains each moved row's natural priority for
Expand Down
17 changes: 17 additions & 0 deletions docs-site/src/content/docs/reference/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -840,3 +840,20 @@ or expiry does not extend the history-recovery contract.
Sender and recipient on Go Responses are context for the receiving model, not a new
machine-readable routing protocol. Tool routing continues to use the existing collaboration
contracts.

## OpenCode Go reasoning efforts

Go catalog rows preserve their configured reasoning efforts exactly, including during
catalog sync. OpenCodex does not append synthetic `max` or `ultra` choices to these rows.
Use `modelReasoningEfforts` and `modelDefaultReasoningEfforts` for each model's accepted
upstream values. Key these per-provider maps by upstream model ID, not the routed
`opencode-go/<model-id>` catalog slug. For example, Omen Alpha (`omen-alpha`) accepts `low`, `high`,
and `max`; Muse Spark 1.3 Contributor (`muse-spark-1.3-contributor`) accepts `minimal`, `low`, `medium`, `high`, and `xhigh` (Go endpoint validation, 2026-09-05).
See the [OpenCode Go model list](https://opencode.ai/docs/go/#models) for the current roster.
A configured subset can exclude the lower tiers. Other providers retain their existing behavior.

For a native-first picker, include native ids in `modelPickerOrder` followed by the
routed ids. This orders the complete picker while preserving the separate subagent
candidate priorities. Routed-only orders keep their previous behavior. See the
[ordering migration note](/guides/model-ordering/#migration-note-native-ids-in-existing-orders).
`modelDisplayNames` on a provider controls readable labels without changing wire ids.
2 changes: 2 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@
"bun-stream-caps.test.ts": "lib",
"cancel-body-on-abort.test.ts": "server",
"catalog-cursor-search.test.ts": "codex-integration",
"catalog-full-picker-order.test.ts": "codex-integration",
"catalog-go-exact-efforts.test.ts": "codex-integration",
"catalog-input-modality-enum.test.ts": "codex-integration",
"catalog-llamacpp-capabilities.test.ts": "codex-integration",
"catalog-oauth-observation.test.ts": "codex-integration",
Expand Down
66 changes: 61 additions & 5 deletions src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ export function deriveEntry(
contextCap?: NativeContextLimitsInput,
): RawEntry {
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
// Go exposes model-specific upstream enums; synthetic tiers mislead subagent overrides.
const preserveExactReasoning = preserveExact || model?.provider === "opencode-go";
const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
? upstreamNativeEntry(model.id)
: null;
Expand Down Expand Up @@ -359,7 +361,7 @@ export function deriveEntry(
e,
model?.reasoningEfforts,
model?.defaultReasoningEffort,
preserveExact || codexForwardNativeCapabilityAlias !== null,
preserveExactReasoning || codexForwardNativeCapabilityAlias !== null,
);
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
Expand Down Expand Up @@ -409,7 +411,7 @@ export function deriveEntry(
};
if (isRouted) {
applyRoutedCodexToolMode(entry, model?.codexToolMode);
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning);
}
else {
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
Expand Down Expand Up @@ -518,7 +520,7 @@ export function buildCatalogEntriesFromObservedState({
// before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so
// this display reorder cannot change which rows are spawn candidates.
const pickerOrder = Array.isArray(modelPickerOrder)
? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.length > 0)
? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.trim().length > 0)
: [];
const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const));
const pickerOrderActive = pickerOrder.length > 0;
Expand Down Expand Up @@ -779,12 +781,33 @@ export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly<
unsupportedNativeEntries: "drop",
});

/** Preserve exact-id precedence while accepting the existing raw/encoded slug spellings. */
function modelPickerRank(order: readonly string[]): (slug: string) => number | undefined {
const exact = new Map(order.map((slug, index) => [slug, index]));
const equivalent = new Map(order.map((slug, index) => [slugEquivalenceKey(slug), index]));
return slug => exact.get(slug) ?? equivalent.get(slugEquivalenceKey(slug));
}

/** A picker order containing native ids orders the whole list, without changing spawn ranks. */
export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly string[]): void {
const pickerOrder = order.filter(slug => slug.trim().length > 0);
if (!pickerOrder.some(slug => !slug.includes("/"))) return;
Comment thread
voiys marked this conversation as resolved.
const rankOf = modelPickerRank(pickerOrder);
for (const entry of entries) {
const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
entry[SPAWN_PRIORITY_FIELD] = natural;
entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
}
}

export interface ObservedCatalogMergeInput {
readonly catalogModels: readonly RawEntry[];
readonly baselineCatalogModels: readonly RawEntry[];
readonly routedEntries: readonly RawEntry[];
readonly baseline: ReadonlyMap<string, number>;
readonly featured: readonly string[];
readonly modelPickerOrder?: readonly string[];
readonly accountSelectors?: readonly string[];
readonly wsEnabled: boolean;
readonly template: RawEntry | null;
readonly disabledModels: ReadonlySet<string>;
Expand Down Expand Up @@ -817,6 +840,8 @@ export function mergeCatalogEntriesFromObservedState({
routedEntries,
baseline,
featured,
modelPickerOrder = [],
accountSelectors = [],
wsEnabled,
template,
disabledModels,
Expand Down Expand Up @@ -975,7 +1000,9 @@ export function mergeCatalogEntriesFromObservedState({
finished.priority = nativePriority(slug, upstream.priority);
return finished;
}
const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m.priority) });
const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m[SPAWN_PRIORITY_FIELD] ?? m.priority) });
// Recompute spawn rank from current featured models, not a prior picker override.
delete preserved[SPAWN_PRIORITY_FIELD];
// Older natives kept from disk still need the mock top tiers (max + ultra always
// for subagent max spawns; wire-clamped to the model's real top rung).
if (!isGpt56NativeSlug(slug) && slug !== NATIVE_RESERVE_MODEL) ensureUltraReasoningLevel(preserved);
Expand Down Expand Up @@ -1060,6 +1087,32 @@ export function mergeCatalogEntriesFromObservedState({
// remain outside provider ownership and survive unless a fresh row replaces their exact slug.
return !isOcxAuthoredRoutedEntry(entry);
});
// Retained rows bypass the builder. Recompute managed spawn ranks from current config
// before either display-order mode; a saved display override is not current roster authority.
const pickerOrder = modelPickerOrder.filter(slug => slug.trim().length > 0);
const fullPickerOrder = pickerOrder.some(slug => !slug.includes("/"));
const rankOf = modelPickerRank(pickerOrder);
const featuredRankOf = modelPickerRank(featured);
const priorityStride = Math.max(accountSelectors.length, 1);
for (const entry of preservedRoutedEntries) {
const natural = entry[SPAWN_PRIORITY_FIELD];
if (typeof natural === "number") {
entry.priority = natural;
delete entry[SPAWN_PRIORITY_FIELD];
}
const slug = String(entry.slug);
if (!isOcxAuthoredRoutedEntry(entry) || isNativeAliasCatalogEntry(entry)) continue;
const featuredRank = featuredRankOf(slug);
entry.priority = featuredRank !== undefined
? featuredRank * priorityStride
: (accountSelectors.length > 0 ? 1_000 : 0) + 5;
if (featuredRank !== undefined || fullPickerOrder) continue;
const pickerIndex = rankOf(slug);
if (pickerIndex !== undefined) {
entry[SPAWN_PRIORITY_FIELD] = entry.priority;
entry.priority = PICKER_ORDER_PRIORITY_BASE + pickerIndex * priorityStride;
}
}
let finalRoutedEntries = [...admittedRoutedEntries, ...preservedRoutedEntries];
finalRoutedEntries = finalRoutedEntries.filter(entry => {
const slug = typeof entry.slug === "string" ? entry.slug : "";
Expand Down Expand Up @@ -1134,7 +1187,7 @@ export function mergeCatalogEntriesFromObservedState({
// Mock-max universality (260709): preserved routed entries from disk may predate
// the max rung — ensure it here so subagent max spawns validate on every
// reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
if (!exactCombo && !reserveProjection) {
if (!exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
const levels = Array.isArray(e.supported_reasoning_levels)
? e.supported_reasoning_levels as Array<{ effort?: string }>
: [];
Expand All @@ -1161,6 +1214,7 @@ export function mergeCatalogEntriesFromObservedState({
multiAgentV2Enabled,
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
);
applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
for (const entry of versionedEntries) {
const kind = entry.opencodex_catalog_kind;
if (trustedAccountBoundNativeCatalogSlug(entry) === undefined
Expand Down Expand Up @@ -1762,6 +1816,8 @@ function writeRetainedCatalogSync({
}).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
: [];
catalog.models = mergeCatalogEntriesFromObservedState({
modelPickerOrder,
accountSelectors,
catalogModels: catalogModelsForMerge,
baselineCatalogModels: baselineCatalog?.models ?? [],
routedEntries: goEntries,
Expand Down
2 changes: 2 additions & 0 deletions src/codex/convergence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ function prepareCatalog(
)),
);
const mergedModels = mergeCatalogEntriesFromObservedState({
modelPickerOrder,
accountSelectors,
catalogModels,
baselineCatalogModels,
routedEntries,
Expand Down
19 changes: 8 additions & 11 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,17 +419,14 @@ export interface OcxConfig {
/** One-time featured-roster upgrade marker; later user ordering is preserved. */
subagentModelsVersion?: number;
/**
* Optional full picker ordering for the Codex model catalog, independent of the
* 5-slot `subagentModels` spawn_agent cap. DISPLAY-ONLY: it controls the visual order of
* the Codex model picker for large routed catalogs (10-20+ models) that would otherwise sort
* arbitrarily and reshuffle on every rebuild. Values are routed `<provider>/<model>` catalog
* slugs (matched by exact slug or `provider/id`); native OpenAI passthrough rows and
* account-qualified native rows are not reordered (order native rows via `subagentModels`).
* Listed routed rows appear in array order; rows not listed keep their normal display order.
* `subagentModels`-featured rows keep their top position. When unset or empty, catalog
* priority is unchanged. This changes ONLY what the user sees in the picker: the spawn_agent
* candidate set is derived from each row's natural priority and is provably unaffected, even
* when every routed row is listed (see opencodex_spawn_priority / effectiveSubagentRoster).
* Display-only order for the Codex picker, independent of subagentModels.
* Routed-only lists order non-featured routed rows; featured and native rows keep
* their normal positions. Including a bare native id opts into ordering the complete
* picker: listed ids appear first in array order, followed by unlisted rows in their
* natural priority order. Exact catalog ids take precedence over equivalent raw/encoded
* routed ids; empty entries are ignored. The separate natural spawn
* priority is preserved, so display order does not change subagent candidates.
* Unset or empty leaves catalog priorities unchanged.
*/
modelPickerOrder?: string[];
/**
Expand Down
Loading
Loading