@warlock.js/ai 4.13.0 → 4.15.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/llms-full.txt CHANGED
@@ -1545,7 +1545,7 @@ if (report.regression && !report.regression.passed) process.exit(1);
1545
1545
 
1546
1546
  ---
1547
1547
  name: generate-images
1548
- description: 'Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter''s image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google imagen-* (per-image). Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; ''generate an image'', ''text to image'', ''gpt-image'', ''dall-e'', ''imagen'', ''product thumbnail'', ''image output''; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.'
1548
+ description: 'Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter''s image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google gemini-* (generateContent + responseModalities IMAGE, usage passed through) / imagen-* and every other id (per-image, generateImages — deprecated by Google); the id picks the transport and is never validated locally. Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; ''generate an image'', ''text to image'', ''gpt-image'', ''dall-e'', ''imagen'', ''product thumbnail'', ''image output''; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.'
1549
1549
  ---
1550
1550
 
1551
1551
  # Generate images — the image-output verb (`ai.image`)
@@ -1626,20 +1626,29 @@ const gpt = openai.image({ name: "gpt-image-1", pricing: { input: 5, output: 40
1626
1626
  const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } });
1627
1627
  ```
1628
1628
 
1629
- A non-image model id (`openai.image({ name: "gpt-4o" })`) throws `InvalidRequestError` **at construction**fail fast, like the embedder/vision guards.
1629
+ The model id is **not validated locally**. `openai.image({ name })` forwards the id to `client.images.generate` exactly as given, so a non-image id (`openai.image({ name: "gpt-4o" })`) constructs fine and fails at OpenAIas a typed provider error on `result.error`, never as a local throw at construction.
1630
1630
 
1631
- ## Google — Imagen (per-image)
1631
+ ## Google — Imagen (per-image) and Gemini (per-token)
1632
1632
 
1633
1633
  ```ts
1634
1634
  import { GoogleSDK } from "@warlock.js/ai-google";
1635
1635
 
1636
1636
  const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
1637
+
1638
+ // Imagen — per-image-metered, via ai.models.generateImages:
1637
1639
  const imagen = google.image({ name: "imagen-4.0-generate-001", pricing: { perImage: 0.04 } });
1638
1640
 
1641
+ // Gemini image model — token-metered, via ai.models.generateContent:
1642
+ const gemini = google.image({ name: "gemini-3.1-flash-lite-image", pricing: { input: 0.3, output: 30 } });
1643
+
1639
1644
  const { data } = await ai.image({ model: imagen, prompt: "a watercolor lighthouse at dawn", aspectRatio: "3:4" });
1640
1645
  ```
1641
1646
 
1642
- Imagen returns base64 bytes (no hosted URL). When every candidate is safety-filtered, `ai.image` surfaces a typed `ContentFilterError` on `result.error`.
1647
+ The **id picks the transport**: a `gemini-` id goes to `generateContent` (token `usage` is passed through as Google reports it — price with `{ input, output }`), anything else to `generateImages` (Imagen always zero usage, price with `{ perImage }`). Both surface images in the same `GeneratedImage` shape (base64 bytes, no hosted URL). When Google filters the request, `ai.image` surfaces a typed `ContentFilterError` on `result.error`; a Gemini response that answered with text instead of an image surfaces a `ProviderError` quoting that text.
1648
+
1649
+ ⚠ **No test calls the live API**, so the Gemini path rests on two tiers of evidence. Measured here: a `gemini-*` id reached `generateContent` and returned a quota error (429) where `generateImages` returned 404 — the endpoint accepts the id. Reported by the maintainer: with billing enabled, an image comes back end-to-end. Still unknown is whether these models report token usage — no `usageMetadata` from a successful image call has been seen. Google has also **deprecated `generateImages`** ("will be removed in the next major release (not before Jan. 1 2027)"), so the Imagen path is on a clock.
1650
+
1651
+ Like every adapter, Google does **not** guard the model id — the id selects a route, it is never refused locally, so an id Google does not serve fails as a typed provider error on `result.error`, not with a local throw at construction.
1643
1652
 
1644
1653
  ## Cost-truth — one rollup, two metering models
1645
1654
 
@@ -1713,7 +1722,7 @@ if (error) {
1713
1722
  }
1714
1723
  ```
1715
1724
 
