Skip to content

g1t/apps/web/app/routes/workspace/gateway.tsx

123 lines5,346 bytesCodeBlame
1import { BookOpen, KeyRound, Plug } from "lucide-react";
2import { Suspense } from "react";
3import { Await, Link, data } from "react-router";
4
5import type { Route } from "./+types/gateway";
6import { GatewaySkeleton, GatewayTable } from "../../components/gateway";
7import { ButtonLink, CopyLine, EmptyState } from "../../components/ui";
8import { GATEWAY_BASE_URL, GATEWAY_DOCS, GATEWAY_OPENAI_BASE_URL } from "../../lib/gateway";
9import { page } from "../../lib/meta";
10import { billing } from "../../lib/services.server";
11import { getViewer, roleIn } from "../../lib/session.server";
12
13const PAGE_SIZE = 50;
14
15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `AI Gateway · ${params.owner} · g1t` });
17}
18
19export async function loader({ params, context, request }: Route.LoaderArgs) {
20 const viewer = getViewer(context);
21 const role = roleIn(viewer, params.owner);
22 if (!role) throw data(null, { status: 404 });
23 const slug = params.owner.toLowerCase();
24 const before = new URL(request.url).searchParams.get("before")?.trim() || null;
25 // Streamed: the page's frame comes at once, the log's skeleton holds
26 // its place, and the requests fill it in.
27 const log = billing
28 .gatewayRequests(slug, viewer, { limit: PAGE_SIZE, before })
29 .then((result) => (result.ok ? { page: result.value, error: null } : { page: null, error: result.error.message }));
30 return { slug, owner: role === "owner", before, log };
31}
32
33export default function WorkspaceGateway({ loaderData }: Route.ComponentProps) {
34 const { slug, owner, before, log } = loaderData;
35 const base = `/${slug}/-/gateway`;
36 return (
37 <div className="space-y-6">
38 <section className="space-y-3">
39 <p className="max-w-3xl text-sm text-muted">
40 Send your own code's model requests to one of these base URLs, in Anthropic's or OpenAI's format, with one of the
41 workspace's access tokens that has the <code className="font-mono text-xs">models:write</code> scope as the API key.
42 Any model works in either format: Claude and open models on g1t's account are charged at the model's price and paid
43 from AI credit; models on the workspace's own providers under Integrations are only counted. Prompts and answers are
44 never kept.
45 </p>
46 <dl className="grid max-w-3xl gap-3 md:grid-cols-2">
47 <div className="min-w-0">
48 <dt className="mb-1.5 text-xs text-faint">Anthropic format</dt>
49 <dd>
50 <CopyLine text={GATEWAY_BASE_URL} />
51 </dd>
52 </div>
53 <div className="min-w-0">
54 <dt className="mb-1.5 text-xs text-faint">OpenAI format</dt>
55 <dd>
56 <CopyLine text={GATEWAY_OPENAI_BASE_URL} />
57 </dd>
58 </div>
59 </dl>
60 <div className="flex flex-wrap gap-2">
61 {owner && (
62 <ButtonLink variant="quiet" to={`/${slug}/-/tokens`}>
63 <KeyRound size={14} />
64 Access tokens
65 </ButtonLink>
66 )}
67 <ButtonLink variant="quiet" to={`/${slug}/-/integrations`}>
68 <Plug size={14} />
69 Your own providers
70 </ButtonLink>
71 <ButtonLink variant="quiet" to={GATEWAY_DOCS} reloadDocument>
72 <BookOpen size={14} />
73 How to use it
74 </ButtonLink>
75 </div>
76 </section>
77
78 <Suspense fallback={<GatewaySkeleton />}>
79 <Await resolve={log}>
80 {(loaded) =>
81 !loaded.page ? (
82 <p className="rounded-lg border border-danger/40 bg-danger/5 px-4 py-2.5 text-sm">
83 {loaded.error ?? "The requests could not be read. Try again in a minute."}
84 </p>
85 ) : (
86 <div className="space-y-4">
87 {loaded.page.requests.length === 0 ? (
88 <EmptyState title={before ? "No older requests" : "No requests yet"}>
89 {before
90 ? "Everything older has passed the log's 30 days."
91 : "Requests appear here as soon as code sends them to the AI Gateway with one of the workspace's tokens."}
92 </EmptyState>
93 ) : (
94 <GatewayTable requests={loaded.page.requests} />
95 )}
96 <div className="flex flex-wrap items-center gap-4 text-sm">
97 {before && (
98 <Link to={base} className="text-muted hover:text-fg">
99 Newest
100 </Link>
101 )}
102 {loaded.page.next && (
103 <Link to={`${base}?before=${encodeURIComponent(loaded.page.next)}`} className="text-muted hover:text-fg">
104 Older
105 </Link>
106 )}
107 <span className="grow" />
108 <span className="text-xs text-faint">
109 Kept {loaded.page.retentionDays} days. What requests were charged is also on{" "}
110 <Link to={`/${slug}/-/usage?product=gateway`} className="hover:text-fg hover:underline">
111 Usage
112 </Link>
113 .
114 </span>
115 </div>
116 </div>
117 )
118 }
119 </Await>
120 </Suspense>
121 </div>
122 );
123}