@copilotkit/aimock 1.38.0 → 1.40.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.
Files changed (81) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/README.md +24 -2
  5. package/dist/agui-handler.cjs +20 -18
  6. package/dist/agui-handler.cjs.map +1 -1
  7. package/dist/agui-handler.d.cts +8 -1
  8. package/dist/agui-handler.d.cts.map +1 -1
  9. package/dist/agui-handler.d.ts +8 -1
  10. package/dist/agui-handler.d.ts.map +1 -1
  11. package/dist/agui-handler.js +20 -18
  12. package/dist/agui-handler.js.map +1 -1
  13. package/dist/agui-stub.d.cts +2 -2
  14. package/dist/agui-stub.d.ts +2 -2
  15. package/dist/agui-types.d.cts +84 -3
  16. package/dist/agui-types.d.cts.map +1 -1
  17. package/dist/agui-types.d.ts +84 -3
  18. package/dist/agui-types.d.ts.map +1 -1
  19. package/dist/aimock-cli.cjs +1 -1
  20. package/dist/aimock-cli.cjs.map +1 -1
  21. package/dist/aimock-cli.js +1 -1
  22. package/dist/aimock-cli.js.map +1 -1
  23. package/dist/cli.cjs +10 -10
  24. package/dist/cli.cjs.map +1 -1
  25. package/dist/cli.js +10 -10
  26. package/dist/cli.js.map +1 -1
  27. package/dist/config-loader.d.cts.map +1 -1
  28. package/dist/helpers.cjs +8 -4
  29. package/dist/helpers.cjs.map +1 -1
  30. package/dist/helpers.d.cts.map +1 -1
  31. package/dist/helpers.d.ts.map +1 -1
  32. package/dist/helpers.js +8 -4
  33. package/dist/helpers.js.map +1 -1
  34. package/dist/index.d.cts +2 -2
  35. package/dist/index.d.ts +2 -2
  36. package/dist/llmock.cjs +23 -9
  37. package/dist/llmock.cjs.map +1 -1
  38. package/dist/llmock.d.cts +22 -0
  39. package/dist/llmock.d.cts.map +1 -1
  40. package/dist/llmock.d.ts +22 -0
  41. package/dist/llmock.d.ts.map +1 -1
  42. package/dist/llmock.js +25 -11
  43. package/dist/llmock.js.map +1 -1
  44. package/dist/openrouter-chat.cjs +41 -0
  45. package/dist/openrouter-chat.cjs.map +1 -1
  46. package/dist/openrouter-chat.js +41 -0
  47. package/dist/openrouter-chat.js.map +1 -1
  48. package/dist/openrouter-video.cjs.map +1 -1
  49. package/dist/openrouter-video.js.map +1 -1
  50. package/dist/recorder.cjs +77 -8
  51. package/dist/recorder.cjs.map +1 -1
  52. package/dist/recorder.d.cts.map +1 -1
  53. package/dist/recorder.d.ts.map +1 -1
  54. package/dist/recorder.js +77 -8
  55. package/dist/recorder.js.map +1 -1
  56. package/dist/server.cjs +80 -17
  57. package/dist/server.cjs.map +1 -1
  58. package/dist/server.d.cts +6 -1
  59. package/dist/server.d.cts.map +1 -1
  60. package/dist/server.d.ts +6 -1
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +80 -18
  63. package/dist/server.js.map +1 -1
  64. package/dist/stream-collapse.cjs +97 -6
  65. package/dist/stream-collapse.cjs.map +1 -1
  66. package/dist/stream-collapse.d.cts +16 -0
  67. package/dist/stream-collapse.d.cts.map +1 -1
  68. package/dist/stream-collapse.d.ts +16 -0
  69. package/dist/stream-collapse.d.ts.map +1 -1
  70. package/dist/stream-collapse.js +97 -6
  71. package/dist/stream-collapse.js.map +1 -1
  72. package/dist/types.d.cts +22 -3
  73. package/dist/types.d.cts.map +1 -1
  74. package/dist/types.d.ts +22 -3
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/vector-types.d.ts.map +1 -1
  77. package/dist/ws-gemini-live.cjs +41 -6
  78. package/dist/ws-gemini-live.cjs.map +1 -1
  79. package/dist/ws-gemini-live.js +41 -6
  80. package/dist/ws-gemini-live.js.map +1 -1
  81. package/package.json +5 -3
