UNPKG

@jovan158/vantage

Version:

See and control what your coding agent does

143 lines (142 loc) • 5.92 kB
// Google's Gemini API — POST …/models/<model>:generateContent and // :streamGenerateContent — as OpenCode and pi send it. pi signed in with a // Google account talks to Code Assist instead // (…/v1internal:streamGenerateContent), which wraps the same request in // "request" and each response in "response"; both are read here. // // usageMetadata is cumulative: the last chunk's counts are the totals. The // prompt count includes cached input and the thoughts are billed as output, // so both are moved to where Anthropic reports them. import { MAX_INPUT, MAX_TEXT, lastUserText, toolTarget, truncate, } from "../turn.js"; import { sseParser } from "./sse.js"; import { stringValues } from "./openai.js"; function unwrap(raw) { if (!raw || typeof raw !== "object") return null; const r = raw; return r.response && typeof r.response === "object" ? r.response : raw; } function call(name, input) { const target = toolTarget(input); return { name, inputPreview: truncate(JSON.stringify(input ?? {}), MAX_INPUT), ...(target ? { target } : {}) }; } class GeminiTurn { usage = { model: null, input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }; text = ""; stopReason = null; tools = []; add(raw) { const r = unwrap(raw); if (!r) return; if (r.modelVersion) this.usage.model = r.modelVersion; const m = r.usageMetadata; if (m) { const cached = m.cachedContentTokenCount ?? 0; const prompt = (m.promptTokenCount ?? 0) + (m.toolUsePromptTokenCount ?? 0); this.usage.input_tokens = Math.max(0, prompt - cached); this.usage.cache_read_input_tokens = cached; this.usage.output_tokens = (m.candidatesTokenCount ?? 0) + (m.thoughtsTokenCount ?? 0); } for (const c of r.candidates ?? []) { for (const p of c.content?.parts ?? []) { if (p.functionCall) this.tools.push(call(p.functionCall.name ?? "tool", p.functionCall.args)); else if (typeof p.text === "string" && !p.thought && this.text.length < MAX_TEXT * 4) this.text += p.text; } if (c.finishReason) this.stopReason = c.finishReason; } } result(model) { if (!this.usage.model) this.usage.model = model; return { model: this.usage.model, stopReason: this.stopReason, text: truncate(this.text, MAX_TEXT), tools: this.tools, usage: this.usage }; } } // The model named in the request path: …/models/gemini-3-pro:streamGenerateContent. export function geminiModelFromPath(path) { const m = /\/models\/([^/:?]+):/.exec(path); return m ? decodeURIComponent(m[1]) : null; } export function createGeminiExtractor(path = "") { const turn = new GeminiTurn(); const sse = sseParser((e) => turn.add(e)); return { feed: (chunk) => sse.feed(chunk), end() { sse.end(); return turn.result(geminiModelFromPath(path)); }, }; } // A whole response, or — for streamGenerateContent without alt=sse — a JSON // array of chunks. export function geminiTurnFromJson(body, path = "") { let json; try { json = JSON.parse(body); } catch { return null; } const chunks = Array.isArray(json) ? json : [json]; if (!chunks.some((c) => unwrap(c)?.usageMetadata || unwrap(c)?.candidates)) return null; const turn = new GeminiTurn(); for (const c of chunks) turn.add(c); return turn.result(geminiModelFromPath(path)); } function requestOf(body) { if (!body || typeof body !== "object") return null; const b = body; const r = b.request && typeof b.request === "object" ? b.request : b; return r; } function partsText(parts) { return (parts ?? []).map((p) => (typeof p.text === "string" && !p.thought ? p.text : "")).join("\n"); } export function geminiRequestInfo(body) { const r = requestOf(body); if (!r) return { prompt: null, background: false }; const messages = (r.contents ?? []).map((c) => ({ role: c.role === "model" ? "assistant" : c.role ?? "user", content: partsText(c.parts) })); return { prompt: lastUserText(messages), background: !Array.isArray(r.tools) || r.tools.length === 0, }; } export function geminiParts(body, agent) { const r = requestOf(body); if (!r) return []; const parts = [{ key: "system", text: partsText(r.systemInstruction?.parts), source: "the system prompt" }]; // Gemini matches a result to its call by name and order, not by id. const pending = []; let n = 0; for (const c of r.contents ?? []) { for (const p of c.parts ?? []) { n++; if (p.functionCall) { const tc = call(p.functionCall.name ?? "tool", p.functionCall.args); const label = `${tc.name}${tc.target ? ` ${tc.target}` : ""}`; pending.push(label); parts.push({ key: `use:${n}`, text: stringValues(p.functionCall.args), source: `${agent}'s ${label} call` }); } else if (p.functionResponse) { const name = p.functionResponse.name ?? "a tool"; const i = pending.findIndex((l) => l === name || l.startsWith(`${name} `)); const label = i === -1 ? name : pending.splice(i, 1)[0]; parts.push({ key: `result:${n}`, text: stringValues(p.functionResponse.response), source: `the output of ${label}` }); } else if (typeof p.text === "string" && !p.thought) { parts.push({ key: `text:${c.role ?? ""}`, text: p.text, source: c.role === "model" ? `${agent}'s reply` : "your message" }); } } } return parts; }