refactor(ai/providers): removed parallel tool call restriction

- Removed the logic that forced sequential tool calling when custom tools were present in both Codex and OpenAI providers.
- Exported `buildTransformedCodexRequestBody` for testing purposes.
This commit is contained in:
can1357
2026-06-21 00:18:08 +02:00
parent 51f5919486
commit c89e0ea9b2
5 changed files with 43 additions and 22 deletions
+3 -2
View File
@@ -257,8 +257,9 @@ session/provider path.
### Responses and Codex custom tools
Responses and Codex both support freeform custom grammar tools for `apply_patch`.
Both disable request-level parallel tool calls when any custom grammar tool is
present. Responses additionally:
Custom grammar tools do not force request-level `parallel_tool_calls`; Codex
`responsesLite` separately disables request-level parallel tool calls whenever
tools are present. Responses additionally:
- sanitizes schemas differently
- quarantines invalid enum/const schema contradictions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Changed
- Changed OpenAI Responses and Codex Responses custom grammar tool requests to leave `parallel_tool_calls` unset instead of forcing serial tool calls; Codex `responsesLite` still disables parallel tool calls when tools are present.
### Fixed
- Fixed Bedrock `/btw` and other no-tool ephemeral turns failing after prior tool calls by sending the required sentinel `toolConfig` whenever replayed history contains `toolUse`/`toolResult` blocks. ([#3124](https://github.com/can1357/oh-my-pi/issues/3124))
@@ -714,7 +714,8 @@ async function buildCodexRequestContext(
};
}
async function buildTransformedCodexRequestBody(
/** @internal Exported for tests. */
export async function buildTransformedCodexRequestBody(
model: Model<"openai-codex-responses">,
context: Context,
options: OpenAICodexResponsesOptions | undefined,
@@ -744,16 +745,6 @@ async function buildTransformedCodexRequestBody(
params.tool_choice = toolChoice;
}
}
// When a custom-tool is active, force serial tool-calling. OpenAI's
// `parallel_tool_calls` is request-scoped — disabling it here affects
// every tool in the turn, not just the custom one. That's coarser
// than spec §1's "supports_parallel_tool_calls = false" (which
// strictly targets `apply_patch`), but the platform API offers no
// per-tool flag.
const emittedTools = params.tools as CodexToolPayload[];
if (emittedTools.some(t => t.type === "custom")) {
params.parallel_tool_calls = false;
}
}
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
@@ -862,15 +862,6 @@ export function buildParams(
params.tool_choice = toolChoice;
}
}
// The apply_patch spec §1 marks only `apply_patch` itself as
// `supports_parallel_tool_calls = false`. OpenAI's Responses API
// exposes `parallel_tool_calls` as a request-scoped flag, not a
// per-tool one, so when a custom grammar tool is in the list we
// disable parallelism for the whole turn. Slightly coarser than
// the spec requires — but the platform API offers no finer knob.
if (params.tools.some(t => (t as { type?: string }).type === "custom")) {
params.parallel_tool_calls = false;
}
}
const reasoningPolicy = resolveOpenAICompatPolicy(model, {
@@ -1,9 +1,11 @@
import { describe, expect, test } from "bun:test";
import {
buildTransformedCodexRequestBody,
convertOpenAICodexResponsesTools as convertCodexTools,
normalizeCodexToolChoice,
} from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import {
buildParams,
convertTools,
mapOpenAIResponsesToolChoiceForTools,
supportsFreeformApplyPatch,
@@ -74,6 +76,13 @@ const plainTool: Tool = {
parameters: type({ path: "string" }),
};
function hasCustomTool(tools: unknown): boolean {
return (
Array.isArray(tools) &&
tools.some(tool => typeof tool === "object" && tool !== null && (tool as { type?: unknown }).type === "custom")
);
}
const unionBranches = [
{
type: "object",
@@ -221,6 +230,31 @@ describe("tool choice mapping: freeform emission", () => {
});
});
describe("request params: freeform custom tools", () => {
test("openai responses leaves parallel tool calls unset", () => {
const { params } = buildParams(
makeModel({ applyPatchToolType: "freeform" }),
{ messages: [{ role: "user", content: "edit", timestamp: 0 }], tools: [editTool] },
undefined,
undefined,
);
expect(hasCustomTool(params.tools)).toBe(true);
expect(params.parallel_tool_calls).toBeUndefined();
});
test("codex responses leaves parallel tool calls unset for custom tools", async () => {
const params = await buildTransformedCodexRequestBody(
makeCodexModel({ applyPatchToolType: "freeform" }),
{ messages: [{ role: "user", content: "edit", timestamp: 0 }], tools: [editTool] },
undefined,
);
expect(hasCustomTool(params.tools)).toBe(true);
expect(params.parallel_tool_calls).toBeUndefined();
});
});
describe("custom_tool_call stream receive", () => {
async function* makeStream(events: unknown[]): AsyncIterable<ResponseStreamEvent> {
for (const e of events) yield e as ResponseStreamEvent;