1716
- `SpeechModelContract` mirrors `EmbedderContract` / `ImageModelContract` — a peer primitive produced by the adapter's optional `speech?()` factory. An adapter without a TTS API simply doesn't define `speech()`, so calling it is a **compile-time** error, not a silent runtime failure. A non-TTS model id (`openai.speech({ name: "gpt-4o" })`) throws `InvalidRequestError` **at construction** fail fast, like the embedder/image guards.
1725
+ `SpeechModelContract` mirrors `EmbedderContract` / `ImageModelContract` — a peer primitive produced by the adapter's optional `speech?()` factory. An adapter without a TTS API simply doesn't define `speech()`, so calling it is a **compile-time** error, not a silent runtime failure. The model id itself is **not validated locally** — a non-TTS id (`openai.speech({ name: "gpt-4o" })`) constructs fine and is forwarded to the provider as given, so it fails as a typed provider error on `result.error`, never as a local throw at construction.
1717
1726
 
1718
1727
  ## The result envelope
1719
1728
 
@@ -5150,7 +5159,7 @@ if (error) console.warn(error.code); // typed AIError
5150
5159
  else console.log(data.text); // the transcript
5151
5160
  ```
5152
5161
 
5153
- `TranscriptionModelContract` mirrors `SpeechModelContract` — a peer primitive produced by the adapter's optional `transcribe?()` factory. A non-STT model id (`openai.transcribe({ name: "gpt-4o" })`) throws `InvalidRequestError` **at construction** fail fast, like the speech/embedder guards.
5162
+ `TranscriptionModelContract` mirrors `SpeechModelContract` — a peer primitive produced by the adapter's optional `transcribe?()` factory. The model id is **not validated locally** — a non-STT id (`openai.transcribe({ name: "gpt-4o" })`) constructs fine and is forwarded to the provider as given, so it fails as a typed provider error on `result.error`, never as a local throw at construction.
5154
5163
 
5155
5164
  ## The `AudioInput` shape + the two builders
5156
5165
 
package/llms.txt CHANGED
@@ -17,7 +17,7 @@
17
17
  - [embed-text](@warlock.js/ai/embed-text/SKILL.md): Text-to-vector via sdk.embedder({...}) — embed(string) for single, embedMany(string[]) for batch. Peer primitive on the SDK adapter, not wired into agents. Compose into RAG tools, workflow run steps, or ai.middleware.semanticCache. Triggers: `sdk.embedder`, `EmbedderContract`, `embedder.embed`, `embedder.embedMany`, `EmbeddingResult`, `EmbeddingBatchResult`, `dimensions`; 'embed text', 'build RAG tool', 'populate vector store', 'embedding batch'; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: cache similarity — `@warlock.js/cache/use-cache-similarity/SKILL.md`; pgvector queries — `@warlock.js/cascade/search-by-vector/SKILL.md`; competing libs `langchain` embeddings, raw `openai.embeddings.create`.
18
18
  - [escalate-block-to-human](@warlock.js/ai/escalate-block-to-human/SKILL.md): Route a hard guardrail block to a human-review surface with @warlock.js/ai-guard — the `escalation.onBlock` seam and an `escalate: true` verdict. Triggers: `escalation`, `onBlock`, `GuardrailEscalation`, `GuardrailBlockEvent`, `escalate: true`, `{ type: "block", escalate: true }`, 'escalate a block to a human', 'human review queue for guardrail', 'page an operator on a guardrail block', 'human-in-the-loop guardrail', 'compose a block with a review surface', 'custom detector that escalates'; typical import `import "@warlock.js/ai-guard"` then `ai.guardrail({ escalation: { onBlock } })`. Skip: composing the guard / phases / verdict model — `@warlock.js/ai-guard/guard-input-output/SKILL.md`; PII/moderation detectors — `@warlock.js/ai-guard/detect-and-redact-pii/SKILL.md`; durable suspend/resume human-step machinery (deferred) — not in this package.
19
19
  - [eval-datasets-and-ci](@warlock.js/ai/eval-datasets-and-ci/SKILL.md): Datasets + regression-gated eval CI with ai.dataset({...}) feeding agent.eval({cases,baseline,tolerance}). Covers the immutable filterable/shardable dataset (cases / fromFile JSONL), DatasetEntry tags, EvalReport.regression (regressed/added/removed/passed) against a baseline, and the ai.eval reporters toJUnit / toJSON / fromJSON for CI artifacts + committed baselines. Triggers: `ai.dataset`, `DatasetContract`, `DatasetEntry`, `DatasetOptions`, `dataset.filter`, `dataset.shard`, `fromFile`, `agent.eval`, `EvalOptions`, `EvalReport`, `EvalCaseResult`, `EvalRegression`, `baseline`, `tolerance`, `ai.eval.toJUnit`, `ai.eval.toJSON`, `ai.eval.fromJSON`, `diff`, JSONL; 'eval dataset from a JSONL file', 'shard an eval suite across CI jobs', 'fail CI on an eval regression', 'emit a JUnit report', 'snapshot an eval baseline'; typical import `import { ai } from "@warlock.js/ai"`. Skip: the scorers + LLM-as-judge + Vitest matchers themselves — `@warlock.js/ai/ai-dx-helpers/SKILL.md` (registerAiMatchers / ai.eval.exact|contains|predicate|judge); record/replay of model calls for deterministic tests — `@warlock.js/ai/record-replay-llm/SKILL.md`; competing libs `promptfoo`, `braintrust`.
20
- - [generate-images](@warlock.js/ai/generate-images/SKILL.md): Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter's image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google imagen-* (per-image). Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; 'generate an image', 'text to image', 'gpt-image', 'dall-e', 'imagen', 'product thumbnail', 'image output'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.
20
+ - [generate-images](@warlock.js/ai/generate-images/SKILL.md): Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter's image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google gemini-* (generateContent + responseModalities IMAGE, usage passed through) / imagen-* and every other id (per-image, generateImages — deprecated by Google); the id picks the transport and is never validated locally. Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; 'generate an image', 'text to image', 'gpt-image', 'dall-e', 'imagen', 'product thumbnail', 'image output'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.
21
21
  - [generate-speech](@warlock.js/ai/generate-speech/SKILL.md): Text-to-speech via ai.speech({ model: sdk.speech({ name }), text }) — the audio-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter's speech() factory: OpenAI tts-1 / tts-1-hd (per-character) or gpt-4o-mini-tts (per-token). Synthesized audio is a discriminated GeneratedAudio = { type: "base64"; base64; mediaType }. Options: voice / format / speed / instructions / signal. Triggers: `ai.speech`, `sdk.speech`, `openai.speech`, `SpeechModelContract`, `GeneratedAudio`, `SpeechModelPricing`, `SpeechOptions`, `MockSpeechModel`; 'text to speech', 'TTS', 'synthesize voice', 'read this aloud', 'tts-1', 'gpt-4o-mini-tts', 'voice narration', 'audio output', 'speak this text'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: speech-to-text / transcribing a voice note — [[transcribe-audio]]; image OUTPUT — [[generate-images]]; competing libs raw `openai.audio.speech.create`, `elevenlabs` SDK.
22
22
  - [guard-input-output](@warlock.js/ai/guard-input-output/SKILL.md): Build the composed guardrail middleware with @warlock.js/ai-guard and wire it into an agent — `ai.guardrail({ input, output, tool, toolNames, escalation })`. Triggers: `ai.guardrail`, `guard`, `GuardOptions`, `GuardrailVerdict`, `GuardrailDetector`, `GuardrailPhase`, `GuardrailMatch`, `GuardrailViolationError`, `ai.guardrail.topic`, `ai.guardrail.injection`, `topicFilter`, `injectionDetector`, `toolNames`, `forTool`; 'add a guardrail to my agent', 'block prompt injection', 'filter banned topics', 'guard agent input and output', 'stop the model leaking data into a tool call', 'scope a detector to one tool'; typical import `import "@warlock.js/ai-guard"` (registers `ai.guardrail`) or `import { guard } from "@warlock.js/ai-guard"`. Skip: PII detection/redaction specifically — `@warlock.js/ai-guard/detect-and-redact-pii/SKILL.md`; routing a block to a human — `@warlock.js/ai-guard/escalate-block-to-human/SKILL.md`; the core middleware pipeline / hook contract — `@warlock.js/ai/run-ai-agent/SKILL.md`.
23
23
  - [handle-ai-errors](@warlock.js/ai/handle-ai-errors/SKILL.md): Typed AIError hierarchy with stable code strings + coarse category for retry-policy dispatch. execute() never throws — errors surface via result.error (the sole exception: OrchestratorConfigError throws at construction). Triggers: `AIError`, `ProviderRateLimitError`, `ProviderAuthError`, `ContextLengthExceededError`, `ContentFilterError`, `SchemaValidationError`, `ToolExecutionError`, `WorkflowDriftError`, `SupervisorDriftError`, `SupervisorFailedError`, `SupervisorRoutingError`, `OrchestratorFailedError`, `OrchestratorDriftError`, `OrchestratorConfigError`, `OrchestratorCancelledError`, `PlannerFailedError`, `PlannerPlanInvalidError`, `PlannerCancelledError`, `BudgetExceededError`, `GuardrailViolationError`, `error.code`, `error.category`; 'handle ai error', 'retry on rate limit', 'branch on error code', 'ORCHESTRATOR_DRIFT', 'PLANNER_PLAN_INVALID', 'build fallback ladder'; typical import `import { AIError } from "@warlock.js/ai"`. Skip: log surfacing — `@warlock.js/ai/log-ai-calls/SKILL.md`; native `try / catch` on raw `openai`.
package/package.json CHANGED
@@ -15,16 +15,36 @@
15
15
  "@standard-schema/spec": "^1.0.0"
16
16
  },
17
17
  "peerDependencies": {
18
- "@warlock.js/ai-openai": "4.13.0",
19
- "@warlock.js/cache": "4.13.0",
20
- "@warlock.js/logger": "4.13.0",
18
+ "@warlock.js/ai-openai": "4.15.0",
19
+ "@warlock.js/cache": "4.15.0",
20
+ "@warlock.js/logger": "4.15.0",
21
21
  "langfuse": "*",
22
22
  "openai": "*",
23
23
  "pdf-parse": "*",
24
24
  "pg": "*",
25
25
  "redis": "*"
26
26
  },
27
- "version": "4.13.0",
27
+ "peerDependenciesMeta": {
28
+ "@warlock.js/ai-openai": {
29
+ "optional": true
30
+ },
31
+ "langfuse": {
32
+ "optional": true
33
+ },
34
+ "openai": {
35
+ "optional": true
36
+ },
37
+ "pdf-parse": {
38
+ "optional": true
39
+ },
40
+ "pg": {
41
+ "optional": true
42
+ },
43
+ "redis": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "version": "4.15.0",
28
48
  "main": "./cjs/index.cjs",
29
49
  "module": "./esm/index.mjs",
30
50
  "types": "./esm/index.d.mts",
package/skills/README.md CHANGED
@@ -38,7 +38,7 @@ Datasets + regression-gated eval CI with ai.dataset({...}) feeding agent.eval({c
38
38
 
39
39
  ### [`generate-images/`](./generate-images/SKILL.md)
40
40
 
41
- Text-to-image with ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws {data, error, usage, report} envelope with cost-truth + panoptic observation. Models come from an adapter's image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google imagen-* (per-image). Result images are a discriminated GeneratedImage = {type:"base64"} | {type:"url"}. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`, `perImage`; 'generate an image', 'text to image', 'gpt-image', 'dall-e', 'imagen', 'product thumbnail', 'image output'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.
41
+ Text-to-image with ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws {data, error, usage, report} envelope with cost-truth + panoptic observation. Models come from an adapter's image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google gemini-* (token-metered, routed to generateContent with responseModalities IMAGE) / imagen-* and every other id (per-image, routed to the older generateImages) — the id picks the transport, and no model id is validated locally. Result images are a discriminated GeneratedImage = {type:"base64"} | {type:"url"}. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`, `perImage`; 'generate an image', 'text to image', 'gpt-image', 'dall-e', 'imagen', 'gemini image', 'product thumbnail', 'image output'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.
42
42
 
