fix(modelstudio): surface Model Studio reasoning in Thinking (#5203)

enable_thinking was a no-op for all four Model Studio plan/dialect
variants: the Chat Completions path never sent DashScope's
enable_thinking switch, and the SSE parser classified every Model
Studio route as ReasoningStreamStyle::None, so reasoning_content
deltas (which qwen3.x models emit by server default) were inlined
into answer text instead of the Thinking surface.

OpenAI dialect (Token Plan compatible-mode + Coding Plan): effort off
now sends enable_thinking=false, any non-off level sends true (the
dialect has no effort ladder), and unset effort stays silent so the
qwen3.x server default (thinking ON) is preserved. Bare qwen3.x model
ids are classified reasoning-capable, so Model Studio routes decode
delta.reasoning_content into the same Thinking channel other
providers use. reasoning_content is deliberately not replayed on
later turns — DashScope does not require it.

Anthropic dialect (*/apps/anthropic): the Messages adapter already
emits the documented {"type":"enabled","budget_tokens":N} shape
once the model gate passes; an explicit off now sends
{"type":"disabled"}, which the endpoint documents, instead of
silently falling through to the server default.

Picker copy now reports stream:structured for all four variants, and
PROVIDERS.md documents exactly what each dialect sends and what is
not replayed.

Sources: alibabacloud.com/help/en/model-studio/deep-thinking and
/help/en/model-studio/anthropic-api-messages (2026-08-03).

Verified: cargo fmt --all --check; cargo check -p codewhale-tui;
clippy clean; 10 new tests green (SSE decode of recorded-style
DashScope frames per plan, request-boundary capture of
enable_thinking streaming + blocking, apply_reasoning_effort matrix
over all four variants, Messages-body thinking shape, model
classification, picker label). Full bin suite: 9770 pass, 19 fail —
identical to the known pre-existing provider-catalog env-key set at
the clean train tip (verified via stash); qa_pty untouched.
This commit is contained in:
Hmbown
2026-08-03 10:08:45 -07:00
parent d2f07593ea
commit 11da42342d
6 changed files with 441 additions and 9 deletions
+163 -8
View File
@@ -3081,13 +3081,21 @@ pub(super) fn apply_reasoning_effort(
// (qwen-max, deepseek-chat, gpt-4o, claude, etc.) accepts the same
// reasoning dialect (#4188 review: verify against actual behavior).
ApiProvider::Telecomjs => {}
// Model Studio: same reasoning as TelecomJS — the Chat Completions
// API does not expose reasoning_effort/thinking controls. Reasoning
// is model-specific on the DashScope side and not surfaced here.
// Model Studio (DashScope): the OpenAI-compatible Chat Completions
// endpoints (Token Plan `compatible-mode/v1`, Coding Plan
// `coding-intl.dashscope.aliyuncs.com/v1`) accept the non-standard
// top-level `enable_thinking` switch for their hybrid-thinking
// catalog (qwen3.x, deepseek-v4, glm-5.x) and stream reasoning as
// `delta.reasoning_content`. The Anthropic-dialect variants never
// reach this Chat path — the Messages adapter shapes their
// `thinking` block instead. Source:
// <https://www.alibabacloud.com/help/en/model-studio/deep-thinking>
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic => {}
| ApiProvider::ModelstudioCodingPlanAnthropic => {
body["enable_thinking"] = json!(false);
}
ApiProvider::OpenaiCodex => {
// OpenAI Codex uses Responses API — thinking handled differently
}
@@ -3156,11 +3164,15 @@ pub(super) fn apply_reasoning_effort(
// TelecomJS: see comment in the "off" branch above — the gateway's
// Chat Completions API does not support reasoning_effort or thinking.
ApiProvider::Telecomjs => {}
// Model Studio: same reasoning as TelecomJS above.
// Model Studio: DashScope's hybrid-thinking switch (see the "off"
// branch). The OpenAI dialect has no effort ladder — any non-off
// level simply turns thinking on.
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic => {}
| ApiProvider::ModelstudioCodingPlanAnthropic => {
body["enable_thinking"] = json!(true);
}
// OpenRouter/Novita/Together: pass through the actual user-chosen value.
// OpenRouter's unified scale is none/minimal/low/medium/high/xhigh;
// DeepSeek models hosted there accept those directly.
@@ -3256,11 +3268,15 @@ pub(super) fn apply_reasoning_effort(
// TelecomJS: see comment in the "off" branch above — the gateway's
// Chat Completions API does not support reasoning_effort or thinking.
ApiProvider::Telecomjs => {}
// Model Studio: same reasoning as TelecomJS above.
// Model Studio: DashScope's hybrid-thinking switch (see the "off"
// branch). The OpenAI dialect has no effort ladder — any non-off
// level simply turns thinking on.
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic => {}
| ApiProvider::ModelstudioCodingPlanAnthropic => {
body["enable_thinking"] = json!(true);
}
ApiProvider::Openrouter | ApiProvider::Novita | ApiProvider::Together => {
body["reasoning_effort"] = json!("xhigh");
body["thinking"] = json!({ "type": "enabled" });
@@ -4214,6 +4230,103 @@ mod tests {
.await
}
fn modelstudio_request_boundary_client(
route_base_url: &str,
model: &str,
transport_base_url: String,
) -> DeepSeekClient {
let _ = rustls::crypto::ring::default_provider().install_default();
let mut client = DeepSeekClient::new(&Config {
provider: Some("modelstudio-token-plan".to_string()),
providers: Some(ProvidersConfig {
modelstudio_token_plan: ProviderConfig {
api_key: Some("modelstudio-request-boundary-key".to_string()),
base_url: Some(route_base_url.to_string()),
model: Some(model.to_string()),
..ProviderConfig::default()
},
..ProvidersConfig::default()
}),
..Config::default()
})
.expect("Model Studio request-boundary client");
assert_eq!(client.base_url, route_base_url);
client.test_chat_transport_base_url = Some(transport_base_url);
client
}
async fn capture_modelstudio_chat_request(
route_base_url: &str,
model: &str,
effort: Option<&str>,
streaming: bool,
) -> (String, Value) {
capture_route_chat_request_body(
model,
k3_request_fixture(model, effort, streaming),
|uri| modelstudio_request_boundary_client(route_base_url, model, uri),
)
.await
}
async fn assert_modelstudio_request_truth(streaming: bool) {
// Token Plan and Coding Plan share the DashScope `enable_thinking`
// switch on their OpenAI-compatible Chat Completions endpoints.
for base_url in [
crate::config::DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
crate::config::DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
] {
let (off_path, off) = capture_modelstudio_chat_request(
base_url,
crate::config::DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL,
Some("off"),
streaming,
)
.await;
assert_eq!(off_path, "/v1/chat/completions");
assert_eq!(off["enable_thinking"], json!(false), "{base_url}: {off}");
assert!(off.get("thinking").is_none(), "{base_url}: {off}");
assert!(off.get("reasoning_effort").is_none(), "{base_url}: {off}");
for effort in ["high", "max"] {
let (_, body) = capture_modelstudio_chat_request(
base_url,
crate::config::DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL,
Some(effort),
streaming,
)
.await;
assert_eq!(
body["enable_thinking"],
json!(true),
"{base_url} {effort}: {body}"
);
assert!(
body.get("thinking").is_none(),
"{base_url} {effort}: {body}"
);
assert!(
body.get("reasoning_effort").is_none(),
"{base_url} {effort}: {body}"
);
}
// Unset effort sends no switch — the server default (thinking-ON
// for the qwen3.x families) is left alone.
let (_, unset) = capture_modelstudio_chat_request(
base_url,
crate::config::DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL,
None,
streaming,
)
.await;
assert!(
unset.get("enable_thinking").is_none(),
"{base_url}: {unset}"
);
}
}
async fn assert_zai_request_truth(streaming: bool) {
for base_url in [
crate::config::DEFAULT_ZAI_BASE_URL,
@@ -4815,6 +4928,16 @@ mod tests {
assert_minimax_request_truth(true).await;
}
#[tokio::test]
async fn create_message_request_json_keeps_modelstudio_enable_thinking_exact() {
assert_modelstudio_request_truth(false).await;
}
#[tokio::test]
async fn create_message_stream_request_json_keeps_modelstudio_enable_thinking_exact() {
assert_modelstudio_request_truth(true).await;
}
#[tokio::test]
async fn create_message_routes_only_strict_deepseek_tools_to_beta() {
assert_deepseek_strict_request_route_boundary(false).await;
@@ -7135,6 +7258,38 @@ mod tests {
assert_eq!(body, json!({ "thinking": { "type": "disabled" } }));
}
#[test]
fn reasoning_effort_modelstudio_speaks_dashscope_enable_thinking() {
// All four plan/dialect variants share the DashScope hybrid-thinking
// switch on the OpenAI-compatible Chat Completions path.
for provider in [
ApiProvider::ModelstudioTokenPlan,
ApiProvider::ModelstudioTokenPlanAnthropic,
ApiProvider::ModelstudioCodingPlan,
ApiProvider::ModelstudioCodingPlanAnthropic,
] {
let mut off = json!({});
apply_reasoning_effort(&mut off, Some("off"), provider);
assert_eq!(off, json!({ "enable_thinking": false }), "{provider:?}");
for effort in ["low", "high", "max"] {
let mut body = json!({});
apply_reasoning_effort(&mut body, Some(effort), provider);
assert_eq!(
body,
json!({ "enable_thinking": true }),
"{provider:?} {effort}"
);
}
// Unset effort stays silent: the qwen3.x families default to
// thinking-ON server-side and the client must not flip that.
let mut unset = json!({});
apply_reasoning_effort(&mut unset, None, provider);
assert_eq!(unset, json!({}), "{provider:?}");
}
}
#[test]
fn reasoning_effort_moonshot_toggles_thinking() {
let mut body = json!({});
+79 -1
View File
@@ -133,6 +133,19 @@ impl DeepSeekClient {
&model,
);
let is_deepseek = self.api_provider == ApiProvider::DeepseekAnthropic;
// Model Studio's Anthropic-compatible endpoint documents the portable
// `{"type":"enabled","budget_tokens":N}` shape AND `{"type":"disabled"}`
// (alibabacloud.com/help/en/model-studio/anthropic-api-messages), so
// an explicit "off" can be honored on the wire instead of silently
// falling through to the server default (which is thinking-ON for the
// qwen3.x families).
let is_modelstudio = matches!(
self.api_provider,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic
);
// MiniMax's exact M3 route and DeepSeek's Messages dialect both
// document adaptive support; everything else needs the native host.
let supports_adaptive =
@@ -144,7 +157,7 @@ impl DeepSeekClient {
match effort.as_deref() {
_ if is_minimax_provider && !is_minimax => {}
Some("off" | "disabled" | "none" | "false")
if (is_minimax || is_deepseek) && thinking_capable =>
if (is_minimax || is_deepseek || is_modelstudio) && thinking_capable =>
{
body["thinking"] = json!({ "type": "disabled" });
}
@@ -895,6 +908,29 @@ mod tests {
DeepSeekClient::new(&config).expect("DeepSeek Messages client constructs")
}
fn modelstudio_test_client(base_url: &str) -> DeepSeekClient {
let _ = rustls::crypto::ring::default_provider().install_default();
let config = crate::config::Config {
provider: Some("modelstudio-token-plan-anthropic".to_string()),
providers: Some(crate::config::ProvidersConfig {
// All four plan/dialect variants share one key slot
// (modelstudio-token-plan); only the base URL is read from the
// anthropic entry.
modelstudio_token_plan: crate::config::ProviderConfig {
api_key: Some("test-key".to_string()),
..Default::default()
},
modelstudio_token_plan_anthropic: crate::config::ProviderConfig {
base_url: Some(base_url.to_string()),
..Default::default()
},
..Default::default()
}),
..Default::default()
};
DeepSeekClient::new(&config).expect("Model Studio Messages client constructs")
}
#[test]
fn body_keeps_native_cache_control_on_system_and_tools() {
let client = test_client();
@@ -1156,6 +1192,48 @@ mod tests {
assert_eq!(untouched[0]["tool_use_id"].as_str(), Some("toolu_ok"));
}
#[test]
fn modelstudio_messages_body_requests_thinking_with_budget() {
// Model Studio's Anthropic-compatible endpoint documents the portable
// {"type":"enabled","budget_tokens":N} shape plus {"type":"disabled"}
// (alibabacloud.com/help/en/model-studio/anthropic-api-messages).
let client = modelstudio_test_client(
"https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic",
);
let mut request = request_with("qwen3.8-max", Some("high"), None, None);
request.max_tokens = 64_000;
let body = client.build_anthropic_body(&request, true);
assert_eq!(
body.pointer("/thinking/type").and_then(Value::as_str),
Some("enabled"),
"{body}"
);
assert!(
body.pointer("/thinking/budget_tokens")
.and_then(Value::as_u64)
.is_some(),
"{body}"
);
assert!(body.get("output_config").is_none(), "{body}");
assert_eq!(
body.get("model").and_then(Value::as_str),
Some("qwen3.8-max"),
"{body}"
);
// An explicit "off" is honored on the wire instead of silently
// falling through to the server default (thinking-ON for qwen3.x).
let mut request = request_with("qwen3.8-max", Some("off"), None, None);
request.max_tokens = 64_000;
let body = client.build_anthropic_body(&request, true);
assert_eq!(
body.pointer("/thinking/type").and_then(Value::as_str),
Some("disabled"),
"{body}"
);
}
#[test]
fn deepseek_messages_body_retires_aliases_and_keeps_thinking_control() {
let client = deepseek_test_client(crate::config::DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL);
+118
View File
@@ -2543,6 +2543,26 @@ fn is_reasoning_model_for_stream_on_route(
if requires_reasoning_content(model) {
return true;
}
// Model Studio's OpenAI-compatible endpoints (Token Plan / Coding Plan)
// stream hybrid-model reasoning as `delta.reasoning_content` (DashScope
// dialect) whenever thinking is on — and for the qwen3.x families thinking
// is on by server default. Surface those deltas as Thinking instead of
// inlining them into the answer text. `reasoning_content` is deliberately
// NOT replayed back on later turns (the provider is absent from
// `provider_accepts_reasoning_content`): DashScope does not require the
// reasoning field in request history.
if matches!(
provider,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic
) && model_supports_reasoning(model)
{
return true;
}
provider_accepts_reasoning_content(provider) && model_supports_reasoning(model)
}
@@ -3872,6 +3892,104 @@ mod stream_decoder_tests {
)));
}
#[test]
fn modelstudio_streams_reasoning_content_as_thinking() {
// Recorded-style DashScope OpenAI-compatible frames (shape lifted from
// Model Studio's deep-thinking docs): reasoning streams in
// `delta.reasoning_content`, the answer in `delta.content`, and a
// trailing usage-only chunk closes the stream.
let chunks = [
r#"{"choices":[{"delta":{"content":null,"role":"assistant","reasoning_content":""},"index":0,"logprobs":null,"finish_reason":null}],"object":"chat.completion.chunk","usage":null,"model":"qwen3.8-max","id":"chatcmpl-ms-1"}"#,
r#"{"choices":[{"delta":{"reasoning_content":"Let me think"},"index":0}],"object":"chat.completion.chunk","model":"qwen3.8-max","id":"chatcmpl-ms-1"}"#,
r#"{"choices":[{"delta":{"reasoning_content":" about this."},"index":0}],"object":"chat.completion.chunk","model":"qwen3.8-max","id":"chatcmpl-ms-1"}"#,
r#"{"choices":[{"delta":{"content":"The answer."},"index":0}],"object":"chat.completion.chunk","model":"qwen3.8-max","id":"chatcmpl-ms-1"}"#,
r#"{"choices":[{"finish_reason":"stop","delta":{"content":"","reasoning_content":null},"index":0}],"object":"chat.completion.chunk","model":"qwen3.8-max","id":"chatcmpl-ms-1"}"#,
r#"{"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":10,"completion_tokens":30,"total_tokens":40,"completion_tokens_details":{"reasoning_tokens":20}},"model":"qwen3.8-max","id":"chatcmpl-ms-1"}"#,
];
// Both OpenAI-dialect plans classify their reasoning catalog.
for (provider, base_url, model) in [
(
ApiProvider::ModelstudioTokenPlan,
crate::config::DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
"qwen3.8-max",
),
(
ApiProvider::ModelstudioCodingPlan,
crate::config::DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
"qwen3.7-plus",
),
] {
let style = reasoning_stream_style_for_route(provider, base_url, model, None);
assert_eq!(style, ReasoningStreamStyle::SeparateField, "{provider:?}");
let mut content_index = 0u32;
let mut text_started = false;
let mut thinking_started = false;
let mut tool_indices = std::collections::HashMap::new();
let mut reasoning_detail_buffers = std::collections::HashMap::new();
let mut inline_reasoning_tags = InlineReasoningTagState::default();
let mut events = Vec::new();
for chunk in chunks {
let value: Value = serde_json::from_str(chunk).expect("valid SSE JSON");
events.extend(parse_sse_chunk_with_reasoning_style(
&value,
&mut content_index,
&mut text_started,
&mut thinking_started,
&mut tool_indices,
&mut reasoning_detail_buffers,
&mut inline_reasoning_tags,
style,
));
}
let thinking: String = events
.iter()
.filter_map(|event| match event {
StreamEvent::ContentBlockDelta {
delta: Delta::ThinkingDelta { thinking },
..
} => Some(thinking.as_str()),
_ => None,
})
.collect();
assert_eq!(thinking, "Let me think about this.", "{provider:?}");
let text: String = events
.iter()
.filter_map(|event| match event {
StreamEvent::ContentBlockDelta {
delta: Delta::TextDelta { text },
..
} => Some(text.as_str()),
_ => None,
})
.collect();
assert_eq!(text, "The answer.", "{provider:?}");
// The trailing usage chunk still surfaces token accounting.
assert!(
events.iter().any(|event| matches!(
event,
StreamEvent::MessageDelta { usage: Some(usage), .. }
if usage.output_tokens == 30
)),
"{provider:?}: {events:?}"
);
}
// A non-reasoning model id on the same route keeps the old
// pass-through semantics (no fabricated Thinking surface).
let style = reasoning_stream_style_for_route(
ApiProvider::ModelstudioTokenPlan,
crate::config::DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
"qwen3.8-max-lite-unknown",
None,
);
assert_eq!(style, ReasoningStreamStyle::None);
}
#[test]
fn decoder_does_not_render_reasoning_as_text_for_known_provider_models() {
let mut content_index = 0u32;
+33
View File
@@ -519,6 +519,20 @@ pub fn model_supports_reasoning(model: &str) -> bool {
| "qwen/qwen3.6-27b"
| "qwen/qwen3.6-plus"
| "qwen/qwen3.7-plus"
// Bare qwen3.x ids are Alibaba Cloud Model Studio's own model ids
// (Token Plan / Coding Plan catalogs). Per Model Studio's
// deep-thinking docs these are hybrid-thinking models that stream
// `reasoning_content` (OpenAI dialect) or thinking blocks
// (Anthropic dialect); qwen3.7/3.6/3.5 families default thinking
// ON server-side.
| "qwen3.8-max"
| "qwen3.8-max-preview"
| "qwen3.7-max"
| "qwen3.7-plus"
| "qwen3.6-plus"
| "qwen3.6-flash"
| "qwen3.5-plus"
| "qwen3.5-flash"
| "tencent/hy3-preview"
| "xiaomi/mimo-v2.5-pro"
| "xiaomi/mimo-v2.5"
@@ -933,6 +947,25 @@ mod tests {
}
}
#[test]
fn modelstudio_bare_qwen_models_support_reasoning() {
// Model Studio's deep-thinking docs: every qwen3.x family the Token /
// Coding Plan catalogs carry is hybrid-thinking (reasoning_content on
// the OpenAI dialect, thinking blocks on the Anthropic dialect).
for model in [
"qwen3.8-max",
"qwen3.8-max-preview",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.6-plus",
"qwen3.6-flash",
"qwen3.5-plus",
"qwen3.5-flash",
] {
assert!(model_supports_reasoning(model), "{model}");
}
}
#[test]
fn model_metadata_catalog_override_flows_through_models_chokepoint() {
let _lock = crate::model_catalog::test_catalog_lock();
+34
View File
@@ -1137,6 +1137,13 @@ fn default_reasoning_stream_visibility(provider: ApiProvider) -> ProviderReasoni
| ApiProvider::Vllm
| ApiProvider::Zai
| ApiProvider::Xai
// Model Studio surfaces reasoning as structured Thinking on both
// dialects: `delta.reasoning_content` on the OpenAI-compatible
// routes, thinking blocks on the Anthropic-compatible routes.
| ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
| ApiProvider::ModelstudioCodingPlanAnthropic
| ApiProvider::Moonshot => ProviderReasoningStreamVisibility::StructuredThinking,
_ => ProviderReasoningStreamVisibility::Unknown,
}
@@ -4561,6 +4568,33 @@ mod tests {
assert!(row.compact_hint().contains("stream:structured"));
}
#[test]
fn provider_dashboard_row_surfaces_modelstudio_structured_thinking() {
let config = Config {
providers: Some(crate::config::ProvidersConfig {
modelstudio_token_plan: crate::config::ProviderConfig {
api_key: Some("modelstudio-key".to_string()),
model: Some("qwen3.8-max".to_string()),
..Default::default()
},
..Default::default()
}),
..Config::default()
};
let row = ProviderDashboardRow::from_config(
ApiProvider::ModelstudioTokenPlan,
ApiProvider::ModelstudioTokenPlan,
&config,
);
assert_eq!(row.reasoning.support, ProviderReasoningSupport::Supported);
assert_eq!(
row.reasoning.stream_visibility,
ProviderReasoningStreamVisibility::StructuredThinking
);
assert!(row.compact_hint().contains("stream:structured"));
}
#[test]
fn provider_dashboard_row_surfaces_kimi_code_k3_reasoning_only_on_exact_route() {
let config = Config {
+14
View File
@@ -232,6 +232,20 @@ Create or copy a Model Studio API key from the
[Bailian console](https://bailian.console.aliyun.com/). The API key is shared
across all four provider IDs above; only the base URL and wire protocol differ.
**Thinking / reasoning.** All listed models are hybrid-thinking per Model
Studio's [deep-thinking docs](https://www.alibabacloud.com/help/en/model-studio/deep-thinking),
and their reasoning surfaces in the TUI's Thinking view on both dialects. On
the OpenAI-compatible routes, `reasoning_effort` maps to DashScope's
`enable_thinking` switch: `off` sends `enable_thinking = false`, any other
level sends `true` (there is no effort ladder on this dialect), and an unset
effort sends nothing — the qwen3.x families default to thinking ON
server-side. Reasoning streams back as `delta.reasoning_content`. On the
Anthropic-compatible routes, thinking uses the documented
`{"type":"enabled","budget_tokens":N}` / `{"type":"disabled"}` shapes from the
[Anthropic-compatible Messages API](https://www.alibabacloud.com/help/en/model-studio/anthropic-api-messages),
with `budget_tokens` derived from the effort level. Reasoning history is not
replayed back to the provider on later turns (DashScope does not require it).
DeepSeek (`deepseek-v4-pro`, `deepseek-v4-flash-0731`) and GLM (`glm-5.2`)
models served by Model Studio are provider-scoped and do not collide with the
first-party DeepSeek or Zhipu/Z.ai routes. Pay-as-you-go workspace-id