Skip to content

feat(providers): add aimlapi.com as a model provider - #1

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

feat(providers): add aimlapi.com as a model provider#1
Lookoff-AIMLAPI wants to merge 2 commits into
mainfrom
feat/aimlapi-provider

Conversation

@Lookoff-AIMLAPI

Copy link
Copy Markdown
Member

What this adds

aimlapi.com as a model provider, in the reseller slot registry.py already has for
"many labs' models behind one key, using THEIR model namespaces". It speaks plain
OpenAI Chat Completions, so the descriptor is the existing _compat() helper and the
footprint follows the BytePlus/Volcengine Ark PR (andrewyng#504) that added the last two providers:
descriptor, curated matrix rows, a vendored brand mark, gallery + key-console wiring,
backend tests, a GUI unit test and a Playwright e2e.

Provider name (machine) aimlapi
Display title (everywhere a user sees it) aimlapi.com
Endpoint https://api.aimlapi.com/v1 (prefilled, editable)
Env var AIMLAPI_API_KEY
Recommended model zhipu/glm-5.2 — same model the three neighbouring resellers recommend
Key console https://aimlapi.com/app/keys

Four curated matrix rows, matching the budget the other resellers get:

Routed id Label Context Vision
aimlapi:openai/gpt-5.6-sol GPT-5.6 Sol · via aimlapi.com 1,050,000 yes
aimlapi:anthropic/claude-sonnet-4.6 Claude Sonnet 4.6 · via aimlapi.com 200,000 yes
aimlapi:zhipu/glm-5.2 GLM-5.2 · via aimlapi.com 1,000,000 no
aimlapi:deepseek/deepseek-v4-pro DeepSeek V4 Pro · via aimlapi.com 1,000,000 no

Two things that are not copy-paste from the neighbouring rows

Model ids do not transfer from OpenRouter. Three of the four ids in the OpenRouter
matrix rows are neither an id nor an alias on this vendor: z-ai/glm-5.2 (theirs is
zhipu/glm-5.2), moonshotai/kimi-k2.6, and meta-llama/llama-4-maverick — there is no
llama-4 entry in their catalog at all. Every id shipped here was taken from
GET /v1/models?include=all, filtered to type == "openai/chat-completions", and then
round-tripped with a real completion
, because on that API an id can be both a model and
an alias of a different model (asking for deepseek/deepseek-chat is served by
deepseek-v4-flash) — so the catalog check can pass while another model answers. The
echoed model was checked for each. Also note their Anthropic ids are duplicated in a
dashed spelling that publishes no capabilities at all; the dotted claude-sonnet-4.6 is
the real entry, and that is the one used here.

Context windows come from info.contextLength; there is no top-level context_length
field on that API.

Key verification cannot use /models. Their /models is public: it returns 200 for a
bogus key, an empty key, and with no Authorization header at all. The generic
list-models probe behind the Test button would therefore green-light a typo and leave the
user to find out at their first real turn. verify_provider_key sends a one-token chat
completion for this provider instead, which is the cheapest call that actually exercises
the credential.

Attribution headers

The gateway reads HTTP-Referer / X-Title (the OpenRouter convention) plus two headers
of its own. Supporting them needed a default_headers kwarg on OpenAIProvider — the
same knob CodexProvider already uses on its own client. It is:

  • opt-in per descriptor — no other compat vendor's request changes by a byte, and
    there is a lockdown test for that;
  • scoped to the resolved request ORIGIN, not the provider name — the endpoint field is
    user-editable, so a key repointed at a corporate proxy or at another vendor carries
    nothing extra. Covered by a test that includes a lookalike host;
  • copied per client, so the module constant can never be mutated;
  • carrying no user data. HTTP-Referer/X-Title name OpenWorker, the calling app, not
    the gateway.

A test asserts the partner id matches ^part_[A-Za-z0-9]{1,64}$ and the source matches the
<channel>/<client> shape, because the gateway silently drops a malformed value rather than
erroring — nothing at runtime would catch a typo.

Regression test for a validator quirk worth knowing about

That API 400s on an explicit null for temperature, top_p, seed, tools,
tool_choice, response_format, stream, stream_options, parallel_tool_calls,
max_tokens and max_completion_tokens — which is exactly what an SDK serialises when a
provider forwards its unset optionals. OpenAIProvider already builds its request from
only what the caller passed and gates tools on truthiness, so this repo is not affected;
tests now pin that on both the complete and stream paths so it stays that way. The
tools one is the nasty variant: a host that clears tools between turns by nulling the
field succeeds on turn 1 and fails on turn 2 of every agent loop, which is why the live
check below runs a full two-turn loop.

Verification

Baseline recorded on a pristine main checkout first.

Suite Baseline (main) This branch
pytest tests 1913 passed, 1 skipped, 0 failed 1926 passed, 1 skipped, 0 failed
npm run build (tsc + vite) exit 0 exit 0
npm test (vitest) 134 passed, 5 failed 136 passed, 5 failed
playwright test e2e/settings.spec.ts 8 passed 8 passed (7 existing + 1 new)

The 5 vitest failures are identical on both trees — all in Sidebar.tsx, untouched here.

One real inference call through the provider path (not curl, not a mock): built via
build_provider_client("aimlapi", {}, None) with only AIMLAPI_API_KEY in the
environment, exactly as ProviderRouter builds it.

provider_names()[0:3] = ['aimlapi', 'openai', 'openai-codex']
base_url = https://api.aimlapi.com/v1
attribution = {'HTTP-Referer': 'https://github.com/andrewyng/openworker',
               'X-AIMLAPI-Partner-ID': 'part_openworker',
               'X-AIMLAPI-Source': 'agent/openworker',
               'X-Title': 'OpenWorker'}
text = 'aimlapi ok'
finish_reason = stop | usage = TokenUsage(input=20, output=130, cache_read=0, cache_write=0)
tool_calls = [ToolCall(id='call_-7293040526591261626', name='get_weather', arguments={'city': 'Lisbon'})]
verify(real key)  = {'ok': True}
verify(bogus key) = {'ok': False, 'error': 'Invalid API key.'}

Two-turn tool loop and streaming, same client:

TURN 1 tool_calls: [ToolCall(id='call_-7293041488663930315', name='get_weather',
                            arguments={'city': 'Lisbon'})] | finish: tool_calls
TURN 2 (tools declared) text: 'The current temperature in Lisbon is 21°C with clear skies.' | finish: stop
TURN 2 (tools=None)     text: 'The current temperature in Lisbon is 21°C with clear skies.' | finish: stop
STREAM text: '4 5 6' | usage: TokenUsage(input=20, output=458, cache_read=0, cache_write=0)

Each of the four shipped model ids was also round-tripped individually; the echoed model
matched the model asked for in every case.

What I could not verify

  • PDF ingestion. Not probed, so no row claims native pdf — images are verified live
    on the two vision rows, PDFs keep the pdf_support.py fallback like every other reseller.
  • Vision on Kimi K2 Thinking. Their catalog advertises vision and file_input for
    moonshotai/kimi-k2-thinking, but a live image request returns
    404 "No endpoints found that support image input". That model is not in the shipped
    rows; noting it because it means the catalog's vision flag is not by itself trustworthy.
  • The max_tokens cap on reasoning models. zhipu/glm-5.2 with max_tokens: 120
    returned completion_tokens: 5002 (reasoning_tokens: 4971) and finish_reason: "stop"
    — the cap did not bound the reasoning phase, and nothing in the response says so. Not
    something this PR can fix, but it means the matrix's cost assumptions for that row are
    optimistic.
  • Cost/latency at scale. Single-call checks only.

Commits

  1. feat(providers): add aimlapi.com as an OpenAI-compatible reseller — the provider.
  2. chore(aimlapi): fork-only placement — do not send upstream — placement only, kept
    separate so it can be dropped whole. It moves the descriptor to the head of the
    hand-ordered DESCRIPTORS and PROVIDER_ORDER, and fronts the README's reseller
    clause. It does not touch the gallery's own sort, which is alphabetical by display
    title by an explicit owner ruling ("a curated order reads as vendor bias", 2026-08-21,
    ProviderSetup.tsx), and it adds no "Recommended" badge because the gallery has no such
    concept to reuse.

Note on the repo's PR policy

README.md asks for screenshots on every PR: "For any PR, please attach screenshots of
what was broken and how it is fixed now."
Nothing was broken here — this is an addition —
and the GUI surface it touches is covered by the new Playwright e2e assertion instead
(gallery card, brand mark, prefilled endpoint, curated model preview). Screenshots should
be attached before this is offered anywhere beyond this fork.

The reseller slot in registry.py exists for exactly this shape — many labs'
models behind one key, in the reseller's own id namespace — and aimlapi.com
fills it with a plain Chat Completions endpoint, so the descriptor is the
existing _compat() four-liner rather than new machinery.

Two things it does NOT inherit from the neighbouring rows:

Model ids. OpenRouter's slugs do not transfer: `z-ai/glm-5.2`,
`moonshotai/kimi-k2.6` and `meta-llama/llama-4-maverick` are neither ids nor
aliases there, and llama-4 has no entry at all. Every curated row was taken
from their live catalog and then round-tripped with a real completion, because
an id there can also be an alias of a *different* model — the catalog check
alone can pass while a different model serves the request.

Key verification. Their `/models` is public: it answers 200 to a bogus key, an
empty key, and no Authorization header, so the generic list-models probe behind
the Test button would accept a typo and leave the user to discover it at the
first real turn. Their Test sends a one-token chat completion instead, which is
the cheapest call that actually exercises the credential.

The gateway also reads app-attribution headers, which needed a `default_headers`
kwarg on OpenAIProvider — the same knob CodexProvider already uses. It is opt-in
per descriptor and scoped to aimlapi.com's own ORIGIN rather than to the provider
name, so a key repointed at a proxy or another vendor carries nothing extra.
Partnership placement, kept as its own commit so it can be dropped whole
before this goes anywhere else. Nothing here is a behaviour change.

- DESCRIPTORS is hand-ordered, so aimlapi.com moves to the head of it.
- PROVIDER_ORDER likewise. Note this list is now only the tiebreaker for
  identical display titles: the gallery itself sorts alphabetically by title
  ("a curated order reads as vendor bias", owner ruling 2026-08-21, quoted at
  ProviderSetup.tsx). That sort is deliberately left alone.
- README's reseller clause names us first.

No "Recommended"/featured badge: the provider gallery has no such concept and
inventing one for a single vendor would be a bigger change than the provider.
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