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
1 change: 1 addition & 0 deletions scripts/mintlify-post-processing/types-to-expose.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"AgentNameRegistry",
"AgentsModule",
"AiGatewayConnection",
"AiGatewayConnectionOptions",
"AiGatewayModule",
"AnalyticsModule",
"AppLogsModule",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export type {
export type {
AiGatewayModule,
AiGatewayConnection,
AiGatewayConnectionOptions,
} from "./modules/ai-gateway.types.js";

export type { AppLogsModule } from "./modules/app-logs.types.js";
Expand Down
23 changes: 19 additions & 4 deletions src/modules/ai-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,32 @@ import {
AiGatewayModule,
AiGatewayModuleConfig,
AiGatewayConnection,
AiGatewayConnectionOptions,
} from "./ai-gateway.types.js";

const PROVIDER_PATHS = {
openai: "openai",
typesafe: "typesafe",
} as const;

export function createAiGatewayModule({
serverUrl,
token,
appId,
}: AiGatewayModuleConfig): AiGatewayModule {
const connection = (): AiGatewayConnection => ({
baseURL: `${serverUrl}/api/apps/${appId}/ai/openai/v1`,
token: token ?? getAccessToken() ?? "",
});
const connection = (
{ provider = "openai" }: AiGatewayConnectionOptions = {}
): AiGatewayConnection => {
if (!Object.prototype.hasOwnProperty.call(PROVIDER_PATHS, provider)) {
throw new Error(`Unsupported AI Gateway provider: ${provider}`);
}
const providerPath = PROVIDER_PATHS[provider];

return {
baseURL: `${serverUrl}/api/apps/${appId}/ai/${providerPath}/v1`,
token: token ?? getAccessToken() ?? "",
};
};

return {
connection,
Expand Down
20 changes: 16 additions & 4 deletions src/modules/ai-gateway.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Connection details for the Base44 AI Gateway.
*/
export interface AiGatewayConnection {
/** Base URL of the gateway's OpenAI-compatible Chat Completions endpoint. */
/** Base URL of the selected gateway provider. */
baseURL: string;
/**
* Bearer token that authenticates the request. Empty string when the caller is
Expand All @@ -11,6 +11,12 @@ export interface AiGatewayConnection {
token: string;
}

/** Options for selecting an AI Gateway provider. */
export interface AiGatewayConnectionOptions {
/** Gateway provider to connect to. Defaults to `openai`. */
provider?: "openai" | "typesafe";
}

/**
* Configuration for the AI Gateway module.
* @internal
Expand All @@ -36,13 +42,17 @@ export interface AiGatewayModuleConfig {
* against the provider directly, no separate account, API key, or billing
* setup with the underlying model provider required.
*
* By default, `connection()` uses the OpenAI-compatible provider. Pass
* `{ provider: "typesafe" }` to connect to TypeSafe for structured evaluations
* with `@ai-sdk/typesafe-ai`.
*
* Call `connection()` from a backend function rather than the browser. This
* keeps your instructions, tools, and business logic server-side, and lets
* you enforce your own auth, rate, and spend limits around the call. The
* `token` it returns is the caller's regular session token, the same one
* used for every other SDK call.
*
* ## Models
* ## OpenAI-compatible models
*
* You can use any of the [models available through `InvokeLLM`](/developers/references/sdk/docs/type-aliases/integrations#invokellm).
* Pass `'automatic'` to let Base44 choose one, or pin a specific model such
Expand Down Expand Up @@ -71,7 +81,9 @@ export interface AiGatewayModule {
/**
* Gets the connection details for the Base44 AI Gateway.
*
* Returns the `baseURL` and `token` to pass to any OpenAI-compatible client.
* Returns the `baseURL` and `token` to pass to the selected provider client.
*
* @param options - Provider selection. Omit it to use the OpenAI-compatible gateway.
*
* @returns The gateway {@linkcode AiGatewayConnection | connection} (`baseURL` and `token`).
*
Expand Down Expand Up @@ -137,5 +149,5 @@ export interface AiGatewayModule {
* await agent.generate({ prompt: `Review this return request: ${JSON.stringify(returnRequest)}` });
* ```
*/
connection(): AiGatewayConnection;
connection(options?: AiGatewayConnectionOptions): AiGatewayConnection;
}
14 changes: 14 additions & 0 deletions tests/types/ai-gateway.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {
AiGatewayConnectionOptions,
AiGatewayModule,
} from "../../src/index.js";

declare const aiGateway: AiGatewayModule;

const options: AiGatewayConnectionOptions = { provider: "typesafe" };
aiGateway.connection(options);
aiGateway.connection();
aiGateway.connection({ provider: "openai" });

// @ts-expect-error Only gateway providers exposed by the SDK are accepted.
aiGateway.connection({ provider: "unsupported" });
51 changes: 40 additions & 11 deletions tests/unit/ai-gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,79 @@ describe("AI Gateway Module", () => {
const appId = "test-app-id";
const serverUrl = "https://api.base44.com";
const baseURL = `${serverUrl}/api/apps/${appId}/ai/openai/v1`;
const typesafeBaseURL = `${serverUrl}/api/apps/${appId}/ai/typesafe/v1`;

describe("connection", () => {
test("should return the OpenAI-compatible gateway baseURL", () => {
test("should default to OpenAI and accept it explicitly", () => {
const base44 = createClient({ serverUrl, appId });
expect(base44.aiGateway.connection().baseURL).toBe(baseURL);
expect(
base44.aiGateway.connection({ provider: "openai" }).baseURL
).toBe(baseURL);
});

test("should return the TypeSafe gateway baseURL", () => {
const base44 = createClient({ serverUrl, appId });
expect(
base44.aiGateway.connection({ provider: "typesafe" }).baseURL
).toBe(typesafeBaseURL);
});

test("should reject unsupported providers", () => {
const base44 = createClient({ serverUrl, appId });
expect(() =>
base44.aiGateway.connection({ provider: "unsupported" } as never)
).toThrow("Unsupported AI Gateway provider: unsupported");
});

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", () => {
test("should use the user token for TypeSafe when authenticated", () => {
const base44 = createClient({ serverUrl, appId, token: "user-token" });
expect(base44.aiGateway.connection()).toEqual({
baseURL,
expect(base44.aiGateway.connection({ provider: "typesafe" })).toEqual({
baseURL: typesafeBaseURL,
token: "user-token",
});
});

test("should build from the Base44-Api-Url header in backend functions", () => {
test("should propagate the backend request host and both authentication modes", () => {
const backendServerUrl = "https://backend.example.com";
const backendTypesafeBaseURL = `${backendServerUrl}/api/apps/${appId}/ai/typesafe/v1`;
const request = new Request("https://functions.internal/run", {
headers: {
"Base44-App-Id": appId,
"Base44-Api-Url": serverUrl,
"Base44-Api-Url": backendServerUrl,
Authorization: "Bearer user-token",
"Base44-Service-Authorization": "Bearer service-token",
},
});
const base44 = createClientFromRequest(request);
expect(base44.aiGateway.connection()).toEqual({
baseURL,
expect(base44.aiGateway.connection({ provider: "typesafe" })).toEqual({
baseURL: backendTypesafeBaseURL,
token: "user-token",
});
expect(
base44.asServiceRole.aiGateway.connection({ provider: "typesafe" })
).toEqual({
baseURL: backendTypesafeBaseURL,
token: "service-token",
});
});

test("should use the service-role token via asServiceRole", () => {
test("should use the service-role token for TypeSafe via asServiceRole", () => {
const base44 = createClient({
serverUrl,
appId,
token: "user-token",
serviceToken: "service-token",
});
expect(base44.asServiceRole.aiGateway.connection()).toEqual({
baseURL,
expect(
base44.asServiceRole.aiGateway.connection({ provider: "typesafe" })
).toEqual({
baseURL: typesafeBaseURL,
token: "service-token",
});
});
Expand Down
Loading