fix: show native Claude CLI usage as unpriced

This commit is contained in:
amosblomqvist
2026-10-28 19:58:07 +02:00
parent 5093809ea0
commit 17c94be327
11 changed files with 64 additions and 13 deletions
+42
View File
@@ -0,0 +1,42 @@
import React from "react";
(globalThis as Record<string, unknown>).React = React;
import assert from "node:assert/strict";
import { test } from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { CLAUDE_CODE_MODELS } from "../src/agent/claude-code/provider.ts";
import { isSubscriptionModel, sumSteps, type TurnUsage } from "../src/core/index.ts";
import { graphCostSummary } from "../src/core/cost-summary.ts";
import { accumulateJobUsage } from "../src/core/contracts/jobs.ts";
import { callCost, jobCost, sessionCost } from "../src/server/pricing.ts";
import { toModelDto } from "../src/server/model-catalog.ts";
import { ModelMenuList } from "../components/settings/model-menu.tsx";
import { StatsBar } from "../components/stats-bar.tsx";
const step = { inputTokens: 1000, outputTokens: 100, cacheReadTokens: 50, cacheWriteTokens: 0, reasoningTokens: undefined };
for (const model of CLAUDE_CODE_MODELS) test(`${model.id}: token usage is retained but dollars are unpriced, not free or subscription use`, () => {
const id = `${model.provider}:${model.id}`;
const turn: TurnUsage = { id: "cli-turn", messageId: "cli-message", at: "2026-10-28T00:00:00Z", model: id, steps: [step] };
assert(!isSubscriptionModel(id));
assert.equal(callCost(step, id), null);
const pricing = sessionCost({ turns: [turn] });
assert.equal(pricing.unpriced, 1); assert.equal(pricing.subscription, undefined);
assert.equal(pricing.turnCosts[turn.id], null);
assert.deepEqual(sumSteps(turn.steps), { ...step, cacheWrite1hTokens: undefined, reasoningTokens: undefined });
assert.equal(jobCost({ usage: accumulateJobUsage(undefined, id, step) }), null);
const html = renderToStaticMarkup(<StatsBar turns={[turn]} pricing={pricing} />);
assert(!html.includes("$0"));
const summary = graphCostSummary({ chats: [{ title: "CLI fixture", extractions: [{ startedAt: 1, at: turn.at, durationMs: 1,
stages: { extractor: { model: id, steps: [step] }, auditor: { model: id, steps: [step] } },
}] }], sources: [] }, callCost);
assert.equal(summary.unpricedRuns, 1); assert.equal(summary.subscriptionRuns, undefined);
assert.equal(summary.runs[0]!.dollars, null);
});
test("model-catalog API and menus never advertise the CLI adapter's placeholder zero rates", () => {
const models = CLAUDE_CODE_MODELS.map(toModelDto);
assert(models.every((model) => model.cost === null));
const html = renderToStaticMarkup(<ModelMenuList models={models} effectiveId={undefined} onPick={() => {}} />);
assert.equal((html.match(/>CLI billing<\/span>/g) ?? []).length, 3);
assert.match(html, /does not estimate it/);
assert(!html.includes("$0")); assert(!html.includes("USD per 1M")); assert(!html.includes("(subscription)"));
});
+3 -1
View File
@@ -64,7 +64,8 @@ describe("the model catalog projection", () => {
assert.equal(typeof m.id, "string");
assert.equal(typeof m.name, "string");
assert.ok(Array.isArray(m.supportedLevels));
assert.ok(typeof m.cost.input === "number" && typeof m.cost.output === "number");
if (m.provider === "claude-code") assert.equal(m.cost, null);
else assert.ok(m.cost && typeof m.cost.input === "number" && typeof m.cost.output === "number");
assert.ok(m.contextWindow > 0);
}
const glm = models.find((m) => m.id === "z-ai/glm-5.3-flash");
@@ -126,6 +127,7 @@ describe("the model catalog projection", () => {
assert.equal(dto.id, raw.id);
assert.equal(dto.name, raw.name);
assert.deepEqual(dto.supportedLevels, getSupportedThinkingLevels(raw));
assert.ok(dto.cost);
assert.equal(dto.cost.input, raw.cost.input);
assert.equal(dto.contextWindow, raw.contextWindow);
});
+2 -2
View File
@@ -10,7 +10,7 @@ const models = piModels().getProviders().flatMap((p) => piModels().getModels(p.i
const nativeUsage = (input: number): Usage => ({ input, output: 1_000, cacheRead: 10_000, cacheWrite: 2_000, cacheWrite1h: 500, totalTokens: input + 13_000, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } });
const saved = (u: Usage) => ({ inputTokens: u.input, outputTokens: u.output, cacheReadTokens: u.cacheRead, cacheWriteTokens: u.cacheWrite, cacheWrite1hTokens: u.cacheWrite1h, reasoningTokens: 500 });
test("all catalog models price through Pi, including every tier boundary; saved usage is never mutated", () => {
test("API catalog models price through Pi at every tier boundary; native CLI stays unpriced and saved usage is never mutated", () => {
assert.ok(models.length > 100);
for (const model of models) {
const sizes = [1_000, ...(model.cost.tiers ?? []).flatMap((t) => [Math.max(0, t.inputTokensAbove - 12_000), Math.max(0, t.inputTokensAbove - 12_000 + 1)])];
@@ -18,7 +18,7 @@ test("all catalog models price through Pi, including every tier boundary; saved
const usage = nativeUsage(input);
const call = Object.freeze(saved(usage));
const before = structuredClone(call);
const expected = calculateCost(model, usage).total;
const expected = model.provider === "claude-code" ? null : calculateCost(model, usage).total;
assert.equal(callCost(call, `${model.provider}:${model.id}`), expected, `${model.provider}:${model.id} input=${input}`);
assert.deepEqual(call, before);
}