refactor(custom-model): keep the DeepSeek patch rewrite on its own branch

This commit is contained in:
Devvyn committed 2026-11-01 21:20:40 +08:00
1 parent a97fa0de17
commit dbd1b6e587
8 files changed
+46 -142

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', 'deepseek-patch-json']),
template: z.enum(['codex-toml', 'pi-models-json', 'omp-models-yml', 'grok-toml']),
launchModel: launchModelTemplate,
})
.strict(),
+21 -5
View File
@@ -1548,12 +1548,28 @@ 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'],
// The installed profile supplies the transport; a per-session patch selects its exact model.
// 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.
customModelInjection: {
kind: 'configDir',
dirEnvVar: 'DSH_HOME',
fileName: 'cordis.patch.yml',
template: 'deepseek-patch-json',
kind: 'env',
baseUrlVar: 'DEEPSEEK_BASE_URL',
apiKeyVar: 'DEEPSEEK_API_KEY',
modelVars: [],
appendV1Suffix: true,
},
},
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' | 'deepseek-patch-json';
template: 'codex-toml' | 'pi-models-json' | 'omp-models-yml' | 'grok-toml';
launchModel?: string;
}
| { kind: 'unsupported' };
-9
View File
@@ -46,14 +46,6 @@ 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 };
}
@@ -292,6 +284,5 @@ export function applyCustomModelInjection(
}
const configDir = customModelConfigDir(sessionId);
const envOverrides = applyConfigDirInjection(configDir, injection);
return { envOverrides, envKeys: Object.keys(envOverrides), configDir, launchModel: injection.launchModel };
}
+1 -40
View File
@@ -215,7 +215,7 @@ 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' | 'deepseek-patch-json',
template: 'codex-toml' | 'pi-models-json' | 'omp-models-yml' | 'grok-toml',
endpoint: CustomModelEndpoint,
modelId: string,
apiKey: string,
@@ -223,45 +223,6 @@ function renderConfigFile(
): { 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
-64
View File
@@ -1,64 +0,0 @@
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');
});
});
+6 -10
View File
@@ -53,12 +53,9 @@ describe('applyCustomModelInjection: context length', () => {
expect(applied?.envOverrides.CLAUDE_CODE_MAX_CONTEXT_TOKENS).toBeUndefined();
});
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');
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']);
});
});
@@ -106,12 +103,11 @@ describe('applyCustomModelInjection: CLAUDE_CONFIG_DIR isolation', () => {
expect(applied?.envOverrides.HOME).toBe(customModelConfigDir(sessionId));
});
it('deepseek: creates an isolated profile patch', () => {
it('deepseek: no configDirVar declared, so no config dir is created at all', () => {
const sessionId = 'sess-cfgdir-deepseek';
sessionsToClean.push(sessionId);
const applied = applyCustomModelInjection(entryOrThrow('deepseek'), endpoint, 'qwen3', sessionId);
expect(applied?.configDir).toBe(customModelConfigDir(sessionId));
expect(existsSync(join(applied!.configDir!, 'cordis.patch.yml'))).toBe(true);
expect(applied?.configDir).toBeUndefined();
expect(existsSync(customModelConfigDir(sessionId))).toBe(false);
});
});
+16 -12
View File
@@ -204,25 +204,29 @@ describe('buildCustomModelInjection', () => {
expect(result.extraEnv).toEqual({ XAI_API_KEY: 'my-key' });
});
it('deepseek: isolates the selected model route and passes its key by reference', () => {
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)").
const result = buildCustomModelInjection(entryOrThrow('deepseek'), endpoint, 'qwen3');
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');
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',
});
});
it('deepseek: preserves an existing version suffix', () => {
it('deepseek: appending the /v1 suffix is idempotent against a baseUrl that already ends in /v1', () => {
const result = buildCustomModelInjection(
entryOrThrow('deepseek'),
{ ...endpoint, baseUrl: endpoint.baseUrl + '/v1' },
{ ...endpoint, baseUrl: 'http://192.168.1.50:8080/v1' },
'qwen3'
);
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');
if (result.kind !== 'env') throw new Error('unreachable');
expect(result.envOverrides.DEEPSEEK_BASE_URL).toBe('http://192.168.1.50:8080/v1');
});
it('antigravity: unsupported', () => {