feat: add managed backend types, queries, and provider field schema

- Add CatalogProvider, ManagedBackend, CreateManagedBackendRequest, UpdateManagedBackendRequest to api/types.ts
- Add useManagedBackends, useCreateManagedBackend, useUpdateManagedBackend, useDeleteManagedBackend hooks to api/queries.ts
- Create utils/providerFields.ts with getProviderFields() mapping protocol+auth to FieldDef[]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
whit3rabbit
2026-04-08 13:32:21 -05:00
co-authored by Claude Sonnet 4.6
parent 0ecd5aeab6
commit 887661f6dc
4 changed files with 283 additions and 8 deletions
File diff suppressed because one or more lines are too long
+38
View File
@@ -6,6 +6,7 @@ import type {
Backend, ConfigResponse, ObservabilityResponse,
ModelsResponse, AuditResponse, TrafficResponse, UptimeResponse,
EnvImportResponse, ProxyStatus, DiscoverResponse,
ManagedBackend, CreateManagedBackendRequest, UpdateManagedBackendRequest,
} from './types'
// ── Status ───────────────────────────────────────────────────────────────────
@@ -232,6 +233,43 @@ export function useImportEnv() {
})
}
// ── Managed backends ──────────────────────────────────────────────────────────
export function useManagedBackends() {
return useQuery<{ backends: ManagedBackend[] }>({
queryKey: ['managed-backends'],
queryFn: () => apiFetch('/admin/api/backends/managed'),
staleTime: Infinity,
})
}
export function useCreateManagedBackend() {
const qc = useQueryClient()
return useMutation<ManagedBackend, Error, CreateManagedBackendRequest>({
mutationFn: (data) =>
mutatingFetch<ManagedBackend>('POST', '/admin/api/backends/managed', data),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['managed-backends'] }) },
})
}
export function useUpdateManagedBackend() {
const qc = useQueryClient()
return useMutation<ManagedBackend, Error, { name: string; data: UpdateManagedBackendRequest }>({
mutationFn: ({ name, data }) =>
mutatingFetch<ManagedBackend>('PUT', `/admin/api/backends/managed/${name}`, data),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['managed-backends'] }) },
})
}
export function useDeleteManagedBackend() {
const qc = useQueryClient()
return useMutation<void, Error, string>({
mutationFn: (name) =>
mutatingFetch<void>('DELETE', `/admin/api/backends/managed/${name}`),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['managed-backends'] }) },
})
}
/**
* Download the current effective env as a .anyllm.env file.
* Not a hook — call directly from an event handler.
+48
View File
@@ -239,6 +239,54 @@ export interface EnvImportError {
warnings: EnvWarning[]
}
// --- Provider catalog ---
export interface CatalogProvider {
id: string
name: string
protocol: string
auth: string
default_base_url: string
env_vars: string[]
}
// --- Managed backends ---
export interface ManagedBackend {
id: string
name: string
provider_id: string
api_key_set: boolean
aws_creds_set: boolean
api_base: string | null
deployment: string | null
api_version: string | null
project: string | null
region: string | null
rpm: number | null
tpm: number | null
created_at: string
updated_at: string
}
export interface CreateManagedBackendRequest {
name: string
provider_id: string
api_key?: string
api_base?: string
deployment?: string
api_version?: string
project?: string
region?: string
aws_access_key_id?: string
aws_secret_access_key?: string
aws_session_token?: string
rpm?: number
tpm?: number
}
export type UpdateManagedBackendRequest = Partial<Omit<CreateManagedBackendRequest, 'name'>>
// --- WebSocket events ---
export type WSEvent =
@@ -0,0 +1,189 @@
import { CatalogProvider } from '../api/types'
export type FieldGroup = 'auth' | 'endpoint' | 'limits'
export interface FieldDef {
name: string
label: string
type: 'text' | 'password' | 'url' | 'number'
required: boolean
placeholder?: string
hint?: string
group: FieldGroup
}
export function getProviderFields(provider: CatalogProvider): FieldDef[] {
const fields: FieldDef[] = []
const firstEnvVar = provider.env_vars.length > 0 ? provider.env_vars[0] : null
const { protocol, auth, default_base_url } = provider
if (protocol === 'openai_compat' && auth === 'bearer') {
fields.push({
name: 'api_key',
label: 'API Key',
type: 'password',
required: true,
group: 'auth',
...(firstEnvVar ? { hint: `or set ${firstEnvVar} env var` } : {}),
})
fields.push({
name: 'api_base',
label: 'API Base URL',
type: 'url',
required: false,
group: 'endpoint',
...(default_base_url ? { placeholder: default_base_url } : {}),
})
} else if (protocol === 'openai_compat' && auth === 'none') {
fields.push({
name: 'api_base',
label: 'API Base URL',
type: 'url',
required: false,
group: 'endpoint',
...(default_base_url ? { placeholder: default_base_url } : {}),
})
} else if (protocol === 'azure_openai' && auth === 'azure_api_key') {
fields.push({
name: 'api_key',
label: 'Azure API Key',
type: 'password',
required: true,
group: 'auth',
})
fields.push({
name: 'api_base',
label: 'Endpoint URL',
type: 'url',
required: true,
placeholder: 'https://<resource>.openai.azure.com',
group: 'endpoint',
})
fields.push({
name: 'deployment',
label: 'Deployment Name',
type: 'text',
required: true,
group: 'endpoint',
})
fields.push({
name: 'api_version',
label: 'API Version',
type: 'text',
required: true,
placeholder: '2024-08-01-preview',
group: 'endpoint',
})
} else if (protocol === 'vertex_ai' && auth === 'google_api_key') {
fields.push({
name: 'api_key',
label: 'API Key',
type: 'password',
required: true,
group: 'auth',
})
fields.push({
name: 'project',
label: 'GCP Project ID',
type: 'text',
required: true,
group: 'endpoint',
})
fields.push({
name: 'region',
label: 'GCP Region',
type: 'text',
required: true,
placeholder: 'us-central1',
group: 'endpoint',
})
} else if (protocol === 'bedrock_native' && auth === 'aws_sigv4') {
fields.push({
name: 'aws_access_key_id',
label: 'AWS Access Key ID',
type: 'text',
required: true,
group: 'auth',
})
fields.push({
name: 'aws_secret_access_key',
label: 'AWS Secret Access Key',
type: 'password',
required: true,
group: 'auth',
})
fields.push({
name: 'aws_session_token',
label: 'AWS Session Token',
type: 'password',
required: false,
group: 'auth',
})
fields.push({
name: 'region',
label: 'AWS Region',
type: 'text',
required: true,
placeholder: 'us-east-1',
group: 'endpoint',
})
} else if ((protocol === 'gemini_openai' || protocol === 'gemini_native') && auth === 'google_api_key') {
fields.push({
name: 'api_key',
label: 'API Key',
type: 'password',
required: true,
group: 'auth',
...(firstEnvVar ? { hint: `or set ${firstEnvVar} env var` } : {}),
})
} else if (protocol === 'anthropic_native' && auth === 'bearer') {
fields.push({
name: 'api_key',
label: 'API Key',
type: 'password',
required: true,
group: 'auth',
...(firstEnvVar ? { hint: `or set ${firstEnvVar} env var` } : {}),
})
} else {
// Fallback
if (auth.includes('bearer')) {
fields.push({
name: 'api_key',
label: 'API Key',
type: 'password',
required: true,
group: 'auth',
})
}
if (auth === 'none' || auth.includes('bearer')) {
fields.push({
name: 'api_base',
label: 'API Base URL',
type: 'url',
required: false,
group: 'endpoint',
...(default_base_url ? { placeholder: default_base_url } : {}),
})
}
}
// All providers get rate/token limits
fields.push({
name: 'rpm',
label: 'Rate Limit (req/min)',
type: 'number',
required: false,
group: 'limits',
})
fields.push({
name: 'tpm',
label: 'Token Limit (tokens/min)',
type: 'number',
required: false,
group: 'limits',
})
return fields
}