diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 10ca6364fd7..704b5d99375 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -134,7 +134,7 @@ const HOSTED_TOOLS = { name: "code_interpreter", input: (item) => ({ code: item.code, container_id: item.container_id }), }, - computer_use_call: { name: "computer_use", input: (item) => item.action ?? {} }, + computer_call: { name: "computer_use", input: (item) => item.action ?? {} }, image_generation_call: { name: "image_generation", input: () => ({}), result: hostedToolResult }, mcp_call: { name: "mcp", diff --git a/packages/ai/src/protocols/utils/open-responses-options.ts b/packages/ai/src/protocols/utils/open-responses-options.ts index 89b72b1fdbd..ff00d74dbf1 100644 --- a/packages/ai/src/protocols/utils/open-responses-options.ts +++ b/packages/ai/src/protocols/utils/open-responses-options.ts @@ -28,7 +28,11 @@ export const ResponseIncludables = [ export type ResponseIncludable = (typeof ResponseIncludables)[number] | (string & {}) export const ServiceTiers = ["auto", "default", "flex", "priority"] as const -export type ServiceTier = (typeof ServiceTiers)[number] +export type ServiceTier = (typeof ServiceTiers)[number] | (string & {}) +export const ServiceTier = Schema.declare( + (value): value is ServiceTier => typeof value === "string", + { title: "ServiceTier" }, +) export const Truncations = ["auto", "disabled"] as const export type Truncation = (typeof Truncations)[number] @@ -38,7 +42,7 @@ export const ResponseIncludableSchema = Schema.declare( (value): value is ResponseIncludable => typeof value === "string", { title: "ResponseIncludable" }, ) -export const ServiceTierSchema = Schema.Literals(ServiceTiers) +export const ServiceTierSchema = ServiceTier export const TruncationSchema = Schema.Literals(Truncations) export const AllowedTools = Schema.Struct({ diff --git a/packages/ai/src/protocols/utils/openai-options.ts b/packages/ai/src/protocols/utils/openai-options.ts index bb47053093c..f3e56be4a06 100644 --- a/packages/ai/src/protocols/utils/openai-options.ts +++ b/packages/ai/src/protocols/utils/openai-options.ts @@ -9,8 +9,8 @@ export type OpenAITextVerbosity = OpenResponsesOptions.TextVerbosity // in lockstep with `openai-node/src/resources/responses/responses.ts`. export const OpenAIResponseIncludables = OpenResponsesOptions.ResponseIncludables export type OpenAIResponseIncludable = OpenResponsesOptions.ResponseIncludable -export const OpenAIServiceTiers = OpenResponsesOptions.ServiceTiers -export type OpenAIServiceTier = OpenResponsesOptions.ServiceTier +export const OpenAIServiceTiers = [...OpenResponsesOptions.ServiceTiers, "scale"] as const +export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number] | (string & {}) export const OpenAIReasoningEffort = OpenResponsesOptions.ReasoningEffort export const OpenAITextVerbosity = OpenResponsesOptions.TextVerbosity diff --git a/packages/ai/src/providers/openai-options.ts b/packages/ai/src/providers/openai-options.ts index ca85642ef3a..5adaf555a1f 100644 --- a/packages/ai/src/providers/openai-options.ts +++ b/packages/ai/src/providers/openai-options.ts @@ -1,9 +1,13 @@ import { mergeProviderOptions, type ProviderOptions } from "../schema/index.js" -import type { OpenResponsesOptionsInput } from "./open-responses-options.js" +import type { OpenAIServiceTier } from "../protocols/utils/openai-options.js" +import type { Options } from "../protocols/utils/open-responses-options.js" export type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options.js" -export type OpenAIOptionsInput = OpenResponsesOptionsInput +export type OpenAIOptionsInput = Omit & { + readonly serviceTier?: OpenAIServiceTier + readonly [key: string]: unknown +} export type OpenAIProviderOptionsInput = OpenAIOptionsInput diff --git a/packages/ai/test/provider-options/google-vertex-chat.types.ts b/packages/ai/test/provider-options/google-vertex-chat.types.ts index 2e748e65377..5b6e9840c4d 100644 --- a/packages/ai/test/provider-options/google-vertex-chat.types.ts +++ b/packages/ai/test/provider-options/google-vertex-chat.types.ts @@ -4,10 +4,4 @@ import { GoogleVertexChat } from "../../src/providers.js" const model = GoogleVertexChat.configure({ accessToken: "test", project: "project" }).model("gemini") LLM.request({ model, prompt: "Hello", providerOptions: { serviceTier: "priority" } }) - -LLM.request({ - model, - prompt: "Hello", - // @ts-expect-error Vertex OpenAI-compatible service tiers use the OpenAI union. - providerOptions: { serviceTier: "premium" }, -}) +LLM.request({ model, prompt: "Hello", providerOptions: { serviceTier: "future-tier" } }) diff --git a/packages/ai/test/provider-options/openai.types.ts b/packages/ai/test/provider-options/openai.types.ts index 6419070b355..e2746750f48 100644 --- a/packages/ai/test/provider-options/openai.types.ts +++ b/packages/ai/test/provider-options/openai.types.ts @@ -8,6 +8,8 @@ LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffo LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } }) LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "low" } }) LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } }) +LLM.request({ model: selected, prompt: "Hello", providerOptions: { serviceTier: "scale" } }) +LLM.request({ model: selected, prompt: "Hello", providerOptions: { serviceTier: "future-tier" } }) LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "max" } }) LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } }) diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts index e1968d6272b..ad46fbc516d 100644 --- a/packages/ai/test/provider/openai-compatible-responses.test.ts +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -189,6 +189,7 @@ describe("Open Responses-compatible route", () => { streamOptions: { includeObfuscation: false }, topLogprobs: 3, truncation: "auto", + serviceTier: "provider-tier", allowedTools: { toolNames: ["lookup"] }, maxToolCalls: 2, parallelToolCalls: false, @@ -213,6 +214,7 @@ describe("Open Responses-compatible route", () => { presence_penalty: 0.2, frequency_penalty: -0.1, truncation: "auto", + service_tier: "provider-tier", tool_choice: { type: "allowed_tools", mode: "auto", diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 15489673fa5..0a0f40af04d 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -188,13 +188,13 @@ describe("OpenAI Responses route", () => { }), ) - it.effect("omits unsupported semantic service tiers", () => + it.effect("passes through provider-defined service tiers", () => Effect.gen(function* () { const prepared = yield* compileRequest( - LLMRequest.update(request, { providerOptions: { serviceTier: "unsupported" } }), + LLMRequest.update(request, { providerOptions: { serviceTier: "scale" } }), ) - expect(prepared.body).not.toHaveProperty("service_tier") + expect(prepared.body.service_tier).toBe("scale") }), ) @@ -2652,6 +2652,47 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("decodes computer_call as provider-executed tool-call + tool-result", () => + Effect.gen(function* () { + const item = { + type: "computer_call", + id: "computer_1", + call_id: "call_1", + status: "completed", + action: { type: "click", x: 100, y: 200 }, + } + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "response.output_item.done", item }, + { type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } }, + ), + ), + ), + ) + + expect(response.events.filter((event) => event.type === "tool-call" || event.type === "tool-result")).toEqual([ + { + type: "tool-call", + id: "computer_1", + name: "computer_use", + input: { type: "click", x: 100, y: 200 }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "computer_1" } }, + }, + { + type: "tool-result", + id: "computer_1", + name: "computer_use", + result: { type: "json", value: item }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "computer_1" } }, + }, + ]) + }), + ) + it.effect("decodes image generation output as image content", () => Effect.gen(function* () { const item = {