feat: add aimlapi.com as an OpenAI-compatible BYOK provider - #1
Open
Lookoff-AIMLAPI wants to merge 5 commits into
Open
feat: add aimlapi.com as an OpenAI-compatible BYOK provider#1Lookoff-AIMLAPI wants to merge 5 commits into
Lookoff-AIMLAPI wants to merge 5 commits into
Conversation
Eigent can already reach aimlapi.com, but only through the generic "OpenAI Compatible" entry, which means the user has to know and type the base URL by hand and gets no model picker. Registering it as its own provider follows the same shape as the Ant Ling and Nebius Token Factory additions, so the searchable dropdown comes from the vendor's public /v1/models listing with no new fetching code. Two details of that listing had to be handled or the picker is unusable. It describes models with a `modalities` object rather than OpenRouter's `architecture`, and it only emits that block when asked with `include=all`; and it repeats an id once per endpoint surface the model serves. Read naively the picker offered 785 undifferentiated entries, image, video and speech models included, many of them duplicated. Teaching the shared parser the second modality shape and de-duplicating by id narrows that to the 362 entries a chat agent can actually drive. Both parser changes are generic and leave the existing providers' output byte-identical, since neither publishes `modalities`.
aimlapi.com credits an integration for the traffic it sends, and does so only when the request carries the partner headers. Eigent already has the mechanism: `default_headers` is a declared init param on the model client and the Codex subscription runtime uses it, so this reuses that path rather than adding new plumbing. The headers are keyed on the request host, not on the configured provider id, so a user who points the aimlapi entry at a different endpoint — or another provider at a proxy — never leaks them to a third party. Merging lets a user's own `default_headers` win on a clash, and a new dict is built per request so the module constant cannot be mutated by a caller. A wrong partner id is not rejected; it is silently treated as untagged traffic, so the id and source shapes are asserted in tests rather than trusted. The same tests pin that no unset request field is serialised as a literal null: the gateway type-checks temperature, top_p, seed, tools, tool_choice, response_format, stream, stream_options, parallel_tool_calls and the max_tokens pair, and answers 400 for null on any of them, which a mocked transport would never reveal.
A listing that publishes one row per endpoint surface names that surface in `type`, and a model can be published behind several. Filtering the listing on modality alone let through ten ids that aimlapi.com serves only from the Responses API — the gpt-5 codex and pro variants. Eigent drives an OpenAI-compatible provider through /chat/completions, so picking one of those got the user `404 Model not found` from a dropdown that had offered it. Rows are now dropped when they name an endpoint surface other than `openai/chat-completions`. A surface is recognised by its `<family>/<endpoint>` shape, so listings that publish no `type` — the OpenRouter shape and the plain OpenAI /v1/models shape used by the other providers here — are unaffected, as is a listing whose `type` is an unrelated single word. This keeps models that are reachable on more than one surface: the Anthropic entries lead with an `anthropic/messages` row but also publish a chat-completions row, and were confirmed live to answer 200 there. Verified against the live listing: 352 models in 51 groups, no duplicates, gpt-5-5 and claude-opus-5 present, gpt-5-2-pro gone.
Moves the aimlapi.com card to the top of INIT_PROVODERS, the hand-ordered list that both the Settings model page and the chat model selector render in array order. This is placement for our own distribution only, and is isolated in a single commit so it can be dropped before anything is proposed upstream. Nothing else is reordered. The per-provider model dropdown is sorted alphabetically inside fetchProviderModels, and the Add Worker list follows whatever order the backend returns from /api/v1/providers. Both are generated, so both are left as they are.
The placeholder part_eigent was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_kK5bWvwrYl5A9aWdwLFoIBQV. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Related Issue
None yet. This is a fork-internal PR into
aimlapi/eigent-aimlapi. UpstreamCONTRIBUTING.mdrequires an accepted issue or Discord thread before any PR isopened there, so that discussion has to happen before the first three commits are
proposed to
eigent-ai/eigent.Description
Adds aimlapi.com as a first-class OpenAI-compatible BYOK model provider.
Eigent can technically reach aimlapi.com today, but only through the generic
"OpenAI Compatible" entry, which means the user has to know and type the base URL
by hand and gets no model picker. This registers it properly, following the same
five-file shape as the Ant Ling (eigent-ai#1823) and Nebius Token Factory (eigent-ai#1646)
additions.
Four commits:
6d3a9b7feat: add aimlapi.com as an OpenAI-compatible BYOK provider051d9d2feat: attribute Eigent's own aimlapi.com traffic43b86eefix: keep Responses-only models out of the chat model picker6105144chore(aimlapi): fork-only placement — do not send upstreamThe last commit is placement for our own distribution only and is isolated so it
can be dropped with a single
git rebase --ontobefore anything goes upstream.1. Provider registration (
6d3a9b7)src/lib/llm.ts— oneINIT_PROVODERSentry. Display name is exactlyaimlapi.com; the machine id isaimlapi, matching the repo's convention.backend/app/model/model_platform.py— one line inPLATFORM_ALIAS_MAPPINGso
aimlapinormalises toopenai-compatible-model.src/shared/modelProviderImages.ts+src/assets/model/aimlapi.svg+ an entryin
DARK_FILL_MODELS(the mark is a dark fill and needs inverting in dark mode,like the openai and anthropic marks).
scripts/check-i18n-source-usage.mjs— the provider description is adocumented non-localised string, same as every other provider's.
modelsEndpointmakes the existingfetchProviderModels()populate thesearchable picker with no new fetching code. Two things about the listing needed
handling first:
modalitiesobject rather than OpenRouter'sarchitecture, and only emits that block for?include=all. Without it thepicker offered 785 undifferentiated entries, image, video and speech models
included.
Both parser changes are generic and leave the other providers' output
byte-identical, since none of them publishes
modalities.2. Attribution (
051d9d2)aimlapi.com credits an integration for the traffic it sends, and only when the
request carries the partner headers. Eigent already has the mechanism —
default_headersis a declared init param on the model client and the Codexsubscription runtime uses it — so this reuses that path rather than adding
plumbing. Four headers go out:
X-AIMLAPI-Partner-ID,X-AIMLAPI-Source, andHTTP-Referer/X-Titleidentifying Eigent (not the vendor), following theOpenRouter convention.
They are keyed on the request host, not the configured provider id, so a user
who points the aimlapi entry at a different endpoint — or another provider at a
proxy — never leaks them to a third party. A user's own
default_headerswin ona key clash, and a new dict is built per request so the module constant cannot be
mutated. Both the backend (
agent_model.py,model_validation.py) and thefrontend model-listing fetch are covered.
A malformed partner id is not rejected by the gateway; it is silently treated as
untagged traffic and earns nothing. So the id shape is asserted in tests on both
sides rather than trusted.
3. Responses-only models (
43b86ee)Found while verifying the picker against the live listing. Ten ids — the gpt-5
codex and pro variants — are served by aimlapi.com only from the Responses
API. Eigent drives an OpenAI-compatible provider through
/chat/completions, sopicking one of those returned
404 Model not foundfrom a dropdown that had justoffered it. Rows are now dropped when they name an endpoint surface other than
openai/chat-completions.Models reachable on more than one surface are kept: the Anthropic entries lead
with an
anthropic/messagesrow but also publish a chat-completions row, andwere confirmed live to answer 200 there.
4. Placement (
6105144, fork-only)Moves the card to the top of
INIT_PROVODERS, the hand-ordered list both theSettings model page and the chat model selector render in array order.
Nothing else is reordered. The per-provider model dropdown is sorted
alphabetically inside
fetchProviderModels, and the Add Worker list followswhatever order the backend returns from
/api/v1/providers. Both are generated,so both are left alone. Eigent has no "recommended"/"featured" badge concept
for model providers — the only
recommendedflag in the tree is on MCPconnector cards (
ConnectorBrowserPage.tsx) — so none was invented.Testing Evidence (REQUIRED)
This machine was under sustained load (load average 60–150) while this was
verified. The full frontend suite was NOT run — it was started, did not
finish inside 8 minutes, and was killed rather than left to produce a
non-deterministic result. Everything below is a targeted selection, and the
baseline was recorded on the same selection on a pristine
mainso thecomparison is honest.
Backend —
backend/tests/app/{model/test_model_platform.py,model/test_chat.py,agent/test_agent_model.py,controller/test_model_controller.py}11 new tests, 0 regressions.
Frontend —
test/unit/lib/llm.test.ts,test/unit/lib/providerModels.test.tsType check / lint / i18n / format
Live end-to-end run through the added code path
Not a mock and not a raw curl: this drives
validate_model_with_details()— thefunction behind the Settings "verify model" button — with
model_platformresolved through the new
normalize_model_platform("aimlapi")mapping, againstthe real API with a real key. It performs an inference call and a tool call,
so the two-turn agent loop is exercised. Outgoing requests were sniffed at the
httpx layer to confirm what actually left the process.
Model used:
anthropic/claude-sonnet-4.5, verified present in the live catalogby the id-or-alias check.
Why the
nullline matters. This gateway type-checkstemperature,top_p,seed,tools,tool_choice,response_format,stream,stream_options,parallel_tool_calls,max_tokensandmax_completion_tokens, and answers 400for a literal
nullon any of them — and the strictness differs per model. Thenastiest case is
tools: null: a client that clears tools between turns bynulling the field succeeds on turn 1 and 400s on turn 2 of every agent loop. The
run above sent two turns with zero null-valued fields, and there is a
regression test (
test_unset_request_fields_are_omitted_not_sent_as_null) thatpins it by asserting on the config handed to the model client, since a mocked
transport would never reveal it.
Live model picker
fetchProviderModels()run against the real listing, no mock:Before
43b86eethat was 362 entries including ten that 404.What is the purpose of this pull request?
Notes for whoever takes this upstream
Three
CONTRIBUTING.mdrules shape the upstream submission and none of them aresatisfied by this PR:
So: file the issue first, get it accepted, read the diff properly, and record the
demo. Then drop
6105144and open the upstream PR with the other three commits.