@gullabs/any-llm 0.7.0 → 0.8.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 CHANGED
@@ -20,15 +20,14 @@ This installs the core engine, Gemini adapter, and `@google/genai`.
20
20
  ```ts
21
21
  import {
22
22
  createClient,
23
+ composeProviders,
23
24
  defaultGeminiRegistry,
24
25
  defineCallSite,
25
- geminiAdapter,
26
- geminiPricingSource,
26
+ googleProvider,
27
27
  } from '@gullabs/any-llm'
28
28
 
29
29
  const client = createClient({
30
- adapters: [geminiAdapter()],
31
- pricingSources: { google: geminiPricingSource() },
30
+ ...composeProviders([googleProvider()]),
32
31
  })
33
32
 
34
33
  const summarize = defineCallSite({
@@ -71,17 +70,20 @@ intended. `priority` remains rejected by the library for now.
71
70
  ## Key exports
72
71
 
73
72
  This package re-exports the full public API of `@gullabs/core` and `@gullabs/google` verbatim —
74
- `createClient`, `defineCallSite`, `geminiAdapter`, `geminiPricingSource`, `LlmError`, and every
75
- other named export from both packages. See their READMEs for details:
76
-
77
- | Export | What it is |
78
- | ----------------------- | ---------------------------------------------------------- |
79
- | `createClient(config)` | Wires ports into a `{ generate, runStructured }` client |
80
- | `defineCallSite(opts)` | Defines a typed, reusable prompt template bound to a model |
81
- | `geminiAdapter(opts?)` | The Gemini `ProviderAdapter`, from `@gullabs/google` |
82
- | `geminiPricingSource()` | Built-in Gemini pricing snapshot |
83
- | `LlmError` | Typed error class — always thrown on call failure |
84
- | `ANY_LLM_VERSION` | This package's version, sourced from `package.json` |
73
+ `createClient`, `composeProviders`, `defineCallSite`, `googleProvider`, `geminiAdapter`,
74
+ `geminiPricingSource`, `LlmError`, and every other named export from both packages. See their
75
+ READMEs for details:
76
+
77
+ | Export | What it is |
78
+ | --------------------------- | ----------------------------------------------------------- |
79
+ | `createClient(config)` | Wires ports into a `{ generate, runStructured }` client |
80
+ | `composeProviders(plugins)` | Merges `ProviderPlugin`s into `ClientConfig` fields |
81
+ | `defineCallSite(opts)` | Defines a typed, reusable prompt template bound to a model |
82
+ | `googleProvider(opts?)` | The Gemini `ProviderPlugin` factory, from `@gullabs/google` |
83
+ | `geminiAdapter(opts?)` | The Gemini `ProviderAdapter`, from `@gullabs/google` |
84
+ | `geminiPricingSource()` | Built-in Gemini pricing snapshot, from `@gullabs/google` |
85
+ | `LlmError` | Typed error class — always thrown on call failure |
86
+ | `ANY_LLM_VERSION` | This package's version, sourced from `package.json` |
85
87
 
86
88
  Use `@gullabs/core` and `@gullabs/google` directly only when you want modular dependency control.
87
89
 
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.7.0";
9
+ var version = "0.8.1";
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.7.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.1\",\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.7.0";
7
+ var version = "0.8.1";
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.7.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.1\",\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.7.0",
3
+ "version": "0.8.1",
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.7.0",
29
- "@gullabs/google": "0.7.0"
28
+ "@gullabs/core": "0.8.0",
29
+ "@gullabs/google": "0.8.0"
30
30
  },
31
31
  "engines": {
32
32
  "node": ">=20.9.0"
@@ -2,31 +2,42 @@
2
2
  name: any-llm
3
3
  description: >-
4
4
  Guidance for writing, reviewing, or debugging TypeScript code that calls
5
- @gullabs/any-llm, @gullabs/core, or @gullabs/google to talk to Gemini models.
6
- Applies when adding a new LLM call site, wiring createClient/generate/runStructured,
5
+ @gullabs/any-llm, @gullabs/core, @gullabs/google, or @gullabs/xai to talk to Gemini
6
+ or xAI Grok models. Applies when adding a new LLM call site, composing provider
7
+ plugins with composeProviders, wiring createClient/generate/runStructured/countTokens,
7
8
  defining a defineCallSite prompt template, requesting structured JSON output,
8
- catching or narrowing an LlmError, configuring reasoning/thinking budgets, or wiring
9
- a UsageSink for cost tracking. Also applies whenever the user mentions any-llm, the
10
- Gemini adapter, Gemini Flex tier, structured-output validation, or per-call auth for
11
- this library. Covers the mandatory per-call `{ auth: { apiKey } }` pattern (there is
12
- no env-var or ambient auth), the caller-owned output-validation contract, the
13
- descriptor-owned strict model-config boundary (`configSchema` / `configJsonSchema`),
14
- and the reject-don't-map error philosophy — the things a developer used to other
15
- LLM SDKs would otherwise get wrong by default.
9
+ catching or narrowing an LlmError, configuring reasoning/thinking budgets, wiring a
10
+ UsageSink for cost tracking, augmenting ProviderOptionsMap for a new provider
11
+ package, using GoogleCacheStore for Gemini context caching, or migrating raw
12
+ @google/genai prompts via geminiContentToMessages. Also applies whenever the user
13
+ mentions any-llm, the Gemini adapter, the xAI/Grok adapter, Gemini Flex tier,
14
+ structured-output validation, token counting, or per-call auth for this library.
15
+ Covers the mandatory per-call `{ auth: { apiKey } }` pattern (there is no env-var or
16
+ ambient auth), the caller-owned output-validation contract, the descriptor-owned
17
+ strict model-config boundary (`configSchema` / `configJsonSchema`), the
18
+ provider-qualified `(provider, model)` identity contract, and the reject-don't-map
19
+ error philosophy — the things a developer used to other LLM SDKs would otherwise get
20
+ wrong by default.
16
21
  ---
17
22
 
18
23
  # any-llm
19
24
 
20
- Typed, provider-agnostic-by-design (currently Gemini-only) LLM call engine with cost
21
- tracking, retries, rate limiting, structured output, and per-call observability.
25
+ Typed, provider-agnostic-by-design LLM call engine with cost tracking, retries, rate
26
+ limiting, structured output, and per-call observability. Providers are plugged in
27
+ explicitly via `composeProviders` — nothing is auto-wired.
22
28
 
23
- Three packages:
29
+ Core packages:
24
30
 
25
- - `@gullabs/core` — engine (`createClient`), types, errors, `defineCallSite`.
26
- - `@gullabs/google` — Gemini adapter (`geminiAdapter`) over `@google/genai`.
27
- - `@gullabs/any-llm` — batteries-included: re-exports both of the above plus `@google/genai` as a dependency.
31
+ - `@gullabs/core` — engine (`createClient`), types, errors, `defineCallSite`,
32
+ `composeProviders`.
33
+ - `@gullabs/google` — Gemini + Gemma adapter (`geminiAdapter`) over `@google/genai`.
34
+ - `@gullabs/xai` — xAI Grok adapter (`xaiAdapter`) over the Responses API.
35
+ - `@gullabs/any-llm` — batteries-included: re-exports `@gullabs/core` + `@gullabs/google`
36
+ plus `@google/genai` as a dependency. Does **not** bundle `@gullabs/xai` or any other
37
+ provider package — install those separately and compose them alongside.
28
38
 
29
- Install `@gullabs/any-llm` for a one-package setup, or the two modular packages for
39
+ Install `@gullabs/any-llm` for a one-package Gemini setup, or `@gullabs/core` plus
40
+ whichever provider package(s) you need (`@gullabs/google`, `@gullabs/xai`, ...) for
30
41
  explicit dependency control. Import names are identical either way.
31
42
 
32
43
  ## #1 gotcha: auth is per-call, always
@@ -39,8 +50,7 @@ explicitly. `createClient()` itself takes no credentials.
39
50
  // WRONG — GenerateOptions.auth is a required field; this will not type-check, and if
40
51
  // bypassed with `as any` it throws LlmError({ kind: 'invalid_auth' }) before any I/O.
41
52
  const client = createClient({
42
- adapters: [geminiAdapter()],
43
- pricingSources: { google: geminiPricingSource() },
53
+ ...composeProviders([googleProvider()]),
44
54
  })
45
55
  await client.generate(request, {} as GenerateOptions)
46
56
 
@@ -69,12 +79,12 @@ same bare model id can exist under multiple providers with different config sche
69
79
  ## Quickstart
70
80
 
71
81
  ```ts
72
- import { createClient, geminiPricingSource, geminiAdapter } from '@gullabs/any-llm'
73
- // (or: from '@gullabs/core' / '@gullabs/google' respectively, if using modular install)
82
+ import { createClient, composeProviders, googleProvider } from '@gullabs/any-llm'
83
+ // (or: composeProviders from '@gullabs/core', googleProvider from '@gullabs/google',
84
+ // if using modular install)
74
85
 
75
86
  const client = createClient({
76
- adapters: [geminiAdapter()],
77
- pricingSources: { google: geminiPricingSource() },
87
+ ...composeProviders([googleProvider()]),
78
88
  })
79
89
 
80
90
  const result = await client.generate(
@@ -95,6 +105,130 @@ console.log(result.cost?.microUsd) // integer micro-USD, or null if unpriced
95
105
  `{ kind: 'text', text }`, `{ kind: 'inline-media', mimeType, data /* raw base64, no data: prefix */ }`,
96
106
  and `{ kind: 'file-uri', uri, mimeType }` freely in one `parts` array.
97
107
 
108
+ ## Counting tokens without generating
109
+
110
+ `client.countTokens` is a metadata-only dry run — no generation, no cost, no
111
+ `result.output`. Same `(provider, model)` routing and required `auth` as `generate`;
112
+ throws `LlmError('bad_request')` when the pair is unregistered or the resolved adapter
113
+ doesn't implement token counting (`ProviderAdapter.countTokens` is optional).
114
+
115
+ ```ts
116
+ const count = await client.countTokens(
117
+ {
118
+ provider: 'google',
119
+ model: 'gemini-2.5-flash',
120
+ system: 'You are a concise summarizer.',
121
+ messages: [{ role: 'user', parts: [{ kind: 'text', text: 'Hello!' }] }],
122
+ },
123
+ { auth: { apiKey: myResolvedGeminiKey } },
124
+ )
125
+
126
+ console.log(count.totalTokens) // number
127
+ console.log(count.details) // optional per-category breakdown, e.g. { cached: 128 }
128
+ console.log(count.raw) // provider's raw token-count response, verbatim
129
+ ```
130
+
131
+ `TokenCountRequest` is deliberately narrower than a generate request — no `config`, no
132
+ `output`, no `providerOptions`; token counting only needs `provider`, `model`,
133
+ `system`, and `messages`.
134
+
135
+ ## Composing multiple providers — xAI Grok example
136
+
137
+ `composeProviders` takes any number of plugins; pass every provider a single client
138
+ should route to. `@gullabs/xai`'s `xaiProvider()` follows the identical plugin shape
139
+ as `googleProvider()`:
140
+
141
+ ```ts
142
+ import { createClient, composeProviders } from '@gullabs/core'
143
+ import { googleProvider } from '@gullabs/google'
144
+ import { xaiProvider } from '@gullabs/xai'
145
+
146
+ const client = createClient({
147
+ ...composeProviders([googleProvider(), xaiProvider()]),
148
+ })
149
+
150
+ const result = await client.generate(
151
+ {
152
+ provider: 'xai',
153
+ model: 'grok-4.5',
154
+ messages: [{ role: 'user', parts: [{ kind: 'text', text: 'Hello, Grok.' }] }],
155
+ config: { reasoning: { effort: 'high' } },
156
+ },
157
+ { auth: { apiKey: myResolvedXaiKey } },
158
+ )
159
+ ```
160
+
161
+ `grok-4.5`'s `reasoning.effort` admits only `'low' | 'high'` (live-verified) —
162
+ `'none'`, `'medium'`, and `'xhigh'` are rejected by both the live API and
163
+ `Grok45ConfigSchema`. Unlike Gemini, xai has no `serviceTier` concept and no `topK`;
164
+ its config schema is a single strict object with no tier branching.
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
+
192
+ ## Migrating raw `@google/genai` prompts
193
+
194
+ `geminiContentToMessages` (from `@gullabs/google`) converts hand-authored
195
+ `@google/genai` `Content[]` into any-llm's normalized `{ system?, messages }` shape.
196
+ Reject-don't-map: a missing/unrecognized `Content.role` (only `'user'` and `'model'`
197
+ are accepted, never inferred), a `systemInstruction` containing anything other than
198
+ plain text parts, or any `Part` sub-field this library can't losslessly represent
199
+ (function calls, executable code, tool results, thought-flagged parts,
200
+ `thoughtSignature`, unknown future fields, etc.) throws `LlmError('bad_request')`
201
+ naming the offending field — nothing is ever silently dropped.
202
+
203
+ ```ts
204
+ import { geminiContentToMessages } from '@gullabs/google'
205
+ import type { Content } from '@google/genai'
206
+
207
+ const contents: Content[] = [
208
+ {
209
+ role: 'user',
210
+ parts: [
211
+ { text: 'Describe this image.' },
212
+ { inlineData: { mimeType: 'image/png', data } },
213
+ ],
214
+ },
215
+ { role: 'model', parts: [{ text: 'A red bicycle leaning against a brick wall.' }] },
216
+ ]
217
+
218
+ const { system, messages } = geminiContentToMessages({
219
+ contents,
220
+ systemInstruction: 'You are a concise visual describer.',
221
+ })
222
+
223
+ const result = await client.generate(
224
+ { provider: 'google', model: 'gemini-2.5-pro', system, messages },
225
+ { auth: { apiKey: myResolvedGeminiKey } },
226
+ )
227
+ ```
228
+
229
+ `system` is derived only from the explicit `systemInstruction` input — never inferred
230
+ from `contents`.
231
+
98
232
  ## `defineCallSite` — reusable prompt templates
99
233
 
100
234
  ```ts
@@ -125,7 +259,7 @@ the selected descriptor's strict runtime schema before dispatch.
125
259
  Treat model config as descriptor-owned:
126
260
 
127
261
  ```ts
128
- import { defaultGeminiRegistry } from '@gullabs/core'
262
+ import { defaultGeminiRegistry } from '@gullabs/google'
129
263
 
130
264
  const descriptor = defaultGeminiRegistry.resolve('google', 'gemini-3.5-flash')
131
265
  if (!descriptor) throw new Error('unknown model')
@@ -148,6 +282,47 @@ Important distinctions:
148
282
  - `providerOptions.google` is a typed provider-extension lane, not a caller-wins
149
283
  override lane for `serviceTier`, sampling, reasoning, or response schema.
150
284
 
285
+ ## Extending `ProviderOptionsMap` for a third-party provider
286
+
287
+ `GenConfig.providerOptions` is typed as `ProviderOptions`, an alias for
288
+ `ProviderOptionsMap` — an empty, augmentable interface owned by `@gullabs/core`. It
289
+ carries no keys until a provider package augments it via TypeScript declaration
290
+ merging. `@gullabs/google` and `@gullabs/xai` are the two reference implementations
291
+ (`packages/google/src/types.ts`, `packages/xai/src/types.ts`):
292
+
293
+ ```ts
294
+ declare module '@gullabs/core' {
295
+ interface ProviderOptionsMap {
296
+ google?: GoogleProviderOptions
297
+ }
298
+ }
299
+ ```
300
+
301
+ A third-party provider package follows the identical pattern. For a hypothetical
302
+ `@acme/my-provider` package:
303
+
304
+ ```ts
305
+ // packages/my-provider/src/types.ts
306
+ export type MyProviderOptions = {
307
+ someAllowlistedKnob?: string
308
+ }
309
+
310
+ declare module '@gullabs/core' {
311
+ interface ProviderOptionsMap {
312
+ myProvider?: MyProviderOptions
313
+ }
314
+ }
315
+ ```
316
+
317
+ Importing anything from that module — even a type-only import — pulls in the
318
+ augmentation, so re-export it unconditionally from the package's `index.ts` (the way
319
+ `packages/google/src/index.ts` and `packages/xai/src/index.ts` both do) to guarantee
320
+ the `myProvider` key is visible on `ProviderOptionsMap` whenever a caller imports
321
+ anything from the package. Once loaded, `config.providerOptions.myProvider`
322
+ type-checks at call sites — but the model's `configSchema` must also allowlist that
323
+ key for the value to survive validation; the Zod schema remains the runtime boundary,
324
+ the type augmentation only makes the shape visible to the compiler.
325
+
151
326
  ## Structured output — auth + validation together
152
327
 
153
328
  `request.output = { jsonSchema }` (or `callSite.jsonSchema`) is forwarded to the
@@ -156,12 +331,11 @@ response and sets `outputParsed`; `result.output` is always `unknown`. **The cal
156
331
  owns shape validation** — this library does not validate output shape itself.
157
332
 
158
333
  ```ts
159
- import { createClient, geminiPricingSource, geminiAdapter } from '@gullabs/any-llm'
334
+ import { createClient, composeProviders, googleProvider } from '@gullabs/any-llm'
160
335
  import type { StandardSchemaV1 } from '@gullabs/core'
161
336
 
162
337
  const client = createClient({
163
- adapters: [geminiAdapter()],
164
- pricingSources: { google: geminiPricingSource() },
338
+ ...composeProviders([googleProvider()]),
165
339
  })
166
340
 
167
341
  const result = await client.generate(
@@ -273,6 +447,47 @@ Exact model reminders:
273
447
  - Omit `serviceTier` for provider-standard; set `flex` explicitly
274
448
  - `priority` remains rejected by the library even though Google documents it
275
449
 
450
+ ## Context caching — `GoogleCacheStore`
451
+
452
+ `GoogleCacheStore` (from `@gullabs/google`) is a thin, **process-scoped** wrapper over
453
+ the Gemini Context Cache API (`create` / `getOrCreate` / `refreshIfExpiringSoon` /
454
+ `delete`). Pass the resulting `cacheName` as `providerOptions.google.cachedContent` on
455
+ a request. Reuse is only within this store instance's in-memory map — it is not
456
+ shared across processes, workers, or restarts.
457
+
458
+ Optional preflight gate: pass `preflight` to the constructor to refuse a cache
459
+ `create()` — including through `getOrCreate()` and its coalesced in-flight path —
460
+ when the token-bearing payload (`model` + `contents` + `systemInstruction` only;
461
+ `ttl` and `displayName` are excluded) doesn't clear a minimum token count. This
462
+ mirrors Gemini's own explicit-caching minimum (2048 tokens on 3.x) without
463
+ hard-coding it into the store.
464
+
465
+ ```ts
466
+ import { GoogleCacheStore } from '@gullabs/google'
467
+
468
+ const cacheStore = new GoogleCacheStore({
469
+ auth: { apiKey: myResolvedGeminiKey },
470
+ preflight: {
471
+ minTokens: 2048,
472
+ // Receives genai-native Content[]/Content|string — NOT the library's
473
+ // Message[] shape; there is no automatic conversion. Hosts building from
474
+ // Message[] should call client.countTokens separately instead.
475
+ countTokens: async (payload) => {
476
+ const result = await genaiClient.models.countTokens(payload)
477
+ return result.totalTokens ?? 0
478
+ },
479
+ },
480
+ })
481
+
482
+ const handle = await cacheStore.create({
483
+ model: 'gemini-3.1-pro-preview',
484
+ ttlSeconds: 3600,
485
+ contents: myGenaiContents,
486
+ })
487
+ // Throws LlmError('bad_request') before any I/O if preflight.countTokens resolves
488
+ // below minTokens — nothing is silently allowed through under the minimum.
489
+ ```
490
+
276
491
  ## Rate limiting and cost tracking
277
492
 
278
493
  - Pre-send backpressure is a `RateLimiter` port (`ClientConfig.rateLimiter`); default