Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 91 additions & 12 deletions desktop/gateway/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,35 @@ pub fn normalize_thinking(body: &mut Value) {
body["thinking"] = serde_json::json!({"type": "disabled"});
return;
}
if body
.get("thinking")
.and_then(Value::as_object)
.and_then(|th| th.get("type"))
.and_then(Value::as_str)
== Some("auto")
{
if let Some(thinking) = body.get_mut("thinking").and_then(Value::as_object_mut) {
thinking.insert("type".to_string(), Value::String("adaptive".to_string()));
// Claude Science sends "auto"; DeepSeek's Anthropic endpoint handles it
// natively — no provider-specific remapping needed.
}

/// Check whether the thinking chain is broken: any assistant message that
/// contains tool_use but is missing thinking blocks means Claude Science
/// already stripped them (native Anthropic semantics). DeepSeek requires
/// thinking to be echoed back when tool_use was present, so further
/// thinking must be disabled for this request.
fn thinking_chain_broken(messages: &Value) -> bool {
let Some(arr) = messages.as_array() else {
return false;
};
arr.iter().any(|msg| {
let role = msg.get("role").and_then(Value::as_str).unwrap_or("");
if role != "assistant" {
return false;
}
}
let Some(items) = msg.get("content").and_then(Value::as_array) else {
return false;
};
let has_tool_use = items
.iter()
.any(|item| item.get("type").and_then(Value::as_str) == Some("tool_use"));
let has_thinking = items
.iter()
.any(|item| item.get("type").and_then(Value::as_str) == Some("thinking"));
has_tool_use && !has_thinking
})
}

pub fn transform_request(mut body: Value, target_model: &str) -> Result<Vec<u8>, String> {
Expand All @@ -51,12 +69,15 @@ pub fn transform_request(mut body: Value, target_model: &str) -> Result<Vec<u8>,
);
}
normalize_thinking(&mut body);
if thinking_chain_broken(&body["messages"]) {
body["thinking"] = serde_json::json!({"type": "disabled"});
}
serde_json::to_vec(&body).map_err(|e| e.to_string())
}

#[cfg(test)]
mod tests {
use super::{clamp_max_tokens, transform_request};
use super::{clamp_max_tokens, thinking_chain_broken, transform_request};
use serde_json::json;

#[test]
Expand All @@ -80,7 +101,7 @@ mod tests {
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["model"], "deepseek-v4-pro");
assert_eq!(v["max_tokens"], 65536);
assert_eq!(v["thinking"]["type"], "adaptive");
assert_eq!(v["thinking"]["type"], "auto");
}

#[test]
Expand All @@ -95,4 +116,62 @@ mod tests {
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["thinking"]["type"], "disabled");
}

#[test]
fn chain_intact_when_tool_use_has_thinking() {
let messages = json!([
{"role": "user", "content": "hello"},
{"role": "assistant", "content": [
{"type": "thinking", "thinking": "I'll use a tool", "signature": "sig"},
{"type": "tool_use", "id": "t1", "name": "read", "input": {}}
]},
{"role": "user", "content": "more"}
]);
assert!(!thinking_chain_broken(&messages));
}

#[test]
fn chain_broken_when_tool_use_missing_thinking() {
// Science stripped thinking → broken
let messages = json!([
{"role": "user", "content": "hello"},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "t1", "name": "read", "input": {}}
]},
{"role": "user", "content": "more"}
]);
assert!(thinking_chain_broken(&messages));
}

#[test]
fn chain_intact_without_tool_use() {
let messages = json!([
{"role": "user", "content": "hello"},
{"role": "assistant", "content": [
{"type": "thinking", "thinking": "hmm", "signature": "sig"},
{"type": "text", "text": "hi"}
]},
{"role": "user", "content": "more"}
]);
assert!(!thinking_chain_broken(&messages));
}

#[test]
fn chain_broken_disables_thinking_in_transform() {
let raw = json!({
"model": "claude-sonnet-5",
"max_tokens": 1000,
"thinking": {"type": "auto"},
"messages": [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "t1", "name": "read", "input": {}}
]},
{"role": "user", "content": "more"}
]
});
let bytes = transform_request(raw, "deepseek-v4-pro").unwrap();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["thinking"]["type"], "disabled");
}
}