43
43
  ### [`handle-ai-errors/`](./handle-ai-errors/SKILL.md)
44
44
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: generate-images
3
- description: 'Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter''s image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google imagen-* (per-image). Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; ''generate an image'', ''text to image'', ''gpt-image'', ''dall-e'', ''imagen'', ''product thumbnail'', ''image output''; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.'
3
+ description: 'Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter''s image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google gemini-* (generateContent + responseModalities IMAGE, usage passed through) / imagen-* and every other id (per-image, generateImages — deprecated by Google); the id picks the transport and is never validated locally. Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; ''generate an image'', ''text to image'', ''gpt-image'', ''dall-e'', ''imagen'', ''product thumbnail'', ''image output''; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.'
4
4
  ---
5
5
 
6
6
  # Generate images — the image-output verb (`ai.image`)
@@ -81,20 +81,29 @@ const gpt = openai.image({ name: "gpt-image-1", pricing: { input: 5, output: 40
81
81
  const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } });
82
82
  ```
83
83
 
84
- A non-image model id (`openai.image({ name: "gpt-4o" })`) throws `InvalidRequestError` **at construction**fail fast, like the embedder/vision guards.
84
+ The model id is **not validated locally**. `openai.image({ name })` forwards the id to `client.images.generate` exactly as given, so a non-image id (`openai.image({ name: "gpt-4o" })`) constructs fine and fails at OpenAIas a typed provider error on `result.error`, never as a local throw at construction.
85
85
 
86
- ## Google — Imagen (per-image)
86
+ ## Google — Imagen (per-image) and Gemini (per-token)
87
87
 
88
88
  ```ts
