fix(agent): call api v1 via loopback instead of public url
CI / checks (push) Has been cancelled

Tool self-fetches were hairpinning to fam.ginnoir.com and failing,
so list_calendars/create_event returned 500 and the model gave up.
This commit is contained in:
ginnoir
2026-07-09 00:38:33 -05:00
parent 60eb101015
commit 3e8fe2d06d
4 changed files with 123 additions and 16 deletions
+48 -9
View File
@@ -1,3 +1,5 @@
import logger from "@/lib/logger";
type ApiCallResult = {
status: number;
body: unknown;
@@ -5,8 +7,22 @@ type ApiCallResult = {
export type ToolExecutor = (name: string, argsJson: string) => Promise<string>;
/** Loopback base for in-process tool → /api/v1 calls. Avoids hairpinning to the public URL. */
export function resolveInternalApiBase(request: Request): string {
const configured = process.env.INTERNAL_API_BASE_URL?.trim();
if (configured) return configured.replace(/\/$/, "");
const port = process.env.PORT?.trim() || "3000";
const requestUrl = new URL(request.url);
if (requestUrl.hostname === "localhost" || requestUrl.hostname === "127.0.0.1") {
return requestUrl.origin;
}
return `http://127.0.0.1:${port}`;
}
export function createApiToolExecutor(request: Request): ToolExecutor {
const origin = new URL(request.url).origin;
const origin = resolveInternalApiBase(request);
return async (name: string, argsJson: string) => {
const args = parseArgs(argsJson);
@@ -522,14 +538,30 @@ async function callApi(
path: string,
body?: Record<string, unknown>,
): Promise<ApiCallResult> {
const response = await fetch(`${origin}${path}`, {
method,
headers: {
"Content-Type": "application/json",
cookie: request.headers.get("cookie") ?? "",
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const url = `${origin}${path}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
const cookie = request.headers.get("cookie");
if (cookie) headers.cookie = cookie;
const authorization = request.headers.get("authorization");
if (authorization) headers.authorization = authorization;
let response: Response;
try {
response = await fetch(url, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
} catch (err) {
const message = err instanceof Error ? err.message : "fetch failed";
logger.error(
{ msg: "agent.tool.fetch_failed", method, url, error: message },
"agent tool internal fetch failed",
);
return { status: 502, body: { error: `Internal API unreachable: ${message}`, url } };
}
const text = await response.text();
let parsed: unknown = null;
@@ -541,5 +573,12 @@ async function callApi(
}
}
if (response.status >= 400) {
logger.warn(
{ msg: "agent.tool.api_error", method, url, status: response.status, body: parsed },
"agent tool API error",
);
}
return { status: response.status, body: parsed };
}