Skip to content

Comments

feat(claude): Support calling Claude channels via /v1/responses#2892

Open
Redtropig wants to merge 8 commits intoQuantumNous:mainfrom
Redtropig:feat/claude-responses-compat
Open

feat(claude): Support calling Claude channels via /v1/responses#2892
Redtropig wants to merge 8 commits intoQuantumNous:mainfrom
Redtropig:feat/claude-responses-compat

Conversation

@Redtropig
Copy link

@Redtropig Redtropig commented Feb 7, 2026

(incl. OpenAI function call --> Claude tool use)
Set "ChatCompletions→Responses 兼容配置" && "Model mapping" to test.

  • Implement ConvertOpenAIResponsesRequest and DoResponsesRequest in Claude adaptor to support the experimental OpenAI Responses API.
  • Add RequestOpenAIResponses2ClaudeMessage to convert Responses inputs to Claude messages, featuring validation to skip empty text blocks causing API errors.
  • Implement response conversion logic in ResponseClaude2OpenAIResponses, correctly mapping Claude's tool_use to function_call outputs.
  • Update ResponsesResponseToChatCompletionsResponse in compatibility service to support messages containing both text and tool calls, ensuring reasoning text is not discarded when tools are used.

-> Tested working config:
image image

Summary by CodeRabbit

  • New Features

    • Added Claude backend support for OpenAI-style "responses" requests, including request/response translation, routing so "responses" mode uses the Claude pathway, and streaming (SSE) support with conversion to OpenAI-style streaming.
  • Bug Fixes

    • Improved tool/function-call handling: tool_calls are captured even when text is present and assistant content is no longer cleared.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 7, 2026

Walkthrough

Routes OpenAI "/responses" through the Claude channel: adaptor delegates request conversion to Claude-specific converter, adds a DoResponsesRequest path that proxies SSE or converts non-streamed Claude replies back to OpenAI Responses format, and adjusts tool-call extraction to include outputs regardless of assistant text.

Changes

Cohort / File(s) Summary
Claude Adaptor / Routing
relay/channel/claude/adaptor.go
Route RelayModeResponses to DoResponsesRequest and delegate ConvertOpenAIResponsesRequest to the Claude converter.
Claude Request/Response Translation
relay/channel/claude/relay-claude.go
Add RequestOpenAIResponses2ClaudeMessage, DoResponsesRequest, and ResponseClaude2OpenAIResponses: translate OpenAI Responses ↔ Claude formats, handle system/tool/tool_choice messages (including function_call/output), support SSE streaming proxying, and aggregate token usage.
OpenAI compatibility logic
service/openaicompat/responses_to_chat.go
Always collect tool_calls when len(resp.Output) > 0 and do not clear assistant content when tool_calls are present.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Adaptor as Claude Adaptor
    participant ConvReq as Converter\n(OpenAI→Claude)
    participant ClaudeAPI as Claude Service
    participant ConvResp as Converter\n(Claude→OpenAI)
    participant HTTP as HTTP Response

    Client->>Adaptor: POST /v1/responses (OpenAI format)
    Adaptor->>ConvReq: RequestOpenAIResponses2ClaudeMessage
    ConvReq-->>Adaptor: ClaudeRequest
    Adaptor->>ClaudeAPI: DoResponsesRequest (forward request, SSE or non-stream)
    ClaudeAPI->>Adaptor: SSE stream or final ClaudeResponse
    Adaptor->>ConvResp: ResponseClaude2OpenAIResponses (or stream passthrough)
    ConvResp-->>HTTP: OpenAI Responses format
    HTTP-->>Client: Response (streamed or final)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested Reviewers

  • Calcium-Ion
  • seefs001

Poem

🐰 I hopped through bytes and message streams,
Turning prompts into Claude-flavored dreams.
I stitched tools, tokens, stream and call,
So responses bounce and never fall.
Hooray — the relay sings for all!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature being added: support for calling Claude channels via the /v1/responses endpoint, which is the primary change across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
relay/channel/claude/relay-claude.go (1)

1028-1039: instructions variable escapes via pointer — consider an explicit copy.

