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:
Vincent Koc
2026-04-02 15:28:57 +09:00
committed by GitHub
parent 9786946b2d
commit f28f0f29ba
9 changed files with 202 additions and 63 deletions

View File

@@ -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 {

View 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");
});
});

View File

@@ -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,