Skip to content
Merged
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
2 changes: 1 addition & 1 deletion bun.lock

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

90 changes: 90 additions & 0 deletions packages/core/ai-providers/src/lib/create-language-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,36 @@ import { buildOpenAICompatibleHeaders, createLanguageModel } from './create-lang

type ModelIdentity = { provider: string, modelId: string, settings?: { plugins?: unknown[] } }

type CustomModelIdentity = { config: { headers: () => Record<string, string>, fetch?: typeof globalThis.fetch } }

function identify(model: unknown): ModelIdentity {
return model as ModelIdentity
}

function identifyCustom(model: unknown): CustomModelIdentity {
return model as CustomModelIdentity
}

async function captureHeaders({ patchedFetch, headers }: {
patchedFetch?: typeof globalThis.fetch
headers: Record<string, string>
}): Promise<Headers> {
const original = globalThis.fetch
let seen = new Headers()
globalThis.fetch = (_input, init) => {
seen = new Headers(init?.headers)
return Promise.resolve(new Response('{}'))
}
try {
await patchedFetch?.('https://example.test/v1/responses', { headers })
}
finally {
globalThis.fetch = original
}
return seen
}


const authFor: Partial<Record<AIProviderName, unknown>> = {
[AIProviderName.BEDROCK]: { accessKeyId: 'a', secretAccessKey: 'b' },
}
Expand Down Expand Up @@ -47,6 +73,70 @@ describe('createLanguageModel', () => {
expect(identify(buildFor(AIProviderName.OPENAI, { openaiResponsesModel: true })).provider).toBe('openai.responses')
})

it('keeps the custom provider on chat completions unless apiStyle asks for responses', () => {
const responsesConfig = { ...(configFor[AIProviderName.CUSTOM] as Record<string, unknown>), apiStyle: 'responses' }
const model = createLanguageModel({
provider: AIProviderName.CUSTOM,
auth: { apiKey: 'test-key' },
config: responsesConfig,
modelId: 'openai.gpt-oss-120b',
})
expect(identify(buildFor(AIProviderName.CUSTOM)).provider).toBe('openai-compatible.chat')
expect(identify(model).provider).toBe('openai.responses')
expect(identify(model).modelId).toBe('openai.gpt-oss-120b')
})

it('drops the SDK default Authorization when the custom provider authenticates with another header', async () => {
const model = createLanguageModel({
provider: AIProviderName.CUSTOM,
auth: { apiKey: 'secret-key' },
config: { apiKeyHeader: 'x-api-key', baseUrl: 'https://example.test/v1', models: [], apiStyle: 'responses' },
modelId: 'some-model-id',
})
const { headers, fetch: patchedFetch } = identifyCustom(model).config
const sent = headers()

expect(sent['x-api-key']).toBe('secret-key')
expect(sent['authorization']).toBe('Bearer secret-key')
expect(patchedFetch).toBeDefined()

const seen = await captureHeaders({ patchedFetch, headers: sent })
expect(seen.get('x-api-key')).toBe('secret-key')
expect(seen.get('authorization')).toBeNull()
})

it('keeps the Authorization header when that is the custom provider\'s own api key header', () => {
const model = createLanguageModel({
provider: AIProviderName.CUSTOM,
auth: { apiKey: 'Bearer bedrock-key' },
config: { apiKeyHeader: 'Authorization', baseUrl: 'https://bedrock-mantle.us-east-1.api.aws/v1', models: [], apiStyle: 'responses' },
modelId: 'openai.gpt-oss-120b',
})
const { headers, fetch: patchedFetch } = identifyCustom(model).config

expect(headers()['authorization']).toBe('Bearer bedrock-key')
expect(patchedFetch).toBeUndefined()
})

it('keeps an Authorization the admin supplied through custom headers', async () => {
const model = createLanguageModel({
provider: AIProviderName.CUSTOM,
auth: { apiKey: 'secret-key' },
config: {
apiKeyHeader: 'x-api-key',
baseUrl: 'https://gateway.test/v1',
models: [],
apiStyle: 'responses',
defaultHeaders: { Authorization: 'Bearer gateway-token' },
},
modelId: 'some-model-id',
})
const { headers, fetch: patchedFetch } = identifyCustom(model).config

expect(headers()['authorization']).toBe('Bearer gateway-token')
expect(patchedFetch).toBeUndefined()
})

