From 54e9b64d105a48c27cd2c36dac0dfb2c331c03e1 Mon Sep 17 00:00:00 2001 From: AmanSwar Date: Sat, 17 Oct 2026 21:27:54 +0530 Subject: [PATCH 1/7] deepseek: offer each hosted model's reasoning levels in dsh's Effort control dsh's llm-pi-ai adapter draws the Effort row only for a model whose entry declares reasoningEfforts, so `wally deepseek` showed only "Model" for our models while DeepSeek's own adapter shows "Effort: High". Each model now declares its /v1/models levels, sent as themselves in reasoning_effort. No route default (the adapter takes one for every model and refuses a request whose model lacks it, e.g. MiMo) and no `off` (pi-ai sends off's wire value whenever no level is chosen). The route pins the system role, which pi-ai would otherwise switch to `developer` for a reasoning model. Verified with dsh 0.1.5-rc.2 against a capture server: low and xhigh reach the wire as reasoning_effort, Provider default sends none, roles stay system. Break test: declaration removed, the new test fails at agents.rs:2263; restored, passes. --- src/harness/agents.rs | 111 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/src/harness/agents.rs b/src/harness/agents.rs index ddde4315..c2f8e1b9 100644 --- a/src/harness/agents.rs +++ b/src/harness/agents.rs @@ -1183,6 +1183,9 @@ pub fn build_deep_seek_llm_config( }; entry["maxTokens"] = json!(max_tokens); } + if let Some(efforts) = deep_seek_reasoning_efforts(model) { + entry["reasoningEfforts"] = efforts; + } entries.push(entry); } let provider = json!({ @@ -1190,6 +1193,12 @@ pub fn build_deep_seek_llm_config( "api": "openai-completions", "baseURL": base_url, "models": entries, + // Once a model reasons, pi-ai sends the system prompt as `developer` + // to any endpoint it does not recognise (pi-ai 0.85 getCompat); keep + // it `system`, as for a model that does not. `reasoning_effort` is + // how the gateway takes a level, stated rather than left to pi-ai's + // baseURL detection. + "compat": { "supportsDeveloperRole": false, "supportsReasoningEffort": true }, // Always referenced, local server included. This used to be omitted // for a loopback endpoint on the theory that no reference meant a // keyless route; dsh 0.1.5 instead refuses the turn with "No API key @@ -1204,6 +1213,40 @@ pub fn build_deep_seek_llm_config( dump(&json!({ "providers": { PROVIDER_ID: provider } })) } +/// pi-ai's reasoning levels (`ModelThinkingLevel` without `off`), the names +/// dsh's llm-pi-ai adapter takes as `reasoningEfforts` keys. +const PI_AI_LEVELS: &[&str] = &["minimal", "low", "medium", "high", "xhigh", "max"]; + +/// The Effort control dsh offers for `model`: its levels from the model list, +/// each sent as itself in `reasoning_effort`. dsh's llm-pi-ai adapter shows an +/// Effort row only for a model whose entry declares `reasoningEfforts`; a +/// hand-declared model without it reasons at the server's default with no +/// control (DeepSeek's own adapter offers off / low / high / max). None for a +/// model the list does not describe and for one that only switches thinking +/// on or off: it gets no control, as before. +/// +/// Two things are left out on purpose. A default: the adapter takes one only +/// from the route's `reasoning`, the same for every model, and refuses a +/// request on a model that lacks it, so with none dsh shows "Provider +/// default", sends no level, and the gateway applies the model's own +/// `default_level`. And `off`: pi-ai sends its wire value both for `off` and +/// when no level is chosen, so declaring `off` as `none` would turn thinking +/// off whenever Provider default is selected. +fn deep_seek_reasoning_efforts(model: &CatalogModel) -> Option { + let reasoning = model.reasoning.as_ref()?; + let mut efforts = serde_json::Map::new(); + for level in &reasoning.levels { + if PI_AI_LEVELS.contains(level) { + efforts.insert((*level).to_string(), json!(level)); + } + } + if efforts.is_empty() { + None + } else { + Some(Value::Object(efforts)) + } +} + /// `models.json` for Prime Agent 0.10. The Rust build dropped `-e`, and a /// custom provider is this document: `baseUrl`, `api`, `apiKey`, and the /// model list. Other providers already in `existing` stay. `apiKey` is the @@ -2171,6 +2214,74 @@ mod tests { assert_eq!(deepseek_models[1]["input"], json!(["text"])); } + fn reasoning_model( + id: &str, + levels: &[&'static str], + default_level: &'static str, + ) -> CatalogModel { + CatalogModel { + id: id.to_string(), + reasoning: Some(crate::account::ReasoningSupport { + levels: levels.to_vec(), + default_level: Some(default_level), + thinking_off: true, + }), + ..Default::default() + } + } + + #[test] + fn dsh_offers_each_models_own_levels_from_the_model_list() { + let models = [ + reasoning_model( + "deepseek-v4.1-flash", + &["low", "high", "xhigh", "max"], + "high", + ), + reasoning_model( + "muse-spark", + &["minimal", "low", "medium", "high", "xhigh"], + "medium", + ), + // Thinking on or off only, and a model the list does not describe. + CatalogModel { + id: "mimo-v2.6-pro".to_string(), + reasoning: Some(crate::account::ReasoningSupport::default()), + ..Default::default() + }, + CatalogModel { + id: "local-model".to_string(), + ..Default::default() + }, + ]; + let doc: Value = serde_json::from_str(&build_deep_seek_llm_config( + "https://example.test/v1", + KEY_VARIABLE, + &models, + )) + .unwrap(); + let provider = &doc["providers"]["runanywhere"]; + let entries = &provider["models"]; + assert_eq!( + entries[0]["reasoningEfforts"], + json!({"low": "low", "high": "high", "xhigh": "xhigh", "max": "max"}) + ); + assert_eq!( + entries[1]["reasoningEfforts"], + json!({"minimal": "minimal", "low": "low", "medium": "medium", "high": "high", "xhigh": "xhigh"}) + ); + assert!(entries[2].get("reasoningEfforts").is_none()); + assert!(entries[3].get("reasoningEfforts").is_none()); + // No `off`: pi-ai would send it whenever no level is chosen. + assert!(entries[0]["reasoningEfforts"].get("off").is_none()); + // No route default: it would refuse every model without that level. + assert!(provider.get("reasoning").is_none()); + assert_eq!( + provider["compat"], + json!({"supportsDeveloperRole": false, "supportsReasoningEffort": true}) + ); + } + #[test] fn prime_agent_argv_names_the_model_before_theirs() { let argv = prime_agent_argv("glm-5.3-flash", &["-c".to_string()]); From d1e984a5be00f99dbdb0de48f8ab493de4d5727f Mon Sep 17 00:00:00 2001 From: AmanSwar Date: Sat, 17 Oct 2026 21:27:55 +0530 Subject: [PATCH 2/7] opencode: declare each hosted model's reasoning levels as variants OpenCode offers no effort choice for a custom provider's model without variants. Each model now gets one variant per /v1/models level, a `none` variant where thinking can be turned off, and the model's default level as its model-level option, which a chosen variant overrides. Verified with OpenCode 2.0.28: against a capture server `#high` sends reasoning_effort high, no variant sends the default, `#none` sends none; against production #none settled 0 reasoning tokens where #low and #max settled 33 and 42. Break test: the call removed, the new test fails at opencode.rs:562; restored, passes. --- src/harness/opencode.rs | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/src/harness/opencode.rs b/src/harness/opencode.rs index b1bae708..626c3166 100644 --- a/src/harness/opencode.rs +++ b/src/harness/opencode.rs @@ -304,6 +304,7 @@ pub fn build_open_code_config( entry["attachment"] = json!(true); entry["modalities"] = json!({ "input": ["text", "image"], "output": ["text"] }); } + open_code_reasoning(&mut entry, model); // The real price, so opencode shows spend instead of $0.00. // opencode's cost is USD per million tokens; the catalog is // micro-dollars per million, so a million micros is one dollar. @@ -346,6 +347,38 @@ pub fn build_open_code_config( dump(&config) } +/// OpenCode's reasoning controls for `model`, from the model list: one +/// variant per level, each sending itself as `reasoning_effort` (the +/// `@ai-sdk/openai-compatible` provider option `reasoningEffort`), plus +/// `none` where the model can turn thinking off, and the model's default +/// level as its model-level option, which a chosen variant overrides. +/// Measured against OpenCode 2.0.28 with a capture server: `-m +/// runanywhere/#high` sends `reasoning_effort: "high"`, no variant sends +/// the default, and `#none` sends `"none"`. Without variants OpenCode offers +/// no effort choice for a custom provider's model at all, and the config's +/// `model` key ignores a `#variant`, so the default lives in `options`. +/// Nothing for a model the list does not describe. +fn open_code_reasoning(entry: &mut serde_json::Value, model: &CatalogModel) { + let Some(reasoning) = model.reasoning.as_ref() else { + return; + }; + let mut variants = serde_json::Map::new(); + for level in &reasoning.levels { + variants.insert((*level).to_string(), json!({ "reasoningEffort": level })); + } + if reasoning.thinking_off { + variants.insert("none".to_string(), json!({ "reasoningEffort": "none" })); + } + if variants.is_empty() { + return; + } + entry["reasoning"] = json!(true); + entry["variants"] = serde_json::Value::Object(variants); + if let Some(default_level) = reasoning.default_level { + entry["options"] = json!({ "reasoningEffort": default_level }); + } +} + /// Compatibility name for callers configuring a hosted catalog. pub fn build_open_code_cloud_config( primary: &str, @@ -479,6 +512,87 @@ mod tests { use super::*; use crate::util::env_lock::lock as env_lock; + fn described( + id: &str, + levels: &[&'static str], + default_level: Option<&'static str>, + thinking_off: bool, + ) -> CatalogModel { + CatalogModel { + id: id.to_string(), + reasoning: Some(crate::account::ReasoningSupport { + levels: levels.to_vec(), + default_level, + thinking_off, + }), + ..Default::default() + } + } + + #[test] + fn each_model_gets_its_own_levels_as_variants_and_its_default_as_an_option() { + let models = [ + described( + "deepseek-v4.1-flash", + &["low", "high", "xhigh", "max"], + Some("high"), + true, + ), + described( + "muse-spark", + &["minimal", "low", "medium", "high", "xhigh"], + Some("medium"), + false, + ), + described("mimo-v2.6-pro", &[], None, true), + CatalogModel { + id: "local-model".to_string(), + ..Default::default() + }, + ]; + let doc: serde_json::Value = serde_json::from_str(&build_open_code_config( + "deepseek-v4.1-flash", + "https://example.test/v1", + "k", + &models, + )) + .unwrap(); + let entries = &doc["provider"]["runanywhere"]["models"]; + let deepseek = &entries["deepseek-v4.1-flash"]; + assert_eq!(deepseek["reasoning"], json!(true)); + assert_eq!( + deepseek["variants"], + json!({ + "low": {"reasoningEffort": "low"}, + "high": {"reasoningEffort": "high"}, + "xhigh": {"reasoningEffort": "xhigh"}, + "max": {"reasoningEffort": "max"}, + "none": {"reasoningEffort": "none"}, + }) + ); + assert_eq!(deepseek["options"], json!({"reasoningEffort": "high"})); + // Thinking cannot be turned off on muse-spark: no `none`. + let muse = &entries["muse-spark"]; + assert!(muse["variants"].get("none").is_none()); + assert_eq!( + muse["variants"]["minimal"], + json!({"reasoningEffort": "minimal"}) + ); + assert_eq!(muse["options"], json!({"reasoningEffort": "medium"})); + // On or off only: thinking off is the one choice, and no default is sent. + let mimo = &entries["mimo-v2.6-pro"]; + assert_eq!( + mimo["variants"], + json!({"none": {"reasoningEffort": "none"}}) + ); + assert!(mimo.get("options").is_none()); + // A model the list does not describe is left as it was. + let local = &entries["local-model"]; + assert!(local.get("variants").is_none()); + assert!(local.get("reasoning").is_none()); + assert!(local.get("options").is_none()); + } + #[test] fn scoped_config_restores_non_utf8_previous_value_on_drop() { let _lock = env_lock(); From b9a48c348082a571159e5de141ae1341aaf61bba Mon Sep 17 00:00:00 2001 From: AmanSwar Date: Sat, 17 Oct 2026 21:27:55 +0530 Subject: [PATCH 3/7] docs: reasoning effort per harness, and how to set it without wally --- docs/EDITORS.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/docs/EDITORS.md b/docs/EDITORS.md index 65b29586..04c5f245 100644 --- a/docs/EDITORS.md +++ b/docs/EDITORS.md @@ -49,9 +49,19 @@ they talk to the endpoint directly. The effort you pick in the tool reaches the model as `reasoning_effort`: Claude Code's and Claude Desktop's effort setting, Claude Code's thinking switch -(off is `none`), and Codex's reasoning level, whose picker lists `none` to -`max`. GLM grades its reasoning by it, DeepSeek reasons less with any level -set, and MiMo only honours `none`. +(off is `none`), Codex's reasoning level, whose picker lists `none` to +`max`, OpenCode's variants (`ctrl+t`, or `-m runanywhere/#`), and +DeepSeek Harness's Effort control. Which levels a model offers, and which one +it runs at when you pick none, come from the console's `/v1/models` `reasoning` +block, never from a list in wally: GLM 5.3 Flash low / high / max, DeepSeek +V4.1 Flash low / high / xhigh / max, Muse Spark minimal to xhigh, and MiMo only +on or off. OpenCode gets every level as a variant, a `none` variant where the +model can stop thinking, and the model's default as its starting level. +DeepSeek Harness shows the levels with "Provider default" first: its pi-ai +adapter takes one default for every model on a route and refuses a request +whose model lacks it, so wally leaves the default to the endpoint, which runs +the model's own. It offers no Off, because pi-ai sends the same value for Off +and for no choice at all. Codex is told the model's context window through the catalog file, so its context meter and auto-compaction work against the real limit. Its hosted web @@ -189,3 +199,83 @@ different hosts; see [AGENTS.md](../AGENTS.md). browser, is not registered in this release; the browser flow above is the only way in. +### Reasoning effort without wally + +A tool pointed at `https://inference.runanywhere.ai/v1` with an API key, no +wally in between, sets a model's reasoning with the request field +`reasoning_effort`. `GET /v1/models` says what each model takes, in its +`reasoning` block: + +```json +"reasoning": { + "levels": ["low", "high", "xhigh", "max"], + "default_level": "high", + "thinking_off": true +} +``` + +`levels` are the values `reasoning_effort` accepts, `default_level` is what a +request that sends none runs at, and `thinking_off: true` means `none` turns +thinking off (a model with `false` always thinks, and `none` is refused). + +OpenCode needs each level declared as a variant, or it offers no choice. In +`opencode.json`: + +```json +{ + "provider": { + "runanywhere": { + "npm": "@ai-sdk/openai-compatible", + "name": "RunAnywhere", + "options": { + "baseURL": "https://inference.runanywhere.ai/v1", + "apiKey": "{env:RUNANYWHERE_API_KEY}" + }, + "models": { + "deepseek-v4.1-flash": { + "reasoning": true, + "options": { "reasoningEffort": "high" }, + "variants": { + "low": { "reasoningEffort": "low" }, + "high": { "reasoningEffort": "high" }, + "xhigh": { "reasoningEffort": "xhigh" }, + "max": { "reasoningEffort": "max" }, + "none": { "reasoningEffort": "none" } + } + } + } + } + } +} +``` + +`options.reasoningEffort` is the starting level and a variant overrides it: +cycle with `ctrl+t`, or name one with `opencode run -m +runanywhere/deepseek-v4.1-flash#max`. Repeat the block for each model with that +model's `levels`, and leave `none` out where `thinking_off` is false. (A +`#variant` in the config's own `model` key is ignored, which is why the +default goes in `options`.) + +DeepSeek Harness takes the levels as `reasoningEfforts` on each model of its +`llm-pi-ai` provider; each key is a level it shows, and its value is what it +sends: + +```yaml +- name: '@deepseek-ai/dsh-llm-pi-ai' + config: + providers: + runanywhere: + api: openai-completions + baseURL: https://inference.runanywhere.ai/v1 + apiKeyEnv: RUNANYWHERE_API_KEY + compat: + supportsDeveloperRole: false + models: + - id: deepseek-v4.1-flash + reasoningEfforts: { low: low, high: high, xhigh: xhigh, max: max } +``` + +Any other OpenAI-compatible client sends the field itself, for example +`"reasoning_effort": "max"` beside `model` and `messages`, or +`extra_body={"reasoning_effort": "max"}` in the OpenAI Python SDK. + From 05b23372053683777b9733cb9ad6e4599b18b178 Mon Sep 17 00:00:00 2001 From: AmanSwar Date: Sat, 17 Oct 2026 21:51:20 +0530 Subject: [PATCH 4/7] openclaw: offer each hosted model's reasoning levels and default OpenClaw took our models not to reason: `--thinking high` was refused ("Use one of: off, ultra") and no reasoning_effort was ever sent. Each model with levels now declares reasoning, supportsReasoningEffort and its levels as compat.supportedReasoningEfforts (`none` first where thinking can be turned off, so /think off sends none), and its default level as agents.defaults.models["runanywhere/"].params.thinking unless the person's config already sets one. A model with no levels is unchanged: with reasoning on and nothing to choose OpenClaw would default it off. Verified with OpenClaw 2026.10.4 (Node 24): against a capture server the default sends high, low/xhigh/max themselves, off sends none; against production through wally, default 166 reasoning tokens, low 105, max 89, off 0 with the prompt 25 tokens shorter. Break test: the declaration removed, the new test fails at agents.rs:2381; restored, passes. --- src/harness/agents.rs | 124 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/src/harness/agents.rs b/src/harness/agents.rs index c2f8e1b9..612c47a3 100644 --- a/src/harness/agents.rs +++ b/src/harness/agents.rs @@ -1104,8 +1104,21 @@ pub fn build_open_claw_config( "cacheWrite": 0, }); } + if let Some(efforts) = open_claw_reasoning_efforts(model) { + entry["reasoning"] = json!(true); + entry["compat"]["supportsReasoningEffort"] = json!(true); + entry["compat"]["supportedReasoningEfforts"] = efforts; + } entries.push(entry); } + for model in models { + if let Some(default_level) = open_claw_reasoning_efforts(model) + .and(model.reasoning.as_ref()) + .and_then(|reasoning| reasoning.default_level) + { + set_open_claw_default_thinking(&mut config, &model.id, default_level); + } + } config["models"]["providers"][PROVIDER_ID] = json!({ "baseUrl": base_url, "apiKey": key_or_placeholder(api_key), @@ -1213,6 +1226,52 @@ pub fn build_deep_seek_llm_config( dump(&json!({ "providers": { PROVIDER_ID: provider } })) } +/// The `/think` levels OpenClaw offers for `model`, as the efforts it may send: +/// the model list's levels, and `none` first where that turns thinking off, +/// so `/think off` sends `none`. Without them OpenClaw takes a model on a +/// custom OpenAI-compatible provider not to reason (`--thinking high` is +/// refused, "Use one of: off, ultra") and sends no `reasoning_effort` at all. +/// Measured against OpenClaw 2026.10.4 with a capture server: low, xhigh and +/// max are sent as themselves, `off` as `none`, and a level OpenClaw has that +/// the model does not (medium on DeepSeek) as its nearest one. None for a +/// model with no levels: with `reasoning` on and nothing to choose, OpenClaw +/// would default it to thinking off, where today the endpoint thinks. +fn open_claw_reasoning_efforts(model: &CatalogModel) -> Option { + let reasoning = model.reasoning.as_ref()?; + if reasoning.levels.is_empty() { + return None; + } + let mut efforts: Vec<&str> = Vec::new(); + if reasoning.thinking_off { + efforts.push("none"); + } + efforts.extend(reasoning.levels.iter().copied()); + Some(json!(efforts)) +} + +/// `level` as OpenClaw's default `/think` for our `model` +/// (`agents.defaults.models["runanywhere/"].params.thinking`), unless the +/// person's own config already sets one there. Without it OpenClaw picks its +/// own default rather than the model list's. That table does not restrict +/// the model picker (OpenClaw docs, tools/thinking.md). A config whose path +/// there is not an object is left alone. +fn set_open_claw_default_thinking(config: &mut Value, model: &str, level: &str) { + let key = format!("{PROVIDER_ID}/{model}"); + let mut node = &mut *config; + for segment in ["agents", "defaults", "models", key.as_str(), "params"] { + if node.get(segment).is_none() { + node[segment] = json!({}); + } + node = &mut node[segment]; + if !node.is_object() { + return; + } + } + if node.get("thinking").is_none() { + node["thinking"] = json!(level); + } +} + /// pi-ai's reasoning levels (`ModelThinkingLevel` without `off`), the names /// dsh's llm-pi-ai adapter takes as `reasoningEfforts` keys. const PI_AI_LEVELS: &[&str] = &["minimal", "low", "medium", "high", "xhigh", "max"]; @@ -2282,6 +2341,71 @@ mod tests { ); } + #[test] + fn openclaw_offers_each_models_levels_and_defaults_to_the_lists_level() { + let models = [ + reasoning_model( + "deepseek-v4.1-flash", + &["low", "high", "xhigh", "max"], + "high", + ), + CatalogModel { + reasoning: Some(crate::account::ReasoningSupport { + levels: vec!["minimal", "low", "medium", "high", "xhigh"], + default_level: Some("medium"), + thinking_off: false, + }), + id: "muse-spark".to_string(), + ..Default::default() + }, + CatalogModel { + id: "mimo-v2.6-pro".to_string(), + reasoning: Some(crate::account::ReasoningSupport { + thinking_off: true, + ..Default::default() + }), + ..Default::default() + }, + ]; + // Their own default for muse stays theirs. + let existing = r#"{"agents":{"defaults":{"models":{"runanywhere/muse-spark":{"params":{"thinking":"low"}}}}}}"#; + let doc: Value = serde_json::from_str(&build_open_claw_config( + existing, + "deepseek-v4.1-flash", + "https://example.test/v1", + "k", + &models, + )) + .unwrap(); + let entries = &doc["models"]["providers"]["runanywhere"]["models"]; + assert_eq!(entries[0]["reasoning"], json!(true)); + assert_eq!(entries[0]["compat"]["supportsReasoningEffort"], json!(true)); + assert_eq!( + entries[0]["compat"]["supportedReasoningEfforts"], + json!(["none", "low", "high", "xhigh", "max"]) + ); + // Thinking cannot be turned off on muse-spark: no `none`. + assert_eq!( + entries[1]["compat"]["supportedReasoningEfforts"], + json!(["minimal", "low", "medium", "high", "xhigh"]) + ); + // No levels: left as before, so it is not defaulted to thinking off. + assert!(entries[2].get("reasoning").is_none()); + assert!(entries[2]["compat"] + .get("supportedReasoningEfforts") + .is_none()); + let defaults = &doc["agents"]["defaults"]["models"]; + assert_eq!( + defaults["runanywhere/deepseek-v4.1-flash"]["params"]["thinking"], + "high" + ); + assert_eq!( + defaults["runanywhere/muse-spark"]["params"]["thinking"], + "low" + ); + assert!(defaults.get("runanywhere/mimo-v2.6-pro").is_none()); + } + #[test] fn prime_agent_argv_names_the_model_before_theirs() { let argv = prime_agent_argv("glm-5.3-flash", &["-c".to_string()]); From e27dc96d011813769de0ddd95c062621b16e350a Mon Sep 17 00:00:00 2001 From: AmanSwar Date: Sat, 17 Oct 2026 22:00:19 +0530 Subject: [PATCH 5/7] claude_effort: the effort Claude Code starts a hosted model at The model list's default level, as the nearest level the model's Claude effort menu has (xhigh on a menu without it is high). None for a model with no menu or no default. --- src/harness/claude_effort.rs | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/harness/claude_effort.rs b/src/harness/claude_effort.rs index 11e70dee..8e892e82 100644 --- a/src/harness/claude_effort.rs +++ b/src/harness/claude_effort.rs @@ -51,6 +51,25 @@ pub fn max_effort(model: &CatalogModel) -> Option<&'static str> { }) } +/// The effort Claude Code starts `model` at (`--effort`): the model list's +/// default level, as the nearest level `model`'s menu has. Without it Claude +/// Code starts at the default of the Claude model it takes the entry to be, +/// not the model's own. None for a model with no menu or no default. +pub fn default_effort(model: &CatalogModel) -> Option<&'static str> { + let menu = effort_menu(model)?; + let level = model.reasoning.as_ref()?.default_level?; + Some(match (level, menu) { + (_, EffortMenu::None) => return None, + ("minimal" | "low", _) => "low", + ("medium", _) => "medium", + ("high", _) => "high", + ("xhigh", EffortMenu::Full) => "xhigh", + ("xhigh", EffortMenu::WithoutXhigh) => "high", + ("max", _) => "max", + _ => return None, + }) +} + /// The Claude model Claude Code takes `model` to be (`modelPicker`'s /// `behavesAs`), for its effort menu. None for a model that keeps its own id. pub fn behaves_as(model: &CatalogModel) -> Option<&'static str> { @@ -128,6 +147,33 @@ mod tests { ] } + fn with_default(mut model: CatalogModel, default_level: &'static str) -> CatalogModel { + if let Some(reasoning) = model.reasoning.as_mut() { + reasoning.default_level = Some(default_level); + } + model + } + + #[test] + fn claude_code_starts_each_model_at_the_lists_default() { + let models = today(); + let defaults: Vec<_> = [ + with_default(models[0].clone(), "max"), + with_default(models[1].clone(), "high"), + models[2].clone(), + with_default(models[3].clone(), "medium"), + ] + .iter() + .map(default_effort) + .collect(); + assert_eq!(defaults, [Some("max"), Some("high"), None, Some("medium")]); + // A level the model's Claude menu lacks takes the nearest it has. + let glm_xhigh = with_default(models[0].clone(), "xhigh"); + assert_eq!(default_effort(&glm_xhigh), Some("high")); + // No model list entry, no default. + assert_eq!(default_effort(&CatalogModel::default()), None); + } + #[test] fn each_model_takes_the_menu_its_levels_call_for() { let menus: Vec<_> = today().iter().map(effort_menu).collect(); From 677ba132e31aaab74fe3ce688010cd311892a8ec Mon Sep 17 00:00:00 2001 From: AmanSwar Date: Sat, 17 Oct 2026 22:00:19 +0530 Subject: [PATCH 6/7] claude-code: start each hosted model at its own default effort Claude Code started every model at the default of the Claude model it takes the entry to be, not the model's own. wally now passes --effort with the model list's default, as it passes --model: a person's own --effort wins, and /effort still changes it. Verified against a stand-in claude: deepseek-v4.1-flash and glm-5.3-flash get --effort max, muse-spark medium, mimo-v2.6-pro none, and --effort low of their own is kept. Break test: the lookup neutered, --effort is gone from the argv; restored, it is back. Production run with no --effort answered correctly. --- src/commands/cmd_editors.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/commands/cmd_editors.rs b/src/commands/cmd_editors.rs index be89fcb6..60dd531c 100644 --- a/src/commands/cmd_editors.rs +++ b/src/commands/cmd_editors.rs @@ -1032,6 +1032,21 @@ fn run( launch_args.push("--model".to_string()); launch_args.push(model.to_string()); } + // The model's own default effort, as --model above: theirs wins, and + // /effort still changes it in the session. + if !args + .iter() + .any(|a| a == "--effort" || a.starts_with("--effort=")) + { + if let Some(level) = catalog + .iter() + .find(|entry| entry.id == model) + .and_then(claude_effort::default_effort) + { + launch_args.push("--effort".to_string()); + launch_args.push(level.to_string()); + } + } launch_args.extend(settings_args(args, settings_file.0.as_deref())); launch_args.extend(args.iter().cloned()); status = harness::launch(editor.command, "", &launch_args, options); From efcf3aefe1bfe4c6e8e32a8e9b3b41460c381403 Mon Sep 17 00:00:00 2001 From: AmanSwar Date: Sat, 17 Oct 2026 22:00:19 +0530 Subject: [PATCH 7/7] docs: what each harness offers for reasoning effort, and its default --- docs/EDITORS.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/EDITORS.md b/docs/EDITORS.md index 04c5f245..eea8d31e 100644 --- a/docs/EDITORS.md +++ b/docs/EDITORS.md @@ -61,7 +61,21 @@ DeepSeek Harness shows the levels with "Provider default" first: its pi-ai adapter takes one default for every model on a route and refuses a request whose model lacks it, so wally leaves the default to the endpoint, which runs the model's own. It offers no Off, because pi-ai sends the same value for Off -and for no choice at all. +and for no choice at all. OpenClaw's `/think` (and `--thinking`) offers the +model's levels, `off` sends `none` where the model can stop thinking, and the +model's default is set for it unless your own config sets one +(`agents.defaults.models["runanywhere/"].params.thinking`). Claude Code +starts each model at its default level (`--effort`, unless you pass your own), +and `/effort` changes it; Codex and the ChatGPT app list each model's levels +and start at its default. + +Three tools keep their own default. Hermes takes any level with `--reasoning` +(`wally hermes -m -- --reasoning high`) or `/reasoning`, and otherwise +runs at `agent.reasoning_effort` in `~/.hermes/config.yaml` (per model under +`agent.reasoning_overrides`), which wally does not write. Claude Desktop shows +each model's levels up to its highest and starts at the app's own default. +Prime Agent is given no reasoning levels yet: its models run at the +endpoint's default, and a level has to be named in Prime Agent itself. Codex is told the model's context window through the catalog file, so its context meter and auto-compaction work against the real limit. Its hosted web