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
5 changes: 5 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/client.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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. */
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
22 changes: 22 additions & 0 deletions src/modules/ai-gateway.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
105 changes: 105 additions & 0 deletions src/modules/ai-gateway.types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions src/modules/types.ts
Original file line number Diff line number Diff line change
@@ -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";
80 changes: 80 additions & 0 deletions tests/unit/ai-gateway.test.ts
Original file line number Diff line number Diff line change
@@ -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",
});
});
});
});
Loading