Q63: model calls are priced and agent and department budgets limit real spend

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 11:08:38 -04:00
1 parent acbafcd6de
commit 168572dd76
21 files changed
+454 -12

No files matched your search

@@ -16,6 +16,8 @@ import { hashPrompt, readProviderResponseId, requireStreamText } from "./provide
import { type AgentTurnMessage, type AgentTurnResult, type ToolDefinition, type ToolTurnFn } from "./provider-tool-turn";
import { selectToolTurn, supportsToolTurn } from "./provider-tool-turns/router";
import { resolveTurnCredential } from "./provider-tool-turns/credential-resolver";
import { currentRunId } from "./run-control";
import { budgetBlock, isPriced, providerPrices, readTokenUsage, requestCostCents } from "./spend";
import {
HEALTH_STATE_HEALTHY,
WORKSPACE_SCOPES,
@@ -47,6 +49,9 @@ export interface ProviderResponseRunnerDeps {
}
export class ProviderResponseRunner {
/** Providers already logged as not reporting token usage. */
private readonly unreportedUsage = new Set<string>();
constructor(private readonly deps: ProviderResponseRunnerDeps) {}
async createResponse(input: ProviderResponseCreateInput): Promise<string> {
@@ -116,9 +121,19 @@ export class ProviderResponseRunner {
if (!model) throw new Error("Provider model is required.");
const owner: ProviderRow & { model: string } = { ...provider, model };
const turn = this.deps.toolTurn ?? selectToolTurn(provider.provider_key);
// A call made for a run is spent by its agent: refuse before calling once its budget is gone.
const taskRunId = currentRunId();
const agent = taskRunId
? this.deps.db.prepare("select a.id, a.department from task_runs r join agents a on a.id = r.agent_id where r.id = ?").get(taskRunId) as { id: string; department: string }
: null;
const blocked = agent ? budgetBlock(this.deps.db, agent.id, isPriced(providerPrices(this.deps.db, provider.id)), new Date()) : null;
if (blocked) throw new Error(blocked);
const credential = await resolveTurnCredential({ authBroker: this.deps.authBroker, integrationKeys: this.deps.integrationKeys, orgId: input.orgId, provider });
const promptChars = JSON.stringify(input.messages).length;
const requestId = this.deps.requestStore.create({
agentId: agent?.id ?? null,
taskRunId,
department: agent?.department ?? null,
orgId: input.orgId,
provider: owner,
model,
@@ -149,6 +164,7 @@ export class ProviderResponseRunner {
action: "response_created",
});
this.deps.requestStore.recordUsage(input.orgId, provider.id, promptChars, result.text.length);
this.recordSpend(requestId, owner, result.rawResponseJson);
return { ...result, requestId };
} catch (error) {
this.deps.responseObserver.recordFailure({
@@ -164,6 +180,16 @@ export class ProviderResponseRunner {
}
}
private recordSpend(requestId: string, provider: { id: string; provider_key: string }, rawResponseJson: string): void {
const usage = readTokenUsage(rawResponseJson);
if (!usage) {
if (!this.unreportedUsage.has(provider.provider_key)) console.warn(`[spend] ${provider.provider_key} returned no token usage; its calls cost nothing on record.`);
this.unreportedUsage.add(provider.provider_key);
return;
}
this.deps.requestStore.recordSpend(requestId, usage, requestCostCents(usage, providerPrices(this.deps.db, provider.id)));
}
async streamResponse(input: StreamProviderResponseInput, onEvent: (event: ProviderStreamRendererEvent) => void): Promise<string> {
const execution = this.prepareExecution(input);
const requestId = this.createRequestRecord(execution);