Skip to content

Commit 75b3c29

Browse files
thdxrdavidgut1982
authored andcommitted
refactor(core): derive catalog availability from integrations (anomalyco#32272)
1 parent 58e668e commit 75b3c29

29 files changed

Lines changed: 622 additions & 509 deletions

packages/core/src/catalog.ts

Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import { Location } from "./location"
1010
import { EventV2 } from "./event"
1111
import { Policy } from "./policy"
1212
import { State } from "./state"
13-
import { Credential } from "./credential"
14-
import { IntegrationSchema } from "./integration/schema"
13+
import { Integration } from "./integration"
1514

1615
export type ProviderRecord = {
1716
provider: ProviderV2.Info
@@ -35,12 +34,7 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
3534
export const PolicyActions = Schema.Literals(["provider.use"])
3635

3736
export const Event = {
38-
ModelUpdated: EventV2.define({
39-
type: "catalog.model.updated",
40-
schema: {
41-
model: ModelV2.Info,
42-
},
43-
}),
37+
Updated: EventV2.define({ type: "catalog.updated", schema: {} }),
4438
}
4539

4640
type Data = {
@@ -96,26 +90,21 @@ export const layer = Layer.effect(
9690
const plugin = yield* PluginV2.Service
9791
const events = yield* EventV2.Service
9892
const policy = yield* Policy.Service
99-
const credentials = yield* Credential.Service
93+
const integrations = yield* Integration.Service
10094
const scope = yield* Scope.Scope
10195

102-
const project = (provider: ProviderV2.Info, active: Map<IntegrationSchema.ID, Credential.Stored>) => {
103-
const credential = active.get(IntegrationSchema.ID.make(provider.id))
104-
if (!credential) return provider
105-
const body = { ...provider.request.body }
106-
if (credential.value.type === "key") {
107-
body.apiKey = credential.value.key
108-
Object.assign(body, credential.value.metadata ?? {})
109-
}
110-
if (credential.value.type === "oauth") body.apiKey = credential.value.access
111-
return new ProviderV2.Info({
112-
...provider,
113-
enabled: { via: "credential", credentialID: credential.id },
114-
request: { ...provider.request, body },
115-
})
96+
const available = (
97+
provider: ProviderV2.Info,
98+
integration: Integration.Info | undefined,
99+
connected: boolean,
100+
) => {
101+
if (provider.disabled) return false
102+
if (typeof provider.request.body.apiKey === "string") return true
103+
if (connected) return true
104+
return !integration
116105
}
117106

118-
const resolve = (model: ModelV2.Info, provider: ProviderV2.Info) => {
107+
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
119108
const api =
120109
model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0
121110
? { ...provider.api, id: model.api.id }
@@ -203,18 +192,16 @@ export const layer = Layer.effect(
203192
},
204193
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) {
205194
if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid)
206-
if (!policy.hasStatements()) return
207-
for (const record of [...catalog.provider.list()]) {
208-
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
209-
catalog.provider.remove(record.provider.id)
195+
if (policy.hasStatements()) {
196+
for (const record of [...catalog.provider.list()]) {
197+
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
198+
catalog.provider.remove(record.provider.id)
199+
}
210200
}
211201
}
202+
yield* events.publish(Event.Updated, {})
212203
}),
213204
})
214-
const active = Effect.fn("CatalogV2.active")(function* () {
215-
return new Map((yield* credentials.all()).map((credential) => [credential.integrationID, credential]))
216-
})
217-
218205
yield* events.subscribe(PluginV2.Event.Added).pipe(
219206
// Plugin registries are location scoped even though the event bus is process scoped.
220207
Stream.filter(
@@ -233,18 +220,23 @@ export const layer = Layer.effect(
233220
provider: {
234221
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
235222
const record = yield* getRecord(providerID)
236-
return project(record.provider, yield* active())
223+
return record.provider
237224
}),
238225

239226
all: Effect.fn("CatalogV2.provider.all")(function* () {
240-
const credentials = yield* active()
241-
return Array.fromIterable(state.get().providers.values()).map((record) =>
242-
project(record.provider, credentials),
243-
)
227+
return Array.fromIterable(state.get().providers.values()).map((record) => record.provider)
244228
}),
245229

246230
available: Effect.fn("CatalogV2.provider.available")(function* () {
247-
return (yield* result.provider.all()).filter((provider) => provider.enabled)
231+
const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration]))
232+
const connections = yield* integrations.connection.list()
233+
return (yield* result.provider.all()).filter((provider) =>
234+
available(
235+
provider,
236+
active.get(Integration.ID.make(provider.id)),
237+
connections.has(Integration.ID.make(provider.id)),
238+
),
239+
)
248240
}),
249241
},
250242

@@ -253,33 +245,32 @@ export const layer = Layer.effect(
253245
const record = yield* getRecord(providerID)
254246
const model = record.models.get(modelID)
255247
if (!model) return yield* new ModelNotFoundError({ providerID, modelID })
256-
return resolve(model, project(record.provider, yield* active()))
248+
return projectModel(model, record.provider)
257249
}),
258250

259251
all: Effect.fn("CatalogV2.model.all")(function* () {
260-
const credentials = yield* active()
261252
return pipe(
262253
Array.fromIterable(state.get().providers.values()),
263254
Array.flatMap((record) => {
264-
const provider = project(record.provider, credentials)
265-
return Array.fromIterable(record.models.values()).map((model) => resolve(model, provider))
255+
return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider))
266256
}),
267257
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
268258
)
269259
}),
270260

