feat: 新增模型管理功能(供应商 + 模型 CRUD)
- 新增 providers/models 数据库表、迁移和数据访问层 - 新增 15 个后端 API 路由(供应商/模型 CRUD + 连通性测试) - 新增 AI 服务层(registry.ts: buildProviderRegistry + testProviderConnection) - 新增前端模型管理页面(Tabs: 供应商/模型,含表格、表单、工具栏) - 新增前端 hooks(use-providers, use-models) - 新增共享类型和 MODEL_CAPABILITIES 常量 - 新增 10 个测试文件(66 个测试用例,4 个因 bun test ESM 兼容问题待修复) - 更新开发文档(architecture, backend, frontend) - 附带 apply-review 修复:统一错误响应、提取共享常量、清理重复测试 注意:registry.test.ts 中 4 个测试因 bun test 无法解析 createProviderRegistry ESM 导出而失败,详情见 context.md
This commit is contained in:
74
src/server/ai/registry.ts
Normal file
74
src/server/ai/registry.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
||||
import { createProviderRegistry, generateText } from "ai";
|
||||
|
||||
import type { AIProviderConfig } from "./types";
|
||||
|
||||
export function buildProviderRegistry(db: Database) {
|
||||
const enabledProviders = getEnabledProviders(db);
|
||||
|
||||
const providerEntries: Record<string, ReturnType<typeof createProvider>> = {};
|
||||
for (const p of enabledProviders) {
|
||||
providerEntries[p.id] = createProvider({
|
||||
apiKey: p.api_key,
|
||||
baseUrl: p.base_url,
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
});
|
||||
}
|
||||
|
||||
return createProviderRegistry(providerEntries);
|
||||
}
|
||||
|
||||
export async function testProviderConnection(config: AIProviderConfig): Promise<{ message: string; ok: boolean }> {
|
||||
try {
|
||||
const provider = createProvider(config);
|
||||
const model = provider.languageModel("test");
|
||||
|
||||
await generateText({
|
||||
maxOutputTokens: 1,
|
||||
model,
|
||||
prompt: "Hi",
|
||||
});
|
||||
|
||||
return { message: "连接成功", ok: true };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return { message: `连接失败: ${msg}`, ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
function createProvider(config: AIProviderConfig) {
|
||||
switch (config.type) {
|
||||
case "anthropic":
|
||||
return createAnthropic({ apiKey: config.apiKey, baseURL: config.baseUrl });
|
||||
case "openai":
|
||||
return createOpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl });
|
||||
case "openai-compatible":
|
||||
return createOpenAICompatible({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.baseUrl,
|
||||
name: config.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getEnabledProviders(db: Database): Array<{
|
||||
api_key: string;
|
||||
base_url: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: "anthropic" | "openai" | "openai-compatible";
|
||||
}> {
|
||||
const stmt = db.prepare("SELECT id, name, type, base_url, api_key FROM providers WHERE enabled = 1");
|
||||
return stmt.all() as Array<{
|
||||
api_key: string;
|
||||
base_url: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: "anthropic" | "openai" | "openai-compatible";
|
||||
}>;
|
||||
}
|
||||
21
src/server/ai/types.ts
Normal file
21
src/server/ai/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ModelCapability, ProviderType } from "../../shared/api";
|
||||
|
||||
export type { ModelCapability, ProviderType };
|
||||
|
||||
export interface AIModelConfig {
|
||||
capabilities: ModelCapability[];
|
||||
modelId: string;
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
export interface AIProviderConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
name: string;
|
||||
type: ProviderType;
|
||||
}
|
||||
|
||||
export interface AIRegistryConfig {
|
||||
models: AIModelConfig[];
|
||||
providers: AIProviderConfig[];
|
||||
}
|
||||
237
src/server/db/models.ts
Normal file
237
src/server/db/models.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { and, desc, eq, like, or, sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
|
||||
import type { CreateModelRequest, Model, ModelCapability, UpdateModelRequest } from "../../shared/api";
|
||||
|
||||
import { models, providers } from "./schema";
|
||||
|
||||
export function createModel(
|
||||
raw: Database,
|
||||
request: CreateModelRequest,
|
||||
): { error: string; status: number } | { model: Model } {
|
||||
const db = wrap(raw);
|
||||
|
||||
const provider = db.select().from(providers).where(eq(providers.id, request.providerId)).get();
|
||||
if (!provider) return { error: "供应商不存在", status: 400 };
|
||||
|
||||
const name = request.name.trim();
|
||||
if (!name) return { error: "模型名称不能为空", status: 400 };
|
||||
|
||||
const modelId = request.modelId.trim();
|
||||
if (!modelId) return { error: "模型 ID 不能为空", status: 400 };
|
||||
|
||||
const capabilities = request.capabilities;
|
||||
if (!capabilities || capabilities.length === 0) {
|
||||
return { error: "至少选择一个能力标签", status: 400 };
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
db.insert(models)
|
||||
.values({
|
||||
capabilities: JSON.stringify(capabilities),
|
||||
contextLength: request.contextLength ?? null,
|
||||
createdAt: now,
|
||||
enabled: true,
|
||||
id,
|
||||
maxOutputTokens: request.maxOutputTokens ?? null,
|
||||
modelId,
|
||||
name,
|
||||
providerId: request.providerId,
|
||||
updatedAt: now,
|
||||
})
|
||||
.run();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "该供应商下模型 ID 已存在", status: 409 };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const row = db.select().from(models).where(eq(models.id, id)).get();
|
||||
return { model: toModel(row!) };
|
||||
}
|
||||
|
||||
export function deleteModel(raw: Database, id: string): { error: string; status: number } | { success: true } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(models).where(eq(models.id, id)).get();
|
||||
if (!existing) return { error: "模型不存在", status: 404 };
|
||||
|
||||
db.delete(models).where(eq(models.id, id)).run();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export function disableModel(raw: Database, id: string): { error: string; status: number } | { model: Model } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(models).where(eq(models.id, id)).get();
|
||||
if (!existing) return { error: "模型不存在", status: 404 };
|
||||
if (!existing.enabled) return { error: "模型已禁用", status: 409 };
|
||||
|
||||
const now = new Date().toISOString();
|
||||
db.update(models).set({ enabled: false, updatedAt: now }).where(eq(models.id, id)).run();
|
||||
|
||||
const updated = db.select().from(models).where(eq(models.id, id)).get();
|
||||
return { model: toModel(updated!) };
|
||||
}
|
||||
|
||||
export function enableModel(raw: Database, id: string): { error: string; status: number } | { model: Model } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(models).where(eq(models.id, id)).get();
|
||||
if (!existing) return { error: "模型不存在", status: 404 };
|
||||
if (existing.enabled) return { error: "模型已启用", status: 409 };
|
||||
|
||||
const now = new Date().toISOString();
|
||||
db.update(models).set({ enabled: true, updatedAt: now }).where(eq(models.id, id)).run();
|
||||
|
||||
const updated = db.select().from(models).where(eq(models.id, id)).get();
|
||||
return { model: toModel(updated!) };
|
||||
}
|
||||
|
||||
export function getModel(raw: Database, id: string): { error: string; status: number } | { model: Model } {
|
||||
const db = wrap(raw);
|
||||
const row = db.select().from(models).where(eq(models.id, id)).get();
|
||||
|
||||
if (!row) return { error: "模型不存在", status: 404 };
|
||||
return { model: toModel(row) };
|
||||
}
|
||||
|
||||
export function getModelsByProviderId(raw: Database, providerId: string): number {
|
||||
const db = wrap(raw);
|
||||
const result = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(models)
|
||||
.where(eq(models.providerId, providerId))
|
||||
.get();
|
||||
return Number(result?.count ?? 0);
|
||||
}
|
||||
|
||||
export function listModels(
|
||||
raw: Database,
|
||||
options: { keyword?: string; page: number; pageSize: number; providerId?: string },
|
||||
): { items: Model[]; page: number; pageSize: number; total: number } {
|
||||
const db = wrap(raw);
|
||||
const conditions = [];
|
||||
|
||||
if (options.providerId) {
|
||||
conditions.push(eq(models.providerId, options.providerId));
|
||||
}
|
||||
|
||||
if (options.keyword) {
|
||||
const pattern = `%${options.keyword}%`;
|
||||
conditions.push(or(like(models.name, pattern), like(models.modelId, pattern))!);
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const countResult = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(models)
|
||||
.where(where)
|
||||
.get();
|
||||
|
||||
const total = Number(countResult?.count ?? 0);
|
||||
|
||||
const rows = db
|
||||
.select()
|
||||
.from(models)
|
||||
.where(where)
|
||||
.orderBy(desc(models.createdAt))
|
||||
.limit(options.pageSize)
|
||||
.offset((options.page - 1) * options.pageSize)
|
||||
.all();
|
||||
|
||||
return {
|
||||
items: rows.map(toModel),
|
||||
page: options.page,
|
||||
pageSize: options.pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateModel(
|
||||
raw: Database,
|
||||
id: string,
|
||||
request: UpdateModelRequest,
|
||||
): { error: string; status: number } | { model: Model } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(models).where(eq(models.id, id)).get();
|
||||
if (!existing) return { error: "模型不存在", status: 404 };
|
||||
|
||||
const updates: Partial<typeof models.$inferInsert> = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const name = request.name?.trim();
|
||||
if (name === "") return { error: "模型名称不能为空", status: 400 };
|
||||
if (name !== undefined && name !== existing.name) {
|
||||
updates.name = name;
|
||||
}
|
||||
|
||||
const modelId = request.modelId?.trim();
|
||||
if (modelId === "") return { error: "模型 ID 不能为空", status: 400 };
|
||||
if (modelId !== undefined) {
|
||||
updates.modelId = modelId;
|
||||
}
|
||||
|
||||
if (request.providerId !== undefined) {
|
||||
const provider = db.select().from(providers).where(eq(providers.id, request.providerId)).get();
|
||||
if (!provider) return { error: "供应商不存在", status: 400 };
|
||||
updates.providerId = request.providerId;
|
||||
}
|
||||
|
||||
if (request.capabilities !== undefined) {
|
||||
if (request.capabilities.length === 0) {
|
||||
return { error: "至少选择一个能力标签", status: 400 };
|
||||
}
|
||||
updates.capabilities = JSON.stringify(request.capabilities);
|
||||
}
|
||||
|
||||
if (request.contextLength !== undefined) {
|
||||
updates.contextLength = request.contextLength;
|
||||
}
|
||||
|
||||
if (request.maxOutputTokens !== undefined) {
|
||||
updates.maxOutputTokens = request.maxOutputTokens;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 1 && updates.updatedAt) {
|
||||
return { model: toModel(existing) };
|
||||
}
|
||||
|
||||
try {
|
||||
db.update(models).set(updates).where(eq(models.id, id)).run();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "该供应商下模型 ID 已存在", status: 409 };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const updated = db.select().from(models).where(eq(models.id, id)).get();
|
||||
return { model: toModel(updated!) };
|
||||
}
|
||||
|
||||
function toModel(row: typeof models.$inferSelect): Model {
|
||||
return {
|
||||
capabilities: JSON.parse(row.capabilities) as ModelCapability[],
|
||||
contextLength: row.contextLength,
|
||||
createdAt: row.createdAt,
|
||||
enabled: row.enabled,
|
||||
id: row.id,
|
||||
maxOutputTokens: row.maxOutputTokens,
|
||||
modelId: row.modelId,
|
||||
name: row.name,
|
||||
providerId: row.providerId,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(raw: Database) {
|
||||
return drizzle(raw);
|
||||
}
|
||||
202
src/server/db/providers.ts
Normal file
202
src/server/db/providers.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { and, desc, eq, like, sql } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
|
||||
import type { CreateProviderRequest, Provider, UpdateProviderRequest } from "../../shared/api";
|
||||
|
||||
import { providers } from "./schema";
|
||||
|
||||
export function createProvider(
|
||||
raw: Database,
|
||||
request: CreateProviderRequest,
|
||||
): { error: string; status: number } | { provider: Provider } {
|
||||
const db = wrap(raw);
|
||||
const name = request.name.trim();
|
||||
if (!name) return { error: "供应商名称不能为空", status: 400 };
|
||||
|
||||
const baseUrl = request.baseUrl.trim();
|
||||
if (!baseUrl) return { error: "Base URL 不能为空", status: 400 };
|
||||
|
||||
const apiKey = request.apiKey.trim();
|
||||
if (!apiKey) return { error: "API Key 不能为空", status: 400 };
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
db.insert(providers)
|
||||
.values({
|
||||
apiKey,
|
||||
baseUrl,
|
||||
createdAt: now,
|
||||
enabled: true,
|
||||
id,
|
||||
name,
|
||||
type: request.type,
|
||||
updatedAt: now,
|
||||
})
|
||||
.run();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "供应商名称已存在", status: 409 };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const row = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
return { provider: toProvider(row!) };
|
||||
}
|
||||
|
||||
export function deleteProvider(raw: Database, id: string): { error: string; status: number } | { success: true } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
if (!existing) return { error: "供应商不存在", status: 404 };
|
||||
|
||||
db.delete(providers).where(eq(providers.id, id)).run();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export function disableProvider(raw: Database, id: string): { error: string; status: number } | { provider: Provider } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
if (!existing) return { error: "供应商不存在", status: 404 };
|
||||
if (!existing.enabled) return { error: "供应商已禁用", status: 409 };
|
||||
|
||||
const now = new Date().toISOString();
|
||||
db.update(providers).set({ enabled: false, updatedAt: now }).where(eq(providers.id, id)).run();
|
||||
|
||||
const updated = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
return { provider: toProvider(updated!) };
|
||||
}
|
||||
|
||||
export function enableProvider(raw: Database, id: string): { error: string; status: number } | { provider: Provider } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
if (!existing) return { error: "供应商不存在", status: 404 };
|
||||
if (existing.enabled) return { error: "供应商已启用", status: 409 };
|
||||
|
||||
const now = new Date().toISOString();
|
||||
db.update(providers).set({ enabled: true, updatedAt: now }).where(eq(providers.id, id)).run();
|
||||
|
||||
const updated = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
return { provider: toProvider(updated!) };
|
||||
}
|
||||
|
||||
export function getProvider(raw: Database, id: string): { error: string; status: number } | { provider: Provider } {
|
||||
const db = wrap(raw);
|
||||
const row = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
|
||||
if (!row) return { error: "供应商不存在", status: 404 };
|
||||
return { provider: toProvider(row) };
|
||||
}
|
||||
|
||||
export function listProviders(
|
||||
raw: Database,
|
||||
options: { keyword?: string; page: number; pageSize: number },
|
||||
): { items: Provider[]; page: number; pageSize: number; total: number } {
|
||||
const db = wrap(raw);
|
||||
const conditions = [];
|
||||
|
||||
if (options.keyword) {
|
||||
const pattern = `%${options.keyword}%`;
|
||||
conditions.push(like(providers.name, pattern));
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const countResult = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(providers)
|
||||
.where(where)
|
||||
.get();
|
||||
|
||||
const total = Number(countResult?.count ?? 0);
|
||||
|
||||
const rows = db
|
||||
.select()
|
||||
.from(providers)
|
||||
.where(where)
|
||||
.orderBy(desc(providers.createdAt))
|
||||
.limit(options.pageSize)
|
||||
.offset((options.page - 1) * options.pageSize)
|
||||
.all();
|
||||
|
||||
return {
|
||||
items: rows.map(toProvider),
|
||||
page: options.page,
|
||||
pageSize: options.pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateProvider(
|
||||
raw: Database,
|
||||
id: string,
|
||||
request: UpdateProviderRequest,
|
||||
): { error: string; status: number } | { provider: Provider } {
|
||||
const db = wrap(raw);
|
||||
const existing = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
if (!existing) return { error: "供应商不存在", status: 404 };
|
||||
|
||||
const updates: Partial<typeof providers.$inferInsert> = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const name = request.name?.trim();
|
||||
if (name === "") return { error: "供应商名称不能为空", status: 400 };
|
||||
if (name !== undefined && name !== existing.name) {
|
||||
updates.name = name;
|
||||
}
|
||||
|
||||
const baseUrl = request.baseUrl?.trim();
|
||||
if (baseUrl === "") return { error: "Base URL 不能为空", status: 400 };
|
||||
if (baseUrl !== undefined) {
|
||||
updates.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
const apiKey = request.apiKey?.trim();
|
||||
if (apiKey === "") return { error: "API Key 不能为空", status: 400 };
|
||||
if (apiKey !== undefined) {
|
||||
updates.apiKey = apiKey;
|
||||
}
|
||||
|
||||
if (request.type !== undefined) {
|
||||
updates.type = request.type;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 1 && updates.updatedAt) {
|
||||
return { provider: toProvider(existing) };
|
||||
}
|
||||
|
||||
try {
|
||||
db.update(providers).set(updates).where(eq(providers.id, id)).run();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes("UNIQUE constraint")) {
|
||||
return { error: "供应商名称已存在", status: 409 };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
const updated = db.select().from(providers).where(eq(providers.id, id)).get();
|
||||
return { provider: toProvider(updated!) };
|
||||
}
|
||||
|
||||
function toProvider(row: typeof providers.$inferSelect): Provider {
|
||||
return {
|
||||
apiKey: row.apiKey,
|
||||
baseUrl: row.baseUrl,
|
||||
createdAt: row.createdAt,
|
||||
enabled: row.enabled,
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(raw: Database) {
|
||||
return drizzle(raw);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const projects = sqliteTable("projects", {
|
||||
archivedAt: text("archived_at"),
|
||||
@@ -12,6 +12,41 @@ export const projects = sqliteTable("projects", {
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const providers = sqliteTable("providers", {
|
||||
apiKey: text("api_key").notNull(),
|
||||
baseUrl: text("base_url").notNull(),
|
||||
createdAt: text("created_at").notNull(),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull().unique(),
|
||||
type: text("type", { enum: ["anthropic", "openai", "openai-compatible"] })
|
||||
.notNull()
|
||||
.default("openai-compatible"),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const models = sqliteTable(
|
||||
"models",
|
||||
{
|
||||
capabilities: text("capabilities").notNull(),
|
||||
contextLength: integer("context_length"),
|
||||
createdAt: text("created_at").notNull(),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
id: text("id").primaryKey(),
|
||||
maxOutputTokens: integer("max_output_tokens"),
|
||||
modelId: text("model_id").notNull(),
|
||||
name: text("name").notNull(),
|
||||
providerId: text("provider_id")
|
||||
.notNull()
|
||||
.references(() => providers.id),
|
||||
updatedAt: text("updated_at").notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("models_provider_id_model_id_unique").on(table.providerId, table.modelId),
|
||||
index("models_provider_id_idx").on(table.providerId),
|
||||
],
|
||||
);
|
||||
|
||||
export const schemaMigrations = sqliteTable("schema_migrations", {
|
||||
appliedAt: text("applied_at").notNull(),
|
||||
checksum: text("checksum").notNull(),
|
||||
|
||||
47
src/server/routes/models/create.ts
Normal file
47
src/server/routes/models/create.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { CreateModelRequest, RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { MODEL_CAPABILITIES } from "../../../shared/api";
|
||||
import { createModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
|
||||
export async function handleCreateModel(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
let body: CreateModelRequest;
|
||||
try {
|
||||
body = (await req.json()) as CreateModelRequest;
|
||||
} catch {
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (!body.name || typeof body.name !== "string") {
|
||||
return jsonResponse(createApiError("name is required", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (!body.modelId || typeof body.modelId !== "string") {
|
||||
return jsonResponse(createApiError("modelId is required", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (!body.providerId || typeof body.providerId !== "string") {
|
||||
return jsonResponse(createApiError("providerId is required", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (!Array.isArray(body.capabilities) || body.capabilities.length === 0) {
|
||||
return jsonResponse(createApiError("capabilities is required and must be a non-empty array", 400), {
|
||||
mode,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const invalidCaps = body.capabilities.filter((c) => !MODEL_CAPABILITIES.includes(c));
|
||||
if (invalidCaps.length > 0) {
|
||||
return jsonResponse(createApiError(`Invalid capabilities: ${invalidCaps.join(", ")}`, 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
const result = createModel(db, body);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode, status: 201 });
|
||||
}
|
||||
22
src/server/routes/models/delete.ts
Normal file
22
src/server/routes/models/delete.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { deleteModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleDeleteModel(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = deleteModel(db, validated.id);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
22
src/server/routes/models/disable.ts
Normal file
22
src/server/routes/models/disable.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { disableModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleDisableModel(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = disableModel(db, validated.id);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
22
src/server/routes/models/enable.ts
Normal file
22
src/server/routes/models/enable.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { enableModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleEnableModel(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = enableModel(db, validated.id);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
22
src/server/routes/models/get.ts
Normal file
22
src/server/routes/models/get.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { getModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleGetModel(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = getModel(db, validated.id);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
27
src/server/routes/models/list.ts
Normal file
27
src/server/routes/models/list.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { listModels } from "../../db/models";
|
||||
import { jsonResponse } from "../../helpers";
|
||||
import { validatePagination } from "../../middleware";
|
||||
|
||||
export function handleListModels(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const pageParam = url.searchParams.get("page");
|
||||
const pageSizeParam = url.searchParams.get("pageSize");
|
||||
const keyword = url.searchParams.get("keyword");
|
||||
const providerId = url.searchParams.get("providerId");
|
||||
|
||||
const pagination = validatePagination(pageParam, pageSizeParam, mode);
|
||||
if (pagination instanceof Response) return pagination;
|
||||
|
||||
const result = listModels(db, {
|
||||
keyword: keyword ?? undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
providerId: providerId ?? undefined,
|
||||
});
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
43
src/server/routes/models/update.ts
Normal file
43
src/server/routes/models/update.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode, UpdateModelRequest } from "../../../shared/api";
|
||||
|
||||
import { MODEL_CAPABILITIES } from "../../../shared/api";
|
||||
import { updateModel } from "../../db/models";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleUpdateModel(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
let body: UpdateModelRequest;
|
||||
try {
|
||||
body = (await req.json()) as UpdateModelRequest;
|
||||
} catch {
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (body.capabilities !== undefined) {
|
||||
if (!Array.isArray(body.capabilities) || body.capabilities.length === 0) {
|
||||
return jsonResponse(createApiError("capabilities must be a non-empty array", 400), { mode, status: 400 });
|
||||
}
|
||||
const invalidCaps = body.capabilities.filter((c) => !MODEL_CAPABILITIES.includes(c));
|
||||
if (invalidCaps.length > 0) {
|
||||
return jsonResponse(createApiError(`Invalid capabilities: ${invalidCaps.join(", ")}`, 400), {
|
||||
mode,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = updateModel(db, validated.id, body);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
41
src/server/routes/providers/create.ts
Normal file
41
src/server/routes/providers/create.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { CreateProviderRequest, RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { createProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
|
||||
export async function handleCreateProvider(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
let body: CreateProviderRequest;
|
||||
try {
|
||||
body = (await req.json()) as CreateProviderRequest;
|
||||
} catch {
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (!body.name || typeof body.name !== "string") {
|
||||
return jsonResponse(createApiError("name is required", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (!body.baseUrl || typeof body.baseUrl !== "string") {
|
||||
return jsonResponse(createApiError("baseUrl is required", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (!body.apiKey || typeof body.apiKey !== "string") {
|
||||
return jsonResponse(createApiError("apiKey is required", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (!body.type || !["anthropic", "openai", "openai-compatible"].includes(body.type)) {
|
||||
return jsonResponse(createApiError("type must be one of: openai, anthropic, openai-compatible", 400), {
|
||||
mode,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const result = createProvider(db, body);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode, status: 201 });
|
||||
}
|
||||
28
src/server/routes/providers/delete.ts
Normal file
28
src/server/routes/providers/delete.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { getModelsByProviderId } from "../../db/models";
|
||||
import { deleteProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleDeleteProvider(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const modelCount = getModelsByProviderId(db, validated.id);
|
||||
if (modelCount > 0) {
|
||||
return jsonResponse(createApiError("该供应商下存在模型,无法删除", 409), { mode, status: 409 });
|
||||
}
|
||||
|
||||
const result = deleteProvider(db, validated.id);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
22
src/server/routes/providers/disable.ts
Normal file
22
src/server/routes/providers/disable.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { disableProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleDisableProvider(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = disableProvider(db, validated.id);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
22
src/server/routes/providers/enable.ts
Normal file
22
src/server/routes/providers/enable.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { enableProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleEnableProvider(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = enableProvider(db, validated.id);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
22
src/server/routes/providers/get.ts
Normal file
22
src/server/routes/providers/get.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { getProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export function handleGetProvider(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const result = getProvider(db, validated.id);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
25
src/server/routes/providers/list.ts
Normal file
25
src/server/routes/providers/list.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { listProviders } from "../../db/providers";
|
||||
import { jsonResponse } from "../../helpers";
|
||||
import { validatePagination } from "../../middleware";
|
||||
|
||||
export function handleListProviders(req: Request, db: Database, mode: RuntimeMode): Response {
|
||||
const url = new URL(req.url);
|
||||
const pageParam = url.searchParams.get("page");
|
||||
const pageSizeParam = url.searchParams.get("pageSize");
|
||||
const keyword = url.searchParams.get("keyword");
|
||||
|
||||
const pagination = validatePagination(pageParam, pageSizeParam, mode);
|
||||
if (pagination instanceof Response) return pagination;
|
||||
|
||||
const result = listProviders(db, {
|
||||
keyword: keyword ?? undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
34
src/server/routes/providers/test.ts
Normal file
34
src/server/routes/providers/test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode } from "../../../shared/api";
|
||||
|
||||
import { testProviderConnection } from "../../ai/registry";
|
||||
import { getProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleTestProvider(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
const providerResult = getProvider(db, validated.id);
|
||||
if ("error" in providerResult) {
|
||||
return jsonResponse(createApiError(providerResult.error, providerResult.status), {
|
||||
mode,
|
||||
status: providerResult.status,
|
||||
});
|
||||
}
|
||||
|
||||
const provider = providerResult.provider;
|
||||
const testResult = await testProviderConnection({
|
||||
apiKey: provider.apiKey,
|
||||
baseUrl: provider.baseUrl,
|
||||
name: provider.name,
|
||||
type: provider.type,
|
||||
});
|
||||
|
||||
return jsonResponse({ providerTestResponse: testResult }, { mode });
|
||||
}
|
||||
36
src/server/routes/providers/update.ts
Normal file
36
src/server/routes/providers/update.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import type { RuntimeMode, UpdateProviderRequest } from "../../../shared/api";
|
||||
|
||||
import { updateProvider } from "../../db/providers";
|
||||
import { createApiError, jsonResponse } from "../../helpers";
|
||||
import { validateIdParam } from "../../middleware";
|
||||
|
||||
export async function handleUpdateProvider(req: Request, db: Database, mode: RuntimeMode): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const idStr = url.pathname.split("/")[3];
|
||||
|
||||
const validated = validateIdParam(idStr ?? "", mode);
|
||||
if (validated instanceof Response) return validated;
|
||||
|
||||
let body: UpdateProviderRequest;
|
||||
try {
|
||||
body = (await req.json()) as UpdateProviderRequest;
|
||||
} catch {
|
||||
return jsonResponse(createApiError("Invalid JSON body", 400), { mode, status: 400 });
|
||||
}
|
||||
|
||||
if (body.type !== undefined && !["anthropic", "openai", "openai-compatible"].includes(body.type)) {
|
||||
return jsonResponse(createApiError("type must be one of: openai, anthropic, openai-compatible", 400), {
|
||||
mode,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const result = updateProvider(db, validated.id, body);
|
||||
if ("error" in result) {
|
||||
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
|
||||
}
|
||||
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
@@ -43,6 +43,42 @@ export function startServer(options: StartServerOptions) {
|
||||
return handleMeta(mode, resolvedVersion);
|
||||
},
|
||||
},
|
||||
"/api/models": {
|
||||
GET: async (req) => {
|
||||
const { handleListModels } = await import("./routes/models/list");
|
||||
return handleListModels(req, db, mode);
|
||||
},
|
||||
POST: async (req) => {
|
||||
const { handleCreateModel } = await import("./routes/models/create");
|
||||
return handleCreateModel(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/models/:id": {
|
||||
DELETE: async (req) => {
|
||||
const { handleDeleteModel } = await import("./routes/models/delete");
|
||||
return handleDeleteModel(req, db, mode);
|
||||
},
|
||||
GET: async (req) => {
|
||||
const { handleGetModel } = await import("./routes/models/get");
|
||||
return handleGetModel(req, db, mode);
|
||||
},
|
||||
PATCH: async (req) => {
|
||||
const { handleUpdateModel } = await import("./routes/models/update");
|
||||
return handleUpdateModel(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/models/:id/disable": {
|
||||
POST: async (req) => {
|
||||
const { handleDisableModel } = await import("./routes/models/disable");
|
||||
return handleDisableModel(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/models/:id/enable": {
|
||||
POST: async (req) => {
|
||||
const { handleEnableModel } = await import("./routes/models/enable");
|
||||
return handleEnableModel(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/projects": {
|
||||
GET: async (req) => {
|
||||
const { handleListProjects } = await import("./routes/projects/list");
|
||||
@@ -79,6 +115,48 @@ export function startServer(options: StartServerOptions) {
|
||||
return handleRestoreProject(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/providers": {
|
||||
GET: async (req) => {
|
||||
const { handleListProviders } = await import("./routes/providers/list");
|
||||
return handleListProviders(req, db, mode);
|
||||
},
|
||||
POST: async (req) => {
|
||||
const { handleCreateProvider } = await import("./routes/providers/create");
|
||||
return handleCreateProvider(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/providers/:id": {
|
||||
DELETE: async (req) => {
|
||||
const { handleDeleteProvider } = await import("./routes/providers/delete");
|
||||
return handleDeleteProvider(req, db, mode);
|
||||
},
|
||||
GET: async (req) => {
|
||||
const { handleGetProvider } = await import("./routes/providers/get");
|
||||
return handleGetProvider(req, db, mode);
|
||||
},
|
||||
PATCH: async (req) => {
|
||||
const { handleUpdateProvider } = await import("./routes/providers/update");
|
||||
return handleUpdateProvider(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/providers/:id/disable": {
|
||||
POST: async (req) => {
|
||||
const { handleDisableProvider } = await import("./routes/providers/disable");
|
||||
return handleDisableProvider(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/providers/:id/enable": {
|
||||
POST: async (req) => {
|
||||
const { handleEnableProvider } = await import("./routes/providers/enable");
|
||||
return handleEnableProvider(req, db, mode);
|
||||
},
|
||||
},
|
||||
"/api/providers/:id/test": {
|
||||
POST: async (req) => {
|
||||
const { handleTestProvider } = await import("./routes/providers/test");
|
||||
return handleTestProvider(req, db, mode);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user