///|
/// 演示:把 `mizchi/llm` 的一轮对话跑在磁带上,全程零网络。
///
/// 这里手工搭了磁带(真实项目里它是 `mooncassette` 录下来的),重点看两件事:
/// 1. 下游代码用的是 `mizchi/llm` 原样的 `Provider` 接口,一行没改;
/// 2. 拿到的用量是 provider 上报值,不是按字符数猜出来的。
fn main {
let messages : Array[@llm.Message] = [
@llm.Message::user("用一句话解释什么是磁带"),
]
let request = @adapter.request_of_messages("openai", "gpt-4o-mini", messages)
let cassette = @core.Cassette::new("llm-demo", interactions=[
@core.Interaction::new(request, taped_response()),
])
let session = @recorder.Session::new(cassette, mode=@recorder.Mode::Playback)
let provider = @adapter.CassetteProvider::new(session, "gpt-4o-mini")
println("== 事件流 ==")
let handler = @llm.StreamHandler::{
on_event: fn(event) { println(" " + describe(event)) },
}
provider.stream(messages, [], handler)
let result = @llm.collect(provider, messages)
println("")
println("== 结果 ==")
println("文本:" + result.text)
println("结束原因:" + result.finish_reason.to_string())
println("")
println("== 用量:猜的 vs 报的 ==")
match provider.token_drift() {
Some((estimated, reported)) => {
println(" mizchi/llm 估算输入:" + estimated.to_string())
println(" provider 上报输入:" + reported.to_string())
let gap = reported - estimated
println(
" 差额:" +
gap.to_string() +
(if gap > 0 { "(估算偏低)" } else { "(估算偏高)" }),
)
}
None => println(" 这一轮没有用量记录")
}
}
///|
/// 一段 OpenAI 兼容端点的流式响应。最后那帧只带账目、`choices` 为空 ——
/// 账目永远比内容晚一步到,这是「精确用量」唯一麻烦的地方。
fn taped_response() -> @core.Response {
let frames : Array[@core.StreamFrame] = [
@core.StreamFrame::new(
"{\"choices\":[{\"delta\":{\"content\":\"磁带就是\"},\"index\":0}]}",
),
@core.StreamFrame::new(
"{\"choices\":[{\"delta\":{\"content\":\"把真实请求录下来,之后照着放。\"},\"index\":0}]}",
),
@core.StreamFrame::new(
"{\"choices\":[{\"delta\":{},\"index\":0,\"finish_reason\":\"stop\"}]}",
),
@core.StreamFrame::new(
"{\"choices\":[],\"usage\":{\"prompt_tokens\":31,\"completion_tokens\":18,\"total_tokens\":49}}",
),
]
@core.Response::ok(
Json::object(Map([])),
usage=@core.Usage::new(31, 18),
stream=frames,
)
}
///|
/// 把事件翻译成一行给人看的话。
fn describe(event : @llm.StreamEvent) -> String {
match event {
@llm.StreamEvent::MessageStart => "MessageStart"
@llm.StreamEvent::TextDelta(text) => "TextDelta " + text
@llm.StreamEvent::ToolCallStart(id~, name~) =>
"ToolCallStart " + id + " -> " + name
@llm.StreamEvent::ToolCallDelta(id~, input_delta~) =>
"ToolCallDelta " + id + " " + input_delta
@llm.StreamEvent::ToolCallEnd(id~, name~, input~) =>
"ToolCallEnd " +
id +
" -> " +
name +
" " +
@canon.to_canonical_string(input)
@llm.StreamEvent::MessageEnd(finish_reason~, usage~) => {
let usage_text = match usage {
Some(u) =>
"输入 " +
u.input_tokens.to_string() +
" / 输出 " +
u.output_tokens.to_string()
None => "无用量记录"
}
"MessageEnd " + finish_reason.to_string() + "," + usage_text
}
@llm.StreamEvent::Error(message) => "Error " + message
}
}