refactor(core): flatten provider config

This commit is contained in:
Dax Raad 2026-06-24 12:51:59 -04:00 committed by Shoubhit Dash
commit aca5adf39e
6 changed files with 76 additions and 439 deletions

View file

@ -1,12 +1,17 @@
export * as ConfigProvider from "./provider"
import { Schema } from "effect"
import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model"
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
export const Overlays = {
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
headers: Overlays.headers,
body: Overlays.body,
}) {}
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
@ -30,32 +35,17 @@ class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
output: Schema.Int.pipe(Schema.optional),
}) {}
const ModelApi = Schema.Union([
Schema.Struct({
id: ModelV2.ID.pipe(Schema.optional),
...ProviderV2.AISDK.fields,
}),
Schema.Struct({
id: ModelV2.ID.pipe(Schema.optional),
...ProviderV2.Native.fields,
}),
Schema.Struct({
id: ModelV2.ID,
}),
])
class Model extends Schema.Class<Model>("ConfigV2.Model")({
id: ModelV2.ID.pipe(Schema.optional),
family: ModelV2.Family.pipe(Schema.optional),
name: Schema.String.pipe(Schema.optional),
api: ModelApi.pipe(Schema.optional),
package: Schema.String.pipe(Schema.optional),
aisdk: Schema.Literal(true).pipe(Schema.optional),
...Overlays,
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
request: Schema.Struct({
...Request.fields,
variant: Schema.String.pipe(Schema.optional),
}).pipe(Schema.optional),
variants: Schema.Struct({
id: ModelV2.VariantID,
...Request.fields,
...Overlays,
}).pipe(Schema.Array, Schema.optional),
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional),
@ -65,7 +55,8 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
name: Schema.String.pipe(Schema.optional),
env: Schema.String.pipe(Schema.Array, Schema.optional),
api: ProviderV2.Api.pipe(Schema.optional),
request: Request.pipe(Schema.optional),
package: Schema.String.pipe(Schema.optional),
aisdk: Schema.Literal(true).pipe(Schema.optional),
...Overlays,
models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
}) {}

View file

