Refactor LLM route-first provider API (#28523)
This commit is contained in:
parent
5381795844
commit
41f6daf96a
87 changed files with 2436 additions and 1506 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { Schema } from "effect"
|
||||
import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids"
|
||||
import { ModelRef } from "./options"
|
||||
import { ModelSchema } from "./options"
|
||||
import { ToolResultValue } from "./messages"
|
||||
|
||||
/**
|
||||
|
|
@ -290,7 +290,7 @@ export class PreparedRequest extends Schema.Class<PreparedRequest>("LLM.Prepared
|
|||
id: Schema.String,
|
||||
route: RouteID,
|
||||
protocol: ProtocolID,
|
||||
model: ModelRef,
|
||||
model: ModelSchema,
|
||||
body: Schema.Unknown,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
}) {}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { Schema } from "effect"
|
||||
import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
|
||||
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelRef, ProviderOptions } from "./options"
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options"
|
||||
import { isRecord } from "../utils/record"
|
||||
|
||||
const systemPartSchema = Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
|
|
@ -41,17 +39,49 @@ export const MediaPart = Schema.Struct({
|
|||
}).annotate({ identifier: "LLM.Content.Media" })
|
||||
export type MediaPart = Schema.Schema.Type<typeof MediaPart>
|
||||
|
||||
export const ToolResultMediaPart = Schema.Struct({
|
||||
type: Schema.Literal("media"),
|
||||
mediaType: Schema.String,
|
||||
data: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
}).annotate({ identifier: "LLM.ToolResult.Media" })
|
||||
export type ToolResultMediaPart = Schema.Schema.Type<typeof ToolResultMediaPart>
|
||||
|
||||
export const ToolResultContentPart = Schema.Union([TextPart, ToolResultMediaPart])
|
||||
export type ToolResultContentPart = Schema.Schema.Type<typeof ToolResultContentPart>
|
||||
|
||||
const isToolResultValue = (value: unknown): value is ToolResultValue =>
|
||||
isRecord(value) && (value.type === "text" || value.type === "json" || value.type === "error") && "value" in value
|
||||
isRecord(value) &&
|
||||
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
|
||||
"value" in value
|
||||
|
||||
export const ToolResultValue = Object.assign(
|
||||
Schema.Struct({
|
||||
type: Schema.Literals(["json", "text", "error"]),
|
||||
value: Schema.Unknown,
|
||||
}).annotate({ identifier: "LLM.ToolResult" }),
|
||||
Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("json"),
|
||||
value: Schema.Unknown,
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
value: Schema.Unknown,
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("error"),
|
||||
value: Schema.Unknown,
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("content"),
|
||||
value: Schema.Array(ToolResultContentPart),
|
||||
}),
|
||||
]).annotate({ identifier: "LLM.ToolResult" }),
|
||||
{
|
||||
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue =>
|
||||
isToolResultValue(value) ? value : { type, value },
|
||||
is: isToolResultValue,
|
||||
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
||||
if (isToolResultValue(value)) return value
|
||||
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
|
||||
return { type, value }
|
||||
},
|
||||
},
|
||||
)
|
||||
export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
|
||||
|
|
@ -197,7 +227,7 @@ export type ResponseFormat = Schema.Schema.Type<typeof ResponseFormat>
|
|||
|
||||
export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
||||
id: Schema.optional(Schema.String),
|
||||
model: ModelRef,
|
||||
model: ModelSchema,
|
||||
system: Schema.Array(SystemPart),
|
||||
messages: Schema.Array(Message),
|
||||
tools: Schema.Array(ToolDefinition),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { Schema } from "effect"
|
||||
import { JsonSchema, ModelID, ProviderID, RouteID } from "./ids"
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
import { JsonSchema, ModelID, ProviderID } from "./ids"
|
||||
import type { AnyRoute } from "../route/client"
|
||||
import { isRecord } from "../utils/record"
|
||||
|
||||
export const mergeJsonRecords = (
|
||||
...items: ReadonlyArray<Record<string, unknown> | undefined>
|
||||
|
|
@ -135,67 +134,59 @@ export namespace ModelLimits {
|
|||
input instanceof ModelLimits ? input : new ModelLimits(input ?? {})
|
||||
}
|
||||
|
||||
export class ModelRef extends Schema.Class<ModelRef>("LLM.ModelRef")({
|
||||
id: ModelID,
|
||||
provider: ProviderID,
|
||||
route: RouteID,
|
||||
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. */
|
||||
auth: Schema.optional(Schema.Any),
|
||||
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
/**
|
||||
* Query params appended to the request URL by `Endpoint.baseURL`. Used for
|
||||
* deployment-level URL-scoped settings such as Azure's `api-version` or any
|
||||
* provider that requires a per-request key in the URL. Generic concern, so
|
||||
* lives as a typed first-class field instead of `native`.
|
||||
*/
|
||||
queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
limits: ModelLimits,
|
||||
/** Provider-neutral generation defaults. Request-level values override them. */
|
||||
generation: Schema.optional(GenerationOptions),
|
||||
/** Provider-owned typed-at-the-facade options for non-portable knobs. */
|
||||
providerOptions: Schema.optional(ProviderOptions),
|
||||
/** Serializable raw HTTP overlays applied to the final outgoing request. */
|
||||
http: Schema.optional(HttpOptions),
|
||||
/**
|
||||
* Provider-specific opaque options. Reach for this only when the value is
|
||||
* genuinely provider-private and does not fit a typed axis (e.g. Bedrock's
|
||||
* `aws_credentials` / `aws_region` for SigV4). Anything used by more than
|
||||
* one route should grow into a typed field instead.
|
||||
*/
|
||||
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
}) {}
|
||||
export class Model {
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: AnyRoute
|
||||
|
||||
export namespace ModelRef {
|
||||
export type Input = ConstructorParameters<typeof ModelRef>[0]
|
||||
constructor(input: Model.ConstructorInput) {
|
||||
this.id = input.id
|
||||
this.provider = input.provider
|
||||
this.route = input.route
|
||||
}
|
||||
|
||||
export const input = (model: ModelRef): Input => ({
|
||||
id: model.id,
|
||||
provider: model.provider,
|
||||
route: model.route,
|
||||
baseURL: model.baseURL,
|
||||
apiKey: model.apiKey,
|
||||
auth: model.auth,
|
||||
headers: model.headers,
|
||||
queryParams: model.queryParams,
|
||||
limits: model.limits,
|
||||
generation: model.generation,
|
||||
providerOptions: model.providerOptions,
|
||||
http: model.http,
|
||||
native: model.native,
|
||||
})
|
||||
static make(input: Model.Input) {
|
||||
return new Model({
|
||||
id: ModelID.make(input.id),
|
||||
provider: ProviderID.make(input.provider),
|
||||
route: input.route,
|
||||
})
|
||||
}
|
||||
|
||||
export const update = (model: ModelRef, patch: Partial<Input>) => {
|
||||
static input(model: Model): Model.ConstructorInput {
|
||||
return {
|
||||
id: model.id,
|
||||
provider: model.provider,
|
||||
route: model.route,
|
||||
}
|
||||
}
|
||||
|
||||
static update(model: Model, patch: Partial<Model.Input>) {
|
||||
if (Object.keys(patch).length === 0) return model
|
||||
return new ModelRef({
|
||||
...input(model),
|
||||
return Model.make({
|
||||
...Model.input(model),
|
||||
...patch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Model {
|
||||
export type ConstructorInput = {
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: AnyRoute
|
||||
}
|
||||
|
||||
export type Input = Omit<ConstructorInput, "id" | "provider"> & {
|
||||
readonly id: string | ModelID
|
||||
readonly provider: string | ProviderID
|
||||
}
|
||||
}
|
||||
|
||||
export type ModelInput = Model.Input
|
||||
|
||||
export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" })
|
||||
|
||||
export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
|
||||
type: Schema.Literals(["ephemeral", "persistent"]),
|
||||
ttlSeconds: Schema.optional(Schema.Number),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue