diff --git a/packages/core/src/__tests__/provider-catalog-contract.test.ts b/packages/core/src/__tests__/provider-catalog-contract.test.ts index ef03f43fa2..2db51dde6a 100644 --- a/packages/core/src/__tests__/provider-catalog-contract.test.ts +++ b/packages/core/src/__tests__/provider-catalog-contract.test.ts @@ -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)), + [], + ); + }); +}); diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index 36c3c44d0a..b14019357b 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -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(