feat(core): record title and compaction usage in session totals (#37441)
This commit is contained in:
parent
91238441a6
commit
73567f3570
14 changed files with 234 additions and 74 deletions
|
|
@ -21,6 +21,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
|
|||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
|
||||
import { asc, eq } from "drizzle-orm"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
|
@ -31,17 +32,44 @@ const model = Model.make({
|
|||
provider: "test",
|
||||
route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }),
|
||||
})
|
||||
const cost = [
|
||||
{
|
||||
input: Money.USDPerMillionTokens.make(1),
|
||||
output: Money.USDPerMillionTokens.make(2),
|
||||
cache: {
|
||||
read: Money.USDPerMillionTokens.make(0.1),
|
||||
write: Money.USDPerMillionTokens.make(0.5),
|
||||
},
|
||||
},
|
||||
]
|
||||
const client = Layer.mock(LLMClient.Service)({
|
||||
prepare: () => Effect.die("unused"),
|
||||
stream: (request: LLMRequest) => {
|
||||
requests.push(request)
|
||||
return Stream.make(LLMEvent.textDelta({ id: "summary", text: "manual summary" }))
|
||||
return Stream.make(
|
||||
LLMEvent.textDelta({ id: "summary", text: "manual summary" }),
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "stop",
|
||||
usage: {
|
||||
inputTokens: 15,
|
||||
outputTokens: 6,
|
||||
nonCachedInputTokens: 10,
|
||||
cacheReadInputTokens: 3,
|
||||
cacheWriteInputTokens: 2,
|
||||
reasoningTokens: 2,
|
||||
},
|
||||
}),
|
||||
LLMEvent.finish({
|
||||
reason: "stop",
|
||||
}),
|
||||
)
|
||||
},
|
||||
generate: () => Effect.die("unused"),
|
||||
})
|
||||
const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) })
|
||||
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model)),
|
||||
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model, undefined, cost)),
|
||||
})
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(
|
||||
|
|
@ -169,6 +197,10 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
|||
expect(yield* store.context(sessionID)).toMatchObject([
|
||||
{ type: "compaction", reason: "manual", summary: "manual summary", recent: "" },
|
||||
])
|
||||
expect(yield* store.get(sessionID)).toMatchObject({
|
||||
cost: 0.0000233,
|
||||
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 2 } },
|
||||
})
|
||||
expect(
|
||||
yield* db
|
||||
.select({ type: EventTable.type })
|
||||
|
|
@ -179,6 +211,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
|||
.pipe(Effect.orDie),
|
||||
).toEqual([
|
||||
{ type: EventV2.versionedType(SessionEvent.Compaction.Started.type, 1) },
|
||||
{ type: EventV2.versionedType(SessionEvent.UsageRecorded.type, 1) },
|
||||
{ type: EventV2.versionedType(SessionEvent.Compaction.Ended.type, 1) },
|
||||
])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator
|
|||
import { SessionRunner } from "@opencode-ai/core/session/runner"
|
||||
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
import { SessionUsage } from "@opencode-ai/core/session/usage"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||
|
|
@ -165,7 +166,7 @@ const recoveryModel = Model.make({
|
|||
|
||||
test("calculates step cost using the matching context tier", () => {
|
||||
expect(
|
||||
SessionRunnerLLM.calculateCost(
|
||||
SessionUsage.calculateCost(
|
||||
[
|
||||
{
|
||||
input: Money.USDPerMillionTokens.make(1),
|
||||
|
|
@ -192,7 +193,7 @@ test("calculates step cost using the matching context tier", () => {
|
|||
|
||||
test("does not apply an ineligible tier without base pricing", () => {
|
||||
expect(
|
||||
SessionRunnerLLM.calculateCost(
|
||||
SessionUsage.calculateCost(
|
||||
[
|
||||
{
|
||||
tier: { type: "context", size: 100 },
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
|
|||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { DateTime, Effect, Layer, Stream } from "effect"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
let requests: LLMRequest[] = []
|
||||
|
|
@ -29,16 +30,43 @@ const model = Model.make({
|
|||
provider: "test",
|
||||
route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }),
|
||||
})
|
||||
const cost = [
|
||||
{
|
||||
input: Money.USDPerMillionTokens.make(1),
|
||||
output: Money.USDPerMillionTokens.make(2),
|
||||
cache: {
|
||||
read: Money.USDPerMillionTokens.make(0.1),
|
||||
write: Money.USDPerMillionTokens.make(0.5),
|
||||
},
|
||||
},
|
||||
]
|
||||
const client = Layer.mock(LLMClient.Service)({
|
||||
prepare: () => Effect.die("unused"),
|
||||
stream: (request: LLMRequest) => {
|
||||
requests.push(request)
|
||||
return Stream.make(LLMEvent.textDelta({ id: "title", text: "Generated Title\n" }))
|
||||
return Stream.make(
|
||||
LLMEvent.textDelta({ id: "title", text: "Generated Title\n" }),
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "stop",
|
||||
usage: {
|
||||
inputTokens: 15,
|
||||
outputTokens: 6,
|
||||
nonCachedInputTokens: 10,
|
||||
cacheReadInputTokens: 3,
|
||||
cacheWriteInputTokens: 2,
|
||||
reasoningTokens: 2,
|
||||
},
|
||||
}),
|
||||
LLMEvent.finish({
|
||||
reason: "stop",
|
||||
}),
|
||||
)
|
||||
},
|
||||
generate: () => Effect.die("unused"),
|
||||
})
|
||||
const models = Layer.mock(SessionRunnerModel.Service)({
|
||||
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model)),
|
||||
resolve: () => Effect.succeed(SessionRunnerModel.resolved(model, undefined, cost)),
|
||||
})
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(
|
||||
|
|
@ -130,6 +158,8 @@ it.effect("generates a title from the sole user message and renames the session"
|
|||
expect(JSON.stringify(requests[0]?.messages)).toContain("Help me debug the failing build")
|
||||
const renamed = yield* store.get(sessionID)
|
||||
expect(renamed?.title).toBe("Generated Title")
|
||||
expect(renamed?.tokens).toEqual({ input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 2 } })
|
||||
expect(renamed?.cost).toBeCloseTo(0.0000233)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue