mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
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:
1 parent
d62ab3f704
commit
9f276d8d17
6 files changed
+103
-53
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user