@stacksjs/ai 0.75.86 → 0.75.87

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.
@@ -1,4 +1,4 @@
1
- import type { AIDriver, AIDriverConfig, AIMessage, AIResult, ChatCompletionOptions } from '../../types';
1
+ import type { AIDriver, AIDriverConfig, AIMessage, AIResult, AIStreamEvent, ChatCompletionOptions } from '../../types';
2
2
  /**
3
3
  * Configure Anthropic globally
4
4
  */
@@ -10,9 +10,22 @@ export declare function createAnthropicDriver(config: AnthropicDriverConfig): AI
10
10
  */
11
11
  export declare function chat(messages: AIMessage[], options?: ChatCompletionOptions & { system?: string }): Promise<AIResult>;
12
12
  /**
13
- * Stream chat completion
13
+ * A streamed chat completion, as events: text as it is written, each tool
14
+ * call once its arguments have streamed, then the whole result.
15
+ *
16
+ * A `tool_use` block streams its input as `input_json_delta` fragments, so a
17
+ * call is reported at its `content_block_stop`, whole. The structured-output
18
+ * tool is internal, as in `chat()`: its JSON streams as text. An `error`
19
+ * event (an overload mid-stream) throws rather than ending the stream as if
20
+ * the model had finished.
14
21
  */
15
- export declare function streamChat(messages: AIMessage[], options?: ChatCompletionOptions & { system?: string }): AsyncGenerator<string>;
22
+ export declare function streamChatEvents(messages: AIMessage[], options?: ChatCompletionOptions & { system?: string }): AsyncGenerator<AIStreamEvent>;
23
+ /**
24
+ * Stream chat completion: the text as it is written. Tool calls, usage and
25
+ * the stop reason are in the result the generator returns; iterate
26
+ * {@link streamChatEvents} to act on a tool call as soon as it arrives.
27
+ */
28
+ export declare function streamChat(messages: AIMessage[], options?: ChatCompletionOptions & { system?: string }): AsyncGenerator<string, AIResult, undefined>;
16
29
  /**
17
30
  * Simple prompt helper
18
31
  */
