ai-lab-bridge 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +79 -21
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -267,11 +267,40 @@ class Session {
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/** 归一化后缓存 + 回传 usage:各家 CLI 字段名不同(input_tokens/input/prompt_tokens...),统一为平台口径 */
|
|
270
271
|
emitUsage(usage) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
const num = (v) => (typeof v === "number" && isFinite(v) ? v : undefined);
|
|
273
|
+
let raw = usage && typeof usage === "object" ? usage : null;
|
|
274
|
+
if (!raw) return;
|
|
275
|
+
// gemini 家族(qwen/iflow 同源)stats 为嵌套结构:
|
|
276
|
+
// {models: {"<模型名>": {tokens: {prompt, candidates, total, cached, thoughts}}}}
|
|
277
|
+
if (raw.models && typeof raw.models === "object") {
|
|
278
|
+
const first = Object.values(raw.models).find((m) => m && m.tokens && typeof m.tokens === "object");
|
|
279
|
+
const tk = first && first.tokens;
|
|
280
|
+
if (tk) {
|
|
281
|
+
raw = {
|
|
282
|
+
...raw,
|
|
283
|
+
input: num(tk.prompt) ?? num(raw.input),
|
|
284
|
+
output: num(tk.candidates) ?? num(raw.output),
|
|
285
|
+
cache: { read: num(tk.cached) },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
274
288
|
}
|
|
289
|
+
// opencode 系: {input, output, cache:{read}} / claude: {input_tokens, output_tokens, cache_read_input_tokens}
|
|
290
|
+
// codex: {input_tokens, output_tokens, total_tokens} / gemini: {inputTokenCount, outputTokenCount, ...}
|
|
291
|
+
const input = num(raw.input_tokens) ?? num(raw.input) ?? num(raw.prompt_tokens) ?? num(raw.inputTokenCount);
|
|
292
|
+
const output =
|
|
293
|
+
num(raw.output_tokens) ?? num(raw.output) ?? num(raw.completion_tokens) ?? num(raw.outputTokenCount) ?? num(raw.candidatesTokenCount);
|
|
294
|
+
const cacheRead = num(raw.cache_read_input_tokens) ?? num(raw.cache && raw.cache.read) ?? num(raw.cachedContentTokenCount);
|
|
295
|
+
if (input === undefined && output === undefined && cacheRead === undefined) return;
|
|
296
|
+
const normalized = {
|
|
297
|
+
input_tokens: input ?? null,
|
|
298
|
+
output_tokens: output ?? null,
|
|
299
|
+
total_tokens: input !== undefined || output !== undefined ? (input ?? 0) + (output ?? 0) : null,
|
|
300
|
+
cache_read_input_tokens: cacheRead ?? null,
|
|
301
|
+
};
|
|
302
|
+
this.usage = normalized;
|
|
303
|
+
this.emitEvent("usage", { usage: normalized });
|
|
275
304
|
}
|
|
276
305
|
|
|
277
306
|
emitToolUse(summary) {
|
|
@@ -472,6 +501,9 @@ function handleOpencodeLine(session, obj) {
|
|
|
472
501
|
session.emitToolUse(part.tool || part.name || (state && state.title));
|
|
473
502
|
}
|
|
474
503
|
}
|
|
504
|
+
// usage 提取:opencode 系在 step_finish 的 part.tokens / 顶层 usage 携带本轮 token 统计
|
|
505
|
+
const usageRaw = (part && (part.tokens || part.usage)) || obj.tokens || obj.usage;
|
|
506
|
+
if (usageRaw && typeof usageRaw === "object") session.emitUsage(usageRaw);
|
|
475
507
|
if (obj.type === "error" || (obj.error && typeof obj.error === "object")) {
|
|
476
508
|
const err = obj.error && typeof obj.error === "object" ? obj.error : null;
|
|
477
509
|
// 提取可读错误消息(opencode 系: error.data.message / error.message;附带 name/ref 便于定位)
|
|
@@ -551,8 +583,10 @@ function handleCursorLine(session, obj) {
|
|
|
551
583
|
|
|
552
584
|
/**
|
|
553
585
|
* Gemini CLI 家族事件解析(gemini / qwen / iflow 的 `-p --output-format stream-json`,JSONL)。
|
|
554
|
-
*
|
|
555
|
-
*
|
|
586
|
+
* 事件格式两代并存,宽松归一化:
|
|
587
|
+
* - Gemini 原生:message(text/content) / tool_use / result(response, stats)
|
|
588
|
+
* - Claude 风格(qwen 0.24+ 同源仿制):assistant(message.content[]) / tool_call / result(result, usage)
|
|
589
|
+
* 终态以进程退出码兜底。
|
|
556
590
|
*/
|
|
557
591
|
function handleGeminiLine(session, obj) {
|
|
558
592
|
if (!obj || typeof obj !== "object") return;
|
|
@@ -562,28 +596,50 @@ function handleGeminiLine(session, obj) {
|
|
|
562
596
|
if (obj.type === "message" || obj.type === "assistant") {
|
|
563
597
|
const role = obj.role || (obj.message && obj.message.role);
|
|
564
598
|
if (role === "user") return;
|
|
599
|
+
// 文本提取顺序:obj.text → obj.content → obj.message.content[](Claude 风格嵌套)
|
|
565
600
|
let text = obj.text;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
601
|
+
let blocks = obj.content;
|
|
602
|
+
if (!blocks && obj.message && Array.isArray(obj.message.content)) {
|
|
603
|
+
blocks = obj.message.content;
|
|
604
|
+
}
|
|
605
|
+
if (!text && blocks) {
|
|
606
|
+
if (typeof blocks === "string") text = blocks;
|
|
607
|
+
else if (Array.isArray(blocks)) {
|
|
608
|
+
const parts = [];
|
|
609
|
+
for (const b of blocks) {
|
|
610
|
+
if (!b) continue;
|
|
611
|
+
if (b.type === "text" && b.text) {
|
|
612
|
+
session.endThink();
|
|
613
|
+
parts.push(b.text);
|
|
614
|
+
} else if (b.type === "thinking" && b.thinking) {
|
|
615
|
+
session.emitThink(b.thinking);
|
|
616
|
+
} else if (typeof b === "string") {
|
|
617
|
+
parts.push(b);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
text = parts.join("");
|
|
573
621
|
}
|
|
574
622
|
}
|
|
575
623
|
if (text) session.emitText(text);
|
|
576
624
|
} else if (obj.type === "tool_use" || obj.type === "tool_call") {
|
|
577
625
|
session.emitToolUse(obj.name || obj.tool_name || obj.tool || "tool");
|
|
578
626
|
} else if (obj.type === "result") {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
627
|
+
// usage:gemini 原生在 stats,qwen/claude 风格在 usage
|
|
628
|
+
session.emitUsage(obj.stats || obj.usage);
|
|
629
|
+
const failed = obj.error || obj.is_error || (obj.subtype && obj.subtype !== "success");
|
|
630
|
+
if (failed) {
|
|
631
|
+
const msg = typeof obj.error === "string"
|
|
632
|
+
? obj.error
|
|
633
|
+
: (obj.error && obj.error.message)
|
|
634
|
+
|| (typeof obj.result === "string" ? obj.result : undefined)
|
|
635
|
+
|| `执行异常: ${obj.subtype || "unknown"}`;
|
|
582
636
|
session.finish("failed", msg);
|
|
583
637
|
return;
|
|
584
638
|
}
|
|
585
|
-
|
|
586
|
-
|
|
639
|
+
// 终态文本兜底:gemini 原生在 response,qwen/claude 风格在 result
|
|
640
|
+
const finalText = (typeof obj.response === "string" && obj.response) || (typeof obj.result === "string" && obj.result);
|
|
641
|
+
if (finalText && !session.textParts.length) {
|
|
642
|
+
session.emitText(finalText);
|
|
587
643
|
}
|
|
588
644
|
session.finish("completed");
|
|
589
645
|
}
|
|
@@ -731,8 +787,10 @@ function startSession(message) {
|
|
|
731
787
|
|
|
732
788
|
let promptViaStdin = true;
|
|
733
789
|
let launch = null;
|
|
734
|
-
if (cli === "claude") {
|
|
735
|
-
|
|
790
|
+
if (cli === "claude" || cli === "qwen") {
|
|
791
|
+
// Qwen CLI 0.24+ 已重构为 Claude Code 同构架构(result 事件带 subtype/session_id/usage,
|
|
792
|
+
// 实测 0.24.6),stream-json 输出与 Claude Code 兼容,复用同一启动参数与解析器
|
|
793
|
+
argv = [cli, ["-p", "--output-format", "stream-json", "--verbose"]];
|
|
736
794
|
if (sessionRef) argv[1].push("--resume", sessionRef);
|
|
737
795
|
if (options.permission_mode) argv[1].push("--permission-mode", String(options.permission_mode));
|
|
738
796
|
if (options.model) argv[1].push("--model", String(options.model));
|
|
@@ -803,10 +861,10 @@ function startSession(message) {
|
|
|
803
861
|
session.child = child;
|
|
804
862
|
|
|
805
863
|
const opencodeLike = cli === "opencode" || cli === "codefree-o";
|
|
806
|
-
const handler = cli === "claude" ? handleClaudeLine
|
|
864
|
+
const handler = (cli === "claude" || cli === "qwen") ? handleClaudeLine
|
|
807
865
|
: cli === "codex" ? handleCodexLine
|
|
808
866
|
: cli === "cursor" ? handleCursorLine
|
|
809
|
-
: (cli === "gemini" || cli === "
|
|
867
|
+
: (cli === "gemini" || cli === "iflow") ? handleGeminiLine
|
|
810
868
|
: (session, obj, rawLine) => {
|
|
811
869
|
// OpenCode / codefree-o:JSON 事件宽松解析;非 JSON 行按纯文本兜底(不支持 --format json 的版本)。
|
|
812
870
|
if (obj) handleOpencodeLine(session, obj);
|