From 6035fd8ba1b0aa90bb1e11cbd44beca43011b17e Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:52:23 -0300 Subject: [PATCH] refactor(api): extrai validators enterprise-cloud + probe compartilhado de validation.ts (#4923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrado em release/v3.8.36 (validation.ts split fatia 3 — enterprise-cloud + probe) --- src/lib/providers/validation.ts | 713 +----------------- .../providers/validation/cloudProviders.ts | 669 ++++++++++++++++ .../providers/validation/directChatProbe.ts | 47 ++ .../validation-cloud-providers-split.test.ts | 38 + 4 files changed, 767 insertions(+), 700 deletions(-) create mode 100644 src/lib/providers/validation/cloudProviders.ts create mode 100644 src/lib/providers/validation/directChatProbe.ts create mode 100644 tests/unit/validation-cloud-providers-split.test.ts diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 4423c9a79e..0f2533e55f 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -25,41 +25,12 @@ import { } from "@/shared/network/safeOutboundFetch"; import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard"; import { resolveNvidiaValidationModel } from "@/lib/providers/nvidiaValidationModel"; -import { getGigachatAccessToken } from "@omniroute/open-sse/services/gigachatAuth.ts"; import { validateQoderCliPat } from "@omniroute/open-sse/services/qoderCli.ts"; -import { - AZURE_AI_DEFAULT_BASE_URL, - buildAzureAiChatUrl, - buildAzureAiModelsUrl, -} from "@omniroute/open-sse/config/azureAi.ts"; import { discoverBedrockNativeModels, isBedrockNativeApiError, isBedrockNativeAuthError, } from "@omniroute/open-sse/services/bedrock.ts"; -import { - DATAROBOT_DEFAULT_BASE_URL, - buildDataRobotCatalogUrl, - buildDataRobotChatUrl, - isDataRobotDeploymentUrl, -} from "@omniroute/open-sse/config/datarobot.ts"; -import { - OCI_DEFAULT_BASE_URL, - buildOciChatUrl, - buildOciModelsUrl, -} from "@omniroute/open-sse/config/oci.ts"; -import { - SAP_DEFAULT_BASE_URL, - buildSapChatUrl, - buildSapModelsUrl, - getSapResourceGroup, - isSapDeploymentUrl, -} from "@omniroute/open-sse/config/sap.ts"; -import { - WATSONX_DEFAULT_BASE_URL, - buildWatsonxChatUrl, - buildWatsonxModelsUrl, -} from "@omniroute/open-sse/config/watsonx.ts"; import { buildRunwayApiUrl, buildRunwayHeaders, @@ -76,16 +47,11 @@ import { OPENAI_LIKE_FORMATS, GEMINI_LIKE_FORMATS, normalizeBaseUrl, - normalizeAzureOpenAIBaseUrl, normalizeAnthropicBaseUrl, normalizeClaudeCodeCompatibleBaseUrl, addModelsSuffix, resolveBaseUrl, resolveChatUrl, - normalizeHerokuChatUrl, - normalizeDatabricksChatUrl, - normalizeSnowflakeChatUrl, - normalizeGigachatChatUrl, } from "./validation/urlHelpers"; import { STANDARD_USER_AGENT, @@ -121,6 +87,19 @@ import { validateJulesProvider, validateInnerAiProvider, } from "./validation/webProvidersB"; +import { validateDirectChatProvider } from "./validation/directChatProbe"; +import { + validateHerokuProvider, + validateDatabricksProvider, + validateDataRobotProvider, + validateSnowflakeProvider, + validateGigachatProvider, + validateAzureOpenAIProvider, + validateAzureAiProvider, + validateWatsonxProvider, + validateOciProvider, + validateSapProvider, +} from "./validation/cloudProviders"; // isRetryableProxyTarget + isSecurityBlockError now live in ./validation/transport. Re-export them // here to preserve the historical public surface (tests + route handlers import them via this module). @@ -274,47 +253,6 @@ async function validateOpenAILikeProvider({ } } -async function validateDirectChatProvider({ - url, - headers, - body, - providerSpecificData = {}, - isLocal = false, -}: any) { - try { - const response = await validationWrite( - url, - { - method: "POST", - headers: applyCustomUserAgent(headers, providerSpecificData), - body: JSON.stringify(body), - }, - isLocal - ); - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if ( - response.ok || - response.status === 400 || - response.status === 422 || - response.status === 429 - ) { - return { valid: true, error: null }; - } - - if (response.status >= 500) { - return { valid: false, error: `Provider unavailable (${response.status})` }; - } - - return { valid: false, error: `Validation failed: ${response.status}` }; - } catch (error: any) { - return toValidationErrorResult(error); - } -} - export async function validateCommandCodeProvider({ apiKey, providerSpecificData = {} }: any) { const entry = getRegistryEntry("command-code"); const baseUrl = normalizeBaseUrl(entry?.baseUrl || "https://api.commandcode.ai"); @@ -1012,631 +950,6 @@ async function validateBailianCodingPlanProvider({ apiKey, providerSpecificData } } -async function validateHerokuProvider({ apiKey, providerSpecificData = {} }: any) { - const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl); - if (!baseUrl) { - return { valid: false, error: "Missing base URL" }; - } - - return validateDirectChatProvider({ - url: normalizeHerokuChatUrl(baseUrl), - headers: buildBearerHeaders(apiKey, providerSpecificData), - body: { - model: providerSpecificData.validationModelId || "claude-4-sonnet", - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }, - providerSpecificData, - }); -} - -async function validateDatabricksProvider({ apiKey, providerSpecificData = {} }: any) { - const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl); - if (!baseUrl) { - return { valid: false, error: "Missing base URL" }; - } - - return validateDirectChatProvider({ - url: normalizeDatabricksChatUrl(baseUrl), - headers: buildBearerHeaders(apiKey, providerSpecificData), - body: { - model: providerSpecificData.validationModelId || "databricks-meta-llama-3-3-70b-instruct", - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }, - providerSpecificData, - }); -} - -async function validateDataRobotProvider({ apiKey, providerSpecificData = {} }: any) { - const configuredBaseUrl = - normalizeBaseUrl(providerSpecificData.baseUrl) || DATAROBOT_DEFAULT_BASE_URL; - - if (isDataRobotDeploymentUrl(configuredBaseUrl)) { - return validateDirectChatProvider({ - url: buildDataRobotChatUrl(configuredBaseUrl), - headers: buildBearerHeaders(apiKey, providerSpecificData), - body: { - model: providerSpecificData.validationModelId || "datarobot-deployed-llm", - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }, - providerSpecificData, - }); - } - - const catalogUrl = buildDataRobotCatalogUrl(configuredBaseUrl); - if (!catalogUrl) { - return { valid: false, error: "Invalid DataRobot base URL" }; - } - - try { - const response = await validationRead(catalogUrl, { - method: "GET", - headers: buildBearerHeaders(apiKey, providerSpecificData), - }); - - if (response.ok) { - return { valid: true, error: null, method: "gateway_catalog" }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status === 429) { - return { - valid: true, - error: null, - method: "gateway_catalog", - warning: "Rate limited, but credentials are valid", - }; - } - - if (response.status >= 400 && response.status < 500) { - return { valid: true, error: null, method: "gateway_catalog" }; - } - - return { valid: false, error: `Validation failed: ${response.status}` }; - } catch (error: any) { - return toValidationErrorResult(error); - } -} - -async function validateSnowflakeProvider({ apiKey, providerSpecificData = {} }: any) { - const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl); - if (!baseUrl) { - return { valid: false, error: "Missing base URL" }; - } - - const usesProgrammaticAccessToken = typeof apiKey === "string" && apiKey.startsWith("pat/"); - return validateDirectChatProvider({ - url: normalizeSnowflakeChatUrl(baseUrl), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${usesProgrammaticAccessToken ? apiKey.slice(4) : apiKey}`, - "X-Snowflake-Authorization-Token-Type": usesProgrammaticAccessToken - ? "PROGRAMMATIC_ACCESS_TOKEN" - : "KEYPAIR_JWT", - }, - body: { - model: providerSpecificData.validationModelId || "llama3.3-70b", - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }, - providerSpecificData, - }); -} - -async function validateGigachatProvider({ apiKey, providerSpecificData = {} }: any) { - const baseUrl = - normalizeBaseUrl(providerSpecificData.baseUrl) || "https://gigachat.devices.sberbank.ru/api/v1"; - - let token; - try { - token = await getGigachatAccessToken({ credentials: apiKey }); - } catch (error: any) { - if (String(error?.message || "").match(/\b(401|403)\b/)) { - return { valid: false, error: "Invalid API key" }; - } - return toValidationErrorResult(error); - } - - return validateDirectChatProvider({ - url: normalizeGigachatChatUrl(baseUrl), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token.accessToken}`, - Accept: "application/json", - }, - body: { - model: providerSpecificData.validationModelId || "GigaChat-2-Pro", - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }, - providerSpecificData, - }); -} - -async function validateAzureOpenAIProvider({ apiKey, providerSpecificData = {} }: any) { - const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl); - if (!rawBaseUrl) { - return { valid: false, error: "Missing base URL" }; - } - - const baseUrl = normalizeAzureOpenAIBaseUrl(rawBaseUrl); - const apiVersion = - typeof providerSpecificData.validationApiVersion === "string" && - providerSpecificData.validationApiVersion.trim() - ? providerSpecificData.validationApiVersion.trim() - : "2024-12-01-preview"; - const headers = applyCustomUserAgent( - { - "Content-Type": "application/json", - "api-key": apiKey, - }, - providerSpecificData - ); - const encodedVersion = encodeURIComponent(apiVersion); - - for (const probeUrl of [ - `${baseUrl}/openai/deployments?api-version=${encodedVersion}`, - `${baseUrl}/openai/models?api-version=${encodedVersion}`, - ]) { - try { - const response = await validationRead(probeUrl, { method: "GET", headers }); - if (response.ok) { - return { valid: true, error: null, method: "azure_probe" }; - } - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - if (response.status === 400 || response.status === 404 || response.status === 405) { - continue; - } - if (response.status === 429) { - return { - valid: true, - error: null, - method: "azure_probe", - warning: "Rate limited, but credentials are valid", - }; - } - if (response.status >= 500) { - return { valid: false, error: `Provider unavailable (${response.status})` }; - } - } catch (error) { - return toValidationErrorResult(error); - } - } - - const deploymentId = - typeof providerSpecificData.validationModelId === "string" - ? providerSpecificData.validationModelId.trim() - : ""; - - if (!deploymentId) { - return { - valid: true, - error: null, - warning: - "Azure key accepted, but no deployment name was provided for a chat probe. Set Model ID to validate a specific deployment.", - }; - } - - const chatUrl = `${baseUrl}/openai/deployments/${encodeURIComponent(deploymentId)}/chat/completions?api-version=${encodedVersion}`; - const response = await validationWrite(chatUrl, { - method: "POST", - headers, - body: JSON.stringify({ - model: deploymentId, - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }), - }); - - if ( - response.ok || - response.status === 400 || - response.status === 422 || - response.status === 429 - ) { - return { valid: true, error: null, method: "chat_probe" }; - } - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - if (response.status === 404) { - return { - valid: true, - error: null, - method: "chat_probe", - warning: "Azure credentials are valid, but the requested deployment was not found.", - }; - } - if (response.status >= 500) { - return { valid: false, error: `Provider unavailable (${response.status})` }; - } - return { valid: false, error: `Validation failed: ${response.status}` }; -} - -async function validateAzureAiProvider({ apiKey, providerSpecificData = {} }: any) { - const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || AZURE_AI_DEFAULT_BASE_URL; - const modelsUrl = buildAzureAiModelsUrl(rawBaseUrl); - const headers = applyCustomUserAgent( - { - "Content-Type": "application/json", - "api-key": apiKey, - }, - providerSpecificData - ); - - try { - const response = await validationRead(modelsUrl, { - method: "GET", - headers, - }); - - if (response.ok) { - return { valid: true, error: null, method: "azure_ai_models" }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status === 429) { - return { - valid: true, - error: null, - method: "azure_ai_models", - warning: "Rate limited, but credentials are valid", - }; - } - } catch { - // Fall through to chat probe when /models is unavailable. - } - - const validationModelId = - typeof providerSpecificData.validationModelId === "string" - ? providerSpecificData.validationModelId.trim() - : ""; - - if (!validationModelId) { - return { - valid: false, - error: "Endpoint /models unavailable. Provide a Model ID to validate via /chat/completions.", - }; - } - - const chatUrl = buildAzureAiChatUrl( - rawBaseUrl, - providerSpecificData.apiType === "responses" ? "responses" : "chat" - ); - const chatBody = - providerSpecificData.apiType === "responses" - ? { - model: validationModelId, - input: "test", - max_output_tokens: 1, - } - : { - model: validationModelId, - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }; - - try { - const response = await validationWrite(chatUrl, { - method: "POST", - headers, - body: JSON.stringify(chatBody), - }); - - if ( - response.ok || - response.status === 400 || - response.status === 404 || - response.status === 422 || - response.status === 429 - ) { - return { valid: true, error: null, method: "azure_ai_chat_probe" }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status >= 500) { - return { valid: false, error: `Provider unavailable (${response.status})` }; - } - } catch (error: any) { - return toValidationErrorResult(error); - } - - return { valid: false, error: "Connection failed while testing Azure AI Foundry" }; -} - -async function validateWatsonxProvider({ apiKey, providerSpecificData = {} }: any) { - const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || WATSONX_DEFAULT_BASE_URL; - const headers = applyCustomUserAgent( - { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - providerSpecificData - ); - - try { - const response = await validationRead(buildWatsonxModelsUrl(rawBaseUrl), { - method: "GET", - headers, - }); - - if (response.ok) { - return { valid: true, error: null, method: "watsonx_models" }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status === 429) { - return { - valid: true, - error: null, - method: "watsonx_models", - warning: "Rate limited, but credentials are valid", - }; - } - } catch { - // Fall through to chat probe when /models is unavailable. - } - - const validationModelId = - typeof providerSpecificData.validationModelId === "string" && - providerSpecificData.validationModelId.trim() - ? providerSpecificData.validationModelId.trim() - : "ibm/granite-3-3-8b-instruct"; - - try { - const response = await validationWrite(buildWatsonxChatUrl(rawBaseUrl), { - method: "POST", - headers, - body: JSON.stringify({ - model: validationModelId, - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }), - }); - - if ( - response.ok || - response.status === 400 || - response.status === 404 || - response.status === 422 || - response.status === 429 - ) { - return { - valid: true, - error: null, - method: "watsonx_chat_probe", - ...(response.status === 404 - ? { warning: "watsonx credentials are valid, but the requested model is not enabled." } - : {}), - }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status >= 500) { - return { valid: false, error: `Provider unavailable (${response.status})` }; - } - } catch (error: any) { - return toValidationErrorResult(error); - } - - return { valid: false, error: "Connection failed while testing watsonx.ai" }; -} - -async function validateOciProvider({ apiKey, providerSpecificData = {} }: any) { - const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || OCI_DEFAULT_BASE_URL; - const projectId = - typeof providerSpecificData.projectId === "string" && providerSpecificData.projectId.trim() - ? providerSpecificData.projectId.trim() - : typeof providerSpecificData.project === "string" && providerSpecificData.project.trim() - ? providerSpecificData.project.trim() - : ""; - const headers = applyCustomUserAgent( - { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - ...(projectId ? { "OpenAI-Project": projectId } : {}), - }, - providerSpecificData - ); - - try { - const response = await validationRead(buildOciModelsUrl(rawBaseUrl), { - method: "GET", - headers, - }); - - if (response.ok) { - return { valid: true, error: null, method: "oci_models" }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status === 429) { - return { - valid: true, - error: null, - method: "oci_models", - warning: "Rate limited, but credentials are valid", - }; - } - } catch { - // Fall through to chat/responses probe when /models is unavailable. - } - - const validationModelId = - typeof providerSpecificData.validationModelId === "string" && - providerSpecificData.validationModelId.trim() - ? providerSpecificData.validationModelId.trim() - : "openai.gpt-oss-20b"; - const apiType = providerSpecificData.apiType === "responses" ? "responses" : "chat"; - const body = - apiType === "responses" - ? { - model: validationModelId, - input: "test", - max_output_tokens: 1, - } - : { - model: validationModelId, - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }; - - try { - const response = await validationWrite(buildOciChatUrl(rawBaseUrl, apiType), { - method: "POST", - headers, - body: JSON.stringify(body), - }); - - if ( - response.ok || - response.status === 400 || - response.status === 404 || - response.status === 422 || - response.status === 429 - ) { - return { - valid: true, - error: null, - method: apiType === "responses" ? "oci_responses_probe" : "oci_chat_probe", - ...(response.status === 404 - ? { warning: "OCI credentials are valid, but the requested model was not found." } - : {}), - }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status >= 500) { - return { valid: false, error: `Provider unavailable (${response.status})` }; - } - } catch (error: any) { - return toValidationErrorResult(error); - } - - return { valid: false, error: "Connection failed while testing OCI Generative AI" }; -} - -async function validateSapProvider({ apiKey, providerSpecificData = {} }: any) { - const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || SAP_DEFAULT_BASE_URL; - const resourceGroup = getSapResourceGroup(providerSpecificData); - const headers = applyCustomUserAgent( - { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - "AI-Resource-Group": resourceGroup, - }, - providerSpecificData - ); - - try { - const response = await validationRead(buildSapModelsUrl(rawBaseUrl), { - method: "GET", - headers, - }); - - if (response.ok) { - return { valid: true, error: null, method: "sap_models" }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status === 429) { - return { - valid: true, - error: null, - method: "sap_models", - warning: "Rate limited, but credentials are valid", - }; - } - } catch { - // Fall through to deployment probe when the discovery API is unavailable. - } - - const canProbeChat = - isSapDeploymentUrl(rawBaseUrl) || /\/chat\/completions$/i.test(normalizeBaseUrl(rawBaseUrl)); - if (!canProbeChat) { - return { - valid: false, - error: - "SAP validation needs either a reachable AI_API_URL or a deployment URL in providerSpecificData.baseUrl", - }; - } - - const validationModelId = - typeof providerSpecificData.validationModelId === "string" && - providerSpecificData.validationModelId.trim() - ? providerSpecificData.validationModelId.trim() - : "gpt-4o"; - - try { - const response = await validationWrite(buildSapChatUrl(rawBaseUrl), { - method: "POST", - headers, - body: JSON.stringify({ - model: validationModelId, - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }), - }); - - if ( - response.ok || - response.status === 400 || - response.status === 404 || - response.status === 422 || - response.status === 429 - ) { - return { - valid: true, - error: null, - method: "sap_chat_probe", - ...(response.status === 404 - ? { warning: "SAP credentials are valid, but the deployment URL or model was not found." } - : {}), - }; - } - - if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - - if (response.status >= 500) { - return { valid: false, error: `Provider unavailable (${response.status})` }; - } - } catch (error: any) { - return toValidationErrorResult(error); - } - - return { valid: false, error: "Connection failed while testing SAP Generative AI Hub" }; -} - async function validateRekaProvider({ apiKey, providerSpecificData = {} }: any) { const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || "https://api.reka.ai/v1"; const headers = buildRekaHeaders(apiKey, providerSpecificData); diff --git a/src/lib/providers/validation/cloudProviders.ts b/src/lib/providers/validation/cloudProviders.ts new file mode 100644 index 0000000000..0000d8cfba --- /dev/null +++ b/src/lib/providers/validation/cloudProviders.ts @@ -0,0 +1,669 @@ +// Enterprise-cloud provider key validators: heroku, databricks, datarobot, snowflake, gigachat, +// azure-openai, azure-ai, watsonx, oci, sap. Extracted from validation.ts (god-file decomposition) — +// top-level functions with no dispatcher-state captures; behavior is byte-identical to the inline defs. +import { + normalizeBaseUrl, + normalizeAzureOpenAIBaseUrl, + normalizeHerokuChatUrl, + normalizeDatabricksChatUrl, + normalizeSnowflakeChatUrl, + normalizeGigachatChatUrl, +} from "./urlHelpers"; +import { applyCustomUserAgent, buildBearerHeaders } from "./headers"; +import { toValidationErrorResult, validationRead, validationWrite } from "./transport"; +import { validateDirectChatProvider } from "./directChatProbe"; +import { getGigachatAccessToken } from "@omniroute/open-sse/services/gigachatAuth.ts"; +import { + AZURE_AI_DEFAULT_BASE_URL, + buildAzureAiChatUrl, + buildAzureAiModelsUrl, +} from "@omniroute/open-sse/config/azureAi.ts"; +import { + DATAROBOT_DEFAULT_BASE_URL, + buildDataRobotCatalogUrl, + buildDataRobotChatUrl, + isDataRobotDeploymentUrl, +} from "@omniroute/open-sse/config/datarobot.ts"; +import { + OCI_DEFAULT_BASE_URL, + buildOciChatUrl, + buildOciModelsUrl, +} from "@omniroute/open-sse/config/oci.ts"; +import { + SAP_DEFAULT_BASE_URL, + buildSapChatUrl, + buildSapModelsUrl, + getSapResourceGroup, + isSapDeploymentUrl, +} from "@omniroute/open-sse/config/sap.ts"; +import { + WATSONX_DEFAULT_BASE_URL, + buildWatsonxChatUrl, + buildWatsonxModelsUrl, +} from "@omniroute/open-sse/config/watsonx.ts"; + +export async function validateHerokuProvider({ apiKey, providerSpecificData = {} }: any) { + const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl); + if (!baseUrl) { + return { valid: false, error: "Missing base URL" }; + } + + return validateDirectChatProvider({ + url: normalizeHerokuChatUrl(baseUrl), + headers: buildBearerHeaders(apiKey, providerSpecificData), + body: { + model: providerSpecificData.validationModelId || "claude-4-sonnet", + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }, + providerSpecificData, + }); +} + +export async function validateDatabricksProvider({ apiKey, providerSpecificData = {} }: any) { + const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl); + if (!baseUrl) { + return { valid: false, error: "Missing base URL" }; + } + + return validateDirectChatProvider({ + url: normalizeDatabricksChatUrl(baseUrl), + headers: buildBearerHeaders(apiKey, providerSpecificData), + body: { + model: providerSpecificData.validationModelId || "databricks-meta-llama-3-3-70b-instruct", + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }, + providerSpecificData, + }); +} + +export async function validateDataRobotProvider({ apiKey, providerSpecificData = {} }: any) { + const configuredBaseUrl = + normalizeBaseUrl(providerSpecificData.baseUrl) || DATAROBOT_DEFAULT_BASE_URL; + + if (isDataRobotDeploymentUrl(configuredBaseUrl)) { + return validateDirectChatProvider({ + url: buildDataRobotChatUrl(configuredBaseUrl), + headers: buildBearerHeaders(apiKey, providerSpecificData), + body: { + model: providerSpecificData.validationModelId || "datarobot-deployed-llm", + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }, + providerSpecificData, + }); + } + + const catalogUrl = buildDataRobotCatalogUrl(configuredBaseUrl); + if (!catalogUrl) { + return { valid: false, error: "Invalid DataRobot base URL" }; + } + + try { + const response = await validationRead(catalogUrl, { + method: "GET", + headers: buildBearerHeaders(apiKey, providerSpecificData), + }); + + if (response.ok) { + return { valid: true, error: null, method: "gateway_catalog" }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status === 429) { + return { + valid: true, + error: null, + method: "gateway_catalog", + warning: "Rate limited, but credentials are valid", + }; + } + + if (response.status >= 400 && response.status < 500) { + return { valid: true, error: null, method: "gateway_catalog" }; + } + + return { valid: false, error: `Validation failed: ${response.status}` }; + } catch (error: any) { + return toValidationErrorResult(error); + } +} + +export async function validateSnowflakeProvider({ apiKey, providerSpecificData = {} }: any) { + const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl); + if (!baseUrl) { + return { valid: false, error: "Missing base URL" }; + } + + const usesProgrammaticAccessToken = typeof apiKey === "string" && apiKey.startsWith("pat/"); + return validateDirectChatProvider({ + url: normalizeSnowflakeChatUrl(baseUrl), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${usesProgrammaticAccessToken ? apiKey.slice(4) : apiKey}`, + "X-Snowflake-Authorization-Token-Type": usesProgrammaticAccessToken + ? "PROGRAMMATIC_ACCESS_TOKEN" + : "KEYPAIR_JWT", + }, + body: { + model: providerSpecificData.validationModelId || "llama3.3-70b", + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }, + providerSpecificData, + }); +} + +export async function validateGigachatProvider({ apiKey, providerSpecificData = {} }: any) { + const baseUrl = + normalizeBaseUrl(providerSpecificData.baseUrl) || "https://gigachat.devices.sberbank.ru/api/v1"; + + let token; + try { + token = await getGigachatAccessToken({ credentials: apiKey }); + } catch (error: any) { + if (String(error?.message || "").match(/\b(401|403)\b/)) { + return { valid: false, error: "Invalid API key" }; + } + return toValidationErrorResult(error); + } + + return validateDirectChatProvider({ + url: normalizeGigachatChatUrl(baseUrl), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token.accessToken}`, + Accept: "application/json", + }, + body: { + model: providerSpecificData.validationModelId || "GigaChat-2-Pro", + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }, + providerSpecificData, + }); +} + +export async function validateAzureOpenAIProvider({ apiKey, providerSpecificData = {} }: any) { + const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl); + if (!rawBaseUrl) { + return { valid: false, error: "Missing base URL" }; + } + + const baseUrl = normalizeAzureOpenAIBaseUrl(rawBaseUrl); + const apiVersion = + typeof providerSpecificData.validationApiVersion === "string" && + providerSpecificData.validationApiVersion.trim() + ? providerSpecificData.validationApiVersion.trim() + : "2024-12-01-preview"; + const headers = applyCustomUserAgent( + { + "Content-Type": "application/json", + "api-key": apiKey, + }, + providerSpecificData + ); + const encodedVersion = encodeURIComponent(apiVersion); + + for (const probeUrl of [ + `${baseUrl}/openai/deployments?api-version=${encodedVersion}`, + `${baseUrl}/openai/models?api-version=${encodedVersion}`, + ]) { + try { + const response = await validationRead(probeUrl, { method: "GET", headers }); + if (response.ok) { + return { valid: true, error: null, method: "azure_probe" }; + } + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + if (response.status === 400 || response.status === 404 || response.status === 405) { + continue; + } + if (response.status === 429) { + return { + valid: true, + error: null, + method: "azure_probe", + warning: "Rate limited, but credentials are valid", + }; + } + if (response.status >= 500) { + return { valid: false, error: `Provider unavailable (${response.status})` }; + } + } catch (error) { + return toValidationErrorResult(error); + } + } + + const deploymentId = + typeof providerSpecificData.validationModelId === "string" + ? providerSpecificData.validationModelId.trim() + : ""; + + if (!deploymentId) { + return { + valid: true, + error: null, + warning: + "Azure key accepted, but no deployment name was provided for a chat probe. Set Model ID to validate a specific deployment.", + }; + } + + const chatUrl = `${baseUrl}/openai/deployments/${encodeURIComponent(deploymentId)}/chat/completions?api-version=${encodedVersion}`; + const response = await validationWrite(chatUrl, { + method: "POST", + headers, + body: JSON.stringify({ + model: deploymentId, + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }), + }); + + if ( + response.ok || + response.status === 400 || + response.status === 422 || + response.status === 429 + ) { + return { valid: true, error: null, method: "chat_probe" }; + } + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + if (response.status === 404) { + return { + valid: true, + error: null, + method: "chat_probe", + warning: "Azure credentials are valid, but the requested deployment was not found.", + }; + } + if (response.status >= 500) { + return { valid: false, error: `Provider unavailable (${response.status})` }; + } + return { valid: false, error: `Validation failed: ${response.status}` }; +} + +export async function validateAzureAiProvider({ apiKey, providerSpecificData = {} }: any) { + const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || AZURE_AI_DEFAULT_BASE_URL; + const modelsUrl = buildAzureAiModelsUrl(rawBaseUrl); + const headers = applyCustomUserAgent( + { + "Content-Type": "application/json", + "api-key": apiKey, + }, + providerSpecificData + ); + + try { + const response = await validationRead(modelsUrl, { + method: "GET", + headers, + }); + + if (response.ok) { + return { valid: true, error: null, method: "azure_ai_models" }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status === 429) { + return { + valid: true, + error: null, + method: "azure_ai_models", + warning: "Rate limited, but credentials are valid", + }; + } + } catch { + // Fall through to chat probe when /models is unavailable. + } + + const validationModelId = + typeof providerSpecificData.validationModelId === "string" + ? providerSpecificData.validationModelId.trim() + : ""; + + if (!validationModelId) { + return { + valid: false, + error: "Endpoint /models unavailable. Provide a Model ID to validate via /chat/completions.", + }; + } + + const chatUrl = buildAzureAiChatUrl( + rawBaseUrl, + providerSpecificData.apiType === "responses" ? "responses" : "chat" + ); + const chatBody = + providerSpecificData.apiType === "responses" + ? { + model: validationModelId, + input: "test", + max_output_tokens: 1, + } + : { + model: validationModelId, + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }; + + try { + const response = await validationWrite(chatUrl, { + method: "POST", + headers, + body: JSON.stringify(chatBody), + }); + + if ( + response.ok || + response.status === 400 || + response.status === 404 || + response.status === 422 || + response.status === 429 + ) { + return { valid: true, error: null, method: "azure_ai_chat_probe" }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status >= 500) { + return { valid: false, error: `Provider unavailable (${response.status})` }; + } + } catch (error: any) { + return toValidationErrorResult(error); + } + + return { valid: false, error: "Connection failed while testing Azure AI Foundry" }; +} + +export async function validateWatsonxProvider({ apiKey, providerSpecificData = {} }: any) { + const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || WATSONX_DEFAULT_BASE_URL; + const headers = applyCustomUserAgent( + { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + providerSpecificData + ); + + try { + const response = await validationRead(buildWatsonxModelsUrl(rawBaseUrl), { + method: "GET", + headers, + }); + + if (response.ok) { + return { valid: true, error: null, method: "watsonx_models" }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status === 429) { + return { + valid: true, + error: null, + method: "watsonx_models", + warning: "Rate limited, but credentials are valid", + }; + } + } catch { + // Fall through to chat probe when /models is unavailable. + } + + const validationModelId = + typeof providerSpecificData.validationModelId === "string" && + providerSpecificData.validationModelId.trim() + ? providerSpecificData.validationModelId.trim() + : "ibm/granite-3-3-8b-instruct"; + + try { + const response = await validationWrite(buildWatsonxChatUrl(rawBaseUrl), { + method: "POST", + headers, + body: JSON.stringify({ + model: validationModelId, + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }), + }); + + if ( + response.ok || + response.status === 400 || + response.status === 404 || + response.status === 422 || + response.status === 429 + ) { + return { + valid: true, + error: null, + method: "watsonx_chat_probe", + ...(response.status === 404 + ? { warning: "watsonx credentials are valid, but the requested model is not enabled." } + : {}), + }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status >= 500) { + return { valid: false, error: `Provider unavailable (${response.status})` }; + } + } catch (error: any) { + return toValidationErrorResult(error); + } + + return { valid: false, error: "Connection failed while testing watsonx.ai" }; +} + +export async function validateOciProvider({ apiKey, providerSpecificData = {} }: any) { + const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || OCI_DEFAULT_BASE_URL; + const projectId = + typeof providerSpecificData.projectId === "string" && providerSpecificData.projectId.trim() + ? providerSpecificData.projectId.trim() + : typeof providerSpecificData.project === "string" && providerSpecificData.project.trim() + ? providerSpecificData.project.trim() + : ""; + const headers = applyCustomUserAgent( + { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + ...(projectId ? { "OpenAI-Project": projectId } : {}), + }, + providerSpecificData + ); + + try { + const response = await validationRead(buildOciModelsUrl(rawBaseUrl), { + method: "GET", + headers, + }); + + if (response.ok) { + return { valid: true, error: null, method: "oci_models" }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status === 429) { + return { + valid: true, + error: null, + method: "oci_models", + warning: "Rate limited, but credentials are valid", + }; + } + } catch { + // Fall through to chat/responses probe when /models is unavailable. + } + + const validationModelId = + typeof providerSpecificData.validationModelId === "string" && + providerSpecificData.validationModelId.trim() + ? providerSpecificData.validationModelId.trim() + : "openai.gpt-oss-20b"; + const apiType = providerSpecificData.apiType === "responses" ? "responses" : "chat"; + const body = + apiType === "responses" + ? { + model: validationModelId, + input: "test", + max_output_tokens: 1, + } + : { + model: validationModelId, + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }; + + try { + const response = await validationWrite(buildOciChatUrl(rawBaseUrl, apiType), { + method: "POST", + headers, + body: JSON.stringify(body), + }); + + if ( + response.ok || + response.status === 400 || + response.status === 404 || + response.status === 422 || + response.status === 429 + ) { + return { + valid: true, + error: null, + method: apiType === "responses" ? "oci_responses_probe" : "oci_chat_probe", + ...(response.status === 404 + ? { warning: "OCI credentials are valid, but the requested model was not found." } + : {}), + }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status >= 500) { + return { valid: false, error: `Provider unavailable (${response.status})` }; + } + } catch (error: any) { + return toValidationErrorResult(error); + } + + return { valid: false, error: "Connection failed while testing OCI Generative AI" }; +} + +export async function validateSapProvider({ apiKey, providerSpecificData = {} }: any) { + const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || SAP_DEFAULT_BASE_URL; + const resourceGroup = getSapResourceGroup(providerSpecificData); + const headers = applyCustomUserAgent( + { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "AI-Resource-Group": resourceGroup, + }, + providerSpecificData + ); + + try { + const response = await validationRead(buildSapModelsUrl(rawBaseUrl), { + method: "GET", + headers, + }); + + if (response.ok) { + return { valid: true, error: null, method: "sap_models" }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status === 429) { + return { + valid: true, + error: null, + method: "sap_models", + warning: "Rate limited, but credentials are valid", + }; + } + } catch { + // Fall through to deployment probe when the discovery API is unavailable. + } + + const canProbeChat = + isSapDeploymentUrl(rawBaseUrl) || /\/chat\/completions$/i.test(normalizeBaseUrl(rawBaseUrl)); + if (!canProbeChat) { + return { + valid: false, + error: + "SAP validation needs either a reachable AI_API_URL or a deployment URL in providerSpecificData.baseUrl", + }; + } + + const validationModelId = + typeof providerSpecificData.validationModelId === "string" && + providerSpecificData.validationModelId.trim() + ? providerSpecificData.validationModelId.trim() + : "gpt-4o"; + + try { + const response = await validationWrite(buildSapChatUrl(rawBaseUrl), { + method: "POST", + headers, + body: JSON.stringify({ + model: validationModelId, + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }), + }); + + if ( + response.ok || + response.status === 400 || + response.status === 404 || + response.status === 422 || + response.status === 429 + ) { + return { + valid: true, + error: null, + method: "sap_chat_probe", + ...(response.status === 404 + ? { warning: "SAP credentials are valid, but the deployment URL or model was not found." } + : {}), + }; + } + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if (response.status >= 500) { + return { valid: false, error: `Provider unavailable (${response.status})` }; + } + } catch (error: any) { + return toValidationErrorResult(error); + } + + return { valid: false, error: "Connection failed while testing SAP Generative AI Hub" }; +} + diff --git a/src/lib/providers/validation/directChatProbe.ts b/src/lib/providers/validation/directChatProbe.ts new file mode 100644 index 0000000000..f34c0a0fad --- /dev/null +++ b/src/lib/providers/validation/directChatProbe.ts @@ -0,0 +1,47 @@ +// Generic "POST /chat/completions auth probe" used by several provider validators (command-code, +// nlpcloud + the enterprise-cloud validators). Extracted from validation.ts (god-file decomposition) +// into its own leaf so both the host dispatcher and validation/cloudProviders.ts can share it without +// a cycle. Behavior is byte-identical to the original inline def. +import { applyCustomUserAgent } from "./headers"; +import { toValidationErrorResult, validationWrite } from "./transport"; + +export async function validateDirectChatProvider({ + url, + headers, + body, + providerSpecificData = {}, + isLocal = false, +}: any) { + try { + const response = await validationWrite( + url, + { + method: "POST", + headers: applyCustomUserAgent(headers, providerSpecificData), + body: JSON.stringify(body), + }, + isLocal + ); + + if (response.status === 401 || response.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + + if ( + response.ok || + response.status === 400 || + response.status === 422 || + response.status === 429 + ) { + return { valid: true, error: null }; + } + + if (response.status >= 500) { + return { valid: false, error: `Provider unavailable (${response.status})` }; + } + + return { valid: false, error: `Validation failed: ${response.status}` }; + } catch (error: any) { + return toValidationErrorResult(error); + } +} diff --git a/tests/unit/validation-cloud-providers-split.test.ts b/tests/unit/validation-cloud-providers-split.test.ts new file mode 100644 index 0000000000..1436cf5df8 --- /dev/null +++ b/tests/unit/validation-cloud-providers-split.test.ts @@ -0,0 +1,38 @@ +// Characterization of the validation.ts enterprise-cloud split (god-file decomposition): the 10 cloud +// validators (heroku/databricks/datarobot/snowflake/gigachat/azure-openai/azure-ai/watsonx/oci/sap) +// moved into validation/cloudProviders.ts, and the shared "POST /chat/completions auth probe" +// (validateDirectChatProvider) moved into validation/directChatProbe.ts so the host and the cloud +// module share it without a cycle. Behavior-preserving move — the locks here are module surface + +// the no-cycle wiring. Runtime behavior stays covered by provider-validation-azure-vertex/branches. +import { test } from "node:test"; +import assert from "node:assert/strict"; + +const cloud = await import("../../src/lib/providers/validation/cloudProviders.ts"); +const probe = await import("../../src/lib/providers/validation/directChatProbe.ts"); +const HOST = await import("../../src/lib/providers/validation.ts"); + +test("cloudProviders exposes the ten enterprise-cloud validators", () => { + for (const name of [ + "validateHerokuProvider", + "validateDatabricksProvider", + "validateDataRobotProvider", + "validateSnowflakeProvider", + "validateGigachatProvider", + "validateAzureOpenAIProvider", + "validateAzureAiProvider", + "validateWatsonxProvider", + "validateOciProvider", + "validateSapProvider", + ]) { + assert.equal(typeof (cloud as Record)[name], "function", `missing ${name}`); + } +}); + +test("directChatProbe exposes the shared validateDirectChatProvider helper", () => { + assert.equal(typeof probe.validateDirectChatProvider, "function"); +}); + +test("host dispatcher surface stays intact after the move", () => { + assert.equal(typeof (HOST as Record).validateProviderApiKey, "function"); + assert.equal(typeof (HOST as Record).validateCommandCodeProvider, "function"); +});