diff --git a/src/client.ts b/src/client.ts index 3965f29..feeab32 100644 --- a/src/client.ts +++ b/src/client.ts @@ -10,6 +10,7 @@ import { import { getAccessToken } from "./utils/auth-utils.js"; import { createFunctionsModule } from "./modules/functions.js"; import { createAgentsModule } from "./modules/agents.js"; +import { createAiGatewayModule } from "./modules/ai-gateway.js"; import { createAppLogsModule } from "./modules/app-logs.js"; import { createUsersModule } from "./modules/users.js"; import { RoomsSocket, RoomsSocketConfig } from "./utils/socket-utils.js"; @@ -190,6 +191,7 @@ export function createClient(config: CreateClientConfig): Base44Client { serverUrl, token, }), + aiGateway: createAiGatewayModule({ serverUrl, appBaseUrl: normalizedAppBaseUrl, token }), appLogs: createAppLogsModule(axiosClient, appId), users: createUsersModule(axiosClient, appId), analytics: createAnalyticsModule({ @@ -233,6 +235,7 @@ export function createClient(config: CreateClientConfig): Base44Client { serverUrl, token, }), + aiGateway: createAiGatewayModule({ serverUrl, appBaseUrl: normalizedAppBaseUrl, token: serviceToken }), appLogs: createAppLogsModule(serviceRoleAxiosClient, appId), cleanup: () => { if (socket) { @@ -392,6 +395,7 @@ export function createClientFromRequest(request: Request): Base44Client { ); const appId = request.headers.get("Base44-App-Id"); const serverUrlHeader = request.headers.get("Base44-Api-Url"); + const appBaseUrlHeader = request.headers.get("Base44-App-Base-Url"); const functionsVersion = request.headers.get("Base44-Functions-Version"); const stateHeader = request.headers.get("Base44-State"); @@ -439,6 +443,7 @@ export function createClientFromRequest(request: Request): Base44Client { return createClient({ serverUrl: serverUrlHeader || "https://base44.app", + appBaseUrl: appBaseUrlHeader ?? undefined, appId, token: userToken, serviceToken: serviceRoleToken, diff --git a/src/client.types.ts b/src/client.types.ts index 44c2a6c..611f81b 100644 --- a/src/client.types.ts +++ b/src/client.types.ts @@ -8,6 +8,7 @@ import type { } from "./modules/connectors.types.js"; import type { FunctionsModule } from "./modules/functions.types.js"; import type { AgentsModule } from "./modules/agents.types.js"; +import type { AiGatewayModule } from "./modules/ai-gateway.types.js"; import type { AppLogsModule } from "./modules/app-logs.types.js"; import type { AnalyticsModule } from "./modules/analytics.types.js"; @@ -87,6 +88,8 @@ export interface CreateClientConfig { export interface Base44Client { /** {@link AgentsModule | Agents module} for managing AI agent conversations. */ agents: AgentsModule; + /** {@link AiGatewayModule | AI Gateway module} for connecting to the Base44 AI Gateway with your own SDK. */ + aiGateway: AiGatewayModule; /** {@link AnalyticsModule | Analytics module} for tracking custom events in your app. */ analytics: AnalyticsModule; /** {@link AppLogsModule | App logs module} for tracking app usage. */ @@ -129,6 +132,8 @@ export interface Base44Client { readonly asServiceRole: { /** {@link AgentsModule | Agents module} with elevated permissions. */ agents: AgentsModule; + /** {@link AiGatewayModule | AI Gateway module} with the service-role token. */ + aiGateway: AiGatewayModule; /** {@link AppLogsModule | App logs module} with elevated permissions. */ appLogs: AppLogsModule; /** {@link ConnectorsModule | Connectors module} for OAuth token retrieval. */ diff --git a/src/index.ts b/src/index.ts index bc531d8..3c445bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,6 +100,11 @@ export type { CreateConversationParams, } from "./modules/agents.types.js"; +export type { + AiGatewayModule, + AiGatewayConnection, +} from "./modules/ai-gateway.types.js"; + export type { AppLogsModule } from "./modules/app-logs.types.js"; export type { SsoModule, SsoAccessTokenResponse } from "./modules/sso.types.js"; diff --git a/src/modules/ai-gateway.ts b/src/modules/ai-gateway.ts new file mode 100644 index 0000000..e4c9be4 --- /dev/null +++ b/src/modules/ai-gateway.ts @@ -0,0 +1,22 @@ +import { getAccessToken } from "../utils/auth-utils.js"; +import { + AiGatewayModule, + AiGatewayModuleConfig, + AiGatewayConnection, +} from "./ai-gateway.types.js"; + +export function createAiGatewayModule({ + serverUrl, + appBaseUrl, + token, +}: AiGatewayModuleConfig): AiGatewayModule { + const gatewayOrigin = appBaseUrl || serverUrl; + const connection = (): AiGatewayConnection => ({ + baseURL: `${gatewayOrigin}/api/ai/openai/v1`, + token: token ?? getAccessToken() ?? "", + }); + + return { + connection, + }; +} diff --git a/src/modules/ai-gateway.types.ts b/src/modules/ai-gateway.types.ts new file mode 100644 index 0000000..899c31d --- /dev/null +++ b/src/modules/ai-gateway.types.ts @@ -0,0 +1,105 @@ +/** + * A connection to the Base44 AI Gateway. + * + * Contains the base URL and bearer token to use with any OpenAI-compatible client + * (OpenAI SDK, Mastra, Vercel AI SDK, and others) pointed at the Base44 AI + * Gateway. + */ +export interface AiGatewayConnection { + /** Base URL of the gateway's OpenAI-compatible endpoint. */ + baseURL: string; + /** Bearer token used to authenticate requests to the gateway. */ + token: string; +} + +/** + * Configuration for the AI Gateway module. + * @internal + */ +export interface AiGatewayModuleConfig { + /** Server URL */ + serverUrl?: string; + /** The app's own public base URL (e.g. https://my-app.base44.app). */ + appBaseUrl?: string; + /** Authentication token */ + token?: string; +} + +/** + * AI Gateway module for calling Base44's managed AI models from your own code. + * + * The gateway exposes an OpenAI-compatible Chat Completions endpoint, so any + * OpenAI-compatible SDK works against it: + * - Build custom AI agents with agent SDKs such as Mastra or the Vercel AI SDK + * - Uses your app's models, billing, and credit quota, no API key to manage + * + * Available in user authentication mode (`base44.aiGateway`) and with the + * service-role token via `base44.asServiceRole.aiGateway`. + */ +export interface AiGatewayModule { + /** + * Gets the connection details for the Base44 AI Gateway. + * + * Returns the `baseURL` and `token` to pass to any OpenAI-compatible client. + * + * The `token` is the current caller's bearer token: the app user's token for + * `base44.aiGateway`, or the service-role token for `base44.asServiceRole.aiGateway`. + * When the caller is unauthenticated, `token` is an empty string. + * + * @returns The gateway {@linkcode AiGatewayConnection | connection} (`baseURL` and `token`). + * + * @example + * ```typescript + * // Build an AI agent with Mastra on top of the gateway, inside a backend function + * import { createClientFromRequest } from 'npm:@base44/sdk'; + * import { Agent } from 'npm:@mastra/core/agent'; + * import { createTool } from 'npm:@mastra/core/tools'; + * import { createOpenAICompatible } from 'npm:@ai-sdk/openai-compatible'; + * import { z } from 'npm:zod'; + * + * Deno.serve(async (req) => { + * const base44 = createClientFromRequest(req); + * const { baseURL, token } = base44.aiGateway.connection(); + * const models = createOpenAICompatible({ name: 'base44', baseURL, apiKey: token }); + * + * const agent = new Agent({ + * id: 'order-helper', + * name: 'order-helper', + * instructions: 'Help the user with their orders.', + * model: models('claude_sonnet_4_6'), + * tools: { + * lookupOrder: createTool({ + * id: 'lookup-order', + * description: 'Fetch an order by id', + * inputSchema: z.object({ orderId: z.string() }), + * execute: async ({ orderId }) => base44.entities.Order.get(orderId), + * }), + * }, + * }); + * + * const { text } = await agent.generate('Where is order 123?'); + * return Response.json({ text }); + * }); + * ``` + * + * @example + * ```typescript + * // Call a model directly with the OpenAI SDK + * import { createClientFromRequest } from 'npm:@base44/sdk'; + * import OpenAI from 'npm:openai'; + * + * Deno.serve(async (req) => { + * const base44 = createClientFromRequest(req); + * const { baseURL, token } = base44.aiGateway.connection(); + * + * const openai = new OpenAI({ baseURL, apiKey: token }); + * const res = await openai.chat.completions.create({ + * model: 'claude_sonnet_4_6', + * messages: [{ role: 'user', content: 'Hello!' }], + * }); + * return Response.json({ text: res.choices[0].message.content }); + * }); + * ``` + */ + connection(): AiGatewayConnection; +} diff --git a/src/modules/types.ts b/src/modules/types.ts index c7423e6..f8f8f19 100644 --- a/src/modules/types.ts +++ b/src/modules/types.ts @@ -1,4 +1,5 @@ export * from "./app.types.js"; export * from "./agents.types.js"; +export * from "./ai-gateway.types.js"; export * from "./connectors.types.js"; export * from "./analytics.types.js"; \ No newline at end of file diff --git a/tests/unit/ai-gateway.test.ts b/tests/unit/ai-gateway.test.ts new file mode 100644 index 0000000..0829a21 --- /dev/null +++ b/tests/unit/ai-gateway.test.ts @@ -0,0 +1,80 @@ +import { describe, test, expect } from "vitest"; +import { createClient, createClientFromRequest } from "../../src/index.ts"; + +describe("AI Gateway Module", () => { + const appId = "test-app-id"; + const serverUrl = "https://api.base44.com"; + const baseURL = `${serverUrl}/api/ai/openai/v1`; + + describe("connection", () => { + test("should return the OpenAI-compatible gateway baseURL", () => { + const base44 = createClient({ serverUrl, appId }); + expect(base44.aiGateway.connection().baseURL).toBe(baseURL); + }); + + test("should return an empty token when unauthenticated", () => { + const base44 = createClient({ serverUrl, appId }); + expect(base44.aiGateway.connection().token).toBe(""); + }); + + test("should use the user token when authenticated", () => { + const base44 = createClient({ serverUrl, appId, token: "user-token" }); + expect(base44.aiGateway.connection()).toEqual({ + baseURL, + token: "user-token", + }); + }); + + test("should prefer appBaseUrl over serverUrl (domain-resolved gateway)", () => { + const base44 = createClient({ + serverUrl, + appBaseUrl: "https://my-app.base44.app", + appId, + }); + expect(base44.aiGateway.connection().baseURL).toBe( + "https://my-app.base44.app/api/ai/openai/v1" + ); + }); + + test("should build from the Base44-App-Base-Url header in backend functions", () => { + const request = new Request("https://functions.internal/run", { + headers: { + "Base44-App-Id": appId, + "Base44-Api-Url": serverUrl, + "Base44-App-Base-Url": "https://my-app.base44.app", + Authorization: "Bearer user-token", + }, + }); + const base44 = createClientFromRequest(request); + expect(base44.aiGateway.connection()).toEqual({ + baseURL: "https://my-app.base44.app/api/ai/openai/v1", + token: "user-token", + }); + }); + + test("should fall back to serverUrl when the app-base-url header is absent", () => { + const request = new Request("https://functions.internal/run", { + headers: { + "Base44-App-Id": appId, + "Base44-Api-Url": serverUrl, + Authorization: "Bearer user-token", + }, + }); + const base44 = createClientFromRequest(request); + expect(base44.aiGateway.connection().baseURL).toBe(baseURL); + }); + + test("should use the service-role token via asServiceRole", () => { + const base44 = createClient({ + serverUrl, + appId, + token: "user-token", + serviceToken: "service-token", + }); + expect(base44.asServiceRole.aiGateway.connection()).toEqual({ + baseURL, + token: "service-token", + }); + }); + }); +});