Browse Source

feat(llm): add CRUD operations for models and providers, including user authorization

- Implemented GET, POST, PUT, and DELETE endpoints for managing LLM models and providers.
- Added user authorization checks to ensure only authorized users can access or modify resources.
- Enhanced provider and model retrieval functions to include user-specific data.
- Updated database service functions to handle user IDs for better data isolation.
- Introduced error handling for invalid inputs and unauthorized actions.
acas
npmrun 1 month ago
parent
commit
1a6cd573cd
  1. 3
      app/components/TopNav.vue
  2. 1
      app/pages/admin.vue
  3. 241
      app/pages/settings.vue
  4. 22
      app/pages/settings/llm-config/index.vue
  5. BIN
      packages/drizzle-pkg/db.sqlite
  6. 13
      packages/drizzle-pkg/lib/schema/llm.ts
  7. 24
      packages/drizzle-pkg/migrations/0013_flaky_marvel_boy.sql
  8. 1674
      packages/drizzle-pkg/migrations/meta/0013_snapshot.json
  9. 7
      packages/drizzle-pkg/migrations/meta/_journal.json
  10. 6
      server/api/llm/models/[providerId].get.ts
  11. 9
      server/api/llm/models/[providerId].post.ts
  12. 6
      server/api/llm/models/detail/[id].delete.ts
  13. 6
      server/api/llm/models/detail/[id].put.ts
  14. 6
      server/api/llm/providers/[id].delete.ts
  15. 6
      server/api/llm/providers/[id].get.ts
  16. 6
      server/api/llm/providers/[id].models.get.ts
  17. 6
      server/api/llm/providers/[id].put.ts
  18. 6
      server/api/llm/providers/index.get.ts
  19. 6
      server/api/llm/providers/index.post.ts
  20. 53
      server/service/llm/index.ts

3
app/components/TopNav.vue

