Skip to content
Open
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: 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)),
[],
);
});
});
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