OpenAI Compatible Provider

OpenAiCompatProvider implements the OpenAI Chat Completions API. One implementation covers OpenAI, xAI, Groq, Cerebras, OpenRouter, Mistral, DeepSeek, MiniMax, Meta (Muse Spark), Z.ai, Qwen, Ollama, and any other compatible API.

For the first-class ModelConfig::* constructors and default model metadata, see Model Presets.

Usage

Requires StreamConfig.model_config; its compat flags are optional (None uses OpenAiCompat::default()):

#![allow(unused)]
fn main() {
use yoagent::provider::ModelConfig;

let agent = Agent::from_config(ModelConfig::openai("gpt-5.5", "GPT-5.5"));
}

OpenAiCompat Quirk Flags

Different providers have behavioral differences even though they share the same API:

#![allow(unused)]
fn main() {
pub struct OpenAiCompat {
    pub supports_store: bool,
    pub supports_developer_role: bool,
    pub supports_reasoning_effort: bool,
    pub supports_thinking_control: bool,
    pub supports_usage_in_streaming: bool,
    pub max_tokens_field: MaxTokensField,       // MaxTokens or MaxCompletionTokens
    pub requires_tool_result_name: bool,
    pub requires_assistant_after_tool_result: bool,
    pub thinking_format: ThinkingFormat,        // OpenAi, Xai, or Qwen
    pub supports_prompt_cache_key: bool,
    pub replays_reasoning_content: bool,
    pub encrypted_reasoning: bool,              // Responses / Azure only; opt-in, see openai-responses.md
    pub max_reasoning_effort: ReasoningEffortCeiling, // High (default), XHigh, Max
}
}

OpenAiCompat is #[non_exhaustive]: start from a preset or Default::default() and set fields.

Provider Presets

ProviderConstructorKey Differences
OpenAIOpenAiCompat::openai()developer role, max_completion_tokens, store, reasoning_effort
xAI (Grok)OpenAiCompat::xai()reasoning field for thinking (not reasoning_content); effort ceiling xhigh
GroqOpenAiCompat::groq()Standard defaults
CerebrasOpenAiCompat::cerebras()Standard defaults
OpenRouterOpenAiCompat::openrouter()max_completion_tokens
MistralOpenAiCompat::mistral()max_tokens field
DeepSeekOpenAiCompat::deepseek()max_tokens, thinking, reasoning_effort, 1M context window
MiniMaxOpenAiCompat::minimax()Standard defaults, 1M context window
Meta (Muse Spark)OpenAiCompat::meta()max_completion_tokens, reasoning_effort, streaming usage
Z.ai (Zhipu)OpenAiCompat::zai()Standard defaults
QwenOpenAiCompat::qwen()Qwen reasoning content format, max_tokens, streaming usage
OllamaOpenAiCompat::ollama()Inserts an empty assistant message after tool result runs

OpenAiCompat presets are lower-level quirk flags. A provider is first-class when it also has a ModelConfig::* constructor; see Model Presets.

DeepSeek context caching is automatic on DeepSeek's side. yoagent does not send cache_control markers for DeepSeek, but it does parse DeepSeek's prompt_cache_hit_tokens and prompt_cache_miss_tokens usage fields into Usage.cache_read and Usage.input.

Adding a New Compatible Provider

  1. Add a constructor to OpenAiCompat:
#![allow(unused)]
fn main() {
impl OpenAiCompat {
    pub fn my_provider() -> Self {
        Self {
            supports_usage_in_streaming: true,
            // set flags as needed...
            ..Default::default()
        }
    }
}
}
  1. Create a ModelConfig that uses it:
#![allow(unused)]
fn main() {
let config = ModelConfig::openai_compat(
    "https://api.myprovider.com/v1",
    "my-model",
    "my-provider",
    OpenAiCompat::my_provider(),
);
}

Thinking/Reasoning

With supports_reasoning_effort, ThinkingLevel becomes reasoning_effort. How far up the ladder it goes is the model's declared max_reasoning_effort (a ReasoningEffortCeiling):

LevelCeiling High (default)Ceiling XHighCeiling MaxDeepSeek-style (supports_thinking_control)
Offomitted¹omitted¹omitted¹omitted; thinking: disabled
Minimal, Lowlowlowlowlow
Mediummediummediummediummedium (DeepSeek rounds it up to high)
Highhighhighhighhigh
XHighhigh (clamped)xhighxhighhigh (clamped)
Maxhigh (clamped)xhigh (clamped)maxmax

¹ Omitting the effort does not turn reasoning off on an OpenAI reasoning model — it runs at the model's default, medium — and xAI's Grok cannot disable reasoning at all (default high). The crate never sends OpenAI's none rung, which several models (GPT-6 Astra, gpt-5, the o-series) reject with HTTP 400. A clamped XHigh/Max is logged once per process with tracing::warn!.

A model rejects an effort string it does not know rather than rounding it, which is why the ceiling is declared per model and never guessed from the id. Per OpenAI's model pages and Azure's reasoning guide (2026-09-25): max exists on GPT-5.6 and GPT-6; xhigh on those plus GPT-5.5, GPT-5.4, gpt-5.2 and gpt-5.1-codex-max; gpt-5 and gpt-5.1 top out at high. The gpt_5_5() preset declares XHigh; OpenAiCompat::xai() declares XHigh, because xAI treats xhigh as high on Grok models without the rung instead of rejecting it. For a model with no preset:

