llm-stream-assemble 1.4.1 → 1.5.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/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # llm-stream-assemble
2
2
 
3
- ![core](https://img.shields.io/badge/core-1.4.1-blue)
3
+ ![core](https://img.shields.io/badge/core-1.5.0-blue)
4
4
  ![node](https://img.shields.io/badge/node-%3E%3D18-339933)
5
5
  ![runtime deps](https://img.shields.io/badge/runtime_deps-0-brightgreen)
6
- ![tests](https://img.shields.io/badge/tests-1183_passing-brightgreen)
6
+ ![tests](https://img.shields.io/badge/tests-1316_passing-brightgreen)
7
7
  [![ci](https://github.com/01laky/llm-stream-assemble/actions/workflows/ci.yml/badge.svg)](https://github.com/01laky/llm-stream-assemble/actions/workflows/ci.yml)
8
- ![status](https://img.shields.io/badge/status-stable_1.4.1-brightgreen)
8
+ ![status](https://img.shields.io/badge/status-stable_1.5.0-brightgreen)
9
9
 
10
10
  **One typed event model for every LLM stream** — text, tool calls, reasoning, JSON, usage, refusals, errors, and non-streaming responses.
11
11
 
12
- > A zero-dependency TypeScript layer between raw LLM provider bytes and your app: six built-in adapters, thirteen host presets, and a single StreamEvent model for text, tools, reasoning, JSON, and lifecycle — from Ollama to Azure to Bedrock to Cloudflare Workers AI.
12
+ > A zero-dependency TypeScript layer between raw LLM provider bytes and your app: seven built-in adapters, thirteen host presets, and a single StreamEvent model for text, tools, reasoning, JSON, and lifecycle — from Ollama to Azure to Bedrock to Cohere to Cloudflare Workers AI.
13
13
 
14
14
  Turn provider SSE fragments into typed events — **not another `+=` loop**.
15
15
 
16
- **Status:** Stable `1.4.1`. Six built-in adapters, thirteen OpenAI-compatible host presets (including **Azure OpenAI** and **Cloudflare Workers AI**), transforms, replay helpers, and examples are production-ready. Pin semver ranges as usual and review [CHANGELOG.md](./CHANGELOG.md) before major upgrades.
16
+ **Status:** Stable `1.5.0`. Seven built-in adapters, thirteen OpenAI-compatible host presets (including **Azure OpenAI** and **Cloudflare Workers AI**), transforms, replay helpers, and examples are production-ready. Pin semver ranges as usual and review [CHANGELOG.md](./CHANGELOG.md) before major upgrades.
17
17
 
18
18
  ---
19
19
 
@@ -146,6 +146,7 @@ Diagram sources: [`docs/img/`](./docs/img/) (Mermaid `.mmd` + committed SVG). Re
146
146
  | `openaiResponsesAdapter()` | OpenAI Responses API | `llm-stream-assemble` |
147
147
  | `geminiAdapter()` | Google AI Gemini | `llm-stream-assemble` or `/adapters/gemini` |
148
148
  | `bedrockAdapter()` | AWS Bedrock Converse / ConverseStream | `llm-stream-assemble` or `/adapters/bedrock` |
149
+ | `cohereAdapter()` | Cohere Chat v2 (`api.cohere.com/v2/chat`) | `llm-stream-assemble` or `/adapters/cohere` |
149
150
 
150
151
  Full feature flags and quirks: [compatibility matrix](./docs/compatibility.md).
151
152
 
@@ -202,6 +203,7 @@ Pick an adapter in ~30 seconds:
202
203
  - **Anthropic Messages** → `anthropicAdapter()`
203
204
  - **Google Gemini** → `geminiAdapter()`
204
205
  - **AWS Bedrock ConverseStream** → `bedrockAdapter()` (decoded JSON per event — see [Bedrock Usage](#bedrock-usage))
206
+ - **Cohere Chat v2 SSE** → `cohereAdapter()` (not OpenAI-compatible — see [Cohere Usage](#cohere-usage))
205
207
  - **Groq, Ollama, Azure, Cloudflare, OpenRouter, …** → `openaiCompatibleAdapter({ provider })`
206
208
  - **Non-streaming JSON body** → `assembleResponse(body, adapter)`
207
209
  - **React chat UI / full agent framework** → not this package — see [comparison](./docs/comparison.md)
@@ -277,6 +279,10 @@ for await (const event of assembleStream(response.body!, adapter)) {
277
279
 
278
280
  → [`examples/node-fetch/bedrock.ts`](./examples/node-fetch/bedrock.ts) · Usage: [Bedrock](#bedrock-usage) · Decode helper: [`examples/bedrock/README.md`](./examples/bedrock/README.md)
279
281
 
282
+ ### Cohere Chat v2
283
+
284
+ → [`examples/node-fetch/cohere.ts`](./examples/node-fetch/cohere.ts) · Usage: [Cohere](#cohere-usage)
285
+
280
286
  ### Streaming JSON (structured output)
281
287
 
282
288
  ```ts
@@ -317,7 +323,7 @@ Wire unified events into **Hono**, **Express**, **Cloudflare Workers**, **LiteLL
317
323
 
318
324
  ### Core Usage
319
325
 
320
- The core pipeline works with any adapter that emits `RawChunk[]`, including the built-in OpenAI Chat, OpenAI-compatible, Anthropic Messages, OpenAI Responses, Google Gemini, and AWS Bedrock adapters:
326
+ The core pipeline works with any adapter that emits `RawChunk[]`, including the built-in OpenAI Chat, OpenAI-compatible, Anthropic Messages, OpenAI Responses, Google Gemini, AWS Bedrock, and Cohere adapters:
321
327
 
322
328
  ```ts
323
329
  import { assembleFromPayloads, type StreamAdapter } from "llm-stream-assemble";
@@ -605,6 +611,41 @@ Subpath import: `import { bedrockAdapter } from "llm-stream-assemble/adapters/be
605
611
 
606
612
  Worker proxy recipe: [`examples/integrations/bedrock-worker-proxy.ts`](./examples/integrations/bedrock-worker-proxy.ts). EventStream decode helper (examples only): [`examples/bedrock/decode-event-stream.ts`](./examples/bedrock/decode-event-stream.ts).
607
613
 
614
+ ### Cohere Usage
615
+
616
+ `cohereAdapter()` parses Cohere Chat **v2** SSE events from `https://api.cohere.com/v2/chat` and non-streaming v2 response bodies. Create one adapter instance per request/stream. Cohere is **not** OpenAI-compatible — use `cohereAdapter()`, not `openaiCompatibleAdapter()`.
617
+
618
+ Core `parseSSE()` frames the HTTP body; `assembleStream` yields one JSON payload string per `data:` line to `cohereAdapter().parseChunk`.
619
+
620
+ ```ts
621
+ import { assembleStream, cohereAdapter } from "llm-stream-assemble";
622
+
623
+ const response = await fetch("https://api.cohere.com/v2/chat", {
624
+ method: "POST",
625
+ headers: {
626
+ Authorization: `Bearer ${process.env.COHERE_API_KEY}`,
627
+ "Content-Type": "application/json",
628
+ },
629
+ body: JSON.stringify({
630
+ model: "command-r-plus-08-2024",
631
+ messages: [{ role: "user", content: "Hello" }],
632
+ stream: true,
633
+ }),
634
+ });
635
+
636
+ for await (const event of assembleStream(response.body!, cohereAdapter())) {
637
+ if (event.type === "text.delta") process.stdout.write(event.text);
638
+ if (event.type === "reasoning.delta") process.stdout.write(event.text);
639
+ if (event.type === "tool_call.done") console.log(event.name, event.args);
640
+ }
641
+ ```
642
+
643
+ Use `cohereAdapter({ jsonMode: true })` when structured JSON output should map to `json.*` instead of `text.*`. **`tool-plan-delta`** events map to `reasoning.*` with `variant: "detail"`. **`citation-start`** payloads are preserved in `metadata.raw` — there are no dedicated `citation.*` unified events in 1.x. Legacy Cohere v1 endpoints are out of scope.
644
+
645
+ Subpath import: `import { cohereAdapter } from "llm-stream-assemble/adapters/cohere"`.
646
+
647
+ Live smoke: `pnpm smoke:cohere` — see [`docs/live-smoke.md`](./docs/live-smoke.md) for `COHERE_API_KEY`, `COHERE_MODEL`, and `COHERE_SMOKE_TOOLS`.
648
+
608
649
  ---
609
650
 
610
651
  ## Transforms & replay
@@ -0,0 +1,594 @@
1
+ 'use strict';
2
+
3
+ // src/core/utils/object.ts
4
+ function stripUndefined(obj) {
5
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== void 0));
6
+ }
7
+
8
+ // src/adapters/utils.ts
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function asString(value) {
13
+ return typeof value === "string" ? value : void 0;
14
+ }
15
+ function asNumber(value) {
16
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
17
+ }
18
+ function optionalRawChunk(input) {
19
+ return stripUndefined(input);
20
+ }
21
+ function prefixedAdapterError(feature, message) {
22
+ return adapterScopedError(feature, message);
23
+ }
24
+ function createStreamAdapter(config) {
25
+ return {
26
+ parseChunk(raw) {
27
+ return config.parser.parseChunk(raw);
28
+ },
29
+ parseResponse(body) {
30
+ return config.parseResponse(body, config.options);
31
+ }
32
+ };
33
+ }
34
+ function parseAdapterJSON(raw, feature) {
35
+ try {
36
+ return JSON.parse(raw);
37
+ } catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ throw prefixedAdapterError(feature, message);
40
+ }
41
+ }
42
+
43
+ // src/adapters/errors.ts
44
+ function libraryError(message) {
45
+ return new Error(`llm-stream-assemble: ${message}`);
46
+ }
47
+ function adapterScopedError(scope, message) {
48
+ return new Error(`llm-stream-assemble: ${scope}: ${message}`);
49
+ }
50
+ function providerErrorChunks(error, recoverable = false) {
51
+ return [
52
+ { kind: "provider-error", error, recoverable },
53
+ { kind: "finish", reason: "error" }
54
+ ];
55
+ }
56
+ function providerErrorChunksFromPayload(errorPayload, scope, recoverable, fallbackMessage) {
57
+ const message = asString(errorPayload.message) ?? fallbackMessage;
58
+ const error = adapterScopedError(scope, message);
59
+ Object.defineProperty(error, "raw", {
60
+ value: errorPayload,
61
+ enumerable: false
62
+ });
63
+ return providerErrorChunks(error, recoverable);
64
+ }
65
+
66
+ // src/adapters/shared/incremental-json.ts
67
+ function incrementalJsonStringDelta(state, nextInput) {
68
+ const prev = state.lastArgsJson;
69
+ if (nextInput === prev) return void 0;
70
+ const delta = prev.length > 0 && nextInput.startsWith(prev) ? nextInput.slice(prev.length) : nextInput;
71
+ state.lastArgsJson = nextInput;
72
+ return delta.length > 0 ? delta : void 0;
73
+ }
74
+
75
+ // src/adapters/shared/parse-payload.ts
76
+ function parseAdapterObjectPayload(raw, scope, options = {}) {
77
+ const { trim = true, allowDone = true } = options;
78
+ const input = trim ? raw.trim() : raw;
79
+ if (input.length === 0 || allowDone && input === "[DONE]") return null;
80
+ const payload = parseAdapterJSON(input, scope);
81
+ if (!isRecord(payload)) {
82
+ throw adapterScopedError(scope, "expected a JSON object");
83
+ }
84
+ return payload;
85
+ }
86
+
87
+ // src/adapters/shared/text-delta.ts
88
+ function textOrJsonDelta(text, options) {
89
+ if (text.length === 0) return void 0;
90
+ if (options.jsonMode) return { kind: "json-delta", delta: text };
91
+ if (options.choiceIndex !== void 0) {
92
+ return { kind: "text-delta", text, choiceIndex: options.choiceIndex };
93
+ }
94
+ return { kind: "text-delta", text };
95
+ }
96
+
97
+ // src/adapters/shared/usage.ts
98
+ var DEFAULT_ALIASES = {
99
+ input: ["inputTokens", "input_tokens", "promptTokens", "promptTokenCount", "inputTokenCount"],
100
+ output: [
101
+ "outputTokens",
102
+ "output_tokens",
103
+ "completionTokens",
104
+ "candidatesTokenCount",
105
+ "outputTokenCount"
106
+ ],
107
+ reasoning: ["reasoningTokens", "reasoning_tokens", "thoughtsTokenCount"],
108
+ total: ["totalTokens", "totalTokenCount", "total_tokens"]
109
+ };
110
+ function firstNumber(value, fields) {
111
+ if (!fields) return void 0;
112
+ for (const field of fields) {
113
+ const number = asNumber(value[field]);
114
+ if (number !== void 0) return number;
115
+ }
116
+ return void 0;
117
+ }
118
+ function buildUsageChunk(value, aliases = DEFAULT_ALIASES, options) {
119
+ if (!isRecord(value)) return void 0;
120
+ const inputTokens = firstNumber(value, aliases.input);
121
+ const outputTokens = firstNumber(value, aliases.output);
122
+ const reasoningTokens = firstNumber(value, aliases.reasoning);
123
+ const totalTokens = firstNumber(value, aliases.total);
124
+ if (inputTokens === void 0 && outputTokens === void 0 && reasoningTokens === void 0 && totalTokens === void 0) {
125
+ return void 0;
126
+ }
127
+ const raw = value;
128
+ return optionalRawChunk({
129
+ kind: "usage",
130
+ inputTokens,
131
+ outputTokens,
132
+ reasoningTokens,
133
+ raw
134
+ });
135
+ }
136
+
137
+ // src/adapters/cohere.ts
138
+ var COHERE_USAGE_ALIASES = {
139
+ input: ["input_tokens", "inputTokens"],
140
+ output: ["output_tokens", "outputTokens"],
141
+ total: ["total_tokens", "totalTokens"]
142
+ };
143
+ function cohereAdapter(options = {}) {
144
+ const parser = new CohereStreamParser(options);
145
+ return createStreamAdapter({
146
+ parser,
147
+ parseResponse,
148
+ options
149
+ });
150
+ }
151
+ var CohereStreamParser = class {
152
+ constructor(options) {
153
+ this.options = options;
154
+ }
155
+ options;
156
+ messageStarted = false;
157
+ metadataEmitted = false;
158
+ toolsByKey = /* @__PURE__ */ new Map();
159
+ indexToKey = /* @__PURE__ */ new Map();
160
+ parseChunk(raw) {
161
+ const payload = parseAdapterObjectPayload(raw, "cohereAdapter.parseChunk");
162
+ if (!payload) return [];
163
+ if (asString(payload.type) === "error" || isRecord(payload.error)) {
164
+ const errorBody = isRecord(payload.error) ? payload.error : payload;
165
+ return providerErrorChunksFromPayload(
166
+ errorBody,
167
+ "cohereAdapter.parseChunk",
168
+ false,
169
+ "Cohere provider error"
170
+ );
171
+ }
172
+ const eventType = asString(payload.type);
173
+ if (!eventType) return optionalMetadataRaw(payload);
174
+ switch (eventType) {
175
+ case "message-start":
176
+ return this.messageStartChunks(payload);
177
+ case "content-start":
178
+ case "content-end":
179
+ return [];
180
+ case "content-delta":
181
+ return this.contentDeltaChunks(payload);
182
+ case "tool-plan-delta":
183
+ return this.toolPlanDeltaChunks(payload);
184
+ case "tool-call-start":
185
+ return this.toolCallStartChunks(payload);
186
+ case "tool-call-delta":
187
+ return this.toolCallDeltaChunks(payload);
188
+ case "tool-call-end":
189
+ return this.toolCallEndChunks(payload);
190
+ case "citation-start":
191
+ return this.citationStartChunks(payload);
192
+ case "citation-end":
193
+ return [];
194
+ case "message-end":
195
+ return this.messageEndChunks(payload);
196
+ default:
197
+ return optionalMetadataRaw(payload);
198
+ }
199
+ }
200
+ messageStartChunks(payload) {
201
+ if (this.messageStarted) return [];
202
+ this.messageStarted = true;
203
+ const chunks = [{ kind: "message-start" }];
204
+ const messageId = asString(payload.id);
205
+ const delta = isRecord(payload.delta) ? payload.delta : void 0;
206
+ const message = delta && isRecord(delta.message) ? delta.message : void 0;
207
+ const role = message ? asString(message.role) : void 0;
208
+ if (messageId || role) {
209
+ this.metadataEmitted = true;
210
+ chunks.push(
211
+ optionalRawChunk({
212
+ kind: "metadata",
213
+ responseId: messageId,
214
+ raw: { id: messageId, role }
215
+ })
216
+ );
217
+ }
218
+ return chunks;
219
+ }
220
+ contentDeltaChunks(payload) {
221
+ const delta = isRecord(payload.delta) ? payload.delta : void 0;
222
+ const message = delta && isRecord(delta.message) ? delta.message : void 0;
223
+ const content = message && isRecord(message.content) ? message.content : void 0;
224
+ const text = content ? asString(content.text) : void 0;
225
+ if (text === void 0 || text.length === 0) return [];
226
+ const textChunk = textOrJsonDelta(text, {
227
+ jsonMode: this.options.jsonMode,
228
+ choiceIndex: asNumber(payload.index) ?? 0
229
+ });
230
+ return textChunk ? [textChunk] : [];
231
+ }
232
+ toolPlanDeltaChunks(payload) {
233
+ const delta = isRecord(payload.delta) ? payload.delta : void 0;
234
+ const message = delta && isRecord(delta.message) ? delta.message : void 0;
235
+ const toolPlan = message ? asString(message.tool_plan) : asString(delta?.tool_plan);
236
+ if (toolPlan === void 0 || toolPlan.length === 0) return [];
237
+ return [{ kind: "reasoning-delta", text: toolPlan, variant: "detail" }];
238
+ }
239
+ toolCallStartChunks(payload) {
240
+ const baseIndex = asNumber(payload.index) ?? 0;
241
+ const toolCalls = toolCallsFromDelta(payload.delta);
242
+ if (toolCalls.length === 0) return [];
243
+ const chunks = [];
244
+ for (let i = 0; i < toolCalls.length; i += 1) {
245
+ const toolCall = toolCalls[i];
246
+ const index = asNumber(toolCall.index) ?? baseIndex + i;
247
+ const id = asString(toolCall.id) ?? `cohere:tool:${index}`;
248
+ const fn = isRecord(toolCall.function) ? toolCall.function : void 0;
249
+ const name = fn ? asString(fn.name) ?? "unknown" : "unknown";
250
+ const key = reconcileToolKey(this.toolsByKey, this.indexToKey, index, id);
251
+ const existing = this.toolsByKey.get(key);
252
+ if (existing?.open && existing.id === id) continue;
253
+ const state = {
254
+ id,
255
+ name,
256
+ index,
257
+ open: true,
258
+ lastArgsJson: asString(fn?.arguments) ?? ""
259
+ };
260
+ this.toolsByKey.set(key, state);
261
+ this.indexToKey.set(index, key);
262
+ if (id !== key) this.toolsByKey.set(id, state);
263
+ chunks.push(
264
+ optionalRawChunk({
265
+ kind: "tool-start",
266
+ id,
267
+ name,
268
+ index,
269
+ choiceIndex: 0
270
+ })
271
+ );
272
+ }
273
+ return chunks;
274
+ }
275
+ toolCallDeltaChunks(payload) {
276
+ const index = asNumber(payload.index) ?? 0;
277
+ const toolCalls = toolCallsFromDelta(payload.delta);
278
+ if (toolCalls.length === 0) return [];
279
+ const chunks = [];
280
+ for (const toolCall of toolCalls) {
281
+ const lateId = asString(toolCall.id);
282
+ const key = this.resolveToolKey(index, lateId);
283
+ let state = key ? this.toolsByKey.get(key) : void 0;
284
+ if (!state) {
285
+ const fn2 = isRecord(toolCall.function) ? toolCall.function : void 0;
286
+ const name = fn2 ? asString(fn2.name) ?? "unknown" : "unknown";
287
+ const id = lateId ?? `cohere:tool:${index}`;
288
+ state = { id, name, index, open: true, lastArgsJson: "" };
289
+ this.toolsByKey.set(id, state);
290
+ this.indexToKey.set(index, id);
291
+ chunks.push(
292
+ optionalRawChunk({
293
+ kind: "tool-start",
294
+ id,
295
+ name,
296
+ index,
297
+ choiceIndex: 0
298
+ })
299
+ );
300
+ } else if (lateId && state.id !== lateId) {
301
+ this.reconcileLateId(state, lateId, index);
302
+ }
303
+ const fn = isRecord(toolCall.function) ? toolCall.function : void 0;
304
+ const argsText = fn ? asString(fn.arguments) : void 0;
305
+ if (argsText !== void 0 && argsText.length > 0 && state) {
306
+ const delta = incrementalJsonStringDelta(state, argsText);
307
+ if (delta) {
308
+ chunks.push(
309
+ optionalRawChunk({
310
+ kind: "tool-args-delta",
311
+ id: state.id,
312
+ delta,
313
+ index: state.index,
314
+ choiceIndex: 0
315
+ })
316
+ );
317
+ }
318
+ }
319
+ }
320
+ return chunks;
321
+ }
322
+ toolCallEndChunks(payload) {
323
+ const index = asNumber(payload.index) ?? 0;
324
+ const delta = isRecord(payload.delta) ? payload.delta : void 0;
325
+ const message = delta && isRecord(delta.message) ? delta.message : void 0;
326
+ const toolCalls = message ? toolCallsFromRecord(message.tool_calls) : [];
327
+ const lateId = toolCalls[0] ? asString(toolCalls[0].id) : void 0;
328
+ const key = this.resolveToolKey(index, lateId);
329
+ const state = key ? this.toolsByKey.get(key) : void 0;
330
+ if (!state?.open) return [];
331
+ if (lateId && state.id !== lateId) {
332
+ this.reconcileLateId(state, lateId, index);
333
+ }
334
+ state.open = false;
335
+ return [
336
+ optionalRawChunk({
337
+ kind: "tool-done",
338
+ id: state.id,
339
+ index: state.index,
340
+ choiceIndex: 0
341
+ })
342
+ ];
343
+ }
344
+ citationStartChunks(payload) {
345
+ const delta = isRecord(payload.delta) ? payload.delta : void 0;
346
+ const message = delta && isRecord(delta.message) ? delta.message : void 0;
347
+ const citations = message?.citations;
348
+ return [
349
+ optionalRawChunk({
350
+ kind: "metadata",
351
+ raw: { citation: citations, index: payload.index }
352
+ })
353
+ ];
354
+ }
355
+ messageEndChunks(payload) {
356
+ const delta = isRecord(payload.delta) ? payload.delta : void 0;
357
+ const chunks = [];
358
+ const usageSource = delta && isRecord(delta.usage) ? usageFromCohereUsage(delta.usage) : delta && isRecord(delta.billed_units) ? delta.billed_units : void 0;
359
+ const usage = buildUsageChunk(usageSource, COHERE_USAGE_ALIASES);
360
+ if (usage) chunks.push(usage);
361
+ const finishReason = delta ? asString(delta.finish_reason) : void 0;
362
+ if (finishReason) {
363
+ chunks.push(
364
+ optionalRawChunk({
365
+ kind: "metadata",
366
+ raw: { finish_reason: finishReason }
367
+ })
368
+ );
369
+ chunks.push({
370
+ kind: "finish",
371
+ reason: mapCohereFinishReason(finishReason),
372
+ choiceIndex: 0
373
+ });
374
+ }
375
+ return chunks;
376
+ }
377
+ resolveToolKey(index, id) {
378
+ if (id && this.toolsByKey.has(id)) return id;
379
+ const byIndex = this.indexToKey.get(index);
380
+ if (byIndex) return byIndex;
381
+ if (id) return id;
382
+ return this.indexToKey.get(index);
383
+ }
384
+ reconcileLateId(state, lateId, index) {
385
+ const oldKey = state.id;
386
+ state.id = lateId;
387
+ this.toolsByKey.delete(oldKey);
388
+ this.toolsByKey.set(lateId, state);
389
+ this.indexToKey.set(index, lateId);
390
+ }
391
+ };
392
+ function parseResponse(body, options) {
393
+ if (!isRecord(body)) {
394
+ throw libraryError("cohereAdapter.parseResponse expected a Cohere Chat v2 response object");
395
+ }
396
+ if (asString(body.type) === "error" || isRecord(body.error)) {
397
+ const errorBody = isRecord(body.error) ? body.error : body;
398
+ return providerErrorChunksFromPayload(
399
+ errorBody,
400
+ "cohereAdapter.parseResponse",
401
+ false,
402
+ "Cohere provider error"
403
+ );
404
+ }
405
+ const message = isRecord(body.message) ? body.message : void 0;
406
+ if (!message) {
407
+ const chunks2 = [];
408
+ const usage = buildUsageChunk(body.usage, COHERE_USAGE_ALIASES);
409
+ if (usage) chunks2.push(usage);
410
+ const finishReason2 = asString(body.finish_reason);
411
+ if (finishReason2) {
412
+ chunks2.push({
413
+ kind: "finish",
414
+ reason: mapCohereFinishReason(finishReason2),
415
+ choiceIndex: 0
416
+ });
417
+ } else {
418
+ chunks2.push({ kind: "finish", reason: "stop", choiceIndex: 0 });
419
+ }
420
+ return chunks2;
421
+ }
422
+ const parser = new CohereStreamParser(options);
423
+ const syntheticEvents = synthesizeCohereStreamEvents(body);
424
+ const chunks = [];
425
+ for (const event of syntheticEvents) {
426
+ chunks.push(...parser.parseChunk(JSON.stringify(event)));
427
+ }
428
+ const finishReason = asString(body.finish_reason);
429
+ if (finishReason && !chunks.some((chunk) => chunk.kind === "finish")) {
430
+ chunks.push({
431
+ kind: "finish",
432
+ reason: mapCohereFinishReason(finishReason),
433
+ choiceIndex: 0
434
+ });
435
+ } else if (!chunks.some((chunk) => chunk.kind === "finish")) {
436
+ chunks.push({ kind: "finish", reason: "stop", choiceIndex: 0 });
437
+ }
438
+ return chunks;
439
+ }
440
+ function synthesizeCohereStreamEvents(body) {
441
+ const events = [];
442
+ const message = isRecord(body.message) ? body.message : void 0;
443
+ if (!message) return events;
444
+ const messageId = asString(body.id);
445
+ events.push({
446
+ type: "message-start",
447
+ ...messageId ? { id: messageId } : {},
448
+ delta: { message: { role: asString(message.role) ?? "assistant" } }
449
+ });
450
+ const toolPlan = asString(message.tool_plan);
451
+ if (toolPlan) {
452
+ events.push({
453
+ type: "tool-plan-delta",
454
+ delta: { message: { tool_plan: toolPlan } }
455
+ });
456
+ }
457
+ const content = Array.isArray(message.content) ? message.content : [];
458
+ for (const block of content) {
459
+ if (!isRecord(block)) continue;
460
+ const text = asString(block.text);
461
+ if (text !== void 0 && text.length > 0) {
462
+ events.push({
463
+ type: "content-delta",
464
+ index: 0,
465
+ delta: { message: { content: { text } } }
466
+ });
467
+ }
468
+ }
469
+ const citations = message.citations;
470
+ if (Array.isArray(citations)) {
471
+ for (const citation of citations) {
472
+ events.push({
473
+ type: "citation-start",
474
+ index: 0,
475
+ delta: { message: { citations: citation } }
476
+ });
477
+ }
478
+ }
479
+ const toolCalls = toolCallsFromRecord(message.tool_calls);
480
+ let toolIndex = 0;
481
+ for (const toolCall of toolCalls) {
482
+ const id = asString(toolCall.id) ?? `cohere:tool:${toolIndex}`;
483
+ const fn = isRecord(toolCall.function) ? toolCall.function : void 0;
484
+ const name = fn ? asString(fn.name) ?? "unknown" : "unknown";
485
+ const args = fn ? asString(fn.arguments) : void 0;
486
+ events.push({
487
+ type: "tool-call-start",
488
+ index: toolIndex,
489
+ delta: {
490
+ message: {
491
+ tool_calls: {
492
+ id,
493
+ type: "function",
494
+ function: { name, arguments: args ?? "" }
495
+ }
496
+ }
497
+ }
498
+ });
499
+ if (args !== void 0 && args.length > 0) {
500
+ events.push({
501
+ type: "tool-call-delta",
502
+ index: toolIndex,
503
+ delta: {
504
+ message: {
505
+ tool_calls: {
506
+ id,
507
+ function: { arguments: args }
508
+ }
509
+ }
510
+ }
511
+ });
512
+ }
513
+ events.push({ type: "tool-call-end", index: toolIndex });
514
+ toolIndex += 1;
515
+ }
516
+ if (body.usage !== void 0 || body.finish_reason !== void 0) {
517
+ events.push({
518
+ type: "message-end",
519
+ delta: {
520
+ ...body.finish_reason !== void 0 ? { finish_reason: body.finish_reason } : {},
521
+ ...body.usage !== void 0 ? { usage: body.usage } : {}
522
+ }
523
+ });
524
+ }
525
+ return events;
526
+ }
527
+ function toolCallsFromDelta(delta) {
528
+ if (!isRecord(delta)) return [];
529
+ const message = isRecord(delta.message) ? delta.message : void 0;
530
+ return toolCallsFromRecord(message?.tool_calls);
531
+ }
532
+ function toolCallsFromRecord(value) {
533
+ if (value === void 0) return [];
534
+ if (Array.isArray(value)) {
535
+ return value.filter((item) => isRecord(item));
536
+ }
537
+ if (isRecord(value)) return [value];
538
+ return [];
539
+ }
540
+ function usageFromCohereUsage(usage) {
541
+ const billed = isRecord(usage.billed_units) ? usage.billed_units : void 0;
542
+ if (billed) return billed;
543
+ const tokens = isRecord(usage.tokens) ? usage.tokens : void 0;
544
+ if (tokens) return tokens;
545
+ return usage;
546
+ }
547
+ function reconcileToolKey(toolsByKey, indexToKey, index, id) {
548
+ const existingIndexKey = indexToKey.get(index);
549
+ if (existingIndexKey && toolsByKey.has(existingIndexKey)) {
550
+ const state = toolsByKey.get(existingIndexKey);
551
+ if (state && state.id.startsWith("cohere:tool:") && !id.startsWith("cohere:tool:")) {
552
+ toolsByKey.delete(existingIndexKey);
553
+ state.id = id;
554
+ toolsByKey.set(id, state);
555
+ indexToKey.set(index, id);
556
+ return id;
557
+ }
558
+ return existingIndexKey;
559
+ }
560
+ return id;
561
+ }
562
+ function mapCohereFinishReason(value) {
563
+ const normalized = value.toUpperCase();
564
+ switch (normalized) {
565
+ case "COMPLETE":
566
+ case "STOP_SEQUENCE":
567
+ return "stop";
568
+ case "MAX_TOKENS":
569
+ return "length";
570
+ case "TOOL_CALL":
571
+ return "tool_calls";
572
+ case "ERROR":
573
+ case "TIMEOUT":
574
+ return "error";
575
+ default:
576
+ if (normalized.includes("FILTER") || normalized.includes("SAFETY")) {
577
+ return "content_filter";
578
+ }
579
+ return "stop";
580
+ }
581
+ }
582
+ function optionalMetadataRaw(payload) {
583
+ if (Object.keys(payload).length === 0) return [];
584
+ return [
585
+ optionalRawChunk({
586
+ kind: "metadata",
587
+ raw: payload
588
+ })
589
+ ];
590
+ }
591
+
592
+ exports.cohereAdapter = cohereAdapter;
593
+ //# sourceMappingURL=cohere.cjs.map
594
+ //# sourceMappingURL=cohere.cjs.map