From fa51354e35b8041987660aa91d2f2ad28de81925 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:57:52 -0300 Subject: [PATCH] refactor(executors): extract pure URL normalizers from default (#6015) Extract the pure per-provider chat-URL normalizers (normalizeBailianMessagesUrl, normalizeDataRobotChatUrl, normalizeAzureAiChatUrl, normalizeWatsonxChatUrl, normalizeOciChatUrl, normalizeSapChatUrl, normalizeXiaomiMimoChatUrl, normalizeOpenAIChatUrl, getOpenRouterConnectionPreset) verbatim into the leaf default/urlNormalizers.ts. Host imports them back into buildUrl/transformRequest; the now-dead build*ChatUrl/normalizeBaseUrl imports move to the leaf. All module-private (no re-export). Host 864 -> 815 LOC (shrunk below its frozen baseline). Byte-identical bodies (verbatim 45/45), leaf does not import the host (no cycle). buildHeaders/execute/auth untouched. Adds a split-guard; consumer tests stay green (executor-default-base 49, anthropic-compatible-bearer 3, strip-client-metadata 3). --- open-sse/executors/default.ts | 74 +++---------------- open-sse/executors/default/urlNormalizers.ts | 64 ++++++++++++++++ .../default-url-normalizers-split.test.ts | 36 +++++++++ 3 files changed, 112 insertions(+), 62 deletions(-) create mode 100644 open-sse/executors/default/urlNormalizers.ts create mode 100644 tests/unit/default-url-normalizers-split.test.ts diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 0d52630666..afabd75645 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -24,17 +24,23 @@ import { isClaudeCodeCompatible, } from "../services/provider.ts"; import { sanitizeQwenThinkingToolChoice } from "../services/qwenThinking.ts"; -import { buildDataRobotChatUrl } from "../config/datarobot.ts"; -import { buildAzureAiChatUrl } from "../config/azureAi.ts"; -import { buildWatsonxChatUrl } from "../config/watsonx.ts"; -import { buildOciChatUrl } from "../config/oci.ts"; -import { buildSapChatUrl, getSapResourceGroup } from "../config/sap.ts"; +import { getSapResourceGroup } from "../config/sap.ts"; +import { + normalizeBailianMessagesUrl, + normalizeDataRobotChatUrl, + normalizeAzureAiChatUrl, + normalizeWatsonxChatUrl, + normalizeOciChatUrl, + normalizeSapChatUrl, + normalizeXiaomiMimoChatUrl, + normalizeOpenAIChatUrl, + getOpenRouterConnectionPreset, +} from "./default/urlNormalizers.ts"; import { buildMaritalkChatUrl } from "../config/maritalk.ts"; import { LOCAL_PROVIDERS } from "@/shared/constants/providers"; import { isForbiddenCustomHeaderName } from "@/shared/constants/upstreamHeaders"; import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults"; import { buildClineHeaders } from "@/shared/utils/clineAuth"; -import { normalizeBaseUrl } from "../utils/urlSanitize.ts"; import { normalizeHerokuChatUrl, normalizeDatabricksChatUrl, @@ -87,62 +93,6 @@ function applyCustomHeaders(headers: Record, rawCustomHeaders: u } } -function normalizeBailianMessagesUrl(baseUrl) { - const normalized = normalizeBaseUrl(baseUrl).replace(/\?beta=true$/, ""); - const messagesUrl = normalized.endsWith("/messages") ? normalized : `${normalized}/messages`; - return messagesUrl; -} - -function normalizeDataRobotChatUrl(baseUrl) { - return buildDataRobotChatUrl(baseUrl); -} - -function normalizeAzureAiChatUrl(baseUrl: string, apiType: "chat" | "responses" = "chat") { - return buildAzureAiChatUrl(baseUrl, apiType); -} - -function normalizeWatsonxChatUrl(baseUrl: string) { - return buildWatsonxChatUrl(baseUrl); -} - -function normalizeOciChatUrl(baseUrl: string, apiType: "chat" | "responses" = "chat") { - return buildOciChatUrl(baseUrl, apiType); -} - -function normalizeSapChatUrl(baseUrl) { - return buildSapChatUrl(baseUrl); -} - -function normalizeXiaomiMimoChatUrl(baseUrl) { - const normalized = normalizeBaseUrl(baseUrl).replace(/\/chat\/completions$/, ""); - return `${normalized}/chat/completions`; -} - -function normalizeOpenAIChatUrl(baseUrl) { - const normalized = normalizeBaseUrl(baseUrl); - if ( - normalized.endsWith("/chat/completions") || - normalized.endsWith("/responses") || - normalized.endsWith("/chat") - ) { - return normalized; - } - if (normalized.endsWith("/v1")) { - return `${normalized}/chat/completions`; - } - // Assume OpenAI-compatible /v1/chat/completions path structure - // when the base URL is a bare hostname or custom path (e.g. llama.cpp, vLLM, LM Studio). - return `${normalized}/v1/chat/completions`; -} - -function getOpenRouterConnectionPreset( - providerSpecificData?: Record | null -): string | null { - const preset = - typeof providerSpecificData?.preset === "string" ? providerSpecificData.preset.trim() : ""; - return preset || null; -} - export class DefaultExecutor extends BaseExecutor { constructor(provider) { super(provider, PROVIDERS[provider] || PROVIDERS.openai); diff --git a/open-sse/executors/default/urlNormalizers.ts b/open-sse/executors/default/urlNormalizers.ts new file mode 100644 index 0000000000..a4a5ed4ae5 --- /dev/null +++ b/open-sse/executors/default/urlNormalizers.ts @@ -0,0 +1,64 @@ +// Pure per-provider chat-URL normalizers + connection-preset reader. +// Extracted verbatim from default.ts (string transforms only, no host state/this). +import { buildDataRobotChatUrl } from "../../config/datarobot.ts"; +import { buildAzureAiChatUrl } from "../../config/azureAi.ts"; +import { buildWatsonxChatUrl } from "../../config/watsonx.ts"; +import { buildOciChatUrl } from "../../config/oci.ts"; +import { buildSapChatUrl } from "../../config/sap.ts"; +import { normalizeBaseUrl } from "../../utils/urlSanitize.ts"; + +export function normalizeBailianMessagesUrl(baseUrl) { + const normalized = normalizeBaseUrl(baseUrl).replace(/\?beta=true$/, ""); + const messagesUrl = normalized.endsWith("/messages") ? normalized : `${normalized}/messages`; + return messagesUrl; +} + +export function normalizeDataRobotChatUrl(baseUrl) { + return buildDataRobotChatUrl(baseUrl); +} + +export function normalizeAzureAiChatUrl(baseUrl: string, apiType: "chat" | "responses" = "chat") { + return buildAzureAiChatUrl(baseUrl, apiType); +} + +export function normalizeWatsonxChatUrl(baseUrl: string) { + return buildWatsonxChatUrl(baseUrl); +} + +export function normalizeOciChatUrl(baseUrl: string, apiType: "chat" | "responses" = "chat") { + return buildOciChatUrl(baseUrl, apiType); +} + +export function normalizeSapChatUrl(baseUrl) { + return buildSapChatUrl(baseUrl); +} + +export function normalizeXiaomiMimoChatUrl(baseUrl) { + const normalized = normalizeBaseUrl(baseUrl).replace(/\/chat\/completions$/, ""); + return `${normalized}/chat/completions`; +} + +export function normalizeOpenAIChatUrl(baseUrl) { + const normalized = normalizeBaseUrl(baseUrl); + if ( + normalized.endsWith("/chat/completions") || + normalized.endsWith("/responses") || + normalized.endsWith("/chat") + ) { + return normalized; + } + if (normalized.endsWith("/v1")) { + return `${normalized}/chat/completions`; + } + // Assume OpenAI-compatible /v1/chat/completions path structure + // when the base URL is a bare hostname or custom path (e.g. llama.cpp, vLLM, LM Studio). + return `${normalized}/v1/chat/completions`; +} + +export function getOpenRouterConnectionPreset( + providerSpecificData?: Record | null +): string | null { + const preset = + typeof providerSpecificData?.preset === "string" ? providerSpecificData.preset.trim() : ""; + return preset || null; +} diff --git a/tests/unit/default-url-normalizers-split.test.ts b/tests/unit/default-url-normalizers-split.test.ts new file mode 100644 index 0000000000..87d7f0adc6 --- /dev/null +++ b/tests/unit/default-url-normalizers-split.test.ts @@ -0,0 +1,36 @@ +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 default executor URL-normalizer extraction. +// The pure per-provider chat-URL normalizers live in default/urlNormalizers.ts +// (string transforms only). Host imports them back into buildUrl/transformRequest. +const HERE = dirname(fileURLToPath(import.meta.url)); +const EXE = join(HERE, "../../open-sse/executors"); +const HOST = join(EXE, "default.ts"); +const LEAF = join(EXE, "default/urlNormalizers.ts"); + +test("leaf hosts the normalizers and does not import the host", () => { + const src = readFileSync(LEAF, "utf8"); + for (const sym of [ + "normalizeOpenAIChatUrl", + "normalizeSapChatUrl", + "getOpenRouterConnectionPreset", + ]) { + assert.match(src, new RegExp(`export function ${sym}\\b`)); + } + assert.doesNotMatch(src, /from "\.\.\/default\.ts"/); +}); + +test("host imports the normalizers back from the leaf", () => { + const host = readFileSync(HOST, "utf8"); + assert.match(host, /from "\.\/default\/urlNormalizers\.ts"/); +}); + +test("normalizeOpenAIChatUrl appends chat/completions for a bare base URL", async () => { + const { normalizeOpenAIChatUrl } = + await import("../../open-sse/executors/default/urlNormalizers.ts"); + assert.match(normalizeOpenAIChatUrl("https://api.example.com"), /\/v1\/chat\/completions$/); +});