Skip to content

Commit 19b201b

Browse files
luoxuanzaoQoder-AI
andauthored
feat(byok): show qodercli custom providers in the model selector (#67)
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) <qoder_ai@qoder.com>
1 parent a3897c3 commit 19b201b

5 files changed

Lines changed: 52 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ version with its date and start a fresh empty `[Unreleased]` above it.
1111

1212
## [Unreleased]
1313

14+
### Added
15+
16+
- Models you configured as bring-your-own-key in qodercli — third-party
17+
models and self-defined providers with a custom base URL — now show up in
18+
the model selector and can be used from the composer.
19+
1420
## [1.0.10] - 2026-09-16
1521

1622
### Added

src/qoder/commands/probe-runtime-commands.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function mapSdkCommands(sdkCommands: SDKSlashCommand[]): SlashCommand[] {
3636
function resolveSdkModelGroup(model: ModelInfo): string {
3737
const source = model.source as string | undefined;
3838
if (model.serverScene === 'byok_enterprise' || source === 'organization') return 'Enterprise';
39-
if (source === 'user') return 'Custom';
39+
if (source === 'user' || source === 'custom') return 'Custom';
4040
if (model.isNew) return 'New models';
4141
return 'Qoder';
4242
}
@@ -376,6 +376,8 @@ export async function probeRuntimeCatalog(
376376
const commands = mapSdkCommands(initialization.commands ?? []);
377377
const agents = mapSdkAgents(initialization.agents ?? []);
378378

379+
// CLI-configured custom providers already ship with the CLI's model list;
380+
// asking the server for them by id fails the live fetch outright.
379381
let models: QoderDiscoveredModel[] = [];
380382
try {
381383
const liveModels: ModelInfo[] = await conversation.getAvailableModels({ fetchStrategy: 'live' });

src/qoder/runtime/custom-spawn.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import {
88
type WindowsCmdShimSpawnSpec,
99
} from './windows-cmd-shim';
1010

11+
/**
12+
* SDK-spawned sessions run qodercli in its "sdk" runtime mode, where
13+
* CLI-configured custom providers (self-defined base URLs) stay disabled
14+
* unless this opt-in is present — without it the CLI hides their models.
15+
*/
16+
const CUSTOM_PROVIDER_CLI_ENV = { QODER_SDK_CUSTOM_BASE_URL_BYOK: '1' } as const;
17+
1118
/**
1219
* Spawn qodercli in Obsidian's Electron process.
1320
*
@@ -36,7 +43,7 @@ export function createCustomSpawnFunction(
3643
const resolvedSpawnSpec = resolveWindowsCmdShimSpawnSpec({ args, command });
3744
const child = spawn(resolvedSpawnSpec.command, resolvedSpawnSpec.args, {
3845
cwd,
39-
env,
46+
env: env ? { ...env, ...CUSTOM_PROVIDER_CLI_ENV } : env,
4047
// stderr is always piped so host code (e.g. the runtime probe) can read
4148
// CLI diagnostics such as "No qodercli login found". A drain listener is
4249
// attached below to avoid pipe backpressure when nobody else listens.

tests/unit/qoder/commands/probe-runtime-commands.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,4 +349,19 @@ describe('probeRuntimeCatalog', () => {
349349
fetchStrategy: 'live',
350350
});
351351
});
352+
it('groups custom-provider models under Custom', async () => {
353+
setInitMessage({});
354+
sdkMock.setMockAvailableModels([
355+
{ value: 'gw-gpt', displayName: 'GW GPT', source: 'custom', isEnabled: true },
356+
{ value: 'auto', displayName: 'Auto', source: 'system', isEnabled: true },
357+
]);
358+
359+
const result = await probeRuntimeCatalog(createMockPlugin());
360+
361+
if ('error' in result) throw new Error('probe failed');
362+
expect(result.models.map((model) => [model.value, model.group])).toEqual([
363+
['gw-gpt', 'Custom'],
364+
['auto', 'Qoder'],
365+
]);
366+
});
352367
});

tests/unit/qoder/runtime/custom-spawn.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,26 @@ describe('createCustomSpawnFunction', () => {
108108
);
109109
});
110110

111+
it('opts every spawned CLI into custom providers', () => {
112+
const mockProcess = createMockProcess();
113+
spawnMock.mockReturnValue(mockProcess as unknown as ReturnType<typeof spawn>);
114+
115+
const spawnFn = createCustomSpawnFunction('/enhanced/path');
116+
spawnFn({
117+
command: 'node',
118+
args: ['cli.js'],
119+
cwd: '/tmp',
120+
env: { PATH: '/usr/bin' },
121+
signal: new AbortController().signal,
122+
});
123+
124+
const spawnOptions = spawnMock.mock.calls[0][2];
125+
expect(spawnOptions.env).toEqual({
126+
PATH: '/usr/bin',
127+
QODER_SDK_CUSTOM_BASE_URL_BYOK: '1',
128+
});
129+
});
130+
111131
it('always pipes stderr so host code can read CLI diagnostics', () => {
112132
const mockProcess = createMockProcess();
113133
spawnMock.mockReturnValue(mockProcess as unknown as ReturnType<typeof spawn>);

0 commit comments

Comments
 (0)