feat(agent): route LLM traffic through wizard.amplitude.com proxy#671
feat(agent): route LLM traffic through wizard.amplitude.com proxy#671kelsonpw wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Skills URL inadvertently redirected to wrong host
- Replaced getLlmGatewayUrlFromHost(host) with getWizardProxyBase(cloudRegion) so skillsBaseUrl resolves to the data API host (core.amplitude.com/wizard) instead of the LLM proxy (wizard.amplitude.com/web-api/wizard).
Or push these changes by commenting:
@cursor push 89cd28f129
Preview (89cd28f129)
diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts
--- a/src/lib/agent-runner.ts
+++ b/src/lib/agent-runner.ts
@@ -27,7 +27,8 @@
buildWizardMetadata,
} from './agent-interface';
import { runAgentDispatch } from './agent/run-agent-dispatch.js';
-import { getLlmGatewayUrlFromHost, getMcpUrlFromZone } from '../utils/urls';
+import { getMcpUrlFromZone } from '../utils/urls';
+import { getWizardProxyBase } from './api.js';
import { DEFAULT_AMPLITUDE_ZONE, OUTBOUND_URLS } from './constants.js';
import { resolveZone } from './zone-resolution.js';
import { getVersionCheckInfo, getVersionWarning } from './version-check';
@@ -1044,11 +1045,12 @@
});
}
- // Skills URL: derived from the same host as the LLM proxy.
+ // Skills URL: derived from the data API host (same as /v1/projects,
+ // /v1/planned-events) — NOT the LLM proxy, which only serves /v1/messages.
// Always tries remote first; falls back to bundled if fetch fails.
// Override with SKILLS_URL env var for testing.
const skillsBaseUrl =
- process.env.SKILLS_URL || getLlmGatewayUrlFromHost(host) + '/skills';
+ process.env.SKILLS_URL || getWizardProxyBase(cloudRegion) + '/skills';
// The previous restore-on-outro hook was paired with the destructive
// `backupAndFixClaudeSettings` flow. The new scoping (writing our envYou can send follow-ups to the cloud agent here.
7c0bc3c to
7edb1a8
Compare
|
Could not push Autofix changes. The PR branch may have changed since the Autofix ran, or the Autofix commit may no longer exist. |
Bugbot caught that after #671 routed getLlmGatewayUrlFromHost to the new wizard.amplitude.com LLM proxy, the skillsBaseUrl derivation inherited that host - but the LLM proxy only serves /v1/messages, not /skills. fetchSkillMenu would always 404 on the new host. Switch the derivation to getWizardProxyBase(cloudRegion) so skills stay on core.amplitude.com/wizard (the data API host) regardless of the LLM gateway move. Bundled-skill fallback path is unchanged. Test: skillsBaseUrl resolves to core.amplitude.com/wizard/skills, not wizard.amplitude.com/web-api/wizard/skills. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kelsonpw
left a comment
There was a problem hiding this comment.
Boundary audit — only /v1/messages should move
Verified ✓ (production callsites of the URL resolvers, post-PR):
getLlmGatewayUrlFromHost—src/lib/agent-interface.ts:1224→ setsANTHROPIC_BASE_URLfor the Claude Agent SDK (POST /v1/messages). Correct.getLlmGatewayUrlFromHost—src/lib/console-query.ts:49→ setsbaseUrlfor the in-process AI SDK call (POST /v1/messages). Correct.getWizardProxyBase—src/lib/api.ts:415→createAmplitudeAppPOSTs to${base}/projectson the data API. Correct (region-pinned,core(.eu).amplitude.com).getWizardProxyBase—src/lib/planned-events.ts:105→commitPlannedEventsPOSTs to${base}/v1/planned-events. Correct.getWizardProxyBase—src/lib/agent-runner.ts:1062→skillsBaseUrl = getWizardProxyBase(cloudRegion) + '/skills'. Correct — this is the5e2feb5cfix; it stuck. Pinned bycreate-project.test.ts:160-194for bothusandeu.getMcpHostFromRegion/getMcpUrlFromZone—src/utils/urls.ts:118-154→mcp.amplitude.com/mcp.eu.amplitude.comfor the Amplitude MCP server. Untouched by this PR. Correct (separate host).fetchAmplitudeUser—src/lib/api.ts:186-225→ usesdataApiUrlfromAMPLITUDE_ZONE_SETTINGS(data-api.amplitude.com/graphql). Untouched. Correct.- OAuth —
src/utils/oauth.ts:447,src/lib/constants.ts:104-127→auth.amplitude.com/auth.eu.amplitude.com. Untouched. Correct. - App API GraphQL —
src/lib/constants.ts:112,124→core(.eu).amplitude.com/t/graphql/org/. Untouched. Correct. direct-signup, telemetry, Sentry, statuspage, npm registry — all unrelated hosts, untouched. Correct.
Env-var hygiene:
WIZARD_LLM_PROXY_URLis consulted only ingetLlmGatewayUrlFromHost(urls.ts:101).WIZARD_PROXY_BASE_URLis consulted only ingetWizardProxyBase(api.ts:386).- No cross-pollination — pinned by the regression test in
create-project.test.ts:131-151("does NOT consultWIZARD_LLM_PROXY_URL").
Only one production callsite resolves to wizard.amplitude.com (the default LLM proxy in urls.ts:67); every other reference is a doc comment.
Potential issues — none rising to "bug". A couple of nits:
-
Stale test inputs (cosmetic) —
src/lib/agent/__tests__/ai-sdk-gateway-probe.test.ts:38,src/lib/__tests__/console-query.test.ts:206,228, andsrc/lib/__tests__/register-gateway-fetch-sanitize.test.ts:11,22still feedhttps://core.amplitude.com/wizardas the inputANTHROPIC_BASE_URLfor URL-shape assertions. The assertions are about path normalization (/v1suffix,/v1/messagesdetection), not about which host the wizard targets in production, so these aren't wrong — but they look stale next towizard-ai-sdk-anthropic.test.ts:148, which now correctly assertshttps://wizard.amplitude.com/web-api/wizard/v1. Worth a one-line follow-up to update the inputs towizard.amplitude.com/web-api/wizardfor consistency. -
getLlmGatewayUrlFromHosthost arg is dead — the parameter is now_hostand ignored. Two production callers (agent-interface.ts:1224,console-query.ts:49) still compute and pass a host. Not a bug; preserved on purpose so the signature stays compatible. Could be tightened to a no-arg function in a follow-up if you want to make the contract obvious to future callers. -
Bearer-refresh comment —
src/lib/llm-gateway-bearer-refresh.ts:11andsrc/lib/agent-interface.ts:2026,2474still describe the gateway ashttps://core.amplitude.com/wizard/v1/messagesin comments. Behavior is correct (the refresh logic doesn't care about the URL); just stale prose.
Verification:
tsc -p tsconfig.json— clean.vitest run --pool=forks --maxWorkers=1 src/lib/__tests__/ src/lib/agent/__tests__/ src/utils/__tests__/— 2132/2132 pass.
Conclusion: LGTM on boundary scope. Only /v1/messages traffic moves to wizard.amplitude.com; every other Amplitude data/auth/MCP surface stays where it was. The decoupling between WIZARD_LLM_PROXY_URL and WIZARD_PROXY_BASE_URL is clean and pinned by tests. The three nits above are doc/test polish, not code fixes — safe to merge as-is or fold into a follow-up.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Exported constant unused; duplicate local copy in tests
- Replaced the duplicated local
PROD_LLM_PROXYstring literal in the test file with an import ofWIZARD_LLM_PROXY_URL_DEFAULTfromurls.ts, ensuring tests always validate against the canonical source-of-truth constant.
- Replaced the duplicated local
Or push these changes by commenting:
@cursor push 9ade53c1b5
Preview (9ade53c1b5)
diff --git a/src/utils/__tests__/urls.test.ts b/src/utils/__tests__/urls.test.ts
--- a/src/utils/__tests__/urls.test.ts
+++ b/src/utils/__tests__/urls.test.ts
@@ -5,6 +5,7 @@
getLlmGatewayUrlFromHost,
getMcpHostFromRegion,
getMcpUrlFromZone,
+ WIZARD_LLM_PROXY_URL_DEFAULT,
} from '../urls.js';
// ── getCloudUrlFromRegion ─────────────────────────────────────────────────────
@@ -96,7 +97,7 @@
// /v1/messages). It accepts the wizard's existing OAuth bearer in
// `Authorization: Bearer ...` (or `x-api-key`). Region selection is a
// server-side concern; the client URL no longer varies by region.
- const PROD_LLM_PROXY = 'https://wizard.amplitude.com/web-api/wizard';
+ const PROD_LLM_PROXY = WIZARD_LLM_PROXY_URL_DEFAULT;
it('returns the override URL when WIZARD_LLM_PROXY_URL is set', () => {
process.env.WIZARD_LLM_PROXY_URL = 'http://my-custom-proxy:9999';You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit baf42e5. Configure here.
Bugbot caught that after #671 routed getLlmGatewayUrlFromHost to the new wizard.amplitude.com LLM proxy, the skillsBaseUrl derivation inherited that host - but the LLM proxy only serves /v1/messages, not /skills. fetchSkillMenu would always 404 on the new host. Switch the derivation to getWizardProxyBase(cloudRegion) so skills stay on core.amplitude.com/wizard (the data API host) regardless of the LLM gateway move. Bundled-skill fallback path is unchanged. Test: skillsBaseUrl resolves to core.amplitude.com/wizard/skills, not wizard.amplitude.com/web-api/wizard/skills. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71a7175 to
7875201
Compare
Bugbot caught that after #671 routed getLlmGatewayUrlFromHost to the new wizard.amplitude.com LLM proxy, the skillsBaseUrl derivation inherited that host - but the LLM proxy only serves /v1/messages, not /skills. fetchSkillMenu would always 404 on the new host. Switch the derivation to getWizardProxyBase(cloudRegion) so skills stay on core.amplitude.com/wizard (the data API host) regardless of the LLM gateway move. Bundled-skill fallback path is unchanged. Test: skillsBaseUrl resolves to core.amplitude.com/wizard/skills, not wizard.amplitude.com/web-api/wizard/skills. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7c71013 to
92f0493
Compare
Bugbot caught that after #671 routed getLlmGatewayUrlFromHost to the new wizard.amplitude.com LLM proxy, the skillsBaseUrl derivation inherited that host - but the LLM proxy only serves /v1/messages, not /skills. fetchSkillMenu would always 404 on the new host. Switch the derivation to getWizardProxyBase(cloudRegion) so skills stay on core.amplitude.com/wizard (the data API host) regardless of the LLM gateway move. Bundled-skill fallback path is unchanged. Test: skillsBaseUrl resolves to core.amplitude.com/wizard/skills, not wizard.amplitude.com/web-api/wizard/skills. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f3ce5cf to
43913bf
Compare
Switch the wizard's LLM gateway URL from the regional core.amplitude.com endpoints (US: core.amplitude.com/wizard, EU: core.eu.amplitude.com/wizard) to a single global proxy at https://wizard.amplitude.com/web-api/wizard for ALL users in ALL regions. The new proxy is the standalone wizard-api service (Next.js + Vertex): - github.com/amplitude/wizard-api - basePath: /web-api/wizard (next.config.ts) - POST /v1/messages → Anthropic-compatible Vertex proxy (src/lib/vertex.ts) - Auth: accepts Authorization: Bearer <token> OR x-api-key — the wizard's existing OAuth bearer flow satisfies this (vertex.ts:219-241). The proxy itself selects Vertex region server-side, so the client URL is the same for US and EU users. Routing is decoupled from the Amplitude data API surface (project creation, planned events) which still uses core.amplitude.com/wizard via getWizardProxyBase in src/lib/api.ts. Changes: - getLlmGatewayUrlFromHost: returns wizard.amplitude.com/web-api/wizard for all hosts; WIZARD_LLM_PROXY_URL still overrides for dev / staging - WIZARD_ZONE no longer routes the LLM client (proxy is global) - getWizardProxyBase decoupled from getLlmGatewayUrlFromHost; introduces WIZARD_PROXY_BASE_URL as the dedicated escape hatch for the data API - getHostFromRegion import dropped from src/lib/api.ts (no longer used) Tests added: - urls.test.ts: prod URL is wizard.amplitude.com for any region - wizard-ai-sdk-anthropic.test.ts: AI SDK provider posts to wizard.amplitude.com/web-api/wizard/v1/messages and forwards the bearer in `authToken` (which @ai-sdk/anthropic sends as `Authorization: Bearer <token>` on the wire) - create-project.test.ts: WIZARD_LLM_PROXY_URL no longer leaks into getWizardProxyBase - planned-events.test.ts: switch fixture to WIZARD_PROXY_BASE_URL
Bugbot caught that after #671 routed getLlmGatewayUrlFromHost to the new wizard.amplitude.com LLM proxy, the skillsBaseUrl derivation inherited that host - but the LLM proxy only serves /v1/messages, not /skills. fetchSkillMenu would always 404 on the new host. Switch the derivation to getWizardProxyBase(cloudRegion) so skills stay on core.amplitude.com/wizard (the data API host) regardless of the LLM gateway move. Bundled-skill fallback path is unchanged. Test: skillsBaseUrl resolves to core.amplitude.com/wizard/skills, not wizard.amplitude.com/web-api/wizard/skills. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43913bf to
2b9ba0a
Compare
|
Permanently blocked from merge per user directive — wizard.amplitude.com proxy has confirmed auth bypass (accepts any bearer). Closing as part of pat-leave handoff cleanup; branch preserved if proxy team fixes the upstream. |


Summary
Switch the wizard's LLM gateway URL from the regional
core.amplitude.comendpoints (US:core.amplitude.com/wizard, EU:core.eu.amplitude.com/wizard) to a single global proxy athttps://wizard.amplitude.com/web-api/wizardfor ALL users in ALL regions.Path chosen: C — keep the existing OAuth bearer flow, only change the URL.
Why path C
Read the proxy at
amplitude/wizard-apifirst:next.config.ts->basePath: '/web-api/wizard'-> public URL ishttps://wizard.amplitude.com/web-api/wizard/v1/messages.src/app/v1/messages/route.ts-> POST handler delegating toproxyMessagesToVertex.src/lib/vertex.ts:219-241->extractClientCredentialaccepts EITHERAuthorization: Bearer <token>ORx-api-key. Anything non-empty passes (TODOat line 238 — credential validation against an auth service is intentionally deferred).src/lib/vertex.ts:39-58-> bothauthorizationandx-api-keyheaders are stripped before the upstream Vertex call; the proxy signs Vertex with its own GCP OAuth.So:
curlgot 401.Authorization: Bearer <amplitude-oauth-token>for/v1/messages. Keep it. Just change the URL.This is the smallest possible diff. We don't blindly strip auth (which would 401), and we don't switch credential schemes.
What changed
src/utils/urls.ts—getLlmGatewayUrlFromHostnow returnshttps://wizard.amplitude.com/web-api/wizardfor every region.WIZARD_LLM_PROXY_URLstill overrides for dev / staging.WIZARD_ZONEno longer affects LLM routing (proxy is global).src/lib/api.ts—getWizardProxyBasedecoupled from the LLM URL resolver. The data API surface (/v1/projects,/v1/planned-events) stays oncore.amplitude.com/wizard(region-pinned) and gets its own escape hatch:WIZARD_PROXY_BASE_URL.src/lib/agent/wizard-ai-sdk-anthropic.ts— comment updated to reflect new URL.urls.test.ts,wizard-ai-sdk-anthropic.test.ts(new integration block),create-project.test.ts,planned-events.test.ts.OAuth + token-refresh code (
src/utils/oauth.ts,src/utils/token-refresh.ts) is untouched — the Amplitude data API still needs them.Verifying the request shape
The wizard now sends:
Quick local sanity check (replace
<token>with a real Amplitude OAuth bearer; any non-empty string passes the proxy's current check):A bare
curlagainst the same URL with NO auth header returns 401 with{"type":"error","error":{"type":"authentication_error","message":"Missing credentials: provide x-api-key or Authorization: Bearer"}}— that's the proxy doing whatvertex.ts:230-241says it does.Test plan
node_modules/.bin/tsc -p tsconfig.json— cleannode_modules/.bin/vitest run --pool=forks --maxWorkers=1— all 3538 tests passpnpm lint— clean (one pre-existing warning inEventPlanFullScreen.test.tsx, unrelated)pnpm tryagainst a real Amplitude account in US AND EU regions, confirm the run completes withoutgatewayUrl404swizard.amplitude.comis the public hostname customers should hit (vsamplitude.com/web-api/wizard— they'd both serve the same Next.js app behind the samebasePath)Generated with Claude Code
Note
Medium Risk
Changes the wizard’s LLM gateway endpoint and URL-resolution logic, which is central to agent execution and could cause widespread run failures if misrouted or misconfigured; data-API paths are also decoupled and could regress region routing if incorrect.
Overview
Routes the wizard’s LLM transport (
getLlmGatewayUrlFromHost) to a single global endpointhttps://wizard.amplitude.com/web-api/wizard, ignoringWIZARD_ZONEand relying only on theWIZARD_LLM_PROXY_URLoverride for non-prod.Decouples Data API base resolution (
getWizardProxyBase) from the LLM proxy, keeping project creation and planned-events writes region-pinned tocore(.eu).amplitude.com/wizardand introducingWIZARD_PROXY_BASE_URLas the new override;agent-runnernow derivesskillsBaseUrlfrom this data-API base instead of the LLM gateway.Updates and expands tests to lock down the new URL contracts (including an integration-style check that the AI SDK normalizes to
…/web-api/wizard/v1/messages) and to prevent regressions whereWIZARD_LLM_PROXY_URLaccidentally rewires data-API or skills endpoints.Reviewed by Cursor Bugbot for commit f3ce5cf. Bugbot is set up for automated code reviews on this repo. Configure here.