feat(providers): add aimlapi.com as an OpenAI-compatible gateway - #1
Open
Lookoff-AIMLAPI wants to merge 3 commits into
Open
feat(providers): add aimlapi.com as an OpenAI-compatible gateway#1Lookoff-AIMLAPI wants to merge 3 commits into
Lookoff-AIMLAPI wants to merge 3 commits into
Conversation
OpenFang already routes every OpenAI-compatible vendor through the shared driver tables, so a gateway needs a base-URL constant, a provider-defaults arm, a ProviderInfo row and a handful of catalog rows rather than a new driver file. This follows the Requesty (issue RightNow-AI#995) and Novita wiring exactly so there is one shape to maintain, not two. The five catalog rows are gateway-priced rather than upstream-priced: the same model costs a different amount through a router than direct, and letting `aimlapi/anthropic/claude-sonnet-4.6` fall through to the generic `contains("sonnet")` arm would silently bill it at Anthropic's own rate. The metering fallback carries matching arms scoped to the `aimlapi/` prefix, with a test that fails if those rates ever drift from the catalog. Attribution headers are keyed on the request *origin*, not on the configured provider name. A user can point any provider at any base_url, and partner headers must never ride a request to a different vendor or to a third-party proxy that merely fronts the same API. `with_extra_headers` now appends instead of assigning, so attribution cannot silently discard headers an earlier caller configured (Copilot's IDE auth is the existing caller). The partner id shape is asserted in a unit test because a malformed id is accepted by the gateway and then ignored — it fails silently, earning nothing, with no runtime error to notice.
Moves aimlapi.com to the head of the two hand-ordered provider lists (the builtin ProviderInfo table and the `openfang init` wizard) and names it in the aggregator group label on the settings page. This is presentation only and carries no functional change, which is why it is isolated here: it reflects our own preference, not the project's, and should be dropped before the provider wiring is offered upstream. The web settings page orders providers within each category by configured-first then alphabetically, and the API returns the table unsorted; that ordering machinery is left untouched.
The placeholder part_openfang was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_Z6HbToJ3l2ht1dFSzHC98vk6. 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.
Summary
Adds aimlapi.com as an OpenAI-compatible LLM gateway, following the wiring
shape already used for Requesty (issue RightNow-AI#995) and Novita (RightNow-AI#928): a base-URL
constant, a
provider_defaults()arm, aProviderInforow, catalog model rows,a metering fallback and an
.env.exampleline. No new driver file — OpenFangroutes every OpenAI-compatible vendor through the shared tables, and this
follows that rather than adding a second shape to maintain.
Base URL
https://api.aimlapi.com/v1, key envAIMLAPI_API_KEY, chat at{base}/chat/completions. There is no{base}/completionsendpoint on thisgateway (it 404s); the existing
chat_url()never derives one, and the base-URLconstant carries a comment saying so.
Changes
crates/openfang-types/src/model_catalog.rsAIMLAPI_BASE_URLcrates/openfang-runtime/src/drivers/mod.rsprovider_defaults()arm; origin-scoped attribution headers; testscrates/openfang-runtime/src/drivers/openai.rswith_extra_headersappends instead of assigning; testscrates/openfang-runtime/src/model_catalog.rsProviderInforow + 5 model rows; testscrates/openfang-kernel/src/metering.rscrates/openfang-cli/src/tui/screens/init_wizard.rsopenfang initentrycrates/openfang-api/static/js/pages/settings.js.env.exampleAttribution headers
Partner headers (
X-AIMLAPI-Partner-ID,X-AIMLAPI-Source, plusHTTP-Refererand
X-Titlenaming OpenFang, not the gateway) are keyed on the requestorigin, not on the configured provider name. Any provider in OpenFang can be
pointed at any
base_url, so a name-keyed check would let partner headers ridea request to a different vendor. Tests cover OpenRouter, OpenAI,
localhost, aproxy path containing
aimlapi, and the look-alike hostapi.aimlapi.com.example.net— all get no headers.OpenAIDriver::with_extra_headerspreviously assigned the header vector.The only existing caller is Copilot IDE auth, which calls it once, so the
behaviour was correct but not composable: adding a second caller would have
silently dropped the first one's headers. It now appends, with a test.
The partner id is asserted against
^part_[A-Za-z0-9]{1,64}$in a unit test.This is not decoration: the gateway accepts a malformed id and then ignores it,
so a typo produces no error anywhere — it just silently stops earning
attribution. A test is the only thing that catches it.
Cost rates
Catalog rates are the gateway's rates, not the upstream vendors' direct
rates, taken from
pricing.units[]inGET /v1/models?include=all. Without anexplicit arm,
aimlapi/anthropic/claude-sonnet-4.6would fall through to thegeneric
contains("sonnet")pattern and be billed at $3/$15 instead of$4.13/$20.63 — a ~35% understatement. The metering arms are scoped to the
aimlapi/prefix so the bare upstream spelling is untouched, andtest_aimlapi_fallback_rates_match_catalogfails if the two tables ever drift.Caveat worth knowing:
max_tokensdoes not reliably bound reasoningtokens on this gateway. Some reasoning models return well over the cap and still
report
finish_reason: "stop", so a max-tokens setting is not a cost bound.Model selection
Five models. Each was checked against
GET /v1/models?include=allas anidoran alias, and then actually called — the catalog alone is not sufficient
proof in either direction (it omits ids that serve traffic, and publishes at
least one id that 404s).
strip_provider_prefixaimlapi/anthropic/claude-sonnet-4.6anthropic/claude-sonnet-4.6aimlapi/openai/gpt-5-5openai/gpt-5-5aimlapi/google/gemini-2.5-flashgoogle/gemini-2.5-flashaimlapi/alibaba/qwen-maxalibaba/qwen-maxaimlapi/meta-llama/Llama-3.3-70B-Instruct-Turbometa-llama/Llama-3.3-70B-Instruct-Turboaimlapi/openai/gpt-5-5echoes backgpt-5.5-2026-04-23in the responsemodelfield rather than the id that was requested. That is the gatewayresolving a floating id to a dated snapshot; the other four echo the id
unchanged. Nothing in OpenFang pins on the echoed value today.
max_output_tokensfor Llama 3.3 70B is set to 32,768 rather than the 127,000the catalog reports, which is all but the whole 128k context window and not a
usable output bound.
Testing
cargo clippy --workspace --all-targets -- -D warnings— see note belowcargo test --workspacepassesTests: baseline on a pristine checkout of this branch point was
2699 passed / 0 failed across 40 test binaries. After this PR:
2711 passed / 0 failed (+12 new tests).
cargo build --workspace --libexits 0.
Clippy: this PR adds zero clippy findings — verified by running
cargo clippy --workspace --all-targets -- -D warningswith only thepre-existing lint categories allowed, which exits 0. The unmodified
-D warningscommand does not pass on this tree with Rust 1.98, for sevenfindings in files this PR does not touch:
The lint categories involved are
chunks_exact_to_as_chunks,question_mark,drain_collect,useless_format,for_kv_map,needless_borrows_for_generic_argsanduseless_borrows_in_formatting.These are newer lints that landed in the toolchain after this code was written;
they are pre-existing and out of scope here.
Live calls: driven through
create_driver("aimlapi")→OpenAIDriver::complete(),i.e. the code path this PR adds, not curl and not a mock. All five models
returned 200 with
stop_reason: EndTurn:Tool calling exercised through the same path:
The harness that drove these was temporary and is not part of the diff.
Null-vs-omitted request fields
This gateway rejects an explicit
nullwith a 400 ontemperature,top_p,seed,tools,tool_choice,response_format,stream,stream_options,parallel_tool_calls,max_tokensandmax_completion_tokensfor at leastsome models — and which models are strict varies per model, so one green live
call proves nothing about the rest.
tools: nullis the dangerous one: a hostthat clears tools between turns by nulling the field succeeds on turn one and
400s on turn two of every agent loop.
OaiRequestalready hadskip_serializing_ifon every optional field, soOpenFang is not affected.
test_unset_request_fields_are_omitted_not_nulllocks that in, because the failure mode is a green test suite with 400s on every
real call.
Security
The origin check parses
base_urlwithreqwest::Urland compareshost_str()case-insensitively, so a path segment or a suffixed domain cannotimpersonate the origin.
Fork-only commit
The final commit,
chore(aimlapi): fork-only placement — do not send upstream,is presentation only — it moves aimlapi.com to the head of the two hand-ordered
provider lists and names it in the settings page's aggregator group label. It is
isolated so it can be dropped before any upstream submission. The web settings
page sorts within a category (configured first, then alphabetical) and the API
returns the table unsorted; that ordering machinery was left alone.
docs/providers.mdwas not updated. It is a hand-numbered list that stops at 20providers and already omits Requesty, Novita, MiniMax, Qwen, Zhipu, Venice,
Chutes and NVIDIA — neither of the two most recent provider PRs touched it, and
adding a 21st entry to a list missing 22 others would be inconsistent rather
than helpful.