diff --git a/docs/guides/CODEX-CLI-CONFIGURATION.md b/docs/guides/CODEX-CLI-CONFIGURATION.md index 943ca8660786..0749637fe50d 100644 --- a/docs/guides/CODEX-CLI-CONFIGURATION.md +++ b/docs/guides/CODEX-CLI-CONFIGURATION.md @@ -79,7 +79,7 @@ Use a real key instead when your OmniRoute server is protected or remote. Codex CLI deprecated `wire_api = "chat"` (Chat Completions) in February 2026 and now **requires** `wire_api = "responses"` (OpenAI Responses API). Setting `wire_api = "chat"` causes an immediate startup crash since v0.138. -DeepSeek, GLM, Kimi and others only expose a Chat Completions endpoint — not the Responses API. If you pointed Codex directly at them, it would fail. +Many providers, including GLM and Kimi, still expose only a Chat Completions endpoint. DeepSeek V4 now exposes a native Responses API as well as an Anthropic-compatible endpoint; OmniRoute uses Responses by default and lets each DeepSeek connection select Anthropic compatibility. **OmniRoute solves this transparently:** @@ -87,8 +87,8 @@ DeepSeek, GLM, Kimi and others only expose a Chat Completions endpoint — not t Codex CLI → wire_api = "responses" → POST /v1/responses (OmniRoute) - → OmniRoute Responses ↔ Chat Completions transformer - → POST /chat/completions (DeepSeek / Mistral / GLM / Kimi / any provider) + → OmniRoute selects the provider's native protocol and translates when needed + → POST /responses (DeepSeek V4) or /chat/completions (Mistral / GLM / Kimi / others) ``` You never need a separate translation proxy when using OmniRoute. **All models use `wire_api = "responses"`** — OmniRoute handles the rest. diff --git a/open-sse/config/providers/registry/deepseek/index.ts b/open-sse/config/providers/registry/deepseek/index.ts index 6825b230789f..933fb9bba1f5 100644 --- a/open-sse/config/providers/registry/deepseek/index.ts +++ b/open-sse/config/providers/registry/deepseek/index.ts @@ -1,25 +1,40 @@ -import type { RegistryEntry } from "../../shared.ts"; +import { getAnthropicCompatHeaders, type RegistryEntry } from "../../shared.ts"; export const deepseekProvider: RegistryEntry = { id: "deepseek", alias: "ds", - format: "openai", + format: "openai-responses", executor: "default", - baseUrl: "https://api.deepseek.com/v1/chat/completions", + baseUrl: "https://api.deepseek.com/responses", authType: "apikey", authHeader: "bearer", + alternateFormats: [ + { + format: "claude", + baseUrl: "https://api.deepseek.com/anthropic/v1/messages", + authHeader: "x-api-key", + headers: getAnthropicCompatHeaders(), + label: "Anthropic-compatible", + }, + ], models: [ { id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", + name: "DeepSeek V4 Pro (0813)", + contextLength: 1_000_000, + maxOutputTokens: 384_000, supportsReasoning: true, supportedThinkingEfforts: ["none", "high", "max"], + toolCalling: true, }, { id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", + name: "DeepSeek V4 Flash (0731)", + contextLength: 1_000_000, + maxOutputTokens: 384_000, supportsReasoning: true, supportedThinkingEfforts: ["none", "low", "high", "max"], + toolCalling: true, }, ], }; diff --git a/open-sse/config/providers/registry/grok-cli/index.ts b/open-sse/config/providers/registry/grok-cli/index.ts index 75effafe78af..f257f8d60a62 100644 --- a/open-sse/config/providers/registry/grok-cli/index.ts +++ b/open-sse/config/providers/registry/grok-cli/index.ts @@ -20,6 +20,15 @@ export const grok_cliProvider: RegistryEntry = { authHeader: "bearer", passthroughModels: true, models: [ + { + id: "grok-4.6", + name: "Grok 4.6", + contextLength: 500000, + supportsReasoning: true, + toolCalling: true, + targetFormat: "openai-responses", + unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"], + }, { id: "grok-4.5", name: "Grok 4.5", diff --git a/open-sse/config/providers/registry/xai/index.ts b/open-sse/config/providers/registry/xai/index.ts index f33e247080b3..6d2cc9621adc 100644 --- a/open-sse/config/providers/registry/xai/index.ts +++ b/open-sse/config/providers/registry/xai/index.ts @@ -14,6 +14,17 @@ export const xaiProvider: RegistryEntry = { authType: "apikey", authHeader: "bearer", models: [ + { + id: "grok-4.6", + name: "Grok 4.6", + contextLength: 500000, + supportsReasoning: true, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh"], + supportsVision: true, + supportsXHighEffort: true, + toolCalling: true, + targetFormat: "openai-responses", + }, { id: "grok-4.3", name: "Grok 4.3" }, { id: "grok-build-0.1", name: "Grok Build 0.1", contextLength: 256000 }, // Responses-only per upstream 9router#2439: xAI serves this id exclusively diff --git a/open-sse/handlers/chatCore/targetFormat.ts b/open-sse/handlers/chatCore/targetFormat.ts index 991a5c3eb2e3..27ce3aa3d83e 100644 --- a/open-sse/handlers/chatCore/targetFormat.ts +++ b/open-sse/handlers/chatCore/targetFormat.ts @@ -3,14 +3,17 @@ * decomposition, #3501). * * Pure resolution of the provider alias + the upstream target format used to translate the request. - * Model/custom overrides win first. A Responses-shaped inbound request normally keeps the Responses - * wire format, except for custom OpenAI-compatible connections explicitly configured for Chat. + * Model/custom overrides win first. A declared connection-level alternate protocol wins next. A + * Responses-shaped inbound request otherwise keeps the Responses wire format, except for custom + * OpenAI-compatible connections explicitly configured for Chat. * AgentRouter may inherit the inbound protocol when no explicit connection override exists. * Returns both `alias` (reused by the handler when stripping the `alias/` prefix off the upstream * model id) and `targetFormat`. */ import { PROVIDER_ID_TO_ALIAS, getModelTargetFormat } from "../../config/providerModels.ts"; +import { getRegistryEntry } from "../../config/providerRegistry.ts"; +import { resolveAlternateFormat } from "../../config/providers/alternateFormats.ts"; import { getTargetFormat } from "../../services/provider.ts"; import { FORMATS } from "../../translator/formats.ts"; @@ -46,15 +49,22 @@ export function resolveChatCoreTargetFormat(opts: { ? sourceFormat : undefined; const providerTargetFormat = getTargetFormat(provider, providerSpecificData); + const declaredConnectionAlternate = resolveAlternateFormat( + getRegistryEntry(provider), + providerSpecificData + ); const customOpenAICompatible = provider.startsWith("openai-compatible-"); // #8994: model-level targetFormat overrides (from registry or custom-model DB override) // take precedence over apiFormat="responses" — otherwise Vertex Claude models with // targetFormat="claude" get wrongly routed to OpenAI Responses format. // #9161: a custom OpenAI-compatible Chat connection must likewise keep its configured // outbound protocol when a Responses-shaped client (for example Codex) calls /responses. + // Registry-declared connection alternates are equally explicit: a DeepSeek connection set to + // Anthropic must stay on /anthropic/v1/messages even when the caller speaks Responses. let targetFormat = modelTargetFormat || customModelTargetFormat || + declaredConnectionAlternate?.format || (apiFormat === "responses" && !customOpenAICompatible ? FORMATS.OPENAI_RESPONSES : inferredAgentRouterTargetFormat || providerTargetFormat); diff --git a/src/shared/constants/pricing/frontier-labs.ts b/src/shared/constants/pricing/frontier-labs.ts index 59e580e97111..a7cbf79935fb 100644 --- a/src/shared/constants/pricing/frontier-labs.ts +++ b/src/shared/constants/pricing/frontier-labs.ts @@ -315,20 +315,20 @@ export const DEFAULT_PRICING_FRONTIER = { reasoning: 2.19, cache_creation: 0.55, }, - // DeepSeek V4 Pro — promo until 2026-05-31, then list ($0.145 / $3.48) + // DeepSeek official API list prices, checked 2026-08-13. "deepseek-v4-pro": { input: 0.435, output: 0.87, - cached: 0.0036, + cached: 0.003625, reasoning: 0.87, cache_creation: 0.435, }, "deepseek-v4-flash": { - input: 0.07, + input: 0.14, output: 0.28, - cached: 0.014, + cached: 0.0028, reasoning: 0.28, - cache_creation: 0.07, + cache_creation: 0.14, }, }, blackbox: { @@ -340,6 +340,15 @@ export const DEFAULT_PRICING_FRONTIER = { "blackboxai-pro": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 }, }, xai: { + // The static rate covers prompts below 200K tokens. xAI's provider-reported + // cost_in_usd_ticks remains authoritative for the >=200K pricing tier. + "grok-4.6": { + input: 2.0, + output: 6.0, + cached: 0.5, + reasoning: 6.0, + cache_creation: 2.0, + }, "grok-3": { input: 3.0, output: 15.0, diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 699c8acbd495..451a11fa09b4 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -1604,7 +1604,7 @@ } }, "deepseek": { - "format": "openai", + "format": "openai-responses", "headers": { "apiKey": { "Accept": "text/event-stream", @@ -1622,8 +1622,8 @@ } }, "url": { - "nonStream": "https://api.deepseek.com/v1/chat/completions", - "stream": "https://api.deepseek.com/v1/chat/completions" + "nonStream": "https://api.deepseek.com/responses", + "stream": "https://api.deepseek.com/responses" } }, "deepseek-web": { diff --git a/tests/unit/alternate-formats.test.ts b/tests/unit/alternate-formats.test.ts index 9aa40d7f348f..c285c7f2c80e 100644 --- a/tests/unit/alternate-formats.test.ts +++ b/tests/unit/alternate-formats.test.ts @@ -4,6 +4,8 @@ import { resolveAlternateFormat } from "../../open-sse/config/providers/alternat import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts"; import { getTargetFormat } from "../../open-sse/services/provider.ts"; import { DefaultExecutor } from "../../open-sse/executors/default.ts"; +import { FORMATS } from "../../open-sse/translator/formats.ts"; +import { translateRequest } from "../../open-sse/translator/index.ts"; import { getAlternateFormats } from "../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts"; const ENTRY: RegistryEntry = { @@ -42,7 +44,10 @@ test("retorna null quando a conexao nao tem targetFormat", () => { }); test("retorna null quando a entry nao declara alternativas", () => { - assert.equal(resolveAlternateFormat({ ...ENTRY, alternateFormats: undefined }, { targetFormat: "claude" }), null); + assert.equal( + resolveAlternateFormat({ ...ENTRY, alternateFormats: undefined }, { targetFormat: "claude" }), + null + ); assert.equal(resolveAlternateFormat(null, { targetFormat: "claude" }), null); }); @@ -103,7 +108,11 @@ test("resolveBaseUrl: baseUrl manual da conexao vence a alternativa", () => { }); test("resolveBaseUrl: alternativa vence o baseUrl padrao", () => { - const url = precedence({ targetFormat: "claude" }, ENTRY_WITH_ALT, "https://default.example.com/v1"); + const url = precedence( + { targetFormat: "claude" }, + ENTRY_WITH_ALT, + "https://default.example.com/v1" + ); assert.equal(url, "https://alt.example.com/anthropic/v1/messages"); }); @@ -179,7 +188,81 @@ test("getAlternateFormats: provedor com alternativas retorna a lista", () => { }); test("getAlternateFormats: provedor sem alternativas retorna lista vazia", () => { - assert.deepEqual(getAlternateFormats("deepseek"), []); + assert.deepEqual(getAlternateFormats("xai"), []); assert.deepEqual(getAlternateFormats(null), []); assert.deepEqual(getAlternateFormats(undefined), []); }); + +test("DeepSeek defaults to Responses and exposes the official Anthropic endpoint", () => { + assert.equal(getTargetFormat("deepseek", null), "openai-responses"); + assert.equal(getTargetFormat("deepseek", { targetFormat: "claude" }), "claude"); + + const defaultExecutor = new DefaultExecutor("deepseek"); + assert.equal( + defaultExecutor.buildUrl("deepseek-v4-pro", true, 0, { apiKey: "sk-test" } as never), + "https://api.deepseek.com/responses" + ); + const defaultHeaders = defaultExecutor.buildHeaders({ apiKey: "sk-test" } as never, true); + assert.equal(defaultHeaders.Authorization, "Bearer sk-test"); + + const anthropicCredentials = { + apiKey: "sk-test", + providerSpecificData: { targetFormat: "claude" }, + } as never; + assert.equal( + defaultExecutor.buildUrl("deepseek-v4-pro", true, 0, anthropicCredentials), + "https://api.deepseek.com/anthropic/v1/messages" + ); + const anthropicHeaders = defaultExecutor.buildHeaders(anthropicCredentials, true); + assert.equal(anthropicHeaders["x-api-key"], "sk-test"); + assert.equal(typeof anthropicHeaders["Anthropic-Version"], "string"); + + const alternates = getAlternateFormats("deepseek"); + assert.equal(alternates.length, 1); + assert.equal(alternates[0].format, "claude"); +}); + +test("DeepSeek reuses the generic Chat-to-Responses and Responses-to-Anthropic translators", () => { + const responsesBody = translateRequest( + FORMATS.OPENAI, + FORMATS.OPENAI_RESPONSES, + "deepseek-v4-pro", + { + model: "deepseek-v4-pro", + messages: [{ role: "user", content: "hello" }], + max_tokens: 123, + stream: true, + }, + true, + {}, + "deepseek" + ) as Record; + assert.equal(responsesBody.messages, undefined); + assert.equal(responsesBody.max_output_tokens, 123); + assert.deepEqual(responsesBody.input, [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "hello" }], + status: "completed", + }, + ]); + + const anthropicBody = translateRequest( + FORMATS.OPENAI_RESPONSES, + FORMATS.CLAUDE, + "deepseek-v4-pro", + { + model: "deepseek-v4-pro", + input: [{ role: "user", content: [{ type: "input_text", text: "hello" }] }], + stream: true, + }, + true, + {}, + "deepseek" + ) as Record; + assert.equal(anthropicBody.input, undefined); + assert.deepEqual(anthropicBody.messages, [ + { role: "user", content: [{ type: "text", text: "hello" }] }, + ]); +}); diff --git a/tests/unit/chat-route-coverage.test.ts b/tests/unit/chat-route-coverage.test.ts index b7174d635815..e0b71badc966 100644 --- a/tests/unit/chat-route-coverage.test.ts +++ b/tests/unit/chat-route-coverage.test.ts @@ -202,6 +202,7 @@ test("handleChat rejects requests without a model", async () => { test("handleChat applies task-aware routing when a semantic override is enabled", async () => { await seedConnection("deepseek", { apiKey: "sk-deepseek-task-route" }); const seenAuthHeaders = []; + const seenRequestBodies = []; setTaskRoutingConfig({ enabled: true, detectionEnabled: true, @@ -214,7 +215,26 @@ test("handleChat applies task-aware routing when a semantic override is enabled" globalThis.fetch = async (_url, init = {}) => { const headers = toPlainHeaders(init.headers); seenAuthHeaders.push(headers.Authorization ?? headers.authorization); - return buildOpenAIResponse("Task-routed response", "deepseek/deepseek-chat"); + seenRequestBodies.push(JSON.parse(String(init.body))); + return new Response( + JSON.stringify({ + id: "resp_task_route", + object: "response", + status: "completed", + model: "deepseek-v4-flash", + output: [ + { + id: "msg_task_route", + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "Task-routed response", annotations: [] }], + }, + ], + usage: { input_tokens: 4, output_tokens: 2, total_tokens: 6 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); }; const response = await handleChat( @@ -230,6 +250,8 @@ test("handleChat applies task-aware routing when a semantic override is enabled" assert.equal(response.status, 200); assert.deepEqual(seenAuthHeaders, ["Bearer sk-deepseek-task-route"]); + assert.equal(seenRequestBodies[0].messages, undefined); + assert.equal(seenRequestBodies[0].input[0].role, "user"); assert.equal(json.choices[0].message.content, "Task-routed response"); }); diff --git a/tests/unit/chatcore-target-format.test.ts b/tests/unit/chatcore-target-format.test.ts index 4eb22d559881..b820b245fe58 100644 --- a/tests/unit/chatcore-target-format.test.ts +++ b/tests/unit/chatcore-target-format.test.ts @@ -115,6 +115,18 @@ test("#8994: customModelTargetFormat takes precedence over apiFormat='responses' assert.equal(r.targetFormat, "claude", "model-level targetFormat must win over apiFormat"); }); +test("a declared connection alternate overrides the inbound Responses protocol", () => { + const r = resolveChatCoreTargetFormat({ + provider: "deepseek", + resolvedModel: "deepseek-v4-pro", + apiFormat: "responses", + sourceFormat: FORMATS.OPENAI_RESPONSES, + customModelTargetFormat: undefined, + providerSpecificData: { targetFormat: FORMATS.CLAUDE }, + }); + assert.equal(r.targetFormat, FORMATS.CLAUDE); +}); + test("unmapped provider → alias falls back to the provider id", () => { const r = resolveChatCoreTargetFormat({ provider: "some-unmapped-provider", diff --git a/tests/unit/chatcore-translation-paths.test.ts b/tests/unit/chatcore-translation-paths.test.ts index 397f3f50d69e..c82d6987b23a 100644 --- a/tests/unit/chatcore-translation-paths.test.ts +++ b/tests/unit/chatcore-translation-paths.test.ts @@ -607,8 +607,10 @@ test("chatCore applies Responses input policy to openai-compatible targets", asy }); test("chatCore replays no-tool reasoning across public Responses turns", async () => { + // Direct DeepSeek now speaks Responses upstream. Keep this regression on a + // Chat-compatible DeepSeek host so it continues to exercise the Responses-to-Chat replay path. saveModelsDevCapabilities({ - deepseek: { + siliconflow: { "deepseek-v4-pro": { ...capabilityEntry(128_000), reasoning: true, @@ -640,7 +642,7 @@ test("chatCore replays no-tool reasoning across public Responses turns", async ( ); const first = await invokeChatCore({ - provider: "deepseek", + provider: "siliconflow", model: "deepseek-v4-pro", endpoint: "/v1/responses", body: { @@ -656,7 +658,7 @@ test("chatCore replays no-tool reasoning across public Responses turns", async ( assert.equal(first.result.success, true); const second = await invokeChatCore({ - provider: "deepseek", + provider: "siliconflow", model: "deepseek-v4-pro", endpoint: "/v1/responses", body: { @@ -687,7 +689,7 @@ test("chatCore replays no-tool reasoning across public Responses turns", async ( }); test("chatCore captures streaming no-tool reasoning for Responses replay", async () => { saveModelsDevCapabilities({ - deepseek: { + siliconflow: { "deepseek-v4-pro": { ...capabilityEntry(128_000), reasoning: true, @@ -730,7 +732,7 @@ test("chatCore captures streaming no-tool reasoning for Responses replay", async ); const first = await invokeChatCore({ - provider: "deepseek", + provider: "siliconflow", model: "deepseek-v4-pro", endpoint: "/v1/responses", body: { @@ -748,7 +750,7 @@ test("chatCore captures streaming no-tool reasoning for Responses replay", async await flushAsyncSideEffects(); const second = await invokeChatCore({ - provider: "deepseek", + provider: "siliconflow", model: "deepseek-v4-pro", endpoint: "/v1/responses", body: { diff --git a/tests/unit/executor-xai.test.ts b/tests/unit/executor-xai.test.ts index 5d4d05fa7421..4668efd22edd 100644 --- a/tests/unit/executor-xai.test.ts +++ b/tests/unit/executor-xai.test.ts @@ -6,6 +6,7 @@ import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/in import { xaiProvider } from "../../open-sse/config/providers/registry/xai/index.ts"; // Real xai catalog ids (open-sse/config/providers/registry/xai/index.ts): +// grok-4.6 — Responses-first flagship with vision + reasoning // grok-4.3 — plain, reasoning-capable // grok-build-0.1 — build/tool model, no reasoning mode // grok-4.20-multi-agent-0309 — neutral (not in either allow/deny list) @@ -28,6 +29,21 @@ test("XaiExecutor can target the separate xAI OAuth provider config", () => { assert.equal(executor.buildUrl("grok-4.5", false), "https://api.x.ai/v1/responses"); }); +test("Grok 4.6 advertises its official capabilities and uses native Responses", () => { + const model = xaiProvider.models.find((entry) => entry.id === "grok-4.6"); + assert.ok(model); + assert.equal(model.contextLength, 500000); + assert.equal(model.supportsVision, true); + assert.equal(model.supportsReasoning, true); + assert.equal(model.toolCalling, true); + assert.equal(model.supportsXHighEffort, true); + assert.deepEqual(model.supportedThinkingEfforts, ["low", "medium", "high", "xhigh"]); + assert.equal(model.targetFormat, "openai-responses"); + + const executor = new XaiExecutor(); + assert.equal(executor.buildUrl("grok-4.6", true), "https://api.x.ai/v1/responses"); +}); + test("strips a -{level} suffix from an allow-listed model and sets reasoning_effort", () => { const executor = new XaiExecutor(); diff --git a/tests/unit/grok-cli-responses-compat.test.ts b/tests/unit/grok-cli-responses-compat.test.ts index ad9888fe9417..3af78305b7ac 100644 --- a/tests/unit/grok-cli-responses-compat.test.ts +++ b/tests/unit/grok-cli-responses-compat.test.ts @@ -22,6 +22,12 @@ test("grok-cli exposes the authenticated grok-build model catalog", () => { targetFormat, })), [ + { + id: "grok-4.6", + name: "Grok 4.6", + contextLength: 500000, + targetFormat: "openai-responses", + }, { id: "grok-4.5", name: "Grok 4.5", @@ -36,13 +42,15 @@ test("grok-cli exposes the authenticated grok-build model catalog", () => { }, ] ); + assert.equal(getModelTargetFormat("gc", "grok-4.6"), "openai-responses"); assert.equal(getModelTargetFormat("gc", "grok-4.5"), "openai-responses"); assert.equal(getModelTargetFormat("gc", "grok-composer-2.5-fast"), "openai-responses"); assert.equal(grok_cliProvider.modelsUrl, GROK_BUILD_MODELS_URL); }); -test("grok-cli routes both models to the Responses endpoint", () => { +test("grok-cli routes its catalog models to the Responses endpoint", () => { const executor = new GrokCliExecutor(); + assert.equal(executor.buildUrl("grok-4.6", true), "https://cli-chat-proxy.grok.com/v1/responses"); assert.equal(executor.buildUrl("grok-4.5", true), "https://cli-chat-proxy.grok.com/v1/responses"); assert.equal( executor.buildUrl("grok-composer-2.5-fast", false),