import { Schema, type Effect } from "effect" import type { LLMError, LLMEvent, LLMRequest, ProtocolID } from "../schema" /** * The semantic API contract of one model server family. * * A `Protocol` owns the parts of a route that are intrinsic to "what does * this API look like": how a common `LLMRequest` becomes a provider-native * body, what schema that body must satisfy before it is JSON-encoded, and * how the streaming response decodes back into common `LLMEvent`s. * * Examples: * * - `OpenAIChat.protocol` — chat completions style * - `OpenAIResponses.protocol` — responses API * - `AnthropicMessages.protocol` — messages API with content blocks * - `Gemini.protocol` — generateContent * - `BedrockConverse.protocol` — Converse with binary event-stream framing * * A `Protocol` is **not** a deployment. It does not know which URL, which * headers, or which auth scheme to use. Those are deployment concerns owned * by `Route.make(...)` along with the chosen `Endpoint`, `Auth`, * and `Framing`. This separation is what lets DeepSeek, TogetherAI, Cerebras, * etc. all reuse `OpenAIChat.protocol` without forking 300 lines per provider. * * The four type parameters reflect the pipeline: * * - `Body` — provider-native request body candidate. `Route.make(...)` * validates and JSON-encodes it with `body.schema`. * - `Frame` — one unit of the framed response stream. SSE: a JSON data * string. AWS event stream: a parsed binary frame. * - `Event` — schema-decoded provider event produced from one frame. * - `State` — accumulator threaded through `stream.step` to translate event * sequences into `LLMEvent` sequences. */ export interface Protocol { /** Stable id for the wire protocol implementation. */ readonly id: ProtocolID /** Request side: schema for the provider-native body and how to build it. */ readonly body: ProtocolBody /** Response side: streaming state machine. */ readonly stream: ProtocolStream } export interface ProtocolBody { /** Schema for the validated provider-native body sent as the JSON request. */ readonly schema: Schema.Codec /** Build the provider-native body from a common `LLMRequest`. */ readonly from: (request: LLMRequest) => Effect.Effect } export interface ProtocolStream { /** Schema for one decoded streaming event, decoded from a transport frame. */ readonly event: Schema.Codec /** Initial parser state. Called once per response. */ readonly initial: () => State /** Translate one event into emitted `LLMEvent`s plus the next state. */ readonly step: (state: State, event: Event) => Effect.Effect], LLMError> /** Optional request-completion signal for transports that do not end naturally. */ readonly terminal?: (event: Event) => boolean /** Optional flush emitted when the framed stream ends. */ readonly onHalt?: (state: State) => ReadonlyArray } /** * Construct a `Protocol` from its body and stream pieces: * * - `body.schema` infers the provider-native request body shape. * - `body.from` ties the common `LLMRequest` to the provider body. * - `stream.event` infers the decoded streaming event and the wire frame. * - `stream.initial`, `stream.step`, and `stream.onHalt` infer the parser state. * * Provider implementations should usually call `Protocol.make({ ... })` * without explicit type arguments; the schemas and parser functions are the * source of truth. The constructor remains as the public seam for future * cross-cutting concerns such as tracing or instrumentation. */ export const make = ( input: Protocol, ): Protocol => input export const jsonEvent = (schema: S) => Schema.fromJsonString(schema) export * as Protocol from "./protocol"