@ -79,7 +79,8 @@ watch(() => route.path, () => {
<NuxtLink v-if="globalConfig.allowRegister" to="/auth/register" class="auth-btn">注册</NuxtLink>
</div>
<div v-else-if="loggedIn && initialized" class="nav-auth">
<NuxtLink to="/admin/dashboard" class="auth-btn">管理</NuxtLink>
<NuxtLink to="/settings/llm-config" class="auth-link">设置</NuxtLink>
<NuxtLink v-if="user?.role === 'admin'" to="/admin/dashboard" class="auth-btn">管理</NuxtLink>
<span class="auth-link" @click="clear()">登出</span>
</div>

1
app/pages/admin.vue

@ -25,7 +25,6 @@ const adminNav: NavItem[] = [
children: [
{ label: '全局配置', to: '/admin/global-config' },
{ label: '用户管理', to: '/admin/users' },
{ label: '模型配置', to: '/admin/llm-config' },
]
},
]

241
app/pages/settings.vue

@ -0,0 +1,241 @@
<script setup lang="ts">
import type { NavItem } from '~/components/admin/AdminSidebarNav.vue'
definePageMeta({
layout: false
})
const { user, clear } = useAuthSession()
const settingsNav: NavItem[] = [
{
label: '模型配置',
to: '/settings/llm-config',
icon: 'lucide:brain-circuit',
},
]
const logout = async () => {
await clear()
navigateTo("/")
}
</script>
<template>
<div class="settings-layout">
<div class="settings-container">
<aside class="settings-sidebar">
<div class="sidebar-header">
<NuxtLink to="/settings/llm-config" class="brand-link">
<span class="brand-icon">
<Icon name="lucide:settings-2" />
</span>
<span class="brand-name">个人设置</span>
</NuxtLink>
</div>
<AdminSidebarNav :nav="settingsNav" />
<div class="sidebar-footer">
<NuxtLink to="/" class="home-link">
<Icon name="lucide:home" />
返回首页
</NuxtLink>
<NuxtLink is="div" class="user-section" v-if="user">
<div class="user-avatar">
{{ user.username?.charAt(0).toUpperCase() }}
</div>
<div class="user-info">
<span class="user-name">{{ user.nickname || user.username }}</span>
<span class="user-role">{{ user.role === "user" ? "普通用户" : "管理员" }}</span>
</div>
<button class="logout-btn" @click.stop.prevent="logout" title="退出登录">
<Icon name="lucide:log-out" />
</button>
</NuxtLink>
</div>
</aside>
<main class="settings-main">
<NuxtPage />
</main>
</div>
</div>
</template>
<style scoped>
.settings-layout {
min-height: 100vh;
background: var(--color-canvas);
}
.settings-container {
display: flex;
}
.settings-sidebar {
width: 260px;
flex-shrink: 0;
background: var(--color-surface-dark);
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
bottom: 0;
height: 100vh;
}
.sidebar-header {
padding: 20px 16px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.brand-link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
}
.brand-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: var(--color-primary);
display: flex;
align-items: center;
justify-content: center;
}
.brand-icon :deep(svg) {
width: 18px;
height: 18px;
color: var(--color-on-primary);
}
.brand-name {
font-size: 16px;
font-weight: 500;
color: var(--color-on-dark);
letter-spacing: -0.01em;
}
.sidebar-footer {
padding: 16px 12px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
flex-direction: column;
gap: 8px;
}
.home-link {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 6px;
text-decoration: none;
color: var(--color-on-dark-soft);
font-size: 13px;
font-weight: 500;
transition: all 0.15s ease;
}
.home-link:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--color-on-dark);
}
.home-link :deep(svg) {
width: 16px;
height: 16px;
}
.user-section {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
cursor: pointer;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 6px;
background: var(--color-primary);
color: var(--color-on-primary);
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.user-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.user-name {
font-size: 13px;
font-weight: 500;
color: var(--color-on-dark);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 11px;
color: var(--color-on-dark-soft);
}
.logout-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 6px;
color: var(--color-on-dark-soft);
cursor: pointer;
transition: all 0.15s ease;
flex-shrink: 0;
}
.logout-btn:hover {
background: rgba(198, 69, 69, 0.2);
color: #c64545;
}
.logout-btn :deep(svg) {
width: 16px;
height: 16px;
}
.settings-main {
flex: 1;
margin-left: 260px;
min-height: 100vh;
background: var(--color-canvas);
}
@media (max-width: 768px) {
.settings-sidebar {
display: none;
}
.settings-main {
margin-left: 0;
}
}
</style>

22
app/pages/admin/llm-config/index.vue → app/pages/settings/llm-config/index.vue

