use hey::llm::ir::{ChatRequest, EffortLevel, Message, Role, ToolCall};
use hey::llm::{Delta, Provider, build_provider};
use std::sync::Arc;
fn live_provider() -> Option<(Arc<dyn Provider>, String)> {
if let (Ok(b), Ok(k), Ok(m)) = (
std::env::var("HEY_E2E_BASE_URL"),
std::env::var("HEY_E2E_API_KEY"),
std::env::var("HEY_E2E_MODEL"),
) {
let pcfg = hey::config::ProviderConfig {
protocol: Some("openai".into()),
base_url: b,
api_key: Some(k),
models: vec![m.clone()],
effort: Some(EffortLevel::Low),
..Default::default()
};
return build(pcfg, m);
}
if let Some((pcfg, m)) = kilo_from_pi() {
return build(pcfg, m);
}
let home = std::env::var("HOME").ok()?;
let cfg_path = std::path::Path::new(&home).join(".config/hey/config.toml");
let cfg = hey::config::Config::from_files(&[cfg_path]).ok()?;
let (pname, model, mut pcfg) = cfg.resolve(None, None, None, None).ok()?;
let _ = pname;
pcfg.effort = Some(EffortLevel::Low);
eprintln!(
"[e2e] using provider config: model={model} base={}",
pcfg.base_url
);
build(pcfg, model)
}
fn kilo_from_pi() -> Option<(hey::config::ProviderConfig, String)> {
let home = std::env::var("HOME").ok()?;
let root = std::path::Path::new(&home).join(".pi/agent");
let auth: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(root.join("auth.json")).ok()?).ok()?;
let models: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(root.join("models.json")).ok()?).ok()?;
let kilo = models.get("providers")?.get("kilo")?;
let base = kilo.get("baseUrl")?.as_str()?;
let key = auth.get("kilo")?.get("key")?.as_str()?;
let model = kilo
.get("models")?
.as_array()?
.iter()
.filter_map(|m| m.get("id").and_then(|v| v.as_str()))
.find(|id| id.contains("hy3"))
.or_else(|| {
kilo.get("models")?
.as_array()?
.iter()
.find_map(|m| m.get("id").and_then(|v| v.as_str()))
})?;
eprintln!("[e2e] kilo: model={model} base={base}");
Some((
hey::config::ProviderConfig {
protocol: Some("openai".into()),
base_url: base.to_string(),
api_key: Some(key.to_string()),
models: vec![model.to_string()],
effort: Some(EffortLevel::Low),
..Default::default()
},
model.to_string(),
))
}
fn build(pcfg: hey::config::ProviderConfig, model: String) -> Option<(Arc<dyn Provider>, String)> {
let retry = hey::config::RetryConfig {
enabled: false,
..Default::default()
};
match build_provider(&pcfg, reqwest::Client::new(), &retry) {
Ok(p) => Some((p, model)),
Err(e) => {
eprintln!("[e2e] build_provider 失败: {e}");
None
}
}
}
fn req(messages: Vec<Message>) -> ChatRequest {
ChatRequest {
messages,
tools: vec![],
effort: EffortLevel::Low,
}
}
#[tokio::test]
#[ignore = "需真实 API key 与网络"]
async fn live_single_turn() {
let Some((provider, model)) = live_provider() else {
return; };
let mut text = String::new();
let mut thinking = String::new();
let c = provider
.stream(
&req(vec![Message::text(Role::User, "用中文回复两个字:连通")]),
&mut |d| match d {
Delta::Text(t) => text.push_str(&t),
Delta::Thinking(t) => thinking.push_str(&t),
},
)
.await
.expect("真实请求应成功");
assert!(
!c.text.trim().is_empty(),
"应有正文输出,got text={text:?} (streamed={text:?})"
);
eprintln!(
"[e2e] single_turn model={model} text={} thinking_len={}",
text.trim(),
thinking.len()
);
}
#[tokio::test]
#[ignore = "需真实 API key 与网络"]
async fn live_stream_thinking_and_text() {
let Some((provider, _model)) = live_provider() else {
return;
};
let mut saw_text = false;
let mut saw_thinking = false;
provider
.stream(
&req(vec![Message::text(
Role::User,
"一步一步推理:1+1 等于几?最后只输出答案。",
)]),
&mut |d| match d {
Delta::Text(_) => saw_text = true,
Delta::Thinking(_) => saw_thinking = true,
},
)
.await
.expect("真实请求应成功");
assert!(saw_text, "应收到 Text delta");
eprintln!("[e2e] stream deltas: text={saw_text} thinking={saw_thinking}");
}
#[tokio::test]
#[ignore = "需真实 API key 与网络"]
async fn live_tool_loop() {
let Some((provider, _model)) = live_provider() else {
return;
};
use hey::tools::{Tool, ToolCtx, ToolOutput};
struct EchoTool;
#[async_trait::async_trait]
impl hey::tools::Tool for EchoTool {
fn name(&self) -> &str {
"echo"
}
fn description(&self) -> &str {
"回显传入的 message 字段原样返回"
}
fn schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": { "message": { "type": "string" } },
"required": ["message"]
})
}
async fn run(&self, _ctx: &ToolCtx, args: serde_json::Value) -> ToolOutput {
let msg = args
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
ToolOutput::ok(msg)
}
}
let tool = Arc::new(EchoTool);
let mut messages = vec![Message::text(
Role::User,
"调用 echo 工具,参数 message 填 hi,然后把工具返回的内容原样输出。",
)];
let mut completed = false;
for _round in 0..4 {
let req = ChatRequest {
messages: messages.clone(),
tools: vec![tool.to_schema()],
effort: EffortLevel::Low,
};
let c = provider
.stream(&req, &mut |_| {})
.await
.expect("真实请求应成功");
if let Some(tc) = c.tool_calls.first() {
eprintln!(
"[e2e] tool_call round: {} {} {:?}",
tc.name, tc.id, tc.arguments
);
let args: serde_json::Value =
serde_json::from_str(&tc.arguments).unwrap_or(serde_json::json!({}));
let out = match tool
.run(
&ToolCtx {
project_root: ".".into(),
},
args,
)
.await
{
ToolOutput { ok: true, text } => text,
ToolOutput { ok: false, text } => text,
};
eprintln!("[e2e] tool result: {out:?}");
messages.push(assistant_msg(tc.clone()));
messages.push(Message::tool_result(&tc.id, out));
} else {
eprintln!("[e2e] final text: {}", c.text.trim());
assert!(
c.text.to_lowercase().contains("hi"),
"最终应复述 echo 返回的 hi,got: {:?}",
c.text
);
completed = true;
break;
}
}
assert!(completed, "工具闭环未在 4 轮内完成");
}
fn assistant_msg(tc: ToolCall) -> Message {
Message {
id: String::new(),
role: Role::Assistant,
content: vec![],
tool_calls: vec![tc],
tool_call_id: None,
}
}