From dbd1b6e587c33b283e9b32d0aa5809eb2080cbc0 Mon Sep 17 00:00:00 2001 From: Devvyn <22340871+opticon454@users.noreply.github.com> Date: Sun, 1 Nov 2026 21:20:40 +0800 Subject: [PATCH] refactor(custom-model): keep the DeepSeek patch rewrite on its own branch --- src/config/cli-registry/schema.ts | 2 +- src/config/cli-registry/stock.ts | 26 +++++++-- src/config/cli-registry/types.ts | 2 +- src/custom-model-injection-apply.ts | 9 ---- src/custom-model-injection.ts | 41 +-------------- test/Deepseek-CustomEndpoint.test.ts | 64 ----------------------- test/custom-model-injection-apply.test.ts | 16 +++--- test/custom-model-injection.test.ts | 28 +++++----- 8 files changed, 46 insertions(+), 142 deletions(-) delete mode 100644 test/Deepseek-CustomEndpoint.test.ts diff --git a/src/config/cli-registry/schema.ts b/src/config/cli-registry/schema.ts index 5c65279e..f5529ba3 100644 --- a/src/config/cli-registry/schema.ts +++ b/src/config/cli-registry/schema.ts @@ -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(), diff --git a/src/config/cli-registry/stock.ts b/src/config/cli-registry/stock.ts index 9cd58853..4ac5b29d 100644 --- a/src/config/cli-registry/stock.ts +++ b/src/config/cli-registry/stock.ts @@ -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: { diff --git a/src/config/cli-registry/types.ts b/src/config/cli-registry/types.ts index 1777eccc..6707d4aa 100644 --- a/src/config/cli-registry/types.ts +++ b/src/config/cli-registry/types.ts @@ -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' }; diff --git a/src/custom-model-injection-apply.ts b/src/custom-model-injection-apply.ts index 76807c88..43cfd5ad 100644 --- a/src/custom-model-injection-apply.ts +++ b/src/custom-model-injection-apply.ts @@ -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 }; } diff --git a/src/custom-model-injection.ts b/src/custom-model-injection.ts index 2394936b..83613878 100644 --- a/src/custom-model-injection.ts +++ b/src/custom-model-injection.ts @@ -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 } { 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 diff --git a/test/Deepseek-CustomEndpoint.test.ts b/test/Deepseek-CustomEndpoint.test.ts deleted file mode 100644 index 7bb63408..00000000 --- a/test/Deepseek-CustomEndpoint.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/test/custom-model-injection-apply.test.ts b/test/custom-model-injection-apply.test.ts index 4f4a887c..9f870100 100644 --- a/test/custom-model-injection-apply.test.ts +++ b/test/custom-model-injection-apply.test.ts @@ -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); }); }); diff --git a/test/custom-model-injection.test.ts b/test/custom-model-injection.test.ts index 05037dfb..19c524ce 100644 --- a/test/custom-model-injection.test.ts +++ b/test/custom-model-injection.test.ts @@ -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', () => {