@copilotkit/aimock 1.40.0 → 1.42.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 +28 -1
- package/README.md +5 -3
- package/dist/a2a-mock.cjs +18 -11
- package/dist/a2a-mock.cjs.map +1 -1
- package/dist/a2a-mock.d.cts +1 -0
- package/dist/a2a-mock.d.cts.map +1 -1
- package/dist/a2a-mock.d.ts +1 -0
- package/dist/a2a-mock.d.ts.map +1 -1
- package/dist/a2a-mock.js +18 -11
- package/dist/a2a-mock.js.map +1 -1
- package/dist/agui-handler.d.cts.map +1 -1
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-mock.cjs +2 -0
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +2 -0
- package/dist/agui-mock.js.map +1 -1
- package/dist/bedrock-converse.cjs +36 -2
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +37 -3
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +50 -13
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +51 -14
- package/dist/bedrock.js.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/chaos.cjs +36 -7
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +8 -3
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +8 -3
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +36 -8
- package/dist/chaos.js.map +1 -1
- 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/cohere.cjs +58 -5
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +59 -6
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/elevenlabs-audio.cjs +36 -2
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +37 -3
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/embeddings.cjs +58 -3
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +59 -4
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +38 -2
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +39 -3
- package/dist/fal-audio.js.map +1 -1
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +9 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +9 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +19 -1
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +20 -2
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +15 -1
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +16 -2
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +70 -6
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +71 -7
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +2 -2
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.d.cts.map +1 -1
- package/dist/grok-video.d.ts.map +1 -1
- package/dist/grok-video.js +2 -2
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +160 -5
- 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 +152 -6
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +24 -6
- 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 +25 -7
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/llmock.cjs +15 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +12 -0
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +12 -0
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +16 -1
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-mock.cjs +11 -0
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +11 -0
- package/dist/mcp-mock.js.map +1 -1
- package/dist/messages.cjs +18 -1
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +19 -2
- package/dist/messages.js.map +1 -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/moderation.cjs +67 -3
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.d.cts +1 -0
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts +1 -0
- package/dist/moderation.d.ts.map +1 -1
- package/dist/moderation.js +68 -4
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +75 -5
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +76 -6
- package/dist/ollama.js.map +1 -1
- package/dist/openrouter-video.cjs +4 -4
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +4 -4
- package/dist/openrouter-video.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/rerank.cjs +65 -2
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.d.cts +1 -1
- package/dist/rerank.d.cts.map +1 -1
- package/dist/rerank.d.ts +1 -1
- package/dist/rerank.d.ts.map +1 -1
- package/dist/rerank.js +66 -3
- package/dist/rerank.js.map +1 -1
- package/dist/responses.cjs +18 -1
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +19 -2
- package/dist/responses.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/search.cjs +67 -3
- package/dist/search.cjs.map +1 -1
- package/dist/search.d.cts +1 -0
- package/dist/search.d.cts.map +1 -1
- package/dist/search.d.ts +1 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +68 -4
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +406 -17
- 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 +410 -21
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +18 -1
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +19 -2
- package/dist/speech.js.map +1 -1
- package/dist/stream-collapse.cjs +2 -1
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.js +2 -1
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +21 -2
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +21 -2
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +58 -5
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +58 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-mock.cjs +88 -0
- package/dist/vector-mock.cjs.map +1 -1
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +89 -1
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/veo-video.cjs +2 -2
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.d.cts.map +1 -1
- package/dist/veo-video.d.ts.map +1 -1
- package/dist/veo-video.js +2 -2
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +19 -2
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +20 -3
- package/dist/video.js.map +1 -1
- package/package.json +2 -2
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/moderation.cjs
CHANGED
|
@@ -61,8 +61,48 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
61
61
|
} }));
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
if (!require_helpers.isJsonObject(body)) {
|
|
65
|
+
journal.add({
|
|
66
|
+
method: req.method ?? "POST",
|
|
67
|
+
path: req.url ?? "/v1/moderations",
|
|
68
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
69
|
+
body: null,
|
|
70
|
+
service: "moderation",
|
|
71
|
+
response: {
|
|
72
|
+
status: 400,
|
|
73
|
+
fixture: null
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
77
|
+
res.end(JSON.stringify({ error: {
|
|
78
|
+
message: "Request body must be a JSON object",
|
|
79
|
+
type: "invalid_request_error"
|
|
80
|
+
} }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const normalized = require_helpers.normalizeTextInput(body.input ?? "");
|
|
84
|
+
if (normalized === null) {
|
|
85
|
+
journal.add({
|
|
86
|
+
method: req.method ?? "POST",
|
|
87
|
+
path: req.url ?? "/v1/moderations",
|
|
88
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
89
|
+
body: null,
|
|
90
|
+
service: "moderation",
|
|
91
|
+
response: {
|
|
92
|
+
status: 400,
|
|
93
|
+
fixture: null
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
97
|
+
res.end(JSON.stringify({ error: {
|
|
98
|
+
message: "Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts",
|
|
99
|
+
type: "invalid_request_error",
|
|
100
|
+
param: null,
|
|
101
|
+
code: null
|
|
102
|
+
} }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const inputText = normalized;
|
|
66
106
|
let matchedResult = DEFAULT_RESULT;
|
|
67
107
|
let matchedFixture = null;
|
|
68
108
|
for (const fixture of fixtures) if (require_helpers.matchesPattern(inputText, fixture.match)) {
|
|
@@ -72,6 +112,29 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
72
112
|
}
|
|
73
113
|
if (matchedFixture) logger.debug(`Moderation fixture matched for input "${inputText.slice(0, 80)}"`);
|
|
74
114
|
else logger.debug(`No moderation fixture matched for input "${inputText.slice(0, 80)}" — returning unflagged`);
|
|
115
|
+
if (!matchedFixture && require_helpers.resolveStrictMode(defaults.strict, req.headers)) {
|
|
116
|
+
const strictMessage = require_helpers.strictNoMatchMessage(0);
|
|
117
|
+
logger.error(require_helpers.strictNoMatchLogLine(req.method ?? "POST", req.url ?? "/v1/moderations", 0));
|
|
118
|
+
journal.add({
|
|
119
|
+
method: req.method ?? "POST",
|
|
120
|
+
path: req.url ?? "/v1/moderations",
|
|
121
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
122
|
+
body: null,
|
|
123
|
+
service: "moderation",
|
|
124
|
+
response: {
|
|
125
|
+
status: 503,
|
|
126
|
+
fixture: null,
|
|
127
|
+
...require_helpers.strictOverrideField(defaults.strict, req.headers)
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
131
|
+
res.end(JSON.stringify({ error: {
|
|
132
|
+
message: strictMessage,
|
|
133
|
+
type: "invalid_request_error",
|
|
134
|
+
code: "no_fixture_match"
|
|
135
|
+
} }));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
75
138
|
journal.add({
|
|
76
139
|
method: req.method ?? "POST",
|
|
77
140
|
path: req.url ?? "/v1/moderations",
|
|
@@ -80,7 +143,8 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
80
143
|
service: "moderation",
|
|
81
144
|
response: {
|
|
82
145
|
status: 200,
|
|
83
|
-
fixture: null
|
|
146
|
+
fixture: null,
|
|
147
|
+
...require_helpers.strictOverrideField(defaults.strict, req.headers)
|
|
84
148
|
}
|
|
85
149
|
});
|
|
86
150
|
res.writeHead(200, { "Content-Type": "application/json" });
|
package/dist/moderation.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation.cjs","names":["flattenHeaders","matchesPattern","generateId"],"sources":["../src/moderation.ts"],"sourcesContent":["/**\n * Moderation API support for LLMock.\n *\n * Handles POST /v1/moderations requests (OpenAI-compatible). Matches\n * fixtures by comparing the request `input` field against registered\n * patterns. First match wins; no match returns a default unflagged result.\n */\n\nimport type * as http from \"node:http\";\nimport {
|
|
1
|
+
{"version":3,"file":"moderation.cjs","names":["flattenHeaders","isJsonObject","normalizeTextInput","matchesPattern","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","generateId"],"sources":["../src/moderation.ts"],"sourcesContent":["/**\n * Moderation API support for LLMock.\n *\n * Handles POST /v1/moderations requests (OpenAI-compatible). Matches\n * fixtures by comparing the request `input` field against registered\n * patterns. First match wins; no match returns a default unflagged result.\n */\n\nimport type * as http from \"node:http\";\nimport {\n flattenHeaders,\n generateId,\n isJsonObject,\n matchesPattern,\n normalizeTextInput,\n resolveStrictMode,\n strictNoMatchLogLine,\n strictNoMatchMessage,\n strictOverrideField,\n} from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Moderation types ─────────────────────────────────────────────────────\n\nexport interface ModerationResult {\n flagged: boolean;\n categories: Record<string, boolean>;\n category_scores?: Record<string, number>;\n}\n\nexport interface ModerationFixture {\n match: string | RegExp;\n result: ModerationResult;\n}\n\n// ─── Default unflagged result ─────────────────────────────────────────────\n\nconst DEFAULT_RESULT: ModerationResult = {\n flagged: false,\n categories: {\n sexual: false,\n hate: false,\n harassment: false,\n \"self-harm\": false,\n \"sexual/minors\": false,\n \"hate/threatening\": false,\n \"violence/graphic\": false,\n \"self-harm/intent\": false,\n \"self-harm/instructions\": false,\n \"harassment/threatening\": false,\n violence: false,\n illicit: false,\n \"illicit/violent\": false,\n },\n category_scores: {\n sexual: 0,\n hate: 0,\n harassment: 0,\n \"self-harm\": 0,\n \"sexual/minors\": 0,\n \"hate/threatening\": 0,\n \"violence/graphic\": 0,\n \"self-harm/intent\": 0,\n \"self-harm/instructions\": 0,\n \"harassment/threatening\": 0,\n violence: 0,\n illicit: 0,\n \"illicit/violent\": 0,\n },\n};\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleModeration(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: ModerationFixture[],\n journal: Journal,\n defaults: { logger: Logger; strict?: boolean },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { input?: string | string[] };\n try {\n body = JSON.parse(raw) as { input?: string | string[] };\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `body.input` throws a TypeError that surfaces\n // as a 500 instead of a 400.\n if (!isJsonObject(body)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to a single string for matching — reject non-string\n // inputs with 400 instead of crashing in matchesPattern()/slice() (500).\n const rawInput: unknown = body.input ?? \"\";\n const normalized = normalizeTextInput(rawInput);\n if (normalized === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n const inputText = normalized;\n\n // Find first matching fixture\n let matchedResult: ModerationResult = DEFAULT_RESULT;\n let matchedFixture: ModerationFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(inputText, fixture.match)) {\n matchedFixture = fixture;\n matchedResult = fixture.result;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Moderation fixture matched for input \"${inputText.slice(0, 80)}\"`);\n } else {\n logger.debug(\n `No moderation fixture matched for input \"${inputText.slice(0, 80)}\" — returning unflagged`,\n );\n }\n\n // Strict mode turns the lenient default (unflagged) into a no-match error,\n // like every other fixture-matching handler. These fixtures carry no\n // sequence/turn state, so the skipped count is always 0.\n if (!matchedFixture && resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(0);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", req.url ?? \"/v1/moderations\", 0));\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 200,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n id: generateId(\"modr\"),\n model: \"text-moderation-latest\",\n results: [matchedResult],\n }),\n );\n}\n"],"mappings":"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"}
|
package/dist/moderation.d.cts
CHANGED
|
@@ -15,6 +15,7 @@ interface ModerationFixture {
|
|
|
15
15
|
}
|
|
16
16
|
declare function handleModeration(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: ModerationFixture[], journal: Journal, defaults: {
|
|
17
17
|
logger: Logger;
|
|
18
|
+
strict?: boolean;
|
|
18
19
|
}, setCorsHeaders: (res: http$1.ServerResponse) => void): Promise<void>;
|
|
19
20
|
//# sourceMappingURL=moderation.d.ts.map
|
|
20
21
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation.d.cts","names":[],"sources":["../src/moderation.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"moderation.d.cts","names":[],"sources":["../src/moderation.ts"],"sourcesContent":[],"mappings":";;;;;;AA+BiB,UANA,gBAAA,CAMiB;EAAA,OAAA,EAAA,OAAA;YAChB,EALJ,MAKI,CAAA,MAAA,EAAA,OAAA,CAAA;iBACR,CAAA,EALU,MAKV,CAAA,MAAA,EAAA,MAAA,CAAA;;AAyCY,UA3CL,iBAAA,CA2CqB;EAAA,KAAA,EAAA,MAAA,GA1CpB,MA0CoB;QAC/B,EA1CG,gBA0CE;;AAGA,iBAJU,gBAAA,CAIV,GAAA,EAHL,MAAA,CAAK,eAGA,EAAA,GAAA,EAFL,MAAA,CAAK,cAEA,EAAA,GAAA,EAAA,MAAA,EAAA,QAAA,EAAA,iBAAA,EAAA,EAAA,OAAA,EACD,OADC,EAAA,QAAA,EAAA;QACD,EACW,MADX;QACW,CAAA,EAAA,OAAA;iBACO,EAAA,CAAA,GAAA,EAAL,MAAA,CAAK,cAAA,EAAA,GAAA,IAAA,CAAA,EAC1B,OAD0B,CAAA,IAAA,CAAA"}
|
package/dist/moderation.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface ModerationFixture {
|
|
|
15
15
|
}
|
|
16
16
|
declare function handleModeration(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: ModerationFixture[], journal: Journal, defaults: {
|
|
17
17
|
logger: Logger;
|
|
18
|
+
strict?: boolean;
|
|
18
19
|
}, setCorsHeaders: (res: http$1.ServerResponse) => void): Promise<void>;
|
|
19
20
|
//# sourceMappingURL=moderation.d.ts.map
|
|
20
21
|
|
package/dist/moderation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation.d.ts","names":[],"sources":["../src/moderation.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"moderation.d.ts","names":[],"sources":["../src/moderation.ts"],"sourcesContent":[],"mappings":";;;;;;AA+BiB,UANA,gBAAA,CAMiB;EAAA,OAAA,EAAA,OAAA;YAChB,EALJ,MAKI,CAAA,MAAA,EAAA,OAAA,CAAA;iBACR,CAAA,EALU,MAKV,CAAA,MAAA,EAAA,MAAA,CAAA;;AAyCY,UA3CL,iBAAA,CA2CqB;EAAA,KAAA,EAAA,MAAA,GA1CpB,MA0CoB;QAC/B,EA1CG,gBA0CE;;AAGA,iBAJU,gBAAA,CAIV,GAAA,EAHL,MAAA,CAAK,eAGA,EAAA,GAAA,EAFL,MAAA,CAAK,cAEA,EAAA,GAAA,EAAA,MAAA,EAAA,QAAA,EAAA,iBAAA,EAAA,EAAA,OAAA,EACD,OADC,EAAA,QAAA,EAAA;QACD,EACW,MADX;QACW,CAAA,EAAA,OAAA;iBACO,EAAA,CAAA,GAAA,EAAL,MAAA,CAAK,cAAA,EAAA,GAAA,IAAA,CAAA,EAC1B,OAD0B,CAAA,IAAA,CAAA"}
|
package/dist/moderation.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { flattenHeaders, generateId, matchesPattern } from "./helpers.js";
|
|
1
|
+
import { flattenHeaders, generateId, isJsonObject, matchesPattern, normalizeTextInput, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
|
|
2
2
|
|
|
3
3
|
//#region src/moderation.ts
|
|
4
4
|
const DEFAULT_RESULT = {
|
|
@@ -61,8 +61,48 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
61
61
|
} }));
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
if (!isJsonObject(body)) {
|
|
65
|
+
journal.add({
|
|
66
|
+
method: req.method ?? "POST",
|
|
67
|
+
path: req.url ?? "/v1/moderations",
|
|
68
|
+
headers: flattenHeaders(req.headers),
|
|
69
|
+
body: null,
|
|
70
|
+
service: "moderation",
|
|
71
|
+
response: {
|
|
72
|
+
status: 400,
|
|
73
|
+
fixture: null
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
77
|
+
res.end(JSON.stringify({ error: {
|
|
78
|
+
message: "Request body must be a JSON object",
|
|
79
|
+
type: "invalid_request_error"
|
|
80
|
+
} }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const normalized = normalizeTextInput(body.input ?? "");
|
|
84
|
+
if (normalized === null) {
|
|
85
|
+
journal.add({
|
|
86
|
+
method: req.method ?? "POST",
|
|
87
|
+
path: req.url ?? "/v1/moderations",
|
|
88
|
+
headers: flattenHeaders(req.headers),
|
|
89
|
+
body: null,
|
|
90
|
+
service: "moderation",
|
|
91
|
+
response: {
|
|
92
|
+
status: 400,
|
|
93
|
+
fixture: null
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
97
|
+
res.end(JSON.stringify({ error: {
|
|
98
|
+
message: "Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts",
|
|
99
|
+
type: "invalid_request_error",
|
|
100
|
+
param: null,
|
|
101
|
+
code: null
|
|
102
|
+
} }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const inputText = normalized;
|
|
66
106
|
let matchedResult = DEFAULT_RESULT;
|
|
67
107
|
let matchedFixture = null;
|
|
68
108
|
for (const fixture of fixtures) if (matchesPattern(inputText, fixture.match)) {
|
|
@@ -72,6 +112,29 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
72
112
|
}
|
|
73
113
|
if (matchedFixture) logger.debug(`Moderation fixture matched for input "${inputText.slice(0, 80)}"`);
|
|
74
114
|
else logger.debug(`No moderation fixture matched for input "${inputText.slice(0, 80)}" — returning unflagged`);
|
|
115
|
+
if (!matchedFixture && resolveStrictMode(defaults.strict, req.headers)) {
|
|
116
|
+
const strictMessage = strictNoMatchMessage(0);
|
|
117
|
+
logger.error(strictNoMatchLogLine(req.method ?? "POST", req.url ?? "/v1/moderations", 0));
|
|
118
|
+
journal.add({
|
|
119
|
+
method: req.method ?? "POST",
|
|
120
|
+
path: req.url ?? "/v1/moderations",
|
|
121
|
+
headers: flattenHeaders(req.headers),
|
|
122
|
+
body: null,
|
|
123
|
+
service: "moderation",
|
|
124
|
+
response: {
|
|
125
|
+
status: 503,
|
|
126
|
+
fixture: null,
|
|
127
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
131
|
+
res.end(JSON.stringify({ error: {
|
|
132
|
+
message: strictMessage,
|
|
133
|
+
type: "invalid_request_error",
|
|
134
|
+
code: "no_fixture_match"
|
|
135
|
+
} }));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
75
138
|
journal.add({
|
|
76
139
|
method: req.method ?? "POST",
|
|
77
140
|
path: req.url ?? "/v1/moderations",
|
|
@@ -80,7 +143,8 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
80
143
|
service: "moderation",
|
|
81
144
|
response: {
|
|
82
145
|
status: 200,
|
|
83
|
-
fixture: null
|
|
146
|
+
fixture: null,
|
|
147
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
84
148
|
}
|
|
85
149
|
});
|
|
86
150
|
res.writeHead(200, { "Content-Type": "application/json" });
|
package/dist/moderation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation.js","names":[],"sources":["../src/moderation.ts"],"sourcesContent":["/**\n * Moderation API support for LLMock.\n *\n * Handles POST /v1/moderations requests (OpenAI-compatible). Matches\n * fixtures by comparing the request `input` field against registered\n * patterns. First match wins; no match returns a default unflagged result.\n */\n\nimport type * as http from \"node:http\";\nimport {
|
|
1
|
+
{"version":3,"file":"moderation.js","names":[],"sources":["../src/moderation.ts"],"sourcesContent":["/**\n * Moderation API support for LLMock.\n *\n * Handles POST /v1/moderations requests (OpenAI-compatible). Matches\n * fixtures by comparing the request `input` field against registered\n * patterns. First match wins; no match returns a default unflagged result.\n */\n\nimport type * as http from \"node:http\";\nimport {\n flattenHeaders,\n generateId,\n isJsonObject,\n matchesPattern,\n normalizeTextInput,\n resolveStrictMode,\n strictNoMatchLogLine,\n strictNoMatchMessage,\n strictOverrideField,\n} from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Moderation types ─────────────────────────────────────────────────────\n\nexport interface ModerationResult {\n flagged: boolean;\n categories: Record<string, boolean>;\n category_scores?: Record<string, number>;\n}\n\nexport interface ModerationFixture {\n match: string | RegExp;\n result: ModerationResult;\n}\n\n// ─── Default unflagged result ─────────────────────────────────────────────\n\nconst DEFAULT_RESULT: ModerationResult = {\n flagged: false,\n categories: {\n sexual: false,\n hate: false,\n harassment: false,\n \"self-harm\": false,\n \"sexual/minors\": false,\n \"hate/threatening\": false,\n \"violence/graphic\": false,\n \"self-harm/intent\": false,\n \"self-harm/instructions\": false,\n \"harassment/threatening\": false,\n violence: false,\n illicit: false,\n \"illicit/violent\": false,\n },\n category_scores: {\n sexual: 0,\n hate: 0,\n harassment: 0,\n \"self-harm\": 0,\n \"sexual/minors\": 0,\n \"hate/threatening\": 0,\n \"violence/graphic\": 0,\n \"self-harm/intent\": 0,\n \"self-harm/instructions\": 0,\n \"harassment/threatening\": 0,\n violence: 0,\n illicit: 0,\n \"illicit/violent\": 0,\n },\n};\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleModeration(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: ModerationFixture[],\n journal: Journal,\n defaults: { logger: Logger; strict?: boolean },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { input?: string | string[] };\n try {\n body = JSON.parse(raw) as { input?: string | string[] };\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `body.input` throws a TypeError that surfaces\n // as a 500 instead of a 400.\n if (!isJsonObject(body)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to a single string for matching — reject non-string\n // inputs with 400 instead of crashing in matchesPattern()/slice() (500).\n const rawInput: unknown = body.input ?? \"\";\n const normalized = normalizeTextInput(rawInput);\n if (normalized === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n const inputText = normalized;\n\n // Find first matching fixture\n let matchedResult: ModerationResult = DEFAULT_RESULT;\n let matchedFixture: ModerationFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(inputText, fixture.match)) {\n matchedFixture = fixture;\n matchedResult = fixture.result;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Moderation fixture matched for input \"${inputText.slice(0, 80)}\"`);\n } else {\n logger.debug(\n `No moderation fixture matched for input \"${inputText.slice(0, 80)}\" — returning unflagged`,\n );\n }\n\n // Strict mode turns the lenient default (unflagged) into a no-match error,\n // like every other fixture-matching handler. These fixtures carry no\n // sequence/turn state, so the skipped count is always 0.\n if (!matchedFixture && resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(0);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", req.url ?? \"/v1/moderations\", 0));\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 200,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n id: generateId(\"modr\"),\n model: \"text-moderation-latest\",\n results: [matchedResult],\n }),\n );\n}\n"],"mappings":"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"}
|
package/dist/ollama.cjs
CHANGED
|
@@ -428,7 +428,7 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
428
428
|
} }));
|
|
429
429
|
return;
|
|
430
430
|
}
|
|
431
|
-
if (!
|
|
431
|
+
if (!require_helpers.isJsonObject(ollamaReq)) {
|
|
432
432
|
journal.add({
|
|
433
433
|
method: req.method ?? "POST",
|
|
434
434
|
path: urlPath,
|
|
@@ -440,7 +440,25 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
440
440
|
}
|
|
441
441
|
});
|
|
442
442
|
require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
443
|
-
message: "
|
|
443
|
+
message: "Request body must be a JSON object",
|
|
444
|
+
type: "invalid_request_error"
|
|
445
|
+
} }));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const chatShapeError = require_helpers.validateChatMessages(ollamaReq.messages) ?? require_helpers.validateToolsField(ollamaReq.tools);
|
|
449
|
+
if (chatShapeError) {
|
|
450
|
+
journal.add({
|
|
451
|
+
method: req.method ?? "POST",
|
|
452
|
+
path: urlPath,
|
|
453
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
454
|
+
body: null,
|
|
455
|
+
response: {
|
|
456
|
+
status: 400,
|
|
457
|
+
fixture: null
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
461
|
+
message: `Invalid request: ${chatShapeError}`,
|
|
444
462
|
type: "invalid_request_error"
|
|
445
463
|
} }));
|
|
446
464
|
return;
|
|
@@ -454,7 +472,7 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
454
472
|
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
455
473
|
logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
|
|
456
474
|
} else logger.debug(`No fixture matched for request`);
|
|
457
|
-
if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
|
|
475
|
+
if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
|
|
458
476
|
method: req.method ?? "POST",
|
|
459
477
|
path: urlPath,
|
|
460
478
|
headers: require_helpers.flattenHeaders(req.headers),
|
|
@@ -686,6 +704,23 @@ async function handleOllamaGenerate(req, res, raw, fixtures, journal, defaults,
|
|
|
686
704
|
} }));
|
|
687
705
|
return;
|
|
688
706
|
}
|
|
707
|
+
if (!require_helpers.isJsonObject(generateReq)) {
|
|
708
|
+
journal.add({
|
|
709
|
+
method: req.method ?? "POST",
|
|
710
|
+
path: urlPath,
|
|
711
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
712
|
+
body: null,
|
|
713
|
+
response: {
|
|
714
|
+
status: 400,
|
|
715
|
+
fixture: null
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
719
|
+
message: "Request body must be a JSON object",
|
|
720
|
+
type: "invalid_request_error"
|
|
721
|
+
} }));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
689
724
|
if (!generateReq.prompt || typeof generateReq.prompt !== "string") {
|
|
690
725
|
journal.add({
|
|
691
726
|
method: req.method ?? "POST",
|
|
@@ -712,7 +747,7 @@ async function handleOllamaGenerate(req, res, raw, fixtures, journal, defaults,
|
|
|
712
747
|
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
713
748
|
defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
|
|
714
749
|
} else defaults.logger.debug(`No fixture matched for request`);
|
|
715
|
-
if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
|
|
750
|
+
if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
|
|
716
751
|
method: req.method ?? "POST",
|
|
717
752
|
path: urlPath,
|
|
718
753
|
headers: require_helpers.flattenHeaders(req.headers),
|
|
@@ -887,6 +922,41 @@ async function handleOllamaEmbeddings(req, res, raw, fixtures, journal, defaults
|
|
|
887
922
|
} }));
|
|
888
923
|
return;
|
|
889
924
|
}
|
|
925
|
+
if (!require_helpers.isJsonObject(embReq)) {
|
|
926
|
+
journal.add({
|
|
927
|
+
method: req.method ?? "POST",
|
|
928
|
+
path: urlPath,
|
|
929
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
930
|
+
body: null,
|
|
931
|
+
response: {
|
|
932
|
+
status: 400,
|
|
933
|
+
fixture: null
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
937
|
+
message: "Request body must be a JSON object",
|
|
938
|
+
type: "invalid_request_error"
|
|
939
|
+
} }));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const promptShapeError = embReq.prompt !== void 0 && embReq.prompt !== null && typeof embReq.prompt !== "string" ? "prompt field must be a string" : embReq.input !== void 0 && embReq.input !== null && require_helpers.normalizeEmbeddingInput(embReq.input) === null ? "input field must be a string or an array of strings" : null;
|
|
943
|
+
if (promptShapeError) {
|
|
944
|
+
journal.add({
|
|
945
|
+
method: req.method ?? "POST",
|
|
946
|
+
path: urlPath,
|
|
947
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
948
|
+
body: null,
|
|
949
|
+
response: {
|
|
950
|
+
status: 400,
|
|
951
|
+
fixture: null
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
955
|
+
message: `Invalid request: ${promptShapeError}`,
|
|
956
|
+
type: "invalid_request_error"
|
|
957
|
+
} }));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
890
960
|
const inputText = embReq.prompt ?? (typeof embReq.input === "string" ? embReq.input : void 0) ?? (Array.isArray(embReq.input) ? embReq.input.join(" ") : void 0);
|
|
891
961
|
if (!embReq.model) {
|
|
892
962
|
journal.add({
|
|
@@ -935,7 +1005,7 @@ async function handleOllamaEmbeddings(req, res, raw, fixtures, journal, defaults
|
|
|
935
1005
|
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
936
1006
|
logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
|
|
937
1007
|
} else logger.debug(`No fixture matched for request`);
|
|
938
|
-
if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
|
|
1008
|
+
if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
|
|
939
1009
|
method: req.method ?? "POST",
|
|
940
1010
|
path: urlPath,
|
|
941
1011
|
headers: require_helpers.flattenHeaders(req.headers),
|