feat(core): add location-scoped config loading (#29625)
This commit is contained in:
parent
5fb85a6aa3
commit
9583e08be4
89 changed files with 3509 additions and 527 deletions
|
|
@ -1,18 +1,22 @@
|
|||
export * as Catalog from "./catalog"
|
||||
|
||||
import { Context, Effect, HashMap, Layer, Option, Order, pipe, Schema, Array, Scope, Stream } from "effect"
|
||||
import { produce, type Draft } from "immer"
|
||||
import { Context, Effect, Layer, Option, Order, pipe, Schema, Array, Scope, Stream } from "effect"
|
||||
import { castDraft, enableMapSet, type Draft } from "immer"
|
||||
import { ModelV2 } from "./model"
|
||||
import { PluginV2 } from "./plugin"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { Location } from "./location"
|
||||
import { EventV2 } from "./event"
|
||||
import { Policy } from "./policy"
|
||||
import { State } from "./state"
|
||||
|
||||
export type ProviderRecord = {
|
||||
provider: ProviderV2.Info
|
||||
models: Map<ModelV2.ID, ModelV2.Info>
|
||||
}
|
||||
|
||||
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
|
||||
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
|
||||
"CatalogV2.ProviderNotFound",
|
||||
{
|
||||
|
|
@ -25,6 +29,8 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
|
|||
modelID: ModelV2.ID,
|
||||
}) {}
|
||||
|
||||
export const PolicyActions = Schema.Literals(["provider.use"])
|
||||
|
||||
export const Event = {
|
||||
ModelUpdated: EventV2.define({
|
||||
type: "catalog.model.updated",
|
||||
|
|
@ -34,24 +40,31 @@ export const Event = {
|
|||
}),
|
||||
}
|
||||
|
||||
export type Context = {
|
||||
data: readonly ProviderRecord[]
|
||||
updateProvider: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => void
|
||||
updateModel: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft<ModelV2.Info>) => void) => void
|
||||
type Data = {
|
||||
providers: Map<ProviderV2.ID, ProviderRecord>
|
||||
defaultModel?: DefaultModel
|
||||
}
|
||||
|
||||
export type Editor = {
|
||||
provider: {
|
||||
list: () => readonly ProviderRecord[]
|
||||
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
|
||||
update: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => void
|
||||
remove: (providerID: ProviderV2.ID) => void
|
||||
}
|
||||
model: {
|
||||
get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
|
||||
update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft<ModelV2.Info>) => void) => void
|
||||
remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
||||
default: {
|
||||
get: () => DefaultModel | undefined
|
||||
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Loader = (update: (ctx: Context) => void) => Effect.Effect<void>
|
||||
|
||||
export interface Interface {
|
||||
readonly loader: () => Effect.Effect<Loader, never, Scope.Scope>
|
||||
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||
readonly provider: {
|
||||
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
|
||||
readonly all: () => Effect.Effect<ProviderV2.Info[]>
|
||||
|
|
@ -65,29 +78,25 @@ export interface Interface {
|
|||
readonly all: () => Effect.Effect<ModelV2.Info[]>
|
||||
readonly available: () => Effect.Effect<ModelV2.Info[]>
|
||||
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||
readonly setDefault: (
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
) => Effect.Effect<void, ProviderNotFoundError | ModelNotFoundError>
|
||||
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||
}
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
|
||||
|
||||
enableMapSet()
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
yield* Location.Service
|
||||
let records = HashMap.empty<ProviderV2.ID, ProviderRecord>()
|
||||
let loaders: { update: (ctx: Context) => void }[] = []
|
||||
let defaultModel: { providerID: ProviderV2.ID; modelID: ModelV2.ID } | undefined
|
||||
const plugin = yield* PluginV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const policy = yield* Policy.Service
|
||||
const scope = yield* Scope.Scope
|
||||
|
||||
const resolve = (model: ModelV2.Info) => {
|
||||
const provider = Option.getOrThrow(HashMap.get(records, model.providerID)).provider
|
||||
const provider = state.get().providers.get(model.providerID)!.provider
|
||||
const endpoint =
|
||||
model.endpoint.type === "unknown"
|
||||
? provider.endpoint
|
||||
|
|
@ -120,9 +129,9 @@ export const layer = Layer.effect(
|
|||
}
|
||||
|
||||
function* getRecord(providerID: ProviderV2.ID) {
|
||||
const match = HashMap.get(records, providerID)
|
||||
if (!match.valueOrUndefined) return yield* new ProviderNotFoundError({ providerID })
|
||||
return match.value
|
||||
const match = state.get().providers.get(providerID)
|
||||
if (!match) return yield* new ProviderNotFoundError({ providerID })
|
||||
return match
|
||||
}
|
||||
|
||||
const normalizeEndpoint = (item: Draft<ProviderV2.Info> | Draft<ModelV2.Info>) => {
|
||||
|
|
@ -131,114 +140,73 @@ export const layer = Layer.effect(
|
|||
delete item.options.aisdk.provider.baseURL
|
||||
}
|
||||
|
||||
const clone = (input: HashMap.HashMap<ProviderV2.ID, ProviderRecord>) =>
|
||||
HashMap.fromIterable(
|
||||
HashMap.toEntries(input).map(([key, value]) => [key, { ...value, models: new Map(value.models) }] as const),
|
||||
)
|
||||
|
||||
const context = (draft: {
|
||||
records: HashMap.HashMap<ProviderV2.ID, ProviderRecord>
|
||||
data: ProviderRecord[]
|
||||
}): Context => {
|
||||
const result: Context = {
|
||||
data: draft.data,
|
||||
updateProvider: (providerID, fn) => result.provider.update(providerID, fn),
|
||||
updateModel: (providerID, modelID, fn) => result.model.update(providerID, modelID, fn),
|
||||
provider: {
|
||||
update: (providerID, fn) => {
|
||||
const current = Option.getOrUndefined(HashMap.get(draft.records, providerID))
|
||||
const provider = produce(current?.provider ?? ProviderV2.Info.empty(providerID), (draft) => {
|
||||
fn(draft)
|
||||
normalizeEndpoint(draft)
|
||||
})
|
||||
const next = {
|
||||
provider,
|
||||
models: current?.models ?? new Map<ModelV2.ID, ModelV2.Info>(),
|
||||
}
|
||||
draft.records = HashMap.set(draft.records, providerID, next)
|
||||
const index = draft.data.findIndex((item) => item.provider.id === providerID)
|
||||
if (index === -1) draft.data.push(next)
|
||||
else draft.data[index] = next
|
||||
const state = State.create<Data, Editor>({
|
||||
initial: () => ({ providers: new Map() }),
|
||||
editor: (draft) => {
|
||||
const result: Editor = {
|
||||
provider: {
|
||||
list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[],
|
||||
get: (providerID) => draft.providers.get(providerID),
|
||||
update: (providerID, fn) => {
|
||||
let current = draft.providers.get(providerID)
|
||||
if (!current) {
|
||||
current = castDraft({
|
||||
provider: ProviderV2.Info.empty(providerID),
|
||||
models: new Map<ModelV2.ID, ModelV2.Info>(),
|
||||
})
|
||||
draft.providers.set(providerID, current)
|
||||
}
|
||||
fn(current.provider)
|
||||
normalizeEndpoint(current.provider)
|
||||
},
|
||||
remove: (providerID) => {
|
||||
draft.providers.delete(providerID)
|
||||
},
|
||||
},
|
||||
remove: (providerID) => {
|
||||
draft.records = HashMap.remove(draft.records, providerID)
|
||||
const index = draft.data.findIndex((item) => item.provider.id === providerID)
|
||||
if (index !== -1) draft.data.splice(index, 1)
|
||||
model: {
|
||||
get: (providerID, modelID) => draft.providers.get(providerID)?.models.get(modelID),
|
||||
update: (providerID, modelID, fn) => {
|
||||
result.provider.update(providerID, () => {})
|
||||
const record = draft.providers.get(providerID)!
|
||||
const model = record.models.get(modelID) ?? castDraft(ModelV2.Info.empty(providerID, modelID))
|
||||
if (!record.models.has(modelID)) record.models.set(modelID, model)
|
||||
fn(model)
|
||||
model.id = modelID
|
||||
model.providerID = providerID
|
||||
normalizeEndpoint(model)
|
||||
},
|
||||
remove: (providerID, modelID) => {
|
||||
draft.providers.get(providerID)?.models.delete(modelID)
|
||||
},
|
||||
default: {
|
||||
get: () => draft.defaultModel,
|
||||
set: (providerID, modelID) => {
|
||||
draft.defaultModel = { providerID, modelID }
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
model: {
|
||||
update: (providerID, modelID, fn) => {
|
||||
const current = Option.getOrThrow(HashMap.get(draft.records, providerID))
|
||||
const model = produce(current.models.get(modelID) ?? ModelV2.Info.empty(providerID, modelID), (draft) => {
|
||||
fn(draft)
|
||||
normalizeEndpoint(draft)
|
||||
})
|
||||
const next = {
|
||||
provider: current.provider,
|
||||
models: new Map(current.models).set(modelID, new ModelV2.Info({ ...model, id: modelID, providerID })),
|
||||
}
|
||||
draft.records = HashMap.set(draft.records, providerID, next)
|
||||
const index = draft.data.findIndex((item) => item.provider.id === providerID)
|
||||
if (index === -1) draft.data.push(next)
|
||||
else draft.data[index] = next
|
||||
},
|
||||
remove: (providerID, modelID) => {
|
||||
const current = Option.getOrUndefined(HashMap.get(draft.records, providerID))
|
||||
if (!current) return
|
||||
const next = {
|
||||
provider: current.provider,
|
||||
models: new Map(current.models),
|
||||
}
|
||||
next.models.delete(modelID)
|
||||
draft.records = HashMap.set(draft.records, providerID, next)
|
||||
const index = draft.data.findIndex((item) => item.provider.id === providerID)
|
||||
if (index !== -1) draft.data[index] = next
|
||||
},
|
||||
},
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const transform = Effect.fn("CatalogV2.transform")(function* () {
|
||||
const draft = { records: clone(records), data: HashMap.toValues(records) }
|
||||
yield* plugin.trigger("catalog.transform", context(draft), {})
|
||||
records = draft.records
|
||||
}
|
||||
return result
|
||||
},
|
||||
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) {
|
||||
if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid)
|
||||
for (const record of [...catalog.provider.list()]) {
|
||||
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
|
||||
catalog.provider.remove(record.provider.id)
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
const rebuild = Effect.fn("CatalogV2.rebuild")(function* () {
|
||||
const draft = { records: HashMap.empty<ProviderV2.ID, ProviderRecord>(), data: [] as ProviderRecord[] }
|
||||
for (const loader of loaders) loader.update(context(draft))
|
||||
yield* plugin.trigger("catalog.transform", context(draft), {})
|
||||
records = draft.records
|
||||
})
|
||||
|
||||
yield* plugin.added().pipe(
|
||||
Stream.runForEach((id) =>
|
||||
Effect.gen(function* () {
|
||||
const draft = { records: clone(records), data: HashMap.toValues(records) }
|
||||
yield* plugin.triggerFor(id, "catalog.transform", context(draft), {})
|
||||
records = draft.records
|
||||
}),
|
||||
yield* events.subscribe(PluginV2.Event.Added).pipe(
|
||||
Stream.runForEach((event) =>
|
||||
state.update((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"),
|
||||
),
|
||||
Effect.forkIn(scope, { startImmediately: true }),
|
||||
)
|
||||
|
||||
const result: Interface = {
|
||||
loader: Effect.fn("CatalogV2.loader")(function* () {
|
||||
const loader = { update: (_ctx: Context) => {} }
|
||||
loaders = [...loaders, loader]
|
||||
const scope = yield* Scope.Scope
|
||||
yield* Scope.addFinalizer(
|
||||
scope,
|
||||
Effect.sync(() => {
|
||||
loaders = loaders.filter((item) => item !== loader)
|
||||
}).pipe(Effect.andThen(rebuild())),
|
||||
)
|
||||
return Effect.fnUntraced(function* (update) {
|
||||
loader.update = update
|
||||
yield* rebuild()
|
||||
})
|
||||
}),
|
||||
transform: state.transform,
|
||||
|
||||
provider: {
|
||||
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
||||
|
|
@ -247,11 +215,11 @@ export const layer = Layer.effect(
|
|||
}),
|
||||
|
||||
all: Effect.fn("CatalogV2.provider.all")(function* () {
|
||||
return globalThis.Array.from(HashMap.values(records)).map((record) => record.provider)
|
||||
return Array.fromIterable(state.get().providers.values()).map((record) => record.provider)
|
||||
}),
|
||||
|
||||
available: Effect.fn("CatalogV2.provider.available")(function* () {
|
||||
return globalThis.Array.from(HashMap.values(records))
|
||||
return Array.fromIterable(state.get().providers.values())
|
||||
.map((record) => record.provider)
|
||||
.filter((provider) => provider.enabled)
|
||||
}),
|
||||
|
|
@ -267,9 +235,8 @@ export const layer = Layer.effect(
|
|||
|
||||
all: Effect.fn("CatalogV2.model.all")(function* () {
|
||||
return pipe(
|
||||
records,
|
||||
HashMap.toValues,
|
||||
Array.flatMap((record) => globalThis.Array.from(record.models.values())),
|
||||
Array.fromIterable(state.get().providers.values()),
|
||||
Array.flatMap((record) => Array.fromIterable(record.models.values())),
|
||||
Array.map(resolve),
|
||||
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
|
||||
)
|
||||
|
|
@ -277,12 +244,13 @@ export const layer = Layer.effect(
|
|||
|
||||
available: Effect.fn("CatalogV2.model.available")(function* () {
|
||||
return (yield* result.model.all()).filter((model) => {
|
||||
const record = Option.getOrUndefined(HashMap.get(records, model.providerID))
|
||||
const record = state.get().providers.get(model.providerID)
|
||||
return record?.provider.enabled !== false && model.enabled
|
||||
})
|
||||
}),
|
||||
|
||||
default: Effect.fn("CatalogV2.model.default")(function* () {
|
||||
const defaultModel = state.get().defaultModel
|
||||
if (defaultModel) {
|
||||
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
|
||||
if (Option.isSome(model) && model.value.enabled) return model
|
||||
|
|
@ -295,13 +263,8 @@ export const layer = Layer.effect(
|
|||
)
|
||||
}),
|
||||
|
||||
setDefault: Effect.fn("CatalogV2.model.setDefault")(function* (providerID, modelID) {
|
||||
yield* result.model.get(providerID, modelID)
|
||||
defaultModel = { providerID, modelID }
|
||||
}),
|
||||
|
||||
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
|
||||
const record = Option.getOrUndefined(HashMap.get(records, providerID))
|
||||
const record = state.get().providers.get(providerID)
|
||||
if (!record) return Option.none<ModelV2.Info>()
|
||||
|
||||
if (providerID === ProviderV2.ID.opencode) {
|
||||
|
|
@ -310,7 +273,7 @@ export const layer = Layer.effect(
|
|||
}
|
||||
|
||||
const candidates = pipe(
|
||||
globalThis.Array.from(record.models.values()),
|
||||
Array.fromIterable(record.models.values()),
|
||||
Array.filter(
|
||||
(model) =>
|
||||
model.providerID === providerID &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue