Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions brain/knowledge/ai-intelligence/ai-providers.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/admin-guide/guides/setup-ai-providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/core/ai-providers/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
64 changes: 63 additions & 1 deletion packages/core/ai-providers/src/lib/create-language-model.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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<string, string>) | Record<string, string>
}

const configOf = (model: unknown): ResolvedConfig => (model as { config: ResolvedConfig }).config
const headersOf = (model: unknown): Record<string, string> => {
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<string, unknown>) => 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()
}
})
})
12 changes: 11 additions & 1 deletion packages/core/ai-providers/src/lib/create-language-model.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/piece-types/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/core/piece-types/src/lib/ai-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('AI_PROVIDER_CAPABILITIES', () => {
AIProviderName.QWEN,
AIProviderName.MINIMAX,
AIProviderName.MOONSHOT,
AIProviderName.AIMLAPI,
].sort())
})

Expand Down
13 changes: 13 additions & 0 deletions packages/core/piece-types/src/lib/ai-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ const NO_IMAGE_GENERATION_PROVIDERS = new Set<AIProviderName>([
AIProviderName.QWEN,
AIProviderName.MINIMAX,
AIProviderName.MOONSHOT,
AIProviderName.AIMLAPI,
])

export const OPENAI_COMPATIBLE_VENDOR_BASE_URLS: Record<OpenAiCompatibleVendor, string> = {
Expand All @@ -336,6 +337,17 @@ export const OPENAI_COMPATIBLE_VENDOR_BASE_URLS: Record<OpenAiCompatibleVendor,
[AIProviderName.MOONSHOT]: 'https://api.moonshot.ai/v1',
}

export const AIMLAPI_BASE_URL = 'https://api.aimlapi.com/v1'

export const AIMLAPI_CHAT_MODEL_TYPE = 'openai/chat-completions'

export const AIMLAPI_ATTRIBUTION_HEADERS: Readonly<Record<string, string>> = 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],
Expand Down Expand Up @@ -375,6 +387,7 @@ export const AI_PROVIDER_CAPABILITIES: Record<AIProviderName, AIProviderCapabili
[AIProviderName.QWEN]: buildProviderCapabilities(AIProviderName.QWEN),
[AIProviderName.MINIMAX]: buildProviderCapabilities(AIProviderName.MINIMAX),
[AIProviderName.MOONSHOT]: buildProviderCapabilities(AIProviderName.MOONSHOT),
[AIProviderName.AIMLAPI]: buildProviderCapabilities(AIProviderName.AIMLAPI),
}

export const aiProviderUtils = {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@activepieces/shared",
"version": "0.156.0",
"version": "0.157.0",
"type": "commonjs",
"sideEffects": false,
"main": "./dist/src/index.js",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/shared/src/lib/management/ai-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@ const ProviderConfigUnion = z.discriminatedUnion('provider', [
config: OpenAiCompatibleVendorConfig,
auth: BaseAIProviderAuthConfig,
}),
z.object({
displayName: z.string().min(1),
provider: z.literal(AIProviderName.AIMLAPI),
config: OpenAiCompatibleVendorConfig,
auth: BaseAIProviderAuthConfig,
}),
])

