@@ -25,30 +25,59 @@ function writeModelsJsonWithPluginCatalog(params: {
2525 root : unknown ;
2626 pluginRelativePath : string ;
2727 pluginCatalog : unknown ;
28+ } ) : string {
29+ return writeModelsJsonWithPluginCatalogs ( {
30+ root : params . root ,
31+ pluginCatalogs : [
32+ {
33+ pluginRelativePath : params . pluginRelativePath ,
34+ pluginCatalog : params . pluginCatalog ,
35+ } ,
36+ ] ,
37+ } ) ;
38+ }
39+
40+ function writeModelsJsonWithPluginCatalogs ( params : {
41+ root : unknown ;
42+ pluginCatalogs : Array < {
43+ pluginRelativePath : string ;
44+ pluginCatalog : unknown ;
45+ } > ;
2846} ) : string {
2947 const dir = mkdtempSync ( join ( tmpdir ( ) , "openclaw-model-registry-" ) ) ;
3048 tempDirs . push ( dir ) ;
3149 const file = join ( dir , "models.json" ) ;
32- const pluginFile = join ( dir , params . pluginRelativePath ) ;
33- mkdirSync ( dirname ( pluginFile ) , { recursive : true } ) ;
3450 writeFileSync ( file , JSON . stringify ( params . root , null , 2 ) , "utf-8" ) ;
35- writeFileSync ( pluginFile , JSON . stringify ( params . pluginCatalog , null , 2 ) , "utf-8" ) ;
51+ for ( const pluginCatalog of params . pluginCatalogs ) {
52+ const pluginFile = join ( dir , pluginCatalog . pluginRelativePath ) ;
53+ mkdirSync ( dirname ( pluginFile ) , { recursive : true } ) ;
54+ writeFileSync ( pluginFile , JSON . stringify ( pluginCatalog . pluginCatalog , null , 2 ) , "utf-8" ) ;
55+ }
3656 return file ;
3757}
3858
3959function pluginOwnerSnapshot ( providerId : string , pluginId : string , enabled = true ) {
60+ return pluginOwnerSnapshotEntries ( [ { providerId, pluginId, enabled } ] ) ;
61+ }
62+
63+ function pluginOwnerSnapshotEntries (
64+ entries : Array < { providerId : string ; pluginId : string ; enabled ?: boolean } > ,
65+ ) {
4066 // The registry only trusts generated provider shards that are still owned by
4167 // an enabled plugin in the current metadata snapshot.
4268 return {
4369 index : {
44- plugins : [ { pluginId, enabled } ] ,
70+ plugins : entries . map ( ( entry ) => ( {
71+ pluginId : entry . pluginId ,
72+ enabled : entry . enabled ?? true ,
73+ } ) ) ,
4574 } ,
4675 normalizePluginId : ( id : string ) => id ,
4776 owners : {
4877 channels : new Map ( ) ,
4978 channelConfigs : new Map ( ) ,
50- providers : new Map ( [ [ providerId , [ pluginId ] ] ] ) ,
51- modelCatalogProviders : new Map ( [ [ providerId , [ pluginId ] ] ] ) ,
79+ providers : new Map ( entries . map ( ( entry ) => [ entry . providerId , [ entry . pluginId ] ] ) ) ,
80+ modelCatalogProviders : new Map ( entries . map ( ( entry ) => [ entry . providerId , [ entry . pluginId ] ] ) ) ,
5281 cliBackends : new Map ( ) ,
5382 setupProviders : new Map ( ) ,
5483 commandAliases : new Map ( ) ,
@@ -145,6 +174,64 @@ describe("ModelRegistry models.json auth", () => {
145174 expect ( registry . find ( "zai" , "glm-5.1" ) ?. name ) . toBe ( "GLM 5.1" ) ;
146175 } ) ;
147176
177+ it ( "isolates invalid generated plugin catalog shards from valid models" , ( ) => {
178+ const modelsPath = writeModelsJsonWithPluginCatalogs ( {
179+ root : {
180+ providers : {
181+ custom : {
182+ baseUrl : "https://models.example/v1" ,
183+ api : "openai-responses" ,
184+ apiKey : "CUSTOM_API_KEY" ,
185+ models : [ { id : "root-model" , name : "Root Model" } ] ,
186+ } ,
187+ } ,
188+ } ,
189+ pluginCatalogs : [
190+ {
191+ pluginRelativePath : join ( "plugins" , "google" , PLUGIN_MODEL_CATALOG_FILE ) ,
192+ pluginCatalog : {
193+ generatedBy : PLUGIN_MODEL_CATALOG_GENERATED_BY ,
194+ providers : {
195+ "google-vertex" : {
196+ baseUrl : "https://us-central1-aiplatform.googleapis.com/v1" ,
197+ apiKey : "GOOGLE_API_KEY" ,
198+ models : [ { id : "gemini-3.1-pro-preview" , name : "Gemini 3.1 Pro" } ] ,
199+ } ,
200+ } ,
201+ } ,
202+ } ,
203+ {
204+ pluginRelativePath : join ( "plugins" , "zai" , PLUGIN_MODEL_CATALOG_FILE ) ,
205+ pluginCatalog : {
206+ generatedBy : PLUGIN_MODEL_CATALOG_GENERATED_BY ,
207+ providers : {
208+ zai : {
209+ baseUrl : "https://api.z.ai/api/paas/v4" ,
210+ api : "openai-completions" ,
211+ apiKey : "ZAI_API_KEY" ,
212+ models : [ { id : "glm-5.1" , name : "GLM 5.1" } ] ,
213+ } ,
214+ } ,
215+ } ,
216+ } ,
217+ ] ,
218+ } ) ;
219+
220+ const registry = ModelRegistry . create ( AuthStorage . inMemory ( ) , modelsPath , {
221+ pluginMetadataSnapshot : pluginOwnerSnapshotEntries ( [
222+ { providerId : "google-vertex" , pluginId : "google" } ,
223+ { providerId : "zai" , pluginId : "zai" } ,
224+ ] ) ,
225+ } ) ;
226+
227+ expect ( registry . getError ( ) ) . toContain (
228+ 'Provider google-vertex, model gemini-3.1-pro-preview: no "api" specified' ,
229+ ) ;
230+ expect ( registry . find ( "custom" , "root-model" ) ?. name ) . toBe ( "Root Model" ) ;
231+ expect ( registry . find ( "zai" , "glm-5.1" ) ?. name ) . toBe ( "GLM 5.1" ) ;
232+ expect ( registry . find ( "google-vertex" , "gemini-3.1-pro-preview" ) ) . toBeUndefined ( ) ;
233+ } ) ;
234+
148235 it ( "preserves model params from generated plugin catalog shards" , ( ) => {
149236 const modelsPath = writeModelsJsonWithPluginCatalog ( {
150237 root : { providers : { } } ,
0 commit comments