@@ -1127,6 +1127,108 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
11271127 }
11281128}
11291129
1130+ function openAICompatibleDiscoveryEnabled ( provider : NonNullable < Config . Info [ "provider" ] > [ string ] ) {
1131+ if ( provider . npm && provider . npm !== "@ai-sdk/openai-compatible" ) return false
1132+ if ( ! provider . options ?. baseURL ) return false
1133+ return provider . discoverModels ?? provider . models === undefined
1134+ }
1135+
1136+ async function discoverOpenAICompatibleModels ( input : {
1137+ providerID : ProviderID
1138+ provider : NonNullable < Config . Info [ "provider" ] > [ string ]
1139+ existing : Info | undefined
1140+ } ) : Promise < Record < string , Model > > {
1141+ const base = String ( input . provider . options ?. baseURL ?? "" ) . replace ( / \/ + $ / , "" )
1142+ if ( ! base ) return { }
1143+ const url = `${ base } /models`
1144+ const controller = new AbortController ( )
1145+ const timer = setTimeout ( ( ) => controller . abort ( ) , 2000 )
1146+ const apiKey = typeof input . provider . options ?. apiKey === "string" ? input . provider . options . apiKey : undefined
1147+ return fetch ( url , {
1148+ signal : controller . signal ,
1149+ headers : apiKey ? { Authorization : `Bearer ${ apiKey } ` } : undefined ,
1150+ } )
1151+ . then ( ( response ) => {
1152+ if ( ! response . ok ) return null
1153+ return response . json ( ) as Promise < { data ?: Array < Record < string , unknown > > } >
1154+ } )
1155+ . then ( ( body ) => {
1156+ if ( ! body ) return { }
1157+ const discovered : Record < string , Model > = { }
1158+ for ( const item of body . data ?? [ ] ) {
1159+ const rawID = item . id
1160+ if ( typeof rawID !== "string" || ! rawID . trim ( ) ) continue
1161+ const modelID = rawID . trim ( )
1162+ const existingModel = input . existing ?. models [ modelID ]
1163+ const name =
1164+ typeof item . name === "string" && item . name . trim ( ) ? item . name . trim ( ) : ( existingModel ?. name ?? modelID )
1165+ const context =
1166+ numberFrom ( item . context_length ) ?? numberFrom ( item . max_context_length ) ?? existingModel ?. limit . context ?? 0
1167+ const output = numberFrom ( item . max_output_tokens ) ?? existingModel ?. limit . output ?? 0
1168+ discovered [ modelID ] = {
1169+ id : ModelID . make ( modelID ) ,
1170+ providerID : input . providerID ,
1171+ name,
1172+ api : {
1173+ id : existingModel ?. api . id ?? modelID ,
1174+ url : input . provider . api ?? existingModel ?. api . url ?? "" ,
1175+ npm : input . provider . npm ?? existingModel ?. api . npm ?? "@ai-sdk/openai-compatible" ,
1176+ } ,
1177+ status : existingModel ?. status ?? "active" ,
1178+ headers : existingModel ?. headers ?? { } ,
1179+ options : existingModel ?. options ?? { } ,
1180+ cost : existingModel ?. cost ?? { input : 0 , output : 0 , cache : { read : 0 , write : 0 } } ,
1181+ limit : {
1182+ context,
1183+ input : existingModel ?. limit . input ,
1184+ output,
1185+ } ,
1186+ capabilities : {
1187+ temperature : existingModel ?. capabilities . temperature ?? true ,
1188+ reasoning : existingModel ?. capabilities . reasoning ?? false ,
1189+ attachment : existingModel ?. capabilities . attachment ?? false ,
1190+ toolcall : existingModel ?. capabilities . toolcall ?? true ,
1191+ input : existingModel ?. capabilities . input ?? {
1192+ text : true ,
1193+ audio : false ,
1194+ image : false ,
1195+ video : false ,
1196+ pdf : false ,
1197+ } ,
1198+ output : existingModel ?. capabilities . output ?? {
1199+ text : true ,
1200+ audio : false ,
1201+ image : false ,
1202+ video : false ,
1203+ pdf : false ,
1204+ } ,
1205+ interleaved : existingModel ?. capabilities . interleaved ?? false ,
1206+ } ,
1207+ family : existingModel ?. family ?? "" ,
1208+ release_date : existingModel ?. release_date ?? "" ,
1209+ variants : existingModel ?. variants ?? { } ,
1210+ }
1211+ }
1212+ return discovered
1213+ } )
1214+ . catch ( ( e : unknown ) => {
1215+ log . warn ( "openai-compatible model discovery failed" , { providerID : input . providerID , url, error : e } )
1216+ return { }
1217+ } )
1218+ . finally ( ( ) => {
1219+ clearTimeout ( timer )
1220+ } )
1221+ }
1222+
1223+ function numberFrom ( input : unknown ) {
1224+ if ( typeof input === "number" && Number . isFinite ( input ) && input >= 0 ) return input
1225+ if ( typeof input === "string" ) {
1226+ const parsed = Number ( input )
1227+ if ( Number . isFinite ( parsed ) && parsed >= 0 ) return parsed
1228+ }
1229+ return undefined
1230+ }
1231+
11301232function suggestionModelIDs ( provider : Info | undefined ) {
11311233 if ( ! provider ) return [ ]
11321234 return Object . keys ( provider . models ) . filter ( ( id ) => {
@@ -1423,6 +1525,29 @@ const layer = Layer.effect(
14231525 mergeProvider ( providerID , partial )
14241526 }
14251527
1528+ const toDiscover = configProviders . flatMap ( ( [ id , provider ] ) => {
1529+ const providerID = ProviderID . make ( id )
1530+ if ( ! isProviderAllowed ( providerID ) ) return [ ]
1531+ if ( ! openAICompatibleDiscoveryEnabled ( provider ) ) return [ ]
1532+ const target = providers [ providerID ]
1533+ if ( ! target ) return [ ]
1534+ return [ { providerID, provider, target } ]
1535+ } )
1536+ if ( toDiscover . length > 0 ) {
1537+ const results = yield * Effect . promise ( ( ) =>
1538+ Promise . all (
1539+ toDiscover . map ( ( { providerID, provider, target } ) =>
1540+ discoverOpenAICompatibleModels ( { providerID, provider, existing : target } ) ,
1541+ ) ,
1542+ ) ,
1543+ )
1544+ toDiscover . forEach ( ( { target } , i ) => {
1545+ for ( const [ modelID , model ] of Object . entries ( results [ i ] ) ) {
1546+ if ( ! target . models [ modelID ] ) target . models [ modelID ] = model
1547+ }
1548+ } )
1549+ }
1550+
14261551 const gitlab = ProviderID . make ( "gitlab" )
14271552 if ( discoveryLoaders [ gitlab ] && providers [ gitlab ] && isProviderAllowed ( gitlab ) ) {
14281553 yield * Effect . promise ( async ( ) => {
0 commit comments