Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/llm-model-pricing-unit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Track a pricing unit (tokens, images, characters, etc.) per LLM model in the model registry, seeded for the default catalog and selectable in the admin model form.
4 changes: 3 additions & 1 deletion apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const UpdateModelSchema = z.object({
maxOutputTokens: z.number().int().nullable().optional(),
capabilities: z.array(z.string()).optional(),
isHidden: z.boolean().optional(),
pricingUnit: z.string().nullable().optional(),
pricingTiers: z
.array(
z.object({
Expand Down Expand Up @@ -86,7 +87,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: "Invalid request body", details: parsed.error.issues }, { status: 400 });
}

const { modelName, matchPattern, startDate, pricingTiers, provider, description, contextWindow, maxOutputTokens, capabilities, isHidden } = parsed.data;
const { modelName, matchPattern, startDate, pricingTiers, provider, description, contextWindow, maxOutputTokens, capabilities, isHidden, pricingUnit } = parsed.data;

// Validate regex if provided — strip (?i) POSIX flag since our registry handles it
if (matchPattern) {
Expand All @@ -112,6 +113,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
...(maxOutputTokens !== undefined && { maxOutputTokens }),
...(capabilities !== undefined && { capabilities }),
...(isHidden !== undefined && { isHidden }),
...(pricingUnit !== undefined && { pricingUnit }),
},
});

Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/routes/admin.api.v1.llm-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const CreateModelSchema = z.object({
maxOutputTokens: z.number().int().optional(),
capabilities: z.array(z.string()).optional(),
isHidden: z.boolean().optional(),
pricingUnit: z.string().optional(),
pricingTiers: z.array(
z.object({
name: z.string().min(1),
Expand Down Expand Up @@ -80,7 +81,7 @@ export async function action({ request }: ActionFunctionArgs) {
return json({ error: "Invalid request body", details: parsed.error.issues }, { status: 400 });
}

const { modelName, matchPattern, startDate, source, pricingTiers, provider, description, contextWindow, maxOutputTokens, capabilities, isHidden } = parsed.data;
const { modelName, matchPattern, startDate, source, pricingTiers, provider, description, contextWindow, maxOutputTokens, capabilities, isHidden, pricingUnit } = parsed.data;

// Validate regex pattern — strip (?i) POSIX flag since our registry handles it
try {
Expand All @@ -105,6 +106,7 @@ export async function action({ request }: ActionFunctionArgs) {
maxOutputTokens: maxOutputTokens ?? null,
capabilities: capabilities ?? [],
isHidden: isHidden ?? false,
pricingUnit: pricingUnit ?? null,
},
});

Expand Down
24 changes: 23 additions & 1 deletion apps/webapp/app/routes/admin.llm-models.$modelId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const SaveSchema = z.object({
maxOutputTokens: z.string().optional(),
capabilities: z.string().optional(),
isHidden: z.string().optional(),
pricingUnit: z.string().optional(),
});

export const action = dashboardAction(
Expand Down Expand Up @@ -101,7 +102,7 @@ export const action = dashboardAction(
}

// Update model
const { provider, description, contextWindow, maxOutputTokens, capabilities, isHidden } = parsed.data;
const { provider, description, contextWindow, maxOutputTokens, capabilities, isHidden, pricingUnit } = parsed.data;
await prisma.llmModel.update({
where: { id: modelId },
data: {
Expand All @@ -113,6 +114,7 @@ export const action = dashboardAction(
maxOutputTokens: maxOutputTokens ? parseInt(maxOutputTokens) || null : null,
capabilities: capabilities ? capabilities.split(",").map((s) => s.trim()).filter(Boolean) : [],
isHidden: isHidden === "on",
pricingUnit: pricingUnit || null,
},
});

Expand Down Expand Up @@ -158,6 +160,7 @@ export default function AdminLlmModelDetailRoute() {
const [maxOutputTokens, setMaxOutputTokens] = useState(model.maxOutputTokens?.toString() ?? "");
const [capabilities, setCapabilities] = useState(model.capabilities?.join(", ") ?? "");
const [isHidden, setIsHidden] = useState(model.isHidden ?? false);
const [pricingUnit, setPricingUnit] = useState(model.pricingUnit ?? "");
const [testInput, setTestInput] = useState("");
const [tiers, setTiers] = useState(() =>
model.pricingTiers.map((t) => ({
Expand Down Expand Up @@ -325,6 +328,23 @@ export default function AdminLlmModelDetailRoute() {
</div>
</div>

<div className="space-y-1">
<label className="text-xs font-medium text-text-dimmed">Pricing Unit</label>
<select
name="pricingUnit"
value={pricingUnit}
onChange={(e) => setPricingUnit(e.target.value)}
className="w-full rounded border border-grid-dimmed bg-charcoal-750 px-2 py-1.5 text-sm text-text-bright"
>
<option value="">(unset)</option>
{PRICING_UNITS.map((u) => (
<option key={u} value={u}>
{u}
</option>
))}
</select>
</div>

<label className="flex items-center gap-2 text-xs text-text-dimmed">
<input
type="checkbox"
Expand Down Expand Up @@ -425,6 +445,8 @@ type TierData = {
prices: Record<string, number>;
};

const PRICING_UNITS = ["tokens", "characters", "images", "minutes", "requests", "free", "not_findable"];

const COMMON_USAGE_TYPES = [
"input",
"output",
Expand Down
24 changes: 23 additions & 1 deletion apps/webapp/app/routes/admin.llm-models.new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const CreateSchema = z.object({
maxOutputTokens: z.string().optional(),
capabilities: z.string().optional(),
isHidden: z.string().optional(),
pricingUnit: z.string().optional(),
});

export const action = dashboardAction(
Expand Down Expand Up @@ -65,7 +66,7 @@ export const action = dashboardAction(
return typedjson({ error: "Invalid pricing tiers JSON" }, { status: 400 });
}

const { provider, description, contextWindow, maxOutputTokens, capabilities, isHidden } = parsed.data;
const { provider, description, contextWindow, maxOutputTokens, capabilities, isHidden, pricingUnit } = parsed.data;

const model = await prisma.llmModel.create({
data: {
Expand All @@ -79,6 +80,7 @@ export const action = dashboardAction(
maxOutputTokens: maxOutputTokens ? parseInt(maxOutputTokens) || null : null,
capabilities: capabilities ? capabilities.split(",").map((s) => s.trim()).filter(Boolean) : [],
isHidden: isHidden === "on",
pricingUnit: pricingUnit || null,
},
});

Expand Down Expand Up @@ -118,6 +120,7 @@ export default function AdminLlmModelNewRoute() {
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<TierData[]>([
{ name: "Standard", isDefault: true, priority: 0, conditions: [], prices: { input: 0, output: 0 } },
Expand Down Expand Up @@ -279,6 +282,23 @@ export default function AdminLlmModelNewRoute() {
</div>
</div>

<div className="space-y-1">
<label className="text-xs font-medium text-text-dimmed">Pricing Unit</label>
<select
name="pricingUnit"
value={pricingUnit}
onChange={(e) => setPricingUnit(e.target.value)}
className="w-full rounded border border-grid-dimmed bg-charcoal-750 px-2 py-1.5 text-sm text-text-bright"
>
<option value="">(unset)</option>
{PRICING_UNITS.map((u) => (
<option key={u} value={u}>
{u}
</option>
))}
</select>
</div>

<label className="flex items-center gap-2 text-xs text-text-dimmed">
<input
type="checkbox"
Expand Down Expand Up @@ -366,6 +386,8 @@ type TierData = {
prices: Record<string, number>;
};

const PRICING_UNITS = ["tokens", "characters", "images", "minutes", "requests", "free", "not_findable"];

const COMMON_USAGE_TYPES = [
"input",
"output",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "public"."llm_models" ADD COLUMN "pricing_unit" TEXT;
2 changes: 2 additions & 0 deletions internal-packages/database/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2909,6 +2909,8 @@ model LlmModel {
capabilities String[] @default([]) @map("capabilities")
isHidden Boolean @default(false) @map("is_hidden")
baseModelName String? @map("base_model_name")
// "tokens", "images", "characters", "minutes", "requests", "free", "not_findable"; null until researched
pricingUnit String? @map("pricing_unit")

pricingTiers LlmPricingTier[]
prices LlmPrice[]
Expand Down
1 change: 1 addition & 0 deletions internal-packages/llm-model-catalog/src/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function seedLlmPricing(prisma: PrismaClient): Promise<{
capabilities: catalog?.capabilities ?? [],
isHidden: catalog?.isHidden ?? false,
baseModelName: catalog?.baseModelName ?? null,
pricingUnit: "tokens",
},
});

Expand Down
1 change: 1 addition & 0 deletions internal-packages/llm-model-catalog/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export async function syncLlmCatalog(prisma: PrismaClient): Promise<{
catalog?.baseModelName === undefined
? existing.baseModelName
: catalog.baseModelName,
pricingUnit: "tokens",
Comment thread
ericallam marked this conversation as resolved.
Outdated
},
});

Expand Down
Loading