Skip to content

feat(ai-providers): add aimlapi.com as a first-class AI provider - #1

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

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

Conversation

@Lookoff-AIMLAPI

Copy link
Copy Markdown
Member

Description

Adds aimlapi.com as a first-class AI provider, alongside the six OpenAI-compatible
vendors added in activepieces#14987 and Vertex in activepieces#15139.

Admins can already reach AI/ML API today through Other (OpenAI Compatible) with a
custom base URL, so this buys discoverability, branding, a correct model list and
attribution — not new capability. Framed that way deliberately.

Why its own strategy file rather than openAiCompatibleVendor

openai-compatible-vendor.ts:12-14 validates a key by listing models:

async validateConnection(authConfig: BaseAIProviderAuthConfig): Promise<void> {
    await listVendorModels({ authConfig, provider, name })
},

https://api.aimlapi.com/v1/models is a public catalog. Probed 2026-09-03:

request status
GET /v1/models no Authorization header 200
GET /v1/models Bearer sk-definitely-not-a-real-key-000 200
GET /v1/models real key 200

So through that factory any string would have saved as a working key, and the first
real failure would have surfaced at run time as "the model is broken" rather than
"your key is wrong" — made worse by the fact that validateProviderCredentials only
surfaces the upstream message for Cloudflare Gateway.

aimlapi-provider.ts validates against GET /v1/key instead, which answers 200
with key metadata for a live key and 401 for a missing, empty or wrong one. That is
the same shape openRouterProvider already relies on for /auth/key, so no change was
made to how the six existing vendors validate — that is their call to make, not this
PR's.

listModels also filters on type === "openai/chat-completions". The catalog returns
936 entries across 15 endpoint types (chat, image generation, video, TTS, STT,
embeddings, batches, OCR…). The generic factory maps every row to
AIProviderModelType.TEXT, which would have offered 218 video models and 132 image
models in a text dropdown. Filtered, the provider lists 353 chat models.

Attribution

Four headers on requests to our base URL only:

HTTP-Referer:          https://www.activepieces.com
X-Title:               Activepieces
X-AIMLAPI-Partner-ID:  part_activepieces
X-AIMLAPI-Source:      agent/activepieces

HTTP-Referer / X-Title follow the OpenRouter convention and identify Activepieces,
the calling app — not the gateway. They are built fresh per request by spreading a frozen
constant, so a caller's own extraHeaders / metadataHeaders still win a key clash and
the constant cannot be mutated by one provider and read by another. A malformed partner id
is ignored silently by the gateway rather than rejected, so its shape is asserted in a test
rather than left to review.

Model ids and the API surface

  • Every id shipped comes from the live catalog at request time — nothing is hardcoded, so
    there is no model list to rot.
  • The AI SDK's .chatModel() is used, which posts to /v1/chat/completions. This matters:
    POST /v1/responses exists on that API but serves only 50 of the 353 chat models, and
    POST /v1/completions does not exist at all. Verified on the wire — see below.
  • Request bodies omit unset optional keys rather than sending null. Several models there
    reject null on temperature / top_p / seed / tools / response_format with a 400
    while others accept it, so a body carrying "tools": null between agent turns would 400
    on turn 2 with a green test suite. Verified on the wire — no key was sent as null.

How was this tested?

Bun 1.3.14, bun install --frozen-lockfile --ignore-scripts (the isolated-vm native build
fails against Node 26 on this machine; unrelated to this change and it affects only the
sandbox package).

Unit tests — baseline on a pristine checkout vs. after, same command, --continue so one
failure does not abort the rest:

package baseline after
@activepieces/ai-providers 37 passed 43 passed
@activepieces/shared 498 passed 498 passed
@activepieces/sandbox 251 passed 251 passed
@activepieces/server-utils 169 passed 169 passed
@activepieces/core-execution 135 passed 135 passed
@activepieces/core-utils 28 passed 28 passed
@activepieces/pieces-framework 15 passed 15 passed
ee-embed-sdk 7 passed 7 passed
worker 267 passed, 1 skipped 267 passed, 1 skipped
@activepieces/engine 19 failed / 447 passed 19 failed / 447 passed
web 7 failed / 671 passed 7 failed / 671 passed
total 2525 passed / 26 failed 2531 passed / 26 failed

The 26 failures are identical on a clean tree: engine's props-validator /
timezone-dependent cases, and web's test/app/builder/shortcuts.test.tsx
(Cannot read properties of undefined (reading 'getItem')). Neither touches AI providers.

