| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import { detectKind } from "./kindDetector.ts";
|
| import { estimateCost } from "./pricing.ts";
|
| import { mergeStream, parseSseStream } from "./sseMerger.ts";
|
| import type { InterceptedRequest, LlmMetadata } from "./types.ts";
|
|
|
| interface ProviderMatch {
|
| pattern: RegExp;
|
| provider: string;
|
| }
|
|
|
| const PROVIDER_MATCHERS: ProviderMatch[] = [
|
| { pattern: /(^|\.)openai\.com$/i, provider: "openai" },
|
| { pattern: /(^|\.)openai\.azure\.com$/i, provider: "azure-openai" },
|
| { pattern: /(^|\.)anthropic\.com$/i, provider: "anthropic" },
|
| { pattern: /generativelanguage\.googleapis\.com$/i, provider: "gemini" },
|
| { pattern: /(^|\.)aiplatform\.googleapis\.com$/i, provider: "vertex" },
|
| { pattern: /(^|\.)mistral\.ai$/i, provider: "mistral" },
|
| { pattern: /(^|\.)deepseek\.com$/i, provider: "deepseek" },
|
| { pattern: /(^|\.)groq\.com$/i, provider: "groq" },
|
| { pattern: /(^|\.)together\.xyz$/i, provider: "together" },
|
| { pattern: /(^|\.)fireworks\.ai$/i, provider: "fireworks" },
|
| { pattern: /(^|\.)cohere\.com$/i, provider: "cohere" },
|
| { pattern: /(^|\.)perplexity\.ai$/i, provider: "perplexity" },
|
| { pattern: /(^|\.)huggingface\.co$/i, provider: "huggingface" },
|
| { pattern: /(^|\.)openrouter\.ai$/i, provider: "openrouter" },
|
| { pattern: /(^|\.)x\.ai$/i, provider: "xai" },
|
| { pattern: /(^|\.)moonshot\.ai$/i, provider: "moonshot" },
|
| { pattern: /bigmodel\.cn$/i, provider: "bigmodel" },
|
| { pattern: /(^|\.)githubcopilot\.com$/i, provider: "github-copilot" },
|
| { pattern: /(^|\.)cursor\.sh$/i, provider: "cursor" },
|
| { pattern: /(^|\.)zed\.dev$/i, provider: "zed" },
|
| ];
|
|
|
| interface ApiKindMatch {
|
| pattern: RegExp;
|
| apiKind: string;
|
| }
|
|
|
| const API_KIND_MATCHERS: ApiKindMatch[] = [
|
| { pattern: /\/(v1|v1beta)?\/?chat\/completions/i, apiKind: "chat.completions" },
|
| { pattern: /\/(v1|v1beta)\/messages/i, apiKind: "messages" },
|
| { pattern: /\/(v1|v1beta)?\/?embeddings/i, apiKind: "embeddings" },
|
| { pattern: /\/(v1|v1beta)?\/?responses/i, apiKind: "responses" },
|
| { pattern: /\/streamGenerateContent/i, apiKind: "streamGenerateContent" },
|
| { pattern: /\/generateContent/i, apiKind: "generateContent" },
|
| { pattern: /\/(v1|v1beta)\/completions/i, apiKind: "completions" },
|
| ];
|
|
|
| function asRecord(value: unknown): Record<string, unknown> | null {
|
| if (value && typeof value === "object" && !Array.isArray(value)) {
|
| return value as Record<string, unknown>;
|
| }
|
| return null;
|
| }
|
|
|
| function safeParseJson(value: string | null | undefined): unknown {
|
| if (!value) return null;
|
| try {
|
| return JSON.parse(value);
|
| } catch {
|
| return null;
|
| }
|
| }
|
|
|
| function inferProvider(host: string): string | null {
|
| for (const m of PROVIDER_MATCHERS) {
|
| if (m.pattern.test(host)) return m.provider;
|
| }
|
| return null;
|
| }
|
|
|
| function inferApiKind(path: string): string | null {
|
| for (const m of API_KIND_MATCHERS) {
|
| if (m.pattern.test(path)) return m.apiKind;
|
| }
|
| return null;
|
| }
|
|
|
| function countMessages(body: Record<string, unknown> | null): number {
|
| if (!body) return 0;
|
| if (Array.isArray(body.messages)) return body.messages.length;
|
| if (Array.isArray(body.contents)) return body.contents.length;
|
| if (Array.isArray(body.input)) return body.input.length;
|
| return 0;
|
| }
|
|
|
| function isSseRequest(req: InterceptedRequest): boolean {
|
| const accept = req.requestHeaders["accept"] ?? req.requestHeaders["Accept"] ?? "";
|
| const ct = req.responseHeaders["content-type"] ?? req.responseHeaders["Content-Type"] ?? "";
|
| return (
|
| accept.includes("event-stream") ||
|
| ct.includes("event-stream") ||
|
| /^\s*event:|^\s*data:/m.test(req.responseBody ?? "")
|
| );
|
| }
|
|
|
| function maybeNumber(v: unknown): number | null {
|
| return typeof v === "number" && Number.isFinite(v) ? v : null;
|
| }
|
|
|
| function extractUsage(resp: unknown): { tokensIn: number | null; tokensOut: number | null } {
|
|
|
| const respObj = asRecord(resp);
|
| const usage = asRecord(respObj?.usage);
|
| if (usage) {
|
| const inTok =
|
| maybeNumber(usage.prompt_tokens) ??
|
| maybeNumber(usage.input_tokens) ??
|
| maybeNumber((asRecord(usage.promptTokensDetails) ?? {}).total) ??
|
| null;
|
| const outTok =
|
| maybeNumber(usage.completion_tokens) ??
|
| maybeNumber(usage.output_tokens) ??
|
| maybeNumber((asRecord(usage.completionTokensDetails) ?? {}).total) ??
|
| null;
|
| return { tokensIn: inTok, tokensOut: outTok };
|
| }
|
|
|
| const um = asRecord(respObj?.usageMetadata);
|
| if (um) {
|
| const inTok = maybeNumber(um.promptTokenCount);
|
| const outTok = maybeNumber(um.candidatesTokenCount);
|
| return { tokensIn: inTok, tokensOut: outTok };
|
| }
|
| return { tokensIn: null, tokensOut: null };
|
| }
|
|
|
| |
| |
| |
|
|
| export function extractLlmMetadata(req: InterceptedRequest): LlmMetadata | null {
|
| const kind = req.detectedKind ?? detectKind(req);
|
| if (kind !== "llm") return null;
|
|
|
| const body = asRecord(safeParseJson(req.requestBody));
|
| let resp: unknown = safeParseJson(req.responseBody);
|
|
|
|
|
| if (!resp && req.responseBody && isSseRequest(req)) {
|
| const merged = mergeStream(parseSseStream(req.responseBody));
|
| resp = merged.message ?? null;
|
| }
|
|
|
| const respObj = asRecord(resp);
|
|
|
| const provider = inferProvider(req.host);
|
| const apiKind = inferApiKind(req.path);
|
| const model =
|
| (body && typeof body.model === "string" ? body.model : null) ??
|
| (respObj && typeof respObj.model === "string" ? respObj.model : null) ??
|
| (respObj && typeof respObj.modelVersion === "string" ? respObj.modelVersion : null) ??
|
| null;
|
| const messages = countMessages(body);
|
| const { tokensIn, tokensOut } = extractUsage(resp);
|
| const streamed = isSseRequest(req);
|
| const mappedTo =
|
| req.mappedModel ??
|
| req.requestHeaders["x-omniroute-mapped"] ??
|
| req.requestHeaders["X-Omniroute-Mapped"] ??
|
| null;
|
|
|
| return {
|
| provider,
|
| apiKind,
|
| model,
|
| messages,
|
| tokensIn,
|
| tokensOut,
|
| streamed,
|
| mappedTo,
|
| costEstimateUsd: estimateCost(model, tokensIn, tokensOut),
|
| };
|
| }
|
|
|