@copilotkit/aimock 1.39.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +4 -2
- package/dist/agui-stub.d.cts +2 -2
- package/dist/agui-stub.d.ts +2 -2
- package/dist/agui-types.d.cts +66 -3
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +66 -3
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/byteplus-video.cjs +954 -0
- package/dist/byteplus-video.cjs.map +1 -0
- package/dist/byteplus-video.d.cts +176 -0
- package/dist/byteplus-video.d.cts.map +1 -0
- package/dist/byteplus-video.d.ts +176 -0
- package/dist/byteplus-video.d.ts.map +1 -0
- package/dist/byteplus-video.js +947 -0
- package/dist/byteplus-video.js.map +1 -0
- package/dist/cli.cjs +5 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/helpers.cjs +8 -4
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +8 -4
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +2 -2
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts +11 -3
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts +11 -3
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +2 -2
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/metrics.cjs +20 -0
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +19 -1
- package/dist/metrics.js.map +1 -1
- package/dist/openrouter-chat.cjs +41 -0
- package/dist/openrouter-chat.cjs.map +1 -1
- package/dist/openrouter-chat.js +41 -0
- package/dist/openrouter-chat.js.map +1 -1
- package/dist/provider-auth.cjs +2 -0
- package/dist/provider-auth.cjs.map +1 -1
- package/dist/provider-auth.js +2 -0
- package/dist/provider-auth.js.map +1 -1
- package/dist/recorder.cjs +77 -8
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +77 -8
- package/dist/recorder.js.map +1 -1
- package/dist/router.cjs +1 -0
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1 -0
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +137 -6
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +2 -0
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +138 -7
- package/dist/server.js.map +1 -1
- package/dist/stream-collapse.cjs +21 -6
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +16 -0
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +16 -0
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +21 -6
- package/dist/stream-collapse.js.map +1 -1
- package/dist/types.d.cts +41 -6
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +41 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-types.d.cts.map +1 -1
- package/package.json +4 -3
package/dist/metrics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.js","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// Exported: server.ts route dispatch matches the same Google Veo and xAI Grok\n// video paths. Veo submit (`:predictLongRunning`) is anchored so it never\n// collides with the bare Gemini `:predict` route; Veo operations live in a\n// fresh `/v1beta/operations/...` namespace. Grok submit is an exact literal\n// path; Grok status reuses the OpenAI `/v1/videos/{id}` shape (the dispatch\n// guards `id !== \"generations\"` and the job-map lookup disambiguates Sora).\nexport const VEO_PREDICT_LRO_RE = /^\\/v1beta\\/models\\/([^:]+):predictLongRunning$/;\nexport const VEO_OPERATION_RE = /^\\/v1beta\\/(operations\\/.+)$/;\nexport const GROK_VIDEO_SUBMIT_PATH = \"/v1/videos/generations\";\nexport const GROK_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 // Google Veo video: submit `:predictLongRunning` + poll `/v1beta/operations/{name}`.\n // Operation names are random UUIDs, so the raw path would mint unbounded\n // label cardinality.\n if (VEO_PREDICT_LRO_RE.test(pathname)) {\n return \"/v1beta/models/{model}:predictLongRunning\";\n }\n if (VEO_OPERATION_RE.test(pathname)) {\n return \"/v1beta/operations/{name}\";\n }\n\n // xAI Grok Imagine submit is a static literal path — keep it distinct from\n // the `/v1/videos/{id}` status label below (which it would otherwise collapse\n // into), exactly as `/api/v1/videos/models` is kept out of the jobId bucket.\n if (pathname === GROK_VIDEO_SUBMIT_PATH) {\n return GROK_VIDEO_SUBMIT_PATH;\n }\n\n // OpenAI/Grok 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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"}
|
|
1
|
+
{"version":3,"file":"metrics.js","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// Exported: server.ts route dispatch matches the same Google Veo and xAI Grok\n// video paths. Veo submit (`:predictLongRunning`) is anchored so it never\n// collides with the bare Gemini `:predict` route; Veo operations live in a\n// fresh `/v1beta/operations/...` namespace. Grok submit is an exact literal\n// path; Grok status reuses the OpenAI `/v1/videos/{id}` shape (the dispatch\n// guards `id !== \"generations\"` and the job-map lookup disambiguates Sora).\nexport const VEO_PREDICT_LRO_RE = /^\\/v1beta\\/models\\/([^:]+):predictLongRunning$/;\nexport const VEO_OPERATION_RE = /^\\/v1beta\\/(operations\\/.+)$/;\nexport const GROK_VIDEO_SUBMIT_PATH = \"/v1/videos/generations\";\nexport const GROK_VIDEO_STATUS_RE = /^\\/v1\\/videos\\/([^/]+)$/;\n\n/**\n * BytePlus Ark (Seedance) async video task routes. The `/api/v3` prefix is\n * ENUMERATED rather than wildcarded: Ark's data-plane base carries it, but a\n * client may point `baseURL` at a bare aimock root instead, so both forms must\n * route. A tolerant `(?:\\/[^?]*)?` prefix would additionally claim\n * `/fal/contents/generations/tasks` — and these routes dispatch in the\n * pre-rewrite band, ~1,100 lines ahead of every fal branch, so it would take\n * that path away from the fal proxy. Enumerating the one real prefix also\n * blocks a doubled suffix from matching the status RE.\n *\n * Declared here (not in server.ts) because normalizePathLabel below consumes\n * them, matching the existing OpenRouter/Veo/Grok route-regex edge where\n * server.ts imports its route regexes from this module.\n */\nexport const BYTEPLUS_VIDEO_SUBMIT_RE = /^(?:\\/api\\/v3)?\\/contents\\/generations\\/tasks$/;\nexport const BYTEPLUS_VIDEO_STATUS_RE = /^(?:\\/api\\/v3)?\\/contents\\/generations\\/tasks\\/([^/]+)$/;\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 // Google Veo video: submit `:predictLongRunning` + poll `/v1beta/operations/{name}`.\n // Operation names are random UUIDs, so the raw path would mint unbounded\n // label cardinality.\n if (VEO_PREDICT_LRO_RE.test(pathname)) {\n return \"/v1beta/models/{model}:predictLongRunning\";\n }\n if (VEO_OPERATION_RE.test(pathname)) {\n return \"/v1beta/operations/{name}\";\n }\n\n // xAI Grok Imagine submit is a static literal path — keep it distinct from\n // the `/v1/videos/{id}` status label below (which it would otherwise collapse\n // into), exactly as `/api/v1/videos/models` is kept out of the jobId bucket.\n if (pathname === GROK_VIDEO_SUBMIT_PATH) {\n return GROK_VIDEO_SUBMIT_PATH;\n }\n\n // OpenAI/Grok video status: /v1/videos/{id}\n if (OPENAI_VIDEO_STATUS_RE.test(pathname)) {\n return \"/v1/videos/{id}\";\n }\n\n // BytePlus Ark video: /[api/v3/]contents/generations/tasks[/{id}] — task ids\n // (`cgt-…`) are unbounded cardinality. Status before submit: the status RE is\n // the more specific of the two and the submit RE cannot match a trailing id\n // segment, so the order is belt-and-braces. Appended at the end of the\n // cascade because the anchored REs above cannot match any path an earlier\n // entry claims.\n if (BYTEPLUS_VIDEO_STATUS_RE.test(pathname)) {\n return \"/contents/generations/tasks/{id}\";\n }\n if (BYTEPLUS_VIDEO_SUBMIT_RE.test(pathname)) {\n return \"/contents/generations/tasks\";\n }\n\n // Static path — return as-is\n return pathname;\n}\n"],"mappings":"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"}
|
package/dist/openrouter-chat.cjs
CHANGED
|
@@ -63,6 +63,41 @@ function toGenId(id) {
|
|
|
63
63
|
return id.startsWith(OPENAI_ID_PREFIX) ? OPENROUTER_ID_PREFIX + id.slice(9) : id;
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
+
* `usage` keys that are already accounted for elsewhere and must therefore NOT
|
|
67
|
+
* be re-emitted through the forward-compat passthrough below: the canonical
|
|
68
|
+
* token counts (and their per-provider aliases), which `resolveUsage` folds into
|
|
69
|
+
* `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage
|
|
70
|
+
* object, plus every field this function shapes explicitly.
|
|
71
|
+
*/
|
|
72
|
+
const SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([
|
|
73
|
+
"prompt_tokens",
|
|
74
|
+
"completion_tokens",
|
|
75
|
+
"total_tokens",
|
|
76
|
+
"input_tokens",
|
|
77
|
+
"output_tokens",
|
|
78
|
+
"promptTokenCount",
|
|
79
|
+
"candidatesTokenCount",
|
|
80
|
+
"totalTokenCount",
|
|
81
|
+
"cost",
|
|
82
|
+
"cost_details",
|
|
83
|
+
"prompt_tokens_details",
|
|
84
|
+
"completion_tokens_details",
|
|
85
|
+
"is_byok"
|
|
86
|
+
]);
|
|
87
|
+
/**
|
|
88
|
+
* Keys that must never flow through the passthrough assignment below: a
|
|
89
|
+
* JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream
|
|
90
|
+
* provider responses) can carry a real own `__proto__`/`constructor`/`prototype`
|
|
91
|
+
* key, and a plain `usageExtras[key] = value` for one of those hits the
|
|
92
|
+
* prototype setter, corrupting the emitted object's prototype chain instead of
|
|
93
|
+
* emitting a data field. Real providers never send these, so skipping is safe.
|
|
94
|
+
*/
|
|
95
|
+
const UNSAFE_PROTO_KEYS = new Set([
|
|
96
|
+
"__proto__",
|
|
97
|
+
"constructor",
|
|
98
|
+
"prototype"
|
|
99
|
+
]);
|
|
100
|
+
/**
|
|
66
101
|
* Resolve the shaping context from a fixture's response overrides and the
|
|
67
102
|
* winning model slug. `provider` defaults to the winning slug's author;
|
|
68
103
|
* `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and
|
|
@@ -95,6 +130,12 @@ function resolveOpenRouterShaping(overrides, winningModel) {
|
|
|
95
130
|
reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0
|
|
96
131
|
};
|
|
97
132
|
if (u?.is_byok !== void 0) usageExtras.is_byok = u.is_byok;
|
|
133
|
+
for (const [key, value] of Object.entries(u ?? {})) {
|
|
134
|
+
if (value === void 0) continue;
|
|
135
|
+
if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;
|
|
136
|
+
if (UNSAFE_PROTO_KEYS.has(key)) continue;
|
|
137
|
+
usageExtras[key] = value;
|
|
138
|
+
}
|
|
98
139
|
return {
|
|
99
140
|
provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),
|
|
100
141
|
...overrides?.nativeFinishReason !== void 0 && { nativeFinishReason: overrides.nativeFinishReason },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openrouter-chat.cjs","names":["flattenHeaders"],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const u = overrides?.usage;\n const usageExtras: OpenRouterUsageExtras = {};\n if (u?.cost !== undefined) {\n usageExtras.cost = u.cost;\n const cd = u.cost_details;\n usageExtras.cost_details = {\n ...(cd ?? {}),\n upstream_inference_cost: cd?.upstream_inference_cost ?? 0,\n upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,\n upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0,\n };\n }\n if (u?.prompt_tokens_details !== undefined) {\n usageExtras.prompt_tokens_details = {\n ...u.prompt_tokens_details,\n cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0,\n };\n }\n if (u?.completion_tokens_details !== undefined) {\n usageExtras.completion_tokens_details = {\n ...u.completion_tokens_details,\n reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0,\n };\n }\n if (u?.is_byok !== undefined) {\n usageExtras.is_byok = u.is_byok;\n }\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return { ...usage, ...shaping.usageExtras };\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify({\n error: {\n code: status,\n message,\n ...(metadata !== undefined && { metadata }),\n },\n });\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\n/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */\nconst DEFAULT_OPENROUTER_MODELS = [\n \"openai/gpt-4o\",\n \"anthropic/claude-3.5-sonnet\",\n \"google/gemini-2.0-flash-001\",\n];\n\n/**\n * Build one OpenRouter model-catalog object (deterministic static filler).\n * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):\n * pricing values are STRINGS; the object also carries hugging_face_id,\n * knowledge_cutoff, supported_voices, links, default_parameters,\n * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).\n */\nfunction openRouterModelObject(id: string): Record<string, unknown> {\n return {\n id,\n canonical_slug: id,\n hugging_face_id: null,\n name: id,\n created: 0,\n description: `Mock OpenRouter model ${id} served by aimock.`,\n context_length: 128000,\n architecture: {\n modality: \"text->text\",\n input_modalities: [\"text\"],\n output_modalities: [\"text\"],\n tokenizer: \"Other\",\n instruct_type: null,\n },\n pricing: {\n prompt: \"0\",\n completion: \"0\",\n request: \"0\",\n image: \"0\",\n web_search: \"0\",\n internal_reasoning: \"0\",\n input_cache_read: \"0\",\n input_cache_write: \"0\",\n },\n top_provider: {\n context_length: 128000,\n max_completion_tokens: 16384,\n is_moderated: false,\n },\n per_request_limits: null,\n supported_parameters: [\n \"tools\",\n \"tool_choice\",\n \"max_tokens\",\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"reasoning\",\n ],\n default_parameters: null,\n supported_voices: null,\n knowledge_cutoff: null,\n expiration_date: null,\n links: { details: `/api/v1/models/${id}/endpoints` },\n reasoning: { mandatory: false, default_enabled: false },\n };\n}\n\n/**\n * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat\n * (or endpoint-less) fixture with a string `match.model`, in first-seen order;\n * the default set when none contribute one. Mirrors the Ollama `/api/tags` and\n * OpenRouter video `/models` synthesis idioms.\n */\nfunction collectModelIds(fixtures: Fixture[]): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const f of fixtures) {\n if (f.match.endpoint !== undefined && f.match.endpoint !== \"chat\") continue;\n const m = f.match.model;\n if (typeof m === \"string\" && m.length > 0 && !seen.has(m)) {\n seen.add(m);\n ids.push(m);\n }\n }\n return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;\n}\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const data = collectModelIds(fixtures).map(openRouterModelObject);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data }));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"openrouter-chat.cjs","names":["flattenHeaders"],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * `usage` keys that are already accounted for elsewhere and must therefore NOT\n * be re-emitted through the forward-compat passthrough below: the canonical\n * token counts (and their per-provider aliases), which `resolveUsage` folds into\n * `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage\n * object, plus every field this function shapes explicitly.\n */\nconst SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([\n \"prompt_tokens\",\n \"completion_tokens\",\n \"total_tokens\",\n \"input_tokens\",\n \"output_tokens\",\n \"promptTokenCount\",\n \"candidatesTokenCount\",\n \"totalTokenCount\",\n \"cost\",\n \"cost_details\",\n \"prompt_tokens_details\",\n \"completion_tokens_details\",\n \"is_byok\",\n]);\n\n/**\n * Keys that must never flow through the passthrough assignment below: a\n * JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream\n * provider responses) can carry a real own `__proto__`/`constructor`/`prototype`\n * key, and a plain `usageExtras[key] = value` for one of those hits the\n * prototype setter, corrupting the emitted object's prototype chain instead of\n * emitting a data field. Real providers never send these, so skipping is safe.\n */\nconst UNSAFE_PROTO_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const u = overrides?.usage;\n const usageExtras: OpenRouterUsageExtras = {};\n if (u?.cost !== undefined) {\n usageExtras.cost = u.cost;\n const cd = u.cost_details;\n usageExtras.cost_details = {\n ...(cd ?? {}),\n upstream_inference_cost: cd?.upstream_inference_cost ?? 0,\n upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,\n upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0,\n };\n }\n if (u?.prompt_tokens_details !== undefined) {\n usageExtras.prompt_tokens_details = {\n ...u.prompt_tokens_details,\n cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0,\n };\n }\n if (u?.completion_tokens_details !== undefined) {\n usageExtras.completion_tokens_details = {\n ...u.completion_tokens_details,\n reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0,\n };\n }\n if (u?.is_byok !== undefined) {\n usageExtras.is_byok = u.is_byok;\n }\n // Forward-compat passthrough (#368): a fixture's `usage` may carry provider\n // fields aimock does not model — typically written by the recorder straight\n // from a real upstream usage frame (OpenRouter `native_tokens_prompt`,\n // `native_tokens_completion`, …). Emit them verbatim rather than silently\n // dropping them, so a recorded usage object round-trips on replay. The\n // canonical token counts and every field shaped above are excluded: those are\n // already resolved onto the base usage object (see helpers.ts `resolveUsage`)\n // or handled with their own required-member defaults.\n for (const [key, value] of Object.entries(u ?? {})) {\n if (value === undefined) continue;\n if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;\n if (UNSAFE_PROTO_KEYS.has(key)) continue;\n usageExtras[key] = value;\n }\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return { ...usage, ...shaping.usageExtras };\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify({\n error: {\n code: status,\n message,\n ...(metadata !== undefined && { metadata }),\n },\n });\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\n/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */\nconst DEFAULT_OPENROUTER_MODELS = [\n \"openai/gpt-4o\",\n \"anthropic/claude-3.5-sonnet\",\n \"google/gemini-2.0-flash-001\",\n];\n\n/**\n * Build one OpenRouter model-catalog object (deterministic static filler).\n * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):\n * pricing values are STRINGS; the object also carries hugging_face_id,\n * knowledge_cutoff, supported_voices, links, default_parameters,\n * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).\n */\nfunction openRouterModelObject(id: string): Record<string, unknown> {\n return {\n id,\n canonical_slug: id,\n hugging_face_id: null,\n name: id,\n created: 0,\n description: `Mock OpenRouter model ${id} served by aimock.`,\n context_length: 128000,\n architecture: {\n modality: \"text->text\",\n input_modalities: [\"text\"],\n output_modalities: [\"text\"],\n tokenizer: \"Other\",\n instruct_type: null,\n },\n pricing: {\n prompt: \"0\",\n completion: \"0\",\n request: \"0\",\n image: \"0\",\n web_search: \"0\",\n internal_reasoning: \"0\",\n input_cache_read: \"0\",\n input_cache_write: \"0\",\n },\n top_provider: {\n context_length: 128000,\n max_completion_tokens: 16384,\n is_moderated: false,\n },\n per_request_limits: null,\n supported_parameters: [\n \"tools\",\n \"tool_choice\",\n \"max_tokens\",\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"reasoning\",\n ],\n default_parameters: null,\n supported_voices: null,\n knowledge_cutoff: null,\n expiration_date: null,\n links: { details: `/api/v1/models/${id}/endpoints` },\n reasoning: { mandatory: false, default_enabled: false },\n };\n}\n\n/**\n * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat\n * (or endpoint-less) fixture with a string `match.model`, in first-seen order;\n * the default set when none contribute one. Mirrors the Ollama `/api/tags` and\n * OpenRouter video `/models` synthesis idioms.\n */\nfunction collectModelIds(fixtures: Fixture[]): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const f of fixtures) {\n if (f.match.endpoint !== undefined && f.match.endpoint !== \"chat\") continue;\n const m = f.match.model;\n if (typeof m === \"string\" && m.length > 0 && !seen.has(m)) {\n seen.add(m);\n ids.push(m);\n }\n }\n return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;\n}\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const data = collectModelIds(fixtures).map(openRouterModelObject);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data }));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":"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"}
|
package/dist/openrouter-chat.js
CHANGED
|
@@ -63,6 +63,41 @@ function toGenId(id) {
|
|
|
63
63
|
return id.startsWith(OPENAI_ID_PREFIX) ? OPENROUTER_ID_PREFIX + id.slice(9) : id;
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
+
* `usage` keys that are already accounted for elsewhere and must therefore NOT
|
|
67
|
+
* be re-emitted through the forward-compat passthrough below: the canonical
|
|
68
|
+
* token counts (and their per-provider aliases), which `resolveUsage` folds into
|
|
69
|
+
* `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage
|
|
70
|
+
* object, plus every field this function shapes explicitly.
|
|
71
|
+
*/
|
|
72
|
+
const SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([
|
|
73
|
+
"prompt_tokens",
|
|
74
|
+
"completion_tokens",
|
|
75
|
+
"total_tokens",
|
|
76
|
+
"input_tokens",
|
|
77
|
+
"output_tokens",
|
|
78
|
+
"promptTokenCount",
|
|
79
|
+
"candidatesTokenCount",
|
|
80
|
+
"totalTokenCount",
|
|
81
|
+
"cost",
|
|
82
|
+
"cost_details",
|
|
83
|
+
"prompt_tokens_details",
|
|
84
|
+
"completion_tokens_details",
|
|
85
|
+
"is_byok"
|
|
86
|
+
]);
|
|
87
|
+
/**
|
|
88
|
+
* Keys that must never flow through the passthrough assignment below: a
|
|
89
|
+
* JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream
|
|
90
|
+
* provider responses) can carry a real own `__proto__`/`constructor`/`prototype`
|
|
91
|
+
* key, and a plain `usageExtras[key] = value` for one of those hits the
|
|
92
|
+
* prototype setter, corrupting the emitted object's prototype chain instead of
|
|
93
|
+
* emitting a data field. Real providers never send these, so skipping is safe.
|
|
94
|
+
*/
|
|
95
|
+
const UNSAFE_PROTO_KEYS = new Set([
|
|
96
|
+
"__proto__",
|
|
97
|
+
"constructor",
|
|
98
|
+
"prototype"
|
|
99
|
+
]);
|
|
100
|
+
/**
|
|
66
101
|
* Resolve the shaping context from a fixture's response overrides and the
|
|
67
102
|
* winning model slug. `provider` defaults to the winning slug's author;
|
|
68
103
|
* `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and
|
|
@@ -95,6 +130,12 @@ function resolveOpenRouterShaping(overrides, winningModel) {
|
|
|
95
130
|
reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0
|
|
96
131
|
};
|
|
97
132
|
if (u?.is_byok !== void 0) usageExtras.is_byok = u.is_byok;
|
|
133
|
+
for (const [key, value] of Object.entries(u ?? {})) {
|
|
134
|
+
if (value === void 0) continue;
|
|
135
|
+
if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;
|
|
136
|
+
if (UNSAFE_PROTO_KEYS.has(key)) continue;
|
|
137
|
+
usageExtras[key] = value;
|
|
138
|
+
}
|
|
98
139
|
return {
|
|
99
140
|
provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),
|
|
100
141
|
...overrides?.nativeFinishReason !== void 0 && { nativeFinishReason: overrides.nativeFinishReason },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openrouter-chat.js","names":[],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const u = overrides?.usage;\n const usageExtras: OpenRouterUsageExtras = {};\n if (u?.cost !== undefined) {\n usageExtras.cost = u.cost;\n const cd = u.cost_details;\n usageExtras.cost_details = {\n ...(cd ?? {}),\n upstream_inference_cost: cd?.upstream_inference_cost ?? 0,\n upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,\n upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0,\n };\n }\n if (u?.prompt_tokens_details !== undefined) {\n usageExtras.prompt_tokens_details = {\n ...u.prompt_tokens_details,\n cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0,\n };\n }\n if (u?.completion_tokens_details !== undefined) {\n usageExtras.completion_tokens_details = {\n ...u.completion_tokens_details,\n reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0,\n };\n }\n if (u?.is_byok !== undefined) {\n usageExtras.is_byok = u.is_byok;\n }\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return { ...usage, ...shaping.usageExtras };\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify({\n error: {\n code: status,\n message,\n ...(metadata !== undefined && { metadata }),\n },\n });\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\n/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */\nconst DEFAULT_OPENROUTER_MODELS = [\n \"openai/gpt-4o\",\n \"anthropic/claude-3.5-sonnet\",\n \"google/gemini-2.0-flash-001\",\n];\n\n/**\n * Build one OpenRouter model-catalog object (deterministic static filler).\n * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):\n * pricing values are STRINGS; the object also carries hugging_face_id,\n * knowledge_cutoff, supported_voices, links, default_parameters,\n * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).\n */\nfunction openRouterModelObject(id: string): Record<string, unknown> {\n return {\n id,\n canonical_slug: id,\n hugging_face_id: null,\n name: id,\n created: 0,\n description: `Mock OpenRouter model ${id} served by aimock.`,\n context_length: 128000,\n architecture: {\n modality: \"text->text\",\n input_modalities: [\"text\"],\n output_modalities: [\"text\"],\n tokenizer: \"Other\",\n instruct_type: null,\n },\n pricing: {\n prompt: \"0\",\n completion: \"0\",\n request: \"0\",\n image: \"0\",\n web_search: \"0\",\n internal_reasoning: \"0\",\n input_cache_read: \"0\",\n input_cache_write: \"0\",\n },\n top_provider: {\n context_length: 128000,\n max_completion_tokens: 16384,\n is_moderated: false,\n },\n per_request_limits: null,\n supported_parameters: [\n \"tools\",\n \"tool_choice\",\n \"max_tokens\",\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"reasoning\",\n ],\n default_parameters: null,\n supported_voices: null,\n knowledge_cutoff: null,\n expiration_date: null,\n links: { details: `/api/v1/models/${id}/endpoints` },\n reasoning: { mandatory: false, default_enabled: false },\n };\n}\n\n/**\n * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat\n * (or endpoint-less) fixture with a string `match.model`, in first-seen order;\n * the default set when none contribute one. Mirrors the Ollama `/api/tags` and\n * OpenRouter video `/models` synthesis idioms.\n */\nfunction collectModelIds(fixtures: Fixture[]): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const f of fixtures) {\n if (f.match.endpoint !== undefined && f.match.endpoint !== \"chat\") continue;\n const m = f.match.model;\n if (typeof m === \"string\" && m.length > 0 && !seen.has(m)) {\n seen.add(m);\n ids.push(m);\n }\n }\n return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;\n}\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const data = collectModelIds(fixtures).map(openRouterModelObject);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data }));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8BA,MAAa,yBAAyB;AAEtC,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;;;;;;;AAQ7B,SAAgB,iBAAiB,kBAAmC;AAClE,QAAO,iBAAiB,WAAW,uBAAuB;;;;;;;;;AAU5D,SAAgB,yBAAyB,OAAmC;AAC1E,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,QAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,GAAG,MAAM;;;;;;;AAQrD,SAAgB,0BAA0B,MAAuC;CAC/E,MAAM,MAAM,CAAC,KAAK,OAAO,GAAI,MAAM,QAAQ,KAAK,OAAO,GAAG,KAAK,SAAS,EAAE,CAAE;CAC5E,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,MAAgB,EAAE;AACxB,MAAK,MAAM,KAAK,KAAK;AACnB,MAAI,OAAO,MAAM,YAAY,EAAE,WAAW,EAAG;AAC7C,MAAI,KAAK,IAAI,EAAE,CAAE;AACjB,OAAK,IAAI,EAAE;AACX,MAAI,KAAK,EAAE;;AAEb,QAAO;;;AAIT,SAAS,QAAQ,IAAoB;AACnC,QAAO,GAAG,WAAW,iBAAiB,GAClC,uBAAuB,GAAG,MAAM,EAAwB,GACxD;;;;;;;;;;;;;AAyBN,SAAgB,yBACd,WACA,cACmB;CACnB,MAAM,IAAI,WAAW;CACrB,MAAM,cAAqC,EAAE;AAC7C,KAAI,GAAG,SAAS,QAAW;AACzB,cAAY,OAAO,EAAE;EACrB,MAAM,KAAK,EAAE;AACb,cAAY,eAAe;GACzB,GAAI,MAAM,EAAE;GACZ,yBAAyB,IAAI,2BAA2B;GACxD,gCAAgC,IAAI,kCAAkC;GACtE,qCAAqC,IAAI,uCAAuC;GACjF;;AAEH,KAAI,GAAG,0BAA0B,OAC/B,aAAY,wBAAwB;EAClC,GAAG,EAAE;EACL,eAAe,EAAE,sBAAsB,iBAAiB;EACzD;AAEH,KAAI,GAAG,8BAA8B,OACnC,aAAY,4BAA4B;EACtC,GAAG,EAAE;EACL,kBAAkB,EAAE,0BAA0B,oBAAoB;EACnE;AAEH,KAAI,GAAG,YAAY,OACjB,aAAY,UAAU,EAAE;AAE1B,QAAO;EACL,UAAU,WAAW,YAAY,yBAAyB,aAAa;EACvE,GAAI,WAAW,uBAAuB,UAAa,EACjD,oBAAoB,UAAU,oBAC/B;EACD;EACD;;;AAIH,SAAS,aACP,OACA,SAC2B;AAC3B,QAAO;EAAE,GAAG;EAAO,GAAG,QAAQ;EAAa;;;AAI7C,SAAS,aAAa,cAA6B,SAA2C;AAC5F,KAAI,gBAAgB,KAAM,QAAO;AACjC,QAAO,QAAQ,sBAAsB;;;;;;;;;;;AAYvC,SAAgB,0BACd,YACA,SACA,cACgB;AAChB,KAAI,CAAC,aAAc,YAAW,KAAK,QAAQ,WAAW,GAAG;AACzD,YAAW,WAAW,QAAQ;AAC9B,KAAI,WAAW,uBAAuB,OAAW,YAAW,qBAAqB;AACjF,KAAI,WAAW,iBAAiB,OAAW,YAAW,eAAe;AACrE,MAAK,MAAM,UAAU,WAAW,SAAS;AAEvC,SAAO,uBAAuB,QAAQ,sBAAsB,OAAO;AACnE,MAAI,OAAO,QAAQ,cAAc,OAC/B,QAAO,QAAQ,YAAY,OAAO,QAAQ,qBAAqB;;AAGnE,YAAW,QAAQ,aAAa,WAAW,OAAO,QAAQ;AAC1D,QAAO;;;;;;;;;;;AAYT,SAAgB,sBACd,QACA,SACA,cACY;AACZ,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,CAAC,aAAc,OAAM,KAAK,QAAQ,MAAM,GAAG;AAC/C,QAAM,WAAW,QAAQ;AACzB,MAAI,MAAM,uBAAuB,OAAW,OAAM,qBAAqB;AACvE,OAAK,MAAM,UAAU,MAAM,SAAS;AAClC,UAAO,uBAAuB,aAAa,OAAO,eAAe,QAAQ;AAGzE,OAAI,OAAO,OAAO,MAAM,YAAY,YAAY,OAAO,MAAM,SAAS,OACpE,QAAO,MAAM,OAAO;;AAGxB,MAAI,MAAM,OAAO;AACf,SAAM,QAAQ,aAAa,MAAM,OAAO,QAAQ;AAChD,SAAM,eAAe;;;AAGzB,QAAO;;;;;;;;;AAUT,SAAgB,yBACd,QACA,SACA,UACQ;AACR,QAAO,KAAK,UAAU,EACpB,OAAO;EACL,MAAM;EACN;EACA,GAAI,aAAa,UAAa,EAAE,UAAU;EAC3C,EACF,CAAC;;;AAMJ,MAAM,4BAA4B;CAChC;CACA;CACA;CACD;;;;;;;;AASD,SAAS,sBAAsB,IAAqC;AAClE,QAAO;EACL;EACA,gBAAgB;EAChB,iBAAiB;EACjB,MAAM;EACN,SAAS;EACT,aAAa,yBAAyB,GAAG;EACzC,gBAAgB;EAChB,cAAc;GACZ,UAAU;GACV,kBAAkB,CAAC,OAAO;GAC1B,mBAAmB,CAAC,OAAO;GAC3B,WAAW;GACX,eAAe;GAChB;EACD,SAAS;GACP,QAAQ;GACR,YAAY;GACZ,SAAS;GACT,OAAO;GACP,YAAY;GACZ,oBAAoB;GACpB,kBAAkB;GAClB,mBAAmB;GACpB;EACD,cAAc;GACZ,gBAAgB;GAChB,uBAAuB;GACvB,cAAc;GACf;EACD,oBAAoB;EACpB,sBAAsB;GACpB;GACA;GACA;GACA;GACA;GACA;GACA;GACD;EACD,oBAAoB;EACpB,kBAAkB;EAClB,kBAAkB;EAClB,iBAAiB;EACjB,OAAO,EAAE,SAAS,kBAAkB,GAAG,aAAa;EACpD,WAAW;GAAE,WAAW;GAAO,iBAAiB;GAAO;EACxD;;;;;;;;AASH,SAAS,gBAAgB,UAA+B;CACtD,MAAM,MAAgB,EAAE;CACxB,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,KAAK,UAAU;AACxB,MAAI,EAAE,MAAM,aAAa,UAAa,EAAE,MAAM,aAAa,OAAQ;EACnE,MAAM,IAAI,EAAE,MAAM;AAClB,MAAI,OAAO,MAAM,YAAY,EAAE,SAAS,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE;AACzD,QAAK,IAAI,EAAE;AACX,OAAI,KAAK,EAAE;;;AAGf,QAAO,IAAI,SAAS,IAAI,MAAM;;AAGhC,SAAS,WACP,KACA,SACA,MACA,QACM;AACN,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB;EACA,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE;GAAQ,SAAS;GAAM;EACpC,CAAC;;;AAIJ,SAAgB,uBACd,KACA,KACA,UACA,SACA,WACA,gBACM;AACN,gBAAe,IAAI;CACnB,MAAM,OAAO,IAAI,OAAO;CACxB,MAAM,OAAO,gBAAgB,SAAS,CAAC,IAAI,sBAAsB;AACjE,YAAW,KAAK,SAAS,MAAM,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,EAAE,MAAM,CAAC,CAAC;;;AAInC,SAAgB,oBACd,KACA,KACA,SACA,gBACM;AACN,gBAAe,IAAI;AAEnB,YAAW,KAAK,SADH,IAAI,OAAO,eACO,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAI1D,KAAI,IACF,KAAK,UAAU,EACb,MAAM;EACJ,OAAO;EACP,mBAAmB;EACnB,qBAAqB;EACrB,OAAO;EACP,aAAa;EACb,iBAAiB;EACjB,uBAAuB;EACvB,OAAO;EACP,aAAa;EACb,cAAc;EACd,eAAe;EACf,YAAY;EACZ,kBAAkB;EAClB,mBAAmB;EACnB,oBAAoB;EACpB,cAAc;EACd,YAAY;EACZ,iBAAiB;EACjB,YAAY;GACV,UAAU;GACV,UAAU;GACV,MAAM;GACP;EACF,EACF,CAAC,CACH;;;AAIH,SAAgB,wBACd,KACA,KACA,SACA,gBACM;AACN,gBAAe,IAAI;AAEnB,YAAW,KAAK,SADH,IAAI,OAAO,mBACO,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,EAAE,MAAM;EAAE,eAAe;EAAG,aAAa;EAAG,EAAE,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"openrouter-chat.js","names":[],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * `usage` keys that are already accounted for elsewhere and must therefore NOT\n * be re-emitted through the forward-compat passthrough below: the canonical\n * token counts (and their per-provider aliases), which `resolveUsage` folds into\n * `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage\n * object, plus every field this function shapes explicitly.\n */\nconst SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([\n \"prompt_tokens\",\n \"completion_tokens\",\n \"total_tokens\",\n \"input_tokens\",\n \"output_tokens\",\n \"promptTokenCount\",\n \"candidatesTokenCount\",\n \"totalTokenCount\",\n \"cost\",\n \"cost_details\",\n \"prompt_tokens_details\",\n \"completion_tokens_details\",\n \"is_byok\",\n]);\n\n/**\n * Keys that must never flow through the passthrough assignment below: a\n * JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream\n * provider responses) can carry a real own `__proto__`/`constructor`/`prototype`\n * key, and a plain `usageExtras[key] = value` for one of those hits the\n * prototype setter, corrupting the emitted object's prototype chain instead of\n * emitting a data field. Real providers never send these, so skipping is safe.\n */\nconst UNSAFE_PROTO_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const u = overrides?.usage;\n const usageExtras: OpenRouterUsageExtras = {};\n if (u?.cost !== undefined) {\n usageExtras.cost = u.cost;\n const cd = u.cost_details;\n usageExtras.cost_details = {\n ...(cd ?? {}),\n upstream_inference_cost: cd?.upstream_inference_cost ?? 0,\n upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,\n upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0,\n };\n }\n if (u?.prompt_tokens_details !== undefined) {\n usageExtras.prompt_tokens_details = {\n ...u.prompt_tokens_details,\n cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0,\n };\n }\n if (u?.completion_tokens_details !== undefined) {\n usageExtras.completion_tokens_details = {\n ...u.completion_tokens_details,\n reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0,\n };\n }\n if (u?.is_byok !== undefined) {\n usageExtras.is_byok = u.is_byok;\n }\n // Forward-compat passthrough (#368): a fixture's `usage` may carry provider\n // fields aimock does not model — typically written by the recorder straight\n // from a real upstream usage frame (OpenRouter `native_tokens_prompt`,\n // `native_tokens_completion`, …). Emit them verbatim rather than silently\n // dropping them, so a recorded usage object round-trips on replay. The\n // canonical token counts and every field shaped above are excluded: those are\n // already resolved onto the base usage object (see helpers.ts `resolveUsage`)\n // or handled with their own required-member defaults.\n for (const [key, value] of Object.entries(u ?? {})) {\n if (value === undefined) continue;\n if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;\n if (UNSAFE_PROTO_KEYS.has(key)) continue;\n usageExtras[key] = value;\n }\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return { ...usage, ...shaping.usageExtras };\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify({\n error: {\n code: status,\n message,\n ...(metadata !== undefined && { metadata }),\n },\n });\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\n/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */\nconst DEFAULT_OPENROUTER_MODELS = [\n \"openai/gpt-4o\",\n \"anthropic/claude-3.5-sonnet\",\n \"google/gemini-2.0-flash-001\",\n];\n\n/**\n * Build one OpenRouter model-catalog object (deterministic static filler).\n * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):\n * pricing values are STRINGS; the object also carries hugging_face_id,\n * knowledge_cutoff, supported_voices, links, default_parameters,\n * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).\n */\nfunction openRouterModelObject(id: string): Record<string, unknown> {\n return {\n id,\n canonical_slug: id,\n hugging_face_id: null,\n name: id,\n created: 0,\n description: `Mock OpenRouter model ${id} served by aimock.`,\n context_length: 128000,\n architecture: {\n modality: \"text->text\",\n input_modalities: [\"text\"],\n output_modalities: [\"text\"],\n tokenizer: \"Other\",\n instruct_type: null,\n },\n pricing: {\n prompt: \"0\",\n completion: \"0\",\n request: \"0\",\n image: \"0\",\n web_search: \"0\",\n internal_reasoning: \"0\",\n input_cache_read: \"0\",\n input_cache_write: \"0\",\n },\n top_provider: {\n context_length: 128000,\n max_completion_tokens: 16384,\n is_moderated: false,\n },\n per_request_limits: null,\n supported_parameters: [\n \"tools\",\n \"tool_choice\",\n \"max_tokens\",\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"reasoning\",\n ],\n default_parameters: null,\n supported_voices: null,\n knowledge_cutoff: null,\n expiration_date: null,\n links: { details: `/api/v1/models/${id}/endpoints` },\n reasoning: { mandatory: false, default_enabled: false },\n };\n}\n\n/**\n * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat\n * (or endpoint-less) fixture with a string `match.model`, in first-seen order;\n * the default set when none contribute one. Mirrors the Ollama `/api/tags` and\n * OpenRouter video `/models` synthesis idioms.\n */\nfunction collectModelIds(fixtures: Fixture[]): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const f of fixtures) {\n if (f.match.endpoint !== undefined && f.match.endpoint !== \"chat\") continue;\n const m = f.match.model;\n if (typeof m === \"string\" && m.length > 0 && !seen.has(m)) {\n seen.add(m);\n ids.push(m);\n }\n }\n return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;\n}\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const data = collectModelIds(fixtures).map(openRouterModelObject);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data }));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":"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"}
|
package/dist/provider-auth.cjs
CHANGED
|
@@ -22,6 +22,7 @@ const PROVIDER_AUTH_SCHEMES = {
|
|
|
22
22
|
openrouter: { kind: "bearer" },
|
|
23
23
|
cohere: { kind: "bearer" },
|
|
24
24
|
grok: { kind: "bearer" },
|
|
25
|
+
byteplus: { kind: "bearer" },
|
|
25
26
|
ollama: { kind: "bearer" },
|
|
26
27
|
anthropic: { kind: "x-api-key" },
|
|
27
28
|
gemini: { kind: "x-goog-api-key" },
|
|
@@ -175,6 +176,7 @@ function readProviderKeysFromEnv(env = process.env) {
|
|
|
175
176
|
if (env.AIMOCK_PROVIDER_OPENROUTER_KEY) keys.openrouter = env.AIMOCK_PROVIDER_OPENROUTER_KEY;
|
|
176
177
|
if (env.AIMOCK_PROVIDER_COHERE_KEY) keys.cohere = env.AIMOCK_PROVIDER_COHERE_KEY;
|
|
177
178
|
if (env.AIMOCK_PROVIDER_GROK_KEY) keys.grok = env.AIMOCK_PROVIDER_GROK_KEY;
|
|
179
|
+
if (env.AIMOCK_PROVIDER_BYTEPLUS_KEY) keys.byteplus = env.AIMOCK_PROVIDER_BYTEPLUS_KEY;
|
|
178
180
|
if (env.AIMOCK_PROVIDER_OLLAMA_KEY) keys.ollama = env.AIMOCK_PROVIDER_OLLAMA_KEY;
|
|
179
181
|
if (env.AIMOCK_PROVIDER_VEO_KEY) keys.veo = env.AIMOCK_PROVIDER_VEO_KEY;
|
|
180
182
|
if (env.AIMOCK_PROVIDER_AZURE_KEY) keys.azure = env.AIMOCK_PROVIDER_AZURE_KEY;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-auth.cjs","names":[],"sources":["../src/provider-auth.ts"],"sourcesContent":["import type { RecordProviderKey } from \"./types.js\";\n\n/**\n * Default marker prefix identifying a \"dummy\" credential that a caller sends\n * only to satisfy an SDK's non-empty API-key requirement (e.g. `sk-aimock-...`).\n * A caller credential that is absent OR begins with this prefix is treated as a\n * placeholder that aimock is free to override with its own built-in upstream key.\n * A caller credential that does NOT begin with this prefix is treated as a real\n * key and forwarded verbatim (the caller overrides aimock).\n *\n * Overridable via the `AIMOCK_DUMMY_KEY_MARKER` env var for setups that mint\n * placeholder keys under a different prefix.\n */\nexport const DEFAULT_DUMMY_KEY_MARKER = \"sk-aimock-\";\n\n/** Resolve the active dummy-key marker, honoring the env override. */\nexport function getDummyKeyMarker(): string {\n const override = process.env.AIMOCK_DUMMY_KEY_MARKER;\n return override && override.length > 0 ? override : DEFAULT_DUMMY_KEY_MARKER;\n}\n\n/**\n * How a given provider carries its API credential on the wire. aimock injects\n * its built-in key using the provider's native scheme so the upstream accepts\n * it. Providers whose auth is a signed/exchanged credential (Bedrock SigV4,\n * Vertex AI OAuth) are deliberately absent — those are NOT simple static-key\n * schemes, so aimock never rewrites their auth and always forwards the caller's\n * credential unchanged.\n *\n * Scheme kinds (all static long-lived keys):\n * - bearer `Authorization: Bearer <key>` (OpenAI, OpenRouter, Cohere, Grok/xAI, Ollama)\n * - fal-key `Authorization: Key <key>` (fal.ai — note the `Key ` prefix, NOT `Bearer`)\n * - x-api-key `x-api-key: <key>` (Anthropic)\n * - x-goog-api-key `x-goog-api-key: <key>` (Gemini, Gemini Interactions, Veo)\n * - api-key `api-key: <key>` (Azure OpenAI static-key auth)\n * - xi-api-key `xi-api-key: <key>` (ElevenLabs)\n *\n * Note on Azure: Azure OpenAI also supports Microsoft Entra ID `Authorization:\n * Bearer <token>` OAuth. aimock only ever injects the static `api-key` header,\n * and a real Entra bearer token from the caller is never dummy-prefixed, so it\n * is always forwarded verbatim (the caller overrides aimock).\n */\nexport type AuthScheme =\n | { kind: \"bearer\" } // Authorization: Bearer <key>\n | { kind: \"fal-key\" } // Authorization: Key <key>\n | { kind: \"x-api-key\" } // x-api-key: <key>\n | { kind: \"x-goog-api-key\" } // x-goog-api-key: <key>\n | { kind: \"api-key\" } // api-key: <key>\n | { kind: \"xi-api-key\" }; // xi-api-key: <key>\n\nconst PROVIDER_AUTH_SCHEMES: Partial<Record<RecordProviderKey, AuthScheme>> = {\n // Bearer-token providers.\n openai: { kind: \"bearer\" },\n openrouter: { kind: \"bearer\" },\n cohere: { kind: \"bearer\" },\n grok: { kind: \"bearer\" }, // xAI\n ollama: { kind: \"bearer\" }, // Ollama Cloud / bearer-gated Ollama servers\n // Anthropic.\n anthropic: { kind: \"x-api-key\" },\n // Google (Gemini + Veo) use the x-goog-api-key header scheme.\n gemini: { kind: \"x-goog-api-key\" },\n \"gemini-interactions\": { kind: \"x-goog-api-key\" },\n veo: { kind: \"x-goog-api-key\" },\n // Azure OpenAI static-key auth.\n azure: { kind: \"api-key\" },\n // ElevenLabs.\n elevenlabs: { kind: \"xi-api-key\" },\n // fal.ai — uses the `Key ` prefix on Authorization, NOT `Bearer `.\n fal: { kind: \"fal-key\" },\n};\n\nexport function hasStaticProviderAuthScheme(providerKey: RecordProviderKey): boolean {\n return PROVIDER_AUTH_SCHEMES[providerKey] !== undefined;\n}\n\n/** Force a known provider credential onto an already-scrubbed outbound map. */\nexport function applyConfiguredProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n providerCredential: string,\n): boolean {\n void target;\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return false;\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders.Authorization = `Bearer ${providerCredential}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders.Authorization = `Key ${providerCredential}`;\n break;\n default:\n takeHeader(forwardHeaders, authHeaderName(scheme));\n forwardHeaders[authHeaderName(scheme)] = providerCredential;\n }\n return true;\n}\n\n/**\n * Case-insensitively delete a header from a forward-header map, returning the\n * value that was present (if any). Header maps preserve the original casing of\n * incoming headers, so we can't assume a canonical key.\n */\nfunction takeHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n let found: string | undefined;\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) {\n found = headers[key];\n delete headers[key];\n }\n }\n return found;\n}\n\n/**\n * Extract the caller's credential value for a given auth scheme from the\n * forwarded headers, if present. For bearer, strips the `Bearer ` prefix.\n */\nfunction readCallerCredential(\n headers: Record<string, string>,\n scheme: AuthScheme,\n): string | undefined {\n if (scheme.kind === \"bearer\" || scheme.kind === \"fal-key\") {\n const raw = scanHeader(headers, \"authorization\");\n if (raw === undefined) return undefined;\n // Strip the scheme prefix (`Bearer ` for bearer, `Key ` for fal) if present,\n // otherwise treat the whole value as the credential.\n const prefix = scheme.kind === \"fal-key\" ? \"Key\" : \"Bearer\";\n const match = new RegExp(`^\\\\s*${prefix}\\\\s+(.+)$`, \"i\").exec(raw);\n return match ? match[1].trim() : raw.trim();\n }\n return scanHeader(headers, authHeaderName(scheme));\n}\n\n/**\n * The wire header name a non-Authorization scheme carries its credential in.\n * (bearer/fal-key both use `Authorization` and are handled separately.)\n */\nfunction authHeaderName(scheme: AuthScheme): string {\n switch (scheme.kind) {\n case \"x-api-key\":\n return \"x-api-key\";\n case \"x-goog-api-key\":\n return \"x-goog-api-key\";\n case \"api-key\":\n return \"api-key\";\n case \"xi-api-key\":\n return \"xi-api-key\";\n case \"bearer\":\n case \"fal-key\":\n return \"authorization\";\n }\n}\n\n/** Case-insensitive header lookup that does not mutate the map. */\nfunction scanHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) return headers[key];\n }\n return undefined;\n}\n\n/**\n * Decide whether aimock should override the caller's credential with its own\n * built-in key. Override when the caller sent no credential OR sent a dummy\n * placeholder (prefixed with the active marker). A real caller key is left\n * untouched so the caller can always override aimock.\n */\nfunction shouldOverride(callerCredential: string | undefined, marker: string): boolean {\n if (callerCredential === undefined || callerCredential.length === 0) return true;\n return callerCredential.startsWith(marker);\n}\n\n/**\n * Inject aimock's built-in upstream credential into the forwarded headers when\n * appropriate (opt-in, backward-compatible). Mutates `forwardHeaders` in place\n * and may mutate `target` (query-param schemes; none currently).\n *\n * Precedence:\n * - No built-in key configured for this provider → no-op (forward verbatim).\n * - Provider not a static-key scheme → no-op (Bedrock SigV4 / Vertex AI OAuth).\n * - Caller credential absent or dummy-prefixed → inject built-in key.\n * - Caller credential is a real key → forward it unchanged.\n *\n * @param forwardHeaders headers already built by buildForwardHeaders(req)\n * @param target upstream URL (reserved for query-param auth schemes)\n * @param providerKey which provider this request is routed to\n * @param builtinKey aimock's configured key for this provider (if any)\n */\nexport function applyProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n builtinKey: string | undefined,\n): void {\n void target; // reserved for future query-param schemes (e.g. Gemini ?key=)\n if (!builtinKey) return; // feature inert for this provider\n\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return; // signed/OAuth provider — never rewrite auth\n\n const marker = getDummyKeyMarker();\n const callerCredential = readCallerCredential(forwardHeaders, scheme);\n if (!shouldOverride(callerCredential, marker)) return; // caller sent a real key\n\n // Strip any existing auth headers for this scheme (across casings) before\n // setting aimock's key, so a dummy caller key can't linger alongside ours.\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Bearer ${builtinKey}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Key ${builtinKey}`;\n break;\n case \"x-api-key\":\n takeHeader(forwardHeaders, \"x-api-key\");\n forwardHeaders[\"x-api-key\"] = builtinKey;\n break;\n case \"x-goog-api-key\":\n takeHeader(forwardHeaders, \"x-goog-api-key\");\n forwardHeaders[\"x-goog-api-key\"] = builtinKey;\n break;\n case \"api-key\":\n takeHeader(forwardHeaders, \"api-key\");\n forwardHeaders[\"api-key\"] = builtinKey;\n break;\n case \"xi-api-key\":\n takeHeader(forwardHeaders, \"xi-api-key\");\n forwardHeaders[\"xi-api-key\"] = builtinKey;\n break;\n }\n}\n\n/**\n * Read per-provider built-in keys from the environment. Returns undefined when\n * no provider key is configured so the feature stays fully inert by default.\n *\n * Each wired static-key provider reads `AIMOCK_PROVIDER_<PROVIDER>_KEY`. An\n * empty-string value is treated as unset (existing truthiness pattern), keeping\n * the feature inert. `gemini-interactions` is not read here: it reuses the\n * Gemini key via the lookup-key remap in `proxyAndRecord`. Signed/OAuth\n * providers (`bedrock`, `vertexai`) have no env var — aimock never rewrites\n * their auth.\n */\nexport function readProviderKeysFromEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Partial<Record<RecordProviderKey, string>> | undefined {\n const keys: Partial<Record<RecordProviderKey, string>> = {};\n if (env.AIMOCK_PROVIDER_OPENAI_KEY) keys.openai = env.AIMOCK_PROVIDER_OPENAI_KEY;\n if (env.AIMOCK_PROVIDER_ANTHROPIC_KEY) keys.anthropic = env.AIMOCK_PROVIDER_ANTHROPIC_KEY;\n if (env.AIMOCK_PROVIDER_GEMINI_KEY) keys.gemini = env.AIMOCK_PROVIDER_GEMINI_KEY;\n if (env.AIMOCK_PROVIDER_OPENROUTER_KEY) keys.openrouter = env.AIMOCK_PROVIDER_OPENROUTER_KEY;\n if (env.AIMOCK_PROVIDER_COHERE_KEY) keys.cohere = env.AIMOCK_PROVIDER_COHERE_KEY;\n if (env.AIMOCK_PROVIDER_GROK_KEY) keys.grok = env.AIMOCK_PROVIDER_GROK_KEY;\n if (env.AIMOCK_PROVIDER_OLLAMA_KEY) keys.ollama = env.AIMOCK_PROVIDER_OLLAMA_KEY;\n if (env.AIMOCK_PROVIDER_VEO_KEY) keys.veo = env.AIMOCK_PROVIDER_VEO_KEY;\n if (env.AIMOCK_PROVIDER_AZURE_KEY) keys.azure = env.AIMOCK_PROVIDER_AZURE_KEY;\n if (env.AIMOCK_PROVIDER_ELEVENLABS_KEY) keys.elevenlabs = env.AIMOCK_PROVIDER_ELEVENLABS_KEY;\n if (env.AIMOCK_PROVIDER_FAL_KEY) keys.fal = env.AIMOCK_PROVIDER_FAL_KEY;\n return Object.keys(keys).length > 0 ? keys : undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider-auth.cjs","names":[],"sources":["../src/provider-auth.ts"],"sourcesContent":["import type { RecordProviderKey } from \"./types.js\";\n\n/**\n * Default marker prefix identifying a \"dummy\" credential that a caller sends\n * only to satisfy an SDK's non-empty API-key requirement (e.g. `sk-aimock-...`).\n * A caller credential that is absent OR begins with this prefix is treated as a\n * placeholder that aimock is free to override with its own built-in upstream key.\n * A caller credential that does NOT begin with this prefix is treated as a real\n * key and forwarded verbatim (the caller overrides aimock).\n *\n * Overridable via the `AIMOCK_DUMMY_KEY_MARKER` env var for setups that mint\n * placeholder keys under a different prefix.\n */\nexport const DEFAULT_DUMMY_KEY_MARKER = \"sk-aimock-\";\n\n/** Resolve the active dummy-key marker, honoring the env override. */\nexport function getDummyKeyMarker(): string {\n const override = process.env.AIMOCK_DUMMY_KEY_MARKER;\n return override && override.length > 0 ? override : DEFAULT_DUMMY_KEY_MARKER;\n}\n\n/**\n * How a given provider carries its API credential on the wire. aimock injects\n * its built-in key using the provider's native scheme so the upstream accepts\n * it. Providers whose auth is a signed/exchanged credential (Bedrock SigV4,\n * Vertex AI OAuth) are deliberately absent — those are NOT simple static-key\n * schemes, so aimock never rewrites their auth and always forwards the caller's\n * credential unchanged.\n *\n * Scheme kinds (all static long-lived keys):\n * - bearer `Authorization: Bearer <key>` (OpenAI, OpenRouter, Cohere, Grok/xAI, BytePlus Ark, Ollama)\n * - fal-key `Authorization: Key <key>` (fal.ai — note the `Key ` prefix, NOT `Bearer`)\n * - x-api-key `x-api-key: <key>` (Anthropic)\n * - x-goog-api-key `x-goog-api-key: <key>` (Gemini, Gemini Interactions, Veo)\n * - api-key `api-key: <key>` (Azure OpenAI static-key auth)\n * - xi-api-key `xi-api-key: <key>` (ElevenLabs)\n *\n * Note on Azure: Azure OpenAI also supports Microsoft Entra ID `Authorization:\n * Bearer <token>` OAuth. aimock only ever injects the static `api-key` header,\n * and a real Entra bearer token from the caller is never dummy-prefixed, so it\n * is always forwarded verbatim (the caller overrides aimock).\n */\nexport type AuthScheme =\n | { kind: \"bearer\" } // Authorization: Bearer <key>\n | { kind: \"fal-key\" } // Authorization: Key <key>\n | { kind: \"x-api-key\" } // x-api-key: <key>\n | { kind: \"x-goog-api-key\" } // x-goog-api-key: <key>\n | { kind: \"api-key\" } // api-key: <key>\n | { kind: \"xi-api-key\" }; // xi-api-key: <key>\n\nconst PROVIDER_AUTH_SCHEMES: Partial<Record<RecordProviderKey, AuthScheme>> = {\n // Bearer-token providers.\n openai: { kind: \"bearer\" },\n openrouter: { kind: \"bearer\" },\n cohere: { kind: \"bearer\" },\n grok: { kind: \"bearer\" }, // xAI\n byteplus: { kind: \"bearer\" }, // BytePlus Ark (ModelArk) — Authorization: Bearer $ARK_API_KEY\n ollama: { kind: \"bearer\" }, // Ollama Cloud / bearer-gated Ollama servers\n // Anthropic.\n anthropic: { kind: \"x-api-key\" },\n // Google (Gemini + Veo) use the x-goog-api-key header scheme.\n gemini: { kind: \"x-goog-api-key\" },\n \"gemini-interactions\": { kind: \"x-goog-api-key\" },\n veo: { kind: \"x-goog-api-key\" },\n // Azure OpenAI static-key auth.\n azure: { kind: \"api-key\" },\n // ElevenLabs.\n elevenlabs: { kind: \"xi-api-key\" },\n // fal.ai — uses the `Key ` prefix on Authorization, NOT `Bearer `.\n fal: { kind: \"fal-key\" },\n};\n\nexport function hasStaticProviderAuthScheme(providerKey: RecordProviderKey): boolean {\n return PROVIDER_AUTH_SCHEMES[providerKey] !== undefined;\n}\n\n/** Force a known provider credential onto an already-scrubbed outbound map. */\nexport function applyConfiguredProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n providerCredential: string,\n): boolean {\n void target;\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return false;\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders.Authorization = `Bearer ${providerCredential}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders.Authorization = `Key ${providerCredential}`;\n break;\n default:\n takeHeader(forwardHeaders, authHeaderName(scheme));\n forwardHeaders[authHeaderName(scheme)] = providerCredential;\n }\n return true;\n}\n\n/**\n * Case-insensitively delete a header from a forward-header map, returning the\n * value that was present (if any). Header maps preserve the original casing of\n * incoming headers, so we can't assume a canonical key.\n */\nfunction takeHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n let found: string | undefined;\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) {\n found = headers[key];\n delete headers[key];\n }\n }\n return found;\n}\n\n/**\n * Extract the caller's credential value for a given auth scheme from the\n * forwarded headers, if present. For bearer, strips the `Bearer ` prefix.\n */\nfunction readCallerCredential(\n headers: Record<string, string>,\n scheme: AuthScheme,\n): string | undefined {\n if (scheme.kind === \"bearer\" || scheme.kind === \"fal-key\") {\n const raw = scanHeader(headers, \"authorization\");\n if (raw === undefined) return undefined;\n // Strip the scheme prefix (`Bearer ` for bearer, `Key ` for fal) if present,\n // otherwise treat the whole value as the credential.\n const prefix = scheme.kind === \"fal-key\" ? \"Key\" : \"Bearer\";\n const match = new RegExp(`^\\\\s*${prefix}\\\\s+(.+)$`, \"i\").exec(raw);\n return match ? match[1].trim() : raw.trim();\n }\n return scanHeader(headers, authHeaderName(scheme));\n}\n\n/**\n * The wire header name a non-Authorization scheme carries its credential in.\n * (bearer/fal-key both use `Authorization` and are handled separately.)\n */\nfunction authHeaderName(scheme: AuthScheme): string {\n switch (scheme.kind) {\n case \"x-api-key\":\n return \"x-api-key\";\n case \"x-goog-api-key\":\n return \"x-goog-api-key\";\n case \"api-key\":\n return \"api-key\";\n case \"xi-api-key\":\n return \"xi-api-key\";\n case \"bearer\":\n case \"fal-key\":\n return \"authorization\";\n }\n}\n\n/** Case-insensitive header lookup that does not mutate the map. */\nfunction scanHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) return headers[key];\n }\n return undefined;\n}\n\n/**\n * Decide whether aimock should override the caller's credential with its own\n * built-in key. Override when the caller sent no credential OR sent a dummy\n * placeholder (prefixed with the active marker). A real caller key is left\n * untouched so the caller can always override aimock.\n */\nfunction shouldOverride(callerCredential: string | undefined, marker: string): boolean {\n if (callerCredential === undefined || callerCredential.length === 0) return true;\n return callerCredential.startsWith(marker);\n}\n\n/**\n * Inject aimock's built-in upstream credential into the forwarded headers when\n * appropriate (opt-in, backward-compatible). Mutates `forwardHeaders` in place\n * and may mutate `target` (query-param schemes; none currently).\n *\n * Precedence:\n * - No built-in key configured for this provider → no-op (forward verbatim).\n * - Provider not a static-key scheme → no-op (Bedrock SigV4 / Vertex AI OAuth).\n * - Caller credential absent or dummy-prefixed → inject built-in key.\n * - Caller credential is a real key → forward it unchanged.\n *\n * @param forwardHeaders headers already built by buildForwardHeaders(req)\n * @param target upstream URL (reserved for query-param auth schemes)\n * @param providerKey which provider this request is routed to\n * @param builtinKey aimock's configured key for this provider (if any)\n */\nexport function applyProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n builtinKey: string | undefined,\n): void {\n void target; // reserved for future query-param schemes (e.g. Gemini ?key=)\n if (!builtinKey) return; // feature inert for this provider\n\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return; // signed/OAuth provider — never rewrite auth\n\n const marker = getDummyKeyMarker();\n const callerCredential = readCallerCredential(forwardHeaders, scheme);\n if (!shouldOverride(callerCredential, marker)) return; // caller sent a real key\n\n // Strip any existing auth headers for this scheme (across casings) before\n // setting aimock's key, so a dummy caller key can't linger alongside ours.\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Bearer ${builtinKey}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Key ${builtinKey}`;\n break;\n case \"x-api-key\":\n takeHeader(forwardHeaders, \"x-api-key\");\n forwardHeaders[\"x-api-key\"] = builtinKey;\n break;\n case \"x-goog-api-key\":\n takeHeader(forwardHeaders, \"x-goog-api-key\");\n forwardHeaders[\"x-goog-api-key\"] = builtinKey;\n break;\n case \"api-key\":\n takeHeader(forwardHeaders, \"api-key\");\n forwardHeaders[\"api-key\"] = builtinKey;\n break;\n case \"xi-api-key\":\n takeHeader(forwardHeaders, \"xi-api-key\");\n forwardHeaders[\"xi-api-key\"] = builtinKey;\n break;\n }\n}\n\n/**\n * Read per-provider built-in keys from the environment. Returns undefined when\n * no provider key is configured so the feature stays fully inert by default.\n *\n * Each wired static-key provider reads `AIMOCK_PROVIDER_<PROVIDER>_KEY`. An\n * empty-string value is treated as unset (existing truthiness pattern), keeping\n * the feature inert. `gemini-interactions` is not read here: it reuses the\n * Gemini key via the lookup-key remap in `proxyAndRecord`. Signed/OAuth\n * providers (`bedrock`, `vertexai`) have no env var — aimock never rewrites\n * their auth.\n */\nexport function readProviderKeysFromEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Partial<Record<RecordProviderKey, string>> | undefined {\n const keys: Partial<Record<RecordProviderKey, string>> = {};\n if (env.AIMOCK_PROVIDER_OPENAI_KEY) keys.openai = env.AIMOCK_PROVIDER_OPENAI_KEY;\n if (env.AIMOCK_PROVIDER_ANTHROPIC_KEY) keys.anthropic = env.AIMOCK_PROVIDER_ANTHROPIC_KEY;\n if (env.AIMOCK_PROVIDER_GEMINI_KEY) keys.gemini = env.AIMOCK_PROVIDER_GEMINI_KEY;\n if (env.AIMOCK_PROVIDER_OPENROUTER_KEY) keys.openrouter = env.AIMOCK_PROVIDER_OPENROUTER_KEY;\n if (env.AIMOCK_PROVIDER_COHERE_KEY) keys.cohere = env.AIMOCK_PROVIDER_COHERE_KEY;\n if (env.AIMOCK_PROVIDER_GROK_KEY) keys.grok = env.AIMOCK_PROVIDER_GROK_KEY;\n if (env.AIMOCK_PROVIDER_BYTEPLUS_KEY) keys.byteplus = env.AIMOCK_PROVIDER_BYTEPLUS_KEY;\n if (env.AIMOCK_PROVIDER_OLLAMA_KEY) keys.ollama = env.AIMOCK_PROVIDER_OLLAMA_KEY;\n if (env.AIMOCK_PROVIDER_VEO_KEY) keys.veo = env.AIMOCK_PROVIDER_VEO_KEY;\n if (env.AIMOCK_PROVIDER_AZURE_KEY) keys.azure = env.AIMOCK_PROVIDER_AZURE_KEY;\n if (env.AIMOCK_PROVIDER_ELEVENLABS_KEY) keys.elevenlabs = env.AIMOCK_PROVIDER_ELEVENLABS_KEY;\n if (env.AIMOCK_PROVIDER_FAL_KEY) keys.fal = env.AIMOCK_PROVIDER_FAL_KEY;\n return Object.keys(keys).length > 0 ? keys : undefined;\n}\n"],"mappings":"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"}
|
package/dist/provider-auth.js
CHANGED
|
@@ -21,6 +21,7 @@ const PROVIDER_AUTH_SCHEMES = {
|
|
|
21
21
|
openrouter: { kind: "bearer" },
|
|
22
22
|
cohere: { kind: "bearer" },
|
|
23
23
|
grok: { kind: "bearer" },
|
|
24
|
+
byteplus: { kind: "bearer" },
|
|
24
25
|
ollama: { kind: "bearer" },
|
|
25
26
|
anthropic: { kind: "x-api-key" },
|
|
26
27
|
gemini: { kind: "x-goog-api-key" },
|
|
@@ -174,6 +175,7 @@ function readProviderKeysFromEnv(env = process.env) {
|
|
|
174
175
|
if (env.AIMOCK_PROVIDER_OPENROUTER_KEY) keys.openrouter = env.AIMOCK_PROVIDER_OPENROUTER_KEY;
|
|
175
176
|
if (env.AIMOCK_PROVIDER_COHERE_KEY) keys.cohere = env.AIMOCK_PROVIDER_COHERE_KEY;
|
|
176
177
|
if (env.AIMOCK_PROVIDER_GROK_KEY) keys.grok = env.AIMOCK_PROVIDER_GROK_KEY;
|
|
178
|
+
if (env.AIMOCK_PROVIDER_BYTEPLUS_KEY) keys.byteplus = env.AIMOCK_PROVIDER_BYTEPLUS_KEY;
|
|
177
179
|
if (env.AIMOCK_PROVIDER_OLLAMA_KEY) keys.ollama = env.AIMOCK_PROVIDER_OLLAMA_KEY;
|
|
178
180
|
if (env.AIMOCK_PROVIDER_VEO_KEY) keys.veo = env.AIMOCK_PROVIDER_VEO_KEY;
|
|
179
181
|
if (env.AIMOCK_PROVIDER_AZURE_KEY) keys.azure = env.AIMOCK_PROVIDER_AZURE_KEY;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-auth.js","names":[],"sources":["../src/provider-auth.ts"],"sourcesContent":["import type { RecordProviderKey } from \"./types.js\";\n\n/**\n * Default marker prefix identifying a \"dummy\" credential that a caller sends\n * only to satisfy an SDK's non-empty API-key requirement (e.g. `sk-aimock-...`).\n * A caller credential that is absent OR begins with this prefix is treated as a\n * placeholder that aimock is free to override with its own built-in upstream key.\n * A caller credential that does NOT begin with this prefix is treated as a real\n * key and forwarded verbatim (the caller overrides aimock).\n *\n * Overridable via the `AIMOCK_DUMMY_KEY_MARKER` env var for setups that mint\n * placeholder keys under a different prefix.\n */\nexport const DEFAULT_DUMMY_KEY_MARKER = \"sk-aimock-\";\n\n/** Resolve the active dummy-key marker, honoring the env override. */\nexport function getDummyKeyMarker(): string {\n const override = process.env.AIMOCK_DUMMY_KEY_MARKER;\n return override && override.length > 0 ? override : DEFAULT_DUMMY_KEY_MARKER;\n}\n\n/**\n * How a given provider carries its API credential on the wire. aimock injects\n * its built-in key using the provider's native scheme so the upstream accepts\n * it. Providers whose auth is a signed/exchanged credential (Bedrock SigV4,\n * Vertex AI OAuth) are deliberately absent — those are NOT simple static-key\n * schemes, so aimock never rewrites their auth and always forwards the caller's\n * credential unchanged.\n *\n * Scheme kinds (all static long-lived keys):\n * - bearer `Authorization: Bearer <key>` (OpenAI, OpenRouter, Cohere, Grok/xAI, Ollama)\n * - fal-key `Authorization: Key <key>` (fal.ai — note the `Key ` prefix, NOT `Bearer`)\n * - x-api-key `x-api-key: <key>` (Anthropic)\n * - x-goog-api-key `x-goog-api-key: <key>` (Gemini, Gemini Interactions, Veo)\n * - api-key `api-key: <key>` (Azure OpenAI static-key auth)\n * - xi-api-key `xi-api-key: <key>` (ElevenLabs)\n *\n * Note on Azure: Azure OpenAI also supports Microsoft Entra ID `Authorization:\n * Bearer <token>` OAuth. aimock only ever injects the static `api-key` header,\n * and a real Entra bearer token from the caller is never dummy-prefixed, so it\n * is always forwarded verbatim (the caller overrides aimock).\n */\nexport type AuthScheme =\n | { kind: \"bearer\" } // Authorization: Bearer <key>\n | { kind: \"fal-key\" } // Authorization: Key <key>\n | { kind: \"x-api-key\" } // x-api-key: <key>\n | { kind: \"x-goog-api-key\" } // x-goog-api-key: <key>\n | { kind: \"api-key\" } // api-key: <key>\n | { kind: \"xi-api-key\" }; // xi-api-key: <key>\n\nconst PROVIDER_AUTH_SCHEMES: Partial<Record<RecordProviderKey, AuthScheme>> = {\n // Bearer-token providers.\n openai: { kind: \"bearer\" },\n openrouter: { kind: \"bearer\" },\n cohere: { kind: \"bearer\" },\n grok: { kind: \"bearer\" }, // xAI\n ollama: { kind: \"bearer\" }, // Ollama Cloud / bearer-gated Ollama servers\n // Anthropic.\n anthropic: { kind: \"x-api-key\" },\n // Google (Gemini + Veo) use the x-goog-api-key header scheme.\n gemini: { kind: \"x-goog-api-key\" },\n \"gemini-interactions\": { kind: \"x-goog-api-key\" },\n veo: { kind: \"x-goog-api-key\" },\n // Azure OpenAI static-key auth.\n azure: { kind: \"api-key\" },\n // ElevenLabs.\n elevenlabs: { kind: \"xi-api-key\" },\n // fal.ai — uses the `Key ` prefix on Authorization, NOT `Bearer `.\n fal: { kind: \"fal-key\" },\n};\n\nexport function hasStaticProviderAuthScheme(providerKey: RecordProviderKey): boolean {\n return PROVIDER_AUTH_SCHEMES[providerKey] !== undefined;\n}\n\n/** Force a known provider credential onto an already-scrubbed outbound map. */\nexport function applyConfiguredProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n providerCredential: string,\n): boolean {\n void target;\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return false;\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders.Authorization = `Bearer ${providerCredential}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders.Authorization = `Key ${providerCredential}`;\n break;\n default:\n takeHeader(forwardHeaders, authHeaderName(scheme));\n forwardHeaders[authHeaderName(scheme)] = providerCredential;\n }\n return true;\n}\n\n/**\n * Case-insensitively delete a header from a forward-header map, returning the\n * value that was present (if any). Header maps preserve the original casing of\n * incoming headers, so we can't assume a canonical key.\n */\nfunction takeHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n let found: string | undefined;\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) {\n found = headers[key];\n delete headers[key];\n }\n }\n return found;\n}\n\n/**\n * Extract the caller's credential value for a given auth scheme from the\n * forwarded headers, if present. For bearer, strips the `Bearer ` prefix.\n */\nfunction readCallerCredential(\n headers: Record<string, string>,\n scheme: AuthScheme,\n): string | undefined {\n if (scheme.kind === \"bearer\" || scheme.kind === \"fal-key\") {\n const raw = scanHeader(headers, \"authorization\");\n if (raw === undefined) return undefined;\n // Strip the scheme prefix (`Bearer ` for bearer, `Key ` for fal) if present,\n // otherwise treat the whole value as the credential.\n const prefix = scheme.kind === \"fal-key\" ? \"Key\" : \"Bearer\";\n const match = new RegExp(`^\\\\s*${prefix}\\\\s+(.+)$`, \"i\").exec(raw);\n return match ? match[1].trim() : raw.trim();\n }\n return scanHeader(headers, authHeaderName(scheme));\n}\n\n/**\n * The wire header name a non-Authorization scheme carries its credential in.\n * (bearer/fal-key both use `Authorization` and are handled separately.)\n */\nfunction authHeaderName(scheme: AuthScheme): string {\n switch (scheme.kind) {\n case \"x-api-key\":\n return \"x-api-key\";\n case \"x-goog-api-key\":\n return \"x-goog-api-key\";\n case \"api-key\":\n return \"api-key\";\n case \"xi-api-key\":\n return \"xi-api-key\";\n case \"bearer\":\n case \"fal-key\":\n return \"authorization\";\n }\n}\n\n/** Case-insensitive header lookup that does not mutate the map. */\nfunction scanHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) return headers[key];\n }\n return undefined;\n}\n\n/**\n * Decide whether aimock should override the caller's credential with its own\n * built-in key. Override when the caller sent no credential OR sent a dummy\n * placeholder (prefixed with the active marker). A real caller key is left\n * untouched so the caller can always override aimock.\n */\nfunction shouldOverride(callerCredential: string | undefined, marker: string): boolean {\n if (callerCredential === undefined || callerCredential.length === 0) return true;\n return callerCredential.startsWith(marker);\n}\n\n/**\n * Inject aimock's built-in upstream credential into the forwarded headers when\n * appropriate (opt-in, backward-compatible). Mutates `forwardHeaders` in place\n * and may mutate `target` (query-param schemes; none currently).\n *\n * Precedence:\n * - No built-in key configured for this provider → no-op (forward verbatim).\n * - Provider not a static-key scheme → no-op (Bedrock SigV4 / Vertex AI OAuth).\n * - Caller credential absent or dummy-prefixed → inject built-in key.\n * - Caller credential is a real key → forward it unchanged.\n *\n * @param forwardHeaders headers already built by buildForwardHeaders(req)\n * @param target upstream URL (reserved for query-param auth schemes)\n * @param providerKey which provider this request is routed to\n * @param builtinKey aimock's configured key for this provider (if any)\n */\nexport function applyProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n builtinKey: string | undefined,\n): void {\n void target; // reserved for future query-param schemes (e.g. Gemini ?key=)\n if (!builtinKey) return; // feature inert for this provider\n\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return; // signed/OAuth provider — never rewrite auth\n\n const marker = getDummyKeyMarker();\n const callerCredential = readCallerCredential(forwardHeaders, scheme);\n if (!shouldOverride(callerCredential, marker)) return; // caller sent a real key\n\n // Strip any existing auth headers for this scheme (across casings) before\n // setting aimock's key, so a dummy caller key can't linger alongside ours.\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Bearer ${builtinKey}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Key ${builtinKey}`;\n break;\n case \"x-api-key\":\n takeHeader(forwardHeaders, \"x-api-key\");\n forwardHeaders[\"x-api-key\"] = builtinKey;\n break;\n case \"x-goog-api-key\":\n takeHeader(forwardHeaders, \"x-goog-api-key\");\n forwardHeaders[\"x-goog-api-key\"] = builtinKey;\n break;\n case \"api-key\":\n takeHeader(forwardHeaders, \"api-key\");\n forwardHeaders[\"api-key\"] = builtinKey;\n break;\n case \"xi-api-key\":\n takeHeader(forwardHeaders, \"xi-api-key\");\n forwardHeaders[\"xi-api-key\"] = builtinKey;\n break;\n }\n}\n\n/**\n * Read per-provider built-in keys from the environment. Returns undefined when\n * no provider key is configured so the feature stays fully inert by default.\n *\n * Each wired static-key provider reads `AIMOCK_PROVIDER_<PROVIDER>_KEY`. An\n * empty-string value is treated as unset (existing truthiness pattern), keeping\n * the feature inert. `gemini-interactions` is not read here: it reuses the\n * Gemini key via the lookup-key remap in `proxyAndRecord`. Signed/OAuth\n * providers (`bedrock`, `vertexai`) have no env var — aimock never rewrites\n * their auth.\n */\nexport function readProviderKeysFromEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Partial<Record<RecordProviderKey, string>> | undefined {\n const keys: Partial<Record<RecordProviderKey, string>> = {};\n if (env.AIMOCK_PROVIDER_OPENAI_KEY) keys.openai = env.AIMOCK_PROVIDER_OPENAI_KEY;\n if (env.AIMOCK_PROVIDER_ANTHROPIC_KEY) keys.anthropic = env.AIMOCK_PROVIDER_ANTHROPIC_KEY;\n if (env.AIMOCK_PROVIDER_GEMINI_KEY) keys.gemini = env.AIMOCK_PROVIDER_GEMINI_KEY;\n if (env.AIMOCK_PROVIDER_OPENROUTER_KEY) keys.openrouter = env.AIMOCK_PROVIDER_OPENROUTER_KEY;\n if (env.AIMOCK_PROVIDER_COHERE_KEY) keys.cohere = env.AIMOCK_PROVIDER_COHERE_KEY;\n if (env.AIMOCK_PROVIDER_GROK_KEY) keys.grok = env.AIMOCK_PROVIDER_GROK_KEY;\n if (env.AIMOCK_PROVIDER_OLLAMA_KEY) keys.ollama = env.AIMOCK_PROVIDER_OLLAMA_KEY;\n if (env.AIMOCK_PROVIDER_VEO_KEY) keys.veo = env.AIMOCK_PROVIDER_VEO_KEY;\n if (env.AIMOCK_PROVIDER_AZURE_KEY) keys.azure = env.AIMOCK_PROVIDER_AZURE_KEY;\n if (env.AIMOCK_PROVIDER_ELEVENLABS_KEY) keys.elevenlabs = env.AIMOCK_PROVIDER_ELEVENLABS_KEY;\n if (env.AIMOCK_PROVIDER_FAL_KEY) keys.fal = env.AIMOCK_PROVIDER_FAL_KEY;\n return Object.keys(keys).length > 0 ? keys : undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider-auth.js","names":[],"sources":["../src/provider-auth.ts"],"sourcesContent":["import type { RecordProviderKey } from \"./types.js\";\n\n/**\n * Default marker prefix identifying a \"dummy\" credential that a caller sends\n * only to satisfy an SDK's non-empty API-key requirement (e.g. `sk-aimock-...`).\n * A caller credential that is absent OR begins with this prefix is treated as a\n * placeholder that aimock is free to override with its own built-in upstream key.\n * A caller credential that does NOT begin with this prefix is treated as a real\n * key and forwarded verbatim (the caller overrides aimock).\n *\n * Overridable via the `AIMOCK_DUMMY_KEY_MARKER` env var for setups that mint\n * placeholder keys under a different prefix.\n */\nexport const DEFAULT_DUMMY_KEY_MARKER = \"sk-aimock-\";\n\n/** Resolve the active dummy-key marker, honoring the env override. */\nexport function getDummyKeyMarker(): string {\n const override = process.env.AIMOCK_DUMMY_KEY_MARKER;\n return override && override.length > 0 ? override : DEFAULT_DUMMY_KEY_MARKER;\n}\n\n/**\n * How a given provider carries its API credential on the wire. aimock injects\n * its built-in key using the provider's native scheme so the upstream accepts\n * it. Providers whose auth is a signed/exchanged credential (Bedrock SigV4,\n * Vertex AI OAuth) are deliberately absent — those are NOT simple static-key\n * schemes, so aimock never rewrites their auth and always forwards the caller's\n * credential unchanged.\n *\n * Scheme kinds (all static long-lived keys):\n * - bearer `Authorization: Bearer <key>` (OpenAI, OpenRouter, Cohere, Grok/xAI, BytePlus Ark, Ollama)\n * - fal-key `Authorization: Key <key>` (fal.ai — note the `Key ` prefix, NOT `Bearer`)\n * - x-api-key `x-api-key: <key>` (Anthropic)\n * - x-goog-api-key `x-goog-api-key: <key>` (Gemini, Gemini Interactions, Veo)\n * - api-key `api-key: <key>` (Azure OpenAI static-key auth)\n * - xi-api-key `xi-api-key: <key>` (ElevenLabs)\n *\n * Note on Azure: Azure OpenAI also supports Microsoft Entra ID `Authorization:\n * Bearer <token>` OAuth. aimock only ever injects the static `api-key` header,\n * and a real Entra bearer token from the caller is never dummy-prefixed, so it\n * is always forwarded verbatim (the caller overrides aimock).\n */\nexport type AuthScheme =\n | { kind: \"bearer\" } // Authorization: Bearer <key>\n | { kind: \"fal-key\" } // Authorization: Key <key>\n | { kind: \"x-api-key\" } // x-api-key: <key>\n | { kind: \"x-goog-api-key\" } // x-goog-api-key: <key>\n | { kind: \"api-key\" } // api-key: <key>\n | { kind: \"xi-api-key\" }; // xi-api-key: <key>\n\nconst PROVIDER_AUTH_SCHEMES: Partial<Record<RecordProviderKey, AuthScheme>> = {\n // Bearer-token providers.\n openai: { kind: \"bearer\" },\n openrouter: { kind: \"bearer\" },\n cohere: { kind: \"bearer\" },\n grok: { kind: \"bearer\" }, // xAI\n byteplus: { kind: \"bearer\" }, // BytePlus Ark (ModelArk) — Authorization: Bearer $ARK_API_KEY\n ollama: { kind: \"bearer\" }, // Ollama Cloud / bearer-gated Ollama servers\n // Anthropic.\n anthropic: { kind: \"x-api-key\" },\n // Google (Gemini + Veo) use the x-goog-api-key header scheme.\n gemini: { kind: \"x-goog-api-key\" },\n \"gemini-interactions\": { kind: \"x-goog-api-key\" },\n veo: { kind: \"x-goog-api-key\" },\n // Azure OpenAI static-key auth.\n azure: { kind: \"api-key\" },\n // ElevenLabs.\n elevenlabs: { kind: \"xi-api-key\" },\n // fal.ai — uses the `Key ` prefix on Authorization, NOT `Bearer `.\n fal: { kind: \"fal-key\" },\n};\n\nexport function hasStaticProviderAuthScheme(providerKey: RecordProviderKey): boolean {\n return PROVIDER_AUTH_SCHEMES[providerKey] !== undefined;\n}\n\n/** Force a known provider credential onto an already-scrubbed outbound map. */\nexport function applyConfiguredProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n providerCredential: string,\n): boolean {\n void target;\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return false;\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders.Authorization = `Bearer ${providerCredential}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders.Authorization = `Key ${providerCredential}`;\n break;\n default:\n takeHeader(forwardHeaders, authHeaderName(scheme));\n forwardHeaders[authHeaderName(scheme)] = providerCredential;\n }\n return true;\n}\n\n/**\n * Case-insensitively delete a header from a forward-header map, returning the\n * value that was present (if any). Header maps preserve the original casing of\n * incoming headers, so we can't assume a canonical key.\n */\nfunction takeHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n let found: string | undefined;\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) {\n found = headers[key];\n delete headers[key];\n }\n }\n return found;\n}\n\n/**\n * Extract the caller's credential value for a given auth scheme from the\n * forwarded headers, if present. For bearer, strips the `Bearer ` prefix.\n */\nfunction readCallerCredential(\n headers: Record<string, string>,\n scheme: AuthScheme,\n): string | undefined {\n if (scheme.kind === \"bearer\" || scheme.kind === \"fal-key\") {\n const raw = scanHeader(headers, \"authorization\");\n if (raw === undefined) return undefined;\n // Strip the scheme prefix (`Bearer ` for bearer, `Key ` for fal) if present,\n // otherwise treat the whole value as the credential.\n const prefix = scheme.kind === \"fal-key\" ? \"Key\" : \"Bearer\";\n const match = new RegExp(`^\\\\s*${prefix}\\\\s+(.+)$`, \"i\").exec(raw);\n return match ? match[1].trim() : raw.trim();\n }\n return scanHeader(headers, authHeaderName(scheme));\n}\n\n/**\n * The wire header name a non-Authorization scheme carries its credential in.\n * (bearer/fal-key both use `Authorization` and are handled separately.)\n */\nfunction authHeaderName(scheme: AuthScheme): string {\n switch (scheme.kind) {\n case \"x-api-key\":\n return \"x-api-key\";\n case \"x-goog-api-key\":\n return \"x-goog-api-key\";\n case \"api-key\":\n return \"api-key\";\n case \"xi-api-key\":\n return \"xi-api-key\";\n case \"bearer\":\n case \"fal-key\":\n return \"authorization\";\n }\n}\n\n/** Case-insensitive header lookup that does not mutate the map. */\nfunction scanHeader(headers: Record<string, string>, name: string): string | undefined {\n const lowerName = name.toLowerCase();\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lowerName) return headers[key];\n }\n return undefined;\n}\n\n/**\n * Decide whether aimock should override the caller's credential with its own\n * built-in key. Override when the caller sent no credential OR sent a dummy\n * placeholder (prefixed with the active marker). A real caller key is left\n * untouched so the caller can always override aimock.\n */\nfunction shouldOverride(callerCredential: string | undefined, marker: string): boolean {\n if (callerCredential === undefined || callerCredential.length === 0) return true;\n return callerCredential.startsWith(marker);\n}\n\n/**\n * Inject aimock's built-in upstream credential into the forwarded headers when\n * appropriate (opt-in, backward-compatible). Mutates `forwardHeaders` in place\n * and may mutate `target` (query-param schemes; none currently).\n *\n * Precedence:\n * - No built-in key configured for this provider → no-op (forward verbatim).\n * - Provider not a static-key scheme → no-op (Bedrock SigV4 / Vertex AI OAuth).\n * - Caller credential absent or dummy-prefixed → inject built-in key.\n * - Caller credential is a real key → forward it unchanged.\n *\n * @param forwardHeaders headers already built by buildForwardHeaders(req)\n * @param target upstream URL (reserved for query-param auth schemes)\n * @param providerKey which provider this request is routed to\n * @param builtinKey aimock's configured key for this provider (if any)\n */\nexport function applyProviderAuth(\n forwardHeaders: Record<string, string>,\n target: URL,\n providerKey: RecordProviderKey,\n builtinKey: string | undefined,\n): void {\n void target; // reserved for future query-param schemes (e.g. Gemini ?key=)\n if (!builtinKey) return; // feature inert for this provider\n\n const scheme = PROVIDER_AUTH_SCHEMES[providerKey];\n if (!scheme) return; // signed/OAuth provider — never rewrite auth\n\n const marker = getDummyKeyMarker();\n const callerCredential = readCallerCredential(forwardHeaders, scheme);\n if (!shouldOverride(callerCredential, marker)) return; // caller sent a real key\n\n // Strip any existing auth headers for this scheme (across casings) before\n // setting aimock's key, so a dummy caller key can't linger alongside ours.\n switch (scheme.kind) {\n case \"bearer\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Bearer ${builtinKey}`;\n break;\n case \"fal-key\":\n takeHeader(forwardHeaders, \"authorization\");\n forwardHeaders[\"Authorization\"] = `Key ${builtinKey}`;\n break;\n case \"x-api-key\":\n takeHeader(forwardHeaders, \"x-api-key\");\n forwardHeaders[\"x-api-key\"] = builtinKey;\n break;\n case \"x-goog-api-key\":\n takeHeader(forwardHeaders, \"x-goog-api-key\");\n forwardHeaders[\"x-goog-api-key\"] = builtinKey;\n break;\n case \"api-key\":\n takeHeader(forwardHeaders, \"api-key\");\n forwardHeaders[\"api-key\"] = builtinKey;\n break;\n case \"xi-api-key\":\n takeHeader(forwardHeaders, \"xi-api-key\");\n forwardHeaders[\"xi-api-key\"] = builtinKey;\n break;\n }\n}\n\n/**\n * Read per-provider built-in keys from the environment. Returns undefined when\n * no provider key is configured so the feature stays fully inert by default.\n *\n * Each wired static-key provider reads `AIMOCK_PROVIDER_<PROVIDER>_KEY`. An\n * empty-string value is treated as unset (existing truthiness pattern), keeping\n * the feature inert. `gemini-interactions` is not read here: it reuses the\n * Gemini key via the lookup-key remap in `proxyAndRecord`. Signed/OAuth\n * providers (`bedrock`, `vertexai`) have no env var — aimock never rewrites\n * their auth.\n */\nexport function readProviderKeysFromEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Partial<Record<RecordProviderKey, string>> | undefined {\n const keys: Partial<Record<RecordProviderKey, string>> = {};\n if (env.AIMOCK_PROVIDER_OPENAI_KEY) keys.openai = env.AIMOCK_PROVIDER_OPENAI_KEY;\n if (env.AIMOCK_PROVIDER_ANTHROPIC_KEY) keys.anthropic = env.AIMOCK_PROVIDER_ANTHROPIC_KEY;\n if (env.AIMOCK_PROVIDER_GEMINI_KEY) keys.gemini = env.AIMOCK_PROVIDER_GEMINI_KEY;\n if (env.AIMOCK_PROVIDER_OPENROUTER_KEY) keys.openrouter = env.AIMOCK_PROVIDER_OPENROUTER_KEY;\n if (env.AIMOCK_PROVIDER_COHERE_KEY) keys.cohere = env.AIMOCK_PROVIDER_COHERE_KEY;\n if (env.AIMOCK_PROVIDER_GROK_KEY) keys.grok = env.AIMOCK_PROVIDER_GROK_KEY;\n if (env.AIMOCK_PROVIDER_BYTEPLUS_KEY) keys.byteplus = env.AIMOCK_PROVIDER_BYTEPLUS_KEY;\n if (env.AIMOCK_PROVIDER_OLLAMA_KEY) keys.ollama = env.AIMOCK_PROVIDER_OLLAMA_KEY;\n if (env.AIMOCK_PROVIDER_VEO_KEY) keys.veo = env.AIMOCK_PROVIDER_VEO_KEY;\n if (env.AIMOCK_PROVIDER_AZURE_KEY) keys.azure = env.AIMOCK_PROVIDER_AZURE_KEY;\n if (env.AIMOCK_PROVIDER_ELEVENLABS_KEY) keys.elevenlabs = env.AIMOCK_PROVIDER_ELEVENLABS_KEY;\n if (env.AIMOCK_PROVIDER_FAL_KEY) keys.fal = env.AIMOCK_PROVIDER_FAL_KEY;\n return Object.keys(keys).length > 0 ? keys : undefined;\n}\n"],"mappings":"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"}
|