feat(custom-model): DeepSeek configDir injection via a per-session patch

Replace the deepseek env redirect with a deepseek-patch-json recipe: a per-session DSH_HOME holding a generated cordis.patch.yml that declares a codeman-custom openai-completions provider over the installed profile. Ported from PR #752 commits 941df574 and 4bb1bfe9, minus the core context-limit changes.
This commit is contained in:
Devvyn committed 2026-11-01 21:29:31 +08:00
1 parent f1f7eec4d6
commit aece02da87
9 files changed
+154 -58

No files matched your search

+1 -1
View File
@@ -580,7 +580,7 @@ const capabilitiesSchema = z
kind: z.literal('configDir'),
dirEnvVar: envName,
fileName: z.string().min(1).max(80),
template: z.enum(['codex-toml', 'pi-models-json', 'omp-models-yml', 'grok-toml']),
template: z.enum(['codex-toml', 'pi-models-json', 'omp-models-yml', 'grok-toml', 'deepseek-patch-json']),
launchModel: launchModelTemplate,
})
.strict(),
+5 -21
View File
@@ -1547,28 +1547,12 @@ const DEEPSEEK: CliEntry = {
// privilege rather than granting it, and clamping it here was a real regression
// (test/deepseek-mode.test.ts) fixed before this shipped.
privilegedEnvKeys: ['DSH_PERMISSION_MODE', 'DSH_HOME', 'DEEPSEEK_BASE_URL'],
// Reuses the already-existing DEEPSEEK_BASE_URL/DEEPSEEK_API_KEY keys above. No
// modelVars — dsh's model is a profile-composition entry (see `model: { source: 'none'
// }` above), not an env var, so forcing a specific model name may not fully work;
// verify against a real profile before shipping.
//
// ⚠️ appendV1Suffix is REQUIRED, not optional-nice-to-have: without it every request
// 404s. Confirmed live and by reading dsh's own bundled source
// (@deepseek-ai/dsh-llm-deepseek): it builds the request URL as
// `${DEEPSEEK_BASE_URL}/chat/completions` with no "/v1" of its own (its real public
// API, https://api.deepseek.com, expects the caller's base URL to already carry any
// needed prefix), while llama-swap/llama.cpp only serves the OpenAI-conventional
// "/v1/chat/completions" — a bare POST to ".../chat/completions" 404s live, and the
// 404 reported here originally ("dsh: HTTP_404: DeepSeek API error (HTTP 404)")
// matches dsh's own error-message template for exactly this failure. See the
// customModelInjection doc comment in cli-registry/types.ts for the full reasoning,
// including why claude/gemini must NOT get this.
// The installed profile supplies the transport; a per-session patch selects its exact model.
customModelInjection: {
kind: 'env',
baseUrlVar: 'DEEPSEEK_BASE_URL',
apiKeyVar: 'DEEPSEEK_API_KEY',
modelVars: [],
appendV1Suffix: true,
kind: 'configDir',
dirEnvVar: 'DSH_HOME',
fileName: 'cordis.patch.yml',
template: 'deepseek-patch-json',
},
},
overlays: {
+1 -1
View File
@@ -888,7 +888,7 @@ export interface CliCapabilities {
kind: 'configDir';
dirEnvVar: string;
fileName: string;
template: 'codex-toml' | 'pi-models-json' | 'omp-models-yml' | 'grok-toml';
template: 'codex-toml' | 'pi-models-json' | 'omp-models-yml' | 'grok-toml' | 'deepseek-patch-json';
launchModel?: string;
}
| { kind: 'unsupported' };
+8
View File
@@ -46,6 +46,14 @@ export function applyConfigDirInjection(baseDir: string, injection: ConfigDirInj
writeFileSync(filePath, file.content, { encoding: 'utf8', mode: 0o600 });
chmodSync(filePath, 0o600);
}
if (injection.dirEnvVar === 'DSH_HOME') {
// Reuse installed profile bundles without changing the user's profile or credentials.
const profiles = join(baseDir, 'profiles');
if (!existsSync(profiles)) {
const realHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
symlinkSync(join(realHome, 'profiles'), profiles, platform() === 'win32' ? 'junction' : 'dir');
}
}
return { [injection.dirEnvVar]: baseDir, ...injection.extraEnv };
}
+43 -3
View File
@@ -141,7 +141,7 @@ export function buildCustomModelInjection(
}
case 'configDir': {
const { content, extraEnv } = renderConfigFile(cap.template, endpoint, modelId, apiKey);
const { content, extraEnv } = renderConfigFile(cap.template, endpoint, modelId, apiKey, contextLength);
return withLaunchModel(
{ kind: 'configDir', dirEnvVar: cap.dirEnvVar, files: [{ relPath: cap.fileName, content }], extraEnv },
cap.launchModel,
@@ -192,13 +192,53 @@ const CODEX_API_KEY_ENV_VAR = 'CODEMAN_CUSTOM_MODEL_API_KEY';
export const GROK_CUSTOM_MODEL_NAME = 'codeman-custom';
function renderConfigFile(
template: 'codex-toml' | 'pi-models-json' | 'omp-models-yml' | 'grok-toml',
template: 'codex-toml' | 'pi-models-json' | 'omp-models-yml' | 'grok-toml' | 'deepseek-patch-json',
endpoint: CustomModelEndpoint,
modelId: string,
apiKey: string
apiKey: string,
contextLength?: number
): { content: string; extraEnv?: Record<string, string> } {
const baseUrl = withV1Suffix(endpoint.baseUrl);
switch (template) {
case 'deepseek-patch-json': {
const route = { provider: 'codeman-custom', model: modelId };
return {
content: JSON.stringify(
[
{
id: 'llm-pi-ai',
name: '@deepseek-ai/dsh-llm-pi-ai',
config: {
providers: {
'codeman-custom': {
apiKeyEnv: 'DEEPSEEK_API_KEY',
api: 'openai-completions',
baseURL: baseUrl,
models: [
{
id: modelId,
name: modelId,
...(contextLength !== undefined && Number.isFinite(contextLength) && contextLength > 0
? {
contextWindow: Math.trunc(contextLength),
maxTokens: Math.max(1, Math.min(8192, Math.floor(contextLength / 4))),
}
: {}),
},
],
},
},
},
},
{ id: 'dsh-tui', name: '@deepseek-harness-tui/dsh-tui', config: route },
{ id: 'agent-default-model', name: '@deepseek-ai/dsh-agent-default-model', config: route },
],
null,
2
),
extraEnv: { DEEPSEEK_API_KEY: apiKey },
};
}
case 'codex-toml': {
// Verified against real codex (>= Feb 2026): `model` is a top-level STRING, never
// a `[model].default` table — codex rejects that with "invalid type: map, expected
+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);
});
});
+10 -10
View File
@@ -188,18 +188,18 @@ describe('custom-model-injection contract (mock server)', () => {
expect(mock.requests[0].headers.authorization).toBe('Bearer contract-test-key');
});
it('deepseek: DEEPSEEK_BASE_URL already carries the /v1 suffix dsh itself never adds, reaching the mock at the real path dsh requests', async () => {
// Confirmed by reading dsh's own bundled source: it fetches
// `${DEEPSEEK_BASE_URL}/chat/completions` verbatim, no /v1 insertion of its own — so
// this call (unlike gemini's above) passes DEEPSEEK_BASE_URL to callOpenAiCompat
// UNMODIFIED, exactly mirroring what the real harness does, rather than the test
// helping it along.
it('deepseek: the session patch carries the /v1 base URL and exact model, reaching the mock at the real path', async () => {
// dsh fetches `${baseURL}/chat/completions` verbatim, so the patch's baseURL already
// carries /v1 and is passed to callOpenAiCompat unmodified.
const injection = buildCustomModelInjection(entryOrThrow('deepseek'), endpointFor(mock), 'qwen3');
if (injection.kind !== 'env') throw new Error('unreachable');
expect(Object.keys(injection.envOverrides).sort()).toEqual(['DEEPSEEK_API_KEY', 'DEEPSEEK_BASE_URL']);
expect(injection.envOverrides.DEEPSEEK_BASE_URL).toBe(`${mock.baseUrl}/v1`);
if (injection.kind !== 'configDir') throw new Error('unreachable');
expect(injection.dirEnvVar).toBe('DSH_HOME');
expect(Object.keys(injection.extraEnv ?? {})).toEqual(['DEEPSEEK_API_KEY']);
const provider = JSON.parse(injection.files[0].content)[0].config.providers['codeman-custom'];
expect(provider.baseURL).toBe(`${mock.baseUrl}/v1`);
expect(provider.models[0].id).toBe('qwen3');
await callOpenAiCompat(injection.envOverrides.DEEPSEEK_BASE_URL, injection.envOverrides.DEEPSEEK_API_KEY, 'qwen3');
await callOpenAiCompat(provider.baseURL, injection.extraEnv!.DEEPSEEK_API_KEY, 'qwen3');
expect(mock.requests[0].path).toBe('/v1/chat/completions');
expect(mock.requests[0].headers.authorization).toBe('Bearer contract-test-key');
+12 -16
View File
@@ -179,29 +179,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', () => {
+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');
});
});