Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ or bind the forward explicitly to loopback (`ssh -L 127.0.0.1:20100:localhost:10
| `modelReasoningEfforts?` | `Record<string,string[]>` | Model-specific reasoning labels. An empty list hides the effort control for that model. |
| `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. |
| `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. |
| `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. |
| `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. |
| `reasoningEffortMap?` | `Record<string,string>` | Provider-wide wire aliases for reasoning labels. Use only when the upstream expects a different value. |
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | Model-specific wire aliases for reasoning labels. |
| `noReasoningModels?` | `string[]` | Models that reject a reasoning/thinking param — the adapter drops `reasoning_effort` for them. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ x-opencodex-api-key: your-secret-token
| `modelReasoningEfforts?` | `Record<string,string[]>` | 模型级 reasoning label。空数组会隐藏该模型的 effort 控件。 |
| `modelSupportsReasoningSummaries?` | `Record<string,boolean>` | 模型级 reasoning summary 能力。设为 `false` 时不再声明 summary 支持,并在 `openai-responses` 请求前移除 summary-delivery 字段。 |
| `modelReasoningSummaryDelivery?` | `Record<string,"sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | 模型级 Responses delivery enum。已配置模型保持 summary 能力,适配器只改写现有的 `stream_options.reasoning_summary_delivery`;同一模型不能同时将 summary 能力设为 `false`。 |
| `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 不接受覆盖。 |
| `reasoningEffortMap?` | `Record<string,string>` | provider 级 reasoning label wire alias。只在上游需要不同值时使用。 |
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | 模型级 reasoning label wire alias。 |
| `noReasoningModels?` | `string[]` | 拒绝 reasoning/thinking 参数的模型;adapter 会为它们移除 `reasoning_effort`。 |
Expand Down
28 changes: 28 additions & 0 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply the DeepSeek effort map on the Responses wire

When a user selects the advertised xhigh effort for deepseek-v4-flash, this default switches the request away from createOpenAIChatAdapter, which is the only adapter that calls mapReasoningEffort. The Responses adapter instead forwards _rawBody.reasoning.effort unchanged, so xhigh reaches DeepSeek rather than the registry-required max mapping declared in modelReasoningEffortMap; 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 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive parallel-tool capability from the effective model wire

The catalog still evaluates DeepSeek's provider-wide adapter: "openai-chat", so applyProviderConfigHints advertises parallelToolCalls: true by default even though this model is now executed by openai-responses. The catalog policy deliberately requires an explicit parallelToolCalls: true for non-chat adapters, but DeepSeek has no such opt-in; Codex can therefore emit parallel_tool_calls: true to a Responses route whose support was never declared or verified. Make per-model catalog derivation use the effective wire default, or explicitly register the capability if DeepSeek Responses supports it.

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.
Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions src/server/adapter-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve routed Responses sampling parameters

For DeepSeek Flash requests entering through /v1/messages or /v1/chat/completions, selecting this registry default makes the wrapper treat the route like the canonical ChatGPT passthrough: claude-messages.ts and chat-completions.ts then delete max_output_tokens, temperature, top_p, stop, and user solely because the effective adapter is openai-responses. Thus Claude Code's required max_tokens translation and equivalent chat-completions controls are silently discarded before calling DeepSeek, potentially removing output limits and changing sampling behavior. Restrict that ChatGPT-specific sanitization to isCanonicalOpenAiForwardProvider rather than every Responses route.

Useful? React with 👍 / 👎.

if (requested
&& MODEL_ADAPTER_OVERRIDE_ALLOWED.has(requested)
&& requested !== providerConfig.adapter
Expand Down
39 changes: 39 additions & 0 deletions tests/adapter-resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 openai-chat override. They do not verify the fallback in src/server/adapter-resolve.ts lines 29-34. Add a case where modelAdapters["deepseek-v4-flash"] is an unsupported value such as "anthropic". Assert that the resolver selects "openai-responses".

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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");
});
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");
});
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");
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/adapter-resolve.test.ts` around lines 113 - 117, Add a focused test
alongside the existing explicit Chat override case in adapter-resolve.test.ts
using an unsupported modelAdapters["deepseek-v4-flash"] value such as
"anthropic"; call resolveWireProtocolOverride for the DeepSeek Flash model and
assert that the returned adapter is "openai-responses", covering the
invalid-override fallback.

Source: 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");
});
});
Loading