// Anthropic/Claude-format provider key validators (anthropic-like, claude-oauth-inline, anthropic-compatible, claude-code-compatible). // Extracted from validation.ts (god-file decomposition) — top-level functions; behavior is // byte-identical to the original inline defs. import { buildClaudeCodeCompatibleHeaders, buildClaudeCodeCompatibleValidationPayload, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH, CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH, joinClaudeCodeCompatibleUrl, joinBaseUrlAndPath, } from "@omniroute/open-sse/services/claudeCodeCompatible.ts"; import { normalizeAnthropicBaseUrl, normalizeClaudeCodeCompatibleBaseUrl, } from "./urlHelpers"; import { applyCustomUserAgent } from "./headers"; import { toValidationErrorResult, validationRead, validationWrite, } from "./transport"; export async function validateAnthropicLikeProvider({ apiKey, baseUrl, modelId = "claude-3-5-sonnet-20240620", headers = {}, providerSpecificData = {}, isLocal = false, }: any) { try { if (!baseUrl) { return { valid: false, error: "Missing base URL" }; } if (typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat")) { return validateClaudeOAuthInline({ apiKey, modelId, providerSpecificData }); } const probeUrl = typeof providerSpecificData?.modelsUrl === "string" && providerSpecificData.modelsUrl.trim() !== "" ? providerSpecificData.modelsUrl.trim() : `${baseUrl}/models`; // Best-effort /models probe. It must not fail validation: canonical Claude // base URLs can already include a path/query (…/messages?beta=true). try { await validationRead( probeUrl, { headers: { "anthropic-version": "2023-06-01", ...headers, }, }, isLocal ); } catch { // ignore probe failures } const requestUrl = typeof providerSpecificData?.modelsUrl === "string" && providerSpecificData.modelsUrl.trim() !== "" ? providerSpecificData.modelsUrl.trim() : ""; if (requestUrl) { const response = await validationRead( requestUrl, { headers: { "anthropic-version": "2023-06-01", ...headers, }, }, isLocal ); if (response.status === 401 || response.status === 403) { return { valid: false, error: "Invalid API key" }; } } const requestHeaders = applyCustomUserAgent( { "Content-Type": "application/json", ...headers, }, providerSpecificData ); if (!requestHeaders["x-api-key"] && !requestHeaders["X-API-Key"]) { requestHeaders["x-api-key"] = apiKey; } if (!requestHeaders["anthropic-version"] && !requestHeaders["Anthropic-Version"]) { requestHeaders["anthropic-version"] = "2023-06-01"; } const testModelId = providerSpecificData?.validationModelId || modelId || "claude-3-5-sonnet-20241022"; const chatResponse = await validationWrite( baseUrl, { method: "POST", headers: requestHeaders, body: JSON.stringify({ model: testModelId, max_tokens: 1, messages: [{ role: "user", content: "test" }], }), }, isLocal ); if (chatResponse.status === 401 || chatResponse.status === 403) { return { valid: false, error: "Invalid API key" }; } return { valid: true, error: null }; } catch (error: any) { return toValidationErrorResult(error); } } export async function validateClaudeOAuthInline({ apiKey, modelId, providerSpecificData = {}, }: { apiKey: string; modelId: string | null | undefined; providerSpecificData?: Record; }) { const testModelId = providerSpecificData?.validationModelId || modelId || "claude-haiku-4-5-20251001"; try { const { getExecutor } = await import("@omniroute/open-sse/executors/index.ts"); const { response } = await getExecutor("claude").execute({ model: testModelId, body: { model: testModelId, max_tokens: 1, messages: [{ role: "user", content: "test" }], }, stream: false, credentials: { accessToken: apiKey, providerSpecificData }, }); if (response.status === 401 || response.status === 403) { return { valid: false, error: "Invalid OAuth token" }; } if (response.status >= 500) { return { valid: false, error: `Provider unavailable (${response.status})` }; } return { valid: true, error: null }; } catch (error: any) { return toValidationErrorResult(error); } } export async function validateAnthropicCompatibleProvider({ apiKey, providerSpecificData = {}, isLocal = false, }: any) { let baseUrl = normalizeAnthropicBaseUrl(providerSpecificData.baseUrl); if (!baseUrl) { return { valid: false, error: "No base URL configured for Anthropic compatible provider" }; } const headers = applyCustomUserAgent( { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01", Authorization: `Bearer ${apiKey}`, }, providerSpecificData ); // Step 1: Best-effort GET /models probe. /models is NOT part of the Anthropic API spec // and many compatible proxies either 404, 401, or 403 on /models even with a valid key — // so a 401/403 here must NOT mark the credentials invalid. Only a 2xx is a positive // signal that the proxy DOES implement /models AND the key was accepted; everything else // (including auth-shaped statuses) falls through to the authoritative POST /v1/messages // probe below. Ported from decolua/9router 584cf66a. try { const modelsRes = await validationRead( joinBaseUrlAndPath(baseUrl, providerSpecificData?.modelsPath || "/models"), { method: "GET", headers, }, isLocal ); if (modelsRes.ok) { return { valid: true, error: null }; } } catch { // /models fetch failed — fall through to messages test } // Step 2: Authoritative probe — POST /v1/messages with max_tokens=1. const testModelId = providerSpecificData?.validationModelId || "claude-3-5-sonnet-20241022"; try { const messagesRes = await validationWrite( joinBaseUrlAndPath(baseUrl, providerSpecificData?.chatPath || "/messages"), { method: "POST", headers, body: JSON.stringify({ model: testModelId, max_tokens: 1, messages: [{ role: "user", content: "test" }], }), }, isLocal ); if (messagesRes.status === 401 || messagesRes.status === 403) { return { valid: false, error: "Invalid API key" }; } // Any other response (200, 400, 422, etc.) means auth passed return { valid: true, error: null }; } catch (error: any) { return toValidationErrorResult(error); } } export async function validateClaudeCodeCompatibleProvider({ apiKey, providerSpecificData = {}, }: any) { const baseUrl = normalizeClaudeCodeCompatibleBaseUrl(providerSpecificData.baseUrl); if (!baseUrl) { return { valid: false, error: "No base URL configured for CC Compatible provider" }; } const modelsPath = providerSpecificData?.modelsPath || CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH; const chatPath = providerSpecificData?.chatPath || CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH; const defaultHeaders = applyCustomUserAgent( buildClaudeCodeCompatibleHeaders(apiKey, false), providerSpecificData ); try { const modelsRes = await validationRead(joinClaudeCodeCompatibleUrl(baseUrl, modelsPath), { method: "GET", headers: defaultHeaders, }); if (modelsRes.ok) { return { valid: true, error: null, method: "models_endpoint" }; } if (modelsRes.status === 401 || modelsRes.status === 403) { return { valid: false, error: "Invalid API key" }; } } catch { // Fall through to bridge request validation. } const payload = buildClaudeCodeCompatibleValidationPayload( providerSpecificData?.validationModelId || "claude-sonnet-4-6" ); const sessionId = JSON.parse(payload.metadata.user_id as string).session_id; try { const messagesRes = await validationWrite(joinClaudeCodeCompatibleUrl(baseUrl, chatPath), { method: "POST", headers: applyCustomUserAgent( buildClaudeCodeCompatibleHeaders(apiKey, true, sessionId), providerSpecificData ), body: JSON.stringify(payload), }); if (messagesRes.status === 401 || messagesRes.status === 403) { return { valid: false, error: "Invalid API key" }; } if (messagesRes.status === 429) { return { valid: true, error: null, method: "cc_bridge_request", warning: "Rate limited, but credentials are valid", }; } if (messagesRes.status >= 400 && messagesRes.status < 500) { return { valid: true, error: null, method: "cc_bridge_request", warning: "Bridge request reached upstream, but the model or payload was rejected", }; } return { valid: messagesRes.ok, error: messagesRes.ok ? null : `Validation failed: ${messagesRes.status}`, method: "cc_bridge_request", }; } catch (error: any) { return toValidationErrorResult(error); } }