mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 22:40:26 +00:00
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:
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
38
src/agents/pi-embedded-runner/proxy-stream-wrappers.test.ts
Normal file
38
src/agents/pi-embedded-runner/proxy-stream-wrappers.test.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
87
src/agents/provider-attribution.test.ts
Normal file
87
src/agents/provider-attribution.test.ts
Normal 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"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
138
src/agents/provider-attribution.ts
Normal file
138
src/agents/provider-attribution.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user