diff --git a/docs/providers/openrouter.md b/docs/providers/openrouter.md index ca17ba009a52..35f83e796218 100644 --- a/docs/providers/openrouter.md +++ b/docs/providers/openrouter.md @@ -354,14 +354,15 @@ openclaw models auth login --provider openrouter --method oauth openclaw models auth login --provider openrouter --method api-key ``` -On verified OpenRouter requests (`https://openrouter.ai/api/v1`), OpenClaw adds -OpenRouter's documented app-attribution headers: +On requests to OpenRouter endpoints (`openrouter.ai`), OpenClaw adds OpenRouter's +documented app-attribution headers. This applies to the bundled `openrouter` +provider and to custom provider ids whose `baseUrl` points at OpenRouter: -| Header | Value | -| ------------------------- | ------------------------------------------------------------------------------------------------------ | -| `HTTP-Referer` | `https://openclaw.ai` | -| `X-OpenRouter-Title` | `OpenClaw` | -| `X-OpenRouter-Categories` | `cli-agent,cloud-agent,programming-app,creative-writing,writing-assistant,general-chat,personal-agent` | +| Header | Value | +| ------------------------- | -------------------------- | +| `HTTP-Referer` | `https://openclaw.ai` | +| `X-OpenRouter-Title` | `OpenClaw` | +| `X-OpenRouter-Categories` | `personal-agent,cli-agent` | If you repoint the OpenRouter provider at some other proxy or base URL, OpenClaw diff --git a/extensions/openrouter/generation-request-context.ts b/extensions/openrouter/generation-request-context.ts index 14623314a332..a9b740f92698 100644 --- a/extensions/openrouter/generation-request-context.ts +++ b/extensions/openrouter/generation-request-context.ts @@ -32,8 +32,6 @@ export async function resolveOpenRouterGenerationRequestContext(params: { defaultHeaders: { Authorization: `Bearer ${auth.apiKey}`, ...(params.jsonContentType ? { "Content-Type": "application/json" } : {}), - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", }, request: sanitizeConfiguredModelProviderRequest( params.cfg.models?.providers?.openrouter?.request, diff --git a/extensions/openrouter/image-generation-provider.test.ts b/extensions/openrouter/image-generation-provider.test.ts index 612e9a923df1..fcd69cb06ed4 100644 --- a/extensions/openrouter/image-generation-provider.test.ts +++ b/extensions/openrouter/image-generation-provider.test.ts @@ -133,8 +133,6 @@ describe("openrouter image generation provider", () => { allowPrivateNetwork: false, defaultHeaders: { Authorization: "Bearer openrouter-key", - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", }, request: undefined, provider: "openrouter", @@ -146,8 +144,6 @@ describe("openrouter image generation provider", () => { const headers = requireHeaders(request.headers); expect(Object.fromEntries(headers.entries())).toEqual({ authorization: "Bearer openrouter-key", - "http-referer": "https://openclaw.ai", - "x-openrouter-title": "OpenClaw", }); expect(request).toEqual({ url: "https://custom.openrouter.test/api/v1/chat/completions", diff --git a/extensions/openrouter/index.test.ts b/extensions/openrouter/index.test.ts index 5f1028887e79..b12e011f3a37 100644 --- a/extensions/openrouter/index.test.ts +++ b/extensions/openrouter/index.test.ts @@ -757,8 +757,6 @@ describe("openrouter provider hooks", () => { const options = baseStreamFn.mock.calls[0]?.[2] as { headers?: HeadersInit } | undefined; const headers = new Headers(options?.headers); expect(headers.get("authorization")).toBe("Bearer or-test-key"); - expect(headers.get("http-referer")).toBe("https://openclaw.ai"); - expect(headers.get("x-openrouter-title")).toBe("OpenClaw"); }); it("merges resolved OpenRouter model params into transport params", async () => { diff --git a/extensions/openrouter/media-understanding-provider.test.ts b/extensions/openrouter/media-understanding-provider.test.ts index d0254a3f78e2..599d7df9202f 100644 --- a/extensions/openrouter/media-understanding-provider.test.ts +++ b/extensions/openrouter/media-understanding-provider.test.ts @@ -91,8 +91,6 @@ describe("openrouter media understanding provider", () => { defaultHeaders: { Authorization: "Bearer sk-openrouter", "Content-Type": "application/json", - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", }, provider: "openrouter", api: "openrouter-stt", @@ -121,8 +119,6 @@ describe("openrouter media understanding provider", () => { throw new Error("expected OpenRouter request headers"); } expect(headers.get("authorization")).toBe("Bearer sk-openrouter"); - expect(headers.get("http-referer")).toBe("https://openclaw.ai"); - expect(headers.get("x-openrouter-title")).toBe("OpenClaw"); expect(release).toHaveBeenCalledOnce(); }); diff --git a/extensions/openrouter/media-understanding-provider.ts b/extensions/openrouter/media-understanding-provider.ts index 3f9d7708ea08..dbb6c2be5342 100644 --- a/extensions/openrouter/media-understanding-provider.ts +++ b/extensions/openrouter/media-understanding-provider.ts @@ -115,8 +115,6 @@ async function transcribeOpenRouterAudio( defaultHeaders: { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json", - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", }, provider: "openrouter", api: "openrouter-stt", diff --git a/extensions/openrouter/speech-provider.test.ts b/extensions/openrouter/speech-provider.test.ts index 580b68c8f008..937eb300d5ae 100644 --- a/extensions/openrouter/speech-provider.test.ts +++ b/extensions/openrouter/speech-provider.test.ts @@ -121,8 +121,6 @@ describe("openrouter speech provider", () => { defaultHeaders: { Authorization: "Bearer sk-openrouter", "Content-Type": "application/json", - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", }, provider: "openrouter", capability: "audio", @@ -137,8 +135,6 @@ describe("openrouter speech provider", () => { expect(Object.fromEntries(headers.entries())).toEqual({ authorization: "Bearer sk-openrouter", "content-type": "application/json", - "http-referer": "https://openclaw.ai", - "x-openrouter-title": "OpenClaw", }); expect(request).toEqual({ url: "https://openrouter.ai/api/v1/audio/speech", diff --git a/extensions/openrouter/speech-provider.ts b/extensions/openrouter/speech-provider.ts index 9e574ce86060..44f94bdc4580 100644 --- a/extensions/openrouter/speech-provider.ts +++ b/extensions/openrouter/speech-provider.ts @@ -34,10 +34,6 @@ export function buildOpenRouterSpeechProvider(): SpeechProviderPlugin { defaultResponseFormat: "mp3", voiceCompatibleResponseFormats: ["mp3"], baseUrlPolicy: { kind: "canonical", aliases: ["https://openrouter.ai/v1"], allowCustom: true }, - extraHeaders: { - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", - }, apiErrorLabel: "OpenRouter TTS API error", missingApiKeyError: "OpenRouter API key missing", readExtraConfig: (raw) => ({ provider: asOptionalRecord(raw?.provider) }), diff --git a/extensions/openrouter/stream.ts b/extensions/openrouter/stream.ts index 375b1fdcedd5..0a2cb1fa67ec 100644 --- a/extensions/openrouter/stream.ts +++ b/extensions/openrouter/stream.ts @@ -50,12 +50,6 @@ function mergeOpenRouterAuthHeaders(options: Parameters[2]): Parameter if (!headers.has("authorization")) { headers.set("Authorization", `Bearer ${apiKey}`); } - if (!headers.has("http-referer")) { - headers.set("HTTP-Referer", "https://openclaw.ai"); - } - if (!headers.has("x-openrouter-title")) { - headers.set("X-OpenRouter-Title", "OpenClaw"); - } return { ...options, headers: Object.fromEntries(headers.entries()), diff --git a/extensions/openrouter/video-model-catalog.ts b/extensions/openrouter/video-model-catalog.ts index 8fadf085a1f5..1dc3cf5ee175 100644 --- a/extensions/openrouter/video-model-catalog.ts +++ b/extensions/openrouter/video-model-catalog.ts @@ -218,8 +218,6 @@ function resolveOpenRouterVideoCatalogRequest(params: { defaultBaseUrl: OPENROUTER_BASE_URL, defaultHeaders: { Authorization: `Bearer ${params.apiKey}`, - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", }, request, }), diff --git a/src/agents/provider-attribution.test.ts b/src/agents/provider-attribution.test.ts index 2f3ab0ec504d..d2100baf8824 100644 --- a/src/agents/provider-attribution.test.ts +++ b/src/agents/provider-attribution.test.ts @@ -337,8 +337,7 @@ describe("provider attribution", () => { ).toEqual({ "HTTP-Referer": "https://openclaw.ai", "X-OpenRouter-Title": "OpenClaw", - "X-OpenRouter-Categories": - "cli-agent,cloud-agent,programming-app,creative-writing,writing-assistant,general-chat,personal-agent", + "X-OpenRouter-Categories": "personal-agent,cli-agent", }); }); @@ -684,7 +683,7 @@ describe("provider attribution", () => { }); }); - it("gates documented OpenRouter attribution to known OpenRouter endpoints", () => { + it("gates documented OpenRouter attribution to OpenRouter endpoints for any provider id", () => { expectRecordFields( resolveProviderRequestPolicy({ provider: "openrouter", @@ -701,6 +700,20 @@ describe("provider attribution", () => { }, ); + expect( + resolveProviderRequestPolicy({ + provider: "or", + api: "openai-completions", + baseUrl: "https://openrouter.ai/api/v1", + transport: "stream", + capability: "llm", + }).attributionHeaders, + ).toEqual({ + "HTTP-Referer": "https://openclaw.ai", + "X-OpenRouter-Title": "OpenClaw", + "X-OpenRouter-Categories": "personal-agent,cli-agent", + }); + expect( resolveProviderRequestPolicy({ provider: "openrouter", diff --git a/src/agents/provider-attribution.ts b/src/agents/provider-attribution.ts index e2deec573be8..3e7420eb86a9 100644 --- a/src/agents/provider-attribution.ts +++ b/src/agents/provider-attribution.ts @@ -148,8 +148,8 @@ function readCompatBoolean( const OPENCLAW_ATTRIBUTION_PRODUCT = "OpenClaw"; const OPENCLAW_ATTRIBUTION_ORIGINATOR = "openclaw"; -const OPENROUTER_ATTRIBUTION_CATEGORIES = - "cli-agent,cloud-agent,programming-app,creative-writing,writing-assistant,general-chat,personal-agent"; +// OpenRouter honors at most two recognized categories per request and silently drops the rest. +const OPENROUTER_ATTRIBUTION_CATEGORIES = "personal-agent,cli-agent"; const LOCAL_ENDPOINT_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]); const OPENAI_RESPONSES_APIS = new Set([ @@ -308,7 +308,8 @@ function resolveProviderAttributionPolicy( return { ...policy, docsUrl: "https://openrouter.ai/docs/app-attribution", - reviewNote: "Documented app attribution headers. Verified in OpenClaw runtime wrapper.", + reviewNote: + "Documented app attribution headers. Applied on OpenRouter endpoints regardless of configured provider id.", headers: { "HTTP-Referer": "https://openclaw.ai", "X-OpenRouter-Title": policy.product, @@ -399,12 +400,6 @@ export function resolveProviderRequestPolicy( let attributionProvider: string | undefined; if (provider === "openai" && usesVerifiedOpenAIAttributionHost) { attributionProvider = "openai"; - } else if (provider === "openrouter" && policy?.enabledByDefault) { - // OpenRouter attribution is documented, but only apply it to known - // OpenRouter endpoints or the default (unset) baseUrl path. - if (endpointClass === "openrouter" || endpointClass === "default") { - attributionProvider = "openrouter"; - } } else if (provider === "xai" && policy?.enabledByDefault) { // Default (unset baseUrl) maps to api.x.ai; custom baseUrls are treated as proxies and withheld. if (endpointClass === "xai-native" || endpointClass === "default") { @@ -419,6 +414,14 @@ export function resolveProviderRequestPolicy( // A custom baseUrl is a proxy and must not inherit OpenClaw attribution. attributionProvider = "opencode-go"; } + // OpenRouter attribution follows the endpoint, so custom provider ids pointed at + // openrouter.ai are attributed too; custom proxy baseUrls are withheld. + if ( + !attributionProvider && + (endpointClass === "openrouter" || (provider === "openrouter" && endpointClass === "default")) + ) { + attributionProvider = "openrouter"; + } if (!attributionProvider && endpointClass === "nvidia-native") { attributionProvider = "nvidia"; } diff --git a/src/agents/provider-request-config.test.ts b/src/agents/provider-request-config.test.ts index 799886dfc1e8..31bd46b57e2a 100644 --- a/src/agents/provider-request-config.test.ts +++ b/src/agents/provider-request-config.test.ts @@ -526,8 +526,7 @@ describe("provider request config", () => { expect(resolved).toEqual({ "HTTP-Referer": "https://openclaw.ai", "X-OpenRouter-Title": "OpenClaw", - "X-OpenRouter-Categories": - "cli-agent,cloud-agent,programming-app,creative-writing,writing-assistant,general-chat,personal-agent", + "X-OpenRouter-Categories": "personal-agent,cli-agent", "X-Custom": "1", }); }); diff --git a/src/agents/sessions/sdk.test.ts b/src/agents/sessions/sdk.test.ts index ba0d6a6ec224..be44ed03b445 100644 --- a/src/agents/sessions/sdk.test.ts +++ b/src/agents/sessions/sdk.test.ts @@ -486,28 +486,29 @@ describe("createAgentSession attribution headers", () => { expect(options.headers).toBeUndefined(); }); - it("keeps OpenRouter attribution headers for provider and endpoint matches", async () => { - const providerOptions = await createSessionAndStreamModel({ - ...testModel, - provider: "openrouter", - baseUrl: "https://example.test", - }); - const endpointOptions = await createSessionAndStreamModel({ - ...testModel, - provider: "custom-openai", - baseUrl: "https://openrouter.ai/api/v1", - }); + it("forwards OpenRouter attribution for openrouter.ai endpoints with telemetry off, but not proxies", async () => { + vi.stubEnv("OPENCLAW_TELEMETRY", "0"); + try { + const proxyOptions = await createSessionAndStreamModel({ + ...testModel, + provider: "openrouter", + baseUrl: "https://example.test", + }); + const endpointOptions = await createSessionAndStreamModel({ + ...testModel, + provider: "custom-openai", + baseUrl: "https://openrouter.ai/api/v1", + }); - expect(providerOptions.headers).toMatchObject({ - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", - "X-OpenRouter-Categories": "cli-agent", - }); - expect(endpointOptions.headers).toMatchObject({ - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", - "X-OpenRouter-Categories": "cli-agent", - }); + expect(proxyOptions.headers).toBeUndefined(); + expect(endpointOptions.headers).toMatchObject({ + "HTTP-Referer": "https://openclaw.ai", + "X-OpenRouter-Title": "OpenClaw", + "X-OpenRouter-Categories": "personal-agent,cli-agent", + }); + } finally { + vi.unstubAllEnvs(); + } }); it("keeps Cloudflare attribution headers for provider and endpoint matches", async () => { diff --git a/src/agents/sessions/sdk.ts b/src/agents/sessions/sdk.ts index aa9515ccdef4..3fe046a98d1b 100644 --- a/src/agents/sessions/sdk.ts +++ b/src/agents/sessions/sdk.ts @@ -23,6 +23,7 @@ import { registerResolvedAgentDir } from "../agent-dir-registry.js"; import { sanitizeCompactionReplayMessages } from "../compaction-replay.js"; import { getAgentDirResolution } from "../config.js"; import { projectModelThinkingCompat } from "../model-catalog-lookup.js"; +import { resolveProviderRequestPolicy } from "../provider-attribution.js"; import { Agent, type AgentMessage, @@ -190,20 +191,24 @@ function getAttributionHeaders( model: Model, settingsManager: SettingsManager, ): Record | undefined { + // Transports outside the OpenAI families do not consult the attribution policy, + // so forward its OpenRouter header set as caller headers. Like the transport-side + // policy, OpenRouter app attribution does not depend on install telemetry. + const { attributionProvider, attributionHeaders } = resolveProviderRequestPolicy({ + provider: model.provider, + api: model.api, + baseUrl: model.baseUrl, + }); + if (attributionProvider === "openrouter") { + return attributionHeaders; + } + if (!isInstallTelemetryEnabled(settingsManager)) { return undefined; } const baseUrl = model.baseUrl ?? ""; - if (model.provider === "openrouter" || baseUrl.includes("openrouter.ai")) { - return { - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", - "X-OpenRouter-Categories": "cli-agent", - }; - } - if ( model.provider === "cloudflare-workers-ai" || model.provider === "cloudflare-ai-gateway" || diff --git a/src/llm/providers/stream-wrappers/proxy.test.ts b/src/llm/providers/stream-wrappers/proxy.test.ts index 44e9c1f751d5..42da7083dd2d 100644 --- a/src/llm/providers/stream-wrappers/proxy.test.ts +++ b/src/llm/providers/stream-wrappers/proxy.test.ts @@ -63,8 +63,7 @@ describe("proxy stream wrappers", () => { headers: { "HTTP-Referer": "https://openclaw.ai", "X-OpenRouter-Title": "OpenClaw", - "X-OpenRouter-Categories": - "cli-agent,cloud-agent,programming-app,creative-writing,writing-assistant,general-chat,personal-agent", + "X-OpenRouter-Categories": "personal-agent,cli-agent", "X-Custom": "1", }, },