Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,11 @@ boundaries, `tool-input-end` continues to complete a call immediately.
`tool-call` and UI `tool-input-available` events also work without preceding
deltas. Repeated completion events do not emit duplicate results.

The [Mistral Responses example](examples/README.md#mistral-responses) translates
`@mistralai/mistralai` frames into the existing OpenAI adapter's event shape.
It unwraps `frame.data` and maps the argument events' `itemId` to `item_id`.
No additional adapter or runtime SDK dependency is required.

For Cohere v2, pass `client.chatStream(...)` from `CohereClientV2` to
`readStructured` with `integration: cohere`. Both the SDK's camelCase
`toolCalls` payloads and decoded wire `tool_calls` payloads are accepted.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ Adapters consume structural event shapes and do not install or load the
provider SDKs. `pushAll()` returns every update when one event contains
multiple calls; existing `push()` usage remains supported. Lifecycle
completion means the arguments are finalized, not that a tool has executed.
The [Mistral Responses example](examples/README.md#mistral-responses) reuses
`streamfold/openai` after unwrapping SDK frames and converting their `itemId`
field to `item_id`.
The Vercel AI SDK adapter uses the final `tool-call` input from `fullStream` or
`tool-input-available` input from `toUIMessageStream()`, preserving schema defaults
and transforms after streamed deltas. It also accepts these events without
Expand Down
49 changes: 49 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,55 @@ network request too. The explicit signal and testing subpath require Streamfold
The assistant-ui adapter is an event translator. This example does not replace
assistant-ui's internal parser or claim an end-to-end performance improvement.

## Mistral Responses

[`mistral.mjs`](./mistral.mjs) accepts the asynchronous stream returned by
`@mistralai/mistralai`'s Responses API. Copy it into your application alongside
the SDK and `streamfold`. It reuses `streamfold/openai`: Mistral emits compatible
tool-argument events, but the SDK wraps them in `frame.data` and converts
`item_id` to `itemId`. The example normalizes those differences before parsing.

```js
import { Mistral } from "@mistralai/mistralai";
import { readToolInputs } from "./mistral.mjs";

const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
const controller = new AbortController();
const events = await client.responses.create({
model: process.env.MISTRAL_MODEL,
input: "What is the weather in Paris?",
tools: [{
type: "function",
name: "weather",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
},
}],
stream: true,
}, { signal: controller.signal });

for await (const update of readToolInputs(events, { signal: controller.signal })) {
console.log(update.id, update.partialValue);
}
```

Set `MISTRAL_MODEL` to a model supported by the
[Mistral Responses API](https://docs.mistral.ai/studio/conversations/responses).
The example yields immutable tool-argument previews keyed by output item ID,
not `callId`. It does not execute tools, validate their schema, or interpret
response-level status events. Keep those responsibilities in the application's
existing response handler. Breaking the loop closes the SDK iterator; pass the
same signal to both calls to also cancel pending network reads.

Run `node --test examples/mistral.test.mjs` for a credential-free check through
the real SDK's SSE decoder. `pnpm typecheck` also checks an actual
`client.responses.create()` stream against this example. These checks use
`@mistralai/mistralai@3.4.0`; they do not make live model requests or establish
an end-to-end performance improvement.

## Interactive assistant-ui demo

The [standalone React demo](./assistant-ui-demo/README.md) consumes the published
Expand Down
28 changes: 28 additions & 0 deletions examples/mistral.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { readStructured } from "streamfold";
import { openAI } from "streamfold/openai";

/**
* @param {AsyncIterable<import('@mistralai/mistralai/models/components').ResponsesStreamFrame>} source
*/
async function* normalizeEvents(source) {
for await (const { data } of source) {
if (data.type === "response.function_call_arguments.delta" ||
data.type === "response.function_call_arguments.done") {
yield { ...data, item_id: data.itemId };
} else {
yield data;
}
}
}

/**
* @param {AsyncIterable<import('@mistralai/mistralai/models/components').ResponsesStreamFrame>} source
* @param {{ signal?: AbortSignal }} [options]
*/
export function readToolInputs(source, options = {}) {
return readStructured(normalizeEvents(source), {
integration: openAI,
limits: { snapshots: "immutable" },
...options,
});
}
65 changes: 65 additions & 0 deletions examples/mistral.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import test from "node:test";
import { Mistral } from "@mistralai/mistralai";
import { HTTPClient } from "@mistralai/mistralai/lib/http.js";
import { readToolInputs } from "./mistral.mjs";

const events = [
{ type: "response.output_item.added", output_index: 0, item: { type: "function_call", id: "a", call_id: "call-a", name: "weather", arguments: "" } },
{ type: "response.output_item.added", output_index: 1, item: { type: "function_call", id: "b", call_id: "call-b", name: "weather", arguments: "" } },
{ type: "response.function_call_arguments.delta", output_index: 0, item_id: "a", delta: '{"city":"San' },
{ type: "response.function_call_arguments.delta", output_index: 1, item_id: "b", delta: '{"city":"Paris"}' },
{ type: "response.function_call_arguments.delta", output_index: 0, item_id: "a", delta: ' Francisco"}' },
{ type: "response.function_call_arguments.done", output_index: 1, item_id: "b", name: "weather", arguments: '{"city":"Paris"}' },
{ type: "response.function_call_arguments.done", output_index: 0, item_id: "a", name: "weather", arguments: '{"city":"San Francisco"}' },
];

const sse = events.map((event, sequence_number) =>
`event: ${event.type}\ndata: ${JSON.stringify({ ...event, sequence_number })}\n\n`
).join("");

function createStream(body) {
const client = new Mistral({
apiKey: "test-key",
httpClient: new HTTPClient({
fetcher: async () => new Response(body, {
headers: { "Content-Type": "text/event-stream" },
}),
}),
});
return client.responses.create({ model: "test-model", input: "Weather?", stream: true });
}

test("Mistral SDK frames preserve interleaved tool IDs and partial snapshots", async () => {
const source = await createStream(sse);
const updates = [];
for await (const update of readToolInputs(source)) updates.push(update);

assert.deepEqual(updates.map(({ type, id }) => [type, id]), [
["start", "a"], ["start", "b"], ["update", "a"], ["update", "b"],
["update", "a"], ["complete", "b"], ["complete", "a"],
]);
assert.deepEqual(updates[2].partialValue, { city: "San" });
assert.ok(Object.isFrozen(updates[2].partialValue));
assert.deepEqual(updates.slice(-2).map(({ value }) => value), [
{ city: "Paris" }, { city: "San Francisco" },
]);
});

test("stopping the Mistral example cancels the SDK response body", async () => {
let cancelled = false;
const body = new ReadableStream({
start(controller) { controller.enqueue(new TextEncoder().encode(sse)); },
cancel() { cancelled = true; },
});
const source = await createStream(body);
const iterator = readToolInputs(source);
try {
assert.equal((await iterator.next()).value.type, "start");
assert.equal((await iterator.next()).value.type, "start");
assert.equal((await iterator.next()).value.type, "update");
} finally {
await iterator.return();
}
assert.equal(cancelled, true);
});
2 changes: 1 addition & 1 deletion examples/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"checkJs": true,
"skipLibCheck": true
},
"files": ["assistant-ui.mjs", "vercel-ai.mjs", "custom.mjs", "../tests/langchain-sdk.ts", "../tests/gemini-sdk.ts", "../tests/openai-sdk.ts", "../tests/anthropic-sdk.ts", "../tests/bedrock-sdk.ts", "../tests/cohere-sdk.ts"]
"files": ["assistant-ui.mjs", "vercel-ai.mjs", "custom.mjs", "../tests/langchain-sdk.ts", "../tests/gemini-sdk.ts", "../tests/openai-sdk.ts", "../tests/anthropic-sdk.ts", "../tests/bedrock-sdk.ts", "../tests/cohere-sdk.ts", "mistral.mjs", "../tests/mistral-sdk.ts"]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@aws-sdk/client-bedrock-runtime": "3.1159.0",
"@google/genai": "2.31.0",
"@langchain/core": "1.3.0",
"@mistralai/mistralai": "3.4.0",
"ai": "7.0.152",
"assistant-stream": "0.3.42",
"bumpp": "^12.3.0",
Expand Down
Loading
Loading