Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions contracts/golden/intents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"comment": "parse 意图词汇单一来源(issue #98;含全角归一/清屏同义词/否定后置不抑制边界)。Rust parse()(crates/oriera-agent/src/lib.rs)与浏览器 kernel parseIntent()(examples/11-ai-cafe/engine/kernel.js)双端共读此文件做镜像断言——改任何一侧的解析行为(加意图、改否定语义、改参数提取)必须同步更新此文件,否则双端 CI 同红。expect.kind 词汇:spawn/clear/gravity/teleport/count/speed/unknown。",
"cases": [
{ "input": "spawn 40 particles", "expect": { "kind": "spawn", "n": 40 } },
{ "input": "生成 50 个粒子", "expect": { "kind": "spawn", "n": 50 } },
{ "input": "放 20 个", "expect": { "kind": "spawn", "n": 20 } },
{ "input": "生成3个", "expect": { "kind": "spawn", "n": 3 } },
{ "input": "clear the world", "expect": { "kind": "clear" } },
{ "input": "清空", "expect": { "kind": "clear" } },
{ "input": "清屏", "expect": { "kind": "clear" } },
{ "input": "清空,但别关灯", "expect": { "kind": "clear" } },
{ "input": "放5个", "expect": { "kind": "spawn", "n": 5 } },
{ "input": "生成10个", "expect": { "kind": "spawn", "n": 10 } },
{ "input": "set gravity to 1.6", "expect": { "kind": "gravity", "g": 1.6 } },
{ "input": "set gravity to 10", "expect": { "kind": "gravity", "g": 10 } },
{ "input": "gravity 9.8", "expect": { "kind": "gravity", "g": 9.8 } },
{ "input": "重力 9.8", "expect": { "kind": "gravity", "g": 9.8 } },
{ "input": "重力 9", "expect": { "kind": "gravity", "g": 9 } },
{ "input": "teleport 100 -25", "expect": { "kind": "teleport", "dx": 100, "dy": -25 } },
{ "input": "teleport to 100 200", "expect": { "kind": "teleport", "dx": 100, "dy": 200 } },
{ "input": "传送到 50,-25", "expect": { "kind": "teleport", "dx": 50, "dy": -25 } },
{ "input": "how many", "expect": { "kind": "count" } },
{ "input": "how many entities", "expect": { "kind": "count" } },
{ "input": "现在有多少个", "expect": { "kind": "count" } },
{ "input": "有多少个", "expect": { "kind": "count" } },
{ "input": "speed 2x", "expect": { "kind": "speed", "k": 2 } },
{ "input": "速度 1.5 倍", "expect": { "kind": "speed", "k": 1.5 } },
{ "input": "倍速 0.5", "expect": { "kind": "speed", "k": 0.5 } },
{ "input": "别清空", "expect": { "kind": "unknown", "text": "别清空" } },
{ "input": "不要生成新的", "expect": { "kind": "unknown", "text": "不要生成新的" } },
{ "input": "不要 spawn 5", "expect": { "kind": "unknown", "text": "不要 spawn 5" } },
{ "input": "do not spawn", "expect": { "kind": "unknown", "text": "do not spawn" } },
{ "input": "hello there", "expect": { "kind": "unknown", "text": "hello there" } },
{ "input": "给我讲个故事", "expect": { "kind": "unknown", "text": "给我讲个故事" } },
{ "input": "teleport to the moon", "expect": { "kind": "unknown", "text": "teleport to the moon" } }
]
}
50 changes: 50 additions & 0 deletions contracts/golden/presets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"comment": "L7 provider 预设表单一来源(issue #98)。Rust PROVIDER_PRESETS(crates/oriera-infer/src/provider.rs)与浏览器 kernel PROVIDER_PRESETS(examples/11-ai-cafe/engine/kernel.js)双端共读此文件做逐行对照——新增/修改/删除预设行必须同步更新此文件。caps 五字段恒显式(contextWindow 为 token 数或 null);price 为 null 或四字段对象(µ 货币 / 每百万 tokens);kind 词汇:rule/zhipu/openai-compat/anthropic。",
"rows": [
{ "id": "rule", "label": "离线规则引擎", "kind": "rule", "model": "rule-v1", "base": "", "local": true,
"caps": { "contextWindow": null, "structuredOutput": false, "tools": false, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "xlite", "label": "XLite 潜元网关", "kind": "openai-compat", "model": "gpt-4o-mini", "base": "https://xlite.dezai.com/v1", "local": false,
"caps": { "contextWindow": null, "structuredOutput": false, "tools": true, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "zhipu", "label": "智谱 GLM", "kind": "zhipu", "model": "glm-4-flash", "base": "https://open.bigmodel.cn/api/paas/v4", "local": false,
"caps": { "contextWindow": null, "structuredOutput": false, "tools": true, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "openai", "label": "OpenAI", "kind": "openai-compat", "model": "gpt-4o-mini", "base": "https://api.openai.com/v1", "local": false,
"caps": { "contextWindow": 128000, "structuredOutput": true, "tools": true, "imageInput": true, "promptCache": true },
"price": { "currency": "USD", "promptMicroPerM": 150000, "completionMicroPerM": 600000, "cachedMicroPerM": 75000 } },
{ "id": "qwen", "label": "Qwen 通义", "kind": "openai-compat", "model": "qwen-plus", "base": "https://dashscope.aliyuncs.com/compatible-mode/v1", "local": false,
"caps": { "contextWindow": null, "structuredOutput": false, "tools": false, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "stepfun", "label": "阶跃 Step", "kind": "openai-compat", "model": "step-3.7-flash", "base": "https://api.stepfun.com/v1", "local": false,
"caps": { "contextWindow": null, "structuredOutput": false, "tools": false, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "deepseek", "label": "DeepSeek", "kind": "openai-compat", "model": "deepseek-chat", "base": "https://api.deepseek.com/v1", "local": false,
"caps": { "contextWindow": 1000000, "structuredOutput": true, "tools": true, "imageInput": true, "promptCache": true },
"price": { "currency": "USD", "promptMicroPerM": 150000, "completionMicroPerM": 600000, "cachedMicroPerM": 3000 } },
{ "id": "moonshot", "label": "Moonshot Kimi", "kind": "openai-compat", "model": "moonshot-v1-8k", "base": "https://api.moonshot.cn/v1", "local": false,
"caps": { "contextWindow": null, "structuredOutput": false, "tools": false, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "groq", "label": "Groq", "kind": "openai-compat", "model": "llama-3.1-8b-instant", "base": "https://api.groq.com/openai/v1", "local": false,
"caps": { "contextWindow": null, "structuredOutput": false, "tools": false, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "openrouter", "label": "OpenRouter", "kind": "openai-compat", "model": "openrouter/auto", "base": "https://openrouter.ai/api/v1", "local": false,
"caps": { "contextWindow": null, "structuredOutput": false, "tools": false, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "anthropic", "label": "Anthropic", "kind": "anthropic", "model": "claude-3-5-haiku-latest", "base": "https://api.anthropic.com/v1", "local": false,
"caps": { "contextWindow": 200000, "structuredOutput": false, "tools": true, "imageInput": true, "promptCache": true },
"price": null },
{ "id": "ollama", "label": "Ollama 本地", "kind": "openai-compat", "model": "qwen3:8b", "base": "http://127.0.0.1:11434/v1", "local": true,
"caps": { "contextWindow": null, "structuredOutput": true, "tools": true, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "vllm", "label": "vLLM 本地", "kind": "openai-compat", "model": "Qwen/Qwen3-8B", "base": "http://127.0.0.1:8000/v1", "local": true,
"caps": { "contextWindow": null, "structuredOutput": true, "tools": true, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "llamacpp", "label": "llama.cpp 本地", "kind": "openai-compat", "model": "glm-4-9b-chat", "base": "http://127.0.0.1:8080/v1", "local": true,
"caps": { "contextWindow": null, "structuredOutput": true, "tools": false, "imageInput": false, "promptCache": false },
"price": null },
{ "id": "lmstudio", "label": "LM Studio 本地", "kind": "openai-compat", "model": "minicpm-v", "base": "http://127.0.0.1:1234/v1", "local": true,
"caps": { "contextWindow": null, "structuredOutput": true, "tools": false, "imageInput": false, "promptCache": false },
"price": null }
]
}
2 changes: 2 additions & 0 deletions crates/oriera-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ rigid = ["oriera-subsys/rigid"]

[dev-dependencies]
oriera-render = { path = "../oriera-render" }
# golden fixture tests (#98) read contracts/golden/*.json on every test leg
serde_json = { workspace = true }
oriera-subsys = { path = "../oriera-subsys" }

[[example]]
Expand Down
75 changes: 74 additions & 1 deletion crates/oriera-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,19 @@ use oriera_infer::Command;
/// the intent keyword suppresses execution ("别清空" is not Clear);
/// a negation after the keyword does not ("清空,但别关灯" still Clear).
pub fn parse(input: &str) -> Command {
let lower = input.to_lowercase();
// 全角数字归一(对镜 kernel v2.1:中文输入法「放5个」常见,评审 P1——
// 此前只有 JS 归一,全角输入双端分叉且 fixture 无法表达该差异)。
let lower: String = input
.to_lowercase()
.chars()
.map(|c| {
if ('\u{ff10}'..='\u{ff19}').contains(&c) {
char::from_u32(c as u32 - 0xfee0).unwrap_or(c)
} else {
c
}
})
.collect();
if negation_precedes_intent(&lower) {
return Command::Unknown(input.to_string());
}
Expand Down Expand Up @@ -211,4 +223,65 @@ mod tests {
assert_eq!(parse("清空,但别关灯"), Command::Clear);
assert_eq!(parse("spawn 5, never mind the rest"), Command::Spawn(5));
}

/// #98 golden fixture: the intent vocabulary has ONE source
/// (`contracts/golden/intents.json`), read by this test and by
/// `kernel.test.mjs` on the JS side. Changing either parser without
/// updating the fixture now breaks CI on both ends instead of
/// drifting silently.
#[test]
fn parse_matches_golden_fixture() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../contracts/golden/intents.json"
);
let fx: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(path).expect("read contracts/golden/intents.json"),
)
.expect("parse intents.json");
let cases = fx["cases"].as_array().expect("cases array");
assert!(!cases.is_empty(), "fixture must not be empty");
for case in cases {
let input = case["input"].as_str().expect("case.input");
let want = &case["expect"];
let kind = want["kind"].as_str().expect("expect.kind");
let got = parse(input);
match kind {
"spawn" => assert_eq!(
got,
Command::Spawn(want["n"].as_f64().expect("n").round() as usize),
"input: {input}"
),
"clear" => assert_eq!(got, Command::Clear, "input: {input}"),
"gravity" => assert_eq!(
got,
Command::SetGravity(want["g"].as_f64().expect("g") as f32),
"input: {input}"
),
"teleport" => assert_eq!(
got,
Command::Teleport(
want["dx"].as_f64().expect("dx") as f32,
want["dy"].as_f64().expect("dy") as f32
),
"input: {input}"
),
"count" => assert_eq!(got, Command::Count, "input: {input}"),
"speed" => assert_eq!(
got,
Command::SetSpeed(want["k"].as_f64().expect("k") as f32),
"input: {input}"
),
"unknown" => match got {
Command::Unknown(s) => assert_eq!(
s,
want["text"].as_str().expect("expect.text"),
"input: {input}"
),
other => panic!("input {input}: expected Unknown, got {other:?}"),
},
other => panic!("fixture has unknown kind: {other}"),
}
}
}
}
4 changes: 4 additions & 0 deletions crates/oriera-infer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ deepseek-recipe-core = { workspace = true, optional = true }
[features]
default = []
native-http = ["dep:ureq", "dep:serde_json", "dep:deepseek-recipe-core"]

[dev-dependencies]
# golden fixture test (#98) reads contracts/golden/presets.json on every test leg
serde_json = { workspace = true }
16 changes: 13 additions & 3 deletions crates/oriera-infer/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,10 @@ fn is_length_truncated_empty(raw: &str) -> bool {
let Ok(v) = serde_json::from_str::<Value>(raw) else {
return false;
};
if v.pointer("/choices/0/finish_reason").and_then(Value::as_str) != Some("length") {
if v.pointer("/choices/0/finish_reason")
.and_then(Value::as_str)
!= Some("length")
{
return false;
}
v.pointer("/choices/0/message/content")
Expand Down Expand Up @@ -1460,7 +1463,10 @@ mod tests {
);
assert_eq!(p.complete(&[ChatMessage::user("hi")]).unwrap(), "好了");
let reqs = mock.requests();
assert!(reqs[0].contains(r#""temperature":0.85"#), "first sends default");
assert!(
reqs[0].contains(r#""temperature":0.85"#),
"first sends default"
);
assert!(
reqs[1].contains(r#""temperature":1.0"#),
"heal round sends 1.0: {}",
Expand Down Expand Up @@ -1521,7 +1527,11 @@ mod tests {
Err(InferError::Status { code: 400, .. }) => {}
other => panic!("expected Status(400), got {other:?}"),
}
assert_eq!(mock.requests().len(), 1, "no heal round, no transport retry");
assert_eq!(
mock.requests().len(),
1,
"no heal round, no transport retry"
);
}

// ---- RFC-3 工具调用 + SSE 流式 ----
Expand Down
7 changes: 5 additions & 2 deletions crates/oriera-infer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,13 @@ pub struct CostMeter {
/// offline rule row and unknown labels read unpriced.
pub fn price_for_label(label: &str) -> Option<&'static ModelPrice> {
let (kind, model) = label.split_once(':')?;
// kind:model 标签可能撞行(xlite 网关转发 openai 同名模型):首个
// *有登记价* 的行胜出——转发网关未登记价时按源厂牌价记账是合理近似;
// 全部无价才返回 None(绝不猜价)。
PROVIDER_PRESETS
.iter()
.find(|p| p.kind.id() == kind && p.model == model)
.and_then(|p| p.price.as_ref())
.filter(|p| p.kind.id() == kind && p.model == model)
.find_map(|p| p.price.as_ref())
}

impl CostMeter {
Expand Down
Loading
Loading