Skip to content

Commit 1b3d8e9

Browse files
feat(mcp): route hosted AI through OpenRouter with gpt-5.6-luna@high (#1213)
## Summary Route hosted MCP AI through OpenRouter and default to `openai/gpt-5.6-luna` with `reasoningEffort=high`. ### Key Changes - Chat playground: hardcoded OpenAI `gpt-4o` → shared OpenRouter provider - Default OpenRouter model: `openai/gpt-5` → `openai/gpt-5.6-luna` - Default OpenRouter reasoning effort: `high` (override with `OPENROUTER_REASONING_EFFORT`) - Chat `maxOutputTokens`: `2000` → `16000` so reasoning does not starve text/tool output - Worker/env docs/tests accept `OPENROUTER_*` + `EMBEDDED_AGENT_PROVIDER` ## Why Prod is moving off a direct OpenAI key onto OpenRouter. Luna@high keeps the cheap GPT-5.6 tier while favoring query-translation quality over the lower latency of medium effort. We can bump effort or model via env if quality dips. ## Deploy notes After merge, hosted worker should have: - `OPENROUTER_API_KEY` - `EMBEDDED_AGENT_PROVIDER=openrouter` - optional `OPENROUTER_MODEL=openai/gpt-5.6-luna` (also the code default) - optional `OPENROUTER_REASONING_EFFORT=high` (also the code default; lower it without redeploy if latency is too high) - `OPENAI_API_KEY` can be removed once this is live ## How we'll measure - Sentry error rate on embedded agents / chat (`callEmbeddedAgent`, LLM provider failures, `NoObjectGeneratedError` rescues) - Tool failure rate for `search_events` / `search_issues` / `search_issue_events` (7d baseline ~11–12% on agent trio) - Existing alert `444138` (global tool failure rate, critical 18%) - Latency + OpenRouter spend vs prior OpenAI baseline ## Risks / follow-ups - Luna may underperform on harder NL→query cases vs gpt-5 / sol / terra; easy model override if needed - Chat now requires `OPENROUTER_API_KEY` - If both OpenAI and OpenRouter keys remain set without `EMBEDDED_AGENT_PROVIDER`, provider selection still errors by design - Did not run live OpenRouter evals in this PR ## Checks run - `pnpm exec vitest run packages/mcp-core/src/internal/agents/openrouter-provider.test.ts` - `pnpm run tsc` - `pnpm run lint` - `pnpm run test` <!-- junior-request-attribution:start --> Requested by **David Cramer** via Junior. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:slack%3AC08J1NSPU6S%3A1785362296.874629 --> -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1785362296.874629) <!-- junior-session-footer:end --> --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: David Cramer <david@sentry.io>
1 parent 533883d commit 1b3d8e9

20 files changed

Lines changed: 340 additions & 40 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ EMBEDDED_AGENT_PROVIDER= # Required when multiple provider keys are set: 'op
107107
OPENAI_API_KEY= # Required if using OpenAI
108108
ANTHROPIC_API_KEY= # Required if using Anthropic
109109
OPENROUTER_API_KEY= # Required if using OpenRouter
110-
OPENROUTER_MODEL= # Optional OpenRouter model, defaults to 'openai/gpt-5'
110+
OPENROUTER_MODEL= # Optional OpenRouter model, defaults to 'openai/gpt-5.6-luna'
111+
OPENROUTER_REASONING_EFFORT= # Optional OpenRouter reasoning effort, defaults to 'high'
111112

112113
# Optional overrides
113114
SENTRY_HOST= # For self-hosted deployments

‎docs/operations/embedded-agents.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ For OpenRouter:
7272

7373
```bash
7474
export OPENROUTER_API_KEY=sk-or-...
75-
# Optional; defaults to openai/gpt-5
76-
export OPENROUTER_MODEL=openai/gpt-5
75+
# Optional; defaults to openai/gpt-5.6-luna with reasoning effort high
76+
export OPENROUTER_MODEL=openai/gpt-5.6-luna
77+
export OPENROUTER_REASONING_EFFORT=high
7778
# Recommended, and required when multiple provider keys are set
7879
export EMBEDDED_AGENT_PROVIDER=openrouter
7980
```
@@ -219,8 +220,9 @@ export OPENAI_MODEL=gpt-4
219220
# Anthropic (default: claude-opus-4-5-20251101)
220221
export ANTHROPIC_MODEL=claude-sonnet-4-5-20250929
221222

222-
# OpenRouter (default: openai/gpt-5)
223+
# OpenRouter (default: openai/gpt-5.6-luna @ high)
223224
export OPENROUTER_MODEL=anthropic/claude-sonnet-4
225+
export OPENROUTER_REASONING_EFFORT=high
224226
```
225227

226228
### Verify Configuration

‎packages/mcp-cloudflare/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
},
4747
"dependencies": {
4848
"@ai-sdk/mcp": "catalog:",
49-
"@ai-sdk/openai": "catalog:",
5049
"@ai-sdk/react": "catalog:",
5150
"@cloudflare/workers-oauth-provider": "catalog:",
5251
"@modelcontextprotocol/sdk": "catalog:",

‎packages/mcp-cloudflare/src/server/lib/mcp-handler.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ function createTestEnv(): Env {
104104
SENTRY_CLIENT_ID: "test-client-id",
105105
SENTRY_CLIENT_SECRET: "test-client-secret",
106106
SENTRY_HOST: "sentry.io",
107-
OPENAI_API_KEY: "test-openai-key",
107+
OPENROUTER_API_KEY: "test-openrouter-key",
108+
OPENROUTER_MODEL: "openai/gpt-5.6-luna",
109+
EMBEDDED_AGENT_PROVIDER: "openrouter",
108110
OAUTH_KV: {} as KVNamespace,
109111
OAUTH_PROVIDER: {
110112
listUserGrants: vi.fn().mockResolvedValue({ items: [] }),
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
2+
import { syncOpenRouterEnvFromBindings } from "./chat";
3+
4+
describe("syncOpenRouterEnvFromBindings", () => {
5+
const originalApiKey = process.env.OPENROUTER_API_KEY;
6+
const originalModel = process.env.OPENROUTER_MODEL;
7+
const originalEffort = process.env.OPENROUTER_REASONING_EFFORT;
8+
9+
beforeEach(() => {
10+
delete process.env.OPENROUTER_API_KEY;
11+
delete process.env.OPENROUTER_MODEL;
12+
delete process.env.OPENROUTER_REASONING_EFFORT;
13+
});
14+
15+
afterEach(() => {
16+
if (originalApiKey === undefined) {
17+
delete process.env.OPENROUTER_API_KEY;
18+
} else {
19+
process.env.OPENROUTER_API_KEY = originalApiKey;
20+
}
21+
22+
if (originalModel === undefined) {
23+
delete process.env.OPENROUTER_MODEL;
24+
} else {
25+
process.env.OPENROUTER_MODEL = originalModel;
26+
}
27+
28+
if (originalEffort === undefined) {
29+
delete process.env.OPENROUTER_REASONING_EFFORT;
30+
} else {
31+
process.env.OPENROUTER_REASONING_EFFORT = originalEffort;
32+
}
33+
});
34+
35+
it("returns false when neither binding nor process.env has a key", () => {
36+
expect(syncOpenRouterEnvFromBindings({})).toBe(false);
37+
expect(process.env.OPENROUTER_API_KEY).toBeUndefined();
38+
});
39+
40+
it("copies worker bindings into process.env for provider helpers", () => {
41+
expect(
42+
syncOpenRouterEnvFromBindings({
43+
OPENROUTER_API_KEY: "from-binding",
44+
OPENROUTER_MODEL: "openai/gpt-5.6-luna",
45+
OPENROUTER_REASONING_EFFORT: "high",
46+
}),
47+
).toBe(true);
48+
49+
expect(process.env.OPENROUTER_API_KEY).toBe("from-binding");
50+
expect(process.env.OPENROUTER_MODEL).toBe("openai/gpt-5.6-luna");
51+
expect(process.env.OPENROUTER_REASONING_EFFORT).toBe("high");
52+
});
53+
54+
it("falls back to process.env when the binding is missing", () => {
55+
process.env.OPENROUTER_API_KEY = "from-process";
56+
57+
expect(syncOpenRouterEnvFromBindings({})).toBe(true);
58+
expect(process.env.OPENROUTER_API_KEY).toBe("from-process");
59+
});
60+
61+
it("prefers the worker binding over an existing process.env key", () => {
62+
process.env.OPENROUTER_API_KEY = "from-process";
63+
64+
expect(
65+
syncOpenRouterEnvFromBindings({
66+
OPENROUTER_API_KEY: "from-binding",
67+
}),
68+
).toBe(true);
69+
70+
expect(process.env.OPENROUTER_API_KEY).toBe("from-binding");
71+
});
72+
73+
it("clears stale model and effort when bindings are removed", () => {
74+
process.env.OPENROUTER_API_KEY = "from-process";
75+
process.env.OPENROUTER_MODEL = "stale-model";
76+
process.env.OPENROUTER_REASONING_EFFORT = "stale-effort";
77+
78+
expect(
79+
syncOpenRouterEnvFromBindings({
80+
OPENROUTER_API_KEY: "from-binding",
81+
}),
82+
).toBe(true);
83+
84+
expect(process.env.OPENROUTER_API_KEY).toBe("from-binding");
85+
expect(process.env.OPENROUTER_MODEL).toBeUndefined();
86+
expect(process.env.OPENROUTER_REASONING_EFFORT).toBeUndefined();
87+
});
88+
89+
it("preserves empty-string reasoning effort as omit-provider-option", () => {
90+
expect(
91+
syncOpenRouterEnvFromBindings({
92+
OPENROUTER_API_KEY: "from-binding",
93+
OPENROUTER_REASONING_EFFORT: "",
94+
}),
95+
).toBe(true);
96+
97+
expect(process.env.OPENROUTER_REASONING_EFFORT).toBe("");
98+
});
99+
});

‎packages/mcp-cloudflare/src/server/routes/chat.ts‎

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { experimental_createMCPClient } from "@ai-sdk/mcp";
2-
import { openai } from "@ai-sdk/openai";
32
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3+
import {
4+
getOpenRouterModel,
5+
getOpenRouterProviderOptions,
6+
} from "@sentry/mcp-core/internal/agents/openrouter-provider";
47
import { logInfo, logIssue, logWarn } from "@sentry/mcp-core/telem/logging";
58
import {
69
convertToModelMessages,
@@ -22,6 +25,40 @@ import { analyzeAuthError, getAuthErrorResponse } from "../utils/auth-errors";
2225

2326
type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>;
2427

28+
/**
29+
* Ensure OpenRouter provider helpers can read worker bindings via process.env.
30+
* Returns false when no API key is available from either source.
31+
*/
32+
export function syncOpenRouterEnvFromBindings(
33+
env: Pick<
34+
Env,
35+
"OPENROUTER_API_KEY" | "OPENROUTER_MODEL" | "OPENROUTER_REASONING_EFFORT"
36+
>,
37+
): boolean {
38+
const openRouterApiKey =
39+
env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY;
40+
if (!openRouterApiKey) {
41+
return false;
42+
}
43+
44+
process.env.OPENROUTER_API_KEY = openRouterApiKey;
45+
46+
// Always mirror or clear optional bindings so a reused worker isolate cannot
47+
// keep a previous request's model/effort after the binding is removed.
48+
if (env.OPENROUTER_MODEL) {
49+
process.env.OPENROUTER_MODEL = env.OPENROUTER_MODEL;
50+
} else {
51+
delete process.env.OPENROUTER_MODEL;
52+
}
53+
if (env.OPENROUTER_REASONING_EFFORT !== undefined) {
54+
process.env.OPENROUTER_REASONING_EFFORT = env.OPENROUTER_REASONING_EFFORT;
55+
} else {
56+
delete process.env.OPENROUTER_REASONING_EFFORT;
57+
}
58+
59+
return true;
60+
}
61+
2562
async function refreshTokenIfNeeded(
2663
c: Context<{ Bindings: Env }>,
2764
): Promise<{ token: string; authData: AuthData } | null> {
@@ -93,9 +130,10 @@ async function refreshTokenIfNeeded(
93130
}
94131

95132
export default new Hono<{ Bindings: Env }>().post("/", async (c) => {
96-
// Validate that we have an OpenAI API key
97-
if (!c.env.OPENAI_API_KEY) {
98-
logIssue("OPENAI_API_KEY is not configured", {
133+
// OpenRouter helpers in mcp-core only read process.env. Bridge worker
134+
// bindings first so validation and provider calls stay in sync.
135+
if (!syncOpenRouterEnvFromBindings(c.env)) {
136+
logIssue("OPENROUTER_API_KEY is not configured", {
99137
loggerScope: ["cloudflare", "chat"],
100138
});
101139
return c.json(
@@ -307,7 +345,7 @@ export default new Hono<{ Bindings: Env }>().post("/", async (c) => {
307345
});
308346

309347
const result = streamText({
310-
model: openai("gpt-4o"),
348+
model: getOpenRouterModel(),
311349
messages: modelMessages,
312350
tools,
313351
system: `You are an AI assistant designed EXCLUSIVELY for testing the Sentry MCP service. Your sole purpose is to help users test MCP functionality with their real Sentry account data - nothing more, nothing less.
@@ -332,8 +370,11 @@ Start conversations by exploring what's available in their account. Use tools li
332370
- \`get_sentry_resource\` to dive deep into a specific issue, event, or trace
333371
334372
Remember: You're a test assistant, not a general-purpose helper. Stay focused on testing the MCP integration with their real data.`,
335-
maxOutputTokens: 2000,
373+
// Reasoning effort can consume completion budget before visible text/tool
374+
// calls, so keep headroom above the old non-reasoning 2k cap.
375+
maxOutputTokens: 16000,
336376
stopWhen: stepCountIs(10),
377+
providerOptions: getOpenRouterProviderOptions(),
337378
experimental_telemetry: {
338379
isEnabled: true,
339380
},

‎packages/mcp-cloudflare/src/server/sentry.config.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ function createEnv(overrides: Partial<Env> = {}): Env {
1313
SENTRY_CLIENT_SECRET: "test-client-secret",
1414
SENTRY_DSN: "https://public@example.ingest.sentry.io/1",
1515
SENTRY_HOST: "sentry.io",
16-
OPENAI_API_KEY: "test-openai-key",
16+
OPENROUTER_API_KEY: "test-openrouter-key",
17+
OPENROUTER_MODEL: "openai/gpt-5.6-luna",
18+
EMBEDDED_AGENT_PROVIDER: "openrouter",
1719
OAUTH_PROVIDER: {} as Env["OAUTH_PROVIDER"],
1820
AI: {} as Ai,
1921
...overrides,

‎packages/mcp-cloudflare/src/server/types.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ export interface Env {
6060
SENTRY_ENVIRONMENT?: string;
6161
SENTRY_DSN?: string;
6262
SENTRY_HOST?: string;
63-
OPENAI_API_KEY: string;
63+
/** @deprecated Prefer OPENROUTER_API_KEY for hosted MCP AI features. */
64+
OPENAI_API_KEY?: string;
65+
OPENROUTER_API_KEY?: string;
66+
OPENROUTER_MODEL?: string;
67+
OPENROUTER_REASONING_EFFORT?: string;
68+
EMBEDDED_AGENT_PROVIDER?: string;
6469
MCP_URL?: string;
6570
OAUTH_PROVIDER: OAuthHelpers;
6671
AI: Ai;

‎packages/mcp-cloudflare/worker-configuration.d.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ declare namespace Cloudflare {
1010
SENTRY_CLIENT_ID: string;
1111
SENTRY_CLIENT_SECRET: string;
1212
SENTRY_DSN: string;
13-
OPENAI_API_KEY: string;
13+
OPENAI_API_KEY?: string;
14+
OPENROUTER_API_KEY?: string;
15+
OPENROUTER_MODEL?: string;
16+
OPENROUTER_REASONING_EFFORT?: string;
17+
EMBEDDED_AGENT_PROVIDER?: string;
1418
COOKIE_SECRET: string;
1519
CHAT_RATE_LIMITER: RateLimit;
1620
SEARCH_RATE_LIMITER: RateLimit;
@@ -24,7 +28,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
2428
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
2529
};
2630
declare namespace NodeJS {
27-
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "SENTRY_CLIENT_ID" | "SENTRY_CLIENT_SECRET" | "SENTRY_DSN" | "OPENAI_API_KEY" | "COOKIE_SECRET">> {}
31+
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "SENTRY_CLIENT_ID" | "SENTRY_CLIENT_SECRET" | "SENTRY_DSN" | "OPENAI_API_KEY" | "OPENROUTER_API_KEY" | "OPENROUTER_MODEL" | "OPENROUTER_REASONING_EFFORT" | "EMBEDDED_AGENT_PROVIDER" | "COOKIE_SECRET">> {}
2832
}
2933

3034
// Begin runtime types

‎packages/mcp-cloudflare/wrangler.test.jsonc‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
"SENTRY_CLIENT_ID": "test-client-id",
1515
"SENTRY_CLIENT_SECRET": "test-client-secret",
1616
"SENTRY_HOST": "sentry.io",
17-
"OPENAI_API_KEY": "test-openai-key"
17+
"OPENROUTER_API_KEY": "test-openrouter-key",
18+
"OPENROUTER_MODEL": "openai/gpt-5.6-luna",
19+
"EMBEDDED_AGENT_PROVIDER": "openrouter"
1820
},
1921
"kv_namespaces": [
2022
{

0 commit comments

Comments
 (0)