fix(provider): handle Gemini thought_signature in multi-turn tool calling and cross-model history
This commit is contained in:
+791
-2
@@ -17,6 +17,18 @@ import * as path from "node:path";
|
|||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||||
import { Type } from "typebox";
|
import { Type } from "typebox";
|
||||||
|
import {
|
||||||
|
type Api,
|
||||||
|
type AssistantMessage,
|
||||||
|
type AssistantMessageEventStream,
|
||||||
|
calculateCost,
|
||||||
|
type Context,
|
||||||
|
createAssistantMessageEventStream,
|
||||||
|
type Model,
|
||||||
|
type SimpleStreamOptions,
|
||||||
|
type ThinkingLevelMap,
|
||||||
|
type ToolCall,
|
||||||
|
} from "@earendil-works/pi-ai/compat";
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -1404,6 +1416,692 @@ function toolUpdate(onUpdate: any, text: string) {
|
|||||||
onUpdate?.({ content: [{ type: "text", text }] });
|
onUpdate?.({ content: [{ type: "text", text }] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Client diretto Antigravity — protocollo v1internal (cloudcode-pa)
|
||||||
|
// Parla direttamente con i server di inferenza di Antigravity usando il
|
||||||
|
// token OAuth dell'account (nessun subprocess agy). Endpoint, envelope e
|
||||||
|
// flusso OAuth sono stati reverse-engineered e documentati pubblicamente
|
||||||
|
// (opencode-antigravity-auth, antigravity-proxy, torana-edge).
|
||||||
|
//
|
||||||
|
// USO CONSERVATIVO: quota per-modello (retrieveUserQuota/fetchAvailableModels),
|
||||||
|
// richieste serializzate (una alla volta), retry limitati (1 refresh + 1 retry
|
||||||
|
// su 401/403). L'uso automatizzato massiccio fa scattare il re-auth di Google
|
||||||
|
// e può portare a ban dell'account — vedi ToS Antigravity/Gemini.
|
||||||
|
// =========================================================================
|
||||||
|
const ANTG_TOKEN_FILE = path.join(os.homedir(), ".gemini", "antigravity-cli", "antigravity-oauth-token");
|
||||||
|
const ANTG_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
||||||
|
const ANTG_HOSTS = ["https://daily-cloudcode-pa.googleapis.com", "https://cloudcode-pa.googleapis.com"];
|
||||||
|
const ANTG_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
||||||
|
const ANTG_CLIENT_SECRET = "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";
|
||||||
|
const ANTG_UA = "antigravity/cli/1.1.17";
|
||||||
|
const ANTG_API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1";
|
||||||
|
const ANTG_METADATA = JSON.stringify({ ideType: "ANTIGRAVITY", platform: "LINUX", pluginType: "GEMINI" });
|
||||||
|
|
||||||
|
interface AntgModelInfo {
|
||||||
|
backend: string;
|
||||||
|
thinkingLevel?: "low" | "medium" | "high";
|
||||||
|
}
|
||||||
|
const ANTG_MODEL_MAP: Record<string, AntgModelInfo> = {
|
||||||
|
"gemini-3.5-flash": { backend: "gemini-3.5-flash-low" },
|
||||||
|
"gemini-3.6-flash-medium": { backend: "gemini-3.6-flash-medium" },
|
||||||
|
"gemini-3.6-flash-high": { backend: "gemini-3.6-flash-high" },
|
||||||
|
"gemini-3.1-pro": { backend: "gemini-3.1-pro-low", thinkingLevel: "low" },
|
||||||
|
"gemini-3.1-pro-high": { backend: "gemini-3.1-pro-high", thinkingLevel: "high" },
|
||||||
|
"claude-sonnet-4.6": { backend: "claude-sonnet-4-6" },
|
||||||
|
"claude-opus-4.6": { backend: "claude-opus-4-6-thinking" },
|
||||||
|
"gpt-oss-120b": { backend: "gpt-oss-120b-medium" },
|
||||||
|
};
|
||||||
|
const ANTG_DEFAULT_MODEL = "gemini-3.5-flash";
|
||||||
|
|
||||||
|
let antgToken: { access: string; expiresAtMs: number; refresh: string } | null = null;
|
||||||
|
let antgProject: { pid: string; base: string } | null = null;
|
||||||
|
let antgQueue: Promise<unknown> = Promise.resolve(); // serializzazione richieste
|
||||||
|
|
||||||
|
function antgParseExpiryMs(s: string | undefined): number {
|
||||||
|
if (!s) return 0;
|
||||||
|
if (typeof s === "number") return s * 1000;
|
||||||
|
// RFC3339 con nanosecondi: tronca la frazione ai ms per Date.parse
|
||||||
|
const m = String(s).match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/);
|
||||||
|
if (!m) return 0;
|
||||||
|
const ms = Date.parse(m[1] + (m[2] ? m[2].slice(0, 4) : "") + (m[3] || "Z"));
|
||||||
|
return Number.isNaN(ms) ? 0 : ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
function antgReadTokenFile(): any {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(ANTG_TOKEN_FILE, "utf8"));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function antgWriteTokenFile(data: any) {
|
||||||
|
try {
|
||||||
|
const tmp = ANTG_TOKEN_FILE + ".tmp";
|
||||||
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
||||||
|
fs.renameSync(tmp, ANTG_TOKEN_FILE);
|
||||||
|
} catch {
|
||||||
|
/* ignora */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function antgRefreshToken(refreshToken: string): Promise<{ access: string; expiresAtMs: number; refresh: string }> {
|
||||||
|
const resp = await fetch(ANTG_OAUTH_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: ANTG_CLIENT_ID,
|
||||||
|
client_secret: ANTG_CLIENT_SECRET,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
});
|
||||||
|
const payload: any = await resp.json().catch(() => ({}));
|
||||||
|
if (!resp.ok) throw new Error(`OAuth refresh fallito (HTTP ${resp.status}): ${JSON.stringify(payload).slice(0, 200)}`);
|
||||||
|
const expiresIn = Number(payload.expires_in ?? 3600);
|
||||||
|
return {
|
||||||
|
access: payload.access_token,
|
||||||
|
expiresAtMs: Date.now() + expiresIn * 1000,
|
||||||
|
refresh: payload.refresh_token || refreshToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function antgGetAccessToken(): Promise<string> {
|
||||||
|
if (antgToken && antgToken.expiresAtMs > Date.now() + 120_000) return antgToken.access;
|
||||||
|
const data = antgReadTokenFile();
|
||||||
|
if (!data?.token?.refresh_token) {
|
||||||
|
throw new Error("Nessun token OAuth Antigravity: avvia `agy` almeno una volta per autenticarti con l'account.");
|
||||||
|
}
|
||||||
|
const tok = data.token;
|
||||||
|
const expMs = antgParseExpiryMs(tok.expiry);
|
||||||
|
if (tok.access_token && expMs > Date.now() + 120_000) {
|
||||||
|
antgToken = { access: tok.access_token, expiresAtMs: expMs, refresh: tok.refresh_token };
|
||||||
|
return tok.access_token;
|
||||||
|
}
|
||||||
|
const fresh = await antgRefreshToken(tok.refresh_token);
|
||||||
|
data.token = {
|
||||||
|
...tok,
|
||||||
|
access_token: fresh.access,
|
||||||
|
token_type: "Bearer",
|
||||||
|
expiry: new Date(fresh.expiresAtMs).toISOString().replace(/\.\d{3}Z$/, ".000000Z"),
|
||||||
|
};
|
||||||
|
antgWriteTokenFile(data);
|
||||||
|
antgToken = fresh;
|
||||||
|
return fresh.access;
|
||||||
|
}
|
||||||
|
|
||||||
|
function antgDeepFind(obj: any, key: string): any {
|
||||||
|
if (obj && typeof obj === "object") {
|
||||||
|
if (key in obj) return obj[key];
|
||||||
|
for (const v of Object.values(obj)) {
|
||||||
|
const r = antgDeepFind(v, key);
|
||||||
|
if (r != null) return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function antgHeaders(token: string, stream = false): Record<string, string> {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": ANTG_UA,
|
||||||
|
"X-Goog-Api-Client": ANTG_API_CLIENT,
|
||||||
|
"Client-Metadata": ANTG_METADATA,
|
||||||
|
...(stream ? { Accept: "text/event-stream" } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function antgLoadCodeAssist(token: string, base: string): Promise<string> {
|
||||||
|
const resp = await fetch(`${base}/v1internal:loadCodeAssist`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: antgHeaders(token),
|
||||||
|
body: "{}",
|
||||||
|
signal: AbortSignal.timeout(60_000),
|
||||||
|
});
|
||||||
|
const payload: any = await resp.json().catch(() => ({}));
|
||||||
|
if (!resp.ok) throw new Error(`loadCodeAssist HTTP ${resp.status}`);
|
||||||
|
const pid = antgDeepFind(payload, "cloudaicompanionProject") ?? antgDeepFind(payload, "cloudaicompanion_project");
|
||||||
|
if (!pid) throw new Error("loadCodeAssist senza project id");
|
||||||
|
return String(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function antgGetProject(): Promise<{ pid: string; base: string }> {
|
||||||
|
if (antgProject) return antgProject;
|
||||||
|
const token = await antgGetAccessToken();
|
||||||
|
let lastErr = "";
|
||||||
|
for (const base of ANTG_HOSTS) {
|
||||||
|
try {
|
||||||
|
antgProject = { pid: await antgLoadCodeAssist(token, base), base };
|
||||||
|
return antgProject;
|
||||||
|
} catch (e: any) {
|
||||||
|
lastErr = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Discovery project fallito: ${lastErr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AntgGenerateOpts {
|
||||||
|
prompt: string;
|
||||||
|
model?: string;
|
||||||
|
system?: string;
|
||||||
|
maxOutputTokens?: number;
|
||||||
|
temperature?: number;
|
||||||
|
thinking?: "low" | "medium" | "high" | "off";
|
||||||
|
stream?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function antgResolveModel(friendly: string | undefined): AntgModelInfo {
|
||||||
|
if (friendly && ANTG_MODEL_MAP[friendly]) return ANTG_MODEL_MAP[friendly];
|
||||||
|
if (friendly) return { backend: friendly };
|
||||||
|
return ANTG_MODEL_MAP[ANTG_DEFAULT_MODEL]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function antgReadStream(resp: any): Promise<{ text: string; details: any }> {
|
||||||
|
const reader = resp.body?.getReader();
|
||||||
|
if (!reader) throw new Error("Nessun body streaming");
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buf = "", text = "", finish = "", usage: any, modelVersion = "", responseId = "", events = 0;
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
// il framing SSE usa CRLF: normalizza a \n
|
||||||
|
buf += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
|
||||||
|
let i;
|
||||||
|
while ((i = buf.indexOf("\n\n")) >= 0) {
|
||||||
|
const chunk = buf.slice(0, i);
|
||||||
|
buf = buf.slice(i + 2);
|
||||||
|
for (const line of chunk.split("\n")) {
|
||||||
|
if (!line.startsWith("data:")) continue;
|
||||||
|
const d = line.slice(5).trim();
|
||||||
|
if (!d) continue;
|
||||||
|
events++;
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(d);
|
||||||
|
const inner = o.response ?? o;
|
||||||
|
for (const pt of inner.candidates?.[0]?.content?.parts ?? []) if (pt.text) text += pt.text;
|
||||||
|
if (inner.candidates?.[0]?.finishReason) finish = inner.candidates[0].finishReason;
|
||||||
|
if (inner.usageMetadata) usage = inner.usageMetadata;
|
||||||
|
if (inner.modelVersion) modelVersion = inner.modelVersion;
|
||||||
|
if (inner.responseId) responseId = inner.responseId;
|
||||||
|
} catch {
|
||||||
|
/* evento non-JSON: ignora */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: text.trim(),
|
||||||
|
details: { finishReason: finish || undefined, usage, modelVersion, responseId, events },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function antgGenerate(opts: AntgGenerateOpts): Promise<{ text: string; details: any }> {
|
||||||
|
// serializza: una richiesta alla volta (uso conservativo del canale account)
|
||||||
|
const run = antgQueue.then(async () => {
|
||||||
|
const { pid, base } = await antgGetProject();
|
||||||
|
const mi = antgResolveModel(opts.model);
|
||||||
|
const requestId = `agent-${crypto.randomUUID().replace(/-/g, "")}`;
|
||||||
|
const gc: any = {
|
||||||
|
maxOutputTokens: opts.maxOutputTokens ?? 8192,
|
||||||
|
temperature: opts.temperature ?? 0.4,
|
||||||
|
};
|
||||||
|
if (mi.thinkingLevel) gc.thinkingConfig = { thinkingLevel: mi.thinkingLevel };
|
||||||
|
if (opts.thinking && opts.thinking !== "off") gc.thinkingConfig = { thinkingLevel: opts.thinking };
|
||||||
|
const request: any = {
|
||||||
|
contents: [{ role: "user", parts: [{ text: opts.prompt }] }],
|
||||||
|
generationConfig: gc,
|
||||||
|
};
|
||||||
|
if (opts.system) request.systemInstruction = { parts: [{ text: opts.system }] };
|
||||||
|
const envelope = {
|
||||||
|
project: pid,
|
||||||
|
model: mi.backend,
|
||||||
|
request,
|
||||||
|
requestType: "agent",
|
||||||
|
userAgent: "antigravity",
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const doCall = async (token: string) => {
|
||||||
|
if (opts.stream !== false) {
|
||||||
|
const resp = await fetch(`${base}/v1internal:streamGenerateContent?alt=sse`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: antgHeaders(token, true),
|
||||||
|
body: JSON.stringify(envelope),
|
||||||
|
signal: AbortSignal.timeout(300_000),
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error(`streamGenerateContent HTTP ${resp.status}`);
|
||||||
|
return await antgReadStream(resp);
|
||||||
|
}
|
||||||
|
const resp = await fetch(`${base}/v1internal:generateContent`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: antgHeaders(token),
|
||||||
|
body: JSON.stringify(envelope),
|
||||||
|
signal: AbortSignal.timeout(180_000),
|
||||||
|
});
|
||||||
|
const raw = await resp.text();
|
||||||
|
if (!resp.ok) throw new Error(`generateContent HTTP ${resp.status}: ${raw.slice(0, 200)}`);
|
||||||
|
const p = JSON.parse(raw);
|
||||||
|
const inner = p.response ?? p;
|
||||||
|
return {
|
||||||
|
text: (inner.candidates?.[0]?.content?.parts ?? []).map((x: any) => x.text ?? "").join("").trim(),
|
||||||
|
details: {
|
||||||
|
finishReason: inner.candidates?.[0]?.finishReason,
|
||||||
|
usage: inner.usageMetadata,
|
||||||
|
modelVersion: inner.modelVersion,
|
||||||
|
responseId: inner.responseId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await doCall(await antgGetAccessToken());
|
||||||
|
} catch (e: any) {
|
||||||
|
// 401/403 → un solo refresh + retry; niente loop
|
||||||
|
if (/401|403|UNAUTHENTICATED/.test(String(e.message))) {
|
||||||
|
antgToken = null;
|
||||||
|
return await doCall(await antgGetAccessToken());
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
antgQueue = run.catch(() => undefined);
|
||||||
|
return run as Promise<{ text: string; details: any }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Provider "antigravity" — modelli del gateway come provider pi nativo
|
||||||
|
// Registrato con pi.registerProvider() + streamSimple: appare nel selettore
|
||||||
|
// modelli (e in `pi --list-models`). Riusa il client v1internal qui sopra.
|
||||||
|
// NOTA: canale account — quota per-modello e rischio ToS/ban reale se usato
|
||||||
|
// come default con traffico continuo. Preferire per uso selettivo.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
interface AntgProviderModelDef {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
reasoning: boolean;
|
||||||
|
images: boolean;
|
||||||
|
thinkingLevelMap?: ThinkingLevelMap;
|
||||||
|
contextWindow: number;
|
||||||
|
maxTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANTG_THINK_MAP: ThinkingLevelMap = {
|
||||||
|
off: null,
|
||||||
|
minimal: "low",
|
||||||
|
low: "low",
|
||||||
|
medium: "medium",
|
||||||
|
high: "high",
|
||||||
|
xhigh: "high",
|
||||||
|
max: "high",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ANTG_PROVIDER_MODELS: AntgProviderModelDef[] = [
|
||||||
|
{ id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)", reasoning: true, images: true, thinkingLevelMap: ANTG_THINK_MAP, contextWindow: 1048576, maxTokens: 65536 },
|
||||||
|
{ id: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash (Medium)", reasoning: true, images: true, thinkingLevelMap: ANTG_THINK_MAP, contextWindow: 1048576, maxTokens: 65536 },
|
||||||
|
{ id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash (High)", reasoning: true, images: true, thinkingLevelMap: ANTG_THINK_MAP, contextWindow: 1048576, maxTokens: 65536 },
|
||||||
|
{ id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Medium)", reasoning: true, images: true, thinkingLevelMap: ANTG_THINK_MAP, contextWindow: 1048576, maxTokens: 65536 },
|
||||||
|
{ id: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash (Medium)", reasoning: true, images: true, thinkingLevelMap: ANTG_THINK_MAP, contextWindow: 1048576, maxTokens: 65536 },
|
||||||
|
{ id: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash (High)", reasoning: true, images: true, thinkingLevelMap: ANTG_THINK_MAP, contextWindow: 1048576, maxTokens: 65536 },
|
||||||
|
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)", reasoning: true, images: true, thinkingLevelMap: ANTG_THINK_MAP, contextWindow: 1048576, maxTokens: 65536 },
|
||||||
|
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro (High)", reasoning: true, images: true, thinkingLevelMap: ANTG_THINK_MAP, contextWindow: 1048576, maxTokens: 65536 },
|
||||||
|
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Thinking)", reasoning: true, images: true, thinkingLevelMap: { off: null }, contextWindow: 1000000, maxTokens: 64000 },
|
||||||
|
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)", reasoning: true, images: true, thinkingLevelMap: { off: null }, contextWindow: 1000000, maxTokens: 64000 },
|
||||||
|
{ id: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)", reasoning: false, images: false, contextWindow: 262144, maxTokens: 32768 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Modelli attualmente registrati nel provider (statici all'avvio, aggiornabili
|
||||||
|
// con /agy:refresh-models dal catalogo vivo fetchAvailableModels).
|
||||||
|
let antgRegisteredModels: AntgProviderModelDef[] = ANTG_PROVIDER_MODELS;
|
||||||
|
|
||||||
|
// Configurazione del provider "antigravity" (riusata all'avvio e al refresh).
|
||||||
|
function antgProviderConfig(models: AntgProviderModelDef[]) {
|
||||||
|
return {
|
||||||
|
name: "Antigravity (account)",
|
||||||
|
baseUrl: ANTG_HOSTS[0],
|
||||||
|
apiKey: "antigravity",
|
||||||
|
api: "antigravity",
|
||||||
|
models: models.map((m) => {
|
||||||
|
const input: ("text" | "image")[] = m.images ? ["text", "image"] : ["text"];
|
||||||
|
return {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
reasoning: m.reasoning,
|
||||||
|
thinkingLevelMap: m.thinkingLevelMap,
|
||||||
|
input,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: m.contextWindow,
|
||||||
|
maxTokens: m.maxTokens,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
streamSimple: streamAntigravity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scarica il catalogo modelli vivo dall'account e lo filtra: niente modelli
|
||||||
|
// interni (displayName vuoto, chat_*, tab_*), niente placeholder, niente
|
||||||
|
// gemini-2.5-* (ritirati → HTTP 429). Restituisce la lista per il provider.
|
||||||
|
async function antgFetchCatalog(token: string, base: string): Promise<AntgProviderModelDef[]> {
|
||||||
|
const resp = await fetch(`${base}/v1internal:fetchAvailableModels`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: antgHeaders(token),
|
||||||
|
body: "{}",
|
||||||
|
signal: AbortSignal.timeout(60_000),
|
||||||
|
});
|
||||||
|
const raw = await resp.text();
|
||||||
|
if (!resp.ok) throw new Error(`fetchAvailableModels HTTP ${resp.status}: ${raw.slice(0, 200)}`);
|
||||||
|
const payload = JSON.parse(raw);
|
||||||
|
const catalog: Record<string, any> = payload.models ?? {};
|
||||||
|
const out: AntgProviderModelDef[] = [];
|
||||||
|
for (const [backendId, info] of Object.entries(catalog)) {
|
||||||
|
const m = info as any;
|
||||||
|
const name = m.displayName;
|
||||||
|
if (!name || typeof name !== "string" || !name.trim()) continue; // interni/autocomplete
|
||||||
|
if (m.isInternal) continue;
|
||||||
|
if (backendId.startsWith("chat_") || backendId.startsWith("tab_")) continue;
|
||||||
|
if (backendId.includes("MODEL_PLACEHOLDER")) continue;
|
||||||
|
if (backendId.startsWith("gemini-2.5-")) continue; // ritirati (429)
|
||||||
|
const ctx = Number(m.maxTokens);
|
||||||
|
if (!ctx) continue; // modelli senza contesto (es. gemini-3.1-flash-image, generazione immagini)
|
||||||
|
const reasoning = m.supportsThinking === true;
|
||||||
|
const images = m.supportsImages === true;
|
||||||
|
const thinkingLevelMap = reasoning ? (backendId.startsWith("gemini-") ? ANTG_THINK_MAP : { off: null }) : undefined;
|
||||||
|
out.push({
|
||||||
|
id: backendId,
|
||||||
|
name,
|
||||||
|
reasoning,
|
||||||
|
images,
|
||||||
|
thinkingLevelMap,
|
||||||
|
contextWindow: ctx,
|
||||||
|
maxTokens: Number(m.maxOutputTokens) || 65536,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Campi accettati dal gateway (validazione protobuf stretta — i campi ignoti
|
||||||
|
// tipo $defs/$ref/$schema causano HTTP 400 INVALID_ARGUMENT). Perplexity +
|
||||||
|
// riproduzione locale confermano: solo type/properties/required/items/enum/…
|
||||||
|
const ANTG_SCHEMA_ALLOWED = new Set(["type", "description", "properties", "required", "items", "enum", "format", "nullable", "minimum", "maximum"]);
|
||||||
|
|
||||||
|
function antgToGeminiParameters(tschema: any): any {
|
||||||
|
try {
|
||||||
|
const root = JSON.parse(JSON.stringify(tschema));
|
||||||
|
const visit = (node: any): any => {
|
||||||
|
if (Array.isArray(node)) return node.map(visit);
|
||||||
|
if (node === null || typeof node !== "object") return node;
|
||||||
|
const out: any = {};
|
||||||
|
for (const [k, v] of Object.entries(node)) {
|
||||||
|
if (!ANTG_SCHEMA_ALLOWED.has(k)) continue;
|
||||||
|
if (k === "properties" && v && typeof v === "object" && !Array.isArray(v)) {
|
||||||
|
out.properties = Object.fromEntries(Object.entries(v as any).map(([n, c]) => [n, visit(c)]));
|
||||||
|
} else if (k === "items") {
|
||||||
|
out.items = visit(v);
|
||||||
|
} else {
|
||||||
|
out[k] = visit(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
return visit(root);
|
||||||
|
} catch {
|
||||||
|
return { type: "object", properties: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function antgAbortSignal(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
|
||||||
|
if (!signal) return AbortSignal.timeout(timeoutMs);
|
||||||
|
const c = new AbortController();
|
||||||
|
const t = setTimeout(() => c.abort(), timeoutMs);
|
||||||
|
const onAbort = () => c.abort(signal.reason);
|
||||||
|
signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
c.signal.addEventListener("abort", () => {
|
||||||
|
clearTimeout(t);
|
||||||
|
signal.removeEventListener("abort", onAbort);
|
||||||
|
}, { once: true });
|
||||||
|
return c.signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function antgWithQueue<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
const p = antgQueue.then(() => fn());
|
||||||
|
antgQueue = p.catch(() => undefined);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function antgBuildGeminiRequest(model: Model<Api>, context: Context, options?: SimpleStreamOptions): any {
|
||||||
|
const contents: any[] = [];
|
||||||
|
const push = (role: "user" | "model", part: any) => {
|
||||||
|
const last = contents[contents.length - 1];
|
||||||
|
if (last && last.role === role) last.parts.push(part);
|
||||||
|
else contents.push({ role, parts: [part] });
|
||||||
|
};
|
||||||
|
const signedToolCallIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const msg of context.messages) {
|
||||||
|
if (msg.role === "user") {
|
||||||
|
const items = typeof msg.content === "string" ? [{ type: "text", text: msg.content }] : msg.content;
|
||||||
|
for (const b of items as any[]) {
|
||||||
|
if (b.type === "text") push("user", { text: b.text });
|
||||||
|
else if (b.type === "image") push("user", { inlineData: { mimeType: b.mimeType, data: b.data } });
|
||||||
|
}
|
||||||
|
} else if (msg.role === "assistant") {
|
||||||
|
const isSame = msg.provider === model.provider && msg.model === model.id;
|
||||||
|
const parts: any[] = [];
|
||||||
|
for (const b of msg.content) {
|
||||||
|
if (b.type === "text") {
|
||||||
|
const sig = b.textSignature || (isSame ? (b as any).thoughtSignature : undefined);
|
||||||
|
if ((!b.text || !b.text.trim()) && !sig) continue;
|
||||||
|
parts.push({ text: b.text, ...(sig ? { thoughtSignature: sig } : {}) });
|
||||||
|
} else if (b.type === "thinking") {
|
||||||
|
const sig = b.thinkingSignature || (isSame ? (b as any).thoughtSignature : undefined);
|
||||||
|
if ((!b.thinking || !b.thinking.trim()) && !sig) continue;
|
||||||
|
parts.push({ thought: true, text: b.thinking, ...(sig ? { thoughtSignature: sig } : {}) });
|
||||||
|
} else if (b.type === "toolCall") {
|
||||||
|
const sig = b.thoughtSignature || (b as any).thought_signature;
|
||||||
|
if (sig) {
|
||||||
|
if (b.id) signedToolCallIds.add(b.id);
|
||||||
|
if (b.name) signedToolCallIds.add(b.name);
|
||||||
|
parts.push({
|
||||||
|
functionCall: { name: b.name, args: b.arguments ?? {} },
|
||||||
|
thoughtSignature: sig,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Chiamata non firmata (cross-model o da provider terzo): Google Gemini 2.5/3.x
|
||||||
|
// rigetta con HTTP 400 i functionCall privi di thought_signature.
|
||||||
|
// La serializziamo come testo per mantenere il contesto senza causare l'errore 400.
|
||||||
|
parts.push({
|
||||||
|
text: `[Tool Call: ${b.name}]\nArgs: ${JSON.stringify(b.arguments ?? {})}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const p of parts) push("model", p);
|
||||||
|
} else if (msg.role === "toolResult") {
|
||||||
|
const items = typeof msg.content === "string" ? [{ type: "text", text: msg.content }] : msg.content;
|
||||||
|
const text = items.map((c: any) => (c.type === "text" ? c.text : "")).join("\n");
|
||||||
|
const isSigned = (msg.toolCallId && signedToolCallIds.has(msg.toolCallId)) || signedToolCallIds.has(msg.toolName);
|
||||||
|
if (isSigned) {
|
||||||
|
push("user", { functionResponse: { name: msg.toolName, response: { result: text, isError: !!msg.isError } } });
|
||||||
|
} else {
|
||||||
|
push("user", { text: `[Tool Result: ${msg.toolName}]\n${text}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const gc: any = { maxOutputTokens: model.maxTokens || 8192 };
|
||||||
|
if (model.id.startsWith("gemini-") && model.reasoning) {
|
||||||
|
const level = options?.reasoning ?? "low";
|
||||||
|
const antgLevel = level === "low" || level === "minimal" ? "low" : level === "medium" ? "medium" : "high";
|
||||||
|
gc.thinkingConfig = { thinkingLevel: antgLevel, includeThoughts: true };
|
||||||
|
}
|
||||||
|
const request: any = { contents, generationConfig: gc };
|
||||||
|
if (context.systemPrompt) request.systemInstruction = { parts: [{ text: context.systemPrompt }] };
|
||||||
|
if (context.tools?.length) {
|
||||||
|
request.tools = [{
|
||||||
|
functionDeclarations: context.tools.map((t) => ({
|
||||||
|
name: t.name,
|
||||||
|
description: t.description,
|
||||||
|
parameters: antgToGeminiParameters(t.parameters),
|
||||||
|
})),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function antgConsumeSse(resp: any, model: Model<Api>, output: AssistantMessage, stream: AssistantMessageEventStream) {
|
||||||
|
const reader = resp.body?.getReader();
|
||||||
|
if (!reader) throw new Error("Nessun body streaming");
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buf = "", finishReason = "", usageMeta: any;
|
||||||
|
const blocks = output.content;
|
||||||
|
let currentBlock: any = null;
|
||||||
|
const blockIndex = () => blocks.length - 1;
|
||||||
|
const endCurrent = () => {
|
||||||
|
if (!currentBlock) return;
|
||||||
|
if (currentBlock.type === "text") stream.push({ type: "text_end", contentIndex: blockIndex(), content: currentBlock.text, partial: output });
|
||||||
|
else stream.push({ type: "thinking_end", contentIndex: blockIndex(), content: currentBlock.thinking, partial: output });
|
||||||
|
currentBlock = null;
|
||||||
|
};
|
||||||
|
let toolCallCounter = 0;
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
// il framing SSE usa CRLF: normalizza a \n
|
||||||
|
buf += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
|
||||||
|
let i;
|
||||||
|
while ((i = buf.indexOf("\n\n")) >= 0) {
|
||||||
|
const chunk = buf.slice(0, i);
|
||||||
|
buf = buf.slice(i + 2);
|
||||||
|
for (const line of chunk.split("\n")) {
|
||||||
|
if (!line.startsWith("data:")) continue;
|
||||||
|
const d = line.slice(5).trim();
|
||||||
|
if (!d) continue;
|
||||||
|
let o: any;
|
||||||
|
try { o = JSON.parse(d); } catch { continue; }
|
||||||
|
const inner = o.response ?? o;
|
||||||
|
const candidate = inner.candidates?.[0];
|
||||||
|
if (candidate?.content?.parts) {
|
||||||
|
for (const part of candidate.content.parts) {
|
||||||
|
const sig = part.thoughtSignature || part.thought_signature || candidate.content?.thoughtSignature || candidate.content?.thought_signature;
|
||||||
|
if (part.text !== undefined) {
|
||||||
|
const isThinking = part.thought === true;
|
||||||
|
if (!currentBlock || (isThinking && currentBlock.type !== "thinking") || (!isThinking && currentBlock.type !== "text")) {
|
||||||
|
endCurrent();
|
||||||
|
if (isThinking) {
|
||||||
|
currentBlock = { type: "thinking", thinking: "", thinkingSignature: sig };
|
||||||
|
blocks.push(currentBlock);
|
||||||
|
stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
|
||||||
|
} else {
|
||||||
|
currentBlock = { type: "text", text: "", textSignature: sig };
|
||||||
|
blocks.push(currentBlock);
|
||||||
|
stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentBlock.type === "thinking") {
|
||||||
|
currentBlock.thinking += part.text;
|
||||||
|
if (sig) currentBlock.thinkingSignature = sig;
|
||||||
|
stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta: part.text, partial: output });
|
||||||
|
} else {
|
||||||
|
currentBlock.text += part.text;
|
||||||
|
if (sig) currentBlock.textSignature = sig;
|
||||||
|
stream.push({ type: "text_delta", contentIndex: blockIndex(), delta: part.text, partial: output });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (part.functionCall) {
|
||||||
|
endCurrent();
|
||||||
|
const tc: ToolCall = {
|
||||||
|
type: "toolCall",
|
||||||
|
id: part.functionCall.id || `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}`,
|
||||||
|
name: part.functionCall.name || "",
|
||||||
|
arguments: part.functionCall.args ?? {},
|
||||||
|
...(sig ? { thoughtSignature: sig } : {}),
|
||||||
|
};
|
||||||
|
blocks.push(tc);
|
||||||
|
const ci = blockIndex();
|
||||||
|
stream.push({ type: "toolcall_start", contentIndex: ci, partial: output });
|
||||||
|
stream.push({ type: "toolcall_end", contentIndex: ci, toolCall: tc, partial: output });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inner.usageMetadata) usageMeta = inner.usageMetadata;
|
||||||
|
if (inner.candidates?.[0]?.finishReason) finishReason = inner.candidates[0].finishReason;
|
||||||
|
if (inner.responseId && !output.responseId) output.responseId = inner.responseId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endCurrent();
|
||||||
|
if (usageMeta) {
|
||||||
|
output.usage.input = usageMeta.promptTokenCount ?? 0;
|
||||||
|
output.usage.output = (usageMeta.candidatesTokenCount ?? 0) + (usageMeta.thoughtsTokenCount ?? 0);
|
||||||
|
output.usage.reasoning = usageMeta.thoughtsTokenCount ?? 0;
|
||||||
|
output.usage.totalTokens = usageMeta.totalTokenCount ?? (output.usage.input + output.usage.output);
|
||||||
|
output.usage.cost = calculateCost(model, output.usage);
|
||||||
|
}
|
||||||
|
output.rawStopReason = finishReason || undefined;
|
||||||
|
output.stopReason = blocks.some((b) => b.type === "toolCall") ? "toolUse" : finishReason === "MAX_TOKENS" ? "length" : "stop";
|
||||||
|
}
|
||||||
|
|
||||||
|
function streamAntigravity(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
||||||
|
const stream = createAssistantMessageEventStream();
|
||||||
|
const output: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
api: model.api,
|
||||||
|
provider: model.provider,
|
||||||
|
model: model.id,
|
||||||
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
||||||
|
stopReason: "pending",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
stream.push({ type: "start", partial: output });
|
||||||
|
await antgWithQueue(async () => {
|
||||||
|
const { pid, base } = await antgGetProject();
|
||||||
|
const token = await antgGetAccessToken();
|
||||||
|
const envelope = {
|
||||||
|
project: pid,
|
||||||
|
model: model.id,
|
||||||
|
request: antgBuildGeminiRequest(model, context, options),
|
||||||
|
requestType: "agent",
|
||||||
|
userAgent: "antigravity",
|
||||||
|
requestId: `agent-${crypto.randomUUID().replace(/-/g, "")}`,
|
||||||
|
};
|
||||||
|
const signal = antgAbortSignal(options?.signal, 300_000);
|
||||||
|
const call = (tok: string) =>
|
||||||
|
fetch(`${base}/v1internal:streamGenerateContent?alt=sse`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: antgHeaders(tok, true),
|
||||||
|
body: JSON.stringify(envelope),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
let resp = await call(token);
|
||||||
|
if (resp.status === 401 || resp.status === 403) {
|
||||||
|
antgToken = null;
|
||||||
|
resp = await call(await antgGetAccessToken());
|
||||||
|
}
|
||||||
|
if (!resp.ok) {
|
||||||
|
const bodyText = await resp.text().catch(() => "");
|
||||||
|
throw new Error(`Antigravity HTTP ${resp.status}: ${bodyText.slice(0, 300)}`);
|
||||||
|
}
|
||||||
|
await antgConsumeSse(resp, model, output, stream);
|
||||||
|
});
|
||||||
|
if (output.stopReason === "pending") throw new Error("Provider stream terminato senza stop reason");
|
||||||
|
if (output.stopReason === "error" || output.stopReason === "aborted") throw new Error(output.errorMessage || "Errore sconosciuto");
|
||||||
|
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse" | "deferred", message: output });
|
||||||
|
stream.end();
|
||||||
|
} catch (error) {
|
||||||
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||||
|
output.errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
stream.push({ type: "error", reason: output.stopReason as "aborted" | "error", error: output });
|
||||||
|
stream.end();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Estensione
|
// Estensione
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -2101,6 +2799,97 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// TOOL: antigravity_chat — client diretto protocollo Antigravity (account)
|
||||||
|
// =========================================================================
|
||||||
|
pi.registerTool({
|
||||||
|
name: "antigravity_chat",
|
||||||
|
label: "antigravity direct protocol chat",
|
||||||
|
description:
|
||||||
|
"Chatta direttamente con i server di inferenza di Antigravity via protocollo cloudcode-pa (v1internal), " +
|
||||||
|
"usando il token OAuth dell'account (nessun subprocess agy, streaming SSE nativo). " +
|
||||||
|
"Modelli del gateway: gemini-3.5-flash (default, economico), gemini-3.6-flash-medium, gemini-3.6-flash-high, " +
|
||||||
|
"gemini-3.1-pro, gemini-3.1-pro-high, claude-sonnet-4.6, claude-opus-4.6, gpt-oss-120b. " +
|
||||||
|
"ATTENZIONE: canale account con quota per-modello e ToS Google (l'uso automatizzato massiccio può far scattare re-auth/ban). " +
|
||||||
|
"Usare con moderazione: una richiesta alla volta, niente batch automatici, preferire gemini-3.5-flash per task semplici.",
|
||||||
|
parameters: Type.Object({
|
||||||
|
prompt: Type.String({ description: "Il messaggio da inviare al modello" }),
|
||||||
|
model: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
description:
|
||||||
|
"Modello (default gemini-3.5-flash): gemini-3.5-flash | gemini-3.6-flash-medium | gemini-3.6-flash-high | gemini-3.1-pro | gemini-3.1-pro-high | claude-sonnet-4.6 | claude-opus-4.6 | gpt-oss-120b",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
system: Type.Optional(Type.String({ description: "System instruction opzionale" })),
|
||||||
|
maxOutputTokens: Type.Optional(Type.Number({ description: "Token massimi di output (default 8192)" })),
|
||||||
|
temperature: Type.Optional(Type.Number({ description: "Temperatura 0-2 (default 0.4)" })),
|
||||||
|
thinking: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
description: "Livello thinking per Gemini: low | medium | high | off (default: auto in base al modello)",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
stream: Type.Optional(Type.Boolean({ description: "Streaming SSE (default true)" })),
|
||||||
|
}),
|
||||||
|
async execute(toolCallId, params) {
|
||||||
|
const p = params as any;
|
||||||
|
try {
|
||||||
|
const res = await antgGenerate({
|
||||||
|
prompt: p.prompt,
|
||||||
|
model: p.model,
|
||||||
|
system: p.system,
|
||||||
|
maxOutputTokens: p.maxOutputTokens,
|
||||||
|
temperature: p.temperature,
|
||||||
|
thinking: p.thinking,
|
||||||
|
stream: p.stream !== false,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: res.text || "(nessun testo nella risposta)" }],
|
||||||
|
details: { ...res.details, model: p.model ?? ANTG_DEFAULT_MODEL, project: antgProject?.pid ?? null },
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Errore Antigravity: ${e.message}` }],
|
||||||
|
details: {},
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PROVIDER: antigravity — modelli del gateway nel selettore modelli
|
||||||
|
// =========================================================================
|
||||||
|
pi.registerProvider("antigravity", antgProviderConfig(antgRegisteredModels));
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Comando: /agy:refresh-models — aggiorna l'elenco modelli del provider dal
|
||||||
|
// catalogo vivo dell'account (fetchAvailableModels) e ri-registra il provider.
|
||||||
|
// =========================================================================
|
||||||
|
pi.registerCommand("agy:refresh-models", {
|
||||||
|
description:
|
||||||
|
"Aggiorna l'elenco modelli del provider Antigravity dal catalogo vivo dell'account (fetchAvailableModels) e ri-registra il provider (selettore modelli).",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
ctx.ui.setStatus("agy:refresh-models", "Aggiornamento modelli Antigravity...");
|
||||||
|
try {
|
||||||
|
const { pid, base } = await antgGetProject();
|
||||||
|
const token = await antgGetAccessToken();
|
||||||
|
const models = await antgFetchCatalog(token, base);
|
||||||
|
if (!models.length) throw new Error("Il catalogo non ha restituito modelli utilizzabili");
|
||||||
|
antgRegisteredModels = models;
|
||||||
|
pi.registerProvider("antigravity", antgProviderConfig(models));
|
||||||
|
ctx.ui.setStatus("agy:refresh-models", "");
|
||||||
|
const lines = models.map((m) => ` ${m.id} — ${m.name}${m.reasoning ? " (thinking)" : ""}`).join("\n");
|
||||||
|
ctx.ui.notify(
|
||||||
|
`✅ Provider antigravity aggiornato: ${models.length} modelli (project ${pid})\n${lines}`,
|
||||||
|
"info",
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
ctx.ui.setStatus("agy:refresh-models", "");
|
||||||
|
ctx.ui.notify(`Errore aggiornamento modelli: ${e.message}`, "error");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Comandi interattivi
|
// Comandi interattivi
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -2266,8 +3055,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
pi.registerShortcut("ctrl+alt+c", {
|
pi.registerShortcut("ctrl+escape", {
|
||||||
description: "Annulla la registrazione microfono in corso (Ctrl+Esc non funziona in legacy: invia lo stesso byte di Esc)",
|
description: "Annulla la registrazione microfono in corso",
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
if (recording) {
|
if (recording) {
|
||||||
await cancelRecording();
|
await cancelRecording();
|
||||||
|
|||||||
Generated
+1976
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user