fix(computer): gate native Responses transport
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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<ResponseInput[number]>;
|
||||
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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" } }];
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Api>): 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<Api>): 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user