@polyweave/openai (1.0.10)
Installation
@polyweave:registry=https://hub.kl1.tenere.ai/api/packages/Polyweave/npm/npm install @polyweave/openai@1.0.10"@polyweave/openai": "1.0.10"About this package
@polyweave/openai
OpenAI adapter for Polyweave — wraps the official OpenAI SDK to provide a Frame-duplex stream interface. Supports chat completions (text, vision, audio I/O, tools, streaming, reasoning), TTS, transcription, translation, image generation/editing, embeddings, and realtime API.
Synopsis
import { createOpenAIAdapter } from '@polyweave/openai';
import { createTextFrame, createParamFrame } from '@polyweave/core';
const adapter = createOpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY });
// Chat with streaming and tools
adapter.sink.write(createParamFrame('model', 'gpt-4o'));
adapter.sink.write(createParamFrame('tools', [{ type: 'function', function: { name: 'get_weather', parameters: { type: 'object', properties: { city: { type: 'string' } } } } }]));
adapter.sink.write(createTextFrame("What's the weather in Tokyo?"));
// Image generation
adapter.sink.write(createParamFrame('task', 'generation'));
adapter.sink.write({ type: 'IMAGE', content: { prompt: 'A sunset over a cyberpunk city', size: '1024x1024' } });
// TTS
adapter.sink.write(createParamFrame('task', 'tts'));
adapter.sink.write(createParamFrame('voice', 'alloy'));
adapter.sink.write({ type: 'AUDIO', content: { text: 'Hello, world!' } });
// Speech transcription
adapter.sink.write(createParamFrame('task', 'transcription'));
adapter.sink.write({ type: 'AUDIO', content: { data: audioBuffer, mimeType: 'audio/mp3' } });
// Embeddings
adapter.sink.write(createParamFrame('task', 'embeddings'));
adapter.sink.write({ type: 'EMBEDDING', content: { text: 'Text to embed' } });
// Cost estimation (no API call)
adapter.sink.write({ type: 'COST_CHECK', content: { maxBudget: 0.05 } });
adapter.sink.write(createTextFrame('Estimate this'));
// Realtime API (model is auto-detected)
adapter.sink.write(createParamFrame('model', 'gpt-realtime'));
adapter.sink.write({ type: 'TEXT', content: { text: 'Hello' } });
// Runtime configuration via PARAM
adapter.sink.write(createParamFrame('apiKey', 'new-key'));
adapter.sink.write(createParamFrame('organization', 'org-xxx'));
adapter.sink.write(createParamFrame('baseURL', 'https://custom-proxy.com/v1'));
Install
npm install @polyweave/openai
Requires openai (>= 6.15.0) package.
Why
OpenAI's ecosystem spans chat, images, audio (TTS + transcription + translation), embeddings, and realtime — each with distinct SDK client patterns. Polyweave harnesses need a unified Frame-duplex interface that wraps the official SDK so all capabilities work through the same PARAM-accumulation, COST_CHECK, topology-scan, and test-injection protocol. This adapter maps OpenAI SDK errors to structured Polyweave error codes, extracts rate-limit headers into normalized metadata, and supports progressive receipts during SSE streaming for real-time budget tracking.
API
createOpenAIAdapter(config)
Creates an IFrameDuplex stream adapter wrapping the OpenAI SDK.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
config.apiKey |
string |
No | process.env.OPENAI_API_KEY |
OpenAI API key |
config.organization |
string |
No | — | OpenAI organization ID (sent as OpenAI-Organization header) |
config.project |
string |
No | — | OpenAI project ID (sent as OpenAI-Project header) |
config.baseURL |
string |
No | 'https://api.openai.com' |
Custom base URL for proxies or compatible APIs |
config.timeout |
number |
No | 60000 |
Request timeout in milliseconds |
config.maxRetries |
number |
No | 2 |
Max retry count for SDK |
config.httpAgent |
object |
No | — | Custom HTTP agent |
config.fetch |
function |
No | — | Custom fetch implementation |
config.dangerouslyAllowBrowser |
boolean |
No | false |
Allow SDK usage in browser (should stay false for Node.js) |
Return value: IFrameDuplex with .source, .sink, .setReady, .resetConversation, ._conversation, .name.
Default Parameters
The adapter initializes with these defaults (all overridable via PARAM frames):
| Parameter | Default | Category |
|---|---|---|
model |
'gpt-3.5-turbo' |
Chat |
stream |
true |
Chat |
voice |
'alloy' |
Audio |
n |
1 |
Images |
receipt_mode |
'final' |
Cost |
receipt_interval |
50 |
Cost |
Frame Protocol
Input frames:
| Frame Type | Handler | Purpose |
|---|---|---|
TEXT |
handleChatCompletion |
Chat prompt (also vision, audio I/O, tools, reasoning) |
TEXT (task=embeddings) |
handleEmbeddings |
Embedding request |
AUDIO |
handleAudio |
TTS, transcription, translation, diarization, custom voice |
IMAGE |
handleImages |
Image generation, editing, variations |
EMBEDDING |
handleEmbeddings |
Direct embedding request |
TOOL_RESULT |
— | Tool call result (accumulated into conversation) |
PARAM |
— | Configuration accumulation |
COST_CHECK |
— | Enable dry-run cost estimation mode |
ESTIMATED_TEXT |
— | Virtual text for cost estimation |
ESTIMATED_IMAGE |
— | Virtual image for cost estimation ({ prompt, width, height, quality }) |
ESTIMATED_AUDIO |
— | Virtual audio for cost estimation |
ESTIMATED_VIDEO |
— | Virtual video for cost estimation ({ duration, size }) |
ESTIMATED_OUTPUT |
— | Estimated output metadata buffer (not a trigger) |
START / END |
— | Request scope markers (pass-through) |
TOPOLOGY_SCAN |
— | Triggers topology report |
TEST_INJECT |
— | Re-injects child frames |
TEST_SNAPSHOT |
— | Returns snapshot |
Output frames:
| Frame Type | Emitted when |
|---|---|
TEXT |
Chat response chunks/full, embedding results (with embedding field) |
TOOL_CALL |
Function calling responses |
AUDIO |
TTS audio buffer |
IMAGE |
Generated/edited image URLs |
EMBEDDING |
Embedding vectors |
RECEIPT |
Cost receipt (final or progressive) |
COST |
Cost check estimate |
ERROR |
Error conditions |
END |
End of response |
META |
Unknown frame type pass-through |
PARAM Keys (complete)
Chat: model, temperature, max_tokens, stream, messages, system, tools, tool_choice, response_format, logprobs, top_logprobs
Audio: voice (default 'alloy'), speed, language
Images: size, quality, style, n
Embeddings: dimensions, encoding_format
Task routing: task — 'tts'/'speech', 'transcription', 'translation', 'image'/'generation'/'edit'/'variation', 'embeddings'/'embedding', default: chat
Cost/receipt: receipt_mode ('none' | 'final' | 'progressive'), receipt_interval (tokens)
Auth: apiKey, organization, baseURL — special-cased to update client config
Task Routing
| task | Frame Type | Handler |
|---|---|---|
| (default) | TEXT |
handleChatCompletion |
embeddings / embedding |
TEXT |
handleEmbeddings |
tts / speech |
AUDIO |
handleAudio (TTS branch) |
transcription |
AUDIO |
handleAudio (transcription branch) |
translation |
AUDIO |
handleAudio (translation branch) |
| (default) | AUDIO |
handleAudio (auto-detect) |
generation / edit / variation |
IMAGE |
handleImages |
| (default) | IMAGE |
handleImages (auto-detect) |
| — | EMBEDDING |
handleEmbeddings |
Realtime API
When params.model is a realtime model (detected by isRealtimeModel()), TEXT and AUDIO frames are routed through a WebSocket session via createRealtimeSession. Sessions are per-streamId and maintained in duplex._realtimeSessions Map. This is transparent to the caller — just set model to a realtime model and send frames as normal.
Costs (inline)
The adapter maintains pricing data in src/costs.js for token-based (mtoks), per-image, per-minute (whisper), per-million (TTS), and per-second (Sora) pricing. Exported functions:
getModelCost(model)→ cost object ornullcalculateTokenCost(model, inputTokens, outputTokens)→{ inputCost, outputCost, totalCost }
Cost configs use:
mtoksInput/mtoksOutput— USD per million tokensperImage— USD per imageperMinute— USD per minute (whisper)perMillion— USD per million characters (TTS)perSecond— USD per second (Sora)
Cost Estimation Protocol
- Send
COST_CHECKframe → enters dry-run mode, clears buffer - Send content frames (including
ESTIMATED_*variants) ESTIMATED_OUTPUTbuffers metadata but doesn't trigger estimation- Any other content frame triggers cost estimation via
@polyweave/costs - If
maxBudgetis set and exceeded → emitsERROR(BUDGET_EXCEEDED) +END - Otherwise → emits
COSTframe with estimate details +END - Exits cost check mode
Request type is determined by determineRequestType():
EMBEDDINGframe →'embedding'TEXTwith task=embeddings→'embedding'IMAGE/ESTIMATED_IMAGE→'image'AUDIO/ESTIMATED_AUDIOwith task=tts→'tts', with task=transcription→'transcription', default →'audio'ESTIMATED_VIDEO→'video'- Default →
'chat'
Estimation frames (ESTIMATED_TEXT, ESTIMATED_IMAGE, ESTIMATED_AUDIO, ESTIMATED_VIDEO, ESTIMATED_OUTPUT) are ignored when not in cost check mode.
Error Conditions
All errors are emitted as ERROR frames followed by END with metadata: { error: true, errorCode }. Errors from handleChatCompletion and other handlers pass through a done callback; uncaught exceptions in duplex.sink.write are caught in a try/catch.
| Condition | Error Code | When |
|---|---|---|
OpenAI.AuthenticationError |
AUTHENTICATION_ERROR |
Invalid API key |
OpenAI.PermissionDeniedError |
AUTHORIZATION_ERROR |
Insufficient permissions |
OpenAI.RateLimitError |
RATE_LIMIT_EXCEEDED |
Rate limit hit |
OpenAI.APIConnectionError |
PROTOCOL_ERROR |
Network connection failure |
OpenAI.APIConnectionTimeoutError |
QUEUE_TIMEOUT |
Request timeout |
OpenAI.NotFoundError |
JOB_NOT_FOUND |
Resource not found |
OpenAI.UnprocessableEntityError |
INVALID_INPUT |
Invalid request parameters |
OpenAI.InternalServerError |
PROVIDER_ERROR |
OpenAI internal error |
OpenAI.APIError (status 400) |
INVALID_INPUT |
Bad request |
OpenAI.APIError (status 401) |
AUTHENTICATION_ERROR |
Unauthorized |
OpenAI.APIError (status 403) |
AUTHORIZATION_ERROR |
Forbidden |
OpenAI.APIError (status 404) |
JOB_NOT_FOUND |
Not found |
OpenAI.APIError (status 408) |
QUEUE_TIMEOUT |
Request timeout |
OpenAI.APIError (status 429) |
RATE_LIMIT_EXCEEDED |
Rate limited |
OpenAI.APIError (status 500) |
PROVIDER_ERROR |
Server error |
OpenAI.APIError (status 503) |
SERVICE_UNAVAILABLE |
Service unavailable |
OpenAI.APIError (other) |
PROVIDER_ERROR |
Other API error |
| Unknown error type | ADAPTER_ERROR |
Non-OpenAI error |
| Budget exceeded (COST_CHECK) | BUDGET_EXCEEDED |
Estimate > maxBudget |
Error Mapper Details (src/utils/errorMapper.js)
mapOpenAIErrorCode(error) // OpenAI.Error → ErrorCode string
mapOpenAIError(error, originalFrame) // OpenAI.Error → ERROR frame
isRetryableError(error) // boolean — true for 5xx, 429, 408, connection errors
The mapOpenAIError function extracts:
- Rate limit headers from
error.headers:x-ratelimit-limit-requests,x-ratelimit-limit-tokens,x-ratelimit-remaining-requests,x-ratelimit-remaining-tokens,x-ratelimit-reset-requests,x-ratelimit-reset-tokens - These are normalized via
normalizeRateLimitfrom@polyweave/errors - Provider error details stored in metadata as
providerError
Retryable errors: InternalServerError, RateLimitError, APIConnectionError, APIConnectionTimeoutError, and any APIError with status >= 500, 429, or 408.
Conversation Management
- Incoming TEXT frames push
{ role: 'user', content: frame.content.text }to internal conversation array - TOOL_RESULT frames push
{ role: 'tool', tool_call_id, content }(serialized to string if not string) duplex.resetConversation()clears the arrayduplex._conversation()returns a snapshot
Re-exports
The adapter re-exports getModelCost and calculateTokenCost from src/costs.js (not from @polyweave/costs — OpenAI maintains its own pricing data).
Tests
npm test
Runs node --test test/openai-error-mapper.test.js test/openai.test.js test/rigor.test.js. Covers:
createOpenAIAdapterconstruction and default configuration- Cost check protocol (COST_CHECK entry/exit, budget exceeded, estimate emission)
- PARAM accumulation (model, temperature, max_tokens, stream)
isEstimationFrameclassificationdetermineRequestTyperouting for all frame typesmapOpenAIErrorCodeerror mapping for all OpenAI error typesisRetryableErrorclassificationgetModelCostpricing table correctnesscalculateTokenCostmonotonicity and arithmetic- Adapter topology report structure
- Budget exceeded in COST_CHECK mode with all ESTIMATED_* frame types
Requirements
- Node.js >= 22.0.0
- ESM only (
"type": "module") openai>= 6.15.0 (runtime dependency)- Peer dependencies:
@polyweave/core,@polyweave/errors,@polyweave/costs,@polyweave/push-websocket
Dependencies
Dependencies
| ID | Version |
|---|---|
| openai | ^6.15.0 |
Development Dependencies
| ID | Version |
|---|---|
| @rigor/core | * |
Peer Dependencies
| ID | Version |
|---|---|
| @polyweave/core | * |
| @polyweave/costs | * |
| @polyweave/errors | * |
| @polyweave/push-websocket | * |