Agents: add provider attribution registry (#48735)

* Agents: add provider attribution registry

* Agents: record provider attribution matrix

* Agents: align OpenRouter attribution headers
This commit is contained in:
Vincent Koc
2026-03-16 21:36:39 -07:00
committed by GitHub
parent 87b9a063ce
commit 5572e6965a
6 changed files with 269 additions and 8 deletions

View File

@@ -1160,7 +1160,8 @@ describe("applyExtraParamsToAgent", () => {
expect(calls).toHaveLength(1);
expect(calls[0]?.headers).toEqual({
"HTTP-Referer": "https://openclaw.ai",
"X-Title": "OpenClaw",
"X-OpenRouter-Title": "OpenClaw",
"X-OpenRouter-Categories": "cli-agent",
"X-Custom": "1",
});
});

View File

@@ -264,7 +264,7 @@ function createParallelToolCallsWrapper(
/**
* Apply extra params (like temperature) to an agent's streamFn.
* Also adds OpenRouter app attribution headers when using the OpenRouter provider.
* Also applies verified provider-specific request wrappers, such as OpenRouter attribution.
*
* @internal Exported for testing
*/

View File

@@ -0,0 +1,38 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Context, Model } from "@mariozechner/pi-ai";
import { createAssistantMessageEventStream } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { createOpenRouterWrapper } from "./proxy-stream-wrappers.js";
describe("proxy stream wrappers", () => {
it("adds OpenRouter attribution headers to stream options", () => {
const calls: Array<{ headers?: Record<string, string> }> = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
calls.push({
headers: options?.headers,
});
return createAssistantMessageEventStream();
};
const wrapped = createOpenRouterWrapper(baseStreamFn);
const model = {
api: "openai-completions",
provider: "openrouter",
id: "openrouter/auto",
} as Model<"openai-completions">;
const context: Context = { messages: [] };
void wrapped(model, context, { headers: { "X-Custom": "1" } });
expect(calls).toEqual([
{
headers: {
"HTTP-Referer": "https://openclaw.ai",
"X-OpenRouter-Title": "OpenClaw",
"X-OpenRouter-Categories": "cli-agent",
"X-Custom": "1",
},
},
]);
});
});

View File