#![allow(unused)]
fn main() {
use yoagent::provider::{ModelConfig, ReasoningEffortCeiling};

let mut config = ModelConfig::openai("gpt-5.4", "GPT-5.4");
let compat = config.compat.as_mut().unwrap();
compat.max_reasoning_effort = ReasoningEffortCeiling::XHigh;
}

The DeepSeek-style column needs both supports_thinking_control and supports_reasoning_effort, and ignores max_reasoning_effort. DeepSeek's reasoning_effort accepts low/high/max (DeepSeek thinking-mode docs). DeepSeek itself maps a requested xhigh to high, so this crate sends XHigh as high there too and only Max selects DeepSeek's max rung.

The OpenAI Responses and Azure OpenAI providers apply the same ceiling columns, read from ModelConfig::compat.

The ThinkingFormat enum controls how reasoning content is parsed from streams:

  • ThinkingFormat::OpenAi — Uses reasoning_content field (DeepSeek, default)
  • ThinkingFormat::Xai — Uses reasoning field (Grok)
  • ThinkingFormat::Qwen — Uses reasoning_content field (Qwen)

Local Servers (LM Studio, Ollama, llama.cpp, vLLM)

Use ModelConfig::ollama() for Ollama, or ModelConfig::local() for any other local OpenAI-compatible server. No API key required:

#![allow(unused)]
fn main() {
use yoagent::agent::Agent;
use yoagent::provider::ModelConfig;

// The `local` provider resolves to an empty API key automatically — none needed.
let agent = Agent::from_config(ModelConfig::local("http://localhost:1234/v1", "my-model"));
}

For Ollama:

#![allow(unused)]
fn main() {
let agent = Agent::from_config(ModelConfig::ollama("http://localhost:11434/v1", "llama3.1:8b"));
}

Or via the CLI example:

cargo run --example cli -- --api-url http://localhost:1234/v1 --model my-model

For locally deployed open-source model families, keep the local endpoint and choose the model-family compat profile:

#![allow(unused)]
fn main() {
let qwen_local = ModelConfig::openai_compat(
    "http://localhost:1234/v1",
    "qwen3-local",
    "qwen",
    OpenAiCompat::qwen(),
);
}

Serving-layer quirks and model-family quirks can be combined because OpenAiCompat fields are public:

#![allow(unused)]
fn main() {
let mut compat = OpenAiCompat::qwen();
compat.requires_assistant_after_tool_result = true;

let qwen_on_ollama = ModelConfig::openai_compat(
    "http://localhost:11434/v1",
    "qwen2.5-coder:7b",
    "ollama",
    compat,
);
}

GitHub Copilot (bring-your-own-token)

Terms of service. api.githubcopilot.com is intended for use through official GitHub Copilot editor integrations. Accessing it from a third-party agent is against GitHub's Copilot terms of service and may result in token revocation or account suspension. yoagent does not ship a first-class Copilot preset for this reason. The configuration below is documented only for users who understand and accept that risk. Use at your own discretion.

Copilot's chat endpoint is OpenAI Chat Completions–shaped, so it works with OpenAiCompatProvider given the right base URL, integration headers, and a valid Copilot token as the API key:

#![allow(unused)]
fn main() {
use yoagent::agent::Agent;
use yoagent::provider::{ModelConfig, OpenAiCompat};

let mut config = ModelConfig::openai_compat(
    "https://api.githubcopilot.com",
    "gpt-5.5",
    "copilot",
    OpenAiCompat::openai(),
);
// Copilot fingerprints clients via these headers; they are required.
config.headers.insert("Copilot-Integration-Id".into(), "vscode-chat".into());
config.headers.insert("Editor-Version".into(), "Neovim/0.10.0".into());

let agent = Agent::from_config(config)
    .with_api_key(copilot_token); // see below
}

The API key is a short-lived Copilot token, not your GitHub token. You obtain it by exchanging a GitHub OAuth token (from the device-login flow, or from the local Copilot config under ~/.config/github-copilot/) at https://api.github.com/copilot_internal/v2/token. That token expires after ~25–30 minutes.

yoagent has no built-in credential refresh — api_key is static for the life of the provider (Authorization: Bearer {api_key}). For anything longer than a single short turn, you must exchange and refresh the token yourself and rebuild the agent's config with a fresh token before it expires; otherwise long runs will fail with 401.

Stop reasons

finish_reason maps to StopReason: stop → Stop, length → Length, tool_calls → ToolUse, and content_filter → Refusal, with the assistant message's error_message set to say the content filter stopped the response. A turn that carries tool calls is reported as ToolUse unless it ended on length or content_filter. A usage token count sent as explicit null reads as 0 instead of dropping the chunk.

A failure reported inside the stream is an error, not a finished answer: a chunk with an error object (OpenRouter and other gateways send one, with a string or numeric code) and finish_reason: "error" both fail the turn. A rate limit or overload among them (rate_limit_exceeded, a numeric 503/529, …) is retried; anything else is final.

Auth

Uses Authorization: Bearer {api_key} header. Extra headers can be added via ModelConfig.headers.