feat(auxiliary): add xAI backend for text and vision tasks - #7076
feat(auxiliary): add xAI backend for text and vision tasks#7076Julientalbot wants to merge 3 commits into
Conversation
The auxiliary client resolution chain (compression, session_search,
vision, web_extract, etc.) had no direct xAI support. Users with
XAI_API_KEY had to rely on either the main provider fallback
(only if they were also on xAI as main) or routing through OpenRouter,
which defeats the purpose of using xAI direct for cheap/fast side tasks.
In particular, the vision auto-detection order was limited to OpenRouter
and Nous, meaning users with both XAI_API_KEY and OPENROUTER_API_KEY
saw every vision analysis routed through OpenRouter even if their main
provider was xai — burning OpenRouter credits and adding latency while
ignoring Grok 4's native vision support.
Add xAI as a first-class auxiliary backend:
- `_try_xai()` — new helper following the same pattern as `_try_openrouter()`
and `_try_nous()`. Reads the credential pool first, then falls back to
`XAI_API_KEY`. Returns a default model that is both vision-capable and
cheap: `grok-4-1-fast-non-reasoning` (text+image input, fast, ~$0.20/$0.50
per M tokens, no reasoning overhead — ideal for side tasks).
- `_XAI_DEFAULT_BASE_URL` and `_XAI_AUX_MODEL` constants next to the
existing `_NOUS_*` and `_ANTHROPIC_*` constants.
- `_PROVIDER_ALIASES` gains `x-ai` and `x.ai` -> `xai` so users can
spell the provider any of the three common ways.
- `_API_KEY_PROVIDER_AUX_MODELS["xai"] = "grok-4-1-fast-non-reasoning"`
so the api-key provider fallback path picks the same default.
- `resolve_provider_client("xai")` gets an explicit case so text
auxiliary tasks with `provider: xai` work without depending on
the PROVIDER_REGISTRY lookup (PR NousResearch#7050 adds the registry entry too,
but this PR stands alone).
- `_VISION_AUTO_PROVIDER_ORDER` gains `"xai"` at the end so users
without OpenRouter or Nous get xAI vision automatically. Back-compat
preserved for users who have OpenRouter — they keep the current behavior.
- `_resolve_strict_vision_backend("xai")` gets the matching case so
explicit `auxiliary.vision.provider: xai` in config.yaml resolves to
the xAI backend (no silent OpenRouter override).
Tests (4 new):
- test_explicit_xai — resolve_provider_client("xai") picks up
XAI_API_KEY and returns the default aux model.
- test_explicit_xai_alias_x_ai — the x-ai alias normalises to xai.
- test_vision_uses_xai_when_no_openrouter_no_nous — vision auto chain
falls back to xAI when nothing else is available.
- test_vision_forced_xai_uses_xai_backend — explicit config override
routes vision to xAI even when OpenRouter is available. Verifies the
api.x.ai base URL is used (not OpenRouter).
100/100 tests pass on tests/agent/test_auxiliary_client.py.
Complements the existing xAI integration (x-grok-conv-id prompt caching,
"grok" in TOOL_USE_ENFORCEMENT_MODELS) and the native provider
registration in NousResearch#7050 / context length fallbacks in NousResearch#7039.
test_codex_fallback_when_nothing_else and test_forced_main_falls_to_codex assert that the auxiliary resolution chain falls through to Codex OAuth when nothing else is available. Both tests were silently dependent on the test environment having no api-key provider env vars set. With the new xAI backend, any developer running the suite with XAI_API_KEY in their shell (common for people working on xAI features) would trip _try_xai() before the chain reached Codex, failing the assertion with a plain OpenAI mock instead of a CodexAuxiliaryClient. Same risk existed for GLM_API_KEY / ZAI_API_KEY / KIMI_API_KEY / MINIMAX_API_KEY / MINIMAX_CN_API_KEY / DEEPSEEK_API_KEY / DASHSCOPE_API_KEY / OPENROUTER_API_KEY — the tests just happened not to hit it because most dev machines don't have those set. Delete the relevant env vars via monkeypatch.delenv at the start of both tests so they deterministically assert the codex fallback behaviour regardless of the developer's shell.
The initial version of this PR used grok-4-1-fast-non-reasoning as the
single default model for every xAI auxiliary task. That is a good fit
for short text processing (compression, session_search) but too small
for the vision task, where nuanced image judgments benefit from the
larger grok-4.20 family.
Concrete case: the content-machine cron asks the vision model "is this
photo suitable for a professional LinkedIn post about work, AI,
ergonomics?" — a judgment that needs grok-4.20's better visual
reasoning, not the cheap fast variant.
Split the defaults:
- _XAI_TEXT_AUX_MODEL = "grok-4-1-fast-non-reasoning"
-> text auxiliary tasks (compression, session_search, etc.).
Fast, cheap, no reasoning overhead.
- _XAI_VISION_MODEL = "grok-4.20-0309-non-reasoning"
-> vision tasks. Larger/smarter model for nuanced image judgments.
Stays on the non-reasoning variant to keep per-call latency and
cost sensible — reasoning adds 15-30s and significant output
tokens for marginal gains on bounded vision tasks.
_try_xai() now takes a vision: bool = False flag (same pattern as
_try_nous), and _resolve_strict_vision_backend("xai") passes
vision=True when routing vision calls. The _API_KEY_PROVIDER_AUX_MODELS
dict entry keeps the text model since that path is used by text-only
auxiliary resolution.
Tests updated to assert the new vision default.
|
Status update (the linked PRs at the bottom have since resolved): #7039 / #7050 were closed; the xAI provider registration they referenced landed via #10783 (merged 16/04). This PR remains self-contained — it only touches Verified still applicable against current Happy to rebase on current |
|
Closing — conflicting against main for 10+ days and the auxiliary client has since been restructured upstream. Not worth rebasing 123 LOC against a moving target without review signal. |
Problem
The auxiliary client resolution chain (compression, session_search, vision, web_extract, etc.) had no direct xAI support. Users with
XAI_API_KEYhad to rely on either the main provider fallback (only if they were also on xAI as main) or routing through OpenRouter, which defeats the purpose of using xAI direct for cheap/fast side tasks.In particular,
_VISION_AUTO_PROVIDER_ORDERwas limited to("openrouter", "nous"). Users with bothXAI_API_KEYandOPENROUTER_API_KEYsaw every vision analysis routed through OpenRouter even when their main chat provider was xai — burning OpenRouter credits and adding latency while ignoring Grok 4's native vision support.Concrete scenario: on my instance, every invocation of the vision tool in my
content-machine-dailycron (which analyses candidate photos for LinkedIn posts) was going through OpenRouter → gemini-3-flash-preview instead of staying on xAI, even though Grok 4.20 supports vision natively and the main agent was already on xai.Solution
Add xAI as a first-class auxiliary backend, following the exact pattern used by the existing
_try_openrouter/_try_noushelpers, with a text/vision default-model split.Changes to
agent/auxiliary_client.pyNew
_try_xai(vision: bool = False)helper — reads the credential pool first, then falls back toXAI_API_KEYenv var. Thevisionflag selects a different default model (same pattern as_try_nous(vision=True)).Text vs vision default models:
_XAI_TEXT_AUX_MODEL = "grok-4-1-fast-non-reasoning"— fast, cheap (~$0.20/$0.50 per M), no reasoning overhead. Used for compression, session_search, skills_hub, web_extract, etc._XAI_VISION_MODEL = "grok-4.20-0309-non-reasoning"— the larger/smarter model, used for vision tasks where nuanced image judgments matter (e.g. "is this photo suitable for a professional LinkedIn post?"). Stays on the non-reasoning variant to keep per-call latency and cost sensible — reasoning adds 15-30s and significant output tokens for marginal gains on bounded vision tasks._XAI_DEFAULT_BASE_URLconstant next to the existing_NOUS_*/_ANTHROPIC_*constants._PROVIDER_ALIASESgainsx-aiandx.ai→xaiso users can spell the provider any of the three common ways (consistent with the existingz-ai/z.ai→zaialiases)._API_KEY_PROVIDER_AUX_MODELS["xai"] = "grok-4-1-fast-non-reasoning"so the api-key provider fallback path picks the text model as its default. Vision tasks route through_try_xai(vision=True)and pick up grok-4.20 instead.resolve_provider_client("xai")gets an explicit case so text auxiliary tasks withprovider: xaiwork standalone (no dependency on the PROVIDER_REGISTRY lookup — feat(providers): add native xAI provider #7050 adds the registry entry too, but this PR stands alone)._VISION_AUTO_PROVIDER_ORDERgains"xai"at the end so users without OpenRouter or Nous get xAI vision automatically. Back-compat preserved: users withOPENROUTER_API_KEYkeep the current behavior._resolve_strict_vision_backend("xai")gets the matching case and passesvision=Trueso explicitauxiliary.vision.provider: xaiin config.yaml resolves to the xAI backend with the grok-4.20 default (no silent OpenRouter fallback when both are available).Usage after this change
Zero-config path (works automatically for users on xAI direct without OpenRouter/Nous):
Explicit path (force xAI vision even when OpenRouter is present):
Testing
4 new tests in
tests/agent/test_auxiliary_client.py:test_explicit_xai—resolve_provider_client("xai")picks upXAI_API_KEYand returns the default text aux model.test_explicit_xai_alias_x_ai— thex-aialias normalises toxai.test_vision_uses_xai_when_no_openrouter_no_nous— vision auto chain falls back to xAI when nothing else is available, returning the grok-4.20 vision default.test_vision_forced_xai_uses_xai_backend— explicit config override routes vision to xAI even whenOPENROUTER_API_KEYis set. Verifies theapi.x.aibase URL is used, not OpenRouter, and the grok-4.20 model is selected.Also hardens
test_codex_fallback_when_nothing_elseandtest_forced_main_falls_to_codexby explicitly clearingXAI_API_KEYand other api-key env vars — they were silently depending on the developer shell having no api-key providers set.Impact
OPENROUTER_API_KEYsee no behavior changex-grok-conv-idprompt caching inrun_agent.py"grok"inTOOL_USE_ENFORCEMENT_MODELSinagent/prompt_builder.pyDEFAULT_CONTEXT_LENGTHSfallbacks in fix(model_metadata): add xAI Grok context length fallbacks #7039This PR is self-contained and does not depend on #7039 or #7050, though it composes cleanly with them.