fix(ai): declare GPT-5.4 and GPT-5.5 tools at the top level

GPT-5.5 and older namespace tools loaded through additional_tools or tool
search and reject replayed calls without the namespace, so switching to them
after another model's tool calls failed with 400. Drop dynamic tool loading
for these models on OpenAI and OpenCode; keep mid-conversation system messages.

closes #11415
This commit is contained in:
David Brailovsky 2026-10-27 14:25:23 +01:00
parent db14038bd8
commit 81d6759fc9
3 changed files with 14 additions and 8 deletions

View file

@ -28,6 +28,7 @@
- Fixed pi-messages streams accepting a `contentIndex` that is not the next or an existing content block, such as `__proto__` or a huge index
- Fixed `pi-ai` CLI auth file being readable by other users. It is now written with mode `0600`
- Fixed OpenCode DeepSeek V4.1 Flash sending `prompt_cache_retention`, which the OpenCode gateway rejects, with long cache retention
- Fixed GPT-5.4 and GPT-5.5 failing with `Missing namespace for function_call` after switching from another model that called tools. On the OpenAI and OpenCode providers they now declare their tools in the top-level tool list instead of loading them through `additional_tools` or tool search ([#11415](https://github.com/earendil-works/pi/issues/11415))
## [1.4.0] - 2026-10-21

View file

@ -366,11 +366,10 @@ const ANT_LING_RING_THINKING_LEVEL_MAP = {
const BEDROCK_INFERENCE_PROFILE_ONLY_MODEL_IDS = new Set(["anthropic.claude-opus-5"]);
const MODELS_DEV_OPENAI_UNSUPPORTED_MODEL_IDS = new Set(["gpt-5.6"]);
// GPT-5.5 and older put tools loaded through tool search or additional_tools in a namespace named
// after the tool and reject replayed calls without it, so history from other models fails there
// (#11415). Like Codex, dynamic tool loading therefore starts with GPT-5.6, which does not do this.
const OPENAI_TOOL_SEARCH_MODEL_IDS = new Set([
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-pro",
"gpt-5.5",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
@ -381,7 +380,13 @@ const OPENAI_TOOL_SEARCH_MODEL_IDS = new Set([
"gpt-6.1-terra",
]);
const OPENAI_ADDITIONAL_TOOLS_MODEL_IDS = OPENAI_TOOL_SEARCH_MODEL_IDS;
const OPENAI_MID_CONVO_SYSTEM_MESSAGE_MODEL_IDS = OPENAI_TOOL_SEARCH_MODEL_IDS;
const OPENAI_MID_CONVO_SYSTEM_MESSAGE_MODEL_IDS = new Set([
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-pro",
"gpt-5.5",
...OPENAI_TOOL_SEARCH_MODEL_IDS,
]);
const OPENAI_CODEX_ADDITIONAL_TOOLS_MODEL_IDS = new Set([
"gpt-5.6-sol",
"gpt-5.6-terra",
@ -985,7 +990,7 @@ function applyOpenAIResponsesTranscriptMetadata(model: Model<Api>): void {
model.compat = {
...(model.compat as OpenAIResponsesCompat | undefined),
supportsMidConvoSystemMessages: true,
...(isProxiedResponses ? { supportsAdditionalTools: true } : {}),
...(isProxiedResponses && OPENAI_ADDITIONAL_TOOLS_MODEL_IDS.has(model.id) ? { supportsAdditionalTools: true } : {}),
};
}

View file

@ -119,7 +119,7 @@ describe("openai-responses provider defaults", () => {
);
const stream = streamOpenAIResponses(
getModel("openai", "gpt-5.4"),
getModel("openai", "gpt-5.6-luna"),
normalizeContext({
messages: [
{
@ -149,7 +149,7 @@ describe("openai-responses provider defaults", () => {
if (event.type === "done" || event.type === "error") break;
}
// gpt-5.4 declares its tools in additional_tools items; tool_choice applies to them.
// gpt-5.6-luna declares its tools in additional_tools items; tool_choice applies to them.
expect(capturedPayload).toMatchObject({
tool_choice: "required",
input: [{ type: "additional_tools", tools: [expect.objectContaining({ name: "ping" })] }, expect.anything()],