From 57188ed78b44532586ddd83678bc276e1a221cbe Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Thu, 25 Jun 2026 18:19:32 -0700 Subject: [PATCH] chore: align config agent proxy defaults --- .../ai-proxy/[[...path]]/route.ts | 4 +- apps/backend/src/lib/ai/models.ts | 3 +- apps/backend/src/lib/ai/proxy-url.ts | 1 + apps/backend/src/lib/config/repo-agent.ts | 4 +- .../shared-backend/src/config-agent.test.ts | 66 +++++++++++++++++++ packages/shared-backend/src/config-agent.ts | 2 +- 6 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 apps/backend/src/lib/ai/proxy-url.ts create mode 100644 packages/shared-backend/src/config-agent.test.ts diff --git a/apps/backend/src/app/api/latest/integrations/ai-proxy/[[...path]]/route.ts b/apps/backend/src/app/api/latest/integrations/ai-proxy/[[...path]]/route.ts index 42d534a24..778b473db 100644 --- a/apps/backend/src/app/api/latest/integrations/ai-proxy/[[...path]]/route.ts +++ b/apps/backend/src/app/api/latest/integrations/ai-proxy/[[...path]]/route.ts @@ -1,4 +1,5 @@ import { ALLOWED_MODEL_IDS } from "@/lib/ai/models"; +import { PRODUCTION_AI_PROXY_BASE_URL } from "@/lib/ai/proxy-url"; import { preprocessProxyBody } from "@/private"; import { handleApiRequest } from "@/route-handlers/smart-route-handler"; import { getEnvVariable } from "@hexclave/shared/dist/utils/env"; @@ -6,7 +7,6 @@ import { StatusError } from "@hexclave/shared/dist/utils/errors"; import { NextRequest } from "next/server"; const OPENROUTER_BASE_URL = "https://openrouter.ai/api"; -const PRODUCTION_PROXY_BASE_URL = "https://api.hexclave.com/api/latest/integrations/ai-proxy"; const OPENROUTER_DEFAULT_MODEL = "anthropic/claude-sonnet-4.6"; function sanitizeBody(raw: ArrayBuffer): Uint8Array { @@ -49,7 +49,7 @@ async function proxyToOpenRouter(req: NextRequest, options: { params: Promise<{ : undefined; if (apiKey === "FORWARD_TO_PRODUCTION") { - const targetUrl = `${PRODUCTION_PROXY_BASE_URL}/${subpath}${req.nextUrl.search}`; + const targetUrl = `${PRODUCTION_AI_PROXY_BASE_URL}/${subpath}${req.nextUrl.search}`; const headers: Record = {}; if (contentType) { headers["Content-Type"] = contentType; diff --git a/apps/backend/src/lib/ai/models.ts b/apps/backend/src/lib/ai/models.ts index 99fef04c3..63acc17da 100644 --- a/apps/backend/src/lib/ai/models.ts +++ b/apps/backend/src/lib/ai/models.ts @@ -2,6 +2,7 @@ import { isLocalEmulatorEnabled } from "@/lib/local-emulator"; import { getNodeEnvironment } from "@hexclave/shared/dist/utils/env"; import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; +import { PRODUCTION_AI_PROXY_BASE_URL } from "./proxy-url"; export const MODEL_QUALITIES = ["dumb", "smart", "smartest"] as const; export const MODEL_SPEEDS = ["slow", "fast"] as const; @@ -62,7 +63,7 @@ export const ALLOWED_MODEL_IDS: ReadonlySet = new Set([ export function createOpenRouterProvider() { const baseURL = (getNodeEnvironment() === "development" || isLocalEmulatorEnabled()) ? "http://localhost:8102/api/latest/integrations/ai-proxy/v1" - : "https://api.hexclave.com/api/latest/integrations/ai-proxy/v1"; + : `${PRODUCTION_AI_PROXY_BASE_URL}/v1`; return createOpenRouter({ apiKey: "forwarded", baseURL, diff --git a/apps/backend/src/lib/ai/proxy-url.ts b/apps/backend/src/lib/ai/proxy-url.ts new file mode 100644 index 000000000..e3c4da6ee --- /dev/null +++ b/apps/backend/src/lib/ai/proxy-url.ts @@ -0,0 +1 @@ +export const PRODUCTION_AI_PROXY_BASE_URL = "https://api.hexclave.com/api/latest/integrations/ai-proxy"; diff --git a/apps/backend/src/lib/config/repo-agent.ts b/apps/backend/src/lib/config/repo-agent.ts index a8aaff0b3..ca0e6183b 100644 --- a/apps/backend/src/lib/config/repo-agent.ts +++ b/apps/backend/src/lib/config/repo-agent.ts @@ -42,13 +42,13 @@ import { getEnvVariable } from "@hexclave/shared/dist/utils/env"; import { captureError } from "@hexclave/shared/dist/utils/errors"; import { Sandbox } from "@vercel/sandbox"; +import { PRODUCTION_AI_PROXY_BASE_URL } from "../ai/proxy-url"; const AGENT_SDK_VERSION = "0.2.73"; const BASE = "/vercel/sandbox"; const REPO_DIR = `${BASE}/repo`; const TOOLS_DIR = BASE; // agent SDK + runner live here, separate from the repo const DEFAULT_AGENT_MODEL = "anthropic/claude-haiku-4.5"; -const DEFAULT_PROXY_URL = "https://api.hexclave.com/api/latest/integrations/ai-proxy"; const SANDBOX_TIMEOUT_MS = 900_000; const REVIEW_SANDBOX_KEEPALIVE_MS = 5 * 60_000; const GIT_BOT_NAME = "Hexclave Config Bot"; @@ -330,7 +330,7 @@ async function runAgent(sandbox: Sandbox, prompt: string, onProgress?: AgentProg const agentInput = { prompt, model: getEnvVariable("STACK_CONFIG_AGENT_MODEL", DEFAULT_AGENT_MODEL), - baseUrl: getEnvVariable("STACK_CLAUDE_PROXY_URL", DEFAULT_PROXY_URL), + baseUrl: getEnvVariable("STACK_CLAUDE_PROXY_URL", PRODUCTION_AI_PROXY_BASE_URL), apiKey: "stack-auth-proxy", }; // Write runner.mjs fresh each run (not baked into the base snapshot) so changes diff --git a/packages/shared-backend/src/config-agent.test.ts b/packages/shared-backend/src/config-agent.test.ts new file mode 100644 index 000000000..8457145e9 --- /dev/null +++ b/packages/shared-backend/src/config-agent.test.ts @@ -0,0 +1,66 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const queryMock = vi.hoisted(() => vi.fn(async function* (_input: unknown) { + yield { type: "result", result: "done" }; +})); + +vi.mock("@anthropic-ai/claude-agent-sdk", () => ({ + query: queryMock, +})); + +let originalProxyUrl: string | undefined; + +beforeEach(() => { + originalProxyUrl = process.env.STACK_CLAUDE_PROXY_URL; + delete process.env.STACK_CLAUDE_PROXY_URL; + vi.resetModules(); + queryMock.mockClear(); +}); + +afterEach(() => { + if (originalProxyUrl === undefined) { + delete process.env.STACK_CLAUDE_PROXY_URL; + } else { + process.env.STACK_CLAUDE_PROXY_URL = originalProxyUrl; + } +}); + +describe("runHeadlessClaudeAgent", () => { + it("defaults to the latest AI proxy endpoint", async () => { + const { runHeadlessClaudeAgent } = await import("./config-agent"); + + await runHeadlessClaudeAgent({ + prompt: "update the config", + cwd: process.cwd(), + allowedTools: [], + }); + + expect(queryMock).toHaveBeenCalledWith(expect.objectContaining({ + options: expect.objectContaining({ + env: expect.objectContaining({ + ANTHROPIC_BASE_URL: "https://api.hexclave.com/api/latest/integrations/ai-proxy", + }), + }), + })); + }); + + it("allows the agent proxy endpoint to be overridden", async () => { + process.env.STACK_CLAUDE_PROXY_URL = "https://example.com/agent-proxy"; + const { runHeadlessClaudeAgent } = await import("./config-agent"); + + await runHeadlessClaudeAgent({ + prompt: "update the config", + cwd: process.cwd(), + allowedTools: [], + }); + + expect(queryMock).toHaveBeenCalledWith(expect.objectContaining({ + options: expect.objectContaining({ + env: expect.objectContaining({ + ANTHROPIC_BASE_URL: "https://example.com/agent-proxy", + }), + }), + })); + }); +}); + diff --git a/packages/shared-backend/src/config-agent.ts b/packages/shared-backend/src/config-agent.ts index 22e6436e3..0b9f3625b 100644 --- a/packages/shared-backend/src/config-agent.ts +++ b/packages/shared-backend/src/config-agent.ts @@ -1,7 +1,7 @@ import { query } from "@anthropic-ai/claude-agent-sdk"; import path from "path"; -const DEFAULT_PROXY_URL = "https://api.hexclave.com/api/v1/integrations/ai-proxy"; +const DEFAULT_PROXY_URL = "https://api.hexclave.com/api/latest/integrations/ai-proxy"; const ANTHROPIC_PROXY_BASE_URL: string = process.env.STACK_CLAUDE_PROXY_URL ?? DEFAULT_PROXY_URL; export type ClaudeAgentToolName = "Read" | "Write" | "Edit" | "MultiEdit" | "NotebookEdit" | "Bash" | "Glob" | "Grep";