feat(models): openai-compatible vendor presets (zai, deepseek, moonshot, groq, xai, openrouter) - #17
feat(models): openai-compatible vendor presets (zai, deepseek, moonshot, groq, xai, openrouter)#17fjgbue wants to merge 6 commits into
Conversation
…_key_env The openai provider only ever targeted api.openai.com. Add optional base_url and api_key_env fields to ModelConfig so the same provider covers any OpenAI-compatible vendor (Z.AI, DeepSeek, Moonshot, Groq, xAI, OpenRouter) or self-hosted server (Ollama, vLLM) without a new provider integration or dependency. api_key_env defaults to OPENAI_API_KEY, so existing YAML is unaffected.
YAML_SPEC.md now documents the new OpenAI-compatible endpoint fields with a table of known vendors. ARCHITECTURE.md and ROADMAP.md previously only mentioned Anthropic + Bedrock as model providers, even though Gemini and OpenAI have been supported in factory.py for a while — updated both to reflect what's actually implemented.
base_url/api_key_env alone meant typing out both fields by hand every time, even for vendors this codebase already knows about. Add agent_engine/models/presets.py, a small registry mapping provider ids (zai, deepseek, moonshot, groq, xai, openrouter) to their base_url and api_key_env, resolved in build_chat_model before dispatch. provider: zai now works with nothing else set; base_url/api_key_env still override a preset explicitly, or work standalone on provider: openai for any vendor or self-hosted server not in the table.
YAML_SPEC.md leads with provider: zai (etc) as the primary path now, keeps base_url/api_key_env as the fallback for anything not in the table plus the override mechanism. ARCHITECTURE.md and ROADMAP.md updated to match, they still described the plain generic fields from the previous commit.
| # a local Ollama/vLLM server, ...) by naming the env var that holds that | ||
| # vendor's key. Defaults to OPENAI_API_KEY to keep existing YAML working | ||
| # unchanged. | ||
| key_env_var = api_key_env.strip() if api_key_env and api_key_env.strip() else "OPENAI_API_KEY" |
There was a problem hiding this comment.
If api_key_env is "" we not should error so user meant to override and not use OPEN_API_KEY?
There was a problem hiding this comment.
empty string should error, yeah.
the OPENAI_API_KEY default is meant for when the field is omitted, not when someone explicitly blanks it. An explicit "" means they were mid-override and got it wrong, and falling back silently just turns that into a confusing auth failure further down. I'll split the two: omitted stays defaulted, empty or whitespace raises. what u think?
There was a problem hiding this comment.
I still wondering if user will declare on yaml api_key_env = " "
we will fall back to open api key default. but, he tried to override and maybe typo, so user experience in my view should error him.
Now we fallback
| @@ -0,0 +1,70 @@ | |||
| """Known OpenAI-compatible vendor presets. | |||
There was a problem hiding this comment.
I'm not sure this file is needed. what you have built, its generic infra for custom models.
This led us to maintain code and each changes of the providers. let user declare api key name and urls and models, they write once time yaml and use model name each place
There was a problem hiding this comment.
the maintenance worry is fair but smaller than it looks I think
these base URLs don't move, and the table is six entries. The generic base_url/api_key_env path already covers everything the registry doesn't, including overriding a preset, so the registry adds no capability, it just saves every user re-typing the same two known values for a vendor the library could already name, or at least that was my initial tought. But if the team would rather ship zero vendor knowledge though, killing presets.py is one commit since the generic path stands on its own, lmk
There was a problem hiding this comment.
Secondly i feel good with that. But we have to doc it also on official docs mdx files. focused and clear
| if preset is not None: | ||
| base_url = base_url or preset.base_url | ||
| api_key_env = api_key_env or preset.api_key_env | ||
| normalized_provider = "openai" |
There was a problem hiding this comment.
if we remove preset always provider will be openai
There was a problem hiding this comment.
yes, right, drop the registry and this branch goes with it, proider is just openai plus whatever base_url/key the user passed. they live or die together
| if not model_name: | ||
| raise ModelConfigurationError("Model name must not be empty.") | ||
|
|
||
| # A preset id (zai, deepseek, moonshot, groq, xai, openrouter, ...) is |
There was a problem hiding this comment.
what about checking if provider is gemini, but user declare base url or api key, or just one of them? we should error him on validate yaml that its not acceptable
fjgbue
left a comment
There was a problem hiding this comment.
Awaiting some further comments in the answers!
| @@ -0,0 +1,70 @@ | |||
| """Known OpenAI-compatible vendor presets. | |||
There was a problem hiding this comment.
the maintenance worry is fair but smaller than it looks I think
these base URLs don't move, and the table is six entries. The generic base_url/api_key_env path already covers everything the registry doesn't, including overriding a preset, so the registry adds no capability, it just saves every user re-typing the same two known values for a vendor the library could already name, or at least that was my initial tought. But if the team would rather ship zero vendor knowledge though, killing presets.py is one commit since the generic path stands on its own, lmk
| if preset is not None: | ||
| base_url = base_url or preset.base_url | ||
| api_key_env = api_key_env or preset.api_key_env | ||
| normalized_provider = "openai" |
There was a problem hiding this comment.
yes, right, drop the registry and this branch goes with it, proider is just openai plus whatever base_url/key the user passed. they live or die together
anthropic, bedrock and gemini target a fixed endpoint and their own key, so base_url/api_key_env passed alongside them were silently dropped in the factory. _validate_model now errors at yaml-load time, naming the field and provider, with each field checked independently so setting just one is caught too.
The yaml secret scanner flags any key whose name looks secret-like, and api_key_env contains "api_key", so it tripped the same check max_tokens was already exempt from. That left the base_url/api_key_env escape hatch unusable from yaml. Exempt the key name; values are still scanned, so a real key pasted there is still rejected.
d64c1e5 to
0a9d92e
Compare
| ValidationError( | ||
| f"{path}.{field_name}", | ||
| f"Not supported for provider '{provider}'; only OpenAI-compatible " | ||
| "providers accept a custom endpoint or key variable.", |
There was a problem hiding this comment.
on error message: base url or api key naming variable?
| ) | ||
| ) | ||
|
|
||
| if provider in _FIXED_ENDPOINT_PROVIDERS: |
There was a problem hiding this comment.
instead of nested ifs lets try make it better like (pseudo) :
for field_name in ("base_url", "api_key_env"):
if raw.get(field_name) is not None && provider in _FIXED_ENDPOINT_PROVIDERS
return error
|
https://docs.extra-ai.co/docs/yaml-spec#supported-providers We should documented this new feature. and here just keep example but add reference to this new section https://docs.extra-ai.co/docs/yaml-spec#supported-providers |
|
Hi, want share with you so i checked and our engine are supporting by that using langchain open api support by using this env var like ollama open router we just put provider in so lets think how its affected since, we currently support it. but need to check how its works, and if we decide go with your fix make sure we have no collisions |
The openai provider was hardcoded to api.openai.com. First pass just added base_url and api_key_env so provider: openai could point anywhere, but that means typing out the endpoint and key var by hand every time, even for vendors this table already knows about. This adds the actual integration on top of that: agent_engine/models/presets.py is a small registry mapping provider ids (zai, deepseek, moonshot, groq, xai, openrouter) to their base_url/api_key_env, resolved in build_chat_model before it dispatches to _build_openai_model. No new provider kind, no new dependency, it's ChatOpenAI either way.
model:
provider: zai
name: glm-5.2
That's the whole config now for a listed vendor. api_key_env still gets checked (ZAI_API_KEY in this case), it's just not typed into the YAML anymore. base_url/api_key_env still work directly, both as the escape hatch for anything not in the table (a different vendor, self-hosted Ollama/vLLM) and as an override on a listed preset, e.g. routing zai through an internal proxy without giving up the shorthand:
model:
provider: zai
name: glm-5.2
base_url: https://llm-proxy.internal.example.com/zai
api_key_env: INTERNAL_PROXY_KEY
Also fixed ARCHITECTURE.md and ROADMAP.md, which still only mentioned Anthropic and Bedrock even though Gemini and OpenAI have been in factory.py for a while, and updated both again now that there's an actual preset registry instead of just two generic fields.
Tests: 379 passing (up from 370), including a parametrized case that checks every preset resolves its own base_url/key correctly, one confirming an explicit base_url/api_key_env override wins over the preset, one for case-insensitive provider ids, and one confirming the unsupported-provider error message lists the preset ids alongside anthropic/bedrock/gemini/openai. ruff and mypy clean on every file touched.