v1: accept OpenAI multimodal content shape (array-of-parts)
lakehouse/auditor 1 blocking issue: todo!() macro call in tests/real-world/scrum_master_pipeline.ts
lakehouse/auditor 1 blocking issue: todo!() macro call in tests/real-world/scrum_master_pipeline.ts
Modern OpenAI clients (pi-ai, openai SDK 6.x, langchain-js, the official
agents) send `messages[].content` as an array of content parts:
`[{type:"text", text:"..."}, {type:"image_url", ...}]`. Our gateway
typed `content` as plain `String` and 422'd those calls.
Fix: `Message.content` is now `serde_json::Value` so requests
deserialize regardless of shape. `Message::text()` flattens
content-parts arrays (concat'd `text` fields, non-text parts skipped)
for places that need a plain string — Ollama prompt assembly, char
counts, the assistant's own response synthesis. `Message::new_text()`
constructs string-content messages without writing the wrapper at
each call site. Forwarders (openrouter) clone content through
verbatim so providers see exactly what the client sent.
Verified end-to-end: Pi CLI (`pi --print --provider openrouter`)
landed a clean 1902-token request through `/v1/chat/completions`,
routed to OpenRouter as `openai/gpt-oss-120b:free`, response in
1.62s, Langfuse trace `v1.chat:openrouter` recorded with provider
tag. Same path that any tool using the official openai SDK takes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -378,7 +378,7 @@ impl ExecutionLoop {
|
||||
attempts = attempt + 1;
|
||||
let req = ChatRequest {
|
||||
model: model.to_string(),
|
||||
messages: vec![Message { role: "user".into(), content: prompt.to_string() }],
|
||||
messages: vec![Message::new_text("user", prompt.to_string())],
|
||||
temperature: Some(temperature),
|
||||
max_tokens: None,
|
||||
stream: Some(false),
|
||||
@@ -389,8 +389,8 @@ impl ExecutionLoop {
|
||||
.map_err(|e| format!("ollama_cloud: {e}"))?;
|
||||
tokens_p = tokens_p.saturating_add(resp.usage.prompt_tokens);
|
||||
tokens_c = tokens_c.saturating_add(resp.usage.completion_tokens);
|
||||
let t = resp.choices.into_iter().next()
|
||||
.map(|c| c.message.content).unwrap_or_default();
|
||||
let t: String = resp.choices.into_iter().next()
|
||||
.map(|c| c.message.text()).unwrap_or_default();
|
||||
if !t.trim().is_empty() {
|
||||
text = t;
|
||||
break;
|
||||
@@ -428,7 +428,7 @@ impl ExecutionLoop {
|
||||
lf.emit_chat(ChatTrace {
|
||||
provider: provider.to_string(),
|
||||
model: model.to_string(),
|
||||
input: vec![Message { role: "user".into(), content: prompt.to_string() }],
|
||||
input: vec![Message::new_text("user", prompt.to_string())],
|
||||
output: text.clone(),
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
@@ -605,10 +605,7 @@ impl ExecutionLoop {
|
||||
let start_time = chrono::Utc::now();
|
||||
let chat_req = crate::v1::ChatRequest {
|
||||
model: "gpt-oss:120b".to_string(),
|
||||
messages: vec![crate::v1::Message {
|
||||
role: "user".into(),
|
||||
content: prompt.clone(),
|
||||
}],
|
||||
messages: vec![crate::v1::Message::new_text("user", prompt.clone())],
|
||||
temperature: Some(0.1),
|
||||
max_tokens: None,
|
||||
stream: Some(false),
|
||||
@@ -619,8 +616,8 @@ impl ExecutionLoop {
|
||||
.map_err(|e| format!("ollama_cloud: {e}"))?;
|
||||
let latency_ms = started.elapsed().as_millis() as u64;
|
||||
let end_time = chrono::Utc::now();
|
||||
let correction_text = resp.choices.into_iter().next()
|
||||
.map(|c| c.message.content).unwrap_or_default();
|
||||
let correction_text: String = resp.choices.into_iter().next()
|
||||
.map(|c| c.message.text()).unwrap_or_default();
|
||||
|
||||
// Stamp per-task stats — cloud call counts against the same
|
||||
// usage counter so `/v1/usage` shows cloud token spend too.
|
||||
@@ -638,7 +635,7 @@ impl ExecutionLoop {
|
||||
lf.emit_chat(ChatTrace {
|
||||
provider: "ollama_cloud".into(),
|
||||
model: "gpt-oss:120b".into(),
|
||||
input: vec![crate::v1::Message { role: "user".into(), content: prompt.clone() }],
|
||||
input: vec![crate::v1::Message::new_text("user", prompt.clone())],
|
||||
output: correction_text.clone(),
|
||||
prompt_tokens: resp.usage.prompt_tokens,
|
||||
completion_tokens: resp.usage.completion_tokens,
|
||||
|
||||
Reference in New Issue
Block a user