From a26ef33630d59aa374d76256a48297fdb49b3ea0 Mon Sep 17 00:00:00 2001 From: Justin Buhagiar Date: Fri, 14 Aug 2026 09:54:19 +1000 Subject: [PATCH 1/4] feat(providers): add configurable API base URL for Go and Zen providers Allow users to point the extension at a compatible gateway instead of the hardcoded opencode.ai endpoints. The extension now reads `opencodego.apiBaseUrl` and `opencodezen.apiBaseUrl` settings, derives the /chat/completions, /messages, /responses, /models, and (Go only) /usage routes from the configured base, and falls back to the defaults when the value is missing or malformed. - Normalize custom URLs and reject non-http(s), embedded credentials, query strings, and hashes via normalizeApiBaseUrl(). - Route every provider (normal, Agents window, inline completions, usage sync) through the configured base URL. - Add unit tests for URL normalization and route construction. --- README.md | 2 ++ package.json | 10 ++++++ src/config.ts | 29 +++++++++++++++ src/extension.ts | 68 +++++++++++++++++++++++++++++------- src/goUsageSync.ts | 3 +- src/goUsageTracker.ts | 6 ++-- src/test/config.test.ts | 18 ++++++++++ src/test/goUsageSync.test.ts | 12 +++++++ 8 files changed, 132 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 486b8a3..c4b7017 100644 --- a/README.md +++ b/README.md @@ -383,6 +383,8 @@ Provider diagnostics also include the VS Code/extension versions, extension host | 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) | diff --git a/package.json b/package.json index b228d1c..b49cc92 100644 --- a/package.json +++ b/package.json @@ -204,6 +204,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, diff --git a/src/config.ts b/src/config.ts index f0ca714..114095b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -38,6 +38,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"; @@ -105,6 +107,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. */ diff --git a/src/extension.ts b/src/extension.ts index ccedda7..68617d7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -83,9 +83,14 @@ import { MODEL_LIST_FETCH_TIMEOUT_MS, MODEL_METADATA_FETCH_TIMEOUT_MS, OPEN_CODE_CLIENT, + appendApiPath, + DEFAULT_GO_API_BASE_URL, + DEFAULT_ZEN_API_BASE_URL, + normalizeApiBaseUrl, RECENT_TRANSPORT_SUMMARY_LIMIT, RECENT_TRANSPORT_SUMMARY_STORAGE_PREFIX, SECRET_KEY, + SETTING_API_BASE_URL, SETTING_AGENTS_WINDOW, SETTING_AUTO_ENABLE_AGENTS_WINDOW, SETTING_DEBUG_REASONING, @@ -212,6 +217,7 @@ function usageTrackerOptions(): GoUsageTrackerOptions { config().get(SETTING_USAGE_TODAY_YESTERDAY_SOURCE, DEFAULT_USAGE_TODAY_YESTERDAY_SOURCE), resolveCodebaseWindowDays: () => config().get(SETTING_USAGE_CODEBASE_WINDOW_DAYS, DEFAULT_USAGE_CODEBASE_WINDOW_DAYS), resolveDayBoundary: () => config().get<"utc" | "local">(SETTING_USAGE_DAY_BOUNDARY, DEFAULT_USAGE_DAY_BOUNDARY), + resolveUsageUrl: () => configuredGoUsageUrl(), }; } @@ -378,10 +384,12 @@ interface ProviderDefinition { vendor: AllProviderVendor; displayName: string; modelNamePrefix: string; + baseUrl: string; modelsUrl: string; chatCompletionsUrl: string; messagesUrl: string; responsesUrl?: string; + usageUrl?: string; testModelId: string; fallbackModels: string[]; filterModel?: (modelId: string) => boolean; @@ -469,10 +477,12 @@ function providerVariant( vendor: agentVendor, displayName, modelNamePrefix: base.modelNamePrefix, + baseUrl: base.baseUrl, modelsUrl: base.modelsUrl, chatCompletionsUrl: base.chatCompletionsUrl, messagesUrl: base.messagesUrl, responsesUrl: base.responsesUrl, + usageUrl: base.usageUrl, testModelId: base.testModelId, fallbackModels: base.fallbackModels, filterModel: base.filterModel, @@ -484,10 +494,12 @@ const PROVIDERS: Record = (() vendor: GO_VENDOR, displayName: "OpenCode Go", modelNamePrefix: "OpenCode Go", - modelsUrl: "https://opencode.ai/zen/go/v1/models", - chatCompletionsUrl: "https://opencode.ai/zen/go/v1/chat/completions", - messagesUrl: "https://opencode.ai/zen/go/v1/messages", - responsesUrl: "https://opencode.ai/zen/go/v1/responses", + baseUrl: DEFAULT_GO_API_BASE_URL, + modelsUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "models"), + chatCompletionsUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "chat/completions"), + messagesUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "messages"), + responsesUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "responses"), + usageUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "usage"), testModelId: "deepseek-v4-flash", fallbackModels: [ "deepseek-v4-pro", @@ -514,10 +526,11 @@ const PROVIDERS: Record = (() vendor: ZEN_VENDOR, displayName: "OpenCode Zen", modelNamePrefix: "OpenCode Zen", - modelsUrl: "https://opencode.ai/zen/v1/models", - chatCompletionsUrl: "https://opencode.ai/zen/v1/chat/completions", - messagesUrl: "https://opencode.ai/zen/v1/messages", - responsesUrl: "https://opencode.ai/zen/v1/responses", + baseUrl: DEFAULT_ZEN_API_BASE_URL, + modelsUrl: appendApiPath(DEFAULT_ZEN_API_BASE_URL, "models"), + chatCompletionsUrl: appendApiPath(DEFAULT_ZEN_API_BASE_URL, "chat/completions"), + messagesUrl: appendApiPath(DEFAULT_ZEN_API_BASE_URL, "messages"), + responsesUrl: appendApiPath(DEFAULT_ZEN_API_BASE_URL, "responses"), testModelId: "deepseek-v4-flash-free", fallbackModels: [ "claude-opus-4-7", @@ -580,6 +593,33 @@ const PROVIDERS: Record = (() }; })(); +/** Read a provider base URL from settings, falling back safely if malformed. */ +function configuredApiBaseUrl(vendor: typeof GO_VENDOR | typeof ZEN_VENDOR): string { + const fallback = vendor === GO_VENDOR ? DEFAULT_GO_API_BASE_URL : DEFAULT_ZEN_API_BASE_URL; + const configured = vscode.workspace.getConfiguration().get(`${vendor}.${SETTING_API_BASE_URL}`, fallback); + return normalizeApiBaseUrl(configured, fallback); +} + +/** Resolve the Go usage route from the same base URL as the Go provider. */ +function configuredGoUsageUrl(): string { + return appendApiPath(configuredApiBaseUrl(GO_VENDOR), "usage"); +} + +/** Build a provider definition using the user-configured API base URL. */ +function configuredProviderDefinition(vendor: typeof GO_VENDOR | typeof ZEN_VENDOR): ProviderDefinition { + const base = PROVIDERS[vendor]; + const baseUrl = configuredApiBaseUrl(vendor); + return { + ...base, + baseUrl, + modelsUrl: appendApiPath(baseUrl, "models"), + chatCompletionsUrl: appendApiPath(baseUrl, "chat/completions"), + messagesUrl: appendApiPath(baseUrl, "messages"), + responsesUrl: appendApiPath(baseUrl, "responses"), + usageUrl: vendor === GO_VENDOR ? appendApiPath(baseUrl, "usage") : undefined, + }; +} + type ApiRole = "user" | "assistant" | "tool"; interface OpenCodeModel extends vscode.LanguageModelChatInformation { @@ -850,8 +890,10 @@ export function activate(context: vscode.ExtensionContext) { // section, which would misread the Zen flag as opencodego.opencodezen.enabled. const goProviderEnabled = vscode.workspace.getConfiguration().get(providerEnabledSetting(GO_VENDOR), true); const zenProviderEnabled = vscode.workspace.getConfiguration().get(providerEnabledSetting(ZEN_VENDOR), true); - const goProvider = new OpenCodeProvider(context, PROVIDERS[GO_VENDOR]); - const zenProvider = new OpenCodeProvider(context, PROVIDERS[ZEN_VENDOR]); + const goDefinition = configuredProviderDefinition(GO_VENDOR); + const zenDefinition = configuredProviderDefinition(ZEN_VENDOR); + const goProvider = new OpenCodeProvider(context, goDefinition); + const zenProvider = new OpenCodeProvider(context, zenDefinition); const modelInfoProviders: OpenCodeProvider[] = [goProvider, zenProvider]; const subscriptions: vscode.Disposable[] = [ @@ -1037,8 +1079,8 @@ export function activate(context: vscode.ExtensionContext) { // Agent-host providers for the Copilot Agents window (opt-in via config). const enableAgents = vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_AGENTS_WINDOW, true); if (enableAgents && (goProviderEnabled || zenProviderEnabled)) { - const agentGoProvider = new OpenCodeProvider(context, PROVIDERS[AGENT_GO_VENDOR]); - const agentZenProvider = new OpenCodeProvider(context, PROVIDERS[AGENT_ZEN_VENDOR]); + const agentGoProvider = new OpenCodeProvider(context, providerVariant(goDefinition, AGENT_GO_VENDOR, "OpenCode Go (Agents)")); + const agentZenProvider = new OpenCodeProvider(context, providerVariant(zenDefinition, AGENT_ZEN_VENDOR, "OpenCode Zen (Agents)")); modelInfoProviders.push(agentGoProvider, agentZenProvider); subscriptions.push( ...(goProviderEnabled ? [vscode.lm.registerLanguageModelChatProvider(AGENT_GO_VENDOR, agentGoProvider)] : []), @@ -1099,7 +1141,7 @@ export function activate(context: vscode.ExtensionContext) { // Experimental inline code suggestions (issue #49). Opt-in via // `opencodego.inlineSuggestions`; the provider reads the config live. registerInlineCompletions(context, { - chatCompletionsUrl: PROVIDERS[GO_VENDOR].chatCompletionsUrl, + chatCompletionsUrl: goDefinition.chatCompletionsUrl, // Same resolution order as the chat path: the active profile's own key // first (covers multi-profile / BYOK-group setups), then the secret. resolveApiKey: async () => profileApiKeys.get(activeProfileFingerprint) ?? _extensionContext?.secrets.get(SECRET_KEY), diff --git a/src/goUsageSync.ts b/src/goUsageSync.ts index d644a1c..b23880b 100644 --- a/src/goUsageSync.ts +++ b/src/goUsageSync.ts @@ -64,13 +64,14 @@ export async function fetchGoUsage( apiKey: string, fetcher: typeof fetch = fetch, timeoutMs: number = GO_USAGE_FETCH_TIMEOUT_MS, + endpointUrl: string = GO_USAGE_API_URL, ): Promise { if (!apiKey) { return { ok: false, reason: "no-key" }; } let response: Response; try { - response = await fetcher(GO_USAGE_API_URL, { + response = await fetcher(endpointUrl, { method: "GET", headers: { Authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout(timeoutMs), diff --git a/src/goUsageTracker.ts b/src/goUsageTracker.ts index d66df4c..44c9a9d 100644 --- a/src/goUsageTracker.ts +++ b/src/goUsageTracker.ts @@ -122,6 +122,8 @@ export interface GoUsageTrackerOptions { resolveCodebaseWindowDays?: () => number; /** Day boundary for Today/Yesterday ("utc" default | "local"). */ resolveDayBoundary?: () => "utc" | "local"; + /** Usage endpoint derived from the configured Go API base URL. */ + resolveUsageUrl?: () => string; } interface UsageBaselinePeriod { @@ -928,7 +930,7 @@ export class GoUsageTracker { if (this.serverUsageFetchedAt > 0 && now - this.serverUsageFetchedAt < GO_USAGE_SYNC_TTL_MS) { return false; } - const result = await fetchGoUsage(apiKey); + const result = await fetchGoUsage(apiKey, fetch, undefined, this.options.resolveUsageUrl?.()); // Pace retries after failures too — an invalid key or unreachable // endpoint must not hammer the API on every request. this.serverUsageFetchedAt = Date.now(); @@ -939,7 +941,7 @@ export class GoUsageTracker { this.serverUsage = result.data; // Persist so the next window start can render the meters instantly. void this.context.globalState.update(this.storageKey(GO_SERVER_USAGE_KEY), result.data); - this.log?.("[go-usage] Server usage synced from /zen/go/v1/usage."); + this.log?.("[go-usage] Server usage synced."); return true; } diff --git a/src/test/config.test.ts b/src/test/config.test.ts index 4c98e76..45181ff 100644 --- a/src/test/config.test.ts +++ b/src/test/config.test.ts @@ -2,6 +2,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { ACTIVE_PROFILE_KEY, + appendApiPath, AGENT_HOST_BYOK_MINOR_VERSION, COMPLETION_REQUEST_TIMEOUT_MS, CONFIG_SECTION, @@ -11,6 +12,8 @@ import { DEFAULT_INLINE_PREFIX_LINES, DEFAULT_INLINE_SUFFIX_CHARS, DEFAULT_INLINE_TIMEOUT_MS, + DEFAULT_GO_API_BASE_URL, + DEFAULT_ZEN_API_BASE_URL, DEFAULT_MODEL_CONTEXT_WINDOW, DEFAULT_MODEL_MAX_OUTPUT_TOKENS, DEFAULT_REQUEST_TIMEOUT_MS, @@ -37,6 +40,7 @@ import { MODEL_METADATA_CACHE_TTL_MS, MODEL_METADATA_REVISION, MODELS_DEV_API_URL, + normalizeApiBaseUrl, PROFILES_REGISTRY_KEY, REASONING_CACHE_LIMIT, RECENT_TRANSPORT_SUMMARY_LIMIT, @@ -194,3 +198,17 @@ describe("config — references", () => { expectValue("vision prompt", DEFAULT_VISION_PROXY_PROMPT, (v) => v.length > 0, "non-empty"); }); }); + +describe("config — provider API URLs", () => { + it("builds routes from the default bases", () => { + assert.equal(appendApiPath(DEFAULT_GO_API_BASE_URL, "/chat/completions"), "https://opencode.ai/zen/go/v1/chat/completions"); + assert.equal(appendApiPath(`${DEFAULT_ZEN_API_BASE_URL}/`, "models"), "https://opencode.ai/zen/v1/models"); + }); + + it("normalizes safe custom HTTP(S) bases and rejects unsafe values", () => { + assert.equal(normalizeApiBaseUrl("https://gateway.example.test/custom/v1///", DEFAULT_GO_API_BASE_URL), "https://gateway.example.test/custom/v1"); + assert.equal(normalizeApiBaseUrl("http://localhost:8080/v1", DEFAULT_GO_API_BASE_URL), "http://localhost:8080/v1"); + assert.equal(normalizeApiBaseUrl("javascript:alert(1)", DEFAULT_GO_API_BASE_URL), DEFAULT_GO_API_BASE_URL); + assert.equal(normalizeApiBaseUrl("https://user:pass@gateway.example.test/v1", DEFAULT_GO_API_BASE_URL), DEFAULT_GO_API_BASE_URL); + }); +}); diff --git a/src/test/goUsageSync.test.ts b/src/test/goUsageSync.test.ts index 3fcf801..774deae 100644 --- a/src/test/goUsageSync.test.ts +++ b/src/test/goUsageSync.test.ts @@ -55,6 +55,18 @@ test("fetchGoUsage — sends the key as Bearer to the official endpoint", async assert.equal(result.ok, true); }); +test("fetchGoUsage — accepts a custom usage endpoint", async () => { + let requestedUrl = ""; + const fetcher: typeof fetch = (input) => { + requestedUrl = typeof input === "string" ? input : ""; + return Promise.resolve(new Response(JSON.stringify(apiResponse()), { status: 200 })); + }; + + const result = await fetchGoUsage("sk-test", fetcher, undefined, "https://gateway.example.test/v1/usage"); + assert.equal(requestedUrl, "https://gateway.example.test/v1/usage"); + assert.equal(result.ok, true); +}); + test("fetchGoUsage — parses a 200 payload", async () => { const result = await fetchGoUsage("sk-test", stubFetch(200, apiResponse())); assert.ok(result.ok); From 6cfe583aff170ff42cb08b2368b02ddbdb9dcbbe Mon Sep 17 00:00:00 2001 From: Justin Buhagiar Date: Fri, 14 Aug 2026 09:59:49 +1000 Subject: [PATCH 2/4] docs(changelog): note configurable API base URL under Unreleased --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a051bd..89dd778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documented here. +## [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. + +--- + ## [0.6.0] — 2026-08-13 ### Added From 3bcce3e6a82a14fc485b273af99edab3541a866f Mon Sep 17 00:00:00 2001 From: Justin Buhagiar Date: Fri, 14 Aug 2026 16:00:03 +1000 Subject: [PATCH 3/4] fix: preserve agent provider metadata for configurable API bases Keep agent-host providers marked as agent variants and retain their base vendor while inheriting configured provider endpoints. Add regression coverage for agent metadata and endpoint preservation. --- src/agentProvider.ts | 19 +++++++++++++++ src/extension.ts | 44 ++++++++-------------------------- src/test/agentProvider.test.ts | 27 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 34 deletions(-) create mode 100644 src/agentProvider.ts create mode 100644 src/test/agentProvider.test.ts diff --git a/src/agentProvider.ts b/src/agentProvider.ts new file mode 100644 index 0000000..e534b1b --- /dev/null +++ b/src/agentProvider.ts @@ -0,0 +1,19 @@ +/** 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 & { + vendor: AgentVendor; + displayName: string; + isAgentVariant: true; + baseVendor: BaseVendor; +} { + return { + ...base, + vendor: agentVendor, + displayName, + isAgentVariant: true, + baseVendor, + }; +} diff --git a/src/extension.ts b/src/extension.ts index 68617d7..f93e7c9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -46,6 +46,7 @@ import { type ProviderVendor, } from "./providerTypes"; import { providerEnabledSetting } from "./providerEnablement"; +import { providerVariant } from "./agentProvider"; import { isInternalDataPart, isReasoningMarkerPart, readReasoningMarker } from "./chatParts"; import { registerInlineCompletions } from "./autocomplete"; import { completionUsageToSeries, type CompletionUsageDay } from "./autocomplete/usage"; @@ -389,7 +390,6 @@ interface ProviderDefinition { chatCompletionsUrl: string; messagesUrl: string; responsesUrl?: string; - usageUrl?: string; testModelId: string; fallbackModels: string[]; filterModel?: (modelId: string) => boolean; @@ -467,29 +467,7 @@ function isTransientFetchError(error: unknown): boolean { return false; } -/** Create an agent-variant provider definition that inherits URLs, models, and filters from a base. */ -function providerVariant( - base: ProviderDefinition, - agentVendor: typeof AGENT_GO_VENDOR | typeof AGENT_ZEN_VENDOR, - displayName: string, -): ProviderDefinition { - return { - vendor: agentVendor, - displayName, - modelNamePrefix: base.modelNamePrefix, - baseUrl: base.baseUrl, - modelsUrl: base.modelsUrl, - chatCompletionsUrl: base.chatCompletionsUrl, - messagesUrl: base.messagesUrl, - responsesUrl: base.responsesUrl, - usageUrl: base.usageUrl, - testModelId: base.testModelId, - fallbackModels: base.fallbackModels, - filterModel: base.filterModel, - }; -} - -const PROVIDERS: Record = (() => { +const PROVIDERS: Record = (() => { const go: ProviderDefinition = { vendor: GO_VENDOR, displayName: "OpenCode Go", @@ -499,7 +477,6 @@ const PROVIDERS: Record = (() chatCompletionsUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "chat/completions"), messagesUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "messages"), responsesUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "responses"), - usageUrl: appendApiPath(DEFAULT_GO_API_BASE_URL, "usage"), testModelId: "deepseek-v4-flash", fallbackModels: [ "deepseek-v4-pro", @@ -584,12 +561,6 @@ const PROVIDERS: Record = (() return { [GO_VENDOR]: go, [ZEN_VENDOR]: zen, - [AGENT_GO_VENDOR]: { ...providerVariant(go, AGENT_GO_VENDOR, "OpenCode Go (Agents)"), isAgentVariant: true, baseVendor: GO_VENDOR }, - [AGENT_ZEN_VENDOR]: { - ...providerVariant(zen, AGENT_ZEN_VENDOR, "OpenCode Zen (Agents)"), - isAgentVariant: true, - baseVendor: ZEN_VENDOR, - }, }; })(); @@ -616,7 +587,6 @@ function configuredProviderDefinition(vendor: typeof GO_VENDOR | typeof ZEN_VEND chatCompletionsUrl: appendApiPath(baseUrl, "chat/completions"), messagesUrl: appendApiPath(baseUrl, "messages"), responsesUrl: appendApiPath(baseUrl, "responses"), - usageUrl: vendor === GO_VENDOR ? appendApiPath(baseUrl, "usage") : undefined, }; } @@ -1079,8 +1049,14 @@ export function activate(context: vscode.ExtensionContext) { // Agent-host providers for the Copilot Agents window (opt-in via config). const enableAgents = vscode.workspace.getConfiguration(CONFIG_SECTION).get(SETTING_AGENTS_WINDOW, true); if (enableAgents && (goProviderEnabled || zenProviderEnabled)) { - const agentGoProvider = new OpenCodeProvider(context, providerVariant(goDefinition, AGENT_GO_VENDOR, "OpenCode Go (Agents)")); - const agentZenProvider = new OpenCodeProvider(context, providerVariant(zenDefinition, AGENT_ZEN_VENDOR, "OpenCode Zen (Agents)")); + const agentGoProvider = new OpenCodeProvider( + context, + providerVariant(goDefinition, AGENT_GO_VENDOR, "OpenCode Go (Agents)", GO_VENDOR), + ); + const agentZenProvider = new OpenCodeProvider( + context, + providerVariant(zenDefinition, AGENT_ZEN_VENDOR, "OpenCode Zen (Agents)", ZEN_VENDOR), + ); modelInfoProviders.push(agentGoProvider, agentZenProvider); subscriptions.push( ...(goProviderEnabled ? [vscode.lm.registerLanguageModelChatProvider(AGENT_GO_VENDOR, agentGoProvider)] : []), diff --git a/src/test/agentProvider.test.ts b/src/test/agentProvider.test.ts new file mode 100644 index 0000000..a7df893 --- /dev/null +++ b/src/test/agentProvider.test.ts @@ -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); + }); +}); From ed601579f5dd776a5c363a59874fc525edb204b1 Mon Sep 17 00:00:00 2001 From: jbuhagiar88 Date: Fri, 14 Aug 2026 21:59:59 +0000 Subject: [PATCH 4/4] Fixed linting issues --- README.md | 52 ++++++++++++++++++++--------------------- src/agentProvider.ts | 11 +++++---- src/test/config.test.ts | 5 +++- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index c4b7017..6714ee1 100644 --- a/README.md +++ b/README.md @@ -381,32 +381,32 @@ Provider diagnostics also include the VS Code/extension versions, extension host ## 🔧 Settings -| 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 `` 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 `` 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` |
📜 Full settings reference with descriptions diff --git a/src/agentProvider.ts b/src/agentProvider.ts index e534b1b..ebc7567 100644 --- a/src/agentProvider.ts +++ b/src/agentProvider.ts @@ -1,9 +1,10 @@ /** 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 & { +export function providerVariant( + base: T, + agentVendor: AgentVendor, + displayName: string, + baseVendor: BaseVendor, +): Omit & { vendor: AgentVendor; displayName: string; isAgentVariant: true; diff --git a/src/test/config.test.ts b/src/test/config.test.ts index 45181ff..5630afa 100644 --- a/src/test/config.test.ts +++ b/src/test/config.test.ts @@ -206,7 +206,10 @@ describe("config — provider API URLs", () => { }); it("normalizes safe custom HTTP(S) bases and rejects unsafe values", () => { - assert.equal(normalizeApiBaseUrl("https://gateway.example.test/custom/v1///", DEFAULT_GO_API_BASE_URL), "https://gateway.example.test/custom/v1"); + assert.equal( + normalizeApiBaseUrl("https://gateway.example.test/custom/v1///", DEFAULT_GO_API_BASE_URL), + "https://gateway.example.test/custom/v1", + ); assert.equal(normalizeApiBaseUrl("http://localhost:8080/v1", DEFAULT_GO_API_BASE_URL), "http://localhost:8080/v1"); assert.equal(normalizeApiBaseUrl("javascript:alert(1)", DEFAULT_GO_API_BASE_URL), DEFAULT_GO_API_BASE_URL); assert.equal(normalizeApiBaseUrl("https://user:pass@gateway.example.test/v1", DEFAULT_GO_API_BASE_URL), DEFAULT_GO_API_BASE_URL);