Skip to content

g1t/services/models/src/route.ts

102 lines3,941 bytesCodeBlame
1import type { ModelUpstream } from "@g1t/contracts";
2
3/** g1t's own way to the models, for runs it pays for. */
4export type HostedRouting = {
5 /** A Cloudflare AI Gateway id; empty sends requests to Anthropic directly. */
6 AI_GATEWAY_ID: string;
7 CLOUDFLARE_ACCOUNT_ID: string;
8 /** Authenticates to the gateway, which holds g1t's key. */
9 AI_GATEWAY_TOKEN?: string;
10 /** g1t's key, when the gateway does not hold it. */
11 ANTHROPIC_API_KEY?: string;
12 /**
13 * A Cloudflare API token that may run Workers AI on g1t's account, for
14 * the AI Gateway's open models. Without one, `AI_GATEWAY_TOKEN` is tried.
15 */
16 WORKERS_AI_TOKEN?: string;
17};
18
19/** Headers the sandbox sends that never go further. */
20const DROPPED = new Set([
21 "x-api-key",
22 "authorization",
23 "host",
24 "cf-aig-authorization",
25 "cf-aig-metadata",
26 "cf-connecting-ip",
27 "x-forwarded-for",
28 "x-real-ip",
29]);
30
31/** The token a sandbox sends instead of a key, from either header. */
32export function presentedToken(headers: Headers): string | null {
33 const key = headers.get("x-api-key");
34 if (key) return key.trim();
35 const bearer = headers.get("authorization")?.match(/^Bearer\s+(.+)$/i);
36 return bearer ? bearer[1].trim() : null;
37}
38
39/**
40 * The caller's headers, less its token and anything that never goes
41 * further. Every `cf-aig-` header is the proxy's to set: one from a caller
42 * could change how Cloudflare's AI Gateway logs, caches or prices a
43 * request (`cf-aig-custom-cost`), which billing settles by.
44 */
45export function passedHeaders(incoming: Headers): Headers {
46 const headers = new Headers();
47 for (const [name, value] of incoming) {
48 const lower = name.toLowerCase();
49 if (!DROPPED.has(lower) && !lower.startsWith("cf-aig-")) headers.set(name, value);
50 }
51 return headers;
52}
53
54/**
55 * Where one request goes and what it carries: the sandbox's request,
56 * stripped of its token, with the credentials for the run's route.
57 * `path` is what follows `/anthropic`, such as `/v1/messages?beta=true`.
58 */
59export function upstreamRequest(
60 upstream: ModelUpstream,
61 hosted: HostedRouting,
62 path: string,
63 incoming: Headers,
64): { url: string; headers: Headers } {
65 const headers = passedHeaders(incoming);
66 if (upstream.route === "g1t") {
67 if (!hosted.AI_GATEWAY_ID) {
68 if (hosted.ANTHROPIC_API_KEY) headers.set("x-api-key", hosted.ANTHROPIC_API_KEY);
69 return { url: `https://api.anthropic.com${path}`, headers };
70 }
71 // The gateway logs these with every request, so spend and failures can
72 // be read per kind of work, tier, repository and pull request. It keeps
73 // five entries and drops the rest, so the tier takes the workspace's
74 // place: the repository names the workspace too. A run from before
75 // routing by tier has no tier, and keeps the workspace.
76 headers.set(
77 "cf-aig-metadata",
78 JSON.stringify({
79 task: upstream.task,
80 ...(upstream.tier ? { tier: upstream.tier } : { workspace: upstream.workspace }),
81 repo: upstream.repo,
82 pull: upstream.number,
83 // What billing finds the run's requests by, to charge what they cost.
84 session: upstream.session,
85 }),
86 );
87 if (hosted.AI_GATEWAY_TOKEN) headers.set("cf-aig-authorization", `Bearer ${hosted.AI_GATEWAY_TOKEN}`);
88 if (hosted.ANTHROPIC_API_KEY) headers.set("x-api-key", hosted.ANTHROPIC_API_KEY);
89 return {
90 url: `https://gateway.ai.cloudflare.com/v1/${hosted.CLOUDFLARE_ACCOUNT_ID}/${hosted.AI_GATEWAY_ID}/anthropic${path}`,
91 headers,
92 };
93 }
94 const key = upstream.apiKey;
95 if (key) {
96 const header = upstream.authHeader ?? "x-api-key";
97 headers.set(header, header === "authorization" ? `Bearer ${key}` : key);
98 }
99 if (upstream.gatewayToken) headers.set("cf-aig-authorization", `Bearer ${upstream.gatewayToken}`);
100 const base = (upstream.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
101 return { url: `${base}${path}`, headers };
102}