feat: add Google Antigravity authentication support
- Add 'antigravity' as new auth choice in onboard and configure wizards - Implement Google Antigravity OAuth flow using loginAntigravity from pi-ai - Update writeOAuthCredentials to accept any OAuthProvider (not just 'anthropic') - Add schema sanitization for Google Cloud Code Assist API to fix tool call errors - Default model set to google-antigravity/claude-opus-4-5 after successful auth The schema sanitization removes unsupported JSON Schema keywords (patternProperties, const, anyOf, etc.) that Google's Cloud Code Assist API doesn't understand.
This commit is contained in:
committed by
Peter Steinberger
parent
5eff541da8
commit
05bd345828
@@ -1,5 +1,90 @@
|
|||||||
|
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
|
||||||
|
index ff9cbcfebfac6b4370d85dc838f5cacf2a60ed64..42096c82aec925b412258348a36ba4a7025b283b 100644
|
||||||
|
--- a/dist/providers/google-shared.js
|
||||||
|
+++ b/dist/providers/google-shared.js
|
||||||
|
@@ -140,6 +140,71 @@ export function convertMessages(model, context) {
|
||||||
|
}
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
+/**
|
||||||
|
+ * Sanitize JSON Schema for Google Cloud Code Assist API.
|
||||||
|
+ * Removes unsupported keywords like patternProperties, const, anyOf, etc.
|
||||||
|
+ * and converts to a format compatible with Google's function declarations.
|
||||||
|
+ */
|
||||||
|
+function sanitizeSchemaForGoogle(schema) {
|
||||||
|
+ if (!schema || typeof schema !== 'object') {
|
||||||
|
+ return schema;
|
||||||
|
+ }
|
||||||
|
+ // If it's an array, sanitize each element
|
||||||
|
+ if (Array.isArray(schema)) {
|
||||||
|
+ return schema.map(item => sanitizeSchemaForGoogle(item));
|
||||||
|
+ }
|
||||||
|
+ const sanitized = {};
|
||||||
|
+ // List of unsupported JSON Schema keywords that Google's API doesn't understand
|
||||||
|
+ const unsupportedKeywords = [
|
||||||
|
+ 'patternProperties',
|
||||||
|
+ 'const',
|
||||||
|
+ 'anyOf',
|
||||||
|
+ 'oneOf',
|
||||||
|
+ 'allOf',
|
||||||
|
+ 'not',
|
||||||
|
+ '$schema',
|
||||||
|
+ '$id',
|
||||||
|
+ '$ref',
|
||||||
|
+ '$defs',
|
||||||
|
+ 'definitions',
|
||||||
|
+ 'if',
|
||||||
|
+ 'then',
|
||||||
|
+ 'else',
|
||||||
|
+ 'dependentSchemas',
|
||||||
|
+ 'dependentRequired',
|
||||||
|
+ 'unevaluatedProperties',
|
||||||
|
+ 'unevaluatedItems',
|
||||||
|
+ 'contentEncoding',
|
||||||
|
+ 'contentMediaType',
|
||||||
|
+ 'contentSchema',
|
||||||
|
+ 'deprecated',
|
||||||
|
+ 'readOnly',
|
||||||
|
+ 'writeOnly',
|
||||||
|
+ 'examples',
|
||||||
|
+ '$comment',
|
||||||
|
+ 'additionalProperties',
|
||||||
|
+ ];
|
||||||
|
+ for (const [key, value] of Object.entries(schema)) {
|
||||||
|
+ // Skip unsupported keywords
|
||||||
|
+ if (unsupportedKeywords.includes(key)) {
|
||||||
|
+ continue;
|
||||||
|
+ }
|
||||||
|
+ // Recursively sanitize nested objects
|
||||||
|
+ if (key === 'properties' && typeof value === 'object' && value !== null) {
|
||||||
|
+ sanitized[key] = {};
|
||||||
|
+ for (const [propKey, propValue] of Object.entries(value)) {
|
||||||
|
+ sanitized[key][propKey] = sanitizeSchemaForGoogle(propValue);
|
||||||
|
+ }
|
||||||
|
+ } else if (key === 'items' && typeof value === 'object') {
|
||||||
|
+ sanitized[key] = sanitizeSchemaForGoogle(value);
|
||||||
|
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||||
|
+ sanitized[key] = sanitizeSchemaForGoogle(value);
|
||||||
|
+ } else {
|
||||||
|
+ sanitized[key] = value;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ return sanitized;
|
||||||
|
+}
|
||||||
|
/**
|
||||||
|
* Convert tools to Gemini function declarations format.
|
||||||
|
*/
|
||||||
|
@@ -151,7 +216,7 @@ export function convertTools(tools) {
|
||||||
|
functionDeclarations: tools.map((tool) => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
- parameters: tool.parameters,
|
||||||
|
+ parameters: sanitizeSchemaForGoogle(tool.parameters),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
|
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
|
||||||
index 20fb0a22aaa28f7ff7c2f44a8b628fa1d9d7d936..c2bc63f483f3285b00755901ba97db810221cea6 100644
|
index 20fb0a22aaa28f7ff7c2f44a8b628fa1d9d7d936..31bae0aface1319487ce62d35f1f3b6ed334863e 100644
|
||||||
--- a/dist/providers/openai-responses.js
|
--- a/dist/providers/openai-responses.js
|
||||||
+++ b/dist/providers/openai-responses.js
|
+++ b/dist/providers/openai-responses.js
|
||||||
@@ -486,7 +486,6 @@ function convertTools(tools) {
|
@@ -486,7 +486,6 @@ function convertTools(tools) {
|
||||||
|
|||||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -9,7 +9,7 @@ overrides:
|
|||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
hash: bf3e904ebaad236b8c3bb48c7d1150a1463735e783acaab6d15d6cd381b43832
|
hash: 9d828603572332a8eba73e7d08d3a32408bc1d87a5c1f27b3f9f8d35c3d2ffb0
|
||||||
path: patches/@mariozechner__pi-ai.patch
|
path: patches/@mariozechner__pi-ai.patch
|
||||||
'@mariozechner/pi-coding-agent@0.31.1':
|
'@mariozechner/pi-coding-agent@0.31.1':
|
||||||
hash: d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745
|
hash: d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
spinner,
|
spinner,
|
||||||
text,
|
text,
|
||||||
} from "@clack/prompts";
|
} from "@clack/prompts";
|
||||||
import { loginAnthropic, type OAuthCredentials } from "@mariozechner/pi-ai";
|
import {
|
||||||
|
loginAnthropic,
|
||||||
|
loginAntigravity,
|
||||||
|
type OAuthCredentials,
|
||||||
|
} from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -223,13 +227,17 @@ async function promptAuthConfig(
|
|||||||
message: "Model/auth choice",
|
message: "Model/auth choice",
|
||||||
options: [
|
options: [
|
||||||
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
|
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
|
||||||
|
{
|
||||||
|
value: "antigravity",
|
||||||
|
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
||||||
|
},
|
||||||
{ value: "apiKey", label: "Anthropic API key" },
|
{ value: "apiKey", label: "Anthropic API key" },
|
||||||
{ value: "minimax", label: "Minimax M2.1 (LM Studio)" },
|
{ value: "minimax", label: "Minimax M2.1 (LM Studio)" },
|
||||||
{ value: "skip", label: "Skip for now" },
|
{ value: "skip", label: "Skip for now" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
runtime,
|
runtime,
|
||||||
) as "oauth" | "apiKey" | "minimax" | "skip";
|
) as "oauth" | "antigravity" | "apiKey" | "minimax" | "skip";
|
||||||
|
|
||||||
let next = cfg;
|
let next = cfg;
|
||||||
|
|
||||||
@@ -266,6 +274,47 @@ async function promptAuthConfig(
|
|||||||
spin.stop("OAuth failed");
|
spin.stop("OAuth failed");
|
||||||
runtime.error(String(err));
|
runtime.error(String(err));
|
||||||
}
|
}
|
||||||
|
} else if (authChoice === "antigravity") {
|
||||||
|
note(
|
||||||
|
[
|
||||||
|
"Browser will open for Google authentication.",
|
||||||
|
"Sign in with your Google account that has Antigravity access.",
|
||||||
|
"The callback will be captured automatically on localhost:51121.",
|
||||||
|
].join("\n"),
|
||||||
|
"Google Antigravity OAuth",
|
||||||
|
);
|
||||||
|
const spin = spinner();
|
||||||
|
spin.start("Starting OAuth flow…");
|
||||||
|
let oauthCreds: OAuthCredentials | null = null;
|
||||||
|
try {
|
||||||
|
oauthCreds = await loginAntigravity(
|
||||||
|
async ({ url, instructions }) => {
|
||||||
|
spin.message(instructions ?? "Complete sign-in in browser…");
|
||||||
|
await openUrl(url);
|
||||||
|
runtime.log(`Open: ${url}`);
|
||||||
|
},
|
||||||
|
(msg) => spin.message(msg),
|
||||||
|
);
|
||||||
|
spin.stop("Antigravity OAuth complete");
|
||||||
|
if (oauthCreds) {
|
||||||
|
await writeOAuthCredentials("google-antigravity", oauthCreds);
|
||||||
|
// Set default model to Claude Opus 4.5 via Antigravity
|
||||||
|
next = {
|
||||||
|
...next,
|
||||||
|
agent: {
|
||||||
|
...next.agent,
|
||||||
|
model: "google-antigravity/claude-opus-4-5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
note(
|
||||||
|
"Default model set to google-antigravity/claude-opus-4-5",
|
||||||
|
"Model configured",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
spin.stop("Antigravity OAuth failed");
|
||||||
|
runtime.error(String(err));
|
||||||
|
}
|
||||||
} else if (authChoice === "apiKey") {
|
} else if (authChoice === "apiKey") {
|
||||||
const key = guardCancel(
|
const key = guardCancel(
|
||||||
await text({
|
await text({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
import type { OAuthCredentials, OAuthProvider } from "@mariozechner/pi-ai";
|
||||||
import { discoverAuthStorage } from "@mariozechner/pi-coding-agent";
|
import { discoverAuthStorage } from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
import { resolveClawdisAgentDir } from "../agents/agent-paths.js";
|
import { resolveClawdisAgentDir } from "../agents/agent-paths.js";
|
||||||
@@ -9,7 +9,7 @@ import type { ClawdisConfig } from "../config/config.js";
|
|||||||
import { CONFIG_DIR } from "../utils.js";
|
import { CONFIG_DIR } from "../utils.js";
|
||||||
|
|
||||||
export async function writeOAuthCredentials(
|
export async function writeOAuthCredentials(
|
||||||
provider: "anthropic",
|
provider: OAuthProvider,
|
||||||
creds: OAuthCredentials,
|
creds: OAuthCredentials,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const dir = path.join(CONFIG_DIR, "credentials");
|
const dir = path.join(CONFIG_DIR, "credentials");
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
spinner,
|
spinner,
|
||||||
text,
|
text,
|
||||||
} from "@clack/prompts";
|
} from "@clack/prompts";
|
||||||
import { loginAnthropic, type OAuthCredentials } from "@mariozechner/pi-ai";
|
import {
|
||||||
|
loginAnthropic,
|
||||||
|
loginAntigravity,
|
||||||
|
type OAuthCredentials,
|
||||||
|
} from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -198,6 +202,10 @@ export async function runInteractiveOnboarding(
|
|||||||
message: "Model/auth choice",
|
message: "Model/auth choice",
|
||||||
options: [
|
options: [
|
||||||
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
|
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
|
||||||
|
{
|
||||||
|
value: "antigravity",
|
||||||
|
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
||||||
|
},
|
||||||
{ value: "apiKey", label: "Anthropic API key" },
|
{ value: "apiKey", label: "Anthropic API key" },
|
||||||
{ value: "minimax", label: "Minimax M2.1 (LM Studio)" },
|
{ value: "minimax", label: "Minimax M2.1 (LM Studio)" },
|
||||||
{ value: "skip", label: "Skip for now" },
|
{ value: "skip", label: "Skip for now" },
|
||||||
@@ -239,6 +247,47 @@ export async function runInteractiveOnboarding(
|
|||||||
spin.stop("OAuth failed");
|
spin.stop("OAuth failed");
|
||||||
runtime.error(String(err));
|
runtime.error(String(err));
|
||||||
}
|
}
|
||||||
|
} else if (authChoice === "antigravity") {
|
||||||
|
note(
|
||||||
|
[
|
||||||
|
"Browser will open for Google authentication.",
|
||||||
|
"Sign in with your Google account that has Antigravity access.",
|
||||||
|
"The callback will be captured automatically on localhost:51121.",
|
||||||
|
].join("\n"),
|
||||||
|
"Google Antigravity OAuth",
|
||||||
|
);
|
||||||
|
const spin = spinner();
|
||||||
|
spin.start("Starting OAuth flow…");
|
||||||
|
let oauthCreds: OAuthCredentials | null = null;
|
||||||
|
try {
|
||||||
|
oauthCreds = await loginAntigravity(
|
||||||
|
async ({ url, instructions }) => {
|
||||||
|
spin.message(instructions ?? "Complete sign-in in browser…");
|
||||||
|
await openUrl(url);
|
||||||
|
runtime.log(`Open: ${url}`);
|
||||||
|
},
|
||||||
|
(msg) => spin.message(msg),
|
||||||
|
);
|
||||||
|
spin.stop("Antigravity OAuth complete");
|
||||||
|
if (oauthCreds) {
|
||||||
|
await writeOAuthCredentials("google-antigravity", oauthCreds);
|
||||||
|
// Set default model to Claude Opus 4.5 via Antigravity
|
||||||
|
nextConfig = {
|
||||||
|
...nextConfig,
|
||||||
|
agent: {
|
||||||
|
...nextConfig.agent,
|
||||||
|
model: "google-antigravity/claude-opus-4-5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
note(
|
||||||
|
"Default model set to google-antigravity/claude-opus-4-5",
|
||||||
|
"Model configured",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
spin.stop("Antigravity OAuth failed");
|
||||||
|
runtime.error(String(err));
|
||||||
|
}
|
||||||
} else if (authChoice === "apiKey") {
|
} else if (authChoice === "apiKey") {
|
||||||
const key = guardCancel(
|
const key = guardCancel(
|
||||||
await text({
|
await text({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type OnboardMode = "local" | "remote";
|
export type OnboardMode = "local" | "remote";
|
||||||
export type AuthChoice = "oauth" | "apiKey" | "minimax" | "skip";
|
export type AuthChoice = "oauth" | "antigravity" | "apiKey" | "minimax" | "skip";
|
||||||
export type GatewayAuthChoice = "off" | "token" | "password";
|
export type GatewayAuthChoice = "off" | "token" | "password";
|
||||||
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||||
export type GatewayBind = "loopback" | "lan" | "tailnet" | "auto";
|
export type GatewayBind = "loopback" | "lan" | "tailnet" | "auto";
|
||||||
|
|||||||
Reference in New Issue
Block a user