diff --git a/messages/en-US.json b/messages/en-US.json index 950952e91..52458fcbe 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1926,8 +1926,6 @@ "aiProviderModelsEditTitle": "Edit Model", "aiProviderModelsEditDescription": "Update the model key or configure its budget.", "aiProviderModelsBudgetTab": "Budget", - "aiProviderModelsBudgetDescription": "Configure how this model restricts usage based on spending or token limits", - "aiProviderModelsBudgetUnsaved": "Save this model first to configure its budget.", "aiProviderModelsKeyLabel": "Model Key", "aiProviderModelsKeyRequired": "Enter a model key", "aiProviderModelsKeyDuplicate": "This model key is already on a list", diff --git a/src/app/[orgId]/settings/ai-providers/[niceId]/models/page.tsx b/src/app/[orgId]/settings/ai-providers/[niceId]/models/page.tsx index b85c0076c..db882fac0 100644 --- a/src/app/[orgId]/settings/ai-providers/[niceId]/models/page.tsx +++ b/src/app/[orgId]/settings/ai-providers/[niceId]/models/page.tsx @@ -11,6 +11,7 @@ import { SettingsSectionTitle } from "@app/components/Settings"; import { + persistPendingModelBudgets, type AiProviderModelListItem, type ModelListType } from "@app/components/AiProviderModelListEditor"; @@ -21,7 +22,9 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { aiProviderQueries } from "@app/lib/queries"; +import type { CreateOrEditAiModelResponse } from "@server/routers/aiProvider/types"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; import { useEffect, useMemo, useState } from "react"; @@ -113,8 +116,7 @@ export default function AiProviderModelsPage() { return; } - const toCreate: { modelKey: string; listType: ModelListType }[] = - []; + const toCreate: AiProviderModelListItem[] = []; const toUpdate: { modelId: number; modelKey: string; @@ -159,7 +161,7 @@ export default function AiProviderModelsPage() { continue; } - toCreate.push({ modelKey, listType }); + toCreate.push(item); } const toDelete = existing @@ -167,13 +169,21 @@ export default function AiProviderModelsPage() { .map((model) => model.modelId); await Promise.all([ - ...toCreate.map(({ modelKey, listType }) => - api.put(`/ai-provider/${provider.providerId}/model`, { - modelKey, - name: modelKey, - listType - }) - ), + ...toCreate.map(async (item) => { + const res = await api.put< + AxiosResponse + >(`/ai-provider/${provider.providerId}/model`, { + modelKey: item.modelKey, + name: item.modelKey, + listType: item.listType + }); + await persistPendingModelBudgets({ + api, + orgId: provider.orgId, + modelId: res.data.data.model.modelId, + pendingBudgets: item.pendingBudgets + }); + }), ...toUpdate.map(({ modelId, modelKey, listType }) => api.post(`/ai-model/${modelId}`, { modelKey, diff --git a/src/app/[orgId]/settings/ai-providers/create/page.tsx b/src/app/[orgId]/settings/ai-providers/create/page.tsx index 29f005e8a..5872594ef 100644 --- a/src/app/[orgId]/settings/ai-providers/create/page.tsx +++ b/src/app/[orgId]/settings/ai-providers/create/page.tsx @@ -22,8 +22,8 @@ import HeaderTitle from "@app/components/SettingsSectionTitle"; import { AiProviderAuthTypeSelect } from "@app/components/AiProviderAuthTypeSelect"; import { AiProviderCapabilitiesSelect } from "@app/components/AiProviderCapabilitiesSelect"; import { - type AiProviderModelListItem, - type ModelListType + persistPendingModelBudgets, + type AiProviderModelListItem } from "@app/components/AiProviderModelListEditor"; import { AiProviderModelsLists } from "@app/components/AiProviderModelsLists"; import { @@ -59,7 +59,10 @@ import { import { zodResolver } from "@hookform/resolvers/zod"; import { authTypeRequiresApiKey } from "@app/lib/aiProviderDefaults"; import { aiProviderQueries } from "@app/lib/queries"; -import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types"; +import type { + CreateOrEditAiModelResponse, + CreateOrEditAiProviderResponse +} from "@server/routers/aiProvider/types"; import { useQuery } from "@tanstack/react-query"; import type { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; @@ -157,14 +160,22 @@ export default function CreateAiProviderPage() { async function createModels( providerId: number, - items: { modelKey: string; listType: ModelListType }[] + items: AiProviderModelListItem[] ) { for (const item of items) { - await api.put(`/ai-provider/${providerId}/model`, { + const res = await api.put< + AxiosResponse + >(`/ai-provider/${providerId}/model`, { modelKey: item.modelKey, name: item.modelKey, listType: item.listType }); + await persistPendingModelBudgets({ + api, + orgId, + modelId: res.data.data.model.modelId, + pendingBudgets: item.pendingBudgets + }); } } @@ -210,8 +221,8 @@ export default function CreateAiProviderPage() { const modelItems = [...allowItems, ...blockItems] .map((item) => ({ - modelKey: item.modelKey.trim(), - listType: item.listType + ...item, + modelKey: item.modelKey.trim() })) .filter((item) => item.modelKey); diff --git a/src/components/AiProviderModelListEditor.tsx b/src/components/AiProviderModelListEditor.tsx index 4da6fb10c..032db8f2b 100644 --- a/src/components/AiProviderModelListEditor.tsx +++ b/src/components/AiProviderModelListEditor.tsx @@ -34,12 +34,6 @@ import { PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger -} from "@app/components/ui/tooltip"; import { cn } from "@app/lib/cn"; import { isModelKeyPattern } from "@server/lib/aiModelKeyMatch"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; @@ -56,16 +50,8 @@ import { createApiClient, formatAxiosError } from "@app/lib/api"; import { aiBudgetQueries } from "@app/lib/queries"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; -import { - Asterisk, - BookMarked, - Check, - Globe, - Pencil, - Plus, - Wallet, - XIcon -} from "lucide-react"; +import type { AxiosInstance } from "axios"; +import { Globe, Plus, XIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useForm } from "react-hook-form"; @@ -78,6 +64,7 @@ export type ModelSource = "catalog" | "custom" | "pattern" | "all"; export const ALL_MODELS_KEY = "*"; const COLLAPSED_ROWS = 5; +const GRID_COLUMNS = 2; export type AiProviderModelListItem = { clientId: string; @@ -85,8 +72,32 @@ export type AiProviderModelListItem = { modelKey: string; listType: ModelListType; hasBudget?: boolean; + pendingBudgets?: BudgetRow[]; }; +export async function persistPendingModelBudgets({ + api, + orgId, + modelId, + pendingBudgets +}: { + api: AxiosInstance; + orgId: string; + modelId: number; + pendingBudgets?: BudgetRow[]; +}): Promise { + if (!pendingBudgets || pendingBudgets.length === 0) { + return; + } + await saveBudgetRows({ + api, + orgId, + scope: { type: "model", id: modelId }, + existingBudgets: [], + rows: pendingBudgets + }); +} + export type AiProviderModelListEditorProps = { orgId: string; listType: ModelListType; @@ -138,27 +149,6 @@ function parseBulkKeys(raw: string): string[] { return keys; } -function useModelGridColumns(): number { - const [columns, setColumns] = useState(1); - - useEffect(() => { - const sm = window.matchMedia("(min-width: 640px)"); - const xl = window.matchMedia("(min-width: 1280px)"); - const update = () => { - setColumns(xl.matches ? 3 : sm.matches ? 2 : 1); - }; - update(); - sm.addEventListener("change", update); - xl.addEventListener("change", update); - return () => { - sm.removeEventListener("change", update); - xl.removeEventListener("change", update); - }; - }, []); - - return columns; -} - export function AiProviderModelListEditor({ orgId, listType, @@ -178,8 +168,7 @@ export function AiProviderModelListEditor({ const [listExpanded, setListExpanded] = useState(false); const [clipHeight, setClipHeight] = useState(null); const gridRef = useRef(null); - const columns = useModelGridColumns(); - const collapsedLimit = columns * COLLAPSED_ROWS; + const collapsedLimit = GRID_COLUMNS * COLLAPSED_ROWS; const hasOverflow = items.length > collapsedLimit; const isCollapsed = hasOverflow && !listExpanded; @@ -358,7 +347,7 @@ export function AiProviderModelListEditor({
{source === "all" ? ( - <> - - {t("aiProviderModelsAllLabel")} - - + + {t("aiProviderModelsAllLabel")} + ) : ( {item.modelKey} )}
- -
- - - e.stopPropagation()} - > - - - - {sourceLabel} - - {item.hasBudget ? ( - - - e.stopPropagation()} - > - - - - - {t("aiProviderModelsBudgetConfigured")} - - - ) : null} -
-
- {budgetScope ? ( - <> -

- {t( - "aiProviderModelsBudgetDescription" - )} -

- - - ) : ( -

- {t("aiProviderModelsBudgetUnsaved")} -

- )} +