feat(x): add aimlapi.com as a named model provider - #1
Open
Lookoff-AIMLAPI wants to merge 4 commits into
Open
Conversation
AI/ML API is already reachable through the openai-compatible flavor — a user can paste https://api.aimlapi.com/v1 and it works. The reason to name it is its catalog. GET /v1/models there answers with every endpoint the account can reach, not just chat: 936 entries across 15 endpoint types (video, image, TTS, STT, embeddings, OCR, batch handles), of which 353 are chat models, and 91 ids appear twice because the same model is served by two endpoints. The generic flavor maps that response straight into the model picker, so today a user scrolls 936 rows — most of which cannot hold a conversation, some listed twice — to find one that can. That is not something the generic flavor can be configured out of; it needs to know the catalog's `type` field. So the flavor is deliberately thin: the same @ai-sdk/openai-compatible transport, a default base URL so a key alone is a complete config (matching aigateway), and one catalog parser. It curates nothing — no hardcoded model list, no reordering — the provider's own order is preserved. The parser fails open in both directions, which is the point of the tests around it. An entry with no type is kept, and if filtering would empty a non-empty catalog the unfiltered list is returned instead. This discriminator has been renamed once already ("chat-completion" -> "openai/chat-completions") and the failure mode is a permanently empty picker that looks like an outage — a longer list is the cheaper failure. Chat only. Image generation is not wired: the catalog types image models separately and their request path was not verified, and shipping a picker entry that 404s is worse than not offering one. Verified against the live service through this code path, not a mock: listModelsForProvider returns 353 de-duplicated ids, testModelConnection succeeds, and generateText completes a two-step tool-calling loop on anthropic/claude-sonnet-4.5.
LlmProvider already carries a per-provider `headers` map that createProvider threads into every request, so this is a default for that map rather than new machinery. Two of the three headers are OpenRouter's convention, which this provider also reads: HTTP-Referer and X-Title name the CALLING app — Rowboat and its repo, not aimlapi.com — and X-AIMLAPI-Source is the provider's own channel tag. Scoped by ORIGIN, not by flavor. A provider entry may be pointed at a proxy or a compatible peer, and attribution must never ride along to someone else's service; pointing this flavor at another host sends the user's own headers and nothing more. The headers merge under whatever the user configured, so a user's key always wins, and the returned map is built fresh per provider so the shared default can never be edited in place. No partner id is included. None has been issued for Rowboat, and an invented one is worse than none: the receiving service DROPS a malformed id instead of rejecting the request, so the call succeeds and the attribution silently goes nowhere. The constant is empty, the header is omitted entirely while it is, and a test asserts the id is either empty or well-formed — that assertion is the only thing that would catch a typo the day one is filled in. Verified on the wire, not only in tests: a real chat completion against api.aimlapi.com carries http-referer / x-title / x-aimlapi-source; the same provider pointed at another host carries none of them; and the generic openai-compatible flavor aimed at the same URL is unchanged.
Moves the aimlapi.com tile to the top of the add-provider list. That array is hand-ordered and its order is exactly what the dialog renders, so this is a placement preference and nothing else — no behaviour, no defaults, no model selection changes with it. It is isolated in this commit precisely so it can be dropped with a single revert before any upstream proposal. Choosing the order of another project's provider list is the maintainers' call, not a contributor's. Rowboat has no "recommended" or "featured" badge for providers, and none was invented for this. The only recommendation concept in the tree picks a recommended MODEL per flavor and is served by Rowboat's own backend, so it is not something a code change can set.
The placeholder export const AIMLAPI_PARTNER_ID: string = ""; was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is export const AIMLAPI_PARTNER_ID: string = "part_VGDbk3ZJHZ1bi3eoaLNwToNC";. 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.
Adds
aimlapi.com(AI/ML API) as a named model provider inapps/x, alongside OpenRouter and the Vercel AI Gateway.Why, given it already "works"
It does — and that was verified by running it, not by reading. Configuring the existing
openai-compatibleflavor withhttps://api.aimlapi.com/v1and driving Rowboat's ownlistModelsForProvider→testModelConnection→generateTextpath produced a real completion and a real two-step tool-calling loop on the first try. There is no allowlist, no hardcoded model list, and no base-URL assumption in the way. Nothing was broken and nothing needed a bugfix.What the generic path cannot do is read the catalog.
GET /v1/modelson this provider answers with every endpoint the account can reach, not just chat:The generic flavor maps that body straight into the model picker, so today a user scrolls 936 rows — most of which cannot hold a conversation, some of them listed twice — to reach one that can. That is not configuration; reading the catalog's
typefield is code. With the named flavor the same account lists 353 de-duplicated chat models.The rest is what a named provider gets for free: the endpoint prefilled so a key alone is a complete config (the same treatment
aigatewayhas), its own tile and display name, and its own analytics flavor.What it is not
openai-compatible, there is no parameter that is safe across ~350 models from a dozen vendors, so effort sends nothing rather than guessing.Failing open, on purpose
parseAimlapiChatModelIdskeeps untyped entries, and if filtering would empty a non-empty catalog it returns the unfiltered list instead. The discriminator this filter reads has been renamed once already (chat-completion→openai/chat-completions), and the failure mode is a permanently empty model picker that looks like an outage. A longer list is the cheaper failure. Both branches are covered by tests.Attribution (second commit, separable)
LlmProvideralready carries a per-providerheadersmap thatcreateProviderthreads into every request, so this is a default for an existing mechanism rather than new machinery. It sendsHTTP-RefererandX-Title— OpenRouter's convention, naming Rowboat as the calling app, not the provider — plusX-AIMLAPI-Source: agent/rowboat.Three properties worth reviewing:
^part_[A-Za-z0-9]{1,64}$— that assertion is the only thing that would catch a typo the day one is filled in.Placement — fork-only, drop before any upstream proposal
The last commit,
chore(aimlapi): fork-only placement — do not send upstream, moves the tile to the top of the hand-ordered add-provider list. It is isolated so a single revert removes it; ordering another project's provider list is the maintainers' call. Rowboat has no "recommended"/"featured" badge concept for providers and none was invented — the only recommendation mechanism picks a recommended model per flavor and is served by Rowboat's backend, so no code change can set it.Verification
Baseline captured on a pristine
main(88a58412) before any change, then repeated after each commit.pnpm install --frozen-lockfileinapps/harborthenapps/x, thennpm test,npm run typecheckandnpm run lintinapps/x— the same three gates CI runs. Exit codes read bare, not through a pipe.packages/sharedpackages/coreapps/serverapps/renderernpm run typecheckandnpm run lint: clean before and after. The 17 new tests are all inaimlapi.test.ts.The typecheck gate earned its place here — the first draft of the attribution helper emitted a
TS2322thattscreported while still writing output, so the compileddistran fine and only the bare exit code showed the failure.Live calls, through this code path
Not curl and not a mock:
createLanguageModel({ flavor: "aimlapi", apiKey })driving the AI SDK, on the builtpackages/core.The second turn is deliberate. This API rejects
tools: nullwith a 400 on its stricter models, so a client that clears tools between turns by nulling the field succeeds on turn 1 and fails on turn 2 of every agent loop. The AI SDK omits the key instead of nulling it, so Rowboat is not exposed to that — confirmed rather than assumed.Attribution was checked on the wire by recording outgoing request headers around a real completion:
Known rough edges
list[0]as the assistant model. That isselectInitialModel's existing rule and it applies equally to OpenRouter and AI Gateway, whose catalog order is just as arbitrary. For this providerlist[0]isopenai/gpt-3.5-turbo, which is a poor first impression for an agent. Rather than hardcode a favourite, the tile carriesmanualModelso the optional model box lets someone name the model they came for at connect time; the durable fix belongs in Rowboat's per-flavor model recommendations, which are backend-served.completion_tokensunder-reports on some models on this provider —google/gemini-2.5-flashreturned prompt 12 + completion 3 against a total of 86, with 71 reasoning tokens uncounted. Anything metering spend from completion tokens alone will under-count on those models.max_tokensdoes not reliably bound output on every model here; some return several times the cap withfinish_reason: "stop". It should not be presented as a cost bound.applyPromptCachingmatches Anthropic models by id, soanthropic/*ids routed here getproviderOptions.anthropicbreakpoints that the openai-compatible transport does not forward. Harmless — the request is unchanged — but the caching those breakpoints are meant to buy does not happen. Left alone deliberately; wiring it would mean a provider-specific transport, which is a much larger change than this one.