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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions docs/issues/110-20261013-issue247-api-base-url-ignored.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
66 changes: 57 additions & 9 deletions src/provider/definitions.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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.
*
Expand Down Expand Up @@ -66,21 +108,27 @@ function providerVariant(
chatCompletionsUrl: base.chatCompletionsUrl,
messagesUrl: base.messagesUrl,
responsesUrl: base.responsesUrl,
usageUrl: base.usageUrl,
testModelId: base.testModelId,
fallbackModels: base.fallbackModels,
filterModel: base.filterModel,
};
}

export const PROVIDERS: Record<ProviderDefinition["vendor"], ProviderDefinition> = (() => {
// 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
Expand Down Expand Up @@ -118,10 +166,10 @@ export const PROVIDERS: Record<ProviderDefinition["vendor"], ProviderDefinition>
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",
Expand Down
18 changes: 18 additions & 0 deletions src/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
MODEL_METADATA_REVISION,
MODELS_DEV_API_URL,
normalizeApiBaseUrl,
apiRoutesFromBaseUrl,
PROFILES_REGISTRY_KEY,
REASONING_CACHE_LIMIT,
RECENT_TRANSPORT_SUMMARY_LIMIT,
Expand Down Expand Up @@ -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",
});
});
});
4 changes: 4 additions & 0 deletions src/usage/dashboard/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,6 +124,9 @@ export function usageTrackerOptions(): GoUsageTrackerOptions {
config().get<UsageTodayYesterdaySource>(SETTING_USAGE_TODAY_YESTERDAY_SOURCE, DEFAULT_USAGE_TODAY_YESTERDAY_SOURCE),
resolveCodebaseWindowDays: () => config().get<number>(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(),
};
}

Expand Down
Loading