Skip to content

Commit 47358c2

Browse files
committed
fix(anda_engine): try to fix LLM provider client
1 parent 4c4bf90 commit 47358c2

7 files changed

Lines changed: 134 additions & 26 deletions

File tree

‎anda_engine/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "anda_engine"
33
description = "Agents engine for Anda -- an AI agent framework built with Rust, powered by ICP and TEEs."
44
repository = "https://github.com/ldclabs/anda/tree/main/anda_engine"
55
publish = true
6-
version = "0.9.38"
6+
version = "0.9.39"
77
edition.workspace = true
88
keywords.workspace = true
99
categories.workspace = true

‎anda_engine/src/model/deepseek.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,8 @@ impl CompletionFeaturesDyn for CompletionModel {
413413
);
414414
body.insert(
415415
"tool_choice".to_string(),
416-
if req.tool_choice_required {
416+
if req.tool_choice_required && model != DEEKSEEK_R1 {
417+
// deepseek-reasoner does not support this tool_choice
417418
Json::from("required")
418419
} else {
419420
Json::from("auto")

‎anda_engine/src/model/doubao.rs‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ pub struct CompletionResponse {
119119
}
120120

121121
impl CompletionResponse {
122+
pub fn parse_output(&mut self) {
123+
for choice in self.choices.iter_mut() {
124+
if let Ok(msg) = serde_json::from_value::<MessageOutput>(choice.message.clone()) {
125+
choice.parsed_message = Some(msg);
126+
}
127+
}
128+
}
129+
122130
fn try_into(
123131
mut self,
124132
raw_history: Vec<Json>,
@@ -143,9 +151,12 @@ impl CompletionResponse {
143151
if !matches!(choice.finish_reason.as_str(), "stop" | "tool_calls") {
144152
output.failed_reason = Some(choice.finish_reason);
145153
} else {
146-
output.raw_history.push(json!(&choice.message));
154+
output.raw_history.push(choice.message);
147155
let timestamp = unix_ms();
148-
let mut msg: Message = choice.message.into();
156+
let mut msg: Message = choice
157+
.parsed_message
158+
.ok_or("Failed to parse message output")?
159+
.into();
149160
msg.name = Some(self.model);
150161
msg.timestamp = Some(timestamp);
151162
output.content = msg.text().unwrap_or_default();
@@ -159,7 +170,11 @@ impl CompletionResponse {
159170
fn maybe_failed(&self) -> bool {
160171
!self.choices.iter().any(|choice| {
161172
matches!(choice.finish_reason.as_str(), "stop" | "tool_calls")
162-
&& (!choice.message.content.is_empty() || choice.message.tool_calls.is_some())
173+
&& choice
174+
.parsed_message
175+
.as_ref()
176+
.map(|m| !m.content.is_empty() || m.tool_calls.is_some())
177+
.unwrap_or(false)
163178
})
164179
}
165180
}
@@ -190,6 +205,11 @@ fn to_message_input(msg: &Message) -> MessageInput {
190205
ContentPart::ToolOutput {
191206
output, call_id, ..
192207
} => {
208+
if msg.content.len() == 1 {
209+
res.content = serde_json::to_value(output).unwrap_or_default();
210+
res.tool_call_id = call_id.clone();
211+
return res;
212+
}
193213
arr.push(json!({
194214
"type": "text",
195215
"text": serde_json::to_string(output).unwrap_or_default(),
@@ -228,8 +248,11 @@ fn to_message_input(msg: &Message) -> MessageInput {
228248
#[derive(Debug, Deserialize, Serialize)]
229249
pub struct Choice {
230250
pub index: usize,
231-
pub message: MessageOutput,
251+
pub message: Json,
232252
pub finish_reason: String,
253+
254+
#[serde(skip)]
255+
pub parsed_message: Option<MessageOutput>,
233256
}
234257

235258
/// Output message structure from Doubao API
@@ -478,7 +501,9 @@ impl CompletionFeaturesDyn for CompletionModel {
478501
if response.status().is_success() {
479502
let text = response.text().await?;
480503
match serde_json::from_str::<CompletionResponse>(&text) {
481-
Ok(res) => {
504+
Ok(mut res) => {
505+
res.parse_output();
506+
482507
if log_enabled!(Debug) {
483508
log::debug!(
484509
request:serde = body,

‎anda_engine/src/model/kimi.rs‎

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use anda_core::{
99
AgentOutput, BoxError, BoxPinFut, CompletionFeatures, CompletionRequest, ContentPart,
1010
FunctionDefinition, Json, Message, Resource, Usage as ModelUsage,
1111
};
12+
1213
use log::{Level::Debug, log_enabled};
1314
use serde::{Deserialize, Serialize};
1415
use serde_json::json;
@@ -116,6 +117,14 @@ pub struct CompletionResponse {
116117
}
117118

118119
impl CompletionResponse {
120+
pub fn parse_output(&mut self) {
121+
for choice in self.choices.iter_mut() {
122+
if let Ok(msg) = serde_json::from_value::<MessageOutput>(choice.message.clone()) {
123+
choice.parsed_message = Some(msg);
124+
}
125+
}
126+
}
127+
119128
fn try_into(
120129
mut self,
121130
raw_history: Vec<Json>,
@@ -140,9 +149,12 @@ impl CompletionResponse {
140149
if !matches!(choice.finish_reason.as_str(), "stop" | "tool_calls") {
141150
output.failed_reason = Some(choice.finish_reason);
142151
} else {
143-
output.raw_history.push(json!(&choice.message));
152+
output.raw_history.push(choice.message);
144153
let timestamp = unix_ms();
145-
let mut msg: Message = choice.message.into();
154+
let mut msg: Message = choice
155+
.parsed_message
156+
.ok_or("Failed to parse message output")?
157+
.into();
146158
msg.name = Some(self.model);
147159
msg.timestamp = Some(timestamp);
148160
output.content = msg.text().unwrap_or_default();
@@ -156,7 +168,11 @@ impl CompletionResponse {
156168
fn maybe_failed(&self) -> bool {
157169
!self.choices.iter().any(|choice| {
158170
matches!(choice.finish_reason.as_str(), "stop" | "tool_calls")
159-
&& (!choice.message.content.is_empty() || choice.message.tool_calls.is_some())
171+
&& choice
172+
.parsed_message
173+
.as_ref()
174+
.map(|m| !m.content.is_empty() || m.tool_calls.is_some())
175+
.unwrap_or(false)
160176
})
161177
}
162178
}
@@ -187,6 +203,11 @@ fn to_message_input(msg: &Message) -> MessageInput {
187203
ContentPart::ToolOutput {
188204
output, call_id, ..
189205
} => {
206+
if msg.content.len() == 1 {
207+
res.content = serde_json::to_value(output).unwrap_or_default();
208+
res.tool_call_id = call_id.clone();
209+
return res;
210+
}
190211
arr.push(json!({
191212
"type": "text",
192213
"text": serde_json::to_string(output).unwrap_or_default(),
@@ -225,8 +246,11 @@ fn to_message_input(msg: &Message) -> MessageInput {
225246
#[derive(Debug, Deserialize, Serialize)]
226247
pub struct Choice {
227248
pub index: usize,
228-
pub message: MessageOutput,
249+
pub message: Json,
229250
pub finish_reason: String,
251+
252+
#[serde(skip)]
253+
pub parsed_message: Option<MessageOutput>,
230254
}
231255

232256
/// Output message structure from Kimi API
@@ -236,6 +260,9 @@ pub struct MessageOutput {
236260
#[serde(default)]
237261
pub content: String,
238262

263+
#[serde(skip_serializing_if = "Option::is_none")]
264+
pub reasoning_content: Option<String>,
265+
239266
#[serde(skip_serializing_if = "Option::is_none")]
240267
pub tool_calls: Option<Vec<ToolCallOutput>>,
241268
}
@@ -440,7 +467,9 @@ impl CompletionFeaturesDyn for CompletionModel {
440467
if response.status().is_success() {
441468
let text = response.text().await?;
442469
match serde_json::from_str::<CompletionResponse>(&text) {
443-
Ok(res) => {
470+
Ok(mut res) => {
471+
res.parse_output();
472+
444473
if log_enabled!(Debug) {
445474
log::debug!(
446475
request:serde = body,

‎anda_engine/src/model/mimo.rs‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ pub struct CompletionResponse {
113113
}
114114

115115
impl CompletionResponse {
116+
pub fn parse_output(&mut self) {
117+
for choice in self.choices.iter_mut() {
118+
if let Ok(msg) = serde_json::from_value::<MessageOutput>(choice.message.clone()) {
119+
choice.parsed_message = Some(msg);
120+
}
121+
}
122+
}
123+
116124
fn try_into(
117125
mut self,
118126
raw_history: Vec<Json>,
@@ -137,9 +145,12 @@ impl CompletionResponse {
137145
if !matches!(choice.finish_reason.as_str(), "stop" | "tool_calls") {
138146
output.failed_reason = Some(choice.finish_reason);
139147
} else {
140-
output.raw_history.push(json!(&choice.message));
148+
output.raw_history.push(choice.message);
141149
let timestamp = unix_ms();
142-
let mut msg: Message = choice.message.into();
150+
let mut msg: Message = choice
151+
.parsed_message
152+
.ok_or("Failed to parse message output")?
153+
.into();
143154
msg.name = Some(self.model);
144155
msg.timestamp = Some(timestamp);
145156
output.content = msg.text().unwrap_or_default();
@@ -153,7 +164,11 @@ impl CompletionResponse {
153164
fn maybe_failed(&self) -> bool {
154165
!self.choices.iter().any(|choice| {
155166
matches!(choice.finish_reason.as_str(), "stop" | "tool_calls")
156-
&& (!choice.message.content.is_empty() || choice.message.tool_calls.is_some())
167+
&& choice
168+
.parsed_message
169+
.as_ref()
170+
.map(|m| !m.content.is_empty() || m.tool_calls.is_some())
171+
.unwrap_or(false)
157172
})
158173
}
159174
}
@@ -184,6 +199,12 @@ fn to_message_input(msg: &Message) -> MessageInput {
184199
ContentPart::ToolOutput {
185200
output, call_id, ..
186201
} => {
202+
if msg.content.len() == 1 {
203+
res.content = serde_json::to_value(output).unwrap_or_default();
204+
res.tool_call_id = call_id.clone();
205+
return res;
206+
}
207+
187208
arr.push(json!({
188209
"type": "text",
189210
"text": serde_json::to_string(output).unwrap_or_default(),
@@ -230,8 +251,11 @@ fn to_message_input(msg: &Message) -> MessageInput {
230251
#[derive(Debug, Deserialize, Serialize)]
231252
pub struct Choice {
232253
pub index: usize,
233-
pub message: MessageOutput,
254+
pub message: Json,
234255
pub finish_reason: String,
256+
257+
#[serde(skip)]
258+
pub parsed_message: Option<MessageOutput>,
235259
}
236260

237261
/// Output message structure from Mimo API
@@ -480,7 +504,9 @@ impl CompletionFeaturesDyn for CompletionModel {
480504
if response.status().is_success() {
481505
let text = response.text().await?;
482506
match serde_json::from_str::<CompletionResponse>(&text) {
483-
Ok(res) => {
507+
Ok(mut res) => {
508+
res.parse_output();
509+
484510
if log_enabled!(Debug) {
485511
log::debug!(
486512
request:serde = body,

‎anda_engine/src/model/openai.rs‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,14 @@ pub struct CompletionResponse {
307307
}
308308

309309
impl CompletionResponse {
310+
pub fn parse_output(&mut self) {
311+
for choice in self.choices.iter_mut() {
312+
if let Ok(msg) = serde_json::from_value::<MessageOutput>(choice.message.clone()) {
313+
choice.parsed_message = Some(msg);
314+
}
315+
}
316+
}
317+
310318
fn try_into(
311319
mut self,
312320
raw_history: Vec<Json>,
@@ -331,13 +339,17 @@ impl CompletionResponse {
331339
if !matches!(choice.finish_reason.as_str(), "stop" | "tool_calls") {
332340
output.failed_reason = Some(choice.finish_reason);
333341
} else {
334-
if let Some(refusal) = &choice.message.refusal {
342+
output.raw_history.push(choice.message);
343+
let timestamp = unix_ms();
344+
let msg = choice
345+
.parsed_message
346+
.ok_or("Failed to parse message output")?;
347+
348+
if let Some(refusal) = &msg.refusal {
335349
output.failed_reason = Some(refusal.clone());
336350
}
337351

338-
output.raw_history.push(json!(&choice.message));
339-
let timestamp = unix_ms();
340-
let mut msg: Message = choice.message.into();
352+
let mut msg: Message = msg.into();
341353
msg.name = Some(self.model);
342354
msg.timestamp = Some(timestamp);
343355
output.content = msg.text().unwrap_or_default();
@@ -351,8 +363,11 @@ impl CompletionResponse {
351363
fn maybe_failed(&self) -> bool {
352364
!self.choices.iter().any(|choice| {
353365
matches!(choice.finish_reason.as_str(), "stop" | "tool_calls")
354-
&& choice.message.refusal.is_none()
355-
&& (!choice.message.content.is_empty() || choice.message.tool_calls.is_some())
366+
&& choice
367+
.parsed_message
368+
.as_ref()
369+
.map(|m| !m.content.is_empty() || m.tool_calls.is_some())
370+
.unwrap_or(false)
356371
})
357372
}
358373
}
@@ -383,6 +398,11 @@ fn to_message_input(msg: &Message) -> MessageInput {
383398
ContentPart::ToolOutput {
384399
output, call_id, ..
385400
} => {
401+
if msg.content.len() == 1 {
402+
res.content = serde_json::to_value(output).unwrap_or_default();
403+
res.tool_call_id = call_id.clone();
404+
return res;
405+
}
386406
arr.push(json!({
387407
"type": "text",
388408
"text": serde_json::to_string(output).unwrap_or_default(),
@@ -443,8 +463,11 @@ fn to_message_input(msg: &Message) -> MessageInput {
443463
#[derive(Debug, Deserialize, Serialize)]
444464
pub struct Choice {
445465
pub index: usize,
446-
pub message: MessageOutput,
466+
pub message: Json,
447467
pub finish_reason: String,
468+
469+
#[serde(skip)]
470+
pub parsed_message: Option<MessageOutput>,
448471
}
449472

450473
#[derive(Debug, Deserialize, Serialize)]
@@ -454,6 +477,9 @@ pub struct MessageOutput {
454477
#[serde(default)]
455478
pub content: String,
456479

480+
#[serde(skip_serializing_if = "Option::is_none")]
481+
pub reasoning_content: Option<String>,
482+
457483
#[serde(skip_serializing_if = "Option::is_none")]
458484
pub refusal: Option<String>,
459485

@@ -657,7 +683,8 @@ impl CompletionFeaturesDyn for CompletionModel {
657683
if response.status().is_success() {
658684
let text = response.text().await?;
659685
match serde_json::from_str::<CompletionResponse>(&text) {
660-
Ok(res) => {
686+
Ok(mut res) => {
687+
res.parse_output();
661688
if log_enabled!(Debug) {
662689
log::debug!(
663690
request:serde = body,

‎anda_engine/src/model/openai/types.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ pub struct CompletionResponse {
9898
pub usage: ResponsesUsage,
9999
/// The model output (messages, etc will go here)
100100
pub output: Vec<Json>,
101-
#[serde(default)]
101+
#[serde(skip, default)]
102102
pub parsed_output: Vec<MessageItem>,
103103
/// Tools
104104
#[serde(default)]

0 commit comments

Comments
 (0)