@@ -27,6 +40,7 @@ export declare const anthropic: {
27
40
  configure: typeof configure;
28
41
  chat: typeof chat;
29
42
  streamChat: typeof streamChat;
43
+ streamChatEvents: typeof streamChatEvents;
30
44
  prompt: typeof prompt;
31
45
  estimateTokens: typeof estimateTokens;
32
46
  createDriver: typeof createAnthropicDriver
@@ -37,4 +51,13 @@ export declare interface AnthropicDriverConfig extends AIDriverConfig {
37
51
  maxTokens?: number
38
52
  anthropicVersion?: string
39
53
  }
54
+ /** The Messages API's streamed events, as far as a chat completion reads them. */
55
+ declare type MessagesStreamEvent = | { type: 'message_start', message: { model: string, usage?: { input_tokens?: number, output_tokens?: number } } }
56
+ | { type: 'content_block_start', index: number, content_block: { type: string, id?: string, name?: string } }
57
+ | { type: 'content_block_delta', index: number, delta: { type: 'text_delta', text: string } | { type: 'input_json_delta', partial_json: string } | { type: string } }
58
+ | { type: 'content_block_stop', index: number }
59
+ | { type: 'message_delta', delta: { stop_reason?: string }, usage?: { output_tokens?: number } }
60
+ | { type: 'message_stop' }
61
+ | { type: 'ping' }
62
+ | { type: 'error', error: { type: string, message: string } }
40
63
  export default anthropic;
@@ -1,3 +1 @@
1
- import{fetchWithRetry}from"../../utils/retry";import{recordUsage}from"../../utils/usage";import{normalizeMessagesForProvider}from"../../utils/vision";import{DEFAULT_ANTHROPIC_MODEL}from"../../models";const DEFAULT_MODEL=DEFAULT_ANTHROPIC_MODEL,DEFAULT_MAX_TOKENS=4096,DEFAULT_VERSION="2023-06-01",BASE_URL="https://api.anthropic.com/v1";let globalConfig=null;export function configure(config){globalConfig=config}function getConfig(config){const merged={...globalConfig,...config};if(!merged.apiKey)merged.apiKey=process.env.ANTHROPIC_API_KEY||"";return merged}export function createAnthropicDriver(config){const{apiKey,model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,anthropicVersion=DEFAULT_VERSION}=config;return{name:"Claude API",async process(command,systemPrompt,history){if(!apiKey)throw Error("Anthropic API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${BASE_URL}/messages`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":apiKey,"anthropic-version":anthropicVersion},body:JSON.stringify({model,max_tokens:maxTokens,system:systemPrompt,messages:[...history,{role:"user",content:command}]})});if(!response.ok){const error=await response.text();throw Error(`Claude API error: ${error}`)}const data=await response.json();if(!data.content||data.content.length===0)throw Error("Claude API returned empty content");return data.content[0].text},async*stream(command,systemPrompt,history){if(!apiKey)throw Error("Anthropic API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${BASE_URL}/messages`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":apiKey,"anthropic-version":anthropicVersion},body:JSON.stringify({model,max_tokens:maxTokens,system:systemPrompt,stream:!0,messages:[...history,{role:"user",content:command}]})});if(!response.ok){const error=await response.text();throw Error(`Claude API error: ${error}`)}const reader=response.body?.getReader();if(!reader)throw Error("No response body");const decoder=new TextDecoder;let buffer="";const handlePayload=function*(data){if(data==="[DONE]")return;let event;try{event=JSON.parse(data)}catch{return}if(event.type==="error"){const msg=event.error?.message??JSON.stringify(event.error??event);throw Error(`[anthropic/stream] mid-stream error: ${msg}`)}if(event.type==="content_block_delta"&&event.delta?.text)yield event.delta.text};while(!0){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0});const lines=buffer.split(`
2
- `);buffer=lines.pop()||"";for(const line of lines)if(line.startsWith("data: "))yield*handlePayload(line.slice(6))}if(buffer.startsWith("data: "))yield*handlePayload(buffer.slice(6))}}}export async function chat(messages,options={}){const config=getConfig(),{model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,temperature,topP,stop,system,tools,toolChoice,responseFormat}=options,normalizedMessages=normalizeMessagesForProvider(messages,"anthropic"),startedAt=Date.now(),body={model,max_tokens:maxTokens,temperature,top_p:topP,stop_sequences:stop?Array.isArray(stop)?stop:[stop]:void 0,system,messages:normalizedMessages};if(tools&&tools.length>0){body.tools=tools.map((t)=>({name:t.name,description:t.description,input_schema:t.parameters??{type:"object",properties:{}}}));if(toolChoice!==void 0)body.tool_choice=mapAnthropicToolChoice(toolChoice)}if(responseFormat&&responseFormat.type!=="text"){const outputTool=buildAnthropicJsonTool(responseFormat),existing=Array.isArray(body.tools)?body.tools:[];body.tools=[...existing,outputTool];if(toolChoice===void 0)body.tool_choice={type:"tool",name:outputTool.name}}const response=await fetchWithRetry(`${BASE_URL}/messages`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":config.apiKey,"anthropic-version":config.anthropicVersion||DEFAULT_VERSION},body:JSON.stringify(body)});if(!response.ok){const error=await response.text();throw Error(`Claude API error: ${error}`)}const data=await response.json();if(!data.content||data.content.length===0)throw Error("Claude API returned empty content");const outputTool=responseFormat&&responseFormat.type!=="text"?buildAnthropicJsonTool(responseFormat).name:void 0,blocks=data.content,structured=outputTool?blocks.find((b)=>b.type==="tool_use"&&b.name===outputTool):void 0,toolCalls=blocks.filter((b)=>b.type==="tool_use"&&b!==structured).map((b)=>({id:b.id??"",name:b.name??"",arguments:b.input??{}})),result={content:structured?JSON.stringify(structured.input):blocks.filter((b)=>b.type==="text").map((b)=>b.text??"").join(""),...toolCalls.length>0?{toolCalls}:{},model:data.model,usage:{promptTokens:data.usage?.input_tokens||0,completionTokens:data.usage?.output_tokens||0,totalTokens:(data.usage?.input_tokens||0)+(data.usage?.output_tokens||0)},finishReason:data.stop_reason};recordUsage({provider:"anthropic",model:data.model,promptTokens:result.usage.promptTokens,completionTokens:result.usage.completionTokens,totalTokens:result.usage.totalTokens,durationMs:Date.now()-startedAt,timestamp:Date.now()});return result}function mapAnthropicToolChoice(choice){if(choice==="auto")return{type:"auto"};if(choice==="required")return{type:"any"};if(choice==="none")return{type:"none"};return{type:"tool",name:choice.name}}function buildAnthropicJsonTool(format){if(format.type==="json_schema")return{name:format.json_schema.name,description:`Returns the result as JSON matching the '${format.json_schema.name}' schema.`,input_schema:format.json_schema.schema};return{name:"structured_output",description:"Returns the result as a JSON object.",input_schema:{type:"object",additionalProperties:!0}}}export async function*streamChat(messages,options={}){const config=getConfig(),{model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,temperature,topP,stop,system}=options,response=await fetchWithRetry(`${BASE_URL}/messages`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":config.apiKey,"anthropic-version":config.anthropicVersion||DEFAULT_VERSION},body:JSON.stringify({model,max_tokens:maxTokens,temperature,top_p:topP,stop_sequences:stop?Array.isArray(stop)?stop:[stop]:void 0,system,stream:!0,messages:normalizeMessagesForProvider(messages,"anthropic")})});if(!response.ok){const error=await response.text();throw Error(`Claude API error: ${error}`)}const reader=response.body?.getReader();if(!reader)throw Error("No response body");const decoder=new TextDecoder;let buffer="";while(!0){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0});const lines=buffer.split(`
3
- `);buffer=lines.pop()||"";for(const line of lines)if(line.startsWith("data: ")){const data=line.slice(6);if(data==="[DONE]")continue;try{const event=JSON.parse(data);if(event.type==="content_block_delta"&&event.delta?.text)yield event.delta.text}catch{}}}}export async function prompt(text,options={}){return(await chat([{role:"user",content:text}],options)).content}export function estimateTokens(text){return Math.ceil(text.length/4)}export const anthropicDriver={create:createAnthropicDriver},anthropic={configure,chat,streamChat,prompt,estimateTokens,createDriver:createAnthropicDriver};export default anthropic;
1
+ import{fetchWithRetry}from"../../utils/retry";import{sseJson,streamText}from"../../utils/stream";import{parseToolArguments}from"../../utils/tools";import{recordUsage}from"../../utils/usage";import{normalizeMessagesForProvider}from"../../utils/vision";import{DEFAULT_ANTHROPIC_MODEL}from"../../models";const DEFAULT_MODEL=DEFAULT_ANTHROPIC_MODEL,DEFAULT_MAX_TOKENS=4096,DEFAULT_VERSION="2023-06-01",BASE_URL="https://api.anthropic.com/v1";let globalConfig=null;export function configure(config){globalConfig=config}function getConfig(config){const merged={...globalConfig,...config};if(!merged.apiKey)merged.apiKey=process.env.ANTHROPIC_API_KEY||"";return merged}export function createAnthropicDriver(config){const{apiKey,model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,anthropicVersion=DEFAULT_VERSION}=config;return{name:"Claude API",async process(command,systemPrompt,history){if(!apiKey)throw Error("Anthropic API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${BASE_URL}/messages`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":apiKey,"anthropic-version":anthropicVersion},body:JSON.stringify({model,max_tokens:maxTokens,system:systemPrompt,messages:[...history,{role:"user",content:command}]})});if(!response.ok){const error=await response.text();throw Error(`Claude API error: ${error}`)}const data=await response.json();if(!data.content||data.content.length===0)throw Error("Claude API returned empty content");return data.content[0].text},async*stream(command,systemPrompt,history){if(!apiKey)throw Error("Anthropic API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${BASE_URL}/messages`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":apiKey,"anthropic-version":anthropicVersion},body:JSON.stringify({model,max_tokens:maxTokens,system:systemPrompt,stream:!0,messages:[...history,{role:"user",content:command}]})});if(!response.ok){const error=await response.text();throw Error(`Claude API error: ${error}`)}if(!response.body)throw Error("No response body");for await(const event of sseJson(response.body,"Claude API")){if(event.type==="error"){const msg=event.error?.message??JSON.stringify(event.error??event);throw Error(`[anthropic/stream] mid-stream error: ${msg}`)}if(event.type==="content_block_delta"&&event.delta?.text)yield event.delta.text}}}}function messagesRequest(messages,options){const{model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,temperature,topP,stop,system,tools,toolChoice,responseFormat}=options,body={model,max_tokens:maxTokens,temperature,top_p:topP,stop_sequences:stop?Array.isArray(stop)?stop:[stop]:void 0,system,messages:normalizeMessagesForProvider(messages,"anthropic")};if(tools&&tools.length>0){body.tools=tools.map((t)=>({name:t.name,description:t.description,input_schema:t.parameters??{type:"object",properties:{}}}));if(toolChoice!==void 0)body.tool_choice=mapAnthropicToolChoice(toolChoice)}if(responseFormat&&responseFormat.type!=="text"){const outputTool=buildAnthropicJsonTool(responseFormat),existing=Array.isArray(body.tools)?body.tools:[];body.tools=[...existing,outputTool];if(toolChoice===void 0)body.tool_choice={type:"tool",name:outputTool.name};return{body,outputTool:outputTool.name}}return{body}}function postMessages(body){const config=getConfig();return fetchWithRetry(`${BASE_URL}/messages`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":config.apiKey,"anthropic-version":config.anthropicVersion||DEFAULT_VERSION},body:JSON.stringify(body)})}export async function chat(messages,options={}){const startedAt=Date.now(),{body,outputTool}=messagesRequest(messages,options),response=await postMessages(body);if(!response.ok){const error=await response.text();throw Error(`Claude API error: ${error}`)}const data=await response.json();if(!data.content||data.content.length===0)throw Error("Claude API returned empty content");const blocks=data.content,structured=outputTool?blocks.find((b)=>b.type==="tool_use"&&b.name===outputTool):void 0,toolCalls=blocks.filter((b)=>b.type==="tool_use"&&b!==structured).map((b)=>({id:b.id??"",name:b.name??"",arguments:b.input??{}})),result={content:structured?JSON.stringify(structured.input):blocks.filter((b)=>b.type==="text").map((b)=>b.text??"").join(""),...toolCalls.length>0?{toolCalls}:{},model:data.model,usage:{promptTokens:data.usage?.input_tokens||0,completionTokens:data.usage?.output_tokens||0,totalTokens:(data.usage?.input_tokens||0)+(data.usage?.output_tokens||0)},finishReason:data.stop_reason};recordUsage({provider:"anthropic",model:data.model,promptTokens:result.usage.promptTokens,completionTokens:result.usage.completionTokens,totalTokens:result.usage.totalTokens,durationMs:Date.now()-startedAt,timestamp:Date.now()});return result}function mapAnthropicToolChoice(choice){if(choice==="auto")return{type:"auto"};if(choice==="required")return{type:"any"};if(choice==="none")return{type:"none"};return{type:"tool",name:choice.name}}function buildAnthropicJsonTool(format){if(format.type==="json_schema")return{name:format.json_schema.name,description:`Returns the result as JSON matching the '${format.json_schema.name}' schema.`,input_schema:format.json_schema.schema};return{name:"structured_output",description:"Returns the result as a JSON object.",input_schema:{type:"object",additionalProperties:!0}}}export async function*streamChatEvents(messages,options={}){const startedAt=Date.now(),{body,outputTool}=messagesRequest(messages,options),response=await postMessages({...body,stream:!0});if(!response.ok){const error=await response.text();throw Error(`Claude API error: ${error}`)}if(!response.body)throw Error("No response body");const toolBlocks=new Map,toolCalls=[];let content="",model=String(body.model),promptTokens=0,completionTokens=0,finishReason;for await(const event of sseJson(response.body,"Claude API"))switch(event.type){case"message_start":model=event.message.model||model;promptTokens=event.message.usage?.input_tokens??0;completionTokens=event.message.usage?.output_tokens??0;break;case"content_block_start":if(event.content_block.type==="tool_use"){const name=event.content_block.name??"";toolBlocks.set(event.index,{id:event.content_block.id??"",name,json:"",structured:name===outputTool})}break;case"content_block_delta":{const delta=event.delta;if("text"in delta&&delta.type==="text_delta"){content+=delta.text;yield{type:"text",text:delta.text}}else if("partial_json"in delta&&delta.type==="input_json_delta"){const block=toolBlocks.get(event.index);if(!block)break;block.json+=delta.partial_json;if(block.structured&&delta.partial_json){content+=delta.partial_json;yield{type:"text",text:delta.partial_json}}}break}case"content_block_stop":{const block=toolBlocks.get(event.index);if(block&&!block.structured){const call={id:block.id,name:block.name,arguments:parseToolArguments(block.json,block.name)};toolCalls.push(call);yield{type:"tool_call",call}}break}case"message_delta":finishReason=event.delta.stop_reason??finishReason;completionTokens=event.usage?.output_tokens??completionTokens;break;case"error":throw Error(`Claude API error: ${event.error.type}: ${event.error.message}`)}const result={content,...toolCalls.length>0?{toolCalls}:{},model,usage:{promptTokens,completionTokens,totalTokens:promptTokens+completionTokens},...finishReason?{finishReason}:{}};recordUsage({provider:"anthropic",model,promptTokens,completionTokens,totalTokens:promptTokens+completionTokens,durationMs:Date.now()-startedAt,timestamp:Date.now()});yield{type:"done",result}}export function streamChat(messages,options={}){return streamText(streamChatEvents(messages,options))}export async function prompt(text,options={}){return(await chat([{role:"user",content:text}],options)).content}export function estimateTokens(text){return Math.ceil(text.length/4)}export const anthropicDriver={create:createAnthropicDriver},anthropic={configure,chat,streamChat,streamChatEvents,prompt,estimateTokens,createDriver:createAnthropicDriver};export default anthropic;
@@ -1,4 +1,4 @@
1
- import type { AIMessage, AIResult, ChatCompletionOptions } from '../../types';
1
+ import type { AIMessage, AIResult, AIStreamEvent, ChatCompletionOptions } from '../../types';
2
2
  import type { ConverseCommandInput } from '@stacksjs/ts-cloud/aws';
3
3
  export declare function configure(config: BedrockDriverConfig): void;
4
4
  /**
@@ -10,6 +10,23 @@ export declare function toConverseMessages(messages: AIMessage[], system?: strin
10
10
  * One chat completion. `options.model` overrides the configured model.
11
11
  */
12
12
  export declare function chat(messages: AIMessage[], options?: ChatCompletionOptions & { system?: string }): Promise<AIResult>;
13
+ /**
14
+ * A streamed chat completion, as events: text as it is written, each tool
15
+ * call once its arguments have streamed, then the whole result.
16
+ *
17
+ * Converse streams a tool call as a `contentBlockStart` carrying its id and
18
+ * name, then `toolUse.input` fragments of one JSON string, so a call is
19
+ * reported at its `contentBlockStop`, whole. The structured-output tool is
20
+ * internal, as in `chat()`: its JSON streams as text. Usage arrives last, in
21
+ * `metadata`.
22
+ */
23
+ export declare function streamChatEvents(messages: AIMessage[], options?: ChatCompletionOptions & { system?: string }): AsyncGenerator<AIStreamEvent>;
24
+ /**
25
+ * Stream chat completion: the text as it is written. Tool calls, usage and
26
+ * the stop reason are in the result the generator returns; iterate
27
+ * {@link streamChatEvents} to act on a tool call as soon as it arrives.
28
+ */
29
+ export declare function streamChat(messages: AIMessage[], options?: ChatCompletionOptions & { system?: string }): AsyncGenerator<string, AIResult, undefined>;
13
30
  /**
14
31
  * Invocable on demand, with no inference profile and no agreement, so it
15
32
  * answers in a fresh account. The Titan Text model this replaces as the
@@ -18,7 +35,9 @@ export declare function chat(messages: AIMessage[], options?: ChatCompletionOpti
18
35
  export declare const DEFAULT_BEDROCK_MODEL: 'amazon.nova-lite-v1:0';
19
36
  export declare const bedrock: {
20
37
  configure: typeof configure;
21
- chat: typeof chat
38
+ chat: typeof chat;
39
+ streamChat: typeof streamChat;
40
+ streamChatEvents: typeof streamChatEvents
22
41
  };
23
42
  export declare interface BedrockDriverConfig {
24
43
  model?: string
@@ -1,2 +1,2 @@
1
- import{converse}from"../../utils/client-bedrock-runtime";import{recordUsage}from"../../utils/usage";export const DEFAULT_BEDROCK_MODEL="amazon.nova-lite-v1:0";let globalConfig={};export function configure(config){globalConfig={...globalConfig,...config}}function resolveConfig(){return{...globalConfig,model:globalConfig.model||process.env.BEDROCK_MODEL_ID||DEFAULT_BEDROCK_MODEL}}const IMAGE_FORMATS=new Set(["png","jpeg","gif","webp"]);function imageBlock(mediaType,data){const format=mediaType.replace(/^image\//,"").replace("jpg","jpeg");if(!IMAGE_FORMATS.has(format))throw Error(`Bedrock Converse takes png, jpeg, gif or webp images, not ${mediaType}`);return{image:{format,source:{bytes:data}}}}function contentBlock(part){if(part.type==="text")return{text:part.text??""};if(part.type==="tool_call"&&part.toolCall)return{toolUse:{toolUseId:part.toolCall.id,name:part.toolCall.name,input:part.toolCall.arguments}};if(part.type==="tool_result"&&part.toolResult)return{toolResult:{toolUseId:part.toolResult.toolCallId,content:[{text:part.toolResult.content}],...part.toolResult.isError?{status:"error"}:{}}};if(part.type==="image"&&part.source)return imageBlock(part.source.media_type,part.source.data);if(part.type==="image_url"&&part.image_url){const inline=/^data:([^;]+);base64,(.*)$/.exec(part.image_url.url);if(!inline)throw Error("Bedrock Converse takes image bytes, not a URL: pass the image as a data: URL or a base64 `image` block");return imageBlock(inline[1],inline[2])}throw Error(`Unsupported message content for Bedrock: ${part.type}`)}export function toConverseMessages(messages,system){const systemBlocks=system?[{text:system}]:[],turns=[];for(const message of messages){if(message.role==="system"){const text=typeof message.content==="string"?message.content:message.content.filter((part)=>part.type==="text").map((part)=>part.text??"").join(`
2
- `);if(text)systemBlocks.push({text});continue}turns.push({role:message.role,content:typeof message.content==="string"?[{text:message.content}]:message.content.map(contentBlock)})}return{messages:turns,...systemBlocks.length>0?{system:systemBlocks}:{}}}function structuredOutputTool(format){if(format.type==="json_schema")return{name:format.json_schema.name,description:`Returns the result as JSON matching the '${format.json_schema.name}' schema.`,inputSchema:{json:format.json_schema.schema}};return{name:"structured_output",description:"Returns the result as a JSON object.",inputSchema:{json:{type:"object",additionalProperties:!0}}}}function toolConfig(options){const{tools=[],toolChoice,responseFormat}=options,specs=toolChoice==="none"?[]:tools.map((tool)=>({name:tool.name,description:tool.description,inputSchema:{json:tool.parameters??{type:"object",properties:{}}}}));let choice;if(toolChoice==="required")choice={any:{}};else if(toolChoice&&typeof toolChoice==="object")choice={tool:{name:toolChoice.name}};else if(toolChoice==="auto")choice={auto:{}};if(responseFormat&&responseFormat.type!=="text"){const output=structuredOutputTool(responseFormat);specs.push(output);choice=choice??{tool:{name:output.name}}}if(specs.length===0)return;return{tools:specs.map((toolSpec)=>({toolSpec})),...choice?{toolChoice:choice}:{}}}function readOutput(output,outputTool){const blocks=output.output.message?.content??[],uses=blocks.filter((block)=>("toolUse"in block)),structured=outputTool?uses.find((block)=>block.toolUse.name===outputTool):void 0,toolCalls=uses.filter((block)=>block!==structured).map((block)=>({id:block.toolUse.toolUseId,name:block.toolUse.name,arguments:block.toolUse.input??{}}));return{content:structured?JSON.stringify(structured.toolUse.input):blocks.filter((block)=>("text"in block)).map((block)=>block.text).join(""),...toolCalls.length>0?{toolCalls}:{}}}export async function chat(messages,options={}){const config=resolveConfig(),model=options.model||config.model,startedAt=Date.now(),inferenceConfig={},maxTokens=options.maxTokens??config.maxTokens;if(maxTokens!==void 0)inferenceConfig.maxTokens=maxTokens;if(options.temperature!==void 0)inferenceConfig.temperature=options.temperature;if(options.topP!==void 0)inferenceConfig.topP=options.topP;if(options.stop!==void 0)inferenceConfig.stopSequences=Array.isArray(options.stop)?options.stop:[options.stop];const tools=toolConfig(options),response=await converse({modelId:model,...toConverseMessages(messages,options.system),...Object.keys(inferenceConfig).length>0?{inferenceConfig}:{},...tools?{toolConfig:tools}:{}},config.region),outputTool=options.responseFormat&&options.responseFormat.type!=="text"?structuredOutputTool(options.responseFormat).name:void 0,result={...readOutput(response,outputTool),model,usage:{promptTokens:response.usage?.inputTokens??0,completionTokens:response.usage?.outputTokens??0,totalTokens:response.usage?.totalTokens??0},finishReason:response.stopReason};recordUsage({provider:"bedrock",model,promptTokens:result.usage.promptTokens,completionTokens:result.usage.completionTokens,totalTokens:result.usage.totalTokens,durationMs:Date.now()-startedAt,timestamp:Date.now()});return result}export const bedrock={configure,chat};
1
+ import{converse,converseStream}from"../../utils/client-bedrock-runtime";import{streamText}from"../../utils/stream";import{parseToolArguments}from"../../utils/tools";import{recordUsage}from"../../utils/usage";export const DEFAULT_BEDROCK_MODEL="amazon.nova-lite-v1:0";let globalConfig={};export function configure(config){globalConfig={...globalConfig,...config}}function resolveConfig(){return{...globalConfig,model:globalConfig.model||process.env.BEDROCK_MODEL_ID||DEFAULT_BEDROCK_MODEL}}const IMAGE_FORMATS=new Set(["png","jpeg","gif","webp"]);function imageBlock(mediaType,data){const format=mediaType.replace(/^image\//,"").replace("jpg","jpeg");if(!IMAGE_FORMATS.has(format))throw Error(`Bedrock Converse takes png, jpeg, gif or webp images, not ${mediaType}`);return{image:{format,source:{bytes:data}}}}function contentBlock(part){if(part.type==="text")return{text:part.text??""};if(part.type==="tool_call"&&part.toolCall)return{toolUse:{toolUseId:part.toolCall.id,name:part.toolCall.name,input:part.toolCall.arguments}};if(part.type==="tool_result"&&part.toolResult)return{toolResult:{toolUseId:part.toolResult.toolCallId,content:[{text:part.toolResult.content}],...part.toolResult.isError?{status:"error"}:{}}};if(part.type==="image"&&part.source)return imageBlock(part.source.media_type,part.source.data);if(part.type==="image_url"&&part.image_url){const inline=/^data:([^;]+);base64,(.*)$/.exec(part.image_url.url);if(!inline)throw Error("Bedrock Converse takes image bytes, not a URL: pass the image as a data: URL or a base64 `image` block");return imageBlock(inline[1],inline[2])}throw Error(`Unsupported message content for Bedrock: ${part.type}`)}export function toConverseMessages(messages,system){const systemBlocks=system?[{text:system}]:[],turns=[];for(const message of messages){if(message.role==="system"){const text=typeof message.content==="string"?message.content:message.content.filter((part)=>part.type==="text").map((part)=>part.text??"").join(`
2
+ `);if(text)systemBlocks.push({text});continue}turns.push({role:message.role,content:typeof message.content==="string"?[{text:message.content}]:message.content.map(contentBlock)})}return{messages:turns,...systemBlocks.length>0?{system:systemBlocks}:{}}}function structuredOutputTool(format){if(format.type==="json_schema")return{name:format.json_schema.name,description:`Returns the result as JSON matching the '${format.json_schema.name}' schema.`,inputSchema:{json:format.json_schema.schema}};return{name:"structured_output",description:"Returns the result as a JSON object.",inputSchema:{json:{type:"object",additionalProperties:!0}}}}function toolConfig(options){const{tools=[],toolChoice,responseFormat}=options,specs=toolChoice==="none"?[]:tools.map((tool)=>({name:tool.name,description:tool.description,inputSchema:{json:tool.parameters??{type:"object",properties:{}}}}));let choice;if(toolChoice==="required")choice={any:{}};else if(toolChoice&&typeof toolChoice==="object")choice={tool:{name:toolChoice.name}};else if(toolChoice==="auto")choice={auto:{}};if(responseFormat&&responseFormat.type!=="text"){const output=structuredOutputTool(responseFormat);specs.push(output);choice=choice??{tool:{name:output.name}}}if(specs.length===0)return;return{tools:specs.map((toolSpec)=>({toolSpec})),...choice?{toolChoice:choice}:{}}}function readOutput(output,outputTool){const blocks=output.output.message?.content??[],uses=blocks.filter((block)=>("toolUse"in block)),structured=outputTool?uses.find((block)=>block.toolUse.name===outputTool):void 0,toolCalls=uses.filter((block)=>block!==structured).map((block)=>({id:block.toolUse.toolUseId,name:block.toolUse.name,arguments:block.toolUse.input??{}}));return{content:structured?JSON.stringify(structured.toolUse.input):blocks.filter((block)=>("text"in block)).map((block)=>block.text).join(""),...toolCalls.length>0?{toolCalls}:{}}}function converseRequest(messages,options){const config=resolveConfig(),model=options.model||config.model,inferenceConfig={},maxTokens=options.maxTokens??config.maxTokens;if(maxTokens!==void 0)inferenceConfig.maxTokens=maxTokens;if(options.temperature!==void 0)inferenceConfig.temperature=options.temperature;if(options.topP!==void 0)inferenceConfig.topP=options.topP;if(options.stop!==void 0)inferenceConfig.stopSequences=Array.isArray(options.stop)?options.stop:[options.stop];const tools=toolConfig(options),outputTool=options.responseFormat&&options.responseFormat.type!=="text"?structuredOutputTool(options.responseFormat).name:void 0;return{input:{modelId:model,...toConverseMessages(messages,options.system),...Object.keys(inferenceConfig).length>0?{inferenceConfig}:{},...tools?{toolConfig:tools}:{}},...outputTool?{outputTool}:{},region:config.region}}function report(model,usage,startedAt){recordUsage({provider:"bedrock",model,promptTokens:usage.promptTokens,completionTokens:usage.completionTokens,totalTokens:usage.totalTokens,durationMs:Date.now()-startedAt,timestamp:Date.now()})}export async function chat(messages,options={}){const startedAt=Date.now(),{input,outputTool,region}=converseRequest(messages,options),response=await converse(input,region),result={...readOutput(response,outputTool),model:input.modelId,usage:{promptTokens:response.usage?.inputTokens??0,completionTokens:response.usage?.outputTokens??0,totalTokens:response.usage?.totalTokens??0},finishReason:response.stopReason};report(input.modelId,result.usage,startedAt);return result}export async function*streamChatEvents(messages,options={}){const startedAt=Date.now(),{input,outputTool,region}=converseRequest(messages,options),{stream}=await converseStream(input,region),toolBlocks=new Map,toolCalls=[];let content="",usage,finishReason;for await(const event of stream)if("contentBlockStart"in event){const use=event.contentBlockStart.start.toolUse;if(use)toolBlocks.set(event.contentBlockStart.contentBlockIndex,{id:use.toolUseId,name:use.name,json:"",structured:use.name===outputTool})}else if("contentBlockDelta"in event){const{contentBlockIndex,delta}=event.contentBlockDelta;if(delta.text){content+=delta.text;yield{type:"text",text:delta.text}}else if(delta.toolUse){const block=toolBlocks.get(contentBlockIndex);if(!block)continue;block.json+=delta.toolUse.input;if(block.structured&&delta.toolUse.input){content+=delta.toolUse.input;yield{type:"text",text:delta.toolUse.input}}}}else if("contentBlockStop"in event){const block=toolBlocks.get(event.contentBlockStop.contentBlockIndex);if(block&&!block.structured){const call={id:block.id,name:block.name,arguments:parseToolArguments(block.json,block.name)};toolCalls.push(call);yield{type:"tool_call",call}}}else if("messageStop"in event)finishReason=event.messageStop.stopReason;else if("metadata"in event)usage={promptTokens:event.metadata.usage.inputTokens,completionTokens:event.metadata.usage.outputTokens,totalTokens:event.metadata.usage.totalTokens};if(usage)report(input.modelId,usage,startedAt);yield{type:"done",result:{content,...toolCalls.length>0?{toolCalls}:{},model:input.modelId,...usage?{usage}:{},...finishReason?{finishReason}:{}}}}export function streamChat(messages,options={}){return streamText(streamChatEvents(messages,options))}export const bedrock={configure,chat,streamChat,streamChatEvents};
@@ -1,4 +1,4 @@
1
- import type { AIDriver, AIDriverConfig, AIMessage, AIResult, ChatCompletionOptions } from '../../types';
1
+ import type { AIDriver, AIDriverConfig, AIMessage, AIResult, AIStreamEvent, ChatCompletionOptions } from '../../types';
2
2
  /**
3
3
  * Configure Ollama globally
4
4
  */
@@ -9,9 +9,20 @@ export declare function createOllamaDriver(config?: OllamaDriverConfig): AIDrive
9
9
  */
10
10
  export declare function chat(messages: AIMessage[], options?: ChatCompletionOptions): Promise<AIResult>;
11
11
  /**
12
- * Stream chat completion
12
+ * A streamed chat completion, as events: text as it is written, each tool
13
+ * call as it arrives, then the whole result.
14
+ *
15
+ * Ollama streams newline-delimited JSON. A tool call arrives whole, on the
16
+ * message of whichever chunk carries it, and the counts and stop reason on
17
+ * the last (`done: true`) one. An `error` line throws.
13
18
  */
14
- export declare function streamChat(messages: AIMessage[], options?: ChatCompletionOptions): AsyncGenerator<string>;
19
+ export declare function streamChatEvents(messages: AIMessage[], options?: ChatCompletionOptions): AsyncGenerator<AIStreamEvent>;
20
+ /**
21
+ * Stream chat completion: the text as it is written. Tool calls, usage and
22
+ * the stop reason are in the result the generator returns; iterate
23
+ * {@link streamChatEvents} to act on a tool call as soon as it arrives.
24
+ */
25
+ export declare function streamChat(messages: AIMessage[], options?: ChatCompletionOptions): AsyncGenerator<string, AIResult, undefined>;
15
26
  /**
16
27
  * Generate text completion (non-chat)
17
28
  */
@@ -76,6 +87,7 @@ export declare const ollama: {
76
87
  configure: typeof configure;
77
88
  chat: typeof chat;
78
89
  streamChat: typeof streamChat;
90
+ streamChatEvents: typeof streamChatEvents;
79
91
  generate: typeof generate;
80
92
  embed: typeof embed;
81
93
  listModels: typeof listModels;
@@ -90,4 +102,5 @@ export declare interface OllamaDriverConfig extends AIDriverConfig {
90
102
  model?: string
91
103
  embeddingModel?: string
92
104
  }
105
+ declare type OllamaToolCalls = Array<{ id?: string, function: { name: string, arguments: unknown } }>;
93
106
  export default ollama;
@@ -1,4 +1,2 @@
1
- import{parseToolArguments,toOllamaMessages}from"../../utils/tools";const DEFAULT_HOST="http://localhost:11434",DEFAULT_MODEL="llama3.2",DEFAULT_EMBEDDING_MODEL="nomic-embed-text";let globalConfig={};export function configure(config){globalConfig={...globalConfig,...config}}function getConfig(config){return{host:config?.host||globalConfig.host||process.env.OLLAMA_HOST||DEFAULT_HOST,model:config?.model||globalConfig.model||process.env.OLLAMA_MODEL||DEFAULT_MODEL,embeddingModel:config?.embeddingModel||globalConfig.embeddingModel||DEFAULT_EMBEDDING_MODEL}}export function createOllamaDriver(config={}){const{host=process.env.OLLAMA_HOST||DEFAULT_HOST,model=process.env.OLLAMA_MODEL||DEFAULT_MODEL,embeddingModel=DEFAULT_EMBEDDING_MODEL}=config;return{name:"Ollama",async process(command,systemPrompt,history){const response=await fetch(`${host}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model,messages:[{role:"system",content:systemPrompt},...history,{role:"user",content:command}],stream:!1})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}return(await response.json()).message.content},async*stream(command,systemPrompt,history){const response=await fetch(`${host}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model,messages:[{role:"system",content:systemPrompt},...history,{role:"user",content:command}],stream:!0})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}const reader=response.body?.getReader();if(!reader)throw Error("No response body");const decoder=new TextDecoder;let buffer="";while(!0){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0});const lines=buffer.split(`
2
- `);buffer=lines.pop()||"";for(const line of lines){if(!line.trim())continue;try{const data=JSON.parse(line);if(data.message?.content)yield data.message.content}catch{}}}},async embed(input){const inputs=Array.isArray(input)?input:[input],embeddings=[];for(const text of inputs){const response=await fetch(`${host}/api/embeddings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:embeddingModel,prompt:text})});if(!response.ok){const error=await response.text();throw Error(`Ollama Embeddings API error: ${error}`)}const data=await response.json();embeddings.push(data.embedding)}return Array.isArray(input)?embeddings:embeddings[0]}}}export async function chat(messages,options={}){const config=getConfig(),{model=config.model,temperature,topP,stop,responseFormat,tools,toolChoice}=options;if(toolChoice==="required"||toolChoice&&typeof toolChoice==="object")throw Error("Ollama cannot be made to call a tool: offer the tools with toolChoice 'auto', or use another driver");const offered=toolChoice==="none"?[]:tools??[],format=responseFormat?.type==="json_schema"?responseFormat.json_schema.schema:responseFormat?.type==="json_object"?"json":void 0,response=await fetch(`${config.host}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model,messages:toOllamaMessages(messages),stream:!1,format,...offered.length>0?{tools:offered.map((tool)=>({type:"function",function:{name:tool.name,description:tool.description,parameters:tool.parameters??{type:"object",properties:{}}}}))}:{},options:{temperature,top_p:topP,stop}})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}const data=await response.json(),toolCalls=(data.message?.tool_calls??[]).map((call,index)=>({id:call.id??`call_${index}`,name:call.function.name,arguments:parseToolArguments(call.function.arguments,call.function.name)}));return{content:data.message?.content??"",...toolCalls.length>0?{toolCalls}:{},model:data.model,usage:{promptTokens:data.prompt_eval_count||0,completionTokens:data.eval_count||0,totalTokens:(data.prompt_eval_count||0)+(data.eval_count||0)},finishReason:data.done_reason||"stop"}}export async function*streamChat(messages,options={}){const config=getConfig(),{model=config.model,temperature,topP,stop}=options,response=await fetch(`${config.host}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model,messages:toOllamaMessages(messages),stream:!0,options:{temperature,top_p:topP,stop}})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}const reader=response.body?.getReader();if(!reader)throw Error("No response body");const decoder=new TextDecoder;let buffer="";while(!0){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0});const lines=buffer.split(`
3
- `);buffer=lines.pop()||"";for(const line of lines){if(!line.trim())continue;try{const data=JSON.parse(line);if(data.message?.content)yield data.message.content}catch{}}}}export async function generate(prompt,options={}){const config=getConfig(),response=await fetch(`${config.host}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:options.model||config.model,prompt,system:options.system,template:options.template,context:options.context,raw:options.raw,format:options.format,images:options.images,stream:!1})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}const data=await response.json();return{content:data.response,model:data.model,usage:{promptTokens:data.prompt_eval_count||0,completionTokens:data.eval_count||0,totalTokens:(data.prompt_eval_count||0)+(data.eval_count||0)}}}export async function embed(input,model){const config=getConfig(),embeddingModel=model||config.embeddingModel,inputs=Array.isArray(input)?input:[input],embeddings=[];for(const text of inputs){const response=await fetch(`${config.host}/api/embeddings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:embeddingModel,prompt:text})});if(!response.ok){const error=await response.text();throw Error(`Ollama Embeddings API error: ${error}`)}const data=await response.json();embeddings.push(data.embedding)}return Array.isArray(input)?embeddings:embeddings[0]}export async function listModels(){const config=getConfig(),response=await fetch(`${config.host}/api/tags`,{method:"GET"});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}return(await response.json()).models}export async function pullModel(name,onProgress){const config=getConfig(),response=await fetch(`${config.host}/api/pull`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name,stream:!0})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}const reader=response.body?.getReader();if(!reader)return;const decoder=new TextDecoder;let buffer="";while(!0){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0});const lines=buffer.split(`
4
- `);buffer=lines.pop()||"";for(const line of lines){if(!line.trim())continue;try{const data=JSON.parse(line);if(onProgress)onProgress(data.status??"",data.completed,data.total)}catch{}}}}export async function deleteModel(name){const config=getConfig(),response=await fetch(`${config.host}/api/delete`,{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({name})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}}export async function showModel(_name){const config=getConfig(),response=await fetch(`${config.host}/api/show`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:_name})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}return response.json()}export async function isRunning(){const config=getConfig();try{return(await fetch(`${config.host}/api/tags`,{method:"GET"})).ok}catch{return!1}}export const ollamaDriver={create:createOllamaDriver},ollama={configure,chat,streamChat,generate,embed,listModels,pullModel,deleteModel,showModel,isRunning,createDriver:createOllamaDriver};export default ollama;
1
+ import{ndjson,streamText}from"../../utils/stream";import{parseToolArguments,toOllamaMessages}from"../../utils/tools";const DEFAULT_HOST="http://localhost:11434",DEFAULT_MODEL="llama3.2",DEFAULT_EMBEDDING_MODEL="nomic-embed-text";let globalConfig={};export function configure(config){globalConfig={...globalConfig,...config}}function getConfig(config){return{host:config?.host||globalConfig.host||process.env.OLLAMA_HOST||DEFAULT_HOST,model:config?.model||globalConfig.model||process.env.OLLAMA_MODEL||DEFAULT_MODEL,embeddingModel:config?.embeddingModel||globalConfig.embeddingModel||DEFAULT_EMBEDDING_MODEL}}export function createOllamaDriver(config={}){const{host=process.env.OLLAMA_HOST||DEFAULT_HOST,model=process.env.OLLAMA_MODEL||DEFAULT_MODEL,embeddingModel=DEFAULT_EMBEDDING_MODEL}=config;return{name:"Ollama",async process(command,systemPrompt,history){const response=await fetch(`${host}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model,messages:[{role:"system",content:systemPrompt},...history,{role:"user",content:command}],stream:!1})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}return(await response.json()).message.content},async*stream(command,systemPrompt,history){const response=await fetch(`${host}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model,messages:[{role:"system",content:systemPrompt},...history,{role:"user",content:command}],stream:!0})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}if(!response.body)throw Error("No response body");for await(const data of ndjson(response.body,"Ollama API")){if(data.error)throw Error(`Ollama API error: ${data.error}`);if(data.message?.content)yield data.message.content}},async embed(input){const inputs=Array.isArray(input)?input:[input],embeddings=[];for(const text of inputs){const response=await fetch(`${host}/api/embeddings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:embeddingModel,prompt:text})});if(!response.ok){const error=await response.text();throw Error(`Ollama Embeddings API error: ${error}`)}const data=await response.json();embeddings.push(data.embedding)}return Array.isArray(input)?embeddings:embeddings[0]}}}function chatRequest(messages,options,stream){const config=getConfig(),{model=config.model,temperature,topP,stop,responseFormat,tools,toolChoice}=options;if(toolChoice==="required"||toolChoice&&typeof toolChoice==="object")throw Error("Ollama cannot be made to call a tool: offer the tools with toolChoice 'auto', or use another driver");const offered=toolChoice==="none"?[]:tools??[],format=responseFormat?.type==="json_schema"?responseFormat.json_schema.schema:responseFormat?.type==="json_object"?"json":void 0;return{model,messages:toOllamaMessages(messages),stream,format,...offered.length>0?{tools:offered.map((tool)=>({type:"function",function:{name:tool.name,description:tool.description,parameters:tool.parameters??{type:"object",properties:{}}}}))}:{},options:{temperature,top_p:topP,stop}}}function toToolCalls(calls,offset=0){return calls.map((call,index)=>({id:call.id??`call_${offset+index}`,name:call.function.name,arguments:parseToolArguments(call.function.arguments,call.function.name)}))}export async function chat(messages,options={}){const config=getConfig(),response=await fetch(`${config.host}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(chatRequest(messages,options,!1))});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}const data=await response.json(),toolCalls=toToolCalls(data.message?.tool_calls??[]);return{content:data.message?.content??"",...toolCalls.length>0?{toolCalls}:{},model:data.model,usage:{promptTokens:data.prompt_eval_count||0,completionTokens:data.eval_count||0,totalTokens:(data.prompt_eval_count||0)+(data.eval_count||0)},finishReason:data.done_reason||"stop"}}export async function*streamChatEvents(messages,options={}){const config=getConfig(),body=chatRequest(messages,options,!0),response=await fetch(`${config.host}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}if(!response.body)throw Error("No response body");const toolCalls=[];let content="",model=String(body.model),usage,finishReason;for await(const chunk of ndjson(response.body,"Ollama API")){if(chunk.error)throw Error(`Ollama API error: ${chunk.error}`);if(chunk.model)model=chunk.model;if(chunk.message?.content){content+=chunk.message.content;yield{type:"text",text:chunk.message.content}}for(const call of toToolCalls(chunk.message?.tool_calls??[],toolCalls.length)){toolCalls.push(call);yield{type:"tool_call",call}}if(chunk.done){finishReason=chunk.done_reason||"stop";usage={promptTokens:chunk.prompt_eval_count||0,completionTokens:chunk.eval_count||0,totalTokens:(chunk.prompt_eval_count||0)+(chunk.eval_count||0)}}}yield{type:"done",result:{content,...toolCalls.length>0?{toolCalls}:{},model,...usage?{usage}:{},...finishReason?{finishReason}:{}}}}export function streamChat(messages,options={}){return streamText(streamChatEvents(messages,options))}export async function generate(prompt,options={}){const config=getConfig(),response=await fetch(`${config.host}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:options.model||config.model,prompt,system:options.system,template:options.template,context:options.context,raw:options.raw,format:options.format,images:options.images,stream:!1})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}const data=await response.json();return{content:data.response,model:data.model,usage:{promptTokens:data.prompt_eval_count||0,completionTokens:data.eval_count||0,totalTokens:(data.prompt_eval_count||0)+(data.eval_count||0)}}}export async function embed(input,model){const config=getConfig(),embeddingModel=model||config.embeddingModel,inputs=Array.isArray(input)?input:[input],embeddings=[];for(const text of inputs){const response=await fetch(`${config.host}/api/embeddings`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:embeddingModel,prompt:text})});if(!response.ok){const error=await response.text();throw Error(`Ollama Embeddings API error: ${error}`)}const data=await response.json();embeddings.push(data.embedding)}return Array.isArray(input)?embeddings:embeddings[0]}export async function listModels(){const config=getConfig(),response=await fetch(`${config.host}/api/tags`,{method:"GET"});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}return(await response.json()).models}export async function pullModel(name,onProgress){const config=getConfig(),response=await fetch(`${config.host}/api/pull`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name,stream:!0})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}const reader=response.body?.getReader();if(!reader)return;const decoder=new TextDecoder;let buffer="";while(!0){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0});const lines=buffer.split(`
2
+ `);buffer=lines.pop()||"";for(const line of lines){if(!line.trim())continue;try{const data=JSON.parse(line);if(onProgress)onProgress(data.status??"",data.completed,data.total)}catch{}}}}export async function deleteModel(name){const config=getConfig(),response=await fetch(`${config.host}/api/delete`,{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({name})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}}export async function showModel(_name){const config=getConfig(),response=await fetch(`${config.host}/api/show`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:_name})});if(!response.ok){const error=await response.text();throw Error(`Ollama API error: ${error}`)}return response.json()}export async function isRunning(){const config=getConfig();try{return(await fetch(`${config.host}/api/tags`,{method:"GET"})).ok}catch{return!1}}export const ollamaDriver={create:createOllamaDriver},ollama={configure,chat,streamChat,streamChatEvents,generate,embed,listModels,pullModel,deleteModel,showModel,isRunning,createDriver:createOllamaDriver};export default ollama;
@@ -1,4 +1,4 @@
1
- import type { AIDriver, AIDriverConfig, AIMessage, AIResult, ChatCompletionOptions } from '../../types';
1
+ import type { AIDriver, AIDriverConfig, AIMessage, AIResult, AIStreamEvent, ChatCompletionOptions } from '../../types';
2
2
  /**
3
3
  * Configure OpenAI globally
4
4
  */
@@ -9,9 +9,22 @@ export declare function createOpenAIDriver(config: OpenAIDriverConfig): AIDriver
9
9
  */
10
10
  export declare function chat(messages: AIMessage[], options?: ChatCompletionOptions): Promise<AIResult>;
11
11
  /**
12
- * Stream chat completion
12
+ * A streamed chat completion, as events: text as it is written, each tool
13
+ * call once its arguments have streamed, then the whole result.
14
+ *
15
+ * A tool call streams as `delta.tool_calls` pieces keyed by `index`: the id
16
+ * and name first, then the arguments a fragment at a time. The calls are
17
+ * complete when the choice finishes, and are reported then, in index order.
18
+ * Usage only comes with `stream_options.include_usage`, on a last chunk with
19
+ * no choices, so it is asked for. An `error` chunk throws.
13
20
  */
14
- export declare function streamChat(messages: AIMessage[], options?: ChatCompletionOptions): AsyncGenerator<string>;
21
+ export declare function streamChatEvents(messages: AIMessage[], options?: ChatCompletionOptions): AsyncGenerator<AIStreamEvent>;
22
+ /**
23
+ * Stream chat completion: the text as it is written. Tool calls, usage and
24
+ * the stop reason are in the result the generator returns; iterate
25
+ * {@link streamChatEvents} to act on a tool call as soon as it arrives.
26
+ */
27
+ export declare function streamChat(messages: AIMessage[], options?: ChatCompletionOptions): AsyncGenerator<string, AIResult, undefined>;
15
28
  /**
16
29
  * Create embeddings
17
30
  */
@@ -50,6 +63,7 @@ export declare const openai: {
50
63
  configure: typeof configure;
51
64
  chat: typeof chat;
52
65
  streamChat: typeof streamChat;
66
+ streamChatEvents: typeof streamChatEvents;
53
67
  embed: typeof embed;
54
68
  generateImage: typeof generateImage;
55
69
  transcribe: typeof transcribe;
@@ -1,3 +1 @@
1
- import{fetchWithRetry}from"../../utils/retry";import{parseToolArguments,toChatCompletionMessages}from"../../utils/tools";import{recordUsage}from"../../utils/usage";const DEFAULT_MODEL="gpt-4o",DEFAULT_MAX_TOKENS=4096,DEFAULT_EMBEDDING_MODEL="text-embedding-3-small",DEFAULT_BASE_URL="https://api.openai.com/v1";let globalConfig=null;export function configure(config){globalConfig=config}function getConfig(config){const merged={...globalConfig,...config};if(!merged.apiKey)merged.apiKey=process.env.OPENAI_API_KEY||"";return merged}export function createOpenAIDriver(config){const{apiKey,model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,baseUrl=DEFAULT_BASE_URL,embeddingModel=DEFAULT_EMBEDDING_MODEL}=config;return{name:"OpenAI",async process(command,systemPrompt,history){if(!apiKey)throw Error("OpenAI API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${baseUrl}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${apiKey}`},body:JSON.stringify({model,max_tokens:maxTokens,messages:[{role:"system",content:systemPrompt},...history,{role:"user",content:command}]})});if(!response.ok){const error=await response.text();throw Error(`OpenAI API error: ${error}`)}const data=await response.json();if(!data.choices||data.choices.length===0)throw Error("OpenAI API returned empty choices");return data.choices[0].message.content},async*stream(command,systemPrompt,history){if(!apiKey)throw Error("OpenAI API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${baseUrl}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${apiKey}`},body:JSON.stringify({model,max_tokens:maxTokens,stream:!0,messages:[{role:"system",content:systemPrompt},...history,{role:"user",content:command}]})});if(!response.ok){const error=await response.text();throw Error(`OpenAI API error: ${error}`)}const reader=response.body?.getReader();if(!reader)throw Error("No response body");const decoder=new TextDecoder;let buffer="";const handlePayload=function*(data){if(data==="[DONE]")return;let parsed;try{parsed=JSON.parse(data)}catch{return}if(parsed?.error){const msg=parsed.error.message||JSON.stringify(parsed.error);throw Error(`[openai/stream] mid-stream error: ${msg}`)}const content=parsed.choices?.[0]?.delta?.content;if(content)yield content};while(!0){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0});const lines=buffer.split(`
2
- `);buffer=lines.pop()||"";for(const line of lines)if(line.startsWith("data: "))yield*handlePayload(line.slice(6))}if(buffer.startsWith("data: "))yield*handlePayload(buffer.slice(6))},async embed(input){if(!apiKey)throw Error("OpenAI API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${baseUrl}/embeddings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${apiKey}`},body:JSON.stringify({model:embeddingModel,input})});if(!response.ok){const error=await response.text();throw Error(`OpenAI Embeddings API error: ${error}`)}const data=await response.json();if(Array.isArray(input))return data.data.map((d)=>d.embedding);return data.data[0].embedding}}}export async function chat(messages,options={}){const config=getConfig(),{model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,temperature,topP,stop,tools,toolChoice,responseFormat}=options,normalizedMessages=toChatCompletionMessages(messages,{argumentsAsString:!0}),startedAt=Date.now(),body={model,max_tokens:maxTokens,temperature,top_p:topP,stop,messages:normalizedMessages};if(tools?.length)body.tools=tools.map((tool)=>({type:"function",function:{name:tool.name,description:tool.description,parameters:tool.parameters??{type:"object",properties:{}}}}));if(toolChoice!==void 0)body.tool_choice=typeof toolChoice==="object"?{type:"function",function:{name:toolChoice.name}}:toolChoice;if(responseFormat&&responseFormat.type!=="text")body.response_format=responseFormat;const response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify(body)});if(!response.ok){const error=await response.text();throw Error(`OpenAI API error: ${error}`)}const data=await response.json();if(!data.choices||data.choices.length===0)throw Error("OpenAI API returned empty choices");const message=data.choices[0].message,toolCalls=(message.tool_calls??[]).map((call)=>({id:call.id,name:call.function.name,arguments:parseToolArguments(call.function.arguments,call.function.name)})),result={content:message.content??"",...toolCalls.length>0?{toolCalls}:{},model:data.model,usage:{promptTokens:data.usage?.prompt_tokens||0,completionTokens:data.usage?.completion_tokens||0,totalTokens:data.usage?.total_tokens||0},finishReason:data.choices[0].finish_reason};recordUsage({provider:"openai",model:data.model,promptTokens:result.usage.promptTokens,completionTokens:result.usage.completionTokens,totalTokens:result.usage.totalTokens,durationMs:Date.now()-startedAt,timestamp:Date.now()});return result}export async function*streamChat(messages,options={}){const config=getConfig(),{model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,temperature,topP,stop}=options,response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify({model,max_tokens:maxTokens,temperature,top_p:topP,stop,stream:!0,messages:toChatCompletionMessages(messages,{argumentsAsString:!0})})});if(!response.ok){const error=await response.text();throw Error(`OpenAI API error: ${error}`)}const reader=response.body?.getReader();if(!reader)throw Error("No response body");const decoder=new TextDecoder;let buffer="";while(!0){const{done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:!0});const lines=buffer.split(`
3
- `);buffer=lines.pop()||"";for(const line of lines)if(line.startsWith("data: ")){const data=line.slice(6);if(data==="[DONE]")continue;try{const content=JSON.parse(data).choices[0]?.delta?.content;if(content)yield content}catch{}}}}export async function embed(input,model=DEFAULT_EMBEDDING_MODEL){const config=getConfig(),response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/embeddings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify({model,input})});if(!response.ok){const error=await response.text();throw Error(`OpenAI Embeddings API error: ${error}`)}const data=await response.json();if(Array.isArray(input))return data.data.map((d)=>d.embedding);return data.data[0].embedding}export async function generateImage(prompt,options={}){const config=getConfig(),{model="dall-e-3",size="1024x1024",quality="standard",n=1,responseFormat="url"}=options,response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/images/generations`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify({model,prompt,size,quality,n,response_format:responseFormat})});if(!response.ok){const error=await response.text();throw Error(`OpenAI Image API error: ${error}`)}return(await response.json()).data}export async function transcribe(audioFile,options={}){const config=getConfig(),formData=new FormData;formData.append("file",audioFile);formData.append("model",options.model||"whisper-1");if(options.language)formData.append("language",options.language);if(options.prompt)formData.append("prompt",options.prompt);if(options.responseFormat)formData.append("response_format",options.responseFormat);if(options.temperature!==void 0)formData.append("temperature",String(options.temperature));const response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/audio/transcriptions`,{method:"POST",headers:{Authorization:`Bearer ${config.apiKey}`},body:formData});if(!response.ok){const error=await response.text();throw Error(`OpenAI Whisper API error: ${error}`)}return response.json()}export async function textToSpeech(input,options={}){const config=getConfig(),{model="tts-1",voice="alloy",responseFormat="mp3",speed=1}=options,response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/audio/speech`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify({model,input,voice,response_format:responseFormat,speed})});if(!response.ok){const error=await response.text();throw Error(`OpenAI TTS API error: ${error}`)}return response.arrayBuffer()}export const openaiDriver={create:createOpenAIDriver},openai={configure,chat,streamChat,embed,generateImage,transcribe,textToSpeech,createDriver:createOpenAIDriver};export default openai;
1
+ import{fetchWithRetry}from"../../utils/retry";import{sseJson,streamText}from"../../utils/stream";import{parseToolArguments,toChatCompletionMessages}from"../../utils/tools";import{recordUsage}from"../../utils/usage";const DEFAULT_MODEL="gpt-4o",DEFAULT_MAX_TOKENS=4096,DEFAULT_EMBEDDING_MODEL="text-embedding-3-small",DEFAULT_BASE_URL="https://api.openai.com/v1";let globalConfig=null;export function configure(config){globalConfig=config}function getConfig(config){const merged={...globalConfig,...config};if(!merged.apiKey)merged.apiKey=process.env.OPENAI_API_KEY||"";return merged}export function createOpenAIDriver(config){const{apiKey,model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,baseUrl=DEFAULT_BASE_URL,embeddingModel=DEFAULT_EMBEDDING_MODEL}=config;return{name:"OpenAI",async process(command,systemPrompt,history){if(!apiKey)throw Error("OpenAI API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${baseUrl}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${apiKey}`},body:JSON.stringify({model,max_tokens:maxTokens,messages:[{role:"system",content:systemPrompt},...history,{role:"user",content:command}]})});if(!response.ok){const error=await response.text();throw Error(`OpenAI API error: ${error}`)}const data=await response.json();if(!data.choices||data.choices.length===0)throw Error("OpenAI API returned empty choices");return data.choices[0].message.content},async*stream(command,systemPrompt,history){if(!apiKey)throw Error("OpenAI API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${baseUrl}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${apiKey}`},body:JSON.stringify({model,max_tokens:maxTokens,stream:!0,messages:[{role:"system",content:systemPrompt},...history,{role:"user",content:command}]})});if(!response.ok){const error=await response.text();throw Error(`OpenAI API error: ${error}`)}if(!response.body)throw Error("No response body");for await(const parsed of sseJson(response.body,"OpenAI API")){if(parsed?.error){const msg=parsed.error.message||JSON.stringify(parsed.error);throw Error(`[openai/stream] mid-stream error: ${msg}`)}const content=parsed.choices?.[0]?.delta?.content;if(content)yield content}},async embed(input){if(!apiKey)throw Error("OpenAI API key not set. Configure your API key in settings.");const response=await fetchWithRetry(`${baseUrl}/embeddings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${apiKey}`},body:JSON.stringify({model:embeddingModel,input})});if(!response.ok){const error=await response.text();throw Error(`OpenAI Embeddings API error: ${error}`)}const data=await response.json();if(Array.isArray(input))return data.data.map((d)=>d.embedding);return data.data[0].embedding}}}function completionRequest(messages,options){const{model=DEFAULT_MODEL,maxTokens=DEFAULT_MAX_TOKENS,temperature,topP,stop,tools,toolChoice,responseFormat}=options,body={model,max_tokens:maxTokens,temperature,top_p:topP,stop,messages:toChatCompletionMessages(messages,{argumentsAsString:!0})};if(tools?.length)body.tools=tools.map((tool)=>({type:"function",function:{name:tool.name,description:tool.description,parameters:tool.parameters??{type:"object",properties:{}}}}));if(toolChoice!==void 0)body.tool_choice=typeof toolChoice==="object"?{type:"function",function:{name:toolChoice.name}}:toolChoice;if(responseFormat&&responseFormat.type!=="text")body.response_format=responseFormat;return body}function postCompletion(body){const config=getConfig();return fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify(body)})}export async function chat(messages,options={}){const startedAt=Date.now(),response=await postCompletion(completionRequest(messages,options));if(!response.ok){const error=await response.text();throw Error(`OpenAI API error: ${error}`)}const data=await response.json();if(!data.choices||data.choices.length===0)throw Error("OpenAI API returned empty choices");const message=data.choices[0].message,toolCalls=(message.tool_calls??[]).map((call)=>({id:call.id,name:call.function.name,arguments:parseToolArguments(call.function.arguments,call.function.name)})),result={content:message.content??"",...toolCalls.length>0?{toolCalls}:{},model:data.model,usage:{promptTokens:data.usage?.prompt_tokens||0,completionTokens:data.usage?.completion_tokens||0,totalTokens:data.usage?.total_tokens||0},finishReason:data.choices[0].finish_reason};recordUsage({provider:"openai",model:data.model,promptTokens:result.usage.promptTokens,completionTokens:result.usage.completionTokens,totalTokens:result.usage.totalTokens,durationMs:Date.now()-startedAt,timestamp:Date.now()});return result}export async function*streamChatEvents(messages,options={}){const startedAt=Date.now(),body=completionRequest(messages,options),response=await postCompletion({...body,stream:!0,stream_options:{include_usage:!0}});if(!response.ok){const error=await response.text();throw Error(`OpenAI API error: ${error}`)}if(!response.body)throw Error("No response body");const pending=new Map,toolCalls=[];let content="",model=String(body.model),usage,finishReason;function*flushCalls(){for(const index of[...pending.keys()].sort((a,b)=>a-b)){const piece=pending.get(index),call={id:piece.id,name:piece.name,arguments:parseToolArguments(piece.args,piece.name)};toolCalls.push(call);yield{type:"tool_call",call}}pending.clear()}for await(const chunk of sseJson(response.body,"OpenAI API")){if(chunk.error)throw Error(`OpenAI API error: ${chunk.error.type?`${chunk.error.type}: `:""}${chunk.error.message??"stream error"}`);if(chunk.model)model=chunk.model;if(chunk.usage)usage={promptTokens:chunk.usage.prompt_tokens??0,completionTokens:chunk.usage.completion_tokens??0,totalTokens:chunk.usage.total_tokens??0};const choice=chunk.choices?.[0];if(!choice)continue;if(choice.delta?.content){content+=choice.delta.content;yield{type:"text",text:choice.delta.content}}for(const piece of choice.delta?.tool_calls??[]){const entry=pending.get(piece.index)??{id:"",name:"",args:""};if(piece.id)entry.id=piece.id;if(piece.function?.name)entry.name+=piece.function.name;if(piece.function?.arguments)entry.args+=piece.function.arguments;pending.set(piece.index,entry)}if(choice.finish_reason){finishReason=choice.finish_reason;yield*flushCalls()}}yield*flushCalls();const result={content,...toolCalls.length>0?{toolCalls}:{},model,...usage?{usage}:{},...finishReason?{finishReason}:{}};if(usage)recordUsage({provider:"openai",model,...usage,durationMs:Date.now()-startedAt,timestamp:Date.now()});yield{type:"done",result}}export function streamChat(messages,options={}){return streamText(streamChatEvents(messages,options))}export async function embed(input,model=DEFAULT_EMBEDDING_MODEL){const config=getConfig(),response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/embeddings`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify({model,input})});if(!response.ok){const error=await response.text();throw Error(`OpenAI Embeddings API error: ${error}`)}const data=await response.json();if(Array.isArray(input))return data.data.map((d)=>d.embedding);return data.data[0].embedding}export async function generateImage(prompt,options={}){const config=getConfig(),{model="dall-e-3",size="1024x1024",quality="standard",n=1,responseFormat="url"}=options,response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/images/generations`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify({model,prompt,size,quality,n,response_format:responseFormat})});if(!response.ok){const error=await response.text();throw Error(`OpenAI Image API error: ${error}`)}return(await response.json()).data}export async function transcribe(audioFile,options={}){const config=getConfig(),formData=new FormData;formData.append("file",audioFile);formData.append("model",options.model||"whisper-1");if(options.language)formData.append("language",options.language);if(options.prompt)formData.append("prompt",options.prompt);if(options.responseFormat)formData.append("response_format",options.responseFormat);if(options.temperature!==void 0)formData.append("temperature",String(options.temperature));const response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/audio/transcriptions`,{method:"POST",headers:{Authorization:`Bearer ${config.apiKey}`},body:formData});if(!response.ok){const error=await response.text();throw Error(`OpenAI Whisper API error: ${error}`)}return response.json()}export async function textToSpeech(input,options={}){const config=getConfig(),{model="tts-1",voice="alloy",responseFormat="mp3",speed=1}=options,response=await fetchWithRetry(`${config.baseUrl||DEFAULT_BASE_URL}/audio/speech`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${config.apiKey}`},body:JSON.stringify({model,input,voice,response_format:responseFormat,speed})});if(!response.ok){const error=await response.text();throw Error(`OpenAI TTS API error: ${error}`)}return response.arrayBuffer()}export const openaiDriver={create:createOpenAIDriver},openai={configure,chat,streamChat,streamChatEvents,embed,generateImage,transcribe,textToSpeech,createDriver:createOpenAIDriver};export default openai;
package/dist/types.d.ts CHANGED
@@ -234,6 +234,18 @@ export type AIResponseFormat = | { type: 'text' }
234
234
  strict?: boolean
235
235
  }
236
236
  }
