fix(providers): default DeepSeek Flash route speaks Responses; effort mapping matches the vendor table

Slice of the #6289 direction (named OpenAI-compatible hosts are ordinary providers):
- `deepseek-flash` (the shipped default) and legacy `deepseek-v4-flash` now resolve over the Responses endpoint per DeepSeek's documented Codex path; `deepseek-v4-pro` stays on Chat Completions until its announced Responses rollout. The curated seam row for deepseek-flash is default_for_provider; the Pro row no longer claims a default it never matched (DEFAULT_DEEPSEEK_MODEL has been deepseek-flash).
- `xhigh` now maps to `high` per DeepSeek's requested-to-actual table (minimal/low->low, medium/xhigh->high, max/ultra->max); module doc carries the table and a 2026-09-17 verified date; unit test pins every row.
- docs/PROVIDERS.md: deepseek rows describe the model-aware wire and the documented mapping.

Gate: cargo fmt --all clean; cargo check -p codewhale-config -p codewhale-tui clean; codewhale-config route::tests 98 passed 0 failed (crates/config/src/route/tests.rs also carries the resolver-level files_api test that lands with the next slice); codewhale-tui deepseek filter 164 passed 0 failed.
This commit is contained in:
CodeWhale Bot committed 2026-09-17 00:46:14 -07:00
1 parent f8d00a31ff
commit a1c1741afa
5 files changed
+131 -21

No files matched your search

