Skip to content
Merged
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
37 changes: 30 additions & 7 deletions src/providers/stale-model-roster-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
* This rewrites one thing: a saved `models` list that is still byte-for-byte the roster
* this file names as `from`, on a provider that still carries the registry adapter, and
* only while the registry currently seeds `to`. A list the user changed does not match and
* is left alone. For each added id it also fills a per-model context window and default
* effort, but only inside a container the row already has: enrichment fills those records
* all-or-nothing, so creating one here would hide the rest of the registry seed.
* is left alone. For each added id it also fills a per-model context window, output limit
* and default effort, but only inside a container the row already has: enrichment fills
* those records all-or-nothing, so creating one here would hide the rest of the registry seed.
* Same shape and restraint as `stale-context-window-migration`.
*/
import { ALIBABA_INTL_TOKEN_PLAN_BASE_URL } from "./base-url-choices";
Expand All @@ -21,6 +21,7 @@ import {
ALIBABA_TOKEN_PLAN_MODELS,
ALIBABA_INTL_TOKEN_PLAN_MODELS,
ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
MINIMAX_M31_DEFAULT_REASONING_EFFORT,
MINIMAX_M31_FLASH_PREVIEW,
MINIMAX_MODELS,
Expand All @@ -37,6 +38,11 @@ export interface StaleModelRoster {
to: readonly string[];
/** Per-model seed values for the added ids, written only into containers that exist. */
contextWindows?: Readonly<Record<string, number>>;
/**
* Custom catalog rows read the saved map directly, so an added id missing from a
* partial map would publish without the registry output limit.
*/
maxOutputTokens?: Readonly<Record<string, number>>;
defaultReasoningEfforts?: Readonly<Record<string, string>>;
/**
* For a provider whose saved URL may point elsewhere (pay-as-you-go or Custom), the
Expand Down Expand Up @@ -87,6 +93,7 @@ export const STALE_MODEL_ROSTERS: readonly StaleModelRoster[] = [
].map(entry => ({
...entry,
contextWindows: ALIBABA_TOKEN_PLAN_CONTEXT_WINDOWS,
maxOutputTokens: ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS,
defaultReasoningEfforts: { "deepseek-v4.1-flash-1017": "high" },
})),
];
Expand All @@ -109,12 +116,17 @@ function servesRoster(baseUrl: unknown, destinations: readonly string[] | undefi
return destinations.some(url => normalizedProviderEndpoint(url) === saved);
}

function fillExisting<T>(container: Record<string, T> | undefined, values: Readonly<Record<string, T>> | undefined, ids: readonly string[]): void {
if (!container || !values) return;
function fillExisting<T>(container: Record<string, T> | undefined, values: Readonly<Record<string, T>> | undefined, ids: readonly string[]): boolean {
if (!container || !values) return false;
let filled = false;
for (const id of ids) {
const value = values[id];
if (value !== undefined && !Object.prototype.hasOwnProperty.call(container, id)) container[id] = value;
if (value !== undefined && !Object.prototype.hasOwnProperty.call(container, id)) {
container[id] = value;
filled = true;
}
}
return filled;
}

/** Pure projection. The caller decides whether to persist. */
Expand All @@ -130,10 +142,21 @@ export function projectStaleModelRosters(
if (!prov) continue;
if (!registrySeeds(entry.provider, prov.adapter, entry.to)) continue;
if (!servesRoster(prov.baseUrl, entry.destinationBaseUrls)) continue;
if (!sameList(prov.models, entry.from)) continue;
const added = entry.to.filter(id => !entry.from.includes(id));
if (sameList(prov.models, entry.to)) {
// A row refreshed before output limits joined the refresh still has a partial map.
// Only that record is revisited: the earlier pass already filled windows and efforts,
// so refilling them could restore a key the operator has since removed.
if (fillExisting(prov.modelMaxOutputTokens, entry.maxOutputTokens, added)) {
changed = true;
warnings.push(`filled output limits for ${added.join(", ")} on the saved "${entry.provider}" row.`);
}
continue;
}
if (!sameList(prov.models, entry.from)) continue;
prov.models = [...entry.to];
fillExisting(prov.modelContextWindows, entry.contextWindows, added);
fillExisting(prov.modelMaxOutputTokens, entry.maxOutputTokens, added);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
fillExisting(prov.modelDefaultReasoningEfforts, entry.defaultReasoningEfforts, added);
changed = true;
warnings.push(
Expand Down
2 changes: 1 addition & 1 deletion structure/providers-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,4 +597,4 @@ The registry may seed `noStructuredOutputModels` at route resolution, unioned wi
operator opt-outs, just as it seeds `noJsonSchemaModels`. The full opt-out wins on
both translated and native Chat paths. For 1017 this deliberately omits JSON requests:
strict schemas are rejected and a `json_object` fallback has not been verified.
The bare model's independently verified downgrade remains separate. Token Plan startup refreshes only exact previous registry rosters for both presets, adding dated models and removing retired International IDs. International rows refresh only on the Token Plan host or the registry default; pay-as-you-go and Custom destinations keep their lists. Edited or reordered lists, saved model defaults and existing per-model window and effort overrides remain untouched. Destination-based enrichment of custom-named rows fills per-model effort defaults, effort maps and structured-output restrictions from the matching registry destination or alias, and operator values win. Both Token Plan presets keep the hosts their official quick starts document; International custom-name enrichment remains excluded by its existing override guard.
The bare model's independently verified downgrade remains separate. Token Plan startup refreshes only exact previous registry rosters for both presets, adding dated models and removing retired International IDs. International rows refresh only on the Token Plan host or the registry default; pay-as-you-go and Custom destinations keep their lists. Edited or reordered lists, saved model defaults and existing per-model window, output-limit and effort overrides remain untouched; an added ID is backfilled only into such a map the row already has, and a row already on the current roster gets only its missing output limits, because custom catalog rows read the saved output-limit map directly. Destination-based enrichment of custom-named rows fills per-model effort defaults, effort maps and structured-output restrictions from the matching registry destination or alias, and operator values win. Both Token Plan presets keep the hosts their official quick starts document; International custom-name enrichment remains excluded by its existing override guard.
44 changes: 43 additions & 1 deletion tests/providers/model-roster-seed-repair.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, expect, test } from "bun:test";
import { mergeConfiguredModelsIntoLiveCatalog } from "../../src/codex/catalog/provider-fetch";
import { routedMaxOutputTokens } from "../../src/codex/catalog/model-hints";
import { projectStartupConfigRepairs } from "../../src/providers/model-rename-startup";
import { projectStaleModelRosters, STALE_MODEL_ROSTERS } from "../../src/providers/stale-model-roster-migration";
import { MINIMAX_MODELS, MINIMAX_MODELS_BEFORE_M31 } from "../../src/providers/registry/model-seeds";
import { ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS, MINIMAX_MODELS, MINIMAX_MODELS_BEFORE_M31 } from "../../src/providers/registry/model-seeds";
import { PROVIDER_REGISTRY } from "../../src/providers/registry";
import type { OcxConfig, OcxProviderConfig } from "../../src/types";

Expand Down Expand Up @@ -164,6 +165,47 @@ describe.each([
expect(absent.providers![entry.provider]!.modelContextWindows).toBeUndefined();
expect(absent.providers![entry.provider]!.modelDefaultReasoningEfforts).toBeUndefined();
});
test("backfills the added id's output limit into a partial saved map", () => {
// Custom catalog rows hand the saved provider straight to routedMaxOutputTokens, so a
// partial map without the added id published it with no limit when discovery had none.
const config = makeConfig([...before]);
const row = config.providers![entry.provider]!;
row.modelMaxOutputTokens = { "qwen3.8-max": 4_096 };
expect(projectStaleModelRosters(config).changed).toBe(true);
const added = entry.to.filter(id => !before.includes(id));
expect(row.modelMaxOutputTokens).toEqual({
"qwen3.8-max": 4_096, ...Object.fromEntries(added.map(id => [id, ALIBABA_TOKEN_PLAN_MAX_OUTPUT_TOKENS[id]])),
});
expect(routedMaxOutputTokens("custom-token-plan", row, { id: "deepseek-v4.1-flash-1017" } as never)).toBe(393_216);
const kept = makeConfig([...before]);
kept.providers![entry.provider]!.modelMaxOutputTokens = { "deepseek-v4.1-flash-1017": 8_192 };
projectStaleModelRosters(kept);
expect(kept.providers![entry.provider]!.modelMaxOutputTokens!["deepseek-v4.1-flash-1017"]).toBe(8_192);
const absent = makeConfig([...before]);
projectStaleModelRosters(absent);
expect(absent.providers![entry.provider]!.modelMaxOutputTokens).toBeUndefined();
});
test("backfills output limits on a row an earlier refresh already moved to the current roster", () => {
const config = makeConfig([...entry.to]);
const row = config.providers![entry.provider]!;
row.modelContextWindows = {};
row.modelMaxOutputTokens = { "qwen3.8-max": 4_096 };
const first = projectStartupConfigRepairs(config);
expect(first.changed).toBe(true);
const saved = first.config.providers![entry.provider]!;
expect(saved.modelMaxOutputTokens!["deepseek-v4.1-flash-1017"]).toBe(393_216);
expect(saved.modelMaxOutputTokens!["qwen3.8-max"]).toBe(4_096);
expect(saved.modelContextWindows).toEqual({});
expect(projectStartupConfigRepairs(structuredClone(first.config)).changed).toBe(false);
const kept = makeConfig([...entry.to]);
kept.providers![entry.provider]!.modelMaxOutputTokens = { "deepseek-v4.1-flash-1017": 8_192 };
projectStaleModelRosters(kept);
expect(kept.providers![entry.provider]!.modelMaxOutputTokens!["deepseek-v4.1-flash-1017"]).toBe(8_192);
const absent = makeConfig([...entry.to]);
const original = structuredClone(absent);
expect(projectStaleModelRosters(absent).changed).toBe(false);
expect(absent).toEqual(original);
});
test("preserves a custom adapter", () => {
const config = makeConfig([...before]);
config.providers![entry.provider]!.adapter = "anthropic";
Expand Down
Loading