import { Form, useActionData, useSearchParams } from "@remix-run/react"; import { redirect } from "@remix-run/server-runtime"; import { typedjson } from "remix-typedjson"; import { z } from "zod"; import { useState } from "react"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Input } from "~/components/primitives/Input"; import { prisma } from "~/db.server"; import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; import { generateFriendlyId } from "~/v3/friendlyIdentifiers"; import { llmPricingRegistry } from "~/v3/llmPricingRegistry.server"; export const loader = dashboardLoader({ authorization: { requireSuper: true } }, async () => { return typedjson({}); }); const CreateSchema = z.object({ modelName: z.string().min(1), matchPattern: z.string().min(1), pricingTiersJson: z.string(), provider: z.string().optional(), description: z.string().optional(), contextWindow: z.string().optional(), maxOutputTokens: z.string().optional(), capabilities: z.string().optional(), isHidden: z.string().optional(), pricingUnit: z.string().optional(), }); export const action = dashboardAction( { authorization: { requireSuper: true } }, async ({ request }) => { const formData = await request.formData(); const raw = Object.fromEntries(formData); console.log("[admin] create model form data:", JSON.stringify(raw).slice(0, 500)); const parsed = CreateSchema.safeParse(raw); if (!parsed.success) { console.log("[admin] create model validation error:", JSON.stringify(parsed.error.issues)); return typedjson( { error: "Invalid form data", details: parsed.error.issues }, { status: 400 } ); } const { modelName, matchPattern, pricingTiersJson } = parsed.data; // Validate regex — strip (?i) POSIX flag since our registry handles it try { const testPattern = matchPattern.startsWith("(?i)") ? matchPattern.slice(4) : matchPattern; new RegExp(testPattern); } catch { return typedjson({ error: "Invalid regex in matchPattern" }, { status: 400 }); } let pricingTiers: Array<{ name: string; isDefault: boolean; priority: number; conditions: Array<{ usageDetailPattern: string; operator: string; value: number }>; prices: Record; }>; try { pricingTiers = JSON.parse(pricingTiersJson) as typeof pricingTiers; } catch { return typedjson({ error: "Invalid pricing tiers JSON" }, { status: 400 }); } const { provider, description, contextWindow, maxOutputTokens, capabilities, isHidden, pricingUnit, } = parsed.data; const model = await prisma.llmModel.create({ data: { friendlyId: generateFriendlyId("llm_model"), modelName, matchPattern, source: "admin", provider: provider || null, description: description || null, contextWindow: contextWindow ? parseInt(contextWindow) || null : null, maxOutputTokens: maxOutputTokens ? parseInt(maxOutputTokens) || null : null, capabilities: capabilities ? capabilities .split(",") .map((s) => s.trim()) .filter(Boolean) : [], isHidden: isHidden === "on", pricingUnit: pricingUnit || null, }, }); for (const tier of pricingTiers) { await prisma.llmPricingTier.create({ data: { modelId: model.id, name: tier.name, isDefault: tier.isDefault, priority: tier.priority, conditions: tier.conditions, prices: { create: Object.entries(tier.prices).map(([usageType, price]) => ({ modelId: model.id, usageType, price, })), }, }, }); } await llmPricingRegistry?.reload(); return redirect(`/admin/llm-models/${model.friendlyId}`); } ); export default function AdminLlmModelNewRoute() { const actionData = useActionData<{ error?: string; details?: unknown[] }>(); const [params] = useSearchParams(); const initialModelName = params.get("modelName") ?? ""; const [modelName, setModelName] = useState(initialModelName); const [matchPattern, setMatchPattern] = useState(""); const [provider, setProvider] = useState(""); const [description, setDescription] = useState(""); const [contextWindow, setContextWindow] = useState(""); const [maxOutputTokens, setMaxOutputTokens] = useState(""); const [capabilities, setCapabilities] = useState(""); const [isHidden, setIsHidden] = useState(false); const [pricingUnit, setPricingUnit] = useState("tokens"); const [testInput, setTestInput] = useState(""); const [tiers, setTiers] = useState([ { name: "Standard", isDefault: true, priority: 0, conditions: [], prices: { input: 0, output: 0 }, }, ]); let testResult: boolean | null = null; if (testInput && matchPattern) { try { const pattern = matchPattern.startsWith("(?i)") ? matchPattern.slice(4) : matchPattern; testResult = new RegExp(pattern, "i").test(testInput); } catch { testResult = null; } } // Auto-generate match pattern from model name function autoPattern() { if (modelName) { const escaped = modelName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); setMatchPattern(`(?i)^(${escaped})$`); } } return (

New LLM Model

Back to list
setModelName(e.target.value)} variant="medium" fullWidth placeholder="e.g. gemini-3-flash" />
setMatchPattern(e.target.value)} variant="medium" fullWidth className="font-mono text-xs" placeholder="(?i)^(google/)?(gemini-3-flash)$" />
setTestInput(e.target.value)} placeholder="Type a model name to test..." variant="medium" fullWidth /> {testInput && ( {testResult ? "Match" : "No match"} )}
{/* Catalog metadata */}
setProvider(e.target.value)} variant="medium" fullWidth placeholder="openai, anthropic, google" />
setContextWindow(e.target.value)} variant="medium" fullWidth placeholder="128000" />
setDescription(e.target.value)} variant="medium" fullWidth placeholder="Brief model description" />
setMaxOutputTokens(e.target.value)} variant="medium" fullWidth placeholder="16384" />
setCapabilities(e.target.value)} variant="medium" fullWidth placeholder="vision, tool_use, streaming, json_mode" />
{/* Pricing tiers */}
{tiers.map((tier, tierIdx) => ( { const next = [...tiers]; next[tierIdx] = updated; setTiers(next); }} onRemove={() => setTiers(tiers.filter((_, i) => i !== tierIdx))} /> ))}
{actionData?.error && (
{actionData.error} {actionData.details && (
                    {JSON.stringify(actionData.details, null, 2)}
                  
)}
)}
Cancel
); } // --------------------------------------------------------------------------- // Shared tier editor (duplicated from detail page — could be extracted later) // --------------------------------------------------------------------------- type TierData = { name: string; isDefault: boolean; priority: number; conditions: Array<{ usageDetailPattern: string; operator: string; value: number }>; prices: Record; }; const PRICING_UNITS = [ "tokens", "characters", "images", "minutes", "requests", "free", "not_findable", ]; const COMMON_USAGE_TYPES = [ "input", "output", "input_cached_tokens", "cache_creation_input_tokens", "reasoning_tokens", ]; function TierEditor({ tier, onChange, onRemove, }: { tier: TierData; onChange: (t: TierData) => void; onRemove: () => void; }) { const [newUsageType, setNewUsageType] = useState(""); return (
onChange({ ...tier, name: e.target.value })} placeholder="Tier name" />
Prices (per token)
{Object.entries(tier.prices).map(([usageType, price]) => (
{usageType} { const val = parseFloat(e.target.value); if (!isNaN(val)) { onChange({ ...tier, prices: { ...tier.prices, [usageType]: val } }); } }} />
))}
{newUsageType && ( )}
); }