The +6 in @activepieces/ai-providers is 5 new assertions plus one extra case of the
existing it.each(supportedProviders) now that the enum has another member.

  • npx turbo run build --filter=api --filter=web --filter=@activepieces/piece-ai --filter=@activepieces/ai-providers — 19/19, exit 0, before and after.
  • npm run lint-core — exit 0, 23/23 tasks, 0 errors and warning counts byte-identical to
    the baseline in every package (api 490, web 72, engine 33, worker 29, shared 7, embed-sdk 6).

One real inference call through the code this PR adds, not a mock and not a raw curl —
aimlapiProvider and createLanguageModel imported from source and driven with a real key
passed through the environment:

=== 1. validateConnection with the real key ===
OK: accepted
=== 2. validateConnection with a bogus key ===
OK: rejected -> Error: [aimlapi.com] failed to validate the api key: Request failed with status code 401
=== 3. listModels ===
models listed: 353
types: text
sample: [{"id":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo","type":"text"}, ...]
contains openai/gpt-4o-mini: true
=== 4. real inference through createLanguageModel ===
text: "activepieces-aimlapi-live-ok"
finishReason: stop
usage: {"inputTokens":19,"outputTokens":8,"totalTokens":27}
model echoed by the api: gpt-4o-mini-2024-07-18
=== 5. tool calling through the same path ===
toolCalls: [{"name":"get_weather","input":{"city":"Cairo"}}]
finishReason: tool-calls
=== 6. what actually went on the wire ===
url: https://api.aimlapi.com/v1/chat/completions
body keys: model,messages
body keys sent as null: (none)
attribution headers: {"authorization":"Bearer <REDACTED>","http-referer":"https://www.activepieces.com","x-aimlapi-partner-id":"part_activepieces","x-aimlapi-source":"agent/activepieces","x-title":"Activepieces"}
---
url: https://api.aimlapi.com/v1/chat/completions
body keys: model,messages,tools,tool_choice
body keys sent as null: (none)
attribution headers: {"authorization":"Bearer <REDACTED>","http-referer":"https://www.activepieces.com","x-aimlapi-partner-id":"part_activepieces","x-aimlapi-source":"agent/activepieces","x-title":"Activepieces"}

The piece-side switch was exercised separately, because a provider missing from
buildLanguageModel in packages/pieces/community/ai/.../ai-sdk.ts connects, validates and
lists models and then dies at step run time on default:. createAIModel was called against
a local stub of GET /v1/ai-providers/aimlapi/config and the model then run for real:

text: "activepieces-piece-path-ok"
finishReason: stop
url: https://api.aimlapi.com/v1/chat/completions
body keys sent as null: (none)
headers: {"authorization":"Bearer <REDACTED>","http-referer":"https://www.activepieces.com","x-aimlapi-partner-id":"part_activepieces","x-aimlapi-source":"agent/activepieces","x-ap-flow-id":"flow","x-ap-platform-id":"plat","x-ap-project-id":"proj","x-ap-run-id":"run","x-title":"Activepieces"}

Attribution and the existing x-ap-* metadata coexist on that path, with the metadata
winning any clash.

Edition paths

Not exercised. The provider is edition-agnostic — it adds an enum member, a strategy, two
switch cases and a UI row, with no ee/ import, no feature flag, no migration and no entity —
but CE / EE / Cloud were not each brought up, so that is stated rather than claimed.

Not verified

  • The logo 404s. https://cdn.activepieces.com/pieces/aimlapi.png returns 404 today
    (moonshot-ai.png returns 200). Per the repo's own note that a provider logo is an asset
    someone has to upload rather than something the code ships, the URL follows the convention
    every other provider uses and needs the asset uploaded before merge, or the platform admin
    list renders a broken-image icon. Nothing in CI catches this.
  • No per-model metadata. MODELS_DEV_PROVIDER in tools/scripts/sync-model-catalog.ts was
    deliberately left alone: models.dev publishes no aimlapi provider (checked against
    https://models.dev/api.json), so there is no upstream id to map. Like cloudflare-gateway
    and custom, this provider legitimately has no source, and its models will show without
    context window or price until models.dev carries it.
  • A disabled-but-real key. GET /v1/key returns a disabled field, so a key that is valid
    but switched off may still answer 200. That case could not be produced, so validation is
    left on the HTTP status alone rather than shipping an untested branch.
  • Image generation is off (NO_IMAGE_GENERATION_PROVIDERS), matching the six existing
    OpenAI-compatible vendors. That API does serve image models; wiring them is a separate change
    with its own verification, not a claim smuggled into this one.

Two things worth knowing if you meter or cap on this provider

  • completion_tokens excludes reasoning tokens on some models there (a Gemini 2.5 Flash
    call reports prompt 12 + completion 3, total 86, with 71 reasoning). Anything metering spend
    from completion_tokens will under-report.
  • max_tokens does not reliably bound reasoning tokens: some models return several times
    the cap with finish_reason: "stop", giving no signal at all, while others cap correctly and
    report length.

Neither is introduced here, and neither is acted on here — flagged because the AI-credits
surfaces read those numbers.

Fixes # (issue)

Breaking change? (required — CI fails if this is left unedited)

  • no — reviewed, not breaking
  • yes — technical (removed/renamed API field or endpoint, dropped column, new required field, removed/required env var)
  • yes — functional (default/limit/behaviour change, new self-hosted setup step)

Additive only: one new AIProviderName member, one new member of the discriminated
ProviderConfigUnion, and a new row in the provider list. No existing id is renamed, no
schema is removed, and the untagged AIProviderConfig / AIProviderAuthConfig unions are
untouched — the provider reuses the existing empty OpenAiCompatibleVendorConfig and
BaseAIProviderAuthConfig members, so the "order matters" hazard in those unions does not
apply.

Security impact? (required — CI fails if this is left unedited)

  • no — reviewed, no security impact
  • yes — security-sensitive (call out the risk and mitigation in the description above)

Touches outbound HTTP and credential validation, so flagged rather than waved through:

  • Both server-side calls go through safeHttp.axios, per .claude/rules/safe-http.md. The URL
    is a hardcoded constant with no admin-supplied component, so there is no SSRF surface to open —
    and deliberately no baseUrl config field, consistent with decision 000031.
  • The API key is only ever sent to api.aimlapi.com, and the attribution headers are scoped to
    the same origin so they cannot ride a request to another provider or a proxy fronting it.
  • The change makes credential validation stricter, not looser: a wrong key is now rejected at
    connect time instead of being stored as valid.
  • The api key is applied after the attribution headers in every merge, so no attribution header
    can clobber it.

Second commit is fork-only

chore(aimlapi): fork-only placement — do not send upstream is separated on purpose. It
contains only placement and featuring — first in SUPPORTED_AI_PROVIDERS, added to the
existing RECOMMENDED_PROVIDERS with a tagline, and first under Gateways in the admin
guide. That is a partnership decision, not a technical one. Drop that commit and the provider
still works and still sits with the other vendors.

For the record on ordering: SUPPORTED_AI_PROVIDERS is a hand-ordered literal with no sort at
any call site, and the entry is placed last among concrete providers in the first commit — even
though alphabetical order by enum key would also have put aimlapi first — so that the
non-promotional position is unambiguous.

aimlapi added 2 commits September 3, 2026 16:12
Admins can already reach AI/ML API through Other (OpenAI Compatible), so
this buys discoverability and a correct model list rather than new
capability: the catalog mixes 936 entries across chat, image, video,
speech, embedding and batch surfaces, and only the 353 chat models belong
in a text-model dropdown.

It gets its own strategy file instead of an openAiCompatibleVendor entry
because that factory validates a key by listing models, and
api.aimlapi.com/v1/models is a public catalog that answers 200 with no
Authorization header and 200 to a bogus bearer. Every typo would have
saved as a working key and surfaced later as a broken model rather than a
bad credential. GET /v1/key answers 401 for a missing, empty or wrong key,
which is the same shape openRouterProvider already relies on for
/auth/key, so the check is a real one without changing how the six
existing vendors validate.

Attribution rides only on requests to our own base URL, built fresh per
request from a frozen constant so a caller's own headers still win and the
constant cannot be mutated across providers. The partner id is silently
ignored by the gateway when malformed, so its shape is asserted in a test
rather than left to review.

Both language-model switches are updated - the server factory and the
piece's own copy - since a provider missing from the piece one connects,
validates and lists models, then dies at step run time.
Placement and featuring are a partnership decision, not a technical one,
so they are isolated here: dropping this commit leaves the provider fully
working and positioned like any other vendor.

SUPPORTED_AI_PROVIDERS is a hand-ordered literal with no sort at any call
site, so the dropdown, the admin cards and the model picker all read it in
file order. RECOMMENDED_PROVIDERS and recommendedTagline are the repo's
existing featured-card mechanism, used today for Anthropic and OpenAI;
nothing new was invented for this.
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