mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
refactor(executors): extract pure upstream-header helpers from base (#6008)
Extract the pure upstream-header helpers (mergeUpstreamExtraHeaders, getCustomUserAgent, setUserAgentHeader, applyConfiguredUserAgent, isOpenAICompatibleEndpoint, stripStainlessHeadersForOpenAICompat) verbatim into the leaf base/headers.ts. base.ts is imported by ~18 executors, so the host re-exports all 6 to keep those import paths intact; it also imports the 4 it uses internally in the BaseExecutor class. The trivial JsonRecord type alias is redefined locally in the leaf to avoid a base<->leaf cycle. Host 1539 -> 1451 LOC. Byte-identical bodies (verbatim 78/78), leaf does not import the host (no cycle). typecheck:core validates all base importers still resolve via the re-export. Adds a split-guard; consumer tests stay green (executor-base-utils 22, executor-default-base 49, executor-strip-stainless-openai-compat 6, plus executor sanity via typecheck).
This commit is contained in:
committed by
GitHub
parent
1a73dd2936
commit
3a3d618fe5
@@ -66,6 +66,22 @@ import {
|
|||||||
stripProxyToolPrefix,
|
stripProxyToolPrefix,
|
||||||
} from "./claudeIdentity.ts";
|
} from "./claudeIdentity.ts";
|
||||||
import { withForcedResponsesUpstream } from "./forceResponsesUpstream.ts";
|
import { withForcedResponsesUpstream } from "./forceResponsesUpstream.ts";
|
||||||
|
import {
|
||||||
|
mergeUpstreamExtraHeaders,
|
||||||
|
setUserAgentHeader,
|
||||||
|
applyConfiguredUserAgent,
|
||||||
|
stripStainlessHeadersForOpenAICompat,
|
||||||
|
} from "./base/headers.ts";
|
||||||
|
// Header helpers extracted to a pure leaf; re-exported for external importers
|
||||||
|
// (executors + tests) that import them from "./base.ts".
|
||||||
|
export {
|
||||||
|
mergeUpstreamExtraHeaders,
|
||||||
|
getCustomUserAgent,
|
||||||
|
setUserAgentHeader,
|
||||||
|
applyConfiguredUserAgent,
|
||||||
|
isOpenAICompatibleEndpoint,
|
||||||
|
stripStainlessHeadersForOpenAICompat,
|
||||||
|
} from "./base/headers.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes a custom API path to prevent path traversal attacks.
|
* Sanitizes a custom API path to prevent path traversal attacks.
|
||||||
@@ -155,95 +171,6 @@ export type CountTokensInput = {
|
|||||||
signal?: AbortSignal | null;
|
signal?: AbortSignal | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Apply model-level extra upstream headers (e.g. Authentication, X-Custom-Auth). */
|
|
||||||
export function mergeUpstreamExtraHeaders(
|
|
||||||
headers: Record<string, string>,
|
|
||||||
extra?: Record<string, string> | null
|
|
||||||
): void {
|
|
||||||
if (!extra) return;
|
|
||||||
for (const [k, v] of Object.entries(extra)) {
|
|
||||||
if (typeof k === "string" && k.length > 0 && typeof v === "string") {
|
|
||||||
if (k.toLowerCase() === "user-agent") {
|
|
||||||
setUserAgentHeader(headers, v);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
headers[k] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCustomUserAgent(providerSpecificData?: JsonRecord | null): string | null {
|
|
||||||
const customUserAgent =
|
|
||||||
typeof providerSpecificData?.customUserAgent === "string"
|
|
||||||
? providerSpecificData.customUserAgent.trim()
|
|
||||||
: "";
|
|
||||||
return customUserAgent || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setUserAgentHeader(headers: Record<string, string>, userAgent: string): void {
|
|
||||||
headers["User-Agent"] = userAgent;
|
|
||||||
if ("user-agent" in headers) {
|
|
||||||
headers["user-agent"] = userAgent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyConfiguredUserAgent(
|
|
||||||
headers: Record<string, string>,
|
|
||||||
providerSpecificData?: JsonRecord | null
|
|
||||||
): void {
|
|
||||||
const customUserAgent = getCustomUserAgent(providerSpecificData);
|
|
||||||
if (customUserAgent) {
|
|
||||||
setUserAgentHeader(headers, customUserAgent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true when the outbound request targets an OpenAI-compatible endpoint
|
|
||||||
* (a `openai-compatible-*` provider, or a Chat Completions / Responses URL).
|
|
||||||
* Used to scope the X-Stainless strip narrowly so genuine SDK-spoofing paths
|
|
||||||
* (e.g. Claude Code compat, which legitimately ADDS X-Stainless-*) are untouched.
|
|
||||||
*/
|
|
||||||
export function isOpenAICompatibleEndpoint(provider: string, url: string): boolean {
|
|
||||||
if (provider?.startsWith?.("openai-compatible-")) return true;
|
|
||||||
return url.includes("/v1/chat/completions") || url.includes("/v1/responses");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip OpenAI SDK (`X-Stainless-*`) metadata headers and normalize an SDK-derived
|
|
||||||
* User-Agent for OpenAI-compatible passthrough requests. Some upstream gateways
|
|
||||||
* 403 on these SDK-identifying headers. Only applied to OpenAI-compatible endpoints —
|
|
||||||
* other providers (Claude/Claude Code compat) may legitimately send X-Stainless-*.
|
|
||||||
*
|
|
||||||
* Mutates `headers` in place and returns the list of stripped header keys (for logging).
|
|
||||||
*/
|
|
||||||
export function stripStainlessHeadersForOpenAICompat(
|
|
||||||
headers: Record<string, string>,
|
|
||||||
provider: string,
|
|
||||||
url: string
|
|
||||||
): string[] {
|
|
||||||
if (!isOpenAICompatibleEndpoint(provider, url)) return [];
|
|
||||||
|
|
||||||
const strippedKeys: string[] = [];
|
|
||||||
for (const key of Object.keys(headers)) {
|
|
||||||
if (key.toLowerCase().startsWith("x-stainless-")) {
|
|
||||||
delete headers[key];
|
|
||||||
strippedKeys.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize User-Agent: SDK-based clients send verbose product strings that some
|
|
||||||
// upstreams block. Replace with a clean browser-like UA only when it looks SDK-derived.
|
|
||||||
const ua = (headers["User-Agent"] || headers["user-agent"] || "").toLowerCase();
|
|
||||||
if (
|
|
||||||
ua.includes("openai") &&
|
|
||||||
(ua.includes("node") || ua.includes("axios") || ua.includes("undici"))
|
|
||||||
) {
|
|
||||||
setUserAgentHeader(headers, "Mozilla/5.0 (compatible; OpenAI Compatible)");
|
|
||||||
}
|
|
||||||
|
|
||||||
return strippedKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
|
export function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
|||||||
93
open-sse/executors/base/headers.ts
Normal file
93
open-sse/executors/base/headers.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Pure upstream header helpers (User-Agent, extra headers, OpenAI-compat stripping).
|
||||||
|
// Extracted verbatim from base.ts. Module-private JsonRecord kept local to avoid a cycle.
|
||||||
|
|
||||||
|
type JsonRecord = Record<string, unknown>;
|
||||||
|
|
||||||
|
/** Apply model-level extra upstream headers (e.g. Authentication, X-Custom-Auth). */
|
||||||
|
export function mergeUpstreamExtraHeaders(
|
||||||
|
headers: Record<string, string>,
|
||||||
|
extra?: Record<string, string> | null
|
||||||
|
): void {
|
||||||
|
if (!extra) return;
|
||||||
|
for (const [k, v] of Object.entries(extra)) {
|
||||||
|
if (typeof k === "string" && k.length > 0 && typeof v === "string") {
|
||||||
|
if (k.toLowerCase() === "user-agent") {
|
||||||
|
setUserAgentHeader(headers, v);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
headers[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCustomUserAgent(providerSpecificData?: JsonRecord | null): string | null {
|
||||||
|
const customUserAgent =
|
||||||
|
typeof providerSpecificData?.customUserAgent === "string"
|
||||||
|
? providerSpecificData.customUserAgent.trim()
|
||||||
|
: "";
|
||||||
|
return customUserAgent || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUserAgentHeader(headers: Record<string, string>, userAgent: string): void {
|
||||||
|
headers["User-Agent"] = userAgent;
|
||||||
|
if ("user-agent" in headers) {
|
||||||
|
headers["user-agent"] = userAgent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyConfiguredUserAgent(
|
||||||
|
headers: Record<string, string>,
|
||||||
|
providerSpecificData?: JsonRecord | null
|
||||||
|
): void {
|
||||||
|
const customUserAgent = getCustomUserAgent(providerSpecificData);
|
||||||
|
if (customUserAgent) {
|
||||||
|
setUserAgentHeader(headers, customUserAgent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the outbound request targets an OpenAI-compatible endpoint
|
||||||
|
* (a `openai-compatible-*` provider, or a Chat Completions / Responses URL).
|
||||||
|
* Used to scope the X-Stainless strip narrowly so genuine SDK-spoofing paths
|
||||||
|
* (e.g. Claude Code compat, which legitimately ADDS X-Stainless-*) are untouched.
|
||||||
|
*/
|
||||||
|
export function isOpenAICompatibleEndpoint(provider: string, url: string): boolean {
|
||||||
|
if (provider?.startsWith?.("openai-compatible-")) return true;
|
||||||
|
return url.includes("/v1/chat/completions") || url.includes("/v1/responses");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip OpenAI SDK (`X-Stainless-*`) metadata headers and normalize an SDK-derived
|
||||||
|
* User-Agent for OpenAI-compatible passthrough requests. Some upstream gateways
|
||||||
|
* 403 on these SDK-identifying headers. Only applied to OpenAI-compatible endpoints —
|
||||||
|
* other providers (Claude/Claude Code compat) may legitimately send X-Stainless-*.
|
||||||
|
*
|
||||||
|
* Mutates `headers` in place and returns the list of stripped header keys (for logging).
|
||||||
|
*/
|
||||||
|
export function stripStainlessHeadersForOpenAICompat(
|
||||||
|
headers: Record<string, string>,
|
||||||
|
provider: string,
|
||||||
|
url: string
|
||||||
|
): string[] {
|
||||||
|
if (!isOpenAICompatibleEndpoint(provider, url)) return [];
|
||||||
|
|
||||||
|
const strippedKeys: string[] = [];
|
||||||
|
for (const key of Object.keys(headers)) {
|
||||||
|
if (key.toLowerCase().startsWith("x-stainless-")) {
|
||||||
|
delete headers[key];
|
||||||
|
strippedKeys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize User-Agent: SDK-based clients send verbose product strings that some
|
||||||
|
// upstreams block. Replace with a clean browser-like UA only when it looks SDK-derived.
|
||||||
|
const ua = (headers["User-Agent"] || headers["user-agent"] || "").toLowerCase();
|
||||||
|
if (
|
||||||
|
ua.includes("openai") &&
|
||||||
|
(ua.includes("node") || ua.includes("axios") || ua.includes("undici"))
|
||||||
|
) {
|
||||||
|
setUserAgentHeader(headers, "Mozilla/5.0 (compatible; OpenAI Compatible)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return strippedKeys;
|
||||||
|
}
|
||||||
55
tests/unit/base-headers-split.test.ts
Normal file
55
tests/unit/base-headers-split.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
|
// Split-guard for the base executor header-helper extraction.
|
||||||
|
// The pure upstream-header helpers live in base/headers.ts (no host state, no fetch).
|
||||||
|
// base.ts re-exports all 6 so the ~18 executors + tests that import them from "./base.ts"
|
||||||
|
// keep resolving unchanged.
|
||||||
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const EXE = join(HERE, "../../open-sse/executors");
|
||||||
|
const HOST = join(EXE, "base.ts");
|
||||||
|
const LEAF = join(EXE, "base/headers.ts");
|
||||||
|
|
||||||
|
test("leaf hosts the header helpers and does not import the host", () => {
|
||||||
|
const src = readFileSync(LEAF, "utf8");
|
||||||
|
for (const sym of [
|
||||||
|
"mergeUpstreamExtraHeaders",
|
||||||
|
"setUserAgentHeader",
|
||||||
|
"isOpenAICompatibleEndpoint",
|
||||||
|
"stripStainlessHeadersForOpenAICompat",
|
||||||
|
]) {
|
||||||
|
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||||
|
}
|
||||||
|
assert.doesNotMatch(src, /from "\.\.\/base\.ts"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("host re-exports all 6 header helpers for external importers", () => {
|
||||||
|
const host = readFileSync(HOST, "utf8");
|
||||||
|
for (const sym of [
|
||||||
|
"mergeUpstreamExtraHeaders",
|
||||||
|
"getCustomUserAgent",
|
||||||
|
"setUserAgentHeader",
|
||||||
|
"applyConfiguredUserAgent",
|
||||||
|
"isOpenAICompatibleEndpoint",
|
||||||
|
"stripStainlessHeadersForOpenAICompat",
|
||||||
|
]) {
|
||||||
|
assert.match(host, new RegExp(`\\b${sym}\\b`));
|
||||||
|
}
|
||||||
|
assert.match(host, /from "\.\/base\/headers\.ts"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("header helpers behave via base.ts (the public import path)", async () => {
|
||||||
|
const mod = await import("../../open-sse/executors/base.ts");
|
||||||
|
assert.equal(mod.isOpenAICompatibleEndpoint("openai-compatible-x", "https://x/y"), true);
|
||||||
|
const headers: Record<string, string> = { "x-stainless-lang": "js", "User-Agent": "openai-node" };
|
||||||
|
const stripped = mod.stripStainlessHeadersForOpenAICompat(
|
||||||
|
headers,
|
||||||
|
"openai-compatible-x",
|
||||||
|
"https://x/v1/chat/completions"
|
||||||
|
);
|
||||||
|
assert.ok(stripped.includes("x-stainless-lang"));
|
||||||
|
assert.equal(headers["x-stainless-lang"], undefined);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user