@gullabs/any-llm 0.6.1 → 0.8.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
@@ -20,19 +20,19 @@ 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
- pricing: geminiPricingSource(),
30
+ ...composeProviders([googleProvider()]),
32
31
  })
33
32
 
34
33
  const summarize = defineCallSite({
35
34
  id: 'summarize',
35
+ provider: 'google',
36
36
  model: 'gemini-2.5-flash',
37
37
  jsonSchema: {
38
38
  type: 'object',
@@ -52,7 +52,7 @@ const result = await client.runStructured(
52
52
  { auth: { apiKey: process.env.GEMINI_API_KEY! } },
53
53
  )
54
54
 
55
- const descriptor = defaultGeminiRegistry.resolve('gemini-3.5-flash')
55
+ const descriptor = defaultGeminiRegistry.resolve('google', 'gemini-3.5-flash')
56
56
  if (!descriptor) throw new Error('unknown model')
57
57
 
58
58
  const parsedConfig = descriptor.configSchema.parse({
@@ -70,17 +70,20 @@ intended. `priority` remains rejected by the library for now.
70
70
  ## Key exports
71
71
 
72
72
  This package re-exports the full public API of `@gullabs/core` and `@gullabs/google` verbatim —
73
- `createClient`, `defineCallSite`, `geminiAdapter`, `geminiPricingSource`, `LlmError`, and every
74
- other named export from both packages. See their READMEs for details:
75
-
76
- | Export | What it is |
77
- | ----------------------- | ---------------------------------------------------------- |
78
- | `createClient(config)` | Wires ports into a `{ generate, runStructured }` client |
79
- | `defineCallSite(opts)` | Defines a typed, reusable prompt template bound to a model |
80
- | `geminiAdapter(opts?)` | The Gemini `ProviderAdapter`, from `@gullabs/google` |
81
- | `geminiPricingSource()` | Built-in Gemini pricing snapshot |
82
- | `LlmError` | Typed error class — always thrown on call failure |
83
- | `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` |
84
87
 
85
88
  Use `@gullabs/core` and `@gullabs/google` directly only when you want modular dependency control.
86
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.6.1";
9
+ var version = "0.8.0";
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.6.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"]}
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"]}
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.6.1";
7
+ var version = "0.8.0";
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.6.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"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gullabs/any-llm",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
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.6.0",
29
- "@gullabs/google": "0.6.1"
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
- pricing: geminiPricingSource(),
53
+ ...composeProviders([googleProvider()]),
44
54
  })
45
55
  await client.generate(request, {} as GenerateOptions)
46
56
 
@@ -54,19 +64,32 @@ before any network call is made. Vertex AI (ADC/service-account auth) is **not
54
64
  supported** — it was removed and is a roadmap item only; do not write code assuming
55
65
  `{ vertex: { project, location } }` works.
56
66
 
67
+ ## #2 gotcha: identity is `(provider, model)`, not a bare `model` string
68
+
69
+ Every `generate()`/`runStructured()` request and every `defineCallSite()` requires an
70
+ explicit top-level `provider` (e.g. `'google'`, `'claude-cli'`, `'codex-cli'`) alongside
71
+ the bare, provider-native `model` string. The engine routes by `req.provider` directly
72
+ — it never derives, parses, or guesses a provider from `model`, and it never accepts a
73
+ slash-joined string like `'google/gemini-2.5-flash'`. A request with no `provider`, an
74
+ unconfigured `provider`, or a `(provider, model)` pair the registry doesn't recognize is
75
+ rejected with `LlmError({ kind: 'bad_request' })` before any I/O. This also means the
76
+ same bare model id can exist under multiple providers with different config schemas —
77
+ `resolve()` always takes both.
78
+
57
79
  ## Quickstart
58
80
 
59
81
  ```ts
60
- import { createClient, geminiPricingSource, geminiAdapter } from '@gullabs/any-llm'
61
- // (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)
62
85
 
63
86
  const client = createClient({
64
- adapters: [geminiAdapter()],
65
- pricing: geminiPricingSource(),
87
+ ...composeProviders([googleProvider()]),
66
88
  })
67
89
 
68
90
  const result = await client.generate(
69
91
  {
92
+ provider: 'google',
70
93
  model: 'gemini-2.5-flash',
71
94
  messages: [{ role: 'user', parts: [{ kind: 'text', text: 'Hello!' }] }],
72
95
  },
@@ -82,6 +105,104 @@ console.log(result.cost?.microUsd) // integer micro-USD, or null if unpriced
82
105
  `{ kind: 'text', text }`, `{ kind: 'inline-media', mimeType, data /* raw base64, no data: prefix */ }`,
83
106
  and `{ kind: 'file-uri', uri, mimeType }` freely in one `parts` array.
84
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
+ ## Migrating raw `@google/genai` prompts
167
+
168
+ `geminiContentToMessages` (from `@gullabs/google`) converts hand-authored
169
+ `@google/genai` `Content[]` into any-llm's normalized `{ system?, messages }` shape.
170
+ Reject-don't-map: a missing/unrecognized `Content.role` (only `'user'` and `'model'`
171
+ are accepted, never inferred), a `systemInstruction` containing anything other than
172
+ plain text parts, or any `Part` sub-field this library can't losslessly represent
173
+ (function calls, executable code, tool results, thought-flagged parts,
174
+ `thoughtSignature`, unknown future fields, etc.) throws `LlmError('bad_request')`
175
+ naming the offending field — nothing is ever silently dropped.
176
+
177
+ ```ts
178
+ import { geminiContentToMessages } from '@gullabs/google'
179
+ import type { Content } from '@google/genai'
180
+
181
+ const contents: Content[] = [
182
+ {
183
+ role: 'user',
184
+ parts: [
185
+ { text: 'Describe this image.' },
186
+ { inlineData: { mimeType: 'image/png', data } },
187
+ ],
188
+ },
189
+ { role: 'model', parts: [{ text: 'A red bicycle leaning against a brick wall.' }] },
190
+ ]
191
+
192
+ const { system, messages } = geminiContentToMessages({
193
+ contents,
194
+ systemInstruction: 'You are a concise visual describer.',
195
+ })
196
+
197
+ const result = await client.generate(
198
+ { provider: 'google', model: 'gemini-2.5-pro', system, messages },
199
+ { auth: { apiKey: myResolvedGeminiKey } },
200
+ )
201
+ ```
202
+
203
+ `system` is derived only from the explicit `systemInstruction` input — never inferred
204
+ from `contents`.
205
+
85
206
  ## `defineCallSite` — reusable prompt templates
86
207
 
87
208
  ```ts
@@ -89,6 +210,7 @@ import { defineCallSite } from '@gullabs/core'
89
210
 
90
211
  const summarize = defineCallSite({
91
212
  id: 'summarize-article', // persisted as callSiteId on every record
213
+ provider: 'google',
92
214
  model: 'gemini-2.5-flash',
93
215
  system: 'You are a concise summarizer.',
94
216
  userTemplate: 'Summarize this article in 3 sentences:\n\n{{article}}',
@@ -111,9 +233,9 @@ the selected descriptor's strict runtime schema before dispatch.
111
233
  Treat model config as descriptor-owned:
112
234
 
113
235
  ```ts
114
- import { defaultGeminiRegistry } from '@gullabs/core'
236
+ import { defaultGeminiRegistry } from '@gullabs/google'
115
237
 
116
- const descriptor = defaultGeminiRegistry.resolve('gemini-3.5-flash')
238
+ const descriptor = defaultGeminiRegistry.resolve('google', 'gemini-3.5-flash')
117
239
  if (!descriptor) throw new Error('unknown model')
118
240
 
119
241
  // UI/forms:
@@ -134,6 +256,47 @@ Important distinctions:
134
256
  - `providerOptions.google` is a typed provider-extension lane, not a caller-wins
135
257
  override lane for `serviceTier`, sampling, reasoning, or response schema.
136
258
 
259
+ ## Extending `ProviderOptionsMap` for a third-party provider
260
+
261
+ `GenConfig.providerOptions` is typed as `ProviderOptions`, an alias for
262
+ `ProviderOptionsMap` — an empty, augmentable interface owned by `@gullabs/core`. It
263
+ carries no keys until a provider package augments it via TypeScript declaration
264
+ merging. `@gullabs/google` and `@gullabs/xai` are the two reference implementations
265
+ (`packages/google/src/types.ts`, `packages/xai/src/types.ts`):
266
+
267
+ ```ts
268
+ declare module '@gullabs/core' {
269
+ interface ProviderOptionsMap {
270
+ google?: GoogleProviderOptions
271
+ }
272
+ }
273
+ ```
274
+
275
+ A third-party provider package follows the identical pattern. For a hypothetical
276
+ `@acme/my-provider` package:
277
+
278
+ ```ts
279
+ // packages/my-provider/src/types.ts
280
+ export type MyProviderOptions = {
281
+ someAllowlistedKnob?: string
282
+ }
283
+
284
+ declare module '@gullabs/core' {
285
+ interface ProviderOptionsMap {
286
+ myProvider?: MyProviderOptions
287
+ }
288
+ }
289
+ ```
290
+
291
+ Importing anything from that module — even a type-only import — pulls in the
292
+ augmentation, so re-export it unconditionally from the package's `index.ts` (the way
293
+ `packages/google/src/index.ts` and `packages/xai/src/index.ts` both do) to guarantee
294
+ the `myProvider` key is visible on `ProviderOptionsMap` whenever a caller imports
295
+ anything from the package. Once loaded, `config.providerOptions.myProvider`
296
+ type-checks at call sites — but the model's `configSchema` must also allowlist that
297
+ key for the value to survive validation; the Zod schema remains the runtime boundary,
298
+ the type augmentation only makes the shape visible to the compiler.
299
+
137
300
  ## Structured output — auth + validation together
138
301
 
139
302
  `request.output = { jsonSchema }` (or `callSite.jsonSchema`) is forwarded to the
@@ -142,16 +305,16 @@ response and sets `outputParsed`; `result.output` is always `unknown`. **The cal
142
305
  owns shape validation** — this library does not validate output shape itself.
143
306
 
144
307
  ```ts
145
- import { createClient, geminiPricingSource, geminiAdapter } from '@gullabs/any-llm'
308
+ import { createClient, composeProviders, googleProvider } from '@gullabs/any-llm'
146
309
  import type { StandardSchemaV1 } from '@gullabs/core'
147
310
 
148
311
  const client = createClient({
149
- adapters: [geminiAdapter()],
150
- pricing: geminiPricingSource(),
312
+ ...composeProviders([googleProvider()]),
151
313
  })
152
314
 
153
315
  const result = await client.generate(
154
316
  {
317
+ provider: 'google',
155
318
  model: 'gemini-2.5-flash',
156
319
  messages: [{ role: 'user', parts: [{ kind: 'text', text: 'Rate this PR 1-10.' }] }],
157
320
  output: {
@@ -222,11 +385,13 @@ try {
222
385
 
223
386
  Bad input or config throws `bad_request` (or `invalid_auth`) **before any I/O** —
224
387
  nothing is silently clamped, coerced, or defaulted around a typo. Examples already
225
- enforced by the engine/adapter: an unrouteable `model` string, a config value that
226
- fails the selected descriptor schema, a `reasoning.budgetTokens` set on a model whose
227
- API only supports `reasoning.effort`, duplicate adapter/middleware `id`s, and a
228
- grounding + structured-output combination outside the exact documented Gemini support
229
- set.
388
+ enforced by the engine/adapter: a request or call site with no `provider`, a `provider`
389
+ that doesn't match any configured adapter, a `(provider, model)` pair absent from the
390
+ registry (including a bare model string with a slash, like `'google/gemini-2.5-flash'`
391
+ — slash strings are never parsed), a config value that fails the selected descriptor
392
+ schema, a `reasoning.budgetTokens` set on a model whose API only supports
393
+ `reasoning.effort`, duplicate adapter/middleware `id`s, and a grounding +
394
+ structured-output combination outside the exact documented Gemini support set.
230
395
 
231
396
  **Do not add defensive fallback/clamping code around this library.** If a call throws
232
397
  `bad_request`, fix the input — do not catch-and-retry with a "safer" guessed value; the
@@ -256,6 +421,47 @@ Exact model reminders:
256
421
  - Omit `serviceTier` for provider-standard; set `flex` explicitly
257
422
  - `priority` remains rejected by the library even though Google documents it
258
423
 
424
+ ## Context caching — `GoogleCacheStore`
425
+
426
+ `GoogleCacheStore` (from `@gullabs/google`) is a thin, **process-scoped** wrapper over
427
+ the Gemini Context Cache API (`create` / `getOrCreate` / `refreshIfExpiringSoon` /
428
+ `delete`). Pass the resulting `cacheName` as `providerOptions.google.cachedContent` on
429
+ a request. Reuse is only within this store instance's in-memory map — it is not
430
+ shared across processes, workers, or restarts.
431
+
432
+ Optional preflight gate: pass `preflight` to the constructor to refuse a cache
433
+ `create()` — including through `getOrCreate()` and its coalesced in-flight path —
434
+ when the token-bearing payload (`model` + `contents` + `systemInstruction` only;
435
+ `ttl` and `displayName` are excluded) doesn't clear a minimum token count. This
436
+ mirrors Gemini's own explicit-caching minimum (2048 tokens on 3.x) without
437
+ hard-coding it into the store.
438
+
439
+ ```ts
440
+ import { GoogleCacheStore } from '@gullabs/google'
441
+
442
+ const cacheStore = new GoogleCacheStore({
443
+ auth: { apiKey: myResolvedGeminiKey },
444
+ preflight: {
445
+ minTokens: 2048,
446
+ // Receives genai-native Content[]/Content|string — NOT the library's
447
+ // Message[] shape; there is no automatic conversion. Hosts building from
448
+ // Message[] should call client.countTokens separately instead.
449
+ countTokens: async (payload) => {
450
+ const result = await genaiClient.models.countTokens(payload)
451
+ return result.totalTokens ?? 0
452
+ },
453
+ },
454
+ })
455
+
456
+ const handle = await cacheStore.create({
457
+ model: 'gemini-3.1-pro-preview',
458
+ ttlSeconds: 3600,
459
+ contents: myGenaiContents,
460
+ })
461
+ // Throws LlmError('bad_request') before any I/O if preflight.countTokens resolves
462
+ // below minTokens — nothing is silently allowed through under the minimum.
463
+ ```
464
+
259
465
  ## Rate limiting and cost tracking
260
466
 
261
467
  - Pre-send backpressure is a `RateLimiter` port (`ClientConfig.rateLimiter`); default
@@ -272,6 +478,8 @@ Exact model reminders:
272
478
 
273
479
  - Forgetting `opts.auth` on a `generate()`/`runStructured()` call — it is required on
274
480
  every call, not just once at `createClient()` time.
481
+ - Omitting `provider` (or writing a slash-joined `'provider/model'` string) on a request
482
+ or call site — `provider` is a required top-level field; the engine never derives it.
275
483
  - Assuming `process.env.GEMINI_API_KEY` (or similar) is read automatically — it is
276
484
  never read by this library; the host must resolve and pass the key itself.
277
485
  - Assuming `result.output`'s shape is validated — it is `unknown`; validate it yourself