@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
130 lines (129 loc) • 5.92 kB
JavaScript
/**
* Which CLI is calling, from its User-Agent.
*
* The proxy already computed a client label for trace spans, but it only
* distinguished `claude-cli/` from `ai/` and dropped the result into an OTel
* attribute. Nothing persisted it, so usage could be attributed to an account
* but never to the tool that spent it — the question a pooled-subscription
* operator actually asks.
*
* Two rules keep this honest as the roster grows:
*
* - **Only prefixes observed in real traffic appear here.** A guessed mapping
* that never matches is indistinguishable from one that works, and silently
* files a client under the wrong name if the guess collides.
* - **The raw header is persisted alongside the derived name.** An unrecognised
* client is then still attributable by its own User-Agent rather than
* collapsing into one bucket with every other unknown.
*/
/**
* Longest prefix wins, so a more specific match cannot be shadowed.
*
* Every entry below was read out of this machine's proxy request log or a
* header capture, never guessed. The measured string is quoted beside each so
* a future reader can tell an observation from an assumption.
*/
const CLIENT_PREFIXES = [
// "claude-cli/2.1.251 (external, cli)"
["claude-cli/", "claude-code"],
// "ai/4.x" — the AI SDK's own UA, used by NeuroLink's SDK callers.
["ai/", "sdk"],
// "opencode/1.3.13 ai-sdk/provider-utils/4.0.21 runtime/bun/1.3.13"
["opencode/", "opencode"],
// "QwenCode/0.17.0 (darwin; arm64)"
["QwenCode/", "qwen-code"],
// "GeminiCLI-tui/0.53.0/gemini-3.1-pro-preview (darwin; arm64; terminal)"
["GeminiCLI-", "gemini-cli"],
// "codex_exec/0.147.0 (Mac OS 26.6.0; arm64)". Only the non-interactive
// `codex exec` binary has been observed; the interactive TUI may well send a
// different token, and it is left unmapped until someone measures it rather
// than pattern-matched on a guess.
["codex_exec/", "codex"],
// "grok-shell/1.0.30 (macos; aarch64)" — measured 2026-09-12 on
// POST /v1/messages from Grok Build 1.0.30.
["grok-shell/", "grok"],
];
/**
* Deliberately NOT mapped, and why each was ruled out.
*
* These two strings are unrelated to each other. They are grouped only because
* both were candidates for a Copilot mapping at some point, and neither can
* carry one.
*
* - `OpenAI/JS 5.20.1` — Copilot CLI's actual User-Agent, and the problem is
* that it is not Copilot's alone: it is the stock OpenAI JS SDK string, sent
* by every caller of that SDK. Mapping it would file unrelated OpenAI-SDK
* traffic under Copilot's name, which is worse than leaving it unattributed.
* Copilot also sends `x-initiator` and `x-interaction-type`, but neither is
* exclusive to it either, so it stays `unknown` and remains traceable
* through the stored raw header.
*
* - `Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)` — not a
* CLI's User-Agent at all, and in particular NOT Copilot's. It is what
* `curl` sends on a machine whose `~/.curlrc` sets `user-agent`, so every
* curl-driven caller on such a host shares it: scripts, agents, health
* probes. It accounted for the largest single block of `unknown` rows in the
* log this table was measured against, which is exactly what made it
* tempting.
*
* Recorded because the first pass got this wrong in a way worth naming. The
* string was seen arriving at two capture servers during a Copilot CLI run
* and a Gemini CLI run, and was written up as "sent by both CLIs". It was
* sent by neither: it was the aliveness `curl` fired at each capture server
* moments before the CLI was pointed at it, picking up that host's curlrc.
* The request paths gave it away on review — the Gemini-side hit was a bare
* `GET /v1beta/models`, which is the probe's URL and not one the CLI
* requests. An identical unusual string arriving from two unrelated clients
* is evidence of shared tooling, never a property of either client.
*/
/**
* The client names this table can produce.
*
* Exported so a test can check the roster of CLIs the proxy configures against
* the roster it can actually name. Those two lists drifted apart silently once
* already: five configurators shipped while the table still knew only Claude
* Code, and nothing failed, because an unattributed client looks exactly like
* a quiet one.
*/
export function getMappedClientNames() {
return new Set(CLIENT_PREFIXES.map(([, name]) => name));
}
/** Cap stored User-Agents. They are attacker-influenced and unbounded. */
const MAX_USER_AGENT_CHARS = 200;
/**
* Derive a stable client name, or "unknown" when the header is absent or
* unrecognised. Never throws: attribution must not be able to fail a request.
*/
export function detectProxyClient(userAgent) {
if (!userAgent) {
return "unknown";
}
const ua = userAgent.trim();
let best;
let bestLength = 0;
for (const [prefix, name] of CLIENT_PREFIXES) {
if (ua.startsWith(prefix) && prefix.length > bestLength) {
best = name;
bestLength = prefix.length;
}
}
return best ?? "unknown";
}
/** Read the User-Agent case-insensitively and bound it for storage. */
export function readUserAgent(headers) {
if (!headers) {
return undefined;
}
for (const [name, value] of Object.entries(headers)) {
if (name.toLowerCase() === "user-agent" && typeof value === "string") {
const trimmed = value.trim();
return trimmed ? trimmed.slice(0, MAX_USER_AGENT_CHARS) : undefined;
}
}
return undefined;
}
/** The pair every proxy engine attaches to its request log entry. */
export function buildClientAttribution(headers) {
const userAgent = readUserAgent(headers);
return { clientApp: detectProxyClient(userAgent), userAgent };
}