Skip to content

Commit fb0caed

Browse files
QuintinBotesclaudeWaylandYang
authored
A deployment that refuses reasoning_effort none on tool requests too is not sent none again (#1353) (#1372)
* A deployment that refuses reasoning_effort none on tool requests too is not sent none again (#1353) Signed-off-by: Quintin Botes <quintinbotes@outlook.com> * Only a retry refused for reasoning_effort forgets none, so an unrelated 400 keeps it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Quintin Botes <quintinbotes@outlook.com> --------- Signed-off-by: Quintin Botes <quintinbotes@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
1 parent 6431a5a commit fb0caed

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

‎crates/utopia-llm/src/lib.rs‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,7 @@ impl LlmClient {
953953
stream: bool,
954954
) -> anyhow::Result<reqwest::Response> {
955955
use std::sync::atomic::Ordering;
956+
let mut retried = false;
956957
loop {
957958
let learned = self.tools_without_effort.load(Ordering::Relaxed);
958959
let resp = self
@@ -975,8 +976,19 @@ impl LlmClient {
975976
{
976977
tracing::info!(model = %self.model, "端点不收带工具的推理强度,带工具的请求改发 none 并记住");
977978
self.tools_without_effort.store(true, Ordering::Relaxed);
979+
retried = true;
978980
continue;
979981
}
982+
// 改发 none 也被拒,且错误还是在说推理强度:这个端点连 none 都不收,记住它只会让以后每一问都带着一个错的值。
983+
// 别的 400(上下文超长、工具定义不对)跟 none 无关,记住的值留着
984+
if retried
985+
&& status == reqwest::StatusCode::BAD_REQUEST
986+
&& err_detail(&parsed, &raw)
987+
.to_lowercase()
988+
.contains("reasoning_effort")
989+
{
990+
self.tools_without_effort.store(false, Ordering::Relaxed);
991+
}
980992
return Err(failure("LLM", status, retry_after, &parsed, &raw));
981993
}
982994
}
@@ -1764,6 +1776,70 @@ mod tests {
17641776
);
17651777
}
17661778

1779+
#[tokio::test]
1780+
async fn a_deployment_that_refuses_none_too_is_not_sent_none_again() {
1781+
use wiremock::{matchers::method, Mock, MockServer, ResponseTemplate};
1782+
let server = MockServer::start().await;
1783+
Mock::given(method("POST"))
1784+
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({ "error": {
1785+
"message": "Function tools with reasoning_effort are not supported for gpt-x." } })))
1786+
.mount(&server)
1787+
.await;
1788+
let client =
1789+
LlmClient::new(&server.uri(), None, "m").with_reasoning_effort(Some("low".into()));
1790+
let tool = serde_json::json!([{ "type": "function", "function": { "name": "ping" } }]);
1791+
let ask = [serde_json::json!({ "role": "user", "content": "x" })];
1792+
assert!(client.chat_tools(&ask, &tool).await.is_err());
1793+
let sent: Vec<serde_json::Value> = server
1794+
.received_requests()
1795+
.await
1796+
.unwrap()
1797+
.iter()
1798+
.map(|r| serde_json::from_slice(&r.body).unwrap())
1799+
.collect();
1800+
assert_eq!(
1801+
sent.iter()
1802+
.map(|b| b["reasoning_effort"].clone())
1803+
.collect::<Vec<_>>(),
1804+
["low", "none"]
1805+
);
1806+
assert_eq!(
1807+
client.tools_body(&ask, Some(&tool), None, false)["reasoning_effort"],
1808+
"low",
1809+
"the flag is cleared"
1810+
);
1811+
}
1812+
1813+
#[tokio::test]
1814+
async fn an_unrelated_400_on_the_retry_keeps_none() {
1815+
use wiremock::{matchers::method, Mock, MockServer, ResponseTemplate};
1816+
let server = MockServer::start().await;
1817+
Mock::given(method("POST"))
1818+
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({ "error": {
1819+
"message": "Function tools with reasoning_effort are not supported for gpt-x." } })))
1820+
.up_to_n_times(1)
1821+
.mount(&server)
1822+
.await;
1823+
Mock::given(method("POST"))
1824+
.respond_with(
1825+
ResponseTemplate::new(400).set_body_json(serde_json::json!({ "error": {
1826+
"message": "This model's maximum context length is 8192 tokens." } })),
1827+
)
1828+
.mount(&server)
1829+
.await;
1830+
let client =
1831+
LlmClient::new(&server.uri(), None, "m").with_reasoning_effort(Some("low".into()));
1832+
let tool = serde_json::json!([{ "type": "function", "function": { "name": "ping" } }]);
1833+
let ask = [serde_json::json!({ "role": "user", "content": "x" })];
1834+
assert!(client.chat_tools(&ask, &tool).await.is_err());
1835+
assert_eq!(server.received_requests().await.unwrap().len(), 2);
1836+
assert_eq!(
1837+
client.tools_body(&ask, Some(&tool), None, false)["reasoning_effort"],
1838+
"none",
1839+
"the flag is kept"
1840+
);
1841+
}
1842+
17671843
#[tokio::test]
17681844
async fn another_400_on_a_tool_request_is_not_retried() {
17691845
use wiremock::{matchers::method, Mock, MockServer, ResponseTemplate};

0 commit comments

Comments
 (0)