@@ -1 +1 @@
1
- {"version":3,"file":"llmock.cjs","names":["loadFixtureFile","loadFixturesFromDir","entryToFixture","validateFixtures","normalizeResponse","imageResponseToFalJson","videoResponseToFalJson","createServerWithResolvedAuth","createServer"],"sources":["../src/llmock.ts"],"sourcesContent":["import type {\n AudioResponse,\n ChaosConfig,\n EmbeddingFixtureOpts,\n FalQueueOpts,\n Fixture,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureMatch,\n FixtureOpts,\n FixtureResponse,\n ImageResponse,\n MockServerOptions,\n Mountable,\n RecordConfig,\n ResponseFactory,\n TranscriptionResponse,\n VideoResponse,\n} from \"./types.js\";\nimport { createServer, createServerWithResolvedAuth, type ServerInstance } from \"./server.js\";\nimport type { ResolvedInboundAuth } from \"./api-key-auth.js\";\nimport {\n loadFixtureFile,\n loadFixturesFromDir,\n entryToFixture,\n normalizeResponse,\n validateFixtures,\n} from \"./fixture-loader.js\";\nimport { Journal } from \"./journal.js\";\nimport type { SearchFixture, SearchResult } from \"./search.js\";\nimport type { RerankFixture, RerankResult } from \"./rerank.js\";\nimport type { ModerationFixture, ModerationResult } from \"./moderation.js\";\nimport { falJobs } from \"./fal-audio.js\";\nimport { falQueueStates, imageResponseToFalJson, videoResponseToFalJson } from \"./fal.js\";\n\nexport class LLMock {\n private fixtures: Fixture[] = [];\n private searchFixtures: SearchFixture[] = [];\n private rerankFixtures: RerankFixture[] = [];\n private moderationFixtures: ModerationFixture[] = [];\n private mounts: Array<{ path: string; handler: Mountable }> = [];\n private serverInstance: ServerInstance | null = null;\n private options: MockServerOptions;\n private readonly resolvedInboundAuth?: ResolvedInboundAuth;\n\n constructor(options?: MockServerOptions, resolvedInboundAuth?: ResolvedInboundAuth) {\n this.options = options ?? {};\n this.resolvedInboundAuth = resolvedInboundAuth;\n }\n\n // ---- Fixture management ----\n\n addFixture(fixture: Fixture): this {\n this.fixtures.push(fixture);\n return this;\n }\n\n addFixtures(fixtures: Fixture[]): this {\n this.fixtures.push(...fixtures);\n return this;\n }\n\n prependFixture(fixture: Fixture): this {\n this.fixtures.unshift(fixture);\n return this;\n }\n\n getFixtures(): readonly Fixture[] {\n return this.fixtures;\n }\n\n loadFixtureFile(filePath: string): this {\n this.fixtures.push(...loadFixtureFile(filePath));\n return this;\n }\n\n loadFixtureDir(dirPath: string): this {\n this.fixtures.push(...loadFixturesFromDir(dirPath));\n return this;\n }\n\n /**\n * Add fixtures from a JSON string or pre-parsed array of fixture entries.\n * Validates all fixtures and throws if any have severity \"error\".\n */\n addFixturesFromJSON(input: string | FixtureFileEntry[]): this {\n let entries: FixtureFileEntry[];\n if (typeof input === \"string\") {\n try {\n entries = JSON.parse(input);\n } catch (err) {\n throw new Error(\n `addFixturesFromJSON: invalid JSON — ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n } else {\n entries = input;\n }\n const converted = entries.map((e) => entryToFixture(e));\n const issues = validateFixtures(converted);\n const errors = issues.filter((i) => i.severity === \"error\");\n if (errors.length > 0) {\n throw new Error(`Fixture validation failed: ${JSON.stringify(errors)}`);\n }\n this.fixtures.push(...converted);\n return this;\n }\n\n // Uses length = 0 to preserve array reference identity — the running\n // server reads this same array on every request.\n clearFixtures(): this {\n this.fixtures.length = 0;\n return this;\n }\n\n // ---- Convenience ----\n\n on(\n match: FixtureMatch,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.addFixture({\n match,\n response: typeof response === \"function\" ? response : normalizeResponse(response),\n ...opts,\n });\n }\n\n onMessage(\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ userMessage: pattern }, response, opts);\n }\n\n onEmbedding(\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: EmbeddingFixtureOpts,\n ): this {\n return this.on({ inputText: pattern }, response, opts);\n }\n\n onJsonOutput(pattern: string | RegExp, jsonContent: object | string, opts?: FixtureOpts): this {\n const content = typeof jsonContent === \"string\" ? jsonContent : JSON.stringify(jsonContent);\n return this.on({ userMessage: pattern, responseFormat: \"json_object\" }, { content }, opts);\n }\n\n onToolCall(\n name: string,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ toolName: name }, response, opts);\n }\n\n onToolResult(\n id: string,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ toolCallId: id }, response, opts);\n }\n\n onTurn(\n turn: number,\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ userMessage: pattern, turnIndex: turn }, response, opts);\n }\n\n onImage(prompt: string | RegExp, response: ImageResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"image\" },\n response,\n });\n }\n\n onSpeech(input: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: input, endpoint: \"speech\" },\n response,\n });\n }\n\n onTranscription(response: TranscriptionResponse): this {\n return this.addFixture({\n match: { endpoint: \"transcription\" },\n response,\n });\n }\n\n onTranslation(response: TranscriptionResponse): this {\n return this.addFixture({\n match: { endpoint: \"translation\" },\n response,\n });\n }\n\n onVideo(prompt: string | RegExp, response: VideoResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"video\" },\n response,\n });\n }\n\n onAudio(input: string | RegExp, response: AudioResponse): this {\n return this.addFixture({ match: { userMessage: input }, response });\n }\n\n onSoundEffect(text: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: text, endpoint: \"audio-gen\" },\n response,\n });\n }\n\n onMusic(prompt: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"audio-gen\" },\n response,\n });\n }\n\n onElevenLabsTTS(text: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: text, endpoint: \"elevenlabs-tts\" },\n response,\n });\n }\n\n onFalAudio(prompt: string | RegExp, response: AudioResponse, model?: string): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"fal-audio\", ...(model ? { model } : {}) },\n response,\n });\n }\n\n // fal.queue.* is the dominant client API; onFalRun is a sync alias.\n //\n // `opts.billableUnits` rides through to the completed `queue-result`\n // response's `x-fal-billable-units` header (emitted alongside\n // `x-fal-request-id`), letting consumers like `@tanstack/ai-fal` surface a\n // billed-units value on replay. Omit it to preserve the header-less default.\n onFalQueue(modelOrPrompt: string | RegExp, response: unknown, opts?: FalQueueOpts): this {\n const { billableUnits, ...fixtureOpts } = opts ?? {};\n return this.addFixture({\n match: { model: modelOrPrompt, endpoint: \"fal\" },\n response: { json: response, ...(billableUnits != null ? { billableUnits } : {}) },\n ...fixtureOpts,\n });\n }\n\n onFalRun(modelOrPrompt: string | RegExp, response: unknown, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, response, opts);\n }\n\n /**\n * Register a fal.ai image fixture. Wraps an `ImageResponse` (the shape used\n * by `onImage` and OpenAI/Azure image fixtures) into fal's image envelope\n * before storing it as a `RawJSONResponse`. Defaults `width`/`height` to\n * 1024 when the fixture's `ImageItem` doesn't carry them.\n */\n onFalImage(modelOrPrompt: string | RegExp, response: ImageResponse, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, imageResponseToFalJson(response), opts);\n }\n\n /**\n * Register a fal.ai video fixture. Wraps a `VideoResponse` into fal's video\n * envelope (`{ video: { url, content_type, file_name, file_size }, seed }`)\n * before storing it as a `RawJSONResponse`.\n */\n onFalVideo(modelOrPrompt: string | RegExp, response: VideoResponse, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, videoResponseToFalJson(response), opts);\n }\n\n // ---- Service mock convenience methods ----\n\n onSearch(pattern: string | RegExp, results: SearchResult[]): this {\n this.searchFixtures.push({ match: pattern, results });\n return this;\n }\n\n onRerank(pattern: string | RegExp, results: RerankResult[]): this {\n this.rerankFixtures.push({ match: pattern, results });\n return this;\n }\n\n onModerate(pattern: string | RegExp, result: ModerationResult): this {\n this.moderationFixtures.push({ match: pattern, result });\n return this;\n }\n\n /**\n * Queue a one-shot error that will be returned for the next matching\n * request, then automatically removed. Implemented as an internal fixture\n * with a `predicate` that always matches (so it fires first) and spliced\n * at the front of the fixture list.\n */\n nextRequestError(\n status: number,\n errorBody?: { message?: string; type?: string; code?: string },\n ): this {\n const errorResponse: FixtureResponse = {\n error: {\n message: errorBody?.message ?? \"Injected error\",\n type: errorBody?.type ?? \"server_error\",\n code: errorBody?.code,\n },\n status,\n };\n // An injected error is only a valid response for endpoints that can carry\n // an error envelope. Mirror the router's endpoint-compat table\n // (matchFixtureDiagnostic in router.ts): error responses are compatible\n // with chat / embedding / realtime* / fal and with requests that carry no\n // endpoint type, but NOT with multimedia endpoints (image, speech, video,\n // transcription, …). Gating consumption on this prevents an incompatible\n // request from matching the predicate and splicing — and thereby\n // destroying — a one-shot error intended for a different endpoint before\n // the router's own compat check would have skipped it.\n const errorEndpointCompatible = (req: import(\"./types.js\").ChatCompletionRequest) => {\n const reqEndpoint = req._endpointType as string | undefined;\n if (\n reqEndpoint === undefined ||\n reqEndpoint === \"chat\" ||\n reqEndpoint === \"embedding\" ||\n reqEndpoint.startsWith(\"realtime\") ||\n reqEndpoint === \"fal\"\n ) {\n return true;\n }\n return false;\n };\n const fixture: Fixture = {\n match: { predicate: errorEndpointCompatible },\n response: errorResponse,\n };\n // Insert at front so it matches before everything else\n this.fixtures.unshift(fixture);\n // Remove after first match — the journal records it so tests can assert.\n // Only consume (and splice) when the request endpoint is compatible; an\n // incompatible request returns false here, falls through to other\n // fixtures, and leaves this error pending for its intended endpoint.\n const original = fixture.match.predicate!;\n fixture.match.predicate = (req) => {\n const result = original(req);\n if (result) {\n // Remove synchronously on first match to prevent race conditions\n const idx = this.fixtures.indexOf(fixture);\n if (idx !== -1) this.fixtures.splice(idx, 1);\n }\n return result;\n };\n return this;\n }\n\n // ---- Mounts ----\n\n mount(path: string, handler: Mountable): this {\n this.mounts.push({ path, handler });\n\n // If server is already running, wire up journal, registry, and baseUrl immediately\n // so late mounts behave identically to pre-start mounts.\n if (this.serverInstance) {\n if (handler.setJournal) handler.setJournal(this.serverInstance.journal);\n if (handler.setBaseUrl) handler.setBaseUrl(this.serverInstance.url + path);\n const registry = this.serverInstance.defaults.registry;\n if (registry && handler.setRegistry) handler.setRegistry(registry);\n }\n\n return this;\n }\n\n // ---- Journal proxies ----\n\n getRequests(): import(\"./types.js\").JournalEntry[] {\n return this.journal.getAll();\n }\n\n getLastRequest(): import(\"./types.js\").JournalEntry | null {\n return this.journal.getLast();\n }\n\n clearRequests(): void {\n this.journal.clear();\n }\n\n resetMatchCounts(testId?: string): this {\n if (this.serverInstance) {\n this.serverInstance.journal.clearMatchCounts(testId);\n }\n return this;\n }\n\n // ---- Chaos ----\n\n setChaos(config: ChaosConfig): this {\n this.options.chaos = config;\n return this;\n }\n\n clearChaos(): this {\n delete this.options.chaos;\n return this;\n }\n\n // ---- Recording ----\n\n enableRecording(config: RecordConfig): this {\n this.options.record = config;\n return this;\n }\n\n disableRecording(): this {\n delete this.options.record;\n return this;\n }\n\n // ---- Reset ----\n\n reset(): this {\n this.clearFixtures();\n this.searchFixtures.length = 0;\n this.rerankFixtures.length = 0;\n this.moderationFixtures.length = 0;\n falJobs.clear();\n falQueueStates.clear();\n if (this.serverInstance) {\n this.serverInstance.journal.clear();\n this.serverInstance.videoStates.clear();\n this.serverInstance.openRouterVideoJobs.clear();\n }\n return this;\n }\n\n // ---- Server lifecycle ----\n\n async start(): Promise<string> {\n if (this.serverInstance) {\n throw new Error(\"Server already started\");\n }\n this.serverInstance = await (this.resolvedInboundAuth\n ? createServerWithResolvedAuth(\n this.fixtures,\n this.options,\n this.resolvedInboundAuth,\n this.mounts,\n {\n search: this.searchFixtures,\n rerank: this.rerankFixtures,\n moderation: this.moderationFixtures,\n },\n )\n : createServer(this.fixtures, this.options, this.mounts, {\n search: this.searchFixtures,\n rerank: this.rerankFixtures,\n moderation: this.moderationFixtures,\n }));\n return this.serverInstance.url;\n }\n\n async stop(): Promise<void> {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n const { server } = this.serverInstance;\n await new Promise<void>((resolve, reject) => {\n server.close((err: Error | undefined) => (err ? reject(err) : resolve()));\n });\n this.serverInstance = null;\n }\n\n // ---- Accessors ----\n\n get journal(): Journal {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n return this.serverInstance.journal;\n }\n\n get url(): string {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n return this.serverInstance.url;\n }\n\n get baseUrl(): string {\n return this.url;\n }\n\n get port(): number {\n const parsed = new URL(this.url); // this.url throws if not started\n if (!parsed.port) {\n throw new Error(`Server URL has no explicit port: ${this.url}`);\n }\n return parseInt(parsed.port, 10);\n }\n\n // ---- Static factory ----\n\n static async create(options?: MockServerOptions): Promise<LLMock> {\n const instance = new LLMock(options);\n await instance.start();\n return instance;\n }\n}\n\n/** @internal Configuration startup preserves a policy resolved from the selected source. */\nexport function createLLMockWithResolvedAuth(\n options: MockServerOptions,\n resolvedAuth: ResolvedInboundAuth,\n): LLMock {\n return new LLMock(options, resolvedAuth);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"llmock.cjs","names":["loadFixtureFile","loadFixturesFromDir","entryToFixture","validateFixtures","normalizeResponse","imageResponseToFalJson","videoResponseToFalJson","createServerWithResolvedAuth","createServer"],"sources":["../src/llmock.ts"],"sourcesContent":["import type {\n AudioResponse,\n ChaosConfig,\n EmbeddingFixtureOpts,\n FalQueueOpts,\n Fixture,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureMatch,\n FixtureOpts,\n FixtureResponse,\n ImageResponse,\n MockServerOptions,\n Mountable,\n RecordConfig,\n ResponseFactory,\n TranscriptionResponse,\n VideoResponse,\n} from \"./types.js\";\nimport {\n createServer,\n createServerWithResolvedAuth,\n performFullReset,\n type ServerInstance,\n} from \"./server.js\";\nimport type { ResolvedInboundAuth } from \"./api-key-auth.js\";\nimport {\n loadFixtureFile,\n loadFixturesFromDir,\n entryToFixture,\n normalizeResponse,\n validateFixtures,\n} from \"./fixture-loader.js\";\nimport { Journal } from \"./journal.js\";\nimport type { SearchFixture, SearchResult } from \"./search.js\";\nimport type { RerankFixture, RerankResult } from \"./rerank.js\";\nimport type { ModerationFixture, ModerationResult } from \"./moderation.js\";\nimport { imageResponseToFalJson, videoResponseToFalJson } from \"./fal.js\";\n\nexport class LLMock {\n private fixtures: Fixture[] = [];\n private searchFixtures: SearchFixture[] = [];\n private rerankFixtures: RerankFixture[] = [];\n private moderationFixtures: ModerationFixture[] = [];\n private mounts: Array<{ path: string; handler: Mountable }> = [];\n private serverInstance: ServerInstance | null = null;\n private options: MockServerOptions;\n private readonly resolvedInboundAuth?: ResolvedInboundAuth;\n\n constructor(options?: MockServerOptions, resolvedInboundAuth?: ResolvedInboundAuth) {\n this.options = options ?? {};\n this.resolvedInboundAuth = resolvedInboundAuth;\n }\n\n // ---- Fixture management ----\n\n addFixture(fixture: Fixture): this {\n this.fixtures.push(fixture);\n return this;\n }\n\n addFixtures(fixtures: Fixture[]): this {\n this.fixtures.push(...fixtures);\n return this;\n }\n\n prependFixture(fixture: Fixture): this {\n this.fixtures.unshift(fixture);\n return this;\n }\n\n getFixtures(): readonly Fixture[] {\n return this.fixtures;\n }\n\n loadFixtureFile(filePath: string): this {\n this.fixtures.push(...loadFixtureFile(filePath));\n return this;\n }\n\n loadFixtureDir(dirPath: string): this {\n this.fixtures.push(...loadFixturesFromDir(dirPath));\n return this;\n }\n\n /**\n * Add fixtures from a JSON string or pre-parsed array of fixture entries.\n * Validates all fixtures and throws if any have severity \"error\".\n */\n addFixturesFromJSON(input: string | FixtureFileEntry[]): this {\n let entries: FixtureFileEntry[];\n if (typeof input === \"string\") {\n try {\n entries = JSON.parse(input);\n } catch (err) {\n throw new Error(\n `addFixturesFromJSON: invalid JSON — ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n } else {\n entries = input;\n }\n const converted = entries.map((e) => entryToFixture(e));\n const issues = validateFixtures(converted);\n const errors = issues.filter((i) => i.severity === \"error\");\n if (errors.length > 0) {\n throw new Error(`Fixture validation failed: ${JSON.stringify(errors)}`);\n }\n this.fixtures.push(...converted);\n return this;\n }\n\n // Uses length = 0 to preserve array reference identity — the running\n // server reads this same array on every request.\n clearFixtures(): this {\n this.fixtures.length = 0;\n return this;\n }\n\n // ---- Convenience ----\n\n on(\n match: FixtureMatch,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.addFixture({\n match,\n response: typeof response === \"function\" ? response : normalizeResponse(response),\n ...opts,\n });\n }\n\n onMessage(\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ userMessage: pattern }, response, opts);\n }\n\n onEmbedding(\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: EmbeddingFixtureOpts,\n ): this {\n return this.on({ inputText: pattern }, response, opts);\n }\n\n onJsonOutput(pattern: string | RegExp, jsonContent: object | string, opts?: FixtureOpts): this {\n const content = typeof jsonContent === \"string\" ? jsonContent : JSON.stringify(jsonContent);\n return this.on({ userMessage: pattern, responseFormat: \"json_object\" }, { content }, opts);\n }\n\n onToolCall(\n name: string,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ toolName: name }, response, opts);\n }\n\n onToolResult(\n id: string,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ toolCallId: id }, response, opts);\n }\n\n onTurn(\n turn: number,\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ userMessage: pattern, turnIndex: turn }, response, opts);\n }\n\n onImage(prompt: string | RegExp, response: ImageResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"image\" },\n response,\n });\n }\n\n onSpeech(input: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: input, endpoint: \"speech\" },\n response,\n });\n }\n\n onTranscription(response: TranscriptionResponse): this {\n return this.addFixture({\n match: { endpoint: \"transcription\" },\n response,\n });\n }\n\n onTranslation(response: TranscriptionResponse): this {\n return this.addFixture({\n match: { endpoint: \"translation\" },\n response,\n });\n }\n\n onVideo(prompt: string | RegExp, response: VideoResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"video\" },\n response,\n });\n }\n\n onAudio(input: string | RegExp, response: AudioResponse): this {\n return this.addFixture({ match: { userMessage: input }, response });\n }\n\n onSoundEffect(text: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: text, endpoint: \"audio-gen\" },\n response,\n });\n }\n\n onMusic(prompt: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"audio-gen\" },\n response,\n });\n }\n\n onElevenLabsTTS(text: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: text, endpoint: \"elevenlabs-tts\" },\n response,\n });\n }\n\n onFalAudio(prompt: string | RegExp, response: AudioResponse, model?: string): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"fal-audio\", ...(model ? { model } : {}) },\n response,\n });\n }\n\n // fal.queue.* is the dominant client API; onFalRun is a sync alias.\n //\n // `opts.billableUnits` rides through to the completed `queue-result`\n // response's `x-fal-billable-units` header (emitted alongside\n // `x-fal-request-id`), letting consumers like `@tanstack/ai-fal` surface a\n // billed-units value on replay. Omit it to preserve the header-less default.\n onFalQueue(modelOrPrompt: string | RegExp, response: unknown, opts?: FalQueueOpts): this {\n const { billableUnits, ...fixtureOpts } = opts ?? {};\n return this.addFixture({\n match: { model: modelOrPrompt, endpoint: \"fal\" },\n response: { json: response, ...(billableUnits != null ? { billableUnits } : {}) },\n ...fixtureOpts,\n });\n }\n\n onFalRun(modelOrPrompt: string | RegExp, response: unknown, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, response, opts);\n }\n\n /**\n * Register a fal.ai image fixture. Wraps an `ImageResponse` (the shape used\n * by `onImage` and OpenAI/Azure image fixtures) into fal's image envelope\n * before storing it as a `RawJSONResponse`. Defaults `width`/`height` to\n * 1024 when the fixture's `ImageItem` doesn't carry them.\n */\n onFalImage(modelOrPrompt: string | RegExp, response: ImageResponse, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, imageResponseToFalJson(response), opts);\n }\n\n /**\n * Register a fal.ai video fixture. Wraps a `VideoResponse` into fal's video\n * envelope (`{ video: { url, content_type, file_name, file_size }, seed }`)\n * before storing it as a `RawJSONResponse`.\n */\n onFalVideo(modelOrPrompt: string | RegExp, response: VideoResponse, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, videoResponseToFalJson(response), opts);\n }\n\n // ---- Service mock convenience methods ----\n\n onSearch(pattern: string | RegExp, results: SearchResult[]): this {\n this.searchFixtures.push({ match: pattern, results });\n return this;\n }\n\n onRerank(pattern: string | RegExp, results: RerankResult[]): this {\n this.rerankFixtures.push({ match: pattern, results });\n return this;\n }\n\n onModerate(pattern: string | RegExp, result: ModerationResult): this {\n this.moderationFixtures.push({ match: pattern, result });\n return this;\n }\n\n /**\n * Queue a one-shot error that will be returned for the next matching\n * request, then automatically removed. Implemented as an internal fixture\n * with a `predicate` that always matches (so it fires first) and spliced\n * at the front of the fixture list.\n */\n nextRequestError(\n status: number,\n errorBody?: { message?: string; type?: string; code?: string },\n ): this {\n const errorResponse: FixtureResponse = {\n error: {\n message: errorBody?.message ?? \"Injected error\",\n type: errorBody?.type ?? \"server_error\",\n code: errorBody?.code,\n },\n status,\n };\n // An injected error is only a valid response for endpoints that can carry\n // an error envelope. Mirror the router's endpoint-compat table\n // (matchFixtureDiagnostic in router.ts): error responses are compatible\n // with chat / embedding / realtime* / fal and with requests that carry no\n // endpoint type, but NOT with multimedia endpoints (image, speech, video,\n // transcription, …). Gating consumption on this prevents an incompatible\n // request from matching the predicate and splicing — and thereby\n // destroying — a one-shot error intended for a different endpoint before\n // the router's own compat check would have skipped it.\n const errorEndpointCompatible = (req: import(\"./types.js\").ChatCompletionRequest) => {\n const reqEndpoint = req._endpointType as string | undefined;\n if (\n reqEndpoint === undefined ||\n reqEndpoint === \"chat\" ||\n reqEndpoint === \"embedding\" ||\n reqEndpoint.startsWith(\"realtime\") ||\n reqEndpoint === \"fal\"\n ) {\n return true;\n }\n return false;\n };\n const fixture: Fixture = {\n match: { predicate: errorEndpointCompatible },\n response: errorResponse,\n };\n // Insert at front so it matches before everything else\n this.fixtures.unshift(fixture);\n // Remove after first match — the journal records it so tests can assert.\n // Only consume (and splice) when the request endpoint is compatible; an\n // incompatible request returns false here, falls through to other\n // fixtures, and leaves this error pending for its intended endpoint.\n const original = fixture.match.predicate!;\n fixture.match.predicate = (req) => {\n const result = original(req);\n if (result) {\n // Remove synchronously on first match to prevent race conditions\n const idx = this.fixtures.indexOf(fixture);\n if (idx !== -1) this.fixtures.splice(idx, 1);\n }\n return result;\n };\n return this;\n }\n\n // ---- Mounts ----\n\n mount(path: string, handler: Mountable): this {\n this.mounts.push({ path, handler });\n\n // If server is already running, wire up journal, registry, and baseUrl immediately\n // so late mounts behave identically to pre-start mounts.\n if (this.serverInstance) {\n if (handler.setJournal) handler.setJournal(this.serverInstance.journal);\n if (handler.setBaseUrl) handler.setBaseUrl(this.serverInstance.url + path);\n const registry = this.serverInstance.defaults.registry;\n if (registry && handler.setRegistry) handler.setRegistry(registry);\n }\n\n return this;\n }\n\n // ---- Journal proxies ----\n\n getRequests(): import(\"./types.js\").JournalEntry[] {\n return this.journal.getAll();\n }\n\n getLastRequest(): import(\"./types.js\").JournalEntry | null {\n return this.journal.getLast();\n }\n\n clearRequests(): void {\n this.journal.clear();\n }\n\n resetMatchCounts(testId?: string): this {\n if (this.serverInstance) {\n this.serverInstance.journal.clearMatchCounts(testId);\n }\n return this;\n }\n\n // ---- Chaos ----\n\n setChaos(config: ChaosConfig): this {\n this.options.chaos = config;\n return this;\n }\n\n clearChaos(): this {\n delete this.options.chaos;\n return this;\n }\n\n // ---- Recording ----\n\n enableRecording(config: RecordConfig): this {\n this.options.record = config;\n return this;\n }\n\n disableRecording(): this {\n delete this.options.record;\n return this;\n }\n\n // ---- Reset ----\n\n /**\n * Full reset — the in-process equivalent of `POST /__aimock/reset`. Shares\n * one implementation with the control-API route so the two cannot drift.\n *\n * The one deliberate difference: search / rerank / moderation fixtures are\n * also cleared here. Those are registered through this class only — the\n * control API has no route that creates them, so the HTTP reset can neither\n * reach nor observe them.\n *\n * NOT ALL OF THIS IS PER-INSTANCE. `performFullReset` clears module-global\n * state as well: the Gemini interaction and event-id counters\n * (`resetInteractionCounter` / `resetEventIdCounter` in\n * `./gemini-interactions.js`) and the fal.ai job/queue maps (`falJobs`,\n * `falQueueStates`). With two `LLMock` instances live in one process,\n * `a.reset()` rewinds the Gemini id sequence that `b` is mid-way through —\n * `b` then re-emits `aimock-int-0` / `evt_1`, ids it has already handed\n * out — and drops `b`'s in-flight fal jobs. Give each instance its own\n * process (or its own vitest worker) if that matters.\n *\n * The global stores are cleared even before `start()`, when there is no\n * server instance to reset.\n */\n reset(): this {\n this.searchFixtures.length = 0;\n this.rerankFixtures.length = 0;\n this.moderationFixtures.length = 0;\n performFullReset(this.fixtures, this.serverInstance);\n return this;\n }\n\n // ---- Server lifecycle ----\n\n async start(): Promise<string> {\n if (this.serverInstance) {\n throw new Error(\"Server already started\");\n }\n this.serverInstance = await (this.resolvedInboundAuth\n ? createServerWithResolvedAuth(\n this.fixtures,\n this.options,\n this.resolvedInboundAuth,\n this.mounts,\n {\n search: this.searchFixtures,\n rerank: this.rerankFixtures,\n moderation: this.moderationFixtures,\n },\n )\n : createServer(this.fixtures, this.options, this.mounts, {\n search: this.searchFixtures,\n rerank: this.rerankFixtures,\n moderation: this.moderationFixtures,\n }));\n return this.serverInstance.url;\n }\n\n async stop(): Promise<void> {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n const { server } = this.serverInstance;\n await new Promise<void>((resolve, reject) => {\n server.close((err: Error | undefined) => (err ? reject(err) : resolve()));\n });\n this.serverInstance = null;\n }\n\n // ---- Accessors ----\n\n get journal(): Journal {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n return this.serverInstance.journal;\n }\n\n get url(): string {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n return this.serverInstance.url;\n }\n\n get baseUrl(): string {\n return this.url;\n }\n\n get port(): number {\n const parsed = new URL(this.url); // this.url throws if not started\n if (!parsed.port) {\n throw new Error(`Server URL has no explicit port: ${this.url}`);\n }\n return parseInt(parsed.port, 10);\n }\n\n // ---- Static factory ----\n\n static async create(options?: MockServerOptions): Promise<LLMock> {\n const instance = new LLMock(options);\n await instance.start();\n return instance;\n }\n}\n\n/** @internal Configuration startup preserves a policy resolved from the selected source. */\nexport function createLLMockWithResolvedAuth(\n options: MockServerOptions,\n resolvedAuth: ResolvedInboundAuth,\n): LLMock {\n return new LLMock(options, resolvedAuth);\n}\n"],"mappings":"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"}
package/dist/llmock.d.cts CHANGED
@@ -83,6 +83,28 @@ declare class LLMock {
83
83
  clearChaos(): this;
84
84
  enableRecording(config: RecordConfig): this;
85
85
  disableRecording(): this;
86
+ /**
87
+ * Full reset — the in-process equivalent of `POST /__aimock/reset`. Shares
88
+ * one implementation with the control-API route so the two cannot drift.
89
+ *
90
+ * The one deliberate difference: search / rerank / moderation fixtures are
91
+ * also cleared here. Those are registered through this class only — the
92
+ * control API has no route that creates them, so the HTTP reset can neither
93
+ * reach nor observe them.
94
+ *
95
+ * NOT ALL OF THIS IS PER-INSTANCE. `performFullReset` clears module-global
96
+ * state as well: the Gemini interaction and event-id counters
97
+ * (`resetInteractionCounter` / `resetEventIdCounter` in
98
+ * `./gemini-interactions.js`) and the fal.ai job/queue maps (`falJobs`,
99
+ * `falQueueStates`). With two `LLMock` instances live in one process,
100
+ * `a.reset()` rewinds the Gemini id sequence that `b` is mid-way through —
101
+ * `b` then re-emits `aimock-int-0` / `evt_1`, ids it has already handed
102
+ * out — and drops `b`'s in-flight fal jobs. Give each instance its own
103
+ * process (or its own vitest worker) if that matters.
104
+ *
105
+ * The global stores are cleared even before `start()`, when there is no
106
+ * server instance to reset.
107
+ */
86
108
  reset(): this;
87
109
  start(): Promise<string>;
88
110
  stop(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"llmock.d.cts","names":[],"sources":["../src/llmock.ts"],"sourcesContent":[],"mappings":"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"}
1
+ {"version":3,"file":"llmock.d.cts","names":[],"sources":["../src/llmock.ts"],"sourcesContent":[],"mappings":"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"}
package/dist/llmock.d.ts CHANGED
@@ -83,6 +83,28 @@ declare class LLMock {
83
83
  clearChaos(): this;
84
84
  enableRecording(config: RecordConfig): this;
85
85
  disableRecording(): this;
86
+ /**
87
+ * Full reset — the in-process equivalent of `POST /__aimock/reset`. Shares
88
+ * one implementation with the control-API route so the two cannot drift.
89
+ *
90
+ * The one deliberate difference: search / rerank / moderation fixtures are
91
+ * also cleared here. Those are registered through this class only — the
92
+ * control API has no route that creates them, so the HTTP reset can neither
93
+ * reach nor observe them.
94
+ *
95
+ * NOT ALL OF THIS IS PER-INSTANCE. `performFullReset` clears module-global
96
+ * state as well: the Gemini interaction and event-id counters
97
+ * (`resetInteractionCounter` / `resetEventIdCounter` in
98
+ * `./gemini-interactions.js`) and the fal.ai job/queue maps (`falJobs`,
99
+ * `falQueueStates`). With two `LLMock` instances live in one process,
100
+ * `a.reset()` rewinds the Gemini id sequence that `b` is mid-way through —
101
+ * `b` then re-emits `aimock-int-0` / `evt_1`, ids it has already handed
102
+ * out — and drops `b`'s in-flight fal jobs. Give each instance its own
103
+ * process (or its own vitest worker) if that matters.
104
+ *
105
+ * The global stores are cleared even before `start()`, when there is no
106
+ * server instance to reset.
107
+ */
86
108
  reset(): this;
87
109
  start(): Promise<string>;
88
110
  stop(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"llmock.d.ts","names":[],"sources":["../src/llmock.ts"],"sourcesContent":[],"mappings":";;;;;;;;cAmCa,MAAA;;EAAA,QAAA,cAAM;EAAA,QAAA,cAAA;UAUK,kBAAA;UAAyC,MAAA;UAO3C,cAAA;UAKE,OAAA;mBAKE,mBAAA;aAKA,CAAA,OAAA,CAAA,EAtBF,iBAsBE,EAAA,mBAAA,CAAA,EAtBuC,mBAsBvC;YAkBY,CAAA,OAAA,EAjChB,OAiCgB,CAAA,EAAA,IAAA;aAiC3B,CAAA,QAAA,EA7Da,OA6Db,EAAA,CAAA,EAAA,IAAA;gBACG,CAAA,OAAA,EAzDY,OAyDZ,CAAA,EAAA,IAAA;aAAsB,CAAA,CAAA,EAAA,SApDV,OAoDU,EAAA;iBACzB,CAAA,QAAA,EAAA,MAAA,CAAA,EAAA,IAAA;gBAUW,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;qBASR,CAAA,KAAA,EAAA,MAAA,GAtDwB,gBAsDxB,EAAA,CAAA,EAAA,IAAA;eAAsB,CAAA,CAAA,EAAA,IAAA;KACzB,KAAA,EAtBA,YAsBA,EAAA,QAAA,EArBG,mBAqBH,GArByB,eAqBzB,EAAA,IAAA,CAAA,EApBA,WAoBA,CAAA,EAAA,IAAA;WAKsB,CAAA,OAAA,EAAA,MAAA,GAfX,MAeW,EAAA,QAAA,EAdnB,mBAcmB,GAdG,eAcH,EAAA,IAAA,CAAA,EAbtB,WAasB,CAAA,EAAA,IAAA;aAA6C,CAAA,OAAA,EAAA,MAAA,GAPxD,MAOwD,EAAA,QAAA,EANhE,mBAMgE,GAN1C,eAM0C,EAAA,IAAA,CAAA,EALnE,oBAKmE,CAAA,EAAA,IAAA;cAOhE,CAAA,OAAA,EAAA,MAAA,GAPmB,MAOnB,EAAA,WAAA,EAAA,MAAA,GAAA,MAAA,EAAA,IAAA,CAAA,EAPgE,WAOhE,CAAA,EAAA,IAAA;YAAsB,CAAA,IAAA,EAAA,MAAA,EAAA,QAAA,EAAtB,mBAAsB,GAAA,eAAA,EAAA,IAAA,CAAA,EACzB,WADyB,CAAA,EAAA,IAAA;cACzB,CAAA,EAAA,EAAA,MAAA,EAAA,QAAA,EAOG,mBAPH,GAOyB,eAPzB,EAAA,IAAA,CAAA,EAQA,WARA,CAAA,EAAA,IAAA;QAOG,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,GAQQ,MARR,EAAA,QAAA,EASA,mBATA,GASsB,eATtB,EAAA,IAAA,CAAA,EAUH,WAVG,CAAA,EAAA,IAAA;SAAsB,CAAA,MAAA,EAAA,MAAA,GAeT,MAfS,EAAA,QAAA,EAeS,aAfT,CAAA,EAAA,IAAA;UACzB,CAAA,KAAA,EAAA,MAAA,GAqBgB,MArBhB,EAAA,QAAA,EAqBkC,aArBlC,CAAA,EAAA,IAAA;iBAOW,CAAA,QAAA,EAqBM,qBArBN,CAAA,EAAA,IAAA;eACR,CAAA,QAAA,EA2BY,qBA3BZ,CAAA,EAAA,IAAA;SAAsB,CAAA,MAAA,EAAA,MAAA,GAkCT,MAlCS,EAAA,QAAA,EAkCS,aAlCT,CAAA,EAAA,IAAA;SACzB,CAAA,KAAA,EAAA,MAAA,GAwCe,MAxCf,EAAA,QAAA,EAwCiC,aAxCjC,CAAA,EAAA,IAAA;eAKgB,CAAA,IAAA,EAAA,MAAA,GAuCI,MAvCJ,EAAA,QAAA,EAuCsB,aAvCtB,CAAA,EAAA,IAAA;SAAkB,CAAA,MAAA,EAAA,MAAA,GA8ClB,MA9CkB,EAAA,QAAA,EA8CA,aA9CA,CAAA,EAAA,IAAA;iBAOlB,CAAA,IAAA,EAAA,MAAA,GA8CM,MA9CN,EAAA,QAAA,EA8CwB,aA9CxB,CAAA,EAAA,IAAA;YAAkB,CAAA,MAAA,EAAA,MAAA,GAqDf,MArDe,EAAA,QAAA,EAqDG,aArDH,EAAA,KAAA,CAAA,EAAA,MAAA,CAAA,EAAA,IAAA;YAOjB,CAAA,aAAA,EAAA,MAAA,GA2DS,MA3DT,EAAA,QAAA,EAAA,OAAA,EAAA,IAAA,CAAA,EA2D2C,YA3D3C,CAAA,EAAA,IAAA;UAOF,CAAA,aAAA,EAAA,MAAA,GA6DS,MA7DT,EAAA,QAAA,EAAA,OAAA,EAAA,IAAA,CAAA,EA6D2C,YA7D3C,CAAA,EAAA,IAAA;;;;;;;YAyBC,CAAA,aAAA,EAAA,MAAA,GA8CU,MA9CV,EAAA,QAAA,EA8C4B,aA9C5B,EAAA,IAAA,CAAA,EA8CkD,YA9ClD,CAAA,EAAA,IAAA;;;;;;YA2BU,CAAA,aAAA,EAAA,MAAA,GA4BA,MA5BA,EAAA,QAAA,EA4BkB,aA5BlB,EAAA,IAAA,CAAA,EA4BwC,YA5BxC,CAAA,EAAA,IAAA;UAAkC,CAAA,OAAA,EAAA,MAAA,GAkC1C,MAlC0C,EAAA,OAAA,EAkCzB,YAlCyB,EAAA,CAAA,EAAA,IAAA;UASpC,CAAA,OAAA,EAAA,MAAA,GA8BN,MA9BM,EAAA,OAAA,EA8BW,YA9BX,EAAA,CAAA,EAAA,IAAA;YAAkC,CAAA,OAAA,EAAA,MAAA,GAmCtC,MAnCsC,EAAA,MAAA,EAmCtB,gBAnCsB,CAAA,EAAA,IAAA;;;;;;;kBAyBxC,CAAA,MAAA,EAAA,MAAA,EAAA,UAAA,EAAA;IAAiB,OAAA,CAAA,EAAA,MAAA;IAKjB,IAAA,CAAA,EAAA,MAAA;IAAiB,IAAA,CAAA,EAAA,MAAA;MAKf,IAAA;OAAgB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAsEhB,SAtEgB,CAAA,EAAA,IAAA;aAsEhB,CAAA,CAAA,EAAS,YAAA,EAAT;gBAAS,CAAA,CAAA,EAiBU,YAAA,GAjBV,IAAA;eAiBU,CAAA,CAAA,EAAA,IAAA;kBAqB/B,CAAA,MAAA,CAAA,EAAA,MAAA,CAAA,EAAA,IAAA;UAYO,CAAA,MAAA,EAZP,WAYO,CAAA,EAAA,IAAA;YA6BT,CAAA,CAAA,EAAA,IAAA;iBAwBD,CAAA,MAAA,EArDU,YAqDV,CAAA,EAAA,IAAA;kBAaC,CAAA,CAAA,EAAA,IAAA;OA4Be,CAAA,CAAA,EAAA,IAAA;OAA4B,CAAA,CAAA,EAjE3C,OAiE2C,CAAA,MAAA,CAAA;MAAR,CAAA,CAAA,EAzCpC,OAyCoC,CAAA,IAAA,CAAA;EAAO,IAAA,OAAA,CAAA,CAAA,EA5B1C,OA4B0C;;;;0BAA3B,oBAAoB,QAAQ"}
1
+ {"version":3,"file":"llmock.d.ts","names":[],"sources":["../src/llmock.ts"],"sourcesContent":[],"mappings":"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"}
package/dist/llmock.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { entryToFixture, loadFixtureFile, loadFixturesFromDir, normalizeResponse, validateFixtures } from "./fixture-loader.js";
2
- import { falJobs } from "./fal-audio.js";
3
- import { falQueueStates, imageResponseToFalJson, videoResponseToFalJson } from "./fal.js";
4
- import { createServer, createServerWithResolvedAuth } from "./server.js";
2
+ import { imageResponseToFalJson, videoResponseToFalJson } from "./fal.js";
3
+ import { createServer, createServerWithResolvedAuth, performFullReset } from "./server.js";
5
4
 
6
5
  //#region src/llmock.ts
7
6
  var LLMock = class LLMock {
@@ -309,18 +308,33 @@ var LLMock = class LLMock {
309
308
  delete this.options.record;
310
309
  return this;
311
310
  }
311
+ /**
312
+ * Full reset — the in-process equivalent of `POST /__aimock/reset`. Shares
313
+ * one implementation with the control-API route so the two cannot drift.
314
+ *
315
+ * The one deliberate difference: search / rerank / moderation fixtures are
316
+ * also cleared here. Those are registered through this class only — the
317
+ * control API has no route that creates them, so the HTTP reset can neither
318
+ * reach nor observe them.
319
+ *
320
+ * NOT ALL OF THIS IS PER-INSTANCE. `performFullReset` clears module-global
321
+ * state as well: the Gemini interaction and event-id counters
322
+ * (`resetInteractionCounter` / `resetEventIdCounter` in
323
+ * `./gemini-interactions.js`) and the fal.ai job/queue maps (`falJobs`,
324
+ * `falQueueStates`). With two `LLMock` instances live in one process,
325
+ * `a.reset()` rewinds the Gemini id sequence that `b` is mid-way through —
326
+ * `b` then re-emits `aimock-int-0` / `evt_1`, ids it has already handed
327
+ * out — and drops `b`'s in-flight fal jobs. Give each instance its own
328
+ * process (or its own vitest worker) if that matters.
329
+ *
330
+ * The global stores are cleared even before `start()`, when there is no
331
+ * server instance to reset.
332
+ */
312
333
  reset() {
313
- this.clearFixtures();
314
334
  this.searchFixtures.length = 0;
315
335
  this.rerankFixtures.length = 0;
316
336
  this.moderationFixtures.length = 0;
317
- falJobs.clear();
318
- falQueueStates.clear();
319
- if (this.serverInstance) {
320
- this.serverInstance.journal.clear();
321
- this.serverInstance.videoStates.clear();
322
- this.serverInstance.openRouterVideoJobs.clear();
323
- }
337
+ performFullReset(this.fixtures, this.serverInstance);
324
338
  return this;
325
339
  }
326
340
  async start() {
@@ -1 +1 @@
1
- {"version":3,"file":"llmock.js","names":[],"sources":["../src/llmock.ts"],"sourcesContent":["import type {\n AudioResponse,\n ChaosConfig,\n EmbeddingFixtureOpts,\n FalQueueOpts,\n Fixture,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureMatch,\n FixtureOpts,\n FixtureResponse,\n ImageResponse,\n MockServerOptions,\n Mountable,\n RecordConfig,\n ResponseFactory,\n TranscriptionResponse,\n VideoResponse,\n} from \"./types.js\";\nimport { createServer, createServerWithResolvedAuth, type ServerInstance } from \"./server.js\";\nimport type { ResolvedInboundAuth } from \"./api-key-auth.js\";\nimport {\n loadFixtureFile,\n loadFixturesFromDir,\n entryToFixture,\n normalizeResponse,\n validateFixtures,\n} from \"./fixture-loader.js\";\nimport { Journal } from \"./journal.js\";\nimport type { SearchFixture, SearchResult } from \"./search.js\";\nimport type { RerankFixture, RerankResult } from \"./rerank.js\";\nimport type { ModerationFixture, ModerationResult } from \"./moderation.js\";\nimport { falJobs } from \"./fal-audio.js\";\nimport { falQueueStates, imageResponseToFalJson, videoResponseToFalJson } from \"./fal.js\";\n\nexport class LLMock {\n private fixtures: Fixture[] = [];\n private searchFixtures: SearchFixture[] = [];\n private rerankFixtures: RerankFixture[] = [];\n private moderationFixtures: ModerationFixture[] = [];\n private mounts: Array<{ path: string; handler: Mountable }> = [];\n private serverInstance: ServerInstance | null = null;\n private options: MockServerOptions;\n private readonly resolvedInboundAuth?: ResolvedInboundAuth;\n\n constructor(options?: MockServerOptions, resolvedInboundAuth?: ResolvedInboundAuth) {\n this.options = options ?? {};\n this.resolvedInboundAuth = resolvedInboundAuth;\n }\n\n // ---- Fixture management ----\n\n addFixture(fixture: Fixture): this {\n this.fixtures.push(fixture);\n return this;\n }\n\n addFixtures(fixtures: Fixture[]): this {\n this.fixtures.push(...fixtures);\n return this;\n }\n\n prependFixture(fixture: Fixture): this {\n this.fixtures.unshift(fixture);\n return this;\n }\n\n getFixtures(): readonly Fixture[] {\n return this.fixtures;\n }\n\n loadFixtureFile(filePath: string): this {\n this.fixtures.push(...loadFixtureFile(filePath));\n return this;\n }\n\n loadFixtureDir(dirPath: string): this {\n this.fixtures.push(...loadFixturesFromDir(dirPath));\n return this;\n }\n\n /**\n * Add fixtures from a JSON string or pre-parsed array of fixture entries.\n * Validates all fixtures and throws if any have severity \"error\".\n */\n addFixturesFromJSON(input: string | FixtureFileEntry[]): this {\n let entries: FixtureFileEntry[];\n if (typeof input === \"string\") {\n try {\n entries = JSON.parse(input);\n } catch (err) {\n throw new Error(\n `addFixturesFromJSON: invalid JSON — ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n } else {\n entries = input;\n }\n const converted = entries.map((e) => entryToFixture(e));\n const issues = validateFixtures(converted);\n const errors = issues.filter((i) => i.severity === \"error\");\n if (errors.length > 0) {\n throw new Error(`Fixture validation failed: ${JSON.stringify(errors)}`);\n }\n this.fixtures.push(...converted);\n return this;\n }\n\n // Uses length = 0 to preserve array reference identity — the running\n // server reads this same array on every request.\n clearFixtures(): this {\n this.fixtures.length = 0;\n return this;\n }\n\n // ---- Convenience ----\n\n on(\n match: FixtureMatch,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.addFixture({\n match,\n response: typeof response === \"function\" ? response : normalizeResponse(response),\n ...opts,\n });\n }\n\n onMessage(\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ userMessage: pattern }, response, opts);\n }\n\n onEmbedding(\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: EmbeddingFixtureOpts,\n ): this {\n return this.on({ inputText: pattern }, response, opts);\n }\n\n onJsonOutput(pattern: string | RegExp, jsonContent: object | string, opts?: FixtureOpts): this {\n const content = typeof jsonContent === \"string\" ? jsonContent : JSON.stringify(jsonContent);\n return this.on({ userMessage: pattern, responseFormat: \"json_object\" }, { content }, opts);\n }\n\n onToolCall(\n name: string,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ toolName: name }, response, opts);\n }\n\n onToolResult(\n id: string,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ toolCallId: id }, response, opts);\n }\n\n onTurn(\n turn: number,\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ userMessage: pattern, turnIndex: turn }, response, opts);\n }\n\n onImage(prompt: string | RegExp, response: ImageResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"image\" },\n response,\n });\n }\n\n onSpeech(input: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: input, endpoint: \"speech\" },\n response,\n });\n }\n\n onTranscription(response: TranscriptionResponse): this {\n return this.addFixture({\n match: { endpoint: \"transcription\" },\n response,\n });\n }\n\n onTranslation(response: TranscriptionResponse): this {\n return this.addFixture({\n match: { endpoint: \"translation\" },\n response,\n });\n }\n\n onVideo(prompt: string | RegExp, response: VideoResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"video\" },\n response,\n });\n }\n\n onAudio(input: string | RegExp, response: AudioResponse): this {\n return this.addFixture({ match: { userMessage: input }, response });\n }\n\n onSoundEffect(text: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: text, endpoint: \"audio-gen\" },\n response,\n });\n }\n\n onMusic(prompt: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"audio-gen\" },\n response,\n });\n }\n\n onElevenLabsTTS(text: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: text, endpoint: \"elevenlabs-tts\" },\n response,\n });\n }\n\n onFalAudio(prompt: string | RegExp, response: AudioResponse, model?: string): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"fal-audio\", ...(model ? { model } : {}) },\n response,\n });\n }\n\n // fal.queue.* is the dominant client API; onFalRun is a sync alias.\n //\n // `opts.billableUnits` rides through to the completed `queue-result`\n // response's `x-fal-billable-units` header (emitted alongside\n // `x-fal-request-id`), letting consumers like `@tanstack/ai-fal` surface a\n // billed-units value on replay. Omit it to preserve the header-less default.\n onFalQueue(modelOrPrompt: string | RegExp, response: unknown, opts?: FalQueueOpts): this {\n const { billableUnits, ...fixtureOpts } = opts ?? {};\n return this.addFixture({\n match: { model: modelOrPrompt, endpoint: \"fal\" },\n response: { json: response, ...(billableUnits != null ? { billableUnits } : {}) },\n ...fixtureOpts,\n });\n }\n\n onFalRun(modelOrPrompt: string | RegExp, response: unknown, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, response, opts);\n }\n\n /**\n * Register a fal.ai image fixture. Wraps an `ImageResponse` (the shape used\n * by `onImage` and OpenAI/Azure image fixtures) into fal's image envelope\n * before storing it as a `RawJSONResponse`. Defaults `width`/`height` to\n * 1024 when the fixture's `ImageItem` doesn't carry them.\n */\n onFalImage(modelOrPrompt: string | RegExp, response: ImageResponse, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, imageResponseToFalJson(response), opts);\n }\n\n /**\n * Register a fal.ai video fixture. Wraps a `VideoResponse` into fal's video\n * envelope (`{ video: { url, content_type, file_name, file_size }, seed }`)\n * before storing it as a `RawJSONResponse`.\n */\n onFalVideo(modelOrPrompt: string | RegExp, response: VideoResponse, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, videoResponseToFalJson(response), opts);\n }\n\n // ---- Service mock convenience methods ----\n\n onSearch(pattern: string | RegExp, results: SearchResult[]): this {\n this.searchFixtures.push({ match: pattern, results });\n return this;\n }\n\n onRerank(pattern: string | RegExp, results: RerankResult[]): this {\n this.rerankFixtures.push({ match: pattern, results });\n return this;\n }\n\n onModerate(pattern: string | RegExp, result: ModerationResult): this {\n this.moderationFixtures.push({ match: pattern, result });\n return this;\n }\n\n /**\n * Queue a one-shot error that will be returned for the next matching\n * request, then automatically removed. Implemented as an internal fixture\n * with a `predicate` that always matches (so it fires first) and spliced\n * at the front of the fixture list.\n */\n nextRequestError(\n status: number,\n errorBody?: { message?: string; type?: string; code?: string },\n ): this {\n const errorResponse: FixtureResponse = {\n error: {\n message: errorBody?.message ?? \"Injected error\",\n type: errorBody?.type ?? \"server_error\",\n code: errorBody?.code,\n },\n status,\n };\n // An injected error is only a valid response for endpoints that can carry\n // an error envelope. Mirror the router's endpoint-compat table\n // (matchFixtureDiagnostic in router.ts): error responses are compatible\n // with chat / embedding / realtime* / fal and with requests that carry no\n // endpoint type, but NOT with multimedia endpoints (image, speech, video,\n // transcription, …). Gating consumption on this prevents an incompatible\n // request from matching the predicate and splicing — and thereby\n // destroying — a one-shot error intended for a different endpoint before\n // the router's own compat check would have skipped it.\n const errorEndpointCompatible = (req: import(\"./types.js\").ChatCompletionRequest) => {\n const reqEndpoint = req._endpointType as string | undefined;\n if (\n reqEndpoint === undefined ||\n reqEndpoint === \"chat\" ||\n reqEndpoint === \"embedding\" ||\n reqEndpoint.startsWith(\"realtime\") ||\n reqEndpoint === \"fal\"\n ) {\n return true;\n }\n return false;\n };\n const fixture: Fixture = {\n match: { predicate: errorEndpointCompatible },\n response: errorResponse,\n };\n // Insert at front so it matches before everything else\n this.fixtures.unshift(fixture);\n // Remove after first match — the journal records it so tests can assert.\n // Only consume (and splice) when the request endpoint is compatible; an\n // incompatible request returns false here, falls through to other\n // fixtures, and leaves this error pending for its intended endpoint.\n const original = fixture.match.predicate!;\n fixture.match.predicate = (req) => {\n const result = original(req);\n if (result) {\n // Remove synchronously on first match to prevent race conditions\n const idx = this.fixtures.indexOf(fixture);\n if (idx !== -1) this.fixtures.splice(idx, 1);\n }\n return result;\n };\n return this;\n }\n\n // ---- Mounts ----\n\n mount(path: string, handler: Mountable): this {\n this.mounts.push({ path, handler });\n\n // If server is already running, wire up journal, registry, and baseUrl immediately\n // so late mounts behave identically to pre-start mounts.\n if (this.serverInstance) {\n if (handler.setJournal) handler.setJournal(this.serverInstance.journal);\n if (handler.setBaseUrl) handler.setBaseUrl(this.serverInstance.url + path);\n const registry = this.serverInstance.defaults.registry;\n if (registry && handler.setRegistry) handler.setRegistry(registry);\n }\n\n return this;\n }\n\n // ---- Journal proxies ----\n\n getRequests(): import(\"./types.js\").JournalEntry[] {\n return this.journal.getAll();\n }\n\n getLastRequest(): import(\"./types.js\").JournalEntry | null {\n return this.journal.getLast();\n }\n\n clearRequests(): void {\n this.journal.clear();\n }\n\n resetMatchCounts(testId?: string): this {\n if (this.serverInstance) {\n this.serverInstance.journal.clearMatchCounts(testId);\n }\n return this;\n }\n\n // ---- Chaos ----\n\n setChaos(config: ChaosConfig): this {\n this.options.chaos = config;\n return this;\n }\n\n clearChaos(): this {\n delete this.options.chaos;\n return this;\n }\n\n // ---- Recording ----\n\n enableRecording(config: RecordConfig): this {\n this.options.record = config;\n return this;\n }\n\n disableRecording(): this {\n delete this.options.record;\n return this;\n }\n\n // ---- Reset ----\n\n reset(): this {\n this.clearFixtures();\n this.searchFixtures.length = 0;\n this.rerankFixtures.length = 0;\n this.moderationFixtures.length = 0;\n falJobs.clear();\n falQueueStates.clear();\n if (this.serverInstance) {\n this.serverInstance.journal.clear();\n this.serverInstance.videoStates.clear();\n this.serverInstance.openRouterVideoJobs.clear();\n }\n return this;\n }\n\n // ---- Server lifecycle ----\n\n async start(): Promise<string> {\n if (this.serverInstance) {\n throw new Error(\"Server already started\");\n }\n this.serverInstance = await (this.resolvedInboundAuth\n ? createServerWithResolvedAuth(\n this.fixtures,\n this.options,\n this.resolvedInboundAuth,\n this.mounts,\n {\n search: this.searchFixtures,\n rerank: this.rerankFixtures,\n moderation: this.moderationFixtures,\n },\n )\n : createServer(this.fixtures, this.options, this.mounts, {\n search: this.searchFixtures,\n rerank: this.rerankFixtures,\n moderation: this.moderationFixtures,\n }));\n return this.serverInstance.url;\n }\n\n async stop(): Promise<void> {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n const { server } = this.serverInstance;\n await new Promise<void>((resolve, reject) => {\n server.close((err: Error | undefined) => (err ? reject(err) : resolve()));\n });\n this.serverInstance = null;\n }\n\n // ---- Accessors ----\n\n get journal(): Journal {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n return this.serverInstance.journal;\n }\n\n get url(): string {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n return this.serverInstance.url;\n }\n\n get baseUrl(): string {\n return this.url;\n }\n\n get port(): number {\n const parsed = new URL(this.url); // this.url throws if not started\n if (!parsed.port) {\n throw new Error(`Server URL has no explicit port: ${this.url}`);\n }\n return parseInt(parsed.port, 10);\n }\n\n // ---- Static factory ----\n\n static async create(options?: MockServerOptions): Promise<LLMock> {\n const instance = new LLMock(options);\n await instance.start();\n return instance;\n }\n}\n\n/** @internal Configuration startup preserves a policy resolved from the selected source. */\nexport function createLLMockWithResolvedAuth(\n options: MockServerOptions,\n resolvedAuth: ResolvedInboundAuth,\n): LLMock {\n return new LLMock(options, resolvedAuth);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"llmock.js","names":[],"sources":["../src/llmock.ts"],"sourcesContent":["import type {\n AudioResponse,\n ChaosConfig,\n EmbeddingFixtureOpts,\n FalQueueOpts,\n Fixture,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureMatch,\n FixtureOpts,\n FixtureResponse,\n ImageResponse,\n MockServerOptions,\n Mountable,\n RecordConfig,\n ResponseFactory,\n TranscriptionResponse,\n VideoResponse,\n} from \"./types.js\";\nimport {\n createServer,\n createServerWithResolvedAuth,\n performFullReset,\n type ServerInstance,\n} from \"./server.js\";\nimport type { ResolvedInboundAuth } from \"./api-key-auth.js\";\nimport {\n loadFixtureFile,\n loadFixturesFromDir,\n entryToFixture,\n normalizeResponse,\n validateFixtures,\n} from \"./fixture-loader.js\";\nimport { Journal } from \"./journal.js\";\nimport type { SearchFixture, SearchResult } from \"./search.js\";\nimport type { RerankFixture, RerankResult } from \"./rerank.js\";\nimport type { ModerationFixture, ModerationResult } from \"./moderation.js\";\nimport { imageResponseToFalJson, videoResponseToFalJson } from \"./fal.js\";\n\nexport class LLMock {\n private fixtures: Fixture[] = [];\n private searchFixtures: SearchFixture[] = [];\n private rerankFixtures: RerankFixture[] = [];\n private moderationFixtures: ModerationFixture[] = [];\n private mounts: Array<{ path: string; handler: Mountable }> = [];\n private serverInstance: ServerInstance | null = null;\n private options: MockServerOptions;\n private readonly resolvedInboundAuth?: ResolvedInboundAuth;\n\n constructor(options?: MockServerOptions, resolvedInboundAuth?: ResolvedInboundAuth) {\n this.options = options ?? {};\n this.resolvedInboundAuth = resolvedInboundAuth;\n }\n\n // ---- Fixture management ----\n\n addFixture(fixture: Fixture): this {\n this.fixtures.push(fixture);\n return this;\n }\n\n addFixtures(fixtures: Fixture[]): this {\n this.fixtures.push(...fixtures);\n return this;\n }\n\n prependFixture(fixture: Fixture): this {\n this.fixtures.unshift(fixture);\n return this;\n }\n\n getFixtures(): readonly Fixture[] {\n return this.fixtures;\n }\n\n loadFixtureFile(filePath: string): this {\n this.fixtures.push(...loadFixtureFile(filePath));\n return this;\n }\n\n loadFixtureDir(dirPath: string): this {\n this.fixtures.push(...loadFixturesFromDir(dirPath));\n return this;\n }\n\n /**\n * Add fixtures from a JSON string or pre-parsed array of fixture entries.\n * Validates all fixtures and throws if any have severity \"error\".\n */\n addFixturesFromJSON(input: string | FixtureFileEntry[]): this {\n let entries: FixtureFileEntry[];\n if (typeof input === \"string\") {\n try {\n entries = JSON.parse(input);\n } catch (err) {\n throw new Error(\n `addFixturesFromJSON: invalid JSON — ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n } else {\n entries = input;\n }\n const converted = entries.map((e) => entryToFixture(e));\n const issues = validateFixtures(converted);\n const errors = issues.filter((i) => i.severity === \"error\");\n if (errors.length > 0) {\n throw new Error(`Fixture validation failed: ${JSON.stringify(errors)}`);\n }\n this.fixtures.push(...converted);\n return this;\n }\n\n // Uses length = 0 to preserve array reference identity — the running\n // server reads this same array on every request.\n clearFixtures(): this {\n this.fixtures.length = 0;\n return this;\n }\n\n // ---- Convenience ----\n\n on(\n match: FixtureMatch,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.addFixture({\n match,\n response: typeof response === \"function\" ? response : normalizeResponse(response),\n ...opts,\n });\n }\n\n onMessage(\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ userMessage: pattern }, response, opts);\n }\n\n onEmbedding(\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: EmbeddingFixtureOpts,\n ): this {\n return this.on({ inputText: pattern }, response, opts);\n }\n\n onJsonOutput(pattern: string | RegExp, jsonContent: object | string, opts?: FixtureOpts): this {\n const content = typeof jsonContent === \"string\" ? jsonContent : JSON.stringify(jsonContent);\n return this.on({ userMessage: pattern, responseFormat: \"json_object\" }, { content }, opts);\n }\n\n onToolCall(\n name: string,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ toolName: name }, response, opts);\n }\n\n onToolResult(\n id: string,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ toolCallId: id }, response, opts);\n }\n\n onTurn(\n turn: number,\n pattern: string | RegExp,\n response: FixtureFileResponse | ResponseFactory,\n opts?: FixtureOpts,\n ): this {\n return this.on({ userMessage: pattern, turnIndex: turn }, response, opts);\n }\n\n onImage(prompt: string | RegExp, response: ImageResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"image\" },\n response,\n });\n }\n\n onSpeech(input: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: input, endpoint: \"speech\" },\n response,\n });\n }\n\n onTranscription(response: TranscriptionResponse): this {\n return this.addFixture({\n match: { endpoint: \"transcription\" },\n response,\n });\n }\n\n onTranslation(response: TranscriptionResponse): this {\n return this.addFixture({\n match: { endpoint: \"translation\" },\n response,\n });\n }\n\n onVideo(prompt: string | RegExp, response: VideoResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"video\" },\n response,\n });\n }\n\n onAudio(input: string | RegExp, response: AudioResponse): this {\n return this.addFixture({ match: { userMessage: input }, response });\n }\n\n onSoundEffect(text: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: text, endpoint: \"audio-gen\" },\n response,\n });\n }\n\n onMusic(prompt: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"audio-gen\" },\n response,\n });\n }\n\n onElevenLabsTTS(text: string | RegExp, response: AudioResponse): this {\n return this.addFixture({\n match: { userMessage: text, endpoint: \"elevenlabs-tts\" },\n response,\n });\n }\n\n onFalAudio(prompt: string | RegExp, response: AudioResponse, model?: string): this {\n return this.addFixture({\n match: { userMessage: prompt, endpoint: \"fal-audio\", ...(model ? { model } : {}) },\n response,\n });\n }\n\n // fal.queue.* is the dominant client API; onFalRun is a sync alias.\n //\n // `opts.billableUnits` rides through to the completed `queue-result`\n // response's `x-fal-billable-units` header (emitted alongside\n // `x-fal-request-id`), letting consumers like `@tanstack/ai-fal` surface a\n // billed-units value on replay. Omit it to preserve the header-less default.\n onFalQueue(modelOrPrompt: string | RegExp, response: unknown, opts?: FalQueueOpts): this {\n const { billableUnits, ...fixtureOpts } = opts ?? {};\n return this.addFixture({\n match: { model: modelOrPrompt, endpoint: \"fal\" },\n response: { json: response, ...(billableUnits != null ? { billableUnits } : {}) },\n ...fixtureOpts,\n });\n }\n\n onFalRun(modelOrPrompt: string | RegExp, response: unknown, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, response, opts);\n }\n\n /**\n * Register a fal.ai image fixture. Wraps an `ImageResponse` (the shape used\n * by `onImage` and OpenAI/Azure image fixtures) into fal's image envelope\n * before storing it as a `RawJSONResponse`. Defaults `width`/`height` to\n * 1024 when the fixture's `ImageItem` doesn't carry them.\n */\n onFalImage(modelOrPrompt: string | RegExp, response: ImageResponse, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, imageResponseToFalJson(response), opts);\n }\n\n /**\n * Register a fal.ai video fixture. Wraps a `VideoResponse` into fal's video\n * envelope (`{ video: { url, content_type, file_name, file_size }, seed }`)\n * before storing it as a `RawJSONResponse`.\n */\n onFalVideo(modelOrPrompt: string | RegExp, response: VideoResponse, opts?: FalQueueOpts): this {\n return this.onFalQueue(modelOrPrompt, videoResponseToFalJson(response), opts);\n }\n\n // ---- Service mock convenience methods ----\n\n onSearch(pattern: string | RegExp, results: SearchResult[]): this {\n this.searchFixtures.push({ match: pattern, results });\n return this;\n }\n\n onRerank(pattern: string | RegExp, results: RerankResult[]): this {\n this.rerankFixtures.push({ match: pattern, results });\n return this;\n }\n\n onModerate(pattern: string | RegExp, result: ModerationResult): this {\n this.moderationFixtures.push({ match: pattern, result });\n return this;\n }\n\n /**\n * Queue a one-shot error that will be returned for the next matching\n * request, then automatically removed. Implemented as an internal fixture\n * with a `predicate` that always matches (so it fires first) and spliced\n * at the front of the fixture list.\n */\n nextRequestError(\n status: number,\n errorBody?: { message?: string; type?: string; code?: string },\n ): this {\n const errorResponse: FixtureResponse = {\n error: {\n message: errorBody?.message ?? \"Injected error\",\n type: errorBody?.type ?? \"server_error\",\n code: errorBody?.code,\n },\n status,\n };\n // An injected error is only a valid response for endpoints that can carry\n // an error envelope. Mirror the router's endpoint-compat table\n // (matchFixtureDiagnostic in router.ts): error responses are compatible\n // with chat / embedding / realtime* / fal and with requests that carry no\n // endpoint type, but NOT with multimedia endpoints (image, speech, video,\n // transcription, …). Gating consumption on this prevents an incompatible\n // request from matching the predicate and splicing — and thereby\n // destroying — a one-shot error intended for a different endpoint before\n // the router's own compat check would have skipped it.\n const errorEndpointCompatible = (req: import(\"./types.js\").ChatCompletionRequest) => {\n const reqEndpoint = req._endpointType as string | undefined;\n if (\n reqEndpoint === undefined ||\n reqEndpoint === \"chat\" ||\n reqEndpoint === \"embedding\" ||\n reqEndpoint.startsWith(\"realtime\") ||\n reqEndpoint === \"fal\"\n ) {\n return true;\n }\n return false;\n };\n const fixture: Fixture = {\n match: { predicate: errorEndpointCompatible },\n response: errorResponse,\n };\n // Insert at front so it matches before everything else\n this.fixtures.unshift(fixture);\n // Remove after first match — the journal records it so tests can assert.\n // Only consume (and splice) when the request endpoint is compatible; an\n // incompatible request returns false here, falls through to other\n // fixtures, and leaves this error pending for its intended endpoint.\n const original = fixture.match.predicate!;\n fixture.match.predicate = (req) => {\n const result = original(req);\n if (result) {\n // Remove synchronously on first match to prevent race conditions\n const idx = this.fixtures.indexOf(fixture);\n if (idx !== -1) this.fixtures.splice(idx, 1);\n }\n return result;\n };\n return this;\n }\n\n // ---- Mounts ----\n\n mount(path: string, handler: Mountable): this {\n this.mounts.push({ path, handler });\n\n // If server is already running, wire up journal, registry, and baseUrl immediately\n // so late mounts behave identically to pre-start mounts.\n if (this.serverInstance) {\n if (handler.setJournal) handler.setJournal(this.serverInstance.journal);\n if (handler.setBaseUrl) handler.setBaseUrl(this.serverInstance.url + path);\n const registry = this.serverInstance.defaults.registry;\n if (registry && handler.setRegistry) handler.setRegistry(registry);\n }\n\n return this;\n }\n\n // ---- Journal proxies ----\n\n getRequests(): import(\"./types.js\").JournalEntry[] {\n return this.journal.getAll();\n }\n\n getLastRequest(): import(\"./types.js\").JournalEntry | null {\n return this.journal.getLast();\n }\n\n clearRequests(): void {\n this.journal.clear();\n }\n\n resetMatchCounts(testId?: string): this {\n if (this.serverInstance) {\n this.serverInstance.journal.clearMatchCounts(testId);\n }\n return this;\n }\n\n // ---- Chaos ----\n\n setChaos(config: ChaosConfig): this {\n this.options.chaos = config;\n return this;\n }\n\n clearChaos(): this {\n delete this.options.chaos;\n return this;\n }\n\n // ---- Recording ----\n\n enableRecording(config: RecordConfig): this {\n this.options.record = config;\n return this;\n }\n\n disableRecording(): this {\n delete this.options.record;\n return this;\n }\n\n // ---- Reset ----\n\n /**\n * Full reset — the in-process equivalent of `POST /__aimock/reset`. Shares\n * one implementation with the control-API route so the two cannot drift.\n *\n * The one deliberate difference: search / rerank / moderation fixtures are\n * also cleared here. Those are registered through this class only — the\n * control API has no route that creates them, so the HTTP reset can neither\n * reach nor observe them.\n *\n * NOT ALL OF THIS IS PER-INSTANCE. `performFullReset` clears module-global\n * state as well: the Gemini interaction and event-id counters\n * (`resetInteractionCounter` / `resetEventIdCounter` in\n * `./gemini-interactions.js`) and the fal.ai job/queue maps (`falJobs`,\n * `falQueueStates`). With two `LLMock` instances live in one process,\n * `a.reset()` rewinds the Gemini id sequence that `b` is mid-way through —\n * `b` then re-emits `aimock-int-0` / `evt_1`, ids it has already handed\n * out — and drops `b`'s in-flight fal jobs. Give each instance its own\n * process (or its own vitest worker) if that matters.\n *\n * The global stores are cleared even before `start()`, when there is no\n * server instance to reset.\n */\n reset(): this {\n this.searchFixtures.length = 0;\n this.rerankFixtures.length = 0;\n this.moderationFixtures.length = 0;\n performFullReset(this.fixtures, this.serverInstance);\n return this;\n }\n\n // ---- Server lifecycle ----\n\n async start(): Promise<string> {\n if (this.serverInstance) {\n throw new Error(\"Server already started\");\n }\n this.serverInstance = await (this.resolvedInboundAuth\n ? createServerWithResolvedAuth(\n this.fixtures,\n this.options,\n this.resolvedInboundAuth,\n this.mounts,\n {\n search: this.searchFixtures,\n rerank: this.rerankFixtures,\n moderation: this.moderationFixtures,\n },\n )\n : createServer(this.fixtures, this.options, this.mounts, {\n search: this.searchFixtures,\n rerank: this.rerankFixtures,\n moderation: this.moderationFixtures,\n }));\n return this.serverInstance.url;\n }\n\n async stop(): Promise<void> {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n const { server } = this.serverInstance;\n await new Promise<void>((resolve, reject) => {\n server.close((err: Error | undefined) => (err ? reject(err) : resolve()));\n });\n this.serverInstance = null;\n }\n\n // ---- Accessors ----\n\n get journal(): Journal {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n return this.serverInstance.journal;\n }\n\n get url(): string {\n if (!this.serverInstance) {\n throw new Error(\"Server not started\");\n }\n return this.serverInstance.url;\n }\n\n get baseUrl(): string {\n return this.url;\n }\n\n get port(): number {\n const parsed = new URL(this.url); // this.url throws if not started\n if (!parsed.port) {\n throw new Error(`Server URL has no explicit port: ${this.url}`);\n }\n return parseInt(parsed.port, 10);\n }\n\n // ---- Static factory ----\n\n static async create(options?: MockServerOptions): Promise<LLMock> {\n const instance = new LLMock(options);\n await instance.start();\n return instance;\n }\n}\n\n/** @internal Configuration startup preserves a policy resolved from the selected source. */\nexport function createLLMockWithResolvedAuth(\n options: MockServerOptions,\n resolvedAuth: ResolvedInboundAuth,\n): LLMock {\n return new LLMock(options, resolvedAuth);\n}\n"],"mappings":"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"}
@@ -63,6 +63,41 @@ function toGenId(id) {
63
63
  return id.startsWith(OPENAI_ID_PREFIX) ? OPENROUTER_ID_PREFIX + id.slice(9) : id;
64
64
  }
65
65
  /**
66
+ * `usage` keys that are already accounted for elsewhere and must therefore NOT
67
+ * be re-emitted through the forward-compat passthrough below: the canonical
68
+ * token counts (and their per-provider aliases), which `resolveUsage` folds into
69
+ * `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage
70
+ * object, plus every field this function shapes explicitly.
71
+ */
72
+ const SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([
73
+ "prompt_tokens",
74
+ "completion_tokens",
75
+ "total_tokens",
76
+ "input_tokens",
77
+ "output_tokens",
78
+ "promptTokenCount",
79
+ "candidatesTokenCount",
80
+ "totalTokenCount",
81
+ "cost",
82
+ "cost_details",
83
+ "prompt_tokens_details",
84
+ "completion_tokens_details",
85
+ "is_byok"
86
+ ]);
87
+ /**
88
+ * Keys that must never flow through the passthrough assignment below: a
89
+ * JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream
90
+ * provider responses) can carry a real own `__proto__`/`constructor`/`prototype`
91
+ * key, and a plain `usageExtras[key] = value` for one of those hits the
92
+ * prototype setter, corrupting the emitted object's prototype chain instead of
93
+ * emitting a data field. Real providers never send these, so skipping is safe.
94
+ */
95
+ const UNSAFE_PROTO_KEYS = new Set([
96
+ "__proto__",
97
+ "constructor",
98
+ "prototype"
99
+ ]);
100
+ /**
66
101
  * Resolve the shaping context from a fixture's response overrides and the
67
102
  * winning model slug. `provider` defaults to the winning slug's author;
68
103
  * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and
@@ -95,6 +130,12 @@ function resolveOpenRouterShaping(overrides, winningModel) {
95
130
  reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0
96
131
  };
97
132
  if (u?.is_byok !== void 0) usageExtras.is_byok = u.is_byok;
133
+ for (const [key, value] of Object.entries(u ?? {})) {
134
+ if (value === void 0) continue;
135
+ if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;
136
+ if (UNSAFE_PROTO_KEYS.has(key)) continue;
137
+ usageExtras[key] = value;
138
+ }
98
139
  return {
99
140
  provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),
100
141
  ...overrides?.nativeFinishReason !== void 0 && { nativeFinishReason: overrides.nativeFinishReason },
@@ -1 +1 @@
1
- {"version":3,"file":"openrouter-chat.cjs","names":["flattenHeaders"],"sources":["../src/openrouter-chat.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type {\n ChatCompletion,\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n OpenRouterUsageExtras,\n ResponseOverrides,\n SSEChunk,\n} from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport { flattenHeaders } from \"./helpers.js\";\n\n/**\n * OpenRouter chat/LLM router shaping. aimock detects an OpenRouter request by\n * the ORIGINAL request path (before `normalizeCompatPath` erases it) starting\n * with the OpenRouter canonical base `/api/v1/` — mirroring a real client that\n * points an OpenAI SDK at `baseURL: https://openrouter.ai/api/v1`. When a\n * request is OpenRouter, the same OpenAI-compatible chat response is shaped\n * with OpenRouter's distinguishing fields (a `gen-` id prefix, a top-level\n * `provider`, `native_finish_reason`, and a rich cost-bearing `usage`) and the\n * `models[]` fallback array is simulated deterministically against fixtures.\n * OpenAI (`/v1/...`) callers are left byte-for-byte unchanged.\n *\n * The shaping is applied as a post-pass over the objects the existing OpenAI\n * builders produce (never a fork of those builders), so every OpenAI code path\n * is untouched.\n */\n\n/** OpenRouter's canonical API base — the detection prefix. */\nexport const OPENROUTER_BASE_PREFIX = \"/api/v1/\";\n\nconst OPENAI_ID_PREFIX = \"chatcmpl-\";\nconst OPENROUTER_ID_PREFIX = \"gen-\";\n\n/**\n * Return true when the ORIGINAL request path marks an OpenRouter request. Must\n * be evaluated on the pre-normalization pathname: `normalizeCompatPath`\n * rewrites `/api/v1/chat/completions` → `/v1/chat/completions`, erasing the\n * `/api/v1/` signal.\n */\nexport function isOpenRouterPath(originalPathname: string): boolean {\n return originalPathname.startsWith(OPENROUTER_BASE_PREFIX);\n}\n\n/**\n * Derive the default OpenRouter `provider` from a model slug: the author\n * segment before the first `/` (`openai/gpt-4o` → `\"openai\"`). A slug without\n * a `/` yields the whole slug. An absent/empty slug — a request that never set\n * `model` yet matched a model-less fixture — yields `\"\"` rather than throwing\n * (guards against a `TypeError` becoming a generic 500).\n */\nexport function deriveOpenRouterProvider(model: string | undefined): string {\n if (!model) return \"\";\n const slash = model.indexOf(\"/\");\n return slash === -1 ? model : model.slice(0, slash);\n}\n\n/**\n * Build the OpenRouter fallback candidate list `[body.model, ...body.models]`,\n * de-duplicated with insertion order preserved. A request without `models`\n * yields just `[body.model]` — identical to today's single-model behavior.\n */\nexport function buildOpenRouterCandidates(body: ChatCompletionRequest): string[] {\n const raw = [body.model, ...(Array.isArray(body.models) ? body.models : [])];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const m of raw) {\n if (typeof m !== \"string\" || m.length === 0) continue;\n if (seen.has(m)) continue;\n seen.add(m);\n out.push(m);\n }\n return out;\n}\n\n/** Rewrite an auto-generated `chatcmpl-` id to OpenRouter's `gen-` prefix. */\nfunction toGenId(id: string): string {\n return id.startsWith(OPENAI_ID_PREFIX)\n ? OPENROUTER_ID_PREFIX + id.slice(OPENAI_ID_PREFIX.length)\n : id;\n}\n\n/**\n * The resolved OpenRouter shaping context for one response. `provider` and the\n * always-present `cost`/`cost_details` are computed here; the detail\n * breakdowns and `is_byok` are carried only when a fixture overrode them.\n */\nexport interface OpenRouterShaping {\n provider: string;\n nativeFinishReason?: string;\n usageExtras: OpenRouterUsageExtras;\n}\n\n/**\n * Resolve the shaping context from a fixture's response overrides and the\n * winning model slug. `provider` defaults to the winning slug's author;\n * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and\n * `cost_details` are NULLABLE — emitted only when the fixture supplies a `cost`\n * (never defaulted to 0); when emitted, `cost_details` carries all three\n * upstream fields (a partial object breaks the canonical SDK). The token-detail\n * breakdowns and `is_byok` are included only when the fixture set them, and an\n * emitted breakdown always carries its required member (`cached_tokens` /\n * `reasoning_tokens`).\n */\nexport function resolveOpenRouterShaping(\n overrides: ResponseOverrides | undefined,\n winningModel: string | undefined,\n): OpenRouterShaping {\n const u = overrides?.usage;\n const usageExtras: OpenRouterUsageExtras = {};\n if (u?.cost !== undefined) {\n usageExtras.cost = u.cost;\n const cd = u.cost_details;\n usageExtras.cost_details = {\n ...(cd ?? {}),\n upstream_inference_cost: cd?.upstream_inference_cost ?? 0,\n upstream_inference_prompt_cost: cd?.upstream_inference_prompt_cost ?? 0,\n upstream_inference_completions_cost: cd?.upstream_inference_completions_cost ?? 0,\n };\n }\n if (u?.prompt_tokens_details !== undefined) {\n usageExtras.prompt_tokens_details = {\n ...u.prompt_tokens_details,\n cached_tokens: u.prompt_tokens_details.cached_tokens ?? 0,\n };\n }\n if (u?.completion_tokens_details !== undefined) {\n usageExtras.completion_tokens_details = {\n ...u.completion_tokens_details,\n reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0,\n };\n }\n if (u?.is_byok !== undefined) {\n usageExtras.is_byok = u.is_byok;\n }\n return {\n provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),\n ...(overrides?.nativeFinishReason !== undefined && {\n nativeFinishReason: overrides.nativeFinishReason,\n }),\n usageExtras,\n };\n}\n\n/** Merge the OpenRouter usage extras onto an existing (OpenAI-shape) usage object. */\nfunction augmentUsage<T extends object>(\n usage: T,\n shaping: OpenRouterShaping,\n): T & OpenRouterUsageExtras {\n return { ...usage, ...shaping.usageExtras };\n}\n\n/** Resolve a choice's native_finish_reason (mirrors finish_reason unless overridden). */\nfunction nativeFinish(finishReason: string | null, shaping: OpenRouterShaping): string | null {\n if (finishReason == null) return null;\n return shaping.nativeFinishReason ?? finishReason;\n}\n\n/**\n * Shape a non-streaming OpenAI completion into OpenRouter form, in place, to\n * match real OpenRouter bytes (or-capture/chat-nonstream.json): `gen-` id\n * (unless the fixture pinned `id`), top-level `provider`, `service_tier: null`,\n * an always-present `system_fingerprint` (null when unset — the canonical\n * `@openrouter/sdk` requires it), per-choice `native_finish_reason`, a\n * `message.reasoning` field (mirrors `reasoning_content`, else null), and\n * cost-bearing `usage`.\n */\nexport function shapeOpenRouterCompletion(\n completion: ChatCompletion,\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): ChatCompletion {\n if (!idOverridden) completion.id = toGenId(completion.id);\n completion.provider = shaping.provider;\n if (completion.system_fingerprint === undefined) completion.system_fingerprint = null;\n if (completion.service_tier === undefined) completion.service_tier = null;\n for (const choice of completion.choices) {\n // Non-streaming finish_reason is always a string, so native mirrors it.\n choice.native_finish_reason = shaping.nativeFinishReason ?? choice.finish_reason;\n if (choice.message.reasoning === undefined) {\n choice.message.reasoning = choice.message.reasoning_content ?? null;\n }\n }\n completion.usage = augmentUsage(completion.usage, shaping);\n return completion;\n}\n\n/**\n * Shape streaming OpenAI chunks into OpenRouter form, in place, to match real\n * OpenRouter bytes (or-capture/chat-stream.sse): EVERY chunk carries the\n * `gen-` id (unless pinned), top-level `provider`, and `system_fingerprint`\n * (null when unset); EVERY delta choice carries `native_finish_reason` (null\n * until the finish chunk, where it mirrors finish_reason); real OR repeats\n * `role: \"assistant\"` on every content delta (mirrored here); and the final\n * usage-bearing chunk gets cost-bearing usage plus `service_tier: null`.\n */\nexport function shapeOpenRouterChunks(\n chunks: SSEChunk[],\n shaping: OpenRouterShaping,\n idOverridden: boolean,\n): SSEChunk[] {\n for (const chunk of chunks) {\n if (!idOverridden) chunk.id = toGenId(chunk.id);\n chunk.provider = shaping.provider;\n if (chunk.system_fingerprint === undefined) chunk.system_fingerprint = null;\n for (const choice of chunk.choices) {\n choice.native_finish_reason = nativeFinish(choice.finish_reason, shaping);\n // Real OR stamps role:\"assistant\" on every content-bearing delta. A\n // `content: null` delta is NOT content-bearing, so require string content.\n if (typeof choice.delta.content === \"string\" && choice.delta.role === undefined) {\n choice.delta.role = \"assistant\";\n }\n }\n if (chunk.usage) {\n chunk.usage = augmentUsage(chunk.usage, shaping);\n chunk.service_tier = null;\n }\n }\n return chunks;\n}\n\n/**\n * Serialize an OpenRouter error envelope: `{ error: { code, message,\n * metadata? } }` where `code` equals the HTTP status (contrast the OpenAI\n * shape `{ error: { message, type, param, code } }`). Used for every error\n * emitted on an OpenRouter request — fixture errors, no-match, strict, and\n * routing failures.\n */\nexport function serializeOpenRouterError(\n status: number,\n message: string,\n metadata?: Record<string, unknown>,\n): string {\n return JSON.stringify({\n error: {\n code: status,\n message,\n ...(metadata !== undefined && { metadata }),\n },\n });\n}\n\n// ─── Discovery endpoints ─────────────────────────────────────────────────────\n\n/** Default model slug set for `GET /api/v1/models` when no chat fixtures name one. */\nconst DEFAULT_OPENROUTER_MODELS = [\n \"openai/gpt-4o\",\n \"anthropic/claude-3.5-sonnet\",\n \"google/gemini-2.0-flash-001\",\n];\n\n/**\n * Build one OpenRouter model-catalog object (deterministic static filler).\n * Field set mirrors real OpenRouter bytes (or-capture/models-first.json):\n * pricing values are STRINGS; the object also carries hugging_face_id,\n * knowledge_cutoff, supported_voices, links, default_parameters,\n * expiration_date, and a `reasoning` descriptor (SDKs tolerate extras).\n */\nfunction openRouterModelObject(id: string): Record<string, unknown> {\n return {\n id,\n canonical_slug: id,\n hugging_face_id: null,\n name: id,\n created: 0,\n description: `Mock OpenRouter model ${id} served by aimock.`,\n context_length: 128000,\n architecture: {\n modality: \"text->text\",\n input_modalities: [\"text\"],\n output_modalities: [\"text\"],\n tokenizer: \"Other\",\n instruct_type: null,\n },\n pricing: {\n prompt: \"0\",\n completion: \"0\",\n request: \"0\",\n image: \"0\",\n web_search: \"0\",\n internal_reasoning: \"0\",\n input_cache_read: \"0\",\n input_cache_write: \"0\",\n },\n top_provider: {\n context_length: 128000,\n max_completion_tokens: 16384,\n is_moderated: false,\n },\n per_request_limits: null,\n supported_parameters: [\n \"tools\",\n \"tool_choice\",\n \"max_tokens\",\n \"temperature\",\n \"top_p\",\n \"stop\",\n \"reasoning\",\n ],\n default_parameters: null,\n supported_voices: null,\n knowledge_cutoff: null,\n expiration_date: null,\n links: { details: `/api/v1/models/${id}/endpoints` },\n reasoning: { mandatory: false, default_enabled: false },\n };\n}\n\n/**\n * Collect the model ids advertised by `GET /api/v1/models`: every loaded chat\n * (or endpoint-less) fixture with a string `match.model`, in first-seen order;\n * the default set when none contribute one. Mirrors the Ollama `/api/tags` and\n * OpenRouter video `/models` synthesis idioms.\n */\nfunction collectModelIds(fixtures: Fixture[]): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const f of fixtures) {\n if (f.match.endpoint !== undefined && f.match.endpoint !== \"chat\") continue;\n const m = f.match.model;\n if (typeof m === \"string\" && m.length > 0 && !seen.has(m)) {\n seen.add(m);\n ids.push(m);\n }\n }\n return ids.length > 0 ? ids : DEFAULT_OPENROUTER_MODELS;\n}\n\nfunction journalGet(\n req: http.IncomingMessage,\n journal: Journal,\n path: string,\n status: number,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n}\n\n/** GET /api/v1/models — the OpenRouter model catalog synthesized from fixtures. */\nexport function handleOpenRouterModels(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n fixtures: Fixture[],\n journal: Journal,\n _defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/models\";\n const data = collectModelIds(fixtures).map(openRouterModelObject);\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data }));\n}\n\n/** GET /api/v1/key — the caller's key metadata/limits/usage (static defaults). */\nexport function handleOpenRouterKey(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/key\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n // Field set + order mirror real OpenRouter bytes (or-capture/key.json).\n // null limit / limit_remaining = unlimited. `creator_user_id` is an account\n // echo — null for the mock (no account). rate_limit is deprecated upstream.\n res.end(\n JSON.stringify({\n data: {\n label: \"sk-or-v1-aimock\",\n is_management_key: false,\n is_provisioning_key: false,\n limit: null,\n limit_reset: null,\n limit_remaining: null,\n include_byok_in_limit: false,\n usage: 0,\n usage_daily: 0,\n usage_weekly: 0,\n usage_monthly: 0,\n byok_usage: 0,\n byok_usage_daily: 0,\n byok_usage_weekly: 0,\n byok_usage_monthly: 0,\n is_free_tier: false,\n expires_at: null,\n creator_user_id: null,\n rate_limit: {\n requests: -1,\n interval: \"10s\",\n note: \"This field is deprecated and safe to ignore.\",\n },\n },\n }),\n );\n}\n\n/** GET /api/v1/credits — account credit totals (static defaults). */\nexport function handleOpenRouterCredits(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? \"/api/v1/credits\";\n journalGet(req, journal, path, 200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ data: { total_credits: 0, total_usage: 0 } }));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8BA,MAAa,yBAAyB;AAEtC,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;;;;;;;AAQ7B,SAAgB,iBAAiB,kBAAmC;AAClE,QAAO,iBAAiB,WAAW,uBAAuB;;;;;;;;;AAU5D,SAAgB,yBAAyB,OAAmC;AAC1E,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,QAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,GAAG,MAAM;;;;;;;AAQrD,SAAgB,0BAA0B,MAAuC;CAC/E,MAAM,MAAM,CAAC,KAAK,OAAO,GAAI,MAAM,QAAQ,KAAK,OAAO,GAAG,KAAK,SAAS,EAAE,CAAE;CAC5E,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,MAAgB,EAAE;AACxB,MAAK,MAAM,KAAK,KAAK;AACnB,MAAI,OAAO,MAAM,YAAY,EAAE,WAAW,EAAG;AAC7C,MAAI,KAAK,IAAI,EAAE,CAAE;AACjB,OAAK,IAAI,EAAE;AACX,MAAI,KAAK,EAAE;;AAEb,QAAO;;;AAIT,SAAS,QAAQ,IAAoB;AACnC,QAAO,GAAG,WAAW,iBAAiB,GAClC,uBAAuB,GAAG,MAAM,EAAwB,GACxD;;;;;;;;;;;;;AAyBN,SAAgB,yBACd,WACA,cACmB;CACnB,MAAM,IAAI,WAAW;CACrB,MAAM,cAAqC,EAAE;AAC7C,KAAI,GAAG,SAAS,QAAW;AACzB,cAAY,OAAO,EAAE;EACrB,MAAM,KAAK,EAAE;AACb,cAAY,eAAe;GACzB,GAAI,MAAM,EAAE;GACZ,yBAAyB,IAAI,2BAA2B;GACxD,gCAAgC,IAAI,kCAAkC;GACtE,qCAAqC,IAAI,uCAAuC;GACjF;;AAEH,KAAI,GAAG,0BAA0B,OAC/B,aAAY,wBAAwB;EAClC,GAAG,EAAE;EACL,eAAe,EAAE,sBAAsB,iBAAiB;EACzD;AAEH,KAAI,GAAG,8BAA8B,OACnC,aAAY,4BAA4B;EACtC,GAAG,EAAE;EACL,kBAAkB,EAAE,0BAA0B,oBAAoB;EACnE;AAEH,KAAI,GAAG,YAAY,OACjB,aAAY,UAAU,EAAE;AAE1B,QAAO;EACL,UAAU,WAAW,YAAY,yBAAyB,aAAa;EACvE,GAAI,WAAW,uBAAuB,UAAa,EACjD,oBAAoB,UAAU,oBAC/B;EACD;EACD;;;AAIH,SAAS,aACP,OACA,SAC2B;AAC3B,QAAO;EAAE,GAAG;EAAO,GAAG,QAAQ;EAAa;;;AAI7C,SAAS,aAAa,cAA6B,SAA2C;AAC5F,KAAI,gBAAgB,KAAM,QAAO;AACjC,QAAO,QAAQ,sBAAsB;;;;;;;;;;;AAYvC,SAAgB,0BACd,YACA,SACA,cACgB;AAChB,KAAI,CAAC,aAAc,YAAW,KAAK,QAAQ,WAAW,GAAG;AACzD,YAAW,WAAW,QAAQ;AAC9B,KAAI,WAAW,uBAAuB,OAAW,YAAW,qBAAqB;AACjF,KAAI,WAAW,iBAAiB,OAAW,YAAW,eAAe;AACrE,MAAK,MAAM,UAAU,WAAW,SAAS;AAEvC,SAAO,uBAAuB,QAAQ,sBAAsB,OAAO;AACnE,MAAI,OAAO,QAAQ,cAAc,OAC/B,QAAO,QAAQ,YAAY,OAAO,QAAQ,qBAAqB;;AAGnE,YAAW,QAAQ,aAAa,WAAW,OAAO,QAAQ;AAC1D,QAAO;;;;;;;;;;;AAYT,SAAgB,sBACd,QACA,SACA,cACY;AACZ,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,CAAC,aAAc,OAAM,KAAK,QAAQ,MAAM,GAAG;AAC/C,QAAM,WAAW,QAAQ;AACzB,MAAI,MAAM,uBAAuB,OAAW,OAAM,qBAAqB;AACvE,OAAK,MAAM,UAAU,MAAM,SAAS;AAClC,UAAO,uBAAuB,aAAa,OAAO,eAAe,QAAQ;AAGzE,OAAI,OAAO,OAAO,MAAM,YAAY,YAAY,OAAO,MAAM,SAAS,OACpE,QAAO,MAAM,OAAO;;AAGxB,MAAI,MAAM,OAAO;AACf,SAAM,QAAQ,aAAa,MAAM,OAAO,QAAQ;AAChD,SAAM,eAAe;;;AAGzB,QAAO;;;;;;;;;AAUT,SAAgB,yBACd,QACA,SACA,UACQ;AACR,QAAO,KAAK,UAAU,EACpB,OAAO;EACL,MAAM;EACN;EACA,GAAI,aAAa,UAAa,EAAE,UAAU;EAC3C,EACF,CAAC;;;AAMJ,MAAM,4BAA4B;CAChC;CACA;CACA;CACD;;;;;;;;AASD,SAAS,sBAAsB,IAAqC;AAClE,QAAO;EACL;EACA,gBAAgB;EAChB,iBAAiB;EACjB,MAAM;EACN,SAAS;EACT,aAAa,yBAAyB,GAAG;EACzC,gBAAgB;EAChB,cAAc;GACZ,UAAU;GACV,kBAAkB,CAAC,OAAO;GAC1B,mBAAmB,CAAC,OAAO;GAC3B,WAAW;GACX,eAAe;GAChB;EACD,SAAS;GACP,QAAQ;GACR,YAAY;GACZ,SAAS;GACT,OAAO;GACP,YAAY;GACZ,oBAAoB;GACpB,kBAAkB;GAClB,mBAAmB;GACpB;EACD,cAAc;GACZ,gBAAgB;GAChB,uBAAuB;GACvB,cAAc;GACf;EACD,oBAAoB;EACpB,sBAAsB;GACpB;GACA;GACA;GACA;GACA;GACA;GACA;GACD;EACD,oBAAoB;EACpB,kBAAkB;EAClB,kBAAkB;EAClB,iBAAiB;EACjB,OAAO,EAAE,SAAS,kBAAkB,GAAG,aAAa;EACpD,WAAW;GAAE,WAAW;GAAO,iBAAiB;GAAO;EACxD;;;;;;;;AASH,SAAS,gBAAgB,UAA+B;CACtD,MAAM,MAAgB,EAAE;CACxB,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,KAAK,UAAU;AACxB,MAAI,EAAE,MAAM,aAAa,UAAa,EAAE,MAAM,aAAa,OAAQ;EACnE,MAAM,IAAI,EAAE,MAAM;AAClB,MAAI,OAAO,MAAM,YAAY,EAAE,SAAS,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE;AACzD,QAAK,IAAI,EAAE;AACX,OAAI,KAAK,EAAE;;;AAGf,QAAO,IAAI,SAAS,IAAI,MAAM;;AAGhC,SAAS,WACP,KACA,SACA,MACA,QACM;AACN,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB;EACA,SAASA,+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,OAAO,gBAAgB,SAAS,CAAC,IAAI,sBAAsB;AACjE,YAAW,KAAK,SAAS,MAAM,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,EAAE,MAAM,CAAC,CAAC;;;AAInC,SAAgB,oBACd,KACA,KACA,SACA,gBACM;AACN,gBAAe,IAAI;AAEnB,YAAW,KAAK,SADH,IAAI,OAAO,eACO,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAI1D,KAAI,IACF,KAAK,UAAU,EACb,MAAM;EACJ,OAAO;EACP,mBAAmB;EACnB,qBAAqB;EACrB,OAAO;EACP,aAAa;EACb,iBAAiB;EACjB,uBAAuB;EACvB,OAAO;EACP,aAAa;EACb,cAAc;EACd,eAAe;EACf,YAAY;EACZ,kBAAkB;EAClB,mBAAmB;EACnB,oBAAoB;EACpB,cAAc;EACd,YAAY;EACZ,iBAAiB;EACjB,YAAY;GACV,UAAU;GACV,UAAU;GACV,MAAM;GACP;EACF,EACF,CAAC,CACH;;;AAIH,SAAgB,wBACd,KACA,KACA,SACA,gBACM;AACN,gBAAe,IAAI;AAEnB,YAAW,KAAK,SADH,IAAI,OAAO,mBACO,IAAI;AACnC,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,EAAE,MAAM;EAAE,eAAe;EAAG,aAAa;EAAG,EAAE,CAAC,CAAC"}
1
+ {"version":3,"file":"openrouter-chat.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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"}
@@ -63,6 +63,41 @@ function toGenId(id) {
63
63
  return id.startsWith(OPENAI_ID_PREFIX) ? OPENROUTER_ID_PREFIX + id.slice(9) : id;
64
64
  }
65
65
  /**
66
+ * `usage` keys that are already accounted for elsewhere and must therefore NOT
67
+ * be re-emitted through the forward-compat passthrough below: the canonical
68
+ * token counts (and their per-provider aliases), which `resolveUsage` folds into
69
+ * `prompt_tokens` / `completion_tokens` / `total_tokens` on the base usage
70
+ * object, plus every field this function shapes explicitly.
71
+ */
72
+ const SHAPED_OR_CANONICAL_USAGE_KEYS = new Set([
73
+ "prompt_tokens",
74
+ "completion_tokens",
75
+ "total_tokens",
76
+ "input_tokens",
77
+ "output_tokens",
78
+ "promptTokenCount",
79
+ "candidatesTokenCount",
80
+ "totalTokenCount",
81
+ "cost",
82
+ "cost_details",
83
+ "prompt_tokens_details",
84
+ "completion_tokens_details",
85
+ "is_byok"
86
+ ]);
87
+ /**
88
+ * Keys that must never flow through the passthrough assignment below: a
89
+ * JSON-parsed usage frame (fixtures are attacker-adjacent — they mirror upstream
90
+ * provider responses) can carry a real own `__proto__`/`constructor`/`prototype`
91
+ * key, and a plain `usageExtras[key] = value` for one of those hits the
92
+ * prototype setter, corrupting the emitted object's prototype chain instead of
93
+ * emitting a data field. Real providers never send these, so skipping is safe.
94
+ */
95
+ const UNSAFE_PROTO_KEYS = new Set([
96
+ "__proto__",
97
+ "constructor",
98
+ "prototype"
99
+ ]);
100
+ /**
66
101
  * Resolve the shaping context from a fixture's response overrides and the
67
102
  * winning model slug. `provider` defaults to the winning slug's author;
68
103
  * `native_finish_reason` mirrors `finish_reason` unless overridden. `cost` and
@@ -95,6 +130,12 @@ function resolveOpenRouterShaping(overrides, winningModel) {
95
130
  reasoning_tokens: u.completion_tokens_details.reasoning_tokens ?? 0
96
131
  };
97
132
  if (u?.is_byok !== void 0) usageExtras.is_byok = u.is_byok;
133
+ for (const [key, value] of Object.entries(u ?? {})) {
134
+ if (value === void 0) continue;
135
+ if (SHAPED_OR_CANONICAL_USAGE_KEYS.has(key)) continue;
136
+ if (UNSAFE_PROTO_KEYS.has(key)) continue;
137
+ usageExtras[key] = value;
138
+ }
98
139
  return {
99
140
  provider: overrides?.provider ?? deriveOpenRouterProvider(winningModel),
100
141
  ...overrides?.nativeFinishReason !== void 0 && { nativeFinishReason: overrides.nativeFinishReason },