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
27 changes: 27 additions & 0 deletions packages/core/src/__tests__/llm-connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,30 @@ test('a quarantined model id is vetoed even when enabled and present in the inve
id: 'nemotron-3-ultra-free',
});
});

test('a quarantined stored default is dropped from the picker, not re-added as a missing-default row', () => {
// The retired `x-preview-f-free` was picker-visible before the quarantine, so
// an upgrade connection can carry it as `defaultModel` and enabled. `models`
// and `enabledModelIds` are filtered against `brokenModelIds`, but the raw
// `defaultModel` used to pass through unfiltered and `makeMissingDefaultEntry`
// re-added it as a selectable `provider_default` row — visible and pickable
// while `authorizeConnectionModel` vetoed the same id. The picker and the send
// authority must agree: neither offers it, and the live model still renders.
const connection = {
connectionId: 'connection-opencode-free',
slug: 'opencode-free',
name: 'OpenCode Free',
providerType: 'opencode-free' as ProviderType,
enabled: true,
defaultModel: 'x-preview-f-free',
enabledModelIds: ['x-preview-f-free', 'nemotron-3-ultra-free'],
models: [{ id: 'x-preview-f-free' }, { id: 'nemotron-3-ultra-free' }],
modelSource: 'fetched' as const,
createdAt: 1,
updatedAt: 1,
};
const models = buildChatModelChoices([connection]).map(({ model }) => model);
assert.ok(!models.includes('x-preview-f-free'), 'quarantined default must not be offered');
assert.ok(models.includes('nemotron-3-ultra-free'), 'live enabled model still renders');
assert.equal(authorizeConnectionModel(connection, 'x-preview-f-free'), undefined);
});
29 changes: 29 additions & 0 deletions packages/core/src/__tests__/provider-catalog-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,32 @@ describe('provider catalog contract — fallback lifecycle', () => {
assert.deepEqual(regressed, []);
});
});

describe('opencode-free retired-model quarantine', () => {
const opencodeFree = PROVIDER_REGISTRY['opencode-free'];

// Ox Alpha Free (x-preview-f-free) was retired upstream on OpenCode Zen while
// models.dev still snapshots it free+active, so the derivation would keep
// offering it as a default-enabled, picker-visible row. Remove this assertion
// together with the OPENCODE_FREE_BROKEN_MODEL_IDS entry once the snapshot
// marks it deprecated (or upstream serves it again).
it('quarantines x-preview-f-free out of the offered free models', () => {
assert.ok(opencodeFree.brokenModelIds?.includes('x-preview-f-free'));
assert.ok(!(opencodeFree.fallbackModels ?? []).includes('x-preview-f-free'));
assert.ok(!(opencodeFree.defaultEnabledModelIds ?? []).includes('x-preview-f-free'));
});

// Mechanism guard, independent of which ids the deny-list holds: a quarantined
// id must never leak back into the offered candidates through either path.
it('never offers a quarantined broken id as a free candidate', () => {
const broken = new Set(opencodeFree.brokenModelIds ?? []);
const offered = [
...(opencodeFree.fallbackModels ?? []),
...(opencodeFree.defaultEnabledModelIds ?? []),
];
assert.deepEqual(
offered.filter((id) => broken.has(id)),
[],
);
});
});
13 changes: 12 additions & 1 deletion packages/core/src/model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,21 @@ export function buildConnectionModelCatalogEntries(
const fallbackModels = [...(catalogFallbackModels ?? defaults.fallbackModels)].filter(
(id) => !broken.has(id),
);
// A quarantined id persisted as this connection's `defaultModel` must not
// re-enter the catalog either. `models` and `enabledModelIds` are filtered
// below, but a broken default reaches `makeMissingDefaultEntry` unfiltered and
// would be re-added as a selectable `provider_default` row — picker-visible
// and default-capable while `authorizeConnectionModel` vetoes the same id. A
// reachable persisted state: the id was picker-visible before the quarantine.
// Dropping it leaves the connection with no valid default (readiness reports
// `missing_model`), which is what a model that can no longer send warrants.
const defaultModel = broken.has((connection.defaultModel ?? '').trim())
? undefined
: connection.defaultModel;
return buildModelCatalogEntries({
providerType: connection.providerType,
connectionSlug: connection.slug,
defaultModel: connection.defaultModel,
defaultModel,
models: connection.models?.filter(({ id }) => !broken.has(id)),
modelSource: connection.modelSource,
modelsFetchedAt: connection.modelsFetchedAt,
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,16 @@ const opencodeGoModelIds = toolCallingModelIds(
// probes, max_tokens 8–200) — a failure shape that even "the send settles it"
// cannot surface, which is why these ids are also vetoed in
// `authorizeConnectionModel` rather than merely dropped from this derivation.
const OPENCODE_FREE_BROKEN_MODEL_IDS = new Set(['muse-spark-1.2-contributor-free']);
// 2026-08-30 x-preview-f-free (Ox Alpha Free): retired upstream — dropped from
// the anonymous /models listing and every completion returns HTTP 401
// {"type":"ModelError","message":"Model x-preview-f-free is not supported"}.
// models.dev still snapshots it as free+active, so the derivation kept offering
// it as a default-enabled, picker-visible row until this quarantine. Remove
// once the snapshot marks it deprecated (or upstream serves it again).
const OPENCODE_FREE_BROKEN_MODEL_IDS = new Set([
'muse-spark-1.2-contributor-free',
'x-preview-f-free',
]);
const opencodeFreeModelIds = toolCallingModelIds(
'OpenCode Free',
Object.fromEntries(
Expand Down