Skip to content
123 linesCodeBlameRaw

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.

AI Gateway: OpenAI's format, open models, and your own providers1import { BookOpen, KeyRound, Plug } from "lucide-react";
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens2import { 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";
AI Gateway: OpenAI's format, open models, and your own providers8import { GATEWAY_BASE_URL, GATEWAY_DOCS, GATEWAY_OPENAI_BASE_URL } from "../../lib/gateway";
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens9import { 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">
AI Gateway: OpenAI's format, open models, and your own providers40 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.
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens45 </p>
AI Gateway: OpenAI's format, open models, and your own providers46 <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>
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens60 <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 )}
AI Gateway: OpenAI's format, open models, and your own providers67 <ButtonLink variant="quiet" to={`/${slug}/-/integrations`}>
68 <Plug size={14} />
69 Your own providers
70 </ButtonLink>
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens71 <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}

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