diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index befcd3dfd..9ed4ae449 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -8,6 +8,7 @@ import { scheduler } from "node:timers/promises"; import { type } from "@oh-my-pi/omptype"; import { calculateCost } from "@oh-my-pi/pi-catalog/models"; import { + ensureAntigravityVersion, getAntigravityModelWireProfile, getAntigravityUserAgent, getGeminiCliHeaders, @@ -587,6 +588,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = ( if (replacementPayload !== undefined) { requestBody = replacementPayload as typeof requestBody; } + // Fire-and-forget: the first request in a fresh process uses the pinned + // fallback version; later requests pick up the manifest-discovered one. + if (isAntigravity) void ensureAntigravityVersion(options?.fetch); const headers = isAntigravity ? { "User-Agent": getAntigravityUserAgent() } : getGeminiCliHeaders(model.id); const requestHeaders = { diff --git a/packages/catalog/src/discovery/antigravity.ts b/packages/catalog/src/discovery/antigravity.ts index 7d6cb68be..2c33ae872 100644 --- a/packages/catalog/src/discovery/antigravity.ts +++ b/packages/catalog/src/discovery/antigravity.ts @@ -162,7 +162,7 @@ export async function fetchAntigravityDiscoveryModels( options: FetchAntigravityDiscoveryModelsOptions, ): Promise[] | null> { if (options.userAgent === undefined) { - await ensureAntigravityVersion(fetch, options.signal); + await ensureAntigravityVersion(options.fetcher ?? fetch, options.signal); } const fetcher = discoveryFetch(options.fetcher); diff --git a/packages/catalog/src/wire/gemini-headers.ts b/packages/catalog/src/wire/gemini-headers.ts index 33ad43955..759cc9015 100644 --- a/packages/catalog/src/wire/gemini-headers.ts +++ b/packages/catalog/src/wire/gemini-headers.ts @@ -90,6 +90,9 @@ export function ensureAntigravityVersion(fetcher: typeof fetch = fetch, signal?: /** Antigravity `User-Agent` header value; rebuilt when the discovered version changes. */ export function getAntigravityUserAgent(): string { const version = getAntigravityVersion(); + // The backend does not validate `cl` (verified live: stale, zero, and absent + // cl all pass model gating on daily-cloudcode-pa; only the version gates). + // The update manifest carries no changelist, so the captured value stays. const cl = process.env.PI_AI_ANTIGRAVITY_CL || "963137146"; const os = process.env.PI_AI_ANTIGRAVITY_OS || "darwin"; const arch = process.env.PI_AI_ANTIGRAVITY_ARCH || "arm64"; diff --git a/packages/coding-agent/test/markit-converters.test.ts b/packages/coding-agent/test/markit-converters.test.ts deleted file mode 100644 index b20a90b9b..000000000 --- a/packages/coding-agent/test/markit-converters.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Runtime coverage for the in-house markit document engine (src/markit), which - * replaced the `markit-ai` package. Each format is generated in-memory via the - * shared zip util (src/utils/zip) — no external fixtures — and converted through - * the public wrapper (src/utils/markit), locking: docx text, xlsx tables, pptx - * slides, epub metadata+spine, shared HTML-table normalization, image - * extraction, the nested/relative zip path resolution regression surface, and - * the unsupported-format error contract. - */ -import { describe, expect, it } from "bun:test"; -import * as fs from "node:fs/promises"; -import * as os from "node:os"; -import * as path from "node:path"; -import { convertBufferWithMarkit, convertFileWithMarkit } from "@oh-my-pi/pi-coding-agent/utils/markit"; -import { zip } from "@oh-my-pi/pi-coding-agent/utils/zip"; -import { removeWithRetries } from "@oh-my-pi/pi-utils"; - -const enc = (s: string): Uint8Array => new TextEncoder().encode(s); -const WML = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; -const repoRoot = path.resolve(import.meta.dir, "..", "..", ".."); -const cliEntry = path.join(repoRoot, "packages", "coding-agent", "src", "cli.ts"); -function makeDocx(bodyXml: string): Uint8Array { - return zip({ - "[Content_Types].xml": enc( - ``, - ), - "_rels/.rels": enc( - ``, - ), - "word/document.xml": enc( - `${bodyXml}`, - ), - }); -} - -describe("markit converters", () => { - it("converts docx paragraphs to markdown", async () => { - const docx = makeDocx( - `First paragraph.Second paragraph.`, - ); - const result = await convertBufferWithMarkit(docx, ".docx"); - expect(result.ok).toBe(true); - expect(result.content).toBe("First paragraph.\n\nSecond paragraph."); - }); - - it("converts xlsx sheets to markdown tables", async () => { - const xlsx = zip({ - "xl/workbook.xml": enc( - ``, - ), - "xl/_rels/workbook.xml.rels": enc( - ``, - ), - "xl/worksheets/sheet1.xml": enc( - `NameAgeAlice30`, - ), - }); - const result = await convertBufferWithMarkit(xlsx, ".xlsx"); - expect(result.ok).toBe(true); - expect(result.content).toContain("## People"); - expect(result.content).toContain("| Name | Age |"); - expect(result.content).toContain("| --- | --- |"); - expect(result.content).toContain("| Alice | 30 |"); - }); - - it("reads an xlsx worksheet through an absolute (/-prefixed) rel target", async () => { - const xlsx = zip({ - "xl/workbook.xml": enc( - ``, - ), - "xl/_rels/workbook.xml.rels": enc( - ``, - ), - "xl/worksheets/sheet1.xml": enc( - `Header42`, - ), - }); - const result = await convertBufferWithMarkit(xlsx, ".xlsx"); - expect(result.ok).toBe(true); - expect(result.content).toContain("| Header |"); - expect(result.content).toContain("| 42 |"); - }); - - it("converts pptx slides with a title heading and body text", async () => { - const pptx = zip({ - "ppt/presentation.xml": enc( - ``, - ), - "ppt/_rels/presentation.xml.rels": enc( - ``, - ), - "ppt/slides/slide1.xml": enc( - `The TitleBody line`, - ), - }); - const result = await convertBufferWithMarkit(pptx, ".pptx"); - expect(result.ok).toBe(true); - expect(result.content).toContain("# The Title"); - expect(result.content).toContain("Body line"); - }); - - it("extracts a pptx image through a ../media relative rel target into imageDir", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "markit-pptx-")); - try { - const pptx = zip({ - "ppt/presentation.xml": enc( - ``, - ), - "ppt/_rels/presentation.xml.rels": enc( - ``, - ), - "ppt/slides/slide1.xml": enc( - ``, - ), - "ppt/slides/_rels/slide1.xml.rels": enc( - ``, - ), - "ppt/media/image1.png": new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), - }); - const pptxPath = path.join(dir, "deck.pptx"); - const imageDir = path.join(dir, "imgs"); - await Bun.write(pptxPath, pptx); - const result = await convertFileWithMarkit(pptxPath, undefined, { imageDir }); - expect(result.ok).toBe(true); - const written = await fs.readdir(imageDir); - expect(written).toHaveLength(1); - expect(result.content).toContain(`](${path.join(imageDir, written[0]!)})`); - } finally { - await removeWithRetries(dir); - } - }); - - it("converts epub spine, normalizes HTML tables, and resolves a non-root OPF basePath", async () => { - const epub = zip({ - "META-INF/container.xml": enc( - ``, - ), - "OEBPS/content.opf": enc( - `Nested BookAda`, - ), - "OEBPS/text/ch1.xhtml": enc( - `

Chapter One

Body text.

AB
12
`, - ), - }); - const result = await convertBufferWithMarkit(epub, ".epub"); - expect(result.ok).toBe(true); - expect(result.content).toContain("**Title:** Nested Book"); - expect(result.content).toContain("**Authors:** Ada"); - expect(result.content).toContain("## Chapter One"); - expect(result.content).toContain("Body text."); - // normalizeTablesHtml promotes the first row to a header so GFM renders a table. - expect(result.content).toContain("| A | B |"); - expect(result.content).toContain("| --- | --- |"); - }); - - it("reads PDF text after inline image binary data containing delimiter bytes", async () => { - const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-pdf-inline-home-")); - const homeRoot = path.parse(homeDir).root; - const homeDrive = homeRoot.endsWith(path.sep) ? homeRoot.slice(0, -1) : homeRoot; - const homePath = homeDir.slice(homeDrive.length) || path.sep; - try { - const pdfPath = path.join( - import.meta.dir, - "fixtures", - "pdf-inline-image-repro", - "bad-inline-image-delimiter.pdf", - ); - const proc = Bun.spawn([process.execPath, cliEntry, "read", pdfPath], { - cwd: repoRoot, - stdout: "pipe", - stderr: "pipe", - env: { - ...process.env, - APPDATA: path.join(homeDir, "AppData", "Roaming"), - HOME: homeDir, - HOMEDRIVE: homeDrive, - HOMEPATH: homePath, - LOCALAPPDATA: path.join(homeDir, "AppData", "Local"), - USERPROFILE: homeDir, - NO_COLOR: "1", - OMP_PROFILE: "", - PI_CODING_AGENT_DIR: path.join(homeDir, ".omp", "agent"), - PI_CONFIG_DIR: ".omp", - PI_NO_TITLE: "1", - PI_PROFILE: "", - XDG_CACHE_HOME: path.join(homeDir, ".cache"), - XDG_DATA_HOME: path.join(homeDir, ".local", "share"), - XDG_STATE_HOME: path.join(homeDir, ".local", "state"), - }, - }); - const stdout = new Response(proc.stdout).text(); - const stderr = new Response(proc.stderr).text(); - // The regression is a synchronous child-process spin; there is no in-process signal to await. - const outcome = await Promise.race([ - proc.exited.then(exitCode => ({ type: "exit" as const, exitCode })), - Bun.sleep(5_000).then(() => ({ type: "timeout" as const })), - ]); - if (outcome.type === "timeout") { - try { - proc.kill("SIGKILL"); - } catch { - // already exited - } - await proc.exited; - throw new Error("read command timed out on inline image PDF"); - } - - const [out, err] = await Promise.all([stdout, stderr]); - expect(outcome.exitCode).toBe(0); - expect(err).toBe(""); - expect(out).toContain("Inline image tokenizer repro issue"); - expect(out).toContain("| Name | Qty |"); - expect(out).toContain("| Wire | 12 |"); - } finally { - await removeWithRetries(homeDir); - } - }); - - it("reports an unsupported format instead of emitting garbage", async () => { - const rtf = enc("{\\rtf1\\ansi binary-ish}"); - const result = await convertBufferWithMarkit(rtf, ".rtf"); - expect(result.ok).toBe(false); - expect(result.error).toContain("Unsupported format"); - }); -});