fix(ai): align Anthropic stream handling (#38733)
This commit is contained in:
parent
d66d0cb904
commit
49bec25ae5
4 changed files with 217 additions and 29 deletions
|
|
@ -9,6 +9,7 @@ import {
|
||||||
LLMEvent,
|
LLMEvent,
|
||||||
Usage,
|
Usage,
|
||||||
type CacheHint,
|
type CacheHint,
|
||||||
|
type FinishReasonDetails,
|
||||||
type FinishReason,
|
type FinishReason,
|
||||||
type JsonSchema,
|
type JsonSchema,
|
||||||
type LLMRequest,
|
type LLMRequest,
|
||||||
|
|
@ -288,7 +289,12 @@ type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
|
||||||
|
|
||||||
interface ParserState {
|
interface ParserState {
|
||||||
readonly tools: ToolStream.State<number>
|
readonly tools: ToolStream.State<number>
|
||||||
|
readonly reasoningSignatures: Readonly<Record<number, string>>
|
||||||
readonly usage?: Usage
|
readonly usage?: Usage
|
||||||
|
readonly pendingFinish?: {
|
||||||
|
readonly reason: FinishReasonDetails
|
||||||
|
readonly providerMetadata?: ProviderMetadata
|
||||||
|
}
|
||||||
readonly lifecycle: Lifecycle.State
|
readonly lifecycle: Lifecycle.State
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -763,6 +769,10 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes
|
||||||
tools: ToolStream.start(state.tools, event.index, {
|
tools: ToolStream.start(state.tools, event.index, {
|
||||||
id: block.id ?? String(event.index),
|
id: block.id ?? String(event.index),
|
||||||
name: block.name ?? "",
|
name: block.name ?? "",
|
||||||
|
input:
|
||||||
|
block.input !== undefined && (!ProviderShared.isRecord(block.input) || Object.keys(block.input).length > 0)
|
||||||
|
? ProviderShared.encodeJson(block.input)
|
||||||
|
: undefined,
|
||||||
providerExecuted: block.type === "server_tool_use",
|
providerExecuted: block.type === "server_tool_use",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
@ -777,20 +787,31 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.type === "text" && block.text) {
|
if (block.type === "text" && block.text !== undefined) {
|
||||||
const events: LLMEvent[] = []
|
const events: LLMEvent[] = []
|
||||||
|
const id = `text-${event.index ?? 0}`
|
||||||
|
const lifecycle = Lifecycle.textStart(state.lifecycle, events, id)
|
||||||
return [
|
return [
|
||||||
{ ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, block.text) },
|
{ ...state, lifecycle: block.text ? Lifecycle.textDelta(lifecycle, events, id, block.text) : lifecycle },
|
||||||
events,
|
events,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.type === "thinking" && block.thinking) {
|
if (block.type === "thinking" && block.thinking !== undefined) {
|
||||||
const events: LLMEvent[] = []
|
const events: LLMEvent[] = []
|
||||||
|
const id = `reasoning-${event.index ?? 0}`
|
||||||
|
const providerMetadata = block.signature === undefined ? undefined : anthropicMetadata({ signature: block.signature })
|
||||||
|
const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, providerMetadata)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...state,
|
...state,
|
||||||
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, block.thinking),
|
lifecycle: block.thinking
|
||||||
|
? Lifecycle.reasoningDelta(lifecycle, events, id, block.thinking, providerMetadata)
|
||||||
|
: lifecycle,
|
||||||
|
reasoningSignatures:
|
||||||
|
event.index === undefined || block.signature === undefined
|
||||||
|
? state.reasoningSignatures
|
||||||
|
: { ...state.reasoningSignatures, [event.index]: block.signature },
|
||||||
},
|
},
|
||||||
events,
|
events,
|
||||||
]
|
]
|
||||||
|
|
@ -799,7 +820,7 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes
|
||||||
// Redacted thinking surfaces as an empty reasoning part carrying the opaque
|
// Redacted thinking surfaces as an empty reasoning part carrying the opaque
|
||||||
// payload as `redactedData` metadata (same model as Vercel's
|
// payload as `redactedData` metadata (same model as Vercel's
|
||||||
// @ai-sdk/anthropic). The existing content_block_stop closes the part.
|
// @ai-sdk/anthropic). The existing content_block_stop closes the part.
|
||||||
if (block.type === "redacted_thinking" && block.data) {
|
if (block.type === "redacted_thinking" && block.data !== undefined) {
|
||||||
const events: LLMEvent[] = []
|
const events: LLMEvent[] = []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -847,18 +868,13 @@ const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(f
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delta?.type === "signature_delta" && delta.signature) {
|
if (delta?.type === "signature_delta" && delta.signature) {
|
||||||
const events: LLMEvent[] = []
|
const index = event.index ?? 0
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...state,
|
...state,
|
||||||
lifecycle: Lifecycle.reasoningEnd(
|
reasoningSignatures: { ...state.reasoningSignatures, [index]: delta.signature },
|
||||||
state.lifecycle,
|
|
||||||
events,
|
|
||||||
`reasoning-${event.index ?? 0}`,
|
|
||||||
anthropicMetadata({ signature: delta.signature }),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
events,
|
NO_EVENTS,
|
||||||
] satisfies StepResult
|
] satisfies StepResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -889,31 +905,53 @@ const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(fun
|
||||||
const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index)
|
const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index)
|
||||||
const events: LLMEvent[] = []
|
const events: LLMEvent[] = []
|
||||||
const resultEvents = result.events ?? []
|
const resultEvents = result.events ?? []
|
||||||
|
const signature = state.reasoningSignatures[event.index]
|
||||||
const lifecycle = resultEvents.length
|
const lifecycle = resultEvents.length
|
||||||
? Lifecycle.stepStart(state.lifecycle, events)
|
? Lifecycle.stepStart(state.lifecycle, events)
|
||||||
: Lifecycle.reasoningEnd(
|
: Lifecycle.reasoningEnd(
|
||||||
Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
|
Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
|
||||||
events,
|
events,
|
||||||
`reasoning-${event.index}`,
|
`reasoning-${event.index}`,
|
||||||
|
signature === undefined ? undefined : anthropicMetadata({ signature }),
|
||||||
)
|
)
|
||||||
events.push(...resultEvents)
|
events.push(...resultEvents)
|
||||||
return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
|
const reasoningSignatures = { ...state.reasoningSignatures }
|
||||||
|
delete reasoningSignatures[event.index]
|
||||||
|
return [{ ...state, lifecycle, tools: result.tools, reasoningSignatures }, events] satisfies StepResult
|
||||||
})
|
})
|
||||||
|
|
||||||
const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => {
|
const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => {
|
||||||
const usage = mergeUsage(state.usage, mapUsage(event.usage))
|
const usage = mergeUsage(state.usage, mapUsage(event.usage))
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
usage,
|
||||||
|
pendingFinish: {
|
||||||
|
reason: {
|
||||||
|
normalized: mapFinishReason(event.delta?.stop_reason),
|
||||||
|
raw: event.delta?.stop_reason ?? undefined,
|
||||||
|
},
|
||||||
|
providerMetadata:
|
||||||
|
event.delta?.stop_sequence === null || event.delta?.stop_sequence === undefined
|
||||||
|
? undefined
|
||||||
|
: anthropicMetadata({ stopSequence: event.delta.stop_sequence }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NO_EVENTS,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMessageStop = (state: ParserState): StepResult => {
|
||||||
const events: LLMEvent[] = []
|
const events: LLMEvent[] = []
|
||||||
const lifecycle = Lifecycle.finish(state.lifecycle, events, {
|
const lifecycle = Lifecycle.finish(state.lifecycle, events, {
|
||||||
reason: {
|
reason: state.pendingFinish?.reason ?? {
|
||||||
normalized: mapFinishReason(event.delta?.stop_reason),
|
normalized: "unknown",
|
||||||
raw: event.delta?.stop_reason ?? undefined,
|
raw: undefined,
|
||||||
},
|
},
|
||||||
usage,
|
usage: state.usage,
|
||||||
providerMetadata: event.delta?.stop_sequence
|
providerMetadata: state.pendingFinish?.providerMetadata,
|
||||||
? anthropicMetadata({ stopSequence: event.delta.stop_sequence })
|
|
||||||
: undefined,
|
|
||||||
})
|
})
|
||||||
return [{ ...state, lifecycle, usage }, events]
|
return [{ ...state, lifecycle }, events]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefix `error.type` so overloads, rate limits, and quota errors are visible
|
// Prefix `error.type` so overloads, rate limits, and quota errors are visible
|
||||||
|
|
@ -938,6 +976,7 @@ const step = (state: ParserState, event: AnthropicEvent) => {
|
||||||
if (event.type === "content_block_delta") return onContentBlockDelta(state, event)
|
if (event.type === "content_block_delta") return onContentBlockDelta(state, event)
|
||||||
if (event.type === "content_block_stop") return onContentBlockStop(state, event)
|
if (event.type === "content_block_stop") return onContentBlockStop(state, event)
|
||||||
if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event))
|
if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event))
|
||||||
|
if (event.type === "message_stop") return Effect.succeed(onMessageStop(state))
|
||||||
if (event.type === "error") return onError(event)
|
if (event.type === "error") return onError(event)
|
||||||
return Effect.succeed<StepResult>([state, NO_EVENTS])
|
return Effect.succeed<StepResult>([state, NO_EVENTS])
|
||||||
}
|
}
|
||||||
|
|
@ -958,7 +997,11 @@ export const protocol = Protocol.make({
|
||||||
},
|
},
|
||||||
stream: {
|
stream: {
|
||||||
event: Protocol.jsonEvent(AnthropicEvent),
|
event: Protocol.jsonEvent(AnthropicEvent),
|
||||||
initial: () => ({ tools: ToolStream.empty<number>(), lifecycle: Lifecycle.initial() }),
|
initial: () => ({
|
||||||
|
tools: ToolStream.empty<number>(),
|
||||||
|
reasoningSignatures: {},
|
||||||
|
lifecycle: Lifecycle.initial(),
|
||||||
|
}),
|
||||||
step,
|
step,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,19 @@ export const stepStart = (state: State, events: LLMEvent[]): State => {
|
||||||
return { ...state, stepStarted: true }
|
return { ...state, stepStarted: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => {
|
export const textStart = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => {
|
||||||
|
if (state.text.has(id)) return state
|
||||||
const stepped = stepStart(state, events)
|
const stepped = stepStart(state, events)
|
||||||
if (stepped.text.has(id)) {
|
events.push(LLMEvent.textStart({ id, providerMetadata }))
|
||||||
events.push(LLMEvent.textDelta({ id, text }))
|
|
||||||
return stepped
|
|
||||||
}
|
|
||||||
events.push(LLMEvent.textStart({ id }), LLMEvent.textDelta({ id, text }))
|
|
||||||
return { ...stepped, text: new Set([...stepped.text, id]) }
|
return { ...stepped, text: new Set([...stepped.text, id]) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => {
|
||||||
|
const started = textStart(state, events, id)
|
||||||
|
events.push(LLMEvent.textDelta({ id, text }))
|
||||||
|
return started
|
||||||
|
}
|
||||||
|
|
||||||
export const reasoningStart = (
|
export const reasoningStart = (
|
||||||
state: State,
|
state: State,
|
||||||
events: LLMEvent[],
|
events: LLMEvent[],
|
||||||
|
|
|
||||||
|
|
@ -506,6 +506,7 @@ describe("Anthropic Messages route", () => {
|
||||||
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
|
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
|
||||||
providerMetadata: { anthropic: { signature: "sig_1" } },
|
providerMetadata: { anthropic: { signature: "sig_1" } },
|
||||||
})
|
})
|
||||||
|
expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toBeUndefined()
|
||||||
expect(response.message.content).toEqual([
|
expect(response.message.content).toEqual([
|
||||||
{ type: "text", text: "Hello!" },
|
{ type: "text", text: "Hello!" },
|
||||||
{ type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
{ type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||||
|
|
@ -518,6 +519,139 @@ describe("Anthropic Messages route", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("requires message_stop before completing a streamed message", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const error = yield* LLMClient.generate(request).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
fixedResponse(
|
||||||
|
sseEvents(
|
||||||
|
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||||
|
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
|
||||||
|
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello" } },
|
||||||
|
{ type: "content_block_stop", index: 0 },
|
||||||
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.flip,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(error.reason).toMatchObject({
|
||||||
|
_tag: "InvalidProviderOutput",
|
||||||
|
message: "Provider stream ended without a terminal finish event",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("round-trips omitted thinking carried only by a signature delta", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const response = yield* LLMClient.generate(request).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
fixedResponse(
|
||||||
|
sseEvents(
|
||||||
|
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||||
|
{
|
||||||
|
type: "content_block_start",
|
||||||
|
index: 0,
|
||||||
|
content_block: { type: "thinking", thinking: "", signature: "" },
|
||||||
|
},
|
||||||
|
{ type: "content_block_delta", index: 0, delta: { type: "signature_delta", signature: "sig_1" } },
|
||||||
|
{ type: "content_block_stop", index: 0 },
|
||||||
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.message.content).toEqual([
|
||||||
|
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||||
|
])
|
||||||
|
|
||||||
|
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||||
|
LLM.request({ model, messages: [response.message], cache: "none" }),
|
||||||
|
)
|
||||||
|
expect(prepared.body.messages).toEqual([
|
||||||
|
{ role: "assistant", content: [{ type: "thinking", thinking: "", signature: "sig_1" }] },
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("retains a thinking signature supplied in content_block_start", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const response = yield* LLMClient.generate(request).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
fixedResponse(
|
||||||
|
sseEvents(
|
||||||
|
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||||
|
{
|
||||||
|
type: "content_block_start",
|
||||||
|
index: 0,
|
||||||
|
content_block: { type: "thinking", thinking: "", signature: "sig_1" },
|
||||||
|
},
|
||||||
|
{ type: "content_block_stop", index: 0 },
|
||||||
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.message.content).toEqual([
|
||||||
|
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||||
|
])
|
||||||
|
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
|
||||||
|
providerMetadata: { anthropic: { signature: "sig_1" } },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("retains complete tool input from content_block_start", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const response = yield* LLMClient.generate(request).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
fixedResponse(
|
||||||
|
sseEvents(
|
||||||
|
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||||
|
{
|
||||||
|
type: "content_block_start",
|
||||||
|
index: 0,
|
||||||
|
content_block: { type: "tool_use", id: "call_1", name: "lookup", input: { query: "weather" } },
|
||||||
|
},
|
||||||
|
{ type: "content_block_stop", index: 0 },
|
||||||
|
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.toolCalls).toMatchObject([
|
||||||
|
{ id: "call_1", name: "lookup", input: { query: "weather" } },
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("retains empty text blocks", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const response = yield* LLMClient.generate(request).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
fixedResponse(
|
||||||
|
sseEvents(
|
||||||
|
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||||
|
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
|
||||||
|
{ type: "content_block_stop", index: 0 },
|
||||||
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.message.content).toEqual([{ type: "text", text: "" }])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("parses redacted thinking into empty reasoning with redactedData metadata", () =>
|
it.effect("parses redacted thinking into empty reasoning with redactedData metadata", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = sseEvents(
|
const body = sseEvents(
|
||||||
|
|
@ -629,6 +763,7 @@ describe("Anthropic Messages route", () => {
|
||||||
delta: { stop_reason: "model_context_window_exceeded" },
|
delta: { stop_reason: "model_context_window_exceeded" },
|
||||||
usage: { output_tokens: 1 },
|
usage: { output_tokens: 1 },
|
||||||
},
|
},
|
||||||
|
{ type: "message_stop" },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -646,6 +781,7 @@ describe("Anthropic Messages route", () => {
|
||||||
sseEvents(
|
sseEvents(
|
||||||
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||||
{ type: "message_delta", delta: { stop_reason: "pause_turn" }, usage: { output_tokens: 1 } },
|
{ type: "message_delta", delta: { stop_reason: "pause_turn" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -664,6 +800,7 @@ describe("Anthropic Messages route", () => {
|
||||||
{ type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: ':"weather"}' } },
|
{ type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: ':"weather"}' } },
|
||||||
{ type: "content_block_stop", index: 0 },
|
{ type: "content_block_stop", index: 0 },
|
||||||
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } },
|
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
)
|
)
|
||||||
const response = yield* LLMClient.generate(
|
const response = yield* LLMClient.generate(
|
||||||
LLMRequest.update(request, {
|
LLMRequest.update(request, {
|
||||||
|
|
@ -849,6 +986,7 @@ describe("Anthropic Messages route", () => {
|
||||||
{ type: "content_block_delta", index: 2, delta: { type: "text_delta", text: "Found it." } },
|
{ type: "content_block_delta", index: 2, delta: { type: "text_delta", text: "Found it." } },
|
||||||
{ type: "content_block_stop", index: 2 },
|
{ type: "content_block_stop", index: 2 },
|
||||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } },
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
)
|
)
|
||||||
const response = yield* LLMClient.generate(
|
const response = yield* LLMClient.generate(
|
||||||
LLMRequest.update(request, {
|
LLMRequest.update(request, {
|
||||||
|
|
@ -912,6 +1050,7 @@ describe("Anthropic Messages route", () => {
|
||||||
},
|
},
|
||||||
{ type: "content_block_stop", index: 1 },
|
{ type: "content_block_stop", index: 1 },
|
||||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
)
|
)
|
||||||
const response = yield* LLMClient.generate(
|
const response = yield* LLMClient.generate(
|
||||||
LLMRequest.update(request, {
|
LLMRequest.update(request, {
|
||||||
|
|
|
||||||
|
|
@ -539,6 +539,7 @@ describe("LLMClient tools", () => {
|
||||||
},
|
},
|
||||||
{ type: "content_block_stop", index: 1 },
|
{ type: "content_block_stop", index: 1 },
|
||||||
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 5 } },
|
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 5 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
)
|
)
|
||||||
: sseEvents(
|
: sseEvents(
|
||||||
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||||
|
|
@ -546,6 +547,7 @@ describe("LLMClient tools", () => {
|
||||||
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Done." } },
|
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Done." } },
|
||||||
{ type: "content_block_stop", index: 0 },
|
{ type: "content_block_stop", index: 0 },
|
||||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
),
|
),
|
||||||
{ headers: { "content-type": "text/event-stream" } },
|
{ headers: { "content-type": "text/event-stream" } },
|
||||||
)
|
)
|
||||||
|
|
@ -801,6 +803,7 @@ describe("LLMClient tools", () => {
|
||||||
{ type: "content_block_delta", index: 2, delta: { type: "text_delta", text: "Done." } },
|
{ type: "content_block_delta", index: 2, delta: { type: "text_delta", text: "Done." } },
|
||||||
{ type: "content_block_stop", index: 2 },
|
{ type: "content_block_stop", index: 2 },
|
||||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } },
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } },
|
||||||
|
{ type: "message_stop" },
|
||||||
),
|
),
|
||||||
{ headers: { "content-type": "text/event-stream" } },
|
{ headers: { "content-type": "text/event-stream" } },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue