mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 15:10:22 +00:00
fix(providers): centralize media request shaping (#59469)
* fix(providers): centralize media request shaping * style(providers): normalize shared request imports * fix(changelog): add media request shaping entry * fix(google): preserve private network guard
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import path from "node:path";
|
||||
import {
|
||||
applyProviderRequestHeaders,
|
||||
assertOkOrThrowHttpError,
|
||||
normalizeBaseUrl,
|
||||
postTranscriptionRequest,
|
||||
resolveProviderHttpRequestConfig,
|
||||
requireTranscriptionText,
|
||||
} from "./shared.js";
|
||||
import type { AudioTranscriptionRequest, AudioTranscriptionResult } from "./types.js";
|
||||
@@ -23,8 +22,18 @@ export async function transcribeOpenAiCompatibleAudio(
|
||||
params: OpenAiCompatibleAudioParams,
|
||||
): Promise<AudioTranscriptionResult> {
|
||||
const fetchFn = params.fetchFn ?? fetch;
|
||||
const baseUrl = normalizeBaseUrl(params.baseUrl, params.defaultBaseUrl);
|
||||
const allowPrivate = Boolean(params.baseUrl?.trim());
|
||||
const { baseUrl, allowPrivateNetwork, headers } = resolveProviderHttpRequestConfig({
|
||||
baseUrl: params.baseUrl,
|
||||
defaultBaseUrl: params.defaultBaseUrl,
|
||||
headers: params.headers,
|
||||
defaultHeaders: {
|
||||
authorization: `Bearer ${params.apiKey}`,
|
||||
},
|
||||
provider: params.provider,
|
||||
api: "openai-audio-transcriptions",
|
||||
capability: "audio",
|
||||
transport: "media-understanding",
|
||||
});
|
||||
const url = `${baseUrl}/audio/transcriptions`;
|
||||
|
||||
const model = resolveModel(params.model, params.defaultModel);
|
||||
@@ -43,25 +52,13 @@ export async function transcribeOpenAiCompatibleAudio(
|
||||
form.append("prompt", params.prompt.trim());
|
||||
}
|
||||
|
||||
const headers = applyProviderRequestHeaders({
|
||||
headers: params.headers,
|
||||
provider: params.provider,
|
||||
api: "openai-audio-transcriptions",
|
||||
baseUrl,
|
||||
capability: "audio",
|
||||
transport: "media-understanding",
|
||||
});
|
||||
if (!headers.has("authorization")) {
|
||||
headers.set("authorization", `Bearer ${params.apiKey}`);
|
||||
}
|
||||
|
||||
const { response: res, release } = await postTranscriptionRequest({
|
||||
url,
|
||||
headers,
|
||||
body: form,
|
||||
timeoutMs: params.timeoutMs,
|
||||
fetchFn,
|
||||
allowPrivateNetwork: allowPrivate,
|
||||
allowPrivateNetwork,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
66
src/media-understanding/shared.test.ts
Normal file
66
src/media-understanding/shared.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProviderHttpRequestConfig } from "./shared.js";
|
||||
|
||||
describe("resolveProviderHttpRequestConfig", () => {
|
||||
it("preserves explicit caller headers over default and attribution headers", () => {
|
||||
const resolved = resolveProviderHttpRequestConfig({
|
||||
baseUrl: "https://api.openai.com/v1/",
|
||||
defaultBaseUrl: "https://api.openai.com/v1",
|
||||
headers: {
|
||||
authorization: "Bearer override",
|
||||
"User-Agent": "custom-agent/1.0",
|
||||
},
|
||||
defaultHeaders: {
|
||||
authorization: "Bearer default-token",
|
||||
"X-Default": "1",
|
||||
},
|
||||
provider: "openai",
|
||||
api: "openai-audio-transcriptions",
|
||||
capability: "audio",
|
||||
transport: "media-understanding",
|
||||
});
|
||||
|
||||
expect(resolved.baseUrl).toBe("https://api.openai.com/v1");
|
||||
expect(resolved.allowPrivateNetwork).toBe(true);
|
||||
expect(resolved.headers.get("authorization")).toBe("Bearer override");
|
||||
expect(resolved.headers.get("x-default")).toBe("1");
|
||||
expect(resolved.headers.get("user-agent")).toBe("custom-agent/1.0");
|
||||
expect(resolved.headers.get("originator")).toBe("openclaw");
|
||||
expect(resolved.headers.get("version")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses the fallback base URL without enabling private-network access", () => {
|
||||
const resolved = resolveProviderHttpRequestConfig({
|
||||
defaultBaseUrl: "https://api.deepgram.com/v1/",
|
||||
defaultHeaders: {
|
||||
authorization: "Token test-key",
|
||||
},
|
||||
provider: "deepgram",
|
||||
capability: "audio",
|
||||
transport: "media-understanding",
|
||||
});
|
||||
|
||||
expect(resolved.baseUrl).toBe("https://api.deepgram.com/v1");
|
||||
expect(resolved.allowPrivateNetwork).toBe(false);
|
||||
expect(resolved.headers.get("authorization")).toBe("Token test-key");
|
||||
});
|
||||
|
||||
it("allows callers to preserve custom-base detection before URL normalization", () => {
|
||||
const resolved = resolveProviderHttpRequestConfig({
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
allowPrivateNetwork: false,
|
||||
defaultHeaders: {
|
||||
"x-goog-api-key": "test-key",
|
||||
},
|
||||
provider: "google",
|
||||
api: "google-generative-ai",
|
||||
capability: "image",
|
||||
transport: "http",
|
||||
});
|
||||
|
||||
expect(resolved.baseUrl).toBe("https://generativelanguage.googleapis.com/v1beta");
|
||||
expect(resolved.allowPrivateNetwork).toBe(false);
|
||||
expect(resolved.headers.get("x-goog-api-key")).toBe("test-key");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,15 @@
|
||||
import type {
|
||||
ProviderRequestCapability,
|
||||
ProviderRequestTransport,
|
||||
} from "../agents/provider-attribution.js";
|
||||
import { resolveProviderRequestAttributionHeaders } from "../agents/provider-attribution.js";
|
||||
import {
|
||||
resolveProviderRequestConfig,
|
||||
type ResolvedProviderRequestConfig,
|
||||
} from "../agents/provider-request-config.js";
|
||||
import type { GuardedFetchResult } from "../infra/net/fetch-guard.js";
|
||||
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
import type { LookupFn, SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { resolveProviderRequestAttributionHeaders } from "../agents/provider-attribution.js";
|
||||
export { fetchWithTimeout } from "../utils/fetch-timeout.js";
|
||||
|
||||
const MAX_ERROR_CHARS = 300;
|
||||
@@ -13,13 +21,21 @@ export function normalizeBaseUrl(baseUrl: string | undefined, fallback: string):
|
||||
|
||||
export function applyProviderRequestHeaders(params: {
|
||||
headers?: HeadersInit;
|
||||
defaultHeaders?: Record<string, string>;
|
||||
provider?: string;
|
||||
api?: string;
|
||||
baseUrl?: string;
|
||||
capability?: "audio" | "image" | "video" | "other";
|
||||
transport?: "http" | "media-understanding";
|
||||
capability?: ProviderRequestCapability;
|
||||
transport?: ProviderRequestTransport;
|
||||
}): Headers {
|
||||
const headers = new Headers(params.headers);
|
||||
if (params.defaultHeaders) {
|
||||
for (const [key, value] of Object.entries(params.defaultHeaders)) {
|
||||
if (!headers.has(key)) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
const attributionHeaders = resolveProviderRequestAttributionHeaders({
|
||||
provider: params.provider,
|
||||
api: params.api,
|
||||
@@ -38,6 +54,53 @@ export function applyProviderRequestHeaders(params: {
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function resolveProviderHttpRequestConfig(params: {
|
||||
baseUrl?: string;
|
||||
defaultBaseUrl: string;
|
||||
allowPrivateNetwork?: boolean;
|
||||
headers?: HeadersInit;
|
||||
defaultHeaders?: Record<string, string>;
|
||||
provider?: string;
|
||||
api?: string;
|
||||
capability?: ProviderRequestCapability;
|
||||
transport?: ProviderRequestTransport;
|
||||
}): {
|
||||
baseUrl: string;
|
||||
allowPrivateNetwork: boolean;
|
||||
headers: Headers;
|
||||
requestConfig: ResolvedProviderRequestConfig;
|
||||
} {
|
||||
const baseUrl = normalizeBaseUrl(params.baseUrl, params.defaultBaseUrl);
|
||||
const requestConfigParams: Parameters<typeof resolveProviderRequestConfig>[0] = {
|
||||
provider: params.provider ?? "",
|
||||
baseUrl,
|
||||
capability: params.capability ?? "other",
|
||||
transport: params.transport ?? "http",
|
||||
};
|
||||
if (params.api !== undefined) {
|
||||
requestConfigParams.api = params.api;
|
||||
}
|
||||
if (params.defaultHeaders !== undefined) {
|
||||
requestConfigParams.providerHeaders = params.defaultHeaders;
|
||||
}
|
||||
const requestConfig = resolveProviderRequestConfig(requestConfigParams);
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
allowPrivateNetwork: params.allowPrivateNetwork ?? Boolean(params.baseUrl?.trim()),
|
||||
headers: applyProviderRequestHeaders({
|
||||
headers: params.headers,
|
||||
defaultHeaders: requestConfig.headers,
|
||||
provider: params.provider,
|
||||
api: params.api,
|
||||
baseUrl,
|
||||
capability: params.capability,
|
||||
transport: params.transport,
|
||||
}),
|
||||
requestConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchWithTimeoutGuarded(
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
|
||||
Reference in New Issue
Block a user