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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to the **OpenCode Go BYOK Provider** extension are documented here.

## [Unreleased]

### Fixed

- **`[Models]` Free-tier Zen models are hidden from the picker, `opencodego.freeOnly` is deprecated, and every free-Zen claim is removed from the docs (Marketplace review, follow-up to #235/#240).** OpenCode has gated free-tier Zen models to its own app/CLI since Sep 17, 2026 (HTTP 403 for third-party clients — upstream anomalyco/opencode#49580), verified live against all 10 free models in the Zen catalog. With `freeOnly` defaulting to `true`, the Zen picker showed _only_ models that could never serve a request. Free-tier Zen models (`-free` suffix, plus `big-pickle`) are now always filtered out via a vendor-scoped `isZenFreeTierModel()` — the gate is Zen-only: Go's free models (`beam-free`, `minimax-m3.1-flash-free`, `longcat-2.5-preview-free`) are unlimited within a paid Go subscription, explicitly permitted for third-party clients, and remain selectable. `freeOnly` stays registered so existing settings don't error but no longer has any effect. README, extension description, and keywords no longer advertise free Zen models: the free-models table is replaced with a dated policy notice, and stale free claims (quick start, comparison table, FAQ, `free-models` keyword, "free Claude Opus" in the description) are corrected. Documented in `docs/issues/112-20261020-zen-free-tier-policy-followup.md`.

- **`[Errors]` The free-tier / Router.Unavailable hint now renders in the chat, not only the toast (#258 follow-up).** `buildOpenCodeRequestError()` placed the actionable hint in `userMessage`, but VS Code's chat surface renders `error.message` — which carried the raw gateway JSON (`Error from provider (Console): OpenCode's free tier can only be used…`). The hint now replaces the raw detail in both messages; the upstream body is still preserved in the OpenCode output channel (`[http-error-body]`).

## [0.7.10] — unreleased

### Added
Expand Down
71 changes: 32 additions & 39 deletions README.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/issues/106-20260923-issue235-240-free-tier-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,9 @@ Paid models (OpenCode Go) are **not** affected. Any client that still serves fre
---

Detected 2026-09-16 (#235) and 2026-09-22 (#240) | Reported by @gp-slick-coder, @AIlaowong | Fixed 2026-09-23

---

## Follow-up (2026-10-20) — doc 112

The 0.7.6 fix only replaced the error text in the toast; chat still showed the raw gateway JSON, and the Zen picker (default `freeOnly: true`) kept steering users into the gated models. A Marketplace review surfaced both gaps. Free-tier Zen models are now hidden from the picker entirely, `freeOnly` is deprecated, and the hint renders in chat. Verified the gate is Zen-only — Go free models stay selectable. See [`112-20261020-zen-free-tier-policy-followup.md`](112-20261020-zen-free-tier-policy-followup.md).
53 changes: 53 additions & 0 deletions docs/issues/112-20261020-zen-free-tier-policy-followup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Hide Zen Free-Tier Models, Deprecate freeOnly, Surface the 403 Hint in Chat

**Status:** ✅ Solved
**Topic:** provider / policy / models / errors
**Updated:** 2026-10-20
**Tags:** #provider #zen #free-tier #policy #models #errors
**Related:** doc [106 — issue235/240 free-tier policy](106-20260923-issue235-240-free-tier-policy.md), [110 — issue247 api-base-url](110-20261013-issue247-api-base-url-ignored.md), upstream [anomalyco/opencode#49580](https://github.com/anomalyco/opencode/issues/49580)

---

## Problem

A Marketplace review reported the extension "suddenly stopped working" with `OpenCode's free tier can only be used from within OpenCode. Download it at https://opencode.ai and keep building`. This is the same upstream policy already handled in doc 106 (issues #235/#240), but with three extension-side defects on top:

1. **`opencodego.freeOnly` defaults to `true`**, so the Zen picker listed _only_ free-tier models — every one of which upstream now 403s for third-party clients. A default install was a guaranteed dead end.
2. **The actionable hint never reached the chat.** `buildOpenCodeRequestError()` put the hint in `userMessage` (toast only); VS Code renders `error.message`, which carried the raw gateway JSON (confirmed by the #258 screenshot).
3. **The Zen `testModelId` was dead** (410 `ModelDeprecated`) — fixed separately in #263, which also confirmed issue #258/#261's "Test Connection" failures.

## Evidence (verified live 2026-10-20)

| Probe | Result |
| --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Zen `POST /chat/completions` on all 10 live free models (`big-pickle`, `deepseek-v4.1-flash-free`, `glm-5.3-flash-free`, …) | HTTP 403 `FreeTierError`, message identical to the review |
| Zen paid model (`gpt-5.4`) | HTTP 401 `AuthError` — paid models are not gated |
| Go endpoint (any model incl. `beam-free`) | HTTP 401 "Go requires a credential" — credential check runs first, no free-tier gate |
| `GET /zen/v1/models` | 85 models, 10 free still advertised in the catalog |

**Key design fact: the gate is Zen (Console) only.** Go's own free models (`beam-free`, `minimax-m3.1-flash-free`, `longcat-2.5-preview-free`) are unlimited within a paid Go subscription and Go explicitly supports third-party clients — so the filter must be vendor-scoped, never global. Upstream maintainer statements in #49580 confirm the restriction is deliberate anti-abuse ("Our free tier is only meant to be used in opencode") and covers only the free tier; client-identity spoofing was tried by third parties and rejected upstream (every header combination still returns `FreeTierError`), so bypassing remains off the table.

## Changes

| File | Change |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/config.ts` | New `isZenFreeTierModel()` with a CONTRACT comment: vendor-scoped (Zen only), lists the Go free-tier exceptions, points at upstream #49580, marked removable if upstream reopens |
| `src/provider/definitions.ts` | Zen `filterModel` now **excludes** free-tier models unconditionally (live, cached, and bundled paths all flow through it); `CONFIG_SECTION`/`SETTING_FREE_ONLY`/`FREE_ZEN_MODEL_IDS` imports dropped |
| `src/errors.ts` | `describeFreeTierRestriction()` / `describeRouterUnavailable()` hint now replaces the raw detail in **both** `message` and `userMessage`; raw body still logged via `[http-error-body]` in the output channel |
| `package.json` | `opencodego.freeOnly` marked deprecated (kept registered so existing user settings don't error); description explains the policy and that the setting is ignored |
| `README.md` | Setup steps no longer point free-tier users at Zen; free-tier models called out as unavailable from VS Code with the why and the workaround |
| `src/test/config.test.ts` | `isZenFreeTierModel` classification (suffix, `big-pickle`, Go suffix sharing, paid negatives) |
| `src/test/errors.test.ts` | Hint asserted in `error.message` (chat surface) and raw gateway text asserted absent from user-facing messages |

## Decisions

- **Ignore `freeOnly` rather than flip it.** Auto-writing user configuration without permission is worse than a documented no-op; the setting description now says so.
- **Go free models stay selectable.** Verified unlimited-for-subscribers and not behind the gate.
- **Model list still fetched live** — if upstream reopens the free tier, removing the `isZenFreeTierModel` gate (one function) restores visibility everywhere.

## Verification

- `npm run lint` — all 7 checks pass (editorconfig, eslint, markdown, prettier, shellcheck, typecheck, unit tests).
- Live probes above re-run after the change to confirm the gate classification still matches the catalog.

Follow-up to doc 106 | Detected via Marketplace review, 2026-10-20
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencode-copilot-chat",
"displayName": "OpenCode for Copilot Chat: BYOK 30+ AI Models",
"description": "Use 30+ frontier AI models (DeepSeek V4, Kimi K2.6, GLM-5.1, Qwen3.7, MiMo V2.5, MiniMax M2.7, free Claude Opus, GPT-5.5, Gemini 3.5, Grok) in GitHub Copilot Chat. Bring Your Own Key, no Copilot Pro needed.",
"description": "Use 30+ frontier AI models (DeepSeek V4, Kimi K2.6, GLM-5.1, Qwen3.7, MiMo V2.5, MiniMax M2.7, Claude Opus, GPT-5.5, Gemini 3.5, Grok) in GitHub Copilot Chat. Bring Your Own Key, no Copilot Pro needed.",
"version": "0.7.9",
"publisher": "ltmoerdani",
"license": "MIT",
Expand Down Expand Up @@ -34,8 +34,7 @@
"chat",
"agent",
"reasoning",
"thinking",
"free-models"
"thinking"
],
"repository": {
"type": "git",
Expand Down Expand Up @@ -261,7 +260,7 @@
"opencodego.freeOnly": {
"type": "boolean",
"default": true,
"description": "When enabled, only free OpenCode Zen models are shown in the Copilot model selector. Disable to include paid Zen models as well."
"description": "Deprecated: OpenCode restricts free Zen models to its own app/CLI since Sep 17, 2026 (403 for third-party clients), so the extension now always hides them and lists paid Zen models. This setting no longer has any effect."
},
"opencodego.enabled": {
"type": "boolean",
Expand Down Expand Up @@ -431,7 +430,7 @@
"off"
],
"default": "off",
"markdownDescription": "Thinking mode for Qwen models (`qwen3.5-plus`, `qwen3.6-plus`, `qwen3.6-plus-free`). Maps to `enable_thinking`. `auto` lets the model decide (omits the flag), `on`/`off` force-enable or disable hybrid thinking. Off is the recommended default to avoid empty Copilot replies."
"markdownDescription": "Thinking mode for Qwen models (`qwen3.5-plus`, `qwen3.6-plus`, `qwen3.7-plus`). Maps to `enable_thinking`. `auto` lets the model decide (omits the flag), `on`/`off` force-enable or disable hybrid thinking. Off is the recommended default to avoid empty Copilot replies."
},
"opencodego.thinking.qwenBudget": {
"type": "string",
Expand All @@ -455,7 +454,7 @@
"xhigh"
],
"default": "off",
"markdownDescription": "Thinking mode for Muse Spark models (`muse-spark-1.2-contributor`, `muse-spark-1.2-contributor-free`). Maps to the Responses API `reasoning.effort` field. Higher effort increases reasoning depth and may increase latency and usage."
"markdownDescription": "Thinking mode for Muse Spark models (`muse-spark-1.2-contributor`, `muse-spark-1.3-contributor`). Maps to the Responses API `reasoning.effort` field. Higher effort increases reasoning depth and may increase latency and usage."
},
"opencodego.thinking.gemini": {
"type": "string",
Expand Down
13 changes: 13 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,19 @@ export const TRANSIENT_FETCH_RETRY_JITTER_MS = 250;

/** Zen free-model IDs that do not end in `-free`. */
export const FREE_ZEN_MODEL_IDS = new Set(["big-pickle"]);

/**
* CONTRACT: OpenCode gates Zen (Console) free-tier models to their own
* clients since 2026-09-17 (anomalyco/opencode#49580 — issues #235/#240), so
* third-party clients get HTTP 403 FreeTierError on every request. Applies to
* ZEN ONLY: Go free models (`beam-free`, `minimax-m3.1-flash-free`,
* `longcat-2.5-preview-free`) are unlimited within a paid Go subscription and
* Go explicitly permits third-party clients, so they must stay selectable.
* Remove this gate if upstream ever reopens the Zen free tier.
*/
export function isZenFreeTierModel(modelId: string): boolean {
return modelId.endsWith("-free") || FREE_ZEN_MODEL_IDS.has(modelId);
}
/**
* Models removed upstream — always filtered from the picker.
* `deepseek-v4-flash-free` / `laguna-s-2.1-free`: gone from the Zen gateway
Expand Down
10 changes: 8 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ export function buildOpenCodeRequestError(
);
}

const userMessage = `${providerDisplayName} API request failed (HTTP ${String(response.status)})${modelId ? ` for ${modelId}` : ""}: ${describeFreeTierRestriction(apiMessage) ?? describeRouterUnavailable(apiError, apiMessage)}${capacityHint}`;
const requestMessage = `${providerDisplayName} API request failed (${String(response.status)})${modelHint}${sizeHint}${capacityHint}: ${apiMessage}`;
// The actionable hint replaces the raw gateway detail in BOTH messages: VS
// Code's chat surface renders `message` (issue #258 — users saw raw gateway
// JSON with no explanation), while `userMessage` feeds the toast. The raw
// upstream body is still preserved in the provider output channel
// (`[http-error-body]` in transports/engine.ts), so no diagnostic is lost.
const hint = describeFreeTierRestriction(apiMessage) ?? describeRouterUnavailable(apiError, apiMessage);
const userMessage = `${providerDisplayName} API request failed (HTTP ${String(response.status)})${modelId ? ` for ${modelId}` : ""}: ${hint}${capacityHint}`;
const requestMessage = `${providerDisplayName} API request failed (${String(response.status)})${modelHint}${sizeHint}${capacityHint}: ${hint}`;
return new OpenCodeRequestError(requestMessage, userMessage);
}

Expand Down
13 changes: 6 additions & 7 deletions src/provider/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ import * as vscode from "vscode";
import {
appendApiPath,
apiRoutesFromBaseUrl,
CONFIG_SECTION,
DEFAULT_GO_API_BASE_URL,
DEFAULT_GO_GOOGLE_MODELS_URL,
DEFAULT_ZEN_API_BASE_URL,
FALLBACK_USER_AGENT,
FREE_ZEN_MODEL_IDS,
isZenFreeTierModel,
normalizeApiBaseUrl,
SETTING_API_BASE_URL,
SETTING_FREE_ONLY,
} from "../config";
import type { ModelEndpointKind } from "../core/registry";
import type { ApiMessage } from "../request/types";
Expand Down Expand Up @@ -236,10 +234,11 @@ export const PROVIDERS: Record<ProviderDefinition["vendor"], ProviderDefinition>
"qwen3.5-plus",
"big-pickle",
],
filterModel: (modelId) =>
vscode.workspace.getConfiguration(CONFIG_SECTION).get<boolean>(SETTING_FREE_ONLY, true)
? modelId.endsWith("-free") || FREE_ZEN_MODEL_IDS.has(modelId)
: true,
// Free-tier Zen models are always hidden: OpenCode gates them to its own
// clients since 2026-09-17 (403 FreeTierError, upstream #49580). The
// `freeOnly` setting is deprecated and deliberately ignored — honoring it
// would leave free-only users with a picker where every model 403s.
filterModel: (modelId) => !isZenFreeTierModel(modelId),
};
return {
[GO_VENDOR]: go,
Expand Down
11 changes: 11 additions & 0 deletions src/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
EXTENSION_ID,
FALLBACK_USER_AGENT,
FREE_ZEN_MODEL_IDS,
isZenFreeTierModel,
GO_LIMITS,
GO_MAX_LOG_ENTRIES,
GO_MAX_SESSIONS,
Expand Down Expand Up @@ -176,6 +177,16 @@ describe("config — model classification sets", () => {
assert.ok(!FREE_ZEN_MODEL_IDS.has(id), `${id} is both unavailable and free`);
}
});

it("classifies Zen free-tier models by suffix and known id (upstream #49580)", () => {
assert.ok(isZenFreeTierModel("deepseek-v4.1-flash-free"));
assert.ok(isZenFreeTierModel("big-pickle"));
// Go free-tier models share the -free suffix — the Zen filter is only
// applied to the Zen provider's filterModel, never globally.
assert.ok(isZenFreeTierModel("beam-free"));
assert.ok(!isZenFreeTierModel("gpt-5.4"));
assert.ok(!isZenFreeTierModel("deepseek-v4.1-flash"));
});
});

describe("config — storage keys", () => {
Expand Down
4 changes: 4 additions & 0 deletions src/test/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ describe("describeFreeTierRestriction (issues #235/#240)", () => {
);
assert.ok(error instanceof OpenCodeRequestError);
assert.match(error.userMessage, /restricts this free-tier model to their official app/);
// Chat renders `message` (issue #258): the hint must be there too, and the
// raw gateway text must not leak into the user-facing message.
assert.match(error.message, /restricts this free-tier model to their official app/);
assert.match(error.message, /muse-spark-1.2-contributor-free/);
assert.doesNotMatch(error.message, /can only be used from within OpenCode/);
});
});

Expand Down
Loading