Spaces:
Runtime error
Runtime error
| /** | |
| * Extract LLM-specific metadata from intercepted requests so the UI can | |
| * render summary chips (provider, model, tokens, cost). Provider/api inference | |
| * is host- and path-based; token counts come from the upstream `usage` block. | |
| * | |
| * Replaces the stub `extractLlmMetadata` left in `kindDetector.ts` by F1. | |
| * Cost estimation uses the minimal pricing table in `pricing.ts` (R5-11). | |
| */ | |
| 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 } { | |
| // Direct JSON | |
| 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 }; | |
| } | |
| // Gemini-style usageMetadata | |
| 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 }; | |
| } | |
| /** | |
| * Extract LLM metadata. Returns `null` for non-LLM requests; otherwise | |
| * returns best-effort fields (any unknown field is `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 response was SSE, try to merge it for usage metadata. | |
| 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), | |
| }; | |
| } | |