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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente

### Fixed

- **`[Models]` A 401 on the model-list fetch now logs an actionable key hint, and both bundled fallback catalogs are refreshed to the live gateways (fixes #269).** The reporter's "DeepSeek v4.1 not shown" had two layers: their stored API key was rejected by the gateway — the `/models` endpoints are a public catalog (anonymous requests return 200; an invalid `Authorization: Bearer` gets 401; verified live on both gateways, deterministic) — so the live catalog never loaded and the September-era bundled list took over, which simply didn't contain `deepseek-v4.1-flash`. `ModelListFetcher` now recognizes the `Model list request failed (401)` shape and logs "the stored API key was rejected (401). Update or clear it via `…: Manage Provider`" alongside the fallback notice (401 stays non-transient: exactly one attempt, no retry; the hint fires on the cached branch too). The bundled Go list now mirrors the 38-model live catalog (13 new models incl. `deepseek-v4.1-flash`, `gpt-6-luna`, `grok-4.7`, `kimi-k3.1-pro`, `mimo-v2.6-flash/pro`, `step-5-preview`, `beam-free`, `claude-haiku-5-5`, `gemini-4-argon`; dead `deepseek-v4-flash`/`-vision-exp` dropped — the plain id is still served on Zen, so it is deliberately not blocklisted), and the Zen list mirrors the 84-model live catalog minus the policy-hidden free tier, the non-chat `jev-*` decision API, and junk entries (`test`, `claude-opus-5-5-backup`) → 72 models. Log forensics also showed the reporter's session was running pre-0.7.10 code (their 38-entry filter line matches the old `freeOnly` keep-free filter exactly), so part of the symptom was already resolved by #266 after a reload. 2 new tests (561/561 pass). Documented in `docs/issues/117-20261027-issue269-models-401-hint-and-bundled-refresh.md`.

- **`[Errors]` HTTP 402 `Endpoint is unavailable` (e.g. `longcat-2.5-preview-free`) now renders an actionable hint instead of raw upstream text.** Live probes show the model is still in the Go catalog and passes gateway model validation (dummy-key probe → 401 at the credential check), so the 402 fires at upstream serving time — the `Router.Unavailable` family reached via a different status/message, not the #204 removed-model class; the model is deliberately not blocklisted. New `describeEndpointUnavailable()` hint ("upstream unavailable right now — retry or switch models") joins the free-tier and Router.Unavailable detectors, and `describeRouterUnavailable()` now returns `undefined` on mismatch — it previously always returned the fallback string, which silently short-circuited the hint chain. 3 new tests (559/559 pass). Documented in `docs/issues/116-20261022-longcat-endpoint-unavailable.md`. Upstream research after a premature blocklist attempt (reverted) confirmed the correct reading: `Endpoint is unavailable` is OpenCode's generic per-model incident error (402/429/503 across models — anomalyco/opencode #55607 for this very model, closed fixed; #56796; #57613), tracked on **status.opencode.ai** — a Free-inference upstream incident ran Oct 21 05:54–19:28 UTC and the reported failures sit in its aftermath window. The model stays selectable and the hint now points at the status page.

- **`[Tool calling]` `beam-free` no longer fails with HTTP 400 `Invalid value for 'tools[N].function.name': maximum string length is 64` when MCP tools with long names are attached.** The issue #165 truncation (first 55 chars + `_` + 8-char hash) only ran on the Responses API path, but `beam-free` routes to chat-completions where Beam's upstream enforces the same OpenAI 64-char function-name limit — so the whole request 400ed before generation. A new registry `beam` family row plus `needsToolNameTruncation()` (muse + beam) gates truncation on both endpoints, and the chat-completions transport now receives the truncated→original `toolNameMap` so tool calls round-trip to VS Code's real tool names. 5 new tests (556/556 pass). Documented in `docs/issues/115-20261022-beam-tool-name-length.md`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Model-List 401 Hint + Bundled Catalog Refresh (Issue #269)

**Status:** ✅ Solved
**Topic:** provider / models / errors / fallback
**Updated:** 2026-10-27
**Tags:** #provider #models #errors #fallback #fetch-models
**Related:** issue [#269](https://github.com/ltmoerdani/opencode-copilot-chat/issues/269), doc [100 — bundled model data sync](100-20260908-bundled-model-data-sync.md), doc [112 — Zen free-tier policy](112-20261020-zen-free-tier-policy-followup.md), doc [78 — deprecated cross-check #182](78-20260822-issue182-deprecated-model-gateway-crosscheck.md)

---

## Problem

Reporter (`@aalam95`, ext 0.7.10, both providers): "Deepseek v.4.1 is not shown" in the model picker. Logs showed every `fetchModels` call failing with `Model list request failed (401): Unauthorized` on **both** Go and Zen, falling back to the bundled list — and the Zen log filtering 38 models down to a nearly-empty picker.

## Evidence (verified live 2026-10-26/27)

| Probe (×3, deterministic) | Result |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `GET /zen/go/v1/models` + `/zen/v1/models`, anonymous | **200** — the model list is a public catalog; the official docs curl it without auth |
| Same endpoints with `Authorization: Bearer sk-invalid-test` | **401** — the gateway validates the bearer whenever one is sent |
| status.opencode.ai (Console component owns keys/auth) | operational; no auth incident around Oct 23 |
| Upstream issue search `401 unauthorized models` (anomalyco/opencode) | no gateway-side `/models` 401 defect; #46169 confirms the "models list works, chat 401" split |

**Root cause:** the reporter's stored API key (both vendors) was invalid/expired/revoked. The live catalog never loaded, so the September-era bundled list took over — and it simply didn't contain `deepseek-v4.1-flash` (Go bundled was a 27-model snapshot vs 38 live; Zen 42 vs 84 live).

**Forensics on the reporter's Zen log:** the 38-entry `Filtered unavailable/deprecated models` line equals the bundled Zen list minus its 4 free-tier ids plus `deepseek-v4-flash-free` (KNOWN_UNAVAILABLE) — exactly the _keep-free-only_ `filterModel` of the pre-#266 era. So the reporting session was running pre-0.7.10 code (VS Code swaps extensions only on reload). On current main, `shouldHideDeprecatedModel` is fail-open without live ids (nothing hidden on fallback), so a fresh 0.7.10 session shows ~37 stale Zen models instead — the stale-bundled problem is the same either way.

## Changes

| File | Change |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/provider/modelList.ts` | New `isUnauthorizedModelListError()` (matches the thrown `Model list request failed (401)` prefix so `retry.ts` classification is untouched); on final failure with 401, logs an actionable hint: "the stored API key was rejected (401). Update or clear it via `…: Manage Provider`". 401 remains non-transient — one attempt, no retry |
| `src/provider/definitions.ts` | Go `fallbackModels`: 27 → 38, mirroring the live gateway catalog 2026-10-27 (adds `deepseek-v4.1-flash`, `gpt-6-luna`, `grok-4.7`, `kimi-k3.1-pro`, `mimo-v2.6-*`, `step-5-preview`, `beam-free`, `claude-haiku-5-5`, `gemini-4-argon`, …; drops dead `deepseek-v4-flash`/`-vision-exp`). Zen `fallbackModels`: 42 → 72, live catalog minus the policy-hidden free tier, the non-chat `jev-*` decision API, and junk ids (`test`, `claude-opus-5-5-backup`) |
| `src/test/modelList401Hint.test.ts` | New: 401 → bundled fallback + hint logged + exactly one fetch attempt; non-401 permanent (400) → fallback without the hint |

## Decisions

- **No anonymous retry on 401.** The catalog endpoint is public, so retrying without the `Authorization` header would load the live list even with a dead key — but it would mask the key problem until the first chat request. The hint keeps the diagnosis loud at the exact point of failure; Test Connection / chat errors already carry actionable messages.
- **`deepseek-v4-flash` is NOT blocklisted.** It is dead on Go but still served (flakily) on Zen; `KNOWN_UNAVAILABLE_MODEL_IDS` is vendor-agnostic, so blocklisting would hide a live Zen model (same reasoning as the #182 cross-check). Removing it from the Go bundled list is enough.
- **Zen bundled mirrors live minus free tier** rather than models.dev: the gateway is the source of truth (#182), and the officially-deprecated-but-still-served ids (`gpt-5-codex` family, `minimax-m2.5`, `glm-5`, `kimi-k2.5`) stay listed exactly as the live path shows them today.
- **Hint applies to the cached branch too** — a 401 with a fresh (<1 h) cached snapshot has the same root cause, so the hint logs before the cached/bundled choice.

## Verification

- `npm run compile` — no errors.
- `npm test` — **561/561 pass** (559 prior + 2 new).
- Live re-probes: both `/models` endpoints anonymous 200 / invalid-bearer 401 (deterministic ×3); Go live catalog 38 ids, Zen 84 ids (2026-10-27).

Follow-up for the reporter: after updating the key, note that **DeepSeek V4.1 Flash / V4 Pro on Go require the "Global" region** in Console → Settings → Privacy (default is EU+US; upstream [#50155](https://github.com/anomalyco/opencode/issues/50155)) — otherwise chat requests for those models are rejected even with a valid key.
120 changes: 84 additions & 36 deletions src/provider/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,27 +144,40 @@ export const PROVIDERS: Record<ProviderDefinition["vendor"], ProviderDefinition>
// fails with "Model is unavailable" before ever validating the key.
testModelId: "deepseek-v4.1-flash",
fallbackModels: [
// Curated active set from models.dev (2026-09-08) — deprecated/legacy
// models are excluded, mirroring the live catalog's own filtering.
"deepseek-v4-flash",
"deepseek-v4-flash-vision-exp",
// Curated from the live Go gateway catalog (2026-10-27, issue #269).
// Removed vs the 2026-09-08 snapshot: deepseek-v4-flash and
// deepseek-v4-flash-vision-exp (dead upstream, replaced by
// deepseek-v4.1-flash — the plain id is still served on Zen, so it is
// deliberately NOT blocklisted in KNOWN_UNAVAILABLE_MODEL_IDS).
"beam-free",
"claude-haiku-5-5",
"deepseek-v4-pro",
"deepseek-v4.1-flash",
"gemini-4-argon",
"glm-5.1",
"glm-5.2",
"glm-5.3",
"glm-5.3-flash",
"gpt-5.6-luna",
"gpt-6-luna",
"grok-4.6",
"grok-4.7",
"hy3",
"hy4-preview",
"iceman-contributor-free",
"kimi-k2.6",
"kimi-k2.7-code",
"kimi-k3",
"kimi-k3.1-pro",
"longcat-2.0",
"minimax-m2.7",
"minimax-m3",
"mimo-v2.5",
"mimo-v2.5-pro",
"mimo-v2.6-flash",
"mimo-v2.6-pro",
"minimax-m2.5",
"minimax-m2.7",
"minimax-m3",
"minimax-m3.1-flash",
"muse-spark-1.2-contributor",
"muse-spark-1.3-contributor",
"omen-alpha",
Expand All @@ -173,6 +186,7 @@ export const PROVIDERS: Record<ProviderDefinition["vendor"], ProviderDefinition>
"qwen3.7-plus",
"qwen3.8-flash",
"qwen3.8-max",
"step-5-preview",
],
};
const zen: ProviderDefinition = {
Expand All @@ -191,48 +205,82 @@ export const PROVIDERS: Record<ProviderDefinition["vendor"], ProviderDefinition>
// cheapest live model (~$0.30/1M in; max_tokens 10 ≈ negligible).
testModelId: "deepseek-v4.1-flash",
fallbackModels: [
"claude-opus-4-7",
"claude-opus-4-6",
// Curated from the live Zen gateway catalog (2026-10-27, issue #269).
// Free-tier ids are excluded (filterModel hides them anyway per the
// 403 policy, doc 112), as are the non-chat jev-* decision API and
// junk catalog entries ("test", "claude-opus-5-5-backup").
"claude-fable-5",
"claude-fable-5-1",
"claude-haiku-4-5",
"claude-haiku-5-5",
"claude-opus-4-5",
"claude-opus-4-1",
"claude-sonnet-4-6",
"claude-opus-4-6",
"claude-opus-4-7",
"claude-opus-4-8",
"claude-opus-5",
"claude-opus-5-5",
"claude-sonnet-4-5",
"claude-sonnet-4",
"claude-haiku-4-5",
"deepseek-v4-flash-free",
"gemini-3.5-flash",
"gemini-3.1-pro",
"claude-sonnet-4-6",
"claude-sonnet-5",
"claude-sonnet-5-5",
"deepseek-v4-pro",
"deepseek-v4.1-flash",
"gemini-3-flash",
"glm-5.1",
"gemini-3.1-pro",
"gemini-3.5-flash",
"gemini-3.5-flash-lite",
"gemini-3.6-flash",
"gemini-3.7-flash",
"gemini-3.8-flash",
"gemini-4-argon",
"glm-5",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.4",
"gpt-5.4-pro",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
"gpt-5.2",
"gpt-5.2-codex",
"glm-5.1",
"glm-5.2",
"glm-5.3",
"glm-5.3-flash",
"gpt-5",
"gpt-5-codex",
"gpt-5-nano",
"gpt-5.1",
"gpt-5.1-codex",
"gpt-5.1-codex-max",
"gpt-5.1-codex-mini",
"gpt-5",
"gpt-5-codex",
"gpt-5-nano",
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.4-pro",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.6-luna",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-6-astra",
"gpt-6-luna",
"gpt-6-sol",
"gpt-6.1-sol",
"grok-4.5",
"grok-4.6",
"grok-4.7",
"grok-build-0.1",
"kimi-k2.6",
"kimi-k2.5",
"minimax-m2.7",
"kimi-k2.6",
"kimi-k2.7-code",
"kimi-k3",
"minimax-m2.5",
"minimax-m2.5-free",
"nemotron-3-super-free",
"qwen3.6-plus",
"qwen3.6-plus-free",
"minimax-m2.7",
"minimax-m3",
"mistral-large-4",
"muse-spark-1.2",
"muse-spark-1.3",
"muse-wonder-1.0",
"qwen3.5-plus",
"big-pickle",
"qwen3.6-plus",
"qwen3.8-flash",
"qwen3.8-max",
],
// Free-tier Zen models are always hidden: OpenCode gates them to its own
// clients since 2026-09-17 (403 FreeTierError, upstream #49580). The
Expand Down
20 changes: 20 additions & 0 deletions src/provider/modelList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ import { getUserAgent, isTransientFetchError, type ModelListEntry, type ModelLis
import { auxiliarySessionId } from "../request/headers";
import { resolveBaseVendor } from "../providerTypes";

/**
* A 401 from the models endpoint means the STORED key was rejected — the
* endpoint itself is a public catalog (the official docs curl it without
* auth), so this is a key problem, never a list/gateway problem (issue
* #269). Matched on the message shape thrown in `fetch()` so the
* `retry.ts` classification (which parses the same prefix) stays untouched.
*/
function isUnauthorizedModelListError(error: unknown): boolean {
return getErrorMessage(error).includes("Model list request failed (401)");
}

/**
* Fetches the live `/models` list with retry/backoff and cancellation support,
* caching the last successful snapshot in globalState (TTL-guarded) so
Expand Down Expand Up @@ -129,6 +140,15 @@ export class ModelListFetcher {
}

// Final failure: prefer cached snapshot (still fresh), then bundled list.
// Issue #269: 401 = the stored key was rejected; the bare "Unauthorized"
// line reads like a fetch bug, so point at the actual fix. The hint
// applies to the cached branch below too — same root cause.
if (isUnauthorizedModelListError(lastError)) {
this.deps.log(
`[fetchModels] ${this.deps.definition.displayName}: the stored API key was rejected (401). ` +
`Update or clear it via "${this.deps.definition.displayName}: Manage Provider" — requests will keep failing until then.`,
);
}
const cached = this.loadCached();
if (cached) {
this.deps.log(
Expand Down
Loading
Loading