@@ -10,8 +10,7 @@ import { Location } from "./location"
1010import { EventV2 } from "./event"
1111import { Policy } from "./policy"
1212import { State } from "./state"
13- import { Credential } from "./credential"
14- import { IntegrationSchema } from "./integration/schema"
13+ import { Integration } from "./integration"
1514
1615export type ProviderRecord = {
1716 provider : ProviderV2 . Info
@@ -35,12 +34,7 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
3534export const PolicyActions = Schema . Literals ( [ "provider.use" ] )
3635
3736export 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
4640type 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(
348339const SMALL_MODEL_RE = / \b ( n a n o | f l a s h | l i t e | m i n i | h a i k u | s m a l l | f a s t ) \b /
349340
350341export const locationLayer = layer . pipe (
342+ Layer . provideMerge ( Integration . locationLayer ) ,
351343 Layer . provideMerge ( PluginV2 . locationLayer ) ,
352344 Layer . provideMerge ( Policy . locationLayer ) ,
353345)
0 commit comments