Skip to content

feat(agent): route LLM traffic through wizard.amplitude.com proxy#671

Closed
kelsonpw wants to merge 2 commits into
mainfrom
feat/wizard-amplitude-com-llm-proxy
Closed

feat(agent): route LLM traffic through wizard.amplitude.com proxy#671
kelsonpw wants to merge 2 commits into
mainfrom
feat/wizard-amplitude-com-llm-proxy

Conversation

@kelsonpw
Copy link
Copy Markdown
Member

@kelsonpw kelsonpw commented May 8, 2026

Manual verification still pending — DO NOT MERGE until run by author against US + EU regions. The "Manual smoke test" and "Confirm with wizard-api owners" checklist items below are unchecked because they require live runs the author has not yet completed. Reviewers please block merge until both boxes are checked by kelsonpw.

Summary

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.

Path chosen: C — keep the existing OAuth bearer flow, only change the URL.

Why path C

Read the proxy at amplitude/wizard-api first:

  • next.config.ts -> basePath: '/web-api/wizard' -> public URL is https://wizard.amplitude.com/web-api/wizard/v1/messages.
  • src/app/v1/messages/route.ts -> POST handler delegating to proxyMessagesToVertex.
  • src/lib/vertex.ts:219-241 -> extractClientCredential accepts EITHER Authorization: Bearer <token> OR x-api-key. Anything non-empty passes (TODO at line 238 — credential validation against an auth service is intentionally deferred).
  • src/lib/vertex.ts:39-58 -> both authorization and x-api-key headers are stripped before the upstream Vertex call; the proxy signs Vertex with its own GCP OAuth.

So:

  • The proxy IS effectively auth-free in practice (any non-empty bearer passes).
  • BUT the route returns 401 if the auth header is missing entirely — which is why the previous attempt's curl got 401.
  • The wizard already sends 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.tsgetLlmGatewayUrlFromHost now returns https://wizard.amplitude.com/web-api/wizard for every region. WIZARD_LLM_PROXY_URL still overrides for dev / staging. WIZARD_ZONE no longer affects LLM routing (proxy is global).
  • src/lib/api.tsgetWizardProxyBase decoupled from the LLM URL resolver. The data API surface (/v1/projects, /v1/planned-events) stays on core.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.
  • Tests: 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:

POST https://wizard.amplitude.com/web-api/wizard/v1/messages
Authorization: Bearer <amplitude-oauth-access-token>
Content-Type: application/json

{ "model": "claude-opus-4-7", "messages": [...], "stream": true }

