geminisdk 0.1.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.
- package/README.md +129 -0
- package/dist/chunk-Y4MC6YDW.mjs +135 -0
- package/dist/index.d.mts +511 -0
- package/dist/index.d.ts +511 -0
- package/dist/index.js +1696 -0
- package/dist/index.mjs +1480 -0
- package/dist/types-ADTG4FSI.mjs +46 -0
- package/package.json +60 -0
- package/src/auth.ts +293 -0
- package/src/backend.ts +615 -0
- package/src/client.ts +230 -0
- package/src/exceptions.ts +289 -0
- package/src/index.ts +148 -0
- package/src/session.ts +380 -0
- package/src/tools.ts +127 -0
- package/src/types.ts +352 -0
package/src/backend.ts
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend for Gemini CLI / Google Code Assist API.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { GeminiOAuthManager } from './auth.js';
|
|
7
|
+
import {
|
|
8
|
+
APIError,
|
|
9
|
+
OnboardingError,
|
|
10
|
+
PermissionDeniedError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
} from './exceptions.js';
|
|
13
|
+
import {
|
|
14
|
+
FunctionCall,
|
|
15
|
+
GenerationConfig,
|
|
16
|
+
HTTP_FORBIDDEN,
|
|
17
|
+
HTTP_UNAUTHORIZED,
|
|
18
|
+
LLMChunk,
|
|
19
|
+
LLMUsage,
|
|
20
|
+
Message,
|
|
21
|
+
Role,
|
|
22
|
+
ThinkingConfig,
|
|
23
|
+
Tool,
|
|
24
|
+
ToolCall,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
|
|
27
|
+
const RETRYABLE_STATUS_CODES = new Set([HTTP_UNAUTHORIZED, HTTP_FORBIDDEN]);
|
|
28
|
+
const ONBOARD_MAX_RETRIES = 30;
|
|
29
|
+
const ONBOARD_SLEEP_SECONDS = 2;
|
|
30
|
+
|
|
31
|
+
function sleep(ms: number): Promise<void> {
|
|
32
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Simple UUID generator if uuid package isn't available
|
|
36
|
+
function generateUUID(): string {
|
|
37
|
+
try {
|
|
38
|
+
return uuidv4();
|
|
39
|
+
} catch {
|
|
40
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
41
|
+
const r = (Math.random() * 16) | 0;
|
|
42
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
43
|
+
return v.toString(16);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BackendOptions {
|
|
49
|
+
timeout?: number;
|
|
50
|
+
oauthPath?: string;
|
|
51
|
+
clientId?: string;
|
|
52
|
+
clientSecret?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class GeminiBackend {
|
|
56
|
+
private timeout: number;
|
|
57
|
+
private oauthManager: GeminiOAuthManager;
|
|
58
|
+
private projectId: string | null = null;
|
|
59
|
+
|
|
60
|
+
constructor(options: BackendOptions = {}) {
|
|
61
|
+
this.timeout = options.timeout ?? 720000;
|
|
62
|
+
this.oauthManager = new GeminiOAuthManager(
|
|
63
|
+
options.oauthPath,
|
|
64
|
+
options.clientId,
|
|
65
|
+
options.clientSecret
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async getAuthHeaders(forceRefresh = false): Promise<Record<string, string>> {
|
|
70
|
+
const accessToken = await this.oauthManager.ensureAuthenticated(forceRefresh);
|
|
71
|
+
return {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
Authorization: `Bearer ${accessToken}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private prepareMessages(messages: Message[]): Array<Record<string, unknown>> {
|
|
78
|
+
const result: Array<Record<string, unknown>> = [];
|
|
79
|
+
|
|
80
|
+
for (const msg of messages) {
|
|
81
|
+
const role = msg.role === Role.ASSISTANT ? 'model' : 'user';
|
|
82
|
+
const contentParts: Array<Record<string, unknown>> = [];
|
|
83
|
+
|
|
84
|
+
if (msg.content) {
|
|
85
|
+
if (typeof msg.content === 'string') {
|
|
86
|
+
contentParts.push({ text: msg.content });
|
|
87
|
+
} else {
|
|
88
|
+
for (const part of msg.content) {
|
|
89
|
+
if (part.text) {
|
|
90
|
+
contentParts.push({ text: part.text });
|
|
91
|
+
} else if (part.imageData && part.imageMimeType) {
|
|
92
|
+
contentParts.push({
|
|
93
|
+
inlineData: {
|
|
94
|
+
mimeType: part.imageMimeType,
|
|
95
|
+
data:
|
|
96
|
+
part.imageData instanceof Uint8Array
|
|
97
|
+
? Buffer.from(part.imageData).toString('base64')
|
|
98
|
+
: part.imageData,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (msg.toolCalls) {
|
|
107
|
+
for (const tc of msg.toolCalls) {
|
|
108
|
+
const args =
|
|
109
|
+
typeof tc.function.arguments === 'string'
|
|
110
|
+
? JSON.parse(tc.function.arguments)
|
|
111
|
+
: tc.function.arguments;
|
|
112
|
+
contentParts.push({
|
|
113
|
+
functionCall: {
|
|
114
|
+
name: tc.function.name,
|
|
115
|
+
args,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (msg.toolCallId) {
|
|
122
|
+
contentParts.push({
|
|
123
|
+
functionResponse: {
|
|
124
|
+
name: msg.name ?? '',
|
|
125
|
+
response:
|
|
126
|
+
typeof msg.content === 'string'
|
|
127
|
+
? { result: msg.content }
|
|
128
|
+
: msg.content,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (contentParts.length > 0) {
|
|
134
|
+
result.push({ role, parts: contentParts });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private prepareTools(tools?: Tool[]): Array<Record<string, unknown>> | undefined {
|
|
142
|
+
if (!tools || tools.length === 0) return undefined;
|
|
143
|
+
|
|
144
|
+
const funcDecls: Array<Record<string, unknown>> = [];
|
|
145
|
+
|
|
146
|
+
for (const tool of tools) {
|
|
147
|
+
const funcDef: Record<string, unknown> = {
|
|
148
|
+
name: tool.name,
|
|
149
|
+
description: tool.description ?? '',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (tool.parameters) {
|
|
153
|
+
funcDef['parameters'] = {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: (tool.parameters as Record<string, unknown>)['properties'] ?? {},
|
|
156
|
+
required: (tool.parameters as Record<string, unknown>)['required'] ?? [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
funcDecls.push(funcDef);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [{ functionDeclarations: funcDecls }];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async ensureProjectId(accessToken: string): Promise<string> {
|
|
167
|
+
if (this.projectId !== null) return this.projectId;
|
|
168
|
+
|
|
169
|
+
const envProjectId = this.oauthManager.getProjectId();
|
|
170
|
+
const headers = {
|
|
171
|
+
Authorization: `Bearer ${accessToken}`,
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const clientMetadata = {
|
|
176
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
177
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
178
|
+
pluginType: 'GEMINI',
|
|
179
|
+
duetProject: envProjectId,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const loadRequest = {
|
|
183
|
+
cloudaicompanionProject: envProjectId,
|
|
184
|
+
metadata: clientMetadata,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const url = `${this.oauthManager.getApiEndpoint()}:loadCodeAssist`;
|
|
189
|
+
const response = await fetch(url, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers,
|
|
192
|
+
body: JSON.stringify(loadRequest),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
throw new Error(`HTTP ${response.status}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
200
|
+
|
|
201
|
+
if (data['currentTier']) {
|
|
202
|
+
const projectFromApi = data['cloudaicompanionProject'] as string | undefined;
|
|
203
|
+
if (projectFromApi) {
|
|
204
|
+
this.projectId = projectFromApi;
|
|
205
|
+
return projectFromApi;
|
|
206
|
+
}
|
|
207
|
+
if (envProjectId) {
|
|
208
|
+
this.projectId = envProjectId;
|
|
209
|
+
return envProjectId;
|
|
210
|
+
}
|
|
211
|
+
this.projectId = '';
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Need to onboard
|
|
216
|
+
const allowedTiers = (data['allowedTiers'] ?? []) as Array<Record<string, unknown>>;
|
|
217
|
+
let tierId = 'free-tier';
|
|
218
|
+
for (const tier of allowedTiers) {
|
|
219
|
+
if (tier['isDefault']) {
|
|
220
|
+
tierId = (tier['id'] as string) ?? 'free-tier';
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return this.onboardForProject(headers, envProjectId, clientMetadata, tierId);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw new APIError(
|
|
228
|
+
`Gemini Code Assist access denied: ${error}`,
|
|
229
|
+
403,
|
|
230
|
+
String(error)
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async onboardForProject(
|
|
236
|
+
headers: Record<string, string>,
|
|
237
|
+
envProjectId: string | null,
|
|
238
|
+
clientMetadata: Record<string, unknown>,
|
|
239
|
+
tierId: string
|
|
240
|
+
): Promise<string> {
|
|
241
|
+
const onboardRequest =
|
|
242
|
+
tierId === 'free-tier'
|
|
243
|
+
? {
|
|
244
|
+
tierId,
|
|
245
|
+
cloudaicompanionProject: null,
|
|
246
|
+
metadata: clientMetadata,
|
|
247
|
+
}
|
|
248
|
+
: {
|
|
249
|
+
tierId,
|
|
250
|
+
cloudaicompanionProject: envProjectId,
|
|
251
|
+
metadata: { ...clientMetadata, duetProject: envProjectId },
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const url = `${this.oauthManager.getApiEndpoint()}:onboardUser`;
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < ONBOARD_MAX_RETRIES; i++) {
|
|
257
|
+
const response = await fetch(url, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers,
|
|
260
|
+
body: JSON.stringify(onboardRequest),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
throw new Error(`Onboard failed: ${response.status}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const lroData = (await response.json()) as Record<string, unknown>;
|
|
268
|
+
|
|
269
|
+
if (lroData['done']) {
|
|
270
|
+
const responseData = (lroData['response'] ?? {}) as Record<string, unknown>;
|
|
271
|
+
const cloudAiCompanion = responseData['cloudaicompanionProject'] as
|
|
272
|
+
| Record<string, unknown>
|
|
273
|
+
| undefined;
|
|
274
|
+
if (cloudAiCompanion?.['id']) {
|
|
275
|
+
this.projectId = cloudAiCompanion['id'] as string;
|
|
276
|
+
return this.projectId;
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await sleep(ONBOARD_SLEEP_SECONDS * 1000);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (tierId === 'free-tier') {
|
|
285
|
+
this.projectId = '';
|
|
286
|
+
return '';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
throw new OnboardingError(undefined, tierId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private buildRequestPayload(
|
|
293
|
+
model: string,
|
|
294
|
+
messages: Message[],
|
|
295
|
+
generationConfig?: GenerationConfig,
|
|
296
|
+
thinkingConfig?: ThinkingConfig,
|
|
297
|
+
tools?: Tool[],
|
|
298
|
+
projectId = ''
|
|
299
|
+
): Record<string, unknown> {
|
|
300
|
+
const genConfig: Record<string, unknown> = {
|
|
301
|
+
temperature: generationConfig?.temperature ?? 0.7,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
if (generationConfig?.maxOutputTokens) {
|
|
305
|
+
genConfig['maxOutputTokens'] = generationConfig.maxOutputTokens;
|
|
306
|
+
}
|
|
307
|
+
if (generationConfig?.topP !== undefined) {
|
|
308
|
+
genConfig['topP'] = generationConfig.topP;
|
|
309
|
+
}
|
|
310
|
+
if (generationConfig?.topK !== undefined) {
|
|
311
|
+
genConfig['topK'] = generationConfig.topK;
|
|
312
|
+
}
|
|
313
|
+
if (generationConfig?.stopSequences) {
|
|
314
|
+
genConfig['stopSequences'] = generationConfig.stopSequences;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (thinkingConfig?.includeThoughts) {
|
|
318
|
+
genConfig['thinkingConfig'] = {
|
|
319
|
+
includeThoughts: thinkingConfig.includeThoughts,
|
|
320
|
+
...(thinkingConfig.thinkingBudget && {
|
|
321
|
+
thinkingBudget: thinkingConfig.thinkingBudget,
|
|
322
|
+
}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const requestBody: Record<string, unknown> = {
|
|
327
|
+
contents: this.prepareMessages(messages),
|
|
328
|
+
generationConfig: genConfig,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const preparedTools = this.prepareTools(tools);
|
|
332
|
+
if (preparedTools) {
|
|
333
|
+
requestBody['tools'] = preparedTools;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const payload: Record<string, unknown> = {
|
|
337
|
+
model,
|
|
338
|
+
request: requestBody,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if (projectId) {
|
|
342
|
+
payload['project'] = projectId;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return payload;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private parseCompletionResponse(data: Record<string, unknown>): LLMChunk {
|
|
349
|
+
const responseData = (data['response'] ?? data) as Record<string, unknown>;
|
|
350
|
+
const candidates = (responseData['candidates'] ?? []) as Array<Record<string, unknown>>;
|
|
351
|
+
|
|
352
|
+
if (candidates.length === 0) {
|
|
353
|
+
return {
|
|
354
|
+
content: '',
|
|
355
|
+
reasoningContent: undefined,
|
|
356
|
+
toolCalls: undefined,
|
|
357
|
+
usage: undefined,
|
|
358
|
+
finishReason: undefined,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const candidate = candidates[0]!;
|
|
363
|
+
const contentObj = (candidate['content'] ?? {}) as Record<string, unknown>;
|
|
364
|
+
const parts = (contentObj['parts'] ?? []) as Array<Record<string, unknown>>;
|
|
365
|
+
|
|
366
|
+
let textContent = '';
|
|
367
|
+
let reasoningContent: string | undefined;
|
|
368
|
+
let toolCalls: ToolCall[] | undefined;
|
|
369
|
+
|
|
370
|
+
for (const part of parts) {
|
|
371
|
+
if (part['text']) {
|
|
372
|
+
textContent += part['text'] as string;
|
|
373
|
+
}
|
|
374
|
+
if (part['thought']) {
|
|
375
|
+
reasoningContent = part['thought'] as string;
|
|
376
|
+
}
|
|
377
|
+
if (part['functionCall']) {
|
|
378
|
+
const fc = part['functionCall'] as Record<string, unknown>;
|
|
379
|
+
if (!toolCalls) toolCalls = [];
|
|
380
|
+
toolCalls.push({
|
|
381
|
+
id: generateUUID(),
|
|
382
|
+
type: 'function',
|
|
383
|
+
function: {
|
|
384
|
+
name: (fc['name'] as string) ?? '',
|
|
385
|
+
arguments: (fc['args'] ?? fc['arguments'] ?? {}) as Record<string, unknown>,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const usageData = (data['usageMetadata'] ??
|
|
392
|
+
responseData['usageMetadata'] ?? {}) as Record<string, unknown>;
|
|
393
|
+
let usage: LLMUsage | undefined;
|
|
394
|
+
|
|
395
|
+
if (Object.keys(usageData).length > 0) {
|
|
396
|
+
usage = {
|
|
397
|
+
promptTokens: (usageData['promptTokenCount'] as number) ?? 0,
|
|
398
|
+
completionTokens: (usageData['candidatesTokenCount'] as number) ?? 0,
|
|
399
|
+
totalTokens: (usageData['totalTokenCount'] as number) ?? 0,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
content: textContent,
|
|
405
|
+
reasoningContent,
|
|
406
|
+
toolCalls,
|
|
407
|
+
usage,
|
|
408
|
+
finishReason: candidate['finishReason'] as string | undefined,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public async complete(options: {
|
|
413
|
+
model: string;
|
|
414
|
+
messages: Message[];
|
|
415
|
+
generationConfig?: GenerationConfig;
|
|
416
|
+
thinkingConfig?: ThinkingConfig;
|
|
417
|
+
tools?: Tool[];
|
|
418
|
+
extraHeaders?: Record<string, string>;
|
|
419
|
+
}): Promise<LLMChunk> {
|
|
420
|
+
return this.completeWithRetry(options, 0);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private async completeWithRetry(
|
|
424
|
+
options: {
|
|
425
|
+
model: string;
|
|
426
|
+
messages: Message[];
|
|
427
|
+
generationConfig?: GenerationConfig;
|
|
428
|
+
thinkingConfig?: ThinkingConfig;
|
|
429
|
+
tools?: Tool[];
|
|
430
|
+
extraHeaders?: Record<string, string>;
|
|
431
|
+
},
|
|
432
|
+
retryCount: number
|
|
433
|
+
): Promise<LLMChunk> {
|
|
434
|
+
const headers = await this.getAuthHeaders(retryCount > 0);
|
|
435
|
+
if (options.extraHeaders) {
|
|
436
|
+
Object.assign(headers, options.extraHeaders);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const accessToken = headers['Authorization']!.replace('Bearer ', '');
|
|
440
|
+
const projectId = await this.ensureProjectId(accessToken);
|
|
441
|
+
const url = `${this.oauthManager.getApiEndpoint()}:generateContent`;
|
|
442
|
+
|
|
443
|
+
const payload = this.buildRequestPayload(
|
|
444
|
+
options.model,
|
|
445
|
+
options.messages,
|
|
446
|
+
options.generationConfig,
|
|
447
|
+
options.thinkingConfig,
|
|
448
|
+
options.tools,
|
|
449
|
+
projectId
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const controller = new AbortController();
|
|
453
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const response = await fetch(url, {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers,
|
|
459
|
+
body: JSON.stringify(payload),
|
|
460
|
+
signal: controller.signal,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && retryCount === 0) {
|
|
464
|
+
this.oauthManager.invalidateCredentials();
|
|
465
|
+
return this.completeWithRetry(options, 1);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!response.ok) {
|
|
469
|
+
this.handleHttpError(response.status, await response.text());
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
473
|
+
return this.parseCompletionResponse(data);
|
|
474
|
+
} finally {
|
|
475
|
+
clearTimeout(timeoutId);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
public async *completeStreaming(options: {
|
|
480
|
+
model: string;
|
|
481
|
+
messages: Message[];
|
|
482
|
+
generationConfig?: GenerationConfig;
|
|
483
|
+
thinkingConfig?: ThinkingConfig;
|
|
484
|
+
tools?: Tool[];
|
|
485
|
+
extraHeaders?: Record<string, string>;
|
|
486
|
+
}): AsyncGenerator<LLMChunk, void, unknown> {
|
|
487
|
+
yield* this.completeStreamingWithRetry(options, 0);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private async *completeStreamingWithRetry(
|
|
491
|
+
options: {
|
|
492
|
+
model: string;
|
|
493
|
+
messages: Message[];
|
|
494
|
+
generationConfig?: GenerationConfig;
|
|
495
|
+
thinkingConfig?: ThinkingConfig;
|
|
496
|
+
tools?: Tool[];
|
|
497
|
+
extraHeaders?: Record<string, string>;
|
|
498
|
+
},
|
|
499
|
+
retryCount: number
|
|
500
|
+
): AsyncGenerator<LLMChunk, void, unknown> {
|
|
501
|
+
const headers = await this.getAuthHeaders(retryCount > 0);
|
|
502
|
+
if (options.extraHeaders) {
|
|
503
|
+
Object.assign(headers, options.extraHeaders);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const accessToken = headers['Authorization']!.replace('Bearer ', '');
|
|
507
|
+
const projectId = await this.ensureProjectId(accessToken);
|
|
508
|
+
const url = `${this.oauthManager.getApiEndpoint()}:streamGenerateContent?alt=sse`;
|
|
509
|
+
|
|
510
|
+
const payload = this.buildRequestPayload(
|
|
511
|
+
options.model,
|
|
512
|
+
options.messages,
|
|
513
|
+
options.generationConfig,
|
|
514
|
+
options.thinkingConfig,
|
|
515
|
+
options.tools,
|
|
516
|
+
projectId
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const controller = new AbortController();
|
|
520
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const response = await fetch(url, {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers,
|
|
526
|
+
body: JSON.stringify(payload),
|
|
527
|
+
signal: controller.signal,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && retryCount === 0) {
|
|
531
|
+
this.oauthManager.invalidateCredentials();
|
|
532
|
+
yield* this.completeStreamingWithRetry(options, 1);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!response.ok) {
|
|
537
|
+
this.handleHttpError(response.status, await response.text());
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!response.body) {
|
|
541
|
+
throw new APIError('No response body', 500);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const reader = response.body.getReader();
|
|
545
|
+
const decoder = new TextDecoder();
|
|
546
|
+
let buffer = '';
|
|
547
|
+
|
|
548
|
+
while (true) {
|
|
549
|
+
const { done, value } = await reader.read();
|
|
550
|
+
if (done) break;
|
|
551
|
+
|
|
552
|
+
buffer += decoder.decode(value, { stream: true });
|
|
553
|
+
const lines = buffer.split('\n');
|
|
554
|
+
buffer = lines.pop() ?? '';
|
|
555
|
+
|
|
556
|
+
for (const line of lines) {
|
|
557
|
+
const trimmed = line.trim();
|
|
558
|
+
if (!trimmed || trimmed.startsWith(':')) continue;
|
|
559
|
+
|
|
560
|
+
if (trimmed.startsWith('data:')) {
|
|
561
|
+
const data = trimmed.slice(5).trim();
|
|
562
|
+
if (data === '[DONE]') continue;
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const parsed = JSON.parse(data) as Record<string, unknown>;
|
|
566
|
+
if (parsed['error']) {
|
|
567
|
+
const errorMsg =
|
|
568
|
+
typeof parsed['error'] === 'object'
|
|
569
|
+
? ((parsed['error'] as Record<string, unknown>)['message'] as string)
|
|
570
|
+
: String(parsed['error']);
|
|
571
|
+
throw new APIError(errorMsg, 500);
|
|
572
|
+
}
|
|
573
|
+
yield this.parseCompletionResponse(parsed);
|
|
574
|
+
} catch (e) {
|
|
575
|
+
if (e instanceof APIError) throw e;
|
|
576
|
+
// Skip invalid JSON
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} finally {
|
|
582
|
+
clearTimeout(timeoutId);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private handleHttpError(status: number, body: string): never {
|
|
587
|
+
let errorMsg = body;
|
|
588
|
+
try {
|
|
589
|
+
const errorData = JSON.parse(body) as Record<string, unknown>;
|
|
590
|
+
if (errorData['error']) {
|
|
591
|
+
const err = errorData['error'] as Record<string, unknown>;
|
|
592
|
+
errorMsg = (err['message'] as string) ?? body;
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
// Use body as-is
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (status === 429) {
|
|
599
|
+
throw new RateLimitError(`Rate limit exceeded: ${errorMsg}`, 429, undefined, body);
|
|
600
|
+
} else if (status === 403) {
|
|
601
|
+
throw new PermissionDeniedError(`Permission denied: ${errorMsg}`, 403, body);
|
|
602
|
+
} else {
|
|
603
|
+
throw new APIError(`API error: ${errorMsg}`, status, body);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
public async listModels(): Promise<string[]> {
|
|
608
|
+
const { GEMINI_CLI_MODELS } = await import('./types.js');
|
|
609
|
+
return Object.keys(GEMINI_CLI_MODELS);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
public async close(): Promise<void> {
|
|
613
|
+
// No persistent connections to close in fetch-based implementation
|
|
614
|
+
}
|
|
615
|
+
}
|