Skip to content

Commit e743660

Browse files
committed
fix(providers): default DeepSeek V4 Flash to Responses
1 parent 6e1cdb7 commit e743660

5 files changed

Lines changed: 78 additions & 3 deletions

File tree

docs-site/src/content/docs/reference/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ or bind the forward explicitly to loopback (`ssh -L 127.0.0.1:20100:localhost:10
325325
| `modelReasoningEfforts?` | `Record<string,string[]>` | Model-specific reasoning labels. An empty list hides the effort control for that model. |
326326
| `modelSupportsReasoningSummaries?` | `Record<string,boolean>` | Model-specific reasoning-summary capability. Set a model to `false` to stop advertising summaries and strip summary-delivery fields before an `openai-responses` request. |
327327
| `modelReasoningSummaryDelivery?` | `Record<string,"sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Model-specific Responses delivery enum. A configured model stays summary-capable and only an existing `stream_options.reasoning_summary_delivery` is rewritten; do not combine it with a `false` summary capability for the same model. |
328-
| `modelAdapters?` | `Record<string,string>` | Per-model wire override for a gateway that fronts models speaking different wires. Keys are upstream native model ids; values must be `openai-chat` or `openai-responses`. Useful when one model needs the Responses API for hosted tools such as `web_search` while its siblings are fine on chat completions. Models the upstream pins to a single wire, and the canonical ChatGPT forward provider, reject overrides. |
328+
| `modelAdapters?` | `Record<string,string>` | Per-model wire override for a gateway that fronts models speaking different wires. Keys are upstream native model ids; values must be `openai-chat` or `openai-responses`. Built-in registry defaults may select a verified mixed-wire route automatically (the DeepSeek preset sends `deepseek-v4-flash` over native Responses); an explicit entry wins and can opt a model back out. Models the upstream pins to a single wire, and the canonical ChatGPT forward provider, reject overrides. |
329329
| `reasoningEffortMap?` | `Record<string,string>` | Provider-wide wire aliases for reasoning labels. Use only when the upstream expects a different value. |
330330
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | Model-specific wire aliases for reasoning labels. |
331331
| `noReasoningModels?` | `string[]` | Models that reject a reasoning/thinking param — the adapter drops `reasoning_effort` for them. |

docs-site/src/content/docs/zh-cn/reference/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ x-opencodex-api-key: your-secret-token
172172
| `modelReasoningEfforts?` | `Record<string,string[]>` | 模型级 reasoning label。空数组会隐藏该模型的 effort 控件。 |
173173
| `modelSupportsReasoningSummaries?` | `Record<string,boolean>` | 模型级 reasoning summary 能力。设为 `false` 时不再声明 summary 支持,并在 `openai-responses` 请求前移除 summary-delivery 字段。 |
174174
| `modelReasoningSummaryDelivery?` | `Record<string,"sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | 模型级 Responses delivery enum。已配置模型保持 summary 能力,适配器只改写现有的 `stream_options.reasoning_summary_delivery`;同一模型不能同时将 summary 能力设为 `false`|
175+
| `modelAdapters?` | `Record<string,string>` | 面向同一 gateway 混合使用不同 wire 的模型级覆盖。键是上游原生模型 id,值只能是 `openai-chat``openai-responses`。已验证的混合 wire 路由会由 registry 自动提供默认值(DeepSeek preset 会让 `deepseek-v4-flash` 使用原生 Responses);显式配置优先,也可以把模型切回 Chat。上游固定单一 wire 的模型和 canonical ChatGPT forward provider 不接受覆盖。 |
175176
| `reasoningEffortMap?` | `Record<string,string>` | provider 级 reasoning label wire alias。只在上游需要不同值时使用。 |
176177
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | 模型级 reasoning label wire alias。 |
177178
| `noReasoningModels?` | `string[]` | 拒绝 reasoning/thinking 参数的模型;adapter 会为它们移除 `reasoning_effort`|

src/providers/registry.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ export interface ProviderRegistryEntry {
123123
defaultModel?: string;
124124
models?: string[];
125125
liveModels?: boolean;
126+
/**
127+
* Registry-only per-model wire defaults for mixed OpenAI-compatible gateways.
128+
* These are intentionally not seeded into saved config: an explicit `modelAdapters`
129+
* entry must remain distinguishable and must always win over a default.
130+
*/
131+
modelWireDefaults?: Record<string, string>;
126132
modelDiscovery?: ProviderModelDiscoverySpec;
127133
contextWindow?: number;
128134
modelContextWindows?: Record<string, number>;
@@ -813,6 +819,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
813819
models: ["deepseek-chat", "deepseek-reasoner", ...DEEPSEEK_THINKING_MODELS],
814820
defaultModel: "deepseek-v4-flash",
815821
modelContextWindows: { "deepseek-v4-flash": 1_000_000, "deepseek-v4-pro": 1_000_000 },
822+
// DeepSeek documents V4-Flash as a native Responses API model adapted for Codex. The
823+
// API id is `deepseek-v4-flash`; `DeepSeek-V4-Flash-0731` is a release/version label.
824+
modelWireDefaults: { "deepseek-v4-flash": "openai-responses" },
816825
/* [Decision Log]
817826
- 목적: DeepSeek V4 thinking mode multi-turn/tool-call requests must replay prior assistant reasoning_content.
818827
- 대안 분석: 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(
12351244
return normalizedProviderEndpoint(provider.baseUrl) === normalizedProviderEndpoint(entry.baseUrl);
12361245
}
12371246

1247+
/**
1248+
* Resolve a registry-only default for a mixed-wire provider. Defaults only move a provider
1249+
* between the two OpenAI-shaped adapters and never override a provider configured on another
1250+
* wire. The resolver receives the allow-list so this helper cannot accidentally widen the
1251+
* adapter-selection boundary when a new registry entry is added.
1252+
*/
1253+
export function providerModelWireDefault(
1254+
id: string,
1255+
provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
1256+
modelId: string,
1257+
allowedWires: ReadonlySet<string>,
1258+
): string | undefined {
1259+
if (!allowedWires.has(provider.adapter)) return undefined;
1260+
const entry = getProviderRegistryEntry(id);
1261+
if (!entry?.modelWireDefaults || !providerMatchesRegistryTransport(id, provider)) return undefined;
1262+
const wire = entry.modelWireDefaults[modelId.trim().toLowerCase()];
1263+
return wire !== undefined && allowedWires.has(wire) ? wire : undefined;
1264+
}
1265+
12381266
/**
12391267
* Effective Codex account mode for a provider. For canonical `openai`, a valid persisted
12401268
* `codexAccountMode` on the provider config wins and a missing/invalid value defaults to

src/server/adapter-resolve.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { createResponsesPassthroughAdapter } from "../adapters/openai-responses"
99
import type { OcxProviderConfig } from "../types";
1010
import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
1111
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
12+
import { providerModelWireDefault } from "../providers/registry";
1213

1314
/**
1415
* Resolve the wire a single model should use: a hard pin first, then a configured
15-
* per-model override, then the provider's own adapter.
16+
* per-model override, then a registry default for a mixed-wire provider, then the provider's
17+
* own adapter.
1618
*
1719
* Safe to call more than once on its own output — the pin check does not look at the
1820
* current adapter, so a second pass cannot let an override displace a pin.
@@ -24,7 +26,12 @@ export function resolveWireProtocolOverride(providerName: string, modelId: strin
2426
}
2527
// Re-check the allow-list here, not just in the config validator: the file may have
2628
// been hand-edited, or written by a build that allowed more values.
27-
const requested = providerConfig.modelAdapters?.[modelId];
29+
const configured = providerConfig.modelAdapters?.[modelId];
30+
// An explicit allowed override wins, including one naming the provider-wide adapter (the
31+
// opt-out from a registry default). Invalid hand-edited values fall through to the default.
32+
const requested = configured && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured)
33+
? configured
34+
: providerModelWireDefault(providerName, providerConfig, modelId, MODEL_ADAPTER_OVERRIDE_ALLOWED);
2835
if (requested
2936
&& MODEL_ADAPTER_OVERRIDE_ALLOWED.has(requested)
3037
&& requested !== providerConfig.adapter

tests/adapter-resolve.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,42 @@ describe("per-model wire override (#404)", () => {
9090
.toBe("openai-responses");
9191
});
9292
});
93+
94+
describe("registry per-model wire defaults", () => {
95+
function deepseek(overrides: Partial<OcxProviderConfig> = {}): OcxProviderConfig {
96+
return gateway({
97+
baseUrl: "https://api.deepseek.com",
98+
authMode: "key",
99+
...overrides,
100+
});
101+
}
102+
103+
test("routes only the official Flash API id through Responses", () => {
104+
expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", deepseek()).adapter)
105+
.toBe("openai-responses");
106+
expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-pro", deepseek()).adapter)
107+
.toBe("openai-chat");
108+
// The dated release label is not the API model id and must not be silently rewritten.
109+
expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash-0731", deepseek()).adapter)
110+
.toBe("openai-chat");
111+
});
112+
113+
test("an explicit Chat override opts Flash back out of the default", () => {
114+
const provider = deepseek({ modelAdapters: { "deepseek-v4-flash": "openai-chat" } });
115+
expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", provider).adapter)
116+
.toBe("openai-chat");
117+
});
118+
119+
test("defaults do not apply when the provider is already on another wire", () => {
120+
expect(resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", deepseek({ adapter: "anthropic" })).adapter)
121+
.toBe("anthropic");
122+
});
123+
124+
test("keeps provider credentials and destination untouched", () => {
125+
const provider = deepseek({ apiKey: "test-key" });
126+
const resolved = resolveWireProtocolOverride("deepseek", "deepseek-v4-flash", provider);
127+
expect(provider.adapter).toBe("openai-chat");
128+
expect(resolved.apiKey).toBe("test-key");
129+
expect(resolved.baseUrl).toBe("https://api.deepseek.com");
130+
});
131+
});

0 commit comments

Comments
 (0)