@polyweave/google (1.0.11)
Installation
@polyweave:registry=https://hub.kl1.tenere.ai/api/packages/Polyweave/npm/npm install @polyweave/google@1.0.11"@polyweave/google": "1.0.11"About this package
@polyweave/google
Google Gemini adapter for Polyweave — multimodal chat, embeddings, vision, and live audio sessions.
Published as @polyweave/google.
Synopsis
import { createGoogleAdapter } from '@polyweave/google';
import { createTextFrame, createFrameSink } from '@polyweave/core';
var adapter = createGoogleAdapter({ apiKey: process.env.GEMINI_API_KEY });
adapter.source.pipe(createFrameSink(function(frame) {
if (frame.type === 'TEXT') console.log(frame.content.text);
if (frame.type === 'RECEIPT') console.log('Cost:', frame.content.totalCost);
if (frame.type === 'ERROR') console.error(frame.content.message);
}));
// Chat
adapter.sink.write(createTextFrame('Write a haiku about clouds'));
// Vision (URL-based)
adapter.sink.write({ type: 'IMAGE', content: { url: 'https://example.com/photo.png', text: 'Describe this image' } });
// Embeddings
adapter.sink.write({ type: 'PARAM', content: { key: 'embedding_model', value: 'text-embedding-004' } });
adapter.sink.write({ type: 'EMBEDDING', content: { text: 'sample text to embed' } });
// Cost check before sending
adapter.sink.write({ type: 'COST_CHECK', content: { maxBudget: 0.05 } });
adapter.sink.write(createTextFrame('Large prompt...'));
Install
npm install @polyweave/google
Why
This is the Google Gemini adapter for Polyweave. It speaks Google's native API format (contents/parts model), not OpenAI-compatible. Supports multimodal chat with streaming SSE, vision via URL-based fileData.fileUri input, text embeddings via embedContent, and function calling. Also supports the Gemini Live API (WebSocket-based bidirectional audio+text) for models containing live, native-audio, or realtime in their name — routed through createGeminiLiveSession. No external HTTP library dependency — uses Node.js built-in https. API key is passed as a query parameter (not Bearer token).
Models: gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.5-pro, gemini-2.0-flash, gemini-2.0-flash-lite, gemini-2.0-pro, gemini-1.5-flash, gemini-1.5-pro, gemini-1.5-flash-8b, text-embedding-004, gemini-embedding, aqa.
API
createGoogleAdapter(config)
Creates a Google Gemini frame duplex adapter.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
config.apiKey |
string |
No | process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || null |
Google API key |
config.baseURL |
string |
No | 'https://generativelanguage.googleapis.com/v1beta' |
Base URL for the Gemini API |
config.model |
string |
No | 'gemini-2.5-flash' |
Default model (settable via PARAM model key) |
config.budget |
number |
No | 0 |
Hard budget cap in dollars. TEXT/IMAGE frames are rejected once totalSpent >= budget |
config.name |
string |
No | null |
Node name for PARAM targeting (set on duplex.name) |
config.timeout |
number |
No | 120000 |
Request timeout in ms |
Return value: IFrameDuplex with source and sink, plus setReady(key) and name.
source.sink— the downstream consumer (set viasource.pipe(sink))sink.write(frame)— push frames induplex.setReady(key)— set/update API key. Flushes buffered frames.duplex.name— the configured name fromconfig.name
Buffering: If apiKey is not set at construction time, frames are buffered in frameBuffer until setReady(key) is called. Once ready, buffered frames are drained.
Budget enforcement: Before TEXT or IMAGE frames are processed, if budgetTotal > 0 and totalSpent >= budgetTotal, the adapter emits ERROR (code: 'BUDGET_EXCEEDED') + END and drops the frame.
Frame Protocol
Sink (input frames):
| Frame Type | Content Properties | Behavior |
|---|---|---|
TEXT |
content.text (string) |
Chat completion. For live models, routes through WebSocket createGeminiLiveSession. Otherwise appends to contents array and calls sendGeminiChat. |
IMAGE |
content.data || content.b64_json, content.mimeType (default 'image/png'), content.url, content.text || content.prompt |
Vision. URL-based: calls sendGeminiVision (non-streaming). Inline data: appends to contents with inlineData and calls sendGeminiChat. |
AUDIO |
content.data (buffer or base64 string) |
Only routed for live models — creates/uses a WebSocket createGeminiLiveSession. Non-live models: silently dropped (no handler emits error). |
EMBEDDING |
content.text (string) |
Calls sendEmbedding with model from params.embedding_model ('text-embedding-004' default) |
PARAM |
content.key, content.value |
Sets params. Special keys: apiKey calls setReady(v), budget sets budgetTotal. All other keys stored in params object. |
COST_CHECK |
content.maxBudget (number, optional) |
Enters cost check mode. Buffers subsequent frames until non-ESTIMATED_OUTPUT frame arrives, then calls costEstimator.estimate(params, buffer, 'chat'). Emits COST frame + END. If maxBudget exceeded, emits ERROR + END instead. |
ESTIMATED_OUTPUT |
— (silently consumed in cost check mode) | In cost check mode, this frame is swallowed without breaking the cost check buffer. |
START |
— | Silently ignored |
END |
— | Silently ignored |
TOPOLOGY_SCAN |
content.path (array, optional) |
Emits TOPOLOGY_REPORT with adapter metadata, then passes through |
TEST_INJECT |
content.frames (array) |
Recursively writes each frame in the array |
TEST_SNAPSHOT |
— | Emits TEST_SNAPSHOT back |
Source (output frames):
| Frame Type | Emitted When | Content |
|---|---|---|
TEXT |
Chat stream chunks or batch response text | { text: deltaText } with metadata { model, usage?, finishReason? } |
TOOL_CALL |
Function call detected in stream or batch | { name, arguments: { toolName, args } } with id = toolCallId |
EMBEDDING |
Embedding response | { vector, dimensions, model } |
RECEIPT |
Stream progress (every 50 tokens), stream end, batch end, error (partial), embedding completion | { inputTokens, outputTokens, inputCost, outputCost, totalCost } with provider/model metadata |
COST |
After COST_CHECK completes | { bounds, accuracy, estimateSource, forwardEstimate, inputBounds, maxOutputTokens, ... } with provider/model/confidence metadata |
ERROR |
HTTP errors, parse errors, budget exceeded, stream errors | { message, code } (code: 'BUDGET_EXCEEDED' or 'ADAPTER_ERROR') |
END |
After every completion or error | { id, streamId, metadata: { model, error?, ... } } |
TOPOLOGY_REPORT |
After TOPOLOGY_SCAN | Adapter topology metadata |
PARAM keys:
model, max_tokens, temperature, top_p, stream, system, tools, tool_choice, embedding_model, task_type, budget, apiKey, voice (live), instructions (live), modalities (live), input_audio_format (live)
Error conditions:
| Condition | Error code | Frame type | Description |
|---|---|---|---|
| Budget exceeded (pre-check) | 'BUDGET_EXCEEDED' |
ERROR + END | TEXT/IMAGE dropped when totalSpent >= budgetTotal |
| Estimated cost exceeds budget | 'BUDGET_EXCEEDED' |
ERROR + END | COST_CHECK with maxBudget fails estimate |
| HTTP error from Gemini API | 'ADAPTER_ERROR' |
ERROR + END | Status >= 400, message from response body error.message |
| Request timeout | 'ADAPTER_ERROR' |
ERROR + END | req.on('timeout') fires |
| Connection error | 'ADAPTER_ERROR' |
ERROR + END | req.on('error') fires, err.message propagated |
| JSON parse error | 'ADAPTER_ERROR' |
ERROR + END | JSON.parse fails on response, message: 'Parse: ...' |
| Stream error | 'ADAPTER_ERROR' |
ERROR + END | event.error.message from SSE stream |
| WebSocket error (live) | ErrorCode.PROVIDER_ERROR |
ERROR + END | Live session transport failure |
| WebSocket close (non-1000, live) | ErrorCode.PROVIDER_ERROR |
ERROR + END | Live session unclean close |
On any error, if duplex._outputTokenCount > 0, the adapter emits a final progressive RECEIPT for tokens accumulated so far, updates totalSpent, then emits ERROR + END.
createGeminiLiveSession(params, duplex, requestConfig)
Creates a Gemini Live API WebSocket session for bidirectional audio+text streaming.
| Parameter | Type | Description |
|---|---|---|
params |
object |
Model params — model, system/instructions, voice, temperature, tools, modalities, input_audio_format |
duplex |
IFrameDuplex |
The adapter duplex to emit frames through |
requestConfig |
object |
{ apiKey, baseURL } — API key and optional base URL |
Returns: function handleFrame(frame) — call this with incoming frames (TEXT, AUDIO, PARAM, END).
Frame routing within live session:
| Frame type | Action |
|---|---|
AUDIO |
Sends as realtimeInput JSON message with base64-encoded PCM chunks. If not yet connected, buffers in pendingAudio. |
TEXT |
Sends as clientContent JSON message with user turn. If not yet connected, buffers as function in pendingFrames. |
PARAM (voice, instructions, system, modalities) |
Updates params for next session setup |
END |
Calls close() — destroys WebSocket, marks sessionEnded = true |
Server events → outbound frames:
| Server event | Outbound frame |
|---|---|
serverContent.modelTurn.parts[].text |
TEXT |
serverContent.modelTurn.parts[].inlineData |
AUDIO (buffer from base64) |
serverContent.outputTranscription.text |
TEXT with metadata.source === 'output_transcription' |
toolCall.functionCalls[] |
TOOL_CALL for each function call |
serverContent.turnComplete |
RECEIPT + END (with turnComplete: true) |
error |
ERROR (code: ErrorCode.PROVIDER_ERROR) + END (with error: true) |
| WebSocket close (non-1000) | ERROR + END |
| WebSocket error | ERROR + END |
isLiveModel(model)
Returns true if the model name contains 'live', 'native-audio', or 'realtime'.
getModelCost(model)
Returns the cost config object { mtoksInput, mtoksOutput } for a Gemini model, or null if unknown. Does case-insensitive key match and substring match on known models.
calculateTokenCost(model, inputTokens, outputTokens)
Returns { inputCost, outputCost, totalCost } in dollars. Uses per-million-token rates from GEMINI_COSTS. Returns zeros for unknown models.
GEMINI_COSTS
Static cost table mapping model names to { mtoksInput, mtoksOutput }. Includes: gemini-2.5-flash (0.15/0.60), gemini-2.5-flash-lite (0.075/0.30), gemini-2.5-pro (1.25/10.00), gemini-2.0-flash (0.10/0.40), gemini-2.0-flash-lite (0.075/0.30), gemini-2.0-pro (1.25/5.00), gemini-1.5-flash (0.075/0.30), gemini-1.5-pro (1.25/5.00), gemini-1.5-flash-8b (0.0375/0.15), text-embedding-004 (0.02/0), gemini-embedding (0.02/0), aqa (0.075/0.30).
Tests
npm test
Runs node --test test/rigor.test.js test/smoke.test.js test/live.test.js. Covers: adapter construction, PARAM accumulation, frame buffering before API key set, budget enforcement, COST_CHECK flow, calculateTokenCost monotonicity for all known models, getModelCost lookup accuracy, live session creation for live-model routing, and topology scan/response.
Requirements
Node.js 22 or later. ESM only.
Peer dependencies: @polyweave/core, @polyweave/costs, @polyweave/errors, @polyweave/push-websocket.
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @rigor/core | * |
Peer Dependencies
| ID | Version |
|---|---|
| @polyweave/core | * |
| @polyweave/costs | * |
| @polyweave/errors | * |
| @polyweave/push-websocket | * |