Review of #158 (Request changes) — core logic verified correct; addressed the test-coverage + localization items: 1. i18n pluralization: the token-count keys were called with {count} but had one form, so ru-RU always rendered the genitive ("1 токенов"). Added _one/_other (en) and _one/_few/_many (ru: токен/токена/токенов) for both "Thinking… · {{count}} tokens" and "Thinking · {{count}} tokens"; de-duped the PR-added duplicate "Thinking" key. Call sites unchanged. 2. ReasoningBlock: new reasoning-block.test.tsx (4 branches: authoritative count wins / estimate fallback / header-only when count-but-no-text / body render). 3. Reasoning-token attribution: extracted the #151 anti-double-count rule into a pure `reasoningTokensForPart(message)` (single reasoning part -> authoritative turn total; multiple/none -> undefined so each estimates). message-item uses it; removed the now-dead lastReasoningIndex reduce (review #5). Unit-tested. 6. adopt-chat-id.ts: refreshed 3 stale `chatStreamStartMetadata` -> `chatStreamMetadata` comment references. 7. chat-markdown.test.ts: assert the export footer's `reasoning: N` line appears when reasoningTokens>0 and is absent at 0/undefined. Skipped optional #4 (mantine useThrottledCallback): the manual throttle has two distinct exit paths (turn-end revert-to-null + the captured-total trailing emit) with no guarding test; remapping risks the streaming behavior — non-blocking. Client tsc clean; ai-chat suite green (171 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
1.9 KiB
TypeScript
57 lines
1.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { UIMessage } from "@ai-sdk/react";
|
|
import { reasoningTokensForPart } from "@/features/ai-chat/utils/reasoning-tokens.ts";
|
|
|
|
/**
|
|
* Pure-helper tests for `reasoningTokensForPart`, the #151 anti-double-count
|
|
* rule: the authoritative `usage.reasoningTokens` is the TURN TOTAL, so it may
|
|
* only be attributed when the turn has exactly one reasoning part. With multiple
|
|
* reasoning parts (or no authoritative usage) every part falls back to its own
|
|
* per-part estimate, signalled here by `undefined`.
|
|
*/
|
|
const msg = (
|
|
parts: UIMessage["parts"],
|
|
metadata?: unknown,
|
|
): UIMessage =>
|
|
({
|
|
id: Math.random().toString(),
|
|
role: "assistant",
|
|
parts,
|
|
metadata,
|
|
}) as UIMessage;
|
|
|
|
describe("reasoningTokensForPart", () => {
|
|
it("single reasoning part -> the authoritative turn total", () => {
|
|
const m = msg(
|
|
[
|
|
{ type: "reasoning", text: "thinking…" } as never,
|
|
{ type: "text", text: "answer" },
|
|
],
|
|
{ usage: { reasoningTokens: 42 } },
|
|
);
|
|
expect(reasoningTokensForPart(m)).toBe(42);
|
|
});
|
|
|
|
it("multiple reasoning parts -> undefined (each estimates on its own)", () => {
|
|
const m = msg(
|
|
[
|
|
{ type: "reasoning", text: "step one" } as never,
|
|
{ type: "reasoning", text: "step two" } as never,
|
|
{ type: "text", text: "answer" },
|
|
],
|
|
{ usage: { reasoningTokens: 99 } },
|
|
);
|
|
// Even with an authoritative total, two reasoning parts must each estimate
|
|
// (attributing the total to one would double-count against the other).
|
|
expect(reasoningTokensForPart(m)).toBeUndefined();
|
|
});
|
|
|
|
it("no authoritative usage -> undefined even for a single reasoning part", () => {
|
|
const m = msg([
|
|
{ type: "reasoning", text: "thinking…" } as never,
|
|
{ type: "text", text: "answer" },
|
|
]);
|
|
expect(reasoningTokensForPart(m)).toBeUndefined();
|
|
});
|
|
});
|