-
Notifications
You must be signed in to change notification settings - Fork 504
fix(providers): default DeepSeek V4 Flash to Responses #808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,6 +123,12 @@ export interface ProviderRegistryEntry { | |
| defaultModel?: string; | ||
| models?: string[]; | ||
| liveModels?: boolean; | ||
| /** | ||
| * Registry-only per-model wire defaults for mixed OpenAI-compatible gateways. | ||
| * These are intentionally not seeded into saved config: an explicit `modelAdapters` | ||
| * entry must remain distinguishable and must always win over a default. | ||
| */ | ||
| modelWireDefaults?: Record<string, string>; | ||
| modelDiscovery?: ProviderModelDiscoverySpec; | ||
| contextWindow?: number; | ||
| modelContextWindows?: Record<string, number>; | ||
|
|
@@ -813,6 +819,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ | |
| models: ["deepseek-chat", "deepseek-reasoner", ...DEEPSEEK_THINKING_MODELS], | ||
| defaultModel: "deepseek-v4-flash", | ||
| modelContextWindows: { "deepseek-v4-flash": 1_000_000, "deepseek-v4-pro": 1_000_000 }, | ||
| // DeepSeek documents V4-Flash as a native Responses API model adapted for Codex. The | ||
| // API id is `deepseek-v4-flash`; `DeepSeek-V4-Flash-0731` is a release/version label. | ||
| modelWireDefaults: { "deepseek-v4-flash": "openai-responses" }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The catalog still evaluates DeepSeek's provider-wide AGENTS.md reference: src/AGENTS.md:L18-L19 Useful? React with 👍 / 👎. |
||
| /* [Decision Log] | ||
| - 목적: DeepSeek V4 thinking mode multi-turn/tool-call requests must replay prior assistant reasoning_content. | ||
| - 대안 분석: Globally preserve reasoning_content for all OpenAI-compatible models; preserve it for legacy deepseek-reasoner too; mark only V4 thinking models in registry metadata. | ||
|
|
@@ -1235,6 +1244,25 @@ export function providerMatchesRegistryTransport( | |
| return normalizedProviderEndpoint(provider.baseUrl) === normalizedProviderEndpoint(entry.baseUrl); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve a registry-only default for a mixed-wire provider. Defaults only move a provider | ||
| * between the two OpenAI-shaped adapters and never override a provider configured on another | ||
| * wire. The resolver receives the allow-list so this helper cannot accidentally widen the | ||
| * adapter-selection boundary when a new registry entry is added. | ||
| */ | ||
| export function providerModelWireDefault( | ||
| id: string, | ||
| provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>, | ||
| modelId: string, | ||
| allowedWires: ReadonlySet<string>, | ||
| ): string | undefined { | ||
| if (!allowedWires.has(provider.adapter)) return undefined; | ||
| const entry = getProviderRegistryEntry(id); | ||
| if (!entry?.modelWireDefaults || !providerMatchesRegistryTransport(id, provider)) return undefined; | ||
| const wire = entry.modelWireDefaults[modelId.trim().toLowerCase()]; | ||
| return wire !== undefined && allowedWires.has(wire) ? wire : undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Effective Codex account mode for a provider. For canonical `openai`, a valid persisted | ||
| * `codexAccountMode` on the provider config wins and a missing/invalid value defaults to | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,10 +9,12 @@ import { createResponsesPassthroughAdapter } from "../adapters/openai-responses" | |
| import type { OcxProviderConfig } from "../types"; | ||
| import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types"; | ||
| import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers"; | ||
| import { providerModelWireDefault } from "../providers/registry"; | ||
|
|
||
| /** | ||
| * Resolve the wire a single model should use: a hard pin first, then a configured | ||
| * per-model override, then the provider's own adapter. | ||
| * per-model override, then a registry default for a mixed-wire provider, then the provider's | ||
| * own adapter. | ||
| * | ||
| * Safe to call more than once on its own output — the pin check does not look at the | ||
| * current adapter, so a second pass cannot let an override displace a pin. | ||
|
|
@@ -24,7 +26,12 @@ export function resolveWireProtocolOverride(providerName: string, modelId: strin | |
| } | ||
| // Re-check the allow-list here, not just in the config validator: the file may have | ||
| // been hand-edited, or written by a build that allowed more values. | ||
| const requested = providerConfig.modelAdapters?.[modelId]; | ||
| const configured = providerConfig.modelAdapters?.[modelId]; | ||
| // An explicit allowed override wins, including one naming the provider-wide adapter (the | ||
| // opt-out from a registry default). Invalid hand-edited values fall through to the default. | ||
| const requested = configured && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured) | ||
| ? configured | ||
| : providerModelWireDefault(providerName, providerConfig, modelId, MODEL_ADAPTER_OVERRIDE_ALLOWED); | ||
|
Comment on lines
+32
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For DeepSeek Flash requests entering through Useful? React with 👍 / 👎. |
||
| if (requested | ||
| && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(requested) | ||
| && requested !== providerConfig.adapter | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -90,3 +90,42 @@ describe("per-model wire override (#404)", () => { | |||||||||||||||||||||||||||||||||
| .toBe("openai-responses"); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| describe("registry per-model wire defaults", () => { | ||||||||||||||||||||||||||||||||||
| function deepseek(overrides: Partial<OcxProviderConfig> = {}): OcxProviderConfig { | ||||||||||||||||||||||||||||||||||
| return gateway({ | ||||||||||||||||||||||||||||||||||
| baseUrl: "https://api.deepseek.com", | ||||||||||||||||||||||||||||||||||
| authMode: "key", | ||||||||||||||||||||||||||||||||||
| ...overrides, | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| test("routes only the official Flash API id through Responses", () => { | ||||||||||||||||||||||||||||||||||
| expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", deepseek()).adapter) | ||||||||||||||||||||||||||||||||||
| .toBe("openai-responses"); | ||||||||||||||||||||||||||||||||||
| expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-pro", deepseek()).adapter) | ||||||||||||||||||||||||||||||||||
| .toBe("openai-chat"); | ||||||||||||||||||||||||||||||||||
| // The dated release label is not the API model id and must not be silently rewritten. | ||||||||||||||||||||||||||||||||||
| expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash-0731", deepseek()).adapter) | ||||||||||||||||||||||||||||||||||
| .toBe("openai-chat"); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| test("an explicit Chat override opts Flash back out of the default", () => { | ||||||||||||||||||||||||||||||||||
| const provider = deepseek({ modelAdapters: { "deepseek-v4-flash": "openai-chat" } }); | ||||||||||||||||||||||||||||||||||
| expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", provider).adapter) | ||||||||||||||||||||||||||||||||||
| .toBe("openai-chat"); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+113
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add coverage for invalid explicit overrides. Lines 113-117 verify a valid Proposed test+ test("an invalid override falls back to the Flash registry default", () => {
+ const provider = deepseek({ modelAdapters: { "deepseek-v4-flash": "anthropic" } });
+ expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", provider).adapter)
+ .toBe("openai-responses");
+ });As per path instructions, shared routing changes need focused regression coverage. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| test("defaults do not apply when the provider is already on another wire", () => { | ||||||||||||||||||||||||||||||||||
| expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", deepseek({ adapter: "anthropic" })).adapter) | ||||||||||||||||||||||||||||||||||
| .toBe("anthropic"); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| test("keeps provider credentials and destination untouched", () => { | ||||||||||||||||||||||||||||||||||
| const provider = deepseek({ apiKey: "test-key" }); | ||||||||||||||||||||||||||||||||||
| const resolved = resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", provider); | ||||||||||||||||||||||||||||||||||
| expect(provider.adapter).toBe("openai-chat"); | ||||||||||||||||||||||||||||||||||
| expect(resolved.apiKey).toBe("test-key"); | ||||||||||||||||||||||||||||||||||
| expect(resolved.baseUrl).toBe("https://api.deepseek.com"); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user selects the advertised
xhigheffort fordeepseek-v4-flash, this default switches the request away fromcreateOpenAIChatAdapter, which is the only adapter that callsmapReasoningEffort. The Responses adapter instead forwards_rawBody.reasoning.effortunchanged, soxhighreaches DeepSeek rather than the registry-requiredmaxmapping declared inmodelReasoningEffortMap; this can make an otherwise supported request fail upstream. Apply the provider effort map while constructing the Responses body, or normalize the raw reasoning effort before selecting this default wire.Useful? React with 👍 / 👎.