@gullabs/google 0.5.1 → 0.6.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/README.md CHANGED
@@ -44,13 +44,33 @@ const result = await client.generate(
44
44
  ## What it maps
45
45
 
46
46
  - `serviceTier: 'flex'` → Gemini Flex service tier when the model descriptor supports it
47
+ - omitted `serviceTier` → provider-default request behavior
47
48
  - `reasoning.includeThoughts` → `thinkingConfig.includeThoughts`; thought parts become `reasoningText`
48
- - `reasoning.effort` → `thinkingBudget` (gemini-2.5) or `thinkingLevel` (gemini-3.x) with a warning when lossy
49
+ - `reasoning.effort` → `thinkingBudget` (Gemini 2.5) or `thinkingLevel` (Gemini 3 / Gemma 4)
50
+ - `reasoning.budgetTokens` → admitted only on Gemini 2.5 budget-api models; strict descriptors reject it on level-api models
49
51
  - `output.jsonSchema` → `responseMimeType: 'application/json'` + verbatim `responseSchema` when native structured output is enabled; the engine returns parsed output and `outputParsed` without validating shape
50
- - `providerOptions.google.*` → forwarded verbatim to the SDK config, including Gemini `safetySettings`
52
+ - `providerOptions.google.*` → typed provider-extension lane for admitted keys such as `cachedContent`, `safetySettings`, and exact tool declarations
51
53
  - Usage: `promptTokenCount`→`inputTokens`, `candidatesTokenCount`+`thoughtsTokenCount`→`outputTokens` (GROSS)
52
54
  - Errors: `401/403`→`invalid_auth`, `429`→`rate_limited`, `5xx`→`server`, timeouts, safety blocks
53
55
 
56
+ ## Strict model-config expectations
57
+
58
+ This adapter expects config that has already been parsed through the selected
59
+ descriptor boundary:
60
+
61
+ - `descriptor.configSchema` is the runtime source of truth.
62
+ - `descriptor.configJsonSchema` is the derived form/UI schema.
63
+ - `providerOptions.google` is not a caller-wins override lane for
64
+ `serviceTier`, sampling, reasoning, or response schema.
65
+ - `priority` stays rejected even though Google documents it, because the
66
+ library has not yet shipped the matching schema, pricing, served-tier
67
+ recording, and tests.
68
+
69
+ For structured output with built-in tools, follow the exact public
70
+ `generateContent` evidence: the current docs only admit that combination for
71
+ `gemini-3.1-pro-preview` and `gemini-3.5-flash`. Other models should fail early
72
+ instead of relying on adapter repair or provider-side surprises.
73
+
54
74
  ## Gemma 4
55
75
 
56
76
  The default registry includes two API-verified Gemma 4 models: `gemma-4-31b-it`
@@ -72,8 +92,9 @@ This follows the library-wide **reject, don't map** rule: unsupported or incorre
72
92
  typed `bad_request` `LlmError` at validation time rather than being silently clamped or coerced
73
93
  into something the model happens to accept.
74
94
 
75
- Gemma 4 models do **not** support Gemini Flex service tier (`serviceTiers` is absent from their
76
- descriptors) and are unpriced (`cost.microUsd` will be `null`).
95
+ Gemma 4 models are intentionally unpriced (`cost.microUsd` will be `null`), and
96
+ the strict contract does not admit any `serviceTier` for them until the public
97
+ docs and live evidence line up on that field.
77
98
 
78
99
  ## Learn more
79
100
 
package/dist/index.cjs CHANGED
@@ -21,6 +21,14 @@ async function buildGoogleClient(auth) {
21
21
  };
22
22
  }
23
23
 
24
+ // src/reasoning-budget.ts
25
+ var GOOGLE_REASONING_EFFORT_BUDGET = {
26
+ none: 0,
27
+ low: 1024,
28
+ medium: 8192,
29
+ high: 24576
30
+ };
31
+
24
32
  // src/flex-fallback.ts
25
33
  var CAPACITY_PATTERNS = [
26
34
  /capacity/i,
@@ -51,6 +59,231 @@ function isGeminiCapacityError(err) {
51
59
  }
52
60
 
53
61
  // src/adapter.ts
62
+ var ALLOWED_GOOGLE_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
63
+ "cachedContent",
64
+ "httpOptions",
65
+ "safetySettings",
66
+ "tools"
67
+ ]);
68
+ var ALLOWED_GOOGLE_HTTP_OPTION_KEYS = /* @__PURE__ */ new Set(["timeout"]);
69
+ var RESERVED_GOOGLE_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
70
+ "abortSignal",
71
+ "imageConfig",
72
+ "maxOutputTokens",
73
+ "mediaResolution",
74
+ "responseFormat",
75
+ "responseMimeType",
76
+ "responseModalities",
77
+ "responseSchema",
78
+ "responseJsonSchema",
79
+ "serviceTier",
80
+ "speechConfig",
81
+ "stopSequences",
82
+ "temperature",
83
+ "thinkingConfig",
84
+ "topK",
85
+ "topP",
86
+ "_responseJsonSchema"
87
+ ]);
88
+ var GOOGLE_SEARCH_SUPPORTED_MODELS = /* @__PURE__ */ new Set([
89
+ "gemini-2.5-flash",
90
+ "gemini-2.5-flash-lite",
91
+ "gemini-2.5-pro",
92
+ "gemini-3-flash-preview",
93
+ "gemini-3.1-flash-lite",
94
+ "gemini-3.1-pro-preview",
95
+ "gemini-3.5-flash"
96
+ ]);
97
+ var STRUCTURED_OUTPUT_GOOGLE_SEARCH_ALLOWLIST = /* @__PURE__ */ new Set([
98
+ "gemini-3.1-pro-preview",
99
+ "gemini-3.5-flash"
100
+ ]);
101
+ function isPlainRecord(value) {
102
+ return typeof value === "object" && value !== null && !Array.isArray(value);
103
+ }
104
+ function isEmptyPlainObject(value) {
105
+ return isPlainRecord(value) && Object.keys(value).length === 0;
106
+ }
107
+ function badGoogleProviderOptions(message) {
108
+ return new core.LlmError(message, { kind: "bad_request", retryable: false });
109
+ }
110
+ function parseGoogleTool(tool, model) {
111
+ if (!isPlainRecord(tool)) {
112
+ throw badGoogleProviderOptions(
113
+ `providerOptions.google.tools entries must be objects for model "${model}".`
114
+ );
115
+ }
116
+ const keys = Object.keys(tool);
117
+ if (keys.length !== 1) {
118
+ throw badGoogleProviderOptions(
119
+ `providerOptions.google.tools entries must have exactly one supported tool key for model "${model}".`
120
+ );
121
+ }
122
+ const key = keys[0];
123
+ switch (key) {
124
+ case "googleSearch": {
125
+ if (!isEmptyPlainObject(tool["googleSearch"])) {
126
+ throw badGoogleProviderOptions(
127
+ `providerOptions.google.tools[].googleSearch must be an empty object for model "${model}".`
128
+ );
129
+ }
130
+ return { googleSearch: {} };
131
+ }
132
+ default:
133
+ throw badGoogleProviderOptions(
134
+ `providerOptions.google.tools[].${key} is not supported for model "${model}".`
135
+ );
136
+ }
137
+ }
138
+ function parseGoogleSafetySetting(setting, index, model) {
139
+ if (!isPlainRecord(setting)) {
140
+ throw badGoogleProviderOptions(
141
+ `providerOptions.google.safetySettings[${index}] must be an object for model "${model}".`
142
+ );
143
+ }
144
+ const keys = Object.keys(setting);
145
+ const unknownKeys = keys.filter((key) => key !== "category" && key !== "threshold");
146
+ if (unknownKeys.length > 0) {
147
+ throw badGoogleProviderOptions(
148
+ `providerOptions.google.safetySettings[${index}] contains unsupported keys [${unknownKeys.join(
149
+ ", "
150
+ )}] for model "${model}". Allowed keys: category, threshold.`
151
+ );
152
+ }
153
+ if (typeof setting["category"] !== "string" || setting["category"].length === 0) {
154
+ throw badGoogleProviderOptions(
155
+ `providerOptions.google.safetySettings[${index}].category must be a non-empty string for model "${model}".`
156
+ );
157
+ }
158
+ if (typeof setting["threshold"] !== "string" || setting["threshold"].length === 0) {
159
+ throw badGoogleProviderOptions(
160
+ `providerOptions.google.safetySettings[${index}].threshold must be a non-empty string for model "${model}".`
161
+ );
162
+ }
163
+ return {
164
+ category: setting["category"],
165
+ threshold: setting["threshold"]
166
+ };
167
+ }
168
+ function mapGoogleProviderOptions(googleOpts, model, structuredOutputRequested, descriptorGrounding) {
169
+ if (googleOpts === void 0) {
170
+ return {};
171
+ }
172
+ if (!isPlainRecord(googleOpts)) {
173
+ throw badGoogleProviderOptions(
174
+ `providerOptions.google must be an object for model "${model}".`
175
+ );
176
+ }
177
+ const reservedKeys = Object.keys(googleOpts).filter(
178
+ (key) => RESERVED_GOOGLE_PROVIDER_OPTION_KEYS.has(key)
179
+ );
180
+ if (reservedKeys.length > 0) {
181
+ throw badGoogleProviderOptions(
182
+ `providerOptions.google reserves keys [${reservedKeys.join(
183
+ ", "
184
+ )}] for typed model config on "${model}".`
185
+ );
186
+ }
187
+ const unknownKeys = Object.keys(googleOpts).filter(
188
+ (key) => !ALLOWED_GOOGLE_PROVIDER_OPTION_KEYS.has(key) && !RESERVED_GOOGLE_PROVIDER_OPTION_KEYS.has(key)
189
+ );
190
+ if (unknownKeys.length > 0) {
191
+ throw badGoogleProviderOptions(
192
+ `providerOptions.google contains unsupported keys [${unknownKeys.join(
193
+ ", "
194
+ )}] for model "${model}". Allowed keys: cachedContent, httpOptions, safetySettings, tools.`
195
+ );
196
+ }
197
+ const mapped = {};
198
+ if (googleOpts["cachedContent"] !== void 0) {
199
+ if (typeof googleOpts["cachedContent"] !== "string" || googleOpts["cachedContent"].length === 0) {
200
+ throw badGoogleProviderOptions(
201
+ `providerOptions.google.cachedContent must be a non-empty string for model "${model}".`
202
+ );
203
+ }
204
+ mapped.cachedContent = googleOpts["cachedContent"];
205
+ }
206
+ if (googleOpts["httpOptions"] !== void 0) {
207
+ if (!isPlainRecord(googleOpts["httpOptions"])) {
208
+ throw badGoogleProviderOptions(
209
+ `providerOptions.google.httpOptions must be an object for model "${model}".`
210
+ );
211
+ }
212
+ const unknownHttpOptionKeys = Object.keys(googleOpts["httpOptions"]).filter(
213
+ (key) => !ALLOWED_GOOGLE_HTTP_OPTION_KEYS.has(key)
214
+ );
215
+ if (unknownHttpOptionKeys.length > 0) {
216
+ throw badGoogleProviderOptions(
217
+ `providerOptions.google.httpOptions contains unsupported keys [${unknownHttpOptionKeys.join(
218
+ ", "
219
+ )}] for model "${model}". Allowed keys: timeout.`
220
+ );
221
+ }
222
+ const timeout = googleOpts["httpOptions"]["timeout"];
223
+ if (timeout !== void 0) {
224
+ if (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout <= 0) {
225
+ throw badGoogleProviderOptions(
226
+ `providerOptions.google.httpOptions.timeout must be a positive integer for model "${model}".`
227
+ );
228
+ }
229
+ mapped.httpOptions = { timeout };
230
+ } else {
231
+ mapped.httpOptions = {};
232
+ }
233
+ }
234
+ if (googleOpts["safetySettings"] !== void 0) {
235
+ if (!Array.isArray(googleOpts["safetySettings"])) {
236
+ throw badGoogleProviderOptions(
237
+ `providerOptions.google.safetySettings must be an array for model "${model}".`
238
+ );
239
+ }
240
+ mapped.safetySettings = googleOpts["safetySettings"].map(
241
+ (setting, index) => parseGoogleSafetySetting(setting, index, model)
242
+ );
243
+ }
244
+ if (googleOpts["tools"] !== void 0) {
245
+ if (!Array.isArray(googleOpts["tools"])) {
246
+ throw badGoogleProviderOptions(
247
+ `providerOptions.google.tools must be an array for model "${model}".`
248
+ );
249
+ }
250
+ const tools = googleOpts["tools"].map((tool) => parseGoogleTool(tool, model));
251
+ const groundingSupported = descriptorGrounding === true || descriptorGrounding === void 0 && GOOGLE_SEARCH_SUPPORTED_MODELS.has(model);
252
+ if (!groundingSupported) {
253
+ throw badGoogleProviderOptions(
254
+ `providerOptions.google.tools is not supported for model "${model}".`
255
+ );
256
+ }
257
+ if (structuredOutputRequested && !STRUCTURED_OUTPUT_GOOGLE_SEARCH_ALLOWLIST.has(model)) {
258
+ throw badGoogleProviderOptions(
259
+ `Structured output with googleSearch is supported only for models "gemini-3.1-pro-preview" and "gemini-3.5-flash"; got "${model}".`
260
+ );
261
+ }
262
+ mapped.tools = tools;
263
+ }
264
+ return mapped;
265
+ }
266
+ function assertSamplingAllowed(config, model, sampling) {
267
+ if (sampling !== "fixed") {
268
+ return;
269
+ }
270
+ const offendingSampling = [];
271
+ if ("temperature" in config) {
272
+ offendingSampling.push("temperature");
273
+ }
274
+ if ("topP" in config) {
275
+ offendingSampling.push("topP");
276
+ }
277
+ if ("topK" in config) {
278
+ offendingSampling.push("topK");
279
+ }
280
+ if (offendingSampling.length > 0) {
281
+ throw new core.LlmError(
282
+ `Sampling parameters [${offendingSampling.join(", ")}] are not supported for model "${model}" (fixed sampling).`,
283
+ { kind: "bad_request", retryable: false }
284
+ );
285
+ }
286
+ }
54
287
  function mapFinishReason(raw) {
55
288
  if (raw === void 0) return void 0;
56
289
  switch (raw) {
@@ -172,12 +405,6 @@ function geminiAdapter(opts) {
172
405
  );
173
406
  }
174
407
  config.serviceTier = explicit;
175
- } else {
176
- if (req.modelDescriptor === void 0) {
177
- config.serviceTier = "flex";
178
- } else if (supported?.includes("flex") === true) {
179
- config.serviceTier = "flex";
180
- }
181
408
  }
