Skip to content

fix(ai-proxy): use the OpenAI Responses API for tool calling#1749

Open
Scra3 wants to merge 5 commits into
mainfrom
fix/ai-proxy-openai-responses-api
Open

fix(ai-proxy): use the OpenAI Responses API for tool calling#1749
Scra3 wants to merge 5 commits into
mainfrom
fix/ai-proxy-openai-responses-api

Conversation

@Scra3

@Scra3 Scra3 commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

OpenAI reasoning models (gpt-5.x) now reject function tools + reasoning_effort on /v1/chat/completions with a 400 ("To use function tools, use /v1/responses or set reasoning_effort to 'none'"). This breaks the LLM Integration Tests (ai-proxy) job — globally, on main and every open PR (e.g. PRD-18 #1741).

reasoning_effort is applied server-side by OpenAI (it appears nowhere in our code), so the fix is to switch endpoint, not to strip a param. Forcing reasoning_effort: 'none' was rejected — it would disable reasoning on every tool-calling step (i.e. all workflow AI steps). Instead we use the Responses API, which supports tools + reasoning together.

Changes

  • useResponsesApi: true on both ChatOpenAI construction sites — ProviderDispatcher (proxy path) and createBaseChatModel (workflow-executor path). A user-supplied useResponsesApi still wins (spread after the default). No dependency bump (@langchain/openai@1.4.7 + openai@6.42.0 already support it).
  • The OpenAI proxy path no longer reads additional_kwargs.__raw_response (the Responses API returns a different raw object); it builds the chat.completion wire shape from the normalized AIMessage via LangChainAdapter.convertResponse — the exact path already used for Anthropic. Consumers/tests keep the same wire contract.

Tests

  • Unit: all green (provider-dispatcher + create-base-chat-model updated to assert useResponsesApi: true, the converted response shape, and tool-call conversion; a user-override test added). additional_kwargs.__raw_response assertions dropped (path removed).

⚠️ Needs CI validation (integration)

The LLM Integration Tests (ai-proxy) job hits the live OpenAI API and is the authoritative signal — please confirm it goes green here. In a local run the 400 disappeared for gpt-5.6-* (the fix took effect), but several models reported success: false (no tool_calls surfaced) — unclear whether that's the local account (stale/preview models, e.g. gpt-3.5-turbo-16k 404s) or a convertResponse ↔ Responses-API tool-call surfacing gap. If CI still shows gpt-5.x failing with success: false, the follow-up is to verify how LangChain exposes tool calls under useResponsesApi and adjust the converter.

Follow-up (out of scope)

  • supported-models.ts excludes -pro/-deep-research as "responses-only"; after this switch they may become supportable — separate PR.

🤖 Generated with Claude Code

Note

Fix tool calling in ai-proxy by switching OpenAI requests to the Responses API

  • Enables useResponsesApi: true by default when constructing ChatOpenAI instances in create-base-chat-model.ts and provider-dispatcher.ts.
  • Replaces __raw_response extraction with LangChainAdapter.convertResponse() to convert LangChain AIMessage output to the OpenAI chat.completion wire format.
  • Adds forceToolChoiceForResponses to coerce 'required'/'any' tool_choice to an explicit function choice when exactly one function tool is present; skips tool binding entirely when tool_choice is 'none'.
  • Updates the unsupported model list: gpt-3.5-turbo-16k is now rejected and gpt-5.6-* models are now allowed.
  • Adds a validation rule requiring messages to be a non-empty array in the ai-query route schema.
  • Risk: OpenAI requests now go through the Responses API by default; callers must pass useResponsesApi: false to opt out.

Macroscope summarized 7d9a0ef.

return rawResponse;
// Build the OpenAI wire shape from LangChain's normalized AIMessage (same path as Anthropic).
// The Responses API returns a different raw object, so we no longer read __raw_response.
return LangChainAdapter.convertResponse(response, this.modelName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium src/provider-dispatcher.ts:113

dispatchOpenAI now rebuilds every response through LangChainAdapter.convertResponse, which hard-codes choices[0].finish_reason to 'stop' (or 'tool_calls' when tool calls exist). The real finish reason from response.response_metadata.finish_reason — such as 'length' or 'content_filter' — is silently dropped, so callers receive 'stop' even when generation was truncated or filtered. Consider reading finish_reason from response.response_metadata in convertResponse instead of always defaulting to 'stop'.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/ai-proxy/src/provider-dispatcher.ts around line 113:

`dispatchOpenAI` now rebuilds every response through `LangChainAdapter.convertResponse`, which hard-codes `choices[0].finish_reason` to `'stop'` (or `'tool_calls'` when tool calls exist). The real finish reason from `response.response_metadata.finish_reason` — such as `'length'` or `'content_filter'` — is silently dropped, so callers receive `'stop'` even when generation was truncated or filtered. Consider reading `finish_reason` from `response.response_metadata` in `convertResponse` instead of always defaulting to `'stop'`.

Comment thread packages/ai-proxy/src/provider-dispatcher.ts
@qltysh

qltysh Bot commented Jul 10, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on main by 0.03%.

Modified Files with Diff Coverage (2)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/provider-dispatcher.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/create-base-chat-model.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@Scra3 Scra3 closed this Jul 10, 2026
@Scra3 Scra3 reopened this Jul 10, 2026
alban bertolini and others added 5 commits July 10, 2026 12:14
OpenAI reasoning models (gpt-5.x) reject function tools combined with
reasoning_effort on /v1/chat/completions (400). Set useResponsesApi: true on
both ChatOpenAI construction sites (ProviderDispatcher + createBaseChatModel),
so tools + reasoning work together. A user-supplied useResponsesApi still wins.

The OpenAI proxy path no longer reads additional_kwargs.__raw_response (the
Responses API returns a different raw shape); it now builds the chat.completion
wire shape from the normalized AIMessage via LangChainAdapter.convertResponse,
the same path already used for Anthropic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LangChain's Responses API path silently drops string tool_choice values
('required'/'any') — its param builder forwards only object-form choices.
Under the global Responses switch this made ~20 OpenAI models return text
instead of a tool call (finish_reason !== 'tool_calls'), failing the LLM
integration test. Re-express "must call a tool" as forcing the single
available function, the only forcing shape the Responses path forwards.

Also deny-list gpt-3.5-turbo-16k (removed from the API, 404) and fix a
date-fragile assertion in the load-related-record executor test
(-1 matched dates like 2026-07-10; anchor on 'return -1').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oice none

The Responses API rejects an empty input with its own wording, breaking the
proxy's error contract — validate messages at the schema boundary instead.
tool_choice 'none' is another string value LangChain's Responses path drops;
not binding the tools is the only way to guarantee the model cannot call them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s API

The deny-list from #1750 worked around the chat/completions 400; the
Responses switch makes these models pass tool calling again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Scra3 Scra3 force-pushed the fix/ai-proxy-openai-responses-api branch from ac553f1 to 7d9a0ef Compare July 10, 2026 10:17
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