mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 01:03:49 +00:00
Add Google Gemini CLI and Antigravity OAuth providers
- Add google-gemini-cli provider: free Gemini 2.0/2.5 via Cloud Code Assist - Add google-antigravity provider: free Gemini 3, Claude, GPT-OSS via sandbox - Move OAuth infrastructure from coding-agent to ai package - Fix thinking signature handling for cross-model handoff - Fix OpenAI message ID length limit (max 64 chars) - Add GitHub Copilot overflow pattern detection - Add OAuth provider tests for context overflow and streaming
This commit is contained in:
parent
3266cac0f1
commit
c359023c3f
25 changed files with 1392 additions and 413 deletions
|
|
@ -482,16 +482,59 @@ async function generateModels() {
|
|||
});
|
||||
}
|
||||
|
||||
// Add Google Cloud Code Assist models (Gemini CLI / Antigravity)
|
||||
// These use OAuth authentication via Google account, cost is $0 (uses account quota)
|
||||
// Google Cloud Code Assist models (Gemini CLI)
|
||||
// Uses production endpoint, standard Gemini models only
|
||||
const CLOUD_CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
||||
const cloudCodeAssistModels: Model<"google-cloud-code-assist">[] = [
|
||||
const cloudCodeAssistModels: Model<"google-gemini-cli">[] = [
|
||||
{
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro (Cloud Code Assist)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65535,
|
||||
},
|
||||
{
|
||||
id: "gemini-2.5-flash",
|
||||
name: "Gemini 2.5 Flash (Cloud Code Assist)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65535,
|
||||
},
|
||||
{
|
||||
id: "gemini-2.0-flash",
|
||||
name: "Gemini 2.0 Flash (Cloud Code Assist)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
];
|
||||
allModels.push(...cloudCodeAssistModels);
|
||||
|
||||
// Antigravity models (Gemini 3, Claude, GPT-OSS via Google Cloud)
|
||||
// Uses sandbox endpoint and different OAuth credentials for access to additional models
|
||||
const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
||||
const antigravityModels: Model<"google-gemini-cli">[] = [
|
||||
{
|
||||
id: "gemini-3-pro-high",
|
||||
name: "Gemini 3 Pro High (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
name: "Gemini 3 Pro High (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: ANTIGRAVITY_ENDPOINT,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
|
|
@ -500,10 +543,10 @@ async function generateModels() {
|
|||
},
|
||||
{
|
||||
id: "gemini-3-pro-low",
|
||||
name: "Gemini 3 Pro Low (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
name: "Gemini 3 Pro Low (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: ANTIGRAVITY_ENDPOINT,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
|
|
@ -512,22 +555,22 @@ async function generateModels() {
|
|||
},
|
||||
{
|
||||
id: "gemini-3-flash",
|
||||
name: "Gemini 3 Flash (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
reasoning: false,
|
||||
name: "Gemini 3 Flash (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: ANTIGRAVITY_ENDPOINT,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65536,
|
||||
maxTokens: 65535,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5 (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
name: "Claude Sonnet 4.5 (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: ANTIGRAVITY_ENDPOINT,
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
|
|
@ -536,10 +579,10 @@ async function generateModels() {
|
|||
},
|
||||
{
|
||||
id: "claude-sonnet-4-5-thinking",
|
||||
name: "Claude Sonnet 4.5 Thinking (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
name: "Claude Sonnet 4.5 Thinking (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: ANTIGRAVITY_ENDPOINT,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
|
|
@ -548,10 +591,10 @@ async function generateModels() {
|
|||
},
|
||||
{
|
||||
id: "claude-opus-4-5-thinking",
|
||||
name: "Claude Opus 4.5 Thinking (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
name: "Claude Opus 4.5 Thinking (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: ANTIGRAVITY_ENDPOINT,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
|
|
@ -560,18 +603,18 @@ async function generateModels() {
|
|||
},
|
||||
{
|
||||
id: "gpt-oss-120b-medium",
|
||||
name: "GPT-OSS 120B Medium (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: CLOUD_CODE_ASSIST_ENDPOINT,
|
||||
name: "GPT-OSS 120B Medium (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: ANTIGRAVITY_ENDPOINT,
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 131072,
|
||||
maxTokens: 32768,
|
||||
},
|
||||
];
|
||||
allModels.push(...cloudCodeAssistModels);
|
||||
allModels.push(...antigravityModels);
|
||||
|
||||
// Group by provider and deduplicate by model ID
|
||||
const providers: Record<string, Record<string, Model<any>>> = {};
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ export * from "./agent/index.js";
|
|||
export * from "./models.js";
|
||||
export * from "./providers/anthropic.js";
|
||||
export * from "./providers/google.js";
|
||||
export * from "./providers/google-cloud-code-assist.js";
|
||||
export * from "./providers/google-gemini-cli.js";
|
||||
export * from "./providers/openai-completions.js";
|
||||
export * from "./providers/openai-responses.js";
|
||||
export * from "./stream.js";
|
||||
export * from "./types.js";
|
||||
export * from "./utils/oauth/index.js";
|
||||
export * from "./utils/overflow.js";
|
||||
export * from "./utils/typebox-helpers.js";
|
||||
export * from "./utils/validation.js";
|
||||
|
|
|
|||
|
|
@ -2906,7 +2906,7 @@ export const MODELS = {
|
|||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nvidia/nemotron-3-nano-30b-a3b:free": {
|
||||
id: "nvidia/nemotron-3-nano-30b-a3b:free",
|
||||
|
|
@ -3280,7 +3280,7 @@ export const MODELS = {
|
|||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 163840,
|
||||
maxTokens: 163840,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"anthropic/claude-opus-4.5": {
|
||||
id: "anthropic/claude-opus-4.5",
|
||||
|
|
@ -3444,13 +3444,13 @@ export const MODELS = {
|
|||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.44999999999999996,
|
||||
output: 2.35,
|
||||
input: 0.39999999999999997,
|
||||
output: 1.75,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 16384,
|
||||
maxTokens: 65535,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"amazon/nova-premier-v1": {
|
||||
id: "amazon/nova-premier-v1",
|
||||
|
|
@ -4600,13 +4600,13 @@ export const MODELS = {
|
|||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.06,
|
||||
output: 0.25,
|
||||
input: 0.07,
|
||||
output: 0.27,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
contextWindow: 160000,
|
||||
maxTokens: 32768,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3-30b-a3b-instruct-2507": {
|
||||
id: "qwen/qwen3-30b-a3b-instruct-2507",
|
||||
|
|
@ -4640,7 +4640,7 @@ export const MODELS = {
|
|||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"z-ai/glm-4.5-air:free": {
|
||||
id: "z-ai/glm-4.5-air:free",
|
||||
|
|
@ -4784,7 +4784,7 @@ export const MODELS = {
|
|||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.071,
|
||||
|
|
@ -5271,23 +5271,6 @@ export const MODELS = {
|
|||
contextWindow: 40960,
|
||||
maxTokens: 40960,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3-235b-a22b:free": {
|
||||
id: "qwen/qwen3-235b-a22b:free",
|
||||
name: "Qwen: Qwen3 235B A22B (free)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3-235b-a22b": {
|
||||
id: "qwen/qwen3-235b-a22b",
|
||||
name: "Qwen: Qwen3 235B A22B",
|
||||
|
|
@ -6036,9 +6019,9 @@ export const MODELS = {
|
|||
contextWindow: 32768,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"anthropic/claude-3.5-haiku-20241022": {
|
||||
id: "anthropic/claude-3.5-haiku-20241022",
|
||||
name: "Anthropic: Claude 3.5 Haiku (2024-10-22)",
|
||||
"anthropic/claude-3.5-haiku": {
|
||||
id: "anthropic/claude-3.5-haiku",
|
||||
name: "Anthropic: Claude 3.5 Haiku",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
|
|
@ -6053,9 +6036,9 @@ export const MODELS = {
|
|||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"anthropic/claude-3.5-haiku": {
|
||||
id: "anthropic/claude-3.5-haiku",
|
||||
name: "Anthropic: Claude 3.5 Haiku",
|
||||
"anthropic/claude-3.5-haiku-20241022": {
|
||||
id: "anthropic/claude-3.5-haiku-20241022",
|
||||
name: "Anthropic: Claude 3.5 Haiku (2024-10-22)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
|
|
@ -6291,23 +6274,6 @@ export const MODELS = {
|
|||
contextWindow: 128000,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-8b-instruct": {
|
||||
id: "meta-llama/llama-3.1-8b-instruct",
|
||||
name: "Meta: Llama 3.1 8B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.02,
|
||||
output: 0.03,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-405b-instruct": {
|
||||
id: "meta-llama/llama-3.1-405b-instruct",
|
||||
name: "Meta: Llama 3.1 405B Instruct",
|
||||
|
|
@ -6325,6 +6291,23 @@ export const MODELS = {
|
|||
contextWindow: 130815,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-8b-instruct": {
|
||||
id: "meta-llama/llama-3.1-8b-instruct",
|
||||
name: "Meta: Llama 3.1 8B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.02,
|
||||
output: 0.03,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-70b-instruct": {
|
||||
id: "meta-llama/llama-3.1-70b-instruct",
|
||||
name: "Meta: Llama 3.1 70B Instruct",
|
||||
|
|
@ -6819,13 +6802,66 @@ export const MODELS = {
|
|||
maxTokens: 30000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
"google-cloud-code-assist": {
|
||||
"google-gemini-cli": {
|
||||
"gemini-2.5-pro": {
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro (Cloud Code Assist)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65535,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
"gemini-2.5-flash": {
|
||||
id: "gemini-2.5-flash",
|
||||
name: "Gemini 2.5 Flash (Cloud Code Assist)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65535,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
"gemini-2.0-flash": {
|
||||
id: "gemini-2.0-flash",
|
||||
name: "Gemini 2.0 Flash (Cloud Code Assist)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
},
|
||||
"google-antigravity": {
|
||||
"gemini-3-pro-high": {
|
||||
id: "gemini-3-pro-high",
|
||||
name: "Gemini 3 Pro High (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
name: "Gemini 3 Pro High (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
|
|
@ -6836,13 +6872,13 @@ export const MODELS = {
|
|||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65535,
|
||||
} satisfies Model<"google-cloud-code-assist">,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
"gemini-3-pro-low": {
|
||||
id: "gemini-3-pro-low",
|
||||
name: "Gemini 3 Pro Low (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
name: "Gemini 3 Pro Low (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
|
|
@ -6853,14 +6889,14 @@ export const MODELS = {
|
|||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65535,
|
||||
} satisfies Model<"google-cloud-code-assist">,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
"gemini-3-flash": {
|
||||
id: "gemini-3-flash",
|
||||
name: "Gemini 3 Flash (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
reasoning: false,
|
||||
name: "Gemini 3 Flash (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
|
|
@ -6869,14 +6905,14 @@ export const MODELS = {
|
|||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"google-cloud-code-assist">,
|
||||
maxTokens: 65535,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
"claude-sonnet-4-5": {
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5 (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
name: "Claude Sonnet 4.5 (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
|
|
@ -6887,13 +6923,13 @@ export const MODELS = {
|
|||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
} satisfies Model<"google-cloud-code-assist">,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
"claude-sonnet-4-5-thinking": {
|
||||
id: "claude-sonnet-4-5-thinking",
|
||||
name: "Claude Sonnet 4.5 Thinking (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
name: "Claude Sonnet 4.5 Thinking (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
|
|
@ -6904,13 +6940,13 @@ export const MODELS = {
|
|||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
} satisfies Model<"google-cloud-code-assist">,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
"claude-opus-4-5-thinking": {
|
||||
id: "claude-opus-4-5-thinking",
|
||||
name: "Claude Opus 4.5 Thinking (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
name: "Claude Opus 4.5 Thinking (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
|
|
@ -6921,15 +6957,15 @@ export const MODELS = {
|
|||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
} satisfies Model<"google-cloud-code-assist">,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
"gpt-oss-120b-medium": {
|
||||
id: "gpt-oss-120b-medium",
|
||||
name: "GPT-OSS 120B Medium (Cloud Code Assist)",
|
||||
api: "google-cloud-code-assist",
|
||||
provider: "google-cloud-code-assist",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
name: "GPT-OSS 120B Medium (Antigravity)",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
|
|
@ -6938,6 +6974,6 @@ export const MODELS = {
|
|||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 32768,
|
||||
} satisfies Model<"google-cloud-code-assist">,
|
||||
} satisfies Model<"google-gemini-cli">,
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/**
|
||||
* Google Cloud Code Assist provider for Gemini CLI / Antigravity authentication.
|
||||
* Google Gemini CLI / Antigravity provider.
|
||||
* Shared implementation for both google-gemini-cli and google-antigravity providers.
|
||||
* Uses the Cloud Code Assist API endpoint to access Gemini and Claude models.
|
||||
*/
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import { convertMessages, convertTools, mapStopReasonString, mapToolChoice } from "./google-shared.js";
|
||||
|
||||
export interface GoogleCloudCodeAssistOptions extends StreamOptions {
|
||||
export interface GoogleGeminiCliOptions extends StreamOptions {
|
||||
toolChoice?: "auto" | "none" | "any";
|
||||
thinking?: {
|
||||
enabled: boolean;
|
||||
|
|
@ -29,11 +30,27 @@ export interface GoogleCloudCodeAssistOptions extends StreamOptions {
|
|||
projectId?: string;
|
||||
}
|
||||
|
||||
const ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
||||
const HEADERS = {
|
||||
"User-Agent": "google-api-nodejs-client/9.15.1",
|
||||
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
||||
// Headers for Gemini CLI (prod endpoint)
|
||||
const GEMINI_CLI_HEADERS = {
|
||||
"User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
"X-Goog-Api-Client": "gl-node/22.17.0",
|
||||
"Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
|
||||
"Client-Metadata": JSON.stringify({
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
}),
|
||||
};
|
||||
|
||||
// Headers for Antigravity (sandbox endpoint) - requires specific User-Agent
|
||||
const ANTIGRAVITY_HEADERS = {
|
||||
"User-Agent": "antigravity/1.11.5 darwin/arm64",
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
"Client-Metadata": JSON.stringify({
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
}),
|
||||
};
|
||||
|
||||
// Counter for generating unique tool call IDs
|
||||
|
|
@ -92,10 +109,10 @@ interface CloudCodeAssistResponseChunk {
|
|||
traceId?: string;
|
||||
}
|
||||
|
||||
export const streamGoogleCloudCodeAssist: StreamFunction<"google-cloud-code-assist"> = (
|
||||
model: Model<"google-cloud-code-assist">,
|
||||
export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
||||
model: Model<"google-gemini-cli">,
|
||||
context: Context,
|
||||
options?: GoogleCloudCodeAssistOptions,
|
||||
options?: GoogleGeminiCliOptions,
|
||||
): AssistantMessageEventStream => {
|
||||
const stream = new AssistantMessageEventStream();
|
||||
|
||||
|
|
@ -103,7 +120,7 @@ export const streamGoogleCloudCodeAssist: StreamFunction<"google-cloud-code-assi
|
|||
const output: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [],
|
||||
api: "google-cloud-code-assist" as Api,
|
||||
api: "google-gemini-cli" as Api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: {
|
||||
|
|
@ -141,7 +158,12 @@ export const streamGoogleCloudCodeAssist: StreamFunction<"google-cloud-code-assi
|
|||
}
|
||||
|
||||
const requestBody = buildRequest(model, context, projectId, options);
|
||||
const url = `${ENDPOINT}/v1internal:streamGenerateContent?alt=sse`;
|
||||
const endpoint = model.baseUrl || DEFAULT_ENDPOINT;
|
||||
const url = `${endpoint}/v1internal:streamGenerateContent?alt=sse`;
|
||||
|
||||
// Use Antigravity headers for sandbox endpoint, otherwise Gemini CLI headers
|
||||
const isAntigravity = endpoint.includes("sandbox.googleapis.com");
|
||||
const headers = isAntigravity ? ANTIGRAVITY_HEADERS : GEMINI_CLI_HEADERS;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
|
|
@ -149,7 +171,7 @@ export const streamGoogleCloudCodeAssist: StreamFunction<"google-cloud-code-assi
|
|||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
...HEADERS,
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: options?.signal,
|
||||
|
|
@ -379,10 +401,10 @@ export const streamGoogleCloudCodeAssist: StreamFunction<"google-cloud-code-assi
|
|||
};
|
||||
|
||||
function buildRequest(
|
||||
model: Model<"google-cloud-code-assist">,
|
||||
model: Model<"google-gemini-cli">,
|
||||
context: Context,
|
||||
projectId: string,
|
||||
options: GoogleCloudCodeAssistOptions = {},
|
||||
options: GoogleGeminiCliOptions = {},
|
||||
): CloudCodeAssistRequest {
|
||||
const contents = convertMessages(model, context);
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from
|
|||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import { transformMessages } from "./transorm-messages.js";
|
||||
|
||||
type GoogleApiType = "google-generative-ai" | "google-cloud-code-assist";
|
||||
type GoogleApiType = "google-generative-ai" | "google-gemini-cli";
|
||||
|
||||
/**
|
||||
* Convert internal messages to Gemini Content[] format.
|
||||
|
|
@ -48,14 +48,23 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|||
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "text") {
|
||||
// Skip empty text blocks - they can cause issues with some models (e.g. Claude via Antigravity)
|
||||
if (!block.text || block.text.trim() === "") continue;
|
||||
parts.push({ text: sanitizeSurrogates(block.text) });
|
||||
} else if (block.type === "thinking") {
|
||||
const thinkingPart: Part = {
|
||||
thought: true,
|
||||
thoughtSignature: block.thinkingSignature,
|
||||
text: sanitizeSurrogates(block.thinking),
|
||||
};
|
||||
parts.push(thinkingPart);
|
||||
// Thinking blocks require signatures for Claude via Antigravity.
|
||||
// If signature is missing (e.g. from GPT-OSS), convert to regular text with delimiters.
|
||||
if (block.thinkingSignature) {
|
||||
parts.push({
|
||||
thought: true,
|
||||
text: sanitizeSurrogates(block.thinking),
|
||||
thoughtSignature: block.thinkingSignature,
|
||||
});
|
||||
} else {
|
||||
parts.push({
|
||||
text: `<thinking>\n${sanitizeSurrogates(block.thinking)}\n</thinking>`,
|
||||
});
|
||||
}
|
||||
} else if (block.type === "toolCall") {
|
||||
const part: Part = {
|
||||
functionCall: {
|
||||
|
|
@ -112,10 +121,17 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|||
});
|
||||
}
|
||||
|
||||
contents.push({
|
||||
role: "user",
|
||||
parts,
|
||||
});
|
||||
// Cloud Code Assist API requires all function responses to be in a single user turn.
|
||||
// Check if the last content is already a user turn with function responses and merge.
|
||||
const lastContent = contents[contents.length - 1];
|
||||
if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) {
|
||||
lastContent.parts.push(...parts);
|
||||
} else {
|
||||
contents.push({
|
||||
role: "user",
|
||||
parts,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,20 @@ import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
|||
|
||||
import { transformMessages } from "./transorm-messages.js";
|
||||
|
||||
/** Fast deterministic hash to shorten long strings */
|
||||
function shortHash(str: string): string {
|
||||
let h1 = 0xdeadbeef;
|
||||
let h2 = 0x41c6ce57;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
||||
}
|
||||
|
||||
// OpenAI Responses-specific options
|
||||
export interface OpenAIResponsesOptions extends StreamOptions {
|
||||
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
|
|
@ -401,6 +415,7 @@ function convertMessages(model: Model<"openai-responses">, context: Context): Re
|
|||
});
|
||||
}
|
||||
|
||||
let msgIndex = 0;
|
||||
for (const msg of transformedMessages) {
|
||||
if (msg.role === "user") {
|
||||
if (typeof msg.content === "string") {
|
||||
|
|
@ -444,12 +459,19 @@ function convertMessages(model: Model<"openai-responses">, context: Context): Re
|
|||
}
|
||||
} else if (block.type === "text") {
|
||||
const textBlock = block as TextContent;
|
||||
// OpenAI requires id to be max 64 characters
|
||||
let msgId = textBlock.textSignature;
|
||||
if (!msgId) {
|
||||
msgId = "msg_" + msgIndex;
|
||||
} else if (msgId.length > 64) {
|
||||
msgId = "msg_" + shortHash(msgId);
|
||||
}
|
||||
output.push({
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: sanitizeSurrogates(textBlock.text), annotations: [] }],
|
||||
status: "completed",
|
||||
id: textBlock.textSignature || "msg_" + Math.random().toString(36).substring(2, 15),
|
||||
id: msgId,
|
||||
} satisfies ResponseOutputMessage);
|
||||
// Do not submit toolcall blocks if the completion had an error (i.e. abort)
|
||||
} else if (block.type === "toolCall" && msg.stopReason !== "error") {
|
||||
|
|
@ -508,6 +530,7 @@ function convertMessages(model: Model<"openai-responses">, context: Context): Re
|
|||
});
|
||||
}
|
||||
}
|
||||
msgIndex++;
|
||||
}
|
||||
|
||||
return messages;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ import { ThinkingLevel } from "@google/genai";
|
|||
import { supportsXhigh } from "./models.js";
|
||||
import { type AnthropicOptions, streamAnthropic } from "./providers/anthropic.js";
|
||||
import { type GoogleOptions, streamGoogle } from "./providers/google.js";
|
||||
import {
|
||||
type GoogleCloudCodeAssistOptions,
|
||||
streamGoogleCloudCodeAssist,
|
||||
} from "./providers/google-cloud-code-assist.js";
|
||||
import { type GoogleGeminiCliOptions, streamGoogleGeminiCli } from "./providers/google-gemini-cli.js";
|
||||
import { type OpenAICompletionsOptions, streamOpenAICompletions } from "./providers/openai-completions.js";
|
||||
import { type OpenAIResponsesOptions, streamOpenAIResponses } from "./providers/openai-responses.js";
|
||||
import type {
|
||||
|
|
@ -19,6 +16,7 @@ import type {
|
|||
ReasoningEffort,
|
||||
SimpleStreamOptions,
|
||||
} from "./types.js";
|
||||
import { getOAuthApiKey, getOAuthProviderForModelProvider } from "./utils/oauth/index.js";
|
||||
|
||||
const apiKeys: Map<string, string> = new Map();
|
||||
|
||||
|
|
@ -28,6 +26,10 @@ export function setApiKey(provider: any, key: string): void {
|
|||
apiKeys.set(provider, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from environment variables (sync).
|
||||
* Does NOT check OAuth credentials - use getApiKeyAsync for that.
|
||||
*/
|
||||
export function getApiKey(provider: KnownProvider): string | undefined;
|
||||
export function getApiKey(provider: string): string | undefined;
|
||||
export function getApiKey(provider: any): string | undefined {
|
||||
|
|
@ -56,6 +58,33 @@ export function getApiKey(provider: any): string | undefined {
|
|||
return envVar ? process.env[envVar] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve API key from OAuth credentials or environment (async).
|
||||
* Automatically refreshes expired OAuth tokens.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Explicitly set keys (via setApiKey)
|
||||
* 2. OAuth credentials from ~/.pi/agent/oauth.json
|
||||
* 3. Environment variables
|
||||
*/
|
||||
export async function resolveApiKey(provider: KnownProvider): Promise<string | undefined>;
|
||||
export async function resolveApiKey(provider: string): Promise<string | undefined>;
|
||||
export async function resolveApiKey(provider: any): Promise<string | undefined> {
|
||||
// Check explicit keys first
|
||||
const key = apiKeys.get(provider);
|
||||
if (key) return key;
|
||||
|
||||
// Check OAuth credentials (auto-refresh if expired)
|
||||
const oauthProvider = getOAuthProviderForModelProvider(provider);
|
||||
if (oauthProvider) {
|
||||
const oauthKey = await getOAuthApiKey(oauthProvider);
|
||||
if (oauthKey) return oauthKey;
|
||||
}
|
||||
|
||||
// Fall back to sync getApiKey for env vars
|
||||
return getApiKey(provider);
|
||||
}
|
||||
|
||||
export function stream<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
|
|
@ -81,11 +110,11 @@ export function stream<TApi extends Api>(
|
|||
case "google-generative-ai":
|
||||
return streamGoogle(model as Model<"google-generative-ai">, context, providerOptions);
|
||||
|
||||
case "google-cloud-code-assist":
|
||||
return streamGoogleCloudCodeAssist(
|
||||
model as Model<"google-cloud-code-assist">,
|
||||
case "google-gemini-cli":
|
||||
return streamGoogleGeminiCli(
|
||||
model as Model<"google-gemini-cli">,
|
||||
context,
|
||||
providerOptions as GoogleCloudCodeAssistOptions,
|
||||
providerOptions as GoogleGeminiCliOptions,
|
||||
);
|
||||
|
||||
default: {
|
||||
|
|
@ -207,10 +236,10 @@ function mapOptionsForApi<TApi extends Api>(
|
|||
} satisfies GoogleOptions;
|
||||
}
|
||||
|
||||
case "google-cloud-code-assist": {
|
||||
case "google-gemini-cli": {
|
||||
// Cloud Code Assist uses thinking budget tokens like Gemini 2.5
|
||||
if (!options?.reasoning) {
|
||||
return { ...base, thinking: { enabled: false } } satisfies GoogleCloudCodeAssistOptions;
|
||||
return { ...base, thinking: { enabled: false } } satisfies GoogleGeminiCliOptions;
|
||||
}
|
||||
|
||||
const effort = clampReasoning(options.reasoning)!;
|
||||
|
|
@ -227,7 +256,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|||
enabled: true,
|
||||
budgetTokens: budgets[effort],
|
||||
},
|
||||
} satisfies GoogleCloudCodeAssistOptions;
|
||||
} satisfies GoogleGeminiCliOptions;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { AnthropicOptions } from "./providers/anthropic.js";
|
||||
import type { GoogleOptions } from "./providers/google.js";
|
||||
import type { GoogleCloudCodeAssistOptions } from "./providers/google-cloud-code-assist.js";
|
||||
import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli.js";
|
||||
import type { OpenAICompletionsOptions } from "./providers/openai-completions.js";
|
||||
import type { OpenAIResponsesOptions } from "./providers/openai-responses.js";
|
||||
import type { AssistantMessageEventStream } from "./utils/event-stream.js";
|
||||
|
|
@ -12,14 +12,14 @@ export type Api =
|
|||
| "openai-responses"
|
||||
| "anthropic-messages"
|
||||
| "google-generative-ai"
|
||||
| "google-cloud-code-assist";
|
||||
| "google-gemini-cli";
|
||||
|
||||
export interface ApiOptionsMap {
|
||||
"anthropic-messages": AnthropicOptions;
|
||||
"openai-completions": OpenAICompletionsOptions;
|
||||
"openai-responses": OpenAIResponsesOptions;
|
||||
"google-generative-ai": GoogleOptions;
|
||||
"google-cloud-code-assist": GoogleCloudCodeAssistOptions;
|
||||
"google-gemini-cli": GoogleGeminiCliOptions;
|
||||
}
|
||||
|
||||
// Compile-time exhaustiveness check - this will fail if ApiOptionsMap doesn't have all KnownApi keys
|
||||
|
|
@ -36,6 +36,8 @@ export type OptionsForApi<TApi extends Api> = ApiOptionsMap[TApi];
|
|||
export type KnownProvider =
|
||||
| "anthropic"
|
||||
| "google"
|
||||
| "google-gemini-cli"
|
||||
| "google-antigravity"
|
||||
| "openai"
|
||||
| "github-copilot"
|
||||
| "xai"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
/**
|
||||
* Anthropic OAuth flow (Claude Pro/Max)
|
||||
*/
|
||||
|
||||
import { createHash, randomBytes } from "crypto";
|
||||
import { type OAuthCredentials, saveOAuthCredentials } from "./storage.js";
|
||||
|
||||
|
|
@ -18,6 +22,9 @@ function generatePKCE(): { verifier: string; challenge: string } {
|
|||
|
||||
/**
|
||||
* Login with Anthropic OAuth (device code flow)
|
||||
*
|
||||
* @param onAuthUrl - Callback to handle the authorization URL (e.g., open browser)
|
||||
* @param onPromptCode - Callback to prompt user for the authorization code
|
||||
*/
|
||||
export async function loginAnthropic(
|
||||
onAuthUrl: (url: string) => void,
|
||||
|
|
@ -90,14 +97,12 @@ export async function loginAnthropic(
|
|||
}
|
||||
|
||||
/**
|
||||
* Refresh Anthropic OAuth token using refresh token
|
||||
* Refresh Anthropic OAuth token
|
||||
*/
|
||||
export async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {
|
||||
const tokenResponse = await fetch(TOKEN_URL, {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
client_id: CLIENT_ID,
|
||||
|
|
@ -105,24 +110,21 @@ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuth
|
|||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
throw new Error(`Token refresh failed: ${error}`);
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Anthropic token refresh failed: ${error}`);
|
||||
}
|
||||
|
||||
const tokenData = (await tokenResponse.json()) as {
|
||||
const data = (await response.json()) as {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
};
|
||||
|
||||
// Calculate expiry time (current time + expires_in seconds - 5 min buffer)
|
||||
const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
refresh: tokenData.refresh_token,
|
||||
access: tokenData.access_token,
|
||||
expires: expiresAt,
|
||||
refresh: data.refresh_token,
|
||||
access: data.access_token,
|
||||
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import { getModels } from "@mariozechner/pi-ai";
|
||||
import type { OAuthCredentials } from "./storage.js";
|
||||
/**
|
||||
* GitHub Copilot OAuth flow
|
||||
*/
|
||||
|
||||
import { getModels } from "../../models.js";
|
||||
import { type OAuthCredentials, saveOAuthCredentials } from "./storage.js";
|
||||
|
||||
const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
||||
|
||||
|
|
@ -182,12 +186,16 @@ async function pollForGitHubAccessToken(
|
|||
throw new Error("Device flow timed out");
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh GitHub Copilot token
|
||||
*/
|
||||
export async function refreshGitHubCopilotToken(
|
||||
refreshToken: string,
|
||||
enterpriseDomain?: string,
|
||||
): Promise<OAuthCredentials> {
|
||||
const domain = enterpriseDomain || "github.com";
|
||||
const urls = getUrls(domain);
|
||||
|
||||
const raw = await fetchJson(urls.copilotTokenUrl, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
|
|
@ -207,14 +215,13 @@ export async function refreshGitHubCopilotToken(
|
|||
throw new Error("Invalid Copilot token response fields");
|
||||
}
|
||||
|
||||
const expires = expiresAt * 1000 - 5 * 60 * 1000;
|
||||
return {
|
||||
type: "oauth",
|
||||
refresh: refreshToken,
|
||||
access: token,
|
||||
expires,
|
||||
expires: expiresAt * 1000 - 5 * 60 * 1000,
|
||||
enterpriseUrl: enterpriseDomain,
|
||||
} satisfies OAuthCredentials;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -265,6 +272,13 @@ export async function enableAllGitHubCopilotModels(
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with GitHub Copilot OAuth (device code flow)
|
||||
*
|
||||
* @param options.onAuth - Callback with URL and optional instructions (user code)
|
||||
* @param options.onPrompt - Callback to prompt user for input
|
||||
* @param options.onProgress - Optional progress callback
|
||||
*/
|
||||
export async function loginGitHubCopilot(options: {
|
||||
onAuth: (url: string, instructions?: string) => void;
|
||||
onPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;
|
||||
|
|
@ -298,5 +312,8 @@ export async function loginGitHubCopilot(options: {
|
|||
options.onProgress?.("Enabling models...");
|
||||
await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);
|
||||
|
||||
// Save credentials
|
||||
saveOAuthCredentials("github-copilot", credentials);
|
||||
|
||||
return credentials;
|
||||
}
|
||||
342
packages/ai/src/utils/oauth/google-antigravity.ts
Normal file
342
packages/ai/src/utils/oauth/google-antigravity.ts
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
/**
|
||||
* Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
|
||||
* Uses different OAuth credentials than google-gemini-cli for access to additional models.
|
||||
*/
|
||||
|
||||
import { createHash, randomBytes } from "crypto";
|
||||
import { createServer, type Server } from "http";
|
||||
import { type OAuthCredentials, saveOAuthCredentials } from "./storage.js";
|
||||
|
||||
// Antigravity OAuth credentials (different from Gemini CLI)
|
||||
const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
||||
const CLIENT_SECRET = "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";
|
||||
const REDIRECT_URI = "http://localhost:51121/oauth-callback";
|
||||
|
||||
// Antigravity requires additional scopes
|
||||
const SCOPES = [
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/cclog",
|
||||
"https://www.googleapis.com/auth/experimentsandconfigs",
|
||||
];
|
||||
|
||||
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||
|
||||
// Antigravity uses sandbox endpoint
|
||||
const CODE_ASSIST_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
||||
|
||||
// Fallback project ID when discovery fails
|
||||
const DEFAULT_PROJECT_ID = "rising-fact-p41fc";
|
||||
|
||||
export interface AntigravityCredentials extends OAuthCredentials {
|
||||
projectId: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PKCE code verifier and challenge
|
||||
*/
|
||||
function generatePKCE(): { verifier: string; challenge: string } {
|
||||
const verifier = randomBytes(32).toString("base64url");
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
return { verifier, challenge };
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a local HTTP server to receive the OAuth callback
|
||||
*/
|
||||
function startCallbackServer(): Promise<{ server: Server; getCode: () => Promise<{ code: string; state: string }> }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let codeResolve: (value: { code: string; state: string }) => void;
|
||||
let codeReject: (error: Error) => void;
|
||||
|
||||
const codePromise = new Promise<{ code: string; state: string }>((res, rej) => {
|
||||
codeResolve = res;
|
||||
codeReject = rej;
|
||||
});
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const url = new URL(req.url || "", `http://localhost:51121`);
|
||||
|
||||
if (url.pathname === "/oauth-callback") {
|
||||
const code = url.searchParams.get("code");
|
||||
const state = url.searchParams.get("state");
|
||||
const error = url.searchParams.get("error");
|
||||
|
||||
if (error) {
|
||||
res.writeHead(400, { "Content-Type": "text/html" });
|
||||
res.end(
|
||||
`<html><body><h1>Authentication Failed</h1><p>Error: ${error}</p><p>You can close this window.</p></body></html>`,
|
||||
);
|
||||
codeReject(new Error(`OAuth error: ${error}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (code && state) {
|
||||
res.writeHead(200, { "Content-Type": "text/html" });
|
||||
res.end(
|
||||
`<html><body><h1>Authentication Successful</h1><p>You can close this window and return to the terminal.</p></body></html>`,
|
||||
);
|
||||
codeResolve({ code, state });
|
||||
} else {
|
||||
res.writeHead(400, { "Content-Type": "text/html" });
|
||||
res.end(
|
||||
`<html><body><h1>Authentication Failed</h1><p>Missing code or state parameter.</p></body></html>`,
|
||||
);
|
||||
codeReject(new Error("Missing code or state in callback"));
|
||||
}
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
server.listen(51121, "127.0.0.1", () => {
|
||||
resolve({
|
||||
server,
|
||||
getCode: () => codePromise,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface LoadCodeAssistPayload {
|
||||
cloudaicompanionProject?: string | { id?: string };
|
||||
currentTier?: { id?: string };
|
||||
allowedTiers?: Array<{ id?: string; isDefault?: boolean }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait helper for onboarding retries
|
||||
*/
|
||||
function wait(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover or provision a project for the user
|
||||
*/
|
||||
async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise<string> {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "google-api-nodejs-client/9.15.1",
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
"Client-Metadata": JSON.stringify({
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
}),
|
||||
};
|
||||
|
||||
// Try endpoints in order: prod first, then sandbox
|
||||
const endpoints = ["https://cloudcode-pa.googleapis.com", "https://daily-cloudcode-pa.sandbox.googleapis.com"];
|
||||
|
||||
onProgress?.("Checking for existing project...");
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
const loadResponse = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
metadata: {
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (loadResponse.ok) {
|
||||
const data = (await loadResponse.json()) as LoadCodeAssistPayload;
|
||||
|
||||
// Handle both string and object formats
|
||||
if (typeof data.cloudaicompanionProject === "string" && data.cloudaicompanionProject) {
|
||||
return data.cloudaicompanionProject;
|
||||
}
|
||||
if (
|
||||
data.cloudaicompanionProject &&
|
||||
typeof data.cloudaicompanionProject === "object" &&
|
||||
data.cloudaicompanionProject.id
|
||||
) {
|
||||
return data.cloudaicompanionProject.id;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Try next endpoint
|
||||
}
|
||||
}
|
||||
|
||||
// Use fallback project ID
|
||||
onProgress?.("Using default project...");
|
||||
return DEFAULT_PROJECT_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user email from the access token
|
||||
*/
|
||||
async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as { email?: string };
|
||||
return data.email;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors, email is optional
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Antigravity token
|
||||
*/
|
||||
export async function refreshAntigravityToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Antigravity token refresh failed: ${error}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token?: string;
|
||||
};
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
refresh: data.refresh_token || refreshToken,
|
||||
access: data.access_token,
|
||||
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
||||
projectId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with Antigravity OAuth
|
||||
*
|
||||
* @param onAuth - Callback with URL and optional instructions
|
||||
* @param onProgress - Optional progress callback
|
||||
*/
|
||||
export async function loginAntigravity(
|
||||
onAuth: (info: { url: string; instructions?: string }) => void,
|
||||
onProgress?: (message: string) => void,
|
||||
): Promise<AntigravityCredentials> {
|
||||
const { verifier, challenge } = generatePKCE();
|
||||
|
||||
// Start local server for callback
|
||||
onProgress?.("Starting local server for OAuth callback...");
|
||||
const { server, getCode } = await startCallbackServer();
|
||||
|
||||
try {
|
||||
// Build authorization URL
|
||||
const authParams = new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
response_type: "code",
|
||||
redirect_uri: REDIRECT_URI,
|
||||
scope: SCOPES.join(" "),
|
||||
code_challenge: challenge,
|
||||
code_challenge_method: "S256",
|
||||
state: verifier,
|
||||
access_type: "offline",
|
||||
prompt: "consent",
|
||||
});
|
||||
|
||||
const authUrl = `${AUTH_URL}?${authParams.toString()}`;
|
||||
|
||||
// Notify caller with URL to open
|
||||
onAuth({
|
||||
url: authUrl,
|
||||
instructions: "Complete the sign-in in your browser. The callback will be captured automatically.",
|
||||
});
|
||||
|
||||
// Wait for the callback
|
||||
onProgress?.("Waiting for OAuth callback...");
|
||||
const { code, state } = await getCode();
|
||||
|
||||
// Verify state matches
|
||||
if (state !== verifier) {
|
||||
throw new Error("OAuth state mismatch - possible CSRF attack");
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
onProgress?.("Exchanging authorization code for tokens...");
|
||||
const tokenResponse = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: REDIRECT_URI,
|
||||
code_verifier: verifier,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
throw new Error(`Token exchange failed: ${error}`);
|
||||
}
|
||||
|
||||
const tokenData = (await tokenResponse.json()) as {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
};
|
||||
|
||||
if (!tokenData.refresh_token) {
|
||||
throw new Error("No refresh token received. Please try again.");
|
||||
}
|
||||
|
||||
// Get user email
|
||||
onProgress?.("Getting user info...");
|
||||
const email = await getUserEmail(tokenData.access_token);
|
||||
|
||||
// Discover project
|
||||
const projectId = await discoverProject(tokenData.access_token, onProgress);
|
||||
|
||||
// Calculate expiry time (current time + expires_in seconds - 5 min buffer)
|
||||
const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
|
||||
|
||||
const credentials: AntigravityCredentials = {
|
||||
type: "oauth",
|
||||
refresh: tokenData.refresh_token,
|
||||
access: tokenData.access_token,
|
||||
expires: expiresAt,
|
||||
projectId,
|
||||
email,
|
||||
};
|
||||
|
||||
saveOAuthCredentials("google-antigravity", credentials);
|
||||
|
||||
return credentials;
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* Gemini CLI OAuth flow (Google Cloud Code Assist)
|
||||
* Standard Gemini models only (gemini-2.0-flash, gemini-2.5-*)
|
||||
*/
|
||||
|
||||
import { createHash, randomBytes } from "crypto";
|
||||
import { createServer, type Server } from "http";
|
||||
import { type OAuthCredentials, saveOAuthCredentials } from "./storage.js";
|
||||
|
|
@ -217,9 +222,47 @@ async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Login with Google Cloud OAuth
|
||||
* Refresh Google Cloud Code Assist token
|
||||
*/
|
||||
export async function loginGoogleCloud(
|
||||
export async function refreshGoogleCloudToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Google Cloud token refresh failed: ${error}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token?: string;
|
||||
};
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
refresh: data.refresh_token || refreshToken,
|
||||
access: data.access_token,
|
||||
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
||||
projectId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with Gemini CLI (Google Cloud Code Assist) OAuth
|
||||
*
|
||||
* @param onAuth - Callback with URL and optional instructions
|
||||
* @param onProgress - Optional progress callback
|
||||
*/
|
||||
export async function loginGeminiCli(
|
||||
onAuth: (info: { url: string; instructions?: string }) => void,
|
||||
onProgress?: (message: string) => void,
|
||||
): Promise<GoogleCloudCredentials> {
|
||||
|
|
@ -311,63 +354,10 @@ export async function loginGoogleCloud(
|
|||
email,
|
||||
};
|
||||
|
||||
saveOAuthCredentials("google-cloud-code-assist", credentials);
|
||||
saveOAuthCredentials("google-gemini-cli", credentials);
|
||||
|
||||
return credentials;
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Google Cloud OAuth token using refresh token
|
||||
*/
|
||||
export async function refreshGoogleCloudToken(
|
||||
refreshToken: string,
|
||||
existingProjectId?: string,
|
||||
): Promise<GoogleCloudCredentials> {
|
||||
const tokenResponse = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
throw new Error(`Token refresh failed: ${error}`);
|
||||
}
|
||||
|
||||
const tokenData = (await tokenResponse.json()) as {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token?: string; // May or may not be returned
|
||||
};
|
||||
|
||||
// Calculate expiry time (current time + expires_in seconds - 5 min buffer)
|
||||
const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
|
||||
|
||||
// Get user email
|
||||
const email = await getUserEmail(tokenData.access_token);
|
||||
|
||||
// Use existing project ID or discover new one
|
||||
let projectId = existingProjectId;
|
||||
if (!projectId) {
|
||||
projectId = await discoverProject(tokenData.access_token);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
refresh: tokenData.refresh_token || refreshToken, // Use new refresh token if provided, otherwise keep existing
|
||||
access: tokenData.access_token,
|
||||
expires: expiresAt,
|
||||
projectId,
|
||||
email,
|
||||
};
|
||||
}
|
||||
210
packages/ai/src/utils/oauth/index.ts
Normal file
210
packages/ai/src/utils/oauth/index.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* OAuth credential management for AI providers.
|
||||
*
|
||||
* This module handles login, token refresh, and credential storage
|
||||
* for OAuth-based providers:
|
||||
* - Anthropic (Claude Pro/Max)
|
||||
* - GitHub Copilot
|
||||
* - Google Cloud Code Assist (Gemini CLI)
|
||||
* - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud)
|
||||
*/
|
||||
|
||||
// Anthropic
|
||||
export { loginAnthropic, refreshAnthropicToken } from "./anthropic.js";
|
||||
// GitHub Copilot
|
||||
export {
|
||||
enableAllGitHubCopilotModels,
|
||||
enableGitHubCopilotModel,
|
||||
getBaseUrlFromToken,
|
||||
getGitHubCopilotBaseUrl,
|
||||
loginGitHubCopilot,
|
||||
normalizeDomain,
|
||||
refreshGitHubCopilotToken,
|
||||
} from "./github-copilot.js";
|
||||
// Google Antigravity
|
||||
export {
|
||||
type AntigravityCredentials,
|
||||
loginAntigravity,
|
||||
refreshAntigravityToken,
|
||||
} from "./google-antigravity.js";
|
||||
// Google Gemini CLI
|
||||
export {
|
||||
type GoogleCloudCredentials,
|
||||
loginGeminiCli,
|
||||
refreshGoogleCloudToken,
|
||||
} from "./google-gemini-cli.js";
|
||||
// Storage
|
||||
export {
|
||||
getOAuthPath,
|
||||
hasOAuthCredentials,
|
||||
listOAuthProviders,
|
||||
loadOAuthCredentials,
|
||||
loadOAuthStorage,
|
||||
type OAuthCredentials,
|
||||
type OAuthProvider,
|
||||
type OAuthStorage,
|
||||
removeOAuthCredentials,
|
||||
saveOAuthCredentials,
|
||||
} from "./storage.js";
|
||||
|
||||
// ============================================================================
|
||||
// High-level API
|
||||
// ============================================================================
|
||||
|
||||
import { refreshAnthropicToken } from "./anthropic.js";
|
||||
import { refreshGitHubCopilotToken } from "./github-copilot.js";
|
||||
import { refreshAntigravityToken } from "./google-antigravity.js";
|
||||
import { refreshGoogleCloudToken } from "./google-gemini-cli.js";
|
||||
import type { OAuthCredentials, OAuthProvider } from "./storage.js";
|
||||
import { loadOAuthCredentials, removeOAuthCredentials, saveOAuthCredentials } from "./storage.js";
|
||||
|
||||
/**
|
||||
* Refresh token for any OAuth provider.
|
||||
* Saves the new credentials and returns the new access token.
|
||||
*/
|
||||
export async function refreshToken(provider: OAuthProvider): Promise<string> {
|
||||
const credentials = loadOAuthCredentials(provider);
|
||||
if (!credentials) {
|
||||
throw new Error(`No OAuth credentials found for ${provider}`);
|
||||
}
|
||||
|
||||
let newCredentials: OAuthCredentials;
|
||||
|
||||
switch (provider) {
|
||||
case "anthropic":
|
||||
newCredentials = await refreshAnthropicToken(credentials.refresh);
|
||||
break;
|
||||
case "github-copilot":
|
||||
newCredentials = await refreshGitHubCopilotToken(credentials.refresh, credentials.enterpriseUrl);
|
||||
break;
|
||||
case "google-gemini-cli":
|
||||
if (!credentials.projectId) {
|
||||
throw new Error("Google Cloud credentials missing projectId");
|
||||
}
|
||||
newCredentials = await refreshGoogleCloudToken(credentials.refresh, credentials.projectId);
|
||||
break;
|
||||
case "google-antigravity":
|
||||
if (!credentials.projectId) {
|
||||
throw new Error("Antigravity credentials missing projectId");
|
||||
}
|
||||
newCredentials = await refreshAntigravityToken(credentials.refresh, credentials.projectId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown OAuth provider: ${provider}`);
|
||||
}
|
||||
|
||||
saveOAuthCredentials(provider, newCredentials);
|
||||
return newCredentials.access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key for a provider from OAuth credentials.
|
||||
* Automatically refreshes expired tokens.
|
||||
*
|
||||
* For google-gemini-cli and antigravity, returns JSON-encoded { token, projectId }
|
||||
*
|
||||
* @returns API key string, or null if no credentials
|
||||
*/
|
||||
export async function getOAuthApiKey(provider: OAuthProvider): Promise<string | null> {
|
||||
const credentials = loadOAuthCredentials(provider);
|
||||
if (!credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Providers that need projectId in the API key
|
||||
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
||||
|
||||
// Check if expired
|
||||
if (Date.now() >= credentials.expires) {
|
||||
try {
|
||||
const newToken = await refreshToken(provider);
|
||||
|
||||
// For providers that need projectId, return JSON
|
||||
if (needsProjectId) {
|
||||
const refreshedCreds = loadOAuthCredentials(provider);
|
||||
if (refreshedCreds?.projectId) {
|
||||
return JSON.stringify({ token: newToken, projectId: refreshedCreds.projectId });
|
||||
}
|
||||
}
|
||||
|
||||
return newToken;
|
||||
} catch (error) {
|
||||
console.error(`Failed to refresh OAuth token for ${provider}:`, error);
|
||||
removeOAuthCredentials(provider);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// For providers that need projectId, return JSON
|
||||
if (needsProjectId) {
|
||||
if (!credentials.projectId) {
|
||||
return null;
|
||||
}
|
||||
return JSON.stringify({ token: credentials.access, projectId: credentials.projectId });
|
||||
}
|
||||
|
||||
return credentials.access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map model provider to OAuth provider.
|
||||
* Returns undefined if the provider doesn't use OAuth.
|
||||
*/
|
||||
export function getOAuthProviderForModelProvider(modelProvider: string): OAuthProvider | undefined {
|
||||
const mapping: Record<string, OAuthProvider> = {
|
||||
anthropic: "anthropic",
|
||||
"github-copilot": "github-copilot",
|
||||
"google-gemini-cli": "google-gemini-cli",
|
||||
"google-antigravity": "google-antigravity",
|
||||
};
|
||||
return mapping[modelProvider];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Login/Logout types for convenience
|
||||
// ============================================================================
|
||||
|
||||
export type OAuthPrompt = {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
allowEmpty?: boolean;
|
||||
};
|
||||
|
||||
export type OAuthAuthInfo = {
|
||||
url: string;
|
||||
instructions?: string;
|
||||
};
|
||||
|
||||
export interface OAuthProviderInfo {
|
||||
id: OAuthProvider;
|
||||
name: string;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of OAuth providers
|
||||
*/
|
||||
export function getOAuthProviders(): OAuthProviderInfo[] {
|
||||
return [
|
||||
{
|
||||
id: "anthropic",
|
||||
name: "Anthropic (Claude Pro/Max)",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "github-copilot",
|
||||
name: "GitHub Copilot",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "google-gemini-cli",
|
||||
name: "Google Cloud Code Assist (Gemini CLI)",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "google-antigravity",
|
||||
name: "Antigravity (Gemini 3, Claude, GPT-OSS)",
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
/**
|
||||
* OAuth credential storage for ~/.pi/agent/oauth.json
|
||||
*/
|
||||
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { getAgentDir, getOAuthPath } from "../../config.js";
|
||||
import { homedir } from "os";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
export interface OAuthCredentials {
|
||||
type: "oauth";
|
||||
|
|
@ -7,28 +12,37 @@ export interface OAuthCredentials {
|
|||
access: string;
|
||||
expires: number;
|
||||
enterpriseUrl?: string;
|
||||
projectId?: string; // For Google Cloud Code Assist
|
||||
email?: string; // For Google Cloud Code Assist
|
||||
projectId?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface OAuthStorageFormat {
|
||||
export interface OAuthStorage {
|
||||
[provider: string]: OAuthCredentials;
|
||||
}
|
||||
|
||||
export type OAuthProvider = "anthropic" | "github-copilot" | "google-gemini-cli" | "google-antigravity";
|
||||
|
||||
/**
|
||||
* Get the path to the OAuth credentials file
|
||||
*/
|
||||
export function getOAuthPath(): string {
|
||||
return join(homedir(), ".pi", "agent", "oauth.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the config directory exists
|
||||
*/
|
||||
function ensureConfigDir(): void {
|
||||
const configDir = getAgentDir();
|
||||
const configDir = dirname(getOAuthPath());
|
||||
if (!existsSync(configDir)) {
|
||||
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all OAuth credentials from oauth.json
|
||||
* Load all OAuth credentials from ~/.pi/agent/oauth.json
|
||||
*/
|
||||
function loadStorage(): OAuthStorageFormat {
|
||||
export function loadOAuthStorage(): OAuthStorage {
|
||||
const filePath = getOAuthPath();
|
||||
if (!existsSync(filePath)) {
|
||||
return {};
|
||||
|
|
@ -37,20 +51,18 @@ function loadStorage(): OAuthStorageFormat {
|
|||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.error(`Warning: Failed to load OAuth credentials: ${error}`);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all OAuth credentials to oauth.json
|
||||
* Save all OAuth credentials to ~/.pi/agent/oauth.json
|
||||
*/
|
||||
function saveStorage(storage: OAuthStorageFormat): void {
|
||||
function saveOAuthStorage(storage: OAuthStorage): void {
|
||||
ensureConfigDir();
|
||||
const filePath = getOAuthPath();
|
||||
writeFileSync(filePath, JSON.stringify(storage, null, 2), "utf-8");
|
||||
// Set permissions to owner read/write only
|
||||
chmodSync(filePath, 0o600);
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +70,7 @@ function saveStorage(storage: OAuthStorageFormat): void {
|
|||
* Load OAuth credentials for a specific provider
|
||||
*/
|
||||
export function loadOAuthCredentials(provider: string): OAuthCredentials | null {
|
||||
const storage = loadStorage();
|
||||
const storage = loadOAuthStorage();
|
||||
return storage[provider] || null;
|
||||
}
|
||||
|
||||
|
|
@ -66,24 +78,31 @@ export function loadOAuthCredentials(provider: string): OAuthCredentials | null
|
|||
* Save OAuth credentials for a specific provider
|
||||
*/
|
||||
export function saveOAuthCredentials(provider: string, creds: OAuthCredentials): void {
|
||||
const storage = loadStorage();
|
||||
const storage = loadOAuthStorage();
|
||||
storage[provider] = creds;
|
||||
saveStorage(storage);
|
||||
saveOAuthStorage(storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove OAuth credentials for a specific provider
|
||||
*/
|
||||
export function removeOAuthCredentials(provider: string): void {
|
||||
const storage = loadStorage();
|
||||
const storage = loadOAuthStorage();
|
||||
delete storage[provider];
|
||||
saveStorage(storage);
|
||||
saveOAuthStorage(storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OAuth credentials exist for a provider
|
||||
*/
|
||||
export function hasOAuthCredentials(provider: string): boolean {
|
||||
return loadOAuthCredentials(provider) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all providers with OAuth credentials
|
||||
*/
|
||||
export function listOAuthProviders(): string[] {
|
||||
const storage = loadStorage();
|
||||
const storage = loadOAuthStorage();
|
||||
return Object.keys(storage);
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import type { AssistantMessage } from "../types.js";
|
|||
* - OpenRouter: "This endpoint's maximum context length is X tokens. However, you requested about Y tokens"
|
||||
* - llama.cpp: "the request exceeds the available context size, try increasing it"
|
||||
* - LM Studio: "tokens to keep from the initial prompt is greater than the context length"
|
||||
* - GitHub Copilot: "prompt token count of X exceeds the limit of Y"
|
||||
* - Cerebras: Returns "400 status code (no body)" - handled separately below
|
||||
* - Mistral: Returns "400 status code (no body)" - handled separately below
|
||||
* - z.ai: Does NOT error, accepts overflow silently - handled via usage.input > contextWindow
|
||||
|
|
@ -28,6 +29,7 @@ const OVERFLOW_PATTERNS = [
|
|||
/maximum prompt length is \d+/i, // xAI (Grok)
|
||||
/reduce the length of the messages/i, // Groq
|
||||
/maximum context length is \d+ tokens/i, // OpenRouter (all backends)
|
||||
/exceeds the limit of \d+/i, // GitHub Copilot
|
||||
/exceeds the available context size/i, // llama.cpp server
|
||||
/greater than the context length/i, // LM Studio
|
||||
/context length exceeded/i, // Generic fallback
|
||||
|
|
|
|||
|
|
@ -15,10 +15,18 @@ import type { ChildProcess } from "child_process";
|
|||
import { execSync, spawn } from "child_process";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { getModel } from "../src/models.js";
|
||||
import { complete } from "../src/stream.js";
|
||||
import { complete, resolveApiKey } from "../src/stream.js";
|
||||
import type { AssistantMessage, Context, Model, Usage } from "../src/types.js";
|
||||
import { isContextOverflow } from "../src/utils/overflow.js";
|
||||
|
||||
// Resolve OAuth tokens at module level (async, runs before tests)
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
]);
|
||||
const [githubCopilotToken, geminiCliToken, antigravityToken] = oauthTokens;
|
||||
|
||||
// Lorem ipsum paragraph for realistic token estimation
|
||||
const LOREM_IPSUM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. `;
|
||||
|
||||
|
|
@ -111,6 +119,43 @@ describe("Context overflow error handling", () => {
|
|||
}, 120000);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// GitHub Copilot (OAuth)
|
||||
// Tests both OpenAI and Anthropic models via Copilot
|
||||
// =============================================================================
|
||||
|
||||
describe("GitHub Copilot (OAuth)", () => {
|
||||
// OpenAI model via Copilot
|
||||
it.skipIf(!githubCopilotToken)(
|
||||
"gpt-4o - should detect overflow via isContextOverflow",
|
||||
async () => {
|
||||
const model = getModel("github-copilot", "gpt-4o");
|
||||
const result = await testContextOverflow(model, githubCopilotToken!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(result.errorMessage).toMatch(/exceeds the limit of \d+/i);
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
},
|
||||
120000,
|
||||
);
|
||||
|
||||
// Anthropic model via Copilot
|
||||
it.skipIf(!githubCopilotToken)(
|
||||
"claude-sonnet-4 - should detect overflow via isContextOverflow",
|
||||
async () => {
|
||||
const model = getModel("github-copilot", "claude-sonnet-4");
|
||||
const result = await testContextOverflow(model, githubCopilotToken!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(result.errorMessage).toMatch(/exceeds the limit of \d+/i);
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
},
|
||||
120000,
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// OpenAI
|
||||
// Expected pattern: "exceeds the context window"
|
||||
|
|
@ -158,6 +203,65 @@ describe("Context overflow error handling", () => {
|
|||
}, 120000);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Google Gemini CLI (OAuth)
|
||||
// Uses same API as Google, expects same error pattern
|
||||
// =============================================================================
|
||||
|
||||
describe("Google Gemini CLI (OAuth)", () => {
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should detect overflow via isContextOverflow",
|
||||
async () => {
|
||||
const model = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
const result = await testContextOverflow(model, geminiCliToken!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(result.errorMessage).toMatch(/input token count.*exceeds the maximum/i);
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
},
|
||||
120000,
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Google Antigravity (OAuth)
|
||||
// Tests both Gemini and Anthropic models via Antigravity
|
||||
// =============================================================================
|
||||
|
||||
describe("Google Antigravity (OAuth)", () => {
|
||||
// Gemini model
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should detect overflow via isContextOverflow",
|
||||
async () => {
|
||||
const model = getModel("google-antigravity", "gemini-3-flash");
|
||||
const result = await testContextOverflow(model, antigravityToken!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(result.errorMessage).toMatch(/input token count.*exceeds the maximum/i);
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
},
|
||||
120000,
|
||||
);
|
||||
|
||||
// Anthropic model via Antigravity
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should detect overflow via isContextOverflow",
|
||||
async () => {
|
||||
const model = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
const result = await testContextOverflow(model, antigravityToken!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
// Anthropic models return "prompt is too long" pattern
|
||||
expect(result.errorMessage).toMatch(/prompt is too long/i);
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
},
|
||||
120000,
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// xAI
|
||||
// Expected pattern: "maximum prompt length is X but the request contains Y"
|
||||
|
|
|
|||
|
|
@ -5,13 +5,22 @@ import { dirname, join } from "path";
|
|||
import { fileURLToPath } from "url";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { getModel } from "../src/models.js";
|
||||
import { complete, stream } from "../src/stream.js";
|
||||
import { complete, resolveApiKey, stream } from "../src/stream.js";
|
||||
import type { Api, Context, ImageContent, Model, OptionsForApi, Tool, ToolResultMessage } from "../src/types.js";
|
||||
import { StringEnum } from "../src/utils/typebox-helpers.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Resolve OAuth tokens at module level (async, runs before tests)
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("anthropic"),
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
]);
|
||||
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken] = oauthTokens;
|
||||
|
||||
// Calculator tool definition (same as examples)
|
||||
// Note: Using StringEnum helper because Google's API doesn't support anyOf/const patterns
|
||||
// that Type.Enum generates. Google requires { type: "string", enum: [...] } format.
|
||||
|
|
@ -314,7 +323,7 @@ async function multiTurn<TApi extends Api>(model: Model<TApi>, options?: Options
|
|||
context.messages.push(...results);
|
||||
|
||||
// If we got a stop response with text content, we're likely done
|
||||
expect(response.stopReason).not.toBe("error");
|
||||
expect(response.stopReason, `Error: ${response.errorMessage}`).not.toBe("error");
|
||||
if (response.stopReason === "stop") {
|
||||
break;
|
||||
}
|
||||
|
|
@ -426,34 +435,6 @@ describe("Generate E2E Tests", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.ANTHROPIC_OAUTH_TOKEN)("Anthropic Provider (claude-sonnet-4-20250514)", () => {
|
||||
const model = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||
|
||||
it("should complete basic text generation", async () => {
|
||||
await basicTextGeneration(model, { thinkingEnabled: true });
|
||||
});
|
||||
|
||||
it("should handle tool calling", async () => {
|
||||
await handleToolCall(model);
|
||||
});
|
||||
|
||||
it("should handle streaming", async () => {
|
||||
await handleStreaming(model);
|
||||
});
|
||||
|
||||
it("should handle thinking", async () => {
|
||||
await handleThinking(model, { thinkingEnabled: true });
|
||||
});
|
||||
|
||||
it("should handle multi-turn with thinking and tools", async () => {
|
||||
await multiTurn(model, { thinkingEnabled: true });
|
||||
});
|
||||
|
||||
it("should handle image input", async () => {
|
||||
await handleImage(model);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI Responses Provider (gpt-5-mini)", () => {
|
||||
const model = getModel("openai", "gpt-5-mini");
|
||||
|
||||
|
|
@ -678,30 +659,163 @@ describe("Generate E2E Tests", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Read GitHub Copilot token from ~/.pi/agent/oauth.json if available
|
||||
let githubCopilotToken: string | undefined;
|
||||
try {
|
||||
const oauthPath = join(process.env.HOME || "", ".pi/agent/oauth.json");
|
||||
const oauthData = JSON.parse(readFileSync(oauthPath, "utf-8"));
|
||||
githubCopilotToken = oauthData["github-copilot"]?.access;
|
||||
} catch {
|
||||
// oauth.json doesn't exist or is invalid
|
||||
}
|
||||
// =========================================================================
|
||||
// OAuth-based providers (credentials from ~/.pi/agent/oauth.json)
|
||||
// Tokens are resolved at module level (see oauthTokens above)
|
||||
// =========================================================================
|
||||
|
||||
describe.skipIf(!githubCopilotToken)("GitHub Copilot Provider (gpt-4o via OpenAI Completions)", () => {
|
||||
describe("Anthropic OAuth Provider (claude-sonnet-4-20250514)", () => {
|
||||
const model = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||
|
||||
it.skipIf(!anthropicOAuthToken)("should complete basic text generation", async () => {
|
||||
await basicTextGeneration(model, { apiKey: anthropicOAuthToken });
|
||||
});
|
||||
|
||||
it.skipIf(!anthropicOAuthToken)("should handle tool calling", async () => {
|
||||
await handleToolCall(model, { apiKey: anthropicOAuthToken });
|
||||
});
|
||||
|
||||
it.skipIf(!anthropicOAuthToken)("should handle streaming", async () => {
|
||||
await handleStreaming(model, { apiKey: anthropicOAuthToken });
|
||||
});
|
||||
|
||||
it.skipIf(!anthropicOAuthToken)("should handle thinking", async () => {
|
||||
await handleThinking(model, { apiKey: anthropicOAuthToken, thinkingEnabled: true });
|
||||
});
|
||||
|
||||
it.skipIf(!anthropicOAuthToken)("should handle multi-turn with thinking and tools", async () => {
|
||||
await multiTurn(model, { apiKey: anthropicOAuthToken, thinkingEnabled: true });
|
||||
});
|
||||
|
||||
it.skipIf(!anthropicOAuthToken)("should handle image input", async () => {
|
||||
await handleImage(model, { apiKey: anthropicOAuthToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHub Copilot Provider (gpt-4o via OpenAI Completions)", () => {
|
||||
const llm = getModel("github-copilot", "gpt-4o");
|
||||
|
||||
it("should complete basic text generation", async () => {
|
||||
it.skipIf(!githubCopilotToken)("should complete basic text generation", async () => {
|
||||
await basicTextGeneration(llm, { apiKey: githubCopilotToken });
|
||||
});
|
||||
|
||||
it("should handle tool calling", async () => {
|
||||
it.skipIf(!githubCopilotToken)("should handle tool calling", async () => {
|
||||
await handleToolCall(llm, { apiKey: githubCopilotToken });
|
||||
});
|
||||
|
||||
it("should handle streaming", async () => {
|
||||
it.skipIf(!githubCopilotToken)("should handle streaming", async () => {
|
||||
await handleStreaming(llm, { apiKey: githubCopilotToken });
|
||||
});
|
||||
|
||||
it.skipIf(!githubCopilotToken)("should handle thinking", { retry: 2 }, async () => {
|
||||
const thinkingModel = getModel("github-copilot", "gpt-5-mini");
|
||||
await handleThinking(thinkingModel, { apiKey: githubCopilotToken, reasoningEffort: "high" });
|
||||
});
|
||||
|
||||
it.skipIf(!githubCopilotToken)("should handle multi-turn with thinking and tools", async () => {
|
||||
const thinkingModel = getModel("github-copilot", "gpt-5-mini");
|
||||
await multiTurn(thinkingModel, { apiKey: githubCopilotToken, reasoningEffort: "high" });
|
||||
});
|
||||
|
||||
it.skipIf(!githubCopilotToken)("should handle image input", async () => {
|
||||
await handleImage(llm, { apiKey: githubCopilotToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider (gemini-2.5-flash)", () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
|
||||
it.skipIf(!geminiCliToken)("should complete basic text generation", async () => {
|
||||
await basicTextGeneration(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle tool calling", async () => {
|
||||
await handleToolCall(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle streaming", async () => {
|
||||
await handleStreaming(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle thinking", async () => {
|
||||
await handleThinking(llm, { apiKey: geminiCliToken, thinking: { enabled: true, budgetTokens: 1024 } });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle multi-turn with thinking and tools", async () => {
|
||||
await multiTurn(llm, { apiKey: geminiCliToken, thinking: { enabled: true, budgetTokens: 2048 } });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle image input", async () => {
|
||||
await handleImage(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider (gemini-3-flash)", () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
|
||||
it.skipIf(!antigravityToken)("should complete basic text generation", async () => {
|
||||
await basicTextGeneration(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle tool calling", async () => {
|
||||
await handleToolCall(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle streaming", async () => {
|
||||
await handleStreaming(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle thinking", async () => {
|
||||
// gemini-3-flash has reasoning: false, use gemini-3-pro-high for thinking
|
||||
const thinkingModel = getModel("google-antigravity", "gemini-3-pro-high");
|
||||
await handleThinking(thinkingModel, {
|
||||
apiKey: antigravityToken,
|
||||
thinking: { enabled: true, budgetTokens: 1024 },
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle multi-turn with thinking and tools", async () => {
|
||||
const thinkingModel = getModel("google-antigravity", "gemini-3-pro-high");
|
||||
await multiTurn(thinkingModel, { apiKey: antigravityToken, thinking: { enabled: true, budgetTokens: 2048 } });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle image input", async () => {
|
||||
await handleImage(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider (claude-sonnet-4-5)", () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
|
||||
it.skipIf(!antigravityToken)("should complete basic text generation", async () => {
|
||||
await basicTextGeneration(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle tool calling", async () => {
|
||||
await handleToolCall(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle streaming", async () => {
|
||||
await handleStreaming(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle thinking", async () => {
|
||||
// claude-sonnet-4-5 has reasoning: false, use claude-sonnet-4-5-thinking
|
||||
const thinkingModel = getModel("google-antigravity", "claude-sonnet-4-5-thinking");
|
||||
await handleThinking(thinkingModel, {
|
||||
apiKey: antigravityToken,
|
||||
thinking: { enabled: true, budgetTokens: 4096 },
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle multi-turn with thinking and tools", async () => {
|
||||
const thinkingModel = getModel("google-antigravity", "claude-sonnet-4-5-thinking");
|
||||
await multiTurn(thinkingModel, { apiKey: antigravityToken, thinking: { enabled: true, budgetTokens: 4096 } });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle image input", async () => {
|
||||
await handleImage(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
});
|
||||
|
||||
// Check if ollama is installed
|
||||
|
|
|
|||
|
|
@ -2,12 +2,20 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Google Gemini CLI OAuth provider**: Access Gemini 2.0/2.5 models for free via Google Cloud Code Assist. Login with `/login` and select "Google Gemini CLI". Uses your Google account with rate limits.
|
||||
|
||||
- **Google Antigravity OAuth provider**: Access Gemini 3, Claude (sonnet/opus thinking models), and GPT-OSS models for free via Google's Antigravity sandbox. Login with `/login` and select "Antigravity". Uses your Google account with rate limits.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Connection errors not retried**: Added "connection error" to the list of retryable errors so Anthropic connection drops trigger auto-retry instead of silently failing. ([#252](https://github.com/badlogic/pi-mono/issues/252))
|
||||
|
||||
- **Thinking level not clamped on model switch**: Fixed TUI showing xhigh thinking level after switching to a model that doesn't support it. Thinking level is now automatically clamped to model capabilities. ([#253](https://github.com/badlogic/pi-mono/issues/253))
|
||||
|
||||
- **Cross-model thinking handoff**: Fixed error when switching between models with different thinking signature formats (e.g., GPT-OSS to Claude thinking models via Antigravity). Thinking blocks without signatures are now converted to text with `<thinking>` delimiters.
|
||||
|
||||
## [0.24.5] - 2025-12-20
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -117,25 +117,30 @@ Set the environment variable for your provider:
|
|||
| OpenRouter | `OPENROUTER_API_KEY` |
|
||||
| ZAI | `ZAI_API_KEY` |
|
||||
|
||||
**Anthropic OAuth (Claude Pro/Max):**
|
||||
**OAuth Providers:**
|
||||
|
||||
Use `/login` to authenticate with subscription-based or free-tier providers:
|
||||
|
||||
| Provider | Models | Cost |
|
||||
|----------|--------|------|
|
||||
| Anthropic (Claude Pro/Max) | Claude models via your subscription | Subscription |
|
||||
| GitHub Copilot | GPT-4o, Claude, Gemini via Copilot subscription | Subscription |
|
||||
| Google Gemini CLI | Gemini 2.0/2.5 models | Free (Google account) |
|
||||
| Google Antigravity | Gemini 3, Claude, GPT-OSS | Free (Google account) |
|
||||
|
||||
```bash
|
||||
pi
|
||||
/login # Select "Anthropic (Claude Pro/Max)", authorize in browser
|
||||
/login # Select provider, authorize in browser
|
||||
```
|
||||
|
||||
Tokens stored in `~/.pi/agent/oauth.json`. Use `/logout` to clear.
|
||||
**GitHub Copilot notes:**
|
||||
- Press Enter for github.com, or enter your GitHub Enterprise Server domain
|
||||
- If you get "model not supported" error, enable it in VS Code: Copilot Chat → model selector → select model → "Enable"
|
||||
|
||||
**GitHub Copilot OAuth:**
|
||||
|
||||
```bash
|
||||
pi
|
||||
/login # Select "GitHub Copilot", authorize in browser
|
||||
```
|
||||
|
||||
Press Enter to use github.com, or enter your GitHub Enterprise Server domain (e.g., `github.mycompany.com`).
|
||||
|
||||
If you get "The requested model is not supported" error, enable the model in VS Code: Copilot Chat → model selector → select model → "Enable".
|
||||
**Google providers notes:**
|
||||
- Gemini CLI uses the production Cloud Code Assist endpoint (standard Gemini models)
|
||||
- Antigravity uses a sandbox endpoint with access to Gemini 3, Claude (sonnet/opus thinking), and GPT-OSS models
|
||||
- Both are free with any Google account, subject to rate limits
|
||||
|
||||
Tokens stored in `~/.pi/agent/oauth.json`. Use `/logout` to clear.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
import { type Api, getApiKey, getModels, getProviders, type KnownProvider, type Model } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
type Api,
|
||||
getApiKey,
|
||||
getGitHubCopilotBaseUrl,
|
||||
getModels,
|
||||
getProviders,
|
||||
type KnownProvider,
|
||||
loadOAuthCredentials,
|
||||
type Model,
|
||||
normalizeDomain,
|
||||
refreshGitHubCopilotToken,
|
||||
removeOAuthCredentials,
|
||||
saveOAuthCredentials,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import AjvModule from "ajv";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { getModelsPath } from "../config.js";
|
||||
import { getGitHubCopilotBaseUrl, normalizeDomain, refreshGitHubCopilotToken } from "./oauth/github-copilot.js";
|
||||
import { getOAuthToken, refreshToken, type SupportedOAuthProvider } from "./oauth/index.js";
|
||||
import { loadOAuthCredentials, removeOAuthCredentials, saveOAuthCredentials } from "./oauth/storage.js";
|
||||
import { getOAuthToken, type OAuthProvider, refreshToken } from "./oauth/index.js";
|
||||
|
||||
// Handle both default and named exports
|
||||
const Ajv = (AjvModule as any).default || AjvModule;
|
||||
|
|
@ -312,9 +323,10 @@ export async function getApiKeyForModel(model: Model<Api>): Promise<string | und
|
|||
return githubToken;
|
||||
}
|
||||
|
||||
// For Google Cloud Code Assist, check OAuth and encode projectId with token
|
||||
if (model.provider === "google-cloud-code-assist") {
|
||||
const credentials = loadOAuthCredentials("google-cloud-code-assist");
|
||||
// For Google Gemini CLI and Antigravity, check OAuth and encode projectId with token
|
||||
if (model.provider === "google-gemini-cli" || model.provider === "google-antigravity") {
|
||||
const oauthProvider = model.provider as "google-gemini-cli" | "google-antigravity";
|
||||
const credentials = loadOAuthCredentials(oauthProvider);
|
||||
if (!credentials) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -322,13 +334,13 @@ export async function getApiKeyForModel(model: Model<Api>): Promise<string | und
|
|||
// Check if token is expired
|
||||
if (Date.now() >= credentials.expires) {
|
||||
try {
|
||||
await refreshToken("google-cloud-code-assist");
|
||||
const refreshedCreds = loadOAuthCredentials("google-cloud-code-assist");
|
||||
await refreshToken(oauthProvider);
|
||||
const refreshedCreds = loadOAuthCredentials(oauthProvider);
|
||||
if (refreshedCreds?.projectId) {
|
||||
return JSON.stringify({ token: refreshedCreds.access, projectId: refreshedCreds.projectId });
|
||||
}
|
||||
} catch {
|
||||
removeOAuthCredentials("google-cloud-code-assist");
|
||||
removeOAuthCredentials(oauthProvider);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -395,10 +407,11 @@ export function findModel(provider: string, modelId: string): { model: Model<Api
|
|||
* Mapping from model provider to OAuth provider ID.
|
||||
* Only providers that support OAuth are listed here.
|
||||
*/
|
||||
const providerToOAuthProvider: Record<string, SupportedOAuthProvider> = {
|
||||
const providerToOAuthProvider: Record<string, OAuthProvider> = {
|
||||
anthropic: "anthropic",
|
||||
"github-copilot": "github-copilot",
|
||||
"google-cloud-code-assist": "google-cloud-code-assist",
|
||||
"google-gemini-cli": "google-gemini-cli",
|
||||
"google-antigravity": "google-antigravity",
|
||||
};
|
||||
|
||||
// Cache for OAuth status per provider (avoids file reads on every render)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|||
anthropic: "claude-sonnet-4-5",
|
||||
openai: "gpt-5.1-codex",
|
||||
google: "gemini-2.5-pro",
|
||||
"google-gemini-cli": "gemini-2.5-pro",
|
||||
"google-antigravity": "gemini-3-pro-high",
|
||||
"github-copilot": "gpt-4o",
|
||||
openrouter: "openai/gpt-5.1-codex",
|
||||
xai: "grok-4-fast-non-reasoning",
|
||||
|
|
|
|||
|
|
@ -1,54 +1,70 @@
|
|||
import { loginAnthropic, refreshAnthropicToken } from "./anthropic.js";
|
||||
import { loginGitHubCopilot, refreshGitHubCopilotToken } from "./github-copilot.js";
|
||||
import { loginGoogleCloud, refreshGoogleCloudToken } from "./google-cloud.js";
|
||||
/**
|
||||
* OAuth management for coding-agent.
|
||||
* Re-exports from @mariozechner/pi-ai and adds convenience wrappers.
|
||||
*/
|
||||
|
||||
import {
|
||||
listOAuthProviders as listOAuthProvidersFromStorage,
|
||||
getOAuthApiKey,
|
||||
listOAuthProviders as listOAuthProvidersFromAi,
|
||||
loadOAuthCredentials,
|
||||
loginAnthropic,
|
||||
loginAntigravity,
|
||||
loginGeminiCli,
|
||||
loginGitHubCopilot,
|
||||
type OAuthCredentials,
|
||||
type OAuthProvider,
|
||||
refreshToken as refreshTokenFromAi,
|
||||
removeOAuthCredentials,
|
||||
saveOAuthCredentials,
|
||||
} from "./storage.js";
|
||||
} from "@mariozechner/pi-ai";
|
||||
|
||||
// Re-export for convenience
|
||||
export { listOAuthProvidersFromStorage as listOAuthProviders };
|
||||
// Re-export types and functions
|
||||
export type { OAuthCredentials, OAuthProvider };
|
||||
export { listOAuthProvidersFromAi as listOAuthProviders };
|
||||
export { getOAuthApiKey, loadOAuthCredentials, removeOAuthCredentials, saveOAuthCredentials };
|
||||
|
||||
export type SupportedOAuthProvider = "anthropic" | "github-copilot" | "google-cloud-code-assist";
|
||||
|
||||
export interface OAuthProviderInfo {
|
||||
id: SupportedOAuthProvider;
|
||||
name: string;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export type OAuthPrompt = {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
allowEmpty?: boolean;
|
||||
};
|
||||
|
||||
export type OAuthAuthInfo = {
|
||||
// Types for OAuth flow
|
||||
export interface OAuthAuthInfo {
|
||||
url: string;
|
||||
instructions?: string;
|
||||
}
|
||||
|
||||
export interface OAuthPrompt {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export type OAuthProviderInfo = {
|
||||
id: OAuthProvider;
|
||||
name: string;
|
||||
description: string;
|
||||
available: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get list of OAuth providers
|
||||
*/
|
||||
export function getOAuthProviders(): OAuthProviderInfo[] {
|
||||
return [
|
||||
{
|
||||
id: "anthropic",
|
||||
name: "Anthropic (Claude Pro/Max)",
|
||||
description: "Use Claude with your Pro/Max subscription",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "github-copilot",
|
||||
name: "GitHub Copilot",
|
||||
description: "Use models via GitHub Copilot subscription",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "google-cloud-code-assist",
|
||||
name: "Google Cloud Code Assist (Gemini CLI)",
|
||||
id: "google-gemini-cli",
|
||||
name: "Google Gemini CLI",
|
||||
description: "Free Gemini 2.0/2.5 models via Google Cloud",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: "google-antigravity",
|
||||
name: "Antigravity",
|
||||
description: "Free Gemini 3, Claude, GPT-OSS via Google Cloud",
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
|
|
@ -58,7 +74,7 @@ export function getOAuthProviders(): OAuthProviderInfo[] {
|
|||
* Login with OAuth provider
|
||||
*/
|
||||
export async function login(
|
||||
provider: SupportedOAuthProvider,
|
||||
provider: OAuthProvider,
|
||||
onAuth: (info: OAuthAuthInfo) => void,
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>,
|
||||
onProgress?: (message: string) => void,
|
||||
|
|
@ -79,8 +95,12 @@ export async function login(
|
|||
saveOAuthCredentials("github-copilot", creds);
|
||||
break;
|
||||
}
|
||||
case "google-cloud-code-assist": {
|
||||
await loginGoogleCloud(onAuth, onProgress);
|
||||
case "google-gemini-cli": {
|
||||
await loginGeminiCli((info) => onAuth({ url: info.url, instructions: info.instructions }), onProgress);
|
||||
break;
|
||||
}
|
||||
case "google-antigravity": {
|
||||
await loginAntigravity((info) => onAuth({ url: info.url, instructions: info.instructions }), onProgress);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
@ -91,62 +111,21 @@ export async function login(
|
|||
/**
|
||||
* Logout from OAuth provider
|
||||
*/
|
||||
export async function logout(provider: SupportedOAuthProvider): Promise<void> {
|
||||
export async function logout(provider: OAuthProvider): Promise<void> {
|
||||
removeOAuthCredentials(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh OAuth token for provider
|
||||
* Refresh OAuth token for provider.
|
||||
* Delegates to the ai package implementation.
|
||||
*/
|
||||
export async function refreshToken(provider: SupportedOAuthProvider): Promise<string> {
|
||||
const credentials = loadOAuthCredentials(provider);
|
||||
if (!credentials) {
|
||||
throw new Error(`No OAuth credentials found for ${provider}`);
|
||||
}
|
||||
|
||||
let newCredentials: OAuthCredentials;
|
||||
|
||||
switch (provider) {
|
||||
case "anthropic":
|
||||
newCredentials = await refreshAnthropicToken(credentials.refresh);
|
||||
break;
|
||||
case "github-copilot":
|
||||
newCredentials = await refreshGitHubCopilotToken(credentials.refresh, credentials.enterpriseUrl);
|
||||
break;
|
||||
case "google-cloud-code-assist":
|
||||
newCredentials = await refreshGoogleCloudToken(credentials.refresh, credentials.projectId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown OAuth provider: ${provider}`);
|
||||
}
|
||||
|
||||
// Save new credentials
|
||||
saveOAuthCredentials(provider, newCredentials);
|
||||
|
||||
return newCredentials.access;
|
||||
export async function refreshToken(provider: OAuthProvider): Promise<string> {
|
||||
return refreshTokenFromAi(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth token for provider (auto-refreshes if expired)
|
||||
* Get OAuth token for provider (auto-refreshes if expired).
|
||||
*/
|
||||
export async function getOAuthToken(provider: SupportedOAuthProvider): Promise<string | null> {
|
||||
const credentials = loadOAuthCredentials(provider);
|
||||
if (!credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if token is expired (with 5 min buffer already applied)
|
||||
if (Date.now() >= credentials.expires) {
|
||||
// Token expired - refresh it
|
||||
try {
|
||||
return await refreshToken(provider);
|
||||
} catch (error) {
|
||||
console.error(`Failed to refresh OAuth token for ${provider}:`, error);
|
||||
// Remove invalid credentials
|
||||
removeOAuthCredentials(provider);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return credentials.access;
|
||||
export async function getOAuthToken(provider: OAuthProvider): Promise<string | null> {
|
||||
return getOAuthApiKey(provider);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export {
|
|||
logout,
|
||||
type OAuthAuthInfo,
|
||||
type OAuthPrompt,
|
||||
type SupportedOAuthProvider,
|
||||
type OAuthProvider,
|
||||
} from "./core/oauth/index.js";
|
||||
export {
|
||||
type CompactionEntry,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { loadOAuthCredentials } from "@mariozechner/pi-ai";
|
||||
import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, TruncatedText } from "@mariozechner/pi-tui";
|
||||
import { getOAuthProviders, type OAuthProviderInfo } from "../../../core/oauth/index.js";
|
||||
import { loadOAuthCredentials } from "../../../core/oauth/storage.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import type { LoadedCustomTool, SessionEvent as ToolSessionEvent } from "../../c
|
|||
import type { HookUIContext } from "../../core/hooks/index.js";
|
||||
import { isBashExecutionMessage } from "../../core/messages.js";
|
||||
import { invalidateOAuthCache } from "../../core/model-config.js";
|
||||
import { listOAuthProviders, login, logout, type SupportedOAuthProvider } from "../../core/oauth/index.js";
|
||||
import { listOAuthProviders, login, logout, type OAuthProvider } from "../../core/oauth/index.js";
|
||||
import { getLatestCompactionEntry, SUMMARY_PREFIX, SUMMARY_SUFFIX } from "../../core/session-manager.js";
|
||||
import { loadSkills } from "../../core/skills.js";
|
||||
import { loadProjectContextFiles } from "../../core/system-prompt.js";
|
||||
|
|
@ -1503,7 +1503,7 @@ export class InteractiveMode {
|
|||
|
||||
try {
|
||||
await login(
|
||||
providerId as SupportedOAuthProvider,
|
||||
providerId as OAuthProvider,
|
||||
(info) => {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
|
||||
|
|
@ -1563,7 +1563,7 @@ export class InteractiveMode {
|
|||
}
|
||||
} else {
|
||||
try {
|
||||
await logout(providerId as SupportedOAuthProvider);
|
||||
await logout(providerId as OAuthProvider);
|
||||
invalidateOAuthCache();
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue