fix: use supported Gemini reasoning controls and isolate runtime fixtures

Use Google's documented OpenAI-compatible reasoning_effort field instead of
an invalid top-level google object. Preserve minimal/low/medium/high controls,
cap unsupported high aliases, and only send none for disable-capable Gemini
2.5 models. Keep the exact endpoint boundary and signed tool-history replay.
Cover Google and custom configuration rows in both transport modes.

Give Runtime API fixtures explicit per-test stores instead of borrowing a
neighboring test's CODEWHALE_RUNTIME_DIR. Correct the builtin marketplace
fixtures to create their home, discover the parent root, and expect escaped
review identifiers without weakening the install/trust assertions.

Validation: npm test 521 passed, 0 failed (75 Node + 446 web); npm run check:web
passed; cargo fmt --all --check passed; all 2,174 source blobs verified against
tree a43355c6d8. New Google wire cases and
marketplace command/API cases pass in the in-progress complete Cargo run.

The preceding full Cargo run had 15,401 passed, 6 failed, 29 ignored. All six
failures passed unchanged in isolation; the runtime store collisions are fixed
here. The new full Cargo/check/Clippy run and final artifacts remain pending;
this commit does not claim release-wide green or publication.

Refs #6018, #5929, #6030.
This commit is contained in:
CodeWhale Bot committed 2026-09-12 04:49:24 -07:00
1 parent d62ab3f704
commit 9f276d8d17
6 files changed
+103 -53

No files matched your search

