{"version":3,"file":"metrics.cjs","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["/**\n * Lightweight Prometheus metrics registry for LLMock.\n *\n * Zero external dependencies — implements counters, histograms, and gauges\n * with Prometheus text exposition format serialization.\n */\n\n// ---------------------------------------------------------------------------\n// Public interface\n// ---------------------------------------------------------------------------\n\nexport interface MetricsRegistry {\n  incrementCounter(name: string, labels: Record<string, string>): void;\n  observeHistogram(name: string, labels: Record<string, string>, value: number): void;\n  setGauge(name: string, labels: Record<string, string>, value: number): void;\n  serialize(): string;\n  reset(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Histogram bucket boundaries (Prometheus default-ish)\n// ---------------------------------------------------------------------------\n\nconst HISTOGRAM_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Build a stable label key string for map lookups: `label1=\"v1\",label2=\"v2\"` */\nfunction labelKey(labels: Record<string, string>): string {\n  const entries = Object.entries(labels).sort(([a], [b]) => a.localeCompare(b));\n  if (entries.length === 0) return \"\";\n  return entries.map(([k, v]) => `${k}=\"${escapeLabelValue(v)}\"`).join(\",\");\n}\n\n/** Escape a label value per Prometheus text exposition format. */\nfunction escapeLabelValue(v: string): string {\n  return v.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"').replace(/\\n/g, \"\\\\n\");\n}\n\n/** Format labels for Prometheus output: `{label1=\"v1\",label2=\"v2\"}` */\nfunction formatLabels(labels: Record<string, string>): string {\n  return `{${labelKey(labels)}}`;\n}\n\n// ---------------------------------------------------------------------------\n// Internal metric storage types\n// ---------------------------------------------------------------------------\n\ninterface CounterData {\n  type: \"counter\";\n  /** Map from labelKey → value */\n  series: Map<string, { labels: Record<string, string>; value: number }>;\n}\n\ninterface HistogramData {\n  type: \"histogram\";\n  /** Map from labelKey → bucket counts, sum, count */\n  series: Map<\n    string,\n    {\n      labels: Record<string, string>;\n      bucketCounts: number[]; // one per HISTOGRAM_BUCKETS entry\n      sum: number;\n      count: number;\n    }\n  >;\n}\n\ninterface GaugeData {\n  type: \"gauge\";\n  /** Map from labelKey → value */\n  series: Map<string, { labels: Record<string, string>; value: number }>;\n}\n\ntype MetricData = CounterData | HistogramData | GaugeData;\n\n// ---------------------------------------------------------------------------\n// Registry implementation\n// ---------------------------------------------------------------------------\n\nexport function createMetricsRegistry(): MetricsRegistry {\n  /** Ordered map: metric name → data. Insertion order preserved for stable output. */\n  const metrics = new Map<string, MetricData>();\n\n  function getOrCreateCounter(name: string): CounterData {\n    let data = metrics.get(name);\n    if (!data) {\n      data = { type: \"counter\", series: new Map() };\n      metrics.set(name, data);\n    }\n    if (data.type !== \"counter\") throw new Error(`Metric ${name} is not a counter`);\n    return data as CounterData;\n  }\n\n  function getOrCreateHistogram(name: string): HistogramData {\n    let data = metrics.get(name);\n    if (!data) {\n      data = { type: \"histogram\", series: new Map() };\n      metrics.set(name, data);\n    }\n    if (data.type !== \"histogram\") throw new Error(`Metric ${name} is not a histogram`);\n    return data as HistogramData;\n  }\n\n  function getOrCreateGauge(name: string): GaugeData {\n    let data = metrics.get(name);\n    if (!data) {\n      data = { type: \"gauge\", series: new Map() };\n      metrics.set(name, data);\n    }\n    if (data.type !== \"gauge\") throw new Error(`Metric ${name} is not a gauge`);\n    return data as GaugeData;\n  }\n\n  return {\n    incrementCounter(name: string, labels: Record<string, string>): void {\n      const counter = getOrCreateCounter(name);\n      const key = labelKey(labels);\n      const existing = counter.series.get(key);\n      if (existing) {\n        existing.value += 1;\n      } else {\n        counter.series.set(key, { labels, value: 1 });\n      }\n    },\n\n    observeHistogram(name: string, labels: Record<string, string>, value: number): void {\n      const histogram = getOrCreateHistogram(name);\n      const key = labelKey(labels);\n      let existing = histogram.series.get(key);\n      if (!existing) {\n        existing = {\n          labels,\n          bucketCounts: new Array(HISTOGRAM_BUCKETS.length).fill(0) as number[],\n          sum: 0,\n          count: 0,\n        };\n        histogram.series.set(key, existing);\n      }\n      // Update cumulative bucket counts\n      for (let i = 0; i < HISTOGRAM_BUCKETS.length; i++) {\n        if (value <= HISTOGRAM_BUCKETS[i]) {\n          existing.bucketCounts[i] += 1;\n        }\n      }\n      existing.sum += value;\n      existing.count += 1;\n    },\n\n    setGauge(name: string, labels: Record<string, string>, value: number): void {\n      const gauge = getOrCreateGauge(name);\n      const key = labelKey(labels);\n      const existing = gauge.series.get(key);\n      if (existing) {\n        existing.value = value;\n      } else {\n        gauge.series.set(key, { labels, value });\n      }\n    },\n\n    serialize(): string {\n      const lines: string[] = [];\n\n      for (const [name, data] of metrics) {\n        switch (data.type) {\n          case \"counter\": {\n            lines.push(`# TYPE ${name} counter`);\n            for (const series of data.series.values()) {\n              lines.push(`${name}${formatLabels(series.labels)} ${series.value}`);\n            }\n            break;\n          }\n          case \"histogram\": {\n            lines.push(`# TYPE ${name} histogram`);\n            for (const series of data.series.values()) {\n              const lblStr = labelKey(series.labels);\n              const lblPrefix = lblStr ? `${lblStr},` : \"\";\n              // Bucket lines\n              for (let i = 0; i < HISTOGRAM_BUCKETS.length; i++) {\n                lines.push(\n                  `${name}_bucket{${lblPrefix}le=\"${HISTOGRAM_BUCKETS[i]}\"} ${series.bucketCounts[i]}`,\n                );\n              }\n              // +Inf bucket\n              lines.push(`${name}_bucket{${lblPrefix}le=\"+Inf\"} ${series.count}`);\n              // Sum and count\n              lines.push(`${name}_sum${formatLabels(series.labels)} ${series.sum}`);\n              lines.push(`${name}_count${formatLabels(series.labels)} ${series.count}`);\n            }\n            break;\n          }\n          case \"gauge\": {\n            lines.push(`# TYPE ${name} gauge`);\n            for (const series of data.series.values()) {\n              lines.push(`${name}${formatLabels(series.labels)} ${series.value}`);\n            }\n            break;\n          }\n        }\n      }\n\n      return lines.length > 0 ? lines.join(\"\\n\") + \"\\n\" : \"\";\n    },\n\n    reset(): void {\n      metrics.clear();\n    },\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Path normalization for metric labels\n// ---------------------------------------------------------------------------\n\n// Regex patterns for parametric API routes\nconst BEDROCK_RE =\n  /^\\/model\\/([^/]+)\\/(invoke|invoke-with-response-stream|converse|converse-stream)$/;\nconst GEMINI_RE = /^\\/v1beta\\/models\\/([^:]+):(generateContent|streamGenerateContent)$/;\nconst AZURE_RE = /^\\/openai\\/deployments\\/([^/]+)\\/(chat\\/completions|embeddings)$/;\nconst ELEVENLABS_TTS_RE = /^\\/v1\\/text-to-speech\\/([^/]+)$/;\nconst VERTEX_RE =\n  /^\\/v1\\/projects\\/([^/]+)\\/locations\\/([^/]+)\\/publishers\\/google\\/models\\/([^:]+):(.+)$/;\n// Exported: server.ts route dispatch matches the same OpenRouter and OpenAI\n// video paths.\nexport const OPENROUTER_VIDEO_CONTENT_RE = /^\\/api\\/v1\\/videos\\/([^/]+)\\/content$/;\nexport const OPENROUTER_VIDEO_STATUS_RE = /^\\/api\\/v1\\/videos\\/([^/]+)$/;\nexport const OPENAI_VIDEO_STATUS_RE = /^\\/v1\\/videos\\/([^/]+)$/;\n\n/**\n * Normalize parametric API paths to route patterns for use as metric labels.\n * Replaces dynamic segments (model IDs, deployment names, etc.) with placeholders.\n */\nexport function normalizePathLabel(pathname: string): string {\n  // Bedrock: /model/{modelId}/{operation}\n  const bedrockMatch = pathname.match(BEDROCK_RE);\n  if (bedrockMatch) {\n    return `/model/{modelId}/${bedrockMatch[2]}`;\n  }\n\n  // Gemini: /v1beta/models/{model}:{action}\n  const geminiMatch = pathname.match(GEMINI_RE);\n  if (geminiMatch) {\n    return `/v1beta/models/{model}:${geminiMatch[2]}`;\n  }\n\n  // Azure: /openai/deployments/{id}/{operation}\n  const azureMatch = pathname.match(AZURE_RE);\n  if (azureMatch) {\n    return `/openai/deployments/{id}/${azureMatch[2]}`;\n  }\n\n  // Vertex AI: /v1/projects/{p}/locations/{l}/publishers/google/models/{m}:{action}\n  const vertexMatch = pathname.match(VERTEX_RE);\n  if (vertexMatch) {\n    return `/v1/projects/{p}/locations/{l}/publishers/google/models/{m}:${vertexMatch[4]}`;\n  }\n\n  // ElevenLabs TTS: /v1/text-to-speech/{voice_id}\n  if (ELEVENLABS_TTS_RE.test(pathname)) {\n    return \"/v1/text-to-speech/{voice_id}\";\n  }\n\n  // OpenRouter video: /api/v1/videos/{jobId}[/content] — jobIds are random\n  // UUIDs, so raw paths would mint unbounded label cardinality. The static\n  // /api/v1/videos/models listing route must not collapse into {jobId}.\n  if (OPENROUTER_VIDEO_CONTENT_RE.test(pathname)) {\n    return \"/api/v1/videos/{jobId}/content\";\n  }\n  if (pathname !== \"/api/v1/videos/models\" && OPENROUTER_VIDEO_STATUS_RE.test(pathname)) {\n    return \"/api/v1/videos/{jobId}\";\n  }\n\n  // OpenAI video status: /v1/videos/{id}\n  if (OPENAI_VIDEO_STATUS_RE.test(pathname)) {\n    return \"/v1/videos/{id}\";\n  }\n\n  // Static path — return as-is\n  return pathname;\n}\n"],"mappings":"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"}