diff --git a/packages/ai/anthropic-api.md b/packages/ai/docs/anthropic-api.md similarity index 100% rename from packages/ai/anthropic-api.md rename to packages/ai/docs/anthropic-api.md diff --git a/packages/ai/gemini-api.md b/packages/ai/docs/gemini-api.md similarity index 100% rename from packages/ai/gemini-api.md rename to packages/ai/docs/gemini-api.md diff --git a/packages/ai/openai-api.md b/packages/ai/docs/openai-api.md similarity index 100% rename from packages/ai/openai-api.md rename to packages/ai/docs/openai-api.md diff --git a/packages/ai/plan.md b/packages/ai/docs/plan.md similarity index 100% rename from packages/ai/plan.md rename to packages/ai/docs/plan.md diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 239e4a32..8b1d7bc8 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -114,15 +114,33 @@ export class AnthropicLLM implements LLM { }, ); + let blockType: "text" | "thinking" | "other" = "other"; for await (const event of stream) { + if (event.type === "content_block_start") { + if (event.content_block.type === "text") { + blockType = "text"; + } else if (event.content_block.type === "thinking") { + blockType = "thinking"; + } else { + blockType = "other"; + } + } if (event.type === "content_block_delta") { if (event.delta.type === "text_delta") { - options?.onText?.(event.delta.text); + options?.onText?.(event.delta.text, false); } if (event.delta.type === "thinking_delta") { - options?.onThinking?.(event.delta.thinking); + options?.onThinking?.(event.delta.thinking, false); } } + if (event.type === "content_block_stop") { + if (blockType === "text") { + options?.onText?.("", true); + } else if (blockType === "thinking") { + options?.onThinking?.("", true); + } + blockType = "other"; + } } const msg = await stream.finalMessage(); const thinking = msg.content.some((block) => block.type === "thinking") diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index cc406c9e..c2129a9c 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -87,17 +87,24 @@ export class OpenAICompletionsLLM implements LLM { }; let finishReason: ChatCompletionChunk.Choice["finish_reason"] | null = null; + let inTextBlock = false; for await (const chunk of stream) { const choice = chunk.choices[0]; // Handle text content if (choice?.delta?.content) { content += choice.delta.content; - options?.onText?.(choice.delta.content); + options?.onText?.(choice.delta.content, false); + inTextBlock = true; } // Handle tool calls if (choice?.delta?.tool_calls) { + if (inTextBlock) { + // If we were in a text block, signal its end + options?.onText?.("", true); + inTextBlock = false; + } for (const toolCall of choice.delta.tool_calls) { const index = toolCall.index; @@ -120,6 +127,11 @@ export class OpenAICompletionsLLM implements LLM { // Capture finish reason if (choice?.finish_reason) { + if (inTextBlock) { + // If we were in a text block, signal its end + options?.onText?.("", true); + inTextBlock = false; + } finishReason = choice.finish_reason; } diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 351f09ac..0ef453c5 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -91,21 +91,23 @@ export class OpenAIResponsesLLM implements LLM { if (event.type === "response.reasoning_summary_text.delta") { const delta = event.delta; thinking += delta; - options?.onThinking?.(delta); + options?.onThinking?.(delta, false); } else if (event.type === "response.reasoning_summary_text.done") { if (event.text) { thinking = event.text; } + options?.onThinking?.("", true); } // Handle main text output else if (event.type === "response.output_text.delta") { const delta = event.delta; content += delta; - options?.onText?.(delta); + options?.onText?.(delta, false); } else if (event.type === "response.output_text.done") { if (event.text) { content = event.text; } + options?.onText?.("", true); } // Handle function calls else if (event.type === "response.output_item.done") { diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 4b39cf2e..1d5e6703 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -1,8 +1,8 @@ export interface LLMOptions { temperature?: number; maxTokens?: number; - onText?: (text: string) => void; - onThinking?: (thinking: string) => void; + onText?: (text: string, complete: boolean) => void; + onThinking?: (thinking: string, complete: boolean) => void; signal?: AbortSignal; } diff --git a/packages/ai/test/examples/anthropic.ts b/packages/ai/test/examples/anthropic.ts index 53eda3d1..8017813a 100644 --- a/packages/ai/test/examples/anthropic.ts +++ b/packages/ai/test/examples/anthropic.ts @@ -24,8 +24,8 @@ const tools: Tool[] = [ ]; const options: AnthropicLLMOptions = { - onText: (t) => process.stdout.write(t), - onThinking: (t) => process.stdout.write(chalk.dim(t)), + onText: (t, complete) => process.stdout.write(t + (complete ? "\n" : "")), + onThinking: (t, complete) => process.stdout.write(chalk.dim(t + (complete ? "\n" : ""))), thinking: { enabled: true } }; const ai = new AnthropicLLM("claude-sonnet-4-0", process.env.ANTHROPIC_OAUTH_TOKEN ?? process.env.ANTHROPIC_API_KEY); diff --git a/packages/ai/test/examples/openai-completions.ts b/packages/ai/test/examples/openai-completions.ts index d1add4ed..6a60c969 100644 --- a/packages/ai/test/examples/openai-completions.ts +++ b/packages/ai/test/examples/openai-completions.ts @@ -21,8 +21,8 @@ const tools: Tool[] = [ ]; const options: OpenAICompletionsLLMOptions = { - onText: (t) => process.stdout.write(t), - onThinking: (t) => process.stdout.write(chalk.dim(t)), + onText: (t, complete) => process.stdout.write(t + (complete ? "\n" : "")), + onThinking: (t, complete) => process.stdout.write(chalk.dim(t + (complete ? "\n" : ""))), reasoningEffort: "medium", toolChoice: "auto" }; diff --git a/packages/ai/test/examples/openai-responses.ts b/packages/ai/test/examples/openai-responses.ts index a13996b4..923a797b 100644 --- a/packages/ai/test/examples/openai-responses.ts +++ b/packages/ai/test/examples/openai-responses.ts @@ -32,8 +32,8 @@ const context: Context = { } const options: OpenAIResponsesLLMOptions = { - onText: (t) => process.stdout.write(t), - onThinking: (t) => process.stdout.write(chalk.dim(t)), + onText: (t, complete) => process.stdout.write(t + (complete ? "\n" : "")), + onThinking: (t, complete) => process.stdout.write(chalk.dim(t + (complete ? "\n" : ""))), reasoningEffort: "low", reasoningSummary: "auto" };