fix(custom-model): pass context limits and selected model to CLIs, add model picker filter and DeepSeek support

- opencode: write limit.context/output from the discovered context length (#749)
- codex: override the native launch model when a custom endpoint is selected (#750), and set model_context_window
- deepseek harness: add a deepseek-patch-json injection template
- model picker: add a search/filter box with a no-matches line

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
DevvynandClaude Sonnet 5.5 committed 2026-11-01 08:26:47 +08:00
1 parent f1f7eec4d6
commit 941df574ab
13 files changed
+427 -54

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { getCli } from '../src/config/cli-registry/index.js';
import { buildCustomModelInjection } from '../src/custom-model-injection.js';
import {
applyCustomModelInjection,
removeConfigDir,
customModelConfigDir,
} from '../src/custom-model-injection-apply.js';
afterEach(() => {
removeConfigDir(customModelConfigDir('deepseek-probe'));
vi.unstubAllEnvs();
});
describe('DeepSeek custom endpoint isolation', () => {
it('uses the chosen model and limits without changing installed profiles or embedding the key', () => {
const realHome = join(homedir(), '.dsh');
const profile = join(realHome, 'profiles', 'dsh-tui');
mkdirSync(profile, { recursive: true });
const original = join(profile, 'cordis.patch.yml');
writeFileSync(original, 'original profile');
vi.stubEnv('DSH_HOME', realHome);
const entry = getCli('deepseek');
if (!entry) throw new Error('Missing DeepSeek entry');
const endpoint = { id: 'local', label: 'Local', baseUrl: 'http://box:8080', apiKey: 'private-probe-key' };
const applied = applyCustomModelInjection(entry, endpoint, 'chosen-model', 'deepseek-probe', 131072);
if (!applied?.configDir) throw new Error('Missing isolated profile');
const content = readFileSync(join(applied.configDir, 'cordis.patch.yml'), 'utf8');
expect(content).not.toContain(endpoint.apiKey);
expect(applied.envOverrides.DEEPSEEK_API_KEY).toBe(endpoint.apiKey);
const patch = JSON.parse(content);
const provider = patch.find((row: { id: string }) => row.id === 'llm-pi-ai').config.providers['codeman-custom'];
expect(provider.api).toBe('openai-completions');
expect(provider.baseURL).toBe('http://box:8080/v1');
expect(provider.models).toEqual([
{ id: 'chosen-model', name: 'chosen-model', contextWindow: 131072, maxTokens: 8192 },
]);
expect(patch.find((row: { id: string }) => row.id === 'dsh-tui').config).toEqual({
provider: 'codeman-custom',
model: 'chosen-model',
});
expect(realpathSync(join(applied.configDir, 'profiles'))).toBe(join(realHome, 'profiles'));
removeConfigDir(applied.configDir);
expect(readFileSync(original, 'utf8')).toBe('original profile');
});
it('gives Codex its known context and compaction budget', () => {
const entry = getCli('codex');
if (!entry) throw new Error('Missing Codex entry');
const injection = buildCustomModelInjection(
entry,
{ id: 'local', label: 'Local', baseUrl: 'http://box' },
'chosen-model',
131072
);
if (injection.kind !== 'configDir') throw new Error('Expected isolated Codex config');
expect(injection.files[0].content).toContain('model_context_window = 131072');
expect(injection.files[0].content).toContain('model_auto_compact_token_limit = 98304');
expect(injection.launchModel).toBe('chosen-model');
});
});
+10 -6
View File
@@ -53,9 +53,12 @@ describe('applyCustomModelInjection: context length', () => {
expect(applied?.envOverrides.CLAUDE_CODE_MAX_CONTEXT_TOKENS).toBeUndefined();
});
it('deepseek: has no contextLengthVar declared, so a passed-in length is a no-op', () => {
const applied = applyCustomModelInjection(entryOrThrow('deepseek'), endpoint, 'qwen3', 'sess-ctx-3', 16384);
expect(Object.keys(applied?.envOverrides ?? {}).sort()).toEqual(['DEEPSEEK_API_KEY', 'DEEPSEEK_BASE_URL']);
it('deepseek: passes the known context through its isolated model catalogue', () => {
const sessionId = 'sess-deepseek-context';
sessionsToClean.push(sessionId);
const applied = applyCustomModelInjection(entryOrThrow('deepseek'), endpoint, 'qwen3', sessionId, 32768);
expect(Object.keys(applied?.envOverrides ?? {}).sort()).toEqual(['DEEPSEEK_API_KEY', 'DSH_HOME']);
expect(readFileSync(join(applied!.configDir!, 'cordis.patch.yml'), 'utf8')).toContain('"contextWindow": 32768');
});
});
@@ -103,11 +106,12 @@ describe('applyCustomModelInjection: CLAUDE_CONFIG_DIR isolation', () => {
expect(applied?.envOverrides.HOME).toBe(customModelConfigDir(sessionId));
});
it('deepseek: no configDirVar declared, so no config dir is created at all', () => {
it('deepseek: creates an isolated profile patch', () => {
const sessionId = 'sess-cfgdir-deepseek';
sessionsToClean.push(sessionId);
const applied = applyCustomModelInjection(entryOrThrow('deepseek'), endpoint, 'qwen3', sessionId);
expect(applied?.configDir).toBeUndefined();
expect(existsSync(customModelConfigDir(sessionId))).toBe(false);
expect(applied?.configDir).toBe(customModelConfigDir(sessionId));
expect(existsSync(join(applied!.configDir!, 'cordis.patch.yml'))).toBe(true);
});
});
+40 -17
View File
@@ -112,6 +112,31 @@ describe('buildCustomModelInjection', () => {
expect(parsed.provider.custom.models.qwen3).toEqual({});
});
it('opencode: threads a discovered context length into the model limit with response headroom (issue #749)', () => {
const result = buildCustomModelInjection(entryOrThrow('opencode'), endpoint, 'qwen3', 16384);
if (result.kind !== 'env') throw new Error('unreachable');
const parsed = JSON.parse(result.envOverrides.OPENCODE_CONFIG_CONTENT);
// One quarter held back for the reply: context 16384-4096, output 4096 — the sum is the
// real window, so a full-size prompt plus a max-length reply never exceeds the server.
expect(parsed.provider.custom.models.qwen3).toEqual({ limit: { context: 12288, output: 4096 } });
});
it('opencode: caps the response headroom reservation at 8192 for big windows', () => {
const result = buildCustomModelInjection(entryOrThrow('opencode'), endpoint, 'qwen3', 262144);
if (result.kind !== 'env') throw new Error('unreachable');
const parsed = JSON.parse(result.envOverrides.OPENCODE_CONFIG_CONTENT);
expect(parsed.provider.custom.models.qwen3).toEqual({ limit: { context: 253952, output: 8192 } });
});
it('opencode: never invents a limit for an unknown or non-finite context length (issue #749 regression)', () => {
for (const contextLength of [undefined, NaN]) {
const result = buildCustomModelInjection(entryOrThrow('opencode'), endpoint, 'qwen3', contextLength);
if (result.kind !== 'env') throw new Error('unreachable');
const parsed = JSON.parse(result.envOverrides.OPENCODE_CONFIG_CONTENT);
expect(parsed.provider.custom.models.qwen3).toEqual({});
}
});
it('codex: configDir writes an isolated config.toml with model/base_url, and the key rides as extraEnv (never a literal TOML field)', () => {
const result = buildCustomModelInjection(entryOrThrow('codex'), endpoint, 'qwen3');
expect(result.kind).toBe('configDir');
@@ -179,29 +204,25 @@ describe('buildCustomModelInjection', () => {
expect(result.extraEnv).toEqual({ XAI_API_KEY: 'my-key' });
});
it('deepseek: env kind sets base URL (with a /v1 suffix appended) and key, no model var', () => {
// appendV1Suffix is REQUIRED here, not cosmetic: confirmed by reading dsh's own
// bundled source (@deepseek-ai/dsh-llm-deepseek) that it builds the request URL as
// `${DEEPSEEK_BASE_URL}/chat/completions` with no "/v1" of its own, while
// llama-swap/llama.cpp only serves "/v1/chat/completions" — without this, every
// request 404s (confirmed live; this is the fix for the originally-reported
// "dsh: HTTP_404: DeepSeek API error (HTTP 404)").
it('deepseek: isolates the selected model route and passes its key by reference', () => {
const result = buildCustomModelInjection(entryOrThrow('deepseek'), endpoint, 'qwen3');
if (result.kind !== 'env') throw new Error('unreachable');
expect(result.envOverrides).toEqual({
DEEPSEEK_BASE_URL: 'http://192.168.1.50:8080/v1',
DEEPSEEK_API_KEY: 'my-key',
});
if (result.kind !== 'configDir') throw new Error('Expected isolated DeepSeek config');
expect(result.dirEnvVar).toBe('DSH_HOME');
expect(result.extraEnv).toEqual({ DEEPSEEK_API_KEY: 'my-key' });
expect(result.files[0].content).not.toContain('my-key');
const patch = JSON.parse(result.files[0].content);
expect(patch.find((row: { id: string }) => row.id === 'dsh-tui').config.model).toBe('qwen3');
});
it('deepseek: appending the /v1 suffix is idempotent against a baseUrl that already ends in /v1', () => {
it('deepseek: preserves an existing version suffix', () => {
const result = buildCustomModelInjection(
entryOrThrow('deepseek'),
{ ...endpoint, baseUrl: 'http://192.168.1.50:8080/v1' },
{ ...endpoint, baseUrl: endpoint.baseUrl + '/v1' },
'qwen3'
);
if (result.kind !== 'env') throw new Error('unreachable');
expect(result.envOverrides.DEEPSEEK_BASE_URL).toBe('http://192.168.1.50:8080/v1');
if (result.kind !== 'configDir') throw new Error('Expected isolated DeepSeek config');
expect(result.files[0].content).toContain('http://192.168.1.50:8080/v1');
expect(result.files[0].content).not.toContain('/v1/v1');
});
it('antigravity: unsupported', () => {
@@ -232,7 +253,9 @@ describe('launchModel (the model launch param that selects the injected provider
});
it('CLIs whose config selects the model on its own declare none', () => {
for (const id of ['claude', 'opencode', 'codex', 'gemini', 'deepseek']) {
// codex now declares launchModel (selected-model launch override) and is covered by
// its own restart coverage, not here.
for (const id of ['claude', 'opencode', 'gemini', 'deepseek']) {
const result = buildCustomModelInjection(entryOrThrow(id), endpoint, 'qwen3');
if (result.kind === 'unsupported') throw new Error('unreachable');
expect(result.launchModel, id).toBeUndefined();
+122
View File
@@ -20,6 +20,10 @@ import { describe, expect, it, vi } from 'vitest';
const CONSTANTS_JS = readFileSync(new URL('../src/web/public/constants.js', import.meta.url), 'utf-8');
const SESSION_UI_JS = readFileSync(new URL('../src/web/public/session-ui.js', import.meta.url), 'utf-8');
// Only for the [hidden] CSS guard below — the filter toggles the `hidden` property, and
// `.run-mode-option`'s own `display: flex` would silently defeat it without the
// `!important` re-assertion in styles.css.
const STYLES_CSS = readFileSync(new URL('../src/web/public/styles.css', import.meta.url), 'utf-8');
// Only for the real _showCenterStatus DOM tests below (`bootAppWithRealCenterStatus`) —
// every other test in this file stubs _showCenterStatus itself and has no need of it.
const PANELS_UI_JS = readFileSync(new URL('../src/web/public/panels-ui.js', import.meta.url), 'utf-8');
@@ -55,6 +59,9 @@ function bootApp(
<div class="modal" id="customModelPickModal">
<h3 id="customModelPickTitle"></h3>
<p id="customModelPickHint"></p>
<input type="search" id="customModelPickSearch" aria-label="Search models"
oninput="app.filterCustomModelPick(this.value)">
<p id="customModelPickNoMatches" role="status" hidden>No models match</p>
<div id="customModelPickList"></div>
</div>
<div class="modal" id="customModelSwapConfirmModal">
@@ -532,6 +539,121 @@ describe('Custom Model Endpoint Profiles: the "which model" picker', () => {
await app.selectCustomModelEntry('claude', 'llama-box');
expect(toastMessage).toMatch(/no models discovered/i);
});
it('resets and focuses the search box every time the picker opens', async () => {
const { win, app } = bootApp({
hosts: [{ id: 'llama-box', label: 'llama.cpp', baseUrl: 'http://x', models: ['qwen3', 'llama3'] }],
});
win.app = app;
await app.selectCustomModelEntry('claude', 'llama-box');
const search = win.document.getElementById('customModelPickSearch') as HTMLInputElement;
// The caret lands in the box, so the first keystroke filters with no click.
expect(win.document.activeElement).toBe(search);
// A stale query from a previous open must not survive into the next one.
search.value = 'zzz';
search.dispatchEvent(new win.Event('input'));
app.closeCustomModelPickModal();
await app.selectCustomModelEntry('claude', 'llama-box');
expect(search.value).toBe('');
expect(win.document.activeElement).toBe(search);
expect(win.document.getElementById('customModelPickNoMatches')!.hasAttribute('hidden')).toBe(true);
const visible = [...win.document.getElementById('customModelPickList')!.querySelectorAll('button')].filter(
(b) => !b.hasAttribute('hidden')
);
expect(visible).toHaveLength(2);
});
it('filters rows case-insensitively on the model id, keeping the surviving row fully functional', async () => {
const { win, app } = bootApp({
hosts: [
{
id: 'llama-box',
label: 'llama.cpp',
baseUrl: 'http://x',
models: ['qwen3', 'llama3'],
defaultModelId: 'qwen3',
},
],
});
let launched: unknown[] | null = null;
app.runCustomModelEntry = async (...args: unknown[]) => {
launched = args;
};
win.app = app;
await app.selectCustomModelEntry('claude', 'llama-box');
const search = win.document.getElementById('customModelPickSearch') as HTMLInputElement;
search.value = 'QWEN';
search.dispatchEvent(new win.Event('input'));
const buttons = [...win.document.getElementById('customModelPickList')!.querySelectorAll('button')];
expect(buttons.filter((b) => b.hasAttribute('hidden')).map((b) => b.textContent!.trim())).toEqual(['llama3']);
const [visible] = buttons.filter((b) => !b.hasAttribute('hidden'));
// The surviving row keeps its Default badge and its real, callable click handler.
expect(visible.textContent).toContain('qwen3');
expect(visible.textContent).toContain('Default');
(visible as unknown as HTMLButtonElement & { onclick: (e: unknown) => void }).onclick(
new (win as any).Event('click')
);
expect(launched).toEqual(['claude', 'llama-box', 'qwen3']);
});
it('shows the no-matches line only while a query matches nothing, and clears it with the query', async () => {
const { win, app } = bootApp({
hosts: [{ id: 'llama-box', label: 'llama.cpp', baseUrl: 'http://x', models: ['qwen3', 'llama3'] }],
});
win.app = app;
await app.selectCustomModelEntry('claude', 'llama-box');
const search = win.document.getElementById('customModelPickSearch') as HTMLInputElement;
const noMatches = win.document.getElementById('customModelPickNoMatches')!;
const isHidden = () =>
win.document.getElementById('customModelPickList')!.querySelectorAll('button[hidden]').length;
// Whitespace is not a query: everything stays visible, no message.
search.value = ' ';
search.dispatchEvent(new win.Event('input'));
expect(noMatches.hasAttribute('hidden')).toBe(true);
expect(isHidden()).toBe(0);
search.value = 'ghost';
search.dispatchEvent(new win.Event('input'));
expect(noMatches.hasAttribute('hidden')).toBe(false);
expect(isHidden()).toBe(2);
search.value = '';
search.dispatchEvent(new win.Event('input'));
expect(noMatches.hasAttribute('hidden')).toBe(true);
expect(isHidden()).toBe(0);
});
it('keeps an HTML-significant model id inert in the picker rows too, and filterable by its raw text', async () => {
// Same vector as the Run-menu row above, now via data-model-id: escapeHtml covers
// the double quote, and the filter reads the decoded raw id back off dataset.
const dangerousModel = '"><img src=x onerror=alert(1)>';
const { win, app } = bootApp({
hosts: [{ id: 'llama-box', label: 'llama.cpp', baseUrl: 'http://x', models: ['qwen3', dangerousModel] }],
});
win.app = app;
await app.selectCustomModelEntry('claude', 'llama-box');
const list = win.document.getElementById('customModelPickList')!;
expect(list.querySelector('img')).toBeNull();
const search = win.document.getElementById('customModelPickSearch') as HTMLInputElement;
search.value = 'src=x';
search.dispatchEvent(new win.Event('input'));
const visible = list.querySelectorAll('button:not([hidden])');
expect(visible).toHaveLength(1);
expect(visible[0].getAttribute('data-model-id')).toBe(dangerousModel);
});
it('re-asserts display:none !important for hidden picker rows so the filter cannot be silently defeated', () => {
// `.run-mode-option` carries its own `display: flex` (styles.css, restated in
// mobile.css), which outranks the UA `[hidden]` rule — without this scoped
// !important re-assertion, toggling `hidden` would change nothing on screen.
const rule = STYLES_CSS.match(/#customModelPickModal \.run-mode-option\[hidden\]\s*\{[^}]*\}/);
expect(rule, 'picker rows need a [hidden] rule scoped to #customModelPickModal').toBeTruthy();
expect(rule![0]).toMatch(/display:\s*none\s*!important/);
});
});
describe('Custom Model Endpoint Profiles: _getCustomModelCurrentlyLoaded is client-side bounded', () => {
+28
View File
@@ -27,6 +27,8 @@ import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { Session } from '../src/session.js';
import { getCli } from '../src/config/cli-registry/index.js';
import { buildCustomModelInjection } from '../src/custom-model-injection.js';
import { TmuxManager } from '../src/tmux-manager.js';
import type { MuxSession, SessionMode } from '../src/types.js';
@@ -225,3 +227,29 @@ describe('launchModel reaches the CLI through its own launch param', () => {
expect(respawn.mock.calls[0][0].model).toBeUndefined();
});
});
describe('Codex custom endpoint model overrides', () => {
it('selects the local model over a native launch default and restores the default on clear', async () => {
const entry = getCli('codex');
if (!entry) throw new Error('Missing Codex entry');
const injection = buildCustomModelInjection(
entry,
{ id: 'ep', label: 'Local', baseUrl: 'http://box:8080' },
'qwen3'
);
if (injection.kind !== 'configDir') throw new Error('Expected isolated Codex config');
const { session, respawn } = liveSession('codex', {
codexConfig: { model: 'native-model', reasoningEffort: 'high' },
});
session.setCustomModel(
{ endpointId: 'ep', modelId: 'qwen3', envKeys: ['CODEX_HOME'], launchModel: injection.launchModel },
{ CODEX_HOME: '/isolated/codex' }
);
await session.restartCli();
expect(respawn.mock.calls[0][0].codexConfig).toEqual({ model: 'qwen3', reasoningEffort: 'high' });
session.setCustomModel(undefined);
respawn.mockClear();
await session.restartCli();
expect(respawn.mock.calls[0][0].codexConfig).toEqual({ model: 'native-model', reasoningEffort: 'high' });
});
});