@ -29,7 +29,7 @@ const searchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(20)
const { data, refresh, pending } = await useHttpFetch('/api/admin/llm/providers', {
const { data, refresh, pending } = await useHttpFetch('/api/llm/providers', {
query: computed(() => ({
page: currentPage.value,
pageSize: pageSize.value,
@ -133,7 +133,7 @@ async function handleProviderSave() {
providerLoading.value = true
try {
if (editingProvider.value) {
await $fetch(`/api/admin/llm/providers/${editingProvider.value.id}` as string, {
await $fetch(`/api/llm/providers/${editingProvider.value.id}` as string, {
method: 'PUT' as const,
body: {
...providerForm.value,
@ -142,7 +142,7 @@ async function handleProviderSave() {
})
showToast('供应商更新成功')
} else {
await $fetch('/api/admin/llm/providers', {
await $fetch('/api/llm/providers', {
method: 'POST',
body: providerForm.value,
})
@ -160,7 +160,7 @@ async function handleProviderSave() {
async function toggleProviderStatus(p: LlmProvider) {
const newStatus = p.status === 'active' ? 'disabled' : 'active'
try {
await $fetch(`/api/admin/llm/providers/${p.id}` as string, {
await $fetch(`/api/llm/providers/${p.id}` as string, {
method: 'PUT' as const,
body: { status: newStatus },
})
@ -179,7 +179,7 @@ async function loadModels(providerId: number) {
expandedProviderId.value = providerId
modelsLoading.value = true
try {
const res = await $fetch(`/api/admin/llm/models/${providerId}`)
const res = await $fetch(`/api/llm/models/${providerId}`)
providerModels.value.set(providerId, (res as any)?.data ?? [])
} catch {
providerModels.value.set(providerId, [])
@ -192,7 +192,7 @@ async function fetchAvailableModels(providerId: number) {
availableModelsLoading.value = true
availableModels.value = []
try {
const res = await $fetch(`/api/admin/llm/providers/${providerId}/models`)
const res = await $fetch(`/api/llm/providers/${providerId}/models`)
const models = (res as any)?.data ?? []
if (models.length > 0) {
availableModels.value = models
@ -246,13 +246,13 @@ async function handleModelSave() {
modelLoading.value = true
try {
if (editingModel.value) {
await $fetch(`/api/admin/llm/models/detail/${editingModel.value.id}` as string, {
await $fetch(`/api/llm/models/detail/${editingModel.value.id}` as string, {
method: 'PUT' as const,
body: modelForm.value,
})
showToast('模型更新成功')
} else {
await $fetch(`/api/admin/llm/models/${currentProviderId.value}` as string, {
await $fetch(`/api/llm/models/${currentProviderId.value}` as string, {
method: 'POST' as const,
body: modelForm.value,
})
@ -272,7 +272,7 @@ async function handleModelSave() {
async function toggleModelEnabled(m: LlmModel) {
const newEnabled = m.enabled ? 0 : 1
try {
await $fetch(`/api/admin/llm/models/detail/${m.id}` as string, {
await $fetch(`/api/llm/models/detail/${m.id}` as string, {
method: 'PUT' as const,
body: { enabled: newEnabled },
})
@ -295,13 +295,13 @@ async function handleDelete() {
deleteLoading.value = true
try {
if (deleteTarget.value.type === 'provider') {
await $fetch(`/api/admin/llm/providers/${deleteTarget.value.id}` as string, { method: 'DELETE' as const })
await $fetch(`/api/llm/providers/${deleteTarget.value.id}` as string, { method: 'DELETE' as const })
if (expandedProviderId.value === deleteTarget.value.id) {
expandedProviderId.value = null
}
await refresh()
} else {
await $fetch(`/api/admin/llm/models/detail/${deleteTarget.value.id}` as string, { method: 'DELETE' as const })
await $fetch(`/api/llm/models/detail/${deleteTarget.value.id}` as string, { method: 'DELETE' as const })
if (expandedProviderId.value) {
await loadModels(expandedProviderId.value)
}

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

13
packages/drizzle-pkg/lib/schema/llm.ts

@ -1,4 +1,5 @@
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core";
import { users } from "./auth";
export const LlmProviderStatuses = ["active", "disabled"] as const;
export type LlmProviderStatus = (typeof LlmProviderStatuses)[number];
@ -13,8 +14,11 @@ export const llmProviders = sqliteTable(
"llm_providers",
{
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name", { length: 100 }).notNull().unique(),
slug: text("slug", { length: 100 }).notNull().unique(),
userId: integer("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
name: text("name", { length: 100 }).notNull(),
slug: text("slug", { length: 100 }).notNull(),
baseUrl: text("base_url", { length: 500 }),
parseMode: text("parse_mode", { enum: LlmParseModes }).notNull().default("openai"),
apiKey: text("api_key"),
@ -27,7 +31,8 @@ export const llmProviders = sqliteTable(
.notNull(),
},
(table) => [
index("idx_llm_provider_slug").on(table.slug),
uniqueIndex("idx_llm_provider_user_slug").on(table.userId, table.slug),
index("idx_llm_provider_user").on(table.userId),
index("idx_llm_provider_status").on(table.status),
],
);

24
packages/drizzle-pkg/migrations/0013_flaky_marvel_boy.sql

@ -0,0 +1,24 @@
DROP INDEX `llm_providers_name_unique`;--> statement-breakpoint
DROP INDEX `llm_providers_slug_unique`;--> statement-breakpoint
DROP INDEX `idx_llm_provider_slug`;--> statement-breakpoint
ALTER TABLE `llm_providers` ADD `user_id` integer REFERENCES users(id);--> statement-breakpoint
UPDATE `llm_providers` SET `user_id` = (SELECT MIN(id) FROM `users`) WHERE `user_id` IS NULL;--> statement-breakpoint
CREATE TABLE `_llm_providers_tmp` (
`id` integer PRIMARY KEY AUTOINCREMENT,
`user_id` integer NOT NULL REFERENCES users(id),
`name` text(100) NOT NULL,
`slug` text(100) NOT NULL,
`base_url` text(500),
`parse_mode` text DEFAULT 'openai' NOT NULL,
`api_key` text,
`status` text DEFAULT 'active' NOT NULL,
`description` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);--> statement-breakpoint
INSERT INTO `_llm_providers_tmp` SELECT `id`, `user_id`, `name`, `slug`, `base_url`, `parse_mode`, `api_key`, `status`, `description`, `created_at`, `updated_at` FROM `llm_providers`;--> statement-breakpoint
DROP TABLE `llm_providers`;--> statement-breakpoint
ALTER TABLE `_llm_providers_tmp` RENAME TO `llm_providers`;--> statement-breakpoint
CREATE UNIQUE INDEX `idx_llm_provider_user_slug` ON `llm_providers` (`user_id`,`slug`);--> statement-breakpoint
CREATE INDEX `idx_llm_provider_user` ON `llm_providers` (`user_id`);--> statement-breakpoint
CREATE INDEX `idx_llm_provider_status` ON `llm_providers` (`status`);

1674
packages/drizzle-pkg/migrations/meta/0013_snapshot.json

File diff suppressed because it is too large

7
packages/drizzle-pkg/migrations/meta/_journal.json

@ -92,6 +92,13 @@
"when": 1782963564430,
"tag": "0012_funny_santa_claus",
"breakpoints": true
},
{
"idx": 13,
"version": "6",
"when": 1782969066349,
"tag": "0013_flaky_marvel_boy",
"breakpoints": true
}
]
}

6
server/api/admin/llm/models/[providerId].get.ts → server/api/llm/models/[providerId].get.ts

@ -1,14 +1,14 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { listModelsByProvider } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const providerId = Number(getRouterParam(event, "providerId"));
if (!providerId) {
return R.throwError(400, "无效的供应商ID", null);
}
const result = await listModelsByProvider(providerId);
const result = await listModelsByProvider(providerId, user.id);
return R.success(result);
});

9
server/api/admin/llm/models/[providerId].post.ts → server/api/llm/models/[providerId].post.ts

@ -1,8 +1,8 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { createModel } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const providerId = Number(getRouterParam(event, "providerId"));
if (!providerId) {
@ -16,7 +16,7 @@ export default defineWrappedResponseHandler(async (event) => {
return R.throwError(422, "模型名称和模型ID不能为空", null);
}
const result = await createModel({
const result = await createModel(user.id, {
providerId,
name,
modelId,
@ -25,5 +25,8 @@ export default defineWrappedResponseHandler(async (event) => {
description,
maxTokens,
});
if (!result) {
return R.throwError(403, "无权操作该供应商", null);
}
return R.success(result);
});

6
server/api/admin/llm/models/detail/[id].delete.ts → server/api/llm/models/detail/[id].delete.ts

@ -1,14 +1,14 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { deleteModel } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
return R.throwError(400, "无效的模型ID", null);
}
await deleteModel(id);
await deleteModel(id, user.id);
return R.success(null);
});

6
server/api/admin/llm/models/detail/[id].put.ts → server/api/llm/models/detail/[id].put.ts

@ -1,8 +1,8 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { updateModel } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
@ -12,7 +12,7 @@ export default defineWrappedResponseHandler(async (event) => {
const body = await readBody(event);
const { name, modelId, type, enabled, description, maxTokens } = body;
await updateModel(id, {
await updateModel(id, user.id, {
name,
modelId,
type,

6
server/api/admin/llm/providers/[id].delete.ts → server/api/llm/providers/[id].delete.ts

@ -1,14 +1,14 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { deleteProvider } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
return R.throwError(400, "无效的供应商ID", null);
}
await deleteProvider(id);
await deleteProvider(id, user.id);
return R.success(null);
});

6
server/api/admin/llm/providers/[id].get.ts → server/api/llm/providers/[id].get.ts

@ -1,15 +1,15 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { getProviderWithModels } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
return R.throwError(400, "无效的供应商ID", null);
}
const result = await getProviderWithModels(id);
const result = await getProviderWithModels(id, user.id);
if (!result) {
return R.throwError(404, "供应商不存在", null);
}

6
server/api/admin/llm/providers/[id].models.get.ts → server/api/llm/providers/[id].models.get.ts

@ -1,15 +1,15 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { getProviderById } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
return R.throwError(400, "无效的供应商 ID", null);
}
const provider = await getProviderById(id);
const provider = await getProviderById(id, user.id);
if (!provider) {
return R.throwError(404, "供应商不存在", null);
}

6
server/api/admin/llm/providers/[id].put.ts → server/api/llm/providers/[id].put.ts

@ -1,8 +1,8 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { updateProvider } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
@ -12,7 +12,7 @@ export default defineWrappedResponseHandler(async (event) => {
const body = await readBody(event);
const { name, slug, baseUrl, parseMode, apiKey, status, description } = body;
await updateProvider(id, {
await updateProvider(id, user.id, {
name,
slug,
baseUrl,

6
server/api/admin/llm/providers/index.get.ts → server/api/llm/providers/index.get.ts

@ -1,8 +1,8 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { listProviders } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const query = getQuery(event);
const page = Number(query.page) || 1;
@ -10,6 +10,6 @@ export default defineWrappedResponseHandler(async (event) => {
const search = query.search as string | undefined;
const status = query.status as string | undefined;
const result = await listProviders(page, pageSize, search, status as any);
const result = await listProviders(user.id, page, pageSize, search, status as any);
return R.success(result);
});

6
server/api/admin/llm/providers/index.post.ts → server/api/llm/providers/index.post.ts

@ -1,8 +1,8 @@
import { requireAdmin } from "#server/utils/admin-guard";
import { requireUser } from "#server/utils/context";
import { createProvider } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
await requireAdmin(event);
const user = await requireUser(event);
const body = await readBody(event);
const { name, slug, baseUrl, parseMode, apiKey, status, description } = body;
@ -11,7 +11,7 @@ export default defineWrappedResponseHandler(async (event) => {
return R.throwError(422, "供应商名称和标识不能为空", null);
}
const result = await createProvider({
const result = await createProvider(user.id, {
name,
slug,
baseUrl,

53
server/service/llm/index.ts

@ -5,6 +5,7 @@ import type { LlmParseMode, LlmProviderStatus, LlmModelType } from "drizzle-pkg/
export interface LlmProviderRow {
id: number;
userId: number;
name: string;
slug: string;
baseUrl: string | null;
@ -35,12 +36,13 @@ export interface LlmProviderWithModels extends Omit<LlmProviderRow, "apiKey"> {
}
export async function listProviders(
userId: number,
page = 1,
pageSize = 20,
search?: string,
status?: LlmProviderStatus,
) {
const conditions = [];
const conditions = [eq(llmProviders.userId, userId)];
if (search) {
conditions.push(like(llmProviders.name, `%${search}%`));
}
@ -48,7 +50,7 @@ export async function listProviders(
conditions.push(eq(llmProviders.status, status));
}
const where = conditions.length > 0 ? and(...conditions) : undefined;
const where = and(...conditions);
const [countResult] = await dbGlobal
.select({ count: sql<number>`count(*)` })
@ -74,17 +76,17 @@ export async function listProviders(
return { list: sanitized, total, page, pageSize, totalPages };
}
export async function getProviderById(id: number) {
export async function getProviderById(id: number, userId: number) {
const [row] = await dbGlobal
.select()
.from(llmProviders)
.where(eq(llmProviders.id, id))
.where(and(eq(llmProviders.id, id), eq(llmProviders.userId, userId)))
.limit(1);
return row ?? null;
}
export async function getProviderWithModels(id: number): Promise<LlmProviderWithModels | null> {
const provider = await getProviderById(id);
export async function getProviderWithModels(id: number, userId: number): Promise<LlmProviderWithModels | null> {
const provider = await getProviderById(id, userId);
if (!provider) return null;
const models = await dbGlobal
@ -101,7 +103,7 @@ export async function getProviderWithModels(id: number): Promise<LlmProviderWith
};
}
export async function createProvider(data: {
export async function createProvider(userId: number, data: {
name: string;
slug: string;
baseUrl?: string;
@ -113,6 +115,7 @@ export async function createProvider(data: {
const [inserted] = await dbGlobal
.insert(llmProviders)
.values({
userId,
name: data.name,
slug: data.slug,
baseUrl: data.baseUrl || null,
@ -127,6 +130,7 @@ export async function createProvider(data: {
export async function updateProvider(
id: number,
userId: number,
data: {
name?: string;
slug?: string;
@ -157,14 +161,17 @@ export async function updateProvider(
await dbGlobal
.update(llmProviders)
.set(updates)
.where(eq(llmProviders.id, id));
.where(and(eq(llmProviders.id, id), eq(llmProviders.userId, userId)));
}
export async function deleteProvider(id: number) {
await dbGlobal.delete(llmProviders).where(eq(llmProviders.id, id));
export async function deleteProvider(id: number, userId: number) {
await dbGlobal.delete(llmProviders).where(and(eq(llmProviders.id, id), eq(llmProviders.userId, userId)));
}
export async function listModelsByProvider(providerId: number) {
export async function listModelsByProvider(providerId: number, userId: number) {
const provider = await getProviderById(providerId, userId);
if (!provider) return [];
return dbGlobal
.select()
.from(llmModels)
@ -172,7 +179,7 @@ export async function listModelsByProvider(providerId: number) {
.orderBy(desc(llmModels.createdAt));
}
export async function createModel(data: {
export async function createModel(userId: number, data: {
providerId: number;
name: string;
modelId: string;
@ -181,6 +188,9 @@ export async function createModel(data: {
description?: string;
maxTokens?: number;
}) {
const provider = await getProviderById(data.providerId, userId);
if (!provider) return null;
const [inserted] = await dbGlobal
.insert(llmModels)
.values({
@ -196,8 +206,19 @@ export async function createModel(data: {
return inserted!;
}
export async function getModelById(id: number, userId: number) {
const [row] = await dbGlobal
.select()
.from(llmModels)
.innerJoin(llmProviders, eq(llmModels.providerId, llmProviders.id))
.where(and(eq(llmModels.id, id), eq(llmProviders.userId, userId)))
.limit(1);
return row?.llm_models ?? null;
}
export async function updateModel(
id: number,
userId: number,
data: {
name?: string;
modelId?: string;
@ -207,6 +228,9 @@ export async function updateModel(
maxTokens?: number;
},
) {
const model = await getModelById(id, userId);
if (!model) return;
const updates: Record<string, any> = {};
if (data.name !== undefined) updates.name = data.name;
if (data.modelId !== undefined) updates.modelId = data.modelId;
@ -223,6 +247,9 @@ export async function updateModel(
.where(eq(llmModels.id, id));
}
export async function deleteModel(id: number) {
export async function deleteModel(id: number, userId: number) {
const model = await getModelById(id, userId);
if (!model) return;
await dbGlobal.delete(llmModels).where(eq(llmModels.id, id));
}

Loading…
Cancel
Save