---
url: 'https://adk.nht.io/batteries/llm/openai.md'
description: >-
  The any-endpoint wire adapter: cloud, self-hosted, or proxy — anything
  speaking the OpenAI Chat Completions shape. The boring, reliable one, because
  someone else runs the model. Its trade-off is exactly that dependency.
---

# OpenAI Chat Completions

## LLM summary — OpenAI Chat Completions battery

* [`OpenAIChatCompletionsAdapter`](https://adk.nht.io/api/@nhtio/adk/batteries/llm/openai_chat_completions/adapter/classes/OpenAIChatCompletionsAdapter) from `@nhtio/adk/batteries/llm/openai_chat_completions`. A complete [`DispatchExecutorFn`](https://adk.nht.io/api/@nhtio/adk/dispatch_runner/type-aliases/DispatchExecutorFn) for any endpoint speaking the OpenAI Chat Completions wire shape — cloud APIs, self-hosted Chat-Completions-compatible servers, proxy gateways.
* Required: `model`. Common: `apiKey`, `baseURL`, `headers`, `stream`, `retry`, `requestTimeoutMs`, `streamIdleTimeoutMs`, plus the shared ADK-control surface (`bucketOrder`, `contextWindow`, `thoughtSurfacing`, `tokenEncoding`, `unsupportedMediaPolicy`, …). Full list: [Assembly → LLM batteries](/assembly/batteries-llm).
* WIRE-FORMAT battery: structured `tool_calls` come back on the wire (no text parsing). Tool-call `arguments` are JSON-decoded to an object. Reasoning surfaces via `reasoningFieldPrecedence` over the provider's reasoning fields.
* `STASH_KEY` `openaiChatCompletions` — per-iteration override via `ctx.stash.set(OpenAIChatCompletionsAdapter.STASH_KEY, {...})`.
* Media: image/audio/document supported as Chat Completions content blocks; VIDEO is not expressible and throws `E_UNSUPPORTED_MEDIA_MODALITY` unless `unsupportedMediaPolicy` degrades it (`fallback-stash`/`synthetic-description`).
* Exceptions: `E_INVALID_OPENAI_CHAT_COMPLETIONS_OPTIONS`, `E_OPENAI_CHAT_COMPLETIONS_{HTTP_ERROR,REQUEST_TIMEOUT,STREAM_ERROR,STREAM_STALLED,CONTEXT_OVERFLOW,INVALID_TOOL_CALL_ARGS}`, `E_UNSUPPORTED_MEDIA_MODALITY`.

This is the one you reach for when the model runs somewhere else and you are fine with that. Point `baseURL`
at any endpoint that speaks the OpenAI Chat Completions wire shape — a hosted API, a self-hosted
Chat-Completions-compatible server, a proxy gateway — give it a `model`, and it is a complete turn executor.
The adapter doesn't care who's behind the URL; it sends `<baseURL>/chat/completions` and parses what comes
back. Construct, hand to the runner, done.

```ts
import { OpenAIChatCompletionsAdapter } from '@nhtio/adk/batteries/llm/openai_chat_completions'

const executor = new OpenAIChatCompletionsAdapter({
  model: process.env.OPENAI_MODEL ?? 'your-model-id',
  apiKey: process.env.OPENAI_API_KEY,
  // baseURL defaults to OpenAI; point it at any Chat-Completions-compatible endpoint.
})
```

It is the boring one, and that is a compliment. It is boring because the hard part — running the model,
serving tokens, separating a tool call from prose into a structured `tool_calls` field — is somebody else's
job, on somebody else's hardware. You get structured output on the wire: tool-call `arguments` arrive as JSON
and are decoded to an object — no string parsing in the common case. One exception, opt-in: when you point this
battery at a *small* model behind an OpenAI-compatible endpoint, it may emit a call in a form the endpoint
doesn't lift into `tool_calls` (a `<call:…>` or ` ```json ` shape), landing it in `content`. Set
`localToolCallParser` to recover it from `content` **only when the provider returned none** — the same
`chat_common` parser layer the [on-device batteries](./) use, here as a fallback behind the provider's own
parse (see [the shared contract](/batteries/llm/shared-contract#the-same-leak-on-the-native-api-batteries-localtoolcallparser)).
The full ADK-control surface — retries, timeouts, context-window
accounting, history buckets — is wired; the [Assembly page](/assembly/batteries-llm) has the exact option
list. Whether you stream or not is your call (`stream`), as is how aggressively you retry — the kit serves the
information and the knobs; it doesn't decide your latency posture for you.

## The trade-off is the dependency, stated plainly

There is no free lunch and we are not going to pretend there is one. The thing that makes this battery easy is
the thing that makes it a dependency: **the model isn't yours.** It runs on infrastructure you don't control,
behind an API that can change, rate-limit, deprecate a model, or bill you by the token for the privilege of
your own agent's chattiness. Reliability is a contract with a third party, not a property of your process.
That's a fine trade for most production agents — and the on-device batteries in this section exist precisely
for the cases where it isn't. Which one is right is yours to weigh; we just won't pretend the dependency
isn't there.

## Media

Image, audio, and document inputs map to Chat Completions content blocks. **Video has no native block** and
raises `E_UNSUPPORTED_MEDIA_MODALITY` under the default `unsupportedMediaPolicy: 'throw'`. If you'd rather
degrade than fail, set `'fallback-stash'` (use a model-readable text entry from `media.stash` — a transcript
or caption — in place of the unrepresentable media) or `'synthetic-description'` (always render a synthetic
text stand-in). Loud failure is the default on purpose: a misconfigured pipeline surfaces immediately instead
of silently dropping a modality the model needed.

## Errors

All typed, all in the `E_OPENAI_CHAT_COMPLETIONS_*` family plus the shared
`E_INVALID_OPENAI_CHAT_COMPLETIONS_OPTIONS` (bad construction) and `E_UNSUPPORTED_MEDIA_MODALITY`: HTTP errors,
request timeout, stream error, **stream stalled** (the connection went quiet mid-stream — a real failure mode
a naive client hangs on forever), context overflow, and invalid tool-call args. The full reference is in the
[exception index](/api/@nhtio/adk/exceptions/).
