Inside the OpenAI provider
This page is for people working on the runtime itself. What the providers connect to, the credentials they need and what they serve are in the guide; this page holds the encoders, the two dialects and their transports, the Azure Foundry wiring, the rules for a third party's bytes, and the listings.
Encoders
How the parts of a Nucleo message become OpenAI's wire JSON (B = com.fasterxml.jackson.databind.node.ObjectNode);
the encoder model they follow is the parent
How each part of a message reaches a provider.
| Class | Role |
|---|---|
| OpenAITextWrapper | The one OpenAI text wrap: string -> {"type":"text","text":...}. Injected into the shared text encoders, which every text-rendered block rides. |
| OpenAIImageBlockEncoder | Native image_url element with a base64 data URL. |
| OpenAIFileBlockEncoder | Image-compatible files -> image_url; anything else falls back to a filename text element via the wrapper. |
| OpenAIToolDefinitionBlockEncoder | Nothing inline: the definition travels in the tools parameter. |
| OpenAIToolUseBlockEncoder | One tool_calls element of the assistant message, under the provider's call id. |
| OpenAIToolResultBlockEncoder | A whole role:tool message answering one call by its id. |
JSON and skill blocks have no OpenAI native form and ride the base text encoders. The non-multimodal collapse (a message of only text blocks becomes a single content string) is the OpenAI client's message-assembly concern, not the encoders'; every encoder reachable in that branch must therefore emit a {type:text} element (what OpenAITextWrapper produces), so the collapse can read its text.
Two transports, two dialects, the dialect a provider's choice
OpenAI has two APIs, and both are written once in AbstractOpenAIChatClient: the content encoders, the request body as JSON (buildRequestJson for Chat Completions, buildResponsesJson for Responses), the response body read back onto the framework's response (finishResponse, finishResponses). Which one a client speaks is its WireApi, and since the API is a surface, it is the provider's choice, the way Bedrock's native and Mantle surfaces are two providers: the catalog picks the client through the provider key and nothing else. Two transports carry either dialect:
| Provider key | Client | Serves | Credential |
|---|---|---|---|
openai | OpenAISDKClient on RESPONSES | OpenAI itself, through OpenAI's own Java client (com.openai:openai-java), on the Responses API: the surface where the reasoning generation reasons and calls tools in one turn | openai-api-key |
openai-chat-completions | OpenAISDKClient on CHAT_COMPLETIONS | the same models on the older surface, for an entry that must be called there; no fragment entries | openai-api-key |
openai-embeddings | OpenAISDKEmbeddingsClient | OpenAI's embedding models, same client | openai-api-key |
openai-compatible | OpenAICompatibleClient on CHAT_COMPLETIONS | any endpoint that speaks Chat Completions without being OpenAI (a self-hosted server, an aggregator, a vendor with an OpenAI-shaped API), through the framework's own HTTP transport (OpenAIDialectEndpoint, an AbstractApiClient) | openai-compatible-api-key: secret the bearer token, host the API root, e.g. https://router.huggingface.co/v1 (Hugging Face Inference Providers, a user access token with the "Inference Providers" permission; it lists its models, so the discovery fills the catalog) or http://localhost:11434/v1 (Ollama) |
openai-compatible-responses | OpenAICompatibleClient on RESPONSES | such an endpoint that speaks the Responses API | the same |
openai-compatible-embeddings | OpenAICompatibleEmbeddingsClient | the embedding models of such an endpoint | the same |
azure-openai-embeddings | AzureOpenAIEmbeddingsClient | Azure OpenAI embedding deployments | openai-api-key with its host part |
azure-foundry-openai | AzureFoundryOpenAIClient on RESPONSES | the OpenAI surface of an Azure AI Foundry resource (/openai/v1): the Azure OpenAI models and the Foundry Models sold by Azure on OpenAI's syntax (DeepSeek, Grok, Llama, Microsoft AI, gpt-oss), through the same SDK pointed at the resource, on the Responses API | azure-foundry-api-key: secret the resource key, host the resource hostname, e.g. my-resource.services.ai.azure.com |
Azure appears twice in that table because the two halves share nothing but the vendor. azure-openai-embeddings serves embeddings on the classic deployments path through the framework's transport; azure-foundry-openai serves chat models on Foundry's Responses surface, which is wire-compatible with the dialect, so AzureFoundryOpenAIClient subclasses OpenAISDKClient and overrides only how the SDK client is built: the base URL from the credential's host, and the SDK's AzureApiKeyCredential, which sends the resource key as an api-key header in place of the bearer token. Everything else - tool calling through the native channel, rehydration of the tool turns, streaming, the truncation escalation, images - is the dialect's, shared with OpenAISDKClient. Foundry is a multi-vendor host the way Bedrock is, one resource with a wire surface per model family, and the key names the surface for that reason: Claude on Foundry is /anthropic/v1/messages, the native Messages API behind the Anthropic SDK, and belongs to an Anthropic provider under the same resource credential, as Bedrock splits Mantle from Converse. The chat key deliberately does not end in -embeddings: that suffix is how ModelSpec.isEmbeddings() identifies the embeddings family, and a chat provider carrying it would be refused for every LLM seat. Both Azure providers share the azure platform, so a model disabled on one is disabled on the other. The Foundry provider lists what the resource can reach on its surface, its deployments, through AzureFoundryDeployments: GET /openai/deployments on the classic authoring surface under the resource key, each deployment by the id a person gave it with its base model as the note, minus the Claude deployments, which the same resource serves on its Anthropic surface and which this one answers with 404 Requested API is currently not supported. That is deliberately not the resource's GET /openai/v1/models, which is its deployable catalog, every model Azure offers on the resource, hundreds, Claude and image and audio models included, each claiming chat completion whatever surface serves it, and which says nothing about what is deployed (a live run: 428 listed, the one deployment answered under its name, its catalog spelling answered 404). The shipped fragment carries no Foundry entries, since deployments are named by the people who create them; the discovery reports each one the deployment's models.json does not name. LiveAzureFoundryProbe is the first thing to run against a new resource: it answers whether the surface is there, whether api-key is the credential it wants, and whether the catalog's wire_model_id is a model it serves.
The SDK client converts the dialect's JSON into the SDK's typed parameters through the SDK's own mapper, calls with the raw response in hand (the account's live x-ratelimit-* limits and the request id are read off it), and hands the typed answer to the shared body reader as JSON; failures arrive typed (RateLimitException is a 429, an insufficient_quota error code among them is out of money, InternalServerException and any 5xx a server error, OpenAIIoException an unreachable provider). It streams on both dialects: on Chat Completions text deltas reach the chunk handler as they arrive, the usage block of the final chunk (stream_options.include_usage) becomes the response's usage, and every chunk is kept verbatim as the wire output; on Responses every output_text.delta is forwarded and the completed response the final event carries is read exactly as a single one, so the streamed turn ends with the same blocks and usage. The compatible client does not stream.
The Responses dialect (OpenAIResponsesDialectTest) differs from Chat Completions in shape, not contract. The system channel is instructions, the admitted skills then the conversation's system content. The turns are input items in conversation order: a user turn's text and images one message of input_text and input_image parts with its tool results as function_call_output items ahead of it, an assistant turn's reasoning replayed as the reasoning items the model produced, its text a message of output_text, its calls function_call items, in the order the model produced them. Tools are flat functions with strict off, since the runtime's schemas are not written for strict mode. Reasoning rides reasoning.effort from the seat's depth beside the tools, which is the point of the surface: Chat Completions refuses the two together for the GPT-5.6 family. Nothing is stored server-side (store: false), the conversation being the runtime's, so the model's reasoning comes back encrypted (include: reasoning.encrypted_content) as a RedactedThinkingBlock holding the whole item and goes back verbatim on the next turn, which is what lets a tool loop continue with its reasoning intact. Reading back, the output items become blocks in order, status: incomplete on max_output_tokens is the truncation the template escalates, a failed response is refused with its error, and a body without output is not an answer.
Tool calling on this dialect is native, and both transports carry it because it lives in the shared dialect. Every definition the request carries goes out as the tools parameter (OpenAIToolDefinitionBlockEncoder suppresses the inline text form; buildRequestJson sweeps the same blocks into the parameter, so the two halves of that decision sit in one class), the model's calls come back as tool_calls and are read into ToolUseBlocks under the provider's own call_ ids, and on the next request a call and its result are message structure rather than content: OpenAIToolUseBlockEncoder renders a call as one element of the assistant message's tool_calls, OpenAIToolResultBlockEncoder renders a result as a whole role:tool message that the assembly emits directly after the assistant message it answers, ahead of whatever else that turn says. The formatter therefore answers yes to native tool calling, so the response schema carries no tool_calls field. On the SDK's streaming path the call deltas are gathered by index into whole calls before the blocks are set. One model fact reaches the request here: a catalog entry declaring tools_suspend_reasoning names a model whose surface refuses tools together with a reasoning effort (400 Function tools with reasoning_effort are not supported ... set reasoning_effort to 'none', the GPT-5.6 family on Chat Completions, on OpenAI and on Foundry alike, where GPT-5 takes both), so a turn carrying tools goes out at none and the seat's depth applies on the other turns (OpenAIToolsSuspendReasoningTest); reasoning and tools together on that generation is the Responses API's, which this dialect does not speak.
The answer's declared shape is held natively too. A handler that describes its answer as a schema (usesSchemaNotation) is asking for one JSON object, and the request carries response_format: json_object; a handler that reads plain text gets no format. The instruction alone was not enough: a model whose system prompt asks for terse answers replies with the bare value, which is valid JSON and not the object, and no correction moves it off that.
LiveOpenAIChatProbe in the test tree drives the whole path against the live model through the dispatcher, once through the SDK and once through the compatible client pointed at OpenAI's API root under the same key: a tool call whose persisted result carries the day's date and whose call id is the provider's own, a resume of that conversation from stored JSON after eviction, streaming, the truncation escalation, and an image. The API root a person provides is normalized where its spelling is unambiguous (surrounding whitespace, trailing slashes), kept exactly as provided otherwise, and refused with the accepted forms named when it carries no scheme or is missing altogether (OpenAIDialectEndpoint, OpenAIDialectEndpointTest), so a connect without the host part says what to provide. The compatible client asks nothing beyond the JSON body and reads what comes back leniently, because such endpoints diverge from OpenAI in exactly the details an official SDK is strict about. A deployment with several compatible endpoints subclasses OpenAICompatibleProvider once per endpoint, with its own key and credential id; the catalog fragments carry no entries for the compatible providers, since the models behind such an endpoint are the deployment's to know.
What a third party's bytes may and may not do
Both transports are held to the same three rules, pinned by OpenAIUpstreamFailureTest against a local server that answers each case with a scripted status, headers and body, once through the SDK pointed at it and once through the hand-rolled transport.
- Nothing degrades silently. An answer that is not an answer is refused with the framework's uncorrectable failure: a body that is not JSON or is empty; a 200 whose body carries no choice with a message (an error object some gateways send with a 200, an empty
choices, a scalar); a tool call without an id or a function name, on the single path and at the end of a stream. A status the endpoint answered reaches the caller as the framework's type, never the transport library's: a 429 rides the rate-limit retry with the endpoint'sretry-after, a 429 whose body carries theinsufficient_quotacode is out of money, a 5xx rides the transient retry, and so does a failure to reach the endpoint at all; every other status is uncorrectable, naming the status and the model. - Nothing we sent comes back in a failure. Neither the bearer token nor the request body - the conversation - appears in any message of a failure's chain. The hand-rolled transport is AbstractApiClient, whose failures are typed (HttpErrorResponse for a status, an ExternalServiceException without one for no answer) and carry the upstream body only; the clients read the status, the retry-after and the quota code off those types (
OpenAIDialectFailures), never off message text, so a conversation that talks about rate limits changes no verdict. - Lenient about the extra, strict about the missing. Unknown fields at any level, an absent usage block, a content type that is not JSON: read as an answer. Content sent as an array of typed parts, as several compatible endpoints answer, is read as its text parts joined; the SDK's typed parse refuses that shape before the reader sees it, so it is the hand-rolled transport's case.
Catalog and discovery
The artifact ships META-INF/nucleo/seed_models.json, the fragment with a bootstrap kernel of OpenAI entries (the current model of each shipped family, public facts and the tier-1 limits), loaded when the deployment has no catalog of its own; the discovery authors the rest. Every provider but the Azure embeddings one implements ModelDiscovery: the OpenAI and compatible providers through the shared OpenAIModelListing, GET /models under the provider's own root and key (OpenAI's two providers list under one key and one root, a compatible endpoint under its own), the Foundry provider through its deployment listing, and the catalog discovery matches each provider's entries against its list. The account's limits are not listed; they arrive on every response's x-ratelimit-* headers where the endpoint sends them (OpenAI does; embeddings responses do not), which the discovery's ping reads. AzureOpenAIEmbeddingsProvider does not list: Azure deployments are enumerated through the management plane, not the inference endpoint, and the fragment carries no entry for it (deployments are named by the people who create them), so its entries come from the deployment's own file, which the discovery keeps as written, unpinged, like every entry of that file.