mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 05:16:50 +01:00
fix(hermes): settle direct API usage with complete wire receipts
Export image-owned qualification assets from cloud builds and cover Claude/OpenAI image, question and billing journeys. Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
8b5d075f0c
commit
e025ff2286
38 files changed
+1347
-120
No files matched your search
@@ -10,11 +10,16 @@ describe("complete Anthropic receipt pricing", () => {
|
||||
const priced = priceAnthropicReceipt(receipt);
|
||||
expect(priced.costUsdExact).toBe("0.155967600");
|
||||
expect(priced.costStatus).toBe("estimated");
|
||||
expect(priced.pricingProvenance).toMatchObject({ source: "rate_card", version: "anthropic-standard-2026-10-07" });
|
||||
expect(priced.pricingProvenance).toMatchObject({ source: "rate_card", version: "anthropic-standard-2026-10-09" });
|
||||
expect(priced.pricingProvenance?.evidence).toContain("one-hour upper rate");
|
||||
expect(priced.usage).toBe(receipt.usage);
|
||||
expect(receipt.costStatus).toBe("unpriced");
|
||||
});
|
||||
it.each([
|
||||
["claude-haiku-4-5-20251001", "0.077983800"], ["claude-sonnet-4-6", "0.233951400"],
|
||||
])("prices the exact %s API model while retaining the conservative cache-write assumption", (model, expected) => {
|
||||
expect(priceAnthropicReceipt({ ...receipt, model })).toMatchObject({ costUsdExact: expected, costStatus: "estimated" });
|
||||
});
|
||||
it.each([
|
||||
{ complete: false }, { billingType: "unknown" }, { billingType: "subscription_included" },
|
||||
{ provider: "unknown" }, { biller: "aws_bedrock" }, { model: "claude-sonnet-latest" },
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { centsToUsd, unitsToCents, usdToUnits } from "@paperclipai/shared";
|
||||
import type { AdapterUsageCheckpoint } from "@paperclipai/adapter-utils";
|
||||
|
||||
// Direct Anthropic API list prices, verified 2026-10-07:
|
||||
// Direct Anthropic API list prices, verified 2026-10-09:
|
||||
// https://platform.claude.com/docs/en/about-claude/pricing
|
||||
// Keep this bounded to the qualified model; aliases and other billers stay unknown.
|
||||
const RATES = ["2", "0.2", "4", "10"] as const;
|
||||
// Exact selected models only; other billers and unreviewed aliases stay unknown.
|
||||
const RATE_CARDS: Record<string, readonly [string, string, string, string]> = {
|
||||
"claude-sonnet-5": ["2", "0.2", "4", "10"],
|
||||
"claude-sonnet-4-6": ["3", "0.3", "6", "15"],
|
||||
"claude-haiku-4-5-20251001": ["1", "0.1", "2", "5"],
|
||||
};
|
||||
|
||||
/** A complete token receipt can receive an explicit estimate, never an invoice
|
||||
* claim. ACP aggregates cache writes without TTL; conservatively price them at
|
||||
@@ -13,19 +17,21 @@ export function priceAnthropicReceipt(receipt: AdapterUsageCheckpoint): AdapterU
|
||||
if (!receipt.complete || !receipt.usage || receipt.usageBasis !== "per_run"
|
||||
|| receipt.provider !== "anthropic" || receipt.biller !== "anthropic"
|
||||
|| !["api", "metered_api"].includes(receipt.billingType ?? "")
|
||||
|| receipt.model !== "claude-sonnet-5" || receipt.usageByModel?.length
|
||||
|| !Object.hasOwn(RATE_CARDS, receipt.model ?? "") || receipt.usageByModel?.length
|
||||
|| receipt.costUsd != null || receipt.costUsdExact != null || receipt.cacheAdjustedCostUsd != null
|
||||
|| (receipt.pricingContext?.serviceTier && !["standard", "default"].includes(receipt.pricingContext.serviceTier))) return receipt;
|
||||
const usage = receipt.usage;
|
||||
const rates = RATE_CARDS[receipt.model!]!;
|
||||
if (usage.cacheWriteTokens === undefined || usage.cachedInputTokens === undefined) return receipt;
|
||||
const counts = [usage.inputTokens - usage.cacheWriteTokens, usage.cachedInputTokens, usage.cacheWriteTokens, usage.outputTokens];
|
||||
if (counts.some(count => !Number.isSafeInteger(count) || count < 0)) return receipt;
|
||||
const numerator = counts.reduce((total, count, index) => total + BigInt(count) * usdToUnits(RATES[index]), 0n);
|
||||
const numerator = counts.reduce((total, count, index) => total + BigInt(count) * usdToUnits(rates[index]), 0n);
|
||||
return { ...receipt, costUsdExact: centsToUsd(unitsToCents((numerator + 500_000n) / 1_000_000n)), costStatus: "estimated",
|
||||
pricingProvenance: {
|
||||
source: "rate_card", version: "anthropic-standard-2026-10-07",
|
||||
source: "rate_card", version: "anthropic-standard-2026-10-09",
|
||||
evidence: "https://platform.claude.com/docs/en/about-claude/pricing; standard global API pricing assumed; cache-write TTL unavailable, one-hour upper rate used; not an invoice",
|
||||
inputCentsPerMillion: "200", cachedInputCentsPerMillion: "20", cacheWriteCentsPerMillion: "400", outputCentsPerMillion: "1000", serviceTier: "standard",
|
||||
inputCentsPerMillion: String(Number(rates[0]) * 100), cachedInputCentsPerMillion: String(Number(rates[1]) * 100),
|
||||
cacheWriteCentsPerMillion: String(Number(rates[2]) * 100), outputCentsPerMillion: String(Number(rates[3]) * 100), serviceTier: "standard",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -57,6 +57,8 @@ import {
|
||||
} from "../../vendor/paperclip-runner/index.js";
|
||||
import * as issueServiceModule from "../issues.js";
|
||||
import { NativePermissionDeclinedError } from "./native-permission-decline.js";
|
||||
import { priceAnthropicReceipt } from "../anthropic-pricing.js";
|
||||
import { priceCodexReceipt } from "../codex-pricing.js";
|
||||
import {
|
||||
createNativeHarnessBackupStamp,
|
||||
verifyNativeHarnessBackupStamp,
|
||||
@@ -1042,6 +1044,78 @@ describe("native provider usage normalization", () => {
|
||||
billing: { complete: false, amountUsd: 0.25 } });
|
||||
expect(nativeUsageCostUsd(snapshot.usage, hermes, "openrouter")).toBe(0.25);
|
||||
});
|
||||
describe("selected-account direct API wire usage", () => {
|
||||
const tokens = { inputTokens: 20, outputTokens: 5, cacheReadTokens: 3, cacheWriteTokens: 2 };
|
||||
const receipt = { schema: "paperclip.usage.tokens/v1", source: "provider_wire", biller: "anthropic", model: "claude-haiku-4-5-20251001", protocol: "messages",
|
||||
complete: true, requestCount: 2, reportedRequestCount: 2, tokens, pricingContext: { serviceTier: "standard", contextTier: "short" } };
|
||||
const directHermes = { ...hermes, model: receipt.model } as NativeExecutionInput["provider"];
|
||||
const event = (eventType: string, turnId: string, payload: object = {}, sourceSeq = 1) => ({
|
||||
eventType, turnId, payload, sourceSeq, sourceInstanceId: "wire-provider",
|
||||
}) as unknown as PrpEvent;
|
||||
const usageEvent = (turnId: string, tokenAccounting: unknown, runDelta: object = tokens, sourceSeq = 2) =>
|
||||
event("item.completed", turnId, { kind: "usage", usage: { runDelta, tokenAccounting } }, sourceSeq);
|
||||
it.each(["anthropic", "openai"] as const)("settles %s tokens across replay and queued turns with an explicit estimate", biller => {
|
||||
const model = biller === "anthropic" ? receipt.model : "gpt-6-luna";
|
||||
const selected = { ...directHermes, model } as NativeExecutionInput["provider"];
|
||||
const authority = { ...receipt, biller, model, protocol: biller === "anthropic" ? "messages" : "chat_completions" };
|
||||
const accounting = createNativeTurnAccounting(selected, biller);
|
||||
accounting.observe(event("turn.started", "first"));
|
||||
const firstUsage = usageEvent("first", authority);
|
||||
accounting.observe(firstUsage); accounting.observe(firstUsage);
|
||||
expect(accounting.snapshot().complete).toBe(false);
|
||||
accounting.observe(event("turn.completed", "first", {}, 3));
|
||||
accounting.observe(event("turn.started", "second", {}, 4));
|
||||
accounting.observe(usageEvent("second", authority, tokens, 5));
|
||||
accounting.observe(event("turn.completed", "second", {}, 6));
|
||||
const snapshot = accounting.snapshot();
|
||||
expect(snapshot).toMatchObject({ complete: true, settlementReady: true, turnId: "second",
|
||||
usage: { runDeltaComplete: true, accountingCostIncomplete: false, tokenAccounting: { complete: true, requestCount: 4, reportedRequestCount: 4,
|
||||
tokens: { inputTokens: 40, outputTokens: 10, cacheReadTokens: 6, cacheWriteTokens: 4 } } } });
|
||||
expect(nativeUsageCostUsd(snapshot.usage, selected, biller)).toBeUndefined();
|
||||
const priced = priceAnthropicReceipt(priceCodexReceipt({ complete: true, usageBasis: "per_run", provider: biller, biller, billingType: "metered_api", model,
|
||||
usage: normalizeNativeUsage(snapshot.usage), pricingContext: authority.pricingContext as { serviceTier: "standard"; contextTier: "short" } }));
|
||||
expect(priced.costStatus).toBe("estimated");
|
||||
expect(priced.pricingProvenance?.source).toBe("rate_card");
|
||||
expect(Number(priced.costUsdExact)).toBeGreaterThan(0);
|
||||
});
|
||||
it.each([
|
||||
undefined, { ...receipt, model: "other-model" }, { ...receipt, biller: "openai", protocol: "responses" },
|
||||
{ ...receipt, source: "agent_claim" }, { ...receipt, tokens: { ...tokens, outputTokens: 6 } },
|
||||
])("keeps unowned or legacy accepted-response counters incomplete (%j)", authority => {
|
||||
const accounting = createNativeTurnAccounting(directHermes, "anthropic");
|
||||
accounting.observe(event("turn.started", "turn"));
|
||||
accounting.observe(usageEvent("turn", authority));
|
||||
accounting.observe(event("turn.completed", "turn", {}, 3));
|
||||
expect(accounting.snapshot()).toMatchObject({ complete: false, settlementReady: false,
|
||||
usage: { runDeltaComplete: false, accountingCostIncomplete: true } });
|
||||
});
|
||||
it("settles owned interrupted work without pricing its known subset or a placeholder zero", () => {
|
||||
const accounting = createNativeTurnAccounting(directHermes, "anthropic");
|
||||
const partial = { ...receipt, complete: false, requestCount: 3, reportedRequestCount: 2 };
|
||||
accounting.observe(event("turn.started", "turn"));
|
||||
accounting.observe(usageEvent("turn", partial, {}));
|
||||
accounting.observe(event("turn.cancelled", "turn", {}, 3));
|
||||
expect(accounting.snapshot()).toMatchObject({ complete: false, settlementReady: true, turnId: "turn",
|
||||
usage: { runDeltaComplete: false, accountingCostIncomplete: true, tokenAccounting: { complete: false, tokens } } });
|
||||
expect(normalizeNativeUsage(accounting.snapshot().usage)).toBeUndefined();
|
||||
});
|
||||
it("refuses to certify an aggregate that switches protocol or overflows safe counters", () => {
|
||||
for (const overflow of [false, true]) {
|
||||
const model = "gpt-6-luna", selected = { ...hermes, model } as NativeExecutionInput["provider"];
|
||||
const accounting = createNativeTurnAccounting(selected, "openai");
|
||||
const counts = overflow ? { inputTokens: Number.MAX_SAFE_INTEGER, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 } : tokens;
|
||||
const authority = { ...receipt, biller: "openai", model, protocol: "chat_completions", tokens: counts };
|
||||
accounting.observe(event("turn.started", "first"));
|
||||
accounting.observe(usageEvent("first", authority, counts));
|
||||
accounting.observe(event("turn.completed", "first", {}, 3));
|
||||
accounting.observe(event("turn.started", "second", {}, 4));
|
||||
accounting.observe(usageEvent("second", { ...authority, protocol: overflow ? "chat_completions" : "responses" }, counts, 5));
|
||||
accounting.observe(event("turn.completed", "second", {}, 6));
|
||||
expect(accounting.snapshot()).toMatchObject({ complete: false, usage: { runDeltaComplete: false } });
|
||||
expect(accounting.snapshot().usage).not.toHaveProperty("tokenAccounting");
|
||||
}
|
||||
});
|
||||
});
|
||||
it.each([
|
||||
{}, { inputTokens: 1 }, { outputTokens: 1 },
|
||||
{ inputTokens: 0, outputTokens: -1 }, { inputTokens: 0.5, outputTokens: 1 },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { dotRunnerBroker } from "../dot-runner-broker.js";
|
||||
import { configuredEnvironment } from "../../vendor/paperclip-runner/index.js";
|
||||
import { CURSOR_DISTRIBUTION_PINS, QUALIFIED_ACPX_PROFILES, QUALIFIED_ACPX_VERSION } from "../../vendor/paperclip-runner/index.js";
|
||||
import { isProviderMode } from "../../vendor/paperclip-runner/index.js";
|
||||
import { readProviderUsageBilling } from "../../vendor/paperclip-runner/index.js";
|
||||
import { readProviderUsageBilling, readProviderTokenAccounting, type ProviderTokenAccounting } from "../../vendor/paperclip-runner/index.js";
|
||||
import { bundledRemoteProviderPackManifestPath, bundledRemoteRunnerBinary } from "../../vendor/paperclip-runner/index.js";
|
||||
import { nativeRetryCancellationEligible, rethrowNativeCancellationLockConflict, assertCancellationRequest, cancellationIntentId as callerCancellationIntentId, cancellationRequestId } from "./native-cancellation-request.js";
|
||||
import { readNativePlanWait } from "./native-plan-wait.js";
|
||||
@@ -7958,6 +7958,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
const costUsdExact = typeof usage?.accountingCostUsdExact === "string" ? usage.accountingCostUsdExact : undefined;
|
||||
await input.onUsage?.({ usage: accountingUsage, ...billing, complete, settlement, usageBasis: "per_run", costUsdExact,
|
||||
model: input.execution.provider.model ?? "unknown", costUsd: accountingCost ?? null, costStatus,
|
||||
...(readProviderTokenAccounting(usage?.tokenAccounting)?.complete ? { pricingContext: readProviderTokenAccounting(usage?.tokenAccounting)!.pricingContext } : {}),
|
||||
...nativeHermesPriceEvidence(usage, input.execution.provider, selectedBiller) });
|
||||
if (!input.onUsage) await input.db.update(heartbeatRuns).set({
|
||||
costAccountingPending: true,
|
||||
@@ -9487,6 +9488,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
? input.execution.provider.kind === "aws_agentcore" ? "estimated" : nativeHermesPriceEvidence(native.usage, input.execution.provider, selectedBiller) ? "reported" : undefined
|
||||
: "unpriced",
|
||||
usageComplete: nativeAccountingComplete,
|
||||
...(readProviderTokenAccounting(native.usage?.tokenAccounting)?.complete ? { pricingContext: readProviderTokenAccounting(native.usage?.tokenAccounting)!.pricingContext } : {}),
|
||||
...(nativeSettlementReady ? { settlement: { schema: "paperclip.accounting.settlement/v1" as const, providerWorkEnded: true as const, usageComplete: nativeAccountingComplete } } : {}),
|
||||
...nativeHermesPriceEvidence(native.usage, input.execution.provider, selectedBiller),
|
||||
usageBasis: "per_run",
|
||||
@@ -9570,6 +9572,10 @@ export function createNativeTurnAccounting(provider: NativeExecutionInput["provi
|
||||
let requestCount = 0, reportedRequestCount = 0;
|
||||
let tokensComplete = true;
|
||||
let settlementReady = hermes;
|
||||
let directAccountingReady = hermes && ["anthropic", "openai"].includes(selectedBiller ?? "");
|
||||
let directAccounting: ProviderTokenAccounting | null = null;
|
||||
let directRequests = 0, directReported = 0;
|
||||
const directTokens = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
||||
for (const [turnId, turn] of turns) {
|
||||
const normalized = normalizeNativeUsage(turn.usage);
|
||||
tokensComplete &&= normalized !== undefined;
|
||||
@@ -9581,9 +9587,22 @@ export function createNativeTurnAccounting(provider: NativeExecutionInput["provi
|
||||
const price = nativeUsageCostUsd(turn.usage, provider, selectedBiller);
|
||||
const billing = provider.kind === "acpx" && provider.agent === "hermes"
|
||||
? readProviderUsageBilling(turn.usage?.billing) : null;
|
||||
settlementReady &&= turnId !== "unattributed" && turn.terminal && billing !== null
|
||||
&& selectedBiller === billing.biller && turn.usage !== null && nativeUsageMeasurement(turn.usage).providerCostUsd === billing.amountUsd;
|
||||
if (price === undefined) costIncomplete = true;
|
||||
const direct = nativeHermesTokenAccounting(turn.usage, provider, selectedBiller);
|
||||
const compatibleDirect = direct !== null && (!directAccounting || directAccounting.protocol === direct.protocol);
|
||||
directAccountingReady &&= compatibleDirect;
|
||||
if (direct) {
|
||||
if (!compatibleDirect) { tokensComplete = false; complete = false; }
|
||||
directAccounting = direct;
|
||||
directRequests += direct.requestCount; directReported += direct.reportedRequestCount;
|
||||
for (const key of Object.keys(directTokens) as (keyof typeof directTokens)[]) directTokens[key] += direct.tokens[key];
|
||||
tokensComplete &&= direct.complete;
|
||||
complete &&= direct.complete;
|
||||
} else if (hermes && ["anthropic", "openai"].includes(selectedBiller ?? "")) {
|
||||
tokensComplete = false; complete = false;
|
||||
}
|
||||
settlementReady &&= turnId !== "unattributed" && turn.terminal && ((billing !== null
|
||||
&& selectedBiller === billing.biller && turn.usage !== null && nativeUsageMeasurement(turn.usage).providerCostUsd === billing.amountUsd) || direct !== null);
|
||||
if (price === undefined) costIncomplete ||= !direct?.complete;
|
||||
else {
|
||||
cost += usdToUnits(billing?.amountUsdExact ?? price); costKnown = true;
|
||||
if (billing) {
|
||||
@@ -9593,6 +9612,12 @@ export function createNativeTurnAccounting(provider: NativeExecutionInput["provi
|
||||
}
|
||||
}
|
||||
}
|
||||
const aggregateSafe = [inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens,
|
||||
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
||||
directRequests, directReported, ...Object.values(directTokens),
|
||||
Object.values(directTokens).reduce((sum, value) => sum + value, 0)]
|
||||
.every(value => Number.isSafeInteger(value) && value >= 0);
|
||||
if (!aggregateSafe) { tokensComplete = false; complete = false; directAccountingReady = false; }
|
||||
// A free empty attempt contributes no price evidence for later paid work.
|
||||
// Keep an incomplete zero subtotal absent so complete aggregate tokens can
|
||||
// still receive an estimate. Preserve positive known spend and true zero totals.
|
||||
@@ -9602,13 +9627,17 @@ export function createNativeTurnAccounting(provider: NativeExecutionInput["provi
|
||||
const current = currentTurn ? turns.get(currentTurn) : undefined;
|
||||
const costExact = `${cost / 1_000_000_000n}.${String(cost % 1_000_000_000n).padStart(9, "0")}`;
|
||||
return { usage: { runDelta, runDeltaComplete: hermes ? tokensComplete : true, accountingCostIncomplete: costIncomplete,
|
||||
...(directAccountingReady && directAccounting && Number.isSafeInteger(directRequests) && Number.isSafeInteger(directReported)
|
||||
? { tokenAccounting: { ...directAccounting, complete: tokensComplete, requestCount: directRequests, reportedRequestCount: directReported,
|
||||
tokens: directTokens } } : {}),
|
||||
...(retainCost && provider.kind === "acpx" && provider.agent === "hermes" ? { billing: {
|
||||
schema: "paperclip.usage.billing/v1", source: "provider_reported", biller: "openrouter", currency: "USD",
|
||||
complete: !costIncomplete, requestCount, reportedRequestCount,
|
||||
amountUsd: Number(costExact), amountUsdExact: costExact,
|
||||
} } : {}),
|
||||
...(retainCost ? { accountingCostUsdExact: costExact } : {}) },
|
||||
turnId: current && (normalizeNativeUsage(current.usage) || (settlementReady && readProviderUsageBilling(current.usage?.billing))) ? currentTurn : undefined,
|
||||
turnId: current && (normalizeNativeUsage(current.usage) || (settlementReady
|
||||
&& (readProviderUsageBilling(current.usage?.billing) || nativeHermesTokenAccounting(current.usage, provider, selectedBiller)))) ? currentTurn : undefined,
|
||||
complete, settlementReady };
|
||||
};
|
||||
return {
|
||||
@@ -9634,6 +9663,7 @@ export function createNativeTurnAccounting(provider: NativeExecutionInput["provi
|
||||
const usage = record(payload.usage);
|
||||
turn.usage = Object.hasOwn(usage, "runDelta")
|
||||
? { runDelta: record(usage.runDelta), runDeltaComplete: usage.runDeltaComplete,
|
||||
...(usage.tokenAccounting === undefined ? {} : { tokenAccounting: usage.tokenAccounting }),
|
||||
...(usage.billing === undefined ? {} : { billing: usage.billing }) } : null;
|
||||
}
|
||||
if (terminal) turn.terminal = true;
|
||||
@@ -9649,6 +9679,18 @@ export function createNativeTurnAccounting(provider: NativeExecutionInput["provi
|
||||
};
|
||||
}
|
||||
|
||||
/** Only this versioned, route/model-bound receipt certifies all direct API attempts. */
|
||||
function nativeHermesTokenAccounting(usage: Record<string, unknown> | null, provider: NativeExecutionInput["provider"], selectedBiller?: string | null) {
|
||||
if (provider.kind !== "acpx" || provider.agent !== "hermes" || !usage || usage.billing !== undefined) return null;
|
||||
const receipt = readProviderTokenAccounting(usage.tokenAccounting);
|
||||
if (!receipt || receipt.biller !== selectedBiller || receipt.model !== provider.model) return null;
|
||||
// Incomplete receipts preserve settlement authority, but never certify a
|
||||
// placeholder breakdown as complete tokens or a zero-cost estimate.
|
||||
if (!receipt.complete) return receipt;
|
||||
const measurement = nativeUsageMeasurement(usage);
|
||||
return Object.entries(receipt.tokens).every(([key, value]) => measurement[key] === value) ? receipt : null;
|
||||
}
|
||||
|
||||
export function nativeUsageCostUsd(
|
||||
usage: Record<string, unknown> | null,
|
||||
provider?: NativeExecutionInput["provider"],
|
||||
|
||||
Reference in new issue
Block a user