237
+ /**
238
+ * One event of a streamed chat completion, the same from every driver.
239
+ *
240
+ * `text` arrives in pieces as the model writes. A `tool_call` arrives whole,
241
+ * once its arguments have finished streaming, so it can be run as soon as
242
+ * it is seen. `done` comes last, exactly once, with everything the stream
243
+ * carried in the shape `chat()` returns - text, tool calls, usage and why
244
+ * the model stopped.
245
+ */
246
+ export type AIStreamEvent = | { type: 'text', text: string }
247
+ | { type: 'tool_call', call: AIToolCall }
248
+ | { type: 'done', result: AIResult }
237
249
  export type AIProvider = 'anthropic' | 'openai' | 'ollama' | 'bedrock';
238
250
  /**
239
251
  * Where a provider's credentials come from. `aws-credential-chain`: Bedrock
@@ -1,4 +1,4 @@
1
- import type { ConverseCommandInput, ConverseCommandOutput, InvokeModelCommandInput, InvokeModelCommandOutput, InvokeModelWithResponseStreamCommandInput, InvokeModelWithResponseStreamCommandOutput } from '@stacksjs/ts-cloud/aws';
1
+ import type { ConverseCommandInput, ConverseCommandOutput, ConverseStreamCommandOutput, InvokeModelCommandInput, InvokeModelCommandOutput, InvokeModelWithResponseStreamCommandInput, InvokeModelWithResponseStreamCommandOutput } from '@stacksjs/ts-cloud/aws';
2
2
  export type { ConverseCommandInput, ConverseCommandOutput, InvokeModelCommandInput, InvokeModelWithResponseStreamCommandInput };
3
3
  /**
4
4
  * The region Bedrock is called in: the one passed, then `AWS_REGION`, then
@@ -13,6 +13,11 @@ export declare function bedrockRegion(region?: string): string;
13
13
  * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
14
14
  */
