fix(tui): map DeepSeek reasoning effort onto the documented wire ladder (#52)
The DeepSeek chat path collapsed low/medium/high onto reasoning_effort: "high" + thinking enabled, so no cheaper tier below high existed and users picking low or medium paid high's latency. DeepSeek's Chat Completions API documents exactly three reasoning_effort values — low, high, max — plus the thinking on/off toggle (https://api-docs.deepseek.com/api/create-chat-completion). First-party routes (deepseek, deepseek-cn) now map honestly: - low/minimal -> reasoning_effort "low" (a real, cheaper tier) - medium/mid -> reasoning_effort "high" (nearest documented tier; the wire has no medium and the thinking-mode server default is also high) - high/max -> unchanged - off -> thinking {"type":"disabled"} (unchanged) Hosted DeepSeek-compatible routes (siliconflow, sglang, volcengine, deepinfra, atlascloud) keep the historic low/medium -> high collapse: their own wire contracts are not verified here, so no unsupported values are invented. Consistency work so receipts, picker, and planning tell the same truth: - ReasoningEffort::normalize_for_route keeps Low on first-party DeepSeek routes (medium still rounds up to high). - The model picker exposes auto/off/low/high/max for first-party DeepSeek routes only; other routes keep the previous default list. - PROVIDERS.md and the reasoning_effort settings hint document the mapping. Tests: per-tier apply_reasoning_effort bodies, wiremock request-body capture for off/low/medium/high/max/unset on the DeepSeek chat route, capability/receipt assertions that DeepSeek low is reported as low while a collapsing hosted route still reports high, picker ladder tests, and the turn-route planner expectation.
This commit is contained in:
+149
-4
@@ -3141,10 +3141,30 @@ pub(super) fn apply_reasoning_effort(
|
||||
ApiProvider::Xai => {}
|
||||
},
|
||||
"low" | "minimal" | "medium" | "mid" | "high" | "" => match provider {
|
||||
// DeepSeek compatibility: low/medium both map to high
|
||||
ApiProvider::Deepseek
|
||||
| ApiProvider::DeepseekCN
|
||||
| ApiProvider::Siliconflow
|
||||
// DeepSeek first-party Chat Completions: the wire documents
|
||||
// exactly three `reasoning_effort` values — `low`, `high`, `max`
|
||||
// (https://api-docs.deepseek.com/api/create-chat-completion) —
|
||||
// plus the `thinking` on/off toggle. There is no `medium` on the
|
||||
// wire, so the honest ladder is:
|
||||
// low/minimal → "low" (a real cheaper tier; it used to be
|
||||
// collapsed onto high, so no tier below
|
||||
// high existed — FINISH-0.9.4 #52)
|
||||
// medium/mid → "high" (nearest documented tier; the wire has
|
||||
// no medium and the server default in
|
||||
// thinking mode is also high)
|
||||
// high/"" → "high"
|
||||
ApiProvider::Deepseek | ApiProvider::DeepseekCN => {
|
||||
let value = match normalized.as_str() {
|
||||
"low" | "minimal" => "low",
|
||||
_ => "high",
|
||||
};
|
||||
body["reasoning_effort"] = json!(value);
|
||||
body["thinking"] = json!({ "type": "enabled" });
|
||||
}
|
||||
// DeepSeek-compatible hosted routes: low/medium both map to high.
|
||||
// Their own wire contracts are not verified here, so the historic
|
||||
// collapse stays rather than inventing unsupported wire values.
|
||||
ApiProvider::Siliconflow
|
||||
| ApiProvider::SiliconflowCn
|
||||
| ApiProvider::Sglang
|
||||
| ApiProvider::Volcengine
|
||||
@@ -7093,6 +7113,131 @@ mod tests {
|
||||
assert!(body.get("extra_body").is_none());
|
||||
}
|
||||
|
||||
/// First-party DeepSeek routes document `reasoning_effort` low/high/max on
|
||||
/// the wire (no medium): low is a real cheaper tier, medium rounds up to
|
||||
/// high (#52). Hosted DeepSeek-compatible routes keep the historic
|
||||
/// low/medium → high collapse because their own wire contracts are not
|
||||
/// verified here.
|
||||
#[test]
|
||||
fn reasoning_effort_deepseek_maps_the_documented_wire_ladder() {
|
||||
let mut body = json!({});
|
||||
apply_reasoning_effort(&mut body, Some("low"), ApiProvider::Deepseek);
|
||||
assert_eq!(
|
||||
body,
|
||||
json!({ "reasoning_effort": "low", "thinking": { "type": "enabled" } })
|
||||
);
|
||||
|
||||
let mut body = json!({});
|
||||
apply_reasoning_effort(&mut body, Some("medium"), ApiProvider::Deepseek);
|
||||
assert_eq!(
|
||||
body,
|
||||
json!({ "reasoning_effort": "high", "thinking": { "type": "enabled" } })
|
||||
);
|
||||
|
||||
for provider in [ApiProvider::Deepseek, ApiProvider::DeepseekCN] {
|
||||
let mut body = json!({});
|
||||
apply_reasoning_effort(&mut body, Some("high"), provider);
|
||||
assert_eq!(
|
||||
body,
|
||||
json!({ "reasoning_effort": "high", "thinking": { "type": "enabled" } }),
|
||||
"provider {provider:?}"
|
||||
);
|
||||
}
|
||||
|
||||
for provider in [ApiProvider::Siliconflow, ApiProvider::Deepinfra] {
|
||||
let mut body = json!({});
|
||||
apply_reasoning_effort(&mut body, Some("low"), provider);
|
||||
assert_eq!(
|
||||
body,
|
||||
json!({ "reasoning_effort": "high", "thinking": { "type": "enabled" } }),
|
||||
"hosted route {provider:?} keeps the collapse"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn capture_deepseek_chat_body_for_effort(effort: Option<&str>) -> Value {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"id": "chatcmpl-deepseek-effort-ladder",
|
||||
"object": "chat.completion",
|
||||
"model": "deepseek-v4-pro",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "ok"},
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let request = MessageRequest {
|
||||
model: "deepseek-v4-pro".to_string(),
|
||||
messages: vec![Message {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentBlock::Text {
|
||||
text: "effort ladder capture".to_string(),
|
||||
cache_control: None,
|
||||
}],
|
||||
}],
|
||||
max_tokens: 64,
|
||||
system: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
metadata: None,
|
||||
thinking: None,
|
||||
reasoning_effort: effort.map(str::to_string),
|
||||
stream: Some(false),
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
};
|
||||
let client = deepseek_request_boundary_client(
|
||||
crate::config::DEFAULT_DEEPSEEK_BASE_URL,
|
||||
server.uri(),
|
||||
);
|
||||
client
|
||||
.create_message(request)
|
||||
.await
|
||||
.expect("non-streaming request succeeds");
|
||||
|
||||
let requests = server.received_requests().await.expect("recorded request");
|
||||
assert_eq!(requests.len(), 1);
|
||||
serde_json::from_slice(&requests[0].body).expect("captured request JSON")
|
||||
}
|
||||
|
||||
/// Request-body capture per effort level on the first-party DeepSeek chat
|
||||
/// route: the wire must carry the documented low/high/max ladder and the
|
||||
/// thinking toggle, never an invented value (#52).
|
||||
#[tokio::test]
|
||||
async fn deepseek_chat_wire_body_tracks_the_documented_effort_ladder() {
|
||||
for (effort, expected_effort, expected_thinking) in [
|
||||
(Some("low"), Some("low"), Some("enabled")),
|
||||
(Some("medium"), Some("high"), Some("enabled")),
|
||||
(Some("high"), Some("high"), Some("enabled")),
|
||||
(Some("max"), Some("max"), Some("enabled")),
|
||||
(Some("off"), None, Some("disabled")),
|
||||
(None, None, None),
|
||||
] {
|
||||
let body = capture_deepseek_chat_body_for_effort(effort).await;
|
||||
assert_eq!(
|
||||
body.get("reasoning_effort").and_then(Value::as_str),
|
||||
expected_effort,
|
||||
"reasoning_effort on the wire for {effort:?}: {body}"
|
||||
);
|
||||
assert_eq!(
|
||||
body.pointer("/thinking/type").and_then(Value::as_str),
|
||||
expected_thinking,
|
||||
"thinking on the wire for {effort:?}: {body}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_off_is_omitted_for_strict_openai_like_providers() {
|
||||
for provider in [
|
||||
|
||||
@@ -666,10 +666,12 @@ pub(crate) fn reasoning_capability_for_route(
|
||||
// route normalizer that shapes the real request.
|
||||
//
|
||||
// This subsumes a min/max floor-and-ceiling and expresses what one cannot:
|
||||
// every non-Codex route coerces `low` and `medium` to `high` while leaving
|
||||
// `off` alone, and an always-thinking route raises `off` instead. Reporting
|
||||
// a `low` a route silently sends as `high` is the invisible substitution
|
||||
// receipts exist to prevent, so the map — not a clamp — is the authority.
|
||||
// most non-Codex routes coerce `low` and `medium` to `high` while leaving
|
||||
// `off` alone (first-party DeepSeek routes are the documented exception —
|
||||
// their wire carries a real `low`), and an always-thinking route raises
|
||||
// `off` instead. Reporting a `low` a route silently sends as `high` is
|
||||
// the invisible substitution receipts exist to prevent, so the map — not
|
||||
// a clamp — is the authority.
|
||||
let wire_tiers = [
|
||||
ReasoningEffort::Off,
|
||||
ReasoningEffort::Low,
|
||||
@@ -2665,12 +2667,13 @@ permissions = "read_only"
|
||||
assert_eq!(capability.control, ProviderReasoningControl::Tiers);
|
||||
}
|
||||
|
||||
/// The capability must report the tier the route *sends*, not the tier the
|
||||
/// selector named. CodeWhale's own normalizer coerces `low`/`medium` to
|
||||
/// `high` on every non-Codex route, so a receipt saying `low` would name a
|
||||
/// request that never happened.
|
||||
/// First-party DeepSeek routes document `reasoning_effort` low/high/max
|
||||
/// on the wire (no medium), so `low` is a real tier there. The capability
|
||||
/// must report the tier the route *sends*, not the tier the selector
|
||||
/// named: low reaches the wire as low, medium rounds up to high because
|
||||
/// the dialect has no such value (#52).
|
||||
#[test]
|
||||
fn a_route_that_collapses_low_onto_high_says_so_instead_of_reporting_low() {
|
||||
fn a_deepseek_route_reports_low_as_low_and_medium_as_high() {
|
||||
let capability = reasoning_capability_for_route(
|
||||
ApiProvider::Deepseek,
|
||||
crate::config::DEFAULT_DEEPSEEK_BASE_URL,
|
||||
@@ -2679,7 +2682,7 @@ permissions = "read_only"
|
||||
|
||||
// Exactly what the request shaping does, read back off the capability.
|
||||
for (requested, expected) in [
|
||||
(ReasoningTier::Low, ReasoningTier::High),
|
||||
(ReasoningTier::Low, ReasoningTier::Low),
|
||||
(ReasoningTier::Medium, ReasoningTier::High),
|
||||
(ReasoningTier::High, ReasoningTier::High),
|
||||
(ReasoningTier::Max, ReasoningTier::Max),
|
||||
@@ -2712,9 +2715,40 @@ permissions = "read_only"
|
||||
assert_eq!(resolved.requested(), RequestedReasoning::Low);
|
||||
assert_eq!(
|
||||
resolved.effective(),
|
||||
codewhale_workflow::EffectiveReasoning::Tier(ReasoningTier::High)
|
||||
codewhale_workflow::EffectiveReasoning::Tier(ReasoningTier::Low)
|
||||
);
|
||||
assert!(resolved.capability_normalized());
|
||||
assert!(!resolved.capability_normalized());
|
||||
}
|
||||
|
||||
/// Routes whose dialect has no low tier still collapse low onto high, and
|
||||
/// the capability must say so instead of reporting a `low` the wire never
|
||||
/// carried. CodeWhale's normalizer keeps the historic low/medium → high
|
||||
/// coercion for these DeepSeek-compatible hosted routes because their own
|
||||
/// wire contracts are not verified.
|
||||
#[test]
|
||||
fn a_route_that_collapses_low_onto_high_says_so_instead_of_reporting_low() {
|
||||
let capability = reasoning_capability_for_route(
|
||||
ApiProvider::Siliconflow,
|
||||
crate::config::DEFAULT_SILICONFLOW_BASE_URL,
|
||||
"deepseek-ai/DeepSeek-V4-Pro",
|
||||
);
|
||||
|
||||
for (requested, expected) in [
|
||||
(ReasoningTier::Low, ReasoningTier::High),
|
||||
(ReasoningTier::Medium, ReasoningTier::High),
|
||||
(ReasoningTier::High, ReasoningTier::High),
|
||||
(ReasoningTier::Max, ReasoningTier::Max),
|
||||
(ReasoningTier::Off, ReasoningTier::Off),
|
||||
] {
|
||||
assert_eq!(
|
||||
capability.wire_tier(requested),
|
||||
expected,
|
||||
"requested {requested:?} must be reported as what the wire carries"
|
||||
);
|
||||
let (effective, normalized) = capability.normalize(requested);
|
||||
assert_eq!(effective, expected);
|
||||
assert_eq!(normalized, requested != expected);
|
||||
}
|
||||
}
|
||||
|
||||
/// Preflight resolves the provider, canonicalizes the model, identifies the
|
||||
@@ -2774,8 +2808,9 @@ permissions = "read_only"
|
||||
/// at the provider default while its receipt claims a tier.
|
||||
#[test]
|
||||
fn a_call_reasoning_value_is_shaped_by_the_configured_route_not_a_tier_label() {
|
||||
// A tiered non-Codex route spells the tiers the ordinary way, after the
|
||||
// same low/medium → high coercion the client performs.
|
||||
// A tiered non-Codex route spells the tiers the ordinary way, after
|
||||
// the same route normalization the client performs (first-party
|
||||
// DeepSeek keeps a real `low`; medium still rounds up to high).
|
||||
for (tier, expected) in [
|
||||
(ReasoningTier::Off, "off"),
|
||||
(ReasoningTier::High, "high"),
|
||||
|
||||
@@ -266,10 +266,12 @@ impl ReasoningEffort {
|
||||
/// the request. Both K3 routes are always-thinking, so `off` becomes the
|
||||
/// lowest supported tier. The Kimi Code membership route otherwise keeps
|
||||
/// its low/high/max mapping; direct Moonshot K3 additionally maps `medium`
|
||||
/// to `high`. Generic Moonshot and every other non-Codex route retain the
|
||||
/// historic high coercion. This intentionally does not change
|
||||
/// [`Self::normalize_for_provider`], whose generic wire semantics are used
|
||||
/// by older callers that do not yet have a route receipt.
|
||||
/// to `high`. First-party DeepSeek routes keep `low` (the wire documents
|
||||
/// low/high/max) while rounding `medium` up to `high`. Generic Moonshot
|
||||
/// and every other non-Codex route retain the historic high coercion.
|
||||
/// This intentionally does not change [`Self::normalize_for_provider`],
|
||||
/// whose generic wire semantics are used by older callers that do not yet
|
||||
/// have a route receipt.
|
||||
#[must_use]
|
||||
pub fn normalize_for_route(
|
||||
self,
|
||||
@@ -294,6 +296,17 @@ impl ReasoningEffort {
|
||||
if provider == ApiProvider::OpenaiCodex {
|
||||
return normalized;
|
||||
}
|
||||
// First-party DeepSeek routes document `reasoning_effort` low/high/max
|
||||
// on the wire (no medium), so `low` is a real, cheaper tier there and
|
||||
// must reach the wire as low; `medium` rounds up to high because the
|
||||
// dialect has no such value (#52).
|
||||
if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
|
||||
return match normalized {
|
||||
Self::Low => Self::Low,
|
||||
Self::Medium => Self::High,
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
match normalized {
|
||||
Self::Low | Self::Medium => Self::High,
|
||||
other => other,
|
||||
|
||||
@@ -54,6 +54,16 @@ const DEFAULT_PICKER_EFFORTS: &[ReasoningEffort] = &[
|
||||
ReasoningEffort::High,
|
||||
ReasoningEffort::Max,
|
||||
];
|
||||
/// First-party DeepSeek routes document a real `low` wire tier alongside
|
||||
/// `high`/`max` (#52), so their picker exposes the cheaper tier the generic
|
||||
/// default list cannot claim for routes where low collapses onto high.
|
||||
const DEEPSEEK_PICKER_EFFORTS: &[ReasoningEffort] = &[
|
||||
ReasoningEffort::Auto,
|
||||
ReasoningEffort::Off,
|
||||
ReasoningEffort::Low,
|
||||
ReasoningEffort::High,
|
||||
ReasoningEffort::Max,
|
||||
];
|
||||
/// Kimi Code K3 accepts route-specific low and medium controls at the
|
||||
/// official membership endpoint. Medium becomes K3's nested high wire effort,
|
||||
/// but keeping the selected intent visible is important for recovery and
|
||||
@@ -2217,6 +2227,12 @@ fn picker_efforts_for_route(
|
||||
if let Some(catalog_efforts) = catalog_picker_efforts(provider, wire_model) {
|
||||
return catalog_efforts;
|
||||
}
|
||||
if matches!(
|
||||
provider,
|
||||
crate::config::ApiProvider::Deepseek | crate::config::ApiProvider::DeepseekCN
|
||||
) {
|
||||
return DEEPSEEK_PICKER_EFFORTS.to_vec();
|
||||
}
|
||||
DEFAULT_PICKER_EFFORTS.to_vec()
|
||||
}
|
||||
|
||||
@@ -3351,8 +3367,8 @@ mod tests {
|
||||
assert_eq!(view.initial_effort, ReasoningEffort::Low);
|
||||
assert_eq!(
|
||||
view.resolved_effort(),
|
||||
ReasoningEffort::High,
|
||||
"the fixed route still previews its normalized tier"
|
||||
ReasoningEffort::Low,
|
||||
"first-party DeepSeek routes carry low as a real wire tier"
|
||||
);
|
||||
|
||||
view.selected_model_idx = view
|
||||
@@ -3468,7 +3484,9 @@ mod tests {
|
||||
.iter()
|
||||
.map(|effort| effort.as_setting())
|
||||
.collect();
|
||||
assert_eq!(effort_labels, vec!["auto", "off", "high", "max"]);
|
||||
// First-party DeepSeek documents a real `low` wire tier (#52), so the
|
||||
// picker exposes it; medium stays hidden because the wire has none.
|
||||
assert_eq!(effort_labels, vec!["auto", "off", "low", "high", "max"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4363,12 +4381,12 @@ mod tests {
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
));
|
||||
assert_eq!(view.focus, Pane::Effort);
|
||||
assert_eq!(view.selected_effort_idx, 2);
|
||||
assert_eq!(view.selected_effort_idx, 3);
|
||||
view.handle_key(KeyEvent::new(
|
||||
KeyCode::Down,
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
));
|
||||
assert_eq!(view.selected_effort_idx, 3);
|
||||
assert_eq!(view.selected_effort_idx, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4455,7 +4473,7 @@ mod tests {
|
||||
app.enable_provider_model("deepseek", "deepseek-v4-flash");
|
||||
let mut view = ModelPickerView::new(&app, &config);
|
||||
assert_eq!(view.selected_model_idx, 1);
|
||||
assert_eq!(view.selected_effort_idx, 2);
|
||||
assert_eq!(view.selected_effort_idx, 3);
|
||||
|
||||
// Move model from Pro to Flash, then switch to effort and move High to Max.
|
||||
view.handle_key(KeyEvent::new(
|
||||
@@ -4500,7 +4518,7 @@ mod tests {
|
||||
app.enable_provider_model("deepseek", "deepseek-v4-flash");
|
||||
let view = ModelPickerView::new(&app, &config);
|
||||
assert_eq!(view.selected_model_idx, 2);
|
||||
assert_eq!(view.selected_effort_idx, 3);
|
||||
assert_eq!(view.selected_effort_idx, 4);
|
||||
assert_eq!(view.focus, Pane::Model);
|
||||
assert_eq!(view.resolved_model(), "deepseek-v4-flash");
|
||||
assert_eq!(view.resolved_effort(), ReasoningEffort::Max);
|
||||
@@ -4578,7 +4596,7 @@ mod tests {
|
||||
let view = ModelPickerView::new(&app, &config);
|
||||
assert!(view.show_custom_model_row);
|
||||
assert_eq!(view.selected_model_idx, view.visible_model_rows().len());
|
||||
assert_eq!(view.selected_effort_idx, 2);
|
||||
assert_eq!(view.selected_effort_idx, 3);
|
||||
assert_eq!(view.resolved_model(), "deepseek-v4-pro-2026-04-XX");
|
||||
assert_eq!(view.resolved_effort(), ReasoningEffort::High);
|
||||
}
|
||||
@@ -4900,7 +4918,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_picker_exposes_auto_off_high_max() {
|
||||
fn deepseek_picker_exposes_the_documented_wire_ladder() {
|
||||
let labels: Vec<&str> = picker_efforts_for_route(
|
||||
crate::config::ApiProvider::Deepseek,
|
||||
crate::config::DEFAULT_DEEPSEEK_BASE_URL,
|
||||
@@ -4910,7 +4928,9 @@ mod tests {
|
||||
.iter()
|
||||
.map(|effort| effort.short_label())
|
||||
.collect();
|
||||
assert_eq!(labels, vec!["auto", "off", "high", "max"]);
|
||||
// First-party DeepSeek documents a real `low` wire tier (#52); medium
|
||||
// stays hidden because the wire has no medium value.
|
||||
assert_eq!(labels, vec!["auto", "off", "low", "high", "max"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2984,7 +2984,7 @@ fn config_hint_for_key(key: &str) -> &'static str {
|
||||
"DeepSeek-only legacy fallback; other providers use their provider-scoped model above"
|
||||
}
|
||||
"reasoning_effort" => {
|
||||
"DeepSeek: auto/off/high/max; Codex: low/medium/high/xhigh; default clears saved value"
|
||||
"DeepSeek: auto/off/low/high/max (medium rounds up to high — the wire has no medium); Codex: low/medium/high/xhigh; default clears saved value"
|
||||
}
|
||||
"mcp_config_path" => "path to mcp.json",
|
||||
"fleet.exec.max_spawn_depth" => {
|
||||
|
||||
@@ -330,8 +330,9 @@ mod tests {
|
||||
);
|
||||
assert!(!planned.auto_controls_reasoning);
|
||||
assert_eq!(planned.selected_reasoning_effort, None);
|
||||
// DeepSeek collapses low to high, but only after the concrete route is
|
||||
// known; the App keeps the unresolved preference as Low.
|
||||
assert_eq!(planned.effective_reasoning_effort.as_deref(), Some("high"));
|
||||
// First-party DeepSeek routes carry low as the real wire tier
|
||||
// (`reasoning_effort` low/high/max are documented); the App keeps the
|
||||
// unresolved preference as Low either way.
|
||||
assert_eq!(planned.effective_reasoning_effort.as_deref(), Some("low"));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -342,7 +342,7 @@ Kimi remains API-key-only; external consent for Kimi is rejected.
|
||||
|
||||
| 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-v4-pro`, `deepseek-v4-flash`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | First-class default. Beta URL enables strict tool mode, chat prefix completion, and FIM completion. Set `https://api.deepseek.com` or `/v1` explicitly to opt out of beta-only features. |
|
||||
| `deepseek` | `[providers.deepseek]` | `DEEPSEEK_API_KEY` | `CODEWHALE_BASE_URL` / `DEEPSEEK_BASE_URL`; default `https://api.deepseek.com/beta` | `deepseek-v4-pro`, `deepseek-v4-flash`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | First-class default. Beta URL enables strict tool mode, chat prefix completion, and FIM completion. 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. |
|
||||
| `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`, fallback `DEEPSEEK_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` | Registry entries: `deepseek-v4-pro`, `deepseek-v4-flash`, `gpt-5.6`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`; default config model `deepseek-v4-pro` | Generic OpenAI-compatible route for gateways and custom endpoints, including Alibaba Bailian / Model Studio DashScope when configured with that endpoint. 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. Use this for explicit third-party OpenAI-compatible routes instead of inventing a new provider ID. `OPENAI_MODEL` is accepted. |
|
||||
|
||||
Reference in New Issue
Block a user