feat(ai): preserve raw finish reasons (#38423)
This commit is contained in:
parent
e7ecee5df2
commit
6401eeaea0
31 changed files with 410 additions and 133 deletions
|
|
@ -659,12 +659,12 @@ function streamPartEvents(
|
|||
return Effect.succeed([
|
||||
LLMEvent.stepFinish({
|
||||
index: state.step++,
|
||||
reason: finishReason(event.finishReason),
|
||||
reason: { normalized: finishReason(event.finishReason), raw: event.finishReason.raw },
|
||||
usage: usage(event.usage),
|
||||
providerMetadata: providerMetadata(event.providerMetadata),
|
||||
}),
|
||||
LLMEvent.finish({
|
||||
reason: finishReason(event.finishReason),
|
||||
reason: { normalized: finishReason(event.finishReason), raw: event.finishReason.raw },
|
||||
usage: usage(event.usage),
|
||||
providerMetadata: providerMetadata(event.providerMetadata),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
let stepFailure: SessionError.Error | undefined
|
||||
let stepSettlement:
|
||||
| {
|
||||
readonly finish: Extract<LLMEvent, { type: "step-finish" }>["reason"]
|
||||
readonly finish: Extract<LLMEvent, { type: "step-finish" }>["reason"]["normalized"]
|
||||
readonly tokens: ReturnType<typeof SessionUsage.tokens>
|
||||
}
|
||||
| undefined
|
||||
|
|
@ -449,8 +449,8 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
case "step-finish":
|
||||
yield* flush()
|
||||
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
|
||||
stepSettlement = { finish: event.reason, tokens: SessionUsage.tokens(event.usage) }
|
||||
if (event.reason === "content-filter") {
|
||||
stepSettlement = { finish: event.reason.normalized, tokens: SessionUsage.tokens(event.usage) }
|
||||
if (event.reason.normalized === "content-filter") {
|
||||
providerFailed = true
|
||||
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" })
|
||||
return
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ it.effect("emits malformed AI SDK tool input without executing it", () =>
|
|||
})
|
||||
expect(response.events.some(LLMEvent.is.toolInputEnd)).toBeTrue()
|
||||
expect(response.events.some(LLMEvent.is.toolCall)).toBeFalse()
|
||||
expect(response.finishReason).toEqual({ normalized: "tool-calls", raw: "tool_calls" })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
LLMEvent.textStart({ id: "generate" }),
|
||||
LLMEvent.textDelta({ id: "generate", text: "OK" }),
|
||||
LLMEvent.textEnd({ id: "generate" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
])
|
||||
if (!response) throw new Error("Incomplete generate response")
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
LLMEvent.textDelta({ id: "summary", text: "manual summary" }),
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "stop",
|
||||
reason: { normalized: "stop" },
|
||||
usage: {
|
||||
inputTokens: 15,
|
||||
outputTokens: 6,
|
||||
|
|
@ -60,7 +60,7 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
},
|
||||
}),
|
||||
LLMEvent.finish({
|
||||
reason: "stop",
|
||||
reason: { normalized: "stop" },
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
LLMEvent.textStart({ id: "generate" }),
|
||||
LLMEvent.textDelta({ id: "generate", text: "Transient answer" }),
|
||||
LLMEvent.textEnd({ id: "generate" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 100, outputTokens: 10 } }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" }, usage: { inputTokens: 100, outputTokens: 10 } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
])
|
||||
if (!response) throw new Error("Incomplete generate response")
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ test("success event data can carry provider-executed result state", () => {
|
|||
test("step finish records settlement without publishing step ended", async () => {
|
||||
const { published, publisher } = capture()
|
||||
await Effect.runPromise(publisher.publish(LLMEvent.stepStart({ index: 0 })))
|
||||
await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: "stop" })))
|
||||
await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } })))
|
||||
|
||||
expect(published.some((event) => event.type === "step.ended.2")).toBe(false)
|
||||
expect(publisher.stepSettlement()).toMatchObject({ finish: "stop" })
|
||||
|
|
@ -268,7 +268,7 @@ test("content-filter finish retains failure evidence until step closeout", async
|
|||
publisher.publish(
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "content-filter",
|
||||
reason: { normalized: "content-filter" },
|
||||
usage: {
|
||||
nonCachedInputTokens: 8,
|
||||
outputTokens: 3,
|
||||
|
|
@ -311,7 +311,7 @@ test("content-filter finish preserves partial streamed text and never ends the s
|
|||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.textStart({ id: "text" }),
|
||||
LLMEvent.textDelta({ id: "text", text: "Partial" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "content-filter" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "content-filter" } }),
|
||||
],
|
||||
(event) => publisher.publish(event),
|
||||
{ discard: true },
|
||||
|
|
|
|||
|
|
@ -119,8 +119,8 @@ const client = Layer.succeed(
|
|||
const reply = {
|
||||
stop: () => [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
],
|
||||
text: (text: string, id: string) => fragmentFixture("text", id, [text]).completeEvents,
|
||||
textWithUsage: (text: string, id: string, inputTokens: number) =>
|
||||
|
|
@ -136,8 +136,8 @@ const reply = {
|
|||
tool: (id: string, name: string, input: unknown) => [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolCall({ id, name, input }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
],
|
||||
}
|
||||
const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route })
|
||||
|
|
@ -682,8 +682,8 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string
|
|||
completeEvents: [
|
||||
...partialEvents,
|
||||
LLMEvent.textEnd({ id }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
],
|
||||
expectedAssistant: { type: "assistant", finish: "stop", content: [expectedContent] },
|
||||
expectedContent,
|
||||
|
|
@ -702,8 +702,8 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string
|
|||
completeEvents: [
|
||||
...partialEvents,
|
||||
LLMEvent.reasoningEnd({ id }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
],
|
||||
expectedAssistant: { type: "assistant", finish: "stop", content: [expectedContent] },
|
||||
expectedContent,
|
||||
|
|
@ -999,8 +999,8 @@ describe("SessionRunnerLLM", () => {
|
|||
[
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolCall({ id: "call-reloaded", name: "reloaded", input: {} }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
],
|
||||
[],
|
||||
]
|
||||
|
|
@ -2377,7 +2377,7 @@ describe("SessionRunnerLLM", () => {
|
|||
}),
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "tool-calls",
|
||||
reason: { normalized: "tool-calls" },
|
||||
usage: {
|
||||
inputTokens: 10,
|
||||
nonCachedInputTokens: 8,
|
||||
|
|
@ -2386,7 +2386,7 @@ describe("SessionRunnerLLM", () => {
|
|||
cacheReadInputTokens: 2,
|
||||
},
|
||||
}),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
]
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -2535,8 +2535,8 @@ describe("SessionRunnerLLM", () => {
|
|||
anthropic: { ignored: true },
|
||||
},
|
||||
}),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
]
|
||||
yield* session.resume(sessionID)
|
||||
yield* replaySessionProjection(sessionID)
|
||||
|
|
@ -2600,8 +2600,8 @@ describe("SessionRunnerLLM", () => {
|
|||
providerExecuted: true,
|
||||
providerMetadata: { openai: { blockType: "web_search_tool_result" }, anthropic: { ignored: true } },
|
||||
}),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
]
|
||||
yield* session.resume(sessionID)
|
||||
yield* replaySessionProjection(sessionID)
|
||||
|
|
@ -2648,8 +2648,8 @@ describe("SessionRunnerLLM", () => {
|
|||
),
|
||||
])
|
||||
const final = Stream.fromIterable([
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
])
|
||||
responseStream = Stream.concat(
|
||||
initial,
|
||||
|
|
@ -3605,7 +3605,7 @@ describe("SessionRunnerLLM", () => {
|
|||
yield* admit(session, "Reject permission")
|
||||
responses = [
|
||||
reply.tool("call-permission", "permissionfail", {}),
|
||||
[LLMEvent.stepStart({ index: 0 }), LLMEvent.stepFinish({ index: 0, reason: "stop" })],
|
||||
[LLMEvent.stepStart({ index: 0 }), LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } })],
|
||||
]
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -3954,10 +3954,10 @@ describe("SessionRunnerLLM", () => {
|
|||
LLMEvent.textDelta({ id: "partial", text: "Partial" }),
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "content-filter",
|
||||
reason: { normalized: "content-filter" },
|
||||
usage: { nonCachedInputTokens: 8, outputTokens: 3, reasoningTokens: 1 },
|
||||
}),
|
||||
LLMEvent.finish({ reason: "content-filter" }),
|
||||
LLMEvent.finish({ reason: { normalized: "content-filter" } }),
|
||||
]
|
||||
|
||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider blocked the response")
|
||||
|
|
@ -3990,8 +3990,8 @@ describe("SessionRunnerLLM", () => {
|
|||
response = [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolCall({ id: "call-before-content-filter", name: "echo", input: { text: "settled" } }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "content-filter" }),
|
||||
LLMEvent.finish({ reason: "content-filter" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "content-filter" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "content-filter" } }),
|
||||
]
|
||||
|
||||
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
|
|
@ -4282,8 +4282,8 @@ describe("SessionRunnerLLM", () => {
|
|||
name: "echo",
|
||||
raw,
|
||||
}),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
],
|
||||
reply.stop(),
|
||||
]
|
||||
|
|
@ -4379,8 +4379,8 @@ describe("SessionRunnerLLM", () => {
|
|||
name: "echo",
|
||||
raw: '{"text":"partial',
|
||||
}),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
],
|
||||
reply.stop(),
|
||||
]
|
||||
|
|
@ -4421,8 +4421,8 @@ describe("SessionRunnerLLM", () => {
|
|||
name: "echo",
|
||||
raw: '{"text":"partial',
|
||||
}),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
]
|
||||
|
||||
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
|
|
@ -4530,8 +4530,8 @@ describe("SessionRunnerLLM", () => {
|
|||
name: "echo",
|
||||
raw: '{"text":"partial',
|
||||
}),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
]
|
||||
responses = [
|
||||
malformed("call-first"),
|
||||
|
|
@ -4565,8 +4565,8 @@ describe("SessionRunnerLLM", () => {
|
|||
name: "echo",
|
||||
raw: '{"text":"partial',
|
||||
}),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||
LLMEvent.finish({ reason: "tool-calls" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "tool-calls" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "tool-calls" } }),
|
||||
]
|
||||
responses = [malformed("call-first"), malformed("call-at-limit")]
|
||||
|
||||
|
|
@ -4727,8 +4727,8 @@ describe("SessionRunnerLLM", () => {
|
|||
response = [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
hostedCall("call-hosted-clean-end", "effect"),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
]
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -4852,8 +4852,8 @@ describe("SessionRunnerLLM", () => {
|
|||
LLMEvent.textStart({ id: "text-2" }),
|
||||
LLMEvent.textDelta({ id: "text-2", text: "Second" }),
|
||||
LLMEvent.textEnd({ id: "text-2" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
]
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
|
@ -4906,8 +4906,8 @@ describe("SessionRunnerLLM", () => {
|
|||
LLMEvent.toolInputDelta({ id: "call-parsed", name: "web_search", text: '{"query":"hello"}' }),
|
||||
LLMEvent.toolInputEnd({ id: "call-parsed", name: "web_search" }),
|
||||
hostedCall("call-parsed", "hello"),
|
||||
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
|
||||
LLMEvent.finish({ reason: "stop" }),
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" } }),
|
||||
LLMEvent.finish({ reason: { normalized: "stop" } }),
|
||||
]
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
LLMEvent.textDelta({ id: "title", text: "Generated Title\n" }),
|
||||
LLMEvent.stepFinish({
|
||||
index: 0,
|
||||
reason: "stop",
|
||||
reason: { normalized: "stop" },
|
||||
usage: {
|
||||
inputTokens: 15,
|
||||
outputTokens: 6,
|
||||
|
|
@ -58,7 +58,7 @@ const client = Layer.mock(LLMClient.Service)({
|
|||
},
|
||||
}),
|
||||
LLMEvent.finish({
|
||||
reason: "stop",
|
||||
reason: { normalized: "stop" },
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue