@gullabs/any-llm 0.8.0 → 0.8.2

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.cjs CHANGED
@@ -6,7 +6,7 @@ var google = require('@gullabs/google');
6
6
  // src/index.ts
7
7
 
8
8
  // package.json
9
- var version = "0.8.0";
9
+ var version = "0.8.2";
10
10
 
11
11
  // src/index.ts
12
12
  var ANY_LLM_VERSION = version;
@@ -1 +1 @@
1
- {"version":3,"sources":["../package.json","../src/index.ts"],"names":[],"mappings":";;;;;;;;AAEE,IAAA,OAAA,GAAW,OAAA;;;ACcN,IAAM,eAAA,GAA0B","file":"index.cjs","sourcesContent":["{\n \"name\": \"@gullabs/any-llm\",\n \"version\": \"0.8.0\",\n \"description\": \"Batteries-included any-llm client for Gemini: engine, adapter, and Google SDK in one install.\",\n \"type\": \"module\",\n \"license\": \"Apache-2.0\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/gullabs/any-llm.git\",\n \"directory\": \"packages/any-llm\"\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"files\": [\n \"dist\",\n \"skills\"\n ],\n \"scripts\": {\n \"build\": \"tsup\"\n },\n \"dependencies\": {\n \"@google/genai\": \"^1.45.0 || ^2\",\n \"@gullabs/core\": \"workspace:*\",\n \"@gullabs/google\": \"workspace:*\"\n },\n \"engines\": {\n \"node\": \">=20.9.0\"\n },\n \"sideEffects\": false,\n \"keywords\": [\n \"llm\",\n \"gemini\",\n \"google-genai\",\n \"ai\",\n \"tokens\",\n \"cost\",\n \"usage\",\n \"observability\",\n \"typescript\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"homepage\": \"https://github.com/gullabs/any-llm/tree/main/packages/any-llm#readme\",\n \"bugs\": \"https://github.com/gullabs/any-llm/issues\"\n}\n","/**\n * @gullabs/any-llm — batteries-included public entrypoint.\n *\n * This package is the default client install path. It re-exports the core\n * engine and Gemini adapter while depending on the Gemini SDK for a one-package\n * setup.\n *\n * @module\n */\n\nexport * from '@gullabs/core'\nexport * from '@gullabs/google'\n\nimport { version } from '../package.json'\n\n/** Library version, sourced from package.json at build time. */\nexport const ANY_LLM_VERSION: string = version\n"]}
1
+ {"version":3,"sources":["../package.json","../src/index.ts"],"names":[],"mappings":";;;;;;;;AAEE,IAAA,OAAA,GAAW,OAAA;;;ACcN,IAAM,eAAA,GAA0B","file":"index.cjs","sourcesContent":["{\n \"name\": \"@gullabs/any-llm\",\n \"version\": \"0.8.2\",\n \"description\": \"Batteries-included any-llm client for Gemini: engine, adapter, and Google SDK in one install.\",\n \"type\": \"module\",\n \"license\": \"Apache-2.0\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/gullabs/any-llm.git\",\n \"directory\": \"packages/any-llm\"\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"files\": [\n \"dist\",\n \"skills\"\n ],\n \"scripts\": {\n \"build\": \"tsup\"\n },\n \"dependencies\": {\n \"@google/genai\": \"^1.45.0 || ^2\",\n \"@gullabs/core\": \"workspace:*\",\n \"@gullabs/google\": \"workspace:*\"\n },\n \"engines\": {\n \"node\": \">=20.9.0\"\n },\n \"sideEffects\": false,\n \"keywords\": [\n \"llm\",\n \"gemini\",\n \"google-genai\",\n \"ai\",\n \"tokens\",\n \"cost\",\n \"usage\",\n \"observability\",\n \"typescript\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"homepage\": \"https://github.com/gullabs/any-llm/tree/main/packages/any-llm#readme\",\n \"bugs\": \"https://github.com/gullabs/any-llm/issues\"\n}\n","/**\n * @gullabs/any-llm — batteries-included public entrypoint.\n *\n * This package is the default client install path. It re-exports the core\n * engine and Gemini adapter while depending on the Gemini SDK for a one-package\n * setup.\n *\n * @module\n */\n\nexport * from '@gullabs/core'\nexport * from '@gullabs/google'\n\nimport { version } from '../package.json'\n\n/** Library version, sourced from package.json at build time. */\nexport const ANY_LLM_VERSION: string = version\n"]}
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export * from '@gullabs/google';
4
4
  // src/index.ts
5
5
 
6
6
  // package.json
7
- var version = "0.8.0";
7
+ var version = "0.8.2";
8
8
 
9
9
  // src/index.ts
10
10
  var ANY_LLM_VERSION = version;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../package.json","../src/index.ts"],"names":[],"mappings":";;;;;;AAEE,IAAA,OAAA,GAAW,OAAA;;;ACcN,IAAM,eAAA,GAA0B","file":"index.js","sourcesContent":["{\n \"name\": \"@gullabs/any-llm\",\n \"version\": \"0.8.0\",\n \"description\": \"Batteries-included any-llm client for Gemini: engine, adapter, and Google SDK in one install.\",\n \"type\": \"module\",\n \"license\": \"Apache-2.0\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/gullabs/any-llm.git\",\n \"directory\": \"packages/any-llm\"\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"files\": [\n \"dist\",\n \"skills\"\n ],\n \"scripts\": {\n \"build\": \"tsup\"\n },\n \"dependencies\": {\n \"@google/genai\": \"^1.45.0 || ^2\",\n \"@gullabs/core\": \"workspace:*\",\n \"@gullabs/google\": \"workspace:*\"\n },\n \"engines\": {\n \"node\": \">=20.9.0\"\n },\n \"sideEffects\": false,\n \"keywords\": [\n \"llm\",\n \"gemini\",\n \"google-genai\",\n \"ai\",\n \"tokens\",\n \"cost\",\n \"usage\",\n \"observability\",\n \"typescript\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"homepage\": \"https://github.com/gullabs/any-llm/tree/main/packages/any-llm#readme\",\n \"bugs\": \"https://github.com/gullabs/any-llm/issues\"\n}\n","/**\n * @gullabs/any-llm — batteries-included public entrypoint.\n *\n * This package is the default client install path. It re-exports the core\n * engine and Gemini adapter while depending on the Gemini SDK for a one-package\n * setup.\n *\n * @module\n */\n\nexport * from '@gullabs/core'\nexport * from '@gullabs/google'\n\nimport { version } from '../package.json'\n\n/** Library version, sourced from package.json at build time. */\nexport const ANY_LLM_VERSION: string = version\n"]}
1
+ {"version":3,"sources":["../package.json","../src/index.ts"],"names":[],"mappings":";;;;;;AAEE,IAAA,OAAA,GAAW,OAAA;;;ACcN,IAAM,eAAA,GAA0B","file":"index.js","sourcesContent":["{\n \"name\": \"@gullabs/any-llm\",\n \"version\": \"0.8.2\",\n \"description\": \"Batteries-included any-llm client for Gemini: engine, adapter, and Google SDK in one install.\",\n \"type\": \"module\",\n \"license\": \"Apache-2.0\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/gullabs/any-llm.git\",\n \"directory\": \"packages/any-llm\"\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"files\": [\n \"dist\",\n \"skills\"\n ],\n \"scripts\": {\n \"build\": \"tsup\"\n },\n \"dependencies\": {\n \"@google/genai\": \"^1.45.0 || ^2\",\n \"@gullabs/core\": \"workspace:*\",\n \"@gullabs/google\": \"workspace:*\"\n },\n \"engines\": {\n \"node\": \">=20.9.0\"\n },\n \"sideEffects\": false,\n \"keywords\": [\n \"llm\",\n \"gemini\",\n \"google-genai\",\n \"ai\",\n \"tokens\",\n \"cost\",\n \"usage\",\n \"observability\",\n \"typescript\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"homepage\": \"https://github.com/gullabs/any-llm/tree/main/packages/any-llm#readme\",\n \"bugs\": \"https://github.com/gullabs/any-llm/issues\"\n}\n","/**\n * @gullabs/any-llm — batteries-included public entrypoint.\n *\n * This package is the default client install path. It re-exports the core\n * engine and Gemini adapter while depending on the Gemini SDK for a one-package\n * setup.\n *\n * @module\n */\n\nexport * from '@gullabs/core'\nexport * from '@gullabs/google'\n\nimport { version } from '../package.json'\n\n/** Library version, sourced from package.json at build time. */\nexport const ANY_LLM_VERSION: string = version\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gullabs/any-llm",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Batteries-included any-llm client for Gemini: engine, adapter, and Google SDK in one install.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -25,8 +25,8 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@google/genai": "^1.45.0 || ^2",
28
- "@gullabs/core": "0.8.0",
29
- "@gullabs/google": "0.8.0"
28
+ "@gullabs/core": "0.9.0",
29
+ "@gullabs/google": "0.8.1"
30
30
  },
31
31
  "engines": {
32
32
  "node": ">=20.9.0"
@@ -163,6 +163,32 @@ const result = await client.generate(
163
163
  `Grok45ConfigSchema`. Unlike Gemini, xai has no `serviceTier` concept and no `topK`;
164
164
  its config schema is a single strict object with no tier branching.
165
165
 
166
+ ## xAI structured-output schemas vs. OpenAI-strict / codex-cli schemas
167
+
168
+ As of the 2026-07-09 live probes, xAI's `strict: true` structured-output validation
169
+ on `text.format` json_schema performed no OpenAI-style compile-time schema checks —
170
+ schemas missing root/nested `additionalProperties: false`, properties omitted from
171
+ `required` (optional properties), `format`/other keywords, `anyOf`, `$defs`/`$ref`,
172
+ and nullable unions (`type: [T, 'null']`) were all accepted with HTTP 200.
173
+ `@gullabs/xai`'s adapter forwards schemas to xAI verbatim; no rewriting is applied.
174
+
175
+ `@gullabs/codex-cli`, by contrast, targets the codex CLI's own `--output-schema`
176
+ backend, which (verified 2026-07-09 via live probes against the real codex CLI
177
+ binary/backend) enforces exactly two structural rules: every object node must carry
178
+ `additionalProperties: false`, and `required` must be present and include every key
179
+ in `properties` (optional semantics are preserved by adding `null` to that
180
+ property's type, not by simply marking it required). `@gullabs/codex-cli` exports
181
+ `toOpenAiStrictOutputSchema` — an explicit opt-in transformer, never called
182
+ automatically by the adapter — that rewrites a schema to satisfy those two rules.
183
+ codex-cli's local preflight (`assertOpenAiStrictOutputSchema` in
184
+ `packages/codex-cli/src/adapter.ts`) enforces both rules locally before dispatch,
185
+ turning what used to be a live-round-trip provider 400 into an immediate local
186
+ `bad_request`.
187
+
188
+ This preflight/transformer pair is specific to codex-cli's own `--output-schema`
189
+ contract, not a general any-llm behavior — it is not applied to xai, which has no
190
+ such preflight (see the xAI Grok section above).
191
+
166
192
  ## Migrating raw `@google/genai` prompts
167
193
 
168
194
  `geminiContentToMessages` (from `@gullabs/google`) converts hand-authored
@@ -203,6 +229,17 @@ const result = await client.generate(
203
229
  `system` is derived only from the explicit `systemInstruction` input — never inferred
204
230
  from `contents`.
205
231
 
232
+ ## Testing with `@gullabs/testing`
233
+
234
+ Real hosts don't call `createClient()` at call sites — they own a factory module that
235
+ assembles the client once and hand call sites the built client. `@gullabs/testing`'s
236
+ fakes (`makeFakeGemini`, `FakeAdapter`, `RecordingSink`, `FakeClock`, `FakeIds`, ...) are
237
+ designed to inject through that same host-owned factory unchanged, via injectable
238
+ override parameters with production defaults — not via `vi.mock()`. See
239
+ `packages/testing/README.md` § "Wiring fakes through a host-owned factory" for a
240
+ complete two-file (factory + vitest test) example, including the port-level
241
+ `FakeAdapter` variant for bypassing the Gemini SDK shape entirely.
242
+
206
243
  ## `defineCallSite` — reusable prompt templates
207
244
 
208
245
  ```ts
@@ -222,12 +259,106 @@ const result = await client.runStructured(summarize, { article: text }, { auth }
222
259
 
223
260
  `{{var}}` interpolation is non-recursive (substituted values are never re-scanned for
224
261
  further `{{...}}`, preventing template injection) and applies to both `system` and
225
- `userTemplate`. A missing var is left as the literal `{{var}}` placeholder, not an
226
- empty string. `runStructured` also accepts a two-arg form, `(callSite, opts)`, when the
227
- template has no vars. Config resolution order everywhere is
262
+ `userTemplate`. **Strict by default (no opt-out):** every `{{var}}` placeholder must have
263
+ a string-typed value in `vars`, or `runStructured` throws before any request is built —
264
+ see "Input contracts" below. `runStructured` also accepts a two-arg form, `(callSite,
265
+ opts)`, when the template has no vars. Config resolution order everywhere is
228
266
  `clientDefaults → callSite.config → opts.config`, and the merged config must still pass
229
267
  the selected descriptor's strict runtime schema before dispatch.
230
268
 
269
+ ## Input contracts — strict interpolation, `inputSchema`, `inputContract`
270
+
271
+ **Strict template interpolation is the default, with no opt-out.** Every `{{var}}`
272
+ placeholder referenced by `callSite.system` or `callSite.userTemplate` must have a
273
+ string-typed value present in `vars`, or `runStructured` refuses the call before any
274
+ request is built — zero tokens spent:
275
+
276
+ ```ts
277
+ import { defineCallSite, LlmError } from '@gullabs/core'
278
+
279
+ const summarize = defineCallSite({
280
+ id: 'summarize-article',
281
+ provider: 'google',
282
+ model: 'gemini-2.5-flash',
283
+ userTemplate: 'Summarize this article in 3 sentences:\n\n{{article}}',
284
+ })
285
+
286
+ try {
287
+ // Missing `article` — throws before any I/O.
288
+ await client.runStructured(summarize, {}, { auth })
289
+ } catch (e) {
290
+ if (e instanceof LlmError && e.kind === 'bad_request') {
291
+ console.log(e.issues) // [{ path: 'article', message: '...' }]
292
+ }
293
+ }
294
+ ```
295
+
296
+ `null`, `undefined`, and non-string values (numbers, objects) are all violations — never
297
+ coerced to a string. Unused `vars` entries (present in `vars` but not referenced by any
298
+ template) are allowed. There is no escape syntax for literal `{{...}}` text.
299
+
300
+ **`CallSite.inputSchema`** validates `vars` with a `StandardSchemaV1` validator (zod,
301
+ valibot, ...) before interpolation runs, so a missing business field surfaces in your own
302
+ schema's vocabulary instead of as a downstream placeholder violation:
303
+
304
+ ```ts
305
+ import { z } from 'zod'
306
+
307
+ const reviewCallSite = defineCallSite({
308
+ id: 'code-review',
309
+ provider: 'google',
310
+ model: 'gemini-2.5-flash',
311
+ userTemplate: 'Review this diff as {{reviewer}}:\n\n{{diff}}',
312
+ inputSchema: z.object({
313
+ reviewer: z.string().min(1),
314
+ diff: z.string().min(1),
315
+ }),
316
+ })
317
+
318
+ await client.runStructured(
319
+ reviewCallSite,
320
+ { reviewer: 'senior-reviewer', diff },
321
+ { auth },
322
+ )
323
+ ```
324
+
325
+ **`LlmRequest.inputContract`** is the equivalent opt-in contract for the `generate()`
326
+ path (callers who render their own prompt strings and never touch `CallSite`):
327
+
328
+ ```ts
329
+ const result = await client.generate(
330
+ {
331
+ provider: 'google',
332
+ model: 'gemini-2.5-flash',
333
+ messages: [{ role: 'user', parts: [{ kind: 'text', text: renderedPrompt }] }],
334
+ inputContract: { schema: myZodSchema, value: sourceContext },
335
+ },
336
+ { auth },
337
+ )
338
+ ```
339
+
340
+ `inputContract` is validated once per logical call, before `@gullabs/quota` or any retry
341
+ middleware runs — a violation never consumes rate-limit budget and is never retried.
342
+ `generate()` and `runStructured()` are independent paths: `runStructured` never
343
+ auto-populates `inputContract` from `inputSchema`, and `generate()` never reads
344
+ `inputSchema`.
345
+
346
+ **`createClient({ requireInputContract: true })`** is a fleet-wide toggle: every
347
+ `generate()` call must carry `inputContract`, and every `runStructured()` call site must
348
+ carry `inputSchema`, or the call is refused. Off by default.
349
+
350
+ **All violations throw `LlmError('bad_request')`** with a structured `issues` array
351
+ (`{ path, message }[]`, one entry per violation) on top of the usual `.message` string.
352
+
353
+ **Ledger semantics of refusals.** A refusal that never got a `callId` (unresolved
354
+ placeholders, `CallSite.inputSchema`, or `requireInputContract` on the `runStructured`
355
+ path — all thrown in the `runStructured` prologue) writes **no** ledger row. A refusal
356
+ that already has a `callId` (`inputContract` violations and `requireInputContract` on the
357
+ `generate()` path, thrown inside the pipeline after `callId` assignment) writes **one**
358
+ zero-usage record with `attemptNumber: 0` — including `@gullabs/quota` denials, which get
359
+ the same treatment with no `@gullabs/quota` code changes. See ADR-025 in `DECISIONS.md`
360
+ for the full boundary table.
361
+
231
362
  ## Strict model-config boundary
232
363
 
233
364
  Treat model config as descriptor-owned:
@@ -379,7 +510,8 @@ try {
379
510
  ```
380
511
 
381
512
  `LlmError` also carries `httpStatus?`, `retryAfterMs?`, `provider?`, `callId?`,
382
- `attemptId?`, `servedServiceTier?`, and `cause` (the original thrown value).
513
+ `attemptId?`, `servedServiceTier?`, `issues?` (structured `{ path, message }[]` — see
514
+ "Input contracts" above), and `cause` (the original thrown value).
383
515
 
384
516
  ## Reject, don't map
385
517