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 +47 -6
- package/dist/adapters/cohere.cjs +594 -0
- package/dist/adapters/cohere.cjs.map +1 -0
- package/dist/adapters/cohere.d.cts +9 -0
- package/dist/adapters/cohere.d.ts +9 -0
- package/dist/adapters/cohere.js +592 -0
- package/dist/adapters/cohere.js.map +1 -0
- package/dist/index.cjs +456 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +456 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# llm-stream-assemble
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|

|
|
5
5
|

|
|
6
|
-

|
|
7
7
|
[](https://github.com/01laky/llm-stream-assemble/actions/workflows/ci.yml)
|
|
8
|
-

|
|
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:
|
|
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.
|
|
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,
|
|
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
|