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 @@ -4,8 +4,16 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente

## [Unreleased]

### Added

- **`[Providers]` Configurable API base URL.** The extension no longer hardcodes the `opencode.ai` endpoints β€” it can now point at any compatible gateway via `opencodego.apiBaseUrl` (default `https://opencode.ai/zen/go/v1`) and `opencodezen.apiBaseUrl` (default `https://opencode.ai/zen/v1`). The extension derives the `/chat/completions`, `/messages`, `/responses`, `/models`, and (Go only) `/usage` routes from the configured base and applies them to every provider (normal chat, Agents window variants, inline completions, usage sync). Custom URLs are normalized and validated β€” non-`http(s)`, embedded credentials, query strings, and hashes are rejected and fall back to the default. Reload the window after changing the setting. Unit tests added for URL normalization and route construction.

---

### Changed

- **`[Providers]` Configurable API base URL.** The extension no longer hardcodes the `opencode.ai` endpoints β€” it can now point at any compatible gateway via `opencodego.apiBaseUrl` (default `https://opencode.ai/zen/go/v1`) and `opencodezen.apiBaseUrl` (default `https://opencode.ai/zen/v1`). The extension derives the `/chat/completions`, `/messages`, `/responses`, `/models`, and (Go only) `/usage` routes from the configured base and applies them to every provider (normal chat, Agents window variants, inline completions, usage sync). Custom URLs are normalized and validated β€” non-`http(s)`, embedded credentials, query strings, and hashes are rejected and fall back to the default. Reload the window after changing the setting. Unit tests added for URL normalization and route construction.

- **`[Internal]` API keys are configured through the BYOK panel only.** The `OpenCode Go: Set API Key` / `OpenCode Zen: Set API Key` commands and the "Set / Clear API Key" menu items inside `Manage Provider` are removed β€” keys are entered once via **Chat: Manage Language Models β†’ "+ Add Models"** (the native BYOK flow). `SecretStorage` is no longer a user-facing entry point; it stays as an internal per-vendor mirror (`opencodego.apiKey` / `opencodezen.apiKey`) that the BYOK resolution writes so agent-host variants and cold-start requests inherit the group key. Splitting the secret per vendor also fixes a latent collision where Go and Zen shared a single `opencodego.apiKey` and overwrote each other's key. `Refresh Models` / `Test Connection` now point at the BYOK flow when no key is configured instead of prompting for one.

- **`[Internal]` Per-provider Thinking strategy classes + single config authority.** The thinking/reasoning system is refactored from one monolithic builder into a per-provider strategy (`src/thinking/`): an interface + factory (`provider.ts`), a shared base class, and one class per model family (`deepseek`, `glm`, `kimi`, `minimax`, `openai`, `qwen`, `mimo`, `fallback`). Each provider now owns its reasoning picker schema, its request-payload mapping, and whether its `reasoning_content` is surfaced as chat content. Configuration resolves from a **single authority** β€” the VS Code per-model configuration (model picker / Manage), with workspace settings and per-family defaults as fallbacks β€” instead of competing sources (workspace + modelConfiguration + a `globalState` shadow copy + defaults). The shadow copy is removed, so a thinking effort chosen for one model can no longer silently leak onto another model or override an explicit "Off". Model IDs are normalized to `effectiveModelId` (the `::sk-***` fp suffix is gone), which also stops the per-model settings group from being recreated on every pick. Request builders are split out of `extension.ts` into per-endpoint modules (`src/request/{types,schema,shared,openai,anthropic,google}.ts`). Windows tooling fixes: `scripts/lint.ts` runs npm `.cmd` shims through the shell and a new `.gitattributes` enforces LF normalization, so `npm run lint` (prettier + shellcheck) is green on Windows; `scripts/staged-lint.ts` and `isCwdInWorkspace` get the same treatment.
Expand Down
50 changes: 26 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,30 +381,32 @@ Provider diagnostics also include the VS Code/extension versions, extension host

## πŸ”§ Settings