271261
available: Effect.fn("CatalogV2.model.available")(function* () {
272-
const providers = new Map((yield* result.provider.all()).map((provider) => [provider.id, provider]))
273-
return (yield* result.model.all()).filter(
274-
(model) => providers.get(model.providerID)?.enabled !== false && model.enabled,
275-
)
262+
const providers = new Set((yield* result.provider.available()).map((provider) => provider.id))
263+
return (yield* result.model.all()).filter((model) => providers.has(model.providerID) && model.enabled)
276264
}),
277265

278266
default: Effect.fn("CatalogV2.model.default")(function* () {
279267
const defaultModel = state.get().defaultModel
280268
if (defaultModel) {
281269
const provider = yield* result.provider.get(defaultModel.providerID).pipe(Effect.option)
282-
if (Option.isSome(provider) && provider.value.enabled !== false) {
270+
if (
271+
Option.isSome(provider) &&
272+
(yield* result.provider.available()).some((item) => item.id === provider.value.id)
273+
) {
283274
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
284275
if (Option.isSome(model) && model.value.enabled) return model
285276
}
@@ -295,11 +286,11 @@ export const layer = Layer.effect(
295286
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
296287
const record = state.get().providers.get(providerID)
297288
if (!record) return Option.none<ModelV2.Info>()
298-
const provider = project(record.provider, yield* active())
289+
const provider = record.provider
299290

300291
if (providerID === ProviderV2.ID.opencode) {
301292
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
302-
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(resolve(gpt5Nano, provider))
293+
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(projectModel(gpt5Nano, provider))
303294
}
304295

305296
const candidates = pipe(
@@ -327,7 +318,7 @@ export const layer = Layer.effect(
327318
return pipe(
328319
items,
329320
Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number),
330-
Array.map((item) => resolve(item.model, provider)),
321+
Array.map((item) => projectModel(item.model, provider)),
331322
Array.head,
332323
)
333324
}
@@ -348,6 +339,7 @@ export const layer = Layer.effect(
348339
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
349340

350341
export const locationLayer = layer.pipe(
342+
Layer.provideMerge(Integration.locationLayer),
351343
Layer.provideMerge(PluginV2.locationLayer),
352344
Layer.provideMerge(Policy.locationLayer),
353345
)

packages/core/src/config/plugin/provider.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * as ConfigProviderPlugin from "./provider"
33
import { Effect } from "effect"
44
import { Catalog } from "../../catalog"
55
import { Config } from "../../config"
6+
import { Integration } from "../../integration"
67
import { ModelV2 } from "../../model"
78
import { ModelRequest } from "../../model-request"
89
import { PluginV2 } from "../../plugin"
@@ -13,9 +14,33 @@ export const Plugin = PluginV2.define({
1314
effect: Effect.gen(function* () {
1415
const catalog = yield* Catalog.Service
1516
const config = yield* Config.Service
17+
const integrations = yield* Integration.Service
1618
const transform = yield* catalog.transform()
19+
const integrationTransform = yield* integrations.transform()
1720
const entries = yield* config.entries()
1821
const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
22+
const configuredIntegrations = new Set(
23+
files.flatMap((file) =>
24+
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => (provider.env === undefined ? [] : [id])),
25+
),
26+
)
27+
yield* integrationTransform((integrations) => {
28+
for (const file of files) {
29+
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
30+
const integrationID = Integration.ID.make(id)
31+
if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
32+
integrations.update(integrationID, (integration) => {
33+
integration.name = item.name ?? integration.name
34+
})
35+
if (item.env !== undefined) {
36+
integrations.method.update({
37+
integrationID,
38+
method: { type: "env", names: [...item.env] },
39+
})
40+
}
41+
}
42+
}
43+
})
1944

2045
yield* transform((catalog) => {
2146
const configuredDefault = Config.latest(entries, "model")
@@ -28,8 +53,6 @@ export const Plugin = PluginV2.define({
2853
const providerID = ProviderV2.ID.make(id)
2954
catalog.provider.update(providerID, (provider) => {
3055
if (item.name !== undefined) provider.name = item.name
31-
if (item.env !== undefined) provider.env = [...item.env]
32-
provider.enabled = { via: "custom", data: {} }
3356
if (item.api !== undefined) provider.api = { ...item.api }
3457
if (item.request !== undefined) {
3558
Object.assign(provider.request.headers, item.request.headers)

packages/core/src/credential.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export interface Interface {
4646
readonly all: () => Effect.Effect<Stored[]>
4747
/** Returns stored credentials belonging to one integration. */
4848
readonly list: (integrationID: IntegrationSchema.ID) => Effect.Effect<Stored[]>
49+
/** Returns one stored credential by ID. */
50+
readonly get: (id: ID) => Effect.Effect<Stored | undefined>
4951
/** Replaces any credential for an integration and returns the new record. */
5052
readonly create: (input: {
5153
readonly integrationID: IntegrationSchema.ID
@@ -99,6 +101,10 @@ export const layer = Layer.effect(
99101
return credential ? [credential] : []
100102
})
101103
}),
104+
get: Effect.fn("Credential.get")(function* (id) {
105+
const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie)
106+
return row ? stored(row) : undefined
107+
}),
102108
create: Effect.fn("Credential.create")(function* (input) {
103109
const credential = new Stored({
104110
id: ID.create(),

0 commit comments

Comments
 (0)