Compatibility Matrix
Client-facing surfaces nexus supports today, how support is proven, and known gaps
Esta página aún no está disponible en tu idioma.
This page records which client-facing surfaces nexus supports today, how that support is proven, and which endpoint families are not yet implemented. It describes current behavior only. “Supported” means the route, request/response shape, error envelope, and (where applicable) streaming shape match the referenced provider behavior and are covered by tests; “Partial” means a usable subset ships with named gaps; “Limited by provider” means nexus reports everything the upstream makes available and the remaining gap is in the provider’s own contract, not in nexus; “Not yet supported” means the surface is not implemented.
Protocol-level certification runs against the official OpenAI and Anthropic SDKs in test-suite/compat/ (just test-compat, just test-gateway-cert). Deterministic gateway behavior is covered by in-process integration tests; live-provider checks are opt-in where external behavior matters.
OpenAI-compatible edge (/v1/*)
Section titled “OpenAI-compatible edge (/v1/*)”| Surface | Status | Evidence / notes |
|---|---|---|
POST /v1/chat/completions (JSON + SSE) |
Supported | Official OpenAI SDK certification in test-suite/compat/; streaming usage capture and OpenAI-shaped errors. |
GET /v1/models |
Supported | Tenant-visible registry catalog (not a full upstream dump). |
GET /v1/models/{id} |
Supported | Exact retrieval of a canonical or provider-pinned model exposed by the tenant-visible registry catalog. |
POST /v1/responses (text, streaming, tools, structured outputs) |
Supported | OpenAI-routed dispatch. |
POST /v1/embeddings |
Supported | OpenAI string, string-array, token-id, and nested token-id inputs; encoding_format float/base64; dimensions; user passthrough. Azure Foundry uses the OpenAI embeddings wire format on the deployment embeddings URL. Native Bedrock Titan Text Embeddings V2 translates InvokeModel into the OpenAI embeddings envelope (string/string-array; token ids and all non-float encoding formats are rejected). Multi-input Titan calls are bounded fan-out: successful calls observed before an aggregate failure are recorded and charged exactly once, while the aggregate error is not replayed or routed to a fallback. Anthropic embeddings remain unsupported. |
POST /v1/files lifecycle + content |
Supported | nexus-owned artifact storage. |
POST /v1/vector_stores lifecycle |
Supported | nexus-owned metadata + bytes; includes create/list/get/modify/delete plus file attach (POST .../files), list, and detach (DELETE .../files/{file_id}). |
POST /v1/vector_stores/{id}/search |
Supported | nexus-native chunking, embeddings, and pgvector search. |
Hosted file_search on /v1/responses |
Supported (OpenAI-routed) | Retrieval is nexus-native; the rewritten request dispatches on /v1/responses (OpenAI today). Any provider can call direct vector-store search. |
POST /v1/images/generations |
Supported | Capability-gated per registry model. Token-priced image models record provider-reported image usage; request-priced image models, including xAI grok-imagine-*, record an estimated request-rate cost for each returned image. Both upstream URL and base64 outputs pass through. xAI rejects OpenAI’s size argument upstream. |
POST /v1/images/edits |
Supported | Multipart proxy; capability-gated per registry model (gpt-image family, dall-e-2). |
POST /v1/images/variations |
Supported | Multipart proxy; dall-e-2 only per the registry capability rows. |
| Image / vision usage accounting | Limited by provider | Image-generation accounting ships, including the text_tokens / image_tokens input split the Images API reports. Chat and Responses report no image-token breakdown — prompt_tokens_details carries audio_tokens and cached_tokens, Responses input_tokens_details carries cached_tokens — so image input on those surfaces is counted inside the text input line rather than as its own modality. The limit is in what the providers report, not in what the gateway reads. |
| Reasoning-token attribution | Partial | completion_tokens_details.reasoning_tokens and the Responses output_tokens_details.reasoning_tokens are recorded per request and returned on the query record, the gRPC record, and webhook exports. Attribution only: providers count reasoning inside the output tokens and bill it at the output rate, so it produces no cost line of its own and moves no billed counter. A breakdown the gateway cannot read costs the breakdown alone and leaves the usage billable. Reads on OpenAI and OpenAI-shaped upstreams, which is where the breakdown is reported. Anthropic reports the thinking share of the output as usage.output_tokens_details.thinking_tokens (the field the pinned SDKs declare on Usage; the gated certification suites are what exercise it against a live upstream); the gateway records it as reasoning_tokens on Anthropic routes (both edges, streamed and buffered) and surfaces it as completion_tokens_details.reasoning_tokens on the OpenAI shape and output_tokens_details.thinking_tokens on the native Anthropic shape. Bedrock reports none, so the count is zero on that route. |
POST /v1/traces/ingest |
Supported (acknowledged) | Accepts OpenAI Agents SDK trace exports and returns a success envelope; the payload is not persisted. Native trace storage is the OTLP trace-ingest service. |
Azure AI Foundry backend (azure-foundry/<model> on POST /v1/chat/completions and POST /v1/embeddings) |
Supported | Chat, streaming, and tool calls to Azure OpenAI and Foundry-hosted deployments via connection metadata (resource, API version, deployment map) with api-key auth; tool support is the deployment’s, and an upstream refusal is surfaced unchanged. Embeddings use /openai/deployments/{deployment}/embeddings with the same api-key auth and echo the catalog model name. /v1/responses is not dispatched. Embedded integration tests assert the deployment URL and header shape. |
OpenAI paid hosted tools (web_search, computer_use, code_interpreter, image_generation) |
Not supported | Rejected with 400 bad_request when the route includes an OpenAI candidate: OpenAI prices these per call internally and reports no countable usage field, so nexus cannot meter them. Matched by family prefix, so dated and preview revisions (web_search_preview, web_search_preview_2025_03_11) are rejected too. The Chat Completions spelling of paid web search — a top-level web_search_options object on the *-search-preview models — is rejected on the same grounds. A tools shape nexus cannot inspect — not an array, an element that is not an object, or a non-string type — is forwarded for the provider to reject, since it cannot invoke a hosted tool and so bills nothing. The error names the other candidates in the route so the caller can retry with one explicitly; nexus does not substitute a model on the caller’s behalf. Hosted file_search is unaffected (retrieval is nexus-native, see above). |
Gemini POST /v1beta/models/gemini-pro:generateContent |
Stubbed | Returns 501 Not Implemented. |
| Batches | Not yet supported | — |
POST /v1/moderations |
Supported | OpenAI upstreams only; capability-gated per registry model, so a route whose candidates do not advertise moderations is rejected before dispatch with 400 no_compatible_candidate. Priced as zero-cost. |
| Audio (speech-to-text / TTS) | Not yet supported | The /v1/audio/* surfaces are not implemented. Audio content on chat completions and Responses is accepted, and its usage is recorded: prompt_tokens_details.audio_tokens / completion_tokens_details.audio_tokens on chat, and input_tokens_details.audio_tokens / output_tokens_details.audio_tokens on Responses, are stored per request alongside a marker distinguishing a reported breakdown from an absent or unreadable one, and a flag for whether the request itself carried audio. Both stored values are request-level roll-ups over two independent halves: reported means at least one of the input and output breakdowns was reported, not that both were, so a zero in either counter is not necessarily a reported zero. Billing does not read either roll-up — it reads the half belonging to the meter it is pricing. Whether the audio share is billed separately is decided by the rate card, per meter. Where a card carries an audio rate matching the line that meter would otherwise emit — same service tier, context band, qualifiers and billing unit — that line splits into a text line and an audio line whose quantities still sum to the provider’s own total. Only the audio that is provably on that line is split out of it: audio_tokens and cached_tokens are independent breakdowns of one prompt, so up to cached_tokens of the reported share can be sitting on the cache meter and the response never says how much. The split therefore bills share - cached (cache-read plus cache-write tokens), clamped to the line, at the audio rate and leaves the rest at the text or cache rate, because billing the larger figure would charge text tokens the dearer audio rate on a guess. Where nothing is cached the two coincide, and output has no cache meter so nothing is withheld there. The stored audio_input_tokens column is the vendor’s report, not the billed audio quantity. On a request with cached prompt tokens the billed audio quantity is the smaller of the two, by the rule above. An audio rate the emitted line does not match is not a match, and the share folds. Where it does not, the line is left alone and the audio tokens bill inside it at the text rate, exactly as they did before these counters existed. A card carrying no audio rates is a supported state indefinitely rather than a gap, and it is inert: no split fires and no request fails. A card that prices audio on a single meter is not inert. That meter behaves like any other priced one — it splits where the split is computable, and it fails the request where the request asked for audio on that side and the upstream omitted the breakdown. What no rate-card shape does is leave a line without a rate. No rate card ships with audio rates today, so the split is inert until an operator adds them. These fields are read when present. An OpenAI-compatible upstream may omit them, and the marker then reads not_reported for that half — as it does on the Anthropic and Bedrock routes, whose response shapes carry no such field. A counter the upstream reports as an integer within UInt32 is stored as sent and is not checked against the total it is a share of (a counter that is present but not an integer in that range — a string, a float, an array, an object, or an out-of-range number — is what the unreadable marker records, while a JSON null reads as not_reported, the vendor declining to answer), so a provider that reports more audio tokens than the request’s own prompt_tokens is recorded faithfully rather than corrected — a consumer computing an audio share should compare the two columns rather than assume the counter is bounded by them. On a card that does not price audio such a report moves no billed counter and no cost line. On a card that does price audio, two conditions fail billing for the request rather than guess a split: a reported share larger than the total it is a share of — prompt_tokens for the input meter, completion_tokens for the output one — and a request that asked for audio on that meter’s own side — an input_audio content part for the input meter, audio or "audio" among modalities for the output one — for which the provider reported no usable breakdown on that side. A transcription therefore never fails on the output meter, because it never asked for audio back. Both are decided per meter, and both record a billing failure naming which of the two occurred. Note that audio_tokens and cached_tokens are independent breakdowns of the same prompt, so an audio share larger than the uncached remainder is ordinary rather than a contradiction. Because the request-side signal is read from client-supplied fields (audio, modalities, or an input_audio content part), a caller can reach the second condition deliberately on an audio-priced endpoint whose upstream returns no breakdown. A request that fails billing is served and not charged: the provider response is returned, the failure is recorded, and a pass-through-billing escrow hold settles at $0. The failure does not remove the endpoint from routing. The ingest billing-health evaluator quarantines it only once failures in its window reach both NEXUS_BILLING_QUARANTINE_MIN_FAILURES (default 10) and NEXUS_BILLING_QUARANTINE_MIN_RATIO_BPS (default 100, 1%) of the endpoint’s priced and failed requests, and never under NEXUS_BILLING_QUARANTINE_ACTION=allow. On an audio-priced endpoint whose upstream does not report the breakdown, a caller can therefore obtain unbilled audio requests in every window up to just below those thresholds — nine per window at the defaults on a quiet endpoint, just under 1% of its traffic on a busy one — or without limit under allow; a caller who reaches them in block mode takes the endpoint out of rotation for every tenant until an operator runs nexus models unquarantine. This is an accepted residual, recorded as threat-model row D5 (see the audio billing note below this table), so the decision to price audio is worth making only against an upstream known to report the audio breakdown. Pricing audio can also raise the pass-through-billing escrow hold on every request through that endpoint, text-only ones included: the reservation prices each token meter at the dearer of its text and audio rate, so wherever the audio rate is the dearer one a project’s available balance has to accommodate the audio figure even when the request bills at the text one. Where audio is priced below text, or where the audio rate does not match the line the meter would emit, the hold is unchanged. |
| Fine-tuning | Not yet supported | — |
| Realtime | Not yet supported | — |
| Assistants / Threads / Runs | Not yet supported | — |
Unimplemented /v1/* routes (including the “Not yet supported” families above) return HTTP 404 with the OpenAI error envelope.
Audio billing note. The audio billing guards were designed with a bound on unbilled requests: a single billing failure quarantined the endpoint immediately, so at most one request went unbilled before it left rotation. Endpoint quarantine has since moved to the ingest billing-health evaluator, which acts on a count and ratio of failures per window rather than on one failure. That change is at odds with the guards’ design and removes the bound, leaving the unbilled-request exposure described in the Audio row, which is accepted as it stands.
Native Anthropic edge (/anthropic/*)
Section titled “Native Anthropic edge (/anthropic/*)”| Surface | Status | Evidence / notes |
|---|---|---|
POST /anthropic/v1/messages (non-streaming + streaming) |
Supported | Native Anthropic request/response and error envelopes; cross-provider routing behind the edge. A turn that ends on a stop sequence reports stop_reason: "stop_sequence" and names the matched sequence. Set ANTHROPIC_BASE_URL to the gateway /anthropic base. |
POST /anthropic/v1/messages/count_tokens |
Supported | — |
Anthropic via /v1/chat/completions (translation) |
Supported | OpenAI-shape in, Anthropic-shape out, including streaming text and tool calls. Streamed tool calls carry delta.role on the opening chunk and 0-based tool_calls[].index ordinals independent of Anthropic content-block position. A message name is dropped on every role, since Anthropic accepts only role and content. system and developer messages hoist into system, carrying a message-object cache_control hint onto the last block they contribute so the prefix still caches; the hoisted system spells the same prompt whether it renders as a block array (any hint present) or a joined string, each message after the first opening with the newline that divides it from the one before and nothing inserted between one message’s own content parts, so a caching breakpoint on a message’s final block names the prefix as sent; tool_choice: "none" maps to the Messages API none kind; parallel_tool_calls: false becomes disable_parallel_tool_use on the tool choice, when tools are declared and the choice is not none; An n other than absent, null, or 1 (greater than 1, or not a positive integer) is refused with 400 invalid_request_error and error.param: "n", since the Messages API returns one choice; the Bedrock route refuses n at any non-null value. The refusal is made once for the whole route before any attempt, so a route that also lists a non-Anthropic candidate is refused too, with those candidates named for an explicit retry. |
| Native fidelity (end-to-end prompt-cache accounting) | Supported | Native-field preservation, anthropic-beta forwarding, thinking pin-to-Anthropic, and cache read/write token pricing at registry rates all ship. Streamed and non-streaming responses build usage through one serializer, so message_start / message_delta report input_tokens excluding the cache classes with cache_creation_input_tokens, cache_read_input_tokens, and the cache_creation tiers, exactly as the buffered body does. Signed thinking blocks are returned on both paths: the buffered response carries the block (signature intact, text empty when the model omits it) at its upstream position among the text and tool_use blocks, so a non-streaming tool loop replays it and the upstream accepts the tool-result turn. Redacted and multiple thinking blocks are preserved in upstream order on both paths, each streamed as its own content block, so interleaved-thinking tool loops replay verbatim. |
| Structured outputs on Anthropic upstreams | Partial | Anthropic has no response_format, so a json_object or json_schema request is steered by system prompt: the schema is quoted in the prompt and the model is asked to return only JSON. Nothing validates the reply against the schema, so an Anthropic-routed response can be JSON that does not satisfy it, or not JSON at all. OpenAI-routed json_schema with strict is unaffected. |
Bare POST /v1/messages |
Not supported (by design) | Returns 404 with the OpenAI error envelope; use the /anthropic edge so the OpenAI and Anthropic dialects do not collide. |
Clients and frameworks (tested)
Section titled “Clients and frameworks (tested)”| Client / framework | Status | Evidence |
|---|---|---|
| Official OpenAI SDK (Python/TS) | Certified | test-suite/compat/ SDK suites. |
| Official Anthropic SDK (Python/TS) | Certified path | test-suite/compat/ Anthropic certification. |
| LibreChat | Partial | Chat, embeddings, and models work, as does GPT reasoning with function tools and vision: the chat→Responses bridge flattens image_url objects to the string URL Responses requires. Anthropic-routed streaming tools and post-tool turns no longer need the client-side delta.role patch or tool-message name stripping; the gateway emits the OpenAI shape directly. |
| vortexcode (VS Code extension) | Working | Operator-confirmed daily-driver flow (chat, responses, models). |
data-codex (openai-agents 0.12.3) |
Working | Tested end-to-end 2026-06-17: streamed Responses agent loop, native vector-store search and file_search, aux chat completions, and embeddings. |
How to reproduce
Section titled “How to reproduce”# Protocol certification against the official SDKs (requires a running gateway)just test-compatjust test-gateway-cert
# Behaviour-focused router suite (streaming, fallback, tools, long-context, structured output)just testFor the per-service surface that backs these claims, see docs/architecture/services.md.
