From 0bf8bfa33dcc1ced452ee28d85a82a6fbe29ebce Mon Sep 17 00:00:00 2001 From: ajspig Date: Fri, 2 Oct 2026 12:56:47 -0400 Subject: [PATCH] fix: honor endpoint.baseUrl when no top-level baseUrl is set Shared configs written by other Honcho integrations carry the server URL as `endpoint.baseUrl`. It was ignored, so a self-hosted setup silently fell back to https://api.honcho.dev and failed with an invalid API key. Resolve baseUrl as: top-level `baseUrl`, else `hosts.opencode.endpoint.baseUrl`, else `endpoint.baseUrl`. Use it for the runtime settings and for the TUI status, settings, import and configured checks. HONCHO_URL still overrides everything. Co-Authored-By: Claude Sonnet 5.5 --- src/core.ts | 12 ++++++++++++ src/index.ts | 3 +++ src/tui/commands.ts | 12 +++++------- tests/tui-behavior.test.js | 9 +++++++++ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/core.ts b/src/core.ts index 4605cc2..82d272a 100644 --- a/src/core.ts +++ b/src/core.ts @@ -129,6 +129,18 @@ export const sharedGlobalSettingsPath = () => export const sharedConfigPath = (configPathOverride?: string) => configPathOverride ? path.resolve(configPathOverride) : sharedGlobalSettingsPath() +const endpointBaseUrl = (scope: unknown) => { + const endpoint = isRecord(scope) ? scope.endpoint : undefined + return isRecord(endpoint) && typeof endpoint.baseUrl === "string" ? endpoint.baseUrl.trim() : "" +} + +/** `baseUrl`, else the `endpoint.baseUrl` other Honcho integrations write (host block, then top level). */ +export const resolveBaseUrl = (raw: Record, hostId = "opencode") => { + const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "" + if (baseUrl) return baseUrl + return endpointBaseUrl(isRecord(raw.hosts) ? raw.hosts[hostId] : undefined) || endpointBaseUrl(raw) +} + export const normalizeId = (value: string) => value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "default" diff --git a/src/index.ts b/src/index.ts index 21d7231..d1f83b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import { normalizeId, observationUpgradeNextSteps, observationUpgradeNotice, + resolveBaseUrl, resolveSessionPeerIds, SETTING_ENUMS, sharedConfigPath, @@ -623,6 +624,8 @@ const normalizeScopedSettings = (raw: Record, hostId = "opencod if (typeof value === "string" && !value.trim() && INHERITABLE_STRING_KEYS.has(field)) continue normalized[field] = value } + const baseUrl = resolveBaseUrl(raw, hostId) + if (baseUrl) normalized.baseUrl = baseUrl const hostBlock = isRecord(raw.hosts) ? hostScopedSettings(raw.hosts[hostId]) : null if (hostBlock) { diff --git a/src/tui/commands.ts b/src/tui/commands.ts index 54bf4d2..017701f 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -12,6 +12,7 @@ import { isRecord, needsObservationUpgradePrompt, observationUpgradeNotice, + resolveBaseUrl, resolveSessionPeerIds, sharedConfigPath, unifiedImportFollowUp, @@ -143,10 +144,7 @@ const writeGlobalSettings = async (settings: GlobalSettings) => { } export const normalizeSettings = (settings: GlobalSettings) => ({ - baseUrl: - typeof settings.baseUrl === "string" && settings.baseUrl.trim() - ? settings.baseUrl - : DEFAULT_SETTINGS.baseUrl, + baseUrl: resolveBaseUrl(settings as Record) || DEFAULT_SETTINGS.baseUrl, apiKey: typeof settings.apiKey === "string" && settings.apiKey.trim() ? settings.apiKey.trim() : "", peerName: typeof settings.peerName === "string" ? settings.peerName.trim() : "", }) @@ -188,7 +186,7 @@ export const settingsMessage = (settings: GlobalSettings) => { `Config path: ${resolveConfigPath()}`, `API key: ${settings.apiKey?.trim() ? "set" : "not set"}`, `Peer name: ${settings.peerName?.trim() || "user"}`, - `Base URL: ${settings.baseUrl?.trim() || DEFAULT_SETTINGS.baseUrl}`, + `Base URL: ${resolveBaseUrl(settings as Record) || DEFAULT_SETTINGS.baseUrl}`, `Workspace: ${host.workspace || DEFAULT_SETTINGS.workspace}`, `AI peer: ${host.aiPeer || DEFAULT_SETTINGS.aiPeer}`, `Recall mode: ${host.recallMode || DEFAULT_SETTINGS.recallMode}`, @@ -271,7 +269,7 @@ const importConfigFromSettings = (settings: GlobalSettings) => { ) return { apiKey: settings.apiKey || "", - baseUrl: settings.baseUrl || DEFAULT_SETTINGS.baseUrl, + baseUrl: resolveBaseUrl(settings as Record) || DEFAULT_SETTINGS.baseUrl, workspaceId, userPeerId, agentPeerId, @@ -297,7 +295,7 @@ const formatImportPreview = (plan: Awaited } const isConfigured = (settings: GlobalSettings) => - Boolean(settings.apiKey?.trim()) || isLocalBaseUrl(settings.baseUrl || "") + Boolean(settings.apiKey?.trim()) || isLocalBaseUrl(resolveBaseUrl(settings as Record)) /** true when the user chose a mode and it was saved; false when they dismissed. */ export const runObservationUpgrade = async (session: TuiSession, followUpLines: string[]) => { diff --git a/tests/tui-behavior.test.js b/tests/tui-behavior.test.js index a2e9904..3b564f4 100644 --- a/tests/tui-behavior.test.js +++ b/tests/tui-behavior.test.js @@ -106,3 +106,12 @@ test("tui honors OPENCODE_HONCHO_CONFIG_PATH override for reads, writes, and dis else process.env.OPENCODE_HONCHO_CONFIG_PATH = previousOverride } }) + +test("tui shows endpoint.baseUrl when there is no top-level baseUrl", () => { + const url = (settings) => __testing.settingsMessage(settings).match(/Base URL: (.*)/)[1] + const endpoint = { baseUrl: "http://selfhosted:8000" } + assert.equal(url({ endpoint }), "http://selfhosted:8000") + assert.equal(url({ endpoint: { baseUrl: "http://top:1" }, hosts: { opencode: { endpoint } } }), "http://selfhosted:8000") + assert.equal(url({ baseUrl: "http://explicit:9", endpoint }), "http://explicit:9") + assert.equal(url({}), "https://api.honcho.dev") +})