























@@ -635,6 +635,34 @@ describe("buildAssistantMessage", () => {
635635expect(result.content).toEqual([{ type: "thinking", thinking: "Reasoning output" }]);
636636});
637637638+it("estimates usage when Ollama omits eval counters", () => {
639+const response = {
640+model: "qwen3:32b",
641+created_at: "2026-01-01T00:00:00Z",
642+message: { role: "assistant" as const, content: "Estimated output" },
643+done: true,
644+};
645+const result = buildAssistantMessage(response, modelInfo, { input: 11, output: 4 });
646+expect(result.usage.input).toBe(11);
647+expect(result.usage.output).toBe(4);
648+expect(result.usage.totalTokens).toBe(15);
649+});
650+651+it("preserves explicit zero usage counters from Ollama", () => {
652+const response = {
653+model: "qwen3:32b",
654+created_at: "2026-01-01T00:00:00Z",
655+message: { role: "assistant" as const, content: "" },
656+done: true,
657+prompt_eval_count: 0,
658+eval_count: 0,
659+};
660+const result = buildAssistantMessage(response, modelInfo, { input: 11, output: 4 });
661+expect(result.usage.input).toBe(0);
662+expect(result.usage.output).toBe(0);
663+expect(result.usage.totalTokens).toBe(0);
664+});
665+638666it("builds response with tool calls", () => {
639667const response = {
640668model: "qwen3:32b",
@@ -1052,6 +1080,65 @@ describe("createOllamaStreamFn streaming events", () => {
10521080);
10531081});
105410821083+it("estimates usage when the final Ollama chunk omits counters", async () => {
1084+await withMockNdjsonFetch(
1085+[
1086+'{"model":"m","created_at":"t","message":{"role":"assistant","content":"Estimated answer"},"done":false}',
1087+'{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true}',
1088+],
1089+async () => {
1090+const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1091+const events = await collectStreamEvents(stream);
1092+1093+const doneEvent = events.at(-1);
1094+expect(doneEvent?.type).toBe("done");
1095+if (doneEvent?.type === "done") {
1096+expect(doneEvent.message.usage.input).toBeGreaterThan(0);
1097+expect(doneEvent.message.usage.output).toBeGreaterThan(0);
1098+expect(doneEvent.message.usage.totalTokens).toBeGreaterThan(0);
1099+}
1100+},
1101+);
1102+});
1103+1104+it("counts image payloads in prompt usage estimates when Ollama omits counters", async () => {
1105+await withMockNdjsonFetch(
1106+[
1107+'{"model":"m","created_at":"t","message":{"role":"assistant","content":"vision answer"},"done":false}',
1108+'{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true}',
1109+],
1110+async () => {
1111+const streamFn = createOllamaStreamFn("http://ollama-host:11434");
1112+const stream = await Promise.resolve(
1113+streamFn(
1114+{
1115+id: "llava",
1116+api: "ollama",
1117+provider: "custom-ollama",
1118+contextWindow: 131072,
1119+} as never,
1120+{
1121+messages: [
1122+{
1123+role: "user",
1124+content: [{ type: "image", data: "a".repeat(400) }],
1125+},
1126+],
1127+} as never,
1128+{} as never,
1129+),
1130+);
1131+const events = await collectStreamEvents(stream);
1132+1133+const doneEvent = events.at(-1);
1134+expect(doneEvent?.type).toBe("done");
1135+if (doneEvent?.type === "done") {
1136+expect(doneEvent.message.usage.input).toBeGreaterThan(50);
1137+}
1138+},
1139+);
1140+});
1141+10551142it("emits text streaming events before done for mixed text + tool responses", async () => {
10561143await withMockNdjsonFetch(
10571144[
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。