15
15
  export declare function converse(params: ConverseCommandInput, region?: string): Promise<ConverseCommandOutput>;
16
+ /*
17
+ * Converse, streamed: the same request, answered as events
18
+ * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
19
+ */
20
+ export declare function converseStream(params: ConverseCommandInput, region?: string): Promise<ConverseStreamCommandOutput>;
16
21
  /*
17
22
  * Invoke Model
18
23
  * @param {InvokeModelCommandInput} params
@@ -1 +1 @@
1
- import process from"node:process";export function bedrockRegion(region){return region||process.env.AWS_REGION||process.env.AWS_DEFAULT_REGION||"us-east-1"}const clients=new Map;async function getClient(region){const resolved=bedrockRegion(region),cached=clients.get(resolved);if(cached)return cached;const mod=await import("@stacksjs/ts-cloud/aws");if(!mod?.BedrockRuntimeClient)throw Error("@stacksjs/ts-cloud/aws does not export BedrockRuntimeClient - rebuild ts-cloud or remove the AI dependency.");const client=new mod.BedrockRuntimeClient(resolved);clients.set(resolved,client);return client}export async function converse(params,region){return(await getClient(region)).converse(params)}export async function invokeModel(params){return(await getClient()).invokeModel(params)}export async function invokeModelWithResponseStream(params){return(await getClient()).invokeModelWithResponseStream(params)}
1
+ import process from"node:process";export function bedrockRegion(region){return region||process.env.AWS_REGION||process.env.AWS_DEFAULT_REGION||"us-east-1"}const clients=new Map;async function getClient(region){const resolved=bedrockRegion(region),cached=clients.get(resolved);if(cached)return cached;const mod=await import("@stacksjs/ts-cloud/aws");if(!mod?.BedrockRuntimeClient)throw Error("@stacksjs/ts-cloud/aws does not export BedrockRuntimeClient - rebuild ts-cloud or remove the AI dependency.");const client=new mod.BedrockRuntimeClient(resolved);clients.set(resolved,client);return client}export async function converse(params,region){return(await getClient(region)).converse(params)}export async function converseStream(params,region){const c=await getClient(region);if(typeof c.converseStream!=="function")throw Error("This @stacksjs/ts-cloud cannot stream the Converse API: upgrade it to 0.16.44 or newer.");return c.converseStream(params)}export async function invokeModel(params){return(await getClient()).invokeModel(params)}export async function invokeModelWithResponseStream(params){return(await getClient()).invokeModelWithResponseStream(params)}
@@ -0,0 +1,29 @@
1
+ import type { AIResult, AIStreamEvent } from '../types';
2
+ /**
3
+ * Plumbing shared by the drivers' streamed chat completions.
4
+ *
5
+ * Each driver parses its provider's wire format into {@link AIStreamEvent}s
6
+ * once, in `streamChatEvents()`. `streamChat()` is the text-only view of that
7
+ * same stream, so a caller that only wants the words keeps writing
8
+ * `for await (const text of streamChat(...))` and nothing about it changed -
9
+ * while a tool call the model makes mid-stream, which used to vanish, is in
10
+ * the result the generator returns.
11
+ */
12
+ /**
13
+ * The text of an event stream, returning the stream's final result.
14
+ *
15
+ * `for await` reads the text and ignores the return value, which is what
16
+ * keeps every existing caller working. A caller that wants the tool calls
17
+ * too can take the return value, or iterate `streamChatEvents()` instead.
18
+ */
19
+ export declare function streamText(events: AsyncIterable<AIStreamEvent>): AsyncGenerator<string, AIResult, undefined>;
20
+ /**
21
+ * The JSON objects of a server-sent event stream: each `data:` payload,
22
+ * parsed. OpenAI's `[DONE]` sentinel ends nothing on its own and is skipped.
23
+ *
24
+ * A payload that is not JSON throws. Skipping it, as the drivers used to,
25
+ * silently drops whatever text or tool-call fragment it carried.
26
+ */
27
+ export declare function sseJson(body: ReadableStream<Uint8Array>, provider: string): AsyncGenerator<any>;
28
+ /** The JSON objects of a newline-delimited JSON stream, as Ollama sends. */
29
+ export declare function ndjson(body: ReadableStream<Uint8Array>, provider: string): AsyncGenerator<any>;
@@ -0,0 +1,2 @@
1
+ export async function*streamText(events){let result;for await(const event of events)if(event.type==="text")yield event.text;else if(event.type==="done")result=event.result;if(!result)throw Error("The stream ended without a result");return result}async function*lines(body){const decoder=new TextDecoder;let buffer="";for await(const chunk of body){buffer+=decoder.decode(chunk,{stream:!0});const parts=buffer.split(`
2
+ `);buffer=parts.pop()??"";for(const line of parts)yield line.endsWith("\r")?line.slice(0,-1):line}buffer+=decoder.decode();if(buffer)yield buffer.endsWith("\r")?buffer.slice(0,-1):buffer}export async function*sseJson(body,provider){for await(const line of lines(body)){if(!line.startsWith("data:"))continue;const data=line.slice(5).trimStart();if(data==="[DONE]")continue;yield parseEvent(data,provider)}}export async function*ndjson(body,provider){for await(const line of lines(body))if(line.trim())yield parseEvent(line,provider)}function parseEvent(data,provider){try{return JSON.parse(data)}catch{throw Error(`${provider} sent a stream event that is not JSON: ${data.slice(0,200)}`)}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/ai",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.86",
5
+ "version": "0.75.87",
6
6
  "description": "Stacks Artificial Intelligence.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -65,10 +65,10 @@
65
65
  "prepublishOnly": "bun run build"
66
66
  },
67
67
  "dependencies": {
68
- "@stacksjs/cli": "0.75.86",
69
- "@stacksjs/config": "0.75.86",
70
- "@stacksjs/storage": "0.75.86",
71
- "@stacksjs/ts-cloud": "^0.16.42"
68
+ "@stacksjs/cli": "0.75.87",
69
+ "@stacksjs/config": "0.75.87",
70
+ "@stacksjs/storage": "0.75.87",
71
+ "@stacksjs/ts-cloud": "^0.16.44"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "@anthropic-ai/claude-agent-sdk": "^0.2.79"