| Setting | Default | Description |
| ----------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `opencodego.temperature` | `0.2` | Sampling temperature (`0`–`2`) |
| `opencodego.maxTokens` | `0` | Max output token override (`0` = per-model max) |
| `opencodego.maxInputTokens` | `0` | Context window override (`0` = per-model default) |
| `opencodego.debugReasoning` | `false` | Log `reasoning_content` to Output panel |
| `opencodego.requestTimeoutSeconds` | `600` | Total request timeout |
| `opencodego.streamIdleTimeoutSeconds` | `120` | Cancel if stream goes idle |
| `opencodego.showUsageStatusBar` | `true` | Show usage summary in status bar |
| `opencodego.showProviderPrefix` | `true` | Include `OpenCode Go` / `OpenCode Zen` in model names |
| `opencodego.visionProxyWholeConversation` | `false` | Vision proxy: describe the whole conversation instead of only the message with a new image (more context, more tokens) |
| `opencodego.freeOnly` | `true` | Zen: free models only. `false` = include paid |
| `opencodego.enabled` | `true` | Register the OpenCode Go provider. `false` removes it from Language Models & every picker (keys kept) |
| `opencodezen.enabled` | `true` | Register the OpenCode Zen provider. `false` removes it from Language Models & every picker (keys kept) |
| `opencodego.agentsWindow` | `true` | Expose agent-host model variants (`targetChatSessionType`) for the Agents window |
| `opencodego.showAgentModelsInManagePanel` | `false` | Show agent vendors in Manage Language Models panel |
| `opencodego.stripThinkTags` | `"auto"` | Strip `<think>` tags (`never`/`auto`/`always`) |
| `opencodego.thinking.deepseek` | `"off"` | `off`/`low`/`medium`/`high`/`max` |
| `opencodego.thinking.glm` | `"off"` | `off`/`high`/`max` |
| `opencodego.thinking.kimi` | `"off"` | `on`/`off` |
| `opencodego.thinking.minimax` | `"off"` | `off`/`on` |
| `opencodego.thinking.mimo` | `"off"` | `off`/`low`/`medium`/`high` |
| `opencodego.thinking.qwen` | `"off"` | `auto`/`on`/`off` |
| `opencodego.thinking.qwenBudget` | `"auto"` | `auto`/`4096`/`16384`/`32768`/`81920` |
| Setting | Default | Description |
| ----------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `opencodego.apiBaseUrl` | `https://opencode.ai/zen/go/v1` | Base URL for a Go-compatible gateway; the extension appends the required API routes. Reload after changing. |
| `opencodezen.apiBaseUrl` | `https://opencode.ai/zen/v1` | Base URL for a Zen-compatible gateway; the extension appends the required API routes. Reload after changing. |
| `opencodego.temperature` | `0.2` | Sampling temperature (`0`–`2`) |
| `opencodego.maxTokens` | `0` | Max output token override (`0` = per-model max) |
| `opencodego.maxInputTokens` | `0` | Context window override (`0` = per-model default) |
| `opencodego.debugReasoning` | `false` | Log `reasoning_content` to Output panel |
| `opencodego.requestTimeoutSeconds` | `600` | Total request timeout |
| `opencodego.streamIdleTimeoutSeconds` | `120` | Cancel if stream goes idle |
| `opencodego.showUsageStatusBar` | `true` | Show usage summary in status bar |
| `opencodego.showProviderPrefix` | `true` | Include `OpenCode Go` / `OpenCode Zen` in model names |
| `opencodego.visionProxyWholeConversation` | `false` | Vision proxy: describe the whole conversation instead of only the message with a new image (more context, more tokens) |
| `opencodego.freeOnly` | `true` | Zen: free models only. `false` = include paid |
| `opencodego.enabled` | `true` | Register the OpenCode Go provider. `false` removes it from Language Models & every picker (keys kept) |
| `opencodezen.enabled` | `true` | Register the OpenCode Zen provider. `false` removes it from Language Models & every picker (keys kept) |
| `opencodego.agentsWindow` | `true` | Expose agent-host model variants (`targetChatSessionType`) for the Agents window |
| `opencodego.showAgentModelsInManagePanel` | `false` | Show agent vendors in Manage Language Models panel |
| `opencodego.stripThinkTags` | `"auto"` | Strip `<think>` tags (`never`/`auto`/`always`) |
| `opencodego.thinking.deepseek` | `"off"` | `off`/`low`/`medium`/`high`/`max` |
| `opencodego.thinking.glm` | `"off"` | `off`/`high`/`max` |
| `opencodego.thinking.kimi` | `"off"` | `on`/`off` |
| `opencodego.thinking.minimax` | `"off"` | `off`/`on` |
| `opencodego.thinking.mimo` | `"off"` | `off`/`low`/`medium`/`high` |
| `opencodego.thinking.qwen` | `"off"` | `auto`/`on`/`off` |
| `opencodego.thinking.qwenBudget` | `"auto"` | `auto`/`4096`/`16384`/`32768`/`81920` |

