Skip to content

Commit 624989a

Browse files
committed
Merge branch 'dev/goal0610' into dev/websearch0610
# Conflicts: # crates/provider/src/anthropic/messages.rs # crates/provider/src/openai/chat_completions.rs # crates/provider/src/openai/responses.rs
2 parents 122bd2a + b359802 commit 624989a

6 files changed

Lines changed: 125 additions & 40 deletions

File tree

crates/provider/src/anthropic/messages.rs

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use crate::ProviderAdapter;
3535
use crate::ProviderCapabilities;
3636
use crate::ProviderHttpOptions;
3737
use crate::hosted_tools::append_anthropic_hosted_tools;
38+
use crate::http::invalid_status_error;
3839
use crate::merge_extra_body;
3940

4041
/// <https://platform.claude.com/docs/en/api/messages>
@@ -121,6 +122,8 @@ struct AnthropicInputMessage {
121122
enum AnthropicInputContentBlock {
122123
#[serde(rename = "text")]
123124
Text { text: String },
125+
#[serde(rename = "thinking")]
126+
Thinking { thinking: String },
124127
#[serde(rename = "tool_use")]
125128
ToolUse {
126129
id: String,
@@ -271,9 +274,22 @@ impl ModelProviderSDK for AnthropicProvider {
271274
.request_builder(&body)
272275
.send()
273276
.await
274-
.context("failed to send anthropic request")?
275-
.error_for_status()
276-
.context("anthropic request failed")?;
277+
.context("failed to send anthropic request")?;
278+
let response = match response.error_for_status_ref() {
279+
Ok(_) => response,
280+
Err(_) => {
281+
let status = response.status();
282+
return Err(invalid_status_error(
283+
"anthropic",
284+
&request.model,
285+
"request",
286+
status,
287+
response,
288+
&body,
289+
)
290+
.await);
291+
}
292+
};
277293

278294
let value: Value = response
279295
.json()
@@ -311,9 +327,24 @@ impl ModelProviderSDK for AnthropicProvider {
311327

312328
futures::pin_mut!(event_source);
313329
while let Some(event) = event_source.next().await {
314-
let event = event.map_err(|error| {
315-
anyhow::anyhow!("anthropic stream error for model {}: {error}", request.model)
316-
})?;
330+
let event = match event {
331+
Ok(event) => event,
332+
Err(reqwest_eventsource::Error::InvalidStatusCode(status, response)) => {
333+
Err(invalid_status_error(
334+
"anthropic",
335+
&request.model,
336+
"stream",
337+
status,
338+
response,
339+
&body,
340+
)
341+
.await)?
342+
}
343+
Err(error) => Err(anyhow::anyhow!(
344+
"anthropic stream error for model {}: {error}",
345+
request.model
346+
))?,
347+
};
317348

318349
match event {
319350
Event::Open => {}
@@ -944,9 +975,9 @@ fn build_message(message: &RequestMessage) -> AnthropicInputMessage {
944975
fn build_content_block(block: &RequestContent) -> AnthropicInputContentBlock {
945976
match block {
946977
RequestContent::Text { text } => AnthropicInputContentBlock::Text { text: text.clone() },
947-
RequestContent::Reasoning { text } => {
948-
AnthropicInputContentBlock::Text { text: text.clone() }
949-
}
978+
RequestContent::Reasoning { text } => AnthropicInputContentBlock::Thinking {
979+
thinking: text.clone(),
980+
},
950981
RequestContent::ToolUse { id, name, input } => AnthropicInputContentBlock::ToolUse {
951982
id: id.clone(),
952983
name: name.clone(),
@@ -1186,6 +1217,58 @@ mod tests {
11861217
assert_eq!(body["tools"][0]["name"], json!("get_weather"));
11871218
}
11881219

1220+
#[test]
1221+
fn build_request_serializes_reasoning_as_thinking_block() {
1222+
let request = ModelRequest {
1223+
model: "deepseek-v4-flash".to_string(),
1224+
system: None,
1225+
messages: vec![RequestMessage {
1226+
role: "assistant".to_string(),
1227+
content: vec![
1228+
RequestContent::Reasoning {
1229+
text: "Need to inspect the file first.".to_string(),
1230+
},
1231+
RequestContent::Text {
1232+
text: "I'll read the README.".to_string(),
1233+
},
1234+
RequestContent::ToolUse {
1235+
id: "toolu_123".to_string(),
1236+
name: "read".to_string(),
1237+
input: json!({"path": "README.md"}),
1238+
},
1239+
],
1240+
}],
1241+
max_tokens: 1024,
1242+
tools: None,
1243+
sampling: SamplingControls::default(),
1244+
thinking: Some("enabled".to_string()),
1245+
reasoning_effort: None,
1246+
extra_body: None,
1247+
};
1248+
1249+
let body = build_request(&request, true);
1250+
1251+
assert_eq!(
1252+
body["messages"][0]["content"],
1253+
json!([
1254+
{
1255+
"type": "thinking",
1256+
"thinking": "Need to inspect the file first."
1257+
},
1258+
{
1259+
"type": "text",
1260+
"text": "I'll read the README."
1261+
},
1262+
{
1263+
"type": "tool_use",
1264+
"id": "toolu_123",
1265+
"name": "read",
1266+
"input": { "path": "README.md" }
1267+
}
1268+
])
1269+
);
1270+
}
1271+
11891272
#[test]
11901273
fn parse_response_extracts_text_tool_use_reasoning_and_usage() {
11911274
let response = parse_response(json!({

crates/provider/src/http.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ use anyhow::Context;
44
use anyhow::Result;
55
use reqwest::Client;
66
use reqwest::RequestBuilder;
7+
use reqwest::Response;
8+
use reqwest::StatusCode;
79
use reqwest::header::HeaderMap;
810
use reqwest::header::HeaderName;
911
use reqwest::header::HeaderValue;
1012
use serde_json::Value;
13+
use tracing::warn;
1114

1215
/// HTTP options shared by model-provider adapters.
1316
#[derive(Clone, Debug, Default)]
@@ -54,6 +57,32 @@ impl ProviderHttpOptions {
5457
}
5558
}
5659

60+
pub(crate) async fn invalid_status_error(
61+
provider: &'static str,
62+
model: &str,
63+
operation: &str,
64+
status: StatusCode,
65+
response: Response,
66+
request_body: &Value,
67+
) -> anyhow::Error {
68+
let response_body = response
69+
.text()
70+
.await
71+
.unwrap_or_else(|error| format!("<failed to read response body: {error}>"));
72+
warn!(
73+
provider,
74+
model,
75+
operation,
76+
status = %status,
77+
http_body = %request_body,
78+
response_body = %response_body,
79+
"provider request failed"
80+
);
81+
anyhow::anyhow!(
82+
"{provider} {operation} error for model {model}: Invalid status code: {status}; response body: {response_body}"
83+
)
84+
}
85+
5786
fn parse_custom_headers(headers: Option<String>) -> Result<HeaderMap> {
5887
let Some(headers) = headers.and_then(|value| non_empty_string(&value)) else {
5988
return Ok(HeaderMap::new());

crates/provider/src/openai/chat_completions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use devo_protocol::Usage;
2727
use super::capabilities::OpenAIReasoningMode;
2828
use super::capabilities::OpenAITransport;
2929
use super::capabilities::resolve_request_profile;
30-
use super::shared::invalid_status_error;
3130
use super::shared::reasoning_value;
3231
use super::shared::request_role;
3332
use super::shared::tool_definitions;
@@ -36,6 +35,7 @@ use crate::ProviderAdapter;
3635
use crate::ProviderCapabilities;
3736
use crate::ProviderHttpOptions;
3837
use crate::hosted_tools::apply_openai_chat_completions_hosted_tools;
38+
use crate::http::invalid_status_error;
3939
use crate::merge_extra_body;
4040
use crate::text_normalization::split_tagged_text;
4141

crates/provider/src/openai/chat_completions/stream.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ use reqwest_eventsource::{Event, EventSource};
1212
use serde::Deserialize;
1313
use serde_json::Value;
1414

15-
use super::super::shared::invalid_status_error;
1615
use super::{
1716
OpenAIChatCompletionChoice, OpenAIChatCompletionCustomToolCall,
1817
OpenAIChatCompletionFunctionCall, OpenAIChatCompletionMessage,
1918
OpenAIChatCompletionMessageToolCall, OpenAIChoiceLogprobs, OpenAICompletionUsage,
2019
OpenAIProvider, build_provider_specific_response_payload, build_request, parse_finish_reason,
2120
parse_tool_use,
2221
};
22+
use crate::http::invalid_status_error;
2323
use crate::text_normalization::{TaggedTextFragment, TaggedTextParser};
2424

2525
/// <https://developers.openai.com/api/reference/resources/chat/subresources/completions/streaming-events>

crates/provider/src/openai/responses.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ use serde_json::{Value, json};
1414
use tracing::debug;
1515

1616
use crate::hosted_tools::append_openai_responses_hosted_tools;
17+
use crate::http::invalid_status_error;
1718
use crate::text_normalization::{TaggedTextFragment, TaggedTextParser, split_tagged_text};
1819
use crate::{ModelProviderSDK, ProviderHttpOptions, merge_extra_body};
1920

2021
use super::capabilities::{OpenAITransport, resolve_request_profile};
2122
use super::{
2223
OpenAIRole,
23-
shared::{invalid_status_error, request_role, tool_definitions},
24+
shared::{request_role, tool_definitions},
2425
};
2526

2627
/// OpenAI Responses API provider.

crates/provider/src/openai/shared.rs

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use devo_protocol::RequestRole;
22
use devo_protocol::ToolDefinition;
3-
use reqwest::Response;
4-
use reqwest::StatusCode;
53
use serde_json::Value;
64
use serde_json::json;
75
use tracing::warn;
@@ -97,29 +95,3 @@ pub(crate) fn tool_definitions(tools: &[ToolDefinition]) -> Value {
9795
.collect(),
9896
)
9997
}
100-
101-
pub(crate) async fn invalid_status_error(
102-
provider: &'static str,
103-
model: &str,
104-
operation: &str,
105-
status: StatusCode,
106-
response: Response,
107-
request_body: &Value,
108-
) -> anyhow::Error {
109-
let response_body = response
110-
.text()
111-
.await
112-
.unwrap_or_else(|error| format!("<failed to read response body: {error}>"));
113-
warn!(
114-
provider,
115-
model,
116-
operation,
117-
status = %status,
118-
http_body = %request_body,
119-
response_body = %response_body,
120-
"provider request failed"
121-
);
122-
anyhow::anyhow!(
123-
"{provider} {operation} error for model {model}: Invalid status code: {status}; response body: {response_body}"
124-
)
125-
}

0 commit comments

Comments
 (0)