| 1 | /** |
| 2 | * How the AI Gateway page speaks of its requests. Pure, so it can be tested. |
| 3 | */ |
| 4 | |
| 5 | import type { GatewayRequest } from "@g1t/contracts"; |
| 6 | |
| 7 | /** Where the AI Gateway is documented. */ |
| 8 | export const GATEWAY_DOCS = "https://docs.g1t.sh/guides/ai-gateway/"; |
| 9 | |
| 10 | /** The base URL an Anthropic SDK or Claude Code is pointed at. */ |
| 11 | export const GATEWAY_BASE_URL = "https://models.g1t.sh/anthropic"; |
| 12 | |
| 13 | /** The base URL an OpenAI SDK, or any tool that speaks OpenAI's API, is pointed at. */ |
| 14 | export const GATEWAY_OPENAI_BASE_URL = "https://models.g1t.sh/openai/v1"; |
| 15 | |
| 16 | /** How a request's format reads. */ |
| 17 | export function formatLabel(format: GatewayRequest["format"] | undefined): string { |
| 18 | return format === "openai" ? "OpenAI" : "Anthropic"; |
| 19 | } |
| 20 | |
| 21 | /** Who served a request, as people read it, and a longer line for its hint. */ |
| 22 | export function servedBy(request: Pick<GatewayRequest, "ownKey" | "provider" | "connection">): { label: string; hint: string } { |
| 23 | if (request.ownKey) { |
| 24 | const name = request.connection || "Your provider"; |
| 25 | return { label: name, hint: `${name}, the workspace's own ${providerName(request.provider)}: counted, not charged.` }; |
| 26 | } |
| 27 | if (!request.provider) return { label: "None", hint: "Refused before it reached a model." }; |
| 28 | return { label: `g1t · ${providerName(request.provider)}`, hint: `g1t's account at ${providerName(request.provider)}, charged at the model's price.` }; |
| 29 | } |
| 30 | |
| 31 | const PROVIDER_NAMES: Record<string, string> = { |
| 32 | anthropic: "Anthropic", |
| 33 | "workers-ai": "Workers AI", |
| 34 | openai: "OpenAI", |
| 35 | openai_endpoint: "OpenAI-compatible endpoint", |
| 36 | anthropic_endpoint: "Anthropic-compatible endpoint", |
| 37 | azure_openai: "Azure OpenAI", |
| 38 | gemini: "Google Gemini", |
| 39 | openrouter: "OpenRouter", |
| 40 | }; |
| 41 | |
| 42 | /** A provider's name, as people know it. */ |
| 43 | export function providerName(provider: string): string { |
| 44 | return PROVIDER_NAMES[provider] ?? (provider || "provider"); |
| 45 | } |
| 46 | |
| 47 | /** A connection's AI Gateway models, as a field shows them, from what was typed. */ |
| 48 | export function parseGatewayModels(text: string): string[] { |
| 49 | return text |
| 50 | .split(/[\s,]+/) |
| 51 | .map((model) => model.trim()) |
| 52 | .filter(Boolean); |
| 53 | } |
| 54 | |
| 55 | /** A count of tokens, short: `812`, `12.4K`, `3.1M`. */ |
| 56 | export function shortCount(n: number): string { |
| 57 | if (n < 1_000) return String(n); |
| 58 | if (n < 1_000_000) return `${trim(n / 1_000)}K`; |
| 59 | return `${trim(n / 1_000_000)}M`; |
| 60 | } |
| 61 | |
| 62 | function trim(value: number): string { |
| 63 | return value >= 100 ? String(Math.round(value)) : value.toFixed(1).replace(/\.0$/, ""); |
| 64 | } |
| 65 | |
| 66 | /** Every token a request used. */ |
| 67 | export function totalTokens(request: Pick<GatewayRequest, "input" | "output" | "cacheRead" | "cacheWrite">): number { |
| 68 | return request.input + request.output + request.cacheRead + request.cacheWrite; |
| 69 | } |
| 70 | |
| 71 | /** The tokens of each kind, in words, for a hint. */ |
| 72 | export function tokenKinds(request: Pick<GatewayRequest, "input" | "output" | "cacheRead" | "cacheWrite">): string { |
| 73 | const n = (v: number) => v.toLocaleString("en-US"); |
| 74 | return `${n(request.input)} input, ${n(request.output)} output, ${n(request.cacheRead)} cache read, ${n(request.cacheWrite)} cache write`; |
| 75 | } |
| 76 | |
| 77 | /** A request's cache tokens in words, for a hint: read, written, and how many of the writes last an hour. */ |
| 78 | export function cacheKinds(request: Pick<GatewayRequest, "cacheRead" | "cacheWrite" | "cacheWriteHour">): string { |
| 79 | const n = (v: number) => v.toLocaleString("en-US"); |
| 80 | const hour = request.cacheWriteHour ? `, ${n(request.cacheWriteHour)} of them to the hour-long cache` : ""; |
| 81 | return `${n(request.cacheRead)} read from the cache, ${n(request.cacheWrite)} written${hour}`; |
| 82 | } |
| 83 | |
| 84 | /** How a status reads, and how much it matters. */ |
| 85 | export function statusTone(status: number): { label: string; tone: "success" | "warn" | "danger" | "neutral" } { |
| 86 | if (status >= 200 && status < 300) return { label: String(status), tone: "success" }; |
| 87 | if (status === 402 || status === 429) return { label: String(status), tone: "warn" }; |
| 88 | if (status >= 400) return { label: String(status), tone: "danger" }; |
| 89 | return { label: String(status), tone: "neutral" }; |
| 90 | } |
| 91 | |
| 92 | /** A request's duration: `840 ms`, `4.2 s`, `2 min 5 s`. */ |
| 93 | export function duration(ms: number): string { |
| 94 | if (ms < 1_000) return `${Math.round(ms)} ms`; |
| 95 | if (ms < 60_000) return `${(ms / 1_000).toFixed(1).replace(/\.0$/, "")} s`; |
| 96 | const seconds = Math.round(ms / 1_000); |
| 97 | return `${Math.floor(seconds / 60)} min ${seconds % 60} s`; |
| 98 | } |