From 49dd2cea342589f829f7a95322fadc56cfeaaba8 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:36:57 -0500 Subject: [PATCH] refactor(ai): clarify responses adapters (#46469) --- packages/ai/src/protocols/open-responses.ts | 79 ++++++++++--------- packages/ai/src/protocols/openai-responses.ts | 12 +-- packages/ai/src/protocols/xai-responses.ts | 10 +-- 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 407af480a12..20ce29b6e5d 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -176,14 +176,14 @@ export const InputItem = Schema.Union([ HostedToolItem, ]) type OpenResponsesInputItem = Schema.Schema.Type -export type ExtendedHostedToolItem = { +export type HostedToolReplayItem = { readonly type: string readonly id: string readonly [key: string]: unknown } type LoweredInputItem = | OpenResponsesInputItem - | ExtendedHostedToolItem + | HostedToolReplayItem | { readonly type: "message" readonly id?: string @@ -373,7 +373,7 @@ export const Event = Schema.StructWithRest( ) export type Event = Schema.Schema.Type -export interface Extension { +export interface ProviderAdapter { readonly id: string readonly name: string readonly lowerMedia?: (input: { @@ -381,10 +381,10 @@ export interface Extension { readonly media: ProviderShared.NormalizedMedia readonly request: LLMRequest }) => MediaInput | undefined - readonly lowerHostedToolItem?: (item: unknown) => ExtendedHostedToolItem | undefined + readonly restoreHostedToolItem?: (item: unknown) => HostedToolReplayItem | undefined } -const BASE: Extension = { id: ADAPTER, name: NAME } +const BASE_ADAPTER: ProviderAdapter = { id: ADAPTER, name: NAME } export interface ParserState { readonly id: string @@ -482,12 +482,12 @@ const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenR const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* ( part: MediaPart, request: LLMRequest, - extension: Extension, + adapter: ProviderAdapter, target: "message" | "tool-result", ) { const media = ProviderShared.normalizeMedia(part) - const extended = extension.lowerMedia?.({ part, media, request }) - if (extended) return extended + const providerMedia = adapter.lowerMedia?.({ part, media, request }) + if (providerMedia) return providerMedia const url = typeof part.data === "string" && (part.data.startsWith("https://") || part.data.startsWith("http://")) ? part.data @@ -507,17 +507,17 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* ( const lowerUserContent = Effect.fnUntraced(function* ( part: LLMRequest["messages"][number]["content"][number], request: LLMRequest, - extension: Extension, + adapter: ProviderAdapter, ) { if (part.type === "text") return { type: "input_text" as const, text: part.text } - if (part.type === "media") return yield* lowerMessageMedia(part, request, extension) - return yield* ProviderShared.unsupportedContent(extension.name, "user", ["text", "media"]) + if (part.type === "media") return yield* lowerMessageMedia(part, request, adapter) + return yield* ProviderShared.unsupportedContent(adapter.name, "user", ["text", "media"]) }) -const lowerMessageMedia = Effect.fnUntraced(function* (part: MediaPart, request: LLMRequest, extension: Extension) { - const lowered = yield* lowerMedia(part, request, extension, "message") +const lowerMessageMedia = Effect.fnUntraced(function* (part: MediaPart, request: LLMRequest, adapter: ProviderAdapter) { + const lowered = yield* lowerMedia(part, request, adapter, "message") if (lowered.type === "input_video") - return yield* ProviderShared.invalidRequest(`${extension.name} user messages do not support input_video`) + return yield* ProviderShared.invalidRequest(`${adapter.name} user messages do not support input_video`) return lowered }) @@ -526,13 +526,13 @@ const lowerMessageMedia = Effect.fnUntraced(function* (part: MediaPart, request: const lowerToolResultContentItem = Effect.fnUntraced(function* ( item: Content, request: LLMRequest, - extension: Extension, + adapter: ProviderAdapter, ) { if (item.type === "text") return { type: "input_text" as const, text: item.text } return yield* lowerMedia( { type: "media", mediaType: item.mime, data: item.uri, filename: item.name }, request, - extension, + adapter, "tool-result", ) }) @@ -540,30 +540,33 @@ const lowerToolResultContentItem = Effect.fnUntraced(function* ( const lowerHostedToolResultContentItem = Effect.fnUntraced(function* ( item: Content, request: LLMRequest, - extension: Extension, + adapter: ProviderAdapter, ) { if (item.type === "text") return { type: "input_text" as const, text: item.text } return yield* lowerMessageMedia( { type: "media", mediaType: item.mime, data: item.uri, filename: item.name }, request, - extension, + adapter, ) }) const lowerToolResultOutput = Effect.fnUntraced(function* ( part: ToolResultPart, request: LLMRequest, - extension: Extension, + adapter: ProviderAdapter, ) { // Text/json/error results are encoded as a plain string for backward // compatibility with existing cassettes and provider expectations. if (part.result.type !== "content") return ProviderShared.toolResultText(part) // Preserve the narrowed array element type when compiled through a consumer package. const content: ReadonlyArray = part.result.value - return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)) + return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, adapter)) }) -const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (request: LLMRequest, extension: Extension) { +const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* ( + request: LLMRequest, + adapter: ProviderAdapter, +) { const input: LoweredInputItem[] = [] const providerMetadataKey = request.model.route.providerMetadataKey ?? "openresponses" @@ -571,13 +574,13 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques if (message.role === "system") { input.push({ role: "developer", - content: ProviderShared.joinText(yield* ProviderShared.systemUpdateText(extension.name, message)), + content: ProviderShared.joinText(yield* ProviderShared.systemUpdateText(adapter.name, message)), }) continue } if (message.role === "user") { - const content = yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request, extension)) + const content = yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request, adapter)) if (content.length > 0) input.push({ role: "user", content }) continue } @@ -644,7 +647,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques ? undefined : Schema.is(HostedToolItem)(part.result.value) ? part.result.value - : extension.lowerHostedToolItem?.(part.result.value) + : adapter.restoreHostedToolItem?.(part.result.value) if (id !== undefined && hosted?.id === id) { if (!hostedToolItems.has(id)) { input.push(hosted) @@ -658,13 +661,11 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques : [{ type: "text", text: ProviderShared.toolResultText(part) }] input.push({ role: "user", - content: yield* Effect.forEach(content, (item) => - lowerHostedToolResultContentItem(item, request, extension), - ), + content: yield* Effect.forEach(content, (item) => lowerHostedToolResultContentItem(item, request, adapter)), }) continue } - return yield* ProviderShared.unsupportedContent(extension.name, "assistant", [ + return yield* ProviderShared.unsupportedContent(adapter.name, "assistant", [ "text", "reasoning", "tool-call", @@ -677,11 +678,11 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques for (const part of message.content) { if (!ProviderShared.supportsContent(part, ["tool-result"])) - return yield* ProviderShared.unsupportedContent(extension.name, "tool", ["tool-result"]) + return yield* ProviderShared.unsupportedContent(adapter.name, "tool", ["tool-result"]) input.push({ type: "function_call_output", call_id: part.id, - output: yield* lowerToolResultOutput(part, request, extension), + output: yield* lowerToolResultOutput(part, request, adapter), }) } } @@ -733,28 +734,28 @@ const allowedToolChoice = (request: LLMRequest) => { } } -export const fromRequestWithExtension = Effect.fn("OpenResponses.fromRequestWithExtension")(function* ( +export const fromRequestWithAdapter = Effect.fn("OpenResponses.fromRequestWithAdapter")(function* ( request: LLMRequest, - extension: Extension, + adapter: ProviderAdapter, ) { const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema return { model: request.model.id, - input: yield* lowerMessages(request, extension), + input: yield* lowerMessages(request, adapter), tools: request.tools.length === 0 ? undefined : yield* Effect.forEach(request.tools, (tool) => lowerTool( - extension.name, + adapter.name, tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility), ), ), tool_choice: allowedToolChoice(request) ?? - (request.toolChoice ? yield* lowerToolChoice(extension.name, request.toolChoice) : undefined), + (request.toolChoice ? yield* lowerToolChoice(adapter.name, request.toolChoice) : undefined), stream: true as const, max_output_tokens: generation?.maxTokens, temperature: generation?.temperature, @@ -768,7 +769,7 @@ export const fromRequestWithExtension = Effect.fn("OpenResponses.fromRequestWith const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesBody)) export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* (request: LLMRequest) { - return yield* decodeBody(yield* fromRequestWithExtension(request, BASE)) + return yield* decodeBody(yield* fromRequestWithAdapter(request, BASE_ADAPTER)) }) // ============================================================================= @@ -1408,9 +1409,9 @@ export const step = (state: ParserState, input: Event) => { * The provider-neutral Open Responses protocol. Provider-specific Responses * implementations compose this baseline with their own tools and event variants. */ -export const initial = (request: LLMRequest, extension: Extension = BASE): ParserState => ({ - id: extension.id, - name: extension.name, +export const initial = (request: LLMRequest, adapter: ProviderAdapter = BASE_ADAPTER): ParserState => ({ + id: adapter.id, + name: adapter.name, providerMetadataKey: request.model.route.providerMetadataKey ?? "openresponses", hasFunctionCall: false, tools: ToolStream.empty(), diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index f88b1edb7f4..e744283a910 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -86,11 +86,11 @@ const OpenAIResponsesBody = Schema.Struct({ }) export type OpenAIResponsesBody = Schema.Schema.Type -const extension = { +const adapter = { id: ADAPTER, name: NAME, - lowerHostedToolItem: (item: unknown) => (Schema.is(OpenAIResponsesHostedToolItem)(item) ? item : undefined), -} satisfies OpenResponses.Extension + restoreHostedToolItem: (item: unknown) => (Schema.is(OpenAIResponsesHostedToolItem)(item) ? item : undefined), +} satisfies OpenResponses.ProviderAdapter const nativeImageToolInput = (tool: ToolDefinition) => { const native = tool.native?.openai @@ -125,9 +125,9 @@ const lowerToolChoice = (toolChoice: NonNullable, tool const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesBody)) const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) { - const body = yield* OpenResponses.fromRequestWithExtension( + const body = yield* OpenResponses.fromRequestWithAdapter( LLMRequest.update(request, { tools: [], toolChoice: undefined }), - extension, + adapter, ) const toolSchemaCompatibility = request.model.compatibility?.toolSchema const parallelToolCalls = OpenResponses.resolveParallelToolCalls(request) @@ -204,7 +204,7 @@ export const protocol = Protocol.make({ }, stream: { event: OpenResponses.protocol.stream.event, - initial: (request) => OpenResponses.initial(request, extension), + initial: (request) => OpenResponses.initial(request, adapter), step, terminal: OpenResponses.terminal, }, diff --git a/packages/ai/src/protocols/xai-responses.ts b/packages/ai/src/protocols/xai-responses.ts index 150237b07e8..5f4c7e10126 100644 --- a/packages/ai/src/protocols/xai-responses.ts +++ b/packages/ai/src/protocols/xai-responses.ts @@ -36,15 +36,15 @@ const XAIResponsesBody = Schema.Struct({ stream: Schema.Literal(true), }) -const extension = { +const adapter = { id: ADAPTER, name: NAME, - lowerHostedToolItem: (item: unknown) => (Schema.is(XAIResponsesHostedToolItem)(item) ? item : undefined), -} satisfies OpenResponses.Extension + restoreHostedToolItem: (item: unknown) => (Schema.is(XAIResponsesHostedToolItem)(item) ? item : undefined), +} satisfies OpenResponses.ProviderAdapter const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(XAIResponsesBody)) const fromRequest = Effect.fn("XAIResponses.fromRequest")(function* (request: LLMRequest) { - return yield* decodeBody(yield* OpenResponses.fromRequestWithExtension(request, extension)) + return yield* decodeBody(yield* OpenResponses.fromRequestWithAdapter(request, adapter)) }) const HOSTED_TOOLS = { @@ -78,7 +78,7 @@ export const protocol = Protocol.make({ }, stream: { event: OpenResponses.protocol.stream.event, - initial: (request) => OpenResponses.initial(request, extension), + initial: (request) => OpenResponses.initial(request, adapter), step, terminal: OpenResponses.terminal, },