Quick local sanity check (replace <token> with a real Amplitude OAuth bearer; any non-empty string passes the proxy's current check):

curl -i -X POST https://wizard.amplitude.com/web-api/wizard/v1/messages \
  -H 'Authorization: Bearer test-bearer' \
  -H 'Content-Type: application/json' \
  -d '{"model":"claude-haiku-4-5","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'

A bare curl against 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 what vertex.ts:230-241 says it does.

Test plan

  • node_modules/.bin/tsc -p tsconfig.json — clean
  • node_modules/.bin/vitest run --pool=forks --maxWorkers=1 — all 3538 tests pass
  • pnpm lint — clean (one pre-existing warning in EventPlanFullScreen.test.tsx, unrelated)
  • Manual smoke test (BLOCKER): run pnpm try against a real Amplitude account in US AND EU regions, confirm the run completes without gatewayUrl 404s
  • Confirm with wizard-api owners (BLOCKER) that wizard.amplitude.com is the public hostname customers should hit (vs amplitude.com/web-api/wizard — they'd both serve the same Next.js app behind the same basePath)

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 endpoint https://wizard.amplitude.com/web-api/wizard, ignoring WIZARD_ZONE and relying only on the WIZARD_LLM_PROXY_URL override for non-prod.

Decouples Data API base resolution (getWizardProxyBase) from the LLM proxy, keeping project creation and planned-events writes region-pinned to core(.eu).amplitude.com/wizard and introducing WIZARD_PROXY_BASE_URL as the new override; agent-runner now derives skillsBaseUrl from 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 where WIZARD_LLM_PROXY_URL accidentally 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.

@kelsonpw kelsonpw requested a review from a team as a code owner May 8, 2026 22:13
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Create PR

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 env

You can send follow-ups to the cloud agent here.

Comment thread src/utils/urls.ts
@kelsonpw kelsonpw force-pushed the feat/wizard-amplitude-com-llm-proxy branch from 7c0bc3c to 7edb1a8 Compare May 8, 2026 22:30
@kelsonpw
Copy link
Copy Markdown
Member Author

kelsonpw commented May 8, 2026

@cursor push 89cd28f

@cursor
Copy link
Copy Markdown
Contributor

cursor Bot commented May 8, 2026

Could not push Autofix changes. The PR branch may have changed since the Autofix ran, or the Autofix commit may no longer exist.

kelsonpw added a commit that referenced this pull request May 8, 2026
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>
Copy link
Copy Markdown
Member Author

@kelsonpw kelsonpw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boundary audit — only /v1/messages should move

Verified ✓ (production callsites of the URL resolvers, post-PR):

  • getLlmGatewayUrlFromHostsrc/lib/agent-interface.ts:1224 → sets ANTHROPIC_BASE_URL for the Claude Agent SDK (POST /v1/messages). Correct.
  • getLlmGatewayUrlFromHostsrc/lib/console-query.ts:49 → sets baseUrl for the in-process AI SDK call (POST /v1/messages). Correct.
  • getWizardProxyBasesrc/lib/api.ts:415createAmplitudeApp POSTs to ${base}/projects on the data API. Correct (region-pinned, core(.eu).amplitude.com).
  • getWizardProxyBasesrc/lib/planned-events.ts:105commitPlannedEvents POSTs to ${base}/v1/planned-events. Correct.
  • getWizardProxyBasesrc/lib/agent-runner.ts:1062skillsBaseUrl = getWizardProxyBase(cloudRegion) + '/skills'. Correct — this is the 5e2feb5c fix; it stuck. Pinned by create-project.test.ts:160-194 for both us and eu.
  • getMcpHostFromRegion / getMcpUrlFromZonesrc/utils/urls.ts:118-154mcp.amplitude.com / mcp.eu.amplitude.com for the Amplitude MCP server. Untouched by this PR. Correct (separate host).
  • fetchAmplitudeUsersrc/lib/api.ts:186-225 → uses dataApiUrl from AMPLITUDE_ZONE_SETTINGS (data-api.amplitude.com/graphql). Untouched. Correct.
  • OAuth — src/utils/oauth.ts:447, src/lib/constants.ts:104-127auth.amplitude.com / auth.eu.amplitude.com. Untouched. Correct.
  • App API GraphQL — src/lib/constants.ts:112,124core(.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_URL is consulted only in getLlmGatewayUrlFromHost (urls.ts:101).
  • WIZARD_PROXY_BASE_URL is consulted only in getWizardProxyBase (api.ts:386).
  • No cross-pollination — pinned by the regression test in create-project.test.ts:131-151 ("does NOT consult WIZARD_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:

  1. Stale test inputs (cosmetic)src/lib/agent/__tests__/ai-sdk-gateway-probe.test.ts:38, src/lib/__tests__/console-query.test.ts:206,228, and src/lib/__tests__/register-gateway-fetch-sanitize.test.ts:11,22 still feed https://core.amplitude.com/wizard as the input ANTHROPIC_BASE_URL for URL-shape assertions. The assertions are about path normalization (/v1 suffix, /v1/messages detection), not about which host the wizard targets in production, so these aren't wrong — but they look stale next to wizard-ai-sdk-anthropic.test.ts:148, which now correctly asserts https://wizard.amplitude.com/web-api/wizard/v1. Worth a one-line follow-up to update the inputs to wizard.amplitude.com/web-api/wizard for consistency.

  2. getLlmGatewayUrlFromHost host arg is dead — the parameter is now _host and 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.

  3. Bearer-refresh commentsrc/lib/llm-gateway-bearer-refresh.ts:11 and src/lib/agent-interface.ts:2026,2474 still describe the gateway as https://core.amplitude.com/wizard/v1/messages in 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.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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_PROXY string literal in the test file with an import of WIZARD_LLM_PROXY_URL_DEFAULT from urls.ts, ensuring tests always validate against the canonical source-of-truth constant.

Create PR

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.

Comment thread src/utils/urls.ts
@kelsonpw
Copy link
Copy Markdown
Member Author

kelsonpw commented May 9, 2026

@cursor push 9ade53c

kelsonpw added a commit that referenced this pull request May 9, 2026
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 kelsonpw force-pushed the feat/wizard-amplitude-com-llm-proxy branch from 71a7175 to 7875201 Compare May 9, 2026 00:37
kelsonpw added a commit that referenced this pull request May 14, 2026
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 kelsonpw force-pushed the feat/wizard-amplitude-com-llm-proxy branch from 7c71013 to 92f0493 Compare May 14, 2026 19:23
@kelsonpw kelsonpw marked this pull request as draft May 14, 2026 21:44
@kelsonpw kelsonpw marked this pull request as ready for review May 14, 2026 21:45
@kelsonpw kelsonpw marked this pull request as draft May 14, 2026 21:45
kelsonpw added a commit that referenced this pull request May 14, 2026
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 kelsonpw force-pushed the feat/wizard-amplitude-com-llm-proxy branch from f3ce5cf to 43913bf Compare May 14, 2026 22:03
kelsonpw and others added 2 commits May 15, 2026 16:46
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>
@kelsonpw kelsonpw force-pushed the feat/wizard-amplitude-com-llm-proxy branch from 43913bf to 2b9ba0a Compare May 15, 2026 23:54
@kelsonpw kelsonpw removed the request for review from a team May 18, 2026 18:05
@kelsonpw
Copy link
Copy Markdown
Member Author

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.

@kelsonpw kelsonpw closed this May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant