mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
refactor(session): consume native LLM events
This commit is contained in:
@@ -28,6 +28,7 @@ import { testEffect } from "../lib/effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { TestConfig } from "../fixture/config"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { LLMEvent, Usage } from "@opencode-ai/llm"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -45,6 +46,10 @@ const ref = {
|
||||
modelID: ModelID.make("test-model"),
|
||||
}
|
||||
|
||||
const usage = (input: ConstructorParameters<typeof Usage>[0]) => new Usage(input)
|
||||
|
||||
const basicUsage = () => usage({ inputTokens: 1, outputTokens: 1, totalTokens: 2 })
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
})
|
||||
@@ -289,11 +294,11 @@ function readCompactionPart(sessionID: SessionID) {
|
||||
|
||||
function llm() {
|
||||
const queue: Array<
|
||||
Stream.Stream<LLM.Event, unknown> | ((input: LLM.StreamInput) => Stream.Stream<LLM.Event, unknown>)
|
||||
Stream.Stream<LLMEvent, unknown> | ((input: LLM.StreamInput) => Stream.Stream<LLMEvent, unknown>)
|
||||
> = []
|
||||
|
||||
return {
|
||||
push(stream: Stream.Stream<LLM.Event, unknown> | ((input: LLM.StreamInput) => Stream.Stream<LLM.Event, unknown>)) {
|
||||
push(stream: Stream.Stream<LLMEvent, unknown> | ((input: LLM.StreamInput) => Stream.Stream<LLMEvent, unknown>)) {
|
||||
queue.push(stream)
|
||||
},
|
||||
layer: Layer.succeed(
|
||||
@@ -312,54 +317,22 @@ function llm() {
|
||||
function reply(
|
||||
text: string,
|
||||
capture?: (input: LLM.StreamInput) => void,
|
||||
): (input: LLM.StreamInput) => Stream.Stream<LLM.Event, unknown> {
|
||||
): (input: LLM.StreamInput) => Stream.Stream<LLMEvent, unknown> {
|
||||
return (input) => {
|
||||
capture?.(input)
|
||||
return Stream.make(
|
||||
{ type: "start" } satisfies LLM.Event,
|
||||
{ type: "text-start", id: "txt-0" } satisfies LLM.Event,
|
||||
{ type: "text-delta", id: "txt-0", delta: text, text } as LLM.Event,
|
||||
{ type: "text-end", id: "txt-0" } satisfies LLM.Event,
|
||||
{
|
||||
type: "finish-step",
|
||||
finishReason: "stop",
|
||||
rawFinishReason: "stop",
|
||||
response: { id: "res", modelId: "test-model", timestamp: new Date() },
|
||||
providerMetadata: undefined,
|
||||
usage: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
totalTokens: 2,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
} satisfies LLM.Event,
|
||||
{
|
||||
type: "finish",
|
||||
finishReason: "stop",
|
||||
rawFinishReason: "stop",
|
||||
totalUsage: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
totalTokens: 2,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
} satisfies LLM.Event,
|
||||
LLMEvent.textStart({ id: "txt-0" }),
|
||||
LLMEvent.textDelta({ id: "txt-0", text }),
|
||||
LLMEvent.textEnd({ id: "txt-0" }),
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "stop",
|
||||
usage: basicUsage(),
|
||||
}),
|
||||
LLMEvent.requestFinish({
|
||||
reason: "stop",
|
||||
usage: basicUsage(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1198,7 +1171,7 @@ describe("session.compaction.process", () => {
|
||||
Stream.fromAsyncIterable(
|
||||
{
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield { type: "start" } as LLM.Event
|
||||
yield LLMEvent.stepStart({ index: 0 })
|
||||
throw new APICallError({
|
||||
message: "boom",
|
||||
url: "https://example.com/v1/chat/completions",
|
||||
@@ -1290,49 +1263,16 @@ describe("session.compaction.process", () => {
|
||||
const stub = llm()
|
||||
stub.push(
|
||||
Stream.make(
|
||||
{ type: "start" } satisfies LLM.Event,
|
||||
{ type: "tool-input-start", id: "call-1", toolName: "_noop" } satisfies LLM.Event,
|
||||
{ type: "tool-call", toolCallId: "call-1", toolName: "_noop", input: {} } satisfies LLM.Event,
|
||||
{
|
||||
type: "finish-step",
|
||||
finishReason: "tool-calls",
|
||||
rawFinishReason: "tool_calls",
|
||||
response: { id: "res", modelId: "test-model", timestamp: new Date() },
|
||||
providerMetadata: undefined,
|
||||
usage: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
totalTokens: 2,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
} satisfies LLM.Event,
|
||||
{
|
||||
type: "finish",
|
||||
finishReason: "tool-calls",
|
||||
rawFinishReason: "tool_calls",
|
||||
totalUsage: {
|
||||
inputTokens: 1,
|
||||
outputTokens: 1,
|
||||
totalTokens: 2,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
} satisfies LLM.Event,
|
||||
LLMEvent.toolCall({ id: "call-1", name: "_noop", input: {} }),
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "tool-calls",
|
||||
usage: basicUsage(),
|
||||
}),
|
||||
LLMEvent.requestFinish({
|
||||
reason: "tool-calls",
|
||||
usage: basicUsage(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
return Effect.gen(function* () {
|
||||
@@ -1543,20 +1483,7 @@ describe("SessionNs.getUsage", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 1000, outputTokens: 500, totalTokens: 1500 }),
|
||||
})
|
||||
|
||||
expect(result.tokens.input).toBe(1000)
|
||||
@@ -1570,20 +1497,7 @@ describe("SessionNs.getUsage", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: 800,
|
||||
cacheReadTokens: 200,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 1000, outputTokens: 500, totalTokens: 1500, cacheReadInputTokens: 200 }),
|
||||
})
|
||||
|
||||
expect(result.tokens.input).toBe(800)
|
||||
@@ -1594,20 +1508,7 @@ describe("SessionNs.getUsage", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 1000, outputTokens: 500, totalTokens: 1500 }),
|
||||
metadata: {
|
||||
anthropic: {
|
||||
cacheCreationInputTokens: 300,
|
||||
@@ -1623,20 +1524,7 @@ describe("SessionNs.getUsage", () => {
|
||||
// AI SDK v6 normalizes inputTokens to include cached tokens for all providers
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: 800,
|
||||
cacheReadTokens: 200,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 1000, outputTokens: 500, totalTokens: 1500, cacheReadInputTokens: 200 }),
|
||||
metadata: {
|
||||
anthropic: {},
|
||||
},
|
||||
@@ -1650,20 +1538,7 @@ describe("SessionNs.getUsage", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: 400,
|
||||
reasoningTokens: 100,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 1000, outputTokens: 500, reasoningTokens: 100, totalTokens: 1500 }),
|
||||
})
|
||||
|
||||
expect(result.tokens.input).toBe(1000)
|
||||
@@ -1684,20 +1559,7 @@ describe("SessionNs.getUsage", () => {
|
||||
})
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 1_000_000,
|
||||
totalTokens: 1_000_000,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: 750_000,
|
||||
reasoningTokens: 250_000,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 0, outputTokens: 1_000_000, reasoningTokens: 250_000, totalTokens: 1_000_000 }),
|
||||
})
|
||||
|
||||
expect(result.tokens.output).toBe(750_000)
|
||||
@@ -1709,20 +1571,7 @@ describe("SessionNs.getUsage", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000 })
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 0, outputTokens: 0, totalTokens: 0 }),
|
||||
})
|
||||
|
||||
expect(result.tokens.input).toBe(0)
|
||||
@@ -1745,20 +1594,7 @@ describe("SessionNs.getUsage", () => {
|
||||
})
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1_000_000,
|
||||
outputTokens: 100_000,
|
||||
totalTokens: 1_100_000,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 1_000_000, outputTokens: 100_000, totalTokens: 1_100_000 }),
|
||||
})
|
||||
|
||||
expect(result.cost).toBe(3 + 1.5)
|
||||
@@ -1769,24 +1605,16 @@ describe("SessionNs.getUsage", () => {
|
||||
(npm) => {
|
||||
const model = createModel({ context: 100_000, output: 32_000, npm })
|
||||
// AI SDK v6: inputTokens includes cached tokens for all providers
|
||||
const usage = {
|
||||
const item = usage({
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: 800,
|
||||
cacheReadTokens: 200,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
}
|
||||
cacheReadInputTokens: 200,
|
||||
})
|
||||
if (npm === "@ai-sdk/amazon-bedrock") {
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage,
|
||||
usage: item,
|
||||
metadata: {
|
||||
bedrock: {
|
||||
usage: {
|
||||
@@ -1807,7 +1635,7 @@ describe("SessionNs.getUsage", () => {
|
||||
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage,
|
||||
usage: item,
|
||||
metadata: {
|
||||
anthropic: {
|
||||
cacheCreationInputTokens: 300,
|
||||
@@ -1828,20 +1656,7 @@ describe("SessionNs.getUsage", () => {
|
||||
const model = createModel({ context: 100_000, output: 32_000, npm: "@ai-sdk/google-vertex/anthropic" })
|
||||
const result = SessionNs.getUsage({
|
||||
model,
|
||||
usage: {
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
inputTokenDetails: {
|
||||
noCacheTokens: 800,
|
||||
cacheReadTokens: 200,
|
||||
cacheWriteTokens: undefined,
|
||||
},
|
||||
outputTokenDetails: {
|
||||
textTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
},
|
||||
},
|
||||
usage: usage({ inputTokens: 1000, outputTokens: 500, totalTokens: 1500, cacheReadInputTokens: 200 }),
|
||||
metadata: {
|
||||
vertex: {
|
||||
cacheCreationInputTokens: 300,
|
||||
|
||||
Reference in New Issue
Block a user