At line 1035, Text: &instructions takes the address of a local variable parsed by common.Unmarshal. While this is safe in the current code (the variable doesn't get reassigned after the pointer is taken), it's a fragile pattern. If someone later adds code that modifies instructions, the system prompt would silently change. This is a minor robustness concern.


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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@relay/channel/claude/relay-claude.go`:
- Around line 1174-1203: The loop over claudeResponse.Content currently ignores
content.Type == "thinking" and thus drops reasoning; update the switch in the
loop that iterates claudeResponse.Content to handle "thinking" (alongside "text"
and "tool_use") by either appending a dto.ResponsesOutputContent with Type
"thinking" and the reasoning text (using content.GetText()) into
currentTextContent or into a new dedicated field on dto.ResponsesOutput (e.g. a
reasoning slice) if the Responses schema supports it, and add a debug log when
encountering "thinking" so it's observable; ensure you still flush
currentTextContent before function_call as done now and reference
variables/functions: claudeResponse.Content, currentTextContent, outputList,
dto.ResponsesOutputContent, dto.ResponsesOutput, and content.GetText().
- Around line 931-957: The code currently builds claudeTool and only appends it
to claudeTools inside the params type-assertion, which drops tools that lack a
"parameters" map; change the logic in the loop that constructs claudeTool so you
always append the claudeTool to claudeTools, but only build
claudeTool.InputSchema when params, ok := tool["parameters"].(map[string]any) is
true — leave InputSchema nil or empty for parameterless tools; locate the
creation of claudeTool and the append to claudeTools in the same loop and move
the append outside the params conditional so parameterless tools (e.g.,
get_current_time) are included.
- Around line 982-1091: The code that builds claudeMessages may leave the first
message with Role "assistant" (e.g., when the first input item is a
function_call); before assigning claudeRequest.Messages = claudeMessages add the
same first-message guard used in RequestOpenAI2ClaudeMessage: if claudeMessages
is empty or claudeMessages[0].Role != "user" then prepend a placeholder
dto.ClaudeMessage with Role "user" (empty Content) so the first Claude message
is always "user". Ensure you reference and update claudeMessages and then set
claudeRequest.Messages.
- Around line 978-1092: The code silently drops `responsesReq.Input` when it's a
plain JSON string because common.Unmarshal into []map[string]any fails; update
the parsing logic in the block that builds claudeRequest.Messages so that if
unmarshalling into inputItems fails you try to interpret responsesReq.Input as a
plain string (e.g., via common.Interface2String or unmarshalling into a string)
and, when non-empty, create a single dto.ClaudeMessage with Role "user" and
Content set to that string and assign it to claudeRequest.Messages; keep the
existing behavior for the array case (the existing loop over inputItems) and
only fall back to the string-path when array parsing fails or the value is a
string.
- Around line 907-917: The RequestOpenAIResponses2ClaudeMessage function
currently sets Model and MaxTokens but omits mapping Temperature, TopP and
Stream; update the function to copy responsesReq.Temperature ->
claudeRequest.Temperature, responsesReq.TopP -> claudeRequest.TopP, and
responsesReq.Stream -> claudeRequest.Stream (perform any necessary type
conversions or nil/zero checks consistent with existing defaults in
dto.ClaudeRequest and dto.OpenAIResponsesRequest) so caller-specified
temperature/topP and streaming preferences are honored.
- Around line 1154-1161: The ResponseClaude2OpenAIResponses function currently
sets the OpenAIResponsesResponse.Object to "chat.completion" which violates the
Responses API; change the Object field assignment in
ResponseClaude2OpenAIResponses (the dto.OpenAIResponsesResponse literal) to
"response" so the returned object conforms to the Responses API contract and
clients can parse it correctly.
🧹 Nitpick comments (3)
relay/channel/claude/relay-claude.go (3)

1126-1130: resp.Body.Close() error is ignored and not deferred.

If io.ReadAll returns an error, resp.Body is still closed (good), but the close error is silently discarded. More importantly, if a panic occurs between ReadAll and Close(), the body leaks. Using defer is safer and idiomatic.

Proposed fix
-	responseBody, err := io.ReadAll(resp.Body)
-	resp.Body.Close()
-	if err != nil {
+	defer resp.Body.Close()
+	responseBody, err := io.ReadAll(resp.Body)
+	if err != nil {
 		return nil, err
 	}

920-963: Tool conversion logic is duplicated with RequestOpenAI2ClaudeMessage (lines 50-72).

Both functions perform nearly identical tool parsing: iterate tools, extract name/description, build dto.Tool with InputSchema, and copy over extra schema fields. Consider extracting this into a shared helper to reduce duplication and ensure both paths stay consistent.


1194-1201: Silently ignored json.Marshal error could produce "null" arguments.

If content.Input is nil, json.Marshal returns []byte("null"), so Arguments becomes the string "null". This might confuse downstream consumers expecting either valid JSON object or empty string. Consider defaulting to "{}" on error/nil.

Proposed fix
-		inputJson, _ := json.Marshal(content.Input)
-		
-		outputList = append(outputList, dto.ResponsesOutput{
-			Type: "function_call",
-			CallId: content.Id,
-			Name: content.Name,
-			Arguments: string(inputJson),
-		})
+		args := "{}"
+		if content.Input != nil {
+			if inputJson, err := json.Marshal(content.Input); err == nil {
+				args = string(inputJson)
+			}
+		}
+		
+		outputList = append(outputList, dto.ResponsesOutput{
+			Type:      "function_call",
+			CallId:    content.Id,
+			Name:      content.Name,
+			Arguments: args,
+		})

…l use)

- Implement `ConvertOpenAIResponsesRequest` and `DoResponsesRequest` in Claude adaptor to support the experimental OpenAI Responses API.
- Add `RequestOpenAIResponses2ClaudeMessage` to convert Responses inputs to Claude messages, featuring validation to skip empty text blocks causing API errors.
- Implement response conversion logic in `ResponseClaude2OpenAIResponses`, correctly mapping Claude's `tool_use` to `function_call` outputs.
- Update `ResponsesResponseToChatCompletionsResponse` in compatibility service to support messages containing both text and tool calls, ensuring reasoning text is not discarded when tools are used.
@Redtropig Redtropig force-pushed the feat/claude-responses-compat branch from c24c3f8 to 4502f81 Compare February 7, 2026 22:59
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@relay/channel/claude/adaptor.go`:
- Around line 93-98: DoResponse currently always routes responses through
ClaudeHandler/ClaudeStreamHandler which expect Claude-format JSON; add a branch
checking info.RelayMode == relayconstant.RelayModeResponses in DoResponse and
handle it differently (either return the response body directly or call a
Responses-specific handler such as DoResponsesRequest/DoResponsesResponse
handler) so Responses-mode traffic is not re-parsed as Claude format; update
DoResponse to detect RelayModeResponses and short-circuit to the appropriate
pass-through or dedicated handler instead of invoking
ClaudeHandler/ClaudeStreamHandler.

In `@relay/channel/claude/relay-claude.go`:
- Around line 1118-1120: The code path that checks "if resp == nil { return nil,
nil }" (the response from channel.DoApiRequest) must not return a nil response
with a nil error; instead return a clear non-nil error so callers cannot
accidentally dereference resp (e.g., replace the nil,nil return with returning
nil and an fmt.Errorf or wrapped error describing "empty response from
channel.DoApiRequest" and include context such as request ID or method name).
Update the function in relay-claude.go that contains this check to construct and
return that error so callers must handle the nil response explicitly.
- Around line 978-1092: The input-parsing logic currently drops plain-string
responses and handles complex message content lossy by calling
common.Interface2String for non-string content; update the block that builds
dto.ClaudeMessage (the else branch that sets newMsg.Content) to preserve
structured content: detect when content is a slice or map (e.g., []any or
map[string]any) and convert it into the expected Claude content representation
(e.g., []dto.ClaudeMediaMessage or map->dto structs) instead of falling back to
common.Interface2String; ensure claudeRequest.Messages receives structured
dto.ClaudeMessage.Content for complex items, and also add a branch to accept a
top-level string responsesReq.Input (when input unmarshals to a string) so
plain-string input is not discarded.
- Around line 1112-1152: In DoResponse, add the same RelayModeResponses guard
used in DoRequest to avoid re-processing Responses-mode payloads: check
info.RelayMode == relaycommon.RelayModeResponses near the start of DoResponse
and if true, skip invoking the Claude handlers (or return the resp produced by
DoResponsesRequest unchanged); this prevents ClaudeHandler/Response parsing
logic from attempting to unmarshal the already-converted OpenAI Responses JSON
that DoResponsesRequest produced.
🧹 Nitpick comments (2)
relay/channel/claude/relay-claude.go (2)

1194-1194: Suppressed json.Marshal error could produce "null" as the arguments string.

If content.Input is nil, json.Marshal returns []byte("null"), nil — so the Arguments field will be the string "null". This matches the pattern at line 503, so it's consistent with the existing codebase, but worth noting for correctness if nil inputs are possible.


1126-1127: Consider deferring resp.Body.Close() for safety.

If io.ReadAll panics (unlikely but defensive), the body won't be closed. A defer resp.Body.Close() before ReadAll is more idiomatic.

Proposed fix
+	defer resp.Body.Close()
 	responseBody, err := io.ReadAll(resp.Body)
-	resp.Body.Close()
 	if err != nil {

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@relay/channel/claude/relay-claude.go`:
- Around line 1140-1158: DoResponsesRequest currently reads the entire response
body (using io.ReadAll) and unmarshals it as a single JSON object, which breaks
when RequestOpenAIResponses2ClaudeMessage maps Stream: true and Claude returns
Server-Sent Events (SSE); either stop mapping Stream into the Claude request in
RequestOpenAIResponses2ClaudeMessage for the Responses path, or update
DoResponsesRequest to detect SSE (resp.Header or Content-Type), stream-decode
SSE chunks rather than calling io.ReadAll, forward each event to the gin Context
response (set proper headers like text/event-stream, flush after each write),
handle "data: [DONE]" and errors, and ensure resp.Body is closed on completion;
adjust DoResponsesRequest and RequestOpenAIResponses2ClaudeMessage accordingly
so streaming paths are handled end-to-end.
- Around line 1230-1237: The code currently ignores the error returned by
json.Marshal(content.Input) and always uses string(inputJson) (which becomes
"null" if Input is nil); change the block that builds the dto.ResponsesOutput
for Type "function_call" to attempt json.Marshal(content.Input), check the error
and on error or when the result is "null" default Arguments to "{}" (or another
safe empty-object string), and set the ResponsesOutput.Status field to
"completed" (or the appropriate status) when appending to outputList; update the
construction of dto.ResponsesOutput (the place using inputJson, content.Id,
content.Name) to include this error handling and the Status field.
- Around line 1102-1107: The guard that prepends a synthetic first user message
uses an empty string (claudeMessages = append(... Content: "" ...)) which may be
rejected by Claude; update that synthetic message to use the same placeholder
used in RequestOpenAI2ClaudeMessage (i.e., Content: "...") so the injected user
message is non-empty and consistent with RequestOpenAI2ClaudeMessage's behavior.
- Around line 1211-1218: The "thinking" branch currently uses content.GetText()
which reads only Text and so drops thinking text; update the case to read the
Thinking field (e.g. content.GetThinking() or check content.Thinking pointer for
nil and dereference) and append that non-empty thinking string to
currentTextContent with Type "thinking" (same DTO: dto.ResponsesOutputContent).
Also remove or redact the detailed SysLog of the thinking content (replace with
a safe placeholder or omit logging) so sensitive reasoning isn't written to
server logs; keep logging minimal (e.g. "Claude thinking block received") if
needed.
- Around line 907-910: RequestOpenAIResponses2ClaudeMessage currently ignores
the OpenAIResponsesRequest.Reasoning settings; update this function to mirror
the mapping logic used in RequestOpenAI2ClaudeMessage by reading
responsesReq.Reasoning and ReasoningEffort, appending the "-thinking" model
suffix when appropriate, and populating claudeRequest.Thinking (including
MaxTokens and any other relevant reasoning fields) so extended thinking
parameters from dto.OpenAIResponsesRequest are translated into the Claude
request.
🧹 Nitpick comments (1)
relay/channel/claude/relay-claude.go (1)

1140-1180: DoResponsesRequest returns (resp, nil) for unmarshal/marshal failures, silently swallowing conversion errors.

When json.Unmarshal (line 1161) or json.Marshal (line 1169) fails, the function silently falls back to returning the raw Claude response body to the client. Since the client expects an OpenAI Responses API format, this will cause client-side parsing failures without any server-side indication of the problem. Consider logging these errors.

Proposed fix
 	var claudeResponse dto.ClaudeResponse
 	if err := json.Unmarshal(responseBody, &claudeResponse); err != nil {
-		// If unmarshal fails, restore body and return original response
+		logger.LogError(c, "failed to unmarshal Claude response for Responses API conversion: "+err.Error())
 		resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 		return resp, nil
 	}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@relay/channel/claude/relay-claude.go`:
- Around line 1125-1142: The loop is creating dto.ClaudeMessage entries with
Role set to "system", which Claude rejects; update the handling in the block
that builds claudeMessages to detect role == "system" and instead append the
content into claudeRequest.System (merging with any existing
Instructions-derived system text) or, if you prefer the alternative, convert it
to a user message by creating a dto.ClaudeMessage with Role "user" and prefixing
the Content (e.g., "System: ..."); modify the code that currently creates newMsg
(dto.ClaudeMessage) so that system-role items are not appended to claudeMessages
but are merged into claudeRequest.System.
- Around line 1226-1253: DoResponsesRequest currently replaces resp.Body with
the OpenAI-converted payload (ResponseClaude2OpenAIResponses), which breaks
ClaudeHandler (it expects a ClaudeResponse) and drops token usage info; fix by
leaving resp.Body as the original ClaudeResponse (restore responseBody into
resp.Body) so ClaudeHandler can unmarshal, and independently extract usage from
claudeResponse and surface it to ResponsesHelper (e.g., add explicit headers
like X-Usage-Prompt-Tokens / X-Usage-Completion-Tokens / X-Usage-Total-Tokens or
another agreed metadata field) plus attach the OpenAI-converted JSON somewhere
the downstream conversion flow can consume (e.g., an X-OpenAI-Converted-Body
header or Response context metadata) while still keeping
ResponseClaude2OpenAIResponses for generating that payload; update
DoResponsesRequest (and any consumers that read those headers/metadata) so usage
is available for billing/quota and ClaudeHandler no longer fails to unmarshal
ClaudeResponse.
🧹 Nitpick comments (1)
relay/channel/claude/relay-claude.go (1)

953-971: Reasoning MaxTokens override and opus effort-suffix not mapped.

The existing RequestOpenAI2ClaudeMessage (lines 202–215) allows Reasoning.MaxTokens to override BudgetTokens directly, and lines 150–158 handle the opus-4-6 effort suffix with "adaptive" thinking type. Neither is replicated here. If the Responses API caller sends reasoning.max_tokens or uses an opus model with an effort suffix, those will be silently ignored.

Consider adding parity with the chat-completions path if these models/parameters are expected via the Responses API.

@seefs001 seefs001 changed the title feat(claude): implement OpenAI Responses API compatibility (incl. too… feat(claude): Support calling Claude channels via /v1/responses Feb 8, 2026
Fix ["system" role input items are sent as Claude messages, which Claude rejects.]:

Unify content extraction into a single contentStr variable and skip empty roles early. Treat messages with role == "system" specially by appending a dto.ClaudeMediaMessage (type: "text") to claudeRequest.System (initializing/casting if needed) and continuing, while non-system roles are converted into dto.ClaudeMessage and appended to claudeMessages. This improves handling of system messages and reduces duplicated conversion logic.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@relay/channel/claude/relay-claude.go`:
- Around line 1220-1243: The SSE branch currently proxies Claude SSE events raw
(resp, scanner, c.Writer) which are not OpenAI Responses format, misses
scanner.Err() handling, never sees Claude-specific end markers (so the data:
[DONE] check is dead), and bypasses token counting/billing; either convert
Claude SSE to OpenAI Responses SSE the same way ClaudeStreamHandler does (map
Claude event types like message_start/content_block_delta/message_stop into
OpenAI-style "data: { ... }" chunks and emit a final "data: [DONE]" while
updating token counts), or disable streaming for the Responses path by not
setting the Stream flag on the outgoing Claude request and returning
non-streaming behavior; additionally ensure you check scanner.Err() after the
loop, close resp.Body in all paths, and update the token-billing hooks where
streaming would otherwise bypass them (e.g., invoke the same token accounting
used in non-streaming responses).
- Around line 1278-1285: ResponseClaude2OpenAIResponses builds
dto.OpenAIResponsesResponse but never sets unique IDs on each
dto.ResponsesOutput (message and function_call branches), leaving output item ID
fields empty; update ResponseClaude2OpenAIResponses to generate and assign a
unique ID for every output item (e.g., using a UUID or a deterministic composite
like claudeResponse.Id + index) when creating ResponsesOutput for both "message"
and "function_call" cases, and apply the same fix to the other similar block
around the 1340-1346 area; consider factoring ID generation into a small helper
(e.g., makeOutputID) and set ResponsesOutput.ID wherever outputs are
constructed.
- Around line 936-971: The Responses API reasoning handler currently always sets
claudeRequest.Thinking with Type "enabled" and fixed BudgetTokens; mirror the
RequestOpenAI2ClaudeMessage logic by detecting claude-opus-4-6 (e.g.
responsesReq.Model contains "claude-opus-4-6") when
responsesReq.Reasoning.Effort is set and instead set claudeRequest.Thinking to
Type "adaptive" with an appropriate OutputConfig (map "low"/"medium"/"high" to
the same budget values used in RequestOpenAI2ClaudeMessage) and otherwise fall
back to the existing "enabled" BudgetTokens behavior; update the block that
handles responsesReq.Reasoning.Effort and the claudeRequest.Thinking assignment
to perform this conditional branching.
🧹 Nitpick comments (2)
relay/channel/claude/relay-claude.go (2)

1287-1293: Cache-related usage details not mapped.

Claude's response may include CacheReadInputTokens and CacheCreationInputTokens in its usage data. The existing HandleClaudeResponseData (line 807-810) maps these to PromptTokensDetails.CachedTokens and CachedCreationTokens. This Responses path only maps InputTokens and OutputTokens, so cache hit information is silently discarded.

Proposed enrichment
 	if claudeResponse.Usage != nil {
 		response.Usage = &dto.Usage{
 			PromptTokens:     claudeResponse.Usage.InputTokens,
 			CompletionTokens: claudeResponse.Usage.OutputTokens,
 			TotalTokens:      claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens,
+			PromptTokensDetails: dto.InputTokenDetails{
+				CachedTokens:         claudeResponse.Usage.CacheReadInputTokens,
+				CachedCreationTokens: claudeResponse.Usage.CacheCreationInputTokens,
+			},
 		}
 	}

1359-1365: Empty-output fallback creates a message with zero content items.

When Claude returns no content blocks, this appends a ResponsesOutput with an empty Content slice. Some clients may not handle a message with "content": [] gracefully. Consider whether this should set Status: "completed" and include a minimal text block, or whether it's more appropriate to reflect an incomplete/failed response.

Special-case Claude model "claude-opus-4-6" when mapping reasoning: set Thinking to adaptive, emit an OutputConfig with the requested effort, and set TopP/Temperature. For other models keep the existing low/medium/high budget token mapping. Add makeOutputID helper and attach unique IDs to each ResponsesOutput (format: "<base>_output_<idx>") while tracking outputIdx to ensure deterministic, incrementing output IDs for message, function_call and fallback outputs.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@relay/channel/claude/relay-claude.go`:
- Around line 953-960: The model check in the Claude responses path uses
strings.Contains on responsesReq.Model which can false-match substrings; change
this to strings.HasPrefix to mirror RequestOpenAI2ClaudeMessage’s check and
avoid unintended matches—update the conditional that references
responsesReq.Model (the block that sets claudeRequest.Thinking,
claudeRequest.OutputConfig, claudeRequest.TopP, claudeRequest.Temperature) to
use strings.HasPrefix(responsesReq.Model, "claude-opus-4-6") instead of
strings.Contains.
- Around line 982-1026: When building claudeTool inside the tools loop
(function/block that constructs dto.Tool and appends to claudeTools), ensure
InputSchema is always a valid JSON Schema object: if the "parameters" field is
missing or not a map, set claudeTool.InputSchema =
map[string]interface{}{"type":"object"} before appending; when "parameters"
exists, populate InputSchema as you do but still default missing fields
appropriately so claudeRequest.Tools always gets tools with a non-nil
InputSchema.

[Use strings.HasPrefix instead of strings.Contains for model matching — inconsistent with the Chat Completions path.]
[Tools without parameters will fail at the Claude API — InputSchema must default to a valid JSON Schema.]
Copy link
Author

@Redtropig Redtropig left a comment

Choose a reason for hiding this comment

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

Done in this branch.

0-don added a commit to 0-don/new-api-fork that referenced this pull request Feb 18, 2026
…chat completions conversion

Enables channels that don't natively support the OpenAI Responses API
(e.g. Claude/Anthropic) to serve /v1/responses requests by converting
them to /v1/chat/completions format, routing through the existing
adaptor pipeline, and converting the response back.

Changes:
- Implement ConvertOpenAIResponsesRequest in Claude adaptor using
  Responses→Chat→Claude conversion chain (reuses existing code)
- Add ChatToResponsesStreamState for converting chat completion
  stream chunks into Responses API SSE events (handles text deltas,
  tool calls, reasoning content, usage)
- Add RelayFormatOpenAIResponses support to Claude stream/non-stream
  handlers in relay-claude.go
- Add ResponsesRequestToChatCompletionsRequest converter with:
  - GetJsonType() for robust input type detection
  - developer role → system mapping
  - PromptCacheKey/PromptCacheRetention passthrough
  - Non-function tool passthrough via Custom field
  - function_call/function_call_output → tool message merging
- Add ChatCompletionsResponseToResponsesResponse for non-streaming
- Fix: tool calls no longer dropped when text content exists in
  ResponsesResponseToChatCompletionsResponse
- Update ResponsesStreamResponse DTO with ResponseID, Text, Arguments,
  ContentIndex, Part fields

Combines and improves on approaches from:
- QuantumNous#2817 (adaptor-level conversion, converter improvements)
- QuantumNous#2892 (tool calls + text coexistence bug fix)

Related: QuantumNous#2941, QuantumNous#2043, QuantumNous#1527
0-don added a commit to 0-don/new-api-fork that referenced this pull request Feb 19, 2026
…chat completions conversion

Enables channels that don't natively support the OpenAI Responses API
(e.g. Claude/Anthropic) to serve /v1/responses requests by converting
them to /v1/chat/completions format, routing through the existing
adaptor pipeline, and converting the response back.

Changes:
- Implement ConvertOpenAIResponsesRequest in Claude adaptor using
  Responses→Chat→Claude conversion chain (reuses existing code)
- Add ChatToResponsesStreamState for converting chat completion
  stream chunks into Responses API SSE events (handles text deltas,
  tool calls, reasoning content, usage)
- Add RelayFormatOpenAIResponses support to Claude stream/non-stream
  handlers in relay-claude.go
- Add ResponsesRequestToChatCompletionsRequest converter with:
  - GetJsonType() for robust input type detection
  - developer role → system mapping
  - PromptCacheKey/PromptCacheRetention passthrough
  - Non-function tool passthrough via Custom field
  - function_call/function_call_output → tool message merging
- Add ChatCompletionsResponseToResponsesResponse for non-streaming
- Fix: tool calls no longer dropped when text content exists in
  ResponsesResponseToChatCompletionsResponse
- Update ResponsesStreamResponse DTO with ResponseID, Text, Arguments,
  ContentIndex, Part fields

Combines and improves on approaches from:
- QuantumNous#2817 (adaptor-level conversion, converter improvements)
- QuantumNous#2892 (tool calls + text coexistence bug fix)

Related: QuantumNous#2941, QuantumNous#2043, QuantumNous#1527
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