182
409
  const reasoning = genConfig.reasoning;
183
410
  if (reasoning !== void 0) {
@@ -189,7 +416,7 @@ function geminiAdapter(opts) {
189
416
  );
190
417
  }
191
418
  if (reasoningApi === "budget") {
192
- const budget = reasoning.budgetTokens !== void 0 ? reasoning.budgetTokens : reasoning.effort !== void 0 ? core.EFFORT_BUDGET[reasoning.effort] : void 0;
419
+ const budget = reasoning.budgetTokens !== void 0 ? reasoning.budgetTokens : reasoning.effort !== void 0 ? GOOGLE_REASONING_EFFORT_BUDGET[reasoning.effort] : void 0;
193
420
  config.thinkingConfig = {
194
421
  ...budget !== void 0 ? { thinkingBudget: budget } : {},
195
422
  ...reasoning.includeThoughts === true ? { includeThoughts: true } : {}
@@ -239,54 +466,25 @@ function geminiAdapter(opts) {
239
466
  config.responseSchema = req.outputJsonSchema;
240
467
  }
241
468
  }
242
- const googleOpts = genConfig.providerOptions?.["google"];
243
- if (googleOpts !== void 0 && typeof googleOpts === "object" && googleOpts !== null) {
244
- Object.assign(config, googleOpts);
469
+ const googleProviderConfig = mapGoogleProviderOptions(
470
+ genConfig.providerOptions?.["google"],
471
+ model,
472
+ structuredOutputRequested,
473
+ req.modelDescriptor?.capabilities?.grounding
474
+ );
475
+ if (googleProviderConfig.cachedContent !== void 0) {
476
+ config.cachedContent = googleProviderConfig.cachedContent;
245
477
  }
246
- const mergedServiceTier = config.serviceTier;
247
- if (mergedServiceTier !== void 0 && req.modelDescriptor !== void 0) {
248
- const supportedAfterMerge = req.modelDescriptor.capabilities?.serviceTiers;
249
- if (supportedAfterMerge === void 0 || !supportedAfterMerge.includes(mergedServiceTier)) {
250
- throw new core.LlmError(
251
- `serviceTier "${mergedServiceTier}" is not supported for model "${model}".`,
252
- { kind: "bad_request", retryable: false }
253
- );
254
- }
478
+ if (googleProviderConfig.httpOptions !== void 0) {
479
+ config.httpOptions = googleProviderConfig.httpOptions;
255
480
  }
256
- if (req.modelDescriptor?.capabilities?.sampling === "fixed") {
257
- const offendingSampling = [];
258
- if ("temperature" in config) {
259
- offendingSampling.push("temperature");
260
- }
261
- if ("topP" in config) {
262
- offendingSampling.push("topP");
263
- }
264
- if ("topK" in config) {
265
- offendingSampling.push("topK");
266
- }
267
- if (offendingSampling.length > 0) {
268
- throw new core.LlmError(
269
- `Sampling parameters [${offendingSampling.join(
270
- ", "
271
- )}] are not supported for model "${model}" (fixed sampling); they were supplied via providerOptions.google.`,
272
- { kind: "bad_request", retryable: false }
273
- );
274
- }
481
+ if (googleProviderConfig.safetySettings !== void 0) {
482
+ config.safetySettings = googleProviderConfig.safetySettings;
275
483
  }
276
- const configAsAny = config;
277
- const groundingRequested = Array.isArray(configAsAny.tools) && configAsAny.tools.some((t) => {
278
- if (t !== null && typeof t === "object") {
279
- const tool = t;
280
- return "googleSearch" in tool || "googleSearchRetrieval" in tool;
281
- }
282
- return false;
283
- });
284
- if (groundingRequested && structuredOutputRequested) {
285
- throw new core.LlmError(
286
- "Grounding (googleSearch) cannot be combined with structured output (output.jsonSchema) on Gemini; choose one.",
287
- { kind: "bad_request", retryable: false }
288
- );
484
+ if (googleProviderConfig.tools !== void 0) {
485
+ config.tools = googleProviderConfig.tools;
289
486
  }
487
+ assertSamplingAllowed(config, model, req.modelDescriptor?.capabilities?.sampling);
290
488
  let tierTimeoutHandle;
291
489
  const clearTierTimeout = () => {
292
490
  if (tierTimeoutHandle !== void 0) {
@@ -477,7 +675,7 @@ async function buildFilesClient(auth) {
477
675
  return {
478
676
  async upload(params) {
479
677
  const fileArg = params.file instanceof Uint8Array ? new Blob(
480
- [params.file],
678
+ [Uint8Array.from(params.file)],
481
679
  params.config?.mimeType !== void 0 && params.config.mimeType.length > 0 ? { type: params.config.mimeType } : {}
482
680
  ) : params.file;
483
681
  const result = await ai.files.upload({
@@ -884,6 +1082,8 @@ function toCitation(chunk) {
884
1082
  } catch {
885
1083
  return void 0;
886
1084
  }
1085
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
1086
+ if (parsed.hostname === "") return void 0;
887
1087
  const title = web["title"];
888
1088
  const sourceName = typeof title === "string" && title.length > 0 ? title : hostnameFrom(parsed);
889
1089
  return { url: uri, sourceName };