SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
3.7 KB · 90 lines typescript
Raw Blame History
1// Shared helpers for the Gemini probes. No secrets here: the key comes from env GOOGLE_GEMINI_API_KEY.2import { GoogleGenAI } from "@google/genai";3import { mkdirSync, writeFileSync } from "node:fs";45export const KEY = process.env.GOOGLE_GEMINI_API_KEY ?? "";6if (!KEY) throw new Error("GOOGLE_GEMINI_API_KEY missing (set -a; . ../../.env; set +a)");78export const ai = new GoogleGenAI({ apiKey: KEY, httpOptions: { timeout: 120_000 } });910// Requested set was 3.8-flash, 3.5-flash, 3.1-pro-preview, 2.5-flash, 2.5-pro, omni — but with this free-tier key11// 2.5-* return 404 "no longer available to new users" and 3.1-pro / omni have free-tier quota 0 (see out/06-availability.json).12// Default probe set = accessible models; override with argv[2] = comma-separated ids.13export const DEFAULT_MODELS = [14  "gemini-3.8-flash",15  "gemini-3.5-flash",16  "gemini-3.5-flash-lite",17  "gemini-3.1-flash-lite",18  "gemini-3-flash-preview",19  "gemma-4-31b-it",20];21export const MODELS = process.argv[2] ? process.argv[2].split(",") : DEFAULT_MODELS;22export const SUFFIX = process.argv[2] ? "-" + process.argv[2].replace(/[^a-z0-9.-]/gi, "_") : "";2324mkdirSync("out", { recursive: true });2526export function save(name: string, data: unknown) {27  writeFileSync(`out/${name}`, JSON.stringify(data, redact, 2));28}2930// Never let a key leak into saved output.31function redact(_k: string, v: unknown) {32  if (typeof v === "string" && KEY && v.includes(KEY)) return v.replaceAll(KEY, "[REDACTED]");33  return v;34}3536export function errInfo(e: any) {37  // SDK throws ApiError {name, message, status}. message is usually the JSON error body as string.38  let parsed: any = null;39  const m: string = e?.message ?? String(e);40  try { parsed = JSON.parse(m); } catch { /* not json */ }41  const inner = parsed?.error ?? parsed;42  return {43    name: e?.name,44    httpStatus: e?.status ?? inner?.code ?? null,45    status: inner?.status ?? null,46    message: (inner?.message ?? m).slice(0, 400),47    details: inner?.details ?? null,48  };49}5051export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));5253/** Classify a 429: per-minute quota → retryable; per-day / limit 0 → give up for this model. */54export function quotaKind(e: any): "minute" | "day" | "other" {55  const info = errInfo(e);56  if (info.httpStatus !== 429) return "other";57  const q = (info.details ?? []).find((d: any) => String(d["@type"]).includes("QuotaFailure"));58  const ids: string[] = (q?.violations ?? []).map((v: any) => v.quotaId);59  if (ids.some((i) => /PerDay/.test(i))) return "day";60  if (ids.some((i) => /PerMinute/.test(i))) return "minute";61  return "other";62}6364/** Retry on per-minute 429 (honouring RetryInfo.retryDelay) and on 503 high demand; max `tries`. */65export async function withRetry<T>(fn: () => Promise<T>, tries = 4): Promise<T> {66  let last: any;67  for (let i = 0; i < tries; i++) {68    try { return await fn(); } catch (e: any) {69      last = e;70      const info = errInfo(e);71      const kind = quotaKind(e);72      if (kind === "minute" || info.httpStatus === 503) {73        const ri = (info.details ?? []).find((d: any) => String(d["@type"]).includes("RetryInfo"));74        const secs = ri?.retryDelay ? parseInt(String(ri.retryDelay)) + 2 : 20;75        console.log(`   ..${info.httpStatus} (${kind}); waiting ${secs}s then retry ${i + 1}/${tries - 1}`);76        await sleep(secs * 1000);77        continue;78      }79      throw e;80    }81  }82  throw last;83}8485/** Pace requests to stay under the free-tier RPM for a given model. */86export const PACE_MS = Number(process.env.PROBE_PACE_MS ?? 7000);8788export const shortText = (r: any) =>89  (r?.candidates?.[0]?.content?.parts ?? []).filter((p: any) => p.text && !p.thought).map((p: any) => p.text).join("").slice(0, 120);90