From 92e8524a92f00bd0a36d8d0e5de19ee60f08bdd3 Mon Sep 17 00:00:00 2001 From: liuxuezhuo Date: Wed, 16 Sep 2026 16:08:15 +0800 Subject: [PATCH] feat(byok): show qodercli custom providers in the model selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDK-spawned sessions run qodercli in its "sdk" runtime mode, which keeps CLI-configured custom providers (self-defined base URLs) disabled unless QODER_SDK_CUSTOM_BASE_URL_BYOK=1 is present — so their models never reached Qoderian. Opt every spawned CLI into that setting at the shared spawn boundary and group models with source "custom" under Custom, next to the existing BYOK ("user") ones. The catalog stays a plain live fetch: asking the server for providers by id rejects it with a 400. Co-authored-by: QoderAI (Qwen 3.8 Max) --- CHANGELOG.md | 6 ++++++ src/qoder/commands/probe-runtime-commands.ts | 4 +++- src/qoder/runtime/custom-spawn.ts | 9 ++++++++- .../commands/probe-runtime-commands.test.ts | 15 ++++++++++++++ tests/unit/qoder/runtime/custom-spawn.test.ts | 20 +++++++++++++++++++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bdbe0..0c379f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ version with its date and start a fresh empty `[Unreleased]` above it. ## [Unreleased] +### Added + +- Models you configured as bring-your-own-key in qodercli — third-party + models and self-defined providers with a custom base URL — now show up in + the model selector and can be used from the composer. + ## [1.0.10] - 2026-09-16 ### Added diff --git a/src/qoder/commands/probe-runtime-commands.ts b/src/qoder/commands/probe-runtime-commands.ts index 120c8ce..e34597c 100644 --- a/src/qoder/commands/probe-runtime-commands.ts +++ b/src/qoder/commands/probe-runtime-commands.ts @@ -36,7 +36,7 @@ function mapSdkCommands(sdkCommands: SDKSlashCommand[]): SlashCommand[] { function resolveSdkModelGroup(model: ModelInfo): string { const source = model.source as string | undefined; if (model.serverScene === 'byok_enterprise' || source === 'organization') return 'Enterprise'; - if (source === 'user') return 'Custom'; + if (source === 'user' || source === 'custom') return 'Custom'; if (model.isNew) return 'New models'; return 'Qoder'; } @@ -376,6 +376,8 @@ export async function probeRuntimeCatalog( const commands = mapSdkCommands(initialization.commands ?? []); const agents = mapSdkAgents(initialization.agents ?? []); + // CLI-configured custom providers already ship with the CLI's model list; + // asking the server for them by id fails the live fetch outright. let models: QoderDiscoveredModel[] = []; try { const liveModels: ModelInfo[] = await conversation.getAvailableModels({ fetchStrategy: 'live' }); diff --git a/src/qoder/runtime/custom-spawn.ts b/src/qoder/runtime/custom-spawn.ts index 7d49b37..0d26ccc 100644 --- a/src/qoder/runtime/custom-spawn.ts +++ b/src/qoder/runtime/custom-spawn.ts @@ -8,6 +8,13 @@ import { type WindowsCmdShimSpawnSpec, } from './windows-cmd-shim'; +/** + * SDK-spawned sessions run qodercli in its "sdk" runtime mode, where + * CLI-configured custom providers (self-defined base URLs) stay disabled + * unless this opt-in is present — without it the CLI hides their models. + */ +const CUSTOM_PROVIDER_CLI_ENV = { QODER_SDK_CUSTOM_BASE_URL_BYOK: '1' } as const; + /** * Spawn qodercli in Obsidian's Electron process. * @@ -36,7 +43,7 @@ export function createCustomSpawnFunction( const resolvedSpawnSpec = resolveWindowsCmdShimSpawnSpec({ args, command }); const child = spawn(resolvedSpawnSpec.command, resolvedSpawnSpec.args, { cwd, - env, + env: env ? { ...env, ...CUSTOM_PROVIDER_CLI_ENV } : env, // stderr is always piped so host code (e.g. the runtime probe) can read // CLI diagnostics such as "No qodercli login found". A drain listener is // attached below to avoid pipe backpressure when nobody else listens. diff --git a/tests/unit/qoder/commands/probe-runtime-commands.test.ts b/tests/unit/qoder/commands/probe-runtime-commands.test.ts index 9e0c0bd..e24f84d 100644 --- a/tests/unit/qoder/commands/probe-runtime-commands.test.ts +++ b/tests/unit/qoder/commands/probe-runtime-commands.test.ts @@ -349,4 +349,19 @@ describe('probeRuntimeCatalog', () => { fetchStrategy: 'live', }); }); + it('groups custom-provider models under Custom', async () => { + setInitMessage({}); + sdkMock.setMockAvailableModels([ + { value: 'gw-gpt', displayName: 'GW GPT', source: 'custom', isEnabled: true }, + { value: 'auto', displayName: 'Auto', source: 'system', isEnabled: true }, + ]); + + const result = await probeRuntimeCatalog(createMockPlugin()); + + if ('error' in result) throw new Error('probe failed'); + expect(result.models.map((model) => [model.value, model.group])).toEqual([ + ['gw-gpt', 'Custom'], + ['auto', 'Qoder'], + ]); + }); }); diff --git a/tests/unit/qoder/runtime/custom-spawn.test.ts b/tests/unit/qoder/runtime/custom-spawn.test.ts index 06b1fd0..318f0e7 100644 --- a/tests/unit/qoder/runtime/custom-spawn.test.ts +++ b/tests/unit/qoder/runtime/custom-spawn.test.ts @@ -108,6 +108,26 @@ describe('createCustomSpawnFunction', () => { ); }); + it('opts every spawned CLI into custom providers', () => { + const mockProcess = createMockProcess(); + spawnMock.mockReturnValue(mockProcess as unknown as ReturnType); + + const spawnFn = createCustomSpawnFunction('/enhanced/path'); + spawnFn({ + command: 'node', + args: ['cli.js'], + cwd: '/tmp', + env: { PATH: '/usr/bin' }, + signal: new AbortController().signal, + }); + + const spawnOptions = spawnMock.mock.calls[0][2]; + expect(spawnOptions.env).toEqual({ + PATH: '/usr/bin', + QODER_SDK_CUSTOM_BASE_URL_BYOK: '1', + }); + }); + it('always pipes stderr so host code can read CLI diagnostics', () => { const mockProcess = createMockProcess(); spawnMock.mockReturnValue(mockProcess as unknown as ReturnType);