@node-llm/core 0.1.0 → 0.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.
Files changed (43) hide show
  1. package/README.md +119 -132
  2. package/dist/chat/Chat.d.ts +29 -10
  3. package/dist/chat/Chat.d.ts.map +1 -1
  4. package/dist/chat/Chat.js +61 -23
  5. package/dist/chat/Content.d.ts +1 -1
  6. package/dist/chat/Content.d.ts.map +1 -1
  7. package/dist/chat/Message.d.ts +2 -0
  8. package/dist/chat/Message.d.ts.map +1 -1
  9. package/dist/chat/Stream.d.ts +21 -0
  10. package/dist/chat/Stream.d.ts.map +1 -0
  11. package/dist/chat/Stream.js +57 -0
  12. package/dist/errors/index.d.ts +66 -0
  13. package/dist/errors/index.d.ts.map +1 -0
  14. package/dist/errors/index.js +97 -0
  15. package/dist/executor/Executor.d.ts.map +1 -1
  16. package/dist/executor/Executor.js +4 -1
  17. package/dist/image/GeneratedImage.d.ts +25 -0
  18. package/dist/image/GeneratedImage.d.ts.map +1 -0
  19. package/dist/image/GeneratedImage.js +59 -0
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -0
  23. package/dist/llm.d.ts +6 -0
  24. package/dist/llm.d.ts.map +1 -1
  25. package/dist/llm.js +15 -1
  26. package/dist/providers/Provider.d.ts +21 -0
  27. package/dist/providers/Provider.d.ts.map +1 -1
  28. package/dist/providers/openai/Chat.d.ts.map +1 -1
  29. package/dist/providers/openai/Chat.js +9 -3
  30. package/dist/providers/openai/Errors.d.ts +2 -0
  31. package/dist/providers/openai/Errors.d.ts.map +1 -0
  32. package/dist/providers/openai/Errors.js +34 -0
  33. package/dist/providers/openai/Image.d.ts +8 -0
  34. package/dist/providers/openai/Image.d.ts.map +1 -0
  35. package/dist/providers/openai/Image.js +39 -0
  36. package/dist/providers/openai/OpenAIProvider.d.ts +3 -1
  37. package/dist/providers/openai/OpenAIProvider.d.ts.map +1 -1
  38. package/dist/providers/openai/OpenAIProvider.js +6 -0
  39. package/dist/providers/openai/Streaming.d.ts.map +1 -1
  40. package/dist/providers/openai/Streaming.js +4 -0
  41. package/dist/providers/openai/types.d.ts +8 -0
  42. package/dist/providers/openai/types.d.ts.map +1 -1
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -1,215 +1,202 @@
1
- # node-llm
1
+ # @node-llm/core
2
2
 
3
- A provider-agnostic LLM core for Node.js, inspired by ruby-llm.
3
+ [![npm version](https://img.shields.io/npm/v/@node-llm/core.svg)](https://www.npmjs.com/package/@node-llm/core)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
4
6
 
5
- node-llm focuses on:
6
- - clean abstractions
7
- - minimal magic
8
- - streaming-first design
9
- - no SDK lock-in
7
+ A provider-agnostic LLM core for Node.js, heavily inspired by the elegant design of [ruby-llm](https://github.com/crmne/ruby_llm).
10
8
 
11
- This is a core library, not a framework.
9
+ `node-llm` focuses on clean abstractions, minimal magic, and a streaming-first design. It provides a unified interface to interact with various LLM providers without being locked into their specific SDKs.
12
10
 
13
11
  ---
14
12
 
15
- ## Features (current)
13
+ ## 🚀 Features
16
14
 
17
- - **Provider-agnostic chat API**: Switch between OpenAI, Anthropic, etc. with one line of config.
18
- - **Ruby-LLM-style configuration**: Simple, global configuration.
19
- - **Streaming responses**: Native AsyncIterator support for progressive token delivery.
20
- - **Tool calling (Function calling)**: Automatic execution loop for model-requested tools.
21
- - **Multi-modal Support**: Built-in support for Vision (images) and Audio.
22
- - **Smart File Handling**: Pass local file paths or URLs; the library handles reading and encoding.
23
- - **Fluent API**: Chainable methods like `.withTool()` for dynamic tool registration.
24
- - **Retry support**: Configurable retry logic at the execution layer.
25
- - **Strict ESM and TypeScript**: Modern, type-safe development.
15
+ - **Provider-Agnostic**: Switch between OpenAI, Anthropic, and others with a single line of config.
16
+ - **Streaming-First**: Native `AsyncIterator` support for real-time token delivery.
17
+ - **Tool Calling**: Automatic execution loop for model-requested functions.
18
+ - **Multi-modal & Smart Files**: Built-in support for Vision (images), Audio, and Text files.
19
+ - **Fluent API**: Chainable methods like `.withTool()` for dynamic registration.
20
+ - **Resilient**: Configurable retry logic at the execution layer.
21
+ - **Type-Safe**: Written in TypeScript with full ESM support.
26
22
 
27
23
  ---
28
24
 
29
- ## Installation
25
+ ## 📦 Installation
30
26
 
31
27
  ```bash
28
+ npm install @node-llm/core
29
+ # or
32
30
  pnpm add @node-llm/core
33
31
  ```
34
32
 
35
33
  ---
36
34
 
37
- ## Configuration
35
+ ## 🛠️ Quick Start
38
36
 
39
- ### Environment variables
37
+ ### 1. Configure the Provider
40
38
 
41
- ```text
42
- OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
39
+ ```ts
40
+ import { LLM } from "@node-llm/core";
41
+ import "dotenv/config";
42
+
43
+ LLM.configure({
44
+ provider: "openai", // Uses OPENAI_API_KEY from env
45
+ retry: { attempts: 3, delayMs: 500 }
46
+ });
43
47
  ```
44
48
 
45
- Load environment variables in your application:
49
+ ### 2. Basic Chat
46
50
 
47
51
  ```ts
48
- import "dotenv/config";
49
- ```
52
+ const chat = LLM.chat("gpt-4o-mini", {
53
+ systemPrompt: "You are a helpful assistant."
54
+ });
50
55
 
51
- ---
56
+ const response = await chat.ask("What is Node.js?");
57
+
58
+ // Use as a string directly
59
+ console.log(response);
60
+
61
+ // Or access metadata (RubyLLM style)
62
+ console.log(response.content);
63
+ console.log(`Model: ${response.model_id}`);
64
+ console.log(`Tokens: ${response.input_tokens} in, ${response.output_tokens} out`);
65
+ ```
52
66
 
53
- ## Basic Chat Usage
67
+ ### 3. Streaming Responses
54
68
 
55
69
  ```ts
56
- import { LLM } from "@node-llm/core";
70
+ for await (const chunk of chat.stream("Write a poem")) {
71
+ process.stdout.write(chunk.content);
72
+ }
73
+ ```
57
74
 
58
- LLM.configure({
59
- provider: "openai",
60
- });
75
+ ### 4. Image Generation (Paint)
61
76
 
62
- const chat = LLM.chat("gpt-4o-mini", {
63
- systemPrompt: "You are a concise assistant",
77
+ Generate images and interact with them using a rich API.
78
+
79
+ ```ts
80
+ const image = await LLM.paint("a sunset over mountains", {
81
+ model: "dall-e-3"
64
82
  });
65
83
 
66
- const reply = await chat.ask("Explain HTTP in one sentence");
67
- console.log(reply);
84
+ // Use as a URL string
85
+ console.log(`URL: ${image}`);
68
86
 
69
- // List available models with metadata (pricing, context window, etc.)
70
- const models = await LLM.listModels();
71
- console.log(models[0]);
87
+ // Or use rich methods
88
+ await image.save("sunset.png");
89
+ console.log(`Format: ${image.mimeType}`);
72
90
  ```
73
91
 
74
- ---
75
-
76
- ## Streaming Responses
92
+ ### 5. Token Usage Tracking
77
93
 
78
- Streaming uses the native AsyncIterator pattern.
94
+ Track tokens for individual turns or the entire conversation.
79
95
 
80
96
  ```ts
81
- import { LLM } from "@node-llm/core";
97
+ const response = await chat.ask("Hello!");
82
98
 
83
- LLM.configure({ provider: "openai" });
99
+ console.log(response.input_tokens); // 10
100
+ console.log(response.output_tokens); // 5
84
101
 
85
- const chat = LLM.chat("gpt-4o-mini");
102
+ // Access aggregated usage for the whole session
103
+ console.log(chat.totalUsage.total_tokens);
104
+ ```
86
105
 
87
- let full = "";
106
+ ---
88
107
 
89
- for await (const token of chat.stream("Explain HTTP in one sentence")) {
90
- process.stdout.write(token);
91
- full += token;
92
- }
108
+ ## 📚 Examples
93
109
 
94
- console.log("\nFinal response:", full);
110
+ Check the [examples](./examples) directory for focused scripts organized by provider:
111
+
112
+ ### OpenAI Examples
113
+ | Example | Description |
114
+ | :--- | :--- |
115
+ | [Basic Chat](../../examples/openai/01-basic-chat.mjs) | Simple completion request |
116
+ | [Streaming](../../examples/openai/02-streaming.mjs) | Real-time token streaming |
117
+ | [Tool Calling](../../examples/openai/03-tool-calling.mjs) | Automatic tool execution loop |
118
+ | [Vision](../../examples/openai/04-vision.mjs) | Image analysis |
119
+ | [List Models](../../examples/openai/05-list-models.mjs) | Enumerate available models |
120
+ | [Paint](../../examples/openai/06-paint.mjs) | Image generation with DALL-E |
121
+ | [Image Features](../../examples/openai/07-image-features.mjs) | Saving and processing generated images |
122
+ | [Token Usage](../../examples/openai/08-token-usage.mjs) | Detailed stats for turns and conversations |
123
+
124
+ To run an example (from the project root):
125
+ ```bash
126
+ node examples/openai/01-basic-chat.mjs
95
127
  ```
96
128
 
97
129
  ---
98
130
 
99
- ## Tool Calling
131
+ ## 🔌 Advanced Usage
100
132
 
101
- You can define tools and pass them to the chat instance. The model will decide when to call them, and the library handles the execution loop automatically.
133
+ ### Tool Calling (Function Calling)
102
134
 
103
- ```ts
104
- import { LLM, Tool } from "@node-llm/core";
135
+ Define your tools and let the library handle the execution loop automatically.
105
136
 
106
- // 1. Define a tool
107
- const weatherTool: Tool = {
137
+ ```ts
138
+ const weatherTool = {
108
139
  type: 'function',
109
140
  function: {
110
141
  name: 'get_weather',
111
- description: 'Get the current weather for a location',
112
142
  parameters: {
113
143
  type: 'object',
114
- properties: {
115
- location: { type: 'string', description: 'The city and state, e.g. San Francisco, CA' },
116
- unit: { type: 'string', enum: ['celsius', 'fahrenheit'] }
117
- },
118
- required: ['location']
144
+ properties: { location: { type: 'string' } }
119
145
  }
120
146
  },
121
- // 2. Implement the handler
122
- handler: async ({ location, unit = 'celsius' }) => {
123
- // Call your real API here
124
- return JSON.stringify({ location, temperature: 22, unit, condition: "Sunny" });
147
+ handler: async ({ location }) => {
148
+ return JSON.stringify({ location, temp: 22, unit: 'celsius' });
125
149
  }
126
150
  };
127
151
 
128
- // 3. Initialize chat (Option A: via constructor)
129
- const chat = LLM.chat("gpt-4o-mini", {
130
- tools: [weatherTool]
131
- });
132
-
133
- // OR Option B: via fluent API (Ruby-LLM style)
134
- const chat2 = LLM.chat("gpt-4o-mini")
135
- .withTool(weatherTool);
136
-
137
- // 4. Ask a question
138
- const reply = await chat.ask("What is the weather in London?");
139
- console.log(reply);
140
- // Output: "The weather in London is currently 22°C and sunny."
152
+ // Use the fluent API to add tools on the fly
153
+ const reply = await chat
154
+ .withTool(weatherTool)
155
+ .ask("What is the weather in London?");
141
156
  ```
142
157
 
143
- ---
144
-
145
- ## File & Multi-modal Support
158
+ ### Multi-modal & File Support
146
159
 
147
- You can send files (images, audio, text, etc.) to models that support them. The library automatically handles local file reading, MIME detection, and base64 encoding.
160
+ Pass local paths or URLs directly. The library handles reading, MIME detection, and encoding.
148
161
 
149
162
  ```ts
150
- // Local files (automatically read & converted)
151
- await chat.ask("Analyze this image", {
152
- files: ["./image.jpg"]
153
- });
154
-
155
- // Text files (content is automatically appended to prompt)
156
- await chat.ask("Summarize this code", {
157
- files: ["./app.ts"]
163
+ // Vision
164
+ await chat.ask("What's in this image?", {
165
+ files: ["./screenshot.png"]
158
166
  });
159
167
 
160
- // Remote URLs (passed through)
161
- await chat.ask("Describe this", {
162
- files: ["https://example.com/photo.png"]
163
- });
164
-
165
- // Audio files (OpenAI input_audio support)
166
- await chat.ask("Transcribe this meeting", {
168
+ // Audio
169
+ await chat.ask("Transcribe this", {
167
170
  files: ["./meeting.mp3"]
168
171
  });
169
- ```
170
-
171
- ---
172
-
173
- ## Retry Support
174
-
175
- Retries are applied before chat execution, not inside providers.
176
172
 
177
- ```ts
178
- LLM.configure({
179
- provider: "openai",
180
- retry: {
181
- attempts: 3,
182
- delayMs: 500,
183
- },
173
+ // Text/Code Analysis
174
+ await chat.ask("Explain this code", {
175
+ files: ["./app.ts"]
184
176
  });
185
177
  ```
186
178
 
187
- Retry behavior:
188
- - Only transient failures are retried
189
- - Chat and providers remain clean
190
- - Designed for future timeouts and circuit breakers
191
-
192
179
  ---
193
180
 
194
- ## Development
181
+ ## 📋 Supported Providers
195
182
 
196
- ```bash
197
- pnpm install
198
- pnpm --filter @node-llm/core build
199
- node test-openai.mjs
200
- ```
183
+ | Provider | Status | Notes |
184
+ | :--- | :--- | :--- |
185
+ | **OpenAI** | ✅ Supported | Chat, Streaming, Tools, Vision, Audio, Images (DALL-E) |
186
+ | **Anthropic** | 🏗️ Roadmap | Coming soon |
187
+ | **Azure OpenAI** | 🏗️ Roadmap | Coming soon |
201
188
 
202
189
  ---
203
190
 
204
- ## Design Philosophy
191
+ ## 🧠 Design Philosophy
205
192
 
206
- - **Explicit over implicit**: No hidden side effects or complex state management.
207
- - **Provider-agnostic core**: The same code works across different LLM providers.
208
- - **Ruby-LLM mental model**: Developer experience inspired by the best of Ruby, executed with Node-native patterns.
209
- - **Production Ready**: Built with TypeScript, ESM, and comprehensive testing.
193
+ - **Explicit over Implicit**: No hidden side effects.
194
+ - **Minimal Dependencies**: Lightweight core with zero bloat.
195
+ - **Developer Experience**: Inspired by Ruby's elegance, built for Node's performance.
196
+ - **Production Ready**: Built-in retries and strict type checking.
210
197
 
211
198
  ---
212
199
 
213
- ## License
200
+ ## 📄 License
214
201
 
215
- MIT
202
+ MIT © [node-llm contributors]
@@ -1,6 +1,28 @@
1
1
  import { Message } from "./Message.js";
2
2
  import { ChatOptions } from "./ChatOptions.js";
3
- import { Provider } from "../providers/Provider.js";
3
+ import { Provider, Usage } from "../providers/Provider.js";
4
+ export interface AskOptions {
5
+ images?: string[];
6
+ files?: string[];
7
+ temperature?: number;
8
+ maxTokens?: number;
9
+ }
10
+ /**
11
+ * Enhanced string that includes token usage metadata.
12
+ * Behaves like a regular string but has .usage and .input_tokens etc.
13
+ */
14
+ export declare class ChatResponseString extends String {
15
+ readonly usage: Usage;
16
+ readonly model: string;
17
+ constructor(content: string, usage: Usage, model: string);
18
+ get input_tokens(): number;
19
+ get output_tokens(): number;
20
+ get total_tokens(): number;
21
+ get cached_tokens(): number | undefined;
22
+ get content(): string;
23
+ get model_id(): string;
24
+ toString(): string;
25
+ }
4
26
  export declare class Chat {
5
27
  private readonly provider;
6
28
  private readonly model;
@@ -12,6 +34,10 @@ export declare class Chat {
12
34
  * Read-only access to message history
13
35
  */
14
36
  get history(): readonly Message[];
37
+ /**
38
+ * Aggregate usage across the entire conversation
39
+ */
40
+ get totalUsage(): Usage;
15
41
  /**
16
42
  * Add a tool to the chat session (fluent API)
17
43
  */
@@ -19,17 +45,10 @@ export declare class Chat {
19
45
  /**
20
46
  * Ask the model a question
21
47
  */
22
- ask(content: string, options?: {
23
- images?: string[];
24
- files?: string[];
25
- temperature?: number;
26
- maxTokens?: number;
27
- }): Promise<string>;
48
+ ask(content: string, options?: AskOptions): Promise<ChatResponseString>;
28
49
  /**
29
50
  * Streams the model's response to a user question.
30
- * @param content The user's question to send to the model.
31
- * @returns An async generator yielding chunks of the assistant's response as strings.
32
51
  */
33
- stream(content: string): AsyncGenerator<string, void, unknown>;
52
+ stream(content: string): AsyncGenerator<import("../providers/Provider.js").ChatChunk, void, unknown>;
34
53
  }
35
54
  //# sourceMappingURL=Chat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../src/chat/Chat.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAIpD,qBAAa,IAAI;IAKb,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAW;gBAGR,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,WAAgB;IAoB5C;;OAEG;IACH,IAAI,OAAO,IAAI,SAAS,OAAO,EAAE,CAEhC;IAED;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI;IAQzB;;OAEG;IACG,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA6FxI;;;;OAIG;IACI,MAAM,CAAC,OAAO,EAAE,MAAM;CAyB9B"}
1
+ {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../src/chat/Chat.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAK3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,MAAM;aAG1B,KAAK,EAAE,KAAK;aACZ,KAAK,EAAE,MAAM;gBAF7B,OAAO,EAAE,MAAM,EACC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM;IAK/B,IAAI,YAAY,WAAsC;IACtD,IAAI,aAAa,WAAuC;IACxD,IAAI,YAAY,WAAsC;IACtD,IAAI,aAAa,uBAAuC;IAExD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,QAAQ;CAGT;AAED,qBAAa,IAAI;IAKb,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAW;gBAGR,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,WAAgB;IAmB5C;;OAEG;IACH,IAAI,OAAO,IAAI,SAAS,OAAO,EAAE,CAEhC;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,KAAK,CAatB;IAED;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI;IAQzB;;OAEG;IACG,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA6G7E;;OAEG;IACI,MAAM,CAAC,OAAO,EAAE,MAAM;CAI9B"}
package/dist/chat/Chat.js CHANGED
@@ -1,6 +1,33 @@
1
1
  import { FileLoader } from "../utils/FileLoader.js";
2
2
  import { Executor } from "../executor/Executor.js";
3
3
  import { LLM } from "../llm.js";
4
+ import { Stream } from "./Stream.js";
5
+ /**
6
+ * Enhanced string that includes token usage metadata.
7
+ * Behaves like a regular string but has .usage and .input_tokens etc.
8
+ */
9
+ export class ChatResponseString extends String {
10
+ usage;
11
+ model;
12
+ constructor(content, usage, model) {
13
+ super(content);
14
+ this.usage = usage;
15
+ this.model = model;
16
+ }
17
+ get input_tokens() { return this.usage.input_tokens; }
18
+ get output_tokens() { return this.usage.output_tokens; }
19
+ get total_tokens() { return this.usage.total_tokens; }
20
+ get cached_tokens() { return this.usage.cached_tokens; }
21
+ get content() {
22
+ return this.valueOf();
23
+ }
24
+ get model_id() {
25
+ return this.model;
26
+ }
27
+ toString() {
28
+ return this.valueOf();
29
+ }
30
+ }
4
31
  export class Chat {
5
32
  provider;
6
33
  model;
@@ -28,6 +55,20 @@ export class Chat {
28
55
  get history() {
29
56
  return this.messages;
30
57
  }
58
+ /**
59
+ * Aggregate usage across the entire conversation
60
+ */
61
+ get totalUsage() {
62
+ return this.messages.reduce((acc, msg) => {
63
+ if (msg.usage) {
64
+ acc.input_tokens += msg.usage.input_tokens;
65
+ acc.output_tokens += msg.usage.output_tokens;
66
+ acc.total_tokens += msg.usage.total_tokens;
67
+ acc.cached_tokens = (acc.cached_tokens ?? 0) + (msg.usage.cached_tokens ?? 0);
68
+ }
69
+ return acc;
70
+ }, { input_tokens: 0, output_tokens: 0, total_tokens: 0, cached_tokens: 0 });
71
+ }
31
72
  /**
32
73
  * Add a tool to the chat session (fluent API)
33
74
  */
@@ -71,11 +112,24 @@ export class Chat {
71
112
  temperature: options?.temperature ?? this.options.temperature,
72
113
  max_tokens: options?.maxTokens ?? this.options.maxTokens,
73
114
  };
115
+ let totalUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
116
+ const trackUsage = (u) => {
117
+ if (u) {
118
+ totalUsage.input_tokens += u.input_tokens;
119
+ totalUsage.output_tokens += u.output_tokens;
120
+ totalUsage.total_tokens += u.total_tokens;
121
+ if (u.cached_tokens) {
122
+ totalUsage.cached_tokens = (totalUsage.cached_tokens ?? 0) + u.cached_tokens;
123
+ }
124
+ }
125
+ };
74
126
  let response = await this.executor.executeChat(executeOptions);
127
+ trackUsage(response.usage);
75
128
  this.messages.push({
76
129
  role: "assistant",
77
- content: response.content,
130
+ content: new ChatResponseString(response.content ?? "", response.usage ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, this.model),
78
131
  tool_calls: response.tool_calls,
132
+ usage: response.usage,
79
133
  });
80
134
  while (response.tool_calls && response.tool_calls.length > 0) {
81
135
  for (const toolCall of response.tool_calls) {
@@ -111,37 +165,21 @@ export class Chat {
111
165
  messages: this.messages,
112
166
  tools: this.options.tools,
113
167
  });
168
+ trackUsage(response.usage);
114
169
  this.messages.push({
115
170
  role: "assistant",
116
- content: response.content,
171
+ content: new ChatResponseString(response.content ?? "", response.usage ?? { input_tokens: 0, output_tokens: 0, total_tokens: 0 }, this.model),
117
172
  tool_calls: response.tool_calls,
173
+ usage: response.usage,
118
174
  });
119
175
  }
120
- return response.content ?? "";
176
+ return new ChatResponseString(response.content ?? "", totalUsage, this.model);
121
177
  }
122
178
  /**
123
179
  * Streams the model's response to a user question.
124
- * @param content The user's question to send to the model.
125
- * @returns An async generator yielding chunks of the assistant's response as strings.
126
180
  */
127
181
  async *stream(content) {
128
- this.messages.push({ role: "user", content });
129
- if (!this.provider.stream) {
130
- throw new Error("Streaming not supported by provider");
131
- }
132
- let full = "";
133
- for await (const chunk of this.provider.stream({
134
- model: this.model,
135
- messages: this.messages,
136
- })) {
137
- if (chunk.content) {
138
- full += chunk.content;
139
- yield chunk.content;
140
- }
141
- }
142
- this.messages.push({
143
- role: "assistant",
144
- content: full,
145
- });
182
+ const streamer = new Stream(this.provider, this.model, this.options, this.messages);
183
+ yield* streamer.stream(content);
146
184
  }
147
185
  }
@@ -18,5 +18,5 @@ export type ContentPart = {
18
18
  url: string;
19
19
  };
20
20
  };
21
- export type MessageContent = string | ContentPart[];
21
+ export type MessageContent = string | String | ContentPart[];
22
22
  //# sourceMappingURL=Content.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Content.d.ts","sourceRoot":"","sources":["../../src/chat/Content.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAEtD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"Content.d.ts","sourceRoot":"","sources":["../../src/chat/Content.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAEtD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,EAAE,CAAC"}
@@ -1,11 +1,13 @@
1
1
  import { Role } from "./Role.js";
2
2
  import { ToolCall } from "./Tool.js";
3
3
  import { MessageContent } from "./Content.js";
4
+ import { Usage } from "../providers/Provider.js";
4
5
  export interface Message {
5
6
  role: Role;
6
7
  content: MessageContent | null;
7
8
  tool_calls?: ToolCall[];
8
9
  tool_call_id?: string;
9
10
  name?: string;
11
+ usage?: Usage;
10
12
  }
11
13
  //# sourceMappingURL=Message.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Message.d.ts","sourceRoot":"","sources":["../../src/chat/Message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,EAAE,QAAQ,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf"}
1
+ {"version":3,"file":"Message.d.ts","sourceRoot":"","sources":["../../src/chat/Message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,EAAE,QAAQ,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;CACf"}
@@ -0,0 +1,21 @@
1
+ import { Message } from "./Message.js";
2
+ import { ChatOptions } from "./ChatOptions.js";
3
+ import { Provider } from "../providers/Provider.js";
4
+ export declare class Stream {
5
+ private readonly provider;
6
+ private readonly model;
7
+ private readonly options;
8
+ private messages;
9
+ constructor(provider: Provider, model: string, options?: ChatOptions, messages?: Message[]);
10
+ /**
11
+ * Read-only access to message history
12
+ */
13
+ get history(): readonly Message[];
14
+ /**
15
+ * Streams the model's response to a user question.
16
+ * @param content The user's question to send to the model.
17
+ * @returns An async generator yielding chunks of the assistant's response as strings.
18
+ */
19
+ stream(content: string): AsyncGenerator<import("../providers/Provider.js").ChatChunk, void, unknown>;
20
+ }
21
+ //# sourceMappingURL=Stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Stream.d.ts","sourceRoot":"","sources":["../../src/chat/Stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,qBAAa,MAAM;IAIf,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAY;gBAGT,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,WAAgB,EAC1C,QAAQ,CAAC,EAAE,OAAO,EAAE;IAmBtB;;OAEG;IACH,IAAI,OAAO,IAAI,SAAS,OAAO,EAAE,CAEhC;IAED;;;;OAIG;IACI,MAAM,CAAC,OAAO,EAAE,MAAM;CA0B9B"}
@@ -0,0 +1,57 @@
1
+ export class Stream {
2
+ provider;
3
+ model;
4
+ options;
5
+ messages;
6
+ constructor(provider, model, options = {}, messages) {
7
+ this.provider = provider;
8
+ this.model = model;
9
+ this.options = options;
10
+ this.messages = messages ?? [];
11
+ // Only initialize if we're starting a new history
12
+ if (this.messages.length === 0) {
13
+ if (options.systemPrompt) {
14
+ this.messages.push({
15
+ role: "system",
16
+ content: options.systemPrompt,
17
+ });
18
+ }
19
+ if (options.messages) {
20
+ this.messages.push(...options.messages);
21
+ }
22
+ }
23
+ }
24
+ /**
25
+ * Read-only access to message history
26
+ */
27
+ get history() {
28
+ return this.messages;
29
+ }
30
+ /**
31
+ * Streams the model's response to a user question.
32
+ * @param content The user's question to send to the model.
33
+ * @returns An async generator yielding chunks of the assistant's response as strings.
34
+ */
35
+ async *stream(content) {
36
+ this.messages.push({ role: "user", content });
37
+ if (!this.provider.stream) {
38
+ throw new Error("Streaming not supported by provider");
39
+ }
40
+ let full = "";
41
+ for await (const chunk of this.provider.stream({
42
+ model: this.model,
43
+ messages: this.messages,
44
+ temperature: this.options.temperature,
45
+ max_tokens: this.options.maxTokens,
46
+ })) {
47
+ if (chunk.content) {
48
+ full += chunk.content;
49
+ }
50
+ yield chunk;
51
+ }
52
+ this.messages.push({
53
+ role: "assistant",
54
+ content: full,
55
+ });
56
+ }
57
+ }