diff --git a/app/components.json b/app/components.json
index e747c75..81d4ba0 100644
--- a/app/components.json
+++ b/app/components.json
@@ -1,25 +1,25 @@
{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "base-lyra",
- "rsc": true,
- "tsx": true,
- "tailwind": {
- "config": "",
- "css": "app/globals.css",
- "baseColor": "gray",
- "cssVariables": true,
- "prefix": ""
- },
- "iconLibrary": "lucide",
- "rtl": false,
- "aliases": {
- "components": "~/components",
- "utils": "~/lib/utils",
- "ui": "~/components/ui",
- "lib": "~/lib",
- "hooks": "~/hooks"
- },
- "menuColor": "default",
- "menuAccent": "subtle",
- "registries": {}
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "base-lyra",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "app/globals.css",
+ "baseColor": "gray",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "rtl": false,
+ "aliases": {
+ "components": "~/components",
+ "utils": "~/lib/utils",
+ "ui": "~/components/ui",
+ "lib": "~/lib",
+ "hooks": "~/hooks"
+ },
+ "menuColor": "default",
+ "menuAccent": "subtle",
+ "registries": {}
}
diff --git a/app/drizzle.config.ts b/app/drizzle.config.ts
index 01bbe99..8a20bb0 100644
--- a/app/drizzle.config.ts
+++ b/app/drizzle.config.ts
@@ -8,6 +8,6 @@ export default {
dbCredentials: {
url: env.DATABASE_URL,
},
- strict: true
+ strict: true,
//tablesFilter: ["analytics-app_*"],
} satisfies Config;
diff --git a/app/drizzle/meta/0000_snapshot.json b/app/drizzle/meta/0000_snapshot.json
index 1648023..f67a6ac 100644
--- a/app/drizzle/meta/0000_snapshot.json
+++ b/app/drizzle/meta/0000_snapshot.json
@@ -1,32 +1,32 @@
{
- "id": "00000000-0000-0000-0000-000000000000",
- "prevId": "",
- "version": "7",
- "dialect": "postgresql",
- "tables": {},
- "enums": {},
- "schemas": {},
- "sequences": {
- "public.company_id_seq": {
- "name": "company_id_seq",
- "schema": "public",
- "startWith": "1",
- "minValue": "1",
- "maxValue": "9223372036854775807",
- "increment": "1",
- "cycle": false,
- "cache": "1"
- }
- },
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "schemas": {},
- "tables": {},
- "columns": {}
- },
- "internal": {
- "tables": {}
- }
-}
\ No newline at end of file
+ "id": "00000000-0000-0000-0000-000000000000",
+ "prevId": "",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {},
+ "enums": {},
+ "schemas": {},
+ "sequences": {
+ "public.company_id_seq": {
+ "name": "company_id_seq",
+ "schema": "public",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "9223372036854775807",
+ "increment": "1",
+ "cycle": false,
+ "cache": "1"
+ }
+ },
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {}
+ }
+}
diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json
index e8c7f81..a72ae83 100644
--- a/app/drizzle/meta/_journal.json
+++ b/app/drizzle/meta/_journal.json
@@ -1,13 +1,13 @@
{
- "version": "7",
- "dialect": "postgresql",
- "entries": [
- {
- "idx": 0,
- "version": "7",
- "when": 1772187438831,
- "tag": "0000_optimal_rachel_grey",
- "breakpoints": true
- }
- ]
-}
\ No newline at end of file
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1772187438831,
+ "tag": "0000_optimal_rachel_grey",
+ "breakpoints": true
+ }
+ ]
+}
diff --git a/app/drizzle/relations.ts b/app/drizzle/relations.ts
index 9417534..4111a88 100644
--- a/app/drizzle/relations.ts
+++ b/app/drizzle/relations.ts
@@ -1,29 +1,29 @@
import { relations } from "drizzle-orm/relations";
-import { apikeys, requests, users, company } from "./schema";
+import { apikeys, company, requests, users } from "./schema";
-export const requestsRelations = relations(requests, ({one}) => ({
+export const requestsRelations = relations(requests, ({ one }) => ({
apikey: one(apikeys, {
fields: [requests.apiKeyId],
- references: [apikeys.uuid]
+ references: [apikeys.uuid],
}),
}));
-export const apikeysRelations = relations(apikeys, ({one, many}) => ({
+export const apikeysRelations = relations(apikeys, ({ one, many }) => ({
requests: many(requests),
user: one(users, {
fields: [apikeys.owner],
- references: [users.id]
+ references: [users.id],
}),
}));
-export const usersRelations = relations(users, ({one, many}) => ({
+export const usersRelations = relations(users, ({ one, many }) => ({
apikeys: many(apikeys),
company: one(company, {
fields: [users.companyId],
- references: [company.id]
+ references: [company.id],
}),
}));
-export const companyRelations = relations(company, ({many}) => ({
+export const companyRelations = relations(company, ({ many }) => ({
users: many(users),
-}));
\ No newline at end of file
+}));
diff --git a/app/drizzle/schema.ts b/app/drizzle/schema.ts
index fef3438..e9ab5f8 100644
--- a/app/drizzle/schema.ts
+++ b/app/drizzle/schema.ts
@@ -1,7 +1,18 @@
-import { pgTable, varchar, bigint, timestamp, text, jsonb, foreignKey, integer, boolean, primaryKey, date, char, pgEnum } from "drizzle-orm/pg-core"
-import { sql } from "drizzle-orm"
-
-
+import {
+ bigint,
+ boolean,
+ char,
+ date,
+ foreignKey,
+ integer,
+ jsonb,
+ pgEnum,
+ pgTable,
+ primaryKey,
+ text,
+ timestamp,
+ varchar,
+} from "drizzle-orm/pg-core";
export const atlasSchemaRevisions = pgTable("atlas_schema_revisions", {
version: varchar().primaryKey().notNull(),
@@ -12,7 +23,10 @@ export const atlasSchemaRevisions = pgTable("atlas_schema_revisions", {
applied: bigint({ mode: "number" }).default(0).notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
total: bigint({ mode: "number" }).default(0).notNull(),
- executedAt: timestamp("executed_at", { withTimezone: true, mode: 'string' }).notNull(),
+ executedAt: timestamp("executed_at", {
+ withTimezone: true,
+ mode: "string",
+ }).notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
executionTime: bigint("execution_time", { mode: "number" }).notNull(),
error: text(),
@@ -24,55 +38,80 @@ export const atlasSchemaRevisions = pgTable("atlas_schema_revisions", {
export const company = pgTable("company", {
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
- id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity({ name: "company_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 9223372036854775807, cache: 1 }),
+ id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity({
+ name: "company_id_seq",
+ startWith: 1,
+ increment: 1,
+ minValue: 1,
+ // biome-ignore lint/correctness/noPrecisionLoss: matches Postgres BIGINT max.
+ maxValue: 9223372036854775807,
+ cache: 1,
+ }),
companyName: varchar("company_name", { length: 255 }).notNull(),
});
-export const requests = pgTable("requests", {
- id: varchar({ length: 255 }).primaryKey().notNull(),
- requestTime: timestamp("request_time", { withTimezone: true, mode: 'string' }).defaultNow(),
- model: varchar({ length: 255 }),
- apiKeyId: varchar("api_key_id", { length: 255 }).notNull(),
- inputTokenCount: integer("input_token_count").default(0).notNull(),
- cachedInputTokenCount: integer("cached_input_token_count").default(0).notNull(),
- outputTokenCount: integer("output_token_count").default(0).notNull(),
- snapshotVersion: varchar("snapshot_version", { length: 255 }),
- isApproximated: boolean("is_approximated").default(false).notNull(),
-}, (table) => [
- foreignKey({
+export const requests = pgTable(
+ "requests",
+ {
+ id: varchar({ length: 255 }).primaryKey().notNull(),
+ requestTime: timestamp("request_time", {
+ withTimezone: true,
+ mode: "string",
+ }).defaultNow(),
+ model: varchar({ length: 255 }),
+ apiKeyId: varchar("api_key_id", { length: 255 }).notNull(),
+ inputTokenCount: integer("input_token_count").default(0).notNull(),
+ cachedInputTokenCount: integer("cached_input_token_count")
+ .default(0)
+ .notNull(),
+ outputTokenCount: integer("output_token_count").default(0).notNull(),
+ snapshotVersion: varchar("snapshot_version", { length: 255 }),
+ isApproximated: boolean("is_approximated").default(false).notNull(),
+ },
+ (table) => [
+ foreignKey({
columns: [table.apiKeyId],
foreignColumns: [apikeys.uuid],
- name: "requests_api_key_id_fkey"
+ name: "requests_api_key_id_fkey",
}),
-]);
+ ],
+);
-export const apikeys = pgTable("apikeys", {
- uuid: varchar({ length: 255 }).primaryKey().notNull(),
- apikey: varchar({ length: 255 }).notNull(),
- owner: varchar({ length: 255 }).notNull(),
- aiapi: varchar({ length: 255 }),
- description: varchar({ length: 255 }),
-}, (table) => [
- foreignKey({
+export const apikeys = pgTable(
+ "apikeys",
+ {
+ uuid: varchar({ length: 255 }).primaryKey().notNull(),
+ apikey: varchar({ length: 255 }).notNull(),
+ owner: varchar({ length: 255 }).notNull(),
+ aiapi: varchar({ length: 255 }),
+ description: varchar({ length: 255 }),
+ },
+ (table) => [
+ foreignKey({
columns: [table.owner],
foreignColumns: [users.id],
- name: "apikeys_owner_fkey"
+ name: "apikeys_owner_fkey",
}),
-]);
+ ],
+);
-export const users = pgTable("users", {
- id: varchar({ length: 255 }).primaryKey().notNull(),
- // You can use { mode: "bigint" } if numbers are exceeding js number limitations
- companyId: bigint("company_id", { mode: "number" }),
- name: varchar({ length: 255 }),
- isAdmin: boolean("is_admin"),
-}, (table) => [
- foreignKey({
+export const users = pgTable(
+ "users",
+ {
+ id: varchar({ length: 255 }).primaryKey().notNull(),
+ // You can use { mode: "bigint" } if numbers are exceeding js number limitations
+ companyId: bigint("company_id", { mode: "number" }),
+ name: varchar({ length: 255 }),
+ isAdmin: boolean("is_admin"),
+ },
+ (table) => [
+ foreignKey({
columns: [table.companyId],
foreignColumns: [company.id],
- name: "users_company_id_fkey"
+ name: "users_company_id_fkey",
}),
-]);
+ ],
+);
export const models = pgTable("models", {
id: varchar({ length: 255 }).primaryKey().notNull(),
@@ -80,15 +119,29 @@ export const models = pgTable("models", {
export const costUnit = pgEnum("cost_unit", ["1M", "1K"]);
-export const costs = pgTable("costs", {
- model: varchar({ length: 255 }).notNull(),
- price: integer().notNull(),
- validFrom: date("valid_from").defaultNow().notNull(),
- tokenType: varchar("token_type", { length: 255 }).notNull(),
- unitOfMessure: costUnit("unit_of_messure"),
- isRegional: boolean("is_regional").notNull(),
- backendName: varchar("backend_name", { length: 255 }).notNull(),
- currency: char({ length: 3 }),
-}, (table) => [
- primaryKey({ columns: [table.model, table.price, table.validFrom, table.tokenType, table.isRegional, table.backendName], name: "costs_pkey"}),
-]);
+export const costs = pgTable(
+ "costs",
+ {
+ model: varchar({ length: 255 }).notNull(),
+ price: integer().notNull(),
+ validFrom: date("valid_from").defaultNow().notNull(),
+ tokenType: varchar("token_type", { length: 255 }).notNull(),
+ unitOfMessure: costUnit("unit_of_messure"),
+ isRegional: boolean("is_regional").notNull(),
+ backendName: varchar("backend_name", { length: 255 }).notNull(),
+ currency: char({ length: 3 }),
+ },
+ (table) => [
+ primaryKey({
+ columns: [
+ table.model,
+ table.price,
+ table.validFrom,
+ table.tokenType,
+ table.isRegional,
+ table.backendName,
+ ],
+ name: "costs_pkey",
+ }),
+ ],
+);
diff --git a/app/next.config.js b/app/next.config.js
index 3e34b6e..57a6828 100644
--- a/app/next.config.js
+++ b/app/next.config.js
@@ -6,7 +6,7 @@ import "./src/env.js";
/** @type {import("next").NextConfig} */
const config = {
- output: "standalone",
+ output: "standalone",
};
export default config;
diff --git a/app/src/app/admin/costs/costs-client.tsx b/app/src/app/admin/costs/costs-client.tsx
index 5c0fe71..9f90d8b 100644
--- a/app/src/app/admin/costs/costs-client.tsx
+++ b/app/src/app/admin/costs/costs-client.tsx
@@ -1,14 +1,16 @@
"use client";
import { useMemo, useState } from "react";
-
+import { toast } from "sonner";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
-import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select";
+import {
+ NativeSelect,
+ NativeSelectOption,
+} from "~/components/ui/native-select";
+import { type CostUnit, costUnitOptions } from "~/lib/costs";
import { api } from "~/trpc/react";
import { CostsTable } from "./costs-table";
-import { toast } from "sonner";
-import { costUnitOptions, type CostUnit } from "~/lib/costs";
const defaultUnit = costUnitOptions[0];
const defaultBackend = "openai";
@@ -81,8 +83,8 @@ export function CostsClient() {
return (
-
Kostenverwaltung
-
+
Kostenverwaltung
+
Neue Preise anlegen oder bestehende Einträge aktualisieren.
@@ -91,55 +93,86 @@ export function CostsClient() {
-
Neuen Eintrag anlegen
-
- Standardwerte: Backend {defaultBackend}, Einheit {defaultUnit}, Währung {defaultCurrency}.
+
Neuen Eintrag anlegen
+
+ Standardwerte: Backend {defaultBackend}, Einheit {defaultUnit},
+ Währung {defaultCurrency}.
diff --git a/app/src/app/admin/costs/costs-table.tsx b/app/src/app/admin/costs/costs-table.tsx
index 4f410e0..0d465c6 100644
--- a/app/src/app/admin/costs/costs-table.tsx
+++ b/app/src/app/admin/costs/costs-table.tsx
@@ -1,6 +1,5 @@
"use client";
-import { useEffect, useMemo, useState } from "react";
import {
type ColumnDef,
flexRender,
@@ -10,7 +9,8 @@ import {
type SortingState,
useReactTable,
} from "@tanstack/react-table";
-import { ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react";
+import { ChevronDown, ChevronsUpDown, ChevronUp } from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
import { Button } from "~/components/ui/button";
import {
@@ -23,6 +23,10 @@ import {
DialogTrigger,
} from "~/components/ui/dialog";
import { Input } from "~/components/ui/input";
+import {
+ NativeSelect,
+ NativeSelectOption,
+} from "~/components/ui/native-select";
import {
Table,
TableBody,
@@ -31,8 +35,7 @@ import {
TableHeader,
TableRow,
} from "~/components/ui/table";
-import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select";
-import { costUnitOptions, type CostUnit } from "~/lib/costs";
+import { type CostUnit, costUnitOptions } from "~/lib/costs";
export type CostRow = {
model: string;
@@ -108,11 +111,15 @@ function CostEditDialog({
const [unitOfMessure, setUnitOfMessure] = useState
(
costUnitOptions.includes((row.unitOfMessure ?? "") as CostUnit)
? ((row.unitOfMessure ?? defaultUnit) as CostUnit)
- : defaultUnit
+ : defaultUnit,
);
const [isRegional, setIsRegional] = useState(row.isRegional);
const [backendName, setBackendName] = useState(row.backendName);
const [currency, setCurrency] = useState(row.currency ?? "");
+ const fieldKey = `${row.model}-${row.tokenType}-${row.price}`.replace(
+ /[^a-zA-Z0-9-_]/g,
+ "-",
+ );
const priceValue = useMemo(() => Number.parseInt(price, 10), [price]);
const canSubmit =
@@ -132,7 +139,7 @@ function CostEditDialog({
backendName: row.backendName,
currency: row.currency ?? null,
}),
- [row]
+ [row],
);
const today = useMemo(() => todayString(), []);
@@ -140,7 +147,6 @@ function CostEditDialog({
return (