mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-02 01:20:23 +08:00
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).
This commit is contained in:
37
lib/admin/auth.ts
Normal file
37
lib/admin/auth.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { timingSafeEqual } from "crypto"
|
||||
|
||||
// Shared auth for admin API routes: compares x-admin-password header
|
||||
// against the ADMIN_PASSWORD env var. Unset password = panel disabled.
|
||||
export function checkAdminAuth(req: Request): Response | null {
|
||||
const password = process.env.ADMIN_PASSWORD
|
||||
if (!password) {
|
||||
return Response.json(
|
||||
{
|
||||
error: "Admin panel is disabled. Set the ADMIN_PASSWORD environment variable to enable it.",
|
||||
},
|
||||
{ status: 403 },
|
||||
)
|
||||
}
|
||||
const provided = req.headers.get("x-admin-password") || ""
|
||||
const a = Buffer.from(provided)
|
||||
const b = Buffer.from(password)
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return Response.json(
|
||||
{ error: "Invalid admin password" },
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export interface MaskedSecret {
|
||||
isSet: true
|
||||
hint: string
|
||||
}
|
||||
|
||||
export function maskSecret(value: string): MaskedSecret {
|
||||
return {
|
||||
isSet: true,
|
||||
hint: value.length > 8 ? `…${value.slice(-4)}` : "••••",
|
||||
}
|
||||
}
|
||||
276
lib/admin/providers.ts
Normal file
276
lib/admin/providers.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { z } from "zod"
|
||||
import {
|
||||
ProviderNameSchema,
|
||||
type ServerModelsConfig,
|
||||
} from "@/lib/server-model-config"
|
||||
import { PROVIDER_INFO, type ProviderName } from "@/lib/types/model-config"
|
||||
import { type MaskedSecret, maskSecret } from "./auth"
|
||||
import { loadSettings } from "./settings"
|
||||
|
||||
// Admin-configured providers, mirroring the user ModelConfigDialog's data
|
||||
// model but stored server-side. The list itself is stored in settings.json
|
||||
// under ADMIN_PROVIDERS; on save we derive AI_MODELS_CONFIG plus the
|
||||
// credential env vars so the existing runtime picks everything up unchanged.
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
// Default credential env vars per provider (first instance). Later
|
||||
// instances of the same provider get a _2/_3 suffix wired up via
|
||||
// apiKeyEnv/baseUrlEnv in the generated AI_MODELS_CONFIG.
|
||||
// bedrock/vertexai use fixed env vars the runtime reads directly,
|
||||
// so only one instance of each can carry server credentials.
|
||||
const CRED_ENV: Partial<Record<ProviderName, { key: string; url: string }>> = {
|
||||
openai: { key: "OPENAI_API_KEY", url: "OPENAI_BASE_URL" },
|
||||
anthropic: { key: "ANTHROPIC_API_KEY", url: "ANTHROPIC_BASE_URL" },
|
||||
google: { key: "GOOGLE_GENERATIVE_AI_API_KEY", url: "GOOGLE_BASE_URL" },
|
||||
azure: { key: "AZURE_API_KEY", url: "AZURE_BASE_URL" },
|
||||
ollama: { key: "OLLAMA_API_KEY", url: "OLLAMA_BASE_URL" },
|
||||
openrouter: { key: "OPENROUTER_API_KEY", url: "OPENROUTER_BASE_URL" },
|
||||
deepseek: { key: "DEEPSEEK_API_KEY", url: "DEEPSEEK_BASE_URL" },
|
||||
siliconflow: { key: "SILICONFLOW_API_KEY", url: "SILICONFLOW_BASE_URL" },
|
||||
sglang: { key: "SGLANG_API_KEY", url: "SGLANG_BASE_URL" },
|
||||
gateway: { key: "AI_GATEWAY_API_KEY", url: "AI_GATEWAY_BASE_URL" },
|
||||
doubao: { key: "DOUBAO_API_KEY", url: "DOUBAO_BASE_URL" },
|
||||
modelscope: { key: "MODELSCOPE_API_KEY", url: "MODELSCOPE_BASE_URL" },
|
||||
glm: { key: "GLM_API_KEY", url: "GLM_BASE_URL" },
|
||||
qwen: { key: "QWEN_API_KEY", url: "QWEN_BASE_URL" },
|
||||
kimi: { key: "KIMI_API_KEY", url: "KIMI_BASE_URL" },
|
||||
qiniu: { key: "QINIU_API_KEY", url: "QINIU_BASE_URL" },
|
||||
minimax: { key: "MINIMAX_API_KEY", url: "MINIMAX_BASE_URL" },
|
||||
novita: { key: "NOVITA_API_KEY", url: "NOVITA_BASE_URL" },
|
||||
}
|
||||
|
||||
export function loadAdminProviders(): StoredAdminProvider[] {
|
||||
const raw = loadSettings()[ADMIN_PROVIDERS_KEY]
|
||||
if (!raw) return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} 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
|
||||
})
|
||||
}
|
||||
|
||||
export function validateAdminProviders(
|
||||
list: StoredAdminProvider[],
|
||||
): string | null {
|
||||
for (const single of ["bedrock", "vertexai"] as const) {
|
||||
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).`
|
||||
}
|
||||
}
|
||||
const names = list.map((p) => displayName(p))
|
||||
if (new Set(names).size !== names.length) {
|
||||
return "Provider display names must be unique."
|
||||
}
|
||||
if (list.filter((p) => p.isDefault).length > 1) {
|
||||
return "Only one provider can be the default."
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function displayName(p: StoredAdminProvider): string {
|
||||
return p.name?.trim() || PROVIDER_INFO[p.provider].label
|
||||
}
|
||||
|
||||
// Env var names for instance `index` (0-based) of a provider
|
||||
function credEnvNames(
|
||||
provider: ProviderName,
|
||||
index: number,
|
||||
): { key?: string; url?: string } {
|
||||
const base = CRED_ENV[provider]
|
||||
if (!base) return {}
|
||||
if (index === 0) return base
|
||||
return { key: `${base.key}_${index + 1}`, url: `${base.url}_${index + 1}` }
|
||||
}
|
||||
|
||||
// All env updates derived from the provider list: credential vars,
|
||||
// AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL for the default model.
|
||||
// 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 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.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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (p.models.length > 0) {
|
||||
config.providers.push({
|
||||
name: displayName(p),
|
||||
provider: p.provider,
|
||||
models: p.models,
|
||||
// First instances use the default env vars the runtime
|
||||
// already falls back to; only extras need explicit wiring
|
||||
...(index > 0 && env.key && p.apiKey
|
||||
? { apiKeyEnv: env.key }
|
||||
: {}),
|
||||
...(index > 0 && env.url && p.baseUrl
|
||||
? { baseUrlEnv: env.url }
|
||||
: {}),
|
||||
...(p.isDefault ? { default: true } : {}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (config.providers.length > 0) {
|
||||
updates[ADMIN_PROVIDERS_KEY] = JSON.stringify(list)
|
||||
updates.AI_MODELS_CONFIG = JSON.stringify(config)
|
||||
const defaultEntry =
|
||||
list.find((p) => p.isDefault && p.models.length > 0) ??
|
||||
list.find((p) => p.models.length > 0)
|
||||
if (defaultEntry) {
|
||||
updates.AI_PROVIDER = defaultEntry.provider
|
||||
updates.AI_MODEL = defaultEntry.models[0]
|
||||
}
|
||||
} else if (list.length > 0) {
|
||||
// Providers configured but no models yet — keep the list only
|
||||
updates[ADMIN_PROVIDERS_KEY] = JSON.stringify(list)
|
||||
} else {
|
||||
updates[ADMIN_PROVIDERS_KEY] = null
|
||||
}
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// Every settings key the panel may have written for a given list
|
||||
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")
|
||||
}
|
||||
const env = credEnvNames(p.provider, index)
|
||||
if (env.key) keys.add(env.key)
|
||||
if (env.url) keys.add(env.url)
|
||||
}
|
||||
return [...keys]
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
// Declarative registry of all env vars editable in the admin panel.
|
||||
// Declarative registry of the general env vars editable in the admin panel.
|
||||
// Drives both server-side validation (app/api/admin/settings) and UI
|
||||
// rendering (app/[lang]/admin). Keys are exactly the env var names.
|
||||
//
|
||||
// AI providers and models are managed separately in the panel's Models
|
||||
// section (lib/admin/providers.ts), not here.
|
||||
//
|
||||
// Not listed here (and therefore rejected by the API):
|
||||
// - NEXT_PUBLIC_* vars: baked into the client bundle at build time
|
||||
// - ADMIN_PASSWORD / SETTINGS_FILE: bootstrap values, env-only to avoid lockout
|
||||
// - Per-provider reasoning/thinking tuning vars: env-only (see env.example)
|
||||
|
||||
import { PROVIDER_INFO, type ProviderName } from "@/lib/types/model-config"
|
||||
|
||||
export type SettingType =
|
||||
| "string"
|
||||
| "secret"
|
||||
| "number"
|
||||
| "boolean"
|
||||
| "enum"
|
||||
| "json"
|
||||
export type SettingType = "string" | "secret" | "number" | "boolean" | "enum"
|
||||
|
||||
export interface SettingDef {
|
||||
key: string
|
||||
@@ -28,9 +24,6 @@ export interface SettingDef {
|
||||
placeholder?: string
|
||||
// Value is only picked up at process start (module-load readers)
|
||||
restartRequired?: boolean
|
||||
// Provider this setting belongs to; rendered inside that provider's
|
||||
// collapsible in the Provider Credentials group
|
||||
provider?: ProviderName
|
||||
}
|
||||
|
||||
export interface SettingGroup {
|
||||
@@ -44,20 +37,9 @@ export interface SettingGroup {
|
||||
|
||||
export const SETTING_GROUPS: SettingGroup[] = [
|
||||
{
|
||||
id: "general",
|
||||
title: "General",
|
||||
description: "Default AI provider and model used by the server.",
|
||||
},
|
||||
{
|
||||
id: "providers",
|
||||
title: "Provider Credentials",
|
||||
description:
|
||||
"API keys, endpoints, and tuning for each supported provider.",
|
||||
},
|
||||
{
|
||||
id: "models",
|
||||
title: "Model Behavior",
|
||||
description: "Generation parameters and the multi-model registry.",
|
||||
id: "generation",
|
||||
title: "Generation",
|
||||
description: "Output parameters applied to all chat requests.",
|
||||
},
|
||||
{
|
||||
id: "access",
|
||||
@@ -84,363 +66,11 @@ export const SETTING_GROUPS: SettingGroup[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const PROVIDER_OPTIONS = Object.keys(PROVIDER_INFO)
|
||||
|
||||
// Providers with just {PREFIX}_API_KEY + {PREFIX}_BASE_URL env vars.
|
||||
// Labels and URL placeholders come from PROVIDER_INFO.
|
||||
const SIMPLE_PROVIDERS: Array<{ provider: ProviderName; prefix?: string }> = [
|
||||
{ provider: "openrouter" },
|
||||
{ provider: "deepseek" },
|
||||
{ provider: "siliconflow" },
|
||||
{ provider: "sglang" },
|
||||
{ provider: "gateway", prefix: "AI_GATEWAY" },
|
||||
{ provider: "doubao" },
|
||||
{ provider: "modelscope" },
|
||||
{ provider: "glm" },
|
||||
{ provider: "qwen" },
|
||||
{ provider: "kimi" },
|
||||
{ provider: "qiniu" },
|
||||
{ provider: "minimax" },
|
||||
{ provider: "novita" },
|
||||
]
|
||||
|
||||
function simpleProviderSettings(): SettingDef[] {
|
||||
return SIMPLE_PROVIDERS.flatMap(
|
||||
({ provider, prefix = provider.toUpperCase() }) => [
|
||||
{
|
||||
key: `${prefix}_API_KEY`,
|
||||
group: "providers",
|
||||
provider,
|
||||
type: "secret" as const,
|
||||
label: "API Key",
|
||||
},
|
||||
{
|
||||
key: `${prefix}_BASE_URL`,
|
||||
group: "providers",
|
||||
provider,
|
||||
type: "string" as const,
|
||||
label: "Base URL",
|
||||
placeholder: PROVIDER_INFO[provider].defaultBaseUrl,
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
export const SETTINGS_REGISTRY: SettingDef[] = [
|
||||
// ── General ──────────────────────────────────────────────────────
|
||||
{
|
||||
key: "AI_PROVIDER",
|
||||
group: "general",
|
||||
type: "enum",
|
||||
label: "AI Provider",
|
||||
description: "Default provider for chat requests.",
|
||||
options: PROVIDER_OPTIONS,
|
||||
},
|
||||
{
|
||||
key: "AI_MODEL",
|
||||
group: "general",
|
||||
type: "string",
|
||||
label: "AI Model",
|
||||
description: "Model ID for the chosen provider.",
|
||||
placeholder: "e.g. gpt-5.2 or global.anthropic.claude-sonnet-4-5…",
|
||||
},
|
||||
|
||||
// ── Providers: AWS Bedrock ───────────────────────────────────────
|
||||
{
|
||||
key: "AWS_REGION",
|
||||
group: "providers",
|
||||
provider: "bedrock",
|
||||
type: "string",
|
||||
label: "AWS Region",
|
||||
placeholder: "us-west-2",
|
||||
},
|
||||
{
|
||||
key: "AWS_ACCESS_KEY_ID",
|
||||
group: "providers",
|
||||
provider: "bedrock",
|
||||
type: "secret",
|
||||
label: "Access Key ID",
|
||||
},
|
||||
{
|
||||
key: "AWS_SECRET_ACCESS_KEY",
|
||||
group: "providers",
|
||||
provider: "bedrock",
|
||||
type: "secret",
|
||||
label: "Secret Access Key",
|
||||
},
|
||||
{
|
||||
key: "BEDROCK_REASONING_BUDGET_TOKENS",
|
||||
group: "providers",
|
||||
provider: "bedrock",
|
||||
type: "number",
|
||||
label: "Reasoning Budget Tokens",
|
||||
description: "Claude extended-thinking budget (1024–64000).",
|
||||
min: 1024,
|
||||
max: 64000,
|
||||
},
|
||||
{
|
||||
key: "BEDROCK_REASONING_EFFORT",
|
||||
group: "providers",
|
||||
provider: "bedrock",
|
||||
type: "enum",
|
||||
label: "Reasoning Effort",
|
||||
description: "For Nova models.",
|
||||
options: ["low", "medium", "high"],
|
||||
},
|
||||
|
||||
// ── Providers: OpenAI ────────────────────────────────────────────
|
||||
{
|
||||
key: "OPENAI_API_KEY",
|
||||
group: "providers",
|
||||
provider: "openai",
|
||||
type: "secret",
|
||||
label: "API Key",
|
||||
},
|
||||
{
|
||||
key: "OPENAI_BASE_URL",
|
||||
group: "providers",
|
||||
provider: "openai",
|
||||
type: "string",
|
||||
label: "Base URL",
|
||||
description: "Custom OpenAI-compatible endpoint.",
|
||||
placeholder: PROVIDER_INFO.openai.defaultBaseUrl,
|
||||
},
|
||||
{
|
||||
key: "OPENAI_REASONING_EFFORT",
|
||||
group: "providers",
|
||||
provider: "openai",
|
||||
type: "enum",
|
||||
label: "Reasoning Effort",
|
||||
options: ["minimal", "low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
key: "OPENAI_REASONING_SUMMARY",
|
||||
group: "providers",
|
||||
provider: "openai",
|
||||
type: "enum",
|
||||
label: "Reasoning Summary",
|
||||
options: ["none", "brief", "detailed"],
|
||||
},
|
||||
|
||||
// ── Providers: Anthropic ─────────────────────────────────────────
|
||||
{
|
||||
key: "ANTHROPIC_API_KEY",
|
||||
group: "providers",
|
||||
provider: "anthropic",
|
||||
type: "secret",
|
||||
label: "API Key",
|
||||
description: "Sent as x-api-key header.",
|
||||
},
|
||||
{
|
||||
key: "ANTHROPIC_AUTH_TOKEN",
|
||||
group: "providers",
|
||||
provider: "anthropic",
|
||||
type: "secret",
|
||||
label: "Auth Token",
|
||||
description:
|
||||
"Alternative to the API key; sent as Authorization: Bearer.",
|
||||
},
|
||||
{
|
||||
key: "ANTHROPIC_BASE_URL",
|
||||
group: "providers",
|
||||
provider: "anthropic",
|
||||
type: "string",
|
||||
label: "Base URL",
|
||||
placeholder: PROVIDER_INFO.anthropic.defaultBaseUrl,
|
||||
},
|
||||
{
|
||||
key: "ANTHROPIC_THINKING_TYPE",
|
||||
group: "providers",
|
||||
provider: "anthropic",
|
||||
type: "enum",
|
||||
label: "Extended Thinking",
|
||||
options: ["enabled"],
|
||||
},
|
||||
{
|
||||
key: "ANTHROPIC_THINKING_BUDGET_TOKENS",
|
||||
group: "providers",
|
||||
provider: "anthropic",
|
||||
type: "number",
|
||||
label: "Thinking Budget Tokens",
|
||||
min: 1024,
|
||||
max: 64000,
|
||||
},
|
||||
|
||||
// ── Providers: Google ────────────────────────────────────────────
|
||||
{
|
||||
key: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
group: "providers",
|
||||
provider: "google",
|
||||
type: "secret",
|
||||
label: "API Key",
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_BASE_URL",
|
||||
group: "providers",
|
||||
provider: "google",
|
||||
type: "string",
|
||||
label: "Base URL",
|
||||
placeholder: PROVIDER_INFO.google.defaultBaseUrl,
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_THINKING_BUDGET",
|
||||
group: "providers",
|
||||
provider: "google",
|
||||
type: "number",
|
||||
label: "Thinking Budget (Gemini 2.5)",
|
||||
min: 1024,
|
||||
max: 100000,
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_THINKING_LEVEL",
|
||||
group: "providers",
|
||||
provider: "google",
|
||||
type: "enum",
|
||||
label: "Thinking Level (Gemini 3)",
|
||||
options: ["low", "high"],
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_REASONING_EFFORT",
|
||||
group: "providers",
|
||||
provider: "google",
|
||||
type: "enum",
|
||||
label: "Reasoning Effort",
|
||||
options: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_CANDIDATE_COUNT",
|
||||
group: "providers",
|
||||
provider: "google",
|
||||
type: "number",
|
||||
label: "Candidate Count",
|
||||
min: 1,
|
||||
max: 8,
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_TOP_K",
|
||||
group: "providers",
|
||||
provider: "google",
|
||||
type: "number",
|
||||
label: "Top K",
|
||||
min: 1,
|
||||
max: 100,
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_TOP_P",
|
||||
group: "providers",
|
||||
provider: "google",
|
||||
type: "number",
|
||||
label: "Top P",
|
||||
min: 0,
|
||||
max: 1,
|
||||
},
|
||||
|
||||
// ── Providers: Vertex AI ─────────────────────────────────────────
|
||||
{
|
||||
key: "GOOGLE_VERTEX_API_KEY",
|
||||
group: "providers",
|
||||
provider: "vertexai",
|
||||
type: "secret",
|
||||
label: "API Key",
|
||||
description: "Express Mode API key.",
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_VERTEX_BASE_URL",
|
||||
group: "providers",
|
||||
provider: "vertexai",
|
||||
type: "string",
|
||||
label: "Base URL",
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_VERTEX_THINKING_BUDGET",
|
||||
group: "providers",
|
||||
provider: "vertexai",
|
||||
type: "number",
|
||||
label: "Thinking Budget (Gemini 2.5)",
|
||||
min: 1024,
|
||||
max: 100000,
|
||||
},
|
||||
{
|
||||
key: "GOOGLE_VERTEX_THINKING_LEVEL",
|
||||
group: "providers",
|
||||
provider: "vertexai",
|
||||
type: "enum",
|
||||
label: "Thinking Level (Gemini 3)",
|
||||
options: ["minimal", "low", "medium", "high"],
|
||||
},
|
||||
|
||||
// ── Providers: Azure OpenAI ──────────────────────────────────────
|
||||
{
|
||||
key: "AZURE_API_KEY",
|
||||
group: "providers",
|
||||
provider: "azure",
|
||||
type: "secret",
|
||||
label: "API Key",
|
||||
},
|
||||
{
|
||||
key: "AZURE_RESOURCE_NAME",
|
||||
group: "providers",
|
||||
provider: "azure",
|
||||
type: "string",
|
||||
label: "Resource Name",
|
||||
description: "Endpoint becomes https://{name}.openai.azure.com.",
|
||||
},
|
||||
{
|
||||
key: "AZURE_BASE_URL",
|
||||
group: "providers",
|
||||
provider: "azure",
|
||||
type: "string",
|
||||
label: "Base URL",
|
||||
description: "Alternative to resource name; takes precedence.",
|
||||
},
|
||||
{
|
||||
key: "AZURE_REASONING_EFFORT",
|
||||
group: "providers",
|
||||
provider: "azure",
|
||||
type: "enum",
|
||||
label: "Reasoning Effort",
|
||||
options: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
key: "AZURE_REASONING_SUMMARY",
|
||||
group: "providers",
|
||||
provider: "azure",
|
||||
type: "enum",
|
||||
label: "Reasoning Summary",
|
||||
options: ["none", "brief", "detailed"],
|
||||
},
|
||||
|
||||
// ── Providers: Ollama ────────────────────────────────────────────
|
||||
{
|
||||
key: "OLLAMA_BASE_URL",
|
||||
group: "providers",
|
||||
provider: "ollama",
|
||||
type: "string",
|
||||
label: "Base URL",
|
||||
placeholder: PROVIDER_INFO.ollama.defaultBaseUrl,
|
||||
},
|
||||
{
|
||||
key: "OLLAMA_API_KEY",
|
||||
group: "providers",
|
||||
provider: "ollama",
|
||||
type: "secret",
|
||||
label: "API Key",
|
||||
description: "For Ollama Cloud or authenticated remote instances.",
|
||||
},
|
||||
{
|
||||
key: "OLLAMA_ENABLE_THINKING",
|
||||
group: "providers",
|
||||
provider: "ollama",
|
||||
type: "boolean",
|
||||
label: "Enable Thinking",
|
||||
},
|
||||
|
||||
...simpleProviderSettings(),
|
||||
|
||||
// ── Model Behavior ───────────────────────────────────────────────
|
||||
// ── Generation ───────────────────────────────────────────────────
|
||||
{
|
||||
key: "TEMPERATURE",
|
||||
group: "models",
|
||||
group: "generation",
|
||||
type: "number",
|
||||
label: "Temperature",
|
||||
description:
|
||||
@@ -450,27 +80,11 @@ export const SETTINGS_REGISTRY: SettingDef[] = [
|
||||
},
|
||||
{
|
||||
key: "MAX_OUTPUT_TOKENS",
|
||||
group: "models",
|
||||
group: "generation",
|
||||
type: "number",
|
||||
label: "Max Output Tokens",
|
||||
min: 1,
|
||||
},
|
||||
{
|
||||
key: "AI_MODELS_CONFIG",
|
||||
group: "models",
|
||||
type: "json",
|
||||
label: "Multi-Model Registry (JSON)",
|
||||
description:
|
||||
'Server model list shown to all users. Schema: {"providers":[{"name":"…","provider":"openai","models":["…"]}]}.',
|
||||
},
|
||||
{
|
||||
key: "AI_MODELS_CONFIG_PATH",
|
||||
group: "models",
|
||||
type: "string",
|
||||
label: "Model Registry File Path",
|
||||
description: "Used only when the JSON registry above is empty.",
|
||||
placeholder: "./ai-models.json",
|
||||
},
|
||||
|
||||
// ── Access Control ───────────────────────────────────────────────
|
||||
{
|
||||
@@ -608,12 +222,3 @@ export const SETTINGS_BY_GROUP: Map<string, SettingDef[]> = new Map(
|
||||
SETTINGS_REGISTRY.filter((d) => d.group === g.id),
|
||||
]),
|
||||
)
|
||||
|
||||
// Provider Credentials group, keyed by provider in registry order
|
||||
export const PROVIDER_SUBGROUPS: Map<ProviderName, SettingDef[]> = new Map()
|
||||
for (const def of SETTINGS_REGISTRY) {
|
||||
if (!def.provider) continue
|
||||
const list = PROVIDER_SUBGROUPS.get(def.provider) ?? []
|
||||
list.push(def)
|
||||
PROVIDER_SUBGROUPS.set(def.provider, list)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user