89
89
  import { GoogleSDK } from "@warlock.js/ai-google";
90
90
 
91
91
  const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
92
+
93
+ // Imagen — per-image-metered, via ai.models.generateImages:
92
94
  const imagen = google.image({ name: "imagen-4.0-generate-001", pricing: { perImage: 0.04 } });
93
95
 
96
+ // Gemini image model — token-metered, via ai.models.generateContent:
97
+ const gemini = google.image({ name: "gemini-3.1-flash-lite-image", pricing: { input: 0.3, output: 30 } });
98
+
94
99
  const { data } = await ai.image({ model: imagen, prompt: "a watercolor lighthouse at dawn", aspectRatio: "3:4" });
95
100
  ```
96
101
 
97
- Imagen returns base64 bytes (no hosted URL). When every candidate is safety-filtered, `ai.image` surfaces a typed `ContentFilterError` on `result.error`.
102
+ The **id picks the transport**: a `gemini-` id goes to `generateContent` (token `usage` is passed through as Google reports it — price with `{ input, output }`), anything else to `generateImages` (Imagen always zero usage, price with `{ perImage }`). Both surface images in the same `GeneratedImage` shape (base64 bytes, no hosted URL). When Google filters the request, `ai.image` surfaces a typed `ContentFilterError` on `result.error`; a Gemini response that answered with text instead of an image surfaces a `ProviderError` quoting that text.
103
+
104
+ ⚠ **No test calls the live API**, so the Gemini path rests on two tiers of evidence. Measured here: a `gemini-*` id reached `generateContent` and returned a quota error (429) where `generateImages` returned 404 — the endpoint accepts the id. Reported by the maintainer: with billing enabled, an image comes back end-to-end. Still unknown is whether these models report token usage — no `usageMetadata` from a successful image call has been seen. Google has also **deprecated `generateImages`** ("will be removed in the next major release (not before Jan. 1 2027)"), so the Imagen path is on a clock.
105
+
106
+ Like every adapter, Google does **not** guard the model id — the id selects a route, it is never refused locally, so an id Google does not serve fails as a typed provider error on `result.error`, not with a local throw at construction.
98
107
 
99
108
  ## Cost-truth — one rollup, two metering models
100
109
 
@@ -26,7 +26,7 @@ if (error) {
26
26
  }
27
27
  ```
28
28
 
29
- `SpeechModelContract` mirrors `EmbedderContract` / `ImageModelContract` — a peer primitive produced by the adapter's optional `speech?()` factory. An adapter without a TTS API simply doesn't define `speech()`, so calling it is a **compile-time** error, not a silent runtime failure. A non-TTS model id (`openai.speech({ name: "gpt-4o" })`) throws `InvalidRequestError` **at construction** fail fast, like the embedder/image guards.
29
+ `SpeechModelContract` mirrors `EmbedderContract` / `ImageModelContract` — a peer primitive produced by the adapter's optional `speech?()` factory. An adapter without a TTS API simply doesn't define `speech()`, so calling it is a **compile-time** error, not a silent runtime failure. The model id itself is **not validated locally** — a non-TTS id (`openai.speech({ name: "gpt-4o" })`) constructs fine and is forwarded to the provider as given, so it fails as a typed provider error on `result.error`, never as a local throw at construction.
30
30
 
31
31
  ## The result envelope
32
32
 
@@ -33,7 +33,7 @@ if (error) console.warn(error.code); // typed AIError
33
33
  else console.log(data.text); // the transcript
34
34
  ```
35
35
 
36
- `TranscriptionModelContract` mirrors `SpeechModelContract` — a peer primitive produced by the adapter's optional `transcribe?()` factory. A non-STT model id (`openai.transcribe({ name: "gpt-4o" })`) throws `InvalidRequestError` **at construction** fail fast, like the speech/embedder guards.
36
+ `TranscriptionModelContract` mirrors `SpeechModelContract` — a peer primitive produced by the adapter's optional `transcribe?()` factory. The model id is **not validated locally** — a non-STT id (`openai.transcribe({ name: "gpt-4o" })`) constructs fine and is forwarded to the provider as given, so it fails as a typed provider error on `result.error`, never as a local throw at construction.
37
37
 
38
38
  ## The `AudioInput` shape + the two builders
39
39