+16 -2
View File
@@ -233,7 +233,11 @@ pub fn codewhale_endpoint_key_for_model(model: &str) -> &'static str {
#[must_use]
pub fn bundled_offerings() -> Vec<ProviderModelOffering> {
// DeepSeek's 2026-07-31 production Flash update added a native Responses
// endpoint without changing the model id. Pro remains Chat Completions
// endpoint without changing the model id, and the 2026-08 unversioned
// rename to `deepseek-flash` kept that wire: DeepSeek's own Codex
// integration documents the Responses API as the path for `deepseek-flash`
// (legacy `deepseek-v4-flash` ids are served by the same model). The
// shipped default therefore rides Responses; Pro remains Chat Completions
// until its announced Responses rollout. These exact-route transport facts
// cannot be represented by the Models.dev-shaped fallback asset.
let deepseek = ProviderId::from("deepseek");
@@ -256,12 +260,22 @@ pub fn bundled_offerings() -> Vec<ProviderModelOffering> {
output_tokens: Some(384_000),
};
let mut offerings = vec![
ProviderModelOffering {
provider: deepseek.clone(),
canonical_model: Some(ModelId::from("deepseek-flash")),
wire_model_id: WireModelId::from("deepseek-flash"),
endpoint_key: "responses".to_string(),
default_for_provider: true,
limits: documented_limits,
capabilities: documented_capabilities,
pricing: PricingSku::UnknownOrStale,
},
ProviderModelOffering {
provider: deepseek.clone(),
canonical_model: Some(ModelId::from("deepseek-v4-pro")),
wire_model_id: WireModelId::from("deepseek-v4-pro"),
endpoint_key: "chat".to_string(),
default_for_provider: true,
default_for_provider: false,
limits: documented_limits,
capabilities: documented_capabilities,
pricing: PricingSku::UnknownOrStale,
+67 -2
View File
@@ -389,6 +389,16 @@ fn resolver_explicit_provider_scoped_model_maps_to_wire_id() {
fn resolver_routes_only_official_deepseek_flash_over_responses() {
let resolver = RouteResolver::new();
let default_flash = resolver
.resolve(&req(Some(ProviderKind::Deepseek), Some("deepseek-flash")))
.expect("official default Flash route resolves");
assert_eq!(default_flash.protocol(), RequestProtocol::Responses);
assert_eq!(default_flash.endpoint().endpoint_key, "responses");
assert_eq!(
default_flash.capabilities().server_side_web_search,
CapabilityState::Supported
);
let flash = resolver
.resolve(&req(
Some(ProviderKind::Deepseek),
@@ -447,6 +457,59 @@ fn resolver_routes_only_official_deepseek_flash_over_responses() {
);
}
#[test]
fn resolver_sources_deepseek_files_api_only_on_exact_official_flash_routes() {
let resolver = RouteResolver::new();
for model in ["deepseek-flash", "deepseek-v4-flash"] {
let route = resolver
.resolve(&req(Some(ProviderKind::Deepseek), Some(model)))
.expect("official Flash route resolves");
assert_eq!(
route.capabilities().files_api,
CapabilityState::Supported,
"{model}"
);
}
for model in [
"deepseek-v4-pro",
"deepseek-v4-flash-vision-exp",
"deepseek-v5-future",
] {
let route = resolver
.resolve(&req(Some(ProviderKind::Deepseek), Some(model)))
.expect("direct DeepSeek route resolves");
assert_eq!(
route.capabilities().files_api,
CapabilityState::Unknown,
"{model} must not inherit the Files API fact"
);
}
let custom = resolver
.resolve(&RouteRequest {
explicit_provider: Some(ProviderKind::Deepseek),
model_selector: Some(LogicalModelRef::from("deepseek-flash")),
saved_provider_model: None,
base_url_override: Some("https://compatible.example/v1".to_string()),
limit_overrides: Vec::new(),
})
.expect("custom compatible endpoint resolves");
assert_eq!(custom.capabilities().files_api, CapabilityState::Unknown);
let aggregator = resolver
.resolve(&req(
Some(ProviderKind::Openrouter),
Some("deepseek/deepseek-v4-flash"),
))
.expect("aggregator route resolves");
assert_eq!(
aggregator.capabilities().files_api,
CapabilityState::Unknown
);
}
#[test]
fn resolver_uncatalogued_deepseek_preview_keeps_chat_and_verbatim_model() {
let model = "deepseek-v4.1-flash-expires-on-0910";
@@ -615,7 +678,8 @@ fn resolver_auto_is_sentinel_not_literal_model() {
assert!(out.logical_model().is_auto());
// ...and "auto" is NOT put on the wire as a literal model.
assert_ne!(out.wire_model_id().as_str(), "auto");
assert_eq!(out.wire_model_id().as_str(), "deepseek-v4-pro");
assert_eq!(out.wire_model_id().as_str(), "deepseek-flash");
assert_eq!(out.protocol(), RequestProtocol::Responses);
}
#[test]
@@ -1420,7 +1484,8 @@ fn resolver_deepseek_none_selector_uses_default_wire_id() {
.resolve(&req(Some(ProviderKind::Deepseek), None))
.expect("none selector should use provider default");
assert_eq!(out.provider_kind(), ProviderKind::Deepseek);
assert_eq!(out.wire_model_id().as_str(), "deepseek-v4-pro");
assert_eq!(out.wire_model_id().as_str(), "deepseek-flash");
assert_eq!(out.protocol(), RequestProtocol::Responses);
}
#[test]
+39 -11
View File
@@ -6,8 +6,9 @@
//! across two files instead of one edit here.
//!
//! Source: <https://api-docs.deepseek.com/api/create-chat-completion> for the
//! Chat Completions `reasoning_effort` values, and DeepSeek's Responses API
//! reference for `reasoning.effort` (verified 2026-08-10).
//! Chat Completions `reasoning_effort` values, and DeepSeek's Thinking Mode
//! guide <https://api-docs.deepseek.com/guides/reasoning_model> for the
//! requested-to-actual mapping (verified 2026-09-17).
//!
//! What the wires document, and what this table encodes:
//!
@@ -18,15 +19,24 @@
//! | High | `reasoning_effort: "high"` | `effort: "high"` |
//! | Max | `reasoning_effort: "max"` | `effort: "max"` |
//!
//! Neither wire documents a `medium`, so CodeWhale's medium rounds up to the
//! nearest documented tier (high), which is also the server default in
//! thinking mode.
//! DeepSeek's Thinking Mode guide documents the requested-to-actual effort
//! mapping; thinking is enabled by default with an actual effort of `high`:
//!
//! **DeepSeek documents that the Pro actual-effort mapping changes in early
//! August 2026.** When it does, edit [`DeepseekEffortTier`]'s two wire
//! accessors and [`DEEPSEEK_EFFORT_ALIASES`] below, bump the verified date in
//! this comment, and both call sites move together — that is the whole point
//! of this module.
//! | Requested | Actual |
//! |-----------|--------|
//! | minimal | low |
//! | low | low |
//! | medium | high |
//! | high | high |
//! | xhigh | high |
//! | max | max |
//! | ultra | max |
//!
//! `medium`, `xhigh`, and `ultra` are client-side requests that collapse onto
//! the documented wire tiers — they are not new wire values. When DeepSeek
//! changes the mapping, edit [`DeepseekEffortTier`]'s two wire accessors and
//! [`DEEPSEEK_EFFORT_ALIASES`] below, bump the verified date in this comment,
//! and both call sites move together — that is the whole point of this module.
/// A documented DeepSeek thinking tier, independent of which wire carries it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -91,7 +101,7 @@ pub(super) const DEEPSEEK_EFFORT_ALIASES: &[(&str, DeepseekEffortTier)] = &[
("mid", DeepseekEffortTier::High),
("high", DeepseekEffortTier::High),
("", DeepseekEffortTier::High),
("xhigh", DeepseekEffortTier::Max),
("xhigh", DeepseekEffortTier::High),
("max", DeepseekEffortTier::Max),
("maximum", DeepseekEffortTier::Max),
("highest", DeepseekEffortTier::Max),
@@ -122,6 +132,24 @@ pub(super) fn deepseek_effort_tier_or_default(raw: &str) -> DeepseekEffortTier {
mod tests {
use super::*;
/// DeepSeek's documented requested-to-actual mapping (Thinking Mode
/// guide, verified 2026-09-17). Pin every row so a future edit to the
/// table cannot silently disagree with the vendor.
#[test]
fn vendor_requested_to_actual_mapping_is_pinned() {
for (requested, tier) in [
("minimal", DeepseekEffortTier::Low),
("low", DeepseekEffortTier::Low),
("medium", DeepseekEffortTier::High),
("high", DeepseekEffortTier::High),
("xhigh", DeepseekEffortTier::High),
("max", DeepseekEffortTier::Max),
("ultra", DeepseekEffortTier::Max),
] {
assert_eq!(deepseek_effort_tier(requested), Some(tier), "{requested}");
}
}
#[test]
fn aliases_are_unique_and_normalized() {
for (index, (alias, _)) in DEEPSEEK_EFFORT_ALIASES.iter().enumerate() {
+2 -2
View File
@@ -745,7 +745,7 @@ fn deepseek_flash_responses_body_uses_stateless_0731_contract() {
"{}",
body["top_p"]
);
assert_eq!(body.pointer("/reasoning/effort"), Some(&json!("max")));
assert_eq!(body.pointer("/reasoning/effort"), Some(&json!("high")));
assert!(body.pointer("/reasoning/summary").is_none());
assert!(body.get("include").is_none());
assert!(body.get("store").is_none());
@@ -894,7 +894,7 @@ fn deepseek_responses_reasoning_effort_uses_documented_labels() {
assert_eq!(responses_reasoning_effort("low", true), Some("low"));
assert_eq!(responses_reasoning_effort("medium", true), Some("high"));
assert_eq!(responses_reasoning_effort("high", true), Some("high"));
assert_eq!(responses_reasoning_effort("xhigh", true), Some("max"));
assert_eq!(responses_reasoning_effort("xhigh", true), Some("high"));
assert_eq!(responses_reasoning_effort("max", true), Some("max"));
// The off tier must disable thinking on the wire, not collapse into
// low: DeepSeek documents `reasoning.effort: "none"` as the off value.
+7 -4
View File
@@ -193,7 +193,7 @@ the listed provider env vars.
| Provider ID | TOML table | Wire protocol | Auth env vars |
| --- | --- | --- | --- |
| `deepseek` | `[providers.deepseek]` | OpenAI Chat Completions | `DEEPSEEK_API_KEY` |
| `deepseek` | `[providers.deepseek]` | Model-aware: Responses default (`deepseek-flash`); Chat Completions (`deepseek-v4-pro`) | `DEEPSEEK_API_KEY` |
| `deepseek-anthropic` | `[providers.deepseek_anthropic]` | Anthropic Messages | `DEEPSEEK_API_KEY` |
| `nvidia-nim` | `[providers.nvidia_nim]` | OpenAI Chat Completions | `NVIDIA_API_KEY`, `NVIDIA_NIM_API_KEY` |
| `openai` | `[providers.openai]` | OpenAI Chat Completions | `OPENAI_API_KEY` |
@@ -243,8 +243,11 @@ Default base URLs and models for each route are listed in the shipped provider
table below. The wire protocol values above are derived from
`crates/config/src/provider.rs`: `ChatCompletions` is the default,
`openai-codex` overrides to `Responses`; `deepseek-anthropic`, `anthropic`, and
`openmodel` override to `AnthropicMessages`; and `opencode-zen` resolves the
protocol from the selected model's curated offering.
`openmodel` override to `AnthropicMessages`; `opencode-zen` resolves the
protocol from the selected model's curated offering; and `deepseek` is
model-aware — the shipped default `deepseek-flash` (and legacy
`deepseek-v4-flash`) rides the Responses endpoint while `deepseek-v4-pro`
stays on Chat Completions.
## Auth And Env Rules
@@ -652,7 +655,7 @@ overlay and lets DSH resolve its own keys.
| Provider ID | TOML table | Auth env | Base URL env and default | Default or static models | Notes |
| --- | --- | --- | --- | --- | --- |
| `deepseek` | `[providers.deepseek]` | `DEEPSEEK_API_KEY` | `CODEWHALE_BASE_URL` / `DEEPSEEK_BASE_URL`; default `https://api.deepseek.com/beta` | `deepseek-flash` (shipped default; V4.1 Flash, unversioned id), `deepseek-v4-pro`, `deepseek-v4-flash`, experimental `deepseek-v4-flash-vision-exp`; vision aliases `flash-vision`, `deepseek-v4flashvisionexp`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | First-class default. The live Pro backend is labeled `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`. Beta URL enables strict tool mode, chat prefix completion, and FIM completion. The documented V4 routes can use provider-native web search through a separate bounded Responses request; compatible custom endpoints do not inherit that capability. Set `https://api.deepseek.com` or `/v1` explicitly to opt out of beta-only features. Reasoning effort maps to the documented wire ladder `low`/`high`/`max` plus the `thinking` toggle: `off` sends `thinking: {"type":"disabled"}`, `low` sends `reasoning_effort: "low"`, `medium` rounds up to `"high"` (the wire has no medium), and `high`/`max` pass through. The experimental vision ID was observed in the authenticated `/models` roster on 2026-08-21 and is advertised as image-input capable on the direct Chat Completions route only. Its limits, reasoning, and tool-call flags provisionally inherit Flash; pricing remains unknown, and no funded image round trip was made during this release work. |
| `deepseek` | `[providers.deepseek]` | `DEEPSEEK_API_KEY` | `CODEWHALE_BASE_URL` / `DEEPSEEK_BASE_URL`; default `https://api.deepseek.com/beta` | `deepseek-flash` (shipped default; V4.1 Flash, unversioned id), `deepseek-v4-pro`, `deepseek-v4-flash`, experimental `deepseek-v4-flash-vision-exp`; vision aliases `flash-vision`, `deepseek-v4flashvisionexp`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | First-class default. The live Pro backend is labeled `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`. Beta URL enables strict tool mode, chat prefix completion, and FIM completion. The documented V4 routes can use provider-native web search through a separate bounded Responses request; compatible custom endpoints do not inherit that capability. Set `https://api.deepseek.com` or `/v1` explicitly to opt out of beta-only features. The shipped default `deepseek-flash` speaks the Responses API (DeepSeek's documented path for Codex-style integration, since the 2026-07-31 Flash production update); the Chat-only controls (the `thinking` toggle and strict-tool `/beta` routing) apply to Chat Completions routes, and `deepseek-v4-pro` stays on Chat Completions until its announced Responses rollout. Reasoning effort follows DeepSeek's documented requested-to-actual mapping: `minimal`/`low` land on `low`, `medium`/`xhigh` on `high`, and `max`/`ultra` on `max`; `off` disables thinking (`thinking: {"type":"disabled"}` on Chat, `reasoning.effort: "none"` on Responses). The experimental vision ID was observed in the authenticated `/models` roster on 2026-08-21 and is advertised as image-input capable on the direct Chat Completions route only. Its limits, reasoning, and tool-call flags provisionally inherit Flash; pricing remains unknown, and no funded image round trip was made during this release work. |
| `deepseek-anthropic` | `[providers.deepseek_anthropic]` | `DEEPSEEK_API_KEY` | `DEEPSEEK_ANTHROPIC_BASE_URL`; default `https://api.deepseek.com/anthropic` | `deepseek-v4-pro`, `deepseek-v4-flash`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | Opt-in DeepSeek route for the Anthropic Messages wire protocol. Uses `/v1/messages`, `x-api-key`, and `anthropic-version: 2023-06-01`. Keep `provider = "deepseek"` for the default Chat Completions path. |
| `nvidia-nim` | `[providers.nvidia_nim]` | `NVIDIA_API_KEY`, `NVIDIA_NIM_API_KEY` | `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, `NVIDIA_BASE_URL`; default `https://integrate.api.nvidia.com/v1` | `deepseek-ai/deepseek-v4-pro`, `deepseek-ai/deepseek-v4-flash` | Hosted DeepSeek V4 through NVIDIA NIM. `NVIDIA_NIM_MODEL` is accepted by the TUI config path. |
| `openai` | `[providers.openai]` | `OPENAI_API_KEY` | `OPENAI_BASE_URL`; default `https://api.openai.com/v1` | `gpt-5.6` (default), `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` | Generic OpenAI-compatible route whose built-in endpoint and fallback catalog are native to OpenAI. The [GPT-5.6 family](https://developers.openai.com/api/docs/models/gpt-5.6-sol) uses OpenAI's documented 1.05M context, 128K max output, and reasoning levels. Custom gateways remain free to select an explicit gateway-owned model. `OPENAI_MODEL` is accepted. |