mirror of https://github.com/openclaw/openclaw.git
490 lines
13 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|