One Chat, Many Model Dialects

NextChat presents model choice as one control in one conversation, but the providers behind that control do not speak one language. This chapter examines the boundary between NextChat’s shared LLM interface and its provider-specific adapters, then connects that translation layer to model visibility and deployment policy. Readers will be able to tell which parts of a setting travel unchanged, which parts are rewritten, and why switching models is sometimes a semantic change rather than a simple substitution.

The common contract is deliberately thin

A useful abstraction does not pretend that different systems are identical. NextChat’s common LLM contract standardizes a small set of product-level operations: chat, speech, usage, and model listing. A client API then selects a concrete implementation for the configured provider. The conversation screen can ask for a chat operation without knowing whether the request will be handled by an OpenAI-compatible adapter, an Anthropic adapter, a Gemini adapter, or one of the other provider implementations in the project.

That common contract is an interface, not a universal wire format. It tells the rest of the application what it may ask an adapter to do. It does not tell every provider how to represent a system instruction, an image, a tool call, or a generation limit. Those decisions remain inside the adapter because the provider, model, and endpoint determine what a valid request means.

This separation gives NextChat a stable place for user-facing state. A session can hold a model name and provider name, and the rest of the application can pass a shared list of messages to the selected client API. The adapter can then construct the request body, choose the endpoint, select the authentication header, and parse the response. The user receives a consistent interaction even though the provider contract changes underneath it.

Model identity is already more complicated than a name in a dropdown. NextChat can associate a model with a provider, display a friendly name, and allow a provider suffix to disambiguate otherwise identical names. Server and user custom-model settings can add or hide entries, rename their display labels, choose a default, or create a custom provider record for a model that is not in the built-in catalog. The visible label is therefore a user-interface identity; the request still depends on the underlying model and provider pairing.

The distinction becomes important when an administrator limits the model set. The client combines the built-in catalog with deployment and user configuration to decide which models are available for selection. Provider routes also apply server-side filtering before forwarding some requests. A model may be known to the client but unavailable in a particular deployment, or it may be displayed with a friendly label while being routed to a provider-specific deployment name. “Selectable” and “accepted by the server” are related states, not synonyms.

classDiagram class LLMApi { +chat() +speech() +usage() +models() } class ClientApi class OpenAI_Azure_Adapter class Anthropic_Adapter class Google_Adapter class Model_Visibility_Policy ClientApi --> LLMApi : exposes operations ClientApi --> OpenAI_Azure_Adapter : selects ClientApi --> Anthropic_Adapter : selects ClientApi --> Google_Adapter : selects Model_Visibility_Policy --> ClientApi : shapes choices OpenAI_Azure_Adapter ..> LLMApi : follows contract Anthropic_Adapter ..> LLMApi : follows contract Google_Adapter ..> LLMApi : follows contract

The diagram is intentionally not a catalogue of every provider. It shows the reason for the boundary: the application depends on a shared contract, while each adapter owns the translation into a provider dialect and the policy layer decides which choices may be offered or forwarded.

Translation is where semantics reappear

Consider the common message list as an intermediate representation. It may contain system, user, and assistant roles; text; images; model settings; and selected tools. The adapter reads that representation and constructs a provider request. The important work is not merely renaming fields. It is preserving the intended conversation where possible, omitting unsupported pieces where necessary, and converting the remaining pieces into the provider’s accepted shape.

OpenAI and Azure: compatible does not mean identical

The OpenAI-compatible adapter handles ordinary chat, Azure deployment paths, vision models, image generation, and tool calls. For a vision-capable model, it can preprocess cached image content before putting it into the request. For image generation, the request is not a chat transcript at all: the latest text becomes an image prompt, and the response may be an image URL or encoded image that NextChat turns into a message representation.

Model-specific rules also change the payload. The adapter recognizes families of models with different support for system messages, tools, streaming, sampling controls, or token fields. It may omit a system message for a model that does not accept one, use a different completion-token field, or reset settings that the model does not support. These adjustments keep a shared UI from sending a formally invalid request, but they also mean that a setting selected in the interface does not necessarily reach every model with the same effect.

Tool calls make the boundary even clearer. The adapter converts selected plugins into the provider’s function-tool schema. During a stream, tool-call fragments may arrive before the assistant has finished speaking. NextChat accumulates those fragments, executes the corresponding function, appends the tool call and result to the request, and asks the provider to continue. The product-level experience is “the assistant used a tool,” but the provider-level exchange contains a sequence of assistant and tool messages with a format defined by that provider family.

Azure uses the same broad adapter because its chat interface is compatible with the OpenAI-shaped request, but deployment identity still matters. A selected model can map to an Azure deployment name and API version rather than to the model name a user sees. The shared abstraction saves the application from maintaining a wholly separate conversation engine, while the endpoint and deployment rules keep Azure from being treated as a cosmetic alias.

