Skip to content

Commit 0eed020

Browse files
committed
fix(openai): respect useResponsesApi: false as explicit opt-out
When useResponsesApi is explicitly set to false, the auto-detection logic (built-in tools, model preferences, etc.) no longer overrides it. This fixes 404 errors when using third-party API providers with non-standard tool types. Closes #10428
1 parent 6db417b commit 0eed020

File tree

5 files changed

+162
-2
lines changed

5 files changed

+162
-2
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@langchain/openai": patch
3+
---
4+
5+
fix(openai): respect useResponsesApi: false as explicit opt-out
6+
7+
When useResponsesApi is explicitly set to false, the auto-detection logic no longer overrides it. This fixes 404 errors when using third-party API providers with non-standard tool types.

libs/langchain-core/src/messages/block_translators/openai.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,21 @@ export function convertToV1FromResponses(
283283
text: String(text),
284284
};
285285
}
286+
} else if (_isContentBlock(block, "reasoning")) {
287+
const summary = _isArray(block.summary)
288+
? block.summary.reduce<string>((acc, item) => {
289+
if (_isObject(item) && _isString(item.text)) {
290+
return `${acc}${item.text}`;
291+
}
292+
return acc;
293+
}, "")
294+
: _isString(block.reasoning)
295+
? block.reasoning
296+
: "";
297+
yield {
298+
type: "reasoning",
299+
reasoning: summary,
300+
};
286301
}
287302
}
288303
for (const toolCall of message.tool_calls ?? []) {

libs/langchain-core/src/messages/block_translators/tests/openai.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,71 @@ describe("openaiTranslator", () => {
252252
expect(message2.content).toEqual(expected);
253253
});
254254

255+
it("should translate reasoning blocks in content array to reasoning content blocks", () => {
256+
const message = new AIMessage({
257+
content: [
258+
{
259+
type: "reasoning",
260+
id: "rs_abc123",
261+
summary: [
262+
{ type: "summary_text", text: "Let me think about this..." },
263+
],
264+
index: 0,
265+
},
266+
{
267+
type: "function_call",
268+
name: "task",
269+
arguments: '{"description":"do something"}',
270+
call_id: "call_xyz789",
271+
index: 1,
272+
},
273+
],
274+
tool_calls: [
275+
{
276+
id: "call_xyz789",
277+
name: "task",
278+
args: { description: "do something" },
279+
},
280+
],
281+
response_metadata: { model_provider: "openai" },
282+
});
283+
284+
const blocks = message.contentBlocks;
285+
const reasoningBlock = blocks.find((b) => b.type === "reasoning");
286+
const toolCallBlock = blocks.find((b) => b.type === "tool_call");
287+
288+
expect(reasoningBlock).toEqual({
289+
type: "reasoning",
290+
reasoning: "Let me think about this...",
291+
});
292+
expect(toolCallBlock).toEqual({
293+
type: "tool_call",
294+
id: "call_xyz789",
295+
name: "task",
296+
args: { description: "do something" },
297+
});
298+
});
299+
300+
it("should handle reasoning block with pre-parsed reasoning string in content array", () => {
301+
const message = new AIMessage({
302+
content: [
303+
{
304+
type: "reasoning",
305+
reasoning: "Already parsed reasoning text",
306+
},
307+
{ type: "text", text: "Hello" },
308+
],
309+
response_metadata: { model_provider: "openai" },
310+
});
311+
312+
const expected: Array<ContentBlock.Standard> = [
313+
{ type: "reasoning", reasoning: "Already parsed reasoning text" },
314+
{ type: "text", text: "Hello" },
315+
];
316+
317+
expect(message.contentBlocks).toEqual(expected);
318+
});
319+
255320
it("should translate responses chunk and include tool_call when args parse", () => {
256321
const chunk1 = new AIMessageChunk({
257322
content: [{ type: "text", text: "Processing ", index: 0 }],

libs/providers/langchain-openai/src/chat_models/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -593,8 +593,10 @@ export class ChatOpenAI<
593593
CallOptions extends ChatOpenAICallOptions = ChatOpenAICallOptions,
594594
> extends BaseChatOpenAI<CallOptions> {
595595
/**
596-
* Whether to use the responses API for all requests. If `false` the responses API will be used
597-
* only when required in order to fulfill the request.
596+
* Whether to use the responses API. When `true`, always use the Responses API.
597+
* When explicitly set to `false`, never use the Responses API (even if
598+
* auto-detection would otherwise enable it). When not set, the Responses API
599+
* is used automatically when required to fulfill the request.
598600
*/
599601
useResponsesApi = false;
600602

@@ -610,6 +612,9 @@ export class ChatOpenAI<
610612
return [...super.callKeys, "useResponsesApi"];
611613
}
612614

615+
/** Whether the user explicitly set `useResponsesApi` to `false`. */
616+
private _useResponsesApiExplicitlyDisabled = false;
617+
613618
protected fields?: ChatOpenAIFields;
614619

615620
constructor(model: string, fields?: Omit<ChatOpenAIFields, "model">);
@@ -622,11 +627,17 @@ export class ChatOpenAI<
622627
super(fields);
623628
this.fields = fields;
624629
this.useResponsesApi = fields?.useResponsesApi ?? false;
630+
this._useResponsesApiExplicitlyDisabled =
631+
fields?.useResponsesApi === false;
625632
this.responses = fields?.responses ?? new ChatOpenAIResponses(fields);
626633
this.completions = fields?.completions ?? new ChatOpenAICompletions(fields);
627634
}
628635

629636
protected _useResponsesApi(options: this["ParsedCallOptions"] | undefined) {
637+
if (this._useResponsesApiExplicitlyDisabled) {
638+
return false;
639+
}
640+
630641
const usesBuiltInTools = options?.tools?.some(isBuiltInTool);
631642
const hasResponsesOnlyKwargs =
632643
options?.previous_response_id != null ||

libs/providers/langchain-openai/src/chat_models/tests/index.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,4 +1680,66 @@ describe("ChatOpenAI", () => {
16801680
expect(result.text).toEqual("Foo bar");
16811681
});
16821682
});
1683+
1684+
describe("useResponsesApi: false should override auto-detection", () => {
1685+
it("should use chat completions endpoint when useResponsesApi is explicitly false, even with non-standard tool types", async () => {
1686+
let requestedUrl = "";
1687+
const mockFetch = vi.fn().mockImplementation(async (url: string) => {
1688+
requestedUrl = url;
1689+
return new Response(
1690+
JSON.stringify({
1691+
id: "chatcmpl-123",
1692+
object: "chat.completion",
1693+
choices: [
1694+
{
1695+
index: 0,
1696+
message: { role: "assistant", content: "test response" },
1697+
finish_reason: "stop",
1698+
},
1699+
],
1700+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
1701+
}),
1702+
{ status: 200, headers: { "Content-Type": "application/json" } }
1703+
);
1704+
});
1705+
1706+
const model = new ChatOpenAI({
1707+
model: "kimi-k2",
1708+
apiKey: "test-key",
1709+
useResponsesApi: false,
1710+
configuration: {
1711+
baseURL: "https://api.example.com/v1",
1712+
fetch: mockFetch,
1713+
},
1714+
});
1715+
1716+
await model.invoke(
1717+
[{ role: "user", content: "test" }],
1718+
{
1719+
tools: [
1720+
{
1721+
type: "builtin_function",
1722+
function: { name: "$web_search" },
1723+
},
1724+
],
1725+
} as any
1726+
);
1727+
1728+
expect(requestedUrl).toContain("/chat/completions");
1729+
expect(requestedUrl).not.toContain("/responses");
1730+
});
1731+
1732+
it("should still auto-detect responses API when useResponsesApi is not explicitly set", () => {
1733+
const model = new ChatOpenAI({
1734+
model: "gpt-4o",
1735+
apiKey: "test-key",
1736+
});
1737+
1738+
// Access protected method for testing
1739+
const useResponses = (model as any)._useResponsesApi({
1740+
tools: [{ type: "web_search_preview" }],
1741+
});
1742+
expect(useResponses).toBe(true);
1743+
});
1744+
});
16831745
});

0 commit comments

Comments
 (0)