From d7f7e2b48949b3595f80a8f3b15d1d32cce20355 Mon Sep 17 00:00:00 2001 From: yardend Date: Thu, 9 Jul 2026 15:04:35 +0300 Subject: [PATCH 1/3] fix(ai-gateway): resolve gateway by app id in the path connection() built the domain-routed gateway URL (${serverUrl}/api/ai/openai/v1), which requires the request Host to resolve the app. In the browser the SDK targets the shared host, so a backend function's gateway call lands on a host that resolves to no app and 404s ("App not found for this domain"). Build the app_id-routed URL instead (${serverUrl}/api/apps/${appId}/ai/openai/v1), matching how every other runtime module (entities, functions, connectors, analytics, ...) places appId in the path. The gateway then resolves the app from the URL regardless of which host the request came in on. Pairs with the backend app_id mount in base44-dev/apper#15443. Co-Authored-By: Claude Opus 4.8 --- src/client.ts | 4 ++-- src/modules/ai-gateway.ts | 3 ++- src/modules/ai-gateway.types.ts | 2 ++ tests/unit/ai-gateway.test.ts | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client.ts b/src/client.ts index 5b46e80..8aa63f3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -191,7 +191,7 @@ export function createClient(config: CreateClientConfig): Base44Client { serverUrl, token, }), - aiGateway: createAiGatewayModule({ serverUrl, token }), + aiGateway: createAiGatewayModule({ serverUrl, token, appId }), appLogs: createAppLogsModule(axiosClient, appId), users: createUsersModule(axiosClient, appId), analytics: createAnalyticsModule({ @@ -235,7 +235,7 @@ export function createClient(config: CreateClientConfig): Base44Client { serverUrl, token, }), - aiGateway: createAiGatewayModule({ serverUrl, token: serviceToken }), + aiGateway: createAiGatewayModule({ serverUrl, token: serviceToken, appId }), appLogs: createAppLogsModule(serviceRoleAxiosClient, appId), cleanup: () => { if (socket) { diff --git a/src/modules/ai-gateway.ts b/src/modules/ai-gateway.ts index 97a43bb..3093960 100644 --- a/src/modules/ai-gateway.ts +++ b/src/modules/ai-gateway.ts @@ -8,9 +8,10 @@ import { export function createAiGatewayModule({ serverUrl, token, + appId, }: AiGatewayModuleConfig): AiGatewayModule { const connection = (): AiGatewayConnection => ({ - baseURL: `${serverUrl}/api/ai/openai/v1`, + baseURL: `${serverUrl}/api/apps/${appId}/ai/openai/v1`, token: token ?? getAccessToken() ?? "", }); diff --git a/src/modules/ai-gateway.types.ts b/src/modules/ai-gateway.types.ts index 57aec70..4ff6178 100644 --- a/src/modules/ai-gateway.types.ts +++ b/src/modules/ai-gateway.types.ts @@ -21,6 +21,8 @@ export interface AiGatewayModuleConfig { serverUrl?: string; /** Authentication token */ token?: string; + /** Application ID */ + appId: string; } /** diff --git a/tests/unit/ai-gateway.test.ts b/tests/unit/ai-gateway.test.ts index a016815..3d36bc9 100644 --- a/tests/unit/ai-gateway.test.ts +++ b/tests/unit/ai-gateway.test.ts @@ -4,7 +4,7 @@ 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`; + const baseURL = `${serverUrl}/api/apps/${appId}/ai/openai/v1`; describe("connection", () => { test("should return the OpenAI-compatible gateway baseURL", () => { From ee4d816df692b55d7194d935b5ebe3ee5928818d Mon Sep 17 00:00:00 2001 From: yardend Date: Thu, 9 Jul 2026 15:18:52 +0300 Subject: [PATCH 2/3] docs(ai-gateway): one generic connection() example, drop named third-party SDKs Replace the Mastra and OpenAI-SDK examples with a single minimal example showing the connection() -> { baseURL, token } handoff against the OpenAI-compatible Chat Completions endpoint, and drop specific library names from the module prose. Keeps the docs client-agnostic ("any OpenAI-compatible client") instead of pinning third-party SDKs/versions. Co-Authored-By: Claude Opus 4.8 --- src/modules/ai-gateway.types.ts | 60 +++++++++------------------------ 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/src/modules/ai-gateway.types.ts b/src/modules/ai-gateway.types.ts index 4ff6178..ace00bf 100644 --- a/src/modules/ai-gateway.types.ts +++ b/src/modules/ai-gateway.types.ts @@ -1,9 +1,8 @@ /** * 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. + * Contains the base URL and bearer token to use with any OpenAI-compatible + * client pointed at the Base44 AI Gateway. */ export interface AiGatewayConnection { /** Base URL of the gateway's OpenAI-compatible endpoint. */ @@ -30,7 +29,7 @@ export interface AiGatewayModuleConfig { * * 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 + * - Build custom AI agents or call models directly from your backend code * - Uses your app's models, billing, and credit quota, no API key to manage * * Available in user authentication mode (`base44.aiGateway`) and with the @@ -50,54 +49,25 @@ export interface AiGatewayModule { * * @example * ```typescript - * // Build an AI agent with Mastra on top of the gateway, inside a backend function + * // Inside a backend function: hand the connection to any OpenAI-compatible client * 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), - * }), - * }, + * // Point any OpenAI-compatible client at `baseURL` with `apiKey: token`. + * // Shown here with a raw request to the Chat Completions endpoint: + * const res = await fetch(`${baseURL}/chat/completions`, { + * method: 'POST', + * headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + * body: JSON.stringify({ + * model: 'claude_sonnet_4_6', + * messages: [{ role: 'user', content: 'Hello!' }], + * }), * }); - * - * 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 }); + * const data = await res.json(); + * return Response.json({ text: data.choices[0].message.content }); * }); * ``` */ From 322205d626982a2ee2d50c2382d230384bddb3a2 Mon Sep 17 00:00:00 2001 From: yardend Date: Thu, 9 Jul 2026 17:08:59 +0300 Subject: [PATCH 3/3] docs(ai-gateway): show a real code-agent example (tool loop), not a raw fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the bare fetch example with a compact ToolLoopAgent — a background action triggered on a record, with tools that carry input schemas and a finish tool — matching the SDK's house example style (no Deno/npm: scaffolding). Keeps the "point any OpenAI-compatible client at baseURL/token" note. Co-Authored-By: Claude Opus 4.8 --- src/modules/ai-gateway.types.ts | 47 ++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/modules/ai-gateway.types.ts b/src/modules/ai-gateway.types.ts index ace00bf..3b4857e 100644 --- a/src/modules/ai-gateway.types.ts +++ b/src/modules/ai-gateway.types.ts @@ -49,26 +49,41 @@ export interface AiGatewayModule { * * @example * ```typescript - * // Inside a backend function: hand the connection to any OpenAI-compatible client - * import { createClientFromRequest } from 'npm:@base44/sdk'; + * import { ToolLoopAgent, tool, stepCountIs, hasToolCall } from "ai"; + * import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; + * import { z } from "zod"; * - * Deno.serve(async (req) => { - * const base44 = createClientFromRequest(req); - * const { baseURL, token } = base44.aiGateway.connection(); + * const request = await base44.entities.ReturnRequest.get(returnId); + * const { baseURL, token } = base44.aiGateway.connection(); + * // Point any OpenAI-compatible client at `baseURL` with `apiKey: token`. + * const models = createOpenAICompatible({ name: "base44", baseURL, apiKey: token }); * - * // Point any OpenAI-compatible client at `baseURL` with `apiKey: token`. - * // Shown here with a raw request to the Chat Completions endpoint: - * const res = await fetch(`${baseURL}/chat/completions`, { - * method: 'POST', - * headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, - * body: JSON.stringify({ - * model: 'claude_sonnet_4_6', - * messages: [{ role: 'user', content: 'Hello!' }], + * const agent = new ToolLoopAgent({ + * model: models("automatic"), + * instructions: + * "Decide whether this return looks fine or needs the owner's attention. " + + * "Check the customer's past orders, then submit your verdict.", + * tools: { + * searchOrders: tool({ + * description: "This customer's past orders, optionally filtered by status", + * inputSchema: z.object({ status: z.string().optional() }), + * execute: ({ status }) => { + * const query = { customer_email: request.customer_email }; + * if (status) query.status = status; + * return base44.entities.Order.filter(query, "-created_date", 50); + * }, * }), - * }); - * const data = await res.json(); - * return Response.json({ text: data.choices[0].message.content }); + * submitVerdict: tool({ + * description: "Record the final verdict", + * inputSchema: z.object({ decision: z.enum(["approved", "flagged"]), reason: z.string() }), + * execute: ({ decision, reason }) => + * base44.entities.ReturnRequest.update(returnId, { status: decision, review_note: reason }), + * }), + * }, + * stopWhen: [stepCountIs(8), hasToolCall("submitVerdict")], * }); + * + * await agent.generate({ prompt: `Review this return request: ${JSON.stringify(request)}` }); * ``` */ connection(): AiGatewayConnection;