From acb7860129d3c6eca88287723d33faf43ca1e28f Mon Sep 17 00:00:00 2001 From: ltmoerdani Date: Tue, 13 Oct 2026 14:41:15 +0700 Subject: [PATCH] fix(providers): honor the apiBaseUrl settings across every request path (fixes #247) Re-wire the PR #149 feature dropped by later structural refactors: the provider definitions had reverted to hardcoded opencode.ai URLs and the settings wiring (configuredApiBaseUrl/configuredGoUsageUrl) was gone, leaving orphaned helpers and an unsupplied resolveUsageUrl tracker hook. - config.ts: pure apiRoutesFromBaseUrl() derives all routes from one base - provider/definitions.ts: PROVIDERS derives URLs from the configured opencodego/opencodezen.apiBaseUrl (root-config full-key read, normalized, safe fallback); agent variants inherit via providerVariant - usage/dashboard/state.ts: supply resolveUsageUrl so Go usage sync follows the configured base too - tests: apiRoutesFromBaseUrl coverage (custom + default base) - docs: issues/110 + CHANGELOG (Unreleased) --- CHANGELOG.md | 4 +- ...-20261013-issue247-api-base-url-ignored.md | 46 +++++++++++++ src/config.ts | 27 ++++++++ src/provider/definitions.ts | 66 ++++++++++++++++--- src/test/config.test.ts | 18 +++++ src/usage/dashboard/state.ts | 4 ++ 6 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 docs/issues/110-20261013-issue247-api-base-url-ignored.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e867228..72bd92985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documented here. -## [0.7.9] — 2026-10-04 +## [0.7.9] — unreleased ### Fixed +- **`[Providers]` The `apiBaseUrl` settings actually apply again — every request path now honors a custom compatible gateway (#247).** `opencodego.apiBaseUrl` / `opencodezen.apiBaseUrl` were accepted but ignored: PR #149 had fully implemented them, but later structural refactors reverted the provider definitions to hardcoded `opencode.ai` URLs and dropped the settings wiring, leaving only orphaned helpers (and a `resolveUsageUrl` tracker hook no one supplied). The base URL is now read once at startup from the root configuration with the full setting key, normalized (non-http(s), embedded credentials, query/hash fall back to the default), and derived into every route — chat transports, Agents window variants, model discovery, connection test, inline completions, and the Go `/usage` sync — via a single pure helper (`apiRoutesFromBaseUrl`). Reload after changing, as documented. Documented in `docs/issues/110-20261013-issue247-api-base-url-ignored.md`. + - **`[Thinking]` The per-model Reasoning Effort picker now actually works for models with no known thinking family (#252).** For any model not matched by a hardcoded family in `core/registry.ts` (`space-bunny-free`, `big-pickle`, `hy3`, …) the picker rendered a Thinking Effort control whose selection was silently discarded — the request never carried a reasoning field, so the model always answered at its default effort. Two independent defects: `FallbackThinking` had no payload path at all (`buildPayload` returned `{}`), _and_ all four `buildPayload` call sites called `thinkingProviderFor(modelId)` without metadata, so any metadata-driven gate could never fire. models.dev is now the source of truth: `metadata.reasoning` decides whether a model gets a control, its own `reasoning_options` decide which levels (falling back to a shared `off`/`low`/`medium`/`high`/`xhigh`/`max` ladder), and the pick is sent as a plain OpenAI-compatible `reasoning_effort`. A new `opencodego.thinking.generic` setting provides a workspace-wide default. Family-less models routed to non-chat endpoints (`claude-*` → Anthropic messages, `gemini-*` → Google, `grok-*` → Responses) are deliberately excluded — `reasoning_effort` is meaningless on those wire formats — and the `big-pickle` repetition penalty (issue #207) is preserved and composes with the effort field. Fixing this exposed a second bug in the same feature: `prepareChatRequest` resolved thinking config _before_ metadata was available and without passing it, so the generic strategy dropped the per-model pick outright while the workspace setting kept working — a confusing split where the Copilot dropdown did nothing but settings worked. Metadata is now a required field on `ResolveThinkingConfigInput` so the compiler rejects that call shape. Documented in `docs/issues/109-20261004-issue252-generic-reasoning-effort.md`. ## [0.7.8] — 2026-09-25 diff --git a/docs/issues/110-20261013-issue247-api-base-url-ignored.md b/docs/issues/110-20261013-issue247-api-base-url-ignored.md new file mode 100644 index 000000000..f142a83a4 --- /dev/null +++ b/docs/issues/110-20261013-issue247-api-base-url-ignored.md @@ -0,0 +1,46 @@ +# Issue #247 — `apiBaseUrl` Settings Ignored (Regression From PR #149) + +**Status:** 🟢 Active → ✅ Solved (this fix) +**Date:** 2026-10-13 +**Issue:** ltmoerdani/opencode-copilot-chat#247 +**Reporter:** NameMeLeo (Leo Choi) + +## Problem + +The `opencodego.apiBaseUrl` and `opencodezen.apiBaseUrl` settings (declared in `package.json`, documented in README/CHANGELOG) were accepted but **completely ignored at runtime** — every request still went to the built-in `https://opencode.ai/zen/{go/,}v1` endpoints, making a custom compatible gateway unusable in v0.7.5+. + +## Root Cause — a refactor regression, not a missing feature + +The feature was fully implemented and merged in **PR #149** (commit `a26ef33`, Justin Buhagiar, 2026-08-14): `configuredApiBaseUrl()` / `configuredGoUsageUrl()` in `extension.ts` read the settings and derived every route, with unit tests. Surviving remnants (`DEFAULT_GO/ZEN_API_BASE_URL`, `normalizeApiBaseUrl()`, `appendApiPath()`, `SETTING_API_BASE_URL` in `config.ts` + their tests) prove the merge happened. + +Later structural refactors dropped the wiring while keeping the helpers: + +- `8b1176f` (extract `provider/definitions.ts`) reverted the provider definitions to **hardcoded static URLs**. +- `e8fb6b2` / `2a3589d` (extract `OpenCodeProvider`, commands) removed `configuredApiBaseUrl()` / `configuredGoUsageUrl()` from `extension.ts` entirely. +- The Go usage tracker kept its **half-wired hook**: `GoUsageTrackerOptions.resolveUsageUrl` (`trackerTypes.ts`) and its consumer (`tracker.ts` → `fetchGoUsage(endpointUrl)`) survived, but no caller ever supplied the option, so usage sync stayed pinned to the static `GO_USAGE_API_URL`. + +Reporter's observation was exact: _"The settings key and URL helper appear in the bundle, but I could not find a runtime path applying the configured setting."_ + +## Fix + +Re-wire within the current (post-refactor) structure — minimal, single-point: + +1. **`src/config.ts`** — new pure helper `apiRoutesFromBaseUrl(baseUrl)` deriving the full route set (`models`, `chat/completions`, `messages`, `responses`, `usage`). Pure (no `vscode` import) so it stays unit-testable; this is the single derivation every consumer now goes through. +2. **`src/provider/definitions.ts`** — `configuredApiBaseUrl(vendor)` reads the **root** configuration with the full key (`opencodego.apiBaseUrl` — a section-scoped read would misresolve, the same trap documented for `enabled`), normalizes via `normalizeApiBaseUrl` (rejects non-http(s), embedded credentials, query/hash → falls back to default), and `PROVIDERS` derives all URLs from it. Agent variants inherit via `providerVariant`, so chat transports (`core/routing.ts`), model discovery (`provider/modelList.ts`), connection test (`providerDialogs.ts`), Agents window, and inline completions (`extension.ts`) are covered by construction. Plus `configuredGoUsageUrl()` for the Go `/usage` route. +3. **`src/usage/dashboard/state.ts`** — `usageTrackerOptions()` now supplies `resolveUsageUrl`, reconnecting the surviving tracker hook so server usage sync also follows the configured base (Go only, as upstream serves it). + +Per the documented "Reload after changing" contract, derivation happens once at module init — no live-reload semantics added. + +### Test-environment note + +Module-scope `PROVIDERS` derivation runs even in bare unit-test environments whose `vscode` stub exposes no `workspace`. The settings read uses a structural cast + optional access so import stays safe there; in the real extension host the setting is read normally. + +## Verification + +- `npm run compile` — clean. +- Unit tests — 515 pass / 0 fail, including a new `apiRoutesFromBaseUrl` test (custom base + default base). +- `npm run lint` — all 7 checks pass (strict gate, no suppressions). + +## Prevention + +Refactor extraction that moves URL-bearing object literals must re-check `config.ts` helpers against the moved code — the "half-wired hook" (`resolveUsageUrl` surviving without a supplier) was the telltale sign this was a regression, not a never-implemented feature. diff --git a/src/config.ts b/src/config.ts index 01ee28f5b..f567c6d4a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -151,6 +151,33 @@ export function appendApiPath(baseUrl: string, route: string): string { return `${baseUrl.replace(/\/+$/, "")}/${route.replace(/^\/+/, "")}`; } +/** The API routes every provider gateway serves, derived from one base URL. */ +export interface ApiRoutes { + modelsUrl: string; + chatCompletionsUrl: string; + messagesUrl: string; + responsesUrl: string; + usageUrl: string; +} + +/** + * Derive the full route set from a (default or user-configured) base URL. + * + * CONTRACT (issue #247): every provider request path — chat transports, model + * discovery, connection test, inline completions, Go usage sync — must get its + * endpoint from this derivation so a custom `apiBaseUrl` setting applies + * everywhere. Pure function (no `vscode` import) so it stays unit-testable. + */ +export function apiRoutesFromBaseUrl(baseUrl: string): ApiRoutes { + return { + modelsUrl: appendApiPath(baseUrl, "models"), + chatCompletionsUrl: appendApiPath(baseUrl, "chat/completions"), + messagesUrl: appendApiPath(baseUrl, "messages"), + responsesUrl: appendApiPath(baseUrl, "responses"), + usageUrl: appendApiPath(baseUrl, "usage"), + }; +} + // ─── Output budget / token-estimate margins ────────────────────────────────── /** Reserve for UI rendering so the advertised output never claims the full window. */ diff --git a/src/provider/definitions.ts b/src/provider/definitions.ts index ec25d9b2d..1c69f8478 100644 --- a/src/provider/definitions.ts +++ b/src/provider/definitions.ts @@ -1,5 +1,16 @@ import * as vscode from "vscode"; -import { CONFIG_SECTION, FALLBACK_USER_AGENT, FREE_ZEN_MODEL_IDS, SETTING_FREE_ONLY } from "../config"; +import { + appendApiPath, + apiRoutesFromBaseUrl, + CONFIG_SECTION, + DEFAULT_GO_API_BASE_URL, + DEFAULT_ZEN_API_BASE_URL, + FALLBACK_USER_AGENT, + FREE_ZEN_MODEL_IDS, + normalizeApiBaseUrl, + SETTING_API_BASE_URL, + SETTING_FREE_ONLY, +} from "../config"; import type { ModelEndpointKind } from "../core/registry"; import type { ApiMessage } from "../request/types"; import { AGENT_GO_VENDOR, AGENT_ZEN_VENDOR, GO_VENDOR, ZEN_VENDOR, type AllProviderVendor } from "../providerTypes"; @@ -14,6 +25,8 @@ export interface ProviderDefinition { chatCompletionsUrl: string; messagesUrl: string; responsesUrl?: string; + /** Go-only server usage meter route; derived from the same base URL. */ + usageUrl?: string; testModelId: string; fallbackModels: string[]; filterModel?: (modelId: string) => boolean; @@ -25,6 +38,35 @@ export interface ProviderDefinition { let cachedUserAgent: string | undefined; +/** + * Read a provider base URL from settings, falling back safely when missing or + * malformed (issue #247). + * + * CONTRACT: + * - Reads from the ROOT configuration with the FULL setting key + * (`opencodego.apiBaseUrl`). A section-scoped read + * (`getConfiguration("opencodego").get("apiBaseUrl")`) would resolve the key + * relative to the section and misread it — same trap as `enabled`. + * - Non-http(s), embedded credentials, query strings, and hashes fall back to + * the built-in default via {@link normalizeApiBaseUrl}. + * - Changes take effect on window reload (documented behavior), matching the + * module-scope derivation in {@link PROVIDERS}. + */ +export 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; + // Structural view keeps module-scope PROVIDERS derivation importable in + // bare unit-test environments where the vscode stub exposes no workspace. + const workspace = vscode.workspace as + { getConfiguration?: () => { get?: (section: string, defaultValue: string) => string | undefined } } | undefined; + const configured = workspace?.getConfiguration?.().get?.(`${vendor}.${SETTING_API_BASE_URL}`, fallback); + return normalizeApiBaseUrl(configured ?? fallback, fallback); +} + +/** Resolve the Go usage route from the same base URL as the Go provider. */ +export function configuredGoUsageUrl(): string { + return appendApiPath(configuredApiBaseUrl(GO_VENDOR), "usage"); +} + /** * Build the User-Agent string from the extension's declared version. * @@ -66,6 +108,7 @@ function providerVariant( chatCompletionsUrl: base.chatCompletionsUrl, messagesUrl: base.messagesUrl, responsesUrl: base.responsesUrl, + usageUrl: base.usageUrl, testModelId: base.testModelId, fallbackModels: base.fallbackModels, filterModel: base.filterModel, @@ -73,14 +116,19 @@ function providerVariant( } export const PROVIDERS: Record = (() => { + // Routes derive from the configured apiBaseUrl setting so a custom compatible + // gateway replaces the built-in opencode.ai endpoints end-to-end (issue #247). + const goRoutes = apiRoutesFromBaseUrl(configuredApiBaseUrl(GO_VENDOR)); + const zenRoutes = apiRoutesFromBaseUrl(configuredApiBaseUrl(ZEN_VENDOR)); const go: ProviderDefinition = { 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", + modelsUrl: goRoutes.modelsUrl, + chatCompletionsUrl: goRoutes.chatCompletionsUrl, + messagesUrl: goRoutes.messagesUrl, + responsesUrl: goRoutes.responsesUrl, + usageUrl: goRoutes.usageUrl, testModelId: "deepseek-v4-flash", fallbackModels: [ // Curated active set from models.dev (2026-09-08) — deprecated/legacy @@ -118,10 +166,10 @@ export 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", + modelsUrl: zenRoutes.modelsUrl, + chatCompletionsUrl: zenRoutes.chatCompletionsUrl, + messagesUrl: zenRoutes.messagesUrl, + responsesUrl: zenRoutes.responsesUrl, testModelId: "deepseek-v4-flash-free", fallbackModels: [ "claude-opus-4-7", diff --git a/src/test/config.test.ts b/src/test/config.test.ts index 865d64ee1..508c2ac9b 100644 --- a/src/test/config.test.ts +++ b/src/test/config.test.ts @@ -41,6 +41,7 @@ import { MODEL_METADATA_REVISION, MODELS_DEV_API_URL, normalizeApiBaseUrl, + apiRoutesFromBaseUrl, PROFILES_REGISTRY_KEY, REASONING_CACHE_LIMIT, RECENT_TRANSPORT_SUMMARY_LIMIT, @@ -224,4 +225,21 @@ describe("config — provider API URLs", () => { 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); }); + + it("derives the full route set from a configured base URL (issue #247)", () => { + assert.deepEqual(apiRoutesFromBaseUrl("https://gateway.example.test/custom/v1/"), { + modelsUrl: "https://gateway.example.test/custom/v1/models", + chatCompletionsUrl: "https://gateway.example.test/custom/v1/chat/completions", + messagesUrl: "https://gateway.example.test/custom/v1/messages", + responsesUrl: "https://gateway.example.test/custom/v1/responses", + usageUrl: "https://gateway.example.test/custom/v1/usage", + }); + assert.deepEqual(apiRoutesFromBaseUrl(DEFAULT_GO_API_BASE_URL), { + 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", + usageUrl: "https://opencode.ai/zen/go/v1/usage", + }); + }); }); diff --git a/src/usage/dashboard/state.ts b/src/usage/dashboard/state.ts index df4914f97..8a627af09 100644 --- a/src/usage/dashboard/state.ts +++ b/src/usage/dashboard/state.ts @@ -18,6 +18,7 @@ import { } from "../../config"; import { GoUsageTracker, type GoUsageTrackerOptions } from "../tracker"; import { LEGACY_FINGERPRINT, type UsageProfile } from "../usageProfile"; +import { configuredGoUsageUrl } from "../../provider/definitions"; /** * Mutable module state for the usage dashboard plus its accessors. All other @@ -123,6 +124,9 @@ export 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), + // Follow the Go provider's configured apiBaseUrl so a custom gateway also + // serves the server usage meters (issue #247). + resolveUsageUrl: () => configuredGoUsageUrl(), }; }