diff --git a/packages/ai/src/protocols/alibaba-responses.ts b/packages/ai/src/protocols/alibaba-responses.ts index 24c1748c374..aecbfa3d571 100644 --- a/packages/ai/src/protocols/alibaba-responses.ts +++ b/packages/ai/src/protocols/alibaba-responses.ts @@ -1,7 +1,7 @@ import { Effect, Schema } from "effect" import { Protocol } from "../route/protocol.js" import { OpenResponses } from "./open-responses.js" -import { JsonObject, optionalArray, ProviderShared } from "./shared.js" +import { JsonObject, ProviderShared } from "./shared.js" import { OpenResponsesOptions } from "./utils/open-responses-options.js" import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js" @@ -25,8 +25,6 @@ const WebExtractorItem = Schema.StructWithRest( ) const Body = Schema.Struct({ ...OpenResponses.coreFields, - input: Schema.Array(Schema.Union([OpenResponses.InputItem, WebExtractorItem])), - tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])), enable_thinking: Options.fields.enableThinking, previous_response_id: Options.fields.previousResponseId, conversation: Options.fields.conversation, @@ -52,7 +50,7 @@ export const protocol = Protocol.make({ const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {}) const body = yield* OpenResponses.fromRequestWithAdapter(req, adapter) const choice = body.tool_choice - return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))({ + return { ...body, enable_thinking: opts.enableThinking, previous_response_id: opts.previousResponseId, @@ -62,7 +60,7 @@ export const protocol = Protocol.make({ typeof choice === "object" && choice.type === "function" ? { type: "allowed_tools" as const, mode: "required" as const, tools: [choice] } : choice, - }) + } }), }, stream: { diff --git a/packages/ai/src/protocols/meta-responses.ts b/packages/ai/src/protocols/meta-responses.ts index 79827fa3178..3df616f8163 100644 --- a/packages/ai/src/protocols/meta-responses.ts +++ b/packages/ai/src/protocols/meta-responses.ts @@ -1,6 +1,5 @@ import { Effect, Encoding, Schema } from "effect" import { Protocol } from "../route/protocol.js" -import { HttpTransport } from "../route/transport/index.js" import { LLMEvent, LLMRequest, Message, ToolResultPart } from "../schema/index.js" import { OpenResponses } from "./open-responses.js" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js" @@ -44,13 +43,6 @@ const ImageItem = Schema.Struct({ error: Schema.optional(Schema.Unknown), }) -const Body = Schema.Struct({ - ...OpenResponses.coreFields, - input: Schema.Array(Schema.Union([OpenResponses.InputItem, ImageItem])), - tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])), - stream: Schema.Literal(true), -}) - const MessageAnnotations = Schema.Struct({ content: Schema.Array(Schema.Struct({ annotations: optionalArray(JsonObject) })), }) @@ -62,12 +54,13 @@ interface ParserState extends OpenResponses.ParserState { const adapter = { id: ADAPTER, name: NAME, + nativeTool: (native) => ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(native.meta), restoreHostedToolItem: (item: unknown) => (Schema.is(ImageItem)(item) ? item : undefined), } satisfies OpenResponses.ProviderAdapter const fromRequest = Effect.fn("MetaResponses.fromRequest")(function* (request: LLMRequest) { const key = request.model.route.providerMetadataKey ?? String(request.model.provider) - const projected = ProviderShared.flattenToolRequest( + return yield* OpenResponses.fromRequestWithAdapter( LLMRequest.update(request, { messages: request.messages.map((message) => Message.make({ @@ -93,23 +86,8 @@ const fromRequest = Effect.fn("MetaResponses.fromRequest")(function* (request: L }), ), }), + adapter, ) - return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))({ - ...(yield* OpenResponses.lowerConversation(projected.request, adapter)), - ...OpenResponses.lowerGeneration(request), - tools: - projected.tools.length === 0 - ? undefined - : yield* Effect.forEach(projected.tools, (tool) => - Effect.gen(function* () { - if (tool.native === undefined) return yield* OpenResponses.lowerTool(NAME, tool) - return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(tool.native.meta) - }), - ), - tool_choice: - OpenResponses.allowedToolChoice(request) ?? - (request.toolChoice ? yield* OpenResponses.lowerToolChoice(NAME, request.toolChoice) : undefined), - }) }) const HOSTED_TOOLS = { @@ -222,7 +200,7 @@ const step = Effect.fn("MetaResponses.step")(function* (state: ParserState, inpu export const protocol = Protocol.make({ id: ADAPTER, - body: { schema: Body, from: fromRequest }, + body: { schema: OpenResponses.OpenResponsesBody, from: fromRequest }, stream: { event: OpenResponses.protocol.stream.event, initial: (request): ParserState => ({ ...OpenResponses.initial(request, adapter), completedItems: new Set() }), @@ -231,6 +209,6 @@ export const protocol = Protocol.make({ }, }) -export const httpTransport = HttpTransport.sseJson.with>() +export const httpTransport = OpenResponses.httpTransport export * as MetaResponses from "./meta-responses.js" diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 129cc1441d0..30a0c055271 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -168,10 +168,20 @@ export const ConfigurationUpdate = Schema.Struct({ type: Schema.Literal("configuration_update"), reasoning: Schema.Struct({ effort: OpenResponsesOptions.ReasoningEffort }), }) -type ConfigurationUpdate = Schema.Schema.Type +export type ConfigurationUpdate = Schema.Schema.Type + +export const HostedToolReplay = Schema.StructWithRest( + Schema.Struct({ + type: Schema.String, + id: Schema.String, + }), + [JsonObject], +) +export type HostedToolReplayItem = Schema.Schema.Type export const InputItem = Schema.Union([ CompactionItem, + ConfigurationUpdate, Schema.Struct({ type: Schema.tag("message"), role: Schema.tag("system"), content: Schema.String }), Schema.Struct({ type: Schema.tag("message"), role: Schema.tag("developer"), content: Schema.String }), Schema.Struct({ @@ -204,24 +214,9 @@ export const InputItem = Schema.Union([ output: OpenResponsesFunctionCallOutput, }), HostedToolItem, + HostedToolReplay, ]) type OpenResponsesInputItem = Schema.Schema.Type -export type HostedToolReplayItem = { - readonly type: string - readonly id: string - readonly [key: string]: unknown -} -type LoweredInputItem = - | OpenResponsesInputItem - | HostedToolReplayItem - | ConfigurationUpdate - | { - readonly type: "message" - readonly id?: string - readonly role: "assistant" - readonly content: ReadonlyArray<{ readonly type: "output_text"; readonly text: string }> - readonly phase?: MessagePhase | null - } // Mutable counterpart of the schema reasoning item so `lowerMessages` can fold // multiple streamed summary parts into the same item before flushing. @@ -239,6 +234,14 @@ export const Tool = Schema.Struct({ strict: Schema.optional(Schema.Boolean), }) +export const HostedTool = Schema.StructWithRest( + Schema.Struct({ + type: Schema.String, + }), + [JsonObject], +) +export type HostedTool = Schema.Schema.Type + export const ToolChoice = Schema.Union([ Schema.Literals(["auto", "none", "required"]), Schema.Struct({ type: Schema.tag("function"), name: Schema.String }), @@ -257,7 +260,7 @@ export const coreFields = { model: Schema.String, input: Schema.Array(InputItem), instructions: Schema.optional(Schema.String), - tools: optionalArray(Tool), + tools: optionalArray(Schema.Union([Tool, HostedTool])), tool_choice: Schema.optional(ToolChoice), store: Schema.optional(Schema.Boolean), metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), @@ -292,7 +295,7 @@ export const coreFields = { frequency_penalty: Schema.optional(Schema.Number), } -const OpenResponsesBody = Schema.Struct({ +export const OpenResponsesBody = Schema.Struct({ ...coreFields, stream: Schema.Literal(true), }) @@ -397,9 +400,7 @@ export const decodeChannelEvent = (frame: string) => export interface ProviderAdapter { readonly id: string readonly name: string - readonly nativeTool?: ( - native: NonNullable, - ) => Effect.Effect<{ readonly type: string }, AIError> + readonly nativeTool?: (native: NonNullable) => Effect.Effect readonly lowerMedia?: (input: { readonly part: MediaPart readonly media: Media.Inline | undefined @@ -455,8 +456,10 @@ export const lowerTool = Effect.fn("OpenResponses.lowerTool")(function* (protoco }) export const lowerTools = (tools: ReadonlyArray, adapter: ProviderAdapter) => - Effect.forEach(tools, (tool) => - tool.native !== undefined && adapter.nativeTool ? adapter.nativeTool(tool.native) : lowerTool(adapter.name, tool), + Effect.forEach( + tools, + (tool): Effect.Effect | HostedTool, AIError> => + tool.native !== undefined && adapter.nativeTool ? adapter.nativeTool(tool.native) : lowerTool(adapter.name, tool), ) export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable) => @@ -592,7 +595,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* ( request: LLMRequest, adapter: ProviderAdapter, ) { - const input: LoweredInputItem[] = [] + const input: OpenResponsesInputItem[] = [] const providerMetadataKey = metadataKey(request.model) for (const message of request.messages) { @@ -827,11 +830,7 @@ export const fromRequestWithAdapter = Effect.fn("OpenResponses.fromRequestWithAd } }) -const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesBody)) - -export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* (request: LLMRequest) { - return yield* decodeBody(yield* fromRequestWithAdapter(request, BASE_ADAPTER)) -}) +export const fromRequest = (request: LLMRequest) => fromRequestWithAdapter(request, BASE_ADAPTER) // ============================================================================= // Stream Parsing diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 610a9883267..60a76e47858 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -103,15 +103,8 @@ const OpenAIResponsesToolChoice = Schema.Union([ Schema.Struct({ type: Schema.tag("image_generation") }), ]) -const OpenAIResponsesInputItem = Schema.Union([ - OpenResponses.InputItem, - OpenAIResponsesHostedToolItem, - OpenResponses.ConfigurationUpdate, -]) - const OpenAIResponsesCoreFields = { ...OpenResponses.coreFields, - input: Schema.Array(OpenAIResponsesInputItem), tools: optionalArray(OpenAIResponsesTools), tool_choice: Schema.optional(OpenAIResponsesToolChoice), context_management: Schema.optional( @@ -134,7 +127,7 @@ export type OpenAIResponsesBody = Schema.Schema.Type export const CompactionTrigger = Schema.Struct({ type: Schema.Literal("compaction_trigger") }) const CheckpointBody = Schema.Struct({ ...OpenAIResponsesBody.fields, - input: Schema.Array(Schema.Union([OpenAIResponsesInputItem, CompactionTrigger])), + input: Schema.Array(Schema.Union([OpenResponses.InputItem, CompactionTrigger])), }) const adapter = { @@ -202,15 +195,13 @@ const lowerToolChoice = (toolChoice: NonNullable, tool : { type: "function" as const, name }, }) -const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesBody)) - const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) { const management = yield* ProviderShared.validateWith( Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)), )(request.providerOptions?.contextManagement) const options = OpenResponsesOptions.resolve(request) const updates = resolveEffortUpdates(request, options.reasoningEffort) - return yield* decodeBody({ + return { ...(yield* OpenResponses.lowerConversation(updates.request, adapter)), ...OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }), context_management: management?.map((edit) => ({ type: edit.type, compact_threshold: edit.compactThreshold })), @@ -220,7 +211,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: ? undefined : (OpenResponses.allowedToolChoice(request) ?? (request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined)), - }) + } }) const checkpointBody = { diff --git a/packages/ai/src/protocols/xai-responses.ts b/packages/ai/src/protocols/xai-responses.ts index bb19ebd843a..130a9fd23fc 100644 --- a/packages/ai/src/protocols/xai-responses.ts +++ b/packages/ai/src/protocols/xai-responses.ts @@ -31,19 +31,12 @@ const XAIResponsesHostedToolItem = Schema.Union([ ), ]) -const XAIResponsesBody = Schema.Struct({ - ...OpenResponses.coreFields, - input: Schema.Array(Schema.Union([OpenResponses.InputItem, XAIResponsesHostedToolItem])), - stream: Schema.Literal(true), -}) - const adapter = { id: ADAPTER, name: NAME, 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) { if (request.providerOptions?.contextManagement !== undefined) return yield* ProviderShared.unsupportedOperation({ @@ -53,7 +46,7 @@ const fromRequest = Effect.fn("XAIResponses.fromRequest")(function* (request: LL message: "xAI requires explicit compaction through LLMClient.compact; automatic context management is not supported", }) - return yield* decodeBody(yield* OpenResponses.fromRequestWithAdapter(request, adapter)) + return yield* OpenResponses.fromRequestWithAdapter(request, adapter) }) const HOSTED_TOOLS = { @@ -83,7 +76,7 @@ const step = (state: OpenResponses.ParserState, input: OpenResponses.Event) => { export const protocol = Protocol.make({ id: ADAPTER, body: { - schema: XAIResponsesBody, + schema: OpenResponses.OpenResponsesBody, from: fromRequest, }, stream: { diff --git a/packages/ai/src/route/client.ts b/packages/ai/src/route/client.ts index 83e762160a1..7addb1a0426 100644 --- a/packages/ai/src/route/client.ts +++ b/packages/ai/src/route/client.ts @@ -358,7 +358,6 @@ function makeFromTransport( input: MakeTransportInput, ): Route { const protocol = input.protocol - const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema)) const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event) const decodeEvent = (route: string) => (frame: Frame) => decodeEventEffect(frame).pipe( @@ -417,7 +416,7 @@ function makeFromTransport( request, endpoint: routeInput.endpoint, auth: routeInput.auth ?? Auth.none, - encodeBody, + encodeBody: ProviderShared.encodeJson, middleware: options?.http, webSocket: options?.webSocket, }), @@ -576,9 +575,7 @@ const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest, options const resolved = prepareRequest(request) const route = resolved.model.route - const body = yield* route.body - .from(resolved) - .pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema)))) + const body = yield* route.body.from(resolved) const prepared = yield* route.prepareTransport(body, resolved, options) return {