feat(core): migrate native provider config
This commit is contained in:
parent
9848587052
commit
62e44a80e7
16 changed files with 358 additions and 99 deletions
|
|
@ -78,12 +78,28 @@ export const layer = Layer.effect(
|
|||
|
||||
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
|
||||
const api =
|
||||
model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0
|
||||
? { ...provider.api, id: model.api.id }
|
||||
model.api.type === "native" && !model.api.package
|
||||
? {
|
||||
...provider.api,
|
||||
id: model.api.id,
|
||||
url: model.api.url ?? provider.api.url,
|
||||
settings: ModelRequest.mergeRecords(provider.api.settings, model.api.settings),
|
||||
}
|
||||
: model.api.type === "native" && provider.api.type === "native" && !model.api.url
|
||||
? {
|
||||
...model.api,
|
||||
package: model.api.package ?? provider.api.package,
|
||||
url: provider.api.url,
|
||||
settings: ModelRequest.mergeRecords(provider.api.settings, model.api.settings),
|
||||
}
|
||||
: model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
|
||||
? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } }
|
||||
? {
|
||||
...model.api,
|
||||
url: provider.api.url,
|
||||
settings: ModelRequest.mergeRecords(provider.api.settings, model.api.settings),
|
||||
}
|
||||
: model.api.type === "aisdk" && provider.api.type === "aisdk"
|
||||
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
|
||||
? { ...model.api, settings: ModelRequest.mergeRecords(provider.api.settings, model.api.settings) }
|
||||
: model.api
|
||||
const request = {
|
||||
...ModelRequest.merge({ ...provider.request, generation: {}, options: {} }, model.request),
|
||||
|
|
|
|||
|
|
@ -48,26 +48,95 @@ export const Plugin = define({
|
|||
const model = ModelV2.parse(configuredDefault)
|
||||
catalog.model.default.set(model.providerID, model.modelID)
|
||||
}
|
||||
const providerAiSDK = new Map<string, boolean>()
|
||||
const modelAiSDK = new Map<string, boolean>()
|
||||
for (const file of files) {
|
||||
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
||||
const providerID = id
|
||||
if (item.aiSDK !== undefined) providerAiSDK.set(providerID, item.aiSDK)
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
if (item.api !== undefined) provider.api = { ...item.api }
|
||||
if (item.request !== undefined) {
|
||||
Object.assign(provider.request.headers, item.request.headers)
|
||||
Object.assign(provider.request.body, item.request.body)
|
||||
if (item.package !== undefined) {
|
||||
const settings = ModelRequest.mergeRecords(provider.api.settings, item.settings)
|
||||
const url =
|
||||
item.settings && Object.hasOwn(item.settings, "baseURL")
|
||||
? typeof settings.baseURL === "string"
|
||||
? settings.baseURL
|
||||
: undefined
|
||||
: provider.api.url
|
||||
provider.api = (providerAiSDK.get(providerID) ?? provider.api.type === "aisdk")
|
||||
? { type: "aisdk", package: item.package, ...(url === undefined ? {} : { url }), settings }
|
||||
: { type: "native", package: item.package, ...(url === undefined ? {} : { url }), settings }
|
||||
} else if (item.settings !== undefined) {
|
||||
provider.api.settings = ModelRequest.mergeRecords(provider.api.settings, item.settings)
|
||||
if (Object.hasOwn(item.settings, "baseURL")) {
|
||||
provider.api.url =
|
||||
typeof provider.api.settings.baseURL === "string" ? provider.api.settings.baseURL : undefined
|
||||
}
|
||||
}
|
||||
if (item.package === undefined && item.aiSDK !== undefined) {
|
||||
if (item.aiSDK && provider.api.type === "native" && provider.api.package !== undefined) {
|
||||
provider.api = { ...provider.api, type: "aisdk", package: provider.api.package }
|
||||
}
|
||||
if (!item.aiSDK && provider.api.type === "aisdk") {
|
||||
provider.api = { ...provider.api, type: "native", settings: provider.api.settings ?? {} }
|
||||
}
|
||||
}
|
||||
ModelRequest.assign(provider.request, { headers: item.headers, body: item.body })
|
||||
})
|
||||
const providerApi = catalog.provider.get(providerID)?.provider.api
|
||||
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
|
||||
|
||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||
const modelKey = `${providerID}/${id}`
|
||||
if (config.aiSDK !== undefined) modelAiSDK.set(modelKey, config.aiSDK)
|
||||
catalog.model.update(providerID, id, (model) => {
|
||||
if (config.family !== undefined) model.family = config.family
|
||||
if (config.name !== undefined) model.name = config.name
|
||||
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
|
||||
if (config.id !== undefined) model.api.id = config.id
|
||||
if (config.package !== undefined) {
|
||||
const aiSDK =
|
||||
modelAiSDK.get(modelKey) ?? providerAiSDK.get(providerID) ?? providerApi?.type === "aisdk"
|
||||
const settings = ModelRequest.mergeRecords(model.api.settings, config.settings)
|
||||
const url =
|
||||
config.settings && Object.hasOwn(config.settings, "baseURL")
|
||||
? typeof settings.baseURL === "string"
|
||||
? settings.baseURL
|
||||
: undefined
|
||||
: model.api.url
|
||||
model.api = aiSDK
|
||||
? {
|
||||
id: model.api.id,
|
||||
type: "aisdk",
|
||||
package: config.package,
|
||||
...(url === undefined ? {} : { url }),
|
||||
settings,
|
||||
}
|
||||
: {
|
||||
id: model.api.id,
|
||||
type: "native",
|
||||
package: config.package,
|
||||
...(url === undefined ? {} : { url }),
|
||||
settings,
|
||||
}
|
||||
} else if (config.settings !== undefined) {
|
||||
model.api.settings = ModelRequest.mergeRecords(model.api.settings, config.settings)
|
||||
if (Object.hasOwn(config.settings, "baseURL")) {
|
||||
model.api.url =
|
||||
typeof model.api.settings.baseURL === "string" ? model.api.settings.baseURL : undefined
|
||||
}
|
||||
}
|
||||
if (config.package === undefined && config.aiSDK !== undefined) {
|
||||
if (config.aiSDK && model.api.type === "native" && model.api.package !== undefined) {
|
||||
model.api = { ...model.api, type: "aisdk", package: model.api.package }
|
||||
}
|
||||
if (!config.aiSDK && model.api.type === "aisdk") {
|
||||
model.api = { ...model.api, type: "native", settings: model.api.settings ?? {} }
|
||||
}
|
||||
}
|
||||
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
|
||||
const aiSDK =
|
||||
modelAiSDK.get(modelKey) ?? providerAiSDK.get(providerID) ?? providerApi?.type === "aisdk"
|
||||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
|
|
@ -75,19 +144,22 @@ export const Plugin = define({
|
|||
output: [...config.capabilities.output],
|
||||
}
|
||||
}
|
||||
if (config.request !== undefined) {
|
||||
if (config.headers !== undefined || config.body !== undefined) {
|
||||
ModelRequest.assign(model.request, {
|
||||
headers: config.request.headers,
|
||||
...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
|
||||
headers: config.headers,
|
||||
...(aiSDK
|
||||
? ModelRequest.normalizeAiSdkOptions(packageName, config.body ?? {})
|
||||
: { body: config.body }),
|
||||
})
|
||||
if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
||||
}
|
||||
if (config.variant !== undefined) model.request.variant = config.variant
|
||||
if (config.variants !== undefined) {
|
||||
for (const variant of config.variants) {
|
||||
let existing = model.variants.find((item) => item.id === variant.id)
|
||||
if (!existing) {
|
||||
existing = {
|
||||
id: variant.id,
|
||||
settings: {},
|
||||
headers: {},
|
||||
body: {},
|
||||
generation: {},
|
||||
|
|
@ -95,9 +167,12 @@ export const Plugin = define({
|
|||
}
|
||||
model.variants.push(existing)
|
||||
}
|
||||
existing.settings = ModelRequest.mergeRecords(existing.settings, variant.settings)
|
||||
ModelRequest.assign(existing, {
|
||||
headers: variant.headers,
|
||||
...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
|
||||
...(aiSDK
|
||||
? ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {})
|
||||
: { body: variant.body }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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")({
|
||||
|
|
@ -9,6 +8,11 @@ export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")(
|
|||
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
const Overlays = {
|
||||
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
...Request.fields,
|
||||
}
|
||||
|
||||
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
|
||||
read: Schema.Finite.pipe(Schema.optional),
|
||||
write: Schema.Finite.pipe(Schema.optional),
|
||||
|
|
@ -30,32 +34,18 @@ 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),
|
||||
package: Schema.String.pipe(Schema.optional),
|
||||
aiSDK: Schema.Boolean.pipe(Schema.optional),
|
||||
...Overlays,
|
||||
family: ModelV2.Family.pipe(Schema.optional),
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
api: ModelApi.pipe(Schema.optional),
|
||||
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
|
||||
request: Schema.Struct({
|
||||
...Request.fields,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}).pipe(Schema.optional),
|
||||
variant: Schema.String.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.Boolean.pipe(Schema.optional),
|
||||
...Overlays,
|
||||
models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
|
|
|||
|
|
@ -67,16 +67,41 @@ const profiles = new Map<string, Profile>([
|
|||
|
||||
export const namespace = (packageName: string) => profiles.get(packageName)?.namespace
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
|
||||
export const mergeRecords = (...items: ReadonlyArray<Readonly<Record<string, unknown>> | undefined>) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const item of items) {
|
||||
for (const [key, value] of Object.entries(item ?? {})) {
|
||||
result[key] = isRecord(result[key]) && isRecord(value) ? mergeRecords(result[key], value) : value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const mergeHeaders = (...items: ReadonlyArray<Readonly<Record<string, string>> | undefined>) => {
|
||||
const result = new Map<string, readonly [string, string]>()
|
||||
for (const item of items) {
|
||||
for (const entry of Object.entries(item ?? {})) result.set(entry[0].toLowerCase(), entry)
|
||||
}
|
||||
return Object.fromEntries(result.values())
|
||||
}
|
||||
|
||||
export const merge = (base: Request, override: Partial<Request>) => ({
|
||||
headers: { ...base.headers, ...override.headers },
|
||||
body: { ...base.body, ...override.body },
|
||||
headers: mergeHeaders(base.headers, override.headers),
|
||||
body: mergeRecords(base.body, override.body),
|
||||
generation: { ...base.generation, ...override.generation },
|
||||
options: { ...base.options, ...override.options },
|
||||
})
|
||||
|
||||
export const assign = (target: MutableRequest, override: Partial<Request>) => {
|
||||
Object.assign(target.headers, override.headers)
|
||||
Object.assign(target.body, override.body)
|
||||
const headers = mergeHeaders(target.headers, override.headers)
|
||||
Object.keys(target.headers).forEach((key) => delete target.headers[key])
|
||||
Object.assign(target.headers, headers)
|
||||
const body = mergeRecords(target.body, override.body)
|
||||
Object.keys(target.body).forEach((key) => delete target.body[key])
|
||||
Object.assign(target.body, body)
|
||||
Object.assign((target.generation ??= {}), override.generation)
|
||||
Object.assign((target.options ??= {}), override.options)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ export type ID = typeof ID.Type
|
|||
export const VariantID = Model.VariantID
|
||||
export type VariantID = typeof VariantID.Type
|
||||
|
||||
export const Package = Model.Package
|
||||
export type Package = Model.Package
|
||||
|
||||
// Grouping of models, eg claude opus, claude sonnet
|
||||
export const Family = Model.Family
|
||||
export type Family = Model.Family
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import { Provider } from "@opencode-ai/schema/provider"
|
|||
export const ID = Provider.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Overlays = Provider.Overlays
|
||||
|
||||
export const Package = Provider.Package
|
||||
export type Package = Provider.Package
|
||||
|
||||
export const AISDK = Provider.AISDK
|
||||
|
||||
export const Native = Provider.Native
|
||||
|
|
|
|||
|
|
@ -122,6 +122,12 @@ const withVariant = (
|
|||
return Effect.succeed(
|
||||
variant
|
||||
? produce(model, (draft) => {
|
||||
if (variant.settings !== undefined) {
|
||||
draft.api.settings = ModelRequest.mergeRecords(draft.api.settings, variant.settings)
|
||||
if (Object.hasOwn(variant.settings, "baseURL")) {
|
||||
draft.api.url = typeof draft.api.settings.baseURL === "string" ? draft.api.settings.baseURL : undefined
|
||||
}
|
||||
}
|
||||
ModelRequest.assign(draft.request, variant)
|
||||
})
|
||||
: model,
|
||||
|
|
|
|||
|
|
@ -174,15 +174,14 @@ 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 : undefined,
|
||||
settings: {
|
||||
...options.settings,
|
||||
...(info.api ?? options.url ? { baseURL: info.api ?? options.url } : {}),
|
||||
},
|
||||
headers: options.headers,
|
||||
body: options.body,
|
||||
models:
|
||||
info.models &&
|
||||
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])),
|
||||
|
|
@ -221,22 +220,13 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
|
|||
return {
|
||||
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 },
|
||||
id: info.id,
|
||||
package: info.provider?.npm,
|
||||
aiSDK: info.provider?.npm ? true : undefined,
|
||||
settings: info.provider?.api ? { baseURL: info.provider.api } : undefined,
|
||||
capabilities,
|
||||
request: (info.headers || request) && {
|
||||
headers: info.headers,
|
||||
body: request,
|
||||
},
|
||||
headers: info.headers,
|
||||
body: request,
|
||||
variants:
|
||||
info.variants &&
|
||||
Object.entries(info.variants).map(([id, options]) => ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue