@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 +19 -16
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/skills/any-llm/SKILL.md +241 -33
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
|
-
|
|
26
|
-
geminiPricingSource,
|
|
26
|
+
googleProvider,
|
|
27
27
|
} from '@gullabs/any-llm'
|
|
28
28
|
|
|
29
29
|
const client = createClient({
|
|
30
|
-
|
|
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`, `
|
|
74
|
-
other named export from both packages. See their
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
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
package/dist/index.cjs.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.cjs","sourcesContent":["{\n \"name\": \"@gullabs/any-llm\",\n \"version\": \"0.
|
|
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
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.
|
|
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.
|
|
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.
|
|
29
|
-
"@gullabs/google": "0.
|
|
28
|
+
"@gullabs/core": "0.8.0",
|
|
29
|
+
"@gullabs/google": "0.8.0"
|
|
30
30
|
},
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=20.9.0"
|
package/skills/any-llm/SKILL.md
CHANGED
|
@@ -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/
|
|
6
|
-
Applies when adding a new LLM call site,
|
|
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,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
29
|
+
Core packages:
|
|
24
30
|
|
|
25
|
-
- `@gullabs/core` — engine (`createClient`), types, errors, `defineCallSite
|
|
26
|
-
|
|
27
|
-
- `@gullabs/
|
|
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
|
|
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
|
-
|
|
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,
|
|
61
|
-
// (or: from '@gullabs/core'
|
|
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
|
-
|
|
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/
|
|
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,
|
|
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
|
-
|
|
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:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|