<details>
<summary><b>πŸ“œ Full settings reference with descriptions</b></summary>
Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,16 @@
"maximum": 2,
"description": "Sampling temperature used for chat completions."
},
"opencodego.apiBaseUrl": {
"type": "string",
"default": "https://opencode.ai/zen/go/v1",
"description": "Base URL for the OpenCode Go-compatible API. The extension appends /models, /chat/completions, /messages, /responses, and /usage. Requires a window reload after changing."
},
"opencodezen.apiBaseUrl": {
"type": "string",
"default": "https://opencode.ai/zen/v1",
"description": "Base URL for the OpenCode Zen-compatible API. The extension appends /models, /chat/completions, /messages, and /responses. Requires a window reload after changing."
},
"opencodego.maxTokens": {
"type": "number",
"default": 0,
Expand Down
20 changes: 20 additions & 0 deletions src/agentProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/** Build an agent-host variant while preserving the base provider endpoints. */
export function providerVariant<T extends { vendor: string; displayName: string }, AgentVendor extends string, BaseVendor extends string>(
base: T,
agentVendor: AgentVendor,
displayName: string,
baseVendor: BaseVendor,
): Omit<T, "vendor" | "displayName"> & {
vendor: AgentVendor;
displayName: string;
isAgentVariant: true;
baseVendor: BaseVendor;
} {
return {
...base,
vendor: agentVendor,
displayName,
isAgentVariant: true,
baseVendor,
};
}
29 changes: 29 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const SETTING_TEMPERATURE = "temperature";
export const SETTING_MAX_TOKENS = "maxTokens";
export const SETTING_MAX_INPUT_TOKENS = "maxInputTokens";
export const SETTING_DEBUG_REASONING = "debugReasoning";
/** Base URL setting key for the provider's OpenAI-compatible API. */
export const SETTING_API_BASE_URL = "apiBaseUrl";
export const SETTING_REQUEST_TIMEOUT_SECONDS = "requestTimeoutSeconds";
export const SETTING_STREAM_IDLE_TIMEOUT_SECONDS = "streamIdleTimeoutSeconds";
export const SETTING_STRIP_THINK_TAGS = "stripThinkTags";
Expand Down Expand Up @@ -115,6 +117,33 @@ export const MODEL_METADATA_CACHE_TTL_MS = 1 * 60 * 60 * 1000;
export const DEFAULT_MODEL_CONTEXT_WINDOW = 262144;
export const DEFAULT_MODEL_MAX_OUTPUT_TOKENS = 65536;

// ─── Provider API endpoints ─────────────────────────────────────────────────

/** Default OpenCode Go API base URL; can be overridden in VS Code settings. */
export const DEFAULT_GO_API_BASE_URL = "https://opencode.ai/zen/go/v1";
/** Default OpenCode Zen API base URL; can be overridden in VS Code settings. */
export const DEFAULT_ZEN_API_BASE_URL = "https://opencode.ai/zen/v1";

/** Normalize a configured API base URL, falling back when it is malformed. */
export function normalizeApiBaseUrl(value: string, fallback: string): string {
const candidate = value.trim();
if (!candidate) return fallback;
try {
const url = new URL(candidate);
if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password || url.search || url.hash) {
return fallback;
}
return url.toString().replace(/\/+$/, "");
} catch {
return fallback;
}
}

/** Append one API route to a normalized or user-supplied base URL. */
export function appendApiPath(baseUrl: string, route: string): string {
return `${baseUrl.replace(/\/+$/, "")}/${route.replace(/^\/+/, "")}`;
}

// ─── Output budget / token-estimate margins ──────────────────────────────────

/** Reserve for UI rendering so the advertised output never claims the full window. */
Expand Down
27 changes: 27 additions & 0 deletions src/test/agentProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { providerVariant } from "../agentProvider.js";

describe("providerVariant", () => {
it("preserves configured endpoints and marks the provider as an agent variant", () => {
const base = {
vendor: "opencodego",
displayName: "OpenCode Go",
baseUrl: "https://example.test/custom/v1",
modelsUrl: "https://example.test/custom/v1/models",
chatCompletionsUrl: "https://example.test/custom/v1/chat/completions",
messagesUrl: "https://example.test/custom/v1/messages",
responsesUrl: "https://example.test/custom/v1/responses",
};

const agent = providerVariant(base, "opencodego-agent", "OpenCode Go (Agents)", "opencodego");

assert.equal(agent.isAgentVariant, true);
assert.equal(agent.baseVendor, "opencodego");
assert.equal(agent.vendor, "opencodego-agent");
assert.equal(agent.modelsUrl, base.modelsUrl);
assert.equal(agent.chatCompletionsUrl, base.chatCompletionsUrl);
assert.equal(agent.messagesUrl, base.messagesUrl);
assert.equal(agent.responsesUrl, base.responsesUrl);
});
});
Loading
Loading