Refactor LLM route-first provider API (#28523)
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user