Skip to content

Commit 505f648

Browse files
committed
feat: add signature and reasoning handling to responses translation and state management
1 parent 2b9733b commit 505f648

File tree

5 files changed

+200
-44
lines changed

5 files changed

+200
-44
lines changed

src/routes/messages/anthropic-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface AnthropicToolUseBlock {
5656
export interface AnthropicThinkingBlock {
5757
type: "thinking"
5858
thinking: string
59+
signature: string
5960
}
6061

6162
export type AnthropicUserContentBlock =

src/routes/messages/responses-stream-translation.ts

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface ResponsesStreamState {
1515
initialInputTokens?: number
1616
functionCallStateByOutputIndex: Map<number, FunctionCallStreamState>
1717
functionCallOutputIndexByItemId: Map<string, number>
18+
summryIndex: number
1819
}
1920

2021
type FunctionCallStreamState = {
@@ -32,6 +33,7 @@ export const createResponsesStreamState = (): ResponsesStreamState => ({
3233
blockHasDelta: new Set(),
3334
functionCallStateByOutputIndex: new Map(),
3435
functionCallOutputIndexByItemId: new Map(),
36+
summryIndex: 0,
3537
})
3638

3739
export const translateResponsesStreamEvent = (
@@ -49,12 +51,18 @@ export const translateResponsesStreamEvent = (
4951
return handleResponseCreated(rawEvent, state)
5052
}
5153

52-
case "response.reasoning_summary_text.delta":
54+
case "response.reasoning_summary_text.delta": {
55+
return handleReasoningSummaryTextDelta(rawEvent, state)
56+
}
57+
5358
case "response.output_text.delta": {
5459
return handleOutputTextDelta(rawEvent, state)
5560
}
5661

57-
case "response.reasoning_summary_part.done":
62+
case "response.reasoning_summary_part.done": {
63+
return handleReasoningSummaryPartDone(rawEvent, state)
64+
}
65+
5866
case "response.output_text.done": {
5967
return handleOutputTextDone(rawEvent, state)
6068
}
@@ -63,6 +71,10 @@ export const translateResponsesStreamEvent = (
6371
return handleOutputItemAdded(rawEvent, state)
6472
}
6573

74+
case "response.output_item.done": {
75+
return handleOutputItemDone(rawEvent, state)
76+
}
77+
6678
case "response.function_call_arguments.delta": {
6779
return handleFunctionCallArgumentsDelta(rawEvent, state)
6880
}
@@ -143,6 +155,51 @@ const handleOutputItemAdded = (
143155
return events
144156
}
145157

158+
const handleOutputItemDone = (
159+
rawEvent: Record<string, unknown>,
160+
state: ResponsesStreamState,
161+
): Array<AnthropicStreamEventData> => {
162+
const events = ensureMessageStart(state)
163+
164+
const item = isRecord(rawEvent.item) ? rawEvent.item : undefined
165+
if (!item) {
166+
return events
167+
}
168+
169+
const itemType = typeof item.type === "string" ? item.type : undefined
170+
if (itemType !== "reasoning") {
171+
return events
172+
}
173+
174+
const outputIndex = toNumber(rawEvent.output_index)
175+
const contentIndex = state.summryIndex
176+
177+
const blockIndex = openThinkingBlockIfNeeded(state, {
178+
outputIndex,
179+
contentIndex,
180+
events,
181+
})
182+
183+
const signature =
184+
typeof item.encrypted_content === "string" ? item.encrypted_content : ""
185+
186+
if (signature) {
187+
events.push({
188+
type: "content_block_delta",
189+
index: blockIndex,
190+
delta: {
191+
type: "signature_delta",
192+
signature,
193+
},
194+
})
195+
state.blockHasDelta.add(blockIndex)
196+
}
197+
198+
closeBlockIfOpen(state, blockIndex, events)
199+
200+
return events
201+
}
202+
146203
const handleFunctionCallArgumentsDelta = (
147204
rawEvent: Record<string, unknown>,
148205
state: ResponsesStreamState,
@@ -257,6 +314,71 @@ const handleOutputTextDelta = (
257314
return events
258315
}
259316

317+
const handleReasoningSummaryTextDelta = (
318+
rawEvent: Record<string, unknown>,
319+
state: ResponsesStreamState,
320+
): Array<AnthropicStreamEventData> => {
321+
const events = ensureMessageStart(state)
322+
323+
const outputIndex = toNumber(rawEvent.output_index)
324+
const contentIndex = toNumber(rawEvent.summary_index)
325+
const deltaText = typeof rawEvent.delta === "string" ? rawEvent.delta : ""
326+
327+
if (!deltaText) {
328+
return events
329+
}
330+
331+
const blockIndex = openThinkingBlockIfNeeded(state, {
332+
outputIndex,
333+
contentIndex,
334+
events,
335+
})
336+
337+
events.push({
338+
type: "content_block_delta",
339+
index: blockIndex,
340+
delta: {
341+
type: "thinking_delta",
342+
thinking: deltaText,
343+
},
344+
})
345+
state.blockHasDelta.add(blockIndex)
346+
347+
return events
348+
}
349+
350+
const handleReasoningSummaryPartDone = (
351+
rawEvent: Record<string, unknown>,
352+
state: ResponsesStreamState,
353+
): Array<AnthropicStreamEventData> => {
354+
const events = ensureMessageStart(state)
355+
356+
const outputIndex = toNumber(rawEvent.output_index)
357+
const contentIndex = toNumber(rawEvent.summary_index)
358+
state.summryIndex = contentIndex
359+
const part = isRecord(rawEvent.part) ? rawEvent.part : undefined
360+
const text = part && typeof part.text === "string" ? part.text : ""
361+
362+
const blockIndex = openThinkingBlockIfNeeded(state, {
363+
outputIndex,
364+
contentIndex,
365+
events,
366+
})
367+
368+
if (text && !state.blockHasDelta.has(blockIndex)) {
369+
events.push({
370+
type: "content_block_delta",
371+
index: blockIndex,
372+
delta: {
373+
type: "thinking_delta",
374+
thinking: text,
375+
},
376+
})
377+
}
378+
379+
return events
380+
}
381+
260382
const handleOutputTextDone = (
261383
rawEvent: Record<string, unknown>,
262384
state: ResponsesStreamState,
@@ -430,6 +552,39 @@ const openTextBlockIfNeeded = (
430552
return blockIndex
431553
}
432554

555+
const openThinkingBlockIfNeeded = (
556+
state: ResponsesStreamState,
557+
params: {
558+
outputIndex: number
559+
contentIndex: number
560+
events: Array<AnthropicStreamEventData>
561+
},
562+
): number => {
563+
const { outputIndex, contentIndex, events } = params
564+
const key = getBlockKey(outputIndex, contentIndex)
565+
let blockIndex = state.blockIndexByKey.get(key)
566+
567+
if (blockIndex === undefined) {
568+
blockIndex = state.nextContentBlockIndex
569+
state.nextContentBlockIndex += 1
570+
state.blockIndexByKey.set(key, blockIndex)
571+
}
572+
573+
if (!state.openBlocks.has(blockIndex)) {
574+
events.push({
575+
type: "content_block_start",
576+
index: blockIndex,
577+
content_block: {
578+
type: "thinking",
579+
thinking: "",
580+
},
581+
})
582+
state.openBlocks.add(blockIndex)
583+
}
584+
585+
return blockIndex
586+
}
587+
433588
const closeBlockIfOpen = (
434589
state: ResponsesStreamState,
435590
blockIndex: number,

src/routes/messages/responses-translation.ts

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type ResponseInputImage,
77
type ResponseInputItem,
88
type ResponseInputMessage,
9+
type ResponseInputReasoning,
910
type ResponseInputText,
1011
type ResponsesResult,
1112
type ResponseOutputContentBlock,
@@ -27,6 +28,7 @@ import {
2728
type AnthropicMessage,
2829
type AnthropicMessagesPayload,
2930
type AnthropicTextBlock,
31+
type AnthropicThinkingBlock,
3032
type AnthropicTool,
3133
type AnthropicToolResultBlock,
3234
type AnthropicToolUseBlock,
@@ -137,6 +139,12 @@ const translateAssistantMessage = (
137139
continue
138140
}
139141

142+
if (block.type === "thinking") {
143+
flushPendingContent("assistant", pendingContent, items)
144+
items.push(createReasoningContent(block))
145+
continue
146+
}
147+
140148
const converted = translateAssistantContentBlock(block)
141149
if (converted) {
142150
pendingContent.push(converted)
@@ -158,9 +166,6 @@ const translateUserContentBlock = (
158166
case "image": {
159167
return createImageContent(block)
160168
}
161-
case "tool_result": {
162-
return undefined
163-
}
164169
default: {
165170
return undefined
166171
}
@@ -174,12 +179,6 @@ const translateAssistantContentBlock = (
174179
case "text": {
175180
return createOutPutTextContent(block.text)
176181
}
177-
case "thinking": {
178-
return createOutPutTextContent(block.thinking)
179-
}
180-
case "tool_use": {
181-
return undefined
182-
}
183182
default: {
184183
return undefined
185184
}
@@ -230,6 +229,19 @@ const createImageContent = (
230229
image_url: `data:${block.source.media_type};base64,${block.source.data}`,
231230
})
232231

232+
const createReasoningContent = (
233+
block: AnthropicThinkingBlock,
234+
): ResponseInputReasoning => ({
235+
type: "reasoning",
236+
summary: [
237+
{
238+
type: "summary_text",
239+
text: block.thinking,
240+
},
241+
],
242+
encrypted_content: block.signature,
243+
})
244+
233245
const createFunctionToolCall = (
234246
block: AnthropicToolUseBlock,
235247
): ResponseFunctionToolCallItem => ({
@@ -376,7 +388,11 @@ const mapOutputToAnthropicContent = (
376388
case "reasoning": {
377389
const thinkingText = extractReasoningText(item)
378390
if (thinkingText.length > 0) {
379-
contentBlocks.push({ type: "thinking", thinking: thinkingText })
391+
contentBlocks.push({
392+
type: "thinking",
393+
thinking: thinkingText,
394+
signature: item.encrypted_content ?? "",
395+
})
380396
}
381397
break
382398
}
@@ -456,31 +472,11 @@ const extractReasoningText = (item: ResponseOutputReasoning): string => {
456472
segments.push(block.text)
457473
continue
458474
}
459-
460-
if (typeof block.thinking === "string") {
461-
segments.push(block.thinking)
462-
continue
463-
}
464-
465-
const reasoningValue = (block as Record<string, unknown>).reasoning
466-
if (typeof reasoningValue === "string") {
467-
segments.push(reasoningValue)
468-
}
469475
}
470476
}
471477

472-
collectFromBlocks(item.reasoning)
473478
collectFromBlocks(item.summary)
474479

475-
if (typeof item.thinking === "string") {
476-
segments.push(item.thinking)
477-
}
478-
479-
const textValue = (item as Record<string, unknown>).text
480-
if (typeof textValue === "string") {
481-
segments.push(textValue)
482-
}
483-
484480
return segments.join("").trim()
485481
}
486482

src/routes/responses/utils.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,12 @@ const getPayloadItems = (
3131
): Array<ResponseInputItem> => {
3232
const result: Array<ResponseInputItem> = []
3333

34-
const { input, instructions } = payload
34+
const { input } = payload
3535

3636
if (Array.isArray(input)) {
3737
result.push(...input)
3838
}
3939

40-
if (Array.isArray(instructions)) {
41-
result.push(...instructions)
42-
}
43-
4440
return result
4541
}
4642

0 commit comments

Comments
 (0)