fix(combo): keep nested combos as single units and stop 0-token detail rows

Nested combos (comboA lists comboB, comboC, …) now stay one slot each:
the inner combo always runs as fallback to produce a single answer.
Failed hops are no longer written to Details/usage, and streaming no
longer inserts a 0-token placeholder row.

- chat.js: comboStack cycle detection; nested combos forced to fallback;
  persistUsage="success-only" for combo hops
- combo.js: discardResponse() cancels unused bodies (fusion timeout /
  fallback) so dropped streams fire onStreamComplete; getComboModelsFromData
  keeps nested names and honors enabled=false
- requestDetail.js: tokensForDetail() canonicalizes Claude/Gemini usage;
  shouldPersistRequestDetail() skips streaming-start and non-success hops
- streamingHandler.js: drop the 0-token streaming placeholder write
- RequestDetailsTab.js: read Gemini/Claude token names; show "streaming"
  status in amber
- tests: add combo-nested.test.js (13 cases)
- gitignore: ignore local .vitest/ artifacts

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
2026-09-17 14:03:59 +07:00
parent a84ba559f3
commit 81a47f36c6
11 changed files with 332 additions and 144 deletions

View File

@@ -110,6 +110,29 @@ export function formatDoneLine({ usage, latency }) {
return `DONE ${latency?.total ?? 0}ms${ttftStr} · ${inStr} · OUT ${outTok}`;
}
// Request-details storage convention: always prompt_tokens / completion_tokens.
// Translators often hand Claude `{input_tokens, output_tokens}` (or Gemini
// counts) to onStreamComplete; the Details tab only reads the OpenAI names,
// so an uncanonicalized object shows up as input=0 / output=0.
export function tokensForDetail(usage) {
if (!usage || typeof usage !== "object") {
return { prompt_tokens: 0, completion_tokens: 0 };
}
return canonicalizeUsage(usage) || {
prompt_tokens: usage.prompt_tokens ?? usage.input_tokens ?? 0,
completion_tokens: usage.completion_tokens ?? usage.output_tokens ?? 0,
};
}
// Combo fallback/account hops must not inflate Details with 0-token rows.
// `streaming-start` is never persisted: the placeholder was status=success at
// tokens=0, and nested/fusion paths often abandon the stream before complete.
export function shouldPersistRequestDetail(persistUsage, kind) {
if (kind === "streaming-start") return false;
if (persistUsage === "success-only") return kind === "success";
return true;
}
export function saveUsageStats({ provider, model, tokens, connectionId, apiKey, endpoint, label = "USAGE", silent = false }) {
if (!tokens || typeof tokens !== "object") return;