blob: 7a576e14cca4e56e2bbd5ed13eafa325a4ed26a7 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_agent_facts::model::ModelPart;
use maka_agent_facts::tools::ToolDefinition;
use maka_model::{
ModelExecutor, ModelRequest, ProviderAuth, ProviderConfig, ProviderKind, StepBuilder,
};
use serde_json::json;
use std::{collections::BTreeMap, time::Duration};
use tokio_util::sync::CancellationToken;
async fn run(protocol: &str, kind: ProviderKind) {
use maka_agent_facts::model::TextKind;
use maka_model::prompt::{AssistantPart, Message, ToolOutput};
let base_url = "https://openrouter.ai/api/v1".to_owned();
let key = std::env::var("OPENROUTER_API_KEY").expect("explicit authorization required");
let model = std::env::var("MAKA_OPENROUTER_FREE_MODEL").expect("explicit free model required");
assert!(
model.ends_with(":free"),
"only explicit free models are allowed"
);
let executor = ModelExecutor::new(Duration::from_secs(120)).unwrap();
let tools = vec![ToolDefinition {
freeform: None,
output_schema: None,
provider: None,
name: "echo".into(),
description: "Echo the supplied token.".into(),
input_schema: json!({"type":"object","properties":{"token":{"type":"string"}},"required":["token"],"additionalProperties":false}),
}];
{
let options = json!({});
let provider = ProviderConfig {
adapter: None,
capabilities: Default::default(),
network: Default::default(),
kind,
base_url: base_url.clone(),
model: model.clone(),
auth: ProviderAuth::ApiKey(key),
headers: BTreeMap::new(),
body_overlay: None,
};
let mut prompt = vec![Message::user(
"Call echo with token amber-native-42 exactly once, then report its result. Do not answer without calling echo.",
)];
for turn in 0..2 {
let mut stream = executor
.stream(
ModelRequest {
language: maka_agent_facts::language::Language::En,
provider: provider.clone(),
prompt: prompt.clone(),
tools: tools.clone(),
provider_options: options.clone(),
max_output_tokens: Some(4096),
},
CancellationToken::new(),
)
.await
.unwrap();
let mut builder = StepBuilder::default();
while let Some(event) = stream.next().await {
builder
.push(
event
.unwrap_or_else(|error| panic!("{protocol} step {turn}: {error}"))
.event,
)
.unwrap();
}
let step = builder.finish().unwrap();
stream
.cancel_and_wait()
.await
.expect("actual stream cleanup");
assert!(
step.usage.input_tokens.is_some(),
"{protocol}: missing input usage"
);
assert!(
step.usage.output_tokens.is_some(),
"{protocol}: missing output usage"
);
if turn == 0 {
let calls = step.tool_calls().collect::<Vec<_>>();
assert_eq!(calls.len(), 1, "{protocol}: expected one tool call");
let call = calls[0];
assert_eq!(call.name, "echo");
assert_eq!(
call.input,
maka_agent_facts::model::ToolInput::Json(json!({"token":"amber-native-42"}))
);
let content = step
.parts
.iter()
.filter_map(|part| match part {
ModelPart::Text {
text_kind: TextKind::Thinking,
text,
provider_options,
} => Some(AssistantPart::Reasoning {
text: text.clone(),
provider_options: provider_options.clone(),
}),
ModelPart::Text {
text,
provider_options,
..
} => Some(AssistantPart::Text {
text: text.clone(),
provider_options: provider_options.clone(),
}),
ModelPart::ToolCall { call } => Some(AssistantPart::ToolCall {
tool_call_id: call.id.clone(),
tool_name: call.name.clone(),
input: call.input.clone(),
provider_options: call.provider_options.clone(),
provider_executed: Some(call.provider_executed),
}),
_ => None,
})
.collect();
prompt.push(Message::Assistant {
content,
provider_options: None,
});
prompt.push(Message::tool(
&call.id,
&call.name,
ToolOutput::Text("cobalt-tool-receipt-73".into()),
));
} else {
let text = step
.parts
.iter()
.filter_map(|p| match p {
ModelPart::Text {
text_kind: TextKind::Text,
text,
..
} => Some(text.as_str()),
_ => None,
})
.collect::<String>();
assert!(
text.contains("cobalt-tool-receipt-73"),
"{protocol}: tool result was not replayed"
);
assert_eq!(
step.tool_calls().count(),
0,
"{protocol}: unexpected repeated tool call"
);
}
}
println!(
"OpenRouter {protocol} {model}: native tool call, canonical replay, final text, usage and cleanup passed"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires authorized OPENROUTER_API_KEY and verified MAKA_OPENROUTER_FREE_MODEL"]
async fn openrouter_chat_tools_and_replay() {
tokio::time::timeout(
Duration::from_secs(240),
run(
"chat",
ProviderKind::OpenaiCompatible {
name: "openrouter".into(),
},
),
)
.await
.expect("bounded live test");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires authorized OPENROUTER_API_KEY and verified MAKA_OPENROUTER_FREE_MODEL"]
async fn openrouter_messages_tools_and_replay() {
tokio::time::timeout(
Duration::from_secs(240),
run("messages", ProviderKind::Anthropic),
)
.await
.expect("bounded live test");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires authorized OPENROUTER_API_KEY and verified MAKA_OPENROUTER_FREE_MODEL"]
async fn openrouter_responses_tools_and_replay() {
tokio::time::timeout(
Duration::from_secs(240),
run("responses", ProviderKind::OpenaiResponses),
)
.await
.expect("bounded live test");
}
async fn vision(protocol: &str, kind: ProviderKind) {
use maka_agent_facts::model::TextKind;
use maka_model::prompt::{ContentPart, FileData, Message};
let model = std::env::var("MAKA_OPENROUTER_FREE_VISION_MODEL")
.expect("explicit free vision model required");
assert!(model.ends_with(":free"));
let mut input = super::provider_stream::request(kind, "https://openrouter.ai/api/v1".into());
input.provider.auth = ProviderAuth::ApiKey(
std::env::var("OPENROUTER_API_KEY").expect("explicit authorization required"),
);
input.provider.model = model.clone();
input.tools.clear();
input.max_output_tokens = Some(1024);
input.prompt = vec![Message::User {
content: vec![
ContentPart::text("Name the solid color in this image. Reply with its English color name only."),
ContentPart::File {
data: FileData::Data("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAb0lEQVR4nO3PAQkAAAyEwO9feoshgnABdLep8QUNyPEFDcjxBQ3I8QUNyPEFDcjxBQ3I8QUNyPEFDcjxBQ3I8QUNyPEFDcjxBQ3I8QUNyPEFDcjxBQ3I8QUNyPEFDcjxBQ3I8QUNyPEFDcjxBQ3IPanc8OLDQitxAAAAAElFTkSuQmCC".into()),
media_type: "image/png".into(),
provider_options: Some(json!({"openai":{"imageDetail":"high"}})),
},
],
provider_options: None,
}];
let executor = ModelExecutor::new(Duration::from_secs(120)).unwrap();
let mut stream = executor
.stream(input, CancellationToken::new())
.await
.unwrap();
let mut builder = StepBuilder::default();
while let Some(event) = stream.next().await {
builder
.push(
event
.unwrap_or_else(|error| panic!("{protocol} vision: {error}"))
.event,
)
.unwrap();
}
stream.cancel_and_wait().await.unwrap();
let step = builder.finish().unwrap();
let text = step
.parts
.iter()
.filter_map(|part| match part {
ModelPart::Text {
text_kind: TextKind::Text,
text,
..
} => Some(text.as_str()),
_ => None,
})
.collect::<String>();
assert!(
text.to_ascii_lowercase().contains("red"),
"{protocol}: actual image color was not recognized: {text}"
);
assert!(step.usage.input_tokens.is_some() && step.usage.output_tokens.is_some());
println!(
"OpenRouter {protocol} {model}: inline PNG, color recognition, usage and cleanup passed"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires authorized OPENROUTER_API_KEY and verified MAKA_OPENROUTER_FREE_VISION_MODEL"]
async fn openrouter_chat_vision() {
tokio::time::timeout(
Duration::from_secs(180),
vision(
"chat",
ProviderKind::OpenaiCompatible {
name: "openrouter".into(),
},
),
)
.await
.expect("bounded live vision test");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires authorized OPENROUTER_API_KEY and verified MAKA_OPENROUTER_FREE_VISION_MODEL"]
async fn openrouter_messages_vision() {
tokio::time::timeout(
Duration::from_secs(180),
vision("messages", ProviderKind::Anthropic),
)
.await
.expect("bounded live vision test");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires authorized OPENROUTER_API_KEY and verified MAKA_OPENROUTER_FREE_VISION_MODEL"]
async fn openrouter_responses_vision() {
tokio::time::timeout(
Duration::from_secs(180),
vision("responses", ProviderKind::OpenaiResponses),
)
.await
.expect("bounded live vision test");
}