From a5fb1724464eaf43ff5de5bc15880c7cff52ad2e Mon Sep 17 00:00:00 2001 From: Sebastian Rumpf Date: Tue, 12 May 2026 16:46:59 +0200 Subject: [PATCH 1/6] chore: ignore .worktrees directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 97eea28..d5dce37 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ coverage/ tmp/ temp/ *.tmp + +# Worktrees +.worktrees/ From a94f859a3649dcb4f15eac0828f4ec99b40e10db Mon Sep 17 00:00:00 2001 From: Sebastian Rumpf Date: Tue, 12 May 2026 16:55:36 +0200 Subject: [PATCH 2/6] fix: eagerly fetch models in config hook to fix OpenCode >=1.14.47 picker issue (#12) OpenCode 1.14.47+ reads provider.models eagerly at list time and by copy, so the previous pattern of hydrating models in-place during the auth loader hook no longer works. The picker only saw the 4 hardcoded fallback models. This change makes the config hook eagerly fetch live models from the /v1/models endpoint when auth is available in the store, ensuring the full model list is visible to Provider.list on all OpenCode versions. - Add readAuthFromStore helper to read ~/.local/share/opencode/auth.json - Refactor createRuntimeConfig to accept options directly for reuse - Update config hook to await fetchModels before setting provider.models - Keep loader hook for runtime refresh and fetch interceptor setup Fixes #12 --- src/plugin.ts | 53 ++++++++++++++++++++++++++++++++++++-------- test/plugin.test.mjs | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 22cb167..4f29a30 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -13,6 +13,9 @@ import { OMNIROUTE_ENDPOINTS, } from './constants.js'; import { fetchModels } from './models.js'; +import { homedir } from 'os'; +import { readFile } from 'fs/promises'; +import { join } from 'path'; const OMNIROUTE_PROVIDER_NAME = 'OmniRoute'; const OMNIROUTE_PROVIDER_NPM = '@ai-sdk/openai-compatible'; @@ -32,6 +35,18 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => { const apiMode = getApiMode(existingProvider?.options); const providerApi = resolveProviderApi(existingProvider?.api, apiMode); + let models: OmniRouteModel[] = OMNIROUTE_DEFAULT_MODELS; + try { + const auth = await readAuthFromStore(OMNIROUTE_PROVIDER_ID); + if (auth?.key) { + const runtimeConfig = createRuntimeConfig(existingProvider?.options ?? {}, auth.key); + const forceRefresh = runtimeConfig.refreshOnList !== false; + models = await fetchModels(runtimeConfig, auth.key, forceRefresh); + } + } catch (error) { + console.warn('[OmniRoute] Eager model fetch failed, using defaults:', error); + } + providers[OMNIROUTE_PROVIDER_ID] = { ...existingProvider, name: existingProvider?.name ?? OMNIROUTE_PROVIDER_NAME, @@ -46,7 +61,7 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => { models: existingProvider?.models && Object.keys(existingProvider.models).length > 0 ? existingProvider.models - : toProviderModels(OMNIROUTE_DEFAULT_MODELS, baseUrl), + : toProviderModels(models, baseUrl), }; config.provider = providers; @@ -79,7 +94,7 @@ async function loadProviderOptions( ); } - const config = createRuntimeConfig(provider, auth.key); + const config = createRuntimeConfig(provider.options, auth.key); let models: OmniRouteModel[] = []; try { @@ -103,17 +118,20 @@ async function loadProviderOptions( }; } -function createRuntimeConfig(provider: ProviderDefinition, apiKey: string): OmniRouteConfig { - const baseUrl = getBaseUrl(provider.options); - const modelCacheTtl = getPositiveNumber(provider.options, 'modelCacheTtl'); - const refreshOnList = getBoolean(provider.options, 'refreshOnList'); - const modelsDev = getModelsDevConfig(provider.options); - const modelMetadata = getModelMetadataConfig(provider.options); +function createRuntimeConfig( + options: Record | undefined, + apiKey: string, +): OmniRouteConfig { + const baseUrl = getBaseUrl(options); + const modelCacheTtl = getPositiveNumber(options, 'modelCacheTtl'); + const refreshOnList = getBoolean(options, 'refreshOnList'); + const modelsDev = getModelsDevConfig(options); + const modelMetadata = getModelMetadataConfig(options); return { baseUrl, apiKey, - apiMode: getApiMode(provider.options), + apiMode: getApiMode(options), modelCacheTtl, refreshOnList, modelsDev, @@ -121,6 +139,23 @@ function createRuntimeConfig(provider: ProviderDefinition, apiKey: string): Omni }; } +async function readAuthFromStore( + providerId: string, +): Promise<{ key?: string; type?: string } | null> { + try { + const dataHome = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'); + const authPath = join(dataHome, 'opencode', 'auth.json'); + const content = await readFile(authPath, 'utf-8'); + const data = JSON.parse(content); + if (!isRecord(data)) return null; + const auth = data[providerId]; + if (!isRecord(auth)) return null; + return auth as { key?: string; type?: string }; + } catch { + return null; + } +} + function resolveProviderApi(api: unknown, apiMode: OmniRouteApiMode): OmniRouteApiMode { if (isApiMode(api)) { if (api !== apiMode) { diff --git a/test/plugin.test.mjs b/test/plugin.test.mjs index 85af736..19e6073 100644 --- a/test/plugin.test.mjs +++ b/test/plugin.test.mjs @@ -1,12 +1,17 @@ import { afterEach, test } from 'node:test'; import assert from 'node:assert/strict'; +import { mkdir, writeFile, rm } from 'fs/promises'; +import { join } from 'path'; +import { tmpdir } from 'os'; import OmniRouteAuthPlugin from '../dist/index.js'; const ORIGINAL_FETCH = global.fetch; +const ORIGINAL_HOME = process.env.HOME; afterEach(() => { global.fetch = ORIGINAL_FETCH; + process.env.HOME = ORIGINAL_HOME; }); function createModelsResponse() { @@ -280,3 +285,51 @@ test('gemini schema sanitization applies to responses endpoint request objects', assert.equal(forwardedBody.tools[0].input_schema.additionalProperties, undefined); assert.equal(forwardedBody.tools[0].input_schema.properties.query.items.additionalProperties, undefined); }); + +test('config hook eagerly fetches models when auth is available', async () => { + const tempHome = join(tmpdir(), `opencode-test-${Date.now()}`); + await mkdir(join(tempHome, '.local', 'share', 'opencode'), { recursive: true }); + await writeFile( + join(tempHome, '.local', 'share', 'opencode', 'auth.json'), + JSON.stringify({ + omniroute: { type: 'api', key: 'test-key' }, + }), + ); + process.env.HOME = tempHome; + + global.fetch = async (input) => { + const url = input instanceof Request ? input.url : String(input); + if (url.endsWith('/v1/models')) { + return new Response( + JSON.stringify({ + object: 'list', + data: [{ id: 'custom-model', name: 'Custom Model' }], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + } + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }; + + const plugin = await OmniRouteAuthPlugin({}); + const config = { + provider: { + omniroute: { + options: { + baseURL: 'http://localhost:20128/v1', + apiMode: 'chat', + }, + }, + }, + }; + + await plugin.config(config); + + assert.ok(config.provider.omniroute.models['custom-model']); + assert.equal(config.provider.omniroute.models['custom-model'].name, 'Custom Model'); + + await rm(tempHome, { recursive: true, force: true }); +}); From c1cd376b0d09ca26e6d04e790d214f4682f740db Mon Sep 17 00:00:00 2001 From: Sebastian Rumpf Date: Wed, 13 May 2026 08:19:21 +0200 Subject: [PATCH 3/6] fix: address code review feedback for issue-12 - Fix import ordering (Node.js built-ins before internal imports) - Fix test temp directory leak with try/finally cleanup - Fix os.homedir() caching by using process.env.HOME directly - Fix eager fetch bypassing cache by using forceRefresh=false --- src/plugin.ts | 7 ++-- test/plugin.test.mjs | 82 +++++++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 4f29a30..ef9cf18 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -12,10 +12,10 @@ import { OMNIROUTE_DEFAULT_MODELS, OMNIROUTE_ENDPOINTS, } from './constants.js'; -import { fetchModels } from './models.js'; import { homedir } from 'os'; import { readFile } from 'fs/promises'; import { join } from 'path'; +import { fetchModels } from './models.js'; const OMNIROUTE_PROVIDER_NAME = 'OmniRoute'; const OMNIROUTE_PROVIDER_NPM = '@ai-sdk/openai-compatible'; @@ -40,8 +40,7 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => { const auth = await readAuthFromStore(OMNIROUTE_PROVIDER_ID); if (auth?.key) { const runtimeConfig = createRuntimeConfig(existingProvider?.options ?? {}, auth.key); - const forceRefresh = runtimeConfig.refreshOnList !== false; - models = await fetchModels(runtimeConfig, auth.key, forceRefresh); + models = await fetchModels(runtimeConfig, auth.key, false); } } catch (error) { console.warn('[OmniRoute] Eager model fetch failed, using defaults:', error); @@ -143,7 +142,7 @@ async function readAuthFromStore( providerId: string, ): Promise<{ key?: string; type?: string } | null> { try { - const dataHome = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'); + const dataHome = process.env.XDG_DATA_HOME || join(process.env.HOME || homedir(), '.local', 'share'); const authPath = join(dataHome, 'opencode', 'auth.json'); const content = await readFile(authPath, 'utf-8'); const data = JSON.parse(content); diff --git a/test/plugin.test.mjs b/test/plugin.test.mjs index 19e6073..9b6ce07 100644 --- a/test/plugin.test.mjs +++ b/test/plugin.test.mjs @@ -288,48 +288,50 @@ test('gemini schema sanitization applies to responses endpoint request objects', test('config hook eagerly fetches models when auth is available', async () => { const tempHome = join(tmpdir(), `opencode-test-${Date.now()}`); - await mkdir(join(tempHome, '.local', 'share', 'opencode'), { recursive: true }); - await writeFile( - join(tempHome, '.local', 'share', 'opencode', 'auth.json'), - JSON.stringify({ - omniroute: { type: 'api', key: 'test-key' }, - }), - ); - process.env.HOME = tempHome; - - global.fetch = async (input) => { - const url = input instanceof Request ? input.url : String(input); - if (url.endsWith('/v1/models')) { - return new Response( - JSON.stringify({ - object: 'list', - data: [{ id: 'custom-model', name: 'Custom Model' }], - }), - { status: 200, headers: { 'Content-Type': 'application/json' } }, - ); - } - return new Response(JSON.stringify({ ok: true }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - }; - - const plugin = await OmniRouteAuthPlugin({}); - const config = { - provider: { - omniroute: { - options: { - baseURL: 'http://localhost:20128/v1', - apiMode: 'chat', + try { + await mkdir(join(tempHome, '.local', 'share', 'opencode'), { recursive: true }); + await writeFile( + join(tempHome, '.local', 'share', 'opencode', 'auth.json'), + JSON.stringify({ + omniroute: { type: 'api', key: 'test-key' }, + }), + ); + process.env.HOME = tempHome; + + global.fetch = async (input) => { + const url = input instanceof Request ? input.url : String(input); + if (url.endsWith('/v1/models')) { + return new Response( + JSON.stringify({ + object: 'list', + data: [{ id: 'custom-model', name: 'Custom Model' }], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + } + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }; + + const plugin = await OmniRouteAuthPlugin({}); + const config = { + provider: { + omniroute: { + options: { + baseURL: 'http://localhost:20128/v1', + apiMode: 'chat', + }, }, }, - }, - }; - - await plugin.config(config); + }; - assert.ok(config.provider.omniroute.models['custom-model']); - assert.equal(config.provider.omniroute.models['custom-model'].name, 'Custom Model'); + await plugin.config(config); - await rm(tempHome, { recursive: true, force: true }); + assert.ok(config.provider.omniroute.models['custom-model']); + assert.equal(config.provider.omniroute.models['custom-model'].name, 'Custom Model'); + } finally { + await rm(tempHome, { recursive: true, force: true }); + } }); From 16ca3573f2f14ccf151d011d6145a8e62a0d6d4c Mon Sep 17 00:00:00 2001 From: Sebastian Rumpf Date: Wed, 13 May 2026 08:42:05 +0200 Subject: [PATCH 4/6] chore: bump version to 1.1.2 --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ebe5f4..51f0de7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project are documented in this file. +## [1.1.2] - 2026-05-13 + +### Fixed + +- Fixed model picker only showing fallback models on OpenCode >=1.14.47 by eagerly fetching live models in the `config` hook before OpenCode reads `provider.models`. (@jms830) +- Refactored `createRuntimeConfig` to accept `options` directly for reuse across both `config` and `loader` hooks. + +### Added + +- Added `readAuthFromStore()` helper to read the stored OmniRoute API key from `~/.local/share/opencode/auth.json` during the `config` hook. +- Added regression test for eager model fetching in the config hook. + ## [1.1.1] - 2026-05-12 ### Fixed diff --git a/package.json b/package.json index 61c16bb..a703155 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-omniroute-auth", - "version": "1.1.1", + "version": "1.1.2", "description": "OpenCode authentication plugin for OmniRoute API with /connect command and dynamic model fetching", "type": "module", "main": "./dist/index.js", From 4992ab6669ec49a3b6003f4a4553c04655a3a310 Mon Sep 17 00:00:00 2001 From: Sebastian Rumpf Date: Wed, 13 May 2026 20:55:52 +0200 Subject: [PATCH 5/6] chore: fix README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 009030a..7266ac3 100644 --- a/README.md +++ b/README.md @@ -365,7 +365,7 @@ If models aren't loading: ### Plugin Not Loading Outside This Repo -If the plugin loads only through a local shim (for example from `.opencode/plugins` or `.opencode/plugi`) but not from npm in `opencode.json`: +If the plugin loads only through a local shim (for example from `.opencode/plugins`) but not from npm in `opencode.json`: 1. Ensure you are using `opencode-omniroute-auth@1.0.1` or newer 2. Confirm your config includes `"plugin": ["opencode-omniroute-auth"]` From 434074f22b9dbadcc6ff167c8bfccb8395ed2861 Mon Sep 17 00:00:00 2001 From: Sebastian Rumpf Date: Wed, 13 May 2026 21:10:04 +0200 Subject: [PATCH 6/6] style: address review feedback - fix import ordering and narrow error handling --- src/plugin.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index ef9cf18..e16e483 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,4 +1,7 @@ import type { Plugin, Hooks } from '@opencode-ai/plugin'; +import { homedir } from 'os'; +import { readFile } from 'fs/promises'; +import { join } from 'path'; import type { OmniRouteApiMode, OmniRouteConfig, @@ -12,9 +15,6 @@ import { OMNIROUTE_DEFAULT_MODELS, OMNIROUTE_ENDPOINTS, } from './constants.js'; -import { homedir } from 'os'; -import { readFile } from 'fs/promises'; -import { join } from 'path'; import { fetchModels } from './models.js'; const OMNIROUTE_PROVIDER_NAME = 'OmniRoute'; @@ -150,7 +150,11 @@ async function readAuthFromStore( const auth = data[providerId]; if (!isRecord(auth)) return null; return auth as { key?: string; type?: string }; - } catch { + } catch (error) { + if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { + return null; + } + console.warn('[OmniRoute] Unexpected error reading auth store:', error); return null; } }