mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
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:
1 parent
4b1a1e411b
commit
d87bdd5712
3 files changed
+237
-3
No files matched your search
@@ -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 }
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user