Skip to content

feat: support OpenAI Responses API on Claude upstream channels - #6707

Open
margrop wants to merge 3 commits into
QuantumNous:mainfrom
margrop:feat/responses-to-claude
Open

feat: support OpenAI Responses API on Claude upstream channels#6707
margrop wants to merge 3 commits into
QuantumNous:mainfrom
margrop:feat/responses-to-claude

Conversation

@margrop

@margrop margrop commented Aug 8, 2026

Copy link
Copy Markdown

Problem

A client calling POST /v1/responses that gets routed to a Claude (type 14) channel fails with:

500 not implemented (code: convert_request_failed)

Root cause: relay/channel/claude/adaptor.go ConvertOpenAIResponsesRequest is a // TODO implement me stub returning errors.New("not implemented").

This blocks Responses-only clients — notably Codex CLI ≥ 0.142, which removed wire_api="chat" and hard-requires the Responses API — from using Claude-backed channels (e.g. as a fallback when OpenAI channels are down).

Key observation

relaykit/relayconvert already registers a complete, bidirectional openai_responses ↔ claude_messages converter (text_converter_registry.go, incl. streaming chunk converters). The conversion logic exists — it is simply never invoked for this path.

Approach: reuse the existing Claude response pipeline

Rather than reimplementing the stub (which only handles the request direction and can't see the response context needed for streaming), this PR wires the existing converter into the Claude relay pipeline, mirroring the existing RelayFormatOpenAI handling:

  1. relay/responses_handler.go — when the selected channel is a Claude API type (constant.APITypeAnthropic) and pass-through is off, convert the Responses request to Claude Messages via service.ConvertRequest(..., types.RelayFormatClaude, ...) and forward it, keeping info.RelayFormat = RelayFormatOpenAIResponses.
  2. HandleClaudeResponseData — add a RelayFormatOpenAIResponses branch converting the upstream Claude JSON back to a Responses response via relayconvert.ConvertResponse.
  3. ClaudeResponsesStreamHandler (new) — convert the upstream Claude SSE stream to Responses SSE via relayconvert.ConvertStreamResponseChunk, reusing the existing usage/billing/tool-call accounting (FormatClaudeResponseInfo, countClaudeStreamBillableTools).

Existing /v1/messages (Claude inbound) and RelayFormatOpenAI behavior is unchanged — the new path activates only when a Responses-format request is routed to a Claude channel. The channel.Adaptor interface is untouched.

Testing

  • Unit tests (relay/channel/claude/relay_responses_test.go): non-streaming Claude→Responses conversion and streaming Claude SSE→Responses SSE both pass; full ./relay/... suite passes with no regressions.
  • End-to-end: built the binary, ran a throwaway instance with a Claude channel pointing at a live Claude Messages upstream, and drove real /v1/responses requests:
    • non-streaming → returns {"object":"response", ... "output_text":"ok"}, consume log shows request_conversion:["OpenAI Responses","Claude Messages"]
    • streaming → emits response.created / response.output_item.added / ...delta SSE events, stream_status: ok

Scope

Claude (type 14) channels only. OpenAI-Chat (type 1) and Gemini upstreams are out of scope (Gemini already has its own Responses bridge).

Summary by CodeRabbit

  • New Features

    • Added support for using the OpenAI Responses format with Claude.
    • Claude responses can now be converted to OpenAI Responses for standard and streaming requests.
    • Streaming responses include response events, metadata, usage information, and tool billing data.
    • Responses-format requests are automatically converted for Claude while preserving existing behavior for other request types.
  • Bug Fixes

    • Improved propagation of conversion and streaming errors.
    • Added handling for streaming timeouts and cleanup.

A client calling POST /v1/responses that is routed to a Claude (type 14)
channel previously failed with '500 not implemented (convert_request_failed)',
because the Claude adaptor's ConvertOpenAIResponsesRequest is an unimplemented
stub. This blocks Responses-only clients (e.g. Codex CLI >= 0.142, which no
longer supports wire_api=chat) from using Claude-backed channels.

The relaykit/relayconvert package already registers a complete bidirectional
openai_responses <-> claude_messages converter; it was simply never invoked for
this path. This change wires it up by reusing the existing Claude response
pipeline:

- responses_handler: when the selected channel is a Claude API type (and
  pass-through is off), convert the Responses request to Claude Messages via
  service.ConvertRequest and forward it, keeping info.RelayFormat =
  RelayFormatOpenAIResponses.
- HandleClaudeResponseData: add a RelayFormatOpenAIResponses branch that
  converts the upstream Claude JSON back to a Responses response.
- ClaudeResponsesStreamHandler (new): convert the upstream Claude SSE stream to
  Responses SSE, reusing the existing usage/billing/tool accounting.

Existing /v1/messages (Claude inbound) and RelayFormatOpenAI behavior is
unchanged; the new path activates only for Responses requests on Claude
channels.

Verified: unit tests for both non-streaming and streaming conversion, and an
end-to-end run driving real streaming + non-streaming /v1/responses requests
through a Claude channel against a live Claude Messages upstream.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74ca4d49-ef89-4e17-bcdc-c1eb14a0a421

📥 Commits

Reviewing files that changed from the base of the PR and between 615d279 and fac5411.

📒 Files selected for processing (1)
  • relay/channel/claude/relay_responses_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • relay/channel/claude/relay_responses_test.go

Walkthrough

Claude relay support converts Anthropic requests and Claude responses for the OpenAI Responses format. It supports non-streaming payloads and streaming SSE events with usage accounting, response IDs, serialization, terminal events, and error handling.

Changes

Claude OpenAI Responses support

Layer / File(s) Summary
Anthropic request conversion
relay/responses_handler.go
Anthropic Responses requests are converted to validated Claude requests and serialized before upstream processing. Other API types retain the adaptor conversion path.
Non-streaming response conversion
relay/channel/claude/relay-claude.go, relay/channel/claude/relay_responses_test.go
Claude responses are converted to OpenAI Responses payloads. The conversion validates the payload type, restores the response ID, serializes the result, and records token usage.
Streaming response conversion
relay/channel/claude/adaptor.go, relay/channel/claude/relay-claude.go, relay/channel/claude/relay_responses_test.go
OpenAI Responses streaming requests use ClaudeResponsesStreamHandler. The handler converts Claude SSE events, tracks usage and billable tools, emits terminal events, propagates errors, and validates the emitted stream.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OpenAIResponsesClient
  participant ClaudeAdaptor
  participant ClaudeResponsesStreamHandler
  participant ClaudeUpstream
  OpenAIResponsesClient->>ClaudeAdaptor: Send streaming Responses request
  ClaudeAdaptor->>ClaudeResponsesStreamHandler: Select Responses stream handler
  ClaudeResponsesStreamHandler->>ClaudeUpstream: Read Claude SSE data
  ClaudeUpstream-->>ClaudeResponsesStreamHandler: Return Claude events and usage
  ClaudeResponsesStreamHandler-->>OpenAIResponsesClient: Write serialized Responses events
Loading

Possibly related issues

Possibly related PRs

Poem

A rabbit follows Claude’s stream,
As Responses events gleam.
IDs and usage hop in line,
Then completion marks the sign.
SSE settles, neat and bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes adding OpenAI Responses API support for Claude upstream channels.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/channel/claude/relay_responses_test.go`:
- Around line 48-50: Strengthen both response contract tests in
relay/channel/claude/relay_responses_test.go:48-50 and
relay/channel/claude/relay_responses_test.go:92-94. In the non-streaming test,
assert the response ID, converted output text/items, and usage fields; in the
SSE test, parse each payload and assert the response ID, text delta, and final
usage or completion data, using the expected converted values.

In `@relay/responses_handler.go`:
- Around line 89-107: Update ResponsesHelper’s pass-through eligibility check to
exclude constant.APITypeAnthropic whenever either pass-through flag is enabled.
Ensure Anthropic requests always reach the APITypeAnthropic branch, where
ConvertRequest produces a dto.ClaudeRequest before forwarding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 515e50ac-938e-487a-84ed-269bac25324d

📥 Commits

Reviewing files that changed from the base of the PR and between 5c3abff and e9e8778.

📒 Files selected for processing (4)
  • relay/channel/claude/adaptor.go
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_responses_test.go
  • relay/responses_handler.go

Comment thread relay/channel/claude/relay_responses_test.go
Comment thread relay/responses_handler.go
- responses_handler: exclude APITypeAnthropic from the pass-through path so a
  raw /v1/responses body is never forwarded to the Claude Messages upstream when
  pass-through is enabled (CodeRabbit major).
- ClaudeResponsesStreamHandler: call relayconvert.FinalizeStreamResponse after
  the stream ends so the terminal response.completed event (with final usage) is
  emitted; previously the stream never signalled completion to the client.
- relay_responses_test: strengthen both conversion tests to assert the response
  ID, converted output text/items, and usage fields; parse SSE payloads and
  assert the response ID, joined text deltas, and terminal usage (CodeRabbit
  minor).
@margrop

margrop commented Aug 8, 2026

Copy link
Copy Markdown
Author

Both CodeRabbit review comments addressed in commit 615d279:

  1. Major — pass-through guard (relay/responses_handler.go): the pass-through eligibility check now excludes constant.APITypeAnthropic, so a raw /v1/responses body is never forwarded to the Claude Messages upstream when pass-through is enabled.

  2. Minor — stronger tests (relay_responses_test.go): both conversion tests now assert the response ID, converted output text/items, and usage fields; the SSE test parses payloads and asserts the response ID, joined text deltas, and terminal usage.

Strengthening the streaming test surfaced one more real issue in the same commit: ClaudeResponsesStreamHandler never emitted the terminal response.completed event. Added a relayconvert.FinalizeStreamResponse call (mirroring openai/responses_via_chat.go) so the stream now signals completion with final usage.

Validation: both conversion tests pass, full ./relay/... suite green, go vet and gofmt clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/channel/claude/relay_responses_test.go`:
- Around line 121-166: Update the SSE event parsing assertions in the test to
record each event’s position while iterating through blocks. Assert that
response.created occurs before response.output_text.delta and response.completed
occurs after the output events, while preserving the existing payload and
content assertions.
- Around line 167-169: Add an assertion in the terminal usage checks for
finalUsage.TotalTokens, verifying it equals 15 alongside the existing
PromptTokens and CompletionTokens assertions in the response completion test.
- Around line 135-154: In the response event parsing test, replace the visible
json.Unmarshal and json.Marshal calls with the centralized common.Unmarshal and
common.Marshal wrappers. Preserve explicit assertion failures for both marshal
and unmarshal errors, including the existing SSE data validation and usage
conversion checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0688bfda-a724-461a-a63e-d44009b11d82

📥 Commits

Reviewing files that changed from the base of the PR and between e9e8778 and 615d279.

📒 Files selected for processing (3)
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_responses_test.go
  • relay/responses_handler.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • relay/responses_handler.go
  • relay/channel/claude/relay-claude.go

Comment thread relay/channel/claude/relay_responses_test.go Outdated
Comment thread relay/channel/claude/relay_responses_test.go Outdated
Comment thread relay/channel/claude/relay_responses_test.go
…json wrappers

Address follow-up review:
- record SSE event positions and assert response.created precedes the output
  deltas and response.completed follows them
- assert the terminal response.completed carries 15 total tokens
- replace json.Unmarshal/json.Marshal with the centralized
  common.Unmarshal/common.Marshal wrappers (with hard assertion failures)
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.

1 participant