feat(server,web): add custom provider/model CRUD HTTP endpoints

- Create custom-store.ts with platform-agnostic CRUD for custom-providers.json
  and custom-models.json (parameterized by aiDataDir)
- Add 6 HTTP endpoints: POST/PUT/DELETE for custom providers and models
- Update web shim to call new endpoints instead of returning WEB_STUB
This commit is contained in:
digua committed 2026-05-14 22:07:54 +08:00
1 parent 4b1a1e411b
commit d87bdd5712
3 files changed
+237 -3

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
/**
* 自定义 Provider / Model 持久化存储(平台无关)
*
* 文件位置:
* - {aiDataDir}/custom-providers.json
* - {aiDataDir}/custom-models.json
*/
import * as fs from 'fs'
import * as path from 'path'
import { randomUUID } from 'crypto'
import type { ProviderDefinition, ModelDefinition } from '@openchatlab/core'
// ==================== Custom Providers ====================
function readProviders(aiDataDir: string): ProviderDefinition[] {
const storePath = path.join(aiDataDir, 'custom-providers.json')
if (!fs.existsSync(storePath)) return []
try {
const content = fs.readFileSync(storePath, 'utf-8')
return JSON.parse(content) as ProviderDefinition[]
} catch {
return []
}
}
function writeProviders(aiDataDir: string, providers: ProviderDefinition[]): void {
const storePath = path.join(aiDataDir, 'custom-providers.json')
const dir = path.dirname(storePath)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(storePath, JSON.stringify(providers, null, 2), 'utf-8')
}
export function addCustomProvider(
aiDataDir: string,
input: Omit<ProviderDefinition, 'id' | 'builtin' | 'enabledByDefault'>
): ProviderDefinition {
const providers = readProviders(aiDataDir)
const newProvider: ProviderDefinition = {
...input,
id: `custom:${randomUUID()}`,
builtin: false,
enabledByDefault: false,
}
providers.push(newProvider)
writeProviders(aiDataDir, providers)
return newProvider
}
export function updateCustomProvider(
aiDataDir: string,
id: string,
updates: Partial<Omit<ProviderDefinition, 'id' | 'builtin'>>
): { success: boolean; error?: string } {
const providers = readProviders(aiDataDir)
const index = providers.findIndex((p) => p.id === id)
if (index === -1) return { success: false, error: 'Custom provider not found' }
providers[index] = { ...providers[index], ...updates }
writeProviders(aiDataDir, providers)
return { success: true }
}
export function deleteCustomProvider(aiDataDir: string, id: string): { success: boolean; error?: string } {
const providers = readProviders(aiDataDir)
const index = providers.findIndex((p) => p.id === id)
if (index === -1) return { success: false, error: 'Custom provider not found' }
providers.splice(index, 1)
writeProviders(aiDataDir, providers)
return { success: true }
}
// ==================== Custom Models ====================
function readModels(aiDataDir: string): ModelDefinition[] {
const storePath = path.join(aiDataDir, 'custom-models.json')
if (!fs.existsSync(storePath)) return []
try {
const content = fs.readFileSync(storePath, 'utf-8')
return JSON.parse(content) as ModelDefinition[]
} catch {
return []
}
}
function writeModels(aiDataDir: string, models: ModelDefinition[]): void {
const storePath = path.join(aiDataDir, 'custom-models.json')
const dir = path.dirname(storePath)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(storePath, JSON.stringify(models, null, 2), 'utf-8')
}
export function addCustomModel(
aiDataDir: string,
input: Omit<ModelDefinition, 'builtin' | 'editable'>
): { success: boolean; model?: ModelDefinition; error?: string } {
const models = readModels(aiDataDir)
const existing = models.find((m) => m.id === input.id && m.providerId === input.providerId)
if (existing) {
return { success: false, error: `Model "${input.id}" already exists under provider "${input.providerId}"` }
}
const newModel: ModelDefinition = {
...input,
builtin: false,
editable: true,
}
models.push(newModel)
writeModels(aiDataDir, models)
return { success: true, model: newModel }
}
export function updateCustomModel(
aiDataDir: string,
providerId: string,
modelId: string,
updates: Partial<Omit<ModelDefinition, 'id' | 'providerId' | 'builtin'>>
): { success: boolean; error?: string } {
const models = readModels(aiDataDir)
const index = models.findIndex((m) => m.id === modelId && m.providerId === providerId)
if (index === -1) return { success: false, error: 'Custom model not found' }
models[index] = { ...models[index], ...updates }
writeModels(aiDataDir, models)
return { success: true }
}
export function deleteCustomModel(
aiDataDir: string,
providerId: string,
modelId: string
): { success: boolean; error?: string } {
const models = readModels(aiDataDir)
const index = models.findIndex((m) => m.id === modelId && m.providerId === providerId)
if (index === -1) return { success: false, error: 'Custom model not found' }
models.splice(index, 1)
writeModels(aiDataDir, models)
return { success: true }
}
+83
View File
@@ -27,6 +27,14 @@ import {
setDefaultAssistantSlot,
setFastModelSlot,
} from '../../ai/llm-config'
import {
addCustomProvider,
updateCustomProvider,
deleteCustomProvider,
addCustomModel,
updateCustomModel,
deleteCustomModel,
} from '../../ai/custom-store'
function getAiDir(dbManager: DatabaseManager): string {
const pathProvider = (dbManager as any)['pathProvider']
@@ -292,6 +300,81 @@ export function registerAiRoutes(
return setFastModelSlot(aiDataDir, request.body)
})
// ==================== Custom Provider CRUD ====================
server.post<{
Body: {
name: string
kind: string
defaultBaseUrl: string
authMode?: string
supportsCustomModels?: boolean
modelIds?: string[]
website?: string
consoleUrl?: string
}
}>('/_web/ai/llm/custom-providers', async (request) => {
const aiDataDir = getAiDir(dbManager)
return addCustomProvider(aiDataDir, {
...request.body,
kind: (request.body.kind || 'openai-compatible') as 'official' | 'aggregator' | 'openai-compatible',
authMode: 'api-key',
supportsCustomModels: request.body.supportsCustomModels ?? true,
modelIds: request.body.modelIds ?? [],
})
})
server.put<{
Params: { id: string }
Body: Record<string, unknown>
}>('/_web/ai/llm/custom-providers/:id', async (request) => {
const aiDataDir = getAiDir(dbManager)
return updateCustomProvider(aiDataDir, request.params.id, request.body as any)
})
server.delete<{ Params: { id: string } }>('/_web/ai/llm/custom-providers/:id', async (request) => {
const aiDataDir = getAiDir(dbManager)
return deleteCustomProvider(aiDataDir, request.params.id)
})
// ==================== Custom Model CRUD ====================
server.post<{
Body: {
id: string
providerId: string
name: string
description?: string
contextWindow?: number
capabilities?: string[]
recommendedFor?: string[]
status?: string
}
}>('/_web/ai/llm/custom-models', async (request) => {
const aiDataDir = getAiDir(dbManager)
return addCustomModel(aiDataDir, {
...request.body,
capabilities: (request.body.capabilities ?? ['chat']) as any,
recommendedFor: (request.body.recommendedFor ?? ['chat']) as any,
status: (request.body.status ?? 'stable') as any,
})
})
server.put<{
Params: { providerId: string; modelId: string }
Body: Record<string, unknown>
}>('/_web/ai/llm/custom-models/:providerId/:modelId', async (request) => {
const aiDataDir = getAiDir(dbManager)
return updateCustomModel(aiDataDir, request.params.providerId, request.params.modelId, request.body as any)
})
server.delete<{
Params: { providerId: string; modelId: string }
}>('/_web/ai/llm/custom-models/:providerId/:modelId', async (request) => {
const aiDataDir = getAiDir(dbManager)
return deleteCustomModel(aiDataDir, request.params.providerId, request.params.modelId)
})
// ==================== Tool Catalog ====================
server.get('/_web/ai/tools/catalog', async () => {
+12 -3
View File
@@ -216,9 +216,18 @@ async function installAiApiShims(): Promise<void> {
validateApiKey: () => Promise.resolve(WEB_STUB),
fetchRemoteModels: () => Promise.resolve({ success: false, error: 'Not available in web mode', models: [] }),
chatStream: () => Promise.resolve({ success: false, error: 'Not available in web mode' }),
addCustomModel: () => Promise.resolve(WEB_STUB),
updateCustomModel: () => Promise.resolve(WEB_STUB),
deleteCustomModel: () => Promise.resolve(WEB_STUB),
addCustomModel: async (input: Record<string, unknown>) => {
const { post: httpPost } = await import('./utils/http')
return httpPost('/ai/llm/custom-models', input)
},
updateCustomModel: async (providerId: string, modelId: string, updates: Record<string, unknown>) => {
const { put: httpPut } = await import('./utils/http')
return httpPut(`/ai/llm/custom-models/${providerId}/${encodeURIComponent(modelId)}`, updates)
},
deleteCustomModel: async (providerId: string, modelId: string) => {
const { del } = await import('./utils/http')
return del(`/ai/llm/custom-models/${providerId}/${encodeURIComponent(modelId)}`)
},
}
const { fetchSSE } = await import('./utils/sse')
const { post: httpPost } = await import('./utils/http')