@gullabs/google 0.2.0 → 0.3.0

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/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { assertNever, LlmError, classifyError, redactSecrets } from '@gullabs/core';
2
- import { ZodOptional, ZodNullable, ZodDefault, ZodObject, ZodString, ZodNumber, ZodBoolean, ZodArray, ZodEnum, ZodLiteral } from 'zod';
1
+ import { LlmError, assertNever, classifyError, redactSecrets } from '@gullabs/core';
3
2
 
4
3
  // src/adapter.ts
5
4
 
6
5
  // src/client.ts
7
6
  var FLEX_DEFAULT_TIMEOUT_MS = 15e5;
7
+ var STANDARD_DEFAULT_TIMEOUT_MS = 3e5;
8
8
  var TRANSPORT_TIMEOUT_BUFFER_MS = 5e3;
9
9
  async function buildGoogleClient(auth) {
10
10
  const { GoogleGenAI } = await import('@google/genai');
@@ -18,121 +18,34 @@ async function buildGoogleClient(auth) {
18
18
  }
19
19
  };
20
20
  }
21
- function isOptionalField(schema) {
22
- if (schema instanceof ZodOptional || schema instanceof ZodDefault) return true;
23
- if (schema instanceof ZodNullable) {
24
- const unwrapped = schema.unwrap();
25
- return isOptionalField(unwrapped);
26
- }
27
- return false;
28
- }
29
- function zodToGeminiSchema(schema) {
30
- return convertSchema(schema, false);
31
- }
32
- function convertSchema(schema, nullable) {
33
- const desc = typeof schema.description === "string" ? schema.description : void 0;
34
- if (schema instanceof ZodOptional) {
35
- const unwrapped = schema.unwrap();
36
- return convertSchema(unwrapped, nullable);
37
- }
38
- if (schema instanceof ZodNullable) {
39
- const unwrapped = schema.unwrap();
40
- return convertSchema(unwrapped, true);
41
- }
42
- if (schema instanceof ZodDefault) {
43
- const innerType = schema._def.innerType;
44
- return convertSchema(innerType, nullable);
45
- }
46
- if (schema instanceof ZodObject) {
47
- const shape = schema.shape;
48
- const properties = {};
49
- const required = [];
50
- for (const [key, fieldType] of Object.entries(shape)) {
51
- const fieldSchema = convertSchema(fieldType, false);
52
- if (fieldSchema === void 0) return void 0;
53
- properties[key] = fieldSchema;
54
- if (!isOptionalField(fieldType)) {
55
- required.push(key);
56
- }
57
- }
58
- const result = {
59
- type: "object",
60
- properties,
61
- ...required.length > 0 ? { required } : {},
62
- ...desc !== void 0 ? { description: desc } : {},
63
- ...nullable ? { nullable: true } : {}
64
- };
65
- return result;
66
- }
67
- if (schema instanceof ZodString) {
68
- return {
69
- type: "string",
70
- ...desc !== void 0 ? { description: desc } : {},
71
- ...nullable ? { nullable: true } : {}
72
- };
73
- }
74
- if (schema instanceof ZodNumber) {
75
- const checks = schema._def.checks ?? [];
76
- const isInt = checks.some((c) => c.kind === "int");
77
- return {
78
- type: isInt ? "integer" : "number",
79
- ...desc !== void 0 ? { description: desc } : {},
80
- ...nullable ? { nullable: true } : {}
81
- };
82
- }
83
- if (schema instanceof ZodBoolean) {
84
- return {
85
- type: "boolean",
86
- ...desc !== void 0 ? { description: desc } : {},
87
- ...nullable ? { nullable: true } : {}
88
- };
89
- }
90
- if (schema instanceof ZodArray) {
91
- const element = schema.element;
92
- const items = convertSchema(element, false);
93
- if (items === void 0) return void 0;
94
- return {
95
- type: "array",
96
- items,
97
- ...desc !== void 0 ? { description: desc } : {},
98
- ...nullable ? { nullable: true } : {}
99
- };
100
- }
101
- if (schema instanceof ZodEnum) {
102
- return {
103
- type: "string",
104
- enum: schema.options,
105
- ...desc !== void 0 ? { description: desc } : {},
106
- ...nullable ? { nullable: true } : {}
107
- };
108
- }
109
- if (schema instanceof ZodLiteral) {
110
- const val = schema.value;
111
- if (typeof val === "string") {
112
- return {
113
- type: "string",
114
- enum: [val],
115
- ...desc !== void 0 ? { description: desc } : {},
116
- ...nullable ? { nullable: true } : {}
117
- };
118
- }
119
- if (typeof val === "number") {
120
- return {
121
- type: "number",
122
- ...desc !== void 0 ? { description: desc } : {},
123
- ...nullable ? { nullable: true } : {}
124
- };
125
- }
126
- if (typeof val === "boolean") {
127
- return {
128
- type: "boolean",
129
- ...desc !== void 0 ? { description: desc } : {},
130
- ...nullable ? { nullable: true } : {}
131
- };
132
- }
133
- return void 0;
21
+
22
+ // src/flex-fallback.ts
23
+ var CAPACITY_PATTERNS = [
24
+ /capacity/i,
25
+ /overload/i,
26
+ /overloaded/i,
27
+ /unavailable/i,
28
+ /no\s+capacity/i,
29
+ /temporar(?:y|ily)/i,
30
+ /try\s+again/i
31
+ ];
32
+ var QUOTA_PATTERNS = [
33
+ /quota/i,
34
+ /billing/i,
35
+ /billable/i,
36
+ /payment/i,
37
+ /rate\s+limit/i,
38
+ /exceeded/i,
39
+ /insufficient/i
40
+ ];
41
+ function isGeminiCapacityError(err) {
42
+ if (err.kind === "server") return err.httpStatus === 503;
43
+ if (err.kind !== "rate_limited") return false;
44
+ const message = err.message;
45
+ if (QUOTA_PATTERNS.some((pattern) => pattern.test(message))) {
46
+ return false;
134
47
  }
135
- return void 0;
48
+ return CAPACITY_PATTERNS.some((pattern) => pattern.test(message));
136
49
  }
137
50
 
138
51
  // src/adapter.ts
@@ -253,24 +166,45 @@ function geminiAdapter(opts) {
253
166
  if (genConfig.stopSequences !== void 0) {
254
167
  config.stopSequences = genConfig.stopSequences;
255
168
  }
256
- config.serviceTier = genConfig.serviceTier === "standard" ? "standard" : "flex";
169
+ const explicit = genConfig.serviceTier;
170
+ const supported = req.modelDescriptor?.capabilities?.serviceTiers;
171
+ if (explicit !== void 0) {
172
+ if (req.modelDescriptor !== void 0 && (supported === void 0 || !supported.includes(explicit))) {
173
+ throw new LlmError(
174
+ `serviceTier "${explicit}" is not supported for model "${model}".`,
175
+ { kind: "bad_request", retryable: false }
176
+ );
177
+ }
178
+ config.serviceTier = explicit;
179
+ } else {
180
+ if (req.modelDescriptor === void 0) {
181
+ config.serviceTier = "flex";
182
+ } else if (supported?.includes("flex") === true) {
183
+ config.serviceTier = "flex";
184
+ }
185
+ }
257
186
  const reasoning = genConfig.reasoning;
258
187
  if (reasoning !== void 0) {
259
188
  const reasoningApi = req.modelDescriptor?.capabilities?.reasoningApi;
189
+ if (reasoning.effort !== void 0 && reasoning.budgetTokens !== void 0) {
190
+ throw new LlmError(
191
+ `Provide either reasoning.effort or reasoning.budgetTokens, not both, for model "${model}".`,
192
+ { kind: "bad_request", retryable: false }
193
+ );
194
+ }
260
195
  if (reasoningApi === "budget") {
261
196
  const budget = reasoning.budgetTokens !== void 0 ? reasoning.budgetTokens : reasoning.effort !== void 0 ? EFFORT_BUDGET[reasoning.effort] ?? 0 : void 0;
262
197
  config.thinkingConfig = {
263
198
  ...budget !== void 0 ? { thinkingBudget: budget } : {},
264
199
  ...reasoning.includeThoughts === true ? { includeThoughts: true } : {}
265
200
  };
266
- if (reasoning.effort !== void 0 && reasoning.budgetTokens !== void 0) {
267
- warnings.push({
268
- type: "reasoning-mapping",
269
- quality: "approximate",
270
- details: "budgetTokens takes precedence over effort for thinkingBudget"
271
- });
272
- }
273
201
  } else if (reasoningApi === "level") {
202
+ if (reasoning.budgetTokens !== void 0) {
203
+ throw new LlmError(
204
+ `reasoning.budgetTokens is not supported for model "${model}" (it uses thinkingLevel, not thinkingBudget); use reasoning.effort instead.`,
205
+ { kind: "bad_request", retryable: false }
206
+ );
207
+ }
274
208
  let thinkingLevel;
275
209
  if (reasoning.effort !== void 0) {
276
210
  switch (reasoning.effort) {
@@ -290,45 +224,23 @@ function geminiAdapter(opts) {
290
224
  assertNever(reasoning.effort);
291
225
  }
292
226
  }
293
- if (reasoning.budgetTokens !== void 0) {
294
- warnings.push({
295
- type: "reasoning-mapping",
296
- quality: "approximate",
297
- details: "budgetTokens is not supported for gemini-3.x models; mapping effort to thinkingLevel instead"
298
- });
299
- }
300
227
  config.thinkingConfig = {
301
228
  ...thinkingLevel !== void 0 ? { thinkingLevel } : {},
302
229
  ...reasoning.includeThoughts === true ? { includeThoughts: true } : {}
303
230
  };
304
231
  } else {
305
- warnings.push({
306
- type: "reasoning-mapping",
307
- quality: "unsupported",
308
- details: `thinkingConfig not mapped for model "${model}"; unknown generation`
309
- });
232
+ throw new LlmError(
233
+ `Model "${model}" does not support reasoning/thinkingConfig.`,
234
+ { kind: "bad_request", retryable: false }
235
+ );
310
236
  }
311
237
  }
312
- let outputSchemaRequested = false;
313
- if (req.outputSchema !== void 0) {
314
- outputSchemaRequested = true;
315
- config.responseMimeType = "application/json";
316
- if (req.outputSchema["~standard"].vendor === "zod") {
317
- const geminiSchema = zodToGeminiSchema(req.outputSchema);
318
- if (geminiSchema !== void 0) {
319
- config.responseSchema = geminiSchema;
320
- } else {
321
- warnings.push({
322
- type: "unsupported-setting",
323
- setting: "output.schema",
324
- details: "Could not convert Zod schema to Gemini responseSchema; proceeding with responseMimeType only. Engine will still validate."
325
- });
326
- }
327
- } else {
328
- warnings.push({
329
- type: "other",
330
- message: `Native Gemini responseSchema conversion is not available for vendor "${req.outputSchema["~standard"].vendor}"; proceeding with responseMimeType only. Engine will validate output client-side via Standard Schema.`
331
- });
238
+ const structuredOutputRequested = req.outputJsonSchema !== void 0;
239
+ if (structuredOutputRequested) {
240
+ const nativeStructuredOutput = req.modelDescriptor?.capabilities?.nativeStructuredOutput !== false;
241
+ if (nativeStructuredOutput) {
242
+ config.responseMimeType = "application/json";
243
+ config.responseSchema = req.outputJsonSchema;
332
244
  }
333
245
  }
334
246
  const googleOpts = genConfig.providerOptions?.["google"];
@@ -336,27 +248,23 @@ function geminiAdapter(opts) {
336
248
  Object.assign(config, googleOpts);
337
249
  }
338
250
  if (req.modelDescriptor?.capabilities?.sampling === "fixed") {
339
- const droppedSampling = [];
251
+ const offendingSampling = [];
340
252
  if ("temperature" in config) {
341
- delete config.temperature;
342
- droppedSampling.push("temperature");
253
+ offendingSampling.push("temperature");
343
254
  }
344
255
  if ("topP" in config) {
345
- delete config.topP;
346
- droppedSampling.push("topP");
256
+ offendingSampling.push("topP");
347
257
  }
348
258
  if ("topK" in config) {
349
- delete config.topK;
350
- droppedSampling.push("topK");
259
+ offendingSampling.push("topK");
351
260
  }
352
- if (droppedSampling.length > 0) {
353
- warnings.push({
354
- type: "unsupported-setting",
355
- setting: droppedSampling.join(", "),
356
- details: `Sampling parameter(s) [${droppedSampling.join(
261
+ if (offendingSampling.length > 0) {
262
+ throw new LlmError(
263
+ `Sampling parameters [${offendingSampling.join(
357
264
  ", "
358
- )}] from providerOptions.google were stripped: this model has fixed sampling (Gemini 3.x) and the API rejects these fields.`
359
- });
265
+ )}] are not supported for model "${model}" (fixed sampling); they were supplied via providerOptions.google.`,
266
+ { kind: "bad_request", retryable: false }
267
+ );
360
268
  }
361
269
  }
362
270
  const configAsAny = config;
@@ -367,32 +275,41 @@ function geminiAdapter(opts) {
367
275
  }
368
276
  return false;
369
277
  });
370
- if (groundingRequested && req.outputSchema !== void 0) {
278
+ if (groundingRequested && structuredOutputRequested) {
371
279
  throw new LlmError(
372
- "Grounding (googleSearch) cannot be combined with structured output (output.schema) on Gemini; choose one.",
280
+ "Grounding (googleSearch) cannot be combined with structured output (output.jsonSchema) on Gemini; choose one.",
373
281
  { kind: "bad_request", retryable: false }
374
282
  );
375
283
  }
376
- let _flexTimeoutHandle;
377
- if (config.serviceTier === "flex" && genConfig.timeoutMs === void 0) {
378
- const flexController = new AbortController();
379
- const timeoutReason = new DOMException(
380
- `Flex timeout: call exceeded ${FLEX_DEFAULT_TIMEOUT_MS}ms client-side ceiling (@google/genai #1277 belt-and-suspenders)`,
381
- "TimeoutError"
382
- );
383
- _flexTimeoutHandle = setTimeout(() => {
384
- flexController.abort(timeoutReason);
385
- }, FLEX_DEFAULT_TIMEOUT_MS);
386
- if (ctx.signal !== void 0) {
387
- config.abortSignal = AbortSignal.any([flexController.signal, ctx.signal]);
388
- } else {
389
- config.abortSignal = flexController.signal;
284
+ let tierTimeoutHandle;
285
+ const clearTierTimeout = () => {
286
+ if (tierTimeoutHandle !== void 0) {
287
+ clearTimeout(tierTimeoutHandle);
288
+ tierTimeoutHandle = void 0;
390
289
  }
391
- } else if (ctx.signal !== void 0) {
392
- config.abortSignal = ctx.signal;
393
- }
290
+ };
291
+ const applyTierTimeout = (tier) => {
292
+ clearTierTimeout();
293
+ delete config.abortSignal;
294
+ const defaultTimeoutMs = genConfig.timeoutMs === void 0 ? tier === "flex" ? FLEX_DEFAULT_TIMEOUT_MS : tier === "standard" ? STANDARD_DEFAULT_TIMEOUT_MS : void 0 : void 0;
295
+ if (defaultTimeoutMs !== void 0) {
296
+ const tierController = new AbortController();
297
+ const tierLabel = tier === "standard" ? "Standard" : "Flex";
298
+ const timeoutReason = new DOMException(
299
+ `${tierLabel} timeout: call exceeded ${defaultTimeoutMs}ms client-side ceiling (@google/genai #1277 belt-and-suspenders)`,
300
+ "TimeoutError"
301
+ );
302
+ tierTimeoutHandle = setTimeout(() => {
303
+ tierController.abort(timeoutReason);
304
+ }, defaultTimeoutMs);
305
+ config.abortSignal = ctx.signal !== void 0 ? AbortSignal.any([tierController.signal, ctx.signal]) : tierController.signal;
306
+ } else if (ctx.signal !== void 0) {
307
+ config.abortSignal = ctx.signal;
308
+ }
309
+ };
310
+ applyTierTimeout(config.serviceTier);
394
311
  const callerHttpOptions = config.httpOptions;
395
- const computedTimeoutMs = genConfig.timeoutMs !== void 0 ? genConfig.timeoutMs + TRANSPORT_TIMEOUT_BUFFER_MS : genConfig.serviceTier === "flex" ? FLEX_DEFAULT_TIMEOUT_MS : void 0;
312
+ const computedTimeoutMs = genConfig.timeoutMs !== void 0 ? genConfig.timeoutMs + TRANSPORT_TIMEOUT_BUFFER_MS : config.serviceTier === "flex" ? FLEX_DEFAULT_TIMEOUT_MS : config.serviceTier === "standard" ? STANDARD_DEFAULT_TIMEOUT_MS : void 0;
396
313
  const mergedHttpOptions = {
397
314
  ...computedTimeoutMs !== void 0 ? { timeout: computedTimeoutMs } : {},
398
315
  ...callerHttpOptions
@@ -400,32 +317,71 @@ function geminiAdapter(opts) {
400
317
  if (Object.keys(mergedHttpOptions).length > 0) {
401
318
  config.httpOptions = mergedHttpOptions;
402
319
  }
403
- const params = {
404
- model,
405
- contents,
406
- config
407
- };
408
320
  let response;
409
- try {
321
+ let servedServiceTier = config.serviceTier;
322
+ const dispatch = async () => {
323
+ const dispatchConfig = {
324
+ ...config,
325
+ ...config.httpOptions !== void 0 ? { httpOptions: { ...config.httpOptions } } : {}
326
+ };
327
+ const params = {
328
+ model,
329
+ contents,
330
+ config: dispatchConfig
331
+ };
410
332
  const buildClient = opts?._clientFactory ?? buildGoogleClient;
411
333
  const client = opts?.client !== void 0 ? opts.client : await buildClient(ctx.auth);
412
334
  ctx.logger.debug(
413
- { model, configKeys: Object.keys(config) },
335
+ {
336
+ model,
337
+ configKeys: Object.keys(dispatchConfig),
338
+ serviceTier: dispatchConfig.serviceTier
339
+ },
414
340
  "llm.adapter.dispatch"
415
341
  );
416
- response = await client.models.generateContent(params);
342
+ return client.models.generateContent(params);
343
+ };
344
+ try {
345
+ response = await dispatch();
417
346
  } catch (rawErr) {
418
347
  const classified = classifyError(rawErr);
419
- throw new LlmError(classified.message, {
348
+ const typed = new LlmError(classified.message, {
420
349
  kind: classified.kind,
421
350
  retryable: classified.retryable,
422
351
  ...classified.httpStatus !== void 0 ? { httpStatus: classified.httpStatus } : {},
423
352
  ...classified.retryAfterMs !== void 0 ? { retryAfterMs: classified.retryAfterMs } : {},
424
353
  provider: "google",
425
- cause: classified.cause ?? rawErr
354
+ cause: classified.cause ?? rawErr,
355
+ ...servedServiceTier !== void 0 ? { servedServiceTier } : {}
426
356
  });
357
+ if (config.serviceTier === "flex" && genConfig.flexFallback !== false && isGeminiCapacityError(typed)) {
358
+ config.serviceTier = "standard";
359
+ servedServiceTier = "standard";
360
+ const fallbackTimeout = genConfig.timeoutMs !== void 0 ? genConfig.timeoutMs + TRANSPORT_TIMEOUT_BUFFER_MS : STANDARD_DEFAULT_TIMEOUT_MS;
361
+ config.httpOptions = {
362
+ timeout: fallbackTimeout,
363
+ ...callerHttpOptions
364
+ };
365
+ applyTierTimeout("standard");
366
+ try {
367
+ response = await dispatch();
368
+ } catch (fallbackRawErr) {
369
+ const fallbackClassified = classifyError(fallbackRawErr);
370
+ throw new LlmError(fallbackClassified.message, {
371
+ kind: fallbackClassified.kind,
372
+ retryable: fallbackClassified.retryable,
373
+ ...fallbackClassified.httpStatus !== void 0 ? { httpStatus: fallbackClassified.httpStatus } : {},
374
+ ...fallbackClassified.retryAfterMs !== void 0 ? { retryAfterMs: fallbackClassified.retryAfterMs } : {},
375
+ provider: "google",
376
+ cause: fallbackClassified.cause ?? fallbackRawErr,
377
+ servedServiceTier: "standard"
378
+ });
379
+ }
380
+ } else {
381
+ throw typed;
382
+ }
427
383
  } finally {
428
- if (_flexTimeoutHandle !== void 0) clearTimeout(_flexTimeoutHandle);
384
+ clearTierTimeout();
429
385
  }
430
386
  const hasBlockReason = response.promptFeedback?.blockReason !== void 0;
431
387
  const hasCandidates = response.candidates !== void 0 && response.candidates.length > 0;
@@ -468,7 +424,7 @@ function geminiAdapter(opts) {
468
424
  const text = textParts.join("");
469
425
  const reasoningText = thoughtParts.length > 0 ? thoughtParts.join("") : void 0;
470
426
  let rawStructured;
471
- if (outputSchemaRequested && text.length > 0) {
427
+ if (structuredOutputRequested && text.length > 0) {
472
428
  try {
473
429
  rawStructured = JSON.parse(text);
474
430
  } catch {
@@ -480,6 +436,7 @@ function geminiAdapter(opts) {
480
436
  model,
481
437
  usage,
482
438
  warnings,
439
+ ...servedServiceTier !== void 0 ? { servedServiceTier } : {},
483
440
  ...text.length > 0 ? { text } : {},
484
441
  ...reasoningText !== void 0 ? { reasoningText } : {},
485
442
  ...rawStructured !== void 0 ? { rawStructured } : {},
@@ -905,6 +862,6 @@ var GoogleCacheStore = class {
905
862
  }
906
863
  };
907
864
 
908
- export { FLEX_DEFAULT_TIMEOUT_MS, GoogleCacheStore, GoogleFileStore, TRANSPORT_TIMEOUT_BUFFER_MS, buildGoogleClient, geminiAdapter, zodToGeminiSchema };
865
+ export { FLEX_DEFAULT_TIMEOUT_MS, GoogleCacheStore, GoogleFileStore, STANDARD_DEFAULT_TIMEOUT_MS, TRANSPORT_TIMEOUT_BUFFER_MS, buildGoogleClient, geminiAdapter, isGeminiCapacityError };
909
866
  //# sourceMappingURL=index.js.map
910
867
  //# sourceMappingURL=index.js.map