refactor(core): centralize reported cost accounting
This commit is contained in:
@@ -548,8 +548,15 @@ function providerOptions(input: LLMRequest["providerOptions"]): SharedV3Provider
|
||||
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
|
||||
}
|
||||
|
||||
interface StreamState {
|
||||
step: number
|
||||
toolNames: Record<string, string>
|
||||
copilot: boolean
|
||||
cost?: number
|
||||
}
|
||||
|
||||
function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions, copilot: boolean) {
|
||||
const state = { step: 0, toolNames: {} as Record<string, string>, copilot, cost: undefined as number | undefined }
|
||||
const state: StreamState = { step: 0, toolNames: {}, copilot }
|
||||
return Stream.concat(
|
||||
Stream.make(LLMEvent.stepStart({ index: state.step })),
|
||||
Stream.unwrap(
|
||||
@@ -572,7 +579,7 @@ function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallO
|
||||
}
|
||||
|
||||
function streamPartEvents(
|
||||
state: { step: number; toolNames: Record<string, string>; copilot: boolean; cost: number | undefined },
|
||||
state: StreamState,
|
||||
event: LanguageModelV3StreamPart,
|
||||
): Effect.Effect<ReadonlyArray<LLMEvent>, AIError> {
|
||||
switch (event.type) {
|
||||
|
||||
@@ -272,10 +272,8 @@ const layer = Layer.effect(
|
||||
snapshot: startSnapshot,
|
||||
assistantMessageID,
|
||||
})
|
||||
const stepUsage = (finish: NonNullable<StepRecord["finish"]>) => ({
|
||||
cost: SessionUsage.calculateCost(resolved.cost, finish.tokens, finish.cost),
|
||||
tokens: finish.tokens,
|
||||
})
|
||||
const stepUsage = (finish: NonNullable<StepRecord["finish"]>) =>
|
||||
SessionUsage.record(finish.usage, resolved.cost)
|
||||
|
||||
const captureStepEnd = Effect.fnUntraced(function* () {
|
||||
const snapshot = yield* snapshots.capture()
|
||||
|
||||
@@ -35,8 +35,7 @@ export interface StepRecord {
|
||||
/** Present once the provider finished the step normally. */
|
||||
readonly finish?: {
|
||||
readonly finish: Extract<LLMEvent, { type: "step-finish" }>["reason"]["normalized"]
|
||||
readonly tokens: ReturnType<typeof SessionUsage.tokens>
|
||||
readonly cost?: number
|
||||
readonly usage: Extract<LLMEvent, { type: "step-finish" }>["usage"]
|
||||
}
|
||||
readonly calls: ReadonlyArray<{
|
||||
readonly id: string
|
||||
@@ -496,11 +495,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
case "step-finish":
|
||||
yield* flush()
|
||||
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
|
||||
stepSettlement = {
|
||||
finish: event.reason.normalized,
|
||||
tokens: SessionUsage.tokens(event.usage),
|
||||
cost: event.usage?.cost,
|
||||
}
|
||||
stepSettlement = { finish: event.reason.normalized, usage: event.usage }
|
||||
if (event.reason.normalized === "content-filter") {
|
||||
providerFailed = true
|
||||
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" })
|
||||
|
||||
@@ -17,8 +17,7 @@ export const tokens = (usage: Usage | undefined): TokenUsage.Info => ({
|
||||
},
|
||||
})
|
||||
|
||||
export function calculateCost(costs: Model.Info["cost"], usage: TokenUsage.Info, reported?: number) {
|
||||
if (reported !== undefined && Number.isFinite(reported) && reported >= 0) return Money.USD.make(reported)
|
||||
export function calculateCost(costs: Model.Info["cost"], usage: TokenUsage.Info) {
|
||||
const context = usage.input + usage.cache.read + usage.cache.write
|
||||
const tier = costs
|
||||
.filter((cost) => cost.tier?.type === "context" && context > cost.tier.size)
|
||||
@@ -38,7 +37,14 @@ export type Recorded = { readonly tokens: TokenUsage.Info; readonly cost: Money.
|
||||
|
||||
export const record = (usage: Usage | undefined, costs: Model.Info["cost"]): Recorded => {
|
||||
const normalized = tokens(usage)
|
||||
return { tokens: normalized, cost: calculateCost(costs, normalized, usage?.cost) }
|
||||
const reported = usage?.cost
|
||||
return {
|
||||
tokens: normalized,
|
||||
cost:
|
||||
reported !== undefined && Number.isFinite(reported) && reported >= 0
|
||||
? Money.USD.make(reported)
|
||||
: calculateCost(costs, normalized),
|
||||
}
|
||||
}
|
||||
|
||||
export const add = (a: Recorded, b: Recorded): Recorded => ({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { LLMEvent } from "@opencode-ai/ai"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
@@ -13,6 +12,7 @@ import { Provider } from "@opencode-ai/core/provider"
|
||||
import { RelativePath } from "@opencode-ai/core/schema"
|
||||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||
import { createLLMEventPublisher } from "@opencode-ai/core/session/runner/publish-llm-event"
|
||||
import { SessionUsage } from "@opencode-ai/core/session/usage"
|
||||
|
||||
const sessionID = Session.ID.make("ses_tool_event_test")
|
||||
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
|
||||
@@ -280,6 +280,7 @@ test("content-filter finish retains failure evidence until step closeout", async
|
||||
nonCachedInputTokens: 8,
|
||||
outputTokens: 3,
|
||||
reasoningTokens: 1,
|
||||
cost: 1.25,
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -289,13 +290,13 @@ test("content-filter finish retains failure evidence until step closeout", async
|
||||
const settlement = publisher.record().finish
|
||||
expect(settlement).toMatchObject({
|
||||
finish: "content-filter",
|
||||
tokens: { input: 8, output: 2, reasoning: 1 },
|
||||
usage: { nonCachedInputTokens: 8, outputTokens: 3, reasoningTokens: 1, cost: 1.25 },
|
||||
})
|
||||
if (!settlement) throw new Error("Expected content-filter settlement")
|
||||
const recorded = SessionUsage.record(settlement.usage, [])
|
||||
await Effect.runPromise(
|
||||
publisher.publishStepFailure({
|
||||
cost: Money.USD.make(1.25),
|
||||
tokens: settlement.tokens,
|
||||
...recorded,
|
||||
snapshot: Snapshot.ID.make("tree-end"),
|
||||
files: [RelativePath.make("src/changed.ts")],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user