feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
import { z } from "zod"
import {
ProviderNameSchema ,
type ServerModelsConfig ,
} from "@/lib/server-model-config"
2026-06-11 18:08:02 +09:00
import {
FIXED_CRED_PROVIDERS ,
PROVIDER_INFO ,
type ProviderName ,
} from "@/lib/types/model-config"
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
import { type MaskedSecret , maskSecret } from "./auth"
import { loadSettings } from "./settings"
// Admin-configured providers, mirroring the user ModelConfigDialog's data
2026-06-11 13:44:38 +09:00
// model but stored server-side (settings.json, ADMIN_PROVIDERS key).
//
// They COEXIST with an env-based AI_MODELS_CONFIG / ai-models.json:
// loadRawServerModelsConfig() merges the env baseline with the panel's
// providers at read time, so .env stays authoritative for its own entries.
// Panel credentials are written to ADMIN_-prefixed env vars (wired up via
// apiKeyEnv/baseUrlEnv) so they never shadow standard vars like
// OPENAI_API_KEY that env-based entries may rely on.
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
export const ADMIN_PROVIDERS_KEY = "ADMIN_PROVIDERS"
// A secret field in transit: plaintext string (new value) or an
// {isSet} marker meaning "keep the stored value".
const SecretInputSchema = z
. union ( [ z . string ( ) , z . object ( { isSet : z.literal ( true ) , hint : z.string ( ) } ) ] )
. optional ( )
export const AdminProviderSchema = z . object ( {
id : z.string ( ) . min ( 1 ) ,
provider : ProviderNameSchema ,
name : z.string ( ) . optional ( ) ,
apiKey : SecretInputSchema ,
baseUrl : z.string ( ) . optional ( ) ,
awsAccessKeyId : SecretInputSchema ,
awsSecretAccessKey : SecretInputSchema ,
awsRegion : z.string ( ) . optional ( ) ,
vertexApiKey : SecretInputSchema ,
models : z.array ( z . string ( ) . min ( 1 ) ) ,
isDefault : z.boolean ( ) . optional ( ) ,
} )
export const AdminProvidersSchema = z . array ( AdminProviderSchema )
2026-06-15 00:16:01 +09:00
// Stored shape: secrets are plain strings (never {isSet} markers, which
// only exist in transit). Used to validate ADMIN_PROVIDERS on load so a
// hand-edited/corrupted value can't slip a marker object past maskSecret.
const StoredAdminProviderSchema = AdminProviderSchema . extend ( {
apiKey : z.string ( ) . optional ( ) ,
awsAccessKeyId : z.string ( ) . optional ( ) ,
awsSecretAccessKey : z.string ( ) . optional ( ) ,
vertexApiKey : z.string ( ) . optional ( ) ,
} )
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
export type AdminProviderInput = z . infer < typeof AdminProviderSchema >
// Stored form: secrets are plain strings
export interface StoredAdminProvider {
id : string
provider : ProviderName
name? : string
apiKey? : string
baseUrl? : string
awsAccessKeyId? : string
awsSecretAccessKey? : string
awsRegion? : string
vertexApiKey? : string
models : string [ ]
isDefault? : boolean
}
const SECRET_FIELDS = [
"apiKey" ,
"awsAccessKeyId" ,
"awsSecretAccessKey" ,
"vertexApiKey" ,
] as const
2026-06-11 13:44:38 +09:00
// ADMIN_-prefixed env var names for instance `index` (0-based) of a provider
function credEnvNames (
provider : ProviderName ,
index : number ,
) : { key? : string ; url? : string } {
if ( FIXED_CRED_PROVIDERS . includes ( provider ) || provider === "edgeone" ) {
return { }
}
const prefix =
provider === "gateway" ? "AI_GATEWAY" : provider . toUpperCase ( )
const suffix = index === 0 ? "" : ` _ ${ index + 1 } `
return {
key : ` ADMIN_ ${ prefix } _API_KEY ${ suffix } ` ,
url : ` ADMIN_ ${ prefix } _BASE_URL ${ suffix } ` ,
}
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
}
export function loadAdminProviders ( ) : StoredAdminProvider [ ] {
const raw = loadSettings ( ) [ ADMIN_PROVIDERS_KEY ]
if ( ! raw ) return [ ]
try {
const parsed = JSON . parse ( raw )
2026-06-12 11:34:11 +09:00
if ( ! Array . isArray ( parsed ) ) return [ ]
// Validate each entry's shape — a malformed/hand-edited value must
// not reach runtime code that assumes provider/models exist.
return parsed . flatMap ( ( entry ) = > {
2026-06-15 00:16:01 +09:00
const result = StoredAdminProviderSchema . safeParse ( entry )
2026-06-12 11:34:11 +09:00
return result . success ? [ result . data as StoredAdminProvider ] : [ ]
} )
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
} catch {
console . error ( "[admin-providers] Failed to parse stored providers" )
return [ ]
}
}
export type MaskedAdminProvider = Omit <
StoredAdminProvider ,
( typeof SECRET_FIELDS ) [ number ]
> & {
apiKey? : MaskedSecret
awsAccessKeyId? : MaskedSecret
awsSecretAccessKey? : MaskedSecret
vertexApiKey? : MaskedSecret
}
export function maskAdminProviders (
list : StoredAdminProvider [ ] ,
) : MaskedAdminProvider [ ] {
return list . map ( ( p ) = > {
const masked : MaskedAdminProvider = { . . . p } as MaskedAdminProvider
for ( const field of SECRET_FIELDS ) {
const value = p [ field ]
masked [ field ] = value ? maskSecret ( value ) : undefined
}
return masked
} )
}
// Resolve {isSet} markers in incoming secrets against the stored list
export function mergeSecrets (
incoming : AdminProviderInput [ ] ,
stored : StoredAdminProvider [ ] ,
) : StoredAdminProvider [ ] {
const storedById = new Map ( stored . map ( ( p ) = > [ p . id , p ] ) )
return incoming . map ( ( p ) = > {
const prev = storedById . get ( p . id )
const merged = { . . . p } as StoredAdminProvider
for ( const field of SECRET_FIELDS ) {
const value = p [ field ]
if ( typeof value === "string" ) {
merged [ field ] = value || undefined
} else if ( value ? . isSet ) {
merged [ field ] = prev ? . [ field ]
} else {
merged [ field ] = undefined
}
}
return merged
} )
}
2026-06-11 13:44:38 +09:00
function displayName ( p : StoredAdminProvider ) : string {
return p . name ? . trim ( ) || PROVIDER_INFO [ p . provider ] . label
}
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
export function validateAdminProviders (
list : StoredAdminProvider [ ] ,
2026-06-11 18:08:02 +09:00
envConfig : ServerModelsConfig | null = null ,
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
) : string | null {
2026-06-11 18:08:02 +09:00
const envProviders = envConfig ? . providers ? ? [ ]
2026-06-11 13:44:38 +09:00
for ( const single of FIXED_CRED_PROVIDERS ) {
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
if ( list . filter ( ( p ) = > p . provider === single ) . length > 1 ) {
return ` Only one ${ PROVIDER_INFO [ single ] . label } provider is supported (its credentials use fixed environment variables). `
}
2026-06-11 18:08:02 +09:00
// Its credentials are global; a panel instance would silently
// override the credentials env-configured models rely on
if (
list . some ( ( p ) = > p . provider === single ) &&
envProviders . some ( ( p ) = > p . provider === single )
) {
return ` ${ PROVIDER_INFO [ single ] . label } is already configured in AI_MODELS_CONFIG / ai-models.json and shares global credentials. Manage it via the environment configuration instead. `
}
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
}
const names = list . map ( ( p ) = > displayName ( p ) )
if ( new Set ( names ) . size !== names . length ) {
return "Provider display names must be unique."
}
2026-06-11 18:08:02 +09:00
const envNames = new Set ( envProviders . map ( ( p ) = > p . name ) )
2026-06-11 13:44:38 +09:00
const clash = names . find ( ( n ) = > envNames . has ( n ) )
if ( clash ) {
return ` " ${ clash } " is already defined in AI_MODELS_CONFIG / ai-models.json. Use a different display name. `
}
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
if ( list . filter ( ( p ) = > p . isDefault ) . length > 1 ) {
return "Only one provider can be the default."
}
return null
}
2026-06-11 13:44:38 +09:00
// The panel's contribution to the server models config, derived at read
// time and merged with the env baseline by loadRawServerModelsConfig().
export function adminProvidersToConfig (
list : StoredAdminProvider [ ] ,
) : ServerModelsConfig {
const config : ServerModelsConfig = { providers : [ ] }
const indexByProvider = new Map < ProviderName , number > ( )
for ( const p of list ) {
const index = indexByProvider . get ( p . provider ) ? ? 0
indexByProvider . set ( p . provider , index + 1 )
if ( p . models . length === 0 ) continue
const env = credEnvNames ( p . provider , index )
config . providers . push ( {
name : displayName ( p ) ,
provider : p.provider ,
models : p.models ,
. . . ( env . key && p . apiKey ? { apiKeyEnv : env.key } : { } ) ,
. . . ( env . url && p . baseUrl ? { baseUrlEnv : env.url } : { } ) ,
. . . ( p . isDefault ? { default : true } : { } ) ,
} )
}
return config
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
}
2026-06-11 13:44:38 +09:00
// Settings updates derived from the provider list: credential env vars,
// the stored list itself, and AI_PROVIDER/AI_MODEL when a default is set.
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
// Keys derived from `previous` but absent now are set to null (removed,
// falling back to the environment).
export function deriveEnvUpdates (
list : StoredAdminProvider [ ] ,
previous : StoredAdminProvider [ ] ,
) : Record < string , string | null > {
const updates : Record < string , string | null > = { }
// Clear everything the previous list owned, then overwrite below
for ( const key of derivedEnvKeys ( previous ) ) updates [ key ] = null
const indexByProvider = new Map < ProviderName , number > ( )
for ( const p of list ) {
const index = indexByProvider . get ( p . provider ) ? ? 0
indexByProvider . set ( p . provider , index + 1 )
if ( p . provider === "bedrock" ) {
if ( p . awsAccessKeyId ) updates . AWS_ACCESS_KEY_ID = p . awsAccessKeyId
if ( p . awsSecretAccessKey )
updates . AWS_SECRET_ACCESS_KEY = p . awsSecretAccessKey
if ( p . awsRegion ) updates . AWS_REGION = p . awsRegion
} else if ( p . provider === "vertexai" ) {
if ( p . vertexApiKey ) updates . GOOGLE_VERTEX_API_KEY = p . vertexApiKey
if ( p . baseUrl ) updates . GOOGLE_VERTEX_BASE_URL = p . baseUrl
2026-06-11 13:44:38 +09:00
} else if ( p . provider === "ollama" ) {
if ( p . apiKey ) updates . OLLAMA_API_KEY = p . apiKey
if ( p . baseUrl ) updates . OLLAMA_BASE_URL = p . baseUrl
} else {
const env = credEnvNames ( p . provider , index )
if ( env . key && p . apiKey ) updates [ env . key ] = p . apiKey
if ( env . url && p . baseUrl ) updates [ env . url ] = p . baseUrl
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
}
}
2026-06-11 13:44:38 +09:00
updates [ ADMIN_PROVIDERS_KEY ] = list . length > 0 ? JSON . stringify ( list ) : null
// The panel's default also becomes the server-wide default model;
// without one, the env-configured default applies.
const defaultEntry = list . find ( ( p ) = > p . isDefault && p . models . length > 0 )
if ( defaultEntry ) {
updates . AI_PROVIDER = defaultEntry . provider
updates . AI_MODEL = defaultEntry . models [ 0 ]
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
}
return updates
}
2026-06-11 13:44:38 +09:00
// Every settings key the panel may have written for a given list.
// AI_MODELS_CONFIG is included to clean up values written by older
// versions of the panel (it is no longer written).
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
function derivedEnvKeys ( list : StoredAdminProvider [ ] ) : string [ ] {
const keys = new Set < string > ( [
"AI_MODELS_CONFIG" ,
"AI_PROVIDER" ,
"AI_MODEL" ,
] )
const indexByProvider = new Map < ProviderName , number > ( )
for ( const p of list ) {
const index = indexByProvider . get ( p . provider ) ? ? 0
indexByProvider . set ( p . provider , index + 1 )
if ( p . provider === "bedrock" ) {
keys . add ( "AWS_ACCESS_KEY_ID" )
keys . add ( "AWS_SECRET_ACCESS_KEY" )
keys . add ( "AWS_REGION" )
} else if ( p . provider === "vertexai" ) {
keys . add ( "GOOGLE_VERTEX_API_KEY" )
keys . add ( "GOOGLE_VERTEX_BASE_URL" )
2026-06-11 13:44:38 +09:00
} else if ( p . provider === "ollama" ) {
keys . add ( "OLLAMA_API_KEY" )
keys . add ( "OLLAMA_BASE_URL" )
} else {
const env = credEnvNames ( p . provider , index )
if ( env . key ) keys . add ( env . key )
if ( env . url ) keys . add ( env . url )
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON
textarea with a Models section mirroring the in-app model settings UI:
provider instance list with logos, credential fields per provider type,
model add/remove with suggestions, per-model connectivity test, and a
default-provider star.
On save the server derives everything the runtime needs into
settings.json: credential env vars (with _2 suffixes for multiple
instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL
for the default. Secrets round-trip as masked markers and are never
sent back to the browser. The general settings registry now only
covers non-provider settings (generation, access, features,
observability, quota).
2026-06-10 23:15:18 +09:00
}
}
return [ . . . keys ]
}