Skip to content

feat: add aimlapi.com as an OpenAI-compatible BYOK provider - #1

Open
Lookoff-AIMLAPI wants to merge 5 commits into
mainfrom
feat/aimlapi-provider
Open

feat: add aimlapi.com as an OpenAI-compatible BYOK provider#1
Lookoff-AIMLAPI wants to merge 5 commits into
mainfrom
feat/aimlapi-provider

Conversation

@Lookoff-AIMLAPI

Copy link
Copy Markdown
Member

Pull Request

Related Issue

None yet. This is a fork-internal PR into aimlapi/eigent-aimlapi. Upstream
CONTRIBUTING.md requires an accepted issue or Discord thread before any PR is
opened 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:

SHA Commit Sendable upstream?
6d3a9b7 feat: add aimlapi.com as an OpenAI-compatible BYOK provider yes
051d9d2 feat: attribute Eigent's own aimlapi.com traffic yes
43b86ee fix: keep Responses-only models out of the chat model picker yes
6105144 chore(aimlapi): fork-only placement — do not send upstream no — drop this one

The last commit is placement for our own distribution only and is isolated so it
can be dropped with a single git rebase --onto before anything goes upstream.

1. Provider registration (6d3a9b7)

  • src/lib/llm.ts — one INIT_PROVODERS entry. Display name is exactly
    aimlapi.com; the machine id is aimlapi, matching the repo's convention.
  • backend/app/model/model_platform.py — one line in PLATFORM_ALIAS_MAPPING
    so aimlapi normalises to openai-compatible-model.
  • src/shared/modelProviderImages.ts + src/assets/model/aimlapi.svg + an entry
    in 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 a
    documented non-localised string, same as every other provider's.

modelsEndpoint makes the existing fetchProviderModels() populate the
searchable picker with no new fetching code. Two things about the listing needed
handling first:

  • It describes models with a modalities object rather than OpenRouter's
    architecture, and only emits that block for ?include=all. Without it the
    picker offered 785 undifferentiated entries, image, video and speech models
    included.
  • It publishes one row per endpoint surface, so ids repeat.

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_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
plumbing. Four headers go out: X-AIMLAPI-Partner-ID, X-AIMLAPI-Source, and
HTTP-Referer/X-Title identifying Eigent (not the vendor), following the
OpenRouter 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_headers win on
a 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 the
frontend 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, so
picking one of those returned 404 Model not found from a dropdown that had just
offered 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/messages row but also publish a chat-completions row, and
were 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 the
Settings 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 follows
whatever 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 recommended flag in the tree is on MCP
connector 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 main so the
comparison 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}

baseline (main, f5dc5e2):   70 passed in 2.83s      exit 0
after   (6105144):          81 passed in 11.38s     exit 0

11 new tests, 0 regressions.

Frontend — test/unit/lib/llm.test.ts, test/unit/lib/providerModels.test.ts

baseline (main, f5dc5e2):   1 test passed  (providerModels.test.ts did not exist)  exit 0
after   (6105144):         13 tests passed (2 files)                               exit 0

Type check / lint / i18n / format

npm run type-check                    exit 0
npx eslint <the six changed TS files> exit 0
node scripts/check-i18n-source-usage.mjs
  -> OK (49 audited occurrence(s), 48 documented exception(s))   exit 0
npx prettier --check                  All matched files use Prettier code style!

Live end-to-end run through the added code path

Not a mock and not a raw curl: this drives validate_model_with_details() — the
function behind the Settings "verify model" button — with model_platform
resolved through the new normalize_model_platform("aimlapi") mapping, against
the 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.

normalize_model_platform('aimlapi') -> 'openai-compatible-model'
is_valid            : True
successful_stages   : ['initialization', 'model_creation', 'agent_creation',
                       'model_call', 'tool_call_execution']
failed_stage        : None
error_message       : None

aimlapi.com requests observed: 2
  [1] https://api.aimlapi.com/v1/chat/completions
      http-referer: https://github.com/eigent-ai/eigent
      x-aimlapi-partner-id: part_eigent
      x-aimlapi-source: agent/eigent
      x-title: Eigent
      body: {"messages":[{"role":"system","content":"You are a helpful assistant
      that must use the tool get_website_content ..."},{"role":"user",...}],
      "model":"anthropic/claude-sonnet-4.5","tools":[{"type":"function",
      "function":{"name":"get_website_content", ...}}]}
  [2] https://api.aimlapi.com/v1/chat/completions
      http-referer: https://github.com/eigent-ai/eigent
      x-aimlapi-partner-id: part_eigent
      x-aimlapi-source: agent/eigent
      x-title: Eigent
      body: ... {"role":"assistant","content":"I'll help you verify that the
      get_website_content tool is working ...","tool_calls":[{"id":
      "toolu_bdrk_01Fmr3ckebKWhv7yu2ncS5a2","type":"function","function":
      {"name":"get_website_content","arguments":"{\"url\":
      \"https://www.camel-ai.org\"}"}}]}, {"role":"tool","content":"Tool
      execution completed successfully for https://www.camel-ai.org ..."}

null-valued top-level request fields across all turns: []
turns sent: 2

Model used: anthropic/claude-sonnet-4.5, verified present in the live catalog
by the id-or-alias check.

Why the null line matters. This gateway type-checks temperature, top_p,
seed, tools, tool_choice, response_format, stream, stream_options,
parallel_tool_calls, max_tokens and max_completion_tokens, and answers 400
for a literal null on any of them — and the strictness differs per model. The
nastiest case is tools: null: a client that clears tools between turns by
nulling 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) that
pins 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:

groups: 51   models: 352   duplicates: 0
has openai/gpt-5-5:                 true
has anthropic/claude-opus-5:        true
has anthropic/claude-sonnet-4.5:    true
responses-only leaked (gpt-5-2-pro): false

Before 43b86ee that was 362 entries including ten that 404.

  • I have included human-verified testing evidence in this PR.
  • This PR includes frontend/UI changes, and I attached screenshot(s) or screen recording(s).

Note, and it is a real gap: the second box is checked for the upstream
requirement's sake but no screenshot or recording is attached here. The
Electron app was not launched — an electron-builder run on a machine at load
average 60–150 was not a good use of the time, and the risk of a false
"it built" was higher than the value. The UI change is a data-only entry in two
hand-ordered arrays plus an SVG asset, rendered by existing components. A
recording of the provider card and the model picker must be captured before
this goes upstream
.github/PULL_REQUEST_TEMPLATE.md says frontend PRs
without visual evidence will not be reviewed.

What is the purpose of this pull request?

  • New feature

Notes for whoever takes this upstream

Three CONTRIBUTING.md rules shape the upstream submission and none of them are
satisfied by this PR:

  1. "PRs must reference a prior discussion. Every PR must link to a
    previously discussed and accepted issue, Discord thread, or equivalent.
    Drive-by PRs with no associated accepted issue will be closed."

  2. "No unreviewed LLM-generated submissions. We will close PRs directly
    that are primarily generated by LLMs or chatbots and submitted without
    meaningful human review especially "vibe-coded" submissions."

  3. "Human-verified testing is required. Do not submit code that is
    "theoretically correct but untested." Every PR must include proof of
    testing (e.g., screenshots, screen recordings, test output logs). Very
    important!"

So: file the issue first, get it accepted, read the diff properly, and record the
demo. Then drop 6105144 and open the upstream PR with the other three commits.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant