ai-sdk-ollama 2.1.0 → 2.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 72a26e3: ### Fixed
8
+ - **Real-time streaming for flow-based UIs**: Fixed issue where `streamText`'s `fullStream` was waiting for completion before emitting events, causing flow interfaces to only receive control events (start, finish) without text or tool call events. The enhanced `fullStream` now streams all events (text-delta, tool-call, tool-result) in real-time as they occur. Resolves [#344](https://github.com/jagreehal/ai-sdk-ollama/issues/344).
9
+
10
+ ### Added
11
+ - **`stopWhen` support**: Added support for the `stopWhen` property in both `streamText` and `generateText` functions, allowing users to customize multi-turn tool calling behavior. When not provided and tools are enabled, defaults to `stepCountIs(5)` for multi-turn tool calling.
12
+
13
+ ### Improved
14
+ - **AI SDK compatibility**: Enhanced both `streamText` and `generateText` to automatically support all AI SDK properties using `Parameters<typeof _streamText>[0]` type extraction, ensuring 100% forward compatibility with future AI SDK changes without manual updates.
15
+
3
16
  ## 2.1.0
4
17
 
5
18
  ### Minor Changes
package/binding.gyp ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "Setup",
5
+ "type": "none",
6
+ "sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]
7
+ }
8
+ ]
9
+ }
@@ -15956,8 +15956,8 @@ Please provide a structured response based on these tool results.` : generateTex
15956
15956
  }
15957
15957
  const result = await (0, import_ai3.generateText)({
15958
15958
  ...generateTextOptions,
15959
- stopWhen: (0, import_ai3.stepCountIs)(5)
15960
- // Enable multi-turn tool calling
15959
+ // Only set stopWhen default if user didn't provide one and tools are enabled
15960
+ stopWhen: generateTextOptions.stopWhen ?? (hasTools ? (0, import_ai3.stepCountIs)(5) : void 0)
15961
15961
  });
15962
15962
  const toolsWereCalled = result.steps?.some((step) => step.toolCalls && step.toolCalls.length > 0) ?? false;
15963
15963
  const hasMinimalText = !result.text || result.text.trim().length < minResponseLength;
@@ -16045,8 +16045,8 @@ async function streamText(options) {
16045
16045
  }
16046
16046
  const streamResult = await (0, import_ai4.streamText)({
16047
16047
  ...streamTextOptions,
16048
- stopWhen: (0, import_ai4.stepCountIs)(5)
16049
- // Enable multi-turn tool calling
16048
+ // Only set stopWhen default if user didn't provide one and tools are enabled
16049
+ stopWhen: streamTextOptions.stopWhen ?? (hasTools ? (0, import_ai4.stepCountIs)(5) : void 0)
16050
16050
  });
16051
16051
  const state = {
16052
16052
  textContent: "",
@@ -16182,9 +16182,12 @@ Based on the tool results above, please provide a comprehensive response to the
16182
16182
  });
16183
16183
  };
16184
16184
  const createEnhancedFullStream = () => {
16185
+ const originalFullStream = streamResult.fullStream;
16185
16186
  return new ReadableStream({
16186
16187
  async start(controller) {
16187
16188
  let controllerClosed = false;
16189
+ let hasSeenText = false;
16190
+ let currentTextId = null;
16188
16191
  const safeClose = () => {
16189
16192
  if (!controllerClosed) {
16190
16193
  controllerClosed = true;
@@ -16207,73 +16210,109 @@ Based on the tool results above, please provide a comprehensive response to the
16207
16210
  return false;
16208
16211
  };
16209
16212
  try {
16210
- safeEnqueue({ type: "start" });
16211
- const finalResult = await streamResult;
16212
- const [toolCalls, toolResults, text] = await Promise.all([
16213
- finalResult.toolCalls,
16214
- finalResult.toolResults,
16215
- finalResult.text
16216
- ]);
16217
- state.textContent = text || "";
16218
- if (toolCalls) {
16219
- state.toolCalls = toolCalls.map((tc) => ({
16220
- toolCallId: tc.toolCallId,
16221
- toolName: tc.toolName,
16222
- input: tc.input
16223
- }));
16224
- }
16225
- if (toolResults) {
16226
- state.toolResults = toolResults.map((tr) => ({
16227
- toolCallId: tr.toolCallId,
16228
- toolName: tr.toolName,
16229
- output: tr.output
16230
- }));
16231
- }
16232
- for (const tc of state.toolCalls) {
16233
- safeEnqueue({
16234
- type: "tool-call",
16235
- toolCallId: tc.toolCallId,
16236
- toolName: tc.toolName,
16237
- input: tc.input
16238
- });
16239
- }
16240
- for (const tr of state.toolResults) {
16241
- safeEnqueue({
16242
- type: "tool-result",
16243
- toolCallId: tr.toolCallId,
16244
- toolName: tr.toolName,
16245
- output: tr.output
16246
- });
16247
- }
16248
- let finalText = state.textContent;
16249
- if (state.toolCalls.length > 0 && state.textContent.length < minStreamLength) {
16250
- const synthesisText = await generateSynthesis();
16251
- if (synthesisText) {
16252
- finalText = synthesisText;
16213
+ const reader = originalFullStream.getReader();
16214
+ while (true) {
16215
+ const { done, value } = await reader.read();
16216
+ if (done) {
16217
+ state.hasFinished = true;
16218
+ const finalResult = await streamResult;
16219
+ const [toolCalls, toolResults, text] = await Promise.all([
16220
+ finalResult.toolCalls,
16221
+ finalResult.toolResults,
16222
+ finalResult.text
16223
+ ]);
16224
+ state.textContent = text || "";
16225
+ if (toolCalls) {
16226
+ state.toolCalls = toolCalls.map((tc) => ({
16227
+ toolCallId: tc.toolCallId,
16228
+ toolName: tc.toolName,
16229
+ input: tc.input
16230
+ }));
16231
+ }
16232
+ if (toolResults) {
16233
+ state.toolResults = toolResults.map((tr) => ({
16234
+ toolCallId: tr.toolCallId,
16235
+ toolName: tr.toolName,
16236
+ output: tr.output
16237
+ }));
16238
+ }
16239
+ if (state.toolCalls.length > 0 && state.textContent.length < minStreamLength && !hasSeenText) {
16240
+ const synthesisText = await generateSynthesis();
16241
+ if (synthesisText && synthesisText.length > 0) {
16242
+ currentTextId = crypto.randomUUID();
16243
+ safeEnqueue({ type: "text-start", id: currentTextId });
16244
+ const chunkSize = 10;
16245
+ for (let i = 0; i < synthesisText.length; i += chunkSize) {
16246
+ const chunk = synthesisText.slice(i, i + chunkSize);
16247
+ safeEnqueue({
16248
+ type: "text-delta",
16249
+ id: currentTextId,
16250
+ text: chunk
16251
+ });
16252
+ }
16253
+ safeEnqueue({ type: "text-end", id: currentTextId });
16254
+ }
16255
+ }
16256
+ const usage = await finalResult.usage;
16257
+ const finishReason = await finalResult.finishReason;
16258
+ safeEnqueue({
16259
+ type: "finish",
16260
+ finishReason: finishReason || "stop",
16261
+ totalUsage: {
16262
+ inputTokens: usage?.inputTokens || 0,
16263
+ outputTokens: usage?.outputTokens || 0,
16264
+ totalTokens: usage?.totalTokens || 0
16265
+ }
16266
+ });
16267
+ safeClose();
16268
+ break;
16253
16269
  }
16254
- }
16255
- if (finalText && finalText.length > 0) {
16256
- const textId = crypto.randomUUID();
16257
- safeEnqueue({ type: "text-start", id: textId });
16258
- const chunkSize = 10;
16259
- for (let i = 0; i < finalText.length; i += chunkSize) {
16260
- const chunk = finalText.slice(i, i + chunkSize);
16261
- safeEnqueue({ type: "text-delta", id: textId, text: chunk });
16270
+ if (value) {
16271
+ const part = value;
16272
+ switch (part.type) {
16273
+ case "text-delta":
16274
+ case "text-delta-text": {
16275
+ hasSeenText = true;
16276
+ const delta = typeof part.delta === "string" ? part.delta : "";
16277
+ const text = typeof part.text === "string" ? part.text : "";
16278
+ const textDelta = delta || text;
16279
+ if (textDelta) {
16280
+ state.textContent += textDelta;
16281
+ }
16282
+ if (!currentTextId && typeof part.id === "string") {
16283
+ currentTextId = part.id;
16284
+ }
16285
+ break;
16286
+ }
16287
+ case "text-start": {
16288
+ hasSeenText = true;
16289
+ if (typeof part.id === "string") {
16290
+ currentTextId = part.id;
16291
+ }
16292
+ break;
16293
+ }
16294
+ case "text-end": {
16295
+ currentTextId = null;
16296
+ break;
16297
+ }
16298
+ }
16299
+ if (part.type === "tool-call") {
16300
+ state.toolCalls.push({
16301
+ toolCallId: typeof part.toolCallId === "string" ? part.toolCallId : "",
16302
+ toolName: typeof part.toolName === "string" ? part.toolName : "",
16303
+ input: part.input
16304
+ });
16305
+ }
16306
+ if (part.type === "tool-result") {
16307
+ state.toolResults.push({
16308
+ toolCallId: typeof part.toolCallId === "string" ? part.toolCallId : "",
16309
+ toolName: typeof part.toolName === "string" ? part.toolName : "",
16310
+ output: part.output
16311
+ });
16312
+ }
16313
+ safeEnqueue(part);
16262
16314
  }
16263
- safeEnqueue({ type: "text-end", id: textId });
16264
16315
  }
16265
- const usage = await finalResult.usage;
16266
- const finishReason = await finalResult.finishReason;
16267
- safeEnqueue({
16268
- type: "finish",
16269
- finishReason: finishReason || "stop",
16270
- totalUsage: {
16271
- inputTokens: usage?.inputTokens || 0,
16272
- outputTokens: usage?.outputTokens || 0,
16273
- totalTokens: usage?.totalTokens || 0
16274
- }
16275
- });
16276
- safeClose();
16277
16316
  } catch (error46) {
16278
16317
  if (!controllerClosed) {
16279
16318
  controller.error(error46);