refactor(llm): collapse Endpoint to path-only; require ModelRef.baseURL
Push URL host knowledge from `Endpoint` (route layer) up to `model.baseURL`
(provider helper layer). The route just composes a path onto whatever
host the model already carries.
Endpoint:
- `Endpoint<Body>` shrinks to `{ path }`. No more `default`, `baseURL`,
or `required` fallback fields.
- `Endpoint.path(value)` replaces the old `Endpoint.baseURL({...})` factory.
- `Endpoint.render()` is now sync — `${model.baseURL}${path}` plus query
params. No fallback chain, no Effect wrapper.
ModelRef:
- `ModelRef.baseURL: Schema.String` (was optional). Every materialized
model carries a host.
- `RouteModelInput.baseURL?: string` stays optional so route defaults
can supply a canonical URL; routes without a default tighten it.
Provider helpers:
- Each protocol exports a `DEFAULT_BASE_URL` constant and bakes it into
`route.defaults.baseURL`. Provider helpers don't need to set baseURL.
- Azure uses a new `AtLeastOne<T>` helper from `auth-options.ts` to
require either `resourceName` or `baseURL` at the type level.
- Bedrock provider computes baseURL from `region` at construction time.
- OpenAI-compatible profiles now have required (not optional) `baseURL`
in their type — all 9 already supplied one.
- The `defaultBaseURL: string | false` knob on protocol endpoint
factories is gone.
Effects:
- Forgetting baseURL is now caught at compile time (TS) or model
construction time (modelWithDefaults runtime check), not request time.
- `Endpoint.render` no longer needs Effect wrapping in the transport
hot path.
This commit is contained in:
@@ -157,17 +157,14 @@ const FakeProtocol = Protocol.make<FakeBody, string, string, void>({
|
||||
const FakeAdapter = Route.make({
|
||||
id: "fake-echo",
|
||||
protocol: FakeProtocol,
|
||||
endpoint: Endpoint.baseURL({
|
||||
default: "https://fake.local",
|
||||
path: "/v1/echo",
|
||||
}),
|
||||
endpoint: Endpoint.path("/v1/echo"),
|
||||
auth: Auth.passthrough,
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
// A provider module exports a Provider definition. The default `model` helper
|
||||
// sets provider identity, protocol id, and the route id resolved by the registry.
|
||||
const fakeEchoModel = Route.model(FakeAdapter, { provider: "fake-echo" })
|
||||
const fakeEchoModel = Route.model(FakeAdapter, { provider: "fake-echo", baseURL: "https://fake.local" })
|
||||
const FakeEcho = Provider.make({
|
||||
id: ProviderID.make("fake-echo"),
|
||||
model: (id: string, options: ProviderModelOptions = {}) => fakeEchoModel({ id, ...options }),
|
||||
|
||||
@@ -20,6 +20,8 @@ import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./share
|
||||
import { ToolStream } from "./utils/tool-stream"
|
||||
|
||||
const ADAPTER = "anthropic-messages"
|
||||
export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
|
||||
export const PATH = "/messages"
|
||||
|
||||
// =============================================================================
|
||||
// Request Body Schema
|
||||
@@ -574,7 +576,7 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
protocol,
|
||||
endpoint: Endpoint.baseURL({ default: "https://api.anthropic.com/v1", path: "/messages" }),
|
||||
endpoint: Endpoint.path(PATH),
|
||||
auth: Auth.apiKeyHeader("x-api-key"),
|
||||
framing: Framing.sse,
|
||||
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
||||
@@ -585,6 +587,7 @@ export const route = Route.make({
|
||||
// =============================================================================
|
||||
export const model = Route.model(route, {
|
||||
provider: "anthropic",
|
||||
baseURL: DEFAULT_BASE_URL,
|
||||
capabilities: capabilities({
|
||||
output: { reasoning: true },
|
||||
tools: { calls: true, streamingInput: true },
|
||||
|
||||
@@ -498,13 +498,11 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
protocol,
|
||||
endpoint: Endpoint.baseURL<BedrockConverseBody>({
|
||||
// Bedrock's URL embeds the region in the host and the validated modelId
|
||||
// in the path. We reach into the validated body so the URL
|
||||
// matches the body that gets signed.
|
||||
default: ({ request }) => `https://bedrock-runtime.${BedrockAuth.region(request)}.amazonaws.com`,
|
||||
path: ({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`,
|
||||
}),
|
||||
// Bedrock's URL embeds the region in the host (set on `model.baseURL` by
|
||||
// the provider helper from credentials) and the validated modelId in the
|
||||
// path. We read the validated body so the URL matches the body that gets
|
||||
// signed.
|
||||
endpoint: Endpoint.path<BedrockConverseBody>(({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`),
|
||||
auth: BedrockAuth.auth,
|
||||
framing,
|
||||
})
|
||||
@@ -529,8 +527,10 @@ const bedrockModel = Route.model<BedrockConverseModelInput>(
|
||||
{
|
||||
mapInput: (input) => {
|
||||
const { credentials, ...rest } = input
|
||||
const region = credentials?.region ?? "us-east-1"
|
||||
return {
|
||||
...rest,
|
||||
baseURL: rest.baseURL ?? `https://bedrock-runtime.${region}.amazonaws.com`,
|
||||
native: nativeCredentials(input.native, credentials),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ import { JsonObject, optionalArray, ProviderShared } from "./shared"
|
||||
import { GeminiToolSchema } from "./utils/gemini-tool-schema"
|
||||
|
||||
const ADAPTER = "gemini"
|
||||
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
||||
|
||||
// =============================================================================
|
||||
// Request Body Schema
|
||||
@@ -380,11 +381,8 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
protocol,
|
||||
endpoint: Endpoint.baseURL({
|
||||
default: "https://generativelanguage.googleapis.com/v1beta",
|
||||
// Gemini's path embeds the model id and pins SSE framing at the URL level.
|
||||
path: ({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`,
|
||||
}),
|
||||
// Gemini's path embeds the model id and pins SSE framing at the URL level.
|
||||
endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`),
|
||||
auth: Auth.apiKeyHeader("x-goog-api-key"),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
@@ -394,6 +392,7 @@ export const route = Route.make({
|
||||
// =============================================================================
|
||||
export const model = Route.model(route, {
|
||||
provider: "google",
|
||||
baseURL: DEFAULT_BASE_URL,
|
||||
capabilities: capabilities({
|
||||
input: { image: true, audio: true, video: true, pdf: true },
|
||||
output: { reasoning: true },
|
||||
|
||||
@@ -20,8 +20,8 @@ import { OpenAIOptions } from "./utils/openai-options"
|
||||
import { ToolStream } from "./utils/tool-stream"
|
||||
|
||||
const ADAPTER = "openai-chat"
|
||||
const DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
||||
const PATH = "/chat/completions"
|
||||
export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
||||
export const PATH = "/chat/completions"
|
||||
|
||||
// =============================================================================
|
||||
// Request Body Schema
|
||||
@@ -378,22 +378,10 @@ export const protocol = Protocol.make({
|
||||
},
|
||||
})
|
||||
|
||||
export const endpoint = (
|
||||
input: {
|
||||
readonly defaultBaseURL?: string | false
|
||||
readonly required?: string
|
||||
} = {},
|
||||
) =>
|
||||
Endpoint.baseURL<OpenAIChatBody>({
|
||||
default: input.defaultBaseURL === false ? undefined : (input.defaultBaseURL ?? DEFAULT_BASE_URL),
|
||||
path: PATH,
|
||||
required: input.required,
|
||||
})
|
||||
|
||||
const encodeBody = Schema.encodeSync(Schema.fromJsonString(OpenAIChatBody))
|
||||
|
||||
export const httpTransport = HttpTransport.httpJson({
|
||||
endpoint: endpoint(),
|
||||
endpoint: Endpoint.path(PATH),
|
||||
auth: Auth.bearer(),
|
||||
framing: Framing.sse,
|
||||
encodeBody,
|
||||
@@ -405,6 +393,7 @@ export const route = Route.make({
|
||||
protocol,
|
||||
transport: httpTransport,
|
||||
defaults: {
|
||||
baseURL: DEFAULT_BASE_URL,
|
||||
capabilities: capabilities({ tools: { calls: true, streamingInput: true } }),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -13,19 +13,14 @@ export type OpenAICompatibleChatModelInput = Omit<RouteRoutedModelInput, "baseUR
|
||||
/**
|
||||
* Route for non-OpenAI providers that expose an OpenAI Chat-compatible
|
||||
* `/chat/completions` endpoint. Reuses `OpenAIChat.protocol` end-to-end and
|
||||
* only overrides:
|
||||
*
|
||||
* - the route id (`openai-compatible-chat`) so providers can be resolved
|
||||
* per-family without colliding with native OpenAI;
|
||||
* - the endpoint, which requires `model.baseURL` (no provider default).
|
||||
* overrides only the route id so providers can be resolved per-family without
|
||||
* colliding with native OpenAI. The model carries the host on `baseURL`,
|
||||
* supplied by whichever profile/provider helper builds it.
|
||||
*/
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.baseURL({
|
||||
path: "/chat/completions",
|
||||
required: "OpenAI-compatible Chat requires a baseURL",
|
||||
}),
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import { OpenAIOptions } from "./utils/openai-options"
|
||||
import { ToolStream } from "./utils/tool-stream"
|
||||
|
||||
const ADAPTER = "openai-responses"
|
||||
const DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
||||
const PATH = "/responses"
|
||||
export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
|
||||
export const PATH = "/responses"
|
||||
|
||||
// =============================================================================
|
||||
// Request Body Schema
|
||||
@@ -501,25 +501,14 @@ export const protocol = Protocol.make({
|
||||
},
|
||||
})
|
||||
|
||||
export const endpoint = (
|
||||
input: {
|
||||
readonly defaultBaseURL?: string | false
|
||||
readonly required?: string
|
||||
} = {},
|
||||
) =>
|
||||
Endpoint.baseURL<OpenAIResponsesBody>({
|
||||
default: input.defaultBaseURL === false ? undefined : (input.defaultBaseURL ?? DEFAULT_BASE_URL),
|
||||
path: PATH,
|
||||
required: input.required,
|
||||
})
|
||||
|
||||
const encodeBody = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesBody))
|
||||
const transportBase = {
|
||||
endpoint: endpoint(),
|
||||
endpoint: Endpoint.path<OpenAIResponsesBody>(PATH),
|
||||
auth: Auth.bearer(),
|
||||
encodeBody,
|
||||
}
|
||||
const routeDefaults = {
|
||||
baseURL: DEFAULT_BASE_URL,
|
||||
capabilities: capabilities({ tools: { calls: true, streamingInput: true } }),
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,21 @@ import type { BedrockCredentials } from "../protocols/bedrock-converse"
|
||||
|
||||
export const id = ProviderID.make("amazon-bedrock")
|
||||
|
||||
export type ModelOptions = Omit<RouteModelInput, "id"> & {
|
||||
export type ModelOptions = Omit<RouteModelInput, "id" | "baseURL"> & {
|
||||
readonly apiKey?: string
|
||||
readonly headers?: Record<string, string>
|
||||
readonly credentials?: BedrockCredentials
|
||||
/** AWS region. Defaults to `us-east-1` when neither this nor `credentials.region` is set. */
|
||||
readonly region?: string
|
||||
/** Override the computed `https://bedrock-runtime.<region>.amazonaws.com` URL. */
|
||||
readonly baseURL?: string
|
||||
}
|
||||
type ModelInput = ModelOptions & Pick<RouteModelInput, "id">
|
||||
|
||||
export const routes = [BedrockConverse.route]
|
||||
|
||||
const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com`
|
||||
|
||||
const converseModel = Route.model<ModelInput>(
|
||||
BedrockConverse.route,
|
||||
{
|
||||
@@ -23,9 +29,11 @@ const converseModel = Route.model<ModelInput>(
|
||||
},
|
||||
{
|
||||
mapInput: (input) => {
|
||||
const { credentials, ...rest } = input
|
||||
const { credentials, region, baseURL, ...rest } = input
|
||||
const resolvedRegion = region ?? credentials?.region ?? "us-east-1"
|
||||
return {
|
||||
...rest,
|
||||
baseURL: baseURL ?? bedrockBaseURL(resolvedRegion),
|
||||
native: BedrockConverse.nativeCredentials(input.native, credentials),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ export const id = ProviderID.make("anthropic")
|
||||
|
||||
export const routes = [AnthropicMessages.route]
|
||||
|
||||
export const model = (id: string | ModelID, options: Omit<RouteModelInput, "id"> = {}) =>
|
||||
export const model = (id: string | ModelID, options: Omit<RouteModelInput, "id" | "baseURL"> & { readonly baseURL?: string } = {}) =>
|
||||
AnthropicMessages.model({ ...options, id })
|
||||
|
||||
export const provider = Provider.make({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Auth } from "../route/auth"
|
||||
import type { ProviderAuthOption } from "../route/auth-options"
|
||||
import { type AtLeastOne, type ProviderAuthOption } from "../route/auth-options"
|
||||
import { Route } from "../route/client"
|
||||
import type { ModelInput } from "../llm"
|
||||
import { Provider } from "../provider"
|
||||
@@ -9,40 +9,33 @@ import * as OpenAIResponses from "../protocols/openai-responses"
|
||||
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
||||
|
||||
export const id = ProviderID.make("azure")
|
||||
const MISSING_BASE_URL = "Azure OpenAI requires resourceName or baseURL"
|
||||
const routeAuth = Auth.remove("authorization").andThen(Auth.apiKeyHeader("api-key"))
|
||||
|
||||
export type ModelOptions = Omit<ModelInput, "id" | "provider" | "route" | "apiKey" | "auth"> &
|
||||
// Azure needs the customer's resource URL; supply either `resourceName`
|
||||
// (helper builds the URL) or `baseURL` directly.
|
||||
type AzureURL = AtLeastOne<{ readonly resourceName: string; readonly baseURL: string }>
|
||||
|
||||
export type ModelOptions = AzureURL &
|
||||
Omit<ModelInput, "id" | "provider" | "route" | "apiKey" | "auth" | "baseURL"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly resourceName?: string
|
||||
readonly apiVersion?: string
|
||||
readonly useCompletionUrls?: boolean
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
type AzureModelInput = ModelOptions & Pick<ModelInput, "id">
|
||||
|
||||
const resourceBaseURL = (resourceName: string | undefined) => {
|
||||
const resource = resourceName?.trim()
|
||||
if (!resource) return undefined
|
||||
return `https://${resource}.openai.azure.com/openai/v1`
|
||||
}
|
||||
const resourceBaseURL = (resourceName: string) => `https://${resourceName.trim()}.openai.azure.com/openai/v1`
|
||||
|
||||
const responsesRoute = OpenAIResponses.route.with({
|
||||
id: "azure-openai-responses",
|
||||
provider: id,
|
||||
transport: OpenAIResponses.httpTransport.with({
|
||||
auth: routeAuth,
|
||||
endpoint: OpenAIResponses.endpoint({ defaultBaseURL: false, required: MISSING_BASE_URL }),
|
||||
}),
|
||||
transport: OpenAIResponses.httpTransport.with({ auth: routeAuth }),
|
||||
})
|
||||
|
||||
const chatRoute = OpenAIChat.route.with({
|
||||
id: "azure-openai-chat",
|
||||
provider: id,
|
||||
transport: OpenAIChat.httpTransport.with({
|
||||
auth: routeAuth,
|
||||
endpoint: OpenAIChat.endpoint({ defaultBaseURL: false, required: MISSING_BASE_URL }),
|
||||
}),
|
||||
transport: OpenAIChat.httpTransport.with({ auth: routeAuth }),
|
||||
})
|
||||
|
||||
export const routes = [responsesRoute, chatRoute]
|
||||
@@ -59,7 +52,8 @@ const mapInput = (input: AzureModelInput) => {
|
||||
.orElse(Auth.config("AZURE_OPENAI_API_KEY"))
|
||||
.pipe(Auth.header("api-key")),
|
||||
),
|
||||
baseURL: rest.baseURL ?? resourceBaseURL(resourceName),
|
||||
// AtLeastOne guarantees at least one is set; baseURL wins if both are.
|
||||
baseURL: rest.baseURL ?? resourceBaseURL(resourceName!),
|
||||
queryParams: {
|
||||
...rest.queryParams,
|
||||
"api-version": apiVersion ?? rest.queryParams?.["api-version"] ?? "v1",
|
||||
@@ -70,12 +64,12 @@ const mapInput = (input: AzureModelInput) => {
|
||||
const chatModel = Route.model<AzureModelInput>(chatRoute, {}, { mapInput })
|
||||
const responsesModel = Route.model<AzureModelInput>(responsesRoute, {}, { mapInput })
|
||||
|
||||
export const responses = (modelID: string | ModelID, options: ModelOptions = {}) =>
|
||||
export const responses = (modelID: string | ModelID, options: ModelOptions) =>
|
||||
responsesModel({ ...options, id: modelID })
|
||||
|
||||
export const chat = (modelID: string | ModelID, options: ModelOptions = {}) => chatModel({ ...options, id: modelID })
|
||||
export const chat = (modelID: string | ModelID, options: ModelOptions) => chatModel({ ...options, id: modelID })
|
||||
|
||||
export const model = (modelID: string | ModelID, options: ModelOptions = {}) => {
|
||||
export const model = (modelID: string | ModelID, options: ModelOptions) => {
|
||||
if (options.useCompletionUrls === true) return chat(modelID, options)
|
||||
return responses(modelID, options)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-opt
|
||||
|
||||
export const id = ProviderID.make("github-copilot")
|
||||
|
||||
// GitHub Copilot has no canonical public URL — callers (opencode, etc.) must
|
||||
// supply `baseURL` explicitly.
|
||||
export type ModelOptions = Omit<ModelInput, "id" | "provider" | "route"> & {
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
@@ -27,12 +29,12 @@ const mapInput = (input: CopilotModelInput) => withOpenAIOptions(input.id, input
|
||||
const chatModel = Route.model<CopilotModelInput>(OpenAIChat.route, { provider: id }, { mapInput })
|
||||
const responsesModel = Route.model<CopilotModelInput>(OpenAIResponses.route, { provider: id }, { mapInput })
|
||||
|
||||
export const responses = (modelID: string | ModelID, options: ModelOptions = {}) =>
|
||||
export const responses = (modelID: string | ModelID, options: ModelOptions) =>
|
||||
responsesModel({ ...options, id: modelID })
|
||||
|
||||
export const chat = (modelID: string | ModelID, options: ModelOptions = {}) => chatModel({ ...options, id: modelID })
|
||||
export const chat = (modelID: string | ModelID, options: ModelOptions) => chatModel({ ...options, id: modelID })
|
||||
|
||||
export const model = (modelID: string | ModelID, options: ModelOptions = {}) => {
|
||||
export const model = (modelID: string | ModelID, options: ModelOptions) => {
|
||||
const create = shouldUseResponsesApi(modelID) ? responsesModel : chatModel
|
||||
return create({ ...options, id: modelID })
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export const id = ProviderID.make("google")
|
||||
|
||||
export const routes = [Gemini.route]
|
||||
|
||||
export const model = (id: string | ModelID, options: Omit<RouteModelInput, "id"> = {}) =>
|
||||
export const model = (id: string | ModelID, options: Omit<RouteModelInput, "id" | "baseURL"> & { readonly baseURL?: string } = {}) =>
|
||||
Gemini.model({ ...options, id })
|
||||
|
||||
export const provider = Provider.make({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { CapabilitiesInput } from "../llm"
|
||||
|
||||
export interface OpenAICompatibleProfile {
|
||||
readonly provider: string
|
||||
readonly baseURL?: string
|
||||
readonly baseURL: string
|
||||
readonly capabilities?: CapabilitiesInput
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,6 @@ export const model = (id: string | ModelID, options: ModelOptions) => {
|
||||
})
|
||||
}
|
||||
|
||||
const profileBaseURL = (profile: OpenAICompatibleProfile, options: FamilyModelOptions) => {
|
||||
const baseURL = options.baseURL ?? profile.baseURL
|
||||
if (baseURL) return baseURL
|
||||
throw new Error(`OpenAI-compatible profile ${profile.provider} requires a baseURL`)
|
||||
}
|
||||
|
||||
export const profileModel = (
|
||||
profile: OpenAICompatibleProfile,
|
||||
id: string | ModelID,
|
||||
@@ -43,7 +37,7 @@ export const profileModel = (
|
||||
...options,
|
||||
id,
|
||||
provider: profile.provider,
|
||||
baseURL: profileBaseURL(profile, options),
|
||||
baseURL: options.baseURL ?? profile.baseURL,
|
||||
capabilities: options.capabilities ?? profile.capabilities,
|
||||
})
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ export const routes = [OpenAIResponses.route, OpenAIResponses.webSocketRoute, Op
|
||||
// This provider facade wraps the lower-level Responses and Chat model factories
|
||||
// with OpenAI-specific conveniences: typed options, API-key sugar, env fallback,
|
||||
// and default option normalization.
|
||||
type OpenAIModelInput<ModelInput> = Omit<ModelInput, "apiKey" | "auth"> &
|
||||
type OpenAIModelInput<ModelInput> = Omit<ModelInput, "apiKey" | "auth" | "baseURL"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ export type OpenRouterProviderOptionsInput = ProviderOptions & {
|
||||
readonly openrouter?: OpenRouterOptions
|
||||
}
|
||||
|
||||
export type ModelOptions = Omit<RouteModelInput, "id" | "providerOptions"> & {
|
||||
export type ModelOptions = Omit<RouteModelInput, "id" | "baseURL" | "providerOptions"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenRouterProviderOptionsInput
|
||||
}
|
||||
type ModelInput = ModelOptions & Pick<RouteModelInput, "id">
|
||||
@@ -69,7 +70,7 @@ const bodyOptions = (input: unknown) => {
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
protocol,
|
||||
endpoint: Endpoint.baseURL({ default: profile.baseURL, path: "/chat/completions" }),
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ import * as OpenAIResponses from "../protocols/openai-responses"
|
||||
|
||||
export const id = ProviderID.make("xai")
|
||||
|
||||
export type ModelOptions = Omit<RouteModelInput, "id" | "apiKey" | "auth"> & ProviderAuthOption<"optional">
|
||||
export type ModelOptions = Omit<RouteModelInput, "id" | "apiKey" | "auth" | "baseURL"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
}
|
||||
|
||||
export const routes = [OpenAIResponses.route, OpenAICompatibleChat.route]
|
||||
|
||||
|
||||
@@ -30,6 +30,15 @@ export type ModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
|
||||
|
||||
export type ModelFactory<Base, Mode extends ApiKeyMode, Model> = (id: string, ...args: ModelArgs<Base, Mode>) => Model
|
||||
|
||||
/**
|
||||
* Require at least one of the keys in `T`. Use for option shapes where any
|
||||
* subset of fields is acceptable but at least one must be present (e.g. Azure
|
||||
* accepts `resourceName` or `baseURL`).
|
||||
*/
|
||||
export type AtLeastOne<T> = {
|
||||
[K in keyof T]: Required<Pick<T, K>> & Partial<Omit<T, K>>
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* Standard bearer-auth resolution for providers: honor an explicit `auth`
|
||||
* override, otherwise resolve `apiKey` (option > config var) and apply it as
|
||||
|
||||
@@ -94,11 +94,22 @@ export type ModelRefInput = Omit<
|
||||
readonly http?: HttpOptionsInput
|
||||
}
|
||||
|
||||
export type RouteModelInput = Omit<ModelRefInput, "provider" | "route">
|
||||
// `baseURL` is required on `ModelRefInput` (every materialized `ModelRef` has
|
||||
// a host) but optional at the route-input layers below. The route's `defaults`
|
||||
// can supply a canonical URL (e.g. OpenAI/Anthropic) so the user's input may
|
||||
// omit it. Routes without a canonical URL (OpenAI-compatible, GitHub Copilot)
|
||||
// re-tighten this in their own input type.
|
||||
export type RouteModelInput = Omit<ModelRefInput, "provider" | "route" | "baseURL"> & {
|
||||
readonly baseURL?: string
|
||||
}
|
||||
|
||||
export type RouteModelDefaults = Omit<ModelRefInput, "id" | "route">
|
||||
export type RouteModelDefaults = Omit<ModelRefInput, "id" | "route" | "baseURL"> & {
|
||||
readonly baseURL?: string
|
||||
}
|
||||
|
||||
export type RouteRoutedModelInput = Omit<ModelRefInput, "route">
|
||||
export type RouteRoutedModelInput = Omit<ModelRefInput, "route" | "baseURL"> & {
|
||||
readonly baseURL?: string
|
||||
}
|
||||
|
||||
export type RouteRoutedModelDefaults = Partial<Omit<ModelRefInput, "id" | "provider" | "route">>
|
||||
|
||||
@@ -133,6 +144,11 @@ const modelWithDefaults =
|
||||
const mapped = options.mapInput === undefined ? (input as RouteMappedModelInput) : options.mapInput(input)
|
||||
const provider = defaults.provider ?? route.provider ?? ("provider" in mapped ? mapped.provider : undefined)
|
||||
if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
|
||||
const baseURL = mapped.baseURL ?? defaults.baseURL ?? route.defaults.baseURL
|
||||
if (!baseURL)
|
||||
throw new Error(
|
||||
`Route.model(${route.id}) requires a baseURL — supply it via input, defaults, or route defaults`,
|
||||
)
|
||||
const generation = mergeGenerationOptions(route.defaults.generation, defaults.generation)
|
||||
const providerOptions = mergeProviderOptions(route.defaults.providerOptions, defaults.providerOptions)
|
||||
const http = mergeHttpOptions(httpOptions(route.defaults.http), httpOptions(defaults.http))
|
||||
@@ -140,6 +156,7 @@ const modelWithDefaults =
|
||||
...route.defaults,
|
||||
...defaults,
|
||||
...mapped,
|
||||
baseURL,
|
||||
provider,
|
||||
route: route.id,
|
||||
capabilities: mapped.capabilities ?? defaults.capabilities ?? route.defaults.capabilities,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import type { LLMRequest } from "../schema"
|
||||
import * as ProviderShared from "../protocols/shared"
|
||||
import type { LLMError, LLMRequest } from "../schema"
|
||||
|
||||
export interface EndpointInput<Body> {
|
||||
readonly request: LLMRequest
|
||||
@@ -12,48 +11,31 @@ export type EndpointPart<Body> = string | ((input: EndpointInput<Body>) => strin
|
||||
/**
|
||||
* Declarative URL construction for one route.
|
||||
*
|
||||
* `Endpoint` is the deployment-side answer to "where does this request go?".
|
||||
* `render(...)` interprets this data after protocol body construction, so
|
||||
* dynamic pieces can read the final `LLMRequest` and validated provider body.
|
||||
* `Endpoint` carries only the path. The host always lives on `model.baseURL`,
|
||||
* supplied by the provider helper that constructs the model. `render(...)`
|
||||
* just appends the path (and any `model.queryParams`) to that host.
|
||||
*
|
||||
* `path` may be a string or a function of `EndpointInput`, for routes whose
|
||||
* URL embeds the model id, region, or another body field (e.g. Bedrock,
|
||||
* Gemini).
|
||||
*/
|
||||
export interface Endpoint<Body> {
|
||||
readonly baseURL?: EndpointPart<Body>
|
||||
readonly path: EndpointPart<Body>
|
||||
/** Error message used when neither `model.baseURL` nor `baseURL` is set. */
|
||||
readonly required?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a URL from the model's `baseURL` (or a default) plus a path. Appends
|
||||
* `model.queryParams` so routes that need request-level query params
|
||||
* (Azure `api-version`, etc.) get them for free.
|
||||
*
|
||||
* Both `default` and `path` may be strings or functions of the
|
||||
* `EndpointInput`, for routes whose URL embeds the model id, region, or
|
||||
* another body field.
|
||||
*/
|
||||
export const baseURL = <Body>(input: {
|
||||
readonly default?: string | ((input: EndpointInput<Body>) => string)
|
||||
readonly path: string | ((input: EndpointInput<Body>) => string)
|
||||
readonly required?: string
|
||||
}): Endpoint<Body> => ({
|
||||
baseURL: input.default,
|
||||
path: input.path,
|
||||
required: input.required,
|
||||
})
|
||||
/** Construct an `Endpoint` from a path string or path function. */
|
||||
export const path = <Body>(value: EndpointPart<Body>): Endpoint<Body> => ({ path: value })
|
||||
|
||||
const renderPart = <Body>(part: EndpointPart<Body> | undefined, input: EndpointInput<Body>) =>
|
||||
const renderPart = <Body>(part: EndpointPart<Body>, input: EndpointInput<Body>) =>
|
||||
typeof part === "function" ? part(input) : part
|
||||
|
||||
export const render = <Body>(endpoint: Endpoint<Body>, input: EndpointInput<Body>) =>
|
||||
Effect.gen(function* () {
|
||||
const base = input.request.model.baseURL ?? renderPart(endpoint.baseURL, input)
|
||||
if (!base) return yield* ProviderShared.invalidRequest(endpoint.required ?? "Missing baseURL")
|
||||
const path = renderPart(endpoint.path, input)
|
||||
const url = new URL(`${ProviderShared.trimBaseUrl(base)}${path}`)
|
||||
const params = input.request.model.queryParams
|
||||
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value)
|
||||
return url
|
||||
})
|
||||
export const render = <Body>(endpoint: Endpoint<Body>, input: EndpointInput<Body>) => {
|
||||
const url = new URL(
|
||||
`${ProviderShared.trimBaseUrl(input.request.model.baseURL)}${renderPart(endpoint.path, input)}`,
|
||||
)
|
||||
const params = input.request.model.queryParams
|
||||
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value)
|
||||
return url
|
||||
}
|
||||
|
||||
export * as Endpoint from "./endpoint"
|
||||
|
||||
@@ -48,7 +48,7 @@ const bodyWithOverlay = <Body>(body: Body, request: LLMRequest, encodeBody: (bod
|
||||
export const jsonRequestParts = <Body>(input: JsonRequestInput<Body>) =>
|
||||
Effect.gen(function* () {
|
||||
const url = applyQuery(
|
||||
(yield* renderEndpoint(input.endpoint, { request: input.request, body: input.body })).toString(),
|
||||
renderEndpoint(input.endpoint, { request: input.request, body: input.body }).toString(),
|
||||
input.request.http?.query,
|
||||
)
|
||||
const body = yield* bodyWithOverlay(input.body, input.request, input.encodeBody)
|
||||
|
||||
@@ -194,7 +194,7 @@ export class ModelRef extends Schema.Class<ModelRef>("LLM.ModelRef")({
|
||||
id: ModelID,
|
||||
provider: ProviderID,
|
||||
route: RouteID,
|
||||
baseURL: Schema.optional(Schema.String),
|
||||
baseURL: Schema.String,
|
||||
/** Provider-specific API key convenience. Provider helpers normalize this into `auth`. */
|
||||
apiKey: Schema.optional(Schema.String),
|
||||
/** Optional transport auth policy. Opaque because it may contain functions. */
|
||||
|
||||
@@ -44,6 +44,7 @@ const request = LLM.request({
|
||||
id: "fake-model",
|
||||
provider: "fake-provider",
|
||||
route: "fake",
|
||||
baseURL: "https://fake.local",
|
||||
}),
|
||||
prompt: "hello",
|
||||
})
|
||||
@@ -78,14 +79,14 @@ const fakeProtocol = Protocol.make<FakeBody, FakeEvent, FakeEvent, void>({
|
||||
const fake = Route.make({
|
||||
id: "fake",
|
||||
protocol: fakeProtocol,
|
||||
endpoint: Endpoint.baseURL({ default: "https://fake.local", path: "/chat" }),
|
||||
endpoint: Endpoint.path("/chat"),
|
||||
framing: fakeFraming,
|
||||
})
|
||||
|
||||
const gemini = Route.make({
|
||||
id: "gemini-fake",
|
||||
protocol: fakeProtocol,
|
||||
endpoint: Endpoint.baseURL({ default: "https://fake.local", path: "/chat" }),
|
||||
endpoint: Endpoint.path("/chat"),
|
||||
framing: fakeFraming,
|
||||
})
|
||||
|
||||
@@ -129,7 +130,7 @@ describe("llm route", () => {
|
||||
Effect.gen(function* () {
|
||||
const mapped = Route.model<RouteModelInput & { readonly region?: string }>(
|
||||
fake,
|
||||
{ provider: "fake-provider" },
|
||||
{ provider: "fake-provider", baseURL: "https://fake.local" },
|
||||
{
|
||||
mapInput: (input) => {
|
||||
const { region, ...rest } = input
|
||||
@@ -154,7 +155,7 @@ describe("llm route", () => {
|
||||
from: () => Effect.succeed({ body: "late-default" }),
|
||||
},
|
||||
}),
|
||||
endpoint: Endpoint.baseURL({ default: "https://fake.local", path: "/chat" }),
|
||||
endpoint: Endpoint.path("/chat"),
|
||||
framing: fakeFraming,
|
||||
}),
|
||||
).toThrow('Duplicate LLM route id "fake"')
|
||||
|
||||
@@ -81,6 +81,7 @@ OpenAI.chat("gpt-4.1-mini", { auth: RuntimeAuth.bearer("oauth-token") })
|
||||
// @ts-expect-error auth is an override, so OpenAI Chat rejects apiKey with auth.
|
||||
OpenAI.chat("gpt-4.1-mini", { apiKey: "sk-test", auth: RuntimeAuth.bearer("oauth-token") })
|
||||
|
||||
// @ts-expect-error Azure requires at least one of `resourceName` or `baseURL`.
|
||||
Azure.responses("deployment")
|
||||
Azure.responses("deployment", { apiKey: "azure-key", resourceName: "resource" })
|
||||
Azure.responses("deployment", { apiKey: configApiKey, resourceName: "resource" })
|
||||
@@ -89,6 +90,7 @@ Azure.responses("deployment", { auth: RuntimeAuth.header("api-key", "azure-key")
|
||||
// @ts-expect-error auth is an override, so Azure rejects apiKey with auth.
|
||||
Azure.responses("deployment", { apiKey: "azure-key", auth: RuntimeAuth.header("api-key", "override") })
|
||||
|
||||
// @ts-expect-error Azure requires at least one of `resourceName` or `baseURL`.
|
||||
Azure.chat("deployment")
|
||||
Azure.chat("deployment", { apiKey: "azure-key", resourceName: "resource" })
|
||||
Azure.chat("deployment", { apiKey: configApiKey, resourceName: "resource" })
|
||||
|
||||
@@ -7,7 +7,7 @@ import { it } from "./lib/effect"
|
||||
|
||||
const request = LLM.request({
|
||||
id: "req_auth",
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "fake" }),
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "fake", baseURL: "https://fake.local" }),
|
||||
prompt: "hello",
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMError } from "../src"
|
||||
import { LLM } from "../src"
|
||||
import { Endpoint } from "../src/route"
|
||||
|
||||
const request = (
|
||||
input: {
|
||||
readonly baseURL?: string
|
||||
readonly baseURL: string
|
||||
readonly queryParams?: Record<string, string>
|
||||
} = {},
|
||||
},
|
||||
) =>
|
||||
LLM.request({
|
||||
model: LLM.model({
|
||||
@@ -21,59 +20,38 @@ const request = (
|
||||
})
|
||||
|
||||
describe("Endpoint", () => {
|
||||
test("renders static base URL and path", async () => {
|
||||
const url = await Effect.runPromise(
|
||||
Endpoint.render(Endpoint.baseURL({ default: "https://api.example.test/v1/", path: "/chat" }), {
|
||||
request: request(),
|
||||
body: {},
|
||||
}),
|
||||
)
|
||||
test("appends a static path to the model's baseURL", () => {
|
||||
const url = Endpoint.render(Endpoint.path("/chat"), {
|
||||
request: request({ baseURL: "https://api.example.test/v1/" }),
|
||||
body: {},
|
||||
})
|
||||
|
||||
expect(url.toString()).toBe("https://api.example.test/v1/chat")
|
||||
})
|
||||
|
||||
test("model baseURL overrides route default and query params are appended", async () => {
|
||||
const url = await Effect.runPromise(
|
||||
Endpoint.render(Endpoint.baseURL({ default: "https://api.example.test/v1", path: "/chat?alt=sse" }), {
|
||||
request: request({
|
||||
baseURL: "https://custom.example.test/root/",
|
||||
queryParams: { "api-version": "2026-01-01", alt: "json" },
|
||||
}),
|
||||
body: {},
|
||||
test("model query params are appended to the rendered URL", () => {
|
||||
const url = Endpoint.render(Endpoint.path("/chat?alt=sse"), {
|
||||
request: request({
|
||||
baseURL: "https://custom.example.test/root/",
|
||||
queryParams: { "api-version": "2026-01-01", alt: "json" },
|
||||
}),
|
||||
)
|
||||
body: {},
|
||||
})
|
||||
|
||||
expect(url.toString()).toBe("https://custom.example.test/root/chat?alt=json&api-version=2026-01-01")
|
||||
})
|
||||
|
||||
test("renders dynamic base URL and final payload path", async () => {
|
||||
const url = await Effect.runPromise(
|
||||
Endpoint.render(
|
||||
Endpoint.baseURL<{ readonly modelId: string }>({
|
||||
default: () => "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
path: ({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`,
|
||||
}),
|
||||
{
|
||||
request: request(),
|
||||
body: { modelId: "us.amazon.nova-micro-v1:0" },
|
||||
},
|
||||
),
|
||||
test("path may be a function of the validated body", () => {
|
||||
const url = Endpoint.render(
|
||||
Endpoint.path<{ readonly modelId: string }>(({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`),
|
||||
{
|
||||
request: request({ baseURL: "https://bedrock-runtime.us-east-1.amazonaws.com" }),
|
||||
body: { modelId: "us.amazon.nova-micro-v1:0" },
|
||||
},
|
||||
)
|
||||
|
||||
expect(url.toString()).toBe(
|
||||
"https://bedrock-runtime.us-east-1.amazonaws.com/model/us.amazon.nova-micro-v1%3A0/converse-stream",
|
||||
)
|
||||
})
|
||||
|
||||
test("fails when no model or route baseURL is available", async () => {
|
||||
const error = await Effect.runPromise(
|
||||
Endpoint.render(Endpoint.baseURL({ path: "/chat", required: "test endpoint requires a baseURL" }), {
|
||||
request: request(),
|
||||
body: {},
|
||||
}).pipe(Effect.flip),
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest", message: "test endpoint requires a baseURL" })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ describe("llm constructors", () => {
|
||||
test("builds canonical schema classes from ergonomic input", () => {
|
||||
const request = LLM.request({
|
||||
id: "req_1",
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat" }),
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }),
|
||||
system: "You are concise.",
|
||||
prompt: "Say hello.",
|
||||
})
|
||||
@@ -23,7 +23,7 @@ describe("llm constructors", () => {
|
||||
test("updates requests without spreading schema class instances", () => {
|
||||
const base = LLM.request({
|
||||
id: "req_1",
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat" }),
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }),
|
||||
prompt: "Say hello.",
|
||||
})
|
||||
const updated = LLM.updateRequest(base, {
|
||||
@@ -44,6 +44,7 @@ describe("llm constructors", () => {
|
||||
id: "fake-model",
|
||||
provider: "fake",
|
||||
route: "openai-chat",
|
||||
baseURL: "https://fake.local",
|
||||
generation: { maxTokens: 100, temperature: 1 },
|
||||
providerOptions: { openai: { store: false, metadata: { model: true } } },
|
||||
http: { body: { metadata: { model: true } }, headers: { "x-shared": "model" }, query: { model: "1" } },
|
||||
@@ -66,7 +67,7 @@ describe("llm constructors", () => {
|
||||
test("updates canonical requests from the request datatype", () => {
|
||||
const base = LLM.request({
|
||||
id: "req_1",
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat" }),
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }),
|
||||
prompt: "Say hello.",
|
||||
})
|
||||
const updated = LLMRequest.update(base, { messages: [...base.messages, LLM.assistant("Hi.")] })
|
||||
@@ -79,7 +80,7 @@ describe("llm constructors", () => {
|
||||
})
|
||||
|
||||
test("updates canonical models from the model datatype", () => {
|
||||
const base = LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat" })
|
||||
const base = LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" })
|
||||
const updated = ModelRef.update(base, { route: "openai-responses" })
|
||||
|
||||
expect(updated).toBeInstanceOf(ModelRef)
|
||||
@@ -104,7 +105,7 @@ describe("llm constructors", () => {
|
||||
expect(LLM.toolChoice("required")).toEqual(new ToolChoice({ type: "required" }))
|
||||
expect(
|
||||
LLM.request({
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat" }),
|
||||
model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }),
|
||||
prompt: "Use tools if needed.",
|
||||
toolChoice: "required",
|
||||
}).toolChoice,
|
||||
|
||||
@@ -23,6 +23,7 @@ const model = new ModelRef({
|
||||
id: ModelID.make("fake-model"),
|
||||
provider: ProviderID.make("fake-provider"),
|
||||
route: "openai-chat",
|
||||
baseURL: "https://fake.local",
|
||||
capabilities,
|
||||
limits: new ModelLimits({}),
|
||||
})
|
||||
|
||||
@@ -179,17 +179,22 @@ const PROVIDERS: Record<string, ProviderModel> = {
|
||||
Anthropic.model(String(input.model.api.id), sharedOptions(input, options, { protocol: "anthropic-messages" })),
|
||||
"@ai-sdk/azure": (input, options) => {
|
||||
const create = options.useCompletionUrls === true ? Azure.chat : Azure.responses
|
||||
// Azure requires at least one of `resourceName` or `baseURL`. The user's
|
||||
// config supplies one of them via opencode's provider settings; if neither
|
||||
// is set we let Azure's runtime check surface a clear error.
|
||||
return create(String(input.model.api.id), {
|
||||
...sharedOptions(input, options, { protocol: azureProtocol(options), providerOptions: openAIOptions(options) }),
|
||||
resourceName: stringOption(options, "resourceName"),
|
||||
apiVersion: stringOption(options, "apiVersion"),
|
||||
})
|
||||
} as Azure.ModelOptions)
|
||||
},
|
||||
"@ai-sdk/baseten": openAICompatibleModel,
|
||||
"@ai-sdk/cerebras": openAICompatibleModel,
|
||||
"@ai-sdk/deepinfra": openAICompatibleModel,
|
||||
"@ai-sdk/fireworks": openAICompatibleModel,
|
||||
"@ai-sdk/github-copilot": (input, options) =>
|
||||
// GitHub Copilot has no canonical public URL; the user's opencode config
|
||||
// is expected to supply `baseURL`. Runtime check kicks in if it's missing.
|
||||
GitHubCopilot.model(
|
||||
String(input.model.api.id),
|
||||
{
|
||||
@@ -197,7 +202,7 @@ const PROVIDERS: Record<string, ProviderModel> = {
|
||||
protocol: GitHubCopilot.shouldUseResponsesApi(String(input.model.api.id)) ? "openai-responses" : "openai-chat",
|
||||
providerOptions: openAIOptions(options),
|
||||
}),
|
||||
},
|
||||
} as GitHubCopilot.ModelOptions,
|
||||
),
|
||||
"@ai-sdk/google": (input, options) =>
|
||||
Google.model(String(input.model.api.id), sharedOptions(input, options, { protocol: "gemini" })),
|
||||
|
||||
@@ -7,7 +7,7 @@ const types = (events: ReadonlyArray<{ readonly type: string }>) => events.map((
|
||||
describe("LLMNativeEvents", () => {
|
||||
test("synthesizes text and reasoning boundaries around native deltas", () => {
|
||||
const events = LLMNativeEvents.toSessionEvents([
|
||||
{ type: "request-start", id: "req_1", model: LLM.model({ id: "gpt-5", provider: "openai", route: "openai-responses" }) },
|
||||
{ type: "request-start", id: "req_1", model: LLM.model({ id: "gpt-5", provider: "openai", route: "openai-responses", baseURL: "https://api.openai.com/v1" }) },
|
||||
{ type: "step-start", index: 0 },
|
||||
{ type: "text-delta", text: "Hello" },
|
||||
{ type: "text-delta", text: "!" },
|
||||
|
||||
Reference in New Issue
Block a user