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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/qoder/commands/probe-runtime-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down Expand Up @@ -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' });
Expand Down
9 changes: 8 additions & 1 deletion src/qoder/runtime/custom-spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/qoder/commands/probe-runtime-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
]);
});
});
20 changes: 20 additions & 0 deletions tests/unit/qoder/runtime/custom-spawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,26 @@ describe('createCustomSpawnFunction', () => {
);
});

it('opts every spawned CLI into custom providers', () => {
const mockProcess = createMockProcess();
spawnMock.mockReturnValue(mockProcess as unknown as ReturnType<typeof spawn>);

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<typeof spawn>);
Expand Down
Loading