export const AIProvider = z.object({
Expand Down
2 changes: 1 addition & 1 deletion packages/core/utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@activepieces/core-utils",
"version": "0.6.1",
"version": "0.7.0",
"type": "commonjs",
"main": "./dist/src/index.js",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions packages/core/utils/src/lib/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ export enum AIProviderName {
QWEN = 'qwen',
MINIMAX = 'minimax',
MOONSHOT = 'moonshot',
AIMLAPI = 'aimlapi',
}
2 changes: 1 addition & 1 deletion packages/pieces/community/ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@activepieces/piece-ai",
"version": "0.10.0",
"version": "0.11.0",
"type": "commonjs",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
Expand Down
11 changes: 10 additions & 1 deletion packages/pieces/community/ai/src/lib/common/ai-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider'
import { EmbeddingModel, ImageModel, LanguageModel } from 'ai'
import { ProviderOptions } from '@ai-sdk/provider-utils'
import { httpClient, HttpMethod } from '@activepieces/pieces-common'
import { AI_PROVIDER_CAPABILITIES, AIProviderName, AzureProviderConfig, BaseAIProviderAuthConfig, BedrockProviderAuthConfig, BedrockProviderConfig, CloudflareGatewayProviderConfig, GetProviderConfigResponse, OPENAI_COMPATIBLE_VENDOR_BASE_URLS, OpenAICompatibleProviderConfig, splitCloudflareGatewayModelId, spreadIfDefined, VertexProviderAuthConfig, VertexProviderConfig } from '@activepieces/pieces-framework'
import { AI_PROVIDER_CAPABILITIES, AIMLAPI_ATTRIBUTION_HEADERS, AIMLAPI_BASE_URL, AIProviderName, AzureProviderConfig, BaseAIProviderAuthConfig, BedrockProviderAuthConfig, BedrockProviderConfig, CloudflareGatewayProviderConfig, GetProviderConfigResponse, OPENAI_COMPATIBLE_VENDOR_BASE_URLS, OpenAICompatibleProviderConfig, splitCloudflareGatewayModelId, spreadIfDefined, VertexProviderAuthConfig, VertexProviderConfig } from '@activepieces/pieces-framework'
import { createAiGateway } from 'ai-gateway-provider';
import { createAnthropic as createAnthropicGateway } from 'ai-gateway-provider/providers/anthropic';
import { createGoogleGenerativeAI as createGoogleGateway } from 'ai-gateway-provider/providers/google';
Expand Down Expand Up @@ -194,6 +194,15 @@ function buildLanguageModel({ provider, auth, config, modelId, openaiResponsesMo
apiKey,
}).chatModel(modelId)
}
case AIProviderName.AIMLAPI: {
const { apiKey } = auth as BaseAIProviderAuthConfig
return createOpenAICompatible({
name: provider,
baseURL: AIMLAPI_BASE_URL,
apiKey,
headers: { ...AIMLAPI_ATTRIBUTION_HEADERS, ...metadataHeaders },
}).chatModel(modelId)
}
case AIProviderName.ACTIVEPIECES: {
const { apiKey } = auth as BaseAIProviderAuthConfig
return createOpenRouter({ apiKey, headers: metadataHeaders }).chat(modelId) as LanguageModel
Expand Down
2 changes: 1 addition & 1 deletion packages/pieces/framework/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@activepieces/pieces-framework",
"version": "0.38.0",
"version": "0.39.0",
"type": "commonjs",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
Expand Down
2 changes: 2 additions & 0 deletions packages/pieces/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export {
FAIL_PARENT_ON_FAILURE_HEADER,
ACTIVEPIECES_CHAT_TIERS,
DEFAULT_CHAT_TIER_ID,
AIMLAPI_BASE_URL,
AIMLAPI_ATTRIBUTION_HEADERS,
} from '@activepieces/core-piece-types';
export type {
McpAuthConfig,
Expand Down
55 changes: 55 additions & 0 deletions packages/server/api/src/app/ai/providers/aimlapi-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { AIMLAPI_ATTRIBUTION_HEADERS, AIMLAPI_BASE_URL, AIMLAPI_CHAT_MODEL_TYPE } from '@activepieces/core-piece-types'
import { safeHttp } from '@activepieces/server-utils'
import { AIProviderModel, AIProviderModelType, BaseAIProviderAuthConfig, isNil, OpenAiCompatibleVendorConfig, tryCatch } from '@activepieces/shared'
import { AIProviderStrategy } from './ai-provider'

const AIMLAPI_DISPLAY_NAME = 'aimlapi.com'

const REQUEST_TIMEOUT_MS = 15_000

export const aimlapiProvider: AIProviderStrategy<BaseAIProviderAuthConfig, OpenAiCompatibleVendorConfig> = {
name: AIMLAPI_DISPLAY_NAME,
async validateConnection(authConfig: BaseAIProviderAuthConfig): Promise<void> {
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<AIProviderModel[]> {
const { data: response, error } = await tryCatch(() => safeHttp.axios.request<AimlapiModelsResponse>({
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 }[]
}
2 changes: 2 additions & 0 deletions packages/server/api/src/app/ai/providers/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -30,6 +31,7 @@ export const aiProviders: Record<AIProviderName, AIProviderStrategy<AIProviderAu
[AIProviderName.QWEN]: openAiCompatibleVendor({ name: 'Qwen', provider: AIProviderName.QWEN }),
[AIProviderName.MINIMAX]: openAiCompatibleVendor({ name: 'MiniMax', provider: AIProviderName.MINIMAX }),
[AIProviderName.MOONSHOT]: openAiCompatibleVendor({ name: 'Moonshot AI', provider: AIProviderName.MOONSHOT }),
[AIProviderName.AIMLAPI]: aimlapiProvider,
[AIProviderName.ACTIVEPIECES]: {
...openRouterProvider,
name: 'Activepieces',
Expand Down
Loading