+4
View File
@@ -42,6 +42,10 @@ reconnect.
login without changing its credential key or restarting healthy siblings (#6030).
A decreasing token-expiry countdown no longer makes a revoked credential look
like a new login from another session.
- Gemini setup uses the official endpoint's supported reasoning-effort field,
avoiding the rejected top-level Google thinking object. Gemini 2.5 and 3 keep
their supported effort ranges; signed tool history still survives reasoning
changes and restart (#6018, thanks @vmakarov-uk).
- Missed automation occurrences coalesce without overlapping a running job;
restart reconciles durable receipts without replaying accepted work. Damaged
neighboring records are isolated while preserving their original bytes.
+4
View File
@@ -42,6 +42,10 @@ reconnect.
login without changing its credential key or restarting healthy siblings (#6030).
A decreasing token-expiry countdown no longer makes a revoked credential look
like a new login from another session.
- Gemini setup uses the official endpoint's supported reasoning-effort field,
avoiding the rejected top-level Google thinking object. Gemini 2.5 and 3 keep
their supported effort ranges; signed tool history still survives reasoning
changes and restart (#6018, thanks @vmakarov-uk).
- Missed automation occurrences coalesce without overlapping a running job;
restart reconciles durable receipts without replaying accepted work. Damaged
neighboring records are isolated while preserving their original bytes.
+75 -43
View File
@@ -594,7 +594,7 @@ pub(super) fn apply_route_reasoning_controls(
apply_kimi_code_k3_reasoning_effort(body, provider, base_url, model, effort);
apply_zai_route_reasoning_controls(body, provider, base_url, model, effort);
apply_mistral_route_reasoning_controls(body, provider, base_url, model, effort);
apply_google_thinking_level(body, provider, base_url, model, effort);
apply_google_reasoning_effort(body, base_url, model, effort);
}
/// Mistral's polymorphic reasoning-content contract is only proven on its
@@ -663,29 +663,37 @@ fn google_model_requires_thought_signatures(model: &str) -> bool {
model.starts_with("gemini-2.5-flash") && !model.starts_with("gemini-2.5-flash-lite")
}
/// Thinking level for the OpenAI-compat route rides the documented
/// `google.thinking_config.thinking_level` body field (low/high; Gemini 3
/// cannot disable thinking).
fn apply_google_thinking_level(
/// Google's compatibility endpoint accepts the ordinary `reasoning_effort`
/// field across Gemini 2.5 and 3. A top-level `google` object is rejected;
/// native thinking controls would require `extra_body.google` instead.
/// Use one control, since the endpoint rejects overlapping effort and native
/// thinking settings. https://ai.google.dev/gemini-api/docs/openai#thinking
fn apply_google_reasoning_effort(
body: &mut serde_json::Value,
_provider: ApiProvider,
base_url: &str,
_model: &str,
model: &str,
effort: Option<&str>,
) {
if !is_google_openai_compat_chat_route(base_url) || effort.is_none() {
if !is_google_openai_compat_chat_route(base_url) {
return;
}
let level = match effort
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
.as_str()
{
"off" | "disabled" | "none" | "false" | "" | "low" | "minimal" | "medium" | "mid" => "low",
_ => "high",
let Some(effort) = effort else {
return;
};
body["google"]["thinking_config"]["thinking_level"] = json!(level);
let model = model.trim().to_ascii_lowercase();
let model = model.strip_prefix("models/").unwrap_or(&model);
let can_disable = model.starts_with("gemini-2.5-") && !model.starts_with("gemini-2.5-pro");
let effort = match effort.trim().to_ascii_lowercase().as_str() {
"off" | "disabled" | "none" | "false" if can_disable => "none",
// Gemini 3 and 2.5 Pro cannot disable thinking. The compatibility
// layer maps minimal to the selected model's lowest supported level.
"off" | "disabled" | "none" | "false" | "minimal" => "minimal",
"low" => "low",
"medium" | "mid" | "" => "medium",
"high" | "xhigh" | "max" | "highest" | "ultra" | "ultracode" => "high",
_ => return,
};
body["reasoning_effort"] = json!(effort);
}
/// Fail closed before transport when Google's OpenAI-compat route would
@@ -7711,33 +7719,57 @@ mod google_thought_signature_tests {
}
#[test]
fn google_thinking_level_maps_effort_onto_documented_body_field() {
let mut request = google_request_with_signed_tool(Some("SIG"));
request.reasoning_effort = Some("high".to_string());
let body = build_chat_wire_body(
&request,
ApiProvider::Google,
DEFAULT_GOOGLE_BASE_URL,
false,
)
.expect("valid google body");
assert_eq!(
body.body
.pointer("/google/thinking_config/thinking_level")
.and_then(serde_json::Value::as_str),
Some("high")
);
fn google_reasoning_uses_compatible_effort_without_rejected_native_fields() {
// Wire examples and model limits from Google's compatibility docs.
// Cover the actual request builder, both transport modes and both
// ways a fresh install can configure the official endpoint (#6018).
for (model, effort, expected) in [
("gemini-3.1-pro-preview", Some("low"), Some("low")),
("gemini-3.1-pro-preview", Some("medium"), Some("medium")),
("gemini-3.1-pro-preview", Some("high"), Some("high")),
("gemini-3.1-pro-preview", Some("max"), Some("high")),
("gemini-3.5-flash-lite", Some("off"), Some("minimal")),
("gemini-2.5-flash", Some("off"), Some("none")),
("models/gemini-2.5-flash-lite", Some("off"), Some("none")),
("gemini-2.5-pro", Some("off"), Some("minimal")),
("gemini-3.1-pro-preview", None, None),
] {
for provider in [ApiProvider::Google, ApiProvider::Custom] {
for streaming in [false, true] {
let mut request = google_request_with_signed_tool(Some("SIG"));
request.model = model.to_string();
request.reasoning_effort = effort.map(str::to_string);
let wire = build_chat_wire_body(
&request,
provider,
DEFAULT_GOOGLE_BASE_URL,
streaming,
)
.expect("valid signed Google request");
assert_eq!(
wire.body.get("reasoning_effort").and_then(Value::as_str),
expected,
"{model}: {effort:?}, {provider:?}, streaming={streaming}"
);
assert!(wire.body.get("google").is_none());
assert!(wire.body.get("extra_body").is_none());
assert!(wire.body.get("thinking").is_none());
}
}
}
}
let mut low = google_request_with_signed_tool(Some("SIG"));
low.reasoning_effort = Some("low".to_string());
let body = build_chat_wire_body(&low, ApiProvider::Google, DEFAULT_GOOGLE_BASE_URL, false)
.expect("valid google body");
assert_eq!(
body.body
.pointer("/google/thinking_config/thinking_level")
.and_then(serde_json::Value::as_str),
Some("low")
);
#[test]
fn google_reasoning_control_does_not_rewrite_other_endpoints() {
for provider in [ApiProvider::Google, ApiProvider::Custom] {
let request = google_request_with_signed_tool(Some("SIG"));
let wire =
build_chat_wire_body(&request, provider, "https://gateway.example.com/v1", false)
.expect("valid gateway request");
assert!(wire.body.get("reasoning_effort").is_none());
assert!(wire.body.get("google").is_none());
assert!(wire.body.get("extra_body").is_none());
}
}
#[test]
@@ -86,6 +86,7 @@ fn marketplace_builtin_candidate_routes_to_existing_bundle_review() {
let _lock = crate::test_support::lock_test_env();
let root = TempDir::new().unwrap();
let codewhale_home = root.path().join("home");
fs::create_dir_all(&codewhale_home).unwrap();
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
let (mut app, _temp) = create_test_app(root.path());
let original = app
@@ -98,11 +99,14 @@ fn marketplace_builtin_candidate_routes_to_existing_bundle_review() {
assert!(!result.is_error);
let text = result.message.unwrap();
assert!(
!text.contains("/plugin marketplace install codewhale computer-use"),
!text.contains(r"/plugin marketplace install codewhale computer\-use"),
"{text}"
);
assert!(
text.contains(&format!("/plugin show {}", original.id.as_str())),
text.contains(&format!(
"/plugin show {}",
escape_review_text(original.id.as_str())
)),
"{text}"
);
assert!(
+11 -5
View File
@@ -1173,7 +1173,14 @@ async fn build_test_server(
let runtime_threads: SharedRuntimeThreadManager = Arc::new(RuntimeThreadManager::open(
config.clone(),
workspace.clone(),
RuntimeThreadManagerConfig::from_task_data_dir(root.join("runtime")),
// A neighboring libtest may temporarily set CODEWHALE_RUNTIME_DIR.
// This fixture owns an explicit root and must never borrow that
// other test's live store or contend for its process-owner lock.
RuntimeThreadManagerConfig {
data_dir: root.join("runtime/runtime"),
task_data_dir: root.join("runtime"),
max_active_threads: 8,
},
)?);
runtime_threads.attach_task_manager(manager.clone());
let automations = Arc::new(Mutex::new(AutomationManager::open_for_test(
@@ -1569,6 +1576,7 @@ async fn health_and_tasks_endpoints_work() -> Result<()> {
#[tokio::test]
async fn omitted_runtime_models_use_the_active_provider_default() -> Result<()> {
let _env = lock_test_env();
for (label, config, expected) in provider_default_model_cases() {
let temp = tempfile::tempdir()?;
let root = temp.path().join(label);
@@ -3641,9 +3649,8 @@ async fn turn_operation_lookup_is_authenticated_read_only_and_survives_restart()
let home = dir.path().join("home");
fs::create_dir_all(&home)?;
let _home = EnvVarGuard::set("CODEWHALE_HOME", &home);
let store_root = dir.path().join("store");
let _runtime_dir = EnvVarGuard::set("CODEWHALE_RUNTIME_DIR", &store_root);
let root = dir.path().join("server");
let store_root = root.join("runtime/runtime");
let sessions = dir.path().join("sessions");
let workspace = dir.path().join("workspace");
let token = "turn-lookup-local-fixture";
@@ -12714,7 +12721,7 @@ async fn marketplace_builtin_name_is_reviewable_without_install_or_network_acces
workspace_plugins_dir: crate::plugins::discovery::default_workspace_plugins_dir(
&workspace,
),
builtin_plugin_dirs: vec![bundle.clone()],
builtin_plugin_dirs: vec![bundle.parent().unwrap().to_path_buf()],
state_path: root.join("plugins/state.json"),
},
crate::plugins::HostEnvironment::from_entries(Vec::new()),
@@ -13397,7 +13404,6 @@ async fn native_notification_replay_rechecks_requests_settled_during_the_read()
let _env = lock_test_env();
let temp = tempfile::tempdir()?;
let _home = EnvVarGuard::set("CODEWHALE_HOME", temp.path().join("home"));
let _runtime = EnvVarGuard::set("CODEWHALE_RUNTIME_DIR", temp.path().join("runtime-store"));
let _backend = EnvVarGuard::set("CODEWHALE_SECRET_BACKEND", "file");
let _proxy = EnvVarGuard::set("NO_PROXY", "*");
let path = temp.path().join("config.toml");
+3 -3
View File
@@ -44,13 +44,13 @@ export const CHANGELOG: ChangelogRelease[] = [
"Plugin suggestions explain their matching term and remember explicit dismissals across restarts. Generic words and repository-host domains no longer trigger unrelated installation prompts (#6031).",
"Plugin trust and automation deletion have keyboard and mouse confirmation controls bound to the exact reviewed content; users can still copy the command and changed content requires a fresh review (#6039).",
"The model-facing MCP start tool can reconnect an existing configured name after login without changing its credential key or restarting healthy siblings (#6030). A decreasing token-expiry countdown no longer makes a revoked credential look like a new login from another session.",
"Gemini setup uses the official endpoint's supported reasoning-effort field, avoiding the rejected top-level Google thinking object. Gemini 2.5 and 3 keep their supported effort ranges; signed tool history still survives reasoning changes and restart (#6018, thanks @vmakarov-uk).",
"Missed automation occurrences coalesce without overlapping a running job; restart reconciles durable receipts without replaying accepted work. Damaged neighboring records are isolated while preserving their original bytes.",
"The bundled first-party marketplace lists the actual plugin bundles and uses the existing install, review, trust and update paths. A read-only connection check verifies catalog and skill mirrors on changes and weekly. Already present bundles lead to their local review and management controls; catalog installs refuse name collisions before downloading, without overwriting or granting trust.",
"Windows deny checks preserve native path separators while retaining the conservative POSIX scan for shell wrappers and substitutions.",
"The config example agrees with the telemetry disclosure: usage analytics are optional and enabled by default; local diagnostics do not require telemetry (#6011).",
"Sub-agents enforce the session's typed shell and file deny rules, including Full Access, delegated edits, and policy updates after a child starts (#6097)."
"The config example agrees with the telemetry disclosure: usage analytics are optional and enabled by default; local diagnostics do not require telemetry (#6011)."
],
"itemCount": 75
"itemCount": 76
},
{
"heading": "Changed",