@copilotkit/aimock 1.45.0 → 1.46.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 +24 -0
- package/README.md +7 -1
- package/dist/agui-mock.cjs +25 -25
- 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 +25 -25
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +15 -1
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +16 -2
- package/dist/aimock-cli.js.map +1 -1
- package/dist/api-key-auth.cjs +10 -9
- package/dist/api-key-auth.cjs.map +1 -1
- package/dist/api-key-auth.d.cts.map +1 -1
- package/dist/api-key-auth.d.ts.map +1 -1
- package/dist/api-key-auth.js +11 -10
- package/dist/api-key-auth.js.map +1 -1
- package/dist/batches.cjs +2 -4
- package/dist/batches.cjs.map +1 -1
- package/dist/batches.d.cts.map +1 -1
- package/dist/batches.d.ts.map +1 -1
- package/dist/batches.js +2 -4
- package/dist/batches.js.map +1 -1
- package/dist/bedrock-converse.cjs +61 -80
- 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 +58 -77
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +65 -97
- 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 +62 -94
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +11 -10
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.d.cts.map +1 -1
- package/dist/byteplus-video.d.ts.map +1 -1
- package/dist/byteplus-video.js +5 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +5 -4
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +5 -4
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +15 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +49 -74
- 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 +47 -72
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +29 -1
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +7 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +7 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +29 -2
- package/dist/config-loader.js.map +1 -1
- package/dist/count-tokens.cjs +146 -0
- package/dist/count-tokens.cjs.map +1 -0
- package/dist/count-tokens.js +146 -0
- package/dist/count-tokens.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +36 -34
- 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 +33 -31
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +38 -37
- package/dist/elevenlabs-voice.cjs.map +1 -1
- package/dist/elevenlabs-voice.d.cts.map +1 -1
- package/dist/elevenlabs-voice.d.ts.map +1 -1
- package/dist/elevenlabs-voice.js +33 -32
- package/dist/elevenlabs-voice.js.map +1 -1
- package/dist/embedding-request.cjs +16 -0
- package/dist/embedding-request.cjs.map +1 -0
- package/dist/embedding-request.js +14 -0
- package/dist/embedding-request.js.map +1 -0
- package/dist/embeddings.cjs +30 -20
- 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 +30 -20
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +45 -43
- 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 +42 -40
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +17 -15
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.js +16 -14
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +2 -4
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +2 -4
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +2 -4
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -4
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader.cjs +37 -42
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +37 -42
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +26 -24
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +26 -24
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +50 -35
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts +0 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts +0 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +49 -34
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +57 -67
- 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 +56 -67
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -12
- 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 +8 -6
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +11 -68
- 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 +12 -67
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +30 -28
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +27 -25
- package/dist/images.js.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/journal.cjs +3 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +2 -1
- package/dist/journal.js.map +1 -1
- package/dist/llmock.cjs +3 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +3 -0
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes.d.cts.map +1 -1
- package/dist/mcp-handler.cjs +14 -4
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +14 -4
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-recorder.cjs +2 -1
- package/dist/mcp-recorder.cjs.map +1 -1
- package/dist/mcp-recorder.js +3 -2
- package/dist/mcp-recorder.js.map +1 -1
- package/dist/messages.cjs +111 -96
- 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 +109 -95
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +5 -2
- 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 +6 -3
- package/dist/metrics.js.map +1 -1
- package/dist/model-utils.cjs +1 -0
- package/dist/model-utils.cjs.map +1 -1
- package/dist/model-utils.js +1 -1
- package/dist/model-utils.js.map +1 -1
- package/dist/moderation.cjs +15 -12
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +14 -11
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +128 -68
- 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 +124 -64
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +323 -3
- package/dist/openapi.cjs.map +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +324 -4
- package/dist/openapi.js.map +1 -1
- package/dist/openrouter-chat.cjs +11 -155
- package/dist/openrouter-chat.cjs.map +1 -1
- package/dist/openrouter-chat.js +11 -155
- package/dist/openrouter-chat.js.map +1 -1
- package/dist/openrouter-video.cjs +76 -71
- 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 +63 -58
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +176 -95
- 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 +176 -95
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +13 -11
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +12 -10
- package/dist/rerank.js.map +1 -1
- package/dist/response/catalog/anthropic-page.cjs +39 -0
- package/dist/response/catalog/anthropic-page.cjs.map +1 -0
- package/dist/response/catalog/anthropic-page.js +38 -0
- package/dist/response/catalog/anthropic-page.js.map +1 -0
- package/dist/response/catalog/collect.cjs +61 -0
- package/dist/response/catalog/collect.cjs.map +1 -0
- package/dist/response/catalog/collect.js +59 -0
- package/dist/response/catalog/collect.js.map +1 -0
- package/dist/response/catalog/gemini-page.cjs +25 -0
- package/dist/response/catalog/gemini-page.cjs.map +1 -0
- package/dist/response/catalog/gemini-page.js +24 -0
- package/dist/response/catalog/gemini-page.js.map +1 -0
- package/dist/response/catalog/render.cjs +182 -0
- package/dist/response/catalog/render.cjs.map +1 -0
- package/dist/response/catalog/render.js +171 -0
- package/dist/response/catalog/render.js.map +1 -0
- package/dist/response/errors.cjs +225 -0
- package/dist/response/errors.cjs.map +1 -0
- package/dist/response/errors.js +224 -0
- package/dist/response/errors.js.map +1 -0
- package/dist/response/finish.cjs +39 -0
- package/dist/response/finish.cjs.map +1 -0
- package/dist/response/finish.js +35 -0
- package/dist/response/finish.js.map +1 -0
- package/dist/response/policy.cjs +126 -0
- package/dist/response/policy.cjs.map +1 -0
- package/dist/response/policy.js +120 -0
- package/dist/response/policy.js.map +1 -0
- package/dist/response/profiles.cjs +69 -0
- package/dist/response/profiles.cjs.map +1 -0
- package/dist/response/profiles.js +65 -0
- package/dist/response/profiles.js.map +1 -0
- package/dist/response/usage/codecs/anthropic.cjs +52 -0
- package/dist/response/usage/codecs/anthropic.cjs.map +1 -0
- package/dist/response/usage/codecs/anthropic.js +50 -0
- package/dist/response/usage/codecs/anthropic.js.map +1 -0
- package/dist/response/usage/codecs/cohere.cjs +36 -0
- package/dist/response/usage/codecs/cohere.cjs.map +1 -0
- package/dist/response/usage/codecs/cohere.js +35 -0
- package/dist/response/usage/codecs/cohere.js.map +1 -0
- package/dist/response/usage/codecs/converse.cjs +16 -0
- package/dist/response/usage/codecs/converse.cjs.map +1 -0
- package/dist/response/usage/codecs/converse.js +15 -0
- package/dist/response/usage/codecs/converse.js.map +1 -0
- package/dist/response/usage/codecs/gemini.cjs +31 -0
- package/dist/response/usage/codecs/gemini.cjs.map +1 -0
- package/dist/response/usage/codecs/gemini.js +31 -0
- package/dist/response/usage/codecs/gemini.js.map +1 -0
- package/dist/response/usage/codecs/interactions.cjs +22 -0
- package/dist/response/usage/codecs/interactions.cjs.map +1 -0
- package/dist/response/usage/codecs/interactions.js +21 -0
- package/dist/response/usage/codecs/interactions.js.map +1 -0
- package/dist/response/usage/codecs/ollama.cjs +17 -0
- package/dist/response/usage/codecs/ollama.cjs.map +1 -0
- package/dist/response/usage/codecs/ollama.js +17 -0
- package/dist/response/usage/codecs/ollama.js.map +1 -0
- package/dist/response/usage/codecs/openai.cjs +52 -0
- package/dist/response/usage/codecs/openai.cjs.map +1 -0
- package/dist/response/usage/codecs/openai.js +50 -0
- package/dist/response/usage/codecs/openai.js.map +1 -0
- package/dist/response/usage/codecs/openrouter.cjs +68 -0
- package/dist/response/usage/codecs/openrouter.cjs.map +1 -0
- package/dist/response/usage/codecs/openrouter.js +67 -0
- package/dist/response/usage/codecs/openrouter.js.map +1 -0
- package/dist/response/usage/codecs/realtime.cjs +19 -0
- package/dist/response/usage/codecs/realtime.cjs.map +1 -0
- package/dist/response/usage/codecs/realtime.js +18 -0
- package/dist/response/usage/codecs/realtime.js.map +1 -0
- package/dist/response/usage/codecs/responses.cjs +21 -0
- package/dist/response/usage/codecs/responses.cjs.map +1 -0
- package/dist/response/usage/codecs/responses.js +20 -0
- package/dist/response/usage/codecs/responses.js.map +1 -0
- package/dist/response/usage/estimate.cjs +23 -0
- package/dist/response/usage/estimate.cjs.map +1 -0
- package/dist/response/usage/estimate.js +21 -0
- package/dist/response/usage/estimate.js.map +1 -0
- package/dist/response/usage/fields.cjs +72 -0
- package/dist/response/usage/fields.cjs.map +1 -0
- package/dist/response/usage/fields.js +70 -0
- package/dist/response/usage/fields.js.map +1 -0
- package/dist/response/usage/recording.cjs +52 -0
- package/dist/response/usage/recording.cjs.map +1 -0
- package/dist/response/usage/recording.js +52 -0
- package/dist/response/usage/recording.js.map +1 -0
- package/dist/responses.cjs +45 -58
- 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 +44 -57
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +45 -0
- package/dist/route-registry.cjs.map +1 -1
- package/dist/route-registry.d.cts.map +1 -1
- package/dist/route-registry.d.ts.map +1 -1
- package/dist/route-registry.js +41 -1
- package/dist/route-registry.js.map +1 -1
- package/dist/router.cjs +4 -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 +3 -0
- package/dist/router.js.map +1 -1
- package/dist/search.cjs +13 -11
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +12 -10
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +222 -146
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +223 -147
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +20 -18
- 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 -17
- package/dist/speech.js.map +1 -1
- package/dist/sse-writer.cjs +7 -0
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +7 -1
- package/dist/sse-writer.js.map +1 -1
- package/dist/stream-collapse.cjs +118 -11
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +10 -9
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +10 -9
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +118 -11
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +12 -10
- 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 +11 -9
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +62 -3
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +62 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-mock.cjs +3 -8
- 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 +3 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-stores.cjs +2 -4
- package/dist/vector-stores.cjs.map +1 -1
- package/dist/vector-stores.d.cts.map +1 -1
- package/dist/vector-stores.d.ts.map +1 -1
- package/dist/vector-stores.js +2 -4
- package/dist/vector-stores.js.map +1 -1
- package/dist/veo-video.cjs +16 -14
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +10 -8
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +21 -19
- 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 -18
- package/dist/video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +11 -3
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +0 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +0 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +10 -2
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-live.cjs +6 -5
- package/dist/ws-live.cjs.map +1 -1
- package/dist/ws-live.js +5 -4
- package/dist/ws-live.js.map +1 -1
- package/dist/ws-realtime.cjs +28 -54
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +25 -51
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +8 -6
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +7 -5
- package/dist/ws-responses.js.map +1 -1
- package/package.json +4 -1
- package/skills/write-fixtures/SKILL.md +25 -16
package/dist/openrouter-chat.cjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
const require_errors = require('./response/errors.cjs');
|
|
1
2
|
const require_helpers = require('./helpers.cjs');
|
|
3
|
+
const require_collect = require('./response/catalog/collect.cjs');
|
|
4
|
+
const require_render = require('./response/catalog/render.cjs');
|
|
5
|
+
const require_openrouter = require('./response/usage/codecs/openrouter.cjs');
|
|
2
6
|
|
|
3
7
|
//#region src/openrouter-chat.ts
|
|
4
8
|
/**
|
|
@@ -63,41 +67,6 @@ function toGenId(id) {
|
|
|
63
67
|
return id.startsWith(OPENAI_ID_PREFIX) ? OPENROUTER_ID_PREFIX + id.slice(9) : id;
|
|
64
68
|
}
|
|
65
69
|
/**
|
|
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
|
-
/**
|
|
101
70
|
* Resolve the shaping context from a fixture's response overrides and the
|
|
102
71
|
* winning model slug. `provider` defaults to the winning slug's author;
|
|
103
72
|
* `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and
|
|
@@ -109,33 +78,7 @@ const UNSAFE_PROTO_KEYS = new Set([
|
|
|
109
78
|
* `reasoning_tokens`).
|
|
110
79
|
*/
|
|
111
80
|
function resolveOpenRouterShaping(overrides, winningModel) {
|
|
112
|
-
const
|
|
113
|
-
const usageExtras = {};
|
|
114
|
-
if (u?.cost !== void 0) {
|
|
115
|
-
usageExtras.cost = u.cost;
|
|
116
|
-
const cd = u.cost_details;
|
|
117
|
-
usageExtras.cost_details = {
|
|
118
|
-
...cd ?? {},
|
|
119
|
-
upstream_inference_cost: cd?.upstream_inference_cost ?? 0,
|
|
120
|
-
upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,
|
|
121
|
-
upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
if (u?.prompt_tokens_details !== void 0) usageExtras.prompt_tokens_details = {
|
|
125
|
-
...u.prompt_tokens_details,
|
|
126
|
-
cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0
|
|
127
|
-
};
|
|
128
|
-
if (u?.completion_tokens_details !== void 0) usageExtras.completion_tokens_details = {
|
|
129
|
-
...u.completion_tokens_details,
|
|
130
|
-
reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0
|
|
131
|
-
};
|
|
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
|
-
}
|
|
81
|
+
const usageExtras = require_openrouter.resolve(overrides);
|
|
139
82
|
return {
|
|
140
83
|
provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),
|
|
141
84
|
...overrides?.nativeFinishReason !== void 0 && { nativeFinishReason: overrides.nativeFinishReason },
|
|
@@ -144,10 +87,7 @@ function resolveOpenRouterShaping(overrides, winningModel) {
|
|
|
144
87
|
}
|
|
145
88
|
/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */
|
|
146
89
|
function augmentUsage(usage, shaping) {
|
|
147
|
-
return
|
|
148
|
-
...usage,
|
|
149
|
-
...shaping.usageExtras
|
|
150
|
-
};
|
|
90
|
+
return require_openrouter.render(usage, shaping.usageExtras);
|
|
151
91
|
}
|
|
152
92
|
/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */
|
|
153
93
|
function nativeFinish(finishReason, shaping) {
|
|
@@ -208,95 +148,11 @@ function shapeOpenRouterChunks(chunks, shaping, idOverridden) {
|
|
|
208
148
|
* routing failures.
|
|
209
149
|
*/
|
|
210
150
|
function serializeOpenRouterError(status, message, metadata) {
|
|
211
|
-
return JSON.stringify(
|
|
151
|
+
return JSON.stringify(require_errors.buildOpenAIError("openrouter-error", {
|
|
212
152
|
code: status,
|
|
213
153
|
message,
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */
|
|
218
|
-
const DEFAULT_OPENROUTER_MODELS = [
|
|
219
|
-
"openai/gpt-4o",
|
|
220
|
-
"anthropic/claude-3.5-sonnet",
|
|
221
|
-
"google/gemini-2.0-flash-001"
|
|
222
|
-
];
|
|
223
|
-
/**
|
|
224
|
-
* Build one OpenRouter model-catalog object (deterministic static filler).
|
|
225
|
-
* Field set mirrors real OpenRouter bytes (or-capture/models-first.json):
|
|
226
|
-
* pricing values are STRINGS; the object also carries hugging_face_id,
|
|
227
|
-
* knowledge_cutoff, supported_voices, links, default_parameters,
|
|
228
|
-
* expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).
|
|
229
|
-
*/
|
|
230
|
-
function openRouterModelObject(id) {
|
|
231
|
-
return {
|
|
232
|
-
id,
|
|
233
|
-
canonical_slug: id,
|
|
234
|
-
hugging_face_id: null,
|
|
235
|
-
name: id,
|
|
236
|
-
created: 0,
|
|
237
|
-
description: `Mock OpenRouter model ${id} served by aimock.`,
|
|
238
|
-
context_length: 128e3,
|
|
239
|
-
architecture: {
|
|
240
|
-
modality: "text->text",
|
|
241
|
-
input_modalities: ["text"],
|
|
242
|
-
output_modalities: ["text"],
|
|
243
|
-
tokenizer: "Other",
|
|
244
|
-
instruct_type: null
|
|
245
|
-
},
|
|
246
|
-
pricing: {
|
|
247
|
-
prompt: "0",
|
|
248
|
-
completion: "0",
|
|
249
|
-
request: "0",
|
|
250
|
-
image: "0",
|
|
251
|
-
web_search: "0",
|
|
252
|
-
internal_reasoning: "0",
|
|
253
|
-
input_cache_read: "0",
|
|
254
|
-
input_cache_write: "0"
|
|
255
|
-
},
|
|
256
|
-
top_provider: {
|
|
257
|
-
context_length: 128e3,
|
|
258
|
-
max_completion_tokens: 16384,
|
|
259
|
-
is_moderated: false
|
|
260
|
-
},
|
|
261
|
-
per_request_limits: null,
|
|
262
|
-
supported_parameters: [
|
|
263
|
-
"tools",
|
|
264
|
-
"tool_choice",
|
|
265
|
-
"max_tokens",
|
|
266
|
-
"temperature",
|
|
267
|
-
"top_p",
|
|
268
|
-
"stop",
|
|
269
|
-
"reasoning"
|
|
270
|
-
],
|
|
271
|
-
default_parameters: null,
|
|
272
|
-
supported_voices: null,
|
|
273
|
-
knowledge_cutoff: null,
|
|
274
|
-
expiration_date: null,
|
|
275
|
-
links: { details: `/api/v1/models/${id}/endpoints` },
|
|
276
|
-
reasoning: {
|
|
277
|
-
mandatory: false,
|
|
278
|
-
default_enabled: false
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Collect the model ids advertised by `GET /api/v1/models`: every loaded chat
|
|
284
|
-
* (or endpoint-less) fixture with a string `match.model`, in first-seen order;
|
|
285
|
-
* the default set when none contribute one. Mirrors the Ollama `/api/tags` and
|
|
286
|
-
* OpenRouter video `/models` synthesis idioms.
|
|
287
|
-
*/
|
|
288
|
-
function collectModelIds(fixtures) {
|
|
289
|
-
const ids = [];
|
|
290
|
-
const seen = /* @__PURE__ */ new Set();
|
|
291
|
-
for (const f of fixtures) {
|
|
292
|
-
if (f.match.endpoint !== void 0 && f.match.endpoint !== "chat") continue;
|
|
293
|
-
const m = f.match.model;
|
|
294
|
-
if (typeof m === "string" && m.length > 0 && !seen.has(m)) {
|
|
295
|
-
seen.add(m);
|
|
296
|
-
ids.push(m);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;
|
|
154
|
+
metadata
|
|
155
|
+
}));
|
|
300
156
|
}
|
|
301
157
|
function journalGet(req, journal, path, status) {
|
|
302
158
|
journal.add({
|
|
@@ -314,10 +170,10 @@ function journalGet(req, journal, path, status) {
|
|
|
314
170
|
function handleOpenRouterModels(req, res, fixtures, journal, _defaults, setCorsHeaders) {
|
|
315
171
|
setCorsHeaders(res);
|
|
316
172
|
const path = req.url ?? "/api/v1/models";
|
|
317
|
-
const
|
|
173
|
+
const body = require_render.renderOpenRouterChatModels(require_collect.collect(fixtures, "chat").ids);
|
|
318
174
|
journalGet(req, journal, path, 200);
|
|
319
175
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
320
|
-
res.end(JSON.stringify(
|
|
176
|
+
res.end(JSON.stringify(body));
|
|
321
177
|
}
|
|
322
178
|
/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */
|
|
323
179
|
function handleOpenRouterKey(req, res, journal, setCorsHeaders) {
|
|
@@ -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 * `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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"}
|
|
1
|
+
{"version":3,"file":"openrouter-chat.cjs","names":["resolveOpenRouterUsage","renderOpenRouterUsage","buildOpenAIError","flattenHeaders","renderOpenRouterChatModels","collect"],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import {\n resolve as resolveOpenRouterUsage,\n render as renderOpenRouterUsage,\n} from \"./response/usage/codecs/openrouter.js\";\nimport 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\";\nimport { collect } from \"./response/catalog/collect.js\";\nimport { renderOpenRouterChatModels } from \"./response/catalog/render.js\";\nimport { buildOpenAIError } from \"./response/errors.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 usageExtras = resolveOpenRouterUsage(overrides);\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 renderOpenRouterUsage(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(buildOpenAIError(\"openrouter-error\", { code: status, message, metadata }));\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\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 body = renderOpenRouterChatModels(collect(fixtures, \"chat\").ids);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\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":";;;;;;;;;;;;;;;;;;;;;;;AAqCA,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,cAAcA,2BAAuB,UAAU;AACrD,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,QAAOC,0BAAsB,OAAO,QAAQ,YAAY;;;AAI1D,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,UAAUC,gCAAiB,oBAAoB;EAAE,MAAM;EAAQ;EAAS;EAAU,CAAC,CAAC;;AAKlG,SAAS,WACP,KACA,SACA,MACA,QACM;AACN,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB;EACA,SAASC,+BAAe,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,OAAOC,0CAA2BC,wBAAQ,UAAU,OAAO,CAAC,IAAI;AACtE,YAAW,KAAK,SAAS,MAAM,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,KAAK,CAAC;;;AAI/B,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"}
|
package/dist/openrouter-chat.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { buildOpenAIError } from "./response/errors.js";
|
|
1
2
|
import { flattenHeaders } from "./helpers.js";
|
|
3
|
+
import { collect } from "./response/catalog/collect.js";
|
|
4
|
+
import { renderOpenRouterChatModels } from "./response/catalog/render.js";
|
|
5
|
+
import { render, resolve } from "./response/usage/codecs/openrouter.js";
|
|
2
6
|
|
|
3
7
|
//#region src/openrouter-chat.ts
|
|
4
8
|
/**
|
|
@@ -63,41 +67,6 @@ function toGenId(id) {
|
|
|
63
67
|
return id.startsWith(OPENAI_ID_PREFIX) ? OPENROUTER_ID_PREFIX + id.slice(9) : id;
|
|
64
68
|
}
|
|
65
69
|
/**
|
|
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
|
-
/**
|
|
101
70
|
* Resolve the shaping context from a fixture's response overrides and the
|
|
102
71
|
* winning model slug. `provider` defaults to the winning slug's author;
|
|
103
72
|
* `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and
|
|
@@ -109,33 +78,7 @@ const UNSAFE_PROTO_KEYS = new Set([
|
|
|
109
78
|
* `reasoning_tokens`).
|
|
110
79
|
*/
|
|
111
80
|
function resolveOpenRouterShaping(overrides, winningModel) {
|
|
112
|
-
const
|
|
113
|
-
const usageExtras = {};
|
|
114
|
-
if (u?.cost !== void 0) {
|
|
115
|
-
usageExtras.cost = u.cost;
|
|
116
|
-
const cd = u.cost_details;
|
|
117
|
-
usageExtras.cost_details = {
|
|
118
|
-
...cd ?? {},
|
|
119
|
-
upstream_inference_cost: cd?.upstream_inference_cost ?? 0,
|
|
120
|
-
upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,
|
|
121
|
-
upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
if (u?.prompt_tokens_details !== void 0) usageExtras.prompt_tokens_details = {
|
|
125
|
-
...u.prompt_tokens_details,
|
|
126
|
-
cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0
|
|
127
|
-
};
|
|
128
|
-
if (u?.completion_tokens_details !== void 0) usageExtras.completion_tokens_details = {
|
|
129
|
-
...u.completion_tokens_details,
|
|
130
|
-
reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0
|
|
131
|
-
};
|
|
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
|
-
}
|
|
81
|
+
const usageExtras = resolve(overrides);
|
|
139
82
|
return {
|
|
140
83
|
provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),
|
|
141
84
|
...overrides?.nativeFinishReason !== void 0 && { nativeFinishReason: overrides.nativeFinishReason },
|
|
@@ -144,10 +87,7 @@ function resolveOpenRouterShaping(overrides, winningModel) {
|
|
|
144
87
|
}
|
|
145
88
|
/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */
|
|
146
89
|
function augmentUsage(usage, shaping) {
|
|
147
|
-
return
|
|
148
|
-
...usage,
|
|
149
|
-
...shaping.usageExtras
|
|
150
|
-
};
|
|
90
|
+
return render(usage, shaping.usageExtras);
|
|
151
91
|
}
|
|
152
92
|
/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */
|
|
153
93
|
function nativeFinish(finishReason, shaping) {
|
|
@@ -208,95 +148,11 @@ function shapeOpenRouterChunks(chunks, shaping, idOverridden) {
|
|
|
208
148
|
* routing failures.
|
|
209
149
|
*/
|
|
210
150
|
function serializeOpenRouterError(status, message, metadata) {
|
|
211
|
-
return JSON.stringify(
|
|
151
|
+
return JSON.stringify(buildOpenAIError("openrouter-error", {
|
|
212
152
|
code: status,
|
|
213
153
|
message,
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */
|
|
218
|
-
const DEFAULT_OPENROUTER_MODELS = [
|
|
219
|
-
"openai/gpt-4o",
|
|
220
|
-
"anthropic/claude-3.5-sonnet",
|
|
221
|
-
"google/gemini-2.0-flash-001"
|
|
222
|
-
];
|
|
223
|
-
/**
|
|
224
|
-
* Build one OpenRouter model-catalog object (deterministic static filler).
|
|
225
|
-
* Field set mirrors real OpenRouter bytes (or-capture/models-first.json):
|
|
226
|
-
* pricing values are STRINGS; the object also carries hugging_face_id,
|
|
227
|
-
* knowledge_cutoff, supported_voices, links, default_parameters,
|
|
228
|
-
* expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).
|
|
229
|
-
*/
|
|
230
|
-
function openRouterModelObject(id) {
|
|
231
|
-
return {
|
|
232
|
-
id,
|
|
233
|
-
canonical_slug: id,
|
|
234
|
-
hugging_face_id: null,
|
|
235
|
-
name: id,
|
|
236
|
-
created: 0,
|
|
237
|
-
description: `Mock OpenRouter model ${id} served by aimock.`,
|
|
238
|
-
context_length: 128e3,
|
|
239
|
-
architecture: {
|
|
240
|
-
modality: "text->text",
|
|
241
|
-
input_modalities: ["text"],
|
|
242
|
-
output_modalities: ["text"],
|
|
243
|
-
tokenizer: "Other",
|
|
244
|
-
instruct_type: null
|
|
245
|
-
},
|
|
246
|
-
pricing: {
|
|
247
|
-
prompt: "0",
|
|
248
|
-
completion: "0",
|
|
249
|
-
request: "0",
|
|
250
|
-
image: "0",
|
|
251
|
-
web_search: "0",
|
|
252
|
-
internal_reasoning: "0",
|
|
253
|
-
input_cache_read: "0",
|
|
254
|
-
input_cache_write: "0"
|
|
255
|
-
},
|
|
256
|
-
top_provider: {
|
|
257
|
-
context_length: 128e3,
|
|
258
|
-
max_completion_tokens: 16384,
|
|
259
|
-
is_moderated: false
|
|
260
|
-
},
|
|
261
|
-
per_request_limits: null,
|
|
262
|
-
supported_parameters: [
|
|
263
|
-
"tools",
|
|
264
|
-
"tool_choice",
|
|
265
|
-
"max_tokens",
|
|
266
|
-
"temperature",
|
|
267
|
-
"top_p",
|
|
268
|
-
"stop",
|
|
269
|
-
"reasoning"
|
|
270
|
-
],
|
|
271
|
-
default_parameters: null,
|
|
272
|
-
supported_voices: null,
|
|
273
|
-
knowledge_cutoff: null,
|
|
274
|
-
expiration_date: null,
|
|
275
|
-
links: { details: `/api/v1/models/${id}/endpoints` },
|
|
276
|
-
reasoning: {
|
|
277
|
-
mandatory: false,
|
|
278
|
-
default_enabled: false
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Collect the model ids advertised by `GET /api/v1/models`: every loaded chat
|
|
284
|
-
* (or endpoint-less) fixture with a string `match.model`, in first-seen order;
|
|
285
|
-
* the default set when none contribute one. Mirrors the Ollama `/api/tags` and
|
|
286
|
-
* OpenRouter video `/models` synthesis idioms.
|
|
287
|
-
*/
|
|
288
|
-
function collectModelIds(fixtures) {
|
|
289
|
-
const ids = [];
|
|
290
|
-
const seen = /* @__PURE__ */ new Set();
|
|
291
|
-
for (const f of fixtures) {
|
|
292
|
-
if (f.match.endpoint !== void 0 && f.match.endpoint !== "chat") continue;
|
|
293
|
-
const m = f.match.model;
|
|
294
|
-
if (typeof m === "string" && m.length > 0 && !seen.has(m)) {
|
|
295
|
-
seen.add(m);
|
|
296
|
-
ids.push(m);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;
|
|
154
|
+
metadata
|
|
155
|
+
}));
|
|
300
156
|
}
|
|
301
157
|
function journalGet(req, journal, path, status) {
|
|
302
158
|
journal.add({
|
|
@@ -314,10 +170,10 @@ function journalGet(req, journal, path, status) {
|
|
|
314
170
|
function handleOpenRouterModels(req, res, fixtures, journal, _defaults, setCorsHeaders) {
|
|
315
171
|
setCorsHeaders(res);
|
|
316
172
|
const path = req.url ?? "/api/v1/models";
|
|
317
|
-
const
|
|
173
|
+
const body = renderOpenRouterChatModels(collect(fixtures, "chat").ids);
|
|
318
174
|
journalGet(req, journal, path, 200);
|
|
319
175
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
320
|
-
res.end(JSON.stringify(
|
|
176
|
+
res.end(JSON.stringify(body));
|
|
321
177
|
}
|
|
322
178
|
/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */
|
|
323
179
|
function handleOpenRouterKey(req, res, journal, setCorsHeaders) {
|
|
@@ -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 * `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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"}
|
|
1
|
+
{"version":3,"file":"openrouter-chat.js","names":["resolveOpenRouterUsage","renderOpenRouterUsage"],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import {\n resolve as resolveOpenRouterUsage,\n render as renderOpenRouterUsage,\n} from \"./response/usage/codecs/openrouter.js\";\nimport 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\";\nimport { collect } from \"./response/catalog/collect.js\";\nimport { renderOpenRouterChatModels } from \"./response/catalog/render.js\";\nimport { buildOpenAIError } from \"./response/errors.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 usageExtras = resolveOpenRouterUsage(overrides);\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 renderOpenRouterUsage(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(buildOpenAIError(\"openrouter-error\", { code: status, message, metadata }));\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\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 body = renderOpenRouterChatModels(collect(fixtures, \"chat\").ids);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\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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"}
|