Files
dotfiles/pi/files/agent/extensions/pi-web-access/gemini-api.ts
2026-02-19 22:23:48 +00:00

104 lines
2.5 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
export const API_BASE = "https://generativelanguage.googleapis.com/v1beta";
const CONFIG_PATH = join(homedir(), ".pi", "web-search.json");
export const DEFAULT_MODEL = "gemini-3-flash-preview";
interface GeminiApiConfig {
geminiApiKey?: string;
}
let cachedConfig: GeminiApiConfig | null = null;
function loadConfig(): GeminiApiConfig {
if (cachedConfig) return cachedConfig;
if (existsSync(CONFIG_PATH)) {
try {
cachedConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as GeminiApiConfig;
return cachedConfig;
} catch {}
}
cachedConfig = {};
return cachedConfig;
}
function withTimeout(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
const timeout = AbortSignal.timeout(timeoutMs);
return signal ? AbortSignal.any([signal, timeout]) : timeout;
}
export function getApiKey(): string | null {
const envKey = process.env.GEMINI_API_KEY;
if (envKey) return envKey;
return loadConfig().geminiApiKey ?? null;
}
export function isGeminiApiAvailable(): boolean {
return getApiKey() !== null;
}
export interface GeminiApiOptions {
model?: string;
mimeType?: string;
signal?: AbortSignal;
timeoutMs?: number;
}
export async function queryGeminiApiWithVideo(
prompt: string,
videoUri: string,
options: GeminiApiOptions = {},
): Promise<string> {
const apiKey = getApiKey();
if (!apiKey) throw new Error("GEMINI_API_KEY not configured");
const model = options.model ?? DEFAULT_MODEL;
const signal = withTimeout(options.signal, options.timeoutMs ?? 120000);
const url = `${API_BASE}/models/${model}:generateContent?key=${apiKey}`;
const fileData: Record<string, string> = { fileUri: videoUri };
if (options.mimeType) fileData.mimeType = options.mimeType;
const body = {
contents: [
{
parts: [
{ fileData },
{ text: prompt },
],
},
],
};
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal,
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Gemini API error ${res.status}: ${errorText.slice(0, 300)}`);
}
const data = (await res.json()) as GenerateContentResponse;
const text = data.candidates?.[0]?.content?.parts
?.map((p) => p.text)
.filter(Boolean)
.join("\n");
if (!text) throw new Error("Gemini API returned empty response");
return text;
}
interface GenerateContentResponse {
candidates?: Array<{
content?: {
parts?: Array<{ text?: string }>;
};
}>;
}