feat(openrouter):add claw-x header (#213)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
Haze
2026-02-28 15:46:26 +08:00
committed by GitHub
Unverified
parent 6859656847
commit b4ef2bd51d
3 changed files with 163 additions and 2 deletions

View File

@@ -6,7 +6,7 @@ import { app } from 'electron';
import path from 'path';
import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import { existsSync } from 'fs';
import { existsSync, writeFileSync } from 'fs';
import WebSocket from 'ws';
import { PORTS } from '../utils/config';
import {
@@ -107,6 +107,57 @@ function getNodeExecutablePath(): string {
return process.execPath;
}
/**
* Ensure the gateway fetch-preload script exists in userData and return
* its absolute path. The script patches globalThis.fetch to inject
* ClawX app-attribution headers (HTTP-Referer, X-Title) for OpenRouter
* API requests, overriding the OpenClaw runner's hardcoded defaults.
*
* Inlined here so it works in dev, packaged, and asar modes without
* extra build config. Loaded by the Gateway child process via
* NODE_OPTIONS --require.
*/
const GATEWAY_FETCH_PRELOAD_SOURCE = `'use strict';
(function () {
var _f = globalThis.fetch;
if (typeof _f !== 'function') return;
if (globalThis.__clawxFetchPatched) return;
globalThis.__clawxFetchPatched = true;
globalThis.fetch = function clawxFetch(input, init) {
var url =
typeof input === 'string' ? input
: input && typeof input === 'object' && typeof input.url === 'string'
? input.url : '';
if (url.indexOf('openrouter.ai') !== -1) {
init = init ? Object.assign({}, init) : {};
var prev = init.headers;
var flat = {};
if (prev && typeof prev.forEach === 'function') {
prev.forEach(function (v, k) { flat[k] = v; });
} else if (prev && typeof prev === 'object') {
Object.assign(flat, prev);
}
delete flat['http-referer'];
delete flat['HTTP-Referer'];
delete flat['x-title'];
delete flat['X-Title'];
flat['HTTP-Referer'] = 'https://claw-x.com';
flat['X-Title'] = 'ClawX';
init.headers = flat;
}
return _f.call(globalThis, input, init);
};
})();
`;
function ensureGatewayFetchPreload(): string {
const dest = path.join(app.getPath('userData'), 'gateway-fetch-preload.cjs');
try { writeFileSync(dest, GATEWAY_FETCH_PRELOAD_SOURCE, 'utf-8'); } catch { /* best-effort */ }
return dest;
}
/**
* Gateway Manager
* Handles starting, stopping, and communicating with the OpenClaw Gateway
@@ -814,6 +865,19 @@ export class GatewayManager extends EventEmitter {
}
}
// Inject fetch preload so OpenRouter requests carry ClawX headers.
// The preload patches globalThis.fetch before any module loads.
try {
const preloadPath = ensureGatewayFetchPreload();
if (existsSync(preloadPath)) {
const quoted = `"${preloadPath}"`;
const opts = spawnEnv['NODE_OPTIONS'] ?? '';
spawnEnv['NODE_OPTIONS'] = `${opts} --require ${quoted}`.trim();
}
} catch (err) {
logger.warn('Failed to set up OpenRouter headers preload:', err);
}
const useShell = !app.isPackaged && process.platform === 'win32';
const spawnCmd = useShell ? quoteForCmd(command) : command;
const spawnArgs = useShell ? args.map(a => quoteForCmd(a)) : args;

View File

@@ -0,0 +1,45 @@
/**
* Gateway fetch preload — loaded via NODE_OPTIONS --require before
* the OpenClaw Gateway starts.
*
* Patches globalThis.fetch so that every request whose URL contains
* "openrouter.ai" carries the ClawX app-attribution headers.
*
* The OpenAI SDK (used by OpenClaw) captures globalThis.fetch in its
* constructor, so patching here guarantees all SDK requests go through
* the interceptor.
*/
'use strict';
(function () {
var _f = globalThis.fetch;
if (typeof _f !== 'function') return;
if (globalThis.__clawxFetchPatched) return;
globalThis.__clawxFetchPatched = true;
globalThis.fetch = function clawxFetch(input, init) {
var url =
typeof input === 'string' ? input
: input && typeof input === 'object' && typeof input.url === 'string'
? input.url : '';
if (url.indexOf('openrouter.ai') !== -1) {
init = init ? Object.assign({}, init) : {};
var prev = init.headers;
var flat = {};
if (prev && typeof prev.forEach === 'function') {
prev.forEach(function (v, k) { flat[k] = v; });
} else if (prev && typeof prev === 'object') {
Object.assign(flat, prev);
}
delete flat['http-referer'];
delete flat['HTTP-Referer'];
delete flat['x-title'];
delete flat['X-Title'];
flat['HTTP-Referer'] = 'https://claw-x.com';
flat['X-Title'] = 'ClawX';
init.headers = flat;
}
return _f.call(globalThis, input, init);
};
})();

54
pnpm-lock.yaml generated
View File

@@ -1415,6 +1415,20 @@ packages:
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-cuda-ext@3.15.1':
resolution: {integrity: sha512-toepvLcXjgaQE/QGIThHBD58jbHGBWT1jhblJkCjYBRHfVOO+6n/PmVsJt+yMfu5Z93A2gF8YOvVyZXNXmGo5g==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-cuda@3.15.1':
resolution: {integrity: sha512-kngwoq1KdrqSr/b6+tn5jbtGHI0tZnW5wofKssZy+Il2ge3eN9FowKbXG4FH452g6qSSVoDccAoTvYOxyLyX+w==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-vulkan@3.15.1':
resolution: {integrity: sha512-CMsyQkGKpHKeOH9+ZPxo0hO0usg8jabq5/aM3JwdX9CiuXhXUa3nu3NH4RObiNi596Zwn/zWzlps0HRwcpL8rw==}
engines: {node: '>=20.0.0'}
@@ -1447,6 +1461,24 @@ packages:
cpu: [arm64, x64]
os: [win32]
'@node-llama-cpp/win-x64-cuda-ext@3.15.1':
resolution: {integrity: sha512-mO3Tf6D3UlFkjQF5J96ynTkjdF7dac/f5f61cEh6oU4D3hdx+cwnmBWT1gVhDSLboJYzCHtx7U2EKPP6n8HoWA==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64-cuda@3.15.1':
resolution: {integrity: sha512-swoyx0/dY4ixiu3mEWrIAinx0ffHn9IncELDNREKG+iIXfx6w0OujOMQ6+X+lGj+sjE01yMUP/9fv6GEp2pmBw==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64-vulkan@3.15.1':
resolution: {integrity: sha512-BPBjUEIkFTdcHSsQyblP0v/aPPypi6uqQIq27mo4A49CYjX22JDmk4ncdBLk6cru+UkvwEEe+F2RomjoMt32aQ==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64@3.15.1':
resolution: {integrity: sha512-jtoXBa6h+VPsQgefrO7HDjYv4WvxfHtUO30ABwCUDuEgM0e05YYhxMZj1z2Ns47UrquNvd/LUPCyjHKqHUN+5Q==}
engines: {node: '>=20.0.0'}
@@ -4173,7 +4205,7 @@ packages:
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
deprecated: Glob versions prior to v9 are no longer supported
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
global-agent@3.0.0:
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
@@ -8479,6 +8511,12 @@ snapshots:
'@node-llama-cpp/linux-armv7l@3.15.1':
optional: true
'@node-llama-cpp/linux-x64-cuda-ext@3.15.1':
optional: true
'@node-llama-cpp/linux-x64-cuda@3.15.1':
optional: true
'@node-llama-cpp/linux-x64-vulkan@3.15.1':
optional: true
@@ -8494,6 +8532,15 @@ snapshots:
'@node-llama-cpp/win-arm64@3.15.1':
optional: true
'@node-llama-cpp/win-x64-cuda-ext@3.15.1':
optional: true
'@node-llama-cpp/win-x64-cuda@3.15.1':
optional: true
'@node-llama-cpp/win-x64-vulkan@3.15.1':
optional: true
'@node-llama-cpp/win-x64@3.15.1':
optional: true
@@ -12963,11 +13010,16 @@ snapshots:
'@node-llama-cpp/linux-arm64': 3.15.1
'@node-llama-cpp/linux-armv7l': 3.15.1
'@node-llama-cpp/linux-x64': 3.15.1
'@node-llama-cpp/linux-x64-cuda': 3.15.1
'@node-llama-cpp/linux-x64-cuda-ext': 3.15.1
'@node-llama-cpp/linux-x64-vulkan': 3.15.1
'@node-llama-cpp/mac-arm64-metal': 3.15.1
'@node-llama-cpp/mac-x64': 3.15.1
'@node-llama-cpp/win-arm64': 3.15.1
'@node-llama-cpp/win-x64': 3.15.1
'@node-llama-cpp/win-x64-cuda': 3.15.1
'@node-llama-cpp/win-x64-cuda-ext': 3.15.1
'@node-llama-cpp/win-x64-vulkan': 3.15.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color