From c12c282aa528f5377091b2e5e17fb8aed53613d3 Mon Sep 17 00:00:00 2001 From: usr-bin-roygbiv Date: Fri, 24 Jul 2026 05:00:39 +0000 Subject: [PATCH] fix(computer): gate native Responses transport --- docs/computer-use.md | 8 +- docs/tools/computer.md | 4 +- .../src/providers/openai-codex-responses.ts | 32 +++-- .../openai-codex/request-transformer.ts | 34 ++++- packages/ai/src/providers/openai-responses.ts | 7 +- .../test/openai-codex-responses-lite.test.ts | 16 +++ .../ai/test/openai-computer-contract.test.ts | 129 +++++++++++++++++- .../openai-responses-tool-quarantine.test.ts | 18 +++ packages/catalog/src/build.ts | 37 +++-- packages/catalog/test/build.test.ts | 42 ++++++ 10 files changed, 290 insertions(+), 37 deletions(-) diff --git a/docs/computer-use.md b/docs/computer-use.md index 6f77a3ead..35a6ac94a 100644 --- a/docs/computer-use.md +++ b/docs/computer-use.md @@ -74,11 +74,13 @@ Models with native OpenAI GA computer-use support receive the wire declaration ` OMP marks a model natively capable when either: - its catalog metadata explicitly sets `supportsComputerUse: true`, or -- it uses `openai-responses`, `openai-codex-responses`, or `azure-openai-responses` and resolves to an OpenAI/OpenAI Codex or Azure model ID matching `gpt-5.4` or later in the `gpt-5.x` family. +- it uses a direct OpenAI Responses or Azure OpenAI Responses endpoint and resolves to a model ID matching `gpt-5.4` or later in the `gpt-5.x` family. -An explicit `supportsComputerUse: false` disables automatic derivation and routes the model through the function-tool form. +Codex subscription endpoints and custom or proxy routes do not infer native support from the model ID. They receive the regular `computer` function tool unless catalog metadata explicitly opts into the GA contract. An explicit `supportsComputerUse: false` also disables automatic derivation. -Natively capable OpenAI Responses routes may receive a forced `{ "type": "computer" }` choice. Function-tool fallback forcing is provider-specific: OpenAI/Ollama use a named function, Anthropic/Bedrock use a named tool, Google uses required-tool mode, and adapters without a forcing form keep provider-default selection. When native computer history is replayed to a non-native OpenAI Responses-family model, that adapter converts prior `computer_call` and `computer_call_output` items into stable text notes rather than sending invalid native items. Other provider adapters serialize the generic call and result through their ordinary tool format. +Natively capable OpenAI Responses routes may receive a forced `{ "type": "computer" }` choice. Function-tool fallback forcing is provider-specific: OpenAI/Ollama use a named function, Anthropic/Bedrock use a named tool, Google uses required-tool mode, and adapters without a forcing form keep provider-default selection. Responses Lite moves tools into `additional_tools`; for an explicitly forced computer declaration it sends only that declaration and uses `tool_choice: "required"`, preserving both selection and forcing without an invalid object choice that refers to removed top-level tools. + +When a session switches from a native-capable API route to a subscription or proxy route, prior native computer history is converted to a representation the target accepts. Codex subscription requests replay it as named `computer` function calls and results, then declare the next computer call as the same named function. Other non-native OpenAI Responses-family targets may use stable assistant text notes; other provider adapters use their ordinary tool format. If the tool never appears: diff --git a/docs/tools/computer.md b/docs/tools/computer.md index dc59d159f..f44094e3f 100644 --- a/docs/tools/computer.md +++ b/docs/tools/computer.md @@ -26,8 +26,8 @@ User setup, safety guidance, platform permissions, and verified limitations: [Na - Concurrency: `exclusive`. - Native descriptor: `{ type: "computer" }`. - Providers serialize the native descriptor only when `model.supportsComputerUse === true`; every other function-calling model receives `computer` as a regular function tool with the typed action schema below. -- Automatic capability derivation covers GA `gpt-5.4+` IDs on OpenAI Responses, OpenAI Codex Responses, and Azure OpenAI Responses; explicit model metadata overrides derivation. -- When OpenAI Responses-family history is replayed to a model without native support, native call/output items become stable assistant text notes. Other provider adapters serialize the generic call/result in their ordinary tool format. +- Automatic capability derivation covers GA `gpt-5.4+` IDs only on direct OpenAI Responses and Azure OpenAI Responses endpoints. Codex subscription and custom or proxy routes fall back unless explicit model metadata opts in. +- When a session switches from a native-capable API route to a non-native route, native history is adapted instead of being replayed as invalid GA items. Codex subscription requests use named function calls/results and the next declaration is a named function; other OpenAI Responses-family targets may use stable assistant text notes. Responses Lite preserves an explicit computer choice by placing only the selected declaration in `additional_tools` and using `tool_choice: "required"`. Other provider adapters use their ordinary tool format. Unlike `browser`, `computer` operates the entire visible host session. It can act in IDEs, terminals, native applications, browser windows, and system dialogs, but has no structured application/DOM inspection. diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 212f7b9e7..8ac6195ec 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1146,7 +1146,8 @@ export function normalizeCodexToolChoice( }; if (choice.type === "computer") { const computer = tools.find(tool => tool.native?.type === "computer"); - return computer ? { type: "function", name: computer.name } : undefined; + if (!computer) return undefined; + return model?.supportsComputerUse === true ? { type: "computer" } : { type: "function", name: computer.name }; } if (choice.type === "function") { if ("function" in choice && choice.function?.name) { @@ -4135,10 +4136,10 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex | undefined; if (historyItems) { const redactedHistoryItems = redactSensitiveInObject(historyItems).result as Array; - const replayItems = unrollCodexComputerItems( - redactedHistoryItems, - model.compat.supportsImageDetailOriginal, - ); + const replayItems = + model.supportsComputerUse === true + ? redactedHistoryItems + : unrollCodexComputerItems(redactedHistoryItems, model.compat.supportsImageDetailOriginal); for (const item of replayItems) { if (item.type === "custom_tool_call") { customCallIds.add(item.call_id); @@ -4173,10 +4174,10 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex if (historyItems) { const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems); if (sanitizedHistoryItems) { - const replayItems = unrollCodexComputerItems( - sanitizedHistoryItems, - model.compat.supportsImageDetailOriginal, - ); + const replayItems = + model.supportsComputerUse === true + ? sanitizedHistoryItems + : unrollCodexComputerItems(sanitizedHistoryItems, model.compat.supportsImageDetailOriginal); for (const item of replayItems) { if (item.type === "custom_tool_call") { customCallIds.add(item.call_id); @@ -4198,7 +4199,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex } const convertedOutputItems = convertResponsesAssistantMessage( - unrollCodexComputerAssistantMessage(msg as AssistantMessage), + model.supportsComputerUse === true ? assistantMsg : unrollCodexComputerAssistantMessage(assistantMsg), model, msgIndex, knownCallIds, @@ -4220,7 +4221,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex if (msg.role === "toolResult") { appendResponsesToolResultMessages( messages, - unrollCodexComputerToolResult(msg), + model.supportsComputerUse === true ? msg : unrollCodexComputerToolResult(msg), model, false, model.compat.supportsImageDetailOriginal, @@ -4255,6 +4256,7 @@ function normalizeInputMessageContent( export { convertMessages as convertCodexResponsesMessages }; type CodexToolPayload = + | { type: "computer"; name?: never } | { type: "function"; name: string; @@ -4277,8 +4279,12 @@ export function convertOpenAICodexResponsesTools( const allowFreeform = model.applyPatchToolType === "freeform"; const payloads: CodexToolPayload[] = []; for (const tool of tools) { - // The ChatGPT Codex endpoints reject the native `{ type: "computer" }` - // shape, so both standard and Lite transports expose it as a function. + // Subscription models default to the function fallback. Explicit metadata + // remains authoritative for future Codex endpoints that implement GA computer use. + if (tool.native?.type === "computer" && model.supportsComputerUse === true) { + payloads.push({ type: "computer" }); + continue; + } if (allowFreeform && tool.customFormat) { payloads.push({ type: "custom", diff --git a/packages/ai/src/providers/openai-codex/request-transformer.ts b/packages/ai/src/providers/openai-codex/request-transformer.ts index 6bb4c1937..b0b157e9c 100644 --- a/packages/ai/src/providers/openai-codex/request-transformer.ts +++ b/packages/ai/src/providers/openai-codex/request-transformer.ts @@ -312,18 +312,38 @@ export interface CodexLiteShapedBody { * rewrite removes top-level `tools`, a forced hosted-tool choice (e.g. * `{ type: "web_search" }`) would leave the backend unable to validate the * choice against a tools collection and it rejects the request with HTTP 400 - * (#5771). Such choices must fall back to `"auto"`; explicit string constraints - * such as `"none"` and `"required"` remain valid. Shared by normal turns and - * both remote-compaction paths — codex-rs routes `/responses/compact` through - * the same builder. + * (#5771). Native computer and named-function choices preserve exact forcing + * by isolating the selected declaration and using `"required"`; other hosted + * choices fall back to `"auto"`. Explicit string constraints such as `"none"` + * and `"required"` remain valid. Shared by normal turns and both remote-compaction + * paths — codex-rs routes `/responses/compact` through the same builder. */ export function applyCodexResponsesLiteShape(body: CodexLiteShapedBody): void { const input = Array.isArray(body.input) ? body.input : []; stripImageDetails(input); body.parallel_tool_calls = false; - const prefix: InputItem[] = [ - { type: "additional_tools", role: "developer", tools: Array.isArray(body.tools) ? body.tools : [] }, - ]; + const declaredTools = Array.isArray(body.tools) ? body.tools : []; + let additionalTools = declaredTools; + if (body.tool_choice && typeof body.tool_choice === "object" && "type" in body.tool_choice) { + const choice = body.tool_choice; + const selected = declaredTools.find(tool => { + if (tool === null || typeof tool !== "object" || !("type" in tool)) return false; + if (choice.type === "computer") return tool.type === "computer"; + return ( + choice.type === "function" && + tool.type === "function" && + "name" in choice && + typeof choice.name === "string" && + "name" in tool && + tool.name === choice.name + ); + }); + if (selected) { + additionalTools = [selected]; + body.tool_choice = "required"; + } + } + const prefix: InputItem[] = [{ type: "additional_tools", role: "developer", tools: additionalTools }]; if (typeof body.instructions === "string" && body.instructions.length > 0) { prefix.push({ type: "message", diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 464598d3b..e9fb69d37 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -1196,10 +1196,15 @@ export function buildParams( const emittedNames = new Set( params.tools.map(t => (t as { name?: string }).name).filter((n): n is string => n !== undefined), ); + const emittedComputer = params.tools.some(tool => tool.type === "computer"); const survivingTools = params.tools.length === context.tools.length ? context.tools - : context.tools.filter(t => emittedNames.has(t.customWireName ?? t.name)); + : context.tools.filter( + t => + emittedNames.has(t.customWireName ?? t.name) || + (t.native?.type === "computer" && emittedComputer), + ); const toolChoice = mapOpenAIResponsesToolChoiceForTools(options.toolChoice, survivingTools, model); if (toolChoice !== undefined && params.tools.length > 0) { params.tool_choice = toolChoice; diff --git a/packages/ai/test/openai-codex-responses-lite.test.ts b/packages/ai/test/openai-codex-responses-lite.test.ts index b1787d40a..0b31a2aae 100644 --- a/packages/ai/test/openai-codex-responses-lite.test.ts +++ b/packages/ai/test/openai-codex-responses-lite.test.ts @@ -365,6 +365,22 @@ describe("openai-codex Responses Lite input shaping", () => { expect(disabled.tools).toBeUndefined(); }); + it.each(["gpt-5.3-codex-spark", "gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"])( + "preserves a forced computer function through Lite for %s", + async modelId => { + const model = createCodexModel(modelId); + const computer = { type: "function", name: "computer", parameters: { type: "object" } }; + const other = { type: "function", name: "read", parameters: { type: "object" } }; + const body = await transformRequestBody( + { model: model.id, tools: [computer, other], tool_choice: { type: "function", name: "computer" } }, + model, + { responsesLite: true }, + ); + expect(body.input?.[0]).toEqual({ type: "additional_tools", role: "developer", tools: [computer] }); + expect(body.tool_choice).toBe("required"); + }, + ); + it("moves instructions and tools into input items under lite", async () => { const model = createCodexModel("gpt-5.6-terra"); const tools = [{ type: "function", name: "shot", parameters: { type: "object" } }]; diff --git a/packages/ai/test/openai-computer-contract.test.ts b/packages/ai/test/openai-computer-contract.test.ts index 51d08cc8e..eab46fbf8 100644 --- a/packages/ai/test/openai-computer-contract.test.ts +++ b/packages/ai/test/openai-computer-contract.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { + buildTransformedCodexRequestBody, convertCodexResponsesMessages, convertOpenAICodexResponsesTools, normalizeCodexToolChoice, @@ -16,7 +17,7 @@ import { convertResponsesAssistantMessage, processResponsesStream, } from "@oh-my-pi/pi-ai/providers/openai-shared"; -import type { AssistantMessage, Model, ModelSpec, Tool, ToolResultMessage } from "@oh-my-pi/pi-ai/types"; +import type { AssistantMessage, Context, Model, ModelSpec, Tool, ToolResultMessage } from "@oh-my-pi/pi-ai/types"; import { sanitizeOpenAIResponsesHistoryItemsForReplay } from "@oh-my-pi/pi-ai/utils"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { type } from "arktype"; @@ -126,6 +127,66 @@ describe("OpenAI GA computer contract", () => { }); }); + test("uses the function fallback for every tested subscription model in regular and Lite requests", async () => { + const otherTool: Tool = { name: "read", description: "read", parameters: type({ path: "string" }) }; + for (const id of ["gpt-5.3-codex-spark", "gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"]) { + const subscription = model("openai-codex-responses", id); + const context: Context = { + messages: [{ role: "user", content: "capture the screen", timestamp: 1 }], + tools: [computerTool, otherTool], + }; + expect(subscription.supportsComputerUse).toBe(false); + + const regular = await buildTransformedCodexRequestBody(subscription, context, { + toolChoice: { type: "computer" }, + responsesLite: false, + }); + expect(regular.tools).toMatchObject([ + { type: "function", name: "computer" }, + { type: "function", name: "read" }, + ]); + expect(regular.tool_choice).toEqual({ type: "function", name: "computer" }); + + const lite = await buildTransformedCodexRequestBody(subscription, context, { + toolChoice: { type: "computer" }, + responsesLite: true, + }); + expect(lite.tools).toBeUndefined(); + expect(lite.input?.[0]).toMatchObject({ + type: "additional_tools", + tools: [{ type: "function", name: "computer" }], + }); + expect(lite.tool_choice).toBe("required"); + } + }); + + test("preserves an explicit future Codex native opt-in through regular and Lite requests", async () => { + const optedIn = buildModel({ + ...model("openai-codex-responses", "gpt-5.6-terra"), + supportsComputerUse: true, + } as ModelSpec<"openai-codex-responses">); + const context: Context = { + messages: [{ role: "user", content: "capture", timestamp: 1 }], + tools: [computerTool], + }; + expect(convertOpenAICodexResponsesTools([computerTool], optedIn)).toEqual([{ type: "computer" }]); + expect(normalizeCodexToolChoice({ type: "computer" }, [computerTool], optedIn)).toEqual({ type: "computer" }); + + const regular = await buildTransformedCodexRequestBody(optedIn, context, { + toolChoice: { type: "computer" }, + responsesLite: false, + }); + expect(regular.tools).toEqual([{ type: "computer" }]); + expect(regular.tool_choice).toEqual({ type: "computer" }); + + const lite = await buildTransformedCodexRequestBody(optedIn, context, { + toolChoice: { type: "computer" }, + responsesLite: true, + }); + expect(lite.tools).toBeUndefined(); + expect(lite.input?.[0]).toEqual({ type: "additional_tools", role: "developer", tools: [{ type: "computer" }] }); + expect(lite.tool_choice).toBe("required"); + }); test("parses batched streamed actions, stable item id, and safety checks", async () => { const output = assistant([]); const emitted: unknown[] = []; @@ -503,4 +564,70 @@ describe("OpenAI GA computer contract", () => { ).toBe(true); expect(JSON.stringify(replay)).toContain("data:image/png;base64,cG5n"); }); + + test("unrolls direct API computer history after switching to a subscription model", async () => { + const current = model("openai-codex-responses", "gpt-5.6-terra"); + const call = assistant([ + { + type: "toolCall", + id: "call_direct_computer|item_direct_computer", + name: "computer", + arguments: {}, + providerMetadata: { + type: "computer", + providerItemId: "item_direct_computer", + actions: [{ type: "screenshot" }], + pendingSafetyChecks: [], + }, + }, + ]); + const result: ToolResultMessage = { + role: "toolResult", + toolCallId: "call_direct_computer|item_direct_computer", + toolName: "computer", + content: [{ type: "image", data: "cG5n", mimeType: "image/png", detail: "original" }], + isError: false, + timestamp: 2, + providerMetadata: { + type: "computer", + screenshot: { type: "computer_screenshot", image_url: "data:image/png;base64,cG5n" }, + acknowledgedSafetyChecks: [], + }, + }; + + const replay = convertCodexResponsesMessages(current, { messages: [call, result] }); + expect(replay.some(item => item.type === "computer_call" || item.type === "computer_call_output")).toBe(false); + expect(replay).toContainEqual( + expect.objectContaining({ type: "function_call", name: "computer", call_id: "call_direct_computer" }), + ); + expect(replay).toContainEqual( + expect.objectContaining({ type: "function_call_output", call_id: "call_direct_computer" }), + ); + + const context: Context = { + messages: [call, result, { role: "user", content: "continue", timestamp: 3 }], + tools: [computerTool], + }; + for (const responsesLite of [false, true]) { + const body = await buildTransformedCodexRequestBody(current, context, { + toolChoice: { type: "computer" }, + responsesLite, + }); + const serialized = JSON.stringify(body); + expect(serialized).not.toContain('"type":"computer_call"'); + expect(serialized).not.toContain('"type":"computer_call_output"'); + expect(serialized).toContain('"type":"function_call"'); + expect(serialized).toContain('"type":"function_call_output"'); + if (responsesLite) { + expect(body.input?.[0]).toMatchObject({ + type: "additional_tools", + tools: [{ type: "function", name: "computer" }], + }); + expect(body.tool_choice).toBe("required"); + } else { + expect(body.tools).toMatchObject([{ type: "function", name: "computer" }]); + expect(body.tool_choice).toEqual({ type: "function", name: "computer" }); + } + } + }); }); diff --git a/packages/ai/test/openai-responses-tool-quarantine.test.ts b/packages/ai/test/openai-responses-tool-quarantine.test.ts index bf7e4ad8a..f3e449104 100644 --- a/packages/ai/test/openai-responses-tool-quarantine.test.ts +++ b/packages/ai/test/openai-responses-tool-quarantine.test.ts @@ -71,6 +71,12 @@ const goodTool: Tool = { description: "read a file", parameters: type({ path: "string" }), }; +const computerTool: Tool = { + name: "computer", + description: "control the desktop", + parameters: type({}), + native: { type: "computer" }, +}; describe("convertTools quarantine (#2652)", () => { test("drops only the tool with the provider-rejecting schema, keeping the rest", () => { @@ -123,4 +129,16 @@ describe("buildParams tool_choice reconciliation (#2652)", () => { ); expect(params.tool_choice).toEqual({ type: "function", name: "read_file" }); }); + + test("keeps a forced native computer choice when only a sibling tool is quarantined", () => { + const nativeModel = { ...makeModel(), supportsComputerUse: true }; + const { params } = buildParams( + nativeModel, + ctx([computerTool, badTool]), + { toolChoice: { type: "computer" } }, + undefined, + ); + expect(params.tools).toEqual([{ type: "computer" }]); + expect(params.tool_choice).toEqual({ type: "computer" }); + }); }); diff --git a/packages/catalog/src/build.ts b/packages/catalog/src/build.ts index 0fab6c322..a5af74d69 100644 --- a/packages/catalog/src/build.ts +++ b/packages/catalog/src/build.ts @@ -20,18 +20,35 @@ import { cleanModelName } from "./utils"; const OPENAI_GA_COMPUTER_MODEL_RE = /^gpt-5\.(?:[4-9]|[1-9]\d)(?:[.-]|$)/i; +function isDirectOpenAIResponsesEndpoint(spec: ModelSpec): boolean { + if (spec.api === "openai-responses") { + if (spec.provider !== "openai") return false; + if (!spec.baseUrl) return true; + try { + const url = new URL(spec.baseUrl); + return url.protocol === "https:" && url.hostname === "api.openai.com"; + } catch { + return false; + } + } + if (spec.api !== "azure-openai-responses" || (spec.provider !== "azure" && spec.provider !== "azure-openai")) { + return false; + } + if (!spec.baseUrl) return spec.provider === "azure"; + try { + const url = new URL(spec.baseUrl); + return ( + url.protocol === "https:" && + (url.hostname.endsWith(".openai.azure.com") || url.hostname === "models.inference.ai.azure.com") + ); + } catch { + return false; + } +} + function supportsOpenAIGAComputerUse(spec: ModelSpec): boolean { if (spec.supportsComputerUse !== undefined) return spec.supportsComputerUse; - if ( - spec.api !== "openai-responses" && - spec.api !== "openai-codex-responses" && - spec.api !== "azure-openai-responses" - ) { - return false; - } - if (spec.api !== "azure-openai-responses" && spec.provider !== "openai" && spec.provider !== "openai-codex") { - return false; - } + if (!isDirectOpenAIResponsesEndpoint(spec)) return false; return OPENAI_GA_COMPUTER_MODEL_RE.test(spec.requestModelId ?? spec.id); } diff --git a/packages/catalog/test/build.test.ts b/packages/catalog/test/build.test.ts index 8507d061f..bcda7d716 100644 --- a/packages/catalog/test/build.test.ts +++ b/packages/catalog/test/build.test.ts @@ -144,6 +144,48 @@ describe("buildModel", () => { expect(buildModel(completionsSpec({ name })).name).toBe(name); } }); + it("limits inferred GA computer capability to first-party Responses transports", () => { + const common = { + id: "gpt-5.6-terra", + name: "GPT-5.6 Terra", + reasoning: true, + input: ["text", "image"] as Array<"text" | "image">, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400_000, + maxTokens: 128_000, + }; + const direct = { + ...common, + api: "openai-responses" as const, + provider: "openai", + baseUrl: "https://api.openai.com/v1", + } satisfies ModelSpec<"openai-responses">; + + expect(buildModel(direct).supportsComputerUse).toBe(true); + expect(buildModel({ ...direct, baseUrl: "https://gateway.example/v1" }).supportsComputerUse).toBe(false); + expect(buildModel({ ...direct, provider: "gpt-proxy" }).supportsComputerUse).toBe(false); + + const subscription = { + ...common, + api: "openai-codex-responses" as const, + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + } satisfies ModelSpec<"openai-codex-responses">; + for (const id of ["gpt-5.3-codex-spark", "gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"]) { + expect(buildModel({ ...subscription, id, name: id }).supportsComputerUse).toBe(false); + } + expect(buildModel({ ...subscription, supportsComputerUse: true }).supportsComputerUse).toBe(true); + + const azure = { + ...common, + api: "azure-openai-responses" as const, + provider: "azure", + baseUrl: "", + } satisfies ModelSpec<"azure-openai-responses">; + expect(buildModel(azure).supportsComputerUse).toBe(true); + expect(buildModel({ ...azure, provider: "custom-azure-proxy" }).supportsComputerUse).toBe(false); + expect(buildModel({ ...azure, baseUrl: "https://gateway.example/openai/v1" }).supportsComputerUse).toBe(false); + }); }); describe("xAI-OAuth Responses reasoning-effort suppression", () => {