OpenAI-compatible Chat Completions — LLM (Groq, Together AI, Fireworks AI, vLLM, Ollama, OpenRouter)
Researched 2026-10-08. Every fact below came from a page fetched that day; URLs are given per section and in "Sources". Pages were read through a summarising fetch tool, so JSON blocks marked "(doc)" were copied from the docs, and blocks marked "(illustrative)" were rebuilt from field lists and are NOT verbatim wire captures. Source-code reads (Ollama Go, vLLM Python, openai-python, groq-python) are labelled "(source)". Anything not confirmed is marked UNVERIFIED. No live capture was done: a live run per vendor must confirm wire shapes before they are trusted.
Recommended (read this, skip the survey).
| You want | Start with | Verified |
|---|---|---|
| A hosted LLM, any language, tools | OpenRouter: preset = "openrouter", api_key_env = "OPENROUTER_API_KEY", model openai/gpt-6-luna with reasoning_effort = "none" (recipe: docs/PROVIDERS.md, "OpenRouter") | Live: streaming, parallel and chained tool calls, voice-to-voice runs in English and Turkish (docs/TOOLS.md, docs/LATENCY.md). Turkish quality is the one thing to judge by listening. |
| A fully local LLM | Ollama: preset = "ollama", model = "gemma4:12b-it-qat" (ollama pull gemma4:12b-it-qat), reasoning_effort = "none", no key | Live: streaming, and the six tool scenarios (parallel, chain, repeated booking, invalid-argument retry, slow tool, barge-in) 18/18 on an RTX 5070 with first sentence in ~270 ms. Five local models were tried, only this one called tools reliably (table below, docs/TOOLS.md, "Local models through Ollama"). |
| Groq, Together, Fireworks, vLLM | The matching preset | Mock servers and contract tests only; no live run against them. Treat the vendor sections below as documentation, and try one call before relying on it. |
The same server speaks the OpenAI-compatible protocol for the local speech side too (a
text-to-speech server); that is a different provider, openai_speech
(docs/providers/openai-speech.md), and the recommended local speech stack is in the first lines
of docs/providers/local-speech.md.
- Docs: see "Sources" at the end (fetched 2026-10-08).
- Auth:
Authorization: Bearer <key>for all hosted vendors. Local servers: Ollama local needs no header (client libraries want a dummy key); vLLM checks a key only if started with--api-key. Details per vendor below. - Endpoint(s) / regions:
POST {base_url}/chat/completions,base_urlincludes the version prefix and differs per vendor (table below). Regions: not researched (UNVERIFIED for every vendor). - Protocol: SSE of
chat.completion.chunkJSON objects, terminated bydata: [DONE]. - Keepalive / timeouts: no vendor documents SSE keepalive or server-side stream timeouts (UNVERIFIED for all five); brisvox must apply its own connect / first-byte / idle timeouts.
- Finalize / flush / cancel semantics: cancel = close the HTTP connection. Only Fireworks says so in docs ("Close the connection to stop generation and avoid billing for ungenerated tokens").
- Timestamps / interim results / tool-call deltas: see "Tool-call streaming matrix".
- Concurrency / rate limits: see "Rate limits and error bodies".
- Turkish support: not researched per model (model-dependent; UNVERIFIED for every vendor/model below). Choose the model by a Turkish live test, not by this document.
- Gotchas found during implementation: see "Implementation notes" below; "Known deviations" lists the ones the docs reveal.
Summary table
| Base URL | Auth | Source | |
|---|---|---|---|
| OpenAI (baseline) | https://api.openai.com/v1 (see openai.md) | Authorization: Bearer | openai.md |
| Groq | https://api.groq.com/openai/v1 | Authorization: Bearer $GROQ_API_KEY (the page says "passing your Groq API key"; the header itself is the OpenAI-client default: UNVERIFIED as text on the page) | console.groq.com/docs/openai |
| Together AI | https://api.together.ai/v1 | -H "Authorization: Bearer $TOGETHER_API_KEY" (doc) | docs.together.ai/docs/openai-api-compatibility |
| Fireworks AI | https://api.fireworks.ai/inference/v1 | Bearer token in the authorization header (cURL example); also OPENAI_API_BASE / OPENAI_API_KEY env | docs.fireworks.ai/tools-sdks/openai-compatibility |
| vLLM | http://localhost:8000/v1 in the page's client example (host/port defaults are NOT stated on the page: UNVERIFIED) | --api-key <k> or env VLLM_API_KEY; only enforced for the /v1, /v2, /inference prefixes; the page does not name the header (UNVERIFIED; OpenAI clients send Bearer) | docs.vllm.ai openai_compatible_server |
| Ollama local | http://localhost:11434/v1/ | none; client needs a placeholder such as 'ollama', ignored by the server | docs.ollama.com/api/openai-compatibility |
| Ollama cloud | https://ollama.com/v1 | Authorization: Bearer $OLLAMA_API_KEY; "x-api-key alone is not supported" (stated for the Anthropic-compatible endpoint) | docs.ollama.com/api/authentication |
OpenRouter (preset = "openrouter"; fetched 2026-10-14 from https://openrouter.ai/docs/api-reference/overview ,
read through a summarising fetch tool): POST https://openrouter.ai/api/v1/chat/completions,
Authorization: Bearer <key>; optional attribution headers HTTP-Referer, X-OpenRouter-Title
(X-Title also accepted), X-OpenRouter-Categories (the preset sends none; an application that
wants them sets extra_headers through the library). Streaming is SSE and "will occasionally
contain a comment payload, which you should ignore" (the adapter ignores comment lines). Usage:
"exactly once in the final chunk before the [DONE] message"; that chunk has a non-empty
choices with a content-free delta that repeats the finish_reason, so the response ends at the
usage chunk or [DONE] (placement TrailingChunk). The token limit is max_tokens. The overview
page lists no reasoning_effort request field, but the live runs of 2026-10-13 sent it
(docs/LATENCY.md: openai/gpt-6-luna thinks by default and adds about 1 s to the first token;
reasoning_effort = "none" turned it off), so set it for voice. Tools: tools, tool_choice,
role: "tool" messages with tool_call_id, finish_reason tool_calls; only some models
support them (openrouter.ai/models?supported_parameters=tools). The quirks of the preset are
OpenAI's own (max_tokens, stream_options.include_usage sent, usage in a trailing chunk), the
settings of those live runs. The model ids are vendor/model; the suggested one is the model of
the live comparison, pick yours by measurement.
vLLM warning (doc): "Do not rely on --api-key alone to secure vLLM." Ollama binds
127.0.0.1:11434 by default; change with OLLAMA_HOST (docs.ollama.com/faq).
Brisvox adapter implications: base_url (string, includes /v1 or /openai/v1 or
/inference/v1), optional bearer key (absent for local Ollama/vLLM), extra static headers map,
model string verbatim (vendor-namespaced: Together openai/gpt-oss-120b, Fireworks
accounts/fireworks/models/...; "OpenAI model strings like gpt-4o ... return a 404" on
Together). Plain http:// only for loopback (same rule as openai.md).
OpenRouter
Not part of the research above (its documentation was not fetched for this file). What the
repository itself uses and has run: base URL https://openrouter.ai/api/v1, bearer auth with the
user's own key, a vendor-namespaced model id (openai/gpt-6-luna), the top-level request field
reasoning_effort (none, because that model thinks by default and adds about a second to the
first token). The openrouter preset fills in the base URL and that default model:
provider = "openai_compatible", [llm.openai_compatible] with preset = "openrouter" (or
base_url and model spelled out), and api_key_env. A ready agent file is
bench/pipecat_baseline/agent_live_tr.toml; the recipe with notes is in docs/PROVIDERS.md
("OpenRouter"), and the live results (tool scenarios, latency) are in docs/TOOLS.md and
docs/LATENCY.md.
Baseline: canonical OpenAI Chat Completions streaming
Sources: https://developers.openai.com/api/reference/resources/chat/subresources/completions/streaming-events ,
openai-python src/openai/types/chat/chat_completion_chunk.py, completion_create_params.py,
lib/streaming/chat/_completions.py (all fetched 2026-10-08). See also the Chat section of
docs/providers/openai.md (same sources). Note: OpenAI recommends Responses for new work and on
GPT-6 models tool calling needs Responses (openai.md; that claim was not re-checked in the
2026-10-08 fact-check of this file: UNVERIFIED here); this document concerns Chat Completions as
implemented by third parties.
Request: stream: true; stream_options: {"include_usage": true} (optionally
include_obfuscation); tools: [{"type":"function","function":{"name","description","parameters"}}];
tool_choice (none default without tools, auto default with tools, or a named function);
parallel_tool_calls ("Whether to enable parallel function calling during tool use");
max_completion_tokens (max_tokens is deprecated in favour of it); n ("Keep n as 1").
Chunk (page field list):
id(same in every chunk),object="chat.completion.chunk",created(Unix seconds, same in every chunk),model,choices[], optionalsystem_fingerprint,service_tier,obfuscation,moderation,usage.choices[]:index,delta,finish_reason(nulluntil the end;stop,length,tool_calls,content_filter, deprecatedfunction_call),logprobs.delta:role,content,refusal,tool_calls[], deprecatedfunction_call,audio.delta.tool_calls[]:index(required int, identifies the call),id(optional, "The ID of the tool call"),type("Currently, onlyfunctionis supported"),function{name,arguments};argumentsis a JSON string that "may be invalid" and must be validated.usage: only present withinclude_usage; "null in every chunk except the last"; the last chunk haschoices: []("The array can be empty on the last chunk whenstream_options.include_usageis set"; thechoices: []quote was not re-confirmed on 2026-10-08: UNVERIFIED); fieldsprompt_tokens,completion_tokens,total_tokens,prompt_tokens_details.cached_tokens,completion_tokens_details.reasoning_tokensand more. If the stream is interrupted "you may not receive the final usage chunk".- Stream ends with
data: [DONE].
Verbatim first chunk (doc; the page example is cut by the fetch tool at the quote limit, the model id is whatever the page shows):
{"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-6-astra",
"system_fingerprint": "fp_44709d6fcb",
"choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],
"obfuscation":"r4N7vQ2m"}
The reference page has NO JSON example of a tool_calls delta or of the usage chunk (reported by
the fetch). Canonical tool-call stream shape (illustrative, from the field list; the standard
OpenAI behaviour is: first fragment carries index, id, type, function.name and
arguments:""; later fragments carry only index and an arguments fragment):
{"id":"chatcmpl-1","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_a","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]}
{"id":"chatcmpl-1","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]},"finish_reason":null}]}
{"id":"chatcmpl-1","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}
{"id":"chatcmpl-1","object":"chat.completion.chunk","created":1,"model":"m","choices":[],"usage":{"prompt_tokens":25,"completion_tokens":12,"total_tokens":37}}
data: [DONE]
Accumulation (source, openai-python lib/streaming/chat/_completions.py): _accumulate_chunk
merges choice.delta.to_dict() into the message snapshot via accumulate_delta; for each
delta.tool_calls element the SDK locates the snapshot call by .index
(choice_snapshot.message.tool_calls[tool_call_chunk.index]). Partial-JSON parsing
(from_json(..., partial_mode=True)) is done ONLY for strict function tools with non-empty
arguments; for other tools parsed_arguments is set only when the arguments.done event fires
(re-checked 2026-10-08). The implementation file _deltas.py returned 404 in the fetch: merge
rules for id/name/strings UNVERIFIED here; the usual rule is string concatenation for
arguments.
Tool result message (illustrative, standard): assistant message with
tool_calls:[{"id","type":"function","function":{"name","arguments"}}] followed by
{"role":"tool","tool_call_id":"<id>","content":"<string>"} (see openai.md Chat section; Groq's
local-tool-calling page confirms in prose that each tool result message includes a tool_call_id
matching the call id ("Unique identifier for this tool call (used when sending results back)");
the earlier quoted sentence "must match the id from the assistant's tool_calls" was NOT found
verbatim and is dropped).
Tool-call streaming matrix (the core question)
| Streaming format | id | index | Evidence | |
|---|---|---|---|---|
| Groq | The local-tool-calling page's Python streaming example appends each chunk's delta.tool_calls to a list (collected_tool_calls.extend(chunk.choices[0].delta.tool_calls)); no prose sentence and no JSON example of a chunk. SDK type ChoiceDeltaToolCall has index: int, id: Optional[str], type, function. Whether arguments come as fragments or whole in one chunk: UNVERIFIED | optional per SDK type | required int per SDK type | console.groq.com/docs/tool-use/local-tool-calling (code example); groq-python chat_completion_chunk.py (source) |
| Together AI | Incremental: "the model returns tool calls incrementally, accessible from the delta.tool_calls object in each chunk" (single-call page; the longer wording "The first delta includes the call id, type, and function name with empty arguments. Later deltas add the argument text." is from the function-calling overview and was not re-confirmed: UNVERIFIED verbatim). That is the OpenAI format. Caveat: the doc's example of a "later delta" shows the whole arguments object in one fragment ("arguments": "{\"location\":\"New York City, USA\"}"), so a model/vendor may emit the full string in one fragment. The non-streaming parallel page shows "index": 0,1,2 in the same-function example but "index": null on every call in the multiple-different-functions example (see deviations) | first delta | per doc example index: 0 in both deltas; null seen in non-streaming multi-function example | docs.together.ai/docs/inference/function-calling/single-call.md, .../parallel.md |
| Fireworks | Incremental: "Arguments are sent incrementally as the model generates them." The page's example code accumulates in a dict keyed by tool_call.index, appends function.arguments fragments and parses after the stream finishes (the quoted "store id and function.name when they appear" / finish_reason == "tool_calls" wording was not re-confirmed: UNVERIFIED verbatim) | when it appears (first) | yes, accumulate by index | docs.fireworks.ai/guides/function-calling |
| vLLM | Depends on the --tool-call-parser; the stream generator delegates to parser.parse_delta(...) (not re-read), so the shape is parser-specific. For the hf parser: "When streaming, each tool call is emitted whole once its region parses." Other parsers (hermes etc.): UNVERIFIED whether fragments or whole | UNVERIFIED for streaming (non-stream default id format chatcmpl-tool-<uuid>, kimi_k2 parser uses functions.<name>:<idx>) | UNVERIFIED | docs.vllm.ai features/tool_calling; vllm chat_completion/serving.py, chat_utils.py (source) |
| Ollama | Not documented either way (streaming page: "Watch for streamed tool_calls in each chunk"). Source: the OpenAI layer copies fully parsed api.ToolCalls into a chunk delta (ToToolCalls JSON-marshals the whole Arguments map into one string), i.e. a call is most likely complete in one chunk; confirmation of "never fragmented" UNVERIFIED (parser internals not read) | generated server-side as "call_" + 8 random [a-z0-9] in ChatHandler (toolCallId(); the native chat path handleNativeChat was cut off in the fetch: UNVERIFIED there); omitempty on ToolCall.ID | Function.Index (int, json:"index", no omitempty) | docs.ollama.com/capabilities/tool-calling, /streaming; ollama openai/openai.go, server/routes.go, api/types.go (source) |
Design consequence (adapter): the accumulator must accept BOTH shapes with the same code path:
fragments (first with id+name+"" arguments, then pure arguments) and whole calls (id+name+full
arguments in one delta). Brisvox's LlmEvent::ToolCallDelta { index, id?, name?, arguments }
already allows both. Do not assume index is non-null (Together non-streaming parallel example)
or that id is present (Groq/Together/Fireworks types make it optional; Ollama omitempty).
Per-vendor details
Groq
Sources: https://console.groq.com/docs/openai , /docs/tool-use , /docs/tool-use/local-tool-calling ,
/docs/api-reference , /docs/rate-limits , /docs/errors , /docs/models , groq-python
types/chat/chat_completion_chunk.py and completion_create_params.py (source), all fetched
2026-10-08.
- Base URL
https://api.groq.com/openai/v1. Compat page: changebase_url, pass the Groq key. - Unsupported fields, "400 error if supplied":
logprobs,logit_bias,top_logprobs,messages[].name;nmust equal 1.temperature: 0is converted to1e-8. (The compat page does not mentionstream_optionsorparallel_tool_calls; the API reference does list both. The groq-pythoncompletion_create_params.pydefinesparallel_tool_callsbut NOTstream_options; it only hasstreamasLiteral[True/False].) - API reference:
stream(SSE, ends with "data: [DONE]");stream_options("Only set this when you setstream: true"; the property list is collapsed in the fetched text, so whetherinclude_usageis accepted is UNVERIFIED);parallel_tool_calls(boolean, defaulttrue);tool_choice(none/auto/required/named function, defaultnonewithout tools,autowith);max_completion_tokens(replaces deprecatedmax_tokens);service_tier(auto,on_demand(default),flex,performance;flex"will succeed or fail quickly").reasoning_effort: valuesnone|default|minimal|low|medium|high|xhigh|max, model dependent (qwen3 models:nonedisables reasoning,default/null = model default;qwen/qwen3.8-27badditionallylow|medium|high, its default isnone;openai/gpt-oss-20band-120b:low|medium|high, defaultmedium); "Values outside a model's supported set are rejected with a 400". - Streaming chunk: SDK type
ChatCompletionChunkhas top-levelusageand anx_groqobject withusage,id,debug,error,seed,usage_breakdown. The API reference callsx_groq"Groq-specific metadata for non-streaming chat completion responses"; where Groq puts usage in a stream (top-level on the last chunk, insidex_groq.usage, or on a trailing empty-choiceschunk): UNVERIFIED. Accept all three.x_groq.errorexists in the type: a mid-stream error may be delivered there (UNVERIFIED). finish_reason(SDK Literal):stop,length,tool_calls,function_call(deprecated). Nocontent_filterin the Groq type.deltaextras (SDK):reasoning(string; per the SDK docstring it is "only populated when the request'sreasoning_formatisparsed", so by default thinking may arrive inline incontentor be hidden depending onreasoning_format: the otherreasoning_formatvalues are UNVERIFIED),refusal,annotations,executed_tools(compound systems),function_call(deprecated). Reasoning text must NOT be spoken (see gotchas).- Parallel tool calls by model (tool-use page): parallel yes:
qwen/qwen3.8-27b,minimaxai/minimax-m2.7,llama-3.3-70b-versatile,llama-3.1-8b-instant; parallel NO:openai/gpt-oss-20b,openai/gpt-oss-120b,openai/gpt-oss-safeguard-20b. All listed support local/remote tool use (the models page listsqwen/qwen3.8-27bandminimaxai/minimax-m2.7under PREVIEW models, "not for production"). Per the local-tool-calling page's Error Handling section:tool_choice: "required"with no call -> 400;"none"can still produce a 400 on some models; a named function the model fails to call (or another function) -> 400. - Invalid tool call -> HTTP 400
failed_generation(local-tool-calling page; message "Invalid tool call generated"; retry advice: adjust temperature). The JSON below is as recorded earlier; on 2026-10-08 the fetch tool only confirmed the field namesmessage,type,failed_generation{reason,tool_call_id,attempted_arguments} and the example reason "Tool call arguments are not valid JSON", so the exact values (call_abc123, etc.) are UNVERIFIED:
{
"error": {
"message": "Invalid tool call generated",
"type": "invalid_request_error",
"failed_generation": {
"reason": "Tool call arguments are not valid JSON",
"tool_call_id": "call_abc123",
"attempted_arguments": "{'location': 'New York'}"
}
}
}
The errors page does not mention tool_use_failed. Whether this 400 can also occur mid-stream
(after 200): UNVERIFIED.
- Models (https://console.groq.com/docs/models, production):
llama-3.1-8b-instant560 tokens/s, 131,072 context, 131,072 max completion;openai/gpt-oss-20b1,000 tokens/s, 131,072 context, 65,536 max completion;openai/gpt-oss-120b500 tokens/s, 131,072 context, 65,536 max completion;llama-3.3-70b-versatile280 tokens/s, 131,072 context, 32,768 max completion. Speed figures are the page's, not measured TTFT. Low-latency candidates for voice:llama-3.1-8b-instant(parallel tools yes) andopenai/gpt-oss-20b(reasoning model, no parallel tools). Turkish quality: UNVERIFIED. - Status codes (errors page): 400, 401, 403, 404, 413, 422, 424, 429, 498 (custom: flex tier at
capacity), 499 (custom: caller cancelled), 500, 502, 503 (plus 200/206 informational); "You will
not be charged for requests that return server error codes" (500/502/503; 498/499 not stated).
Body (doc):
{"error": {"message": "String - description of the specific error", "type": "invalid_request_error"}}.
Together AI
Sources: https://docs.together.ai/docs/openai-api-compatibility , /docs/inference/function-calling/{overview,single-call,parallel,best-practices}.md , /reference/chat-completions.md , /docs/error-codes.md , /docs/serverless/rate-limits.md , /docs/serverless/models.md , /docs/inference/chat/parameters.md (fetched 2026-10-08).
- Base URL
https://api.together.ai/v1. Chat completions incl. streaming, function calling andresponse_formatare supported. - Differences from OpenAI (compat page):
service_tier,store,metadata,predictionaccepted but ignored;seedbest-effort;logit_biasunsupported on most models;nnot on every model; "Extra token counts such as cached and reasoning tokens can appear in different locations depending on the model, so clients may need to check both shapes"; chain-of-thought in areasoningorreasoning_contentfield on the assistant message, "the field name depends on the model";idandsystem_fingerprintuse Together formats; errors "follow OpenAI's shape, buttypeandcodevalues are Together's. Match on HTTP status codes". - Reference (
ChatCompletionChunk): requiredid,object(chat.completion.chunk),created,choices,model; optionalsystem_fingerprint,usage,warnings.choices[]:index,delta,finish_reason(nullable),logprobs,top_logprobs,seed.delta:role(required: UNVERIFIED as "required" on 2026-10-08), optionalcontent,reasoning,reasoning_content,tool_calls,function_call(deprecated),token_id.usage(prompt_tokens,completion_tokens,total_tokens) is nullable, "typically null except where populated".finish_reasonvalues:stop,eos,length,tool_calls,function_call.eosis a Together-only value; treat asStop. stream_optionsandparallel_tool_calls: NOT defined on the reference page (confirmed 2026-10-08:ChatCompletionRequesthas onlystreamas a boolean) nor the parameters page: support UNVERIFIED. Do not rely oninclude_usage; accept usage if present. The function-calling parallel page documents parallel calls as a model behaviour (three calls withindex0,1,2 and distinctids such ascall_aisak3q1px3m2lzb41ay6rwf), without a toggle.tool_choice(best-practices):"auto"default,"none","required", or{"type": "function", "function": {"name": "get_current_stock_price"}}.strict: trueconstrains arguments (streaming interaction not described).max_tokens;context_length_exceeded_behaviordefaults toerror(truncateoverridesmax_tokenswith the model maximum; per the reference,error"returns 400 with an appropriate error message", which CONFLICTS with the error-codes page's 403, see below).- Models (serverless catalog page; column "function calling" as extracted):
Qwen/Qwen3.5-9B(262,144 context, yes; $0.17 in / $0.25 out per 1M),zai-org/GLM-5.3-Flash(1,048,575, yes; $0.15 / $0.50, cached input $0.03),openai/gpt-oss-120b(131,072, yes; $0.15 / $0.60),meta-llama/Llama-3.3-70B-Instruct-Turbo(131,072, yes; $1.04 / $1.04),Qwen/Qwen3.8-Flash(1,000,000 context, no function calling listed),together/Tev1-4B-experimental(32,768, no). The page does not mention Llama 3.1 8B (earlier text "isn't in the serverless chat catalog" was an inference, not a page statement). The page has no latency figures; pick by measurement. Turkish: UNVERIFIED. - Errors (error-codes page, no JSON body shown): 400, 401, 402 (monthly spend limit), 403 is
documented as "Bad Request": "Input token count +
max_tokensparameter must be less than the context length of the model being queried" (a context overflow is documented as a 403 here, not 400, while the chat-completions reference says 400 forcontext_length_exceeded_behavior=error: pages disagree, handle both), 404 (bad URL or model name), 429 (serverless limited under high demand, or GPU quota for dedicated), 500, 503, 504, 524 (Cloudflare timeout), 529 (unspecified server error). "Serverless performance is best-effort."
Fireworks AI
Sources: https://docs.fireworks.ai/tools-sdks/openai-compatibility , /api-reference/post-chatcompletions , /guides/function-calling , /guides/querying-text-models , /guides/quotas_usage/rate-limits , /guides/inference-error-codes , /getting-started/quickstart (fetched 2026-10-08).
- Base URL
https://api.fireworks.ai/inference/v1. Model IDs areaccounts/fireworks/models/<id>(examples on the quickstart:glm-5p3-flash,kimi-k3,deepseek-v3p1,glm-5p3); dedicated deploymentsaccounts/<ACCOUNT_ID>/deployments/<DEPLOYMENT_ID>(UNVERIFIED on 2026-10-08, not re-seen in fetched text). - Compat page, "minor differences":
max_tokensbehaves differently when the context is exceeded (context_length_exceeded_behavior:truncateis the DEFAULT and lowersmax_tokens;errormatches OpenAI). No supported/unsupported parameter list exists on the page. - Streaming usage: Fireworks returns usage stats for streaming too. The compat page says "the
usagefield is returned in the very last chunk on the response"; the querying-text-models page says usage is in the final chunk, "the chunk withfinish_reasonset", and calls this a "Fireworks extension" (that label appears only on the querying page, not on the compat page). Earlier chunks have"usage":null(UNVERIFIED on 2026-10-08). The API reference says instead:stream_options(streaming only) "Unlike the OpenAI spec, Fireworks includes usage by default for streaming responses; set this tofalseto opt out" (parameterinclude_usage), and usage arrives as a final chunk, "not the chunk carryingfinish_reason", beforedata: [DONE](the "emptychoicesarray" wording and "Per-choiceusagemay appear on the finish chunk, depending on the deployment" were not re-confirmed: UNVERIFIED verbatim). The pages disagree; the adapter must take usage from whichever chunk carries it (finish chunk, trailing empty-choices chunk, or a per-choiceusage). Otherstream_optionsfields:include_internal_content("Omitted entirely from response chunks when false"),buffer_tokens("Honored only on the vLLM backend"),buffer_mode(default'any'),buffer_ms(not re-confirmed: UNVERIFIED); coalescing, adds latency, leave unset. - Request:
parallel_tool_calls(boolean, "Enable parallel function calling" per the reference; the guide says only "Some models support parallel tool calling ... Check the model's capabilities"),tool_choice(defaultauto;none;required(aliasany);allowed_tools; named function: these enumerations were not re-confirmed on 2026-10-08, UNVERIFIED). The API reference writes the named form as{ "type": "function", "name": "my_function" }while the guide writes{"type": "function", "function": {"name": "get_weather"}}(doc): conflicting; use the OpenAI nested form (guide) and test the other.max_tokens(max_completion_tokensis an "Alias for max_tokens. Cannot be specified together with max_tokens."),prompt_cache_key,prompt_cache_isolation_key(not re-confirmed),perf_metrics_in_response(streaming:perf_metricsobject on the final chunk; the reference says setting it totrue"additionally includes the same metrics in the response body under theperf_metricsfield"),response_format. Tool support per model is exposed as asupportsToolsfield (guide troubleshooting; where it appears is not described). - Chunk
delta(reference):role,content,reasoning_content,tool_calls,internal_content.finish_reason:stop,length,function_call,tool_calls; null until the end. - Cancel: "Close the connection to stop generation and avoid billing for ungenerated tokens."
- Errors (codes page): 400, 401, 402, 403, 404, 405, 408, 412, 413, 429, 500, 502, 503, 504, 520.
429 on serverless = account request/TPM limit exceeded; on dedicated/on-demand = deployment
saturated ("there are no account-level rate limits"). JSON error body example: not on the page
(UNVERIFIED shape; assume
{"error":{"message":...}}). - Model examples:
accounts/fireworks/models/glm-5p3-flash(a "Flash" variant; latency UNVERIFIED),deepseek-v3p1,kimi-k3. The model overview page does not list small fast models; see https://fireworks.ai/models (not fetched: UNVERIFIED). Turkish: UNVERIFIED.
vLLM (OpenAI-compatible server)
Sources: https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/ ,
https://docs.vllm.ai/en/latest/features/tool_calling/ , vllm source
entrypoints/openai/chat_completion/serving.py, entrypoints/chat_utils.py (raw GitHub, main)
(fetched 2026-10-08). The old URL .../serving/openai_compatible_server.html redirects to the one
above (UNVERIFIED on 2026-10-08).
- Start:
vllm serve NousResearch/Meta-Llama-3-8B-Instruct --dtype auto --api-key token-abc123(doc; the fetch confirmed the beginningvllm serve NousResearch/Meta-Llama-3-8B-Instruct, the remaining flags UNVERIFIED); client examplebase_url="http://localhost:8000/v1". Default served model name = the model argument (UNVERIFIED: not read). - Chat API:
parallel_tool_callssupported: "Setting theparallel_tool_callsparameter tofalseensures vLLM only returns zero or one tool call per request"; multiple calls withtrueare model dependent. Only unsupported item noted:image_url.detail;useris ignored. Extra parameters (selected):top_k,min_p,repetition_penalty,min_tokens,chat_template_kwargs,structured_outputs,priority,request_id,cache_salt(the list other thanstream_intervalwas not re-confirmed: UNVERIFIED),stream_interval("Number of tokens to batch into each streamed chunk"; leave at default for latency). The page does not mentionstream_options/continuous_usage_stats. - Source (
serving.py): withinclude_usagea trailing chunk withchoices=[]andusageis emitted after the token loop (serialized withexclude_none=True; the earlier note that it also usesexclude_unset=Truewas not confirmed);include_continuous_usageputsusageon EVERY chunk (also the initial role chunk and echo chunks, running counts; the exactcompletion_tokens=0on the first chunk is UNVERIFIED); every path ends withdata: [DONE](yielded unconditionally at the end of the generator, even after an error chunk). Errors mid-stream are yielded asdata: {...}error chunks (viacreate_streaming_error_responseor_convert_generation_error_to_streaming_responseforGenerationError), followed by[DONE](body shape not read: UNVERIFIED). AGenerationErrorframe is sent without an explicit extra[DONE]other than the generator's final one. finish_reason(source): when tool calls were streamed and the reason isstop, it is rewritten to"tool_calls"- EXCEPT when a namedtool_choicefunction is used, where it stays"stop"even though a tool call was produced. (Non-streaming: alsotool_callswhentool_choice == "required"and the engine saidstop.) Adapter rule: deriveToolCallswhen any tool call was accumulated, whatever the finish_reason says.- Tool calling requires
--enable-auto-tool-choice("mandatory") plus--tool-call-parser <name>(e.g.hermesfor Hermes/Qwen2.5,mistral,llama3_json,llama4_pythonic,pythonic,granite,deepseek_v3,kimi_k2,glm45,qwen3_xml, ... plus--tool-parser-plugin; the page lists many more, e.g.internlm,jamba,xlam,openai,hf). Without the flags a request withtool_choice: "auto"fails (exact behaviour/error text UNVERIFIED: the page only says the flag is mandatory).tool_choice: named function (default on),auto,required(vllm >= 0.8.3),none. Named function andrequireduse structured outputs: "guaranteed a validly-parsable function call - not a high-quality one". Parallel support is per model ("Parallel tool calls are not supported for Llama 3, but it is supported in Llama 4 models"; Mistral 7B struggling: not re-confirmed). Thehfparser rejects "strict tools, required or named tool choice, orparallel_tool_calls=false". Mistral parsing "requires tool call IDs that are exactly 9 digits" - per the page this applies to the Transformers tokenization backend only, where vLLM's longer ids cause an error; "providedtool_call_idfields are truncated to the last 9 digits". (Not inchat_utils.py.) - Tool call id format (source
make_tool_call_id):chatcmpl-tool-<uuid>;kimi_k2parser:functions.<name>:<idx>(used for model typeskimi_k2,kimi_k25,kimi_k3; so ids can be long; do not impose a short max). - Performance / concurrency: set by the deployment (GPU, batch size); not an API limit. 429/503 behaviour: not researched (UNVERIFIED). Model examples: any HF model; latency = deployment.
Ollama (OpenAI compatibility)
Sources: https://docs.ollama.com/api/openai-compatibility , /api/authentication , /api/errors ,
/api/usage , /capabilities/tool-calling , /capabilities/streaming , /faq ; ollama source
openai/openai.go, middleware/openai.go, server/routes.go, api/types.go (raw GitHub, main)
(fetched 2026-10-08). The old github.com/ollama/ollama/blob/main/docs/openai.md returned 404
(not re-checked 2026-10-08).
-
Base URL local
http://localhost:11434/v1/, cloudhttps://ollama.com/v1. Responses API (/v1/responses, non-stateful only) exists since v0.13.3. -
/v1/chat/completionssupported request fields:model,messages(text, base64 images, parts arrays; image URLs not supported),frequency_penalty,presence_penalty,response_format,seed,stop,stream,stream_options(onlyinclude_usage),temperature,top_p,max_tokens,tools,reasoning_effort,reasoning(effort: theeffortsub-field was not re-confirmed). Not supported:tool_choice,logit_bias,user,n, logprobs.max_completion_tokensandparallel_tool_callsare not in the list: whether they are ignored or rejected is UNVERIFIED (assume unknown JSON fields are ignored; make both opt-in per provider config). -
Streaming (source
middleware/openai.go): each chunk is written asdata: <json>followed by a blank line; order is content chunks, a dedicated finish chunk (emptydelta,finish_reason), then (only withinclude_usage) a second finish-style chunk withusageand a non-standardtimingsobject (durations in milliseconds) andchoices: [], thendata: [DONE]. Successful streams useContent-Type: text/event-stream. Chunk id isfmt.Sprintf("chatcmpl-%d", rand.Intn(999))(i.e. 0..998; collisions possible; do not treatidas unique).system_fingerprintis"fp_ollama".createdis taken from the first response and reused. The role is sent only on the first chunk. A response with both thinking and content/tool calls may be split into two chunks (ToStreamChunks); thinking goes indelta.reasoning(field namereasoningnot re-confirmed: UNVERIFIED). -
finish_reason(source):DoneReasonor"stop"; overridden to"tool_calls"only when the reason isstopand a tool call was sent; other reasons (e.g.length) pass through. -
Tool calls (source): each carries
id(generated ascall_+ 8 lowercase alphanumerics in the chat handler viamath/rand;omitemptyin the struct, so absent if a path does not set it),index,type: "function",function.name,function.arguments(a JSON string made by marshalling the parsed arguments map; empty if marshalling fails). The native API example showsindexinsidefunction({"type":"function","function":{"index":0,"name":"get_temperature","arguments":{"city":"New York"}}}, native API, arguments as an OBJECT, not a string); tool results in the native API use{"role":"tool","tool_name":...,"content":...}; on the OpenAI path usetool_call_id(UNVERIFIED how the server mapstool_call_idback; the docs never mention tool call ids). The native non-streaming path returns several parallel calls in one assistant message. -
Usage:
prompt_tokens,completion_tokens,total_tokens = prompt + eval, optionalprompt_tokens_details.cached_tokens(only if the cached count is present). Native metrics:prompt_eval_cached_count, durations in nanoseconds (load_duration,total_duration, ...); for streaming they are in the final chunk wheredoneistrue. -
Errors: native body is
{"error": "the model failed to generate a response"}(a STRING) with codes 200/400/404/429/500/502; "If an error occurs mid-stream, the error will be returned as an object in theapplication/x-ndjsonformat with anerrorproperty" and the status stays 200. On the OpenAI path (sourceopenai/openai.go):{"error": {"message","type","param","code"}}withtypeinvalid_request_error(400),not_found_error(404), elseapi_error;paramandcodeserialize asnull(not re-confirmed). The chat writer has no SSE error event: a non-200 status is sent as plainapplication/json; how a mid-stream failure appears on the OpenAI path: UNVERIFIED (routes.gostreamResponsewrites a final NDJSON error line for the native path once output has started). -
Concurrency (faq):
OLLAMA_NUM_PARALLELdefault 1 per model (the page says "default 1"),OLLAMA_MAX_LOADED_MODELSdefault 3 * GPUs (3 on CPU),OLLAMA_MAX_QUEUEdefault 512; a full queue answers "503 ... server is overloaded". Models stay loaded 5 minutes (OLLAMA_KEEP_ALIVE, per-requestkeep_aliveoverrides; negative = forever, 0 = unload: the per-request/negative/0 details were not re-confirmed). The documented OpenAI fields have nokeep_alive, so setOLLAMA_KEEP_ALIVEon the server or warm via the native API; the first request after unload paysload_duration. A cold model is the main latency risk for a voice call: send a warm-up request before the call. -
Context size: "The OpenAI API does not have a way of setting the context size for a model"; use a Modelfile with
PARAMETER num_ctxandollama create. -
Model examples on the compat page:
gpt-oss:20b,qwen3:8b,qwen3-vl:8b(chat examples),llama3.2(ollama pull llama3.2),gemma4:31b(cloud direct access;gemma4:cloudthrough a signed-in local server). Turkish quality: UNVERIFIED. -
Live results (2026-10-14, Ollama 0.34.1, RTX 5070 12 GB,
reasoning_effort = "none", default context 4096). Tool calling is per model and was measured: six scenarios x three runs through the real engine, and a warm voice-style latency (5 runs, p50). Details and failure notes:docs/TOOLS.md, "Local models through Ollama".Model GPU memory Tool scenarios (of 18) TTFT p50 First sentence p50 gemma4:12b-it-qat7.7 GB 18 69 ms 267 ms gemma4:e4b-it-qat3.1 GB 12 (fails the 3-round chain and the invalid-argument retry) 42 ms 157 ms qwen3.5:4b3.1 GB 12 (same two) 65 ms 208 ms qwen3.5:9b5.5 GB 12 (flaky; once answered with made-up weather, no tool call) 79 ms 291 ms qwen2.5-coder:7b4.7 GB 0 (writes the call as JSON text, which would be spoken) 34 ms 220 ms -
Thinking models. Set
reasoning_effort = "none"(the config key of the same name): theollamapreset does not send it by itself./v1/chat/completionshonours it (and the nestedreasoning: {effort: "none"}) but ignores the nativethink: false. Without itqwen3.5spent a whole 200-token budget on hidden reasoning and never spoke; withmediumeffort the 9b model passed all tool runs but took 14 to 35 s to the first word. -
GPU memory.
gemma4:12b-it-qatis 7.7 GB at the default context. With only ~5 GB free, 30 of 48 layers on the GPU gave first sentence p50 1.66 s (TTFT 376 ms; native API withnum_gpu = 30).
Known deviations (checklist for the adapter and its tests)
- Tool-call shape: fragments (OpenAI, Together, Fireworks) vs whole calls (Ollama most
likely; vLLM
hfparser). Same code path must accept both. Never requirearguments:""on the first delta. - Missing or null fields:
idoptional on delta types for Groq, Fireworks, Together, Ollama (omitempty);indexisnullin Together's non-streaming parallel-multiple example. Fallbacks: ifindexis missing, use the position intool_calls[]of that delta, and if a newidappears for an index already holding a differentid, start a new call. If noidever arrives, synthesise one (call_<n>) so thetoolmessage can reference it. Different servers emit ids of very different formats (call_xxx,chatcmpl-tool-<uuid>,functions.name:0): treat ids as opaque strings, no length limit assumed (OpenAI function names are limited to 64 chars, see openai.md). finish_reasonvalues:stop,length,tool_calls,content_filter(OpenAI),function_call(deprecated, OpenAI/Groq/Fireworks/Together),eos(Together). Mapping to brisvoxFinishReason:length->Length;tool_calls/function_call->ToolCalls;stop/eos/content_filter->Stop(content_filter has no variant, same gap as openai.md); additionally forceToolCallswhen any call was accumulated (vLLM namedtool_choicekeepsstop).- Usage placement: OpenAI: trailing chunk,
choices: [], before[DONE]. vLLM: same, plus optional per-chunk usage. Ollama: same (extratimings). Fireworks: documented as BOTH "on the finish chunk" and "separate trailing chunk" (pages disagree; on by default, opt-out). Groq: top-levelusageandx_groq.usageexist in the SDK type, placement UNVERIFIED. Together:usagenullable,stream_optionssupport UNVERIFIED, cached/reasoning counts "in different locations depending on the model". Rule: remember the last non-nullusageseen anywhere (top-level,x_groq.usage, per-choiceusageUNVERIFIED), emit it inDone; do not wait for it past the terminal event more than a short grace (a missing usage must not delayDone; the stream can end at[DONE]or at EOF). Cached tokens:usage.prompt_tokens_details.cached_tokens(OpenAI, Ollama); other vendors UNVERIFIED ->cached_input_tokens = 0. - Reasoning deltas: Groq
delta.reasoning(only withreasoning_format=parsed), Ollamadelta.reasoning, Fireworksdelta.reasoning_content, Togetherreasoning/reasoning_content(model dependent). These must never reach TTS: ignore them forTextDelta(optionally count latency to first non-reasoning token). With reasoning models the first visible token is later than the first chunk: first-output timeout must exceed that (as in openai.md). Prefer non-reasoning models orreasoning_effort: nonewhere supported (Groq: Qwen3; OpenAI: see openai.md). - Unsupported / rejected request fields: Groq 400 on
logprobs,logit_bias,top_logprobs,messages[].name,n != 1; Ollama lackstool_choice,n,logit_bias; Together ignoresservice_tier,store,metadata,prediction. Send only what is needed:model,messages,stream,tools(when non-empty),max_tokensormax_completion_tokens(configurable name),temperature; everything else behind explicit per-provider config (stream_options.include_usage,parallel_tool_calls,tool_choice). - Max-token field name: OpenAI/Groq prefer
max_completion_tokens(max_tokensdeprecated); Ollama, Together, Fireworks (accepts both, not together), vLLM (docs not read for it) documentmax_tokens. Make it a config enum, defaultmax_tokens. - Error bodies: OpenAI-style
{"error":{"message","type","param","code"}}(OpenAI, Groqmessage+type, Together "OpenAI's shape ... Together's values", Ollama OpenAI path); Ollama native{"error":"string"}; vLLM mid-streamdata: {...error...}then[DONE]; Ollama native mid-stream NDJSON{"error":...}with status 200. Parser: accepterroras an object or a string; also acceptx_groq.error. Never copy free text into logs/errors verbatim (can echo prompt), same policy as openai.md. HTTP status is the portable signal (Together's own advice). - Context-overflow status: Together documents 403 (not 400) for input +
max_tokensover the context on its error-codes page (but its chat-completions reference says 400 forcontext_length_exceeded_behavior=error); Fireworks silently truncatesmax_tokensby default (truncate); OpenAI/Groq 400 (Groq 400 not re-verified for this case). Map any of 400/403/413/422 on the pre-stream response toRejected { status }unchanged; the LLM stage decides. - Custom status codes: Groq 498 (flex tier at capacity) and 499; Together 524, 529; Fireworks 520; Ollama 502 (unreachable cloud model) and 503 (queue full). Treat 429, 498, 502, 503, 504, 520, 524, 529 as retryable-at-turn-level (the adapter itself never retries, like openai.md).
- SSE terminator: all five document or implement
data: [DONE](Groq stated in the API reference; Together, Fireworks stated in references (not re-confirmed for Together); vLLM and Ollama in source). A stream that ends without[DONE]and without afinish_reasonwas cut off ->Error(Disconnected). A stream that has afinish_reasonbut no[DONE]before EOF: accept as complete only after a short bounded wait for the usage chunk (UNVERIFIED how each vendor behaves). - Non-standard chunk fields: Together
warnings,token_id,seed,top_logprobs; Ollamatimings,system_fingerprint: "fp_ollama"; Fireworksperf_metrics,internal_content; Groqx_groq. Parse leniently, nodeny_unknown_fields; parsetype-like discrimination in two steps as openai.md does for events. - Chunk
idnot unique (Ollama random number < 999): never key state by chunkid. - Empty
choicescan appear for usage chunks (all) and possibly for keep-alives (UNVERIFIED): treatchoices: []as "no content", not an error.choices[0]only (n = 1). - Role-only first chunk
delta: {"role":"assistant","content":""}(OpenAI) orcontent: null: empty string must not produce aTextDelta.
Rate limits and error bodies
| Headers (documented) | Notes | |
|---|---|---|
| OpenAI | x-ratelimit-limit-requests, -limit-tokens, -remaining-requests, -remaining-tokens, -reset-requests, -reset-tokens, x-request-id, Retry-After sometimes (openai.md) | |
| Groq | retry-after (seconds, set only on 429; sample value 2), x-ratelimit-limit-requests (14400, always refers to daily RPD), x-ratelimit-limit-tokens (18000, always per-minute TPM), x-ratelimit-remaining-requests (14370), x-ratelimit-remaining-tokens (17997), x-ratelimit-reset-requests (2m59.56s), x-ratelimit-reset-tokens (7.66s) (page says the sample values are "illustrative"; the six x-ratelimit-* headers are on every response) | Example limits for openai/gpt-oss-120b: 30 RPM, 1K RPD, 8K TPM, 200K TPD; the page says the listed values are base Developer plan limits (the page does not say which plan that specific row is: UNVERIFIED). Limits per model and plan; check the account Limits page. Voice agents on low plans will hit TPM quickly |
| Together | none documented (rate-limits page names no headers, tiers or values) | 429 and 503 under high demand; "avoid sending large bursts"; "Use exponential backoff when retrying"; "Serverless performance is best-effort" |
| Fireworks | none documented on the rate-limits page (response-header names UNVERIFIED; the error-codes page mentions "rate-limit response headers" for serverless 429s without naming them) | Account-wide: no payment method/credits 10 RPM; with payment method and credits 6,000 RPM (fixed ceiling, "applies to all API usage on the account", "regardless of spending tier"); tiers by spend: Tier 1 valid payment method, Tier 2 $50, Tier 3 $500, Tier 4 $5,000 (spend or add credits), Unlimited (contact sales) set serverless TPM upper bounds (not RPM). Dedicated/on-demand: no account limits, 429 = saturation (the claim that such requests still count towards the account request limit is UNVERIFIED) |
| vLLM | none (deployment-defined) | UNVERIFIED |
| Ollama | none; queue-full -> 503 | OLLAMA_MAX_QUEUE 512, OLLAMA_NUM_PARALLEL 1 default |
Adapter rule: log retry-after and x-ratelimit-* when present (bounded body read, same limits as
openai.md: 8 KiB, 1 s), never retry inside the adapter.
Mapping onto brisvox (crates/brisvox-providers/src/llm.rs)
LlmRequest { messages, tools }->{"model", "messages":[...], "stream":true, "tools":[{"type": "function","function":{"name","description","parameters"}}]}.Role::Tool->{"role":"tool","tool_call_id","content"};Role::Assistantwithtool_calls->tool_calls:[{"id","type":"function","function":{"name","arguments"}}]withcontentnull/"" when empty.Message::provider_datais ignored (not this adapter's). Tools whoseparameters_schemais not valid JSON ->Protocolbefore any I/O. Omittoolswhen empty (some servers rejecttools: []: UNVERIFIED, but omitting is safe).- Events:
delta.content(non-empty) ->TextDelta;delta.tool_calls[]->ToolCallDelta { index, id, name, arguments }(fragment or whole); terminal chunk /[DONE]->Done. Dense index renumbering is safe (key by upstreamindex, fallback by id). - Pre-stream HTTP errors ->
Err(Rejected { status }); in-stream error object ->LlmEvent::Error. - Timeouts: connect, first-byte, first-output (longer for reasoning models), idle between events,
total; none of the five publishes a keepalive spec (SSE comment lines
:should be tolerated, they do not count as progress, same as openai.md). - Cancel: drop
LlmStream-> drop the HTTP body (closes the connection; only Fireworks documents billing stops). - Warm-up:
HEAD/connection pre-warm as in openai.md is optional; for Ollama a real tiny generation (native API) may be needed to load the model (see above). UNVERIFIED thatHEAD {base}/chat/completionsanswers any status on all five.
Latency-oriented model examples (docs list names/speeds only; no TTFT numbers anywhere)
- Groq:
llama-3.1-8b-instant(560 tokens/s),openai/gpt-oss-20b(1,000 tokens/s),openai/gpt-oss-120b(500 tokens/s). - Together:
Qwen/Qwen3.5-9B,zai-org/GLM-5.3-Flash,openai/gpt-oss-120b(function calling listed yes for these). - Fireworks:
accounts/fireworks/models/glm-5p3-flash. - Ollama: measured, see "Live results" in the Ollama section above (
gemma4:12b-it-qatrecommended);gpt-oss:20b,qwen3:8b,llama3.2are names from the vendor's examples (not measured). - vLLM: whichever model the deployment serves. Pricing pages were not fetched (UNVERIFIED for all vendors except the Together per-1M prices above, which were re-checked 2026-10-08 on the serverless models page).
Implementation notes
Code: brisvox_providers::openai_compatible (feature provider-openai-compatible), tests in
crates/brisvox-providers/tests/openai_compatible/ (one loopback mock, one wire dialect per
preset, the shared contract suite once per preset, protocol tests, an opt-in live test).
- Reading is one tolerant path, not a flag per vendor. The docs disagree with themselves
(Fireworks usage placement, Together
index: null, Groq usage location), so the chunk mapper accepts every documented shape at once: tool calls as fragments or whole, missing /nullindex, empty or missingid(syntheticcall_<n>), usage at the top level, inx_groq.usageor in the choice, finish wordsstop/length/tool_calls/function_call/eos/content_filter/ unknown,argumentsas a string (or, defensively, a JSON object), error bodies as objects or strings,x_groq.error.Quirksonly decides what is sent and how long the end of a response waits (see below). - Call identity. A fragment with an
indexbelongs to the newest call with that index, unless it carries a different non-emptyid(then it opens a new call: a vendor numbering every call 0), and so does a fragment whosenamediffers from the name the call already has (whole calls of an Ollama-style vendor may all sayindex: 0and carry no id; a repeated identical name is taken as a continuation). Without anindex: a knownidcontinues that call; noidbut anameopens a call; neither continues the newest call. Names are emitted once. - Finish. A response that accumulated tool calls ends in
ToolCallswhen the vendor saidstop/eos/ unknown (vLLM keepsstopfor a namedtool_choice), but staysLengthwhen it was cut bylength(the arguments may be truncated; the caller must not run them). - When
Doneis emitted. At the chunk withfinish_reasonif that chunk carries usage orQuirks::usageisFinishChunk/Absent; otherwise (TrailingChunk, the default and the presets for Groq, Fireworks, vLLM, Ollama) at the usage chunk,[DONE], EOF or afterusage_grace(250 ms), whichever is first. Only usage on the finish chunk itself counts as final: running totals on earlier chunks (vLLMcontinuous_usage_stats) are kept, but the response still waits for the trailing chunk and reports the last usage seen. WithFinishChunk/Absenta separate usage chunk after the finish chunk is deliberately not waited for (the preset says there is none; a vendor that sends one belongs onTrailingChunk). A stream that ends or breaks (EOF, reset, cut chunked body) after itsfinish_reasonis complete; one that ends before it isDisconnected;[DONE]without anyfinish_reasoncompletes asStopwhen no tool call is open (text only). Decision (review fix):[DONE]without afinish_reasonwhile tool-call fragments were seen is a cut, mapped toDisconnectedexactly like an end of body there: a proxy or gateway can abort a generation mid call and still write[DONE], and the consumer must not getDone { ToolCalls }with truncated arguments (a SideEffect tool could run on them). The conservative side was chosen because a well-formed tool-call turn always carries afinish_reasonon all five vendors' documented streams; a custom server that omits it for tool calls will seeDisconnected. - Decision: tool-call shape,
finish_reasonvariants and error body shapes are read by one tolerant path, not selected by per-vendor flags (the original plan asked for quirk flags for them). Every shape the docs list is accepted at once, so a preset cannot be wrong about them and a custom server needs no flag;Quirkskeeps only what changes the request or the wait (max_tokens_field,request_stream_usage,usage,parallel_tool_calls,tool_choice) and the one read-side switchsynthesize_missing_ids(off: a call without an id is a protocol error). Not offered: forcing fragmented-only parsing, or makingstopwith tool calls an error. Add a flag when a live run shows a vendor whose stream the tolerant path misreads. - URL assembly. The chat URL is built with a real URL parser, not by string concatenation:
chat_pathis appended to the path ofbase_url, the query ofbase_url(?key=...) stays a query after the path, a query onchat_pathis appended to it with&, and fragments are dropped (they are never sent).https://gw.example/v1?key=abc+/chat/completionsposts to/v1/chat/completions?key=abc. A base URL that cannot carry a path (mailto:) is refused at construction. - Secrets in the endpoint. The base URL and chat path may carry credentials. They are never
logged (transport errors are logged by class, not with reqwest's URL-bearing Display), and
Debugof the configuration prints scheme, host and port plus header and body-field names.extra_bodymust not settemperature/parallel_tool_callswhile the adapter sets them (no duplicate keys in the body). - Never spoken / never used.
delta.reasoning,reasoning_content,internal_content,refusal,annotations,x_groqextras,timings,warnings, the chunkid. Arefusalis dropped, so a model that refuses answers with silence (UNVERIFIED whether any of the five sends refusals on Chat Completions; revisit with the live run). - Sent only when configured:
stream_options.include_usage(Groq, vLLM, Ollama presets),parallel_tool_calls,tool_choice(no preset sets them: Ollama has notool_choice, the parallel-call support of Groq differs per model),max_completion_tokensinstead ofmax_tokens(Groq preset).toolsis omitted when empty. Replayed assistant tool calls with empty arguments are sent as"{}"(a server that parses the string back would reject""), an assistant turn that only called tools hascontent: null.Message::provider_datais ignored. - Errors. Pre-stream non-2xx is
Rejected { status }unchanged (including Together's 403 for context overflow, Groq 498/499, Fireworks 520, Together 524/529).Retry-Afterand the sanitised errortype/codeare logged, the message never. In-streamerror(object or string): a numericcode/status400-599 is that status;rate_limit_*-> 429,server_error/api_error-> 500,overloaded/service_unavailable-> 503,invalid_request_error-> 400,authentication_error-> 401; anything else isProtocol. Ollama's native mid-stream NDJSON error line ({"error": "..."}withoutdata:) is not SSE: the SSE parser ignores it and the response ends asDisconnected(UNVERIFIED whether the/v1path ever emits it). - Warm-up is an unauthenticated
HEADto the chat URL (UNVERIFIED that every vendor answers it; any HTTP status counts, a refused connection isDisconnected). It does not load an Ollama model: send a real tiny request before the call (see Ollama above). - Usage cached tokens are read from
usage.prompt_tokens_details.cached_tokensonly (OpenAI, Ollama); other locations Together mentions are UNVERIFIED, so those vendors report 0. - Bounds: 128 tool calls, 1 MiB arguments per call, 4 MiB per response, SSE event cap 1 MiB
(all
Protocolwhen exceeded), event channel 64 with back-pressure. - Plain http: allowed for loopback; a vLLM / Ollama on another host needs
allow_insecure_http. - Live:
BRISVOX_LIVE_TESTS=1plusGROQ_API_KEY/TOGETHER_API_KEY/FIREWORKS_API_KEYand/orOPENAI_COMPATIBLE_BASE_URL+OPENAI_COMPATIBLE_MODEL(+OPENAI_COMPATIBLE_API_KEY). It prints the number of tool-call fragments and the time to first token per vendor; use the output to fill in the UNVERIFIED items below .
Live-test acceptance
Blocked: needs API keys (Groq, Together, Fireworks) and a local vLLM / Ollama.
BRISVOX_LIVE_TESTS=1with the keys in.env:tests/openai_compatible/live.rspasses per vendor (streaming text and a one-tool loop).- Confirm or correct every item in the UNVERIFIED list below, and fix the mock dialects in
tests/openai_compatible/wire.rswhere a live capture differs: tool-call fragmentation per vendor, usage placement andstream_optionsacceptance,finish_reasonwords, mid-stream error body shapes, whether the five vendors send[DONE]after a tool-callfinish_reason,HEADwarm-up answers.
UNVERIFIED (consolidated)
- Wire captures of
delta.tool_callsfor every vendor (no live run; docs have no chunk JSON except Together's single "later delta" and the OpenAI first chunk). - Groq: placement of streaming usage (top-level vs
x_groq.usage),stream_options.include_usageacceptance (not in groq-python params), whether tool-call arguments are fragmented, mid-stream error delivery (x_groq.error), Authorization header spelled out as text, exactfailed_generationJSON values,reasoning_formatvalues other thanparsed. - Together:
stream_options/include_usageandparallel_tool_callsparameters, usage in the last chunk, rate-limit headers, JSON error body, streamingindexalways integer, 400 vs 403 for context overflow, verbatim wording of the overview-page streaming sentence. - Fireworks: which usage placement is real (pages conflict), error JSON body, response rate-limit
headers, small/fast model list,
parallel_tool_callsper-model support, namedtool_choiceform (conflict{"type":"function","name"}vs nested),tool_choiceenumeration,buffer_ms,prompt_cache_isolation_key, dedicated-deployment model id format. - vLLM: default host/port, auth header name, streaming fragmentation per parser (only
hfdocumented as whole), exact failure behaviour without--enable-auto-tool-choice, mid-stream error body shape,stream_optionsacceptance in docs (source showsinclude_usage),max_tokensvsmax_completion_tokenshandling, 429/503 behaviour, fullvllm serveflags. - Ollama: whether streamed tool calls are always whole, whether
max_completion_tokens/parallel_tool_callsare ignored or rejected, mid-stream error shape on/v1, mapping oftool_call_idback to a call,Authorizationhandling for a reverse-proxied local server,handleNativeChattool-call id assignment. - Turkish support of every listed model and vendor; regions/data residency of every vendor; keepalive/idle timeouts of every vendor; pricing; TTFT numbers.
- openai-python
_deltas.pymerge rules (404 in the fetch); the openai.md claim about GPT-6 and Responses.
Sources (all fetched 2026-10-08)
- OpenAI chunk reference: https://developers.openai.com/api/reference/resources/chat/subresources/completions/streaming-events
- OpenAI SDK: https://raw.githubusercontent.com/openai/openai-python/main/src/openai/types/chat/chat_completion_chunk.py , .../types/chat/completion_create_params.py , .../lib/streaming/chat/_completions.py , https://developers.openai.com/api/docs/guides/function-calling
- Groq: https://console.groq.com/docs/openai , https://console.groq.com/docs/tool-use , https://console.groq.com/docs/tool-use/local-tool-calling , https://console.groq.com/docs/api-reference , https://console.groq.com/docs/rate-limits , https://console.groq.com/docs/errors , https://console.groq.com/docs/models , https://console.groq.com/docs/text-chat , https://raw.githubusercontent.com/groq/groq-python/main/src/groq/types/chat/chat_completion_chunk.py , .../completion_create_params.py
- Together: https://docs.together.ai/docs/openai-api-compatibility , https://docs.together.ai/docs/function-calling , https://docs.together.ai/docs/inference/function-calling/single-call.md , .../parallel.md , .../overview.md , .../best-practices.md , https://docs.together.ai/reference/chat-completions.md , https://docs.together.ai/docs/error-codes.md , https://docs.together.ai/docs/serverless/rate-limits.md , https://docs.together.ai/docs/rate-limits , https://docs.together.ai/docs/serverless/models.md , https://docs.together.ai/docs/inference/chat/parameters.md , https://docs.together.ai/llms.txt
- Fireworks: https://docs.fireworks.ai/tools-sdks/openai-compatibility , https://docs.fireworks.ai/api-reference/post-chatcompletions , https://docs.fireworks.ai/guides/function-calling , https://docs.fireworks.ai/guides/querying-text-models , https://docs.fireworks.ai/guides/quotas_usage/rate-limits , https://docs.fireworks.ai/guides/inference-error-codes , https://docs.fireworks.ai/getting-started/quickstart , https://docs.fireworks.ai/models/overview
- OpenRouter: https://openrouter.ai/docs/api-reference/overview (fetched 2026-10-14)
- vLLM: https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/ , https://docs.vllm.ai/en/latest/features/tool_calling/ , https://raw.githubusercontent.com/vllm-project/vllm/main/vllm/entrypoints/openai/chat_completion/serving.py , https://raw.githubusercontent.com/vllm-project/vllm/main/vllm/entrypoints/chat_utils.py
- Ollama: https://docs.ollama.com/api/openai-compatibility , https://docs.ollama.com/api/authentication , https://docs.ollama.com/api/errors , https://docs.ollama.com/api/usage , https://docs.ollama.com/capabilities/tool-calling , https://docs.ollama.com/capabilities/streaming , https://docs.ollama.com/faq , https://raw.githubusercontent.com/ollama/ollama/main/openai/openai.go , .../middleware/openai.go , .../server/routes.go , .../api/types.go
Fact-checked 2026-10-08: 78 claims checked, 13 corrected. Corrections: (1) Groq local-tool-calling page has no "Accumulate delta.content and delta.tool_calls" sentence, only a code example (collected_tool_calls.extend); (2) Groq "tool_call_id must match" quote not found verbatim, replaced by the page's actual wording; (3) Groq rate limits: values are "base Developer plan" limits, header sample values "illustrative", retry-after sample 2; (4) Groq delta.reasoning only populated when reasoning_format is parsed; (5) groq-python params do not define stream_options (only the API reference does); (6) Groq errors page does show a {message, type} body example; Groq tool_choice 400 behaviours and qwen/minimax models (preview, not production) clarified; (7) Fireworks: "finish_reason set" and "Fireworks extension" wording is from querying-text-models only, compat page says "very last chunk"; (8) Together context overflow: reference says 400 for context_length_exceeded_behavior=error, error-codes page says 403 (conflict recorded); Together 429/524/529 meanings and Llama 3.1 8B statement (page silent) fixed; (9) Mistral 9-digit ids apply to the Transformers tokenization backend only and are not in chat_utils.py; (10) vLLM serialisation is exclude_none (the exclude_unset claim unconfirmed), include_continuous_usage also covers initial/echo chunks; vLLM parser list is longer; (11) openai-python partial-JSON parsing applies only to strict tools; (12) Ollama chunk id is chatcmpl-%d with rand.Intn(999); timings only on the usage chunk; stream_options only include_usage; extra model examples qwen3-vl:8b; (13) Together Qwen3.8-Flash context is 1,000,000 and prices added; Fireworks 6,000 RPM is a fixed ceiling and the "requests still count towards the account limit" claim is unconfirmed. Items downgraded to UNVERIFIED: see the consolidated list above.