|
@@ -1,27 +1,22 @@
|
|
|
-import { Effect, Encoding, Schema, Stream } from "effect"
|
|
|
|
|
|
|
+import { Effect, Encoding, Schema } from "effect"
|
|
|
import { Headers } from "effect/unstable/http"
|
|
import { Headers } from "effect/unstable/http"
|
|
|
import { Route } from "../route/client"
|
|
import { Route } from "../route/client"
|
|
|
import { Auth } from "../route/auth"
|
|
import { Auth } from "../route/auth"
|
|
|
import { Endpoint } from "../route/endpoint"
|
|
import { Endpoint } from "../route/endpoint"
|
|
|
-import { Framing } from "../route/framing"
|
|
|
|
|
import { Protocol } from "../route/protocol"
|
|
import { Protocol } from "../route/protocol"
|
|
|
-import {
|
|
|
|
|
- HttpTransport,
|
|
|
|
|
- WebSocketTransport,
|
|
|
|
|
- type Transport,
|
|
|
|
|
- type WebSocketChannelDriver,
|
|
|
|
|
- type WebSocketChannelExchange,
|
|
|
|
|
-} from "../route/transport"
|
|
|
|
|
|
|
+import { HttpTransport } from "../route/transport"
|
|
|
import { LLMEvent, LLMRequest, type JsonSchema, type ToolDefinition } from "../schema"
|
|
import { LLMEvent, LLMRequest, type JsonSchema, type ToolDefinition } from "../schema"
|
|
|
import { OpenResponses } from "./open-responses"
|
|
import { OpenResponses } from "./open-responses"
|
|
|
import { optionalArray, ProviderShared } from "./shared"
|
|
import { optionalArray, ProviderShared } from "./shared"
|
|
|
import { Lifecycle } from "./utils/lifecycle"
|
|
import { Lifecycle } from "./utils/lifecycle"
|
|
|
import { OpenAIImage } from "./utils/openai-image"
|
|
import { OpenAIImage } from "./utils/openai-image"
|
|
|
import { ToolSchemaProjection } from "./utils/tool-schema"
|
|
import { ToolSchemaProjection } from "./utils/tool-schema"
|
|
|
-import { OpenAIResponsesChannel } from "./openai-responses-channel"
|
|
|
|
|
|
|
+import { OpenResponsesChannel } from "./open-responses-channel"
|
|
|
|
|
|
|
|
const ADAPTER = "openai-responses"
|
|
const ADAPTER = "openai-responses"
|
|
|
const NAME = "OpenAI Responses"
|
|
const NAME = "OpenAI Responses"
|
|
|
|
|
+const WEBSOCKET_PROTOCOL_HEADER = "responses_websockets=2026-02-06"
|
|
|
|
|
+const WEBSOCKET_ROTATE_AFTER_MS = 55 * 60 * 1000
|
|
|
export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
|
export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
|
|
export const PATH = OpenResponses.PATH
|
|
export const PATH = OpenResponses.PATH
|
|
|
|
|
|
|
@@ -66,16 +61,6 @@ const OpenAIResponsesBody = Schema.Struct({
|
|
|
})
|
|
})
|
|
|
export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
|
|
export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
|
|
|
|
|
|
|
|
-const OpenAIResponsesWebSocketMessage = Schema.StructWithRest(
|
|
|
|
|
- Schema.Struct({
|
|
|
|
|
- type: Schema.tag("response.create"),
|
|
|
|
|
- ...OpenAIResponsesCoreFields,
|
|
|
|
|
- }),
|
|
|
|
|
- [Schema.Record(Schema.String, Schema.Unknown)],
|
|
|
|
|
-)
|
|
|
|
|
-type OpenAIResponsesWebSocketMessage = Schema.Schema.Type<typeof OpenAIResponsesWebSocketMessage>
|
|
|
|
|
-const encodeWebSocketMessage = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesWebSocketMessage))
|
|
|
|
|
-
|
|
|
|
|
const extension = {
|
|
const extension = {
|
|
|
id: ADAPTER,
|
|
id: ADAPTER,
|
|
|
name: NAME,
|
|
name: NAME,
|
|
@@ -258,60 +243,12 @@ const endpoint = Endpoint.path<OpenAIResponsesBody>(PATH, { baseURL: DEFAULT_BAS
|
|
|
const auth = Auth.none
|
|
const auth = Auth.none
|
|
|
|
|
|
|
|
export const httpTransport = HttpTransport.sseJson.with<OpenAIResponsesBody>()
|
|
export const httpTransport = HttpTransport.sseJson.with<OpenAIResponsesBody>()
|
|
|
-
|
|
|
|
|
-const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage))
|
|
|
|
|
-
|
|
|
|
|
-const webSocketMessage = (body: OpenAIResponsesBody | Record<string, unknown>) =>
|
|
|
|
|
- Effect.gen(function* () {
|
|
|
|
|
- if (!ProviderShared.isRecord(body))
|
|
|
|
|
- return yield* ProviderShared.invalidRequest("OpenAI Responses WebSocket body must be a JSON object")
|
|
|
|
|
- const { stream: _stream, ...message } = body
|
|
|
|
|
- return yield* decodeWebSocketMessage({ ...message, type: "response.create" })
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
-export interface OpenAIResponsesPrepared {
|
|
|
|
|
- readonly http: HttpTransport.HttpPrepared<string>
|
|
|
|
|
- readonly channel?: {
|
|
|
|
|
- readonly url: string
|
|
|
|
|
- readonly headers: Headers.Headers
|
|
|
|
|
- readonly driver: WebSocketChannelDriver
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export const transport: Transport<OpenAIResponsesBody, OpenAIResponsesPrepared, string> = {
|
|
|
|
|
- id: httpTransport.id,
|
|
|
|
|
- prepare: (input) =>
|
|
|
|
|
- Effect.gen(function* () {
|
|
|
|
|
- const parts = yield* HttpTransport.jsonRequestParts(input)
|
|
|
|
|
- return {
|
|
|
|
|
- http: {
|
|
|
|
|
- request: ProviderShared.jsonPost({ url: parts.url, body: parts.bodyText, headers: parts.headers }),
|
|
|
|
|
- framing: Framing.sse,
|
|
|
|
|
- middleware: input.middleware,
|
|
|
|
|
- },
|
|
|
|
|
- channel: input.webSocket
|
|
|
|
|
- ? {
|
|
|
|
|
- url: yield* WebSocketTransport.toWebSocketUrl(parts.url),
|
|
|
|
|
- headers: parts.headers,
|
|
|
|
|
- driver: OpenAIResponsesChannel.make(encodeWebSocketMessage(yield* webSocketMessage(parts.jsonBody))),
|
|
|
|
|
- }
|
|
|
|
|
- : undefined,
|
|
|
|
|
- }
|
|
|
|
|
- }),
|
|
|
|
|
- execute: (prepared, request, runtime, options) => {
|
|
|
|
|
- if (!options?.webSocket || !prepared.channel) return httpTransport.execute(prepared.http, request, runtime)
|
|
|
|
|
- const exchange: WebSocketChannelExchange = {
|
|
|
|
|
- id: request.id ?? "request",
|
|
|
|
|
- connect: { url: prepared.channel.url, headers: prepared.channel.headers },
|
|
|
|
|
- fallback: () =>
|
|
|
|
|
- Stream.unwrap(
|
|
|
|
|
- httpTransport.execute(prepared.http, request, runtime).pipe(Effect.map((execution) => execution.frames)),
|
|
|
|
|
- ),
|
|
|
|
|
- driver: prepared.channel.driver,
|
|
|
|
|
- }
|
|
|
|
|
- return options.webSocket.execute(exchange)
|
|
|
|
|
- },
|
|
|
|
|
-}
|
|
|
|
|
|
|
+export const transport = OpenResponsesChannel.transport<OpenAIResponsesBody>({
|
|
|
|
|
+ id: ADAPTER,
|
|
|
|
|
+ name: NAME,
|
|
|
|
|
+ rotateAfterMs: WEBSOCKET_ROTATE_AFTER_MS,
|
|
|
|
|
+ headers: (headers) => Headers.set(headers, "openai-beta", headers["openai-beta"] ?? WEBSOCKET_PROTOCOL_HEADER),
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
export const route = Route.make({
|
|
export const route = Route.make({
|
|
|
id: ADAPTER,
|
|
id: ADAPTER,
|