feat(llm): add aimlapi.com as an LLM provider - #1
Open
Lookoff-AIMLAPI wants to merge 4 commits into
Open
Conversation
Fincept already speaks to a dozen OpenAI-compatible backends, but every aggregator it ships (AIHubMix, AstraFlow) publishes a different slice of the model market. aimlapi.com routes 353 chat models — OpenAI, Anthropic, Google, DeepSeek, Qwen, xAI, Mistral — through one /v1/chat/completions endpoint, so a user with one key reaches all of them from the existing provider combo without a per-vendor account. Follows the AIHubMix shape exactly, which is the last provider added: one row per catalogue function in ProviderCatalog, a models-URL fallback for when the prefilled base_url has been cleared, a wildcard output cap, and an entry in the streaming allow-list. The provider id stays `aimlapi`; the label users see is the vendor's own name for itself, the bare domain. Two things are not copies of the AIHubMix row and are worth the reviewer's eye: * The catalogue is one list for every modality — 936 rows for 353 chat models — so parse_models_response filters on `type`. Without it the model combo fills with image, video, TTS and speech-to-text ids that answer /chat/completions with a 404. That field is in the default response, so the Fetch button does not pay for ?include=all. * Call attribution. OpenRouter's HTTP-Referer/X-Title pair was hardcoded inline in LlmService::get_headers; this adds ProviderCatalog::attribution_headers() so the same idea is table-driven, and wires it into all three places the app originates a request (chat, the Fetch button, and Alpha Arena, which builds its own QNetworkRequest and never goes through LlmService). It is merged into the caller's header map, never assigned over it, returns a fresh map per call, and returns nothing at all unless base_url still resolves to the provider's own host — a row left on this provider but repointed at a proxy must not carry the identifiers off-site. The starter model list is deliberately short and every id in it was verified with a live POST to /v1/chat/completions, not just looked up in /v1/models: that catalogue omits ids that serve traffic and publishes at least one that 404s, so membership in it is not evidence either way. New unit suite tst_provider_catalog covers the seam. The partner id assertion in it exists because a malformed id is dropped silently by the gateway — the request still succeeds and nothing anywhere reports the loss, so a regex in a test is the only place a typo can ever be caught.
ArenaStore::token_totals and the decision log both bill a round as prompt_tokens + completion_tokens. That assumes completion_tokens covers everything the model generated, and on reasoning models it does not — the split is per-route, not per-vendor. Measured against one gateway with one request shape: a one-word reply from gemini-2.5-pro came back as completion_tokens=1, reasoning_tokens=57, total_tokens=69, and grok-4.6 as completion_tokens=1, reasoning_tokens=94, total_tokens=745 — while gpt-5.5 and deepseek-v4 fold reasoning into completion_tokens and reconcile exactly. So the arena was reporting a reasoning agent's usage as a small fraction of the work actually billed for it, with no error and nothing on screen to suggest the number was wrong. Taking the shortfall from total_tokens is provider-agnostic and a no-op wherever the two already agree, so it needs no per-model table to keep up to date. Separable from the provider change it ships with, if the reviewer would rather see it on its own; it is here because that route is where the discrepancy showed up.
Moves aimlapi.com to the head of ProviderCatalog::known_providers(), the one provider list in the repo that is ordered by hand rather than sorted. Deliberately narrow. The two lists a user actually sees — the Settings provider combo via LlmConfigSection::providers_sorted(), and the Alpha Arena wizard's provider pane — both sort by display name at render time, and that sorting is left untouched: reordering a provider there would mean special-casing one vendor inside a general comparator, which is not a change this project should carry. The repo has no featured/badge mechanism to hook into either (the only thing resembling one is the literal "(recommended)" suffix inside Fincept's own display name, and this provider's label has to stay the bare domain), so none is invented here. Separated into its own commit so it can be dropped with a single revert before this work is offered anywhere outside our fork.
The placeholder part_finceptterminal was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_7BsLIBzelgOXobyArFlFtmup. 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.
What this does
Adds aimlapi.com as a selectable LLM provider (machine id
aimlapi), followingthe shape of AIHubMix — the most recently added provider — as closely as the
differences allow.
A user pastes one key and reaches 353 chat models from OpenAI, Anthropic, Google,
DeepSeek, Alibaba, xAI, Meta and Mistral through the existing provider combo, with
no per-vendor account. Chat, streaming, the Fetch-models button, tool calling and
Alpha Arena all work.
Files
services/llm/ProviderCatalog.{h,cpp}attribution_headers()services/llm/LlmService.{h,cpp}get_headers()services/llm/LlmModelsApi.cpptype-filtered catalogue parse, attribution on the Fetch requestservices/llm/ModelCatalog.cppservices/llm/LlmRequestBuilders.cppmax_completion_tokenslistservices/alpha_arena/ArenaLlmClient.cpptests/tst_provider_catalog.cpp,tests/CMakeLists.txtWhy some of it is not a copy of the AIHubMix row
The catalogue is one list for every modality.
GET /v1/modelsreturns 936 rowsfor 353 chat models — the rest are image, video, TTS, speech-to-text, embeddings
and batch targets that answer
/chat/completionswith a 404.parse_models_responsefilters on
type == "openai/chat-completions". 71 ids appear on more than one rowunder different
types, so that filter also de-duplicates: the 353 chat rows carry353 distinct ids.
typeis in the default response, so the Fetch button does notpay for
?include=all(that flag adds pricing/capabilities/modalities and takes thepayload from ~0.5 MB to ~1.6 MB for fields the model combo never reads).
Call attribution. OpenRouter's
HTTP-Referer/X-Titlepair was hardcoded inlinein
LlmService::get_headers. This addsProviderCatalog::attribution_headers()sothe same idea is table-driven, and wires it into all three places the app originates
a request: chat, the Fetch button, and Alpha Arena — which builds its own
QNetworkRequestand never goes throughLlmService. The helper:base_urlstill resolves toapi.aimlapi.com,so a row left on this provider but repointed at a proxy or another vendor cannot
carry the identifiers off-site.
HTTP-RefererandX-Titlename this project (https://fincept.in/Fincept Terminal), matching the OpenRouter convention, not the gateway.Model ids
Twelve starter ids. Every one of them was called live against
/v1/chat/completionson 2026-09-03 and returned HTTP 200 with a non-emptycompletion — not merely looked up in
/v1/models. That distinction matters here:the catalogue omits ids that serve traffic (
flux-pro/v1.1is in neitheridnoraliasesand returns 200) and publishes at least one that does not work(
meta-llama/llama-3.3-70b-versatileis listed as a chat model with an alias and404s on both spellings). Catalogue membership is a necessary pre-filter, never proof.
Where the vendor publishes both a dotted and a dashed spelling the dotted one is
used —
anthropic/claude-sonnet-4.6, not-4-6.x-ai/grok-4-6andopenai/gpt-5-5have no dotted twin in the catalogue and are correct as written.Several ids echo back a different string than they were sent
(
openai/gpt-5-5→gpt-5.5-2026-04-23,x-ai/grok-4-6→grok-4.6), which isnormal aliasing, but it is why the ids here were chosen by probing rather than by
copying another integration's array.
The full list is still one Fetch click away; this is only what the combo shows
before the user fetches.
Two traps this provider does not fall into, recorded in comments
max_tokensvsmax_completion_tokens. AIHubMix is a pass-through and needsmax_completion_tokensfor OpenAI-family models. This gateway translates theparameter itself —
openai/gpt-5-5withmax_tokensreturns 200 — so it isdeliberately absent from that list in
apply_openai_token_limit.nullontemperature,top_p,seed,tools,tool_choice,response_format,stream,max_tokensand others with a 400, and which fields it rejects varies per model —tools: nullis the worst of them, because a host that clears tools between turnsby nulling the field succeeds on turn 1 and 400s on turn 2 of every agent loop.
build_openai_requestalready builds bodies by omitting unset keys rather thansetting them to null, so the whole class of failure does not arise. Nothing was
changed for this; it is called out so it stays that way.
max_tokensis not a spend ceiling here. On some reasoning models the billedreasoning tokens run well past the value with
finish_reason: "stop"— no signal atall.
ModelCatalog.cppsays so next to the entry, so nobody later presents thatcontrol to a user as a cost cap.
The arena token-accounting commit
ArenaStore::token_totalsand the decision log both bill a round asprompt_tokens + completion_tokens. That assumescompletion_tokenscoverseverything generated, and on reasoning models it does not — and the split is
per-route, not per-vendor. Measured through one gateway with one request shape:
google/gemini-2.5-prox-ai/grok-4-6openai/gpt-5-5deepseek/deepseek-v4-flashSo the arena was reporting a reasoning agent's usage as a fraction of the work
actually billed for it, silently. Taking the shortfall from
total_tokensisprovider-agnostic, needs no per-model table, and is a no-op wherever the two figures
already agree. It is a separate commit and can be dropped or split out if you would
rather see it on its own.
There is no cost or pricing display anywhere in the LLM or arena code — only
token counts — so nothing here reads the catalogue's
pricingblock. Worth knowingif one is ever added: 227 of the 353 chat rows repeat their output-token price in
pricing.units[]and label every unitmeasure: "output", including the inputone, so a naive sum over that array roughly doubles the number.
How it was verified
Build. Configured with
-DFINCEPT_BUILD_TESTS=ON, Ninja, Release, macOS arm64,AppleClang 21, Qt 6.11.2 via
-DFINCEPT_QT_PIN_MODE=ANY(Qt 6.8.3 was not availableon the build host — this is the documented escape hatch, not a change to the pin).
Full
FinceptTerminaltarget: exit 0, 0 errors, before and after. Exit codes readfrom the bare command, not through a pipe.
Tests. Baseline on a pristine checkout of this branch's merge base, then again
with the change:
73e1929)ctestexit 0 both times. The 15 new cases are all in the new suite; no existingcase changed behaviour.
--selftest-llm-tools(the in-app provider/tool-format self-test) reports PASSbefore and after. Its section 7 enumerates
known_providers()and asserts everyprovider resolves an OpenAI-shaped endpoint with the base_url field cleared, so this
provider picked up that coverage automatically.
Live inference through the added code path — not curl, not a mock. A harness
linking the real
ProviderCatalog.cppfrom this tree, usingchat_endpoint()andattribution_headers()verbatim plus the header-merge loop fromget_headers()andthe body shape from
build_openai_request(), over Qt's ownQNetworkAccessManager:Tool calling through the same path,
openai/gpt-5-5:Streaming was confirmed separately against
/v1/chat/completionswith"stream": true— standarddata: {...}SSE chunks withchoices[].delta.content,on both an OpenAI-family and an Anthropic-family model, which is what
provider_supports_streamingpromises.Not verified: the Settings and Alpha Arena screens were not driven by hand — this
was built and exercised on a headless host. The code those screens call is covered by
the unit suite and by the live call above, but nobody has clicked the combo.
Placement
The last commit,
chore(aimlapi): fork-only placement — do not send upstream, isseparated so it can be dropped with a single revert.
It moves this provider to the head of
ProviderCatalog::known_providers()— the onlyprovider list in the repo that is hand-ordered rather than sorted. The two lists a
user actually sees are left alone on purpose:
LlmConfigSection::providers_sorted()and the Alpha Arena wizard both sort by display name at render time, and biasing a
general comparator toward one vendor is not a change this project should carry. The
repo has no featured/badge mechanism to hook into either — the nearest thing is the
literal
(recommended)suffix inside Fincept's own display name, and this provider'slabel has to stay the bare domain — so none was invented.
Scope
.github/CONTRIBUTING.mdrequires every PR to link an issue carryinggood-first-issue,help-wantedorscope:approved. No such issue is linkedhere, because this PR targets our own fork rather than upstream. Anyone taking this
upstream must open that issue and get it labelled first; the
PR Scope Gateworkflowcloses unlabelled PRs after 7 days.
No formatter was run over any file. The only reformatted lines are the
known_providers()initialiser, which clang-format re-wraps because the list grew byone element. No docs were touched — the same policy file forbids unsolicited
docs/*changes, andREADME.md's provider list already omits several shippedproviders, so adding only this one there would be out of place.