cognitive-modules-cli 2.2.0 → 2.2.5

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 (94) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -29
  4. package/dist/cli.js +572 -28
  5. package/dist/commands/add.d.ts +33 -14
  6. package/dist/commands/add.js +222 -13
  7. package/dist/commands/compose.d.ts +31 -0
  8. package/dist/commands/compose.js +185 -0
  9. package/dist/commands/index.d.ts +5 -0
  10. package/dist/commands/index.js +5 -0
  11. package/dist/commands/init.js +23 -1
  12. package/dist/commands/migrate.d.ts +30 -0
  13. package/dist/commands/migrate.js +650 -0
  14. package/dist/commands/pipe.d.ts +1 -0
  15. package/dist/commands/pipe.js +31 -11
  16. package/dist/commands/remove.js +33 -2
  17. package/dist/commands/run.d.ts +1 -0
  18. package/dist/commands/run.js +37 -27
  19. package/dist/commands/search.d.ts +28 -0
  20. package/dist/commands/search.js +143 -0
  21. package/dist/commands/test.d.ts +65 -0
  22. package/dist/commands/test.js +454 -0
  23. package/dist/commands/update.d.ts +1 -0
  24. package/dist/commands/update.js +106 -14
  25. package/dist/commands/validate.d.ts +36 -0
  26. package/dist/commands/validate.js +97 -0
  27. package/dist/errors/index.d.ts +218 -0
  28. package/dist/errors/index.js +412 -0
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +5 -1
  31. package/dist/mcp/server.js +84 -79
  32. package/dist/modules/composition.d.ts +251 -0
  33. package/dist/modules/composition.js +1330 -0
  34. package/dist/modules/index.d.ts +2 -0
  35. package/dist/modules/index.js +2 -0
  36. package/dist/modules/loader.d.ts +22 -2
  37. package/dist/modules/loader.js +171 -6
  38. package/dist/modules/runner.d.ts +422 -1
  39. package/dist/modules/runner.js +1472 -71
  40. package/dist/modules/subagent.d.ts +6 -1
  41. package/dist/modules/subagent.js +20 -13
  42. package/dist/modules/validator.d.ts +28 -0
  43. package/dist/modules/validator.js +637 -0
  44. package/dist/providers/anthropic.d.ts +15 -0
  45. package/dist/providers/anthropic.js +147 -5
  46. package/dist/providers/base.d.ts +11 -0
  47. package/dist/providers/base.js +18 -0
  48. package/dist/providers/gemini.d.ts +15 -0
  49. package/dist/providers/gemini.js +122 -5
  50. package/dist/providers/ollama.d.ts +15 -0
  51. package/dist/providers/ollama.js +111 -3
  52. package/dist/providers/openai.d.ts +11 -0
  53. package/dist/providers/openai.js +133 -0
  54. package/dist/registry/client.d.ts +204 -0
  55. package/dist/registry/client.js +356 -0
  56. package/dist/registry/index.d.ts +4 -0
  57. package/dist/registry/index.js +4 -0
  58. package/dist/server/http.js +173 -42
  59. package/dist/types.d.ts +123 -8
  60. package/dist/types.js +4 -1
  61. package/dist/version.d.ts +1 -0
  62. package/dist/version.js +4 -0
  63. package/package.json +32 -7
  64. package/src/cli.ts +0 -410
  65. package/src/commands/add.ts +0 -315
  66. package/src/commands/index.ts +0 -12
  67. package/src/commands/init.ts +0 -94
  68. package/src/commands/list.ts +0 -33
  69. package/src/commands/pipe.ts +0 -76
  70. package/src/commands/remove.ts +0 -57
  71. package/src/commands/run.ts +0 -80
  72. package/src/commands/update.ts +0 -130
  73. package/src/commands/versions.ts +0 -79
  74. package/src/index.ts +0 -55
  75. package/src/mcp/index.ts +0 -5
  76. package/src/mcp/server.ts +0 -403
  77. package/src/modules/index.ts +0 -7
  78. package/src/modules/loader.ts +0 -318
  79. package/src/modules/runner.ts +0 -495
  80. package/src/modules/subagent.ts +0 -275
  81. package/src/providers/anthropic.ts +0 -89
  82. package/src/providers/base.ts +0 -29
  83. package/src/providers/deepseek.ts +0 -83
  84. package/src/providers/gemini.ts +0 -117
  85. package/src/providers/index.ts +0 -78
  86. package/src/providers/minimax.ts +0 -81
  87. package/src/providers/moonshot.ts +0 -82
  88. package/src/providers/ollama.ts +0 -83
  89. package/src/providers/openai.ts +0 -84
  90. package/src/providers/qwen.ts +0 -82
  91. package/src/server/http.ts +0 -316
  92. package/src/server/index.ts +0 -6
  93. package/src/types.ts +0 -495
  94. package/tsconfig.json +0 -17
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Anthropic Provider - Claude API
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  export class AnthropicProvider extends BaseProvider {
@@ -15,11 +17,16 @@ export class AnthropicProvider extends BaseProvider {
15
17
  isConfigured() {
16
18
  return !!this.apiKey;
17
19
  }
18
- async invoke(params) {
19
- if (!this.isConfigured()) {
20
- throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable.');
21
- }
22
- const url = `${this.baseUrl}/messages`;
20
+ /**
21
+ * Anthropic supports streaming.
22
+ */
23
+ supportsStreaming() {
24
+ return true;
25
+ }
26
+ /**
27
+ * Build request body for Anthropic API
28
+ */
29
+ buildRequestBody(params, stream) {
23
30
  // Extract system message
24
31
  const systemMessage = params.messages.find(m => m.role === 'system');
25
32
  const otherMessages = params.messages.filter(m => m.role !== 'system');
@@ -39,10 +46,19 @@ export class AnthropicProvider extends BaseProvider {
39
46
  model: this.model,
40
47
  messages: messages.map(m => ({ role: m.role, content: m.content })),
41
48
  max_tokens: params.maxTokens ?? 4096,
49
+ stream,
42
50
  };
43
51
  if (systemMessage) {
44
52
  body.system = systemMessage.content;
45
53
  }
54
+ return { body, systemContent: systemMessage?.content };
55
+ }
56
+ async invoke(params) {
57
+ if (!this.isConfigured()) {
58
+ throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable.');
59
+ }
60
+ const url = `${this.baseUrl}/messages`;
61
+ const { body } = this.buildRequestBody(params, false);
46
62
  const response = await fetch(url, {
47
63
  method: 'POST',
48
64
  headers: {
@@ -67,4 +83,130 @@ export class AnthropicProvider extends BaseProvider {
67
83
  } : undefined,
68
84
  };
69
85
  }
86
+ /**
87
+ * Stream-based invoke using Anthropic's streaming API.
88
+ * Yields content chunks as they arrive from the API.
89
+ */
90
+ async *invokeStream(params) {
91
+ if (!this.isConfigured()) {
92
+ throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable.');
93
+ }
94
+ const url = `${this.baseUrl}/messages`;
95
+ const { body } = this.buildRequestBody(params, true);
96
+ const response = await fetch(url, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ 'x-api-key': this.apiKey,
101
+ 'anthropic-version': '2023-06-01',
102
+ },
103
+ body: JSON.stringify(body),
104
+ });
105
+ if (!response.ok) {
106
+ const error = await response.text();
107
+ throw new Error(`Anthropic API error: ${response.status} - ${error}`);
108
+ }
109
+ if (!response.body) {
110
+ throw new Error('Anthropic API returned no body for streaming request');
111
+ }
112
+ const reader = response.body.getReader();
113
+ const decoder = new TextDecoder('utf-8');
114
+ const collectedChunks = [];
115
+ let usage;
116
+ let buffer = '';
117
+ try {
118
+ while (true) {
119
+ const { done, value } = await reader.read();
120
+ if (done)
121
+ break;
122
+ buffer += decoder.decode(value, { stream: true });
123
+ // Process complete lines from the buffer
124
+ const lines = buffer.split('\n');
125
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
126
+ for (const line of lines) {
127
+ const trimmed = line.trim();
128
+ if (!trimmed || trimmed.startsWith(':'))
129
+ continue; // Skip empty lines and comments
130
+ if (trimmed.startsWith('data: ')) {
131
+ try {
132
+ const data = JSON.parse(trimmed.slice(6));
133
+ // Extract content chunk (content_block_delta event)
134
+ if (data.type === 'content_block_delta' && data.delta?.type === 'text_delta') {
135
+ const text = data.delta.text || '';
136
+ if (text) {
137
+ collectedChunks.push(text);
138
+ yield text;
139
+ }
140
+ }
141
+ // Extract usage info (message_delta or message_stop event)
142
+ if (data.type === 'message_delta' && data.usage) {
143
+ usage = {
144
+ promptTokens: data.usage.input_tokens || 0,
145
+ completionTokens: data.usage.output_tokens || 0,
146
+ totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
147
+ };
148
+ }
149
+ // Also check message_start for input tokens
150
+ if (data.type === 'message_start' && data.message?.usage) {
151
+ const inputTokens = data.message.usage.input_tokens || 0;
152
+ usage = {
153
+ promptTokens: inputTokens,
154
+ completionTokens: usage?.completionTokens || 0,
155
+ totalTokens: inputTokens + (usage?.completionTokens || 0),
156
+ };
157
+ }
158
+ }
159
+ catch {
160
+ // Skip invalid JSON chunks
161
+ }
162
+ }
163
+ }
164
+ }
165
+ // Flush decoder and process trailing buffered data even without trailing newline.
166
+ buffer += decoder.decode();
167
+ for (const line of buffer.split('\n')) {
168
+ const trimmed = line.trim();
169
+ if (!trimmed || trimmed.startsWith(':'))
170
+ continue;
171
+ if (trimmed.startsWith('data: ')) {
172
+ try {
173
+ const data = JSON.parse(trimmed.slice(6));
174
+ if (data.type === 'content_block_delta' && data.delta?.type === 'text_delta') {
175
+ const text = data.delta.text || '';
176
+ if (text) {
177
+ collectedChunks.push(text);
178
+ yield text;
179
+ }
180
+ }
181
+ if (data.type === 'message_delta' && data.usage) {
182
+ usage = {
183
+ promptTokens: data.usage.input_tokens || 0,
184
+ completionTokens: data.usage.output_tokens || 0,
185
+ totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
186
+ };
187
+ }
188
+ if (data.type === 'message_start' && data.message?.usage) {
189
+ const inputTokens = data.message.usage.input_tokens || 0;
190
+ usage = {
191
+ promptTokens: inputTokens,
192
+ completionTokens: usage?.completionTokens || 0,
193
+ totalTokens: inputTokens + (usage?.completionTokens || 0),
194
+ };
195
+ }
196
+ }
197
+ catch {
198
+ // Skip invalid JSON chunks
199
+ }
200
+ }
201
+ }
202
+ }
203
+ finally {
204
+ reader.releaseLock();
205
+ }
206
+ const fullContent = collectedChunks.join('');
207
+ return {
208
+ content: fullContent,
209
+ usage,
210
+ };
211
+ }
70
212
  }
@@ -6,6 +6,17 @@ export declare abstract class BaseProvider implements Provider {
6
6
  abstract name: string;
7
7
  abstract invoke(params: InvokeParams): Promise<InvokeResult>;
8
8
  abstract isConfigured(): boolean;
9
+ /**
10
+ * Check if this provider supports streaming.
11
+ * Override in subclasses that implement streaming.
12
+ */
13
+ supportsStreaming(): boolean;
14
+ /**
15
+ * Stream-based invoke (optional).
16
+ * Default implementation falls back to non-streaming invoke.
17
+ * Override in subclasses that support streaming.
18
+ */
19
+ invokeStream(params: InvokeParams): AsyncGenerator<string, InvokeResult, unknown>;
9
20
  protected buildJsonPrompt(schema: object): string;
10
21
  protected parseJsonResponse(content: string): unknown;
11
22
  }
@@ -2,6 +2,24 @@
2
2
  * Base Provider - Abstract class for all LLM providers
3
3
  */
4
4
  export class BaseProvider {
5
+ /**
6
+ * Check if this provider supports streaming.
7
+ * Override in subclasses that implement streaming.
8
+ */
9
+ supportsStreaming() {
10
+ return false;
11
+ }
12
+ /**
13
+ * Stream-based invoke (optional).
14
+ * Default implementation falls back to non-streaming invoke.
15
+ * Override in subclasses that support streaming.
16
+ */
17
+ async *invokeStream(params) {
18
+ // Default fallback: use non-streaming invoke and yield the entire result at once
19
+ const result = await this.invoke(params);
20
+ yield result.content;
21
+ return result;
22
+ }
5
23
  buildJsonPrompt(schema) {
6
24
  return `\n\nYou MUST respond with valid JSON matching this schema:\n${JSON.stringify(schema, null, 2)}\n\nRespond with ONLY the JSON, no markdown code blocks.`;
7
25
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Gemini Provider - Google Gemini API
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  import type { InvokeParams, InvokeResult } from '../types.js';
@@ -10,10 +12,23 @@ export declare class GeminiProvider extends BaseProvider {
10
12
  private baseUrl;
11
13
  constructor(apiKey?: string, model?: string);
12
14
  isConfigured(): boolean;
15
+ /**
16
+ * Gemini supports streaming.
17
+ */
18
+ supportsStreaming(): boolean;
13
19
  /**
14
20
  * Clean JSON Schema for Gemini API compatibility
15
21
  * Removes unsupported fields like additionalProperties
16
22
  */
17
23
  private cleanSchemaForGemini;
24
+ /**
25
+ * Build request body for Gemini API
26
+ */
27
+ private buildRequestBody;
18
28
  invoke(params: InvokeParams): Promise<InvokeResult>;
29
+ /**
30
+ * Stream-based invoke using Gemini's streaming API.
31
+ * Yields content chunks as they arrive from the API.
32
+ */
33
+ invokeStream(params: InvokeParams): AsyncGenerator<string, InvokeResult, unknown>;
19
34
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Gemini Provider - Google Gemini API
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  export class GeminiProvider extends BaseProvider {
@@ -15,6 +17,12 @@ export class GeminiProvider extends BaseProvider {
15
17
  isConfigured() {
16
18
  return !!this.apiKey;
17
19
  }
20
+ /**
21
+ * Gemini supports streaming.
22
+ */
23
+ supportsStreaming() {
24
+ return true;
25
+ }
18
26
  /**
19
27
  * Clean JSON Schema for Gemini API compatibility
20
28
  * Removes unsupported fields like additionalProperties
@@ -38,11 +46,10 @@ export class GeminiProvider extends BaseProvider {
38
46
  };
39
47
  return clean(schema);
40
48
  }
41
- async invoke(params) {
42
- if (!this.isConfigured()) {
43
- throw new Error('Gemini API key not configured. Set GEMINI_API_KEY environment variable.');
44
- }
45
- const url = `${this.baseUrl}/models/${this.model}:generateContent?key=${this.apiKey}`;
49
+ /**
50
+ * Build request body for Gemini API
51
+ */
52
+ buildRequestBody(params) {
46
53
  // Convert messages to Gemini format
47
54
  const contents = params.messages
48
55
  .filter(m => m.role !== 'system')
@@ -71,6 +78,14 @@ export class GeminiProvider extends BaseProvider {
71
78
  responseSchema: cleanedSchema,
72
79
  };
73
80
  }
81
+ return body;
82
+ }
83
+ async invoke(params) {
84
+ if (!this.isConfigured()) {
85
+ throw new Error('Gemini API key not configured. Set GEMINI_API_KEY environment variable.');
86
+ }
87
+ const url = `${this.baseUrl}/models/${this.model}:generateContent?key=${this.apiKey}`;
88
+ const body = this.buildRequestBody(params);
74
89
  const response = await fetch(url, {
75
90
  method: 'POST',
76
91
  headers: { 'Content-Type': 'application/json' },
@@ -91,4 +106,106 @@ export class GeminiProvider extends BaseProvider {
91
106
  } : undefined,
92
107
  };
93
108
  }
109
+ /**
110
+ * Stream-based invoke using Gemini's streaming API.
111
+ * Yields content chunks as they arrive from the API.
112
+ */
113
+ async *invokeStream(params) {
114
+ if (!this.isConfigured()) {
115
+ throw new Error('Gemini API key not configured. Set GEMINI_API_KEY environment variable.');
116
+ }
117
+ // Use streamGenerateContent endpoint
118
+ const url = `${this.baseUrl}/models/${this.model}:streamGenerateContent?key=${this.apiKey}&alt=sse`;
119
+ const body = this.buildRequestBody(params);
120
+ const response = await fetch(url, {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify(body),
124
+ });
125
+ if (!response.ok) {
126
+ const error = await response.text();
127
+ throw new Error(`Gemini API error: ${response.status} - ${error}`);
128
+ }
129
+ if (!response.body) {
130
+ throw new Error('Gemini API returned no body for streaming request');
131
+ }
132
+ const reader = response.body.getReader();
133
+ const decoder = new TextDecoder('utf-8');
134
+ const collectedChunks = [];
135
+ let usage;
136
+ let buffer = '';
137
+ try {
138
+ while (true) {
139
+ const { done, value } = await reader.read();
140
+ if (done)
141
+ break;
142
+ buffer += decoder.decode(value, { stream: true });
143
+ // Process complete lines from the buffer
144
+ const lines = buffer.split('\n');
145
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
146
+ for (const line of lines) {
147
+ const trimmed = line.trim();
148
+ if (!trimmed)
149
+ continue;
150
+ if (trimmed.startsWith('data: ')) {
151
+ try {
152
+ const data = JSON.parse(trimmed.slice(6));
153
+ // Extract content chunk
154
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
155
+ if (text) {
156
+ collectedChunks.push(text);
157
+ yield text;
158
+ }
159
+ // Extract usage info (usually in the last chunk)
160
+ if (data.usageMetadata) {
161
+ usage = {
162
+ promptTokens: data.usageMetadata.promptTokenCount || 0,
163
+ completionTokens: data.usageMetadata.candidatesTokenCount || 0,
164
+ totalTokens: data.usageMetadata.totalTokenCount || 0,
165
+ };
166
+ }
167
+ }
168
+ catch {
169
+ // Skip invalid JSON chunks
170
+ }
171
+ }
172
+ }
173
+ }
174
+ // Flush decoder and process trailing buffered data even without trailing newline.
175
+ buffer += decoder.decode();
176
+ for (const line of buffer.split('\n')) {
177
+ const trimmed = line.trim();
178
+ if (!trimmed)
179
+ continue;
180
+ if (trimmed.startsWith('data: ')) {
181
+ try {
182
+ const data = JSON.parse(trimmed.slice(6));
183
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
184
+ if (text) {
185
+ collectedChunks.push(text);
186
+ yield text;
187
+ }
188
+ if (data.usageMetadata) {
189
+ usage = {
190
+ promptTokens: data.usageMetadata.promptTokenCount || 0,
191
+ completionTokens: data.usageMetadata.candidatesTokenCount || 0,
192
+ totalTokens: data.usageMetadata.totalTokenCount || 0,
193
+ };
194
+ }
195
+ }
196
+ catch {
197
+ // Skip invalid JSON chunks
198
+ }
199
+ }
200
+ }
201
+ }
202
+ finally {
203
+ reader.releaseLock();
204
+ }
205
+ const fullContent = collectedChunks.join('');
206
+ return {
207
+ content: fullContent,
208
+ usage,
209
+ };
210
+ }
94
211
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Ollama Provider - Local LLM via Ollama
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  import type { InvokeParams, InvokeResult } from '../types.js';
@@ -9,5 +11,18 @@ export declare class OllamaProvider extends BaseProvider {
9
11
  private baseUrl;
10
12
  constructor(model?: string, baseUrl?: string);
11
13
  isConfigured(): boolean;
14
+ /**
15
+ * Ollama supports streaming.
16
+ */
17
+ supportsStreaming(): boolean;
18
+ /**
19
+ * Build request body for Ollama API
20
+ */
21
+ private buildRequestBody;
12
22
  invoke(params: InvokeParams): Promise<InvokeResult>;
23
+ /**
24
+ * Stream-based invoke using Ollama's streaming API.
25
+ * Yields content chunks as they arrive from the API.
26
+ */
27
+ invokeStream(params: InvokeParams): AsyncGenerator<string, InvokeResult, unknown>;
13
28
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Ollama Provider - Local LLM via Ollama
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  export class OllamaProvider extends BaseProvider {
@@ -14,8 +16,16 @@ export class OllamaProvider extends BaseProvider {
14
16
  isConfigured() {
15
17
  return true; // Ollama doesn't need API key
16
18
  }
17
- async invoke(params) {
18
- const url = `${this.baseUrl}/api/chat`;
19
+ /**
20
+ * Ollama supports streaming.
21
+ */
22
+ supportsStreaming() {
23
+ return true;
24
+ }
25
+ /**
26
+ * Build request body for Ollama API
27
+ */
28
+ buildRequestBody(params, stream) {
19
29
  let messages = params.messages.map(m => ({ role: m.role, content: m.content }));
20
30
  // Add JSON mode if schema provided
21
31
  if (params.jsonSchema) {
@@ -31,7 +41,7 @@ export class OllamaProvider extends BaseProvider {
31
41
  const body = {
32
42
  model: this.model,
33
43
  messages,
34
- stream: false,
44
+ stream,
35
45
  options: {
36
46
  temperature: params.temperature ?? 0.7,
37
47
  num_predict: params.maxTokens ?? 4096,
@@ -41,6 +51,11 @@ export class OllamaProvider extends BaseProvider {
41
51
  if (params.jsonSchema) {
42
52
  body.format = 'json';
43
53
  }
54
+ return body;
55
+ }
56
+ async invoke(params) {
57
+ const url = `${this.baseUrl}/api/chat`;
58
+ const body = this.buildRequestBody(params, false);
44
59
  const response = await fetch(url, {
45
60
  method: 'POST',
46
61
  headers: { 'Content-Type': 'application/json' },
@@ -61,4 +76,97 @@ export class OllamaProvider extends BaseProvider {
61
76
  },
62
77
  };
63
78
  }
79
+ /**
80
+ * Stream-based invoke using Ollama's streaming API.
81
+ * Yields content chunks as they arrive from the API.
82
+ */
83
+ async *invokeStream(params) {
84
+ const url = `${this.baseUrl}/api/chat`;
85
+ const body = this.buildRequestBody(params, true);
86
+ const response = await fetch(url, {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify(body),
90
+ });
91
+ if (!response.ok) {
92
+ const error = await response.text();
93
+ throw new Error(`Ollama API error: ${response.status} - ${error}`);
94
+ }
95
+ if (!response.body) {
96
+ throw new Error('Ollama API returned no body for streaming request');
97
+ }
98
+ const reader = response.body.getReader();
99
+ const decoder = new TextDecoder('utf-8');
100
+ const collectedChunks = [];
101
+ let promptTokens = 0;
102
+ let completionTokens = 0;
103
+ let buffer = '';
104
+ try {
105
+ while (true) {
106
+ const { done, value } = await reader.read();
107
+ if (done)
108
+ break;
109
+ buffer += decoder.decode(value, { stream: true });
110
+ // Process complete lines from the buffer (NDJSON format)
111
+ const lines = buffer.split('\n');
112
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
113
+ for (const line of lines) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed)
116
+ continue;
117
+ try {
118
+ const data = JSON.parse(trimmed);
119
+ // Extract content chunk
120
+ const content = data.message?.content;
121
+ if (content) {
122
+ collectedChunks.push(content);
123
+ yield content;
124
+ }
125
+ // Extract usage info (in the final message when done=true)
126
+ if (data.done) {
127
+ promptTokens = data.prompt_eval_count || 0;
128
+ completionTokens = data.eval_count || 0;
129
+ }
130
+ }
131
+ catch {
132
+ // Skip invalid JSON lines
133
+ }
134
+ }
135
+ }
136
+ // Flush decoder and process trailing buffered data even without trailing newline.
137
+ buffer += decoder.decode();
138
+ for (const line of buffer.split('\n')) {
139
+ const trimmed = line.trim();
140
+ if (!trimmed)
141
+ continue;
142
+ try {
143
+ const data = JSON.parse(trimmed);
144
+ const content = data.message?.content;
145
+ if (content) {
146
+ collectedChunks.push(content);
147
+ yield content;
148
+ }
149
+ if (data.done) {
150
+ promptTokens = data.prompt_eval_count || 0;
151
+ completionTokens = data.eval_count || 0;
152
+ }
153
+ }
154
+ catch {
155
+ // Skip invalid JSON lines
156
+ }
157
+ }
158
+ }
159
+ finally {
160
+ reader.releaseLock();
161
+ }
162
+ const fullContent = collectedChunks.join('');
163
+ return {
164
+ content: fullContent,
165
+ usage: {
166
+ promptTokens,
167
+ completionTokens,
168
+ totalTokens: promptTokens + completionTokens,
169
+ },
170
+ };
171
+ }
64
172
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * OpenAI Provider - OpenAI API (and compatible APIs)
3
+ *
4
+ * Supports both streaming and non-streaming invocation.
3
5
  */
4
6
  import { BaseProvider } from './base.js';
5
7
  import type { InvokeParams, InvokeResult } from '../types.js';
@@ -10,5 +12,14 @@ export declare class OpenAIProvider extends BaseProvider {
10
12
  private baseUrl;
11
13
  constructor(apiKey?: string, model?: string, baseUrl?: string);
12
14
  isConfigured(): boolean;
15
+ /**
16
+ * OpenAI supports streaming.
17
+ */
18
+ supportsStreaming(): boolean;
13
19
  invoke(params: InvokeParams): Promise<InvokeResult>;
20
+ /**
21
+ * Stream-based invoke using OpenAI's streaming API.
22
+ * Yields content chunks as they arrive from the API.
23
+ */
24
+ invokeStream(params: InvokeParams): AsyncGenerator<string, InvokeResult, unknown>;
14
25
  }