Fix Gemini local execution and diagnostics
This commit is contained in:
@@ -262,7 +262,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
}
|
||||
}
|
||||
const commandNotes = (() => {
|
||||
const notes: string[] = ["Prompt is passed to Gemini as the final positional argument."];
|
||||
const notes: string[] = ["Prompt is passed to Gemini via --prompt for non-interactive execution."];
|
||||
notes.push("Added --approval-mode yolo for unattended execution.");
|
||||
if (!instructionsFilePath) return notes;
|
||||
if (instructionsPrefix.length > 0) {
|
||||
@@ -324,7 +324,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
args.push("--sandbox=none");
|
||||
}
|
||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||
args.push(prompt);
|
||||
args.push("--prompt", prompt);
|
||||
return args;
|
||||
};
|
||||
|
||||
|
||||
@@ -231,6 +231,8 @@ export function describeGeminiFailure(parsed: Record<string, unknown>): string |
|
||||
}
|
||||
|
||||
const GEMINI_AUTH_REQUIRED_RE = /(?:not\s+authenticated|please\s+authenticate|api[_ ]?key\s+(?:required|missing|invalid)|authentication\s+required|unauthorized|invalid\s+credentials|not\s+logged\s+in|login\s+required|run\s+`?gemini\s+auth(?:\s+login)?`?\s+first)/i;
|
||||
const GEMINI_QUOTA_EXHAUSTED_RE =
|
||||
/(?:resource_exhausted|quota|rate[-\s]?limit|too many requests|\b429\b|billing details)/i;
|
||||
|
||||
export function detectGeminiAuthRequired(input: {
|
||||
parsed: Record<string, unknown> | null;
|
||||
@@ -248,6 +250,22 @@ export function detectGeminiAuthRequired(input: {
|
||||
return { requiresAuth };
|
||||
}
|
||||
|
||||
export function detectGeminiQuotaExhausted(input: {
|
||||
parsed: Record<string, unknown> | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}): { exhausted: boolean } {
|
||||
const errors = extractGeminiErrorMessages(input.parsed ?? {});
|
||||
const messages = [...errors, input.stdout, input.stderr]
|
||||
.join("\n")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const exhausted = messages.some((line) => GEMINI_QUOTA_EXHAUSTED_RE.test(line));
|
||||
return { exhausted };
|
||||
}
|
||||
|
||||
export function isGeminiTurnLimitResult(
|
||||
parsed: Record<string, unknown> | null | undefined,
|
||||
exitCode?: number | null,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
} from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
asBoolean,
|
||||
asNumber,
|
||||
asString,
|
||||
asStringArray,
|
||||
ensureAbsoluteDirectory,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
||||
import { detectGeminiAuthRequired, parseGeminiJsonl } from "./parse.js";
|
||||
import { detectGeminiAuthRequired, detectGeminiQuotaExhausted, parseGeminiJsonl } from "./parse.js";
|
||||
import { firstNonEmptyLine } from "./utils.js";
|
||||
|
||||
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
||||
@@ -134,13 +135,14 @@ export async function testEnvironment(
|
||||
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
||||
const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, false) ? "yolo" : "default");
|
||||
const sandbox = asBoolean(config.sandbox, false);
|
||||
const helloProbeTimeoutSec = Math.max(1, asNumber(config.helloProbeTimeoutSec, 10));
|
||||
const extraArgs = (() => {
|
||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||
return asStringArray(config.args);
|
||||
})();
|
||||
|
||||
const args = ["--output-format", "stream-json"];
|
||||
const args = ["--output-format", "stream-json", "--prompt", "Respond with hello."];
|
||||
if (model && model !== DEFAULT_GEMINI_LOCAL_MODEL) args.push("--model", model);
|
||||
if (approvalMode !== "default") args.push("--approval-mode", approvalMode);
|
||||
if (sandbox) {
|
||||
@@ -149,7 +151,6 @@ export async function testEnvironment(
|
||||
args.push("--sandbox=none");
|
||||
}
|
||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||
args.push("Respond with hello.");
|
||||
|
||||
const probe = await runChildProcess(
|
||||
`gemini-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
@@ -158,7 +159,7 @@ export async function testEnvironment(
|
||||
{
|
||||
cwd,
|
||||
env,
|
||||
timeoutSec: 45,
|
||||
timeoutSec: helloProbeTimeoutSec,
|
||||
graceSec: 5,
|
||||
onLog: async () => { },
|
||||
},
|
||||
@@ -170,8 +171,23 @@ export async function testEnvironment(
|
||||
stdout: probe.stdout,
|
||||
stderr: probe.stderr,
|
||||
});
|
||||
const quotaMeta = detectGeminiQuotaExhausted({
|
||||
parsed: parsed.resultEvent,
|
||||
stdout: probe.stdout,
|
||||
stderr: probe.stderr,
|
||||
});
|
||||
|
||||
if (probe.timedOut) {
|
||||
if (quotaMeta.exhausted) {
|
||||
checks.push({
|
||||
code: "gemini_hello_probe_quota_exhausted",
|
||||
level: "warn",
|
||||
message: probe.timedOut
|
||||
? "Gemini CLI is retrying after quota exhaustion."
|
||||
: "Gemini CLI authentication is configured, but the current account or API key is over quota.",
|
||||
...(detail ? { detail } : {}),
|
||||
hint: "The configured Gemini account or API key is over quota. Check ai.google.dev usage/billing, then retry the probe.",
|
||||
});
|
||||
} else if (probe.timedOut) {
|
||||
checks.push({
|
||||
code: "gemini_hello_probe_timed_out",
|
||||
level: "warn",
|
||||
|
||||
@@ -27,6 +27,20 @@ console.log(JSON.stringify({
|
||||
return commandPath;
|
||||
}
|
||||
|
||||
async function writeQuotaGeminiCommand(binDir: string): Promise<string> {
|
||||
const commandPath = path.join(binDir, "gemini");
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--help")) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error("429 RESOURCE_EXHAUSTED: You exceeded your current quota and billing details.");
|
||||
process.exit(1);
|
||||
`;
|
||||
await fs.writeFile(commandPath, script, "utf8");
|
||||
await fs.chmod(commandPath, 0o755);
|
||||
return commandPath;
|
||||
}
|
||||
|
||||
describe("gemini_local environment diagnostics", () => {
|
||||
it("creates a missing working directory when cwd is absolute", async () => {
|
||||
const cwd = path.join(
|
||||
@@ -86,6 +100,35 @@ describe("gemini_local environment diagnostics", () => {
|
||||
expect(args).toContain("gemini-2.5-pro");
|
||||
expect(args).toContain("--approval-mode");
|
||||
expect(args).toContain("yolo");
|
||||
expect(args).toContain("--prompt");
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("classifies quota exhaustion as a quota warning instead of a generic failure", async () => {
|
||||
const root = path.join(
|
||||
os.tmpdir(),
|
||||
`paperclip-gemini-local-quota-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
);
|
||||
const binDir = path.join(root, "bin");
|
||||
const cwd = path.join(root, "workspace");
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
await writeQuotaGeminiCommand(binDir);
|
||||
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "gemini_local",
|
||||
config: {
|
||||
command: "gemini",
|
||||
cwd,
|
||||
env: {
|
||||
GEMINI_API_KEY: "test-key",
|
||||
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.status).toBe("warn");
|
||||
expect(result.checks.some((check) => check.code === "gemini_hello_probe_quota_exhausted")).toBe(true);
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ type CapturePayload = {
|
||||
};
|
||||
|
||||
describe("gemini execute", () => {
|
||||
it("passes prompt as final argument and injects paperclip env vars", async () => {
|
||||
it("passes prompt via --prompt and injects paperclip env vars", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-gemini-execute-"));
|
||||
const workspace = path.join(root, "workspace");
|
||||
const commandPath = path.join(root, "gemini");
|
||||
@@ -96,10 +96,13 @@ describe("gemini execute", () => {
|
||||
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
||||
expect(capture.argv).toContain("--output-format");
|
||||
expect(capture.argv).toContain("stream-json");
|
||||
expect(capture.argv).toContain("--prompt");
|
||||
expect(capture.argv).toContain("--approval-mode");
|
||||
expect(capture.argv).toContain("yolo");
|
||||
expect(capture.argv.at(-1)).toContain("Follow the paperclip heartbeat.");
|
||||
expect(capture.argv.at(-1)).toContain("Paperclip runtime note:");
|
||||
const promptFlagIndex = capture.argv.indexOf("--prompt");
|
||||
const promptArg = promptFlagIndex >= 0 ? capture.argv[promptFlagIndex + 1] : "";
|
||||
expect(promptArg).toContain("Follow the paperclip heartbeat.");
|
||||
expect(promptArg).toContain("Paperclip runtime note:");
|
||||
expect(capture.paperclipEnvKeys).toEqual(
|
||||
expect.arrayContaining([
|
||||
"PAPERCLIP_AGENT_ID",
|
||||
|
||||
Reference in New Issue
Block a user