import { getConfig } from "../../config/store.ts"; import { hasCredentialSource, redactCredential, resolveCredential } from "../../config/credentials.ts"; import { getAdcAccessToken, getAdcLocation, getAdcProject, getVertexApiBase, isGeminiAdcAvailable, isVertexHost, } from "./adc.ts"; const DEFAULT_API_HOST = "https://generativelanguage.googleapis.com"; const API_VERSION = "v1beta"; export const API_BASE = `${DEFAULT_API_HOST}/${API_VERSION}`; export const DEFAULT_MODEL = "gemini-3.6-flash"; type GeminiApiConfig = { geminiApiKey?: unknown; geminiBaseUrl?: unknown; cloudflareApiKey?: unknown; }; function loadConfig(): GeminiApiConfig { return getConfig() as GeminiApiConfig; } function normalized(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim() : null; } function normalizedBase(value: unknown): string | null { return normalized(value)?.replace(/\/+$/, "") ?? null; } export async function getApiKey(signal?: AbortSignal): Promise { return resolveCredential({ provider: "Gemini", configuredValue: loadConfig().geminiApiKey, environmentValue: process.env.GEMINI_API_KEY, signal, }); } export function getApiHost(): string { return normalizedBase(process.env.GOOGLE_GEMINI_BASE_URL) ?? normalizedBase(loadConfig().geminiBaseUrl) ?? DEFAULT_API_HOST; } export function getVersionedApiBase(): string { const project = getAdcProject(); const location = getAdcLocation(); return isGeminiAdcAvailable() && project && location ? getVertexApiBase(project, location) : `${getApiHost()}/${API_VERSION}`; } function isCloudflareGateway(): boolean { return getApiHost().includes("gateway.ai.cloudflare.com"); } function legacyCloudflareKey(): string | null { return normalized(process.env.CLOUDFLARE_API_KEY) ?? normalized(loadConfig().cloudflareApiKey); } async function resolveCloudflareKey(signal?: AbortSignal): Promise { return resolveCredential({ provider: "Cloudflare", configuredValue: loadConfig().cloudflareApiKey, environmentValue: process.env.CLOUDFLARE_API_KEY, signal, }); } export function getCloudflareApiKey(): string | null { return legacyCloudflareKey(); } export function isGatewayConfigured(): boolean { return isCloudflareGateway() && hasCredentialSource({ provider: "Cloudflare", configuredValue: loadConfig().cloudflareApiKey, environmentValue: process.env.CLOUDFLARE_API_KEY, }); } export function buildAuthHeaders(apiKey: string | null = null, cloudflareApiKey: string | null = legacyCloudflareKey()): Record { if (!isCloudflareGateway()) return apiKey ? { "x-goog-api-key": apiKey } : {}; return cloudflareApiKey ? { "cf-aig-authorization": `Bearer ${cloudflareApiKey}` } : {}; } function redactAll(text: string, apiKey?: string | null, cloudflareKey?: string | null, adcToken?: string | null): string { let output = redactCredential(redactCredential(text, apiKey), cloudflareKey); if (adcToken) output = redactCredential(output, adcToken); return output; } const responseCredentials = new WeakMap(); export function redactGeminiApiResponse(response: Response, text: string, apiKey?: string | null, adcToken?: string | null): string { const credentials = responseCredentials.get(response); return redactAll(text, credentials?.apiKey ?? apiKey, credentials?.cloudflareKey, credentials?.adcToken ?? adcToken); } export async function fetchGeminiApi(url: string | URL, init: RequestInit = {}, apiKey?: string | null): Promise { const parsed = new URL(url); for (const name of parsed.searchParams.keys()) { if (name.toLowerCase() === "key" || name.toLowerCase() === "api_key") throw new Error("Gemini API credential query parameters are not allowed"); } const project = getAdcProject(); const location = getAdcLocation(); const adcMode = isGeminiAdcAvailable() && !!project && !!location && isVertexHost(parsed.origin); const adcToken = adcMode ? await getAdcAccessToken(init.signal ?? undefined) : null; const resolvedApiKey = adcMode ? null : apiKey === undefined ? await getApiKey(init.signal ?? undefined) : apiKey; const cloudflareKey = isCloudflareGateway() ? await resolveCloudflareKey(init.signal ?? undefined) : null; const allowedOrigins = new Set([new URL(getApiHost()).origin, new URL(DEFAULT_API_HOST).origin, ...(adcMode ? [parsed.origin] : [])]); if ((resolvedApiKey || isGatewayConfigured() || adcMode) && !allowedOrigins.has(parsed.origin)) throw new Error("Gemini API request host is not allowed"); const headers = new Headers(init.headers); headers.delete("x-goog-api-key"); headers.delete("cf-aig-authorization"); headers.delete("authorization"); if (adcToken) headers.set("Authorization", `Bearer ${adcToken}`); else for (const [name, value] of Object.entries(buildAuthHeaders(resolvedApiKey, cloudflareKey))) headers.set(name, value); try { const response = await fetch(parsed, { ...init, headers }); responseCredentials.set(response, { apiKey: resolvedApiKey, cloudflareKey, adcToken }); return response; } catch (error) { const message = error instanceof Error ? error.message : String(error); const redacted = redactAll(message, resolvedApiKey, cloudflareKey, adcToken); if (redacted === message) throw error; const wrapped = new Error(redacted); if (error instanceof Error) wrapped.name = error.name; throw wrapped; } } export function isGeminiApiAvailable(): boolean { return isGeminiAdcAvailable() || isGatewayConfigured() || hasCredentialSource({ provider: "Gemini", configuredValue: loadConfig().geminiApiKey, environmentValue: process.env.GEMINI_API_KEY, }); }