@@ -1,11 +1,7 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
const OPENROUTER_APP_HEADERS: Record<string, string> = {
"HTTP-Referer": "https://openclaw.ai",
"X-Title": "OpenClaw",
};
import { resolveProviderAttributionHeaders } from "../provider-attribution.js";
const KILOCODE_FEATURE_HEADER = "X-KILOCODE-FEATURE";
const KILOCODE_FEATURE_DEFAULT = "openclaw";
const KILOCODE_FEATURE_ENV_VAR = "KILOCODE_FEATURE";
@@ -105,10 +101,11 @@ export function createOpenRouterWrapper(
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
const onPayload = options?.onPayload;
const attributionHeaders = resolveProviderAttributionHeaders("openrouter");
return underlying(model, context, {
...options,
headers: {
...OPENROUTER_APP_HEADERS,
...attributionHeaders,
...options?.headers,
},
onPayload: (payload) => {

View File

@@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";
import {
listProviderAttributionPolicies,
resolveProviderAttributionHeaders,
resolveProviderAttributionIdentity,
resolveProviderAttributionPolicy,
} from "./provider-attribution.js";
describe("provider attribution", () => {
it("resolves the canonical OpenClaw product and runtime version", () => {
const identity = resolveProviderAttributionIdentity({
OPENCLAW_VERSION: "2026.3.99",
});
expect(identity).toEqual({
product: "OpenClaw",
version: "2026.3.99",
});
});
it("returns a documented OpenRouter attribution policy", () => {
const policy = resolveProviderAttributionPolicy("openrouter", {
OPENCLAW_VERSION: "2026.3.14",
});
expect(policy).toEqual({
provider: "openrouter",
enabledByDefault: true,
verification: "vendor-documented",
hook: "request-headers",
docsUrl: "https://openrouter.ai/docs/app-attribution",
reviewNote: "Documented app attribution headers. Verified in OpenClaw runtime wrapper.",
product: "OpenClaw",
version: "2026.3.14",
headers: {
"HTTP-Referer": "https://openclaw.ai",
"X-OpenRouter-Title": "OpenClaw",
"X-OpenRouter-Categories": "cli-agent",
},
});
});
it("normalizes aliases when resolving provider headers", () => {
expect(
resolveProviderAttributionHeaders("OpenRouter", {
OPENCLAW_VERSION: "2026.3.14",
}),
).toEqual({
"HTTP-Referer": "https://openclaw.ai",
"X-OpenRouter-Title": "OpenClaw",
"X-OpenRouter-Categories": "cli-agent",
});
});
it("tracks SDK-hook-only providers without enabling them", () => {
expect(resolveProviderAttributionPolicy("openai", { OPENCLAW_VERSION: "2026.3.14" })).toEqual({
provider: "openai",
enabledByDefault: false,
verification: "vendor-sdk-hook-only",
hook: "default-headers",
reviewNote:
"OpenAI JS SDK exposes defaultHeaders, but public app attribution support is not yet verified.",
product: "OpenClaw",
version: "2026.3.14",
});
expect(resolveProviderAttributionHeaders("openai")).toBeUndefined();
});
it("lists the current attribution support matrix", () => {
expect(
listProviderAttributionPolicies({ OPENCLAW_VERSION: "2026.3.14" }).map((policy) => [
policy.provider,
policy.enabledByDefault,
policy.verification,
policy.hook,
]),
).toEqual([
["openrouter", true, "vendor-documented", "request-headers"],
["anthropic", false, "vendor-sdk-hook-only", "default-headers"],
["google", false, "vendor-sdk-hook-only", "user-agent-extra"],
["groq", false, "vendor-sdk-hook-only", "default-headers"],
["mistral", false, "vendor-sdk-hook-only", "custom-user-agent"],
["openai", false, "vendor-sdk-hook-only", "default-headers"],
["together", false, "vendor-sdk-hook-only", "default-headers"],
]);
});
});

View File

@@ -0,0 +1,138 @@
import type { RuntimeVersionEnv } from "../version.js";
import { resolveRuntimeServiceVersion } from "../version.js";
import { normalizeProviderId } from "./model-selection.js";
export type ProviderAttributionVerification =
| "vendor-documented"
| "vendor-sdk-hook-only"
| "internal-runtime";
export type ProviderAttributionHook =
| "request-headers"
| "default-headers"
| "user-agent-extra"
| "custom-user-agent";
export type ProviderAttributionPolicy = {
provider: string;
enabledByDefault: boolean;
verification: ProviderAttributionVerification;
hook?: ProviderAttributionHook;
docsUrl?: string;
reviewNote?: string;
product: string;
version: string;
headers?: Record<string, string>;
};
export type ProviderAttributionIdentity = Pick<ProviderAttributionPolicy, "product" | "version">;
const OPENCLAW_ATTRIBUTION_PRODUCT = "OpenClaw";
export function resolveProviderAttributionIdentity(
env: RuntimeVersionEnv = process.env as RuntimeVersionEnv,
): ProviderAttributionIdentity {
return {
product: OPENCLAW_ATTRIBUTION_PRODUCT,
version: resolveRuntimeServiceVersion(env),
};
}
function buildOpenRouterAttributionPolicy(
env: RuntimeVersionEnv = process.env as RuntimeVersionEnv,
): ProviderAttributionPolicy {
const identity = resolveProviderAttributionIdentity(env);
return {
provider: "openrouter",
enabledByDefault: true,
verification: "vendor-documented",
hook: "request-headers",
docsUrl: "https://openrouter.ai/docs/app-attribution",
reviewNote: "Documented app attribution headers. Verified in OpenClaw runtime wrapper.",
...identity,
headers: {
"HTTP-Referer": "https://openclaw.ai",
"X-OpenRouter-Title": identity.product,
"X-OpenRouter-Categories": "cli-agent",
},
};
}
function buildSdkHookOnlyPolicy(
provider: string,
hook: ProviderAttributionHook,
reviewNote: string,
env: RuntimeVersionEnv = process.env as RuntimeVersionEnv,
): ProviderAttributionPolicy {
return {
provider,
enabledByDefault: false,
verification: "vendor-sdk-hook-only",
hook,
reviewNote,
...resolveProviderAttributionIdentity(env),
};
}
export function listProviderAttributionPolicies(
env: RuntimeVersionEnv = process.env as RuntimeVersionEnv,
): ProviderAttributionPolicy[] {
return [
buildOpenRouterAttributionPolicy(env),
buildSdkHookOnlyPolicy(
"anthropic",
"default-headers",
"Anthropic JS SDK exposes defaultHeaders, but app attribution is not yet verified.",
env,
),
buildSdkHookOnlyPolicy(
"google",
"user-agent-extra",
"Google GenAI JS SDK exposes userAgentExtra/httpOptions, but provider-side attribution is not yet verified.",
env,
),
buildSdkHookOnlyPolicy(
"groq",
"default-headers",
"Groq JS SDK exposes defaultHeaders, but app attribution is not yet verified.",
env,
),
buildSdkHookOnlyPolicy(
"mistral",
"custom-user-agent",
"Mistral JS SDK exposes a custom userAgent option, but app attribution is not yet verified.",
env,
),
buildSdkHookOnlyPolicy(
"openai",
"default-headers",
"OpenAI JS SDK exposes defaultHeaders, but public app attribution support is not yet verified.",
env,
),
buildSdkHookOnlyPolicy(
"together",
"default-headers",
"Together JS SDK exposes defaultHeaders, but app attribution is not yet verified.",
env,
),
];
}
export function resolveProviderAttributionPolicy(
provider?: string | null,
env: RuntimeVersionEnv = process.env as RuntimeVersionEnv,
): ProviderAttributionPolicy | undefined {
const normalized = normalizeProviderId(provider ?? "");
return listProviderAttributionPolicies(env).find((policy) => policy.provider === normalized);
}
export function resolveProviderAttributionHeaders(
provider?: string | null,
env: RuntimeVersionEnv = process.env as RuntimeVersionEnv,
): Record<string, string> | undefined {
const policy = resolveProviderAttributionPolicy(provider, env);
if (!policy?.enabledByDefault) {
return undefined;
}
return policy.headers;
}