Docs/Skills and code/Top-level code

test-gemini.js

Code118 lines4.7 KBTop-level code

import { pathToFileURL } from "node:url";
import { parseJson } from "./src/llm.js";
import {
  GEMMA_MODELS, LIMITS_URL, callGeminiWithRetry, explainGeminiError, listGemmaModels
} from "./src/gemini.js";

// ============================================================
// GOOGLE GEMINI API: GEMMA 4 TEST
//
//   node test-gemini.js                     ask both Gemma 4 models a short question
//   node test-gemini.js --list              list the Gemma models your key can use
//   node test-gemini.js --model=<id>        test one model (gemma-4-31b-it or gemma-4-26b-a4b-it)
//   node test-gemini.js --prompt="..."      custom user prompt
//   node test-gemini.js --no-stream         wait for the full response instead of streaming
//   node test-gemini.js --json              ask for JSON and run it through parseJson()
//   node test-gemini.js --thinking=minimal  or =high; omitted means the model default
//   node test-gemini.js --no-retry          never wait and retry after a per-minute limit
//
// Needs GEMINI_API_KEY (or GOOGLE_API_KEY), from the environment or a .env file.
// Optional: GEMINI_MODEL, GEMINI_BASE_URL. The adapter itself lives in src/gemini.js.
// Limits are per project (requests/min, input tokens/min, requests/day); see LIMITS_URL below.
// ============================================================

const DEFAULT_PROMPT = "Reply with one short sentence confirming that you are working.";
const JSON_SYSTEM = "You are a classifier. Return only a JSON object, with no commentary.";
const JSON_PROMPT = [
  'Classify the YouTube channel "Veritasium" against the ecosystem "science".',
  'Return {"classification":"core|adjacent|unknown|irrelevant","confidence":0.0,"rationale":"one sentence"}.'
].join("\n");

const args = process.argv.slice(2);
const hasFlag = (name) => args.includes(`--${name}`);
const getOption = (name) => args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);

async function main() {
  const available = await listGemmaModels();

  console.log(`Gemma models available to this key (${available.length}):`);
  for (const m of available) {
    console.log(`  ${m.id}  (input ${m.inputTokenLimit ?? "?"} tokens, output ${m.outputTokenLimit ?? "?"})`);
  }
  console.log(`Limits are per project: requests/min, input tokens/min, requests/day. Check yours at ${LIMITS_URL}`);

  if (hasFlag("list")) {
    return;
  }

  // GEMINI_MODEL, when set, narrows the run to that one model.
  const explicit = getOption("model") || process.env.GEMINI_MODEL;
  const candidates = explicit ? [explicit] : GEMMA_MODELS;
  const availableIds = new Set(available.map((m) => m.id));

  const useJson = hasFlag("json");
  const stream = !hasFlag("no-stream");
  const request = {
    system: useJson ? JSON_SYSTEM : "",
    user: getOption("prompt") || (useJson ? JSON_PROMPT : DEFAULT_PROMPT),
    stream,
    thinkingLevel: getOption("thinking") || ""
  };
  // A per-minute limit or a brief overload is worth one short wait; the daily limit never is.
  const retry = {
    maxRetries: hasFlag("no-retry") ? 0 : 1,
    maxWaitSeconds: 65,
    onWait: ({ error, waitSeconds }) =>
      console.warn(`  ${explainGeminiError(error)}\n  Waiting ${waitSeconds}s, then retrying once ...`)
  };

  const summary = [];

  for (const model of candidates) {
    console.log(`\nCalling ${model} ...`);

    if (!availableIds.has(model)) {
      console.warn(`  ${model} is not in the list above for this key; trying anyway.`);
    }

    try {
      const result = await callGeminiWithRetry({ ...request, model }, retry);

      if (!stream) {
        console.log(`[LLM] ${result.content}`);
      }

      const u = result.usage ?? {};
      console.log(
        `OK: ${result.seconds.toFixed(1)}s, finish=${result.finishReason ?? "?"}, tokens in=${u.promptTokenCount ?? "?"} ` +
        `out=${u.candidatesTokenCount ?? "?"} thinking=${u.thoughtsTokenCount ?? 0} total=${u.totalTokenCount ?? "?"}`
      );

      if (useJson) {
        console.log("parseJson() result:");
        console.log(parseJson(result.content));
      }

      summary.push({ model, status: "ok", seconds: Number(result.seconds.toFixed(1)), tries: result.attempts, tokens: u.totalTokenCount ?? null, note: "" });
    } catch (error) {
      const note = explainGeminiError(error);
      console.error(`FAILED: ${note}`);
      summary.push({ model, status: error.kind ?? "failed", seconds: null, tries: null, tokens: null, note: note.slice(0, 90) });
    }
  }

  console.log("\nSummary:");
  console.table(summary);

  if (summary.some((row) => row.status !== "ok")) {
    process.exitCode = 1;
  }
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  main().catch((error) => {
    console.error(explainGeminiError(error));
    process.exitCode = 1;
  });
}

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.