@copilotkit/aimock 1.37.4 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +48 -0
- package/README.md +50 -11
- package/dist/agui-handler.cjs +20 -18
- package/dist/agui-handler.cjs.map +1 -1
- package/dist/agui-handler.d.cts +8 -1
- package/dist/agui-handler.d.cts.map +1 -1
- package/dist/agui-handler.d.ts +8 -1
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-handler.js +20 -18
- package/dist/agui-handler.js.map +1 -1
- package/dist/agui-recorder.cjs +6 -0
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +6 -0
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-stub.d.cts +2 -2
- package/dist/agui-stub.d.ts +2 -2
- package/dist/agui-types.d.cts +19 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +19 -1
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +1 -1
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.js +1 -1
- package/dist/aimock-cli.js.map +1 -1
- package/dist/api-key-auth.cjs +166 -0
- package/dist/api-key-auth.cjs.map +1 -0
- package/dist/api-key-auth.d.cts +17 -0
- package/dist/api-key-auth.d.cts.map +1 -0
- package/dist/api-key-auth.d.ts +17 -0
- package/dist/api-key-auth.d.ts.map +1 -0
- package/dist/api-key-auth.js +158 -0
- package/dist/api-key-auth.js.map +1 -0
- package/dist/cli.cjs +14 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +14 -11
- package/dist/cli.js.map +1 -1
- package/dist/config-loader.cjs +4 -2
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +2 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +2 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +5 -3
- package/dist/config-loader.js.map +1 -1
- package/dist/fal-audio.cjs +2 -0
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +2 -0
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +4 -1
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +4 -1
- package/dist/fal.js.map +1 -1
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +3 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +3 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/grok-video.cjs +9 -2
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.js +10 -3
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +16 -22
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +16 -21
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/llmock.cjs +37 -12
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +26 -2
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +26 -2
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +39 -15
- package/dist/llmock.js.map +1 -1
- package/dist/openrouter-chat.cjs +333 -0
- package/dist/openrouter-chat.cjs.map +1 -0
- package/dist/openrouter-chat.js +325 -0
- package/dist/openrouter-chat.js.map +1 -0
- package/dist/openrouter-video.cjs +24 -7
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +25 -8
- package/dist/openrouter-video.js.map +1 -1
- package/dist/provider-auth.cjs +20 -0
- package/dist/provider-auth.cjs.map +1 -1
- package/dist/provider-auth.js +20 -1
- package/dist/provider-auth.js.map +1 -1
- package/dist/recorder.cjs +50 -8
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +50 -10
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +200 -89
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +18 -3
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +18 -3
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +199 -90
- package/dist/responses.js.map +1 -1
- package/dist/server.cjs +190 -83
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +7 -2
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +7 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +190 -85
- package/dist/server.js.map +1 -1
- package/dist/sse-writer.cjs +3 -0
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts +8 -0
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +8 -0
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +3 -0
- package/dist/sse-writer.js.map +1 -1
- package/dist/stream-collapse.cjs +103 -0
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +7 -0
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +7 -0
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +103 -0
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +49 -2
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +50 -3
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +187 -5
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +187 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/veo-video.cjs +9 -2
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +10 -3
- package/dist/veo-video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +41 -6
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.js +41 -6
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +306 -57
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +1 -0
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +1 -0
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +307 -58
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +11 -6
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +12 -7
- package/dist/ws-responses.js.map +1 -1
- package/package.json +3 -2
- package/skills/write-fixtures/SKILL.md +53 -19
package/dist/sse-writer.cjs
CHANGED
|
@@ -10,6 +10,8 @@ function delay(ms, signal) {
|
|
|
10
10
|
}, { once: true });
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
|
+
/** OpenRouter's SSE keepalive comment line (colon-prefixed, per SSE syntax). */
|
|
14
|
+
const OPENROUTER_PROCESSING_COMMENT = ": OPENROUTER PROCESSING\n\n";
|
|
13
15
|
function calculateDelay(chunkIndex, profile, fallbackLatency, recordedTimings, replaySpeed) {
|
|
14
16
|
const speed = replaySpeed ?? 1;
|
|
15
17
|
let delayMs;
|
|
@@ -49,6 +51,7 @@ async function writeSSEStream(res, chunks, optionsOrLatency) {
|
|
|
49
51
|
res.setHeader("Content-Type", "text/event-stream");
|
|
50
52
|
res.setHeader("Cache-Control", "no-cache");
|
|
51
53
|
res.setHeader("Connection", "keep-alive");
|
|
54
|
+
if (opts.openRouterProcessing && !res.writableEnded) res.write(OPENROUTER_PROCESSING_COMMENT);
|
|
52
55
|
let chunkIndex = 0;
|
|
53
56
|
for (const chunk of chunks) {
|
|
54
57
|
const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);
|
package/dist/sse-writer.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-writer.cjs","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n}\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":";;AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD
|
|
1
|
+
{"version":3,"file":"sse-writer.cjs","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n /**\n * When true, a single OpenRouter `: OPENROUTER PROCESSING` SSE comment\n * (keepalive) line is emitted FIRST — before the first data chunk — matching\n * real OpenRouter bytes (or-capture/chat-stream.sse). A faithful client skips\n * `:`-prefixed lines; opt-in, default off (so strict OpenAI parsers aren't\n * surprised). Used only by the OpenRouter chat surface.\n */\n openRouterProcessing?: boolean;\n}\n\n/** OpenRouter's SSE keepalive comment line (colon-prefixed, per SSE syntax). */\nconst OPENROUTER_PROCESSING_COMMENT = \": OPENROUTER PROCESSING\\n\\n\";\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n // OpenRouter emits exactly ONE `: OPENROUTER PROCESSING` keepalive comment\n // first — before any data frame (opt-in; see or-capture/chat-stream.sse). A\n // faithful client discards `:`-prefixed lines before JSON-parsing a `data:`.\n if (opts.openRouterProcessing && !res.writableEnded) {\n res.write(OPENROUTER_PROCESSING_COMMENT);\n }\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":"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"}
|
package/dist/sse-writer.d.cts
CHANGED
|
@@ -12,6 +12,14 @@ interface StreamOptions {
|
|
|
12
12
|
onChunkSent?: () => void;
|
|
13
13
|
/** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */
|
|
14
14
|
usageChunk?: SSEChunk;
|
|
15
|
+
/**
|
|
16
|
+
* When true, a single OpenRouter `: OPENROUTER PROCESSING` SSE comment
|
|
17
|
+
* (keepalive) line is emitted FIRST — before the first data chunk — matching
|
|
18
|
+
* real OpenRouter bytes (or-capture/chat-stream.sse). A faithful client skips
|
|
19
|
+
* `:`-prefixed lines; opt-in, default off (so strict OpenAI parsers aren't
|
|
20
|
+
* surprised). Used only by the OpenRouter chat surface.
|
|
21
|
+
*/
|
|
22
|
+
openRouterProcessing?: boolean;
|
|
15
23
|
}
|
|
16
24
|
declare function calculateDelay(chunkIndex: number, profile?: StreamingProfile, fallbackLatency?: number, recordedTimings?: RecordedTimings, replaySpeed?: number): number;
|
|
17
25
|
declare function writeSSEStream(res: http$1.ServerResponse, chunks: SSEChunk[], optionsOrLatency?: number | StreamOptions): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-writer.d.cts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;
|
|
1
|
+
{"version":3,"file":"sse-writer.d.cts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;;;;AAoBrB;;;sBAIoB,CAAA,EAAA,OAAA;;AA+CE,iBAnDN,cAAA,CAmDoB,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAjDxB,gBAiDwB,EAAA,eAAA,CAAA,EAAA,MAAA,EAAA,eAAA,CAAA,EA/ChB,eA+CgB,EAAA,WAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAAA,iBAAd,cAAA,CAAc,GAAA,EAC7B,MAAA,CAAK,cADwB,EAAA,MAAA,EAE1B,QAF0B,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAGN,aAHM,CAAA,EAIjC,OAJiC,CAAA,OAAA,CAAA;AAC7B,UA6DU,oBAAA,CA7DL;;YAEkB,CAAA,EAAA,MAAA;;AACpB,iBA+DM,kBAAA,CA/DN,GAAA,EAgEH,MAAA,CAAK,cAhEF,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAmEE,oBAnEF,CAAA,EAAA,IAAA;AA0DV"}
|
package/dist/sse-writer.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ interface StreamOptions {
|
|
|
12
12
|
onChunkSent?: () => void;
|
|
13
13
|
/** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */
|
|
14
14
|
usageChunk?: SSEChunk;
|
|
15
|
+
/**
|
|
16
|
+
* When true, a single OpenRouter `: OPENROUTER PROCESSING` SSE comment
|
|
17
|
+
* (keepalive) line is emitted FIRST — before the first data chunk — matching
|
|
18
|
+
* real OpenRouter bytes (or-capture/chat-stream.sse). A faithful client skips
|
|
19
|
+
* `:`-prefixed lines; opt-in, default off (so strict OpenAI parsers aren't
|
|
20
|
+
* surprised). Used only by the OpenRouter chat surface.
|
|
21
|
+
*/
|
|
22
|
+
openRouterProcessing?: boolean;
|
|
15
23
|
}
|
|
16
24
|
declare function calculateDelay(chunkIndex: number, profile?: StreamingProfile, fallbackLatency?: number, recordedTimings?: RecordedTimings, replaySpeed?: number): number;
|
|
17
25
|
declare function writeSSEStream(res: http$1.ServerResponse, chunks: SSEChunk[], optionsOrLatency?: number | StreamOptions): Promise<boolean>;
|
package/dist/sse-writer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-writer.d.ts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;
|
|
1
|
+
{"version":3,"file":"sse-writer.d.ts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;;;;AAoBrB;;;sBAIoB,CAAA,EAAA,OAAA;;AA+CE,iBAnDN,cAAA,CAmDoB,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAjDxB,gBAiDwB,EAAA,eAAA,CAAA,EAAA,MAAA,EAAA,eAAA,CAAA,EA/ChB,eA+CgB,EAAA,WAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAAA,iBAAd,cAAA,CAAc,GAAA,EAC7B,MAAA,CAAK,cADwB,EAAA,MAAA,EAE1B,QAF0B,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAGN,aAHM,CAAA,EAIjC,OAJiC,CAAA,OAAA,CAAA;AAC7B,UA6DU,oBAAA,CA7DL;;YAEkB,CAAA,EAAA,MAAA;;AACpB,iBA+DM,kBAAA,CA/DN,GAAA,EAgEH,MAAA,CAAK,cAhEF,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAmEE,oBAnEF,CAAA,EAAA,IAAA;AA0DV"}
|
package/dist/sse-writer.js
CHANGED
|
@@ -9,6 +9,8 @@ function delay(ms, signal) {
|
|
|
9
9
|
}, { once: true });
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
+
/** OpenRouter's SSE keepalive comment line (colon-prefixed, per SSE syntax). */
|
|
13
|
+
const OPENROUTER_PROCESSING_COMMENT = ": OPENROUTER PROCESSING\n\n";
|
|
12
14
|
function calculateDelay(chunkIndex, profile, fallbackLatency, recordedTimings, replaySpeed) {
|
|
13
15
|
const speed = replaySpeed ?? 1;
|
|
14
16
|
let delayMs;
|
|
@@ -48,6 +50,7 @@ async function writeSSEStream(res, chunks, optionsOrLatency) {
|
|
|
48
50
|
res.setHeader("Content-Type", "text/event-stream");
|
|
49
51
|
res.setHeader("Cache-Control", "no-cache");
|
|
50
52
|
res.setHeader("Connection", "keep-alive");
|
|
53
|
+
if (opts.openRouterProcessing && !res.writableEnded) res.write(OPENROUTER_PROCESSING_COMMENT);
|
|
51
54
|
let chunkIndex = 0;
|
|
52
55
|
for (const chunk of chunks) {
|
|
53
56
|
const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);
|
package/dist/sse-writer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-writer.js","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n}\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":";AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD
|
|
1
|
+
{"version":3,"file":"sse-writer.js","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n /**\n * When true, a single OpenRouter `: OPENROUTER PROCESSING` SSE comment\n * (keepalive) line is emitted FIRST — before the first data chunk — matching\n * real OpenRouter bytes (or-capture/chat-stream.sse). A faithful client skips\n * `:`-prefixed lines; opt-in, default off (so strict OpenAI parsers aren't\n * surprised). Used only by the OpenRouter chat surface.\n */\n openRouterProcessing?: boolean;\n}\n\n/** OpenRouter's SSE keepalive comment line (colon-prefixed, per SSE syntax). */\nconst OPENROUTER_PROCESSING_COMMENT = \": OPENROUTER PROCESSING\\n\\n\";\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n // OpenRouter emits exactly ONE `: OPENROUTER PROCESSING` keepalive comment\n // first — before any data frame (opt-in; see or-capture/chat-stream.sse). A\n // faithful client discards `:`-prefixed lines before JSON-parsing a `data:`.\n if (opts.openRouterProcessing && !res.writableEnded) {\n res.write(OPENROUTER_PROCESSING_COMMENT);\n }\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":"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"}
|
package/dist/stream-collapse.cjs
CHANGED
|
@@ -239,6 +239,10 @@ function collapseOpenAISSE(rawBody) {
|
|
|
239
239
|
const inputTruncated = isCollapseInputTruncated(rawBody);
|
|
240
240
|
const lines = splitSSEEvents(guardCollapseBody(rawBody));
|
|
241
241
|
let content = "";
|
|
242
|
+
let transcript = "";
|
|
243
|
+
let transcriptSeen = false;
|
|
244
|
+
let transcriptLanguages;
|
|
245
|
+
let transcriptUsage;
|
|
242
246
|
let reasoning = "";
|
|
243
247
|
const webSearchQueries = [];
|
|
244
248
|
let droppedChunks = 0;
|
|
@@ -262,6 +266,18 @@ function collapseOpenAISSE(rawBody) {
|
|
|
262
266
|
if (droppedChunks === 1) firstDroppedSample = `parse failed (${err instanceof Error ? err.message : "unknown"}): ${surrogateSafeSlice(payload, 200)}`;
|
|
263
267
|
continue;
|
|
264
268
|
}
|
|
269
|
+
if (parsed.type === "transcript.text.delta" && typeof parsed.delta === "string") {
|
|
270
|
+
transcript += parsed.delta;
|
|
271
|
+
transcriptSeen = true;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (parsed.type === "transcript.text.done" && typeof parsed.text === "string") {
|
|
275
|
+
transcript = parsed.text;
|
|
276
|
+
transcriptSeen = true;
|
|
277
|
+
if (Array.isArray(parsed.languages)) transcriptLanguages = parsed.languages.filter((language) => typeof language === "object" && language !== null && typeof language.code === "string").map((language) => ({ code: language.code }));
|
|
278
|
+
if (parsed.usage && typeof parsed.usage === "object") transcriptUsage = parsed.usage;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
265
281
|
if (parsed.type === "response.reasoning_summary_text.delta" && typeof parsed.delta === "string") {
|
|
266
282
|
reasoning += parsed.delta;
|
|
267
283
|
continue;
|
|
@@ -275,9 +291,86 @@ function collapseOpenAISSE(rawBody) {
|
|
|
275
291
|
continue;
|
|
276
292
|
}
|
|
277
293
|
}
|
|
294
|
+
if (item?.type === "function_call" && typeof parsed.output_index === "number") {
|
|
295
|
+
const entry = toolCallMap.get(parsed.output_index);
|
|
296
|
+
if (entry) {
|
|
297
|
+
if (!entry.id && typeof item.call_id === "string") entry.id = item.call_id;
|
|
298
|
+
if (!entry.name && typeof item.name === "string") entry.name = item.name;
|
|
299
|
+
if (entry.arguments === "" && typeof item.arguments === "string") entry.arguments = item.arguments;
|
|
300
|
+
} else {
|
|
301
|
+
const created = {
|
|
302
|
+
id: typeof item.call_id === "string" ? item.call_id : "",
|
|
303
|
+
name: typeof item.name === "string" ? item.name : "",
|
|
304
|
+
arguments: typeof item.arguments === "string" ? item.arguments : ""
|
|
305
|
+
};
|
|
306
|
+
toolCallMap.set(parsed.output_index, created);
|
|
307
|
+
orderAtoms.push({
|
|
308
|
+
kind: "toolCall",
|
|
309
|
+
ref: created
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (parsed.type === "response.output_item.added") {
|
|
316
|
+
const item = parsed.item;
|
|
317
|
+
if (item?.type === "function_call" && typeof parsed.output_index === "number") {
|
|
318
|
+
if (!toolCallMap.has(parsed.output_index)) {
|
|
319
|
+
const created = {
|
|
320
|
+
id: typeof item.call_id === "string" ? item.call_id : "",
|
|
321
|
+
name: typeof item.name === "string" ? item.name : "",
|
|
322
|
+
arguments: ""
|
|
323
|
+
};
|
|
324
|
+
toolCallMap.set(parsed.output_index, created);
|
|
325
|
+
orderAtoms.push({
|
|
326
|
+
kind: "toolCall",
|
|
327
|
+
ref: created
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (parsed.type === "response.function_call_arguments.delta" && typeof parsed.delta === "string" && typeof parsed.output_index === "number") {
|
|
334
|
+
let entry = toolCallMap.get(parsed.output_index);
|
|
335
|
+
if (!entry) {
|
|
336
|
+
entry = {
|
|
337
|
+
id: "",
|
|
338
|
+
name: "",
|
|
339
|
+
arguments: ""
|
|
340
|
+
};
|
|
341
|
+
toolCallMap.set(parsed.output_index, entry);
|
|
342
|
+
orderAtoms.push({
|
|
343
|
+
kind: "toolCall",
|
|
344
|
+
ref: entry
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
entry.arguments += parsed.delta;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (parsed.type === "response.function_call_arguments.done" && typeof parsed.output_index === "number") {
|
|
351
|
+
const entry = toolCallMap.get(parsed.output_index);
|
|
352
|
+
if (entry) {
|
|
353
|
+
if (entry.arguments === "" && typeof parsed.arguments === "string") entry.arguments = parsed.arguments;
|
|
354
|
+
} else if (typeof parsed.arguments === "string") {
|
|
355
|
+
const created = {
|
|
356
|
+
id: "",
|
|
357
|
+
name: "",
|
|
358
|
+
arguments: parsed.arguments
|
|
359
|
+
};
|
|
360
|
+
toolCallMap.set(parsed.output_index, created);
|
|
361
|
+
orderAtoms.push({
|
|
362
|
+
kind: "toolCall",
|
|
363
|
+
ref: created
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
278
367
|
}
|
|
279
368
|
if (parsed.type === "response.output_text.delta" && typeof parsed.delta === "string") {
|
|
280
369
|
content += parsed.delta;
|
|
370
|
+
if (parsed.delta.length > 0) orderAtoms.push({
|
|
371
|
+
kind: "text",
|
|
372
|
+
text: parsed.delta
|
|
373
|
+
});
|
|
281
374
|
continue;
|
|
282
375
|
}
|
|
283
376
|
if (typeof parsed.type === "string" && parsed.type.startsWith("response.")) continue;
|
|
@@ -350,6 +443,11 @@ function collapseOpenAISSE(rawBody) {
|
|
|
350
443
|
...tc.id ? { id: tc.id } : {}
|
|
351
444
|
}));
|
|
352
445
|
return {
|
|
446
|
+
...transcriptSeen ? { transcription: {
|
|
447
|
+
text: transcript,
|
|
448
|
+
...transcriptLanguages ? { languages: transcriptLanguages } : {},
|
|
449
|
+
...transcriptUsage ? { usage: transcriptUsage } : {}
|
|
450
|
+
} } : {},
|
|
353
451
|
...blocks ? { blocks } : {},
|
|
354
452
|
...content ? { content } : {},
|
|
355
453
|
toolCalls: [...blocks ? orderedToolCalls : indexSortedToolCalls, ...harmonyToolCalls],
|
|
@@ -363,6 +461,11 @@ function collapseOpenAISSE(rawBody) {
|
|
|
363
461
|
};
|
|
364
462
|
}
|
|
365
463
|
return {
|
|
464
|
+
...transcriptSeen ? { transcription: {
|
|
465
|
+
text: transcript,
|
|
466
|
+
...transcriptLanguages ? { languages: transcriptLanguages } : {},
|
|
467
|
+
...transcriptUsage ? { usage: transcriptUsage } : {}
|
|
468
|
+
} } : {},
|
|
366
469
|
content,
|
|
367
470
|
...reasoning ? { reasoning } : {},
|
|
368
471
|
...webSearchQueries.length > 0 ? { webSearches: webSearchQueries } : {},
|