@@ -82,16 +82,17 @@ export function resolveConfig(envOrOpts: NodeJS.ProcessEnv | ResolveConfigOption
8282 if ( creds . source === "byok" && creds . provider === "openai-compat" && creds . baseUrl ) {
8383 // Custom Chat Completions endpoint saved by the wizard (Ollama,
8484 // LM Studio, vLLM, …). Same synthesis as the OPENAI_BASE_URL env
85- // path, but persisted so it survives restarts.
85+ // path, but persisted so it survives restarts. A /model override
86+ // swaps the id against the same local endpoint + key.
8687 const compat = buildOpenAiCompatConfig ( {
8788 baseUrl : creds . baseUrl ,
88- modelId : creds . model ?? "default" ,
89+ modelId : override ?. modelId ?? creds . model ?? "default" ,
8990 apiKey : creds . accessToken ,
9091 contextWindow : creds . contextWindow ,
9192 } ) ;
9293 if ( compat ) return { ...compat , source : "byok" } ;
9394 } else if ( creds . source === "byok" && creds . provider ) {
94- const byok = buildByokConfig ( creds . provider as KnownProvider , creds . accessToken ) ;
95+ const byok = buildByokConfig ( creds . provider as KnownProvider , creds . accessToken , override ) ;
9596 if ( byok ) return byok ;
9697 } else if ( useProxy ) {
9798 const proxied = buildProxiedConfig ( env , creds . accessToken , override ) ;
@@ -109,7 +110,7 @@ export function resolveConfig(envOrOpts: NodeJS.ProcessEnv | ResolveConfigOption
109110 if ( env . OPENAI_BASE_URL && env . OPENAI_API_KEY && env . OPENAI_MODEL ) {
110111 const compat = buildOpenAiCompatConfig ( {
111112 baseUrl : env . OPENAI_BASE_URL ,
112- modelId : env . OPENAI_MODEL ,
113+ modelId : override ?. modelId ?? env . OPENAI_MODEL ,
113114 apiKey : env . OPENAI_API_KEY ,
114115 } ) ;
115116 if ( compat ) return compat ;
@@ -119,7 +120,16 @@ export function resolveConfig(envOrOpts: NodeJS.ProcessEnv | ResolveConfigOption
119120 const explicitModel = env . CODEBASE_MODEL ;
120121
121122 if ( explicitProvider && explicitModel ) {
122- const model = getModel ( explicitProvider , explicitModel as never ) ;
123+ // A /model override swaps the id at runtime; the launch env value is
124+ // the default. An id pi-ai doesn't know is id-cloned from the launch
125+ // model so any model the provider lists is switchable (only the
126+ // initial launch id, with no override, must be registry-known).
127+ const wantId = override ?. modelId ?? explicitModel ;
128+ let model = getModel ( explicitProvider , wantId as never ) as Model < string > | undefined ;
129+ if ( ! model && override ?. modelId ) {
130+ const base = getModel ( explicitProvider , explicitModel as never ) as Model < string > | undefined ;
131+ if ( base ) model = { ...base , id : wantId , name : wantId } ;
132+ }
123133 if ( ! model ) {
124134 throw new ConfigError (
125135 `CODEBASE_PROVIDER=${ explicitProvider } CODEBASE_MODEL=${ explicitModel } not in pi-ai's model registry. ` +
@@ -312,20 +322,44 @@ function buildOpenAiCompatConfig(opts: {
312322
313323/**
314324 * BYOK mode: caller has saved a provider's own API key. Use the
315- * provider's normal baseUrl from pi-ai's registry — no proxy.
325+ * provider's normal baseUrl from pi-ai's registry — no proxy. A /model
326+ * override swaps the id within the same provider; an id pi-ai doesn't
327+ * know natively is id-cloned from the provider's default so every model
328+ * the provider's /models endpoint lists is switchable.
316329 */
317- function buildByokConfig ( provider : KnownProvider , apiKey : string ) : ResolvedConfig | null {
318- const modelId = DEFAULT_MODELS [ provider ] ;
319- if ( ! modelId ) return null ;
320- const model = getModel ( provider , modelId as never ) as Model < string > | undefined ;
330+ function buildByokConfig (
331+ provider : KnownProvider ,
332+ apiKey : string ,
333+ override ?: { provider ?: string ; modelId : string } ,
334+ ) : ResolvedConfig | null {
335+ const defaultId = DEFAULT_MODELS [ provider ] ;
336+ const wantId = override ?. modelId ?? defaultId ;
337+ if ( ! wantId ) return null ;
338+ let model = getModel ( provider , wantId as never ) as Model < string > | undefined ;
339+ if ( ! model && defaultId ) {
340+ const template = getModel ( provider , defaultId as never ) as Model < string > | undefined ;
341+ if ( template ) {
342+ model = {
343+ ...template ,
344+ id : wantId ,
345+ name : wantId ,
346+ contextWindow : guessContextWindow ( wantId , template . contextWindow ) ,
347+ } ;
348+ }
349+ }
321350 if ( ! model ) return null ;
322351 return { model, apiKey, source : "byok" } ;
323352}
324353
354+ const OPTION_KEYS = new Set ( [ "env" , "credentials" , "modelOverride" ] ) ;
355+
325356function isProcessEnv ( value : NodeJS . ProcessEnv | ResolveConfigOptions ) : value is NodeJS . ProcessEnv {
326357 if ( ! value ) return true ;
327- // ResolveConfigOptions has at most env/credentials properties; ProcessEnv has many.
358+ // ResolveConfigOptions only ever holds env/credentials/modelOverride; a
359+ // real ProcessEnv has many other keys. (Missing modelOverride here was a
360+ // real bug: resolveConfig({modelOverride}) was treated as an env, so the
361+ // /model override was silently dropped and the model reverted to default.)
328362 const keys = Object . keys ( value ) ;
329363 if ( keys . length === 0 ) return true ;
330- return ! keys . every ( ( k ) => k === "env" || k === "credentials" ) ;
364+ return ! keys . every ( ( k ) => OPTION_KEYS . has ( k ) ) ;
331365}
0 commit comments