it('sends Mistral to its own API by default and via OpenRouter when requested', () => {
expect(identify(buildFor(AIProviderName.MISTRAL)).provider).toBe('mistral.chat')
expect(identify(buildFor(AIProviderName.MISTRAL, { mistralViaOpenRouter: true })).provider).toBe('openrouter')
Expand Down
33 changes: 31 additions & 2 deletions packages/core/ai-providers/src/lib/create-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createOpenRouter, OpenRouterChatSettings } from '@openrouter/ai-sdk-pro
import { LanguageModel } from 'ai'

const MISTRAL_BASE_URL = 'https://api.mistral.ai/v1'
const AUTHORIZATION_HEADER = 'authorization'

export function createLanguageModel({ provider, auth, config, modelId, options = {} }: CreateLanguageModelParams): LanguageModel {
const observed = spreadIfDefined('fetch', observedProviderFetch(options.onOutcome))
Expand Down Expand Up @@ -39,11 +40,24 @@ export function createLanguageModel({ provider, auth, config, modelId, options =
}
case AIProviderName.CUSTOM: {
const { apiKey } = auth as BaseAIProviderAuthConfig
const { apiKeyHeader, baseUrl, defaultHeaders } = config as OpenAICompatibleProviderConfig
const { apiKeyHeader, baseUrl, defaultHeaders, apiStyle } = config as OpenAICompatibleProviderConfig
const headers = buildOpenAICompatibleHeaders({ apiKeyHeader, apiKey, defaultHeaders, extraHeaders: options.extraHeaders })
if (apiStyle === 'responses') {
return createOpenAI({
baseURL: baseUrl,
apiKey,
headers,
...observed,
...spreadIfDefined('fetch', stripDefaultAuthorization({
headers,
delegate: observedProviderFetch(options.onOutcome),
})),
}).responses(modelId)
}
return createOpenAICompatible({
name: 'openai-compatible',
baseURL: baseUrl,
headers: buildOpenAICompatibleHeaders({ apiKeyHeader, apiKey, defaultHeaders, extraHeaders: options.extraHeaders }),
headers,
...observed,
}).chatModel(modelId)
}
Expand Down Expand Up @@ -82,6 +96,21 @@ export function createLanguageModel({ provider, auth, config, modelId, options =
}
}

function stripDefaultAuthorization({ headers, delegate }: {
headers: Record<string, string>
delegate?: typeof globalThis.fetch
}): typeof globalThis.fetch | undefined {
const carriesAuthorization = Object.keys(headers).some((key) => key.trim().toLowerCase() === AUTHORIZATION_HEADER)
if (carriesAuthorization) {
return undefined
}
return (input, init) => {
const sent = new Headers(init?.headers)
sent.delete(AUTHORIZATION_HEADER)
return (delegate ?? globalThis.fetch)(input, { ...init, headers: sent })
}
}

function createOpenRouterChatModel({ apiKey, modelId, options }: {
apiKey: string
modelId: string
Expand Down
1 change: 1 addition & 0 deletions packages/core/piece-types/src/lib/ai-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const OpenAICompatibleProviderConfig = z.object({
baseUrl: z.string(),
models: z.array(ProviderModelConfig),
defaultHeaders: z.optional(z.record(z.string(), z.string())),
apiStyle: z.optional(z.enum(['chat', 'responses'])),
})
export type OpenAICompatibleProviderConfig = z.infer<typeof OpenAICompatibleProviderConfig>

Expand Down
19 changes: 19 additions & 0 deletions packages/core/shared/src/lib/ee/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ const Agent = z.object({
published: Nullable(AgentConfig),
})

const AgentUsage = z.object({
total: z.number().int().nonnegative(),
names: z.array(z.string()),
})

const AgentWithUsage = Agent.extend({
publishedFlowsUsingAgent: AgentUsage.optional(),
})

const AgentSummary = Agent.omit({ draft: true, published: true }).extend({
isPublished: z.boolean(),
toolCount: z.number(),
Expand Down Expand Up @@ -109,6 +118,10 @@ const DraftAgentRequest = z.object({
prompt: z.string().min(1, formErrors.required).max(MAX_DRAFT_PROMPT_LENGTH),
})

const GetAgentRequest = z.object({
includeUsage: z.coerce.boolean().optional(),
})

const ListAgentsRequest = z.object({
projectId: z.optional(ApId),
cursor: z.string().optional(),
Expand All @@ -121,6 +134,9 @@ const agentUtils = {

export {
Agent,
AgentUsage,
AgentWithUsage,
GetAgentRequest,
AgentSummary,
agentUtils,
AgentConfig,
Expand Down Expand Up @@ -148,6 +164,9 @@ export {

export type Agent = z.infer<typeof Agent>
export type AgentSummary = z.infer<typeof AgentSummary>
export type AgentUsage = z.infer<typeof AgentUsage>
export type AgentWithUsage = z.infer<typeof AgentWithUsage>
export type GetAgentRequest = z.infer<typeof GetAgentRequest>
export type AgentConfig = z.infer<typeof AgentConfig>
export type CreateAgentRequest = z.infer<typeof CreateAgentRequest>
export type DraftAgentRequest = z.infer<typeof DraftAgentRequest>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const OpenAICompatibleProviderConfig = z.object({
baseUrl: z.string(),
models: z.array(ProviderModelConfig),
defaultHeaders: z.record(z.string(), z.string()).optional(),
apiStyle: z.enum(['chat', 'responses']).optional(),
})
export type OpenAICompatibleProviderConfig = z.infer<typeof OpenAICompatibleProviderConfig>

Expand Down
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.9.0",
"version": "0.10.0",
"type": "commonjs",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
Expand Down
37 changes: 30 additions & 7 deletions packages/pieces/community/ai/src/lib/common/ai-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ 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 } from '@activepieces/pieces-framework'
import { AI_PROVIDER_CAPABILITIES, AIProviderName, AzureProviderConfig, BaseAIProviderAuthConfig, BedrockProviderAuthConfig, BedrockProviderConfig, CloudflareGatewayProviderConfig, GetProviderConfigResponse, OPENAI_COMPATIBLE_VENDOR_BASE_URLS, OpenAICompatibleProviderConfig, splitCloudflareGatewayModelId, spreadIfDefined } 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';

const AUTHORIZATION_HEADER = 'authorization'

async function fetchProviderConfig(params: { provider: AIProviderName, engineToken: string, apiUrl: string, configId?: string }) {
const { body } = await httpClient.sendRequest<GetProviderConfigResponse>({
method: HttpMethod.GET,
Expand Down Expand Up @@ -147,15 +149,24 @@ function buildLanguageModel({ provider, auth, config, modelId, openaiResponsesMo
}
case AIProviderName.CUSTOM: {
const { apiKey } = auth as BaseAIProviderAuthConfig
const { apiKeyHeader, baseUrl, defaultHeaders } = config as OpenAICompatibleProviderConfig
const { apiKeyHeader, baseUrl, defaultHeaders, apiStyle } = config as OpenAICompatibleProviderConfig
const headers = {
...metadataHeaders,
...(defaultHeaders ?? {}),
[apiKeyHeader]: apiKey,
}
if (apiStyle === 'responses') {
return createOpenAI({
baseURL: baseUrl,
apiKey,
headers,
...spreadIfDefined('fetch', stripDefaultAuthorization(headers)),
}).responses(modelId)
}
return createOpenAICompatible({
name: 'openai-compatible',
baseURL: baseUrl,
headers: {
...metadataHeaders,
...(defaultHeaders ?? {}),
[apiKeyHeader]: apiKey,
},
headers,
}).chatModel(modelId)
}
case AIProviderName.MISTRAL: {
Expand Down Expand Up @@ -188,6 +199,18 @@ function buildLanguageModel({ provider, auth, config, modelId, openaiResponsesMo
}
}

function stripDefaultAuthorization(headers: Record<string, string>): typeof globalThis.fetch | undefined {
const carriesAuthorization = Object.keys(headers).some((key) => key.trim().toLowerCase() === AUTHORIZATION_HEADER)
if (carriesAuthorization) {
return undefined
}
return (input, init) => {
const sent = new Headers(init?.headers)
sent.delete(AUTHORIZATION_HEADER)
return fetch(input, { ...init, headers: sent })
}
}

function buildCustomMetadataHeaders({ projectId, platformId, flowId, runId }: {
projectId: string
platformId: string
Expand Down
11 changes: 1 addition & 10 deletions packages/server/api/src/app/database/seeds/dev-seeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { authenticationService } from '../../authentication/authentication.servi
import { FlagEntity } from '../../flags/flag.entity'
import { system } from '../../helper/system/system'
import { AppSystemProp } from '../../helper/system/system-props'
import { platformService } from '../../platform/platform.service'
import { databaseConnection } from '../database-connection'
import { DataSeed } from './data-seed'

Expand Down Expand Up @@ -35,7 +34,7 @@ const seedDevUser = async (): Promise<void> => {
const DEV_PASSWORD = '12345678'


const response = await authenticationService(log).signUp({
await authenticationService(log).signUp({
email: DEV_EMAIL,
password: DEV_PASSWORD,
firstName: 'Dev',
Expand All @@ -46,14 +45,6 @@ const seedDevUser = async (): Promise<void> => {
provider: UserIdentityProvider.EMAIL,
})

await platformService(log).createPlatformWithProject({
identityId: response.id,
name: 'dev\'s Platform',
invalidatePreviousTokens: true,
isFirstPlatform: true,
callerTokenVersion: undefined,
})

log.info({ email: DEV_EMAIL, password: DEV_PASSWORD }, '[devSeeds#seedDevUser] Dev user and platform created')
}
const seedDevData = async (): Promise<void> => {
Expand Down
20 changes: 14 additions & 6 deletions packages/server/api/src/app/ee/agent/agent-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ActivepiecesError, ApId, assertNotNullOrUndefined, ErrorCode, Permission, SeekPage, UserId } from '@activepieces/core-utils'
import { Agent, AgentSummary, ApplicationEventName, CreateAgentRequest, DraftAgentRequest, DraftAgentResponse, ListAgentsRequest, PrincipalType, SERVICE_KEY_SECURITY_OPENAPI, UpdateAgentRequest } from '@activepieces/shared'
import { Agent, AgentSummary, AgentWithUsage, ApplicationEventName, CreateAgentRequest, DraftAgentRequest, DraftAgentResponse, GetAgentRequest, ListAgentsRequest, PrincipalType, SERVICE_KEY_SECURITY_OPENAPI, UpdateAgentRequest } from '@activepieces/shared'
import { FastifyRequest } from 'fastify'
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'
import { StatusCodes } from 'http-status-codes'
Expand Down Expand Up @@ -59,12 +59,19 @@ export const agentController: FastifyPluginAsyncZod = async (app) => {
return agentDraftAi(request.log).draft({ platformId, projectId: request.projectId, prompt: request.body.prompt })
})

app.get('/:id', GetAgentRoute, async (request): Promise<Agent> => {
return agentRedaction.withoutToolSecrets(await agentService(request.log).getOneOrThrow({
app.get('/:id', GetAgentRoute, async (request): Promise<AgentWithUsage> => {
const userId = await resolveUserId(request)
const agent = await agentService(request.log).getOneOrThrow({
id: request.params.id,
projectId: request.projectId,
userId: await resolveUserId(request),
}))
userId,
})
const redacted = agentRedaction.withoutToolSecrets(agent)
if (request.query.includeUsage !== true) {
return redacted
}
const publishedFlowsUsingAgent = await agentService(request.log).publishedFlowsUsing({ agent, projectId: request.projectId, userId })
return { ...redacted, publishedFlowsUsingAgent }
})

app.post('/:id', UpdateAgentRoute, async (request): Promise<Agent> => {
Expand Down Expand Up @@ -194,8 +201,9 @@ const GetAgentRoute = {
security: [SERVICE_KEY_SECURITY_OPENAPI],
description: 'Get an agent',
params: z.object({ id: ApId }),
querystring: GetAgentRequest,
response: {
[StatusCodes.OK]: Agent,
[StatusCodes.OK]: AgentWithUsage,
},
},
}
Expand Down
Loading
Loading