/** * Your bill, read from the provider, without anybody exporting anything. * * Every command in this product reads a file somebody produced by hand, and * the export step is where adoption dies: the person who would benefit most * from a cost report is the person least likely to have a `usage.jsonl` lying * around. Every provider that bills by the token also serves that data over an * API, and this module turns those payloads into figures the rest of Trazum * already knows how to reason about. * * **Pure, and in the core, so it is testable without a network.** The fetch, * the credentials and the pagination live in the CLI — the same split * `openrouterOverlay` has had since 1.13. Everything here is a transformation * of a document the caller already holds. * * **The honest part is what the providers cannot tell you.** Usage APIs serve * *aggregates*: tokens per bucket per model, and — depending on the provider — * a request count or nothing at all. They do not serve per-call rows. That * makes a whole class of Trazum's findings impossible on this source: the * shape of the calls, the truncation retries, the conversations, the largest * call's context pressure. Those findings need per-call data and no amount of * arithmetic recovers them from a sum. * * So a connected report is a **restricted** report, and it is restricted out * loud. It carries its own shape rather than a `UsageProfileReport` with holes * in it, precisely so a per-call finding can never read a zero this module * wrote and report "nothing found" about something nobody measured. Not * recorded is not not-happened, at the level of the type system. */ import type { PricingCatalogue } from './pricing.js'; /** * `per-call` sources serve one row per request and unlock every finding in * the product. `bucketed` sources serve sums over a window. */ export type ConnectorGranularity = 'per-call' | 'bucketed'; /** * A finding this source cannot support, why, and what would unlock it. * * Carried into the report and printed there. A restricted report that only * omits things reads as a report that found nothing wrong. */ export interface UnavailableFinding { finding: string; because: string; unlockedBy: string; } export interface ConnectorDescriptor { id: string; displayName: string; granularity: ConnectorGranularity; /** * Environment variables the CLI reads the credential from, in order. * * Named here so `trazum connect` can say exactly what it looked for when it * finds nothing. Trazum stores no secret: a key lives in the environment or * in a keychain the operating system owns, and never in this repository's * config, cache or output. */ credentialEnv: readonly string[]; /** The narrowest key that works, so nobody hands this tool a wider one. */ keyKind: string; /** Whether the source serves a request count, or only token sums. */ servesCallCounts: boolean; /** Findings impossible on this source. */ unavailable: readonly UnavailableFinding[]; docs: string; } /** * The two providers this release connects to, and the asymmetry between them * that a report must not paper over. * * OpenAI's usage endpoint serves a request count per bucket; Anthropic's * serves token sums without one. So a connected OpenAI report can say "$412 * over 9,004 calls" and a connected Anthropic report can only say "$412", and * every per-call average is available on one and absent on the other. Printing * a call count of zero, or dividing by a denominator that does not exist, * would be this module inventing the number it is here to stop inventing. */ export declare const CONNECTORS: readonly ConnectorDescriptor[]; export declare function connectorFor(id: string): ConnectorDescriptor | null; /** * One provider bucket: a window, a model, and the tokens billed inside it. * * The cache-write TTL split is kept apart for the same reason `UsageBreakdown` * keeps it apart — the two are billed at different multipliers, and a total * that has lost the split cannot be repriced, only guessed at. */ export interface UsageBucket { fromMs: number; toMs: number; model: string; /** null when the provider serves no request count. Never zero for absent. */ calls: number | null; inputTokens: number; cacheReadTokens: number; cacheWrite5mTokens: number; cacheWrite1hTokens: number; /** False when the provider reported writes without saying which TTL. */ writeTtlKnown: boolean; outputTokens: number; /** Whatever the provider grouped by beyond the model — workspace, key, tier. */ group: Record; } /** * Something the pull did not get. * * A bill quietly short by an unknown amount is the failure this repository * refuses everywhere it can occur, and a paginated API behind a rate limit is * exactly where it occurs. Every gap is carried to the report and printed. */ export interface PullGap { kind: 'rate-limited' | 'retention-boundary' | 'cursor-expired' | 'page-limit' | 'unreadable-entry' | 'unreadable-field'; detail: string; } export interface ConnectorPull { provider: string; granularity: ConnectorGranularity; buckets: UsageBucket[]; /** The window the buckets actually cover, or null when none parsed. */ window: { fromMs: number; toMs: number; } | null; gaps: PullGap[]; unavailable: readonly UnavailableFinding[]; } /** * Anthropic's messages usage report. * * Shape: `{ data: [ { starting_at, ending_at, results: [ {...tokens, model} ] } ] }`. * The fields read are the documented ones; anything unreadable is reported as * a gap rather than defaulted to zero, because a zero here is a bill that is * quietly smaller than the real one. */ export declare function normalizeAnthropicUsage(payload: unknown): ConnectorPull; /** * OpenAI's completions usage endpoint. * * Shape: `{ data: [ { start_time, end_time, results: [ { input_tokens, * output_tokens, input_cached_tokens, num_model_requests, model } ] } ] }`. * * This one serves a request count, so every per-call average is available on * it — and the report says so, rather than making both providers look alike. */ export declare function normalizeOpenAIUsage(payload: unknown): ConnectorPull; export interface BucketedSlice { model: string; /** null when the source serves no request count. */ calls: number | null; inputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; outputTokens: number; inputUsd: number; cacheReadUsd: number; cacheWriteUsd: number; outputUsd: number; totalUsd: number; /** What the cache-touched tokens would have cost as ordinary input. */ cachedTokensAtInputRateUsd: number; /** Writes priced at the 1-hour rate, when the source did not state the TTL. */ cacheWriteUsdIfAssumed1h: number; writeTtlKnown: boolean; } export interface BucketedReport { schemaVersion: 1; provider: string; granularity: ConnectorGranularity; span: { fromMs: number; toMs: number; } | null; total: { totalUsd: number; /** null when unknown — never zero, which would read as "no traffic". */ calls: number | null; inputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; outputTokens: number; }; byModel: BucketedSlice[]; /** Spend per UTC day, oldest first — the shape a total hides. */ byDay: { day: string; usd: number; calls: number | null; }[]; /** Models the catalogue could not price: named, with their tokens kept. */ unpricedModels: { model: string; inputTokens: number; outputTokens: number; }[]; gaps: PullGap[]; unavailable: readonly UnavailableFinding[]; } /** * Prices the buckets a connector pulled. * * Every figure here is the provider's own billed token count at the * catalogue's rates — the same arithmetic `profile` does, over sums instead of * rows. What it deliberately does not do is synthesise the per-call findings: * they are listed as unavailable and left absent, so nothing downstream can * read a zero this function wrote. */ export declare function bucketedProfile(pull: ConnectorPull, options: { catalogue: PricingCatalogue; on?: Date; }): BucketedReport; /** * The cache verdict over a connected report. * * Same counterfactual `cacheEconomics` runs on a per-call report: what the * cache-touched tokens cost, against what they would have cost as ordinary * input. The worst case is carried separately for the same reason it is * there — when the source did not state the write TTL, the cheaper rate was * assumed for the headline and the verdict can move under the other one. */ export declare function bucketedCacheEconomics(report: BucketedReport): { spentUsd: number; withoutCachingUsd: number; deltaUsd: number; verdict: 'paid-off' | 'lost-money' | 'no-cache'; worstCaseVerdict: 'paid-off' | 'lost-money' | 'no-cache'; }; //# sourceMappingURL=connector.d.ts.map