From c790a9c8ab713de03b87e9460d8b2b67057659b2 Mon Sep 17 00:00:00 2001 From: Brian Genisio Date: Thu, 3 Sep 2026 10:44:24 -0400 Subject: [PATCH] Add deepseek-ai model prefix Co-authored-by: Cursor --- README.md | 2 +- lib/llm/model-ref.js | 2 ++ lib/llm/openai.js | 7 ++++--- tests/llm-model-ref.test.js | 8 +++++++- tests/llm-openai.test.js | 21 +++++++++++++++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c15c2ca..ff2cb82 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Fill in `.env` with the API key (and optional `*_BASE_URL`) for the provider you - `anthropic/claude-sonnet-4-6` — needs `ANTHROPIC_API_KEY`, optional `ANTHROPIC_BASE_URL` - `openai/gpt-5.6-luna` — needs `OPENAI_API_KEY`, optional `OPENAI_BASE_URL` - `google/gemini-3.6-flash` — needs `GOOGLE_API_KEY`, optional `GOOGLE_BASE_URL` (`gemini/…` also routes to Gemini) -- `~deepseek/deepseek-v4-flash-latest` — needs `DEEPSEEK_API_KEY` and `DEEPSEEK_BASE_URL` (`deepseek/…` also routes here; uses the OpenAI SDK). If both DeepSeek vars are unset, it reuses `OPENAI_API_KEY` / `OPENAI_BASE_URL` (production proxy hack). +- `~deepseek/deepseek-v4-flash-latest` — needs `DEEPSEEK_API_KEY` and `DEEPSEEK_BASE_URL` (`deepseek/…` and `deepseek-ai/…` also route here; uses the OpenAI SDK). If both DeepSeek vars are unset, it reuses `OPENAI_API_KEY` / `OPENAI_BASE_URL` (production proxy hack). `session.config.json` is separate from `.env`. It is local (not checked in) and holds **session defaults**, not secrets: diff --git a/lib/llm/model-ref.js b/lib/llm/model-ref.js index a729d3c..7b5dc37 100644 --- a/lib/llm/model-ref.js +++ b/lib/llm/model-ref.js @@ -8,6 +8,7 @@ * gemini/… → gemini * deepseek/… → deepseek (OpenAI-compatible) * ~deepseek/… → deepseek (OpenAI-compatible) + * deepseek-ai/… → deepseek (OpenAI-compatible) */ export const DEFAULT_MODEL_REF = 'anthropic/claude-sonnet-4-6'; @@ -19,6 +20,7 @@ const PROVIDERS = { google: { name: 'gemini', apiKeyName: 'GOOGLE_API_KEY' }, gemini: { name: 'gemini', apiKeyName: 'GOOGLE_API_KEY' }, deepseek: { name: 'deepseek', apiKeyName: 'DEEPSEEK_API_KEY' }, + 'deepseek-ai': { name: 'deepseek', apiKeyName: 'DEEPSEEK_API_KEY' }, }; /** diff --git a/lib/llm/openai.js b/lib/llm/openai.js index efc31d2..3f432c9 100644 --- a/lib/llm/openai.js +++ b/lib/llm/openai.js @@ -7,7 +7,7 @@ * traffic reuses the OpenAI key/URL (production proxy hack in * lib/llm/provider.js). OpenAI model ids may still use an `openai/` * prefix; it is stripped before the API call. DeepSeek preserves the - * `deepseek/` or `~deepseek/` prefix supplied by the caller. + * `deepseek/`, `~deepseek/`, or `deepseek-ai/` prefix supplied by the caller. */ import OpenAI from 'openai'; @@ -27,13 +27,14 @@ export function normalizeOpenAiModelId(modelId = '') { /** * DeepSeek hosts expect the full prefixed model name, not a bare id. - * Preserve whether the caller supplied `deepseek/` or `~deepseek/`. + * Preserve whether the caller supplied `deepseek/`, `~deepseek/`, or + * `deepseek-ai/`. * @param {string} [modelId] * @returns {string} */ export function normalizeDeepSeekModelId(modelId = '') { const value = String(modelId).trim(); - const match = value.match(/^(~deepseek|deepseek)\//i); + const match = value.match(/^(~deepseek|deepseek|deepseek-ai)\//i); if (match) return `${match[1]}/${value.slice(match[0].length)}`; return value ? `~deepseek/${value}` : ''; } diff --git a/tests/llm-model-ref.test.js b/tests/llm-model-ref.test.js index 89d4f9c..fa9c2e3 100644 --- a/tests/llm-model-ref.test.js +++ b/tests/llm-model-ref.test.js @@ -40,7 +40,7 @@ describe('parseModelRef', () => { }); }); - it('routes deepseek/ and ~deepseek/ to the same OpenAI-compatible provider', () => { + it('routes DeepSeek prefixes to the same OpenAI-compatible provider', () => { expect(parseModelRef('deepseek/deepseek-v4-flash-latest')).toMatchObject({ provider: 'deepseek', modelId: 'deepseek-v4-flash-latest', @@ -53,6 +53,12 @@ describe('parseModelRef', () => { prefix: 'deepseek', apiKeyName: 'DEEPSEEK_API_KEY', }); + expect(parseModelRef('deepseek-ai/deepseek-v4-flash-latest')).toMatchObject({ + provider: 'deepseek', + modelId: 'deepseek-v4-flash-latest', + prefix: 'deepseek-ai', + apiKeyName: 'DEEPSEEK_API_KEY', + }); }); it('trims whitespace and lowercases the prefix', () => { diff --git a/tests/llm-openai.test.js b/tests/llm-openai.test.js index e6b7551..148cc6a 100644 --- a/tests/llm-openai.test.js +++ b/tests/llm-openai.test.js @@ -41,6 +41,8 @@ describe('normalizeDeepSeekModelId', () => { .toBe('deepseek/deepseek-v4-flash-latest'); expect(normalizeDeepSeekModelId('~deepseek/deepseek-v4-flash-latest')) .toBe('~deepseek/deepseek-v4-flash-latest'); + expect(normalizeDeepSeekModelId('deepseek-ai/deepseek-v4-flash-latest')) + .toBe('deepseek-ai/deepseek-v4-flash-latest'); }); }); @@ -219,6 +221,25 @@ describe('createLlmProvider deepseek', () => { }); }); + it('sends a deepseek-ai/ model ref with its original prefix', async () => { + createMock.mockResolvedValue({ + id: 'chatcmpl-ds-ai', + choices: [{ message: { content: 'Paris' } }], + }); + const llm = createLlmProvider({ + DEEPSEEK_API_KEY: 'sk-deepseek', + DEEPSEEK_BASE_URL: 'https://api.deepseek.test/v1', + }, 'deepseek-ai/deepseek-v4-flash-latest'); + + await llm.complete({ model: llm.model, messages: [{ role: 'user', content: 'Hi' }] }); + + expect(llm.model).toBe('deepseek-ai/deepseek-v4-flash-latest'); + expect(createMock).toHaveBeenCalledWith({ + model: 'deepseek-ai/deepseek-v4-flash-latest', + messages: [{ role: 'user', content: 'Hi' }], + }); + }); + it('falls back to OPENAI_* when both DeepSeek env vars are unset (prod proxy hack)', () => { createLlmProvider({ OPENAI_API_KEY: 'sk-openai',