openclaw/src/agents/provider-request-config.tes...

490 lines
13 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildProviderRequestDispatcherPolicy,
mergeProviderRequestOverrides,
resolveProviderRequestPolicyConfig,
resolveProviderRequestConfig,
resolveProviderRequestHeaders,
sanitizeConfiguredModelProviderRequest,
sanitizeConfiguredProviderRequest,
sanitizeRuntimeProviderRequestOverrides,
} from "./provider-request-config.js";
describe("provider request config", () => {
it("merges discovered, provider, and model headers in precedence order", () => {
const resolved = resolveProviderRequestConfig({
provider: "custom-openai",
api: "openai-responses",
baseUrl: "https://proxy.example.com/v1",
discoveredHeaders: {
"X-Discovered": "1",
"X-Shared": "discovered",
},
providerHeaders: {
"X-Provider": "2",
"X-Shared": "provider",
},
modelHeaders: {
"X-Model": "3",
"X-Shared": "model",
},
capability: "llm",
transport: "stream",
});
expect(resolved.headers).toEqual({
"X-Discovered": "1",
"X-Provider": "2",
"X-Model": "3",
"X-Shared": "model",
});
});
it("surfaces authHeader intent without mutating headers yet", () => {
const resolved = resolveProviderRequestConfig({
provider: "google",
api: "google-generative-ai",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
authHeader: true,
capability: "llm",
transport: "stream",
});
expect(resolved.auth).toEqual({
configured: false,
mode: "authorization-bearer",
injectAuthorizationHeader: true,
});
expect(resolved.headers).toBeUndefined();
});
it("keeps future proxy and tls slots stable for current callers", () => {
const resolved = resolveProviderRequestConfig({
provider: "openrouter",
api: "openai-responses",
baseUrl: "https://openrouter.ai/api/v1",
capability: "llm",
transport: "stream",
});
expect(resolved.proxy).toEqual({ configured: false });
expect(resolved.tls).toEqual({ configured: false });
expect(resolved.policy.endpointClass).toBe("openrouter");
expect(resolved.policy.attributionProvider).toBe("openrouter");
expect(resolved.extraHeaders).toEqual({
configured: false,
headers: undefined,
});
});
it("normalizes transport overrides into auth, extra headers, proxy, and tls slots", () => {
const resolved = resolveProviderRequestConfig({
provider: "custom-openai",
api: "openai-responses",
baseUrl: "https://proxy.example.com/v1",
request: {
headers: {
"X-Tenant": "acme",
},
auth: {
mode: "header",
headerName: "api-key",
value: "secret",
},
proxy: {
mode: "explicit-proxy",
url: "http://proxy.internal:8443",
tls: {
ca: "proxy-ca",
},
},
tls: {
cert: "client-cert",
key: "client-key",
serverName: "gateway.internal",
},
},
capability: "llm",
transport: "stream",
});
expect(resolved.extraHeaders).toEqual({
configured: true,
headers: {
"X-Tenant": "acme",
"api-key": "secret",
},
});
expect(resolved.auth).toEqual({
configured: true,
mode: "header",
headerName: "api-key",
value: "secret",
injectAuthorizationHeader: false,
});
expect(resolved.proxy).toEqual({
configured: true,
mode: "explicit-proxy",
proxyUrl: "http://proxy.internal:8443",
tls: {
configured: true,
ca: "proxy-ca",
},
});
expect(resolved.tls).toEqual({
configured: true,
cert: "client-cert",
key: "client-key",
serverName: "gateway.internal",
});
});
it("drops legacy Authorization when a custom auth header override is configured", () => {
const resolved = resolveProviderRequestConfig({
provider: "custom-openai",
api: "openai-responses",
baseUrl: "https://proxy.example.com/v1",
providerHeaders: {
Authorization: "Bearer stale-token",
"X-Tenant": "acme",
},
request: {
auth: {
mode: "header",
headerName: "api-key",
value: "secret",
},
},
capability: "llm",
transport: "stream",
});
expect(resolved.headers).toEqual({
"X-Tenant": "acme",
"api-key": "secret",
});
});
it("builds explicit proxy dispatcher policy from normalized transport config", () => {
const resolved = resolveProviderRequestConfig({
provider: "custom-openai",
baseUrl: "https://proxy.example.com/v1",
request: {
proxy: {
mode: "explicit-proxy",
url: "http://proxy.internal:8443",
tls: {
ca: "proxy-ca",
},
},
tls: {
cert: "client-cert",
key: "client-key",
},
},
});
expect(buildProviderRequestDispatcherPolicy(resolved)).toEqual({
mode: "explicit-proxy",
proxyUrl: "http://proxy.internal:8443",
proxyTls: {
ca: "proxy-ca",
},
});
});
it("does not copy target TLS into env proxy TLS", () => {
const resolved = resolveProviderRequestConfig({
provider: "custom-openai",
baseUrl: "https://proxy.example.com/v1",
request: {
proxy: {
mode: "env-proxy",
},
tls: {
cert: "client-cert",
key: "client-key",
serverName: "gateway.internal",
},
},
});
expect(buildProviderRequestDispatcherPolicy(resolved)).toEqual({
mode: "env-proxy",
connect: {
cert: "client-cert",
key: "client-key",
servername: "gateway.internal",
},
});
});
it("rejects insecure TLS transport overrides", () => {
expect(() =>
resolveProviderRequestConfig({
provider: "custom-openai",
baseUrl: "https://proxy.example.com/v1",
request: {
tls: {
insecureSkipVerify: true,
},
},
}),
).toThrow(/insecureskipverify/i);
});
it("rejects proxy and tls runtime auth overrides", () => {
expect(() =>
sanitizeRuntimeProviderRequestOverrides({
headers: {
"X-Tenant": "acme",
},
proxy: {
mode: "explicit-proxy",
url: "http://proxy.internal:8443",
},
}),
).toThrow(/runtime auth request overrides do not allow proxy or tls/i);
});
it("sanitizes configured request overrides into runtime transport overrides", () => {
expect(
sanitizeConfiguredProviderRequest({
headers: {
"X-Tenant": "acme",
},
auth: {
mode: "authorization-bearer",
token: "secret",
},
proxy: {
mode: "explicit-proxy",
url: "http://proxy.internal:8443",
tls: {
ca: "proxy-ca",
},
},
tls: {
cert: "client-cert",
key: "client-key",
serverName: "gateway.internal",
},
}),
).toEqual({
headers: {
"X-Tenant": "acme",
},
auth: {
mode: "authorization-bearer",
token: "secret",
},
proxy: {
mode: "explicit-proxy",
url: "http://proxy.internal:8443",
tls: {
ca: "proxy-ca",
},
},
tls: {
cert: "client-cert",
key: "client-key",
serverName: "gateway.internal",
},
});
});
it("fails fast when configured request overrides still contain unresolved SecretRefs", () => {
expect(() =>
sanitizeConfiguredProviderRequest({
headers: {
"X-Tenant": { source: "env", provider: "default", id: "MEDIA_AUDIO_TENANT" },
},
auth: {
mode: "authorization-bearer",
token: { source: "env", provider: "default", id: "MEDIA_AUDIO_TOKEN" },
},
tls: {
cert: { source: "env", provider: "default", id: "MEDIA_AUDIO_CERT" },
},
}),
).toThrow(/request\.(headers\.X-Tenant|auth\.token|tls\.cert): unresolved SecretRef/i);
});
it("keeps model-provider transport overrides once the llm path can carry them", () => {
expect(
sanitizeConfiguredModelProviderRequest({
headers: {
"X-Tenant": "acme",
},
proxy: {
mode: "explicit-proxy",
url: "http://proxy.internal:8443",
},
}),
).toEqual({
headers: {
"X-Tenant": "acme",
},
proxy: {
mode: "explicit-proxy",
url: "http://proxy.internal:8443",
},
});
});
it("merges configured request overrides with later entries winning", () => {
expect(
mergeProviderRequestOverrides(
{
headers: {
"X-Provider": "1",
"X-Shared": "provider",
},
auth: {
mode: "authorization-bearer",
token: "provider-token",
},
},
{
headers: {
"X-Entry": "2",
"X-Shared": "entry",
},
auth: {
mode: "header",
headerName: "api-key",
value: "entry-key",
},
},
),
).toEqual({
headers: {
"X-Provider": "1",
"X-Shared": "entry",
"X-Entry": "2",
},
auth: {
mode: "header",
headerName: "api-key",
value: "entry-key",
},
});
});
it("lets defaults override caller headers when requested", () => {
const resolved = resolveProviderRequestHeaders({
provider: "openai",
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
capability: "llm",
transport: "stream",
callerHeaders: {
originator: "spoofed",
"User-Agent": "spoofed/0.0.0",
"X-Custom": "1",
},
precedence: "defaults-win",
});
expect(resolved).toMatchObject({
originator: "openclaw",
version: expect.any(String),
"User-Agent": expect.stringMatching(/^openclaw\//),
"X-Custom": "1",
});
});
it("lets caller headers override defaults when requested", () => {
const resolved = resolveProviderRequestHeaders({
provider: "openrouter",
api: "openai-completions",
capability: "llm",
transport: "stream",
callerHeaders: {
"HTTP-Referer": "https://example.com",
"X-Custom": "1",
},
precedence: "caller-wins",
});
expect(resolved).toEqual({
"HTTP-Referer": "https://openclaw.ai",
"X-OpenRouter-Title": "OpenClaw",
"X-OpenRouter-Categories": "cli-agent",
"X-Custom": "1",
});
});
it("merges header names case-insensitively", () => {
const resolved = resolveProviderRequestHeaders({
provider: "openai",
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
capability: "llm",
transport: "stream",
callerHeaders: {
"user-agent": "custom-agent/1.0",
},
precedence: "caller-wins",
});
expect(
Object.keys(resolved ?? {}).filter((key) => key.toLowerCase() === "user-agent"),
).toHaveLength(1);
expect(resolved?.["User-Agent"]).toMatch(/^openclaw\//);
});
it("drops forbidden header keys while merging", () => {
const resolved = resolveProviderRequestHeaders({
provider: "custom-openai",
callerHeaders: {
__proto__: "polluted",
constructor: "polluted",
"X-Custom": "1",
} as Record<string, string>,
defaultHeaders: {
prototype: "polluted",
} as Record<string, string>,
});
expect(resolved).toEqual({
"X-Custom": "1",
});
expect(Object.getPrototypeOf(resolved ?? {})).toBeNull();
});
it("unifies policy, capabilities, headers, base URL, and private-network posture", () => {
const resolved = resolveProviderRequestPolicyConfig({
provider: "openai",
api: "openai-responses",
baseUrl: "https://api.openai.com/v1/",
defaultBaseUrl: "https://fallback.example/v1/",
callerHeaders: {
"User-Agent": "custom-agent/1.0",
"X-Custom": "1",
},
providerHeaders: {
authorization: "Bearer test-key",
},
compat: {
supportsStore: true,
},
capability: "llm",
transport: "stream",
precedence: "defaults-win",
});
expect(resolved.baseUrl).toBe("https://api.openai.com/v1");
expect(resolved.allowPrivateNetwork).toBe(true);
expect(resolved.policy.endpointClass).toBe("openai-public");
expect(resolved.capabilities.allowsResponsesStore).toBe(true);
expect(resolved.headers).toMatchObject({
authorization: "Bearer test-key",
originator: "openclaw",
version: expect.any(String),
"User-Agent": expect.stringMatching(/^openclaw\//),
"X-Custom": "1",
});
});
});