Files
maka/packages/runtime/src/provider-request-telemetry.ts
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

918 lines
33 KiB
TypeScript

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
MODEL_CALL_ATTEMPT_SCHEMA_VERSION,
type HistoryCompactRoute,
type ModelCallAttempt,
type ModelCallKind,
type ModelCallUsageBasis,
type PreparedRequestObservation,
} from '@maka/core/model-call-attempt';
import type { PricingConfig } from '@maka/core/usage-stats/types';
import { prepareRequestObservation, type PreparedRequestMaterial } from './request-shape.js';
import { rawFinishReasonString } from './model-protocol.js';
import {
providerFailureDiagnostic,
type ProviderFailureDiagnostic,
} from './provider-error-classification.js';
import { latestContextProjectionInput } from './latest-context-snapshot.js';
import type { ContextDiagnosticsCompaction } from './context-diagnostics.js';
import type { ModelCallCommit } from '@maka/core/agent-run';
export type ProviderRequestCacheValueSource = 'provider' | 'derived';
export interface ProviderRequestUsage {
inputTokens?: number;
cacheReadInputTokens?: number;
cacheReadInputSource?: ProviderRequestCacheValueSource;
cacheMissInputTokens?: number;
cacheMissInputSource?: ProviderRequestCacheValueSource;
cacheWriteInputTokens?: number;
cacheWriteInputSource?: ProviderRequestCacheValueSource;
outputTokens?: number;
reasoningTokens?: number;
}
export interface ProviderRequestUsageLike {
inputTokens?:
| number
| {
total?: number;
noCache?: number;
cacheRead?: number;
cacheWrite?: number;
};
outputTokens?: number | { total?: number; text?: number; reasoning?: number };
raw?: Record<string, unknown>;
}
export type ProviderRequestAttemptStatus = 'completed' | 'failed' | 'interrupted' | 'aborted';
export interface PreparedRequestArtifactInput extends PreparedRequestMaterial {
traceId: string;
captureId: string;
turnId: string;
step: number;
providerId: string;
modelId: string;
}
export interface PreparedRequestArtifactRef {
artifactId: string;
}
interface SettledProviderAttempt extends ProviderRequestUsage {
traceId: string;
attemptId: string;
turnId: string;
step: number;
attempt: number;
captureArtifactId?: string;
providerId: string;
modelId: string;
contextWindow?: number;
startedAt: number;
completedAt: number;
status: ProviderRequestAttemptStatus;
finishReason?: string;
failure?: ProviderFailureDiagnostic;
latencyMs: number;
timeToFirstTokenMs?: number;
}
/**
* Cost resolved at the moment a call settles, plus the basis it was resolved
* against. Recording the basis alongside the amount is what makes a stored
* figure auditable later, when rates may have changed.
*/
export interface ResolvedModelCallCost {
costUsd?: number;
pricingRevision?: number;
pricingRates?: PricingConfig;
}
export interface ProviderRequestTrackerInput {
traceId: string;
turnId: string;
contextWindow?: number;
now: () => number;
newId: () => string;
/**
* Optional private artifact sink. Failure leaves the canonical observation
* intact and the attempt explicitly has no artifact join.
*/
persistArtifact?: (input: PreparedRequestArtifactInput) => Promise<PreparedRequestArtifactRef>;
/**
* Durable run metadata that must exist before any physical provider call.
* Kept outside accounting because a dispatch gate is an execution contract,
* not a metering concern.
*/
beforeDispatch?: () => void | Promise<void>;
/**
* Canonical metering. Present as a unit or not at all: a `ModelCallAttempt`
* without session, run, and kind is unattributable, so identity and sink are
* wired together rather than as independently optional fields. Absent leaves
* the tracker purely diagnostic, which is what the capture-only tests use.
*/
accounting?: ModelCallAccountingInput;
}
export interface ModelCallAccountingInput {
sessionId: string;
/** Resolved per send: one tracker does not outlive a single run. */
resolveRunId: () => string | undefined;
/**
* The connection the request was dispatched over. Without it a record is
* attributable to a provider and model but not to the configured connection,
* which is what the Usage surface filters and groups by.
*/
connectionSlug?: string;
/**
* The connection's provider type, which is the vocabulary the Usage surface
* groups by. The diagnostic record carries the SDK's own provider id instead
* (`moonshot.chat` where this says `moonshot`); metering must not split one
* provider across two bucket keys, so accounting uses this when given.
*/
providerId?: string;
callKind: ModelCallKind;
historyCompactRoute?: ModelCallAttempt['historyCompactRoute'];
/**
* Commits the attempt, and with it the derived latest-context row when this
* request is one that answers "what is the context made of" (#2323). One
* call, so the two cannot fail or arrive independently.
*/
record: (commit: ModelCallCommit<ModelCallAttempt>) => void | Promise<void>;
/** Resolves cost at settlement time; absent means the price is unknown. */
resolveCost?: (usage: ProviderRequestUsage) => ResolvedModelCallCost | undefined;
/**
* Pre-dispatch accounting gate. Throws when the canonical record could not be
* written for this dispatch. Checked before the provider is called and never
* from settlement — a rejection inside `finalize` would surface through the
* stream's `pull` handler and error an otherwise-complete model response.
*/
assertReady?: () => void;
}
export interface TrackProviderStreamInput {
providerId: string;
modelId: string;
params: Record<string, unknown>;
abortSignal?: AbortSignal;
doStream: () => PromiseLike<ProviderStreamResult>;
/**
* The compaction boundary THIS request's prompt was built from (#2323).
*
* Per request rather than per tracker: one tracker spans every physical
* request of a send, and mid-turn compaction or overflow recovery can
* prepare the next request against a different boundary. Read at settlement
* it would be whatever the session holds by then — a boundary this prompt
* may never have seen.
*/
historyCompactBoundary?: ContextDiagnosticsCompaction;
/** Physical history-compaction route used by this provider request. */
historyCompactRoute?: HistoryCompactRoute;
}
export interface TrackProviderGenerateInput {
providerId: string;
modelId: string;
/** As `TrackProviderStreamInput.historyCompactBoundary`. */
historyCompactBoundary?: ContextDiagnosticsCompaction;
/** Physical history-compaction route used by this provider request. */
historyCompactRoute?: HistoryCompactRoute;
params: Record<string, unknown>;
abortSignal?: AbortSignal;
doGenerate: () => PromiseLike<ProviderGenerateResult>;
}
interface ProviderMiddlewareGenerateInput {
doGenerate: () => PromiseLike<ProviderGenerateResult>;
params: Record<string, unknown> & { abortSignal?: AbortSignal };
model: { provider: string; modelId: string };
}
interface ProviderMiddlewareStreamInput {
doStream: () => PromiseLike<ProviderStreamResult>;
params: Record<string, unknown> & { abortSignal?: AbortSignal };
model: { provider: string; modelId: string };
}
/**
* Wraps a language model so its single `generate` call is tracked.
*
* The AI SDK's `wrapLanguageModel` is passed in rather than imported: both
* callers load the `ai` package dynamically, and there is no reason for this
* module to load it a third time. Non-streaming counterpart of what
* `ModelAdapter.startStream` does with `wrapStream`, and the only place either
* auxiliary caller attaches a tracker to a model.
*/
export function withProviderGenerateTracking(input: {
model: unknown;
wrapLanguageModel: (input: Record<string, unknown>) => unknown;
tracker: ProviderRequestTracker;
abortSignal?: AbortSignal;
historyCompactRoute?: HistoryCompactRoute;
}): unknown {
return input.wrapLanguageModel({
model: input.model,
middleware: {
wrapGenerate: async ({ doGenerate, params, model }: ProviderMiddlewareGenerateInput) =>
await input.tracker.trackGenerate({
providerId: model.provider,
modelId: model.modelId,
params,
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
...(input.historyCompactRoute ? { historyCompactRoute: input.historyCompactRoute } : {}),
doGenerate,
}),
},
});
}
/** Wraps a language model so its single streaming call is tracked. */
export function withProviderStreamTracking(input: {
model: unknown;
wrapLanguageModel: (input: Record<string, unknown>) => unknown;
tracker: ProviderRequestTracker;
abortSignal?: AbortSignal;
historyCompactRoute?: HistoryCompactRoute;
}): unknown {
return input.wrapLanguageModel({
model: input.model,
middleware: {
wrapStream: async ({ doStream, params, model }: ProviderMiddlewareStreamInput) =>
await input.tracker.trackStream({
providerId: model.provider,
modelId: model.modelId,
params,
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
...(input.historyCompactRoute ? { historyCompactRoute: input.historyCompactRoute } : {}),
doStream,
}),
},
});
}
export interface ProviderStreamResult {
stream: ReadableStream<unknown>;
request?: unknown;
response?: unknown;
}
export interface ProviderGenerateResult {
finishReason?: unknown;
usage?: ProviderRequestUsageLike;
request?: unknown;
response?: unknown;
[key: string]: unknown;
}
interface StoredCapture {
material: PreparedRequestMaterial;
/** Absent when artifact persistence is unavailable or failed. */
artifactId?: string;
}
const CANONICAL_USAGE_FIELDS = [
'inputTokens',
'outputTokens',
'cacheReadInputTokens',
'cacheMissInputTokens',
'cacheWriteInputTokens',
'reasoningTokens',
] as const;
/**
* Distinguishes "the provider never reported usage" from "it reported some of
* it". Both differ from an unresolvable price, which `costBasis` carries.
*/
function resolveUsageBasis(usage: ProviderRequestUsage | undefined): ModelCallUsageBasis {
if (!usage) return 'missing';
const present = CANONICAL_USAGE_FIELDS.filter((field) => usage[field] !== undefined);
if (present.length === 0) return 'missing';
return usage.inputTokens !== undefined && usage.outputTokens !== undefined
? 'reported'
: 'partial';
}
function modelCallUsageFields(
usage: ProviderRequestUsage | undefined,
): Partial<Record<(typeof CANONICAL_USAGE_FIELDS)[number], number>> {
if (!usage) return {};
const fields: Partial<Record<(typeof CANONICAL_USAGE_FIELDS)[number], number>> = {};
for (const field of CANONICAL_USAGE_FIELDS) {
const value = usage[field];
if (value !== undefined) fields[field] = value;
}
return fields;
}
export class ProviderRequestTracker {
private step = 0;
private readonly attemptsByStep = new Map<number, number>();
private readonly captures = new Map<string, StoredCapture>();
/**
* One logical call per step. Retries of the same step are further attempts of
* that call, not new calls, so they share this id.
*/
private readonly logicalCallIdByStep = new Map<number, string>();
constructor(private readonly input: ProviderRequestTrackerInput) {}
get traceId(): string {
return this.input.traceId;
}
setStep(step: number): void {
this.step = step;
}
async trackStream(input: TrackProviderStreamInput): Promise<ProviderStreamResult> {
throwIfAbortedBeforeDispatch(input.abortSignal);
await this.input.beforeDispatch?.();
throwIfAbortedBeforeDispatch(input.abortSignal);
this.input.accounting?.assertReady?.();
const step = this.step;
const capture = this.capture(step, input);
throwIfAbortedBeforeDispatch(input.abortSignal);
let sawOutput = false;
const attempt = this.beginAttempt(step, capture, input);
let result: ProviderStreamResult;
try {
result = await input.doStream();
} catch (error) {
await attempt.finalize(abortStatus(input.abortSignal, error), { error });
throw error;
}
const reader = result.stream.getReader();
const stream = new ReadableStream<unknown>({
pull: async (controller) => {
try {
const next = await reader.read();
if (next.done) {
await attempt.finalize(input.abortSignal?.aborted ? 'aborted' : 'interrupted');
controller.close();
return;
}
const part = asRecord(next.value);
if (part && isOutputPart(part.type)) {
sawOutput = true;
attempt.observeOutput();
}
if (part?.type === 'finish') {
await attempt.finalize(input.abortSignal?.aborted ? 'aborted' : 'completed', {
reason: rawFinishReasonString(part.finishReason),
usage: asUsage(part.usage),
});
} else if (part?.type === 'error') {
await attempt.finalize(
input.abortSignal?.aborted ? 'aborted' : sawOutput ? 'interrupted' : 'failed',
{ error: part.error },
);
}
controller.enqueue(next.value);
} catch (error) {
await attempt.finalize(
input.abortSignal?.aborted
? 'aborted'
: sawOutput
? 'interrupted'
: abortStatus(input.abortSignal, error),
{ error },
);
controller.error(error);
}
},
cancel: async (reason) => {
try {
await reader.cancel(reason);
} finally {
await attempt.finalize(input.abortSignal?.aborted ? 'aborted' : 'interrupted');
}
},
});
return { ...result, stream };
}
async trackGenerate(input: TrackProviderGenerateInput): Promise<ProviderGenerateResult> {
throwIfAbortedBeforeDispatch(input.abortSignal);
await this.input.beforeDispatch?.();
throwIfAbortedBeforeDispatch(input.abortSignal);
this.input.accounting?.assertReady?.();
const step = this.step;
const capture = this.capture(step, input);
throwIfAbortedBeforeDispatch(input.abortSignal);
const attempt = this.beginAttempt(step, capture, input);
try {
const result = await input.doGenerate();
await attempt.finalize(input.abortSignal?.aborted ? 'aborted' : 'completed', {
reason: rawFinishReasonString(result.finishReason),
usage: result.usage,
});
return result;
} catch (error) {
await attempt.finalize(abortStatus(input.abortSignal, error), { error });
throw error;
}
}
private beginAttempt(
step: number,
capture: StoredCapture,
input: Pick<
TrackProviderStreamInput | TrackProviderGenerateInput,
'providerId' | 'modelId' | 'abortSignal' | 'historyCompactBoundary' | 'historyCompactRoute'
>,
): {
observeOutput(): void;
finalize(
status: ProviderRequestAttemptStatus,
finish?: { reason?: string; usage?: ProviderRequestUsageLike; error?: unknown },
): Promise<void>;
} {
const attempt = (this.attemptsByStep.get(step) ?? 0) + 1;
this.attemptsByStep.set(step, attempt);
let logicalCallId = this.logicalCallIdByStep.get(step);
if (logicalCallId === undefined) {
logicalCallId = this.input.newId();
this.logicalCallIdByStep.set(step, logicalCallId);
}
const attemptId = this.input.newId();
const startedAt = this.input.now();
let timeToFirstTokenMs: number | undefined;
// Cancellation and provider settlement are different events. An abort that
// carries no usage records provisionally but does not close the attempt, so
// a `finish` arriving afterwards can still settle it. Both writes share one
// `attemptId`, and the canonical record dedupes on that key keeping the last
// — otherwise a cancelled call that really did consume tokens would be
// frozen as a permanently token-less, cost-less record.
let settled = false;
let provisionallyRecorded = false;
let accountingSettlement = Promise.resolve();
let abortListener: (() => void) | undefined;
const finalize = async (
status: ProviderRequestAttemptStatus,
finish?: { reason?: string; usage?: ProviderRequestUsageLike; error?: unknown },
): Promise<void> => {
if (settled) return;
const provisional = status === 'aborted' && finish?.usage === undefined;
if (provisional && provisionallyRecorded) return;
if (provisional) provisionallyRecorded = true;
else settled = true;
if (abortListener && !provisional) {
input.abortSignal?.removeEventListener('abort', abortListener);
}
const completedAt = this.input.now();
const usage = strictProviderRequestUsage(finish?.usage);
const contextWindow = positiveInteger(this.input.contextWindow);
const failure =
finish?.error !== undefined ? providerFailureDiagnostic(finish.error) : undefined;
const record: SettledProviderAttempt = {
traceId: this.input.traceId,
attemptId,
turnId: this.input.turnId,
step,
attempt,
...(capture.artifactId ? { captureArtifactId: capture.artifactId } : {}),
providerId: input.providerId,
modelId: input.modelId,
...(contextWindow !== undefined ? { contextWindow } : {}),
startedAt,
completedAt,
status,
...(finish?.reason !== undefined ? { finishReason: finish.reason } : {}),
...(failure !== undefined ? { failure } : {}),
latencyMs: Math.max(0, completedAt - startedAt),
...(timeToFirstTokenMs !== undefined ? { timeToFirstTokenMs } : {}),
...(usage ?? {}),
};
accountingSettlement = accountingSettlement.then(async () => {
await this.emitModelCallAttempt(record, {
logicalCallId,
usage,
contextWindow,
requestObservation: capture.material.observation,
// Frozen when THIS request was prepared, so a checkpoint published
// mid-flight by another turn cannot be sealed into a prompt built
// before it existed.
historyCompactBoundary: input.historyCompactBoundary,
historyCompactRoute:
input.historyCompactRoute ?? this.input.accounting?.historyCompactRoute,
});
});
await accountingSettlement;
};
const observeOutput = () => {
if (timeToFirstTokenMs === undefined) {
timeToFirstTokenMs = Math.max(0, this.input.now() - startedAt);
}
};
if (input.abortSignal) {
abortListener = () => {
void finalize('aborted');
};
if (input.abortSignal.aborted) void finalize('aborted');
else
input.abortSignal.addEventListener('abort', abortListener, {
once: true,
});
}
return { observeOutput, finalize };
}
/**
* Projects a settled attempt into the canonical accounting record.
*
* Never throws. This runs from the stream's `pull` handler, where a rejection
* would reach `controller.error` and fail an otherwise-complete model
* response. The dispatch-time gate is `assertAccountingReady`; a failure here
* means the call happened and was billed but went unrecorded, which is
* reported, not raised.
*/
private async emitModelCallAttempt(
record: SettledProviderAttempt,
context: {
logicalCallId: string;
usage: ProviderRequestUsage | undefined;
contextWindow: number | undefined;
requestObservation: PreparedRequestObservation;
historyCompactBoundary: ContextDiagnosticsCompaction | undefined;
historyCompactRoute: HistoryCompactRoute | undefined;
},
): Promise<void> {
const accounting = this.input.accounting;
if (!accounting) return;
const runId = accounting.resolveRunId();
if (runId === undefined) return;
const usage = context.usage;
const usageBasis = resolveUsageBasis(usage);
const cost = usage ? accounting.resolveCost?.(usage) : undefined;
const priced = cost?.costUsd !== undefined;
const attempt: ModelCallAttempt = {
schemaVersion: MODEL_CALL_ATTEMPT_SCHEMA_VERSION,
logicalCallId: context.logicalCallId,
attemptId: record.attemptId,
traceId: record.traceId,
sessionId: accounting.sessionId,
runId,
turnId: record.turnId,
...(accounting.connectionSlug !== undefined
? { connectionSlug: accounting.connectionSlug }
: {}),
// The physical ordinals are one-based on the diagnostic record; the
// canonical record counts retries from zero.
step: Math.max(0, record.step),
attempt: Math.max(0, record.attempt - 1),
callKind: accounting.callKind,
...(context.historyCompactRoute !== undefined
? { historyCompactRoute: context.historyCompactRoute }
: {}),
providerId: accounting.providerId ?? record.providerId,
modelId: record.modelId,
...(context.contextWindow !== undefined ? { contextWindow: context.contextWindow } : {}),
...(record.captureArtifactId !== undefined
? { captureArtifactId: record.captureArtifactId }
: {}),
requestObservation: context.requestObservation,
startedAt: record.startedAt,
completedAt: record.completedAt,
latencyMs: record.latencyMs,
...(record.timeToFirstTokenMs !== undefined
? { timeToFirstTokenMs: record.timeToFirstTokenMs }
: {}),
status: record.status,
...(record.finishReason !== undefined ? { finishReason: record.finishReason } : {}),
...(record.failure ?? {}),
usageBasis,
...(usageBasis === 'missing' ? {} : modelCallUsageFields(usage)),
costBasis: priced ? 'priced' : 'unpriced',
...(priced
? {
costUsd: cost?.costUsd,
...(cost?.pricingRevision !== undefined
? { pricingRevision: cost.pricingRevision }
: {}),
...(cost?.pricingRates !== undefined ? { pricingRates: cost.pricingRates } : {}),
}
: {}),
};
// Only a completed MAIN call describes the conversation's own context, so
// only that one carries the derived row. A failed, aborted or compaction
// call commits its metering alone and leaves the last answer standing.
const latestContext =
attempt.callKind === 'main' && attempt.status === 'completed'
? latestContextProjectionInput(
attempt,
attempt.requestObservation?.segments,
context.historyCompactBoundary,
)
: undefined;
try {
await accounting.record({ attempt, ...(latestContext ? { latestContext } : {}) });
} catch {
// Reported through the run's accounting-incomplete signal by the sink
// itself. Settlement must not fail the turn the call already completed.
}
}
private capture(
step: number,
input: TrackProviderStreamInput | TrackProviderGenerateInput,
): StoredCapture {
const material = preparedRequestMaterial(input.params);
const key = `${step}:${input.providerId}:${input.modelId}:${material.observation.digest}`;
const existing = this.captures.get(key);
if (existing) return existing;
const persistArtifact = this.input.persistArtifact;
const capture: StoredCapture = { material };
this.captures.set(key, capture);
if (persistArtifact) {
const artifactInput: PreparedRequestArtifactInput = {
...material,
traceId: this.input.traceId,
captureId: this.input.newId(),
turnId: this.input.turnId,
step,
providerId: input.providerId,
modelId: input.modelId,
};
// Persist the private body in parallel. Dispatch and canonical accounting
// are both allowed to finish without it; the bounded observation already
// lives on the canonical attempt. If persistence wins the race, the
// attempt also carries the optional artifact join.
try {
void persistArtifact(artifactInput)
.then((ref) => {
capture.artifactId = ref.artifactId;
})
.catch(() => undefined);
} catch {
// A synchronous adapter failure is the same optional-artifact miss.
}
}
return capture;
}
}
function throwIfAbortedBeforeDispatch(signal: AbortSignal | undefined): void {
if (signal?.aborted) {
throw new DOMException('The provider request was cancelled before dispatch', 'AbortError');
}
}
function preparedRequestMaterial(params: Record<string, unknown>): PreparedRequestMaterial {
const safeParams = secretFreeParams(params);
return prepareRequestObservation(safeParams);
}
function secretFreeParams(params: Record<string, unknown>): Record<string, unknown> {
const { abortSignal: _abortSignal, headers: _headers, ...safe } = params;
if (!Array.isArray(safe.prompt)) return safe;
return { ...safe, prompt: safe.prompt.map(redactPromptCompactionState) };
}
function redactPromptCompactionState(value: unknown): unknown {
if (!isPlainRecord(value) || !Array.isArray(value.content)) return value;
return { ...value, content: value.content.map(redactCompactionContentPart) };
}
function redactCompactionContentPart(value: unknown): unknown {
if (!isPlainRecord(value) || value.type !== 'custom' || value.kind !== 'openai.compaction') {
return value;
}
const providerOptions = isPlainRecord(value.providerOptions) ? value.providerOptions : undefined;
const openai = isPlainRecord(providerOptions?.openai) ? providerOptions.openai : undefined;
const { itemId: _itemId, encryptedContent: _encryptedContent, ...safeOpenai } = openai ?? {};
return {
...value,
providerOptions: {
...(providerOptions ?? {}),
openai: { ...safeOpenai, redacted: true },
},
};
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== 'object') return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
function abortStatus(signal: AbortSignal | undefined, error: unknown): 'failed' | 'aborted' {
if (signal?.aborted) return 'aborted';
return error instanceof Error && error.name === 'AbortError' ? 'aborted' : 'failed';
}
function isOutputPart(type: unknown): boolean {
return (
typeof type === 'string' &&
![
'stream-start',
'response-metadata',
'raw',
'finish',
'error',
'text-start',
'text-end',
'reasoning-start',
'reasoning-end',
'tool-input-start',
'tool-input-end',
].includes(type)
);
}
function asUsage(value: unknown): ProviderRequestUsageLike | undefined {
return asRecord(value) as ProviderRequestUsageLike | undefined;
}
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === 'object'
? (value as Record<string, unknown>)
: undefined;
}
/**
* Extract provider-request usage without inheriting adapter-filled zeroes.
* Cache evidence is read from the raw provider payload; only cache miss may be
* derived, and only when the total plus every cache component needed for the
* subtraction was explicitly reported.
*/
export function strictProviderRequestUsage(
usage: ProviderRequestUsageLike | undefined,
): ProviderRequestUsage | undefined {
if (!usage) return undefined;
const raw = usage.raw;
const normalizedInputTokens = tokenTotal(usage.inputTokens);
const normalizedOutputTokens = tokenTotal(usage.outputTokens);
const inputTokens = canUseNormalizedTotal(raw, [
'prompt_tokens',
'input_tokens',
'promptTokenCount',
])
? normalizedInputTokens
: undefined;
const outputTokens = canUseNormalizedTotal(raw, [
'completion_tokens',
'output_tokens',
'candidatesTokenCount',
])
? normalizedOutputTokens
: undefined;
const result: ProviderRequestUsage = {
...(inputTokens !== undefined ? { inputTokens } : {}),
...(outputTokens !== undefined ? { outputTokens } : {}),
};
if (raw) {
const normalizedCacheMiss =
typeof usage.inputTokens === 'object' && usage.inputTokens !== null
? finiteToken(usage.inputTokens.noCache)
: undefined;
applyAnthropicCacheUsage(result, raw, normalizedCacheMiss);
applyOpenAiCacheUsage(result, raw);
applyGoogleCacheUsage(result, raw);
const reasoningTokens = firstToken(
nestedToken(raw, 'completion_tokens_details', 'reasoning_tokens'),
nestedToken(raw, 'output_tokens_details', 'reasoning_tokens'),
ownToken(raw, 'thoughtsTokenCount'),
);
if (reasoningTokens !== undefined) result.reasoningTokens = reasoningTokens;
}
return Object.keys(result).length > 0 ? result : undefined;
}
function applyGoogleCacheUsage(result: ProviderRequestUsage, raw: Record<string, unknown>): void {
const totalInput = ownToken(raw, 'promptTokenCount');
const cacheRead = ownToken(raw, 'cachedContentTokenCount');
if (cacheRead === undefined) return;
result.cacheReadInputTokens = cacheRead;
result.cacheReadInputSource = 'provider';
if (totalInput === undefined || cacheRead > totalInput) return;
result.cacheMissInputTokens = totalInput - cacheRead;
result.cacheMissInputSource = 'derived';
}
function applyAnthropicCacheUsage(
result: ProviderRequestUsage,
raw: Record<string, unknown>,
normalizedCacheMiss: number | undefined,
): void {
const cacheRead = ownToken(raw, 'cache_read_input_tokens');
const cacheWrite = ownToken(raw, 'cache_creation_input_tokens');
if (cacheRead !== undefined) {
result.cacheReadInputTokens = cacheRead;
result.cacheReadInputSource = 'provider';
}
if (cacheWrite !== undefined) {
result.cacheWriteInputTokens = cacheWrite;
result.cacheWriteInputSource = 'provider';
}
// Anthropic defines input_tokens as the non-cached input component. Treat it
// as cache-miss evidence only when this is recognizably an Anthropic cache
// usage object, rather than an OpenAI Responses usage object with the same
// top-level input_tokens spelling.
if (cacheRead !== undefined || cacheWrite !== undefined) {
const rawCacheMiss = ownToken(raw, 'input_tokens');
if (rawCacheMiss !== undefined) {
result.cacheMissInputTokens = normalizedCacheMiss ?? rawCacheMiss;
result.cacheMissInputSource = 'provider';
}
}
}
function applyOpenAiCacheUsage(result: ProviderRequestUsage, raw: Record<string, unknown>): void {
const promptInput = ownToken(raw, 'prompt_tokens');
const responsesInput = ownToken(raw, 'input_tokens');
const cacheRead = firstToken(
nestedToken(raw, 'prompt_tokens_details', 'cached_tokens'),
nestedToken(raw, 'input_tokens_details', 'cached_tokens'),
);
const cacheWrite = firstToken(
nestedToken(raw, 'prompt_tokens_details', 'cache_write_tokens'),
nestedToken(raw, 'input_tokens_details', 'cache_write_tokens'),
);
if (cacheRead === undefined && cacheWrite === undefined) return;
if (cacheRead !== undefined) {
result.cacheReadInputTokens = cacheRead;
result.cacheReadInputSource = 'provider';
}
if (cacheWrite !== undefined) {
result.cacheWriteInputTokens = cacheWrite;
result.cacheWriteInputSource = 'provider';
}
const totalInput = promptInput ?? responsesInput;
if (totalInput === undefined || cacheRead === undefined) return;
const accountedInput = cacheRead + (cacheWrite ?? 0);
if (accountedInput > totalInput) return;
result.cacheMissInputTokens = totalInput - accountedInput;
result.cacheMissInputSource = 'derived';
}
function canUseNormalizedTotal(
raw: Record<string, unknown> | undefined,
keys: readonly string[],
): boolean {
return raw === undefined || keys.some((key) => ownToken(raw, key) !== undefined);
}
function tokenTotal(
value: ProviderRequestUsageLike['inputTokens'] | ProviderRequestUsageLike['outputTokens'],
): number | undefined {
return finiteToken(typeof value === 'object' && value !== null ? value.total : value);
}
function ownToken(value: Record<string, unknown>, key: string): number | undefined {
return Object.hasOwn(value, key) ? finiteToken(value[key]) : undefined;
}
function nestedToken(
value: Record<string, unknown>,
key: string,
nestedKey: string,
): number | undefined {
if (!Object.hasOwn(value, key)) return undefined;
const nested = value[key];
if (!nested || typeof nested !== 'object' || !Object.hasOwn(nested, nestedKey)) return undefined;
return finiteToken((nested as Record<string, unknown>)[nestedKey]);
}
function firstToken(...values: Array<number | undefined>): number | undefined {
return values.find((value) => value !== undefined);
}
function finiteToken(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
}
function positiveInteger(value: unknown): number | undefined {
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
}