@ -144,12 +144,12 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
const packageName = config.provider?.npm ?? item.npm
const lowerer = ConfigProviderOptionsV1.get(packageName)
Object.assign(model.request.headers, config.headers)
Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options)))
Object.assign(model.request.body, lowerer.model(withoutCredentials(config.options)))
if (config.variants !== undefined) {
model.variants = Object.entries(config.variants).map(([id, options]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(options.headers ?? {}) },
body: lowerer.request(withoutCredentials(options)),
body: lowerer.model(withoutCredentials(options)),
}))
}
if (config.release_date !== undefined) {

View file

@ -173,15 +173,11 @@ function migrateProvider(info: ConfigProviderV1.Info) {
return {
name: info.name,
env: info.env,
api: info.npm
? {
type: "aisdk" as const,
package: info.npm,
url: info.api ?? options.url,
settings: options.settings ?? {},
}
: undefined,
request: info.options && { headers: options.headers, body: options.body },
package: info.npm,
aisdk: info.npm ? (true as const) : undefined,
settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings,
headers: info.options && options.headers,
body: info.options && options.body,
models:
info.models &&
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])),
@ -191,7 +187,7 @@ function migrateProvider(info: ConfigProviderV1.Info) {
function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) {
const packageID = info.provider?.npm ?? packageName
const lowerer = ConfigProviderOptionsV1.get(packageID)
const request = info.options && lowerer.request(info.options)
const settings = info.options && lowerer.model(info.options)
const costs = info.cost && [
{
input: info.cost.input,
@ -214,29 +210,19 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
: undefined
return {
id: info.id,
family: info.family,
name: info.name,
api: info.provider?.npm
? {
...(info.id === undefined ? {} : { id: info.id }),
type: "aisdk" as const,
package: info.provider.npm,
url: info.provider.api,
settings: {},
}
: info.id === undefined
? undefined
: { id: info.id },
package: info.provider?.npm,
aisdk: info.provider?.npm ? (true as const) : undefined,
settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
capabilities,
request: (info.headers || request) && {
headers: info.headers,
body: request,
},
headers: info.headers,
variants:
info.variants &&
Object.entries(info.variants).map(([id, options]) => ({
id,
body: lowerer.request(options),
settings: lowerer.model(options),
})),
cost: costs,
disabled: info.status === "deprecated" ? true : undefined,

View file

@ -3,209 +3,32 @@ export * as ConfigProviderOptionsV1 from "./provider-options"
type Options = Readonly<Record<string, unknown>>
export interface ProviderResult {
readonly settings: Record<string, unknown>
readonly headers?: Record<string, string>
readonly body?: Record<string, unknown>
readonly url?: string
readonly settings?: Record<string, unknown>
}
export interface Lowerer {
readonly provider: (options: Options) => ProviderResult
readonly request: (options: Options) => Record<string, unknown>
readonly model: (options: Options) => Record<string, unknown>
}
export function get(packageName?: string): Lowerer {
const key = packageName ?? ""
return Object.hasOwn(lowerers, key) ? lowerers[key]! : raw
}
const raw: Lowerer = {
provider(options) {
return { body: clone(options) }
},
request: clone,
}
const openai: Lowerer = {
const lowerer: Lowerer = {
provider(options) {
return {
url: string(options.baseURL),
headers: compact({
Authorization: bearer(options.apiKey),
"OpenAI-Organization": string(options.organization),
"OpenAI-Project": string(options.project),
...headers(options.headers),
}),
body: body(options.body),
settings: omit(options, ["apiKey", "baseURL", "organization", "project", "headers", "body"]),
settings: Object.fromEntries(Object.entries(options).filter(([key]) => key !== "headers" && key !== "body")),
headers: record(options.headers, (value): value is string => typeof value === "string"),
body: record(options.body, () => true),
}
},
request: snake,
model: (options) => ({ ...options }),
}
const anthropic: Lowerer = {
provider(options) {
return {
url: string(options.baseURL),
headers: compact({
"x-api-key": string(options.apiKey),
Authorization: options.authToken ? bearer(options.authToken) : undefined,
...headers(options.headers),
}),
body: body(options.body),
settings: omit(options, ["apiKey", "authToken", "baseURL", "headers", "body"]),
}
},
request(options) {
const result = snake(options)
if (options.effort !== undefined || options.taskBudget !== undefined) {
result.output_config = compactUnknown({ effort: options.effort, task_budget: options.taskBudget })
delete result.effort
delete result.task_budget
}
if (isRecord(options.metadata) && options.metadata.userId !== undefined) {
result.metadata = { ...(isRecord(result.metadata) ? result.metadata : {}), user_id: options.metadata.userId }
}
return result
},
export function get(_packageName?: string): Lowerer {
return lowerer
}
const google: Lowerer = {
provider(options) {
return {
url: string(options.baseURL),
headers: compact({ "x-goog-api-key": string(options.apiKey), ...headers(options.headers) }),
body: body(options.body),
settings: omit(options, ["apiKey", "baseURL", "headers", "body"]),
}
},
request(options) {
const generationConfig = pick(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"])
return {
...omit(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"]),
...(Object.keys(generationConfig).length ? { generationConfig } : {}),
}
},
}
const azure: Lowerer = {
provider(options) {
return {
url: string(options.baseURL),
headers: compact({ "api-key": string(options.apiKey), ...headers(options.headers) }),
body: body(options.body),
settings: omit(options, ["apiKey", "baseURL", "headers", "body"]),
}
},
request: openai.request,
}
const bedrock: Lowerer = {
provider(options) {
return direct(options)
},
request(options) {
return { additionalModelRequestFields: clone(options) }
},
}
const openaiCompatible: Lowerer = {
provider(options) {
return { ...direct(options, ["baseURL"]), url: string(options.baseURL) }
},
request(options) {
const result = clone(options)
if (options.reasoningEffort !== undefined) {
result.reasoning_effort = options.reasoningEffort
delete result.reasoningEffort
}
return result
},
}
const lowerers: Readonly<Record<string, Lowerer>> = {
"@ai-sdk/openai": openai,
"@ai-sdk/anthropic": anthropic,
"@ai-sdk/google-vertex/anthropic": anthropic,
"@ai-sdk/google": google,
"@ai-sdk/google-vertex": google,
"@ai-sdk/azure": azure,
"@ai-sdk/amazon-bedrock": bedrock,
"@ai-sdk/openai-compatible": openaiCompatible,
"@ai-sdk/cerebras": openaiCompatible,
"@ai-sdk/deepinfra": openaiCompatible,
"@ai-sdk/groq": openaiCompatible,
"@ai-sdk/mistral": openaiCompatible,
"@ai-sdk/togetherai": openaiCompatible,
"@ai-sdk/xai": openaiCompatible,
"@openrouter/ai-sdk-provider": openaiCompatible,
"ai-gateway-provider": openaiCompatible,
"venice-ai-sdk-provider": openaiCompatible,
}
function direct(options: Options, extraKeys: ReadonlyArray<string> = []): ProviderResult {
return {
headers: headers(options.headers),
body: body(options.body),
settings: omit(options, ["headers", "body", ...extraKeys]),
}
}
function body(input: unknown) {
if (!isRecord(input)) return undefined
return { ...input }
}
function snake(options: Options) {
return Object.fromEntries(Object.entries(options).map(([key, value]) => [snakeKey(key), snakeValue(value)]))
}
function snakeValue(value: unknown): unknown {
if (Array.isArray(value)) return value.map(snakeValue)
if (!isRecord(value)) return value
return Object.fromEntries(Object.entries(value).map(([key, value]) => [snakeKey(key), snakeValue(value)]))
}
function snakeKey(key: string) {
return key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase())
}
function clone(options: Options) {
return { ...options }
}
function omit(options: Options, keys: ReadonlyArray<string>) {
return Object.fromEntries(Object.entries(options).filter(([key]) => !keys.includes(key)))
}
function pick(options: Options, keys: ReadonlyArray<string>) {
return Object.fromEntries(Object.entries(options).filter(([key]) => keys.includes(key)))
}
function headers(input: unknown) {
if (!isRecord(input)) return undefined
return Object.fromEntries(
Object.entries(input).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
)
}
function compact(input: Record<string, string | undefined>) {
const entries = Object.entries(input).filter((entry): entry is [string, string] => entry[1] !== undefined)
return entries.length ? Object.fromEntries(entries) : undefined
}
function compactUnknown(input: Record<string, unknown>) {
return Object.fromEntries(Object.entries(input).filter((entry) => entry[1] !== undefined))
}
function string(input: unknown) {
return typeof input === "string" && input ? input : undefined
}
function bearer(input: unknown) {
return typeof input === "string" && input ? `Bearer ${input}` : undefined
}
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input)
function record<T>(input: unknown, guard: (value: unknown) => value is T) {
if (typeof input !== "object" || input === null || Array.isArray(input)) return undefined
return Object.fromEntries(Object.entries(input).filter((entry): entry is [string, T] => guard(entry[1])))
}