blob: ecfb0ea80b7fe121216c63aa8909d419c3520324 [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::model::ModelSource;
use maka_agent_facts::tools::ProviderTool;
use maka_agent_facts::tools::ToolDefinition;
use maka_model::{
Conversation, ModelExecutor, ModelRequest, ProviderAuth, ProviderConfig, ProviderKind,
StepBuilder,
};
use serde_json::{Value, json};
use std::{collections::BTreeMap, time::Duration};
use tokio_util::sync::CancellationToken;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires explicit MAKA_SGLANG_TEST_URL and authorized deepseek-v4.1-flash access"]
async fn sglang_protocols_preserve_reasoning_tools_and_replay() {
use maka_agent_facts::model::PlaintextReasoningReplay;
use maka_agent_facts::model::PlaintextResponses;
use maka_agent_facts::model::TextKind;
use maka_model::prompt::{AssistantPart, Message, ToolOutput};
let base_url = std::env::var("MAKA_SGLANG_TEST_URL").expect("explicit endpoint required");
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}),
}];
for (protocol, kind, options) in [
(
"chat",
ProviderKind::OpenaiCompatible {
name: "sglang".into(),
},
json!({"sglang":{"reasoningEffort":"high"}}),
),
(
"responses",
ProviderKind::OpenResponses(PlaintextResponses {
reasoning_replay: PlaintextReasoningReplay::PlaintextContent,
compatibility: None,
}),
json!({"openResponses":{"reasoningEffort":"high"}}),
),
(
"messages",
ProviderKind::Anthropic,
json!({"anthropic":{"thinking":{"type":"enabled","budgetTokens":1024}}}),
),
] {
let provider = ProviderConfig {
adapter: None,
capabilities: Default::default(),
network: Default::default(),
kind,
base_url: base_url.clone(),
model: "deepseek-v4.1-flash".into(),
auth: ProviderAuth::ApiKey("local-test".into()),
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 {
assert!(step.parts.iter().any(|part| matches!(part, ModelPart::Text { text_kind: TextKind::Thinking, text, .. } if !text.is_empty())),
"{protocol}: reasoning was lost");
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("amber-native-42".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("amber-native-42"),
"{protocol}: tool result was not replayed"
);
assert_eq!(
step.tool_calls().count(),
0,
"{protocol}: unexpected repeated tool call"
);
}
}
println!("SGLang {protocol}: reasoning, tool call, replay and usage passed");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires explicit MAKA_CODEX_AUTH_FILE and authorized gpt-5.6-luna subscription access"]
async fn codex_search_preserves_real_provider_calls_and_citations() {
use std::io::Read;
let path = std::env::var_os("MAKA_CODEX_AUTH_FILE").expect("explicit auth path required");
let mut bytes = Vec::new();
std::fs::File::open(path)
.unwrap()
.take(1024 * 1024 + 1)
.read_to_end(&mut bytes)
.unwrap();
assert!(bytes.len() <= 1024 * 1024);
let auth: Value = serde_json::from_slice(&bytes).expect("invalid auth document");
let access_token = auth["tokens"]["access_token"]
.as_str()
.expect("ChatGPT subscription token required")
.to_owned();
let mut network = maka_network::Policy::default();
if let Ok(port) = std::env::var("MAKA_LIVE_PROXY_PORT") {
let mut proxy =
maka_agent_facts::configuration::policy::RuntimePolicy::default().network_proxy;
proxy.enabled = true;
proxy.host = "127.0.0.1".into();
proxy.port = port.parse().expect("invalid proxy port");
network = maka_network::Policy::from_settings(&proxy, None).unwrap();
}
tokio::time::timeout(Duration::from_secs(120), async {
let executor = ModelExecutor::new(Duration::from_secs(60)).unwrap();
let lane = Conversation::default();
let request = ModelRequest {
language: maka_agent_facts::language::Language::En,
provider: ProviderConfig {
adapter: Some(maka_providers::codex::ADAPTER.into()),
capabilities: Default::default(),
kind: ProviderKind::OpenaiResponses,
model: "gpt-5.6-luna".into(),
base_url: "https://chatgpt.com/backend-api/codex".into(),
auth: ProviderAuth::RequestHeaders(maka_providers::codex::request_headers(&access_token, &format!("maka-web-test-{}", std::process::id())).unwrap()),
headers: BTreeMap::new(), body_overlay: None, network,
},
prompt: vec![
maka_model::prompt::Message::System { content: "Use the provided web search tool for current information. Reply briefly with a cited source.".into(), provider_options: None },
maka_model::prompt::Message::user("Search rust-lang.org for the current stable Rust release. Give its version and release date with a citation.")
],
tools: vec![ToolDefinition {
freeform: None,
output_schema: None,
name: "WebSearch".into(),
description: "Search the web".into(),
input_schema: json!({"type":"object","properties":{}}),
provider: Some(ProviderTool { id: "openai.web_search".into(), args: json!({"searchContextSize":"medium"}) }),
}],
provider_options: json!({"openai":{"reasoningEffort":"low"}}),
max_output_tokens: None,
};
let mut stream = executor.stream_in_conversation(request, CancellationToken::new(), Some(lane.clone())).await.unwrap();
let mut builder = StepBuilder::for_step("live-native-search").unwrap();
while let Some(event) = stream.next().await { builder.push(event.unwrap().event).unwrap(); }
let step = builder.finish().unwrap();
stream.cancel_and_wait().await.expect("actual stream cleanup");
let calls = step.tool_calls().filter(|call| call.provider_executed && call.name == "WebSearch").count();
let sources = step.parts.iter().filter(|part| matches!(part, ModelPart::Source { source: ModelSource::Url { .. } })).count();
println!("Codex native search: {calls} provider calls, {sources} cited sources; websocket={}", lane.needs_confirmation());
assert!(calls > 0, "ordinary text is not evidence of a provider search");
assert!(sources > 0, "citations must survive the adapter and canonical step");
drop(lane);
}).await.expect("live search must remain bounded");
}