Refactor LLM route-first provider API (#28523)

This commit is contained in:
Kit Langton
2026-05-20 20:15:52 -04:00
committed by GitHub
parent 5381795844
commit 41f6daf96a
87 changed files with 2450 additions and 1520 deletions
+45 -39
View File
@@ -2,11 +2,11 @@ import { Effect, Schema } from "effect"
import { Route } from "../route/client"
import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import { HttpTransport, WebSocketTransport } from "../route/transport"
import { Protocol } from "../route/protocol"
import {
LLMEvent,
type MediaPart,
Usage,
type FinishReason,
type LLMRequest,
@@ -31,6 +31,12 @@ const OpenAIResponsesInputText = Schema.Struct({
type: Schema.tag("input_text"),
text: Schema.String,
})
const OpenAIResponsesInputImage = Schema.Struct({
type: Schema.tag("input_image"),
image_url: Schema.String,
})
const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage])
type OpenAIResponsesInputContent = Schema.Schema.Type<typeof OpenAIResponsesInputContent>
const OpenAIResponsesOutputText = Schema.Struct({
type: Schema.tag("output_text"),
@@ -39,7 +45,7 @@ const OpenAIResponsesOutputText = Schema.Struct({
const OpenAIResponsesInputItem = Schema.Union([
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputText) }),
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }),
Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }),
Schema.Struct({
type: Schema.tag("function_call"),
@@ -151,12 +157,15 @@ const OpenAIResponsesEvent = Schema.Struct({
item_id: Schema.optional(Schema.String),
item: Schema.optional(OpenAIResponsesStreamItem),
response: Schema.optional(
Schema.Struct({
id: Schema.optional(Schema.String),
service_tier: Schema.optional(Schema.String),
incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })),
usage: optionalNull(OpenAIResponsesUsage),
}),
Schema.StructWithRest(
Schema.Struct({
id: Schema.optional(Schema.String),
service_tier: optionalNull(Schema.String),
incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })),
usage: optionalNull(OpenAIResponsesUsage),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
),
),
code: Schema.optional(Schema.String),
message: Schema.optional(Schema.String),
@@ -196,6 +205,22 @@ const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({
arguments: ProviderShared.encodeJson(part.input),
})
const imageUrl = (part: MediaPart) =>
typeof part.data === "string" && part.data.startsWith("data:")
? part.data
: `data:${part.mediaType};base64,${ProviderShared.mediaBytes(part)}`
const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* (
part: LLMRequest["messages"][number]["content"][number],
) {
if (part.type === "text") return { type: "input_text" as const, text: part.text }
if (part.type === "media" && part.mediaType.startsWith("image/")) {
return { type: "input_image" as const, image_url: imageUrl(part) }
}
if (part.type === "media") return yield* invalid("OpenAI Responses user media content only supports images")
return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"])
})
const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) {
const system: OpenAIResponsesInputItem[] =
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
@@ -203,13 +228,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
for (const message of request.messages) {
if (message.role === "user") {
const content: TextPart[] = []
for (const part of message.content) {
if (!ProviderShared.supportsContent(part, ["text"]))
return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text"])
content.push(part)
}
input.push({ role: "user", content: content.map((part) => ({ type: "input_text", text: part.text })) })
input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) })
continue
}
@@ -536,27 +555,18 @@ export const protocol = Protocol.make({
},
})
const encodeBody = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesBody))
const transportBase = {
endpoint: Endpoint.path<OpenAIResponsesBody>(PATH),
auth: Auth.bearer(),
encodeBody,
}
const routeDefaults = {
baseURL: DEFAULT_BASE_URL,
}
const endpoint = Endpoint.path<OpenAIResponsesBody>(PATH, { baseURL: DEFAULT_BASE_URL })
const auth = Auth.none
export const httpTransport = HttpTransport.httpJson({
...transportBase,
framing: Framing.sse,
})
export const httpTransport = HttpTransport.sseJson.with<OpenAIResponsesBody>()
export const route = Route.make({
id: ADAPTER,
provider: "openai",
protocol,
endpoint,
auth,
transport: httpTransport,
defaults: routeDefaults,
})
const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage))
@@ -569,8 +579,10 @@ const webSocketMessage = (body: OpenAIResponsesBody | Record<string, unknown>) =
return yield* decodeWebSocketMessage({ ...message, type: "response.create" })
})
export const webSocketTransport = WebSocketTransport.json({
...transportBase,
export const webSocketTransport = WebSocketTransport.jsonTransport.with<
OpenAIResponsesBody,
OpenAIResponsesWebSocketMessage
>({
toMessage: webSocketMessage,
encodeMessage: encodeWebSocketMessage,
})
@@ -579,15 +591,9 @@ export const webSocketRoute = Route.make({
id: `${ADAPTER}-websocket`,
provider: "openai",
protocol,
endpoint,
auth,
transport: webSocketTransport,
defaults: routeDefaults,
})
// =============================================================================
// Model Helper
// =============================================================================
export const model = route.model
export const webSocketModel = webSocketRoute.model
export * as OpenAIResponses from "./openai-responses"