Skip to content

Preserve Anthropic thinking signatures by routing thinking-capable /messages requests through Responses API - #1

Merged
Godzilla675 merged 5 commits into
responses-api-supportfrom
copilot/add-reasoning-encrypted-content
Mar 20, 2026
Merged

Godzilla675 merged 5 commits into
responses-api-supportfrom
copilot/add-reasoning-encrypted-content

Conversation

Copilot AI commented Mar 20, 2026 •

Copy link
Copy Markdown
  • Inspect the review threads, recent commits, and affected messages/Responses translation files
  • Fix duplicated tool argument deltas in Responses stream translation
  • Make Responses stream block indices monotonic when interleaved blocks reopen
  • Normalize Anthropic model names on the Responses path the same way as the chat-completions path
  • Redact/truncate sensitive Responses-path debug logging
  • Add focused regression tests for stream and model-normalization behavior
  • Run targeted lint/tests for the reviewed messages/Responses changes
  • Run final full validation, automated review, and security checks

⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

Copilot AI and others added 2 commits March 20, 2026 15:54
Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com>
Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix responses API to include reasoning and thought signatures Preserve Anthropic thinking signatures by routing thinking-capable /messages requests through Responses API Mar 20, 2026
Copilot AI requested a review from Godzilla675 March 20, 2026 16:00
@Godzilla675
Godzilla675 marked this pull request as ready for review March 20, 2026 16:16
Copilot AI review requested due to automatic review settings March 20, 2026 16:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Routes Anthropic /messages requests that involve thinking state through the Copilot Responses API so encrypted reasoning signatures can be round-tripped (including streaming), preserving continuity across turns.

Changes:

  • Added Anthropic ↔ Responses translation for signed thinking blocks via Responses reasoning items (including include: ["reasoning.encrypted_content"]).
  • Added Responses streaming event translation into Anthropic SSE thinking/signature deltas.
  • Updated /messages handler to conditionally use Responses API, and extended Responses typings + added tests.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/messages-responses-translation.test.ts Adds coverage for request/response and streaming translation of reasoning/thinking signatures.
src/services/copilot/create-responses.ts Extends Responses payload/types to include reasoning input/output and include.
src/routes/messages/responses-translation.ts Implements non-streaming Anthropic ↔ Responses translation, including signature parsing/building.
src/routes/messages/responses-stream-translation.ts Implements Responses stream → Anthropic SSE translation for thinking/text/tools.
src/routes/messages/handler.ts Routes thinking-capable /messages requests through Responses; wires translators + streaming.
src/routes/messages/anthropic-types.ts Extends Anthropic thinking block type to optionally include a signature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +254 to +319
function handleToolArgumentsDelta(
parsedEvent: {
output_index?: unknown
delta?: unknown
},
state: ResponsesStreamState,
): Array<AnthropicStreamEventData> {
if (
typeof parsedEvent.output_index !== "number"
|| typeof parsedEvent.delta !== "string"
) {
return []
}

const blockIndex = state.blockIndexByKey.get(
`tool:${parsedEvent.output_index}`,
)
if (blockIndex === undefined) {
return []
}

return [
{
type: "content_block_delta",
index: blockIndex,
delta: {
type: "input_json_delta",
partial_json: parsedEvent.delta,
},
},
]
}

function handleToolArgumentsDone(
parsedEvent: {
output_index?: unknown
arguments?: unknown
},
state: ResponsesStreamState,
): Array<AnthropicStreamEventData> {
if (
typeof parsedEvent.output_index !== "number"
|| typeof parsedEvent.arguments !== "string"
) {
return []
}

const blockIndex = state.blockIndexByKey.get(
`tool:${parsedEvent.output_index}`,
)
if (blockIndex === undefined || state.blockHasDelta.has(blockIndex)) {
return []
}

state.blockHasDelta.add(blockIndex)
return [
{
type: "content_block_delta",
index: blockIndex,
delta: {
type: "input_json_delta",
partial_json: parsedEvent.arguments,
},
},
]
}

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleToolArgumentsDelta emits input_json_delta events but never marks the tool block as having received deltas. As a result, when response.function_call_arguments.done arrives, handleToolArgumentsDone will also emit the full arguments again, duplicating tool input in the Anthropic stream. Track deltas for tool blocks (e.g., add the block index to blockHasDelta in the delta handler, or use a dedicated set for tool-arg deltas) so .done only backfills when no deltas were seen.

