openai_dart 11.0.0
openai_dart: ^11.0.0 copied to clipboard
Dart client for the OpenAI REST APIs and realtime WebSocket/WebRTC workflows with type-safe access to GPT, image, audio, and Responses APIs.
OpenAI Dart Client #
Dart client for the OpenAI API with Responses API, Decisions API, Chat Completions, images, videos, audio, custom tools, embeddings, evals, realtime, signed webhooks, safety detail retrieval, and more. It gives Dart and Flutter applications a pure Dart, type-safe client across iOS, Android, macOS, Windows, Linux, Web, and server-side Dart.
Tip
Coding agents: start with llms.txt. It links to the package docs, examples, and optional references in a compact format.
Table of Contents
Features #
Generation and streaming #
- Responses API with streaming, multi-turn conversations, structured output, background mode, cache prewarming, and typed cache diagnostics
- Decisions API for typed predicate, choice, and score answers from text or data URL/HTTP(S) images
- Chat Completions with tool calling, vision, structured output, detailed usage, audio completion/streaming, and obfuscation controls
- Images, videos, audio (TTS, transcription, translation), and embeddings
- Realtime API via WebSocket and WebRTC with audio streaming
- Live HTTP signaling, primary/sideband WebSockets, typed events and explicit call controls
- Input token counting via
inputTokensfor cost estimation
Tools #
- Web search, file search, code interpreter, computer use, and custom tools
- Async function/custom tools with faithful call replay and conversation metadata
Operational APIs #
- Saved agent CRUD with persisted function, MCP, web search and computer-use configuration through
client.agents - Durable sessions with JSON/SSE creation, persistent raw event observation, manual function/approval input and explicit cancellation through
client.agents.sessions - Root session/turn history and currently published OTLP trace pages through session items, turns and traces resources
- Vault CRUD and write-only credential creation/rotation with safe returned authentication metadata
- Owned hosted environment prewarming and reusable environment template CRUD with safe metadata views
- Files, uploads, batches, fine-tuning, moderations, evals, and model management
- Conversations, containers, content provenance checks, ChatKit, and skills
- Local signed webhook verification with 26 typed received event variants, plus project endpoint management/discovery
- Read-only project safety alerts and organization cases through
client.safety - Shared typed monitoring details on HTTP/failed Responses/WebSocket errors, with canonical flat SSE errors
- Assistants and vector stores (deprecated — use Responses API instead)
See API Coverage for the full coverage table.
Why choose this client? #
- Pure Dart with no Flutter dependency — works in mobile apps, backends, and CLIs.
- Type-safe request and response models with minimal dependencies (
http,crypto,logging,meta). - Streaming, retries, interceptors, and error handling built into the client.
- Supports OpenAI generation, media, operational, and Realtime APIs; see the coverage table for remaining gaps.
- Resource-based API design matching official SDKs.
- Strict semver versioning so downstream packages can depend on stable, predictable version ranges.
Quickstart #
Requires Dart 3.12 or later. See the Migration Guide for upgrade instructions.
dependencies:
openai_dart: ^11.0.0
import 'package:openai_dart/openai_dart.dart';
Future<void> main() async {
final client = OpenAIClient.fromEnvironment();
try {
final response = await client.responses.create(
CreateResponseRequest(
model: 'gpt-5.5',
input: ResponseInput.text('What is the capital of France?'),
),
);
print(response.outputText);
} finally {
client.close();
}
}
Import structure #
The package provides multiple entry points for different APIs:
// Main entry point (recommended) — includes Chat Completions, Responses API,
// Embeddings, Images, Videos, Audio, Files, Batches, Fine-tuning, and more.
import 'package:openai_dart/openai_dart.dart';
// Assistants API (deprecated — use Responses API instead)
import 'package:openai_dart/openai_dart_assistants.dart' as assistants;
// Realtime API — WebSocket and WebRTC sessions
import 'package:openai_dart/openai_dart_realtime.dart' as realtime;
Configuration #
The HTTP/2 transport evaluation recommends deferring http2 3.1.0 adoption: local multiplexing saves connections, but cancellation, buffering and repeated SSE compatibility prevent a general recommendation. The existing default transport and client injection API remain unchanged.
Configure auth, retries, and custom endpoints
Use OpenAIClient.fromEnvironment() for the default OPENAI_API_KEY workflow. Switch to OpenAIConfig when you need a proxy, custom timeout, or a non-default retry policy.
import 'package:openai_dart/openai_dart.dart';
final client = OpenAIClient(
config: OpenAIConfig(
authProvider: ApiKeyProvider('YOUR_API_KEY'),
baseUrl: 'https://api.openai.com/v1', // Default
timeout: Duration(minutes: 10),
connectTimeout: Duration(seconds: 30),
retryPolicy: RetryPolicy(maxRetries: 3),
organization: 'org-xxx', // Optional
project: 'proj-xxx', // Optional
),
);
From environment variables:
final client = OpenAIClient.fromEnvironment();
// Reads OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_ORG_ID, OPENAI_PROJECT_ID
// Also reads optional OPENAI_WEBHOOK_SECRET for local verification.
With API key directly:
final client = OpenAIClient.withApiKey('sk-...');
Custom base URL (for proxies or Azure):
final client = OpenAIClient(
config: OpenAIConfig(
baseUrl: 'https://my-resource.openai.azure.com/openai/deployments/my-deployment',
authProvider: AzureApiKeyProvider('YOUR_AZURE_KEY'),
),
);
Usage #
How do I save and update an agent configuration? #
Use client.agents to create, list, retrieve, update and delete reusable agent
configuration. These five operations use your ordinary API key and project context,
and force OpenAI-Beta: agents=v1 after caller headers. Creating an agent stores
configuration; it does not start a session, execute tools or run a model.
final agent = await client.agents.create(
CreateAgentRequest(
model: 'gpt-6-astra',
name: 'Research assistant',
reasoning: AgentReasoningConfig(summary: AgentReasoningSummaryParam.auto),
serviceTier: AgentServiceTierParam.fast,
tools: [
AgentTool.function(
name: 'lookup',
description: 'Look up an application-provided reference.',
parameters: {
'type': 'object',
'properties': {'query': {'type': 'string'}},
},
),
],
),
);
final page = await client.agents.list(limit: 10, order: AgentListOrder.asc);
if (page.hasMore && page.lastId != null) {
await client.agents.list(
limit: 10,
order: AgentListOrder.asc,
after: page.lastId,
);
}
final saved = await client.agents.retrieve(agent.id);
final reset = await client.agents.update(
saved.id,
UpdateAgentRequest(clearName: true, clearReasoning: true, metadata: {}),
);
assert(reset.name == null);
final deletion = await client.agents.delete(saved.id);
assert(deletion.deleted);
Update omission keeps a field unchanged. Supplied objects, lists and maps replace
the whole field. For example, reasoning: AgentReasoningConfig(effort: ...) replaces
the saved reasoning configuration, including its summary. clearReasoning: true
sends explicit JSON null to reset it; clearName, clearTools and clearMetadata
clear those fields. Copies distinguish omission from explicit null, and clearX
wins over a simultaneous value. An absent create field leaves the service default
in control. The requested model string is retained without normalization.
Saved-agent tools have separate request and received types. AgentTool supports
function, tool search, programmatic tool calling, MCP, web search and computer use.
Persisted MCP HTTP/stdio configuration is credential-free; an optional credential
ID refers to a vault credential rather than embedding a secret. AgentTextConfig
supports plain text or JSON Schema output, and AgentMultiAgentConfig configures
subagent availability without running subagents. Future received tool/transport
variants and enum strings are retained; unknown request variants are rejected.
Request limits are checked in release builds: names use at most 128 Unicode
characters, metadata at most 16 pairs with 64-character keys and 512-character
values, and tools at most 2,000 entries / 3 MiB of compact UTF-8 JSON. Returned
resources follow their separate limits. Page cursors may be null on empty pages;
keep the same order and limit when using lastId as after. Opaque path IDs are
encoded as one segment; empty and exact dot segments are rejected as local URI
safeguards. Configuration and HTTP diagnostics redact private payloads, while
explicit properties and toJson() remain available to the caller.
Injected HTTP clients remain caller-owned. Durable sessions, root/turn history
and published traces are available through client.agents.sessions; Vaults and
write-only credentials use client.vaults. Owned environments and templates use
client.agents.environments and .templates; live files use .files and
published artifacts use client.agents.sessions.artifacts. Subagent state and child item/turn history use client.agents.sessions.subagents. The runnable example exercises all six saved tool types,
pagination, replacement, clear/reset and deletion with seven mock requests and
explicit transport cleanup, without an API key or paid API call.
Durable agent sessions #
client.agents.sessions creates sessions as HTTP 201 JSON or, through
createStream, HTTP 201 SSE. It also lists, retrieves, updates and deletes sessions.
events.stream observes HTTP 200 SSE, and events.create submits inputs with
HTTP 202 empty acceptance. The caller handles function results and browser approvals
explicitly; the client does not run callbacks or provision a self-hosted executor.
final session = await client.agents.sessions.create(CreateAgentSessionRequest(
agent: AgentSessionAgentConfig(model: 'requested-model'),
environment: AgentSessionEnvironment.none(),
input: AgentSessionInitialInput.text('Hello'),
));
final observer = client.agents.sessions.events.stream(session.id).listen((event) {
// Handle typed AgentSessionRequiresActionEvent and other raw events here.
});
await client.agents.sessions.events.create(session.id,
CreateAgentSessionEventsRequest(events: [AgentSessionInput.cancel()]),
);
await observer.cancel();
Subscribe before submitting new work. Observation stays open through idle, required actions, turn completion and typed error events. Cancelling an observer or reaching HTTP EOF releases observation only; neither proves completion nor cancels or deletes durable work. Cancel input has no target turn ID. Retrieve or observe cancellation before the separate DELETE action. GET observation has no replay cursor and does not reconnect automatically.
Inline agent configuration needs a model if no saved agentId is supplied.
Environment none needs initial input; streamed hosted creation also needs input.
Self-hosted creation may wait for executor connectivity. Saved agents, Vaults and
prewarmed environments are optional. An existing hosted environmentId excludes
all inline/template/container fields, including explicitly null desktop settings.
Supplied saved-agent overrides replace configuration fields.
Spending controls use whole USD cents up to 4503599627370495. Create omission/null
is unlimited; update omission retains the cap, while clearSpendControl: true or
AgentSessionSpendControlConfig(limit: null) removes it without resetting spend.
This session cap is separate from organization usage tiers and rate limits.
Follow-up inputs and initial input/output schemas have a 4 MiB compact UTF-8 JSON budget; service metadata adds overhead. Browser authentication values have a 120 KiB JSON budget and are never automatically retried, even by configured interceptors. After uncertain delivery, retrieve current required actions before deciding whether to resubmit. No authentication values are saved to local history. Default logging and diagnostics redact private session URLs, headers, tokens, commands and archives. Use idempotency keys of 1–256 Unicode characters for logical event submissions; this endpoint makes no 24-hour retention promise.
→ Offline durable-session example — inline creation, persistent observation, manual function result, cap changes and explicit cancellation followed by deletion; no live calls or API key ($0).
Session history and traces #
Read known session IDs without creating a session or resuming observation:
final sessions = client.agents.sessions;
final items = await sessions.items.list('session_id', limit: 20,
order: AgentListOrder.asc);
final turns = await sessions.turns.list('session_id', order: AgentListOrder.asc);
final turn = await sessions.turns.retrieve('session_id', 'turn_id');
final turnItems = await sessions.turns.items.list('session_id', 'turn_id');
final traces = await sessions.traces.list('session_id', limit: 1,
order: AgentListOrder.asc);
All four list methods accept limit (1–100), order and exclusive after IDs.
Use lastId with the same order/context for the next page; empty boundaries remain
null. Root history includes coordinator interactions with children; each child has
separate history. The 17 typed history branches include safe browser authentication
records without submitted form-value fields. Historical function/approval output
is inspection state and does not authorize replaying an action. Required-nullable
turn usage, errors and timestamps remain present; a terminal turn is distinct from
session idle or observer EOF, and recorded usage is not a final spending ledger.
traces.list returns finite, deeply owned arbitrary OTLP maps with private default
diagnostics. Trace IDs are root-turn pagination anchors. Pages contain only data
published when read, skip unpublished traces and never await late updates. A page
sequence therefore does not promise a complete historical export. The service limits
trace reads and JSON responses to 16 MiB per request; request fewer traces when it
reports that limit. API exceptions retain status and explicit caller-readable HTTP
context while default logging and messages redact private payloads.
Trace export must be enabled for the organization. The key must belong to the
session's project and have api.traces.read or api.agents.read permission.
→ Offline session-history example — known-ID root/turn inspection and two trace pages, with no live calls or key ($0).
Agent environments and templates #
Reuse hosted configuration and prewarm an owned environment through
client.agents.environments:
final templates = client.agents.environments.templates;
final template = await templates.create(CreateAgentEnvironmentTemplateRequest(
name: 'Reusable setup',
network: AgentSessionNetworkPolicyConfig(
access: AgentSessionNetworkAccessConfig.disabled),
));
await templates.retrieve(template.id);
await templates.list(limit: 20, order: AgentListOrder.desc);
await templates.update(template.id, UpdateAgentEnvironmentTemplateRequest(
clearNetwork: true, clearDesktop: true, name: 'Updated setup'));
final environment = await client.agents.environments.create(
CreateAgentEnvironmentRequest(environment: AgentPrewarmEnvironment.openaiHosted(
environmentTemplateId: template.id)),
idempotencyKey: 'my-application-prewarm-001',
);
await client.agents.environments.list(type: AgentEnvironmentType.openaiHosted);
await client.agents.environments.retrieve(environment.id);
await templates.delete(template.id);
Prewarming is beta and requires account eligibility. Its hosted-only configuration
has no environmentId or containerSize, and accepts at most ten vault IDs.
Sessions have a separate environment attachment contract. Shared hosted configuration
supports packages, desktop, network, variables, setup commands, capability directories,
file-ID or plain-base64 inline files, skill references or inline ZIP skills, and inline
ZIP plugins. These reuse the corresponding AgentSession...Config types because the
canonical contracts are identical. File destinations stay within /workspace;
capability and setup directories are absolute. Inline files allow 5 MiB decoded
bytes per file and 10 MiB total per submitted configuration. Template-inherited
combined limits and Files API sizes are checked by the service. The API validates
archive contents.
Templates apply before inline settings; network overrides cannot broaden their
policy. Omitted network defaults belong to the API version. On template update,
omission preserves a field, clearNetwork: true resets network policy, and
clearDesktop: true disables desktop. Maps and arrays replace complete fields.
Package installation and setup run before the runtime network policy takes effect.
Returned environments expose ID, hosting type, status and installed safe file/skill/plugin metadata; templates also expose timestamps and safe configuration metadata. They never recover confidential setup command bodies, environment values or inline contents from request caches. Template file references and skill version selectors remain distinct from installed environment metadata. Received future values retain their wire strings with private diagnostics; malformed known resource values fail privately.
Environment creation keys have 1–256 Unicode characters. The service deduplicates for 24 hours within the authenticated organization/project/creator. Retry the same JSON and key for current state; mismatched parameters or incomplete creation return HTTP 409. Retained deleted keys do not recreate environments. After retention expires the key may create a fresh environment; without a key each call creates anew. The client carries the header and surfaces service responses, with no local deduplication cache.
All seven statuses—pending, ready, connected, disconnected, suspended, expired and failed—are represented. Session, environment, provider and artifact lifetimes are independent. Status names do not imply environment suspend/resume/reset/delete methods; template deletion does not promise cleanup of sessions, environments or artifacts.
→ Offline environments example — all eight operations with mock HTTP, no API key or paid prewarming ($0).
Agent environment files and published artifacts #
Live workspace files require a connected hosted environment. Copy an existing Files API file ID or standard-base64 bytes into a hosted path:
final files = client.agents.environments.files;
await files.create(environmentId, AgentSessionHostedEnvironmentFileConfig.fileId(
fileId: existingFileId, path: '/workspace/input.bin'));
await files.create(environmentId, AgentSessionHostedEnvironmentFileConfig.inline(
data: base64Encode(inputBytes), path: '/workspace/another.bin'));
final page = await files.list(environmentId, path: '/workspace', limit: 20,
order: AgentListOrder.asc);
if (page.next != null) {
await files.list(environmentId, path: '/workspace', limit: 20,
order: AgentListOrder.asc, page: page.next);
}
final artifacts = client.agents.sessions.artifacts;
final published = await artifacts.list(sessionId, environmentId: environmentId);
final artifact = await artifacts.retrieve(sessionId, published.data.first.id);
final bytes = await artifacts.download(sessionId, artifact.id);
await for (final chunk in artifacts.downloadStream(sessionId, artifact.id)) {
// Consume exact binary chunks in the application.
}
await artifacts.delete(sessionId, artifact.id);
Import dart:convert for base64Encode. Inline files accept standard base64,
including empty data, and at most 5 MiB decoded per file; this field does not
accept a data URL. File destinations are absolute POSIX paths under /workspace.
The service checks existing destinations, symlinks, component lengths and the
50 MiB limit for Files API references. The client does not read a local path or
upload a local file automatically. After an uncertain copy, check the destination
before retrying; copying is a side effect and has no local deduplication cache.
Live pages use object: page, opaque next/page tokens and required nullable
next. Keep directory, limit and order consistent between pages. Artifact pages
instead use firstId/lastId, and advance with after: page.lastId, retaining
order and environment filter. Null query arguments are omitted. Empty pages keep
null boundary values. File metadata has environment/path/size; artifact metadata
also has immutable ID, session and completed-turn ownership, and publication time.
Outputs under /workspace/outputs are published as immutable artifacts when a
hosted turn completes. Published copies survive environment expiry; unpublished
outputs are not guaranteed to survive cancellation or session deletion. Download
anything to retain before deleting the session. Self-hosted files use the
provider's filesystem API. Service limits are 200 MiB per artifact and 500 MiB
per co-publication; metadata sizes have no client-imposed upper limit.
Downloads preserve arbitrary bytes, including NUL and non-UTF-8 data, with no
JSON/text decoding. download buffers in memory; downloadStream is incremental
and uses the existing private byte transport without replay or interceptors.
Both force Agents beta and octet-stream Accept, preserve auth/project context,
and apply header/active-read timeouts. Pause suspends the body idle deadline.
Abort or subscription cancellation releases this local download only; it does
not delete an artifact or cancel a turn. Injected HTTP clients remain borrowed;
a distinct streamClientFactory client is operation-owned. Hosted paths are never
chosen as local destinations. Deleting an artifact leaves the live file intact.
→ Offline files and artifacts example — stages input, pages live files and downloads exact bytes after mocked expiry, with all six endpoints, no API key or paid execution ($0).
Agent session subagents #
Inspect service-owned children separately from the root coordinator's history:
final subagents = client.agents.sessions.subagents;
final children = await subagents.list(sessionId, limit: 20);
final child = await subagents.retrieve(sessionId, children.data.first.id);
final rootItems = await client.agents.sessions.items.list(sessionId);
final childItems = await subagents.items.list(sessionId, child.id,
limit: 20, order: AgentListOrder.asc);
if (childItems.lastId != null) {
await subagents.items.list(sessionId, child.id,
limit: 20, order: AgentListOrder.asc, after: childItems.lastId);
}
final turns = await subagents.turns.list(sessionId, child.id,
limit: 20, order: AgentListOrder.asc);
final turn = await subagents.turns.retrieve(sessionId, child.id, turns.data.first.id);
final turnItems = await subagents.turns.items.list(sessionId, child.id, turn.id);
All six methods are inspection GETs, using ordinary auth/project/HTTP policy and
forced OpenAI-Beta: agents=v1. Known IDs are independently callable: no new
coordinator turn or local worker process is required. The child list has a typed
AgentSessionSubagentList envelope for the actual inline response. Every list
uses exclusive ID after pagination with limit 1–100, order and required nullable
first/last boundaries. Keep the same order and parent IDs between pages; null
query arguments are omitted. Opaque IDs are encoded separately at every level;
empty and exact dot segments are rejected as local URI safeguards.
Child name, instructions and closedAt are required nullable received fields.
Instructions preserve output-text/encrypted-content branches; text may include
service-provided placeholders for image/audio previews. No missing content is
reconstructed. A closed child may later resume: its original openedAt remains
unchanged and closedAt becomes null. These are service-owned state transitions;
a GET does not trigger them. Child turns retain their own IDs, nullable
start/completion/error/usage, and canonical creation-time ordering.
Root coordinator interactions and child history are separate collections. Child
history reuses all 17 canonical item branches and the complete shared turn shape,
including browser-safe history and finite private future values. Reading a past
tool, approval or subagent-call item grants no permission to replay it. This
resource has no child create, resume, interrupt or close HTTP action, and does
not start independent workers/providers. Private instructions, content and
metadata stay out of default diagnostics; explicit fields and toJson() remain
caller-readable. Borrowed transport ownership and local request abort behavior
follow the ordinary client policy.
→ Offline subagent inspection example — all six inspection routes, root versus child history, separate turn items and received closed/resumed state using nine mock GETs, with no key or paid execution ($0).
Vaults and write-only credentials #
Manage Vaults independently of session creation through client.vaults:
final vault = await client.vaults.create(CreateVaultRequest(name: 'Example Vault'));
final credentials = client.vaults.credentials;
final credential = await credentials.create(vault.id,
CreateVaultCredentialRequest(name: 'Hosted API credential',
auth: CreateVaultEnvironmentVariableAuth(secretName: 'SERVICE_API_KEY',
secretValue: 'application-supplied-secret',
networking: VaultCredentialNetworking.limited(
allowedHosts: ['api.example.com']))));
await client.vaults.retrieve(vault.id);
final vaultPage = await client.vaults.list(metadata: {'application': 'demo'},
status: VaultStatusFilter.single(VaultStatus.active));
await client.vaults.update(vault.id, UpdateVaultRequest(metadata: {}));
final safe = await credentials.retrieve(vault.id, credential.id);
final credentialPage = await credentials.list(vault.id, limit: 20);
await credentials.rotate(vault.id, credential.id,
RotateVaultCredentialRequest(auth: RotateVaultEnvironmentVariableAuth(
secretValue: 'application-supplied-replacement'), metadata: {}));
await credentials.delete(vault.id, credential.id);
await client.vaults.delete(vault.id);
Read real secrets from application-owned secret storage; the snippet illustrates
transport fields. Creation supports mcp_oauth, static_bearer and
environment_variable through separate create, rotation and returned auth unions.
Returned auth has no token/access-token/refresh-token/client-secret/secret-value
readback fields. Known write-only fields in malformed returned auth are rejected;
other finite future fields remain privately owned. Default diagnostics redact
request bodies, metadata, URLs, headers and received values. Explicit exception
fields retain HTTP context for deliberate inspection.
Rotation needs auth or metadata and cannot change auth method, destination,
environment secret name or networking. Omitted OAuth expiry preserves it unless
a new access token is supplied; a new token without expiry or explicit-null expiry
clears it. Omitted/null refresh tokens and client secrets retain stored secrets;
omitted scope retains it and explicit null clears it. Nullable request fields use
clearX: true to emit JSON null; these flags express wire presence, whose storage
meaning follows the individual field. Metadata update/rotation is optional
nonnull: omission retains, {} clears, and JSON null is invalid.
Names retain their supplied spelling and must contain 1–256 UTF-8 bytes after
trimming. Vault creation metadata permits 1,024 pairs with 256-character keys and
1,048,576-character values and supports explicit null; vault updates, credential
creation/rotation and list filters use 16 pairs, 64-character keys and
512-character values. List metadata uses metadata[key]=value with AND matching
and eventual consistency. Scalar status uses status; arrays use repeated
status[]. Both active and archived are included by default; an empty status
array supplies no pairs. Lists accept limits 1–100 and exclusive after IDs;
reuse lastId with the same order/filters, and retain nullable empty-page boundaries.
Hosted environment-variable secrets supply placeholders to sandbox code, with
substitution only for allowed outgoing HTTPS requests on ports 443/8443. The
environment network policy must also permit the destination. Credential networking
unrestricted requires restricted environment network access with explicit
allowed_domains; it does not grant unrestricted sandbox networking. Limited
networking permits 1–16 distinct hostnames/IPv4 addresses after lowercase
normalization, without scheme/path/port/wildcard/IPv6. Request host spelling remains
intact for service normalization. Secret names use ASCII identifier syntax;
CODEX_* and managed proxy/certificate names are reserved. Known conventional
proxy/certificate names are rejected locally; additional managed names remain
service-owned. Secret values are nonempty and reject CR/LF/NUL.
Placeholders cannot supply secrets for local computation, self-hosted environments or application function tools. Rotation affects values used by new sessions or environments and does not promise immediate replacement inside an existing sandbox. Provider OAuth consent/revocation stays caller-owned; deleting a stored credential or Vault does not revoke provider tokens or cancel running work.
→ Offline Vaults example — all ten operations, synthetic write-only secrets and safe returned metadata, no live API or key ($0).
How do I classify or score shared input? #
The Decisions API returns ordered typed answers to predicate, choice, and score questions. It currently supports gpt-6-luna and accepts text, inline data URLs and publicly accessible HTTP(S) images.
final decision = await client.decisions.create(
DecisionRequest(
model: 'gpt-6-luna',
input: DecisionInput.text('The screen arrived broken.'),
questions: [
DecisionQuestion.predicate(
name: 'damaged',
instructions: 'Does the customer report a damaged item?',
),
],
),
);
for (final answer in decision.answers) {
if (answer case PredicateDecisionAnswer(:final probability)) {
print('Damage probability: $probability');
} else if (answer case RefusalDecisionAnswer()) {
print('The question was refused.');
}
}
Use DecisionInput.messages with DecisionContent.parts for mixed text/image input. DecisionInputPart.image(imageUrl: ...) accepts a data URL or a publicly accessible HTTP(S) URL; DecisionInputPart.imageBytes(bytes, mediaType: 'image/png') builds the MIME/base64 data URL. File IDs, other roles, tools, and audio are unsupported. Unset image detail defaults to auto on the server.
final image = DecisionInputPart.image(
imageUrl: 'https://images.example.com/product.png',
detail: ImageDetail.original,
);
final inline = DecisionInputPart.imageBytes([1, 2, 3], mediaType: 'image/png');
Image constructors, parsers and copies admit the case-sensitive prefixes data:,
http:// and https://, preserving the supplied string. OpenAI checks image
validity and public accessibility; the Dart client sends the reference without
downloading or normalizing it. The current HTTP reference and schema support
remote images; older guide and SDK descriptions may still say inline-only.
→ Offline HTTP(S)/inline image example
verifies one mock POST, exact URL strings, mixed part order and image details at $0 cost.
Requests support 1–200 questions, 2–255 choices per choice question, 2–10 levels per score question, and up to 128 images across all messages. An optional safetyIdentifier is limited to 128 characters. Choice values preserve strings and booleans as distinct types; score answers can be fractional. Refusals can occur alongside successful answers. There is no model-event streaming mode.
How do I configure an execution container? #
Create a standalone container with a memory tier and network policy, then pass its ID to Code Interpreter:
final container = await client.containers.create(
CreateContainerRequest(
name: 'my-container',
memoryLimit: ContainerMemoryLimit.gb1,
networkPolicy: ContainerNetworkPolicy.disabled,
),
);
final tool = ResponseTool.codeInterpreter(
container: CodeInterpreterContainer.id(container.id),
);
// Use tool in a Responses request, then delete the container when finished.
await client.containers.delete(container.id);
The current memory tiers are gb1, gb4, gb16, and gb64, which emit 1g, 4g, 16g, and 64g. Automatic containers accept the same configuration through CodeInterpreterContainer.auto(...). An allowlist uses ContainerNetworkPolicy.allowlist(['api.example.com'], domainSecrets: [...]), with typed ContainerNetworkPolicyDomainSecret entries. Secret values are redacted in model diagnostics.
Standalone creation also supports ContainerSkill.reference(skillId: ..., version: 'latest') and inline skills whose ContainerSkillSource.base64(data: ...) contains an already encoded ZIP bundle. Returned memory/network settings and expiration members can be absent; check them before use. Filter listing with client.containers.list(name: 'my-container').
See the migration guide for changes to integer memory values, allowedHosts, required names, response expiration, and const construction.
How do I create a response? #
Show example
The Responses API is the recommended way to generate text. Pass a model and input, and access the result via response.outputText.
import 'package:openai_dart/openai_dart.dart';
final client = OpenAIClient.fromEnvironment();
final response = await client.responses.create(
CreateResponseRequest(
model: 'gpt-5.5',
input: ResponseInput.text('What is the capital of France?'),
),
);
print('Response: ${response.outputText}');
print('Usage: ${response.usage}');
client.close();
How do I use persistent Responses WebSockets? #
Show example
On native Dart/Flutter, open an authenticated connection through the existing client configuration and read its lane-aware envelopes:
final connection = await client.responses.connect();
final latest = <String, String>{};
final bothLaneCompletions = Completer<void>();
final subscription = connection.events.listen(
(message) {
if (message case ResponsesStreamEvent(
streamId: final lane?,
event: ResponseCompletedEvent(:final response),
)) {
latest[lane] = response.id;
if (latest.containsKey('planner') &&
latest.containsKey('research') &&
!bothLaneCompletions.isCompleted) {
bothLaneCompletions.complete();
}
} else if (message is ResponsesErrorEvent) {
// Inspect message.error.code and message.status to choose lane recovery.
if (!bothLaneCompletions.isCompleted) {
bothLaneCompletions.completeError(
StateError('A lane request failed.'),
);
}
}
},
onError: (Object error) {
// Protocol/transport failures are identifiable, payload-redacted exceptions.
if (!bothLaneCompletions.isCompleted) {
bothLaneCompletions.completeError(error);
}
},
onDone: () {
if (!bothLaneCompletions.isCompleted) {
bothLaneCompletions.completeError(
StateError('Connection closed before both lanes completed.'),
);
}
},
);
try {
await Future.wait<void>([
Future<void>.sync(
() => connection
..create(
const CreateResponseRequest(
model: 'gpt-6-sol',
input: ResponseInput.text('Draft a plan.'),
),
streamId: 'planner',
)
..create(
const CreateResponseRequest(
model: 'gpt-6-sol',
input: ResponseInput.text('Research the risks.'),
),
streamId: 'research',
),
),
bothLaneCompletions.future,
], eagerError: true);
} finally {
try {
await connection.close();
await connection.done;
} finally {
await subscription.cancel();
}
}
streamId routes events and orders requests within a lane; previousResponseId
selects ancestry. Continue with the actual returned ID and only new input. A lane
reused without a parent starts a new response. generate: false warms state and
returns an ID for subsequent chaining. These are WebSocket-only fields;
ResponsesCreateEvent composes the normal request, omits stream/false
background, rejects background: true, and retains streamOptions.
Response completion and request-scoped server errors leave the connection open.
ResponsesErrorEvent retains full error metadata and absent-versus-null
code/param. Shared events use ResponsesStreamEvent.event; future events
retain raw JSON. One socket reader emits interleaved envelopes. The first listener
receives buffered early events, including events before an early close; the default
opening capacity is 1,024 and overflow fails explicitly. After the first listener,
only active subscribers receive broadcasts. Cancelling a listener removes that
listener; explicitly await connection cleanup. done completes independently of
event draining. By default, close code/reason expose observed transport facts;
with recovery enabled they describe final logical closure as detailed below.
Browser WebSockets cannot send custom headers. Any configured auth/default/org/
project/version headers reject before dialing with proxy guidance. Use an explicit
headerless client configuration and an authenticated backend WebSocket proxy.
additionalHeaders and configured header precedence apply on native platforms;
beta: true forces the multi-agent opt-in header. connector is an injectable
seam, and connectionTimeout defaults to the configured connect timeout.
The current WebSocket guide
describes 16 in-flight responses, 32 named lanes plus default, and a 60-minute
connection limit. On disconnection, recover manually using a stored response ID
or replay full context with no parent. A store: false fork should reach
response.in_progress before the source lane advances. Standalone compaction
starts a new chain using the complete returned compacted window. Mid-turn steering
is described below. Opt-in socket recovery
and multi-agent tool-result injection
are available.
→ Runnable offline example, with warm-up, two lanes, incremental continuation and awaited cleanup for $0.
The shared annotation-added event accepts its required nullable annotation value.
See the migration guide
before dereferencing OutputTextAnnotationAddedEvent.annotation.
How do I steer a running Responses request? #
Show example
Send user input after the original response.created, then keep reading until
the successor completes:
final connection = await client.responses.connect();
final reader = StreamIterator(connection.events);
String? originalId;
String? successorId;
var completed = false;
try {
connection.create(
const CreateResponseRequest(
model: 'gpt-6-sol',
input: ResponseInput.text('Draft a project plan.'),
),
);
while (await reader.moveNext()) {
final message = reader.current;
if (message case ResponsesStreamEvent(
event: ResponseCreatedEvent(:final response),
)) {
if (originalId == null) {
originalId = response.id;
connection.steer(
previousResponseId: originalId,
input: const ResponsesSteerInput.text('Keep the scope small.'),
);
} else {
successorId = response.id; // The queued input is now committed.
}
} else if (message is ResponsesSteerAcceptedEvent) {
// Save message.steer.id. Acceptance queues input; keep reading.
} else if (message is ResponsesSteerPendingEvent) {
// This minimal example has no tool runner. Return saved results through
// one explicit create per parent on message.streamId; see the full example.
throw StateError('The continuation needs saved tool results or approval.');
} else if (message is ResponsesSteerFailedEvent ||
message is ResponsesErrorEvent) {
throw StateError('The request or steering failed.');
} else if (message case ResponsesStreamEvent(event: ResponseFailedEvent())) {
throw StateError('A response failed before the continuation completed.');
} else if (message case ResponsesStreamEvent(
event: ResponseIncompleteEvent(:final response),
)) {
if (response.id != originalId ||
response.incompleteDetails?.reason != 'steered') {
throw StateError('A response ended incomplete without successful steering.');
}
} else if (message case ResponsesStreamEvent(
event: ResponseCompletedEvent(:final response),
)) {
if (successorId != null && response.id == successorId) {
completed = true;
break;
}
}
}
if (!completed) {
throw StateError('Connection closed with an unknown steering outcome.');
}
} finally {
try {
await connection.close();
await connection.done;
} finally {
await reader.cancel();
}
}
Steering supports the GPT-6 family in supported single-agent modes, without a
conversation binding or automatic compaction. Unsupported modes surface server
failures; the client does not maintain a model allowlist. ResponsesSteerEvent
contains only type, previous_response_id and input. ResponsesSteerInput
accepts text or a nonempty list of user messages containing text/image/file parts.
It excludes create settings, lane fields, message IDs/status and tool results.
sendSteer accepts an already-built event; steer is the convenience method.
response.steer.accepted queues the update; successor response.created commits
it. The original can end as incomplete with reason steered or complete normally
before that successor arrives. Steering does not rewrite emitted output, undo
actions or cancel tools already running.
For waiting_for_required_input, inspect ResponsesSteerPendingEvent.requiredInput.
Its seven typed stub kinds identify function, custom, computer, shell, apply-patch,
tool-search results or MCP approval; they are not complete result items. Use saved
results in one explicit create per parent on the original lane. That request uses
its own settings; the server prepends accepted steering. Several pending
submissions can share the same stubs, and a matching create can arrive before a
pending notification. Do not rerun tools or resend accepted input.
Pending reasons and failure codes remain open strings. A failure preserves the original rejected raw input, optional allocated ID and lane, including future metadata. A failure after acceptance retains the same steering ID. Correct invalid input before deciding whether to submit it again. A missing acknowledgment or disconnect leaves the outcome unknown; no frame is automatically replayed.
→ Runnable offline steering example demonstrates automatic continuation and two pending submissions sharing one saved tool result, with exactly one continuation create and awaited cleanup for $0. See migration guidance for exhaustive WebSocket event switches and the official steering guide.
How do I recover a Responses WebSocket? #
Show example
Supply reconnect to opt in. The required preparation callback lets your
application reconcile history and update its authentication provider before a
replacement socket opens. Pass your normal application session loop to this
wrapper; submitted work is never replayed for you:
Future<void> runRecoverableSession(
OpenAIClient client,
Future<void> Function(ResponsesConnection) runSession,
) async {
final connection = await client.responses.connect(
reconnect: ResponsesReconnectOptions(
onReconnecting: (context) {
// Reconcile application state before continuing. Abort if it is unsafe.
return const ResponsesReconnectDecision.continueWith();
},
),
);
final recovery = connection.recovery!;
final lifecycle = recovery.events.listen((change) {
// Lifecycle diagnostics redact payloads, header/query values and reasons.
print(change);
});
try {
await runSession(connection);
} finally {
try {
await connection.close();
await connection.done;
final report = await recovery.done;
print('${report.unsentMessages.length} never-attempted frames remain.');
// Inspect snapshots deliberately; decide whether/how to submit new work.
} finally {
await lifecycle.cancel();
}
}
}
Omitting reconnect keeps the default behavior. With recovery enabled, defaults
are five attempts, an initial 500 ms exponential delay capped at eight seconds,
and jitter between 0.75 and 1.0. Durations truncate to whole milliseconds before
jitter; the resulting delay rounds to the nearest millisecond. Only closes 1001, 1005, 1006, 1011, 1012, 1013 and
1015 admit recovery; clean, protocol, policy and unknown closes stop it.
Authentication is rebuilt for each dial. Initial handshake failures are not
retried. Explicit close stops logical recovery promptly during preparation,
waiting or dialing. User callbacks may finish later; late errors are consumed
and late sockets are disposed. A callback exception or abort ends recovery.
ResponsesReconnectDecision.continueWith(queryParameters: ..., headers: ...)
replaces the previous override maps for subsequent attempts. Null reuses them;
empty maps clear the overrides. The helper snapshots returned maps before its
delay. Fresh configured defaults/auth still apply, and beta opt-in is forced
last. Attempt context exposes timing and the triggering close, while your
application retains its own credential, query and conversation state.
Only new frames sent while recovery is in progress enter the FIFO queue. Its
strict default limit is 1 MiB (1,048,576 serialized UTF-8 bytes), including the
first frame. Bytes are captured at enqueue, so caller mutation cannot change the
message or accounting. Overflow throws ResponsesSendQueueOverflowException
and emits ResponsesRecoveryQueueOverflow; only the new frame is rejected and
recovery continues. Zero attempts disables retries, zero queue bytes disables queuing and rejects
every new frame, and zero delay/cap permits immediate attempts.
An attempted send or queued flush failure has unknown delivery. It emits
ResponsesRecoveryDeliveryUnknown, permanently closes the logical connection,
and never retries that frame. A direct send also throws
ResponsesDeliveryUnknownException. recovery.done and unsentMessages report
only the immutable never-attempted remainder; a rejected overflow frame is also
excluded. ResponsesUnsentMessage.text holds exact wire text, byteLength its
UTF-8 size, and message an immutable decoded JSON object when valid. These
payloads may contain sensitive input; do not log or replay them automatically.
The strict first-frame bound matches Python and differs from Node's oversized first-frame exception. Never replaying a failed attempted write matches Python; terminating on that failure is this Dart helper's explicit policy. Python logs flush failures and Node may requeue an attempted failed frame. These choices do not imply parity with other SDK output-parsing or application recovery helpers.
recovery.events reports reconnecting, reconnected, overflow, uncertain delivery,
transport error and final closure. currentAttempt, lastEvent and the final
report remain inspectable without an active listener. Cancelling a lifecycle
listener does not close the connection. recovery.done completes independently
of listener draining. With recovery enabled, connection.closeCode/closeReason
represent final logical closure, including caller-requested close values; they
are not necessarily a physical peer notification. currentAttempt retains the
triggering physical close context, and the final report's cause classifies why
recovery ended.
Opening a socket does not restore any lane's cache, conversation history or accepted steering. Continue using a valid stored response ID when available, or start a new chain with full retained input and no parent. Missing acknowledgments remain unknown outcomes. Native connectors support headers; browser connectors reject all custom headers, including returned overrides, so use an authenticated backend proxy with a headerless browser configuration. See the official recovery guide.
→ Runnable offline recovery example proves original create/accepted-steer frames never replay, new frames flush FIFO, overflow stays nonfatal, and final unsent snapshots remain immutable with awaited cleanup and no API key or charges.
How do I inject multi-agent tool results? #
Show example
Open a native beta connection with client.responses.connect(beta: true) and
set multiAgent: MultiAgentConfig(enabled: true) on your create request. Choose
a model that supports the beta using the current model documentation; this client
does not impose a model allowlist. Return each developer-owned result as it becomes
available, using the response ID received from response.created. This helper
uses a caller-owned open connection and returns the completed ID, lane and raw
uncommitted input. Choose any continuation on that same socket, then await
connection.close() in your outer finally block:
Future<({String responseId, String streamId, List<Object?> uncommitted})>
runInjectedTurn(
ResponsesConnection connection,
CreateResponseRequest request,
Future<String> Function(FunctionCallOutputItemResponse) executeTool,
) async {
final reader = StreamIterator(connection.events);
final savedCallIds = <String>{};
final uncommitted = <Object?>[];
String? responseId;
var pending = 0;
String? completedResponseId;
try {
connection.create(request, streamId: 'planner');
while (await reader.moveNext()) {
final message = reader.current;
// Other subscribers may process other lanes on this caller-owned socket.
if (message.streamId != 'planner' &&
!(message is ResponsesErrorEvent && message.streamId == null)) {
continue;
}
if (message case ResponsesStreamEvent(
event: ResponseCreatedEvent(:final response),
)) {
responseId = response.id;
} else if (message case ResponsesStreamEvent(
event: OutputItemDoneEvent(
item: final FunctionCallOutputItemResponse call,
),
)) {
if (responseId == null) throw StateError('Missing response identity');
if (!savedCallIds.add(call.callId)) continue;
final output = await executeTool(
call,
); // The application owns execution.
pending++;
connection.inject(
responseId: responseId,
input: [
FunctionCallOutputItem.string(callId: call.callId, output: output),
],
);
} else if (message is ResponseInjectCreatedEvent ||
message is ResponseInjectFailedEvent) {
final target = message is ResponseInjectCreatedEvent
? message.responseId
: (message as ResponseInjectFailedEvent).responseId;
if (target != responseId || pending == 0) {
throw StateError('Unexpected injection acknowledgment');
}
pending--;
if (message is ResponseInjectFailedEvent) {
if (message.error.code !=
ResponseInjectErrorCode.responseAlreadyCompleted) {
throw StateError(
'Injection was not committed; inspect the failure',
);
}
uncommitted.addAll(message.input); // Keep the original raw JSON.
}
} else if (message case ResponsesStreamEvent(
event: ResponseCompletedEvent(:final response),
)) {
if (response.id != responseId) {
throw StateError('Unexpected completion');
}
completedResponseId = response.id;
} else if (message is ResponsesErrorEvent) {
throw StateError('A WebSocket error interrupted the turn');
} else if (message case ResponsesStreamEvent(
event: ResponseFailedEvent() || ResponseIncompleteEvent(),
)) {
throw StateError('The response ended unsuccessfully');
}
if (completedResponseId != null && pending == 0) {
return (
responseId: completedResponseId,
streamId: 'planner',
uncommitted: uncommitted,
);
}
}
throw StateError('Socket ended before completion and every acknowledgment');
} finally {
await reader
.cancel(); // The caller retains the connection for continuation.
}
}
sendInject(ResponseInjectEvent(...)) sends the same explicit frame. Its wire
fields are exactly type, response_id and input; injection has no stream_id
argument because the target determines the lane. Requests retain the existing
broad List<Item> surface and enforce the canonical 16,384-item maximum. The
server currently accepts client-owned tool outputs. A nonempty rule or narrower
client tool whitelist is not invented. Both methods require a beta connection.
The shared Item decoder still has legacy subtype/unknown-field limitations; this
integration does not establish complete generated input-union parity.
An accepted acknowledgment has no submission ID or echoed input. Track the count
of submissions for each response and its lane, and read until the response is
terminal and every injection has a created/failed acknowledgment. Calls may
originate from the root or a subagent; existing item/event agent tags remain
available. Hosted multi_agent_call and multi_agent_call_output are server-owned
and must not be executed or injected as developer tools.
Failed input is now a deeply immutable raw List<Object?>, preserving future
metadata and malformed nested values. Its outer value must still be an array.
response_already_completed permits the application to choose an explicit new
create with the uncommitted results and completed parent on the original lane.
Validate the saved results first; ResponseInput.fromOutputItems(validatedMaps)
preserves their raw fields. Do not send accepted results again or rerun a tool.
response_not_found needs identity reconciliation. Unknown error strings survive
in error.rawCode; the existing enum remains available. Malformed request frames
can produce a generic status-400 error followed by socket closure; observe both.
Lost acknowledgments or attempted write failures leave delivery unknown and do
not trigger replay. Opt-in recovery queues only newly unsent frames as described
above.
Injection acknowledgments add two sealed WebSocket variants; the SSE dispatcher is unchanged. See the migration guide for exhaustive switches, the raw failed-input getter, and stricter known-frame validation.
The standard browser connector rejects custom headers, including the forced beta
header. Use a backend proxy that authenticates and adds the beta header server
side; wrap an already-open headerless proxy socket with
ResponsesConnection(proxySocket, beta: true) or use an explicit proxy connector.
Realtime ephemeral bearer authentication does not apply. See the
official multi-agent guide.
→ Runnable offline injection example shows root/subagent function calls, a hosted action, multiple outstanding injections and an acknowledgment after completion, with one explicit raw-result continuation, no tool rerun and awaited cleanup for $0.
How do I return client-discovered tools? #
Show example
Configure client-side tool search, then return the complete definitions with the original search call ID:
final first = await client.responses.create(
const CreateResponseRequest(
model: 'gpt-6-sol',
input: ResponseInput.text('Find the inventory tools.'),
tools: [
ToolSearchTool(
execution: ToolSearchExecutionType.client,
description: 'Search a local tool catalog.',
parameters: {
'type': 'object',
'properties': {'goal': {'type': 'string'}},
'required': ['goal'],
'additionalProperties': false,
},
),
],
parallelToolCalls: false,
),
);
final call = first.output.whereType<ToolSearchCallOutputItem>().single;
final callId = call.callId;
if (call.execution != ToolSearchExecutionType.client || callId == null) {
throw StateError('Expected a client search call with an ID.');
}
// Resolve call.arguments against your catalog; check its JSON type first.
final next = await client.responses.create(
CreateResponseRequest(
model: 'gpt-6-sol',
previousResponseId: first.id,
input: ResponseInput.items([
ToolSearchOutputItemParam(
callId: callId,
execution: ToolSearchExecutionType.client,
tools: const [
NamespaceTool(
name: 'inventory',
description: 'Inventory tools from the local catalog.',
tools: [
FunctionTool(name: 'get.status'),
CustomTool(name: 'describe', format: {'type': 'text'}),
],
),
],
status: ItemStatus.completed,
),
]),
),
);
Search outputs preserve complete function/custom definitions, including parameter
and output schemas, formats, allowed callers, deferred loading and async flags.
Nested discovered functions support dotted names and may contain only type and
name. Top-level discovered functions always serialize the required nullable
parameters and strict keys. For standalone discovered-definition JSON, use
ResponseTool.fromToolSearchOutputJson and toToolSearchOutputJson; ordinary
NamespaceTool.fromJson uses the narrower request naming rules.
Writable ToolSearchCallItemParam.arguments requires a JSON object. Returned
calls use Object? because their arguments may be any JSON value, including null
or a list. Check the type before indexing. Returned calls/results preserve null
hosted call IDs and require their execution, status and payload fields. Search
records from responses.inputItems.list use ToolSearchCallResourceItem and
ToolSearchOutputResourceItem; their explicit conversion methods create writable
items, and call conversion rejects non-object arguments. To retain raw output
history, ResponseInput.fromOutputItems accepts raw JSON maps.
The tool-search guide documents hosted search and client continuation. The canonical writable discovered namespace schema allows dotted function names, while the returned schema references ordinary namespaces with narrower names. Search-result parsing deliberately uses the discovered context to retain loaded definitions; this is a compatibility interpretation of those sources. The canonical top-level function schema also requires nullable keys that some guide examples omit.
→ Runnable offline example, demonstrating two
local requests, the original call ID and complete discovered definitions without
an API key or tool execution. See the migration guide
for required constructor arguments and new Item variants.
How do I select and inspect an access program? #
Show example
Use AccessProgramsParam to select a cyber access program and inspect the effective
returned selection through AccessProgramsBody:
const request = CreateResponseRequest(
model: 'gpt-6-sol',
input: ResponseInput.text('Explain how to validate a patch in a test setup.'),
accessPrograms: AccessProgramsParam(cyber: CyberAccessProgram.daybreakBlue),
);
final response = await client.responses.create(request);
print(response.accessPrograms?.cyber.toJson() ?? 'Not reported or implicit Standard');
CyberAccessProgram supports standard, daybreakBlue and daybreakRed with
their exact wire values. Omit accessPrograms or use const AccessProgramsParam()
to leave selection to the server. No default is inserted by the client. An implicit
Standard selection is reported as null by the server; explicit Standard retains
its returned object. Older responses that omit this metadata remain accepted.
Request JSON rejects supplied null, unsupported keys inside the selection object,
and invalid cyber values. Returned objects require a nonnull known cyber value;
extra returned fields remain tolerated. Clearing a Dart selection with
copyWith(accessPrograms: null) omits the request key. Ordinary, retrieved,
cancelled and streamed lifecycle responses retain the effective program, including
through ResponseStreamAccumulator.response.
Explicit selection requires compatible model and organization/project access; it does not grant approval. Match the program to the model rather than the approval level. Follow the current Daybreak guide for model eligibility, project setup, omission defaults and 400/403 errors. Usage tiers such as Build, Launch and Grow are separate from access-program selection.
→ Runnable offline example, demonstrating omission, an empty selection and all three explicit values without an API key, Daybreak provisioning or API charges.
How do I observe compaction progress? #
Show example
Enable inline compaction with context management and watch for the typed progress event while continuing to consume the stream:
const request = CreateResponseRequest(
model: 'gpt-6-astra',
input: ResponseInput.text('Continue the current task.'),
contextManagement: [
ContextManagement.compaction(compactThreshold: 200000),
],
);
await for (final event in client.responses.createStream(request)) {
switch (event) {
case ResponseCompactionCompactingEvent(:final itemId, :final outputIndex):
print('Compacting $itemId at output index $outputIndex.');
case ResponseCompletedEvent():
print('Response completed.');
default:
break;
}
}
ResponseCompactionCompactingEvent carries required sequenceNumber,
outputIndex and itemId, plus optional beta agent metadata. It is nonterminal
and contains no summary or encrypted content. ResponseStreamAccumulator exposes
it through latestEvent while preserving response, text, reasoning and status.
The final response retains the opaque CompactionOutputItem.
The compaction guide
describes automatic threshold-based compaction and standalone responses.compact.
For standalone compaction, replay the complete returned output window with
compacted.toInput(). Existing context management,
explicit triggers and encrypted replay remain available; future event types still
use UnknownEvent.
→ Runnable offline example, with no API key or large context. See the migration guide when updating exhaustive switches or manual unknown-event handlers.
How do I configure hosted shell and return local results? #
Show example
Configure a hosted container, local runtime, or existing container on the shell tool. Hosted configuration reuses container memory, network and skill models:
final hostedTool = ResponseTool.shell(
environment: ShellToolEnvironment.containerAuto(
memoryLimit: ContainerMemoryLimit.gb4,
networkPolicy: ContainerNetworkPolicy.disabled,
skills: const [ContainerSkill.reference(skillId: 'skill_uploaded')],
),
);
final localTool = ResponseTool.shell(
environment: ShellToolEnvironment.local(
skills: const [
ShellLocalSkill(
name: 'csv-summary',
description: 'Summarize a local CSV dataset.',
path: '/workspace/skills/csv-summary',
),
],
),
);
final request = CreateResponseRequest(
model: 'gpt-6-astra',
input: const ResponseInput.text('Summarize the dataset.'),
tools: [hostedTool],
toolChoice: ResponseToolChoice.shell(),
);
For a locally executed call, inspect the commands and apply your application's execution policy. Return captured output on the original call ID and retain the model's output limit. This snippet supplies synthetic output:
final continuation = CreateResponseRequest(
model: 'gpt-6-astra',
previousResponseId: response.id,
input: ResponseInput.items([
ShellCallOutputInputItem(
callId: call.callId,
maxOutputLength: call.action.maxOutputLength,
output: const [
ShellCallOutputContentInput(
stdout: 'Synthetic summary: 12 rows.',
stderr: '',
outcome: ShellCallExitOutcome(exitCode: 0),
),
],
),
]),
);
Typed command added/delta/done and output-content delta/done events expose
commandIndex, outputIndex, sequence and optional agent metadata. Output
fragments carry independent stdout/stderr fields; empty fragments and obfuscation
stay intact. Completed lifecycle responses retain full results. There is no
automatic command runner or generalized shell-output accumulator.
The shell guide and
skills guide describe
hosted bundles versus local name/description/path skills. Hosted network access
requires organization configuration and an explicit request policy. Server limits
are 50 uploaded file IDs and 200 skills per environment; the SDK documents these
limits and leaves enforcement to the service. container_auto belongs on a
definition; direct call history allows only local/reference environments. Returned
local environments have no skills. toShellCallInputItem() and
toShellCallOutputInputItem() bridge output, input listings and conversation
items while omitting returned-only creator metadata.
→ Runnable offline example, with hosted configuration, local continuation and typed stream events. It requires no API key, creates no containers and executes no proposed commands. See migration guidance for corrected required nullable returned keys and distinct writable DTOs.
How do I filter web search and inspect image results? #
Show example
ResponseTool.webSearch() and WebSearchTool() now default to GA web_search.
Use domain filters and image settings, then request results and source metadata:
final response = await client.responses.create(
CreateResponseRequest(
model: 'gpt-6-astra',
reasoning: const ReasoningConfig(effort: ReasoningEffort.low),
input: const ResponseInput.text('Find images of the Golden Gate Bridge.'),
tools: [
ResponseTool.webSearch(
filters: WebSearchFilters(
allowedDomains: const ['nps.gov', 'wikimedia.org'],
blockedDomains: const ['spam.example'],
),
searchContentTypes: const [SearchContentType.image, SearchContentType.text],
imageSettings: WebSearchImageSettings(maxResults: 3, caption: true),
),
],
toolChoice: ResponseToolChoice.webSearch(),
include: const [Include.webSearchResults, Include.webSearchActionSources],
),
);
for (final call in response.output.whereType<WebSearchCallOutputItem>()) {
for (final result in call.results ?? <WebSearchResult>[]) {
if (result case WebSearchImageResult(:final imageUrl, :final sourceWebsiteUrl)) {
print('$imageUrl from $sourceWebsiteUrl');
}
}
}
Calls expose WebSearchCallStatus, typed search/open-page/find actions, and
WebSearchActionSearch.sources. Image results are separate from assistant text
and citations. Future action/result objects retain recursively immutable JSON;
no text-result shape is assumed. Optional null image metadata normalizes to
absence. Input listings and conversation items retain the same metadata, and
toWebSearchCallItem() supports typed history replay.
The official guide
documents scheme-free domains (up to 100 in each allow/block list), positive
image counts, and returnTokenBudget for GPT-5+ reasoning web search. Use
WebSearchReturnTokenBudget.unlimited selectively; it can increase latency and
cost. Omission leaves the server's usual budget unchanged. externalWebAccess: false selects cached content on the real service.
Explicit preview types remain available with type: 'web_search_preview' or
'web_search_preview_2025_03_11'; GA also accepts 'web_search_2025_08_26'.
GA-only controls are rejected on preview tools. Block lists, return budgets,
GA image controls/results, and forced GA choice follow the guide ahead of the
canonical schema; these differences stay visible in verification evidence.
→ Runnable local REST/SSE example, without an API key or charges. See migration guidance for the GA default and status type changes.
How do I use async function and custom tools? #
Show example
Set async: true on a Responses function or custom tool to let the model continue
while your application runs that tool. Both tool definitions and returned calls
preserve omitted, false and true flags; copyWith(async: null) removes the flag.
The client provides typed contracts; your application owns tool execution and
returning the result.
final tool = ResponseTool.function(
name: 'get_weather',
async: true,
strict: true,
parameters: const {
'type': 'object',
'properties': {'city': {'type': 'string'}},
'required': ['city'],
'additionalProperties': false,
},
);
final response = await client.responses.create(
CreateResponseRequest(
model: 'gpt-6-astra',
input: const ResponseInput.text('Look up the weather in Paris.'),
tools: [tool],
),
);
final call = response.output.whereType<FunctionCallOutputItemResponse>().single;
// Run your application tool and keep call.callId associated with its result.
// If other turns happen, replace this with the latest response's ID.
final latestResponseId = response.id;
await client.responses.create(
CreateResponseRequest(
model: 'gpt-6-astra',
previousResponseId: latestResponseId,
tools: [tool],
input: ResponseInput.items([
FunctionCallOutputItem.string(
callId: call.callId,
output: '{"city":"Paris","celsius":24}',
),
]),
),
);
ResponseTool.custom(async: true, ...) uses the same execution flag.
CustomToolCallItem.toCustomToolCallInputItem() supplies typed custom-call replay;
function replay preserves agent, namespace, caller, status and async metadata.
Custom replay omits output-only status/creator fields. Ordinary output/result
items do not gain an async flag.
The official async guide documents support for GPT-6 Astra and later models. Use direct application tools, not hosted tools or programmatic calls; in Multi-agent mode, do not combine async with parallel tool calls. There is no dedicated async streaming event: existing item-added/done and response lifecycle events carry the calls.
→ Runnable function/custom example, using a local transport with an intervening turn, no API key and no charges.
How do I change reasoning effort during a conversation? #
Show example
Append a ConfigurationUpdateItem before the next user message to change effort
for subsequent responses. Keep the request-level ReasoningConfig and stable
instructions unchanged so the original prompt prefix remains reusable for caching.
const baseline = ReasoningConfig(effort: ReasoningEffort.low);
final first = await client.responses.create(
const CreateResponseRequest(
model: 'gpt-6-astra',
reasoning: baseline,
input: ResponseInput.text('Draft a database migration plan.'),
),
);
final next = await client.responses.create(
CreateResponseRequest(
model: 'gpt-6-astra',
reasoning: baseline,
previousResponseId: first.id,
input: ResponseInput.items([
const ConfigurationUpdateItem(
reasoning: ConfigurationUpdateReasoning(effort: ReasoningEffort.high),
),
MessageItem.userText('Analyze failure modes and rollback steps.'),
]),
),
);
print(next.outputText);
The server keeps the selected effort until another update replaces it. Continue
with previousResponseId, or preserve updates in their original positions when
replaying history. response.reasoning.effort still reports request-level effort.
ConfigurationUpdateReasoning exposes only effort. Omitted reasoning and {}
remain distinct. Nullable input IDs and effort values accept null and serialize
as omission; supplied null reasoning is invalid. Responses input listings return
ConfigurationUpdateItemResponse; conversation items return
ConversationConfigurationUpdateItem. Both require an ID and provide
toConfigurationUpdateItem() for typed replay.
The official reasoning guide
documents GPT-6 support in standard, single-agent mode. Avoid adjacent updates,
automatic compaction, automatic truncation, and standalone /responses/compact
for histories containing updates. After explicit compaction_trigger compaction,
insert a fresh update before the next user message. Normal caching requirements
still apply.
→ Runnable local example shows low → high → high → low across four turns, without an API key or charges.
How do I prewarm the prompt cache and inspect diagnostics? #
Show example
On supported GPT-5.6 and later models, Responses uses
ResponsePromptCacheOptionsParam for mode/TTL, prewarming, and comparison IDs.
Chat and compaction use the narrower PromptCacheOptionsParam with mode/TTL only.
A reusable prefix must meet the model's minimum cacheable length (1,024 tokens
for GPT-5.6 and later). Supply your application's stable context as stablePrefix;
keep its content and cache-affecting settings stable.
final baseline = await client.responses.create(
CreateResponseRequest(
model: 'gpt-6-luna',
input: ResponseInput.text(stablePrefix),
promptCacheOptions: const ResponsePromptCacheOptionsParam(prewarm: true),
),
);
final response = await client.responses.create(
CreateResponseRequest(
model: 'gpt-6-luna',
input: ResponseInput.text(stablePrefix),
promptCacheOptions: ResponsePromptCacheOptionsParam(
ttl: PromptCacheTtl.minutes30,
comparisonResponseId: baseline.id,
prewarm: false,
),
),
);
if (response.promptCacheDiagnostics case PromptCacheMissDiagnostics(:final reason)) {
print('Cache miss reason: ${reason.value}');
}
print('Cached tokens: ${response.usage?.inputTokensDetails?.cachedTokens}');
Use a recent completed baseline from the same organization. A comparison ID
requests diagnostics; it does not load conversation history or change caching.
Diagnostics can be absent, unavailable, or refer to an expired comparison.
A hit means no miss was detected for the comparison; usage counters measure
actual reuse and billing. Future diagnostic variants and reason strings are
preserved. During streaming, inspect ResponseCompletedEvent.response.
The existing retention control is deprecated and expresses a maximum policy; modern TTL expresses a minimum lifetime. They are independent. See the migration guide for the Responses options type change and official diagnostics guide.
How do I use chat completions? #
Show example
Use client.chat.completions.create(...) for multi-turn conversations. The response.text convenience getter returns the first choice's message content.
The API's existing promptCacheRetention setting is deprecated in favor of prompt_cache_options.ttl; cache-options alignment is tracked in #322. For the existing control, PromptCacheRetention.h24 emits 24h, while inMemory emits the canonical in_memory. GPT-5.5 and newer models support only h24; select inMemory only for models that support it. Legacy in-memory JSON still parses, and reserialization uses in_memory.
import 'package:openai_dart/openai_dart.dart';
final client = OpenAIClient.fromEnvironment();
final response = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'gpt-5.5',
messages: [
ChatMessage.system('You are a helpful assistant.'),
ChatMessage.user('What is the capital of France?'),
],
maxTokens: 100,
promptCacheRetention: PromptCacheRetention.h24,
),
);
// response.text is a convenience extension for the first choice's message content
print('Response: ${response.text}');
print('Finish reason: ${response.choices.first.finishReason}');
print('Usage: ${response.usage?.promptTokens} in, ${response.usage?.completionTokens} out');
// Build message lists fluently with extension methods
final messages = <ChatMessage>[]
.withSystemMessage('You are helpful')
.withUserMessage('Hello!');
client.close();
How do I generate, replay, and stream Chat audio? #
Show example
Use an audio-capable Chat model and request audio output. Complete output exposes its ID, base64 data, transcript, and replay expiry; text can be null.
final completion = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'gpt-audio-1.5',
messages: [ChatMessage.user('Say only OK.')],
modalities: const [ChatModality.text, ChatModality.audio],
audio: const ChatAudioConfig(
voice: ChatAudioVoice.alloy,
format: ChatAudioFormat.wav,
),
maxCompletionTokens: 128,
store: false,
),
);
if (completion.audio case final audio?) {
final bytes = base64Decode(audio.data); // import dart:convert
await File('reply.wav').writeAsBytes(bytes); // import dart:io
print(audio.transcript);
final reference = ChatMessage.assistant(
audio: ChatAudio.reference(id: audio.id),
);
// Add reference to a later request before audio.expiresAt.
}
ChatAudioReference is ID-only; ChatCompletionAudio requires all four output
fields. You can reuse the returned assistant message directly: actual request
serialization projects its audio to {id} without sending data/transcript/expiry.
Local response serialization retains the complete output and provider extensions.
ChatAudioConfig.voice accepts existing ChatAudioVoice constants (including
marin/cedar), AudioVoice.named('provider-voice-name'), or
AudioVoice.custom('voice_existing_id'). Availability depends on the model and
project access. ChatAudioFormat.aac is supported by the canonical request;
Chat PCM uses pcm16, while Speech uses pcm. See the
offline voice/file-audio example and
migration notes.
Stream with PCM16 output and pass events to ChatStreamAccumulator. Each
ChatDelta.audio is an independent partial update. accumulator.audio and each
accumulator.choices[i].audio expose stable ChatAudioDelta snapshots with
isComplete. Data/transcript fragments append separately; ID and expiry update
when supplied. Decode complete data once after accumulation. Usage-only events
and padding stay separate from audio.
toChatCompletion() retains complete audio and throws StateError if any choice
has incomplete audio, including after interruption; inspect partial snapshots
instead. Text-only conversion behaves as before. A pure final expiry-only update
can infer stop during final conversion when every audio field is present, while
raw event and snapshot finish metadata remain unchanged. Reset clears all audio.
→ Runnable audio, replay, and streaming example
See the official audio guide and migration guide for parsing and conversion boundaries.
How do I stream responses? #
Show example
Streaming returns content deltas plus metadata as they arrive. Request final usage
with includeUsage; its additional chunk has an empty choices list. Use
event.textDelta to safely handle content and usage-only events.
final accumulator = ChatStreamAccumulator();
final stream = client.chat.completions.createStream(
ChatCompletionCreateRequest(
model: 'gpt-6-luna',
reasoningEffort: ReasoningEffort.none,
messages: [ChatMessage.user('Tell me a short story')],
maxCompletionTokens: 64,
store: false,
streamOptions: const StreamOptions(
includeUsage: true,
includeObfuscation: true,
),
),
);
await for (final event in stream) {
accumulator.add(event);
stdout.write(event.textDelta ?? '');
}
final completion = accumulator.toChatCompletion();
print(completion.usage?.promptTokensDetails?.cacheWriteTokens);
print(completion.usage?.promptTokensDetails?.textTokens);
print(completion.usage?.promptTokensDetails?.imageTokens);
print(completion.usage?.completionTokensDetails?.textTokens);
Prompt details preserve audio, cached, unadjusted cache-write, image, and text counts. Completion details preserve audio, reasoning, prediction, and text counts. These details also work on ordinary completions; missing counters remain absent, and zero remains zero. Interrupted streams can end before final usage arrives.
includeObfuscation controls server padding that normalizes streamed payload
sizes. Omit it to keep the server default (enabled), or set it to false to reduce
bandwidth when you trust the network links. ChatStreamEvent.obfuscation exposes
padding as metadata, including an empty string. It never becomes accumulated
text, refusal, reasoning, or tool arguments; textDeltas() and collectText()
also yield content only. The shared options model supports the flag on Responses
requests too; includeUsage is Chat-specific. See the
official streaming reference
for the wire contract.
How do I use tool calling? #
Show example
Define tools with JSON Schema parameters and pass them in the request. The response indicates when the model wants to call a tool, and you can inspect the function name and arguments.
final response = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'gpt-5.5',
messages: [
ChatMessage.user("What's the weather in Tokyo?"),
],
tools: [
Tool.function(
name: 'get_weather',
description: 'Get the current weather for a location',
parameters: {
'type': 'object',
'properties': {
'location': {'type': 'string', 'description': 'City name'},
},
'required': ['location'],
},
),
],
),
);
if (response.hasToolCalls) {
for (final toolCall in response.allToolCalls) {
print('Function: ${toolCall.function.name}');
print('Arguments: ${toolCall.function.arguments}');
}
}
How do I analyze images? #
Show example
Pass image URLs or base64-encoded images as content parts alongside text in a user message.
final response = await client.chat.completions.create(
ChatCompletionCreateRequest(
model: 'gpt-5.5',
messages: [
ChatMessage.user([
ContentPart.text('What is in this image?'),
ContentPart.imageUrl('https://example.com/image.jpg'),
]),
],
),
);
print(response.text);
How do I create embeddings? #
Show example
Use client.embeddings.create(...) to generate vector representations of text. You can optionally reduce dimensions for smaller storage.
final response = await client.embeddings.create(
EmbeddingRequest(
model: 'text-embedding-3-small',
input: EmbeddingInput.text('Hello, world!'),
dimensions: 256, // Optional: reduce dimensions
),
);
final vector = response.firstEmbedding;
print('Embedding dimensions: ${vector.length}');
How do I generate and edit images with GPT Image 2.5? #
Show example
Use ImageModels.gptImage25Flare for fast generation and editing, or
ImageModels.gptImage25Sunburst for precise edits and detailed creative work.
Both models support low, medium, high, xhigh, max, and auto quality,
custom sizes, and transparent backgrounds with PNG or WebP output.
import 'dart:convert';
import 'dart:io';
final response = await client.images.generate(
const ImageGenerationRequest(
model: ImageModels.gptImage25Flare,
prompt: 'A cute robot holding a flower',
size: ImageSize.custom('1536x864'),
quality: ImageQuality.xhigh,
background: ImageBackground.transparent,
outputFormat: ImageOutputFormat.webp,
),
);
// GPT image models return base64 image data.
final b64 = response.data.first.b64Json;
if (b64 != null) {
File('robot.webp').writeAsBytesSync(base64Decode(b64));
}
final edited = await client.images.editJson(
const ImageEditJsonRequest(
model: ImageModels.gptImage25Sunburst,
images: [ImageReference.url('https://example.com/source.png')],
prompt: 'Change only the flower to a sunflower',
quality: ImageQuality.max,
size: ImageSize.custom('1536x864'),
),
);
ImageGenerationRequest and multipart ImageEditRequest require an explicit
nonnull model, including for generateStream and editStream. String IDs are
forwarded unchanged; constants are conveniences rather than an allowlist.
ImageEditJsonRequest has a distinct optional/nullable model: omission keeps the
server's gpt-image-2.5-sunburst default, and an explicit ID overrides it.
Use client.images.edit(...) to upload image bytes, or editJson(...) for
up to 16 image references (URLs, data URLs, or uploaded file IDs). The streaming
variants are generateStream, editStream, and editJsonStream.
Snapshot constants are also available: gptImage25Sunburst20260908 and
gptImage25Flare20260908.
→ Full image example and local model-selection example
See the generation reference, editing reference, and migration guide.
How do I use audio? #
Show example
Use client.audio.speech.create(...) for complete audio bytes,
createByteStream(...) for audio chunks, or createStream(...) for typed speech
SSE events. All three accept abortTrigger. Requests support instructions,
all six audio formats, speed 0.25–4, thirteen built-in voice conveniences, open
voice names and custom voice references.
Buffered speech:
const speech = SpeechRequest(
model: 'gpt-4o-mini-tts',
input: 'Hello! How are you today?',
voice: SpeechVoice.marin,
instructions: 'Speak warmly and clearly.',
responseFormat: SpeechResponseFormat.mp3,
);
final audioBytes = await client.audio.speech.create(speech);
Audio chunks:
await for (final chunk in client.audio.speech.createByteStream(speech)) {
// Write or play each chunk in delivery order using the requested audio format.
print('Received ${chunk.length} audio bytes');
}
Typed SSE audio and usage:
await for (final event in client.audio.speech.createStream(speech)) {
switch (event) {
case SpeechAudioDeltaEvent():
final bytes = event.decodeAudio(); // Raw Base64 audio, without a data URL.
print('Received ${bytes.length} audio bytes');
case SpeechAudioDoneEvent():
print('Total tokens: ${event.usage.totalTokens}');
case SpeechUnknownEvent():
// Future event fields remain available in the immutable event.rawJson.
print('Received a future speech event');
}
}
AudioVoice.named('provider-voice-name') forwards an open name;
AudioVoice.custom('voice_existing_id') sends the closed {id: ...} reference.
Custom voices require eligible project access and an existing voice.
Consent management is available;
sample-derived voice creation uses an explicit
consent and audio sample.
SpeechVoice stays an enum, including the original six constants and seven new
ones. See the migration guide
for the widened SpeechRequest.voice type and exhaustive-switch updates.
An explicitly incompatible streamFormat is rejected before dispatch. Buffered
and byte methods use audio mode, while createStream selects SSE. SSE requires a
supporting model and does not work with tts-1/tts-1-hd. A stream borrows an
injected HTTP client unless streamClientFactory supplies an owned client; canceling
one stream leaves a borrowed client usable. Production streams own and dispose a
dedicated client. A valid speech.audio.done completes SSE and releases its
transport even if the HTTP body stays open; later events are ignored. An unexpected
EOF before that event reports a stream failure without replaying consumed output.
The deprecation notice
schedules tts-1, tts-1-hd and the listed mini-TTS snapshots for January 6,
2027 shutdown. The model page marks the mini-TTS family deprecated; the notice
does not separately list the alias. Moving to the recommended Realtime model
requires its Realtime workflow, rather than changing the model ID on /audio/speech.
The January 20, 2027 legacy Audio/Realtime/transcription snapshot sunset and the
February 26, 2027 file-transcription sunset are separate migrations.
Speech-to-Text:
final response = await client.audio.transcriptions.create(
TranscriptionRequest(
file: File('audio.mp3').readAsBytesSync(),
filename: 'audio.mp3',
model: 'gpt-transcribe',
languages: ['en'],
keywords: ['OpenAI'],
),
);
print('Transcription: ${response.text}');
All 14 existing file-transcription fields are forwarded, including repeated
keywords/languages, speaker names/data URLs, logprob inclusion and timestamp
options. fileContentType: 'audio/mpeg' optionally sets the file MIME header;
it is upload metadata, separate from those form fields. Requests snapshot their
bytes/lists. Use plural languages for gpt-transcribe; its singular language
is rejected, keywords cannot contain angle brackets/CR/LF, and no numeric prompt
limit is published. Only chunkingStrategy and stream permit explicit null,
which normalizes to omission as in Python; Node rejects null. False remains false,
and createStream selects true. Speaker samples are data URLs; audio format and
duration remain server-validated.
Choose transcriptions.create for JSON, createVerbose for timestamps,
createDiarized for speakers, and createRaw for text/SRT/VTT with the matching
responseFormat. These specialist modes depend on the model. All buffered methods
accept abortTrigger. createStream yields typed deltas, diarized segments and
done events plus future received variants. A valid transcript.text.done releases
the transport promptly; EOF or [DONE] before it is a stream error. Streams never
replay consumed output and cancellation preserves injected borrowed clients.
Translation to English:
final request = TranslationRequest(
file: File('audio.mp3').readAsBytesSync(),
filename: 'audio.mp3',
model: 'whisper-1',
);
final verbose = await client.audio.translations.createVerbose(request);
print('${verbose.language}: ${verbose.text}'); // Output language is English.
final subtitles = await client.audio.translations.createRaw(
request.copyWith(responseFormat: TranslationResponseFormat.vtt),
);
print(subtitles); // Raw text/subtitle whitespace is retained.
Translation create accepts JSON; verbose responses need no legacy task field,
and segments are optional. All translation methods accept abortTrigger.
Received JSON is deeply immutable and future metadata is preserved; known malformed
fields fail with safe diagnostics while explicit response data remains available.
The February 26, 2027 sunset
lists Whisper/GPT-4o file-transcription models and recommends gpt-transcribe or
gpt-live-transcribe. A replacement must support your translation, diarization,
timestamp or subtitle workflow; currently operational specialist modes remain
available. This date is separate from the January 6 TTS and January 20 legacy
snapshot notices.
→ Audio example and offline speech streaming example ($0 API cost) and offline file-audio/Chat voice example.
How do I manage voice consent recordings? #
Show example
The cached client.audio.voiceConsents resource uploads, lists, retrieves,
renames and deletes consent recordings. Production creation requires an eligible
project, api.voices.write, and a recording of the current approved consent phrase.
Keep the consent and later voice sample in the same project and from the same
person. Follow the official custom voice guide.
final consent = await client.audio.voiceConsents.create(
VoiceConsentCreateRequest(
name: 'Actor consent',
recording: audioBytes, // Uint8List from an explicitly obtained recording.
filename: 'consent.webm',
language: 'en-US',
recordingContentType: 'audio/webm;codecs=opus',
),
);
final page = await client.audio.voiceConsents.list(limit: 20);
final retrieved = await client.audio.voiceConsents.retrieve(consent.id);
final renamed = await client.audio.voiceConsents.update(
consent.id,
const VoiceConsentUpdateRequest(name: 'Updated actor label'),
);
// Select pagination and deletion explicitly in your application.
final after = page.lastId;
if (page.hasMore && after != null) {
final nextPage = await client.audio.voiceConsents.list(after: after, limit: 20);
}
if (deleteRequested) { // Your application's explicit deletion choice.
final deleted = await client.audio.voiceConsents.delete(consent.id);
}
Uploads snapshot the original bytes and normalize MIME parameters to one of the
supported base types: audio/mpeg, audio/wav, audio/x-wav, audio/ogg, audio/aac,
audio/flac, audio/webm or audio/mp4. The maximum is 10 MiB. A recognized filename
extension can supply omitted MIME metadata; other filenames need an explicit
supported recordingContentType. This does not transcode or inspect recording
content. name and the BCP 47 language are open strings; the service validates
phrases and eligibility. Sample speech minimums belong to later voice creation.
list takes only after and limit (1–100, omitted service default 20).
It returns one page without fetching more or inferring a cursor. firstId/lastId
and hasFirstId/hasLastId distinguish omitted, null and supplied cursors; check
hasMore and choose the next cursor deliberately. deleted preserves the returned
boolean, including false. Rename is a required-name JSON POST; recording bytes
cannot be changed through rename. Opaque IDs are encoded as one path segment.
All five methods accept abortTrigger and use shared auth, errors and conservative
retry: multipart creation is sent once, rename follows POST retry rules and
idempotent DELETE follows the configured shared retry policy. Models preserve
immutable receive-only future metadata while rejecting malformed known fields;
update JSON remains closed. Consent IDs, cursors, labels, recordings and echoed
headers stay private in default diagnostics and built-in logging. Explicit error
messages, response bodies, URLs and causes remain available for caller inspection.
→ Runnable offline lifecycle example
(five mock requests by default, six with explicit --delete, $0 API cost).
This manages consent recordings; custom voice creation
is a separate workflow. GET /audio/consent_phrases is documented
upstream but has no canonical typed response, so no phrase DTO is invented here.
How do I create a custom voice? #
Show example
The cached client.audio.voices resource uploads a sample through create, using
a previously obtained consent recording. Use an approved project with api.voices.write to
create consents/voices and api.voices.read to use a voice. The consent and sample
must come from the same person and project; obtain the current consent phrase
before recording. Follow the official custom voice guide.
final consent = await client.audio.voiceConsents.create(
VoiceConsentCreateRequest(
name: 'Actor consent',
recording: consentBytes, // Explicitly obtained Uint8List recording.
filename: 'consent.webm',
language: 'en-US',
recordingContentType: 'audio/webm;codecs=opus',
),
);
final voice = await client.audio.voices.create(
CustomVoiceCreateRequest(
name: 'Actor voice',
audioSample: sampleBytes, // Uint8List from the same actor/project.
filename: 'sample.webm',
consent: consent.id,
audioSampleContentType: 'audio/webm;codecs=opus',
// Optional type: 'audio_sample'; omission uses the service default.
),
);
final reference = AudioVoice.custom(voice.id);
final referenceJson = reference.toJson(); // {'id': voice.id}
Creation is a multipart POST with required name, audio_sample and consent.
The optional type accepts only audio_sample, and omitted type stays omitted.
Names contain 1–256 Unicode code points. The sample upload preserves original
bytes/views and filename, supports the same eight base MIME types as consent
uploads, normalizes browser MIME parameters, and enforces 10 MiB. A recognized
filename extension can supply omitted MIME metadata; other filenames need an
explicit supported audioSampleContentType.
Production samples need at least five seconds of actual speech and 15 transcribed tokens, with a maximum of 30 seconds. Eligibility, consent and speech content are service checks; the client does not record, transcribe, transcode or inspect audio quality. Synthetic example bytes are for offline verification only.
The returned CustomVoice validates object: audio.voice, type: audio_sample,
ID, name and integer creation time. Known malformed fields or future creation
types fail contextually; finite future metadata remains an immutable receive-only
snapshot, with typed fields authoritative. A response name has no request-only
length restriction. The reference above is caller-selected JSON; it does not make
a speech or Live request, and each consuming API retains its own voice contract.
Creation accepts abortTrigger, uses shared auth/errors/closed-client guards,
and sends the multipart body once without automatic replay. Samples, consent
context, IDs and echoed headers are private in default diagnostics and built-in
logging; explicit caller model/HTTP/error context remains readable. The API
exposes creation only, with no client voice list/retrieve/update/delete methods.
→ Runnable offline consent → voice → reference example (two mock requests, $0 API cost).
How do I signal Live sessions and control calls? #
Show example
Use the cached client.live.sessions resource for Live HTTP signaling and call
controls. Supply SDP from your application's WebRTC peer, then apply the returned
answer to that peer. Your application owns capture, playback and media transport.
final created = await client.live.sessions.create(
LiveSessionCreateRequest(
session: LiveMediaSessionCreateParams(
model: 'gpt-live-1',
instructions: 'Help the caller schedule an appointment.',
store: true,
),
transport: LiveWebRTCTransport(sdp: callerOfferSdp),
),
);
final answer = created.transport as LiveWebRTCResponseTransport;
// Apply answer.sdp to your WebRTC peer; retain created.session.id unchanged.
// Once this stored session has finished and its recording is finalized:
final wav = await client.live.sessions.downloadRecording(created.session.id);
final chunks = client.live.sessions.downloadRecordingStream(created.session.id);
final fork = await client.live.sessions.fork(
created.session.id,
LiveForkRequest(transport: LiveWebRTCTransport(sdp: newPeerOfferSdp)),
);
// Apply fork.transport.sdp to the new peer. fork.session.id is a new session.
Downloads require a finalized stored recording. Storage defaults to false,
requires project policy and is unavailable with Zero Data Retention. Finalized
recordings remain available for 30 days. WAV bytes are preserved, with input on
the left channel and output on the right. A 503 retains its original HTTP
response and Retry-After header in ApiException.cause; the client does not poll
or enable storage automatically. Stream completion, cancellation and timeouts
release owned transports while preserving an injected shared client.
For outbound SIP, supply LiveSIPTransport(destination: ..., trunk: ...) with
an E.164 destination and LiveSIPTrunk containing the provider URL, caller number
and LiveSIPTrunkAuth Digest credentials. Keep credentials on your server.
Outbound eligibility, a TLS-signaling/Opus/SDES-SRTP trunk and provider setup are
service requirements. A 201 means initialized, before the callee answers.
Outbound SIP bodies are limited to 1 MiB of serialized UTF-8 JSON. The service
limits ringing to three minutes and connected calls to two hours; these limits
cannot be configured in the request.
Each create places a new call; ambiguous timeout/connection/5xx failures are
sent once. A tracing ID does not deduplicate calls.
Verify incoming webhook bytes first and use LiveTransportIncomingWebhookEvent
data.sessionId for your chosen accept or reject action. Acceptance requires
LiveCallAcceptRequest(session: LiveCallAcceptSession(model: ...)); rejection
requires LiveCallRejectRequest(statusCode: ...) from 300 through 699.
refer(id, LiveCallReferRequest(targetUri: ...)) transfers a SIP call and
hangup(id) sends no body. Verification never accepts, rejects or transfers a
call automatically. IDs are opaque and encoded once; only recording download
applies its own documented stored-session ID pattern.
Shared startup models cover text-only history, frontend permissions, audio,
delegation and all 13 canonical Live tool input branches. Live tool configuration
is distinct from Responses tool models; wire coverage does not guarantee runtime
support for every branch. Tool selection has typed scalar modes, 12 specific
choices and an allowed_tools set of 1–128 specific choices. Optional nullable
settings preserve omission, explicit null and values; hasInstructions: true
with a null value emits null, while copyWith(clearInstructions: true) omits it.
Model, voice, initial history, audio format, startup instructions and delegation
mode are immutable after startup; LiveSessionUpdateParams changes backend
settings only. History allows 128 messages with one text part each; the 8,192
rendered-token and 16,384 frontend-instruction limits are enforced by the service.
LiveVoice supports open names and its own open custom-ID object (1–128 Unicode
characters), independently of Speech's closed custom reference. Primary WebSocket
audio configuration admits PCM16LE at 16/24 kHz or G.711 PCMA/PCMU at 8 kHz.
WebRTC/SIP negotiate media and omit audio.format. Frontend capability omission
or all allows all events; an empty selection permits none, and restrictions
do not apply to trusted sideband connections. Primary/sideband WebSocket
connections, stored WebSocket forks and transcript helpers are available below.
Models retain finite immutable open JSON and private caller-readable wire data. Default diagnostics and built-in logging redact SDP, credentials, audio, instructions, transcripts, identifiers and future private metadata.
→ Runnable offline Live HTTP example
(seven default mock requests, $0 API cost). Add --accept-incoming or
--reject-incoming to choose an incoming action, or --recording-not-ready to
inspect a simulated 503. See the Live guide
and SIP setup.
How do I use primary and sideband Live WebSockets? #
Show example
Open a fresh primary connection with client.live.connect(). It sends no startup
automatically and adds no model query. Install your event listeners, explicitly
start the session, and await session.started before sending application work:
final connection = await client.live.connect();
final captions = connection.events.listen((event) {
if (event is LiveInputTranscriptDelta) {
print(event.delta);
}
});
try {
await connection.start(
LiveSessionStartEvent(
session: LiveSessionCreateParams(
model: 'gpt-live-1',
audio: LiveInitialSessionAudioParam(
format: LiveAudioFormat.pcm(rate: 24000),
),
),
),
);
// Supply raw Base64 from caller-owned mono PCM16LE capture at 24 kHz.
connection.send(LiveInputAudioAppendEvent(audio: base64Pcm16Audio));
final finalized = await connection.closeSession(
timeout: const Duration(seconds: 15),
);
print(finalized.usage.seconds);
} finally {
await captions.cancel();
await connection.close();
}
client.live.attach(sessionId, gracefulClose: true) attaches a trusted sideband
to an existing WebRTC/SIP session. Its writer accepts nine shared commands and
excludes session.start and audio append; primary writers accept eleven. Both
use the complete 22-event received hierarchy, including reflected sideband audio,
DTMF notifications and SIP progress, with immutable raw fallback for future event
types. This resolves the narrower canonical/SDK sideband-union discrepancy.
Reflected sideband audio is mono PCM16LE at 24 kHz; output frames include
start_ms/end_ms. Keep original bytes, delivery order and timeline gaps.
LiveResponseEvent.event retains any finite nested object, including compact
Responses snapshots and objects without type. Correlate its outer
delegationId with delegation metadata; omission/null remain distinct. The Live
backend accepts LiveInputItem adapters for every declared canonical input branch,
including item shapes the ordinary Responses Item helper cannot represent.
For function requests, designate one application action owner, submit every
pending result with LiveResponseItemCreateParam, then explicitly send one
LiveResponseCreateParam. There is no item-create success acknowledgment to
await. Client delegation instead uses transcripts/application state and explicit
thinking/commentary/instruction appends with the received delegation ID.
closeSession() stops new work, sends one close request, drains until
LiveSessionClosed, and releases owned resources within bounded waits.
isFinalized confirms that event; a socket close alone leaves finalization
unconfirmed. latestUsageSeconds replaces cumulative snapshots rather than
summing them. Keep delegated backend token usage separately. close() releases
the local transport without requesting session finalization. Concurrent event
taps may observe captions and application work; canceling one tap does not close
the connection. There is no automatic reconnect, startup/audio/tool replay or
external action runner. SIP progress may replay only the preceding three seconds
with original IDs; application deduplication is explicit.
Default browser sockets reject every configured handshake header before asking
for credentials. Use trusted server signaling and caller-owned WebRTC media/data
channels, or an explicitly injected backend proxy connector. Live has no client
secret endpoint. Wrap an HTTP-started data channel with
LivePrimaryConnection(adapter, sessionAlreadyStarted: true, ownsSocket: false);
this admits application commands while forbidding duplicate startup and audio
append. Supply initialSession when its resolved configuration is known. The
caller retains ownership of the adapter and media.
For a broadcast typed channel, LiveConnection.dataChannel(callerChannel)
returns an already-started primary writer and owns only its message tap.
→ Runnable offline primary/sideband example demonstrates concurrent observers, manual delegation, two function results before one continuation and confirmed usage, using synthetic peers at $0 API cost. See WebSockets, server controls and session management.
How do I fork stored Live sessions and group captions? #
Show example
Finish the original with store: true, wait for session.closed, and retain its
ID and your application task state. Storage must be enabled and permitted for
the project; ZDR disables it. A WebSocket fork gets a new connection and ID:
final fork = await client.live.forkConnection(sourceSessionId);
final captions = LiveTranscriptGrouper();
final captionTextById = <String, String>{};
final updates = captions.updates.listen((update) {
// Each update is a complete snapshot. Replace text instead of appending it.
captionTextById[update.segment.id] = update.segment.text;
});
final attachment = captions.attach(fork);
try {
final started = await fork.start(
LiveForkSessionStartEvent(session: LiveForkSessionConfigParam()),
);
print(started.session.id); // Save the new ID for controls and sidebands.
// Receive application events on an independent fork.events tap. Explicitly
// finish backend work before requesting session finalization.
final ended = await fork.closeSession();
print(ended.usage.seconds);
} finally {
await attachment.detach();
captions.close();
await updates.cancel();
await fork.close();
}
The empty startup object inherits model, voice, frontend instructions and saved
history. New WebSocket writers admit the documented store, Responses backend
settings and audio.format overrides; WebRTC client permissions and unknown
startup/audio override keys are rejected. The new connection defaults to PCM16
24 kHz independently of the source format. It cannot change delegation ownership.
The shared HTTP fork DTO retains its canonical open metadata contract. A fork
requires a finalized recording; it does not restore or replay external actions.
Check the outcome of previously submitted work before deliberately routing current
results to the new delegation and session.
Dispatch a LiveResponseEvent with LiveResponsesEvent.fromLiveEvent(event).
LiveResponsesLifecycleEvent.response is a sparse LiveCompactResponse view,
preserving omission/null and future data. LiveResponsesGranularEvent.granularEvent
uses the ordinary granular codec where compatible. Future nested types, and
source-valid child shapes the ordinary codec cannot represent, remain lossless
LiveResponsesRawEvent values. Known malformed fields fail contextually. The
adapter's type is the nested discriminator; source.type and toJson() retain
the complete outer response.event frame and correlation.
Keep a call ledger from granular events, not empty lifecycle output/tools.
Collect every active call through the backend generation's final event, authorize
and submit all results, then send one continuation without an item-create ack
wait. Canceled or stale application outcomes need an explicit policy. Backend
completion, Live finalization and audible playback have separate lifetimes.
Grouping follows the SDK speaker/backchannel policy with defaults of 500 ms turn
separation, 2,000 ms assistant inactivity, 1,000 ms backchannel duration and
2,000 ms isolation, plus a 50 ms speaker settle window. Duplicate IDs are ignored;
timestamp resets end the previous local epoch. Some acknowledgments are suppressed,
so keep raw event taps for a lossless transcript. Segment IDs and close reasons
are local projection values, not server turns or voice activity detection.
flush(), reset(clearSegments: ...), close() and attachment detachment are
explicit. Each helper owns only its timer and tap; application listeners and other
groupers keep working. isSessionFinalized requires observing session.closed.
LiveTranscriptGrouping offers a pure explicit-time policy. The Dart convenience
projectLiveTranscriptPlayback(segment, sourceAnchorMs: ..., playbackAnchorMs: ..., rate: ...) maps source timing to a caller-supplied playback clock; it schedules no
audio and confirms no audible completion. Hardware playback remains caller-owned.
Reconnect, unsent queues, raw transport escape hatches and full SDK media helpers
remain separate inventory.
The transcript helpers include upstream attribution and license notices.
→ Runnable offline fork/transcript example demonstrates both delegation modes, finalized originals, two mock WAV downloads, inherited state, interleaved/canceled call IDs, caption taps and borrowed-channel cleanup at $0 cost. See stored session management and delegation.
How do I use the Realtime API? #
Show example
The Realtime API supports two transports: WebSocket for persistent bidirectional streaming, and WebRTC for browser-friendly audio sessions.
WebSocket:
import 'package:openai_dart/openai_dart.dart';
import 'package:openai_dart/openai_dart_realtime.dart' as realtime;
final client = OpenAIClient.fromEnvironment();
// Connect to a realtime session via WebSocket
final session = await client.realtime.connect(
model: 'gpt-realtime-2',
config: const realtime.RealtimeSessionCreateRequest(
model: 'gpt-realtime-2',
audio: realtime.RealtimeAudioConfig(
output: realtime.RealtimeAudioConfigOutput(voice: 'alloy'),
),
instructions: 'You are a helpful assistant.',
),
);
// Send a user text message and process events until the response is complete.
// (Use `session.appendAudioBytes(rawPcmBytes)` to stream raw audio instead.)
session.sendUserMessage('Say hello and nothing else.');
await for (final event in session.events) {
switch (event) {
case realtime.SessionCreatedEvent(:final session):
print('Session created: ${session.id}');
case realtime.ResponseTextDeltaEvent(:final delta):
stdout.write(delta);
case realtime.ResponseDoneEvent():
await session.close();
case realtime.ErrorEvent(:final error):
print('Error: ${error.message}');
await session.close();
default:
break;
}
}
client.close();
WebRTC:
Note: For WebRTC peer connections in Flutter, use the
flutter_webrtcpackage.
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:openai_dart/openai_dart.dart';
import 'package:openai_dart/openai_dart_realtime.dart' as realtime;
final client = OpenAIClient.fromEnvironment();
// 1. Create a peer connection and generate an SDP offer
final pc = await createPeerConnection({'iceServers': []});
final offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 2. Send the SDP offer to OpenAI and get the SDP answer
final sdpAnswer = await client.realtimeSessions.calls.create(
realtime.RealtimeCallCreateRequest(
sdp: offer.sdp!,
session: const realtime.RealtimeSessionCreateRequest(
model: 'gpt-realtime-2',
audio: realtime.RealtimeAudioConfig(
output: realtime.RealtimeAudioConfigOutput(voice: 'alloy'),
),
),
),
);
// 3. Set the SDP answer to complete the WebRTC handshake
await pc.setRemoteDescription(RTCSessionDescription(sdpAnswer, 'answer'));
// Call management operations (callId is obtained from your SIP/telephony layer)
const callId = 'call_xxx';
// Accept the call (optionally override the session configuration on accept).
await client.realtimeSessions.calls.accept(
callId,
request: const realtime.RealtimeSessionCreateRequest(
model: 'gpt-realtime-2',
audio: realtime.RealtimeAudioConfig(
output: realtime.RealtimeAudioConfigOutput(voice: 'alloy'),
),
instructions: 'Greet the caller in English.',
),
);
await client.realtimeSessions.calls.hangup(callId);
await client.realtimeSessions.calls.refer(
callId,
realtime.RealtimeCallReferRequest(targetUri: 'tel:+14155550123'),
);
await client.realtimeSessions.calls.reject(
callId,
request: realtime.RealtimeCallRejectRequest(statusCode: 486),
);
client.close();
How do I generate videos? #
Show example
Use client.videos.create(...) to generate videos from text prompts with Sora. Poll with retrieve until the video is complete, then download the content.
final video = await client.videos.create(
CreateVideoRequest(
prompt: 'A cat playing piano in a jazz club',
model: 'sora-2',
size: VideoSize.size1280x720,
seconds: VideoSeconds.s8,
),
);
final status = await client.videos.retrieve(video.id);
if (status.isCompleted) {
final content = await client.videos.retrieveContent(video.id);
}
How do I manage files? #
Show example
Use the files API to upload, list, and retrieve file content. Files are used for fine-tuning, batches, and other workflows.
final file = await client.files.upload(
bytes: fileBytes,
filename: 'training.jsonl',
purpose: FilePurpose.fineTune,
);
final files = await client.files.list();
final content = await client.files.retrieveContent(file.id);
How do I fine-tune a model? #
Show example
Create a fine-tuning job by specifying a base model and a training file. Poll the job status to monitor progress and retrieve the fine-tuned model name when complete.
final job = await client.fineTuning.jobs.create(
CreateFineTuningJobRequest(
model: 'gpt-4o-mini-2024-07-18',
trainingFile: 'file-abc123',
),
);
final status = await client.fineTuning.jobs.retrieve(job.id);
print('Fine-tuned model: ${status.fineTunedModel}');
How do I use batch processing? #
Show example
Batches let you queue many requests for asynchronous processing at lower cost. Create a batch from an input file and poll for completion.
final batch = await client.batches.create(
CreateBatchRequest(
inputFileId: 'file-abc123',
endpoint: BatchEndpoint.chatCompletions,
completionWindow: CompletionWindow.hours24,
),
);
final status = await client.batches.retrieve(batch.id);
print('Status: ${status.status}');
How do I evaluate models? #
Show example
Use the evals API to create evaluation definitions with grading criteria, then run them against test data to measure model performance.
final eval = await client.evals.create(
CreateEvalRequest(
name: 'My Evaluation',
dataSourceConfig: EvalDataSourceConfig.custom(
itemSchema: {
'type': 'object',
'properties': {
'prompt': {'type': 'string'},
'expected': {'type': 'string'},
},
},
),
testingCriteria: [
EvalGrader.stringCheck(
name: 'matches_expected',
input: '{{sample.output_text}}',
operation: StringCheckOperation.ilike,
reference: '%{{item.expected}}%',
),
],
),
);
final run = await client.evals.runs.create(
eval.id,
CreateEvalRunRequest(
dataSource: EvalRunDataSource.jsonlContent([
{'prompt': 'Say hello', 'expected': 'hello'},
]),
),
);
How do I moderate content? #
Show example
Use the moderations API to check whether text violates content policies. The result indicates whether the input was flagged.
final result = await client.moderations.create(
ModerationRequest(
input: ModerationInput.text('Check this text'),
),
);
print('Flagged: ${result.results.first.flagged}');
How do I check content provenance? #
Show example
Use the content provenance checks API to detect OpenAI C2PA/SynthID signals in an uploaded image or audio file. Results are a sealed union — dispatch on the concrete type to read the fields specific to each signal.
final check = await client.contentProvenanceChecks.create(
bytes: File('image.png').readAsBytesSync(),
filename: 'image.png',
);
for (final result in check.results) {
switch (result) {
case C2PAProvenanceResult():
print('C2PA: ${result.outcome}, trust: ${result.validationState}');
case SynthIDProvenanceResult():
print('SynthID: ${result.outcome}');
case UnknownProvenanceResult():
print('Unrecognized signal: ${result.type}');
}
}
How do I receive signed webhooks? #
Use the original request bytes and headers with WebhookVerifier.unwrapBytes.
The standalone verifier needs only a signing secret. On a trusted receiver,
WebhookVerifier.fromEnvironment() reads OPENAI_WEBHOOK_SECRET without requiring
an API key. An existing client also exposes these local methods through
client.webhooks, using OpenAIConfig(webhookSecret: ...) or a per-call secret.
Local verification remains available after the client closes.
import 'package:openai_dart/openai_dart.dart';
WebhookEvent receiveDelivery(
List<int> originalBytes,
Map<String, String> headers,
String signingSecret,
) {
final verifier = WebhookVerifier(secret: signingSecret);
// Verification happens before UTF-8, JSON or model parsing.
return verifier.unwrapBytes(originalBytes, headers);
}
The 26 received event variants include Responses, Batch, Evals, fine-tuning,
Agent sessions, incoming Live/Realtime calls and safety notifications. Dispatch on
concrete types such as ResponseCompletedWebhookEvent. Future event types,
including video notifications without a published inbound schema, become
UnknownWebhookEvent and retain their raw JSON. Parsed known events also preserve
finite future metadata. Replacing a typed child uses that child's complete JSON;
metadata from the previous child is not restored. Optional object stays omitted
when absent, while Agent and safety events require object: "event".
verifySignature/unwrap accept an untouched string; their Bytes counterparts
preserve exact original bytes. Required signed headers are case-insensitive,
nonempty and unambiguous. A whsec_ secret uses canonical padded standard Base64;
other secrets are literal UTF-8 keys. Signature candidates accept v1,<Base64> or
bare canonical Base64, including rotation lists. The default five-minute timestamp
tolerance has inclusive past/future bounds. Timestamp text must be ASCII decimal
in 0..2^53−1; leading zeros retain their signed spelling. These strict policies
intentionally reject the loose timestamp/Base64 coercions used by some official
SDK helpers. An empty per-call secret fails instead of falling back.
Catch InvalidWebhookSignatureException for invalid signatures/timestamps,
FormatException for authenticated malformed event bodies, and ArgumentError
for invalid secret/tolerance configuration. This signature exception is separate
from HTTP OpenAIException. The verifier performs no automatic acknowledgment,
deduplication, API lookup or workflow action. Applications persist/queue their own
work and acknowledge promptly. Return a 2xx status for successful receipt. The
provider retries failed or timed-out deliveries for up to 72 hours with exponential
backoff and treats 3xx redirects as failures. Duplicate deliveries can use the
authenticated webhook-id as an idempotency key. Safety webhook contracts also
specify 410 Gone as a signal to stop retries. These are delivery policies owned
by the receiver, with no SDK scheduler. Signing secrets belong on trusted infrastructure;
JavaScript/Wasm runtime support does not make browser deployment of secrets safe.
→ Runnable offline signed receiver, which sends three loopback deliveries (valid, duplicate, tampered) and makes no API calls. See the official Webhooks guide.
How do I investigate verified safety notifications? #
Verify the original delivery bytes, then use data.id for an explicit lookup.
Project alerts need api.safety.alerts.read; organization cases need
api.safety.read on a key for the notified organization. Select separately scoped
clients on trusted infrastructure.
import 'package:openai_dart/openai_dart.dart';
Future<void> inspectSafetyNotice(
List<int> originalBytes,
Map<String, String> headers,
String signingSecret,
OpenAIClient projectClient,
OpenAIClient organizationClient,
) async {
final event = WebhookVerifier(
secret: signingSecret,
).unwrapBytes(originalBytes, headers);
switch (event) {
case SafetyAlertCreatedWebhookEvent():
final alert = await projectClient.safety.alerts.retrieve(event.data.id);
print('Block registered: ${alert.requestPaused}');
case SafetyWarningIssuedWebhookEvent(:final data) ||
SafetyDeactivationIssuedWebhookEvent(:final data):
final safetyCase = await organizationClient.safety.cases.retrieve(
data.id,
);
print('Notice: ${safetyCase.notice.type.name}');
case SafetyOrgAlertCreatedWebhookEvent():
print(
'Workspace notice requires a separately scoped administrator lookup.',
);
default:
break;
}
}
event.id identifies the notification. data.id identifies the alert or case;
entityIdentifier is the application's safety identifier. requestPaused reports
block registration, without confirming execution stopped or earlier effects were
reversed. Both detail types retain required reason: null; alert reasons can be
null for Zero Data Retention requests. The models preserve open response metadata
in deeply immutable rawJson. Future received enum strings use unknown with
rawErrorType or rawType, preserving their exact spelling on serialization.
Model diagnostics redact reasons, explanations, identifiers and future values.
Project alerts also expose optional nullable detailedExplanation with
hasDetailedExplanation to distinguish an omitted key from an explicit null.
Empty text remains a present value. The service temporarily provides generated
explanations for eligible Zero Data Retention alerts and omits them when
unavailable. A null reason does not establish eligibility or availability.
import 'package:openai_dart/openai_dart.dart';
Future<void> inspectSafetyExplanation(
OpenAIClient projectClient,
String alertId,
) async {
final alert = await projectClient.safety.alerts.retrieve(alertId);
switch ((alert.hasDetailedExplanation, alert.detailedExplanation)) {
case (false, _):
print('Explanation omitted by the service.');
case (true, null):
print('Explanation explicitly null.');
case (true, final String explanation):
// Access the actual text explicitly; default model diagnostics redact it.
print('Explanation received: ${explanation.length} characters.');
}
final explicitNull = alert.copyWith(detailedExplanation: null);
final omitted = alert.copyWith(hasDetailedExplanation: false);
print('Null retained: ${explicitNull.hasDetailedExplanation}');
print('Key omitted: ${!omitted.hasDetailedExplanation}');
}
copyWith() preserves the value and presence. Explicit null retains the key;
hasDetailedExplanation: false removes it from serialized JSON. Known raw
values are validated and cannot override explicit typed
fields or resurrect a cleared explanation. rawJson remains a detached received
snapshot and can still contain the original explanation; toJson() represents
the effective typed fields. Constructor omission can adopt a valid explanation
from legacy raw-only construction. This response field is separate from the
nonnull optional explanation in Responses monitoring errors.
→ Runnable offline explanation example: four explicit mock GETs cover absent, null, text and empty text, plus copy/clear and private diagnostics. No API key or charges. See the official Safety alert retrieve reference.
These are two GET operations with no list or enforcement action. ID maxima are 38 and 128 Unicode characters respectively; IDs are encoded once. Empty and dot segments are rejected because they cannot address an individual resource through Dart's URI path normalization. Normal HTTP exceptions and request IDs apply.
Workspace safety.org_alert.created uses
https://api.chatgpt.com/v1/safety/alerts/{id} with a workspace administrator key
and chatgpt.enterprise.safety_alerts.read. That separate lookup remains in the
Administration inventory. The webhook parser performs no automatic GET.
→ Runnable offline safety example: two scoped MockClient GETs after signature verification, duplicate/tampered delivery checks, and explicit workspace routing. No API key or charges. Applications own durable queuing, prompt acknowledgment and deduplication before background investigation. See Misalignment monitoring and Safety enforcement notifications.
How do I inspect monitoring failures? #
HTTP failures expose passive ApiException.misalignment, including a 403 received
before streaming starts. Failed Responses expose response.error?.misalignment;
WebSocket errors use event.error.misalignment. All share
ResponsesMisalignmentDetails and ResponsesMisalignmentSteer, retaining their
existing WebSocket names and direct imports.
import 'package:openai_dart/openai_dart.dart';
Future<void> inspectMonitoringFailure(OpenAIClient client) async {
try {
await for (final event in client.responses.createStream(
const CreateResponseRequest(
model: 'gpt-6-sol',
input: ResponseInput.text('Run the requested task.'),
),
)) {
switch (event) {
case ErrorEvent():
print('Flat error: code present=${event.hasCode}; '
'code is null=${event.code == null}.');
case ResponseFailedEvent(:final response):
print('Failed response: monitoring details present='
'${response.error?.misalignment != null}.');
default:
break;
}
}
} on PermissionDeniedException catch (error) {
print('HTTP ${error.statusCode}: monitoring details present='
'${error.misalignment != null}.');
// Original code, requestId and body remain readable for trusted investigation.
}
}
Details retain an open errorType, optional explanation/steer and an opaque
nullable reviewTarget; hasReviewTarget distinguishes omission from explicit
null. Nonnull targets require 1–96 ASCII letters, digits or ._~:- characters,
without newlines. Parsed future
metadata is finite and deeply immutable. HTTP optional malformed details return
null while preserving the original exception/status/code/request ID/body and retry
classification; malformed supplied details on failed Responses or WS models fail
contextually. Default diagnostics and automatic monitoring response logs redact
sensitive explanations, instructions, tokens and identifiers. Caller-readable
values remain intact; avoid printing raw bodies or opaque strings.
Flat SSE ErrorEvent keeps nullable code/param and sequence presence; it does
not declare typed misalignment or headers. Failed ResponseError keeps its
distinct code/message/misalignment shape and emits no invented legacy type/param.
Legacy input/const constructor compatibility and the targeted output corrections
are described in the migration guide.
A policy 403 is not retried. Inspect failures without automatic reconnect,
replay, tool execution or continuation; one blocked WS response does not close
other multiplexed lanes. Existing opt-in recovery for unrelated transport failures
remains available. Use IDs from trusted verified notices for separately scoped
alert/case retrieval as shown above. reviewTarget is not a safety resource ID,
and steer.message is not an instruction to execute. The API offers no generic
resume/unblock method or monitoring configuration parameter.
→ Runnable offline monitoring example: HTTP 403 before output, flat error/failed response after output, then two explicit scoped GETs from signed notices. Five MockClient requests, no real API key or charges.
How do I manage webhook endpoints? #
The authenticated project API exposes client.webhooks.create, list, retrieve,
update, delete, rotateSecret and test, plus eventTypes.list() to discover webhook event types.
Create/update/test use the 23 canonical WebhookEventType choices, including
video. Returned endpoint/discovery strings stay open to future values. Discovery
alone does not make a future string admissible in a typed writable request.
import 'package:openai_dart/openai_dart.dart';
Future<int> configureProjectWebhook(
OpenAIClient client,
Future<void> Function(String endpointId, String secret) saveSecret,
) async {
final created = await client.webhooks.create(WebhookEndpointCreateRequest(
name: 'Completed responses',
url: 'https://receiver.example/webhook',
eventTypes: const [WebhookEventType.responseCompleted],
));
await saveSecret(created.id, created.signingSecret);
final rotated = await client.webhooks.rotateSecret(created.id,
request: WebhookEndpointRotateSecretRequest(
keepOldSecretActiveFor24Hours: true,
),
);
await saveSecret(rotated.id, rotated.signingSecret);
final tested = await client.webhooks.test(created.id,
WebhookEndpointTestRequest(eventType: WebhookEventType.responseCompleted),
);
// success:true means the test request completed. Check the receiver separately.
return tested.statusCode;
}
Create requires a 1–256 character name, an HTTPS-prefixed URL of at most 2,048
characters and a nonempty event list. Update uses POST, accepts any subset including
WebhookEndpointUpdateRequest() and replaces the complete event set when supplied.
Use list(limit: ..., after: ...) with the returned lastId cursor and hasMore;
there is no inferred cursor, before or ordering parameter. Limits are 1–100,
with server default 20. Required nullable signing hints/page cursors remain explicit
nulls, while optional updatedAt stays omitted when absent.
Create/rotate return WebhookEndpointWithSecret. Store the returned key securely;
ordinary endpoint retrieval never returns it. Rotation with no request body or
with the default false option invalidates the old key immediately. Explicit true
allows a 24-hour overlap. Applications coordinate stored keys and receiver updates;
rotation does not mutate OpenAIConfig.webhookSecret. test sends a real delivery:
its success: true result may still report a 4xx/5xx statusCode from the receiver.
Endpoint HTTP methods use normal auth, cancellation, closed-client checks and retry
policy; local verification remains independent of HTTP/client lifetime.
Built-in response logging redacts signing_secret structurally before truncation,
including nested/escaped keys and short/raw-looking values. Safe default error
messages and diagnostics omit secret values; raw response/model JSON and exception
body remain available to the caller. Error codes/types/parameters and quota retry
classification remain intact. As with existing client logging, configure
hierarchicalLoggingEnabled = true when setting a nonroot client logger level.
Custom logging of raw responses or toJson() remains caller-owned.
→ Offline endpoint lifecycle example, which uses MockClient for all eight operations, returned cursor/event discovery, secret storage/rotation and a completed test with receiver status 500. It makes no API calls and sends no external test delivery. See the official Webhooks reference.
Error Handling #
Handle retries, validation failures, and request aborts
openai_dart throws typed exceptions so retry logic and validation handling stay explicit. Catch ApiException and its subclasses first, then fall back to OpenAIException for other transport or parsing failures.
import 'dart:io';
import 'package:openai_dart/openai_dart.dart';
Future<void> main() async {
final client = OpenAIClient.fromEnvironment();
try {
await client.responses.create(
CreateResponseRequest(
model: 'gpt-5.5',
input: ResponseInput.text('Ping'),
),
);
} on RateLimitException catch (error) {
stderr.writeln('Rate/quota error ${error.code ?? error.type}: ${error.message}');
stderr.writeln('Server retry hint: ${error.retryAfter}');
// Billing/spend/quota errors require action before resending.
} on InternalServerException catch (error) {
stderr.writeln('Server error ${error.statusCode}; hint: ${error.retryAfter}');
} on ApiException catch (error) {
stderr.writeln('OpenAI API error ${error.statusCode}: ${error.message}');
} on OpenAIException catch (error) {
stderr.writeln('OpenAI client error: $error');
} finally {
client.close();
}
}
Regular cloneable requests retry transient 429 responses and retry
5xx/timeouts/connections only for idempotent methods. Structured billing/spend/quota
429 codes and insufficient_quota type require action and stop automatic retries.
Unknown provider 429 bodies retain the existing fallback. POST 5xx, multipart
requests, and streams are not automatically replayed.
retry-after-ms takes precedence when valid; otherwise Retry-After accepts
seconds or HTTP dates. Fractional hints remain precise, scheduled waits round
up, and past dates mean zero. A valid hint above twice RetryPolicy.maxDelay
returns the original error immediately with its full retryAfter; shorter
automatic replay would violate the server minimum. This hint is exposed on
both RateLimitException and InternalServerException, including before a
stream starts. maxRetries: 0 disables automatic retries for application-owned
retry loops. Cancellation can interrupt permitted waits.
→ Error-handling example and local retry/quota example
See the official retry guidance and migration guide for exact boundaries.
Examples #
See the example/ directory for complete examples:
| Example | Description |
|---|---|
agent_environments_example.dart |
Offline template CRUD and owned hosted environment prewarming, pagination and safe inspection |
vaults_example.dart |
Offline Vault CRUD and write-only credential create/rotate with safe inspection |
agent_session_history_example.dart |
Offline root/turn history and published OTLP trace pagination |
agent_sessions_example.dart |
Offline durable sessions, persistent observation, manual function result and explicit cancellation |
saved_agents_example.dart |
Offline saved-agent CRUD, all six persisted tools, pagination, replacement and clear/reset |
monitoring_errors_example.dart |
Offline typed HTTP/SSE monitoring failures and explicit scoped investigation |
safety_example.dart |
Offline verified notifications and separately scoped alert/case retrieval |
safety_explanations_example.dart |
Offline project alert explanation presence, copy/clear and private diagnostics |
webhook_endpoints_example.dart |
Offline project endpoint lifecycle, pagination, discovery, rotation and test status |
webhooks_example.dart |
Offline signed receiver, acknowledgment and caller-owned deduplication |
chat_example.dart |
Chat completions, multi-turn conversations, and legacy cache retention |
streaming_example.dart |
Content streaming, detailed final usage, and obfuscation controls |
tool_calling_example.dart |
Function calling with tool definitions |
vision_example.dart |
Image analysis with vision models |
responses_example.dart |
Responses API with built-in tools |
async_tools_example.dart |
Local async function/custom jobs, original call IDs and latest-response continuation |
prompt_cache_example.dart |
Responses prewarming, comparison diagnostics, and narrow Chat cache options |
decisions_example.dart |
Typed Decisions questions, refusals, usage, and inline images |
decision_image_urls_example.dart |
Offline HTTP(S)/data image references, exact mixed input and one mock POST |
embeddings_example.dart |
Text embeddings with dimension control |
image_model_selection_example.dart |
Local generation/multipart/JSON-edit model contracts without API calls |
images_example.dart |
GPT Image generation |
videos_example.dart |
Sora video generation, editing, and extension |
audio_example.dart |
Text-to-speech and transcription |
speech_streaming_example.dart |
Offline buffered/byte/SSE speech, voice references and usage |
existing_audio_example.dart |
Offline modern file fields, verbose/raw translation, open/custom Chat voices and AAC |
voice_consents_example.dart |
Offline upload/list/retrieve/rename/delete consent lifecycle and explicit pagination |
voices_example.dart |
Offline explicit consent and sample upload, then caller-selected custom voice reference |
live_http_example.dart |
Offline WebRTC/SIP signaling, explicit call controls, verified incoming notice, WAV downloads and REST fork |
live_websocket_example.dart |
Offline primary/sideband Live, concurrent event taps, manual delegation and graceful finalization |
live_workflows_example.dart |
Offline stored forks, inherited state, compact Responses, caption grouping and borrowed-channel cleanup |
chat_audio_example.dart |
Chat audio output, ID-only replay, and partial stream accumulation |
files_example.dart |
File upload and management |
conversations_example.dart |
Conversations API for state management |
containers_example.dart |
Container memory/network configuration, Code Interpreter IDs, and files |
chatkit_example.dart |
ChatKit sessions and threads |
assistants_example.dart |
Assistants API (deprecated) |
evals_example.dart |
Model evaluation and testing |
retry_guidance_example.dart |
Local transient, permanent-quota, and long-hint scenarios without API calls |
error_handling_example.dart |
Exception handling patterns |
models_example.dart |
Model listing and retrieval |
batches_example.dart |
Batch processing for async jobs |
moderation_example.dart |
Content moderation |
content_provenance_checks_example.dart |
Content provenance (C2PA/SynthID) detection |
web_search_example.dart |
Web search with Responses API |
web_search_controls_example.dart |
Local GA filters, image results, sources, and REST/SSE parsing without API calls |
shell_tools_example.dart |
Offline hosted configuration, synthetic local continuation, and typed shell stream events |
compaction_progress_example.dart |
Offline nonterminal compaction progress and opaque final output preservation |
access_programs_example.dart |
Offline access-program selection, server defaults and effective returned metadata |
tool_search_example.dart |
Offline client tool-search continuation with the original call ID and complete discovered definitions |
responses_websocket_example.dart |
Offline warm-up, two persistent lanes and incremental continuation with awaited cleanup |
realtime_example.dart |
Realtime API (WebSocket and WebRTC) |
fine_tuning_example.dart |
Fine-tuning job management |
completions_example.dart |
Legacy completions API |
uploads_example.dart |
Large file multipart uploads |
skills_example.dart |
Skills management |
input_tokens_example.dart |
Input token counting |
openai_dart_example.dart |
Quick-start overview |
API Coverage #
| API | Status |
|---|---|
| Agents | Saved agent CRUD and durable session CRUD, JSON/SSE creation, persistent event observation and manual inputs; root/turn history and published OTLP traces; owned hosted environments and template CRUD; live files and published artifacts; child inspection and item/turn history |
| Vaults | Vault CRUD and write-only OAuth, bearer and hosted environment-variable credential management |
| Chat Completions | Supported; stored-completion management pending |
| Responses API | Supported with persistent WebSockets, mid-turn steering, opt-in recovery and beta tool-result injection; additional tool/configuration details pending |
| Decisions API | ✅ Full |
| Embeddings | ✅ Full |
| Images | ✅ Full |
| Videos (Sora) | ✅ Full |
| Audio (Speech, Transcription, Translation, Custom Voices) | Buffered/streamed speech and file transcription; explicit JSON/verbose/raw translation, current options/open/custom references, all five consent operations and sample-derived voice creation; guide-only consent phrase lookup remains untyped |
| Live | All seven HTTP operations, recordings, shared configuration/tools, primary/sideband/fork WebSockets, complete event codecs, compact Responses dispatch and transcript helpers; reconnect/queue/media convenience parity remains inventoried |
| Files | ✅ Full |
| Uploads | ✅ Full |
| Batches | ✅ Full |
| Models | ✅ Full |
| Moderations | ✅ Full |
| Fine-tuning | Supported; some job/grader/checkpoint operations pending |
| Evals | ✅ Full |
| Conversations | ✅ Full |
| Containers | Supported with memory, network policies/domain secrets, and skill configuration |
| Content Provenance Checks | ✅ Full |
| ChatKit Beta | ✅ Full |
| Realtime | ✅ Full (separate import) |
| Webhooks | Local signed verification, 26 typed received events and all eight project endpoint/discovery operations |
| Safety | Read-only project alerts with typed explanation presence, organization cases and shared HTTP/Responses/WS monitoring details; workspace lookup remains separate |
| Assistants (Deprecated) | ✅ Full (separate import) |
| Threads (Deprecated) | ✅ Full (separate import) |
| Messages (Deprecated) | ✅ Full (separate import) |
| Runs (Deprecated) | ✅ Full (separate import) |
| Vector Stores (Deprecated) | ✅ Full (separate import) |
| Completions (Legacy) | ✅ Full |
Remaining Agents and Vaults operations belong to the bounded API alignment roadmap. Administration and retained SDK transport/media conveniences are deferred inventory.
Official Documentation #
Sponsor #
If these packages are useful to you or your company, please consider sponsoring the project. Development and maintenance are provided to the community for free, but integration tests against real APIs and the tooling required to build and verify releases still have real costs. Your support, at any level, helps keep these packages maintained and free for the Dart & Flutter community.
License #
This package is licensed under the MIT License.
This is a community-maintained package and is not affiliated with or endorsed by OpenAI.