11import path from "path" ;
2+ import { mergeDeep } from "remeda" ;
23import { z } from "zod" ;
34
45import { Provider , Model } from "./schema.js" ;
@@ -8,15 +9,23 @@ const ExtendsModel = Model.sourceType()
89 . extend ( {
910 extends : z
1011 . object ( {
11- from : z . string ( ) ,
12+ from : z
13+ . string ( )
14+ . regex ( / ^ [ ^ / ] + \/ [ ^ / ] + $ / , "Must be in provider/model format" ) ,
1215 omit : z . array ( z . string ( ) ) . optional ( ) ,
1316 } )
1417 . strict ( ) ,
1518 } )
1619 . strict ( ) ;
1720
1821export async function generate ( directory : string ) {
19- const result = { } as Record < string , Provider > ;
22+ const result : Record < string , Provider > = { } ;
23+ const extendsModels : Array < {
24+ providerID : string ;
25+ modelID : string ;
26+ modelPath : string ;
27+ model : z . infer < typeof ExtendsModel > ;
28+ } > = [ ] ;
2029 for await ( const providerPath of new Bun . Glob ( "*/provider.toml" ) . scan ( {
2130 cwd : directory ,
2231 absolute : true ,
@@ -48,6 +57,20 @@ export async function generate(directory: string) {
4857 } ,
4958 } ) . then ( ( mod ) => mod . default ) ;
5059 toml . id = modelID ;
60+ if ( toml . extends !== undefined ) {
61+ const model = ExtendsModel . safeParse ( toml ) ;
62+ if ( ! model . success ) {
63+ model . error . cause = { modelPath, toml } ;
64+ throw model . error ;
65+ }
66+ extendsModels . push ( {
67+ providerID,
68+ modelID,
69+ modelPath,
70+ model : model . data ,
71+ } ) ;
72+ continue ;
73+ }
5174 const model = Model . safeParse ( toml ) ;
5275 if ( ! model . success ) {
5376 model . error . cause = { modelPath, toml } ;
@@ -58,5 +81,77 @@ export async function generate(directory: string) {
5881 result [ providerID ] = provider . data ;
5982 }
6083
84+ for ( const pendingModel of extendsModels ) {
85+ const [ providerID , modelID ] = pendingModel . model . extends . from . split ( "/" ) ;
86+ const baseModel = result [ providerID ] ?. models [ modelID ] ;
87+ if ( baseModel === undefined ) {
88+ throw new Error ( `Unable to resolve extends.from: ${ pendingModel . model . extends . from } ` , {
89+ cause : { modelPath : pendingModel . modelPath , toml : pendingModel . model } ,
90+ } ) ;
91+ }
92+
93+ const { extends : extendsConfig , ...overrides } = pendingModel . model ;
94+ const merged : Record < string , unknown > = structuredClone (
95+ mergeDeep ( baseModel , overrides ) ,
96+ ) ;
97+
98+ for ( const omit of extendsConfig . omit ?? [ ] ) {
99+ const parts = omit . split ( "." ) ;
100+ const parents : Array < {
101+ value : Record < string , unknown > ;
102+ key : string ;
103+ } > = [ ] ;
104+ let current = merged ;
105+
106+ for ( const part of parts . slice ( 0 , - 1 ) ) {
107+ const next = current [ part ] ;
108+ if (
109+ next === undefined ||
110+ next === null ||
111+ typeof next !== "object" ||
112+ Array . isArray ( next )
113+ ) {
114+ throw new Error ( `Unable to omit missing path: ${ omit } ` , {
115+ cause : { modelPath : pendingModel . modelPath , toml : pendingModel . model } ,
116+ } ) ;
117+ }
118+ parents . push ( { value : current , key : part } ) ;
119+ current = next as Record < string , unknown > ;
120+ }
121+
122+ const lastPart = parts . at ( - 1 ) ;
123+ if ( lastPart === undefined || ! ( lastPart in current ) ) {
124+ throw new Error ( `Unable to omit missing path: ${ omit } ` , {
125+ cause : { modelPath : pendingModel . modelPath , toml : pendingModel . model } ,
126+ } ) ;
127+ }
128+
129+ delete current [ lastPart ] ;
130+
131+ for ( let index = parents . length - 1 ; index >= 0 ; index -- ) {
132+ const parent = parents [ index ] ;
133+ const value = parent ?. value [ parent . key ] ;
134+ if (
135+ value === null ||
136+ value === undefined ||
137+ typeof value !== "object" ||
138+ Array . isArray ( value ) ||
139+ Object . keys ( value ) . length > 0
140+ ) {
141+ break ;
142+ }
143+ delete parent . value [ parent . key ] ;
144+ }
145+ }
146+
147+ const model = Model . safeParse ( merged ) ;
148+ if ( ! model . success ) {
149+ model . error . cause = { modelPath : pendingModel . modelPath , toml : merged } ;
150+ throw model . error ;
151+ }
152+
153+ result [ pendingModel . providerID ] ! . models [ pendingModel . modelID ] = model . data ;
154+ }
155+
61156 return result ;
62157}
0 commit comments