Copilot uses AI. Check for mistakes.
Comment on lines +365 to +400
function openBlock(params: {
state: ResponsesStreamState
key: string
contentBlock:
| { type: "thinking"; thinking: string }
| { type: "text"; text: string }
| {
type: "tool_use"
id: string
name: string
input: Record<string, unknown>
}
events: Array<AnthropicStreamEventData>
}): number {
const { state, key, contentBlock, events } = params
let blockIndex = state.blockIndexByKey.get(key)
if (blockIndex === undefined) {
blockIndex = state.nextContentBlockIndex
state.nextContentBlockIndex += 1
state.blockIndexByKey.set(key, blockIndex)
}

if (state.openBlockKey === key) {
return blockIndex
}

closeOpenBlock(state, events)
events.push({
type: "content_block_start",
index: blockIndex,
content_block: contentBlock,
})
state.openBlockKey = key
state.openBlockIndex = blockIndex
return blockIndex
}

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openBlock reuses the same index for a given key even after a content_block_stop, which means interleaved Responses events can cause multiple content_block_start events for the same index (e.g., switching from thinking:0 to text:1:0 and back). The existing chat-completions stream translator only ever increments indices and never restarts an earlier index; emitting multiple starts for the same index is likely incompatible with Anthropic’s streaming protocol and can break clients. Consider buffering/reordering interleaved deltas or allocating a new monotonically increasing index each time a block is (re)started.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +40
return {
model: payload.model,
input,
stream: payload.stream,

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

translateAnthropicToResponses forwards payload.model directly, but the existing /messages → chat-completions translation normalizes certain Claude subagent model variants because Copilot doesn’t support them (see translateModelName in non-stream-translation.ts). Without similar normalization here, the same request may succeed on the chat-completions path but fail when routed to Responses. Reuse the same model normalization logic (or centralize it) before calling the Responses API.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +98
const responsesPayload = translateAnthropicToResponses(anthropicPayload)
consola.debug(
"Translated Responses payload:",
JSON.stringify(responsesPayload),
)

const response = await createResponses(responsesPayload)

if (isNonStreamingResponse(response)) {
consola.debug(
"Non-streaming response from Responses API:",
JSON.stringify(response).slice(-400),
)
const anthropicResponse = translateResponsesToAnthropic(response)
consola.debug(
"Translated Anthropic response:",
JSON.stringify(anthropicResponse),
)
return c.json(anthropicResponse)
}

consola.debug("Streaming response from Responses API")
return streamSSE(c, async (stream) => {
const streamState = createResponsesStreamState()

for await (const rawEvent of response) {
consola.debug("Responses raw stream event:", JSON.stringify(rawEvent))
if (!rawEvent.data || rawEvent.data === "[DONE]") {
continue
}

const events = translateResponsesStreamEvent(
JSON.parse(rawEvent.data) as {
type?: string
[key: string]: unknown
},
streamState,
)

for (const event of events) {
consola.debug("Translated Anthropic event:", JSON.stringify(event))
await stream.writeSSE({
event: event.type,
data: JSON.stringify(event),
})

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new debug logs in the Responses path print full request/stream payloads, including reasoning.encrypted_content / thinking signatures and potentially user content or base64 images. These values are effectively replayable state and can be sensitive; logging entire events can also be very noisy in production. Consider redacting encrypted_content/signature fields and truncating payloads (similar to existing .slice(-400) usage) or gating these logs behind an explicit verbose/debug flag.

Copilot uses AI. Check for mistakes.
@Godzilla675

Copy link
Copy Markdown
Owner

@copilot address code review

Copilot AI and others added 2 commits March 20, 2026 16:38
Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com>
Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com>

Copilot AI commented Mar 20, 2026 •

Copy link
Copy Markdown
Author

@copilot address code review

Addressed in eb06825 and a1edfd5. I fixed the Responses stream review issues (tool-arg delta duplication, reopened block index reuse, Claude model normalization on the Responses path, and sensitive Responses-path debug logging), added regression coverage, and resolved the shared Responses type issue that full lint exposed afterward.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • aur.archlinux.org
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/bun bun test ion.ts est.ts nt (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Godzilla675
Godzilla675 merged commit 34cbde3 into responses-api-support Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants