Skip to content

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

107 lines4,679 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1import { BookOpen, KeyRound } 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 } 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 in Anthropic's Messages format to this base URL, with one of the workspace's
41 access tokens that has the <code className="font-mono text-xs">models:write</code> scope as the API key. On g1t's
42 models each request is charged at the model's price and paid from AI credit; with the workspace's own Anthropic key
43 under Integrations it is only counted. Prompts and answers are never kept.
44 </p>
45 <div className="max-w-xl">
46 <CopyLine text={GATEWAY_BASE_URL} />
47 </div>
48 <div className="flex flex-wrap gap-2">
49 {owner && (
50 <ButtonLink variant="quiet" to={`/${slug}/-/tokens`}>
51 <KeyRound size={14} />
52 Access tokens
53 </ButtonLink>
54 )}
55 <ButtonLink variant="quiet" to={GATEWAY_DOCS} reloadDocument>
56 <BookOpen size={14} />
57 How to use it
58 </ButtonLink>
59 </div>
60 </section>
61
62 <Suspense fallback={<GatewaySkeleton />}>
63 <Await resolve={log}>
64 {(loaded) =>
65 !loaded.page ? (
66 <p className="rounded-lg border border-danger/40 bg-danger/5 px-4 py-2.5 text-sm">
67 {loaded.error ?? "The requests could not be read. Try again in a minute."}
68 </p>
69 ) : (
70 <div className="space-y-4">
71 {loaded.page.requests.length === 0 ? (
72 <EmptyState title={before ? "No older requests" : "No requests yet"}>
73 {before
74 ? "Everything older has passed the log's 30 days."
75 : "Requests appear here as soon as code sends them to the AI Gateway with one of the workspace's tokens."}
76 </EmptyState>
77 ) : (
78 <GatewayTable requests={loaded.page.requests} />
79 )}
80 <div className="flex flex-wrap items-center gap-4 text-sm">
81 {before && (
82 <Link to={base} className="text-muted hover:text-fg">
83 Newest
84 </Link>
85 )}
86 {loaded.page.next && (
87 <Link to={`${base}?before=${encodeURIComponent(loaded.page.next)}`} className="text-muted hover:text-fg">
88 Older
89 </Link>
90 )}
91 <span className="grow" />
92 <span className="text-xs text-faint">
93 Kept {loaded.page.retentionDays} days. What requests were charged is also on{" "}
94 <Link to={`/${slug}/-/usage?product=gateway`} className="hover:text-fg hover:underline">
95 Usage
96 </Link>
97 .
98 </span>
99 </div>
100 </div>
101 )
102 }
103 </Await>
104 </Suspense>
105 </div>
106 );
107}

This file's history is long; its oldest lines are credited to the oldest commit read.