mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 15:41:40 +00:00
714 lines
21 KiB
TypeScript
714 lines
21 KiB
TypeScript
import type { Api } from "@mariozechner/pi-ai";
|
|
import type { ModelDefinitionConfig } from "../config/types.js";
|
|
import type { ConfiguredModelProviderRequest } from "../config/types.provider-request.js";
|
|
import { assertSecretInputResolved } from "../config/types.secrets.js";
|
|
import type { PinnedDispatcherPolicy } from "../infra/net/ssrf.js";
|
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
|
import type {
|
|
ProviderRequestCapabilities,
|
|
ProviderRequestCapability,
|
|
ProviderRequestTransport,
|
|
} from "./provider-attribution.js";
|
|
import {
|
|
resolveProviderRequestCapabilities,
|
|
resolveProviderRequestPolicy,
|
|
type ProviderRequestPolicyResolution,
|
|
} from "./provider-attribution.js";
|
|
|
|
type RequestApi = Api | ModelDefinitionConfig["api"];
|
|
|
|
export type ProviderRequestAuthOverride =
|
|
| {
|
|
mode: "provider-default";
|
|
}
|
|
| {
|
|
mode: "authorization-bearer";
|
|
token: string;
|
|
}
|
|
| {
|
|
mode: "header";
|
|
headerName: string;
|
|
value: string;
|
|
prefix?: string;
|
|
};
|
|
|
|
export type ProviderRequestTlsOverride = {
|
|
ca?: string;
|
|
cert?: string;
|
|
key?: string;
|
|
passphrase?: string;
|
|
serverName?: string;
|
|
insecureSkipVerify?: boolean;
|
|
};
|
|
|
|
export type ProviderRequestProxyOverride =
|
|
| {
|
|
mode: "env-proxy";
|
|
tls?: ProviderRequestTlsOverride;
|
|
}
|
|
| {
|
|
mode: "explicit-proxy";
|
|
url: string;
|
|
tls?: ProviderRequestTlsOverride;
|
|
};
|
|
|
|
export type ProviderRequestTransportOverrides = {
|
|
headers?: Record<string, string>;
|
|
auth?: ProviderRequestAuthOverride;
|
|
proxy?: ProviderRequestProxyOverride;
|
|
tls?: ProviderRequestTlsOverride;
|
|
};
|
|
|
|
export type ResolvedProviderRequestAuthConfig =
|
|
| {
|
|
configured: false;
|
|
mode: "provider-default" | "authorization-bearer";
|
|
injectAuthorizationHeader: boolean;
|
|
}
|
|
| {
|
|
configured: true;
|
|
mode: "authorization-bearer";
|
|
headerName: "Authorization";
|
|
value: string;
|
|
injectAuthorizationHeader: true;
|
|
}
|
|
| {
|
|
configured: true;
|
|
mode: "header";
|
|
headerName: string;
|
|
value: string;
|
|
prefix?: string;
|
|
injectAuthorizationHeader: false;
|
|
};
|
|
|
|
export type ResolvedProviderRequestProxyConfig =
|
|
| {
|
|
configured: false;
|
|
}
|
|
| {
|
|
configured: true;
|
|
mode: "env-proxy";
|
|
tls: ResolvedProviderRequestTlsConfig;
|
|
}
|
|
| {
|
|
configured: true;
|
|
mode: "explicit-proxy";
|
|
proxyUrl: string;
|
|
tls: ResolvedProviderRequestTlsConfig;
|
|
};
|
|
|
|
export type ResolvedProviderRequestTlsConfig =
|
|
| {
|
|
configured: false;
|
|
}
|
|
| {
|
|
configured: true;
|
|
ca?: string;
|
|
cert?: string;
|
|
key?: string;
|
|
passphrase?: string;
|
|
serverName?: string;
|
|
rejectUnauthorized?: boolean;
|
|
};
|
|
|
|
export type ResolvedProviderRequestExtraHeadersConfig = {
|
|
configured: boolean;
|
|
headers?: Record<string, string>;
|
|
};
|
|
|
|
export type ResolvedProviderRequestConfig = {
|
|
api?: RequestApi;
|
|
baseUrl?: string;
|
|
headers?: Record<string, string>;
|
|
extraHeaders: ResolvedProviderRequestExtraHeadersConfig;
|
|
auth: ResolvedProviderRequestAuthConfig;
|
|
proxy: ResolvedProviderRequestProxyConfig;
|
|
tls: ResolvedProviderRequestTlsConfig;
|
|
policy: ProviderRequestPolicyResolution;
|
|
};
|
|
|
|
export type ProviderRequestHeaderPrecedence = "caller-wins" | "defaults-win";
|
|
|
|
export type ResolvedProviderRequestPolicyConfig = ResolvedProviderRequestConfig & {
|
|
allowPrivateNetwork: boolean;
|
|
capabilities: ProviderRequestCapabilities;
|
|
};
|
|
|
|
const FORBIDDEN_HEADER_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
const FORBIDDEN_INSECURE_TLS_MESSAGE =
|
|
"Provider transport overrides do not allow insecureSkipVerify";
|
|
const FORBIDDEN_RUNTIME_TRANSPORT_OVERRIDE_MESSAGE =
|
|
"Runtime auth request overrides do not allow proxy or TLS transport settings";
|
|
|
|
type ResolveProviderRequestPolicyConfigParams = {
|
|
provider?: string;
|
|
api?: RequestApi;
|
|
baseUrl?: string;
|
|
defaultBaseUrl?: string;
|
|
capability?: ProviderRequestCapability;
|
|
transport?: ProviderRequestTransport;
|
|
discoveredHeaders?: Record<string, string>;
|
|
providerHeaders?: Record<string, string>;
|
|
modelHeaders?: Record<string, string>;
|
|
callerHeaders?: Record<string, string>;
|
|
precedence?: ProviderRequestHeaderPrecedence;
|
|
authHeader?: boolean;
|
|
compat?: {
|
|
supportsStore?: boolean;
|
|
} | null;
|
|
modelId?: string | null;
|
|
allowPrivateNetwork?: boolean;
|
|
request?: ProviderRequestTransportOverrides;
|
|
};
|
|
|
|
function sanitizeConfiguredRequestString(value: unknown, path: string): string | undefined {
|
|
if (typeof value !== "string") {
|
|
// Config transport overrides are sanitized after secrets runtime resolution.
|
|
// Fail closed if a raw SecretRef leaks into this path instead of silently dropping it.
|
|
assertSecretInputResolved({ value, path });
|
|
return undefined;
|
|
}
|
|
const trimmed = value.trim();
|
|
return trimmed ? trimmed : undefined;
|
|
}
|
|
|
|
export function sanitizeConfiguredProviderRequest(
|
|
request: ConfiguredModelProviderRequest | ProviderRequestTransportOverrides | undefined,
|
|
): ProviderRequestTransportOverrides | undefined {
|
|
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
return undefined;
|
|
}
|
|
|
|
let headers: Record<string, string> | undefined;
|
|
if (request.headers && typeof request.headers === "object" && !Array.isArray(request.headers)) {
|
|
const nextHeaders: Record<string, string> = {};
|
|
for (const [key, value] of Object.entries(request.headers)) {
|
|
const sanitized = sanitizeConfiguredRequestString(value, `request.headers.${key}`);
|
|
if (sanitized) {
|
|
nextHeaders[key] = sanitized;
|
|
}
|
|
}
|
|
if (Object.keys(nextHeaders).length > 0) {
|
|
headers = nextHeaders;
|
|
}
|
|
}
|
|
|
|
let auth: ProviderRequestAuthOverride | undefined;
|
|
const rawAuth = request.auth;
|
|
if (rawAuth && typeof rawAuth === "object" && !Array.isArray(rawAuth)) {
|
|
if (rawAuth.mode === "provider-default") {
|
|
auth = { mode: "provider-default" };
|
|
} else if (rawAuth.mode === "authorization-bearer") {
|
|
const token = sanitizeConfiguredRequestString(rawAuth.token, "request.auth.token");
|
|
if (token) {
|
|
auth = { mode: "authorization-bearer", token };
|
|
}
|
|
} else if (rawAuth.mode === "header") {
|
|
const headerName = sanitizeConfiguredRequestString(
|
|
rawAuth.headerName,
|
|
"request.auth.headerName",
|
|
);
|
|
const value = sanitizeConfiguredRequestString(rawAuth.value, "request.auth.value");
|
|
const prefix = sanitizeConfiguredRequestString(rawAuth.prefix, "request.auth.prefix");
|
|
if (headerName && value) {
|
|
auth = {
|
|
mode: "header",
|
|
headerName,
|
|
value,
|
|
...(prefix ? { prefix } : {}),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
const sanitizeTls = (
|
|
tls: unknown,
|
|
pathPrefix: "request.tls" | "request.proxy.tls",
|
|
): ProviderRequestTlsOverride | undefined => {
|
|
if (!tls || typeof tls !== "object" || Array.isArray(tls)) {
|
|
return undefined;
|
|
}
|
|
const rawTls = tls as Record<string, unknown>;
|
|
const next: ProviderRequestTlsOverride = {};
|
|
const ca = sanitizeConfiguredRequestString(rawTls.ca, `${pathPrefix}.ca`);
|
|
const cert = sanitizeConfiguredRequestString(rawTls.cert, `${pathPrefix}.cert`);
|
|
const key = sanitizeConfiguredRequestString(rawTls.key, `${pathPrefix}.key`);
|
|
const passphrase = sanitizeConfiguredRequestString(
|
|
rawTls.passphrase,
|
|
`${pathPrefix}.passphrase`,
|
|
);
|
|
const serverName = sanitizeConfiguredRequestString(
|
|
rawTls.serverName,
|
|
`${pathPrefix}.serverName`,
|
|
);
|
|
if (ca) {
|
|
next.ca = ca;
|
|
}
|
|
if (cert) {
|
|
next.cert = cert;
|
|
}
|
|
if (key) {
|
|
next.key = key;
|
|
}
|
|
if (passphrase) {
|
|
next.passphrase = passphrase;
|
|
}
|
|
if (serverName) {
|
|
next.serverName = serverName;
|
|
}
|
|
if (rawTls.insecureSkipVerify === true) {
|
|
next.insecureSkipVerify = true;
|
|
} else if (rawTls.insecureSkipVerify === false) {
|
|
next.insecureSkipVerify = false;
|
|
}
|
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
};
|
|
|
|
let proxy: ProviderRequestProxyOverride | undefined;
|
|
const rawProxy = request.proxy;
|
|
if (rawProxy && typeof rawProxy === "object" && !Array.isArray(rawProxy)) {
|
|
const tls = sanitizeTls(rawProxy.tls, "request.proxy.tls");
|
|
if (rawProxy.mode === "env-proxy") {
|
|
proxy = {
|
|
mode: "env-proxy",
|
|
...(tls ? { tls } : {}),
|
|
};
|
|
} else if (rawProxy.mode === "explicit-proxy") {
|
|
const url = sanitizeConfiguredRequestString(rawProxy.url, "request.proxy.url");
|
|
if (url) {
|
|
proxy = {
|
|
mode: "explicit-proxy",
|
|
url,
|
|
...(tls ? { tls } : {}),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
const tls = sanitizeTls(request.tls, "request.tls");
|
|
|
|
if (!headers && !auth && !proxy && !tls) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...(headers ? { headers } : {}),
|
|
...(auth ? { auth } : {}),
|
|
...(proxy ? { proxy } : {}),
|
|
...(tls ? { tls } : {}),
|
|
};
|
|
}
|
|
|
|
export function sanitizeConfiguredModelProviderRequest(
|
|
request: ConfiguredModelProviderRequest | undefined,
|
|
): ProviderRequestTransportOverrides | undefined {
|
|
return sanitizeConfiguredProviderRequest(request);
|
|
}
|
|
|
|
export function mergeProviderRequestOverrides(
|
|
...overrides: Array<ProviderRequestTransportOverrides | undefined>
|
|
): ProviderRequestTransportOverrides | undefined {
|
|
let merged: ProviderRequestTransportOverrides | undefined;
|
|
for (const current of overrides) {
|
|
if (!current) {
|
|
continue;
|
|
}
|
|
merged = {
|
|
...merged,
|
|
...(current.headers
|
|
? {
|
|
headers: {
|
|
...merged?.headers,
|
|
...current.headers,
|
|
},
|
|
}
|
|
: {}),
|
|
...(current.auth ? { auth: current.auth } : {}),
|
|
...(current.proxy ? { proxy: current.proxy } : {}),
|
|
...(current.tls ? { tls: current.tls } : {}),
|
|
};
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
export function normalizeBaseUrl(baseUrl: string | undefined, fallback: string): string;
|
|
export function normalizeBaseUrl(
|
|
baseUrl: string | undefined,
|
|
fallback?: string,
|
|
): string | undefined;
|
|
export function normalizeBaseUrl(
|
|
baseUrl: string | undefined,
|
|
fallback?: string,
|
|
): string | undefined {
|
|
const raw = baseUrl?.trim() || fallback?.trim();
|
|
if (!raw) {
|
|
return undefined;
|
|
}
|
|
return raw.replace(/\/+$/, "");
|
|
}
|
|
|
|
export function mergeProviderRequestHeaders(
|
|
...headerSets: Array<Record<string, string> | undefined>
|
|
): Record<string, string> | undefined {
|
|
let merged: Record<string, string> | undefined;
|
|
const headerNamesByLowerKey = new Map<string, string>();
|
|
for (const headers of headerSets) {
|
|
if (!headers) {
|
|
continue;
|
|
}
|
|
if (!merged) {
|
|
merged = Object.create(null) as Record<string, string>;
|
|
}
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
const normalizedKey = normalizeLowercaseStringOrEmpty(key);
|
|
if (FORBIDDEN_HEADER_KEYS.has(normalizedKey)) {
|
|
continue;
|
|
}
|
|
const previousKey = headerNamesByLowerKey.get(normalizedKey);
|
|
if (previousKey && previousKey !== key) {
|
|
delete merged[previousKey];
|
|
}
|
|
merged[key] = value;
|
|
headerNamesByLowerKey.set(normalizedKey, key);
|
|
}
|
|
}
|
|
return merged && Object.keys(merged).length > 0 ? merged : undefined;
|
|
}
|
|
|
|
function resolveTlsOverride(
|
|
tls: ProviderRequestTlsOverride | undefined,
|
|
): ResolvedProviderRequestTlsConfig {
|
|
if (!tls) {
|
|
return { configured: false };
|
|
}
|
|
if (tls.insecureSkipVerify === true) {
|
|
throw new Error(FORBIDDEN_INSECURE_TLS_MESSAGE);
|
|
}
|
|
const ca = tls.ca?.trim();
|
|
const cert = tls.cert?.trim();
|
|
const key = tls.key?.trim();
|
|
const passphrase = tls.passphrase?.trim();
|
|
const serverName = tls.serverName?.trim();
|
|
const rejectUnauthorized = tls.insecureSkipVerify === false ? true : undefined;
|
|
if (!ca && !cert && !key && !passphrase && !serverName && rejectUnauthorized === undefined) {
|
|
return { configured: false };
|
|
}
|
|
return {
|
|
configured: true,
|
|
...(ca ? { ca } : {}),
|
|
...(cert ? { cert } : {}),
|
|
...(key ? { key } : {}),
|
|
...(passphrase ? { passphrase } : {}),
|
|
...(serverName ? { serverName } : {}),
|
|
...(rejectUnauthorized !== undefined ? { rejectUnauthorized } : {}),
|
|
};
|
|
}
|
|
|
|
function resolveAuthOverride(params: {
|
|
authHeader?: boolean;
|
|
request?: ProviderRequestTransportOverrides;
|
|
}): ResolvedProviderRequestAuthConfig {
|
|
const auth = params.request?.auth;
|
|
if (auth?.mode === "authorization-bearer") {
|
|
const value = auth.token.trim();
|
|
if (value) {
|
|
return {
|
|
configured: true,
|
|
mode: "authorization-bearer",
|
|
headerName: "Authorization",
|
|
value,
|
|
injectAuthorizationHeader: true,
|
|
};
|
|
}
|
|
}
|
|
if (auth?.mode === "header") {
|
|
const headerName = auth.headerName.trim();
|
|
const value = auth.value.trim();
|
|
const prefix = auth.prefix?.trim();
|
|
if (headerName && value) {
|
|
return {
|
|
configured: true,
|
|
mode: "header",
|
|
headerName,
|
|
value,
|
|
...(prefix ? { prefix } : {}),
|
|
injectAuthorizationHeader: false,
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
configured: false,
|
|
mode: params.authHeader ? "authorization-bearer" : "provider-default",
|
|
injectAuthorizationHeader: params.authHeader === true,
|
|
};
|
|
}
|
|
|
|
export function sanitizeRuntimeProviderRequestOverrides(
|
|
request: ProviderRequestTransportOverrides | undefined,
|
|
): ProviderRequestTransportOverrides | undefined {
|
|
if (!request) {
|
|
return undefined;
|
|
}
|
|
if (request.proxy || request.tls) {
|
|
throw new Error(FORBIDDEN_RUNTIME_TRANSPORT_OVERRIDE_MESSAGE);
|
|
}
|
|
const headers = request.headers;
|
|
const auth = request.auth;
|
|
if (!headers && !auth) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...(headers ? { headers } : {}),
|
|
...(auth ? { auth } : {}),
|
|
};
|
|
}
|
|
|
|
function resolveProxyOverride(
|
|
request: ProviderRequestTransportOverrides | undefined,
|
|
): ResolvedProviderRequestProxyConfig {
|
|
const proxy = request?.proxy;
|
|
if (!proxy) {
|
|
return { configured: false };
|
|
}
|
|
const tls = resolveTlsOverride(proxy.tls);
|
|
if (proxy.mode === "env-proxy") {
|
|
return {
|
|
configured: true,
|
|
mode: "env-proxy",
|
|
tls,
|
|
};
|
|
}
|
|
const proxyUrl = proxy.url.trim();
|
|
if (!proxyUrl) {
|
|
return { configured: false };
|
|
}
|
|
return {
|
|
configured: true,
|
|
mode: "explicit-proxy",
|
|
proxyUrl,
|
|
tls,
|
|
};
|
|
}
|
|
|
|
function applyResolvedAuthHeader(
|
|
headers: Record<string, string> | undefined,
|
|
auth: ResolvedProviderRequestAuthConfig,
|
|
): Record<string, string> | undefined {
|
|
if (!auth.configured) {
|
|
return headers;
|
|
}
|
|
const next = mergeProviderRequestHeaders(headers) ?? Object.create(null);
|
|
const keysToDelete = new Set([normalizeLowercaseStringOrEmpty(auth.headerName)]);
|
|
if (auth.mode === "header") {
|
|
keysToDelete.add("authorization");
|
|
}
|
|
for (const key of Object.keys(next)) {
|
|
if (keysToDelete.has(normalizeLowercaseStringOrEmpty(key))) {
|
|
delete next[key];
|
|
}
|
|
}
|
|
next[auth.headerName] =
|
|
auth.mode === "authorization-bearer"
|
|
? `Bearer ${auth.value}`
|
|
: `${auth.prefix ?? ""}${auth.value}`;
|
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
}
|
|
|
|
function toTlsConnectOptions(
|
|
tls: ResolvedProviderRequestTlsConfig,
|
|
): Record<string, unknown> | undefined {
|
|
if (!tls.configured) {
|
|
return undefined;
|
|
}
|
|
const next: Record<string, unknown> = {};
|
|
if (tls.ca) {
|
|
next.ca = tls.ca;
|
|
}
|
|
if (tls.cert) {
|
|
next.cert = tls.cert;
|
|
}
|
|
if (tls.key) {
|
|
next.key = tls.key;
|
|
}
|
|
if (tls.passphrase) {
|
|
next.passphrase = tls.passphrase;
|
|
}
|
|
if (tls.serverName) {
|
|
next.servername = tls.serverName;
|
|
}
|
|
if (tls.rejectUnauthorized !== undefined) {
|
|
next.rejectUnauthorized = tls.rejectUnauthorized;
|
|
}
|
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
}
|
|
|
|
export function buildProviderRequestDispatcherPolicy(
|
|
request: Pick<ResolvedProviderRequestConfig, "proxy" | "tls">,
|
|
): PinnedDispatcherPolicy | undefined {
|
|
const targetTls = toTlsConnectOptions(request.tls);
|
|
if (!request.proxy.configured) {
|
|
return targetTls ? { mode: "direct", connect: targetTls } : undefined;
|
|
}
|
|
const proxiedTls = toTlsConnectOptions(request.proxy.tls);
|
|
if (request.proxy.mode === "env-proxy") {
|
|
return {
|
|
mode: "env-proxy",
|
|
...(targetTls ? { connect: { ...targetTls } } : {}),
|
|
...(proxiedTls ? { proxyTls: { ...proxiedTls } } : {}),
|
|
};
|
|
}
|
|
return {
|
|
mode: "explicit-proxy",
|
|
proxyUrl: request.proxy.proxyUrl,
|
|
...(proxiedTls ? { proxyTls: proxiedTls } : {}),
|
|
};
|
|
}
|
|
|
|
export function buildProviderRequestTlsClientOptions(
|
|
request: Pick<ResolvedProviderRequestConfig, "tls">,
|
|
): Record<string, unknown> | undefined {
|
|
return toTlsConnectOptions(request.tls);
|
|
}
|
|
|
|
export function resolveProviderRequestPolicyConfig(
|
|
params: ResolveProviderRequestPolicyConfigParams,
|
|
): ResolvedProviderRequestPolicyConfig {
|
|
const baseUrl = normalizeBaseUrl(params.baseUrl, params.defaultBaseUrl);
|
|
const capability = params.capability ?? "llm";
|
|
const transport = params.transport ?? "http";
|
|
const policyInput = {
|
|
provider: params.provider,
|
|
api: params.api,
|
|
baseUrl,
|
|
capability,
|
|
transport,
|
|
} satisfies Parameters<typeof resolveProviderRequestPolicy>[0];
|
|
const policy = resolveProviderRequestPolicy(policyInput);
|
|
const capabilities = resolveProviderRequestCapabilities({
|
|
...policyInput,
|
|
compat: params.compat,
|
|
modelId: params.modelId,
|
|
});
|
|
const auth = resolveAuthOverride({
|
|
authHeader: params.authHeader,
|
|
request: params.request,
|
|
});
|
|
const extraHeaders = applyResolvedAuthHeader(
|
|
mergeProviderRequestHeaders(
|
|
params.discoveredHeaders,
|
|
params.providerHeaders,
|
|
params.modelHeaders,
|
|
params.request?.headers,
|
|
),
|
|
auth,
|
|
);
|
|
const protectedAttributionKeys = new Set(
|
|
Object.keys(policy.attributionHeaders ?? {}).map((key) => normalizeLowercaseStringOrEmpty(key)),
|
|
);
|
|
const unprotectedCallerHeaders = params.callerHeaders
|
|
? Object.fromEntries(
|
|
Object.entries(params.callerHeaders).filter(
|
|
([key]) => !protectedAttributionKeys.has(normalizeLowercaseStringOrEmpty(key)),
|
|
),
|
|
)
|
|
: undefined;
|
|
const mergedDefaults = mergeProviderRequestHeaders(extraHeaders, policy.attributionHeaders);
|
|
const headers =
|
|
params.precedence === "caller-wins"
|
|
? mergeProviderRequestHeaders(mergedDefaults, unprotectedCallerHeaders)
|
|
: mergeProviderRequestHeaders(unprotectedCallerHeaders, mergedDefaults);
|
|
|
|
return {
|
|
api: params.api,
|
|
baseUrl,
|
|
headers,
|
|
extraHeaders: {
|
|
configured: Boolean(extraHeaders),
|
|
headers: extraHeaders,
|
|
},
|
|
auth,
|
|
proxy: resolveProxyOverride(params.request),
|
|
tls: resolveTlsOverride(params.request?.tls),
|
|
policy,
|
|
capabilities,
|
|
allowPrivateNetwork: params.allowPrivateNetwork ?? false,
|
|
};
|
|
}
|
|
|
|
export function resolveProviderRequestConfig(params: {
|
|
provider: string;
|
|
api?: RequestApi;
|
|
baseUrl?: string;
|
|
capability?: ProviderRequestCapability;
|
|
transport?: ProviderRequestTransport;
|
|
discoveredHeaders?: Record<string, string>;
|
|
providerHeaders?: Record<string, string>;
|
|
modelHeaders?: Record<string, string>;
|
|
authHeader?: boolean;
|
|
request?: ProviderRequestTransportOverrides;
|
|
}): ResolvedProviderRequestConfig {
|
|
const resolved = resolveProviderRequestPolicyConfig(params);
|
|
return {
|
|
api: resolved.api,
|
|
baseUrl: resolved.baseUrl,
|
|
// Model resolution intentionally excludes attribution headers. Those are
|
|
// applied later at transport/request time so native-host gating stays tied
|
|
// to the final resolved route instead of the catalog/config merge step.
|
|
headers: resolved.extraHeaders.headers,
|
|
extraHeaders: resolved.extraHeaders,
|
|
auth: resolved.auth,
|
|
proxy: resolved.proxy,
|
|
tls: resolved.tls,
|
|
policy: resolved.policy,
|
|
};
|
|
}
|
|
|
|
export function resolveProviderRequestHeaders(params: {
|
|
provider: string;
|
|
api?: RequestApi;
|
|
baseUrl?: string;
|
|
capability?: ProviderRequestCapability;
|
|
transport?: ProviderRequestTransport;
|
|
callerHeaders?: Record<string, string>;
|
|
defaultHeaders?: Record<string, string>;
|
|
precedence?: ProviderRequestHeaderPrecedence;
|
|
request?: ProviderRequestTransportOverrides;
|
|
}): Record<string, string> | undefined {
|
|
return resolveProviderRequestPolicyConfig({
|
|
provider: params.provider,
|
|
api: params.api,
|
|
baseUrl: params.baseUrl,
|
|
capability: params.capability,
|
|
transport: params.transport,
|
|
callerHeaders: params.callerHeaders,
|
|
providerHeaders: params.defaultHeaders,
|
|
precedence: params.precedence,
|
|
request: params.request,
|
|
}).headers;
|
|
}
|
|
|
|
const MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL = Symbol.for(
|
|
"openclaw.modelProviderRequestTransport",
|
|
);
|
|
|
|
type ModelWithProviderRequestTransport = {
|
|
[MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL]?: ProviderRequestTransportOverrides;
|
|
};
|
|
|
|
export function attachModelProviderRequestTransport<TModel extends object>(
|
|
model: TModel,
|
|
request: ProviderRequestTransportOverrides | undefined,
|
|
): TModel {
|
|
if (!request) {
|
|
return model;
|
|
}
|
|
const next = { ...model } as TModel & ModelWithProviderRequestTransport;
|
|
next[MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL] = request;
|
|
return next;
|
|
}
|
|
|
|
export function getModelProviderRequestTransport(
|
|
model: object,
|
|
): ProviderRequestTransportOverrides | undefined {
|
|
return (model as ModelWithProviderRequestTransport)[MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL];
|
|
}
|