|
|
@@ -10,8 +10,7 @@ import { Location } from "./location"
|
|
|
import { EventV2 } from "./event"
|
|
|
import { Policy } from "./policy"
|
|
|
import { State } from "./state"
|
|
|
-import { Credential } from "./credential"
|
|
|
-import { IntegrationSchema } from "./integration/schema"
|
|
|
+import { Integration } from "./integration"
|
|
|
|
|
|
export type ProviderRecord = {
|
|
|
provider: ProviderV2.Info
|
|
|
@@ -35,12 +34,7 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
|
|
|
export const PolicyActions = Schema.Literals(["provider.use"])
|
|
|
|
|
|
export const Event = {
|
|
|
- ModelUpdated: EventV2.define({
|
|
|
- type: "catalog.model.updated",
|
|
|
- schema: {
|
|
|
- model: ModelV2.Info,
|
|
|
- },
|
|
|
- }),
|
|
|
+ Updated: EventV2.define({ type: "catalog.updated", schema: {} }),
|
|
|
}
|
|
|
|
|
|
type Data = {
|
|
|
@@ -96,26 +90,21 @@ export const layer = Layer.effect(
|
|
|
const plugin = yield* PluginV2.Service
|
|
|
const events = yield* EventV2.Service
|
|
|
const policy = yield* Policy.Service
|
|
|
- const credentials = yield* Credential.Service
|
|
|
+ const integrations = yield* Integration.Service
|
|
|
const scope = yield* Scope.Scope
|
|
|
|
|
|
- const project = (provider: ProviderV2.Info, active: Map<IntegrationSchema.ID, Credential.Stored>) => {
|
|
|
- const credential = active.get(IntegrationSchema.ID.make(provider.id))
|
|
|
- if (!credential) return provider
|
|
|
- const body = { ...provider.request.body }
|
|
|
- if (credential.value.type === "key") {
|
|
|
- body.apiKey = credential.value.key
|
|
|
- Object.assign(body, credential.value.metadata ?? {})
|
|
|
- }
|
|
|
- if (credential.value.type === "oauth") body.apiKey = credential.value.access
|
|
|
- return new ProviderV2.Info({
|
|
|
- ...provider,
|
|
|
- enabled: { via: "credential", credentialID: credential.id },
|
|
|
- request: { ...provider.request, body },
|
|
|
- })
|
|
|
+ const available = (
|
|
|
+ provider: ProviderV2.Info,
|
|
|
+ integration: Integration.Info | undefined,
|
|
|
+ connected: boolean,
|
|
|
+ ) => {
|
|
|
+ if (provider.disabled) return false
|
|
|
+ if (typeof provider.request.body.apiKey === "string") return true
|
|
|
+ if (connected) return true
|
|
|
+ return !integration
|
|
|
}
|
|
|
|
|
|
- const resolve = (model: ModelV2.Info, provider: ProviderV2.Info) => {
|
|
|
+ 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 }
|
|
|
@@ -203,18 +192,16 @@ export const layer = Layer.effect(
|
|
|
},
|
|
|
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) {
|
|
|
if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid)
|
|
|
- if (!policy.hasStatements()) return
|
|
|
- for (const record of [...catalog.provider.list()]) {
|
|
|
- if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
|
|
|
- catalog.provider.remove(record.provider.id)
|
|
|
+ if (policy.hasStatements()) {
|
|
|
+ for (const record of [...catalog.provider.list()]) {
|
|
|
+ if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
|
|
|
+ catalog.provider.remove(record.provider.id)
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ yield* events.publish(Event.Updated, {})
|
|
|
}),
|
|
|
})
|
|
|
- const active = Effect.fn("CatalogV2.active")(function* () {
|
|
|
- return new Map((yield* credentials.all()).map((credential) => [credential.integrationID, credential]))
|
|
|
- })
|
|
|
-
|
|
|
yield* events.subscribe(PluginV2.Event.Added).pipe(
|
|
|
// Plugin registries are location scoped even though the event bus is process scoped.
|
|
|
Stream.filter(
|
|
|
@@ -233,18 +220,23 @@ export const layer = Layer.effect(
|
|
|
provider: {
|
|
|
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
|
|
const record = yield* getRecord(providerID)
|
|
|
- return project(record.provider, yield* active())
|
|
|
+ return record.provider
|
|
|
}),
|
|
|
|
|
|
all: Effect.fn("CatalogV2.provider.all")(function* () {
|
|
|
- const credentials = yield* active()
|
|
|
- return Array.fromIterable(state.get().providers.values()).map((record) =>
|
|
|
- project(record.provider, credentials),
|
|
|
- )
|
|
|
+ return Array.fromIterable(state.get().providers.values()).map((record) => record.provider)
|
|
|
}),
|
|
|
|
|
|
available: Effect.fn("CatalogV2.provider.available")(function* () {
|
|
|
- return (yield* result.provider.all()).filter((provider) => provider.enabled)
|
|
|
+ const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration]))
|
|
|
+ const connections = yield* integrations.connection.list()
|
|
|
+ return (yield* result.provider.all()).filter((provider) =>
|
|
|
+ available(
|
|
|
+ provider,
|
|
|
+ active.get(Integration.ID.make(provider.id)),
|
|
|
+ connections.has(Integration.ID.make(provider.id)),
|
|
|
+ ),
|
|
|
+ )
|
|
|
}),
|
|
|
},
|
|
|
|
|
|
@@ -253,33 +245,32 @@ export const layer = Layer.effect(
|
|
|
const record = yield* getRecord(providerID)
|
|
|
const model = record.models.get(modelID)
|
|
|
if (!model) return yield* new ModelNotFoundError({ providerID, modelID })
|
|
|
- return resolve(model, project(record.provider, yield* active()))
|
|
|
+ return projectModel(model, record.provider)
|
|
|
}),
|
|
|
|
|
|
all: Effect.fn("CatalogV2.model.all")(function* () {
|
|
|
- const credentials = yield* active()
|
|
|
return pipe(
|
|
|
Array.fromIterable(state.get().providers.values()),
|
|
|
Array.flatMap((record) => {
|
|
|
- const provider = project(record.provider, credentials)
|
|
|
- return Array.fromIterable(record.models.values()).map((model) => resolve(model, provider))
|
|
|
+ return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider))
|
|
|
}),
|
|
|
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
|
|
|
)
|
|
|
}),
|
|
|
|
|
|
available: Effect.fn("CatalogV2.model.available")(function* () {
|
|
|
- const providers = new Map((yield* result.provider.all()).map((provider) => [provider.id, provider]))
|
|
|
- return (yield* result.model.all()).filter(
|
|
|
- (model) => providers.get(model.providerID)?.enabled !== false && model.enabled,
|
|
|
- )
|
|
|
+ const providers = new Set((yield* result.provider.available()).map((provider) => provider.id))
|
|
|
+ return (yield* result.model.all()).filter((model) => providers.has(model.providerID) && model.enabled)
|
|
|
}),
|
|
|
|
|
|
default: Effect.fn("CatalogV2.model.default")(function* () {
|
|
|
const defaultModel = state.get().defaultModel
|
|
|
if (defaultModel) {
|
|
|
const provider = yield* result.provider.get(defaultModel.providerID).pipe(Effect.option)
|
|
|
- if (Option.isSome(provider) && provider.value.enabled !== false) {
|
|
|
+ if (
|
|
|
+ Option.isSome(provider) &&
|
|
|
+ (yield* result.provider.available()).some((item) => item.id === provider.value.id)
|
|
|
+ ) {
|
|
|
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
|
|
|
if (Option.isSome(model) && model.value.enabled) return model
|
|
|
}
|
|
|
@@ -295,11 +286,11 @@ export const layer = Layer.effect(
|
|
|
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
|
|
|
const record = state.get().providers.get(providerID)
|
|
|
if (!record) return Option.none<ModelV2.Info>()
|
|
|
- const provider = project(record.provider, yield* active())
|
|
|
+ const provider = record.provider
|
|
|
|
|
|
if (providerID === ProviderV2.ID.opencode) {
|
|
|
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
|
|
|
- if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(resolve(gpt5Nano, provider))
|
|
|
+ if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(projectModel(gpt5Nano, provider))
|
|
|
}
|
|
|
|
|
|
const candidates = pipe(
|
|
|
@@ -327,7 +318,7 @@ export const layer = Layer.effect(
|
|
|
return pipe(
|
|
|
items,
|
|
|
Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number),
|
|
|
- Array.map((item) => resolve(item.model, provider)),
|
|
|
+ Array.map((item) => projectModel(item.model, provider)),
|
|
|
Array.head,
|
|
|
)
|
|
|
}
|
|
|
@@ -348,6 +339,7 @@ export const layer = Layer.effect(
|
|
|
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
|
|
|
|
|
|
export const locationLayer = layer.pipe(
|
|
|
+ Layer.provideMerge(Integration.locationLayer),
|
|
|
Layer.provideMerge(PluginV2.locationLayer),
|
|
|
Layer.provideMerge(Policy.locationLayer),
|
|
|
)
|