diff --git a/brain/knowledge/ai-intelligence/ai-providers.md b/brain/knowledge/ai-intelligence/ai-providers.md index e2d2efd80f5e..3f5a1e009338 100644 --- a/brain/knowledge/ai-intelligence/ai-providers.md +++ b/brain/knowledge/ai-intelligence/ai-providers.md @@ -73,6 +73,7 @@ renders and preserves every real price; the cheapest in the set is 0.01. - **AWS's OpenAI-compatible surface is two endpoints, and "Bedrock Mantle" is a real AWS product name, not a customer's nickname.** `bedrock-runtime.{region}.amazonaws.com/openai/v1` (AWS-recommended) and `bedrock-mantle.{region}.api.aws/v1` both serve Chat Completions *and* Responses, both authenticate with a Bedrock API key as `Authorization: Bearer`, and both take an unmodified OpenAI SDK. Mantle alone adds server-side tools and web search, `background=true` async inference, and Projects/Workspaces; bedrock-runtime alone has Guardrails, cross-Region inference and intelligent prompt routing. So a ticket claiming "Mantle only supports responses" is half right — it serves chat/completions too, and the real loss is the Responses-only capabilities. - **Vertex is a different door onto the same models, not a different vendor.** The `GOOGLE` provider is the Gemini Developer API (`generativelanguage.googleapis.com`, static API key); `VERTEX` is Vertex AI (`{region}-aiplatform.googleapis.com/v1beta1/projects/{project}/locations/{region}/publishers/google`, service-account OAuth2 with ~1h tokens) — the same Gemini models, but billed to the customer's GCP account and inheriting IAM, VPC-SC, CMEK, data residency and audit logs, plus Model Garden's Claude/Llama/Mistral. That rotating token is why the CUSTOM provider can never reach Vertex: `buildOpenAICompatibleHeaders` injects one static header, which is why the workaround used to be a LiteLLM proxy. `@ai-sdk/google-vertex` does the JWT-to-token exchange itself, so the native provider needs no proxy. Vertex models are entered by hand (`MANUAL_MODEL_PROVIDERS`) on purpose: availability is project-, region- and Model-Garden-specific, and a `publishers/google` listing would miss exactly the third-party models that motivate choosing Vertex. **But "same models" only holds for Gemini.** Model Garden also serves Anthropic, Meta, Mistral and xAI, and those do not share Gemini's API surface — `@ai-sdk/google-vertex` ships separate `/anthropic`, `/maas` and `/xai` entry points for exactly that reason, so one `createVertex(...)` call does not cover Vertex. Because models are typed in by hand, a Claude id reaches the factory as an ordinary string and, unrouted, gets built with the Gemini client and fails at the endpoint. `create-language-model.ts` routes ids containing `claude` to `createVertexAnthropic`; `vertexClientFor` picks the client from the id shape, so all three are routed. Because the shapes are unambiguous the check is a lookup, not a tuned heuristic — and note the publisher prefix has to be tested *before* the `claude` one, or a MaaS path containing "claude" routes to the Anthropic client. Routing is cheap because the id shapes are distinct, not because a heuristic was tuned: `gemini-2.5-pro` is bare, Anthropic carries an `@date` (`claude-3-5-sonnet@20241022`), and MaaS is publisher-prefixed and suffixed (`meta/llama-4-scout-17b-16e-instruct-maas`). The publisher prefix is a stronger discriminator than the `includes('claude')` match currently shipped, and it looks like an argument for listing models rather than taking them by hand — but it is not, see below: the listing is scoped to a single publisher, so Model Garden ids stay hand-typed and the string matching stays with them. **Listing was considered and rejected**, and the reason is worth keeping so it does not get reopened, and **the response shape does not need a GCP account to learn** — every Google API publishes an unauthenticated discovery document: `curl -s 'https://aiplatform.googleapis.com/$discovery/rest?version=v1beta1'` returns ~5MB of JSON whose `schemas` are the authoritative request/response types. `publishers.models.list` returns `{ publisherModels: PublisherModel[], nextPageToken }`, and `PublisherModel` is `{ name: 'publishers/google/models/', versionId, versionState (STABLE|UNSTABLE), launchStage (GA|PUBLIC_PREVIEW|PRIVATE_PREVIEW|EXPERIMENTAL), openSourceCategory, supportedActions, predictSchemata, frameworks, parent }`. **There is no `displayName` on it** — the one in `google-vertexai`'s piece comes from `@google/genai`'s `Model` type, which is a different shape, so copying that code gives every row a blank label with nothing failing. `displayName` exists only on the nested `parent`, which is the base model a tuned model derives from. The decisive fact is in that same doc: `publishers.models.list` takes `parent` matching `^publishers/[^/]+$` — **one publisher per call**. Listing everything therefore means hardcoding the set of publishers to enumerate, which is the same shape of hardcoding it was meant to remove, and Model Garden models would still need hand entry, leaving two mechanisms where there is now one. `VERTEX` stays in `MANUAL_MODEL_PROVIDERS` alongside `CUSTOM` and `CLOUDFLARE_GATEWAY`, which fits: Vertex model availability really is per-project. Reach for the discovery doc before a live probe on any `*.googleapis.com` integration; a curl against the real endpoint needs **billing attached to the project even for a read** (`403 BILLING_DISABLED`), so it is the slower path and it is not free to set up. The `x-goog-user-project` header is not optional when probing with a personal login: without it aiplatform answers `403 SERVICE_DISABLED` naming `consumer: projects/32555940559`, which is Google's shared gcloud CLI project rather than yours, and the message talks about quota projects rather than the missing header. This is a user-credential quirk only — a service account carries its own `project_id`, so the provider itself never hits it. **The multi-vendor story is text only.** Vertex's image models are Google's own Imagen (`imagen-3.0-*`, `imagen-4.0-*`), not Model Garden's third parties, and `createVertex(...)` also exposes `imageModel`, `video`, `speech`, `transcription` and `textEmbeddingModel` — none of which we wire today. Imagen is wired (`createVertex(...).imageModel()`, with a `VERTEX` case in the piece's image switch, so `VERTEX` is no longer in `NO_IMAGE_GENERATION_PROVIDERS`); video, speech, transcription and embeddings are not. Embeddings still have no `DEFAULT_EMBEDDING_MODELS` entry, so that path paths fail with an explanatory message instead of reaching a switch that has no case for it. - **Chat-tier resolution answers "which model" from three places, and only one of them is the key.** `resolveModelIdForProvider` (`ee/agent/agent-helpers.ts`) now prefers the resolved key's own configured text models when its config carries a `models` array — that is the `MANUAL_MODEL_PROVIDERS` case (Vertex, Custom, Cloudflare Gateway), where the admin typed the exact ids the key exposes. **An empty catalog is not a missing one**, and collapsing the two is how the first cut of this went wrong: a helper returning `string[] | undefined` treated "lists zero text models" the same as "has no catalog", so a key configured with only image models fell through to the curated list and resolved to a Gemini id it never offered. An admin-listed catalog is the whole truth about a key, so an empty one now refuses the turn with a message rather than guessing. Otherwise it falls back to the static `ALLOWED_CHAT_MODELS_BY_PROVIDER`, and if the provider is missing from that too it returns the raw tier id with its vendor prefix stripped — which is how a Vertex key once resolved to `claude-sonnet-4-6` and handed it to the Gemini client. That map is `Partial<`, so omitting a chat-capable provider compiles cleanly and fails only at the endpoint. **The key's own `modelScope`/`modelIds` allow-list is applied last, to whichever candidate list was chosen**, and a resolution with nothing left refuses the turn rather than returning a model the key forbids. `GetProviderConfigResponse` carries both fields for that, populated at all three construction sites (`getChatProvider`, `getConfigOrThrow`, `enrichWithKeysIfNeeded`); adding fields there is backward compatible because the AI piece reads the response as a typed shape rather than parsing it. I first recorded this as too big for a provider PR — "a wire contract the engine and the AI piece both consume" — having never measured it. It was about thirty lines across four files. Measure before recording something as out of scope; the estimate outlives the guess. **The resolver is exported and has five call sites across four files, and only two of them are runs.** `agent-rpc-handlers` resolves the chat turn's model *and* its fast model, and `chat-personalization-service` resolves both for research — those four must be given the resolved row's `config`, `modelScope` and `modelIds`, or the key's constraints are bypassed on exactly the paths a user takes. `resolveTierModel` inside `agent-helpers` is the fifth. `agent-service` and `agent-draft-ai` also call it but hold only a provider name and set a stored default on a draft, so they are correctly left thin. Adding a parameter here means grepping `resolveModelIdForProvider|resolveFastModelId` across `packages/server`, not editing the call site in front of you — threading only `resolveTierModel` looks complete, passes every test, and still leaves the real chat path unconstrained. One trap when touching this function: a tier id (`fast`, `smart`) is **not** a model id, so the preferred pick must be the resolved native id unless the selection is itself among the candidates — passing `selectedModel` straight through makes `resolveFastModelId` return the first curated model instead of the fast tier's. +- **`aimlapi.com` is an aggregator with its own strategy file, not an `openAiCompatibleVendor` entry, because the vendor factory's `validateConnection` cannot tell a good key from a typo there.** That factory validates by GETting `{baseUrl}/models`, and `https://api.aimlapi.com/v1/models` is a public catalog: it answers `200` with no `Authorization` header at all and `200` to `Bearer sk-not-a-real-key` (probed 2026-09-03), so any string would have saved as a working key and the first failure would have surfaced at run time as a broken model rather than a bad credential. `aimlapi-provider.ts` validates against `GET /v1/key` instead — `200` with key metadata for a live key, `401` for a missing, empty or bogus one — which is the same shape as `openRouterProvider`'s `/auth/key` check. Its `listModels` also filters the catalog on `type === 'openai/chat-completions'`: the response mixes 936 entries across chat, image, video, speech, embeddings and batch surfaces, and the vendor factory would have offered all of them as text models. Do not fold this provider back into the factory without replacing that key check for every vendor it serves. - **A failed credential validation tells the admin nothing, for every provider except Cloudflare Gateway.** `aiProviderService.validateProviderCredentials` gates the upstream message behind `includeHttpErrorInMessage`, which is `provider === CLOUDFLARE_GATEWAY` and nothing else, so everyone else gets a bare `Failed to validate credentials for `. The cause is not lost — it is logged one line earlier (`log.error({ error }, '[aiProviderService#validateProviderCredentials] ...')`) and passed as the `httpErrorResponse` error param — but **web never renders `httpErrorResponse`**, so the only way to diagnose a rejected key is the server log. Grep the log for `validateProviderCredentials` before assuming the provider integration is broken — that text is the whole diagnosis, and it is often not about credentials at all. Confirmed case: a brand-new xAI team with no credits purchased answers `GET /v1/models` with `403 permission-denied — Your newly created team doesn't have any credits or licenses yet`, naming the console page that fixes it, and we render that as "Failed to validate credentials for xAI" — sending the admin off to regenerate a key that was never wrong. Vendors also phrase real key failures inconsistently (xAI uses `400 Incorrect API key provided`, not a 401). The corollary: **a provider that saves without error is not a working provider.** A no-credits 403 and a bad key are indistinguishable in the UI, so only an actual generation proves a key end to end. This is an admin-only surface (`platformAdminOnly`), so there is little reason to keep hiding it. - **A provider's logo is an asset someone has to upload, not something the code ships.** `AiProviderInfo.logoUrl` in `packages/web/src/features/agents/ai-providers.ts` is a plain string rendered into an ``, and every provider points at `https://cdn.activepieces.com/pieces/.png` — nothing is bundled. Adding a provider therefore carries a cross-team dependency with no compile-time or test signal: a slug with no asset behind it renders a broken-image icon in the platform admin list, and only a live request tells you. Check the URL with `curl -o /dev/null -w '%{http_code}'` before assuming it works — a vendor that already ships as a *piece* usually has its logo there already (`deepseek.png`, `grok-xai.png` did), so start the upload request only for the genuinely missing ones. A Vite asset import also satisfies `logoUrl` (see `GoogleIcon` in `platform/security/sso/index.tsx`) and removes the runtime CDN dependency for air-gapped installs, but it diverges from every other provider — treat it as a fallback, not the default. - **`AIProviderConfig` is an *untagged* `z.union`, so a new provider's config schema must sit ahead of the empty ones — and "empty" includes a schema whose every field is optional.** Zod strips unknown keys and a union returns the first member that parses, so `AnthropicProviderConfig` (`z.object({})`) matches *any* object: list it before a `{ baseUrl?: string }` config and a configured base URL is silently reduced to `{}` — no error, no log, the admin's override just stops existing on the next read. The file carries an `Order matters` comment, but it says "empty ones last", which reads as though only a literal `z.object({})` is at risk. The rule is **not** "after the last schema with a required field" — that phrasing reads as "append after `BedrockProviderConfig`" and is how a Vertex config got silently reduced to `{ region }`, losing `project` and `models` with no error. What matters is the *subset relation*, not the count: `BedrockProviderConfig` is `{ region }`, a strict subset of `{ project, region, models }`, so it matches a Vertex object first and strips the rest. Order by specificity — any schema whose required keys are a subset of yours must sit **after** you. Check with a one-line parse before trusting the order: `AIProviderConfig.parse(yourConfig)` must return every field it was given. `ProviderConfigUnion` is discriminated on `provider` and so is immune; only the two untagged unions (`AIProviderConfig`, `AIProviderAuthConfig`) bite. Both live twice — `packages/core/shared/.../management/ai-providers/index.ts` (zod classic) and `packages/core/piece-types/.../ai-providers.ts` (zod/mini, the copy pieces use) — and every provider edit has to land in both. **The third copy moved rather than disappearing.** `.../setup/ai/universal-pieces/` is gone and the connect dialog now builds from `PROVIDER_CREDENTIAL_FIELDS`, but `config-detail.tsx` declares its own `ManualProviderConfig = z.union([...])` that gates whether the manual-models panel sends `config` at all. A provider missing from it fails `safeParse`, its models are dropped from the payload, and they silently vanish on reload — which for a `MANUAL_MODEL_PROVIDERS` member means the only way to choose a model does not work. So it is still **three** unions per provider: the two shared copies plus this one, and it is the easiest to miss because it lives in a component and nothing references the provider by name. The rest of this sentence describes that deleted file and is kept only as history: `createFormSchema` in the admin dialog (`.../setup/ai/universal-pieces/upsert-provider-dialog.tsx`) re-declares a per-provider schema, branching explicitly on Azure / Cloudflare / Custom / Bedrock and falling through to a generic case whose `config` is a union of three empty objects. **A provider with a non-empty config and no branch there loses that config entirely** — `zodResolver` hands react-hook-form the *parsed* value, so the strip happens before submit and the setting is never sent, with no error anywhere. Fixing the shared union does not fix this one; grep for every union of config schemas when adding a provider. The dialog only diverges from the correct `ProviderConfigUnion` to make auth optional in edit mode, so collapsing it onto the shared discriminated union is the real repair. (Testing that file directly is awkward: importing it pulls in a transitive dep that touches `document` at import time, which a node-env vitest cannot load — the schema factory would have to move out of the component file first.) diff --git a/bun.lock b/bun.lock index 8ed1d4b8a2d4..a81a3610d649 100644 --- a/bun.lock +++ b/bun.lock @@ -97,7 +97,7 @@ }, "packages/core/ai-providers": { "name": "@activepieces/ai-providers", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", @@ -150,7 +150,7 @@ }, "packages/core/piece-types": { "name": "@activepieces/core-piece-types", - "version": "0.7.0", + "version": "0.8.0", "dependencies": { "@activepieces/core-utils": "workspace:*", "tslib": "2.6.2", @@ -163,7 +163,7 @@ }, "packages/core/shared": { "name": "@activepieces/shared", - "version": "0.156.0", + "version": "0.157.0", "dependencies": { "@activepieces/core-execution": "workspace:*", "@activepieces/core-formula": "workspace:*", @@ -188,7 +188,7 @@ }, "packages/core/utils": { "name": "@activepieces/core-utils", - "version": "0.6.1", + "version": "0.7.0", "dependencies": { "deepmerge-ts": "7.1.0", "ipaddr.js": "2.3.0", @@ -336,7 +336,7 @@ }, "packages/pieces/community/ai": { "name": "@activepieces/piece-ai", - "version": "0.10.0", + "version": "0.11.0", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", @@ -10664,7 +10664,7 @@ }, "packages/pieces/framework": { "name": "@activepieces/pieces-framework", - "version": "0.38.0", + "version": "0.39.0", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", diff --git a/docs/admin-guide/guides/setup-ai-providers.mdx b/docs/admin-guide/guides/setup-ai-providers.mdx index 99ba50c97a96..c8393bc6d776 100644 --- a/docs/admin-guide/guides/setup-ai-providers.mdx +++ b/docs/admin-guide/guides/setup-ai-providers.mdx @@ -34,6 +34,7 @@ Go to **Platform Admin** → **AI Center**, pick a provider, and add your key. T - AWS Bedrock **Gateways** + - aimlapi.com - OpenRouter - Cloudflare AI Gateway diff --git a/packages/core/ai-providers/package.json b/packages/core/ai-providers/package.json index 299b00757ee0..b87ccd6868c2 100644 --- a/packages/core/ai-providers/package.json +++ b/packages/core/ai-providers/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/ai-providers", - "version": "0.3.0", + "version": "0.4.0", "type": "commonjs", "sideEffects": false, "main": "./dist/src/index.js", diff --git a/packages/core/ai-providers/src/lib/create-language-model.test.ts b/packages/core/ai-providers/src/lib/create-language-model.test.ts index efc9c99da17f..686a16506d82 100644 --- a/packages/core/ai-providers/src/lib/create-language-model.test.ts +++ b/packages/core/ai-providers/src/lib/create-language-model.test.ts @@ -1,5 +1,5 @@ import { AIProviderName } from '@activepieces/core-utils' -import { AIProviderConfig, AIProviderModelType, VertexProviderConfig } from '@activepieces/core-piece-types' +import { AIMLAPI_ATTRIBUTION_HEADERS, AIProviderConfig, AIProviderModelType, VertexProviderConfig } from '@activepieces/core-piece-types' import { describe, expect, it } from 'vitest' import { buildOpenAICompatibleHeaders, createLanguageModel } from './create-language-model' @@ -317,3 +317,65 @@ describe('resolved endpoint, credentials and headers', () => { expect(headers['x-shared']).toBe('from-default') }) }) + +describe('aimlapi.com attribution', () => { + type ResolvedConfig = { + url: (opts: { path: string, modelId: string }) => string + headers: (() => Record) | Record + } + + const configOf = (model: unknown): ResolvedConfig => (model as { config: ResolvedConfig }).config + const headersOf = (model: unknown): Record => { + const { headers } = configOf(model) + const resolved = typeof headers === 'function' ? headers() : headers + return Object.fromEntries(Object.entries(resolved).map(([name, value]) => [name.toLowerCase(), value])) + } + const buildAimlapi = (options?: Record) => createLanguageModel({ + provider: AIProviderName.AIMLAPI, + auth: { apiKey: 'SECRET' }, + config: {}, + modelId: 'openai/gpt-4o-mini', + options, + }) + + it('keeps the partner id in the shape the gateway accepts', () => { + expect(AIMLAPI_ATTRIBUTION_HEADERS['X-AIMLAPI-Partner-ID']).toMatch(/^part_[A-Za-z0-9]{1,64}$/) + expect(AIMLAPI_ATTRIBUTION_HEADERS['X-AIMLAPI-Source']).toMatch(/^(web|agent|mcp)\/[a-z0-9-]{1,32}$/) + }) + + it('identifies the calling app, not the gateway, to analytics', () => { + expect(AIMLAPI_ATTRIBUTION_HEADERS['HTTP-Referer']).toBe('https://www.activepieces.com') + expect(AIMLAPI_ATTRIBUTION_HEADERS['X-Title']).toBe('Activepieces') + }) + + it('sends every attribution header on chat completions against the aimlapi.com base url', () => { + const model = buildAimlapi() + const headers = headersOf(model) + + expect(identify(model).provider).toBe('aimlapi.chat') + expect(configOf(model).url({ path: '/chat/completions', modelId: 'openai/gpt-4o-mini' })).toBe('https://api.aimlapi.com/v1/chat/completions') + expect(headers['authorization']).toBe('Bearer SECRET') + for (const [name, value] of Object.entries(AIMLAPI_ATTRIBUTION_HEADERS)) { + expect(headers[name.toLowerCase()]).toBe(value) + } + }) + + it('lets caller metadata win a clash and never mutates the shared constant', () => { + const before = { ...AIMLAPI_ATTRIBUTION_HEADERS } + const overridden = headersOf(buildAimlapi({ extraHeaders: { 'X-Title': 'Embedded', 'x-ap-project-id': 'proj' } })) + + expect(overridden['x-title']).toBe('Embedded') + expect(overridden['x-ap-project-id']).toBe('proj') + expect(headersOf(buildAimlapi())['x-title']).toBe('Activepieces') + expect({ ...AIMLAPI_ATTRIBUTION_HEADERS }).toEqual(before) + }) + + it('never rides attribution onto another provider', () => { + const others = [AIProviderName.OPENROUTER, AIProviderName.CUSTOM, AIProviderName.DEEPSEEK] + for (const provider of others) { + const headers = headersOf(buildFor(provider)) + expect(headers['x-aimlapi-partner-id']).toBeUndefined() + expect(headers['x-aimlapi-source']).toBeUndefined() + } + }) +}) diff --git a/packages/core/ai-providers/src/lib/create-language-model.ts b/packages/core/ai-providers/src/lib/create-language-model.ts index 287db8519c45..ec70d2d23b21 100644 --- a/packages/core/ai-providers/src/lib/create-language-model.ts +++ b/packages/core/ai-providers/src/lib/create-language-model.ts @@ -1,5 +1,5 @@ import { AIProviderName, observedProviderFetch, ProviderOutcomeReporter, spreadIfDefined } from '@activepieces/core-utils' -import { AzureProviderConfig, BaseAIProviderAuthConfig, BedrockProviderAuthConfig, BedrockProviderConfig, OPENAI_COMPATIBLE_VENDOR_BASE_URLS, OpenAICompatibleProviderConfig, VertexProviderAuthConfig, VertexProviderConfig } from '@activepieces/core-piece-types' +import { AIMLAPI_ATTRIBUTION_HEADERS, AIMLAPI_BASE_URL, AzureProviderConfig, BaseAIProviderAuthConfig, BedrockProviderAuthConfig, BedrockProviderConfig, OPENAI_COMPATIBLE_VENDOR_BASE_URLS, OpenAICompatibleProviderConfig, VertexProviderAuthConfig, VertexProviderConfig } from '@activepieces/core-piece-types' import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock' import { createVertex } from '@ai-sdk/google-vertex' import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic' @@ -93,6 +93,16 @@ export function createLanguageModel({ provider, auth, config, modelId, options = ...observed, }).chatModel(modelId) } + case AIProviderName.AIMLAPI: { + const { apiKey } = auth as BaseAIProviderAuthConfig + return createOpenAICompatible({ + name: provider, + baseURL: AIMLAPI_BASE_URL, + apiKey, + headers: { ...AIMLAPI_ATTRIBUTION_HEADERS, ...(options.extraHeaders ?? {}) }, + ...observed, + }).chatModel(modelId) + } case AIProviderName.OPENROUTER: case AIProviderName.ACTIVEPIECES: { const { apiKey } = auth as BaseAIProviderAuthConfig diff --git a/packages/core/piece-types/package.json b/packages/core/piece-types/package.json index e83468658878..f5d8f3135ff7 100644 --- a/packages/core/piece-types/package.json +++ b/packages/core/piece-types/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/core-piece-types", - "version": "0.7.0", + "version": "0.8.0", "type": "commonjs", "main": "./dist/src/index.js", "scripts": { diff --git a/packages/core/piece-types/src/lib/ai-providers.test.ts b/packages/core/piece-types/src/lib/ai-providers.test.ts index 80698bb8d6a4..45c395edec3e 100644 --- a/packages/core/piece-types/src/lib/ai-providers.test.ts +++ b/packages/core/piece-types/src/lib/ai-providers.test.ts @@ -22,6 +22,7 @@ describe('AI_PROVIDER_CAPABILITIES', () => { AIProviderName.QWEN, AIProviderName.MINIMAX, AIProviderName.MOONSHOT, + AIProviderName.AIMLAPI, ].sort()) }) diff --git a/packages/core/piece-types/src/lib/ai-providers.ts b/packages/core/piece-types/src/lib/ai-providers.ts index 53cde753be74..e02e71570175 100644 --- a/packages/core/piece-types/src/lib/ai-providers.ts +++ b/packages/core/piece-types/src/lib/ai-providers.ts @@ -325,6 +325,7 @@ const NO_IMAGE_GENERATION_PROVIDERS = new Set([ AIProviderName.QWEN, AIProviderName.MINIMAX, AIProviderName.MOONSHOT, + AIProviderName.AIMLAPI, ]) export const OPENAI_COMPATIBLE_VENDOR_BASE_URLS: Record = { @@ -336,6 +337,17 @@ export const OPENAI_COMPATIBLE_VENDOR_BASE_URLS: Record> = Object.freeze({ + 'HTTP-Referer': 'https://www.activepieces.com', + 'X-Title': 'Activepieces', + 'X-AIMLAPI-Partner-ID': 'part_activepieces', + 'X-AIMLAPI-Source': 'agent/activepieces', +}) + function buildProviderCapabilities(provider: AIProviderName): AIProviderCapabilities { return { chatModels: ALLOWED_CHAT_MODELS_BY_PROVIDER[provider], @@ -375,6 +387,7 @@ export const AI_PROVIDER_CAPABILITIES: Record = { + name: AIMLAPI_DISPLAY_NAME, + async validateConnection(authConfig: BaseAIProviderAuthConfig): Promise { + const { error } = await tryCatch(() => safeHttp.axios.request({ + method: 'GET', + url: `${AIMLAPI_BASE_URL}/key`, + timeout: REQUEST_TIMEOUT_MS, + headers: { + ...AIMLAPI_ATTRIBUTION_HEADERS, + 'Authorization': `Bearer ${authConfig.apiKey}`, + 'Content-Type': 'application/json', + }, + })) + + if (!isNil(error)) { + throw new Error(`[${AIMLAPI_DISPLAY_NAME}] failed to validate the api key: ${error instanceof Error ? error.message : String(error)}`) + } + }, + async listModels(): Promise { + const { data: response, error } = await tryCatch(() => safeHttp.axios.request({ + method: 'GET', + url: `${AIMLAPI_BASE_URL}/models`, + timeout: REQUEST_TIMEOUT_MS, + headers: { + ...AIMLAPI_ATTRIBUTION_HEADERS, + 'Content-Type': 'application/json', + }, + })) + + if (!isNil(error) || isNil(response)) { + throw new Error(`[${AIMLAPI_DISPLAY_NAME}] failed to list models: ${error instanceof Error ? error.message : String(error)}`) + } + + return (response.data.data ?? []) + .filter((model) => model.type === AIMLAPI_CHAT_MODEL_TYPE) + .map((model) => ({ + id: model.id, + name: model.id, + type: AIProviderModelType.TEXT, + })) + }, +} + +type AimlapiModelsResponse = { + data?: { id: string, type?: string }[] +} diff --git a/packages/server/api/src/app/ai/providers/index.ts b/packages/server/api/src/app/ai/providers/index.ts index 27027e01f67b..553d9083216b 100644 --- a/packages/server/api/src/app/ai/providers/index.ts +++ b/packages/server/api/src/app/ai/providers/index.ts @@ -1,6 +1,7 @@ import { AIProviderName } from '@activepieces/core-utils' import { AIProviderAuthConfig, AIProviderConfig } from '@activepieces/shared' import { AIProviderStrategy } from './ai-provider' +import { aimlapiProvider } from './aimlapi-provider' import { anthropicProvider } from './anthropic-provider' import { azureProvider } from './azure-provider' import { bedrockProvider } from './bedrock-provider' @@ -30,6 +31,7 @@ export const aiProviders: Record