Anthropic: conversation roles are a structural constraint

Anthropic’s message rules require a different transformation. The adapter maps NextChat’s system, user, and assistant roles into the roles accepted by Claude. In the inspected implementation, a system message is represented in the user side of the conversation, and the adapter ensures that adjacent messages do not violate the provider’s alternating-role expectation. When two neighboring messages would have the same relevant role, it inserts a small assistant separator.

That separator is not conversational content chosen by the user. It is an adapter repair that makes the shared conversation structurally acceptable to the provider. The distinction matters when readers evaluate output differences: a model may respond differently not because the user changed the prompt, but because the provider adapter had to reshape the history to satisfy another protocol.

Anthropic’s multimodal path also uses its own content blocks. Cached data-URI images are split into media type, encoding, and data, then represented as image sources rather than OpenAI-style image URLs. Tool declarations are converted into Anthropic input schemas, and tool results are returned in the provider’s tool-result structure. The common concept is still “an image” or “a callable tool”; the wire-level object is not shared.

Google: roles, media, and safety settings follow another model

The Gemini adapter maps assistant messages to the provider’s model role and system messages to a user-side representation. Google requires neighboring messages to alternate roles, so adjacent messages with the same role are merged instead of separated with a synthetic message. This produces a different history shape from Anthropic’s repair even when both adapters start from the same NextChat session.

Images are encoded as inline data with a MIME type and payload. The adapter also sends generation settings and a set of safety settings. Those safety thresholds are part of the request policy, not a generic property of the conversation object. A user can think they selected “the same temperature and prompt,” while the provider request includes a safety configuration and a role structure that another adapter would not use.

Google’s tool path illustrates the same point. Function declarations are nested in the provider’s expected representation, function calls are read from streamed response parts, and function responses are appended as Google-specific content entries before the request continues. The tool’s purpose may be shared across providers, but its lifecycle is mediated by a protocol that each adapter must understand.

Why the adapter boundary matters to the reader

These transformations explain why NextChat can be broad without being perfectly uniform. A common interface reduces repeated product logic: session handling, message display, cancellation hooks, and much of the streaming surface can be shared. It also gives users a single place to choose models and configure conversations. But the adapter remains responsible for the last mile, where an abstract message becomes a provider-valid request.

That last mile introduces three kinds of non-portability. The first is structural: roles, content blocks, and tool-call messages may have different shapes. The second is capability-based: one model may accept images or tools while another ignores or rejects them. The third is semantic: the same parameter name may be supported with different behavior, or an adapter may omit it to keep a request valid. A dropdown can hide these differences from the interface, but it cannot remove them from the model interaction.

The abstraction also creates a responsibility boundary for errors. If a provider rejects a role sequence, the relevant cause may be the adapter’s transformation. If an image cannot be decoded, the failure may occur before the request leaves the client. If a tool schema is accepted but the external API fails, the model adapter may have worked correctly while the capability path did not. A single “request failed” message is not enough to identify the owner of the problem.

Policy surrounds translation

Provider adaptation does not operate in isolation from administration. The client receives non-secret deployment configuration that can describe defaults, hidden models, custom model expressions, vision-model additions, or whether user keys are allowed. It uses that information to shape the visible selection list. The server separately holds provider credentials and can reject disallowed model requests before forwarding them.

This split is useful for a self-hosted service. An operator can expose a curated set of models without editing the conversation engine, while users can still add personal model entries when deployment policy permits. It is also a hidden coupling. The client’s display identity, provider suffix, server availability check, and actual upstream deployment name must remain consistent. If they diverge, the user may select an option that looks valid but fails at the route or provider boundary.

A deployment restriction is not the same as a provider limitation. The provider may support a model that an administrator has disabled in deployment configuration. Conversely, a client may list a model that the configured key or endpoint cannot use. NextChat’s model table and server filtering reduce some of that ambiguity, but the existence of both layers means troubleshooting must ask two questions: what does the client offer, and what will the deployment forward?

What the abstraction buys—and what it cannot promise

NextChat’s provider abstraction is valuable precisely because it stops at a real boundary. It standardizes enough behavior for one product experience, then gives each provider implementation responsibility for roles, media, safety, tools, endpoints, and model-specific limits. That design supports breadth without requiring the core chat screen to understand every provider protocol.

The price is conditional portability. A model switch can change how history is serialized, which settings take effect, whether images or tools are accepted, and what the response stream contains. A user who needs exact behavioral continuity must inspect the selected provider and model rather than relying on the shared interface alone. The right mental model is not “one API with many names,” but “one interaction layer with several translation contracts.”

The next boundary lies before any adapter runs: NextChat must decide which parts of the stored conversation belong in the request at all. Understanding that selection policy—masks, recent history, summaries, and clear-context markers—explains why provider translation can only act on the context it receives.

References