refactor(core): consolidate provider helpers
This commit is contained in:
parent
5e12dbdbfb
commit
864f16c703
6 changed files with 152 additions and 137 deletions
|
|
@ -8,7 +8,6 @@ import { EventV2 } from "./event"
|
|||
import { Policy } from "./policy"
|
||||
import { State } from "./state"
|
||||
import { Integration } from "./integration"
|
||||
import { ProviderOverlay } from "./provider-overlay"
|
||||
|
||||
export type ProviderRecord = {
|
||||
provider: ProviderV2.MutableInfo
|
||||
|
|
@ -79,9 +78,9 @@ export const layer = Layer.effect(
|
|||
return ModelV2.Info.make({
|
||||
...model,
|
||||
package: model.package ?? provider.package,
|
||||
settings: ProviderOverlay.merge(provider.settings, model.settings),
|
||||
headers: ProviderOverlay.headers(provider.headers, model.headers),
|
||||
body: ProviderOverlay.merge(provider.body, model.body),
|
||||
settings: ProviderV2.mergeOverlay(provider.settings, model.settings),
|
||||
headers: ProviderV2.mergeHeaders(provider.headers, model.headers),
|
||||
body: ProviderV2.mergeOverlay(provider.body, model.body),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { define } from "../../plugin/internal"
|
|||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ProviderOverlay } from "../../provider-overlay"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const Plugin = define({
|
||||
|
|
@ -55,9 +54,9 @@ export const Plugin = define({
|
|||
if (item.name !== undefined) provider.name = item.name
|
||||
if (item.package !== undefined) provider.package = item.package
|
||||
if (item.settings !== undefined)
|
||||
provider.settings = ProviderOverlay.merge(provider.settings, item.settings)
|
||||
if (item.headers !== undefined) provider.headers = ProviderOverlay.headers(provider.headers, item.headers)
|
||||
if (item.body !== undefined) provider.body = ProviderOverlay.merge(provider.body, item.body)
|
||||
provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings)
|
||||
if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers)
|
||||
if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body)
|
||||
})
|
||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||
catalog.model.update(providerID, id, (model) => {
|
||||
|
|
@ -66,9 +65,9 @@ export const Plugin = define({
|
|||
if (config.modelID !== undefined) model.modelID = config.modelID
|
||||
if (config.package !== undefined) model.package = config.package
|
||||
if (config.settings !== undefined)
|
||||
model.settings = ProviderOverlay.merge(model.settings, config.settings)
|
||||
if (config.headers !== undefined) model.headers = ProviderOverlay.headers(model.headers, config.headers)
|
||||
if (config.body !== undefined) model.body = ProviderOverlay.merge(model.body, config.body)
|
||||
model.settings = ProviderV2.mergeOverlay(model.settings, config.settings)
|
||||
if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers)
|
||||
if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body)
|
||||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
|
|
@ -87,10 +86,10 @@ export const Plugin = define({
|
|||
model.variants.push(existing)
|
||||
}
|
||||
if (variant.settings !== undefined)
|
||||
existing.settings = ProviderOverlay.merge(existing.settings, variant.settings)
|
||||
existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings)
|
||||
if (variant.headers !== undefined)
|
||||
existing.headers = ProviderOverlay.headers(existing.headers, variant.headers)
|
||||
if (variant.body !== undefined) existing.body = ProviderOverlay.merge(existing.body, variant.body)
|
||||
existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers)
|
||||
if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body)
|
||||
}
|
||||
}
|
||||
if (config.cost !== undefined) {
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
export * as ProviderOverlay from "./provider-overlay"
|
||||
|
||||
export function merge(
|
||||
base: Readonly<Record<string, unknown>> | undefined,
|
||||
overlay: Readonly<Record<string, unknown>> | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (base === undefined) return overlay && { ...overlay }
|
||||
if (overlay === undefined) return { ...base }
|
||||
return Object.fromEntries(
|
||||
new Set([...Object.keys(base), ...Object.keys(overlay)]).values().map((key) => {
|
||||
const left = base[key]
|
||||
const right = overlay[key]
|
||||
if (right === undefined) return [key, left]
|
||||
if (plain(left) && plain(right)) return [key, merge(left, right)]
|
||||
return [key, right]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function headers(
|
||||
base: Readonly<Record<string, string>> | undefined,
|
||||
overlay: Readonly<Record<string, string>> | undefined,
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
[...Object.entries(base ?? {}), ...Object.entries(overlay ?? {})]
|
||||
.reduce((result, entry) => {
|
||||
result.set(entry[0].toLowerCase(), entry)
|
||||
return result
|
||||
}, new Map<string, [string, string]>())
|
||||
.values(),
|
||||
)
|
||||
}
|
||||
|
||||
function plain(input: unknown): input is Readonly<Record<string, unknown>> {
|
||||
if (typeof input !== "object" || input === null || Array.isArray(input)) return false
|
||||
const prototype = Object.getPrototypeOf(input)
|
||||
return prototype === Object.prototype || prototype === null
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
export * as ProviderPackage from "./provider-package"
|
||||
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import type { Model, ProviderPackageDefinition, ProviderPackageSettings } from "@opencode-ai/llm"
|
||||
import { Npm } from "./npm"
|
||||
|
||||
const cache = new Map<string, Promise<unknown>>()
|
||||
|
||||
export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderPackage.LoadError", {
|
||||
package: Schema.String,
|
||||
cause: Schema.Defect(),
|
||||
}) {}
|
||||
|
||||
export const load = Effect.fn("ProviderPackage.load")(function* (specifier: string) {
|
||||
const npm = Option.getOrUndefined(yield* Effect.serviceOption(Npm.Service))
|
||||
const resolved =
|
||||
specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")
|
||||
? specifier
|
||||
: yield* Effect.sync(() => {
|
||||
try {
|
||||
return import.meta.resolve(specifier)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
if (resolved) return yield* importProviderPackage(specifier, resolved)
|
||||
if (!npm) {
|
||||
return yield* new LoadError({
|
||||
package: specifier,
|
||||
cause: new Error(`Provider package ${specifier} is not installed`),
|
||||
})
|
||||
}
|
||||
const installed = yield* npm
|
||||
.add(packageName(specifier))
|
||||
.pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
|
||||
const entrypoint = yield* Effect.try({
|
||||
try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
|
||||
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||
})
|
||||
return yield* importProviderPackage(specifier, entrypoint)
|
||||
})
|
||||
|
||||
const importProviderPackage = Effect.fn("ProviderPackage.import")(function* (specifier: string, entrypoint: string) {
|
||||
const module = yield* Effect.tryPromise({
|
||||
try: () => {
|
||||
const existing = cache.get(entrypoint)
|
||||
if (existing) return existing
|
||||
const loaded = import(entrypoint)
|
||||
cache.set(entrypoint, loaded)
|
||||
return loaded
|
||||
},
|
||||
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||
})
|
||||
if (!isProviderPackage(module)) {
|
||||
return yield* new LoadError({
|
||||
package: specifier,
|
||||
cause: new Error(`Provider package ${specifier} does not export model(id, settings)`),
|
||||
})
|
||||
}
|
||||
return module
|
||||
})
|
||||
|
||||
export const make = (module: ProviderPackageDefinition, modelID: string, settings: ProviderPackageSettings): Model =>
|
||||
module.model(modelID, settings)
|
||||
|
||||
function isProviderPackage(input: unknown): input is ProviderPackageDefinition {
|
||||
return typeof input === "object" && input !== null && "model" in input && typeof input.model === "function"
|
||||
}
|
||||
|
||||
function packageName(specifier: string) {
|
||||
const parts = specifier.split("/")
|
||||
if (specifier.startsWith("@")) return parts.slice(0, 2).join("/")
|
||||
return parts[0]
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
export * as ProviderV2 from "./provider"
|
||||
|
||||
import { Effect, Schema } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import type { Model, ProviderPackageDefinition, ProviderPackageSettings } from "@opencode-ai/llm"
|
||||
import { Npm } from "./npm"
|
||||
import type { DeepMutable } from "./schema"
|
||||
|
||||
export const ID = Provider.ID
|
||||
|
|
@ -12,6 +16,81 @@ export const aisdk = (packageName: string) => (isAISDK(packageName) ? packageNam
|
|||
export const packageName = (packageName: string | undefined) =>
|
||||
isAISDK(packageName) ? packageName!.slice(AISDK_PREFIX.length) : packageName
|
||||
|
||||
export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.LoadError", {
|
||||
package: Schema.String,
|
||||
cause: Schema.Defect(),
|
||||
}) {}
|
||||
export type ProviderPackage = ProviderPackageDefinition
|
||||
|
||||
const packages = new Map<string, Promise<unknown>>()
|
||||
|
||||
export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
|
||||
const resolved = yield* resolvePackage(specifier)
|
||||
if (resolved) return yield* importPackage(specifier, resolved)
|
||||
if (!npm) {
|
||||
return yield* new LoadError({
|
||||
package: specifier,
|
||||
cause: new Error(`Provider package ${specifier} is not installed`),
|
||||
})
|
||||
}
|
||||
const installed = yield* npm
|
||||
.add(rootPackage(specifier))
|
||||
.pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
|
||||
const entrypoint = yield* Effect.try({
|
||||
try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
|
||||
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||
})
|
||||
return yield* importPackage(specifier, entrypoint)
|
||||
})
|
||||
|
||||
export const makeModel = (
|
||||
module: ProviderPackageDefinition,
|
||||
modelID: string,
|
||||
settings: ProviderPackageSettings,
|
||||
): Model => module.model(modelID, settings)
|
||||
|
||||
export function mergeOverlay(
|
||||
base: Readonly<Record<string, unknown>> | undefined,
|
||||
overlay: Readonly<Record<string, unknown>> | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (base === undefined) return overlay && { ...overlay }
|
||||
if (overlay === undefined) return { ...base }
|
||||
return Object.fromEntries(
|
||||
new Set([...Object.keys(base), ...Object.keys(overlay)]).values().map((key) => {
|
||||
const left = base[key]
|
||||
const right = overlay[key]
|
||||
if (right === undefined) return [key, left]
|
||||
if (
|
||||
typeof left === "object" &&
|
||||
left !== null &&
|
||||
!Array.isArray(left) &&
|
||||
typeof right === "object" &&
|
||||
right !== null &&
|
||||
!Array.isArray(right)
|
||||
)
|
||||
return [
|
||||
key,
|
||||
mergeOverlay(left as Readonly<Record<string, unknown>>, right as Readonly<Record<string, unknown>>),
|
||||
]
|
||||
return [key, right]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function mergeHeaders(
|
||||
base: Readonly<Record<string, string>> | undefined,
|
||||
overlay: Readonly<Record<string, string>> | undefined,
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
[...Object.entries(base ?? {}), ...Object.entries(overlay ?? {})]
|
||||
.reduce((result, entry) => {
|
||||
result.set(entry[0].toLowerCase(), entry)
|
||||
return result
|
||||
}, new Map<string, [string, string]>())
|
||||
.values(),
|
||||
)
|
||||
}
|
||||
|
||||
export const Request = Provider.Request
|
||||
export type Request = Provider.Request
|
||||
|
||||
|
|
@ -19,3 +98,43 @@ export const Info = Provider.Info
|
|||
export type Info = Provider.Info
|
||||
|
||||
export type MutableInfo = DeepMutable<Info>
|
||||
|
||||
const resolvePackage = (specifier: string) =>
|
||||
Effect.sync(() => {
|
||||
if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")) return specifier
|
||||
try {
|
||||
return import.meta.resolve(specifier)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
||||
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) {
|
||||
const module = yield* Effect.tryPromise({
|
||||
try: () => {
|
||||
const existing = packages.get(entrypoint)
|
||||
if (existing) return existing
|
||||
const loaded = import(entrypoint)
|
||||
packages.set(entrypoint, loaded)
|
||||
return loaded
|
||||
},
|
||||
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||
})
|
||||
if (!isProviderPackage(module)) {
|
||||
return yield* new LoadError({
|
||||
package: specifier,
|
||||
cause: new Error(`Provider package ${specifier} does not export model(modelID, settings)`),
|
||||
})
|
||||
}
|
||||
return module
|
||||
})
|
||||
|
||||
function isProviderPackage(input: unknown): input is ProviderPackageDefinition {
|
||||
return typeof input === "object" && input !== null && "model" in input && typeof input.model === "function"
|
||||
}
|
||||
|
||||
function rootPackage(specifier: string) {
|
||||
const parts = specifier.split("/")
|
||||
if (specifier.startsWith("@")) return parts.slice(0, 2).join("/")
|
||||
return parts[0]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@ import { Catalog } from "../../catalog"
|
|||
import { Credential } from "../../credential"
|
||||
import { Integration } from "../../integration"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { Npm } from "../../npm"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { ProviderPackage } from "../../provider-package"
|
||||
import { ProviderOverlay } from "../../provider-overlay"
|
||||
import { SessionSchema } from "../schema"
|
||||
|
||||
export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelectedError>()(
|
||||
|
|
@ -119,9 +118,9 @@ const withVariant = (
|
|||
return Effect.succeed(
|
||||
variant
|
||||
? produce(model, (draft) => {
|
||||
draft.settings = ProviderOverlay.merge(draft.settings, variant.settings)
|
||||
draft.headers = ProviderOverlay.headers(draft.headers, variant.headers)
|
||||
draft.body = ProviderOverlay.merge(draft.body, variant.body)
|
||||
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
|
||||
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
|
||||
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
|
||||
})
|
||||
: model,
|
||||
)
|
||||
|
|
@ -130,12 +129,15 @@ const withVariant = (
|
|||
export const fromCatalogModel = (
|
||||
model: ModelV2.Info,
|
||||
credential?: Credential.Value,
|
||||
loadPackage: (
|
||||
specifier: string,
|
||||
) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError> = ProviderV2.loadPackage,
|
||||
): Effect.Effect<Model, UnsupportedPackageError> => {
|
||||
const resolved =
|
||||
credential?.metadata === undefined
|
||||
? model
|
||||
: produce(model, (draft) => {
|
||||
draft.settings = ProviderOverlay.merge(draft.settings, credential.metadata)
|
||||
draft.settings = ProviderV2.mergeOverlay(draft.settings, credential.metadata)
|
||||
})
|
||||
const key = apiKey(resolved, credential)
|
||||
const packageName = ProviderV2.packageName(resolved.package)
|
||||
|
|
@ -167,7 +169,7 @@ export const fromCatalogModel = (
|
|||
if (!ProviderV2.isAISDK(resolved.package) && resolved.package) {
|
||||
const specifier = resolved.package
|
||||
return Effect.gen(function* () {
|
||||
const module = yield* ProviderPackage.load(specifier).pipe(
|
||||
const module = yield* loadPackage(specifier).pipe(
|
||||
Effect.mapError(
|
||||
() =>
|
||||
new UnsupportedPackageError({
|
||||
|
|
@ -187,7 +189,7 @@ export const fromCatalogModel = (
|
|||
limits: { context: resolved.limit.context, output: resolved.limit.output },
|
||||
}
|
||||
return yield* Effect.try({
|
||||
try: () => ProviderPackage.make(module, resolved.modelID ?? resolved.id, settings),
|
||||
try: () => ProviderV2.makeModel(module, resolved.modelID ?? resolved.id, settings),
|
||||
catch: () =>
|
||||
new UnsupportedPackageError({
|
||||
providerID: resolved.providerID,
|
||||
|
|
@ -206,8 +208,15 @@ export const fromCatalogModel = (
|
|||
)
|
||||
}
|
||||
|
||||
export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
|
||||
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
|
||||
export const resolve = (
|
||||
session: SessionSchema.Info,
|
||||
model: ModelV2.Info,
|
||||
credential?: Credential.Value,
|
||||
loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>,
|
||||
) =>
|
||||
withVariant(model, session.model?.variant).pipe(
|
||||
Effect.flatMap((model) => fromCatalogModel(model, credential, loadPackage)),
|
||||
)
|
||||
|
||||
export const supported = (model: ModelV2.Info) =>
|
||||
Boolean(model.package) &&
|
||||
|
|
@ -223,6 +232,7 @@ export const locationLayer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const integrations = yield* Integration.Service
|
||||
const npm = yield* Npm.Service
|
||||
return Service.of({
|
||||
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
||||
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
||||
|
|
@ -248,6 +258,7 @@ export const locationLayer = Layer.effect(
|
|||
session,
|
||||
selected,
|
||||
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
||||
(specifier) => ProviderV2.loadPackage(specifier, npm),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue