diff --git a/packages/llm/src/schema/events.ts b/packages/llm/src/schema/events.ts index 98fcc9a24d41..aaebd4280e8f 100644 --- a/packages/llm/src/schema/events.ts +++ b/packages/llm/src/schema/events.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids" +import { ContentBlockID, FinishReason, ModelID, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids" import { ModelSchema } from "./options" import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages" import { ProviderFailureClassification } from "./errors" @@ -184,6 +184,7 @@ export const StepFinish = Schema.Struct({ type: Schema.tag("step-finish"), index: Schema.Number, reason: FinishReason, + modelID: Schema.optional(ModelID), usage: Schema.optional(Usage), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.StepFinish" }) diff --git a/packages/opencode/src/session/llm/ai-sdk.ts b/packages/opencode/src/session/llm/ai-sdk.ts index 13d427aab62c..ec6fb3538e1b 100644 --- a/packages/opencode/src/session/llm/ai-sdk.ts +++ b/packages/opencode/src/session/llm/ai-sdk.ts @@ -1,4 +1,4 @@ -import { FinishReason, LLMEvent, ProviderMetadata, ToolResultValue } from "@opencode-ai/llm" +import { FinishReason, LLMEvent, ModelID, ProviderMetadata, ToolResultValue } from "@opencode-ai/llm" import { Effect, Schema } from "effect" import { type streamText } from "ai" import { errorMessage } from "@/util/error" @@ -105,6 +105,7 @@ export function toLLMEvents( LLMEvent.stepFinish({ index: state.step++, reason: finishReason(event.finishReason), + modelID: ModelID.make(event.response.modelId), usage: usage(event.usage), providerMetadata: metadata, }), diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 9f8530929c15..bdb920fc40c9 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -25,6 +25,7 @@ import { isRecord } from "@/util/record" import { EventV2Bridge } from "@/event-v2-bridge" import { Database } from "@opencode-ai/core/database/database" import { Usage, type LLMEvent } from "@opencode-ai/llm" +import { ModelV2 } from "@opencode-ai/core/model" const DOOM_LOOP_THRESHOLD = 3 export type Result = "compact" | "stop" | "continue" @@ -464,6 +465,7 @@ const layer = Layer.effect( messageID: ctx.assistantMessage.id, sessionID: ctx.assistantMessage.sessionID, type: "step-finish", + modelID: value.modelID ? ModelV2.ID.make(value.modelID) : undefined, tokens: usage.tokens, cost: usage.cost, }) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index fcb536f46d91..3e34c2905894 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -220,7 +220,12 @@ describe("session.llm.ai-sdk adapter", () => { }, { type: "finish-step", - response: { id: "response-1", timestamp: new Date(0), modelId: "gpt-test" }, + response: { + id: "response-1", + timestamp: new Date(0), + modelId: "gpt-test", + headers: { authorization: "must-not-be-persisted" }, + }, finishReason: "other", rawFinishReason: "other", usage: { @@ -281,6 +286,7 @@ describe("session.llm.ai-sdk adapter", () => { type: "step-finish", index: 0, reason: "unknown", + modelID: "gpt-test", usage: { inputTokens: 10, outputTokens: 5, @@ -303,6 +309,7 @@ describe("session.llm.ai-sdk adapter", () => { }, }, ]) + expect(events.find((event) => event.type === "step-finish")).not.toHaveProperty("responseHeaders") }) test("creates stable block ids when AI SDK omits them", async () => { diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index c67f82d9c71b..42fdde6df78f 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -299,6 +299,7 @@ it.live("session.processor effect tests preserve text start time", () => { id: "chatcmpl-test", object: "chat.completion.chunk", + model: "actual-model", choices: [{ delta: { role: "assistant" } }], }, { @@ -358,10 +359,13 @@ it.live("session.processor effect tests preserve text start time", () => gate.resolve() const exit = yield* Fiber.await(run) - const text = (yield* MessageV2.parts(msg.id)).find((part): part is SessionV1.TextPart => part.type === "text") + const parts = yield* MessageV2.parts(msg.id) + const text = parts.find((part): part is SessionV1.TextPart => part.type === "text") + const finish = parts.find((part): part is SessionV1.StepFinishPart => part.type === "step-finish") expect(Exit.isSuccess(exit)).toBe(true) expect(text?.text).toBe("hello") + expect(finish?.modelID).toBe(ModelV2.ID.make("actual-model")) expect(text?.time?.start).toBeDefined() expect(text?.time?.end).toBeDefined() if (!text?.time?.start || !text.time.end) return diff --git a/packages/schema/src/v1/session.ts b/packages/schema/src/v1/session.ts index 75e9282f117c..5e3a8edc6f81 100644 --- a/packages/schema/src/v1/session.ts +++ b/packages/schema/src/v1/session.ts @@ -241,6 +241,7 @@ export const StepFinishPart = Schema.Struct({ ...partBase, type: Schema.Literal("step-finish"), reason: Schema.String, + modelID: optional(Model.ID), snapshot: Schema.optional(Schema.String), cost: Schema.Finite, tokens: Schema.Struct({ diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f06c20cc413e..5dd25b8a58bc 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -558,6 +558,7 @@ export type StepFinishPart = { messageID: string type: "step-finish" reason: string + modelID?: string snapshot?: string cost: number tokens: {