minovative-mind-cli 2.3.0 → 2.3.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.
@@ -1,18 +1,4 @@
1
1
  import type { Content, Tool, ToolConfig, FunctionCall } from '@google/generative-ai';
2
- /**
3
- * ============================================================================
4
- * PROXY CLIENT SERVICE
5
- * ============================================================================
6
- * Facilitates stream-based communication with the secure, Firebase-authenticated
7
- * serverless Gemini content generation proxy.
8
- *
9
- * Core Capabilities:
10
- * - Server-Sent Events (SSE) parsing for thoughts, text, and function calls.
11
- * - Secure Authentication handling (Firebase ID Tokens).
12
- * - Real-time stream-callback piping for instant responses.
13
- * - Accurate token usage, credit consumption, and grounding metadata parsing.
14
- * ============================================================================
15
- */
16
2
  /**
17
3
  * Metadata containing real-time proxy token and credit usage diagnostics.
18
4
  */
@@ -1,4 +1,38 @@
1
1
  import { debugLog } from '../utils/logger.js';
2
+ /**
3
+ * ============================================================================
4
+ * PROXY CLIENT SERVICE
5
+ * ============================================================================
6
+ * Facilitates stream-based communication with the secure, Firebase-authenticated
7
+ * serverless Gemini content generation proxy.
8
+ *
9
+ * Core Capabilities:
10
+ * - Server-Sent Events (SSE) parsing for thoughts, text, and function calls.
11
+ * - Secure Authentication handling (Firebase ID Tokens).
12
+ * - Real-time stream-callback piping for instant responses.
13
+ * - Accurate token usage, credit consumption, and grounding metadata parsing.
14
+ * ============================================================================
15
+ */
16
+ /**
17
+ * Helper to pause execution for a specified duration, respecting abort signals.
18
+ */
19
+ async function delay(ms, abortSignal) {
20
+ return new Promise((resolve, reject) => {
21
+ let timeout;
22
+ const abortHandler = () => {
23
+ clearTimeout(timeout);
24
+ reject(new Error('Operation aborted'));
25
+ };
26
+ if (abortSignal?.aborted) {
27
+ return abortHandler();
28
+ }
29
+ abortSignal?.addEventListener('abort', abortHandler);
30
+ timeout = setTimeout(() => {
31
+ abortSignal?.removeEventListener('abort', abortHandler);
32
+ resolve();
33
+ }, ms);
34
+ });
35
+ }
2
36
  let globalSessionAccumulatedUsage = {
3
37
  promptTokens: 0,
4
38
  candidatesTokens: 0,
@@ -45,132 +79,162 @@ export class ProxyClient {
45
79
  * @throws {Error} If authentication fails (401), credits are insufficient (402), or network/proxy errors occur.
46
80
  */
47
81
  async generateFunctionCallViaProxy(idToken, modelName, contents, tools, toolConfig, systemInstruction, generationConfig, streamCallbacks, abortSignal) {
48
- const response = await fetch(this.PROXY_URL, {
49
- method: 'POST',
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- 'X-Firebase-Auth': `Bearer ${idToken}`,
53
- },
54
- body: JSON.stringify({
55
- model: modelName,
56
- contents,
57
- tools,
58
- toolConfig,
59
- systemInstruction,
60
- generationConfig,
61
- }),
62
- signal: abortSignal,
63
- });
64
- debugLog(`Proxy Request to ${modelName} complete. Status: ${response.status} ${response.statusText}`);
65
- if (response.status === 401) {
66
- let details = '';
67
- try {
68
- const text = await response.text();
82
+ const MAX_RETRIES = 5;
83
+ const BASE_DELAY_MS = 2000;
84
+ const MAX_DELAY_MS = 30000;
85
+ let attempt = 0;
86
+ retryLoop: while (true) {
87
+ const response = await fetch(this.PROXY_URL, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ 'X-Firebase-Auth': `Bearer ${idToken}`,
92
+ },
93
+ body: JSON.stringify({
94
+ model: modelName,
95
+ contents,
96
+ tools,
97
+ toolConfig,
98
+ systemInstruction,
99
+ generationConfig,
100
+ }),
101
+ signal: abortSignal,
102
+ });
103
+ debugLog(`Proxy Request to ${modelName} complete. Status: ${response.status} ${response.statusText}`);
104
+ if ((response.status === 429 || response.status === 503) && attempt < MAX_RETRIES) {
105
+ const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
106
+ const delayTime = Math.round(exponentialDelay * (0.5 + Math.random() * 0.5));
107
+ console.warn(`Rate limit or service unavailable hit (${response.status}). Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
108
+ await delay(delayTime, abortSignal);
109
+ attempt++;
110
+ continue;
111
+ }
112
+ if (response.status === 401) {
113
+ let details = '';
69
114
  try {
70
- const errorData = JSON.parse(text);
71
- details = errorData.details || errorData.error || text;
115
+ const text = await response.text();
116
+ try {
117
+ const errorData = JSON.parse(text);
118
+ details = errorData.details || errorData.error || text;
119
+ }
120
+ catch (e) {
121
+ details = text;
122
+ }
72
123
  }
73
124
  catch (e) {
74
- details = text;
125
+ details = 'Unknown error reading body';
75
126
  }
127
+ throw new Error(`Authentication failed: ${details}. Please login again.`);
76
128
  }
77
- catch (e) {
78
- details = 'Unknown error reading body';
129
+ if (response.status === 402) {
130
+ throw new Error('Insufficient credits. Please visit minovativemind.dev to purchase more credits.');
79
131
  }
80
- throw new Error(`Authentication failed: ${details}. Please login again.`);
81
- }
82
- if (response.status === 402) {
83
- throw new Error('Insufficient credits. Please visit minovativemind.dev to purchase more credits.');
84
- }
85
- if (!response.ok) {
86
- const errorData = await response.json().catch(() => ({}));
87
- throw new Error(`Proxy error ${response.status}: ${errorData.error || response.statusText}`);
88
- }
89
- if (!response.body) {
90
- throw new Error('No response body received from proxy');
91
- }
92
- // Node 18+ fetch body is a ReadableStream which is async iterable but type might mismatch.
93
- // Let's read chunks manually
94
- const reader = response.body.getReader();
95
- const decoder = new TextDecoder();
96
- let buffer = '';
97
- let functionCall = null;
98
- const functionCalls = [];
99
- let thought = '';
100
- let parts = undefined;
101
- let usageMetadata = undefined;
102
- let groundingMetadata = undefined;
103
- try {
104
- while (true) {
105
- const { done, value } = await reader.read();
106
- if (done)
107
- break;
108
- buffer += decoder.decode(value, { stream: true });
109
- const lines = buffer.split('\n\n');
110
- buffer = lines.pop() || '';
111
- for (const line of lines) {
112
- if (!line.startsWith('data: '))
113
- continue;
114
- const dataStr = line.slice(6);
115
- try {
116
- const data = JSON.parse(dataStr);
117
- if (data.type === 'functionCall' && data.functionCall) {
118
- functionCall = data.functionCall;
119
- functionCalls.push(data.functionCall);
120
- }
121
- else if (data.type === 'thought' && data.thought) {
122
- thought += data.thought;
123
- if (streamCallbacks?.onChunk)
124
- streamCallbacks.onChunk(data.thought);
125
- }
126
- else if (data.type === 'chunk' && data.text) {
127
- thought += data.text;
128
- if (streamCallbacks?.onChunk)
129
- streamCallbacks.onChunk(data.text);
130
- }
131
- else if (data.type === 'parts' && data.parts) {
132
- parts = data.parts;
133
- }
134
- else if (data.type === 'done') {
135
- if (data.usage) {
136
- usageMetadata = data.usage;
137
- globalSessionAccumulatedUsage.promptTokens += data.usage.promptTokens || 0;
138
- globalSessionAccumulatedUsage.candidatesTokens += data.usage.candidatesTokens || 0;
139
- globalSessionAccumulatedUsage.cachedTokens += data.usage.cachedTokens || 0;
140
- globalSessionAccumulatedUsage.creditsUsed += data.usage.creditsUsed || 0;
141
- globalSessionAccumulatedUsage.totalTokenCount +=
142
- (data.usage.promptTokens || 0) + (data.usage.cachedTokens || 0) + (data.usage.candidatesTokens || 0);
143
- if (data.usage.remainingBalance !== undefined) {
144
- globalSessionAccumulatedUsage.remainingBalance = data.usage.remainingBalance;
132
+ if (!response.ok) {
133
+ const errorData = await response.json().catch(() => ({}));
134
+ throw new Error(`Proxy error ${response.status}: ${errorData.error || response.statusText}`);
135
+ }
136
+ if (!response.body) {
137
+ throw new Error('No response body received from proxy');
138
+ }
139
+ // Node 18+ fetch body is a ReadableStream which is async iterable but type might mismatch.
140
+ // Let's read chunks manually
141
+ const reader = response.body.getReader();
142
+ const decoder = new TextDecoder();
143
+ let buffer = '';
144
+ let functionCall = null;
145
+ const functionCalls = [];
146
+ let thought = '';
147
+ let parts = undefined;
148
+ let usageMetadata = undefined;
149
+ let groundingMetadata = undefined;
150
+ try {
151
+ while (true) {
152
+ const { done, value } = await reader.read();
153
+ if (done)
154
+ break;
155
+ buffer += decoder.decode(value, { stream: true });
156
+ const lines = buffer.split('\n\n');
157
+ buffer = lines.pop() || '';
158
+ for (const line of lines) {
159
+ if (!line.startsWith('data: '))
160
+ continue;
161
+ const dataStr = line.slice(6);
162
+ try {
163
+ const data = JSON.parse(dataStr);
164
+ if (data.type === 'functionCall' && data.functionCall) {
165
+ functionCall = data.functionCall;
166
+ functionCalls.push(data.functionCall);
167
+ }
168
+ else if (data.type === 'thought' && data.thought) {
169
+ thought += data.thought;
170
+ if (streamCallbacks?.onChunk)
171
+ streamCallbacks.onChunk(data.thought);
172
+ }
173
+ else if (data.type === 'chunk' && data.text) {
174
+ thought += data.text;
175
+ if (streamCallbacks?.onChunk)
176
+ streamCallbacks.onChunk(data.text);
177
+ }
178
+ else if (data.type === 'parts' && data.parts) {
179
+ parts = data.parts;
180
+ }
181
+ else if (data.type === 'done') {
182
+ if (data.usage) {
183
+ usageMetadata = data.usage;
184
+ globalSessionAccumulatedUsage.promptTokens += data.usage.promptTokens || 0;
185
+ globalSessionAccumulatedUsage.candidatesTokens += data.usage.candidatesTokens || 0;
186
+ globalSessionAccumulatedUsage.cachedTokens += data.usage.cachedTokens || 0;
187
+ globalSessionAccumulatedUsage.creditsUsed += data.usage.creditsUsed || 0;
188
+ globalSessionAccumulatedUsage.totalTokenCount +=
189
+ (data.usage.promptTokens || 0) + (data.usage.cachedTokens || 0) + (data.usage.candidatesTokens || 0);
190
+ if (data.usage.remainingBalance !== undefined) {
191
+ globalSessionAccumulatedUsage.remainingBalance = data.usage.remainingBalance;
192
+ }
193
+ }
194
+ if (data.groundingMetadata) {
195
+ groundingMetadata = data.groundingMetadata;
145
196
  }
146
197
  }
147
- if (data.groundingMetadata) {
148
- groundingMetadata = data.groundingMetadata;
198
+ else if (data.type === 'error') {
199
+ throw new Error(`Proxy generation error: ${data.message}`);
149
200
  }
150
201
  }
151
- else if (data.type === 'error') {
152
- throw new Error(`Proxy generation error: ${data.message}`);
202
+ catch (parseError) {
203
+ if (parseError.message && parseError.message.startsWith('Proxy generation error:')) {
204
+ throw parseError;
205
+ }
206
+ debugLog(`Failed to parse SSE data: ${dataStr} - Error: ${parseError.message || parseError}`);
153
207
  }
154
208
  }
155
- catch (parseError) {
156
- if (parseError.message && parseError.message.startsWith('Proxy generation error:')) {
157
- throw parseError;
158
- }
159
- debugLog(`Failed to parse SSE data: ${dataStr} - Error: ${parseError.message || parseError}`);
209
+ }
210
+ }
211
+ catch (streamError) {
212
+ if (streamError.message?.includes('429') ||
213
+ streamError.message?.includes('503') ||
214
+ streamError.message?.includes('RESOURCE_EXHAUSTED') ||
215
+ streamError.message?.includes('Too Many Requests')) {
216
+ if (attempt < MAX_RETRIES) {
217
+ const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
218
+ const delayTime = Math.round(exponentialDelay * (0.5 + Math.random() * 0.5));
219
+ console.warn(`Rate limit or service unavailable hit during stream. Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
220
+ await delay(delayTime, abortSignal);
221
+ attempt++;
222
+ continue retryLoop;
160
223
  }
161
224
  }
225
+ throw streamError;
162
226
  }
227
+ finally {
228
+ reader.releaseLock();
229
+ }
230
+ return {
231
+ functionCall,
232
+ functionCalls,
233
+ thought: thought.trim(),
234
+ parts,
235
+ usageMetadata,
236
+ groundingMetadata,
237
+ };
163
238
  }
164
- finally {
165
- reader.releaseLock();
166
- }
167
- return {
168
- functionCall,
169
- functionCalls,
170
- thought: thought.trim(),
171
- parts,
172
- usageMetadata,
173
- groundingMetadata,
174
- };
175
239
  }
176
240
  }
@@ -65,5 +65,5 @@
65
65
  ]
66
66
  }
67
67
  },
68
- "version": "2.3.0"
68
+ "version": "2.3.1"
69
69
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "minovative-mind-cli",
3
3
  "description": "An automated AI agent powered by Vertex AI that helps you write software",
4
- "version": "2.3.0",
4
+ "version": "2.3.1",
5
5
  "author": "Daniel Ward",
6
6
  "bin": {
7
7
  "minovative-mind-cli": "bin/run.js"