Repository navigation
fix(translator): preserve Responses Lite tools across Chat providers - #2983
Closed
nguyenha935 wants to merge 4 commits into
Closed
nguyenha935 wants to merge 4 commits into
nguyenha935 wants to merge 4 commits into
Conversation
…PI clients When a Responses-API client (e.g. Codex) is routed to a chat-native OpenAI-compatible provider, non-streaming requests returned the raw chat.completion body: object was "chat.completion" and tool_calls were invisible to the client. Related to #2347 (openai -> openai-responses direction of the leaked-format family). Changes: - nonStreamingHandler: add the openai(provider) -> openai-responses(client) case to translateNonStreamingResponse, projecting tool_calls to function_call items, content to output_text, reasoning to summary text; guard chat-specific post-processing so the Responses envelope is not mangled back into chat.completion - sseToJsonHandler: branch the forced-SSE path on the upstream format (targetFormat) instead of the client format, so a Responses client behind a chat-native forced-streaming provider is parsed as chat SSE and then projected to Responses shape - chatCore: pass targetFormat (providerResponseFormat) to handleForcedSSEToJson, consistent with the other two handlers Tests: add tests/unit/openai-responses-nonstream.test.js (5 tests covering tool_calls projection, text projection, chat->chat passthrough, forced-SSE projection, and chat-client regression). The three projection tests fail on the previous code and pass with this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…stream The Responses->Chat request translator stashes reasoning encrypted_content on assistant messages so an openai->responses round-trip can restore the store=false continuity blob. That stash leaked into the body sent to chat-native upstreams: OpenAI-compatible proxies reject the unknown assistant-message field and answer every turn with a literal "400" body, breaking multi-turn Codex sessions (the blob grows per turn, so every retry failed too). Strip encrypted_content / reasoning_encrypted_content from translated messages at the dispatch boundary in handleChatCore, after translation. The translator-level stash is preserved for round-trip symmetry (existing multiturn tests untouched). Tests: add tests/unit/continuity-strip.test.js — boundary strip removes both keys while keeping reasoning_content and tool_calls; end-to-end Responses multi-turn translation is clean after the strip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r ID The runtime derived an OpenAI-compatible node transport from a substring of the immutable provider ID instead of the stored apiType, so editing a node API Type (Chat <-> Responses) in the dashboard had no effect. Add resolveOpenAICompatibleApiType(provider, credentials): prefer the stored providerSpecificData.apiType kept in sync with the node on create/update, fall back to the ID substring for legacy nodes. Use it in getProviderConfig/getTargetFormat and both executors buildUrl so format selection and endpoint path agree. chatCore now passes credentials into getTargetFormat.
nguyenha935
force-pushed
the
fix/responses-nonstream-chat-projection
branch
from
August 2, 2026 09:09
474ad86 to
f0f6c91
Compare
Owner
|
Thanks @nguyenha935 for the contribution! Reviewed and merged into master. 🙏 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
A Responses-API client (notably Codex) routed to a chat-native OpenAI-compatible provider (
openai-compatible-chat-*) lost tool use in three independent places:chat.completionenvelope instead of returning Responsesoutputitems, making upstreamtool_callsinvisible.reasoning.encrypted_contentcontinuity stash leaked into the outbound Chat body. Compatible upstreams reject that unknown assistant-message field and may return a literal"400"assistant response.tools. It sends a developer input item such as:{ "type": "additional_tools", "role": "developer", "tools": [{ "type": "custom", "name": "exec", "format": { "...": "..." } }] }The Responses→Chat request translator ignored
additional_tools,custom_tool_call, andcustom_tool_call_output. The outbound provider request therefore hadtool_choice: "auto"but no tool declaration, and the model correctly reported that it had no terminal/SSH tool.Root causes
translateNonStreamingResponsehad no Chat-provider → Responses-client JSON projection.Fix
encrypted_contentandreasoning_encrypted_contentimmediately before upstream dispatch while retaining translator-level round-trip continuity.input[].type == "additional_tools"declarations into Chattools.inputparameter.custom_tool_callandcustom_tool_call_outputitems into Chat assistanttool_callsand tool messages.custom_tool_callitems/events and unwrap the Chat{ "input": "..." }argument wrapper into rawinput.function_callitems.response.output_item.addeduntil both tool-call id and name are known, preventing split Chat deltas from being announced with the wrong Responses item type.Additional fix: OpenAI-compatible apiType resolution
Editing an OpenAI-compatible node's API Type (chat ↔ responses) had no effect at runtime: the transport was derived from the immutable provider ID substring (
openai-compatible-<chat|responses>-<uuid>) instead of the stored node config. The edit route updatesnode.apiTypeand propagates it into each connection'sproviderSpecificDatawithout ever changing the ID, so a node edited after creation kept its original transport.Fix — new
resolveOpenAICompatibleApiType(provider, credentials)inopen-sse/services/provider.js: the storedproviderSpecificData.apiTypeis authoritative, falling back to the ID substring only for legacy nodes created beforeapiTypewas persisted. All three runtime call sites now use it, so format selection and endpoint path always agree:open-sse/handlers/chatCore.js— target-format resolution viagetTargetFormat(provider, credentials)open-sse/executors/base.js—buildUrlendpoint path (/responsesvs/chat/completions)open-sse/executors/default.js— same for the default executorTests —
tests/unit/openai-compatible-apitype-resolution.test.js(12 cases): stored-value precedence in both directions, ID fallback with absent/null credentials, invalid-value fallback, target-format selection, and endpoint path for both executors.Verification
Automated
tests/unit/openai-responses-custom-tools.test.js: 6 tests foradditional_tools, merged tools, custom call/output history, streaming custom events, split id/name deltas, and normal-function regression.tests/unit/openai-responses-nonstream.test.js: true JSON and forced-SSE custom-tool projections.it.failscases still expected.Live end-to-end
Tested through the exact routed model
op-ericding/cx/gpt-5.6-soland/v1/responsesusing a Responses Liteadditional_toolscustomexecdeclaration:exec, with required argumentinput.response.output_item.addedwithtype: "custom_tool_call",name: "exec";response.custom_tool_call_input.deltawith raw inputpwd;response.custom_tool_call_input.done;response.output_item.donewith rawinput: "pwd";response.completed.custom_tool_callpluscustom_tool_call_output: "/root"returned a normal final text response, confirming history translation.This is a targeted fix for the
openai ↔ openai-responsesdirection of the format-loss family described in #2347. #2348 proposes a broader unified projector.