claude-glm 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/anthropic-gateway.ts +108 -2
- package/adapters/google-auth.ts +602 -0
- package/adapters/map.ts +12 -2
- package/adapters/providers/gemini-oauth.ts +470 -0
- package/adapters/providers/gemini.ts +2 -3
- package/adapters/providers/openai.ts +2 -3
- package/adapters/providers/openrouter.ts +2 -3
- package/adapters/types.ts +1 -1
- package/package.json +10 -7
|
@@ -5,8 +5,16 @@ import type { AnthropicRequest, ProviderModel } from "./types.js";
|
|
|
5
5
|
import { chatOpenAI } from "./providers/openai.js";
|
|
6
6
|
import { chatOpenRouter } from "./providers/openrouter.js";
|
|
7
7
|
import { chatGemini } from "./providers/gemini.js";
|
|
8
|
+
import { chatGeminiOAuth } from "./providers/gemini-oauth.js";
|
|
8
9
|
import { passThrough } from "./providers/anthropic-pass.js";
|
|
9
10
|
import { preprocessImages } from "./vision-preprocess.js";
|
|
11
|
+
import {
|
|
12
|
+
buildLoginUrl,
|
|
13
|
+
handleOAuthCallback,
|
|
14
|
+
getLoginStatus,
|
|
15
|
+
googleLogout,
|
|
16
|
+
loginPage,
|
|
17
|
+
} from "./google-auth.js";
|
|
10
18
|
import { config } from "dotenv";
|
|
11
19
|
import { join } from "path";
|
|
12
20
|
import { homedir } from "os";
|
|
@@ -32,6 +40,76 @@ fastify.get("/_status", async () => {
|
|
|
32
40
|
return active ?? { provider: "glm", model: "glm-5" };
|
|
33
41
|
});
|
|
34
42
|
|
|
43
|
+
// ── Google OAuth endpoints ─────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
// Landing page with sign-in button
|
|
46
|
+
fastify.get("/google/login", async (_req, reply) => {
|
|
47
|
+
reply.type("text/html").send(loginPage(PORT));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Start OAuth flow (redirects to Google)
|
|
51
|
+
fastify.get("/google/login/start", async (_req, reply) => {
|
|
52
|
+
const authUrl = buildLoginUrl(PORT);
|
|
53
|
+
reply.redirect(authUrl);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// OAuth callback (receives auth code from Google)
|
|
57
|
+
fastify.get("/google/callback", async (req, reply) => {
|
|
58
|
+
const query = req.query as Record<string, string>;
|
|
59
|
+
const { code, state, error } = query;
|
|
60
|
+
|
|
61
|
+
if (error) {
|
|
62
|
+
return reply.type("text/html").code(400).send(
|
|
63
|
+
`<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0f172a;color:#e2e8f0;">
|
|
64
|
+
<h1 style="color:#f87171;">Login Failed</h1><p>${error}</p>
|
|
65
|
+
<a href="/google/login" style="color:#60a5fa;">Try again</a>
|
|
66
|
+
</body></html>`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!code || !state) {
|
|
71
|
+
return reply.type("text/html").code(400).send(
|
|
72
|
+
`<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0f172a;color:#e2e8f0;">
|
|
73
|
+
<h1 style="color:#f87171;">Missing Parameters</h1><p>No authorization code received.</p>
|
|
74
|
+
<a href="/google/login" style="color:#60a5fa;">Try again</a>
|
|
75
|
+
</body></html>`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const tokens = await handleOAuthCallback(code, state);
|
|
81
|
+
reply.type("text/html").send(
|
|
82
|
+
`<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0f172a;">
|
|
83
|
+
<div style="text-align:center;color:#e2e8f0;max-width:500px;">
|
|
84
|
+
<div style="font-size:48px;">✓</div>
|
|
85
|
+
<h1 style="color:#4ade80;">Authenticated Successfully</h1>
|
|
86
|
+
<p>Logged in as: <strong>${tokens.email || "unknown"}</strong></p>
|
|
87
|
+
${tokens.project_id ? `<p>Code Assist Project: <code style="background:#1e293b;padding:2px 8px;border-radius:4px;">${tokens.project_id}</code></p>` : `<p style="color:#94a3b8;">Using standard Generative Language API</p>`}
|
|
88
|
+
<p style="color:#64748b;margin-top:24px;">You can close this window.<br>Use <code style="background:#1e293b;padding:2px 8px;border-radius:4px;">go:gemini-3.1-pro-preview</code> as your model in Claude Code.</p>
|
|
89
|
+
</div>
|
|
90
|
+
</body></html>`
|
|
91
|
+
);
|
|
92
|
+
} catch (e: any) {
|
|
93
|
+
reply.type("text/html").code(500).send(
|
|
94
|
+
`<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0f172a;color:#e2e8f0;">
|
|
95
|
+
<h1 style="color:#f87171;">Login Failed</h1><p>${e.message}</p>
|
|
96
|
+
<a href="/google/login" style="color:#60a5fa;">Try again</a>
|
|
97
|
+
</body></html>`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Login status
|
|
103
|
+
fastify.get("/google/status", async () => {
|
|
104
|
+
return getLoginStatus();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Logout
|
|
108
|
+
fastify.post("/google/logout", async () => {
|
|
109
|
+
await googleLogout();
|
|
110
|
+
return { ok: true, message: "Logged out of Google" };
|
|
111
|
+
});
|
|
112
|
+
|
|
35
113
|
// Main messages endpoint - routes by model prefix
|
|
36
114
|
fastify.post("/v1/messages", async (req, res) => {
|
|
37
115
|
try {
|
|
@@ -81,6 +159,15 @@ fastify.post("/v1/messages", async (req, res) => {
|
|
|
81
159
|
return chatOpenRouter(res, body, model, key);
|
|
82
160
|
}
|
|
83
161
|
|
|
162
|
+
if (provider === "gemini-oauth") {
|
|
163
|
+
res.raw.setHeader("Content-Type", "text/event-stream");
|
|
164
|
+
res.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
165
|
+
res.raw.setHeader("Connection", "keep-alive");
|
|
166
|
+
// @ts-ignore
|
|
167
|
+
res.raw.flushHeaders?.();
|
|
168
|
+
return chatGeminiOAuth(res, body, model);
|
|
169
|
+
}
|
|
170
|
+
|
|
84
171
|
if (provider === "gemini") {
|
|
85
172
|
const key = process.env.GEMINI_API_KEY;
|
|
86
173
|
if (!key) {
|
|
@@ -142,7 +229,18 @@ fastify.post("/v1/messages", async (req, res) => {
|
|
|
142
229
|
});
|
|
143
230
|
} catch (e: any) {
|
|
144
231
|
const status = e?.statusCode ?? 500;
|
|
145
|
-
|
|
232
|
+
const msg = e?.message || "proxy error";
|
|
233
|
+
console.error(`[ccx] ERROR: ${msg}`);
|
|
234
|
+
|
|
235
|
+
// If SSE headers already sent, we can't send a JSON error - write error as SSE event
|
|
236
|
+
if (res.raw.headersSent) {
|
|
237
|
+
try {
|
|
238
|
+
res.raw.write(`event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: msg } })}\n\n`);
|
|
239
|
+
res.raw.end();
|
|
240
|
+
} catch { /* stream already closed */ }
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
return res.code(status).send({ error: msg });
|
|
146
244
|
}
|
|
147
245
|
});
|
|
148
246
|
|
|
@@ -155,9 +253,17 @@ function apiError(status: number, message: string) {
|
|
|
155
253
|
|
|
156
254
|
fastify
|
|
157
255
|
.listen({ port: PORT, host: "127.0.0.1" })
|
|
158
|
-
.then(() => {
|
|
256
|
+
.then(async () => {
|
|
159
257
|
console.log(`[ccx] Proxy listening on http://127.0.0.1:${PORT}`);
|
|
160
258
|
console.log(`[ccx] Configure API keys in: ${envPath}`);
|
|
259
|
+
|
|
260
|
+
// Show Google login status
|
|
261
|
+
const gStatus = await getLoginStatus();
|
|
262
|
+
if (gStatus.loggedIn) {
|
|
263
|
+
console.log(`[ccx] Google: logged in as ${gStatus.email || "unknown"} (${gStatus.mode})`);
|
|
264
|
+
} else {
|
|
265
|
+
console.log(`[ccx] Google: not logged in. Visit http://127.0.0.1:${PORT}/google/login to authenticate`);
|
|
266
|
+
}
|
|
161
267
|
})
|
|
162
268
|
.catch((err) => {
|
|
163
269
|
console.error("[ccx] Failed to start proxy:", err.message);
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
// Google OAuth 2.0 authentication for Gemini via Code Assist API
|
|
2
|
+
// Uses the same client credentials as the official Gemini CLI
|
|
3
|
+
// (safe to include - this is an "installed application" per Google's OAuth2 docs)
|
|
4
|
+
|
|
5
|
+
import * as http from "http";
|
|
6
|
+
import * as crypto from "crypto";
|
|
7
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
|
|
12
|
+
// ── OAuth constants (from official Gemini CLI) ─────────────────────────
|
|
13
|
+
|
|
14
|
+
const OAUTH_CLIENT_ID =
|
|
15
|
+
"681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
16
|
+
const OAUTH_CLIENT_SECRET = "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";
|
|
17
|
+
|
|
18
|
+
const OAUTH_SCOPES = [
|
|
19
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
20
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
21
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
25
|
+
const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
26
|
+
const USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
27
|
+
|
|
28
|
+
// Code Assist API for Pro subscribers
|
|
29
|
+
const CA_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
30
|
+
const CA_VERSION = "v1internal";
|
|
31
|
+
|
|
32
|
+
// ── Storage ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const PROXY_DIR = join(homedir(), ".claude-proxy");
|
|
35
|
+
const AUTH_FILE = join(PROXY_DIR, "google-oauth.json");
|
|
36
|
+
|
|
37
|
+
export interface GoogleTokens {
|
|
38
|
+
access_token: string;
|
|
39
|
+
refresh_token: string;
|
|
40
|
+
expires_at: number; // timestamp in ms
|
|
41
|
+
email?: string;
|
|
42
|
+
project_id?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function loadTokens(): Promise<GoogleTokens | null> {
|
|
46
|
+
try {
|
|
47
|
+
const data = await readFile(AUTH_FILE, "utf-8");
|
|
48
|
+
const parsed = JSON.parse(data);
|
|
49
|
+
if (!parsed.access_token || !parsed.refresh_token) return null;
|
|
50
|
+
return parsed as GoogleTokens;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function saveTokens(tokens: GoogleTokens): Promise<void> {
|
|
57
|
+
await mkdir(PROXY_DIR, { recursive: true });
|
|
58
|
+
await writeFile(AUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── PKCE helpers ───────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function generatePKCE() {
|
|
64
|
+
const verifier = crypto.randomBytes(32).toString("base64url");
|
|
65
|
+
const challenge = crypto
|
|
66
|
+
.createHash("sha256")
|
|
67
|
+
.update(verifier)
|
|
68
|
+
.digest("base64url");
|
|
69
|
+
return { verifier, challenge };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Token refresh ──────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
async function refreshAccessToken(
|
|
75
|
+
refreshToken: string
|
|
76
|
+
): Promise<{ access_token: string; expires_in: number }> {
|
|
77
|
+
const resp = await fetch(TOKEN_ENDPOINT, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
80
|
+
body: new URLSearchParams({
|
|
81
|
+
client_id: OAUTH_CLIENT_ID,
|
|
82
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
83
|
+
refresh_token: refreshToken,
|
|
84
|
+
grant_type: "refresh_token",
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!resp.ok) {
|
|
89
|
+
const text = await resp.text();
|
|
90
|
+
throw new Error(`Token refresh failed (${resp.status}): ${text}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (await resp.json()) as { access_token: string; expires_in: number };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Get a valid access token, auto-refreshing if needed */
|
|
97
|
+
export async function getAccessToken(): Promise<string> {
|
|
98
|
+
const tokens = await loadTokens();
|
|
99
|
+
if (!tokens) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Not logged in to Google. Visit http://127.0.0.1:" +
|
|
102
|
+
(process.env.CLAUDE_PROXY_PORT || "17870") +
|
|
103
|
+
"/google/login to authenticate."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Refresh if expired or within 5-minute buffer
|
|
108
|
+
if (Date.now() > tokens.expires_at - 5 * 60 * 1000) {
|
|
109
|
+
console.log("[gemini-oauth] Access token expired, refreshing...");
|
|
110
|
+
try {
|
|
111
|
+
const refreshed = await refreshAccessToken(tokens.refresh_token);
|
|
112
|
+
tokens.access_token = refreshed.access_token;
|
|
113
|
+
tokens.expires_at = Date.now() + refreshed.expires_in * 1000;
|
|
114
|
+
await saveTokens(tokens);
|
|
115
|
+
console.log("[gemini-oauth] Token refreshed successfully");
|
|
116
|
+
} catch (e: any) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Token refresh failed: ${e.message}. Please re-login at /google/login`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return tokens.access_token;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── User info ──────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
async function fetchUserEmail(
|
|
129
|
+
accessToken: string
|
|
130
|
+
): Promise<string | undefined> {
|
|
131
|
+
try {
|
|
132
|
+
const resp = await fetch(USERINFO_ENDPOINT, {
|
|
133
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
134
|
+
});
|
|
135
|
+
if (resp.ok) {
|
|
136
|
+
const data = (await resp.json()) as any;
|
|
137
|
+
return data.email;
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Code Assist project setup ──────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async function setupCodeAssist(
|
|
146
|
+
accessToken: string
|
|
147
|
+
): Promise<string | undefined> {
|
|
148
|
+
try {
|
|
149
|
+
console.log("[gemini-oauth] Setting up Code Assist project...");
|
|
150
|
+
|
|
151
|
+
const resp = await fetch(`${CA_ENDPOINT}/${CA_VERSION}:loadCodeAssist`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
Authorization: `Bearer ${accessToken}`,
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
metadata: {
|
|
159
|
+
ideType: "IDE_UNSPECIFIED",
|
|
160
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
161
|
+
pluginType: "GEMINI",
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!resp.ok) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`[gemini-oauth] Code Assist setup returned ${resp.status} - will use standard API`
|
|
169
|
+
);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const data = (await resp.json()) as any;
|
|
174
|
+
|
|
175
|
+
// Already set up
|
|
176
|
+
if (data.cloudaicompanionProject) {
|
|
177
|
+
console.log(
|
|
178
|
+
`[gemini-oauth] Code Assist project: ${data.cloudaicompanionProject}`
|
|
179
|
+
);
|
|
180
|
+
if (data.currentTier) {
|
|
181
|
+
console.log(
|
|
182
|
+
`[gemini-oauth] Tier: ${data.currentTier.name || data.currentTier.id}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return data.cloudaicompanionProject;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Need to onboard
|
|
189
|
+
const tier =
|
|
190
|
+
data.paidTier ||
|
|
191
|
+
data.currentTier ||
|
|
192
|
+
data.availableTiers?.find(
|
|
193
|
+
(t: any) => t.id === "STANDARD" || t.id === "FREE"
|
|
194
|
+
) ||
|
|
195
|
+
data.availableTiers?.[0];
|
|
196
|
+
|
|
197
|
+
if (!tier) {
|
|
198
|
+
console.warn("[gemini-oauth] No tier available for onboarding");
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(
|
|
203
|
+
`[gemini-oauth] Onboarding to tier: ${tier.name || tier.id}...`
|
|
204
|
+
);
|
|
205
|
+
const onboardResp = await fetch(
|
|
206
|
+
`${CA_ENDPOINT}/${CA_VERSION}:onboardUser`,
|
|
207
|
+
{
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
Authorization: `Bearer ${accessToken}`,
|
|
212
|
+
},
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
tierId: tier.id,
|
|
215
|
+
metadata: {
|
|
216
|
+
ideType: "IDE_UNSPECIFIED",
|
|
217
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
218
|
+
pluginType: "GEMINI",
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (!onboardResp.ok) {
|
|
225
|
+
console.warn(
|
|
226
|
+
`[gemini-oauth] Onboard failed (${onboardResp.status}) - will use standard API`
|
|
227
|
+
);
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const onboardData = (await onboardResp.json()) as any;
|
|
232
|
+
|
|
233
|
+
// Handle long-running operation
|
|
234
|
+
if (!onboardData.done && onboardData.name) {
|
|
235
|
+
console.log("[gemini-oauth] Waiting for onboarding to complete...");
|
|
236
|
+
const opName = onboardData.name;
|
|
237
|
+
let attempts = 0;
|
|
238
|
+
while (attempts < 12) {
|
|
239
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
240
|
+
const opResp = await fetch(
|
|
241
|
+
`${CA_ENDPOINT}/${CA_VERSION}/operations/${opName}`,
|
|
242
|
+
{
|
|
243
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
if (!opResp.ok) break;
|
|
247
|
+
const opData = (await opResp.json()) as any;
|
|
248
|
+
if (opData.done) {
|
|
249
|
+
const projectId = opData.response?.cloudaicompanionProject?.id;
|
|
250
|
+
if (projectId) {
|
|
251
|
+
console.log(
|
|
252
|
+
`[gemini-oauth] Onboarded to project: ${projectId}`
|
|
253
|
+
);
|
|
254
|
+
return projectId;
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
attempts++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const projectId = onboardData.response?.cloudaicompanionProject?.id;
|
|
263
|
+
if (projectId) {
|
|
264
|
+
console.log(`[gemini-oauth] Onboarded to project: ${projectId}`);
|
|
265
|
+
return projectId;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return undefined;
|
|
269
|
+
} catch (e: any) {
|
|
270
|
+
console.warn("[gemini-oauth] Code Assist setup error:", e.message);
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Browser opener ─────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
function openBrowser(url: string) {
|
|
278
|
+
try {
|
|
279
|
+
if (process.platform === "darwin") {
|
|
280
|
+
execSync(`open "${url}"`);
|
|
281
|
+
} else if (process.platform === "win32") {
|
|
282
|
+
execSync(`start "" "${url}"`);
|
|
283
|
+
} else {
|
|
284
|
+
execSync(`xdg-open "${url}"`);
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Browser open failed - URL is shown in console anyway
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Standalone login (browser-based OAuth from terminal) ───────────────
|
|
292
|
+
|
|
293
|
+
export async function googleLoginStandalone(): Promise<GoogleTokens> {
|
|
294
|
+
// Find an available port for callback
|
|
295
|
+
const port = await new Promise<number>((resolve, reject) => {
|
|
296
|
+
const srv = http.createServer();
|
|
297
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
298
|
+
const addr = srv.address();
|
|
299
|
+
if (addr && typeof addr === "object") {
|
|
300
|
+
const p = addr.port;
|
|
301
|
+
srv.close(() => resolve(p));
|
|
302
|
+
} else {
|
|
303
|
+
reject(new Error("Could not find available port"));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const redirectUri = `http://127.0.0.1:${port}/oauth2callback`;
|
|
309
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
310
|
+
const { verifier, challenge } = generatePKCE();
|
|
311
|
+
|
|
312
|
+
const params = new URLSearchParams({
|
|
313
|
+
client_id: OAUTH_CLIENT_ID,
|
|
314
|
+
redirect_uri: redirectUri,
|
|
315
|
+
response_type: "code",
|
|
316
|
+
scope: OAUTH_SCOPES.join(" "),
|
|
317
|
+
access_type: "offline",
|
|
318
|
+
prompt: "consent",
|
|
319
|
+
state,
|
|
320
|
+
code_challenge_method: "S256",
|
|
321
|
+
code_challenge: challenge,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const authUrl = `${AUTH_ENDPOINT}?${params}`;
|
|
325
|
+
|
|
326
|
+
return new Promise((resolve, reject) => {
|
|
327
|
+
const timeout = setTimeout(() => {
|
|
328
|
+
server.close();
|
|
329
|
+
reject(new Error("Login timed out after 5 minutes"));
|
|
330
|
+
}, 5 * 60 * 1000);
|
|
331
|
+
|
|
332
|
+
const server = http.createServer(async (req, res) => {
|
|
333
|
+
if (!req.url?.startsWith("/oauth2callback")) {
|
|
334
|
+
res.writeHead(404);
|
|
335
|
+
res.end("Not found");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
341
|
+
const error = url.searchParams.get("error");
|
|
342
|
+
const code = url.searchParams.get("code");
|
|
343
|
+
const returnedState = url.searchParams.get("state");
|
|
344
|
+
|
|
345
|
+
if (error) {
|
|
346
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
347
|
+
res.end(errorPage(`OAuth error: ${error}`));
|
|
348
|
+
clearTimeout(timeout);
|
|
349
|
+
server.close();
|
|
350
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (returnedState !== state) {
|
|
355
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
356
|
+
res.end(errorPage("State mismatch - possible CSRF attack"));
|
|
357
|
+
clearTimeout(timeout);
|
|
358
|
+
server.close();
|
|
359
|
+
reject(new Error("OAuth state mismatch"));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!code) {
|
|
364
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
365
|
+
res.end(errorPage("No authorization code received"));
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
server.close();
|
|
368
|
+
reject(new Error("No authorization code"));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Exchange code for tokens
|
|
373
|
+
const tokenResp = await fetch(TOKEN_ENDPOINT, {
|
|
374
|
+
method: "POST",
|
|
375
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
376
|
+
body: new URLSearchParams({
|
|
377
|
+
client_id: OAUTH_CLIENT_ID,
|
|
378
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
379
|
+
code,
|
|
380
|
+
code_verifier: verifier,
|
|
381
|
+
grant_type: "authorization_code",
|
|
382
|
+
redirect_uri: redirectUri,
|
|
383
|
+
}),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (!tokenResp.ok) {
|
|
387
|
+
const text = await tokenResp.text();
|
|
388
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
389
|
+
res.end(errorPage(`Token exchange failed: ${text}`));
|
|
390
|
+
clearTimeout(timeout);
|
|
391
|
+
server.close();
|
|
392
|
+
reject(new Error(`Token exchange failed: ${text}`));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const tokenData = (await tokenResp.json()) as any;
|
|
397
|
+
|
|
398
|
+
const tokens: GoogleTokens = {
|
|
399
|
+
access_token: tokenData.access_token,
|
|
400
|
+
refresh_token: tokenData.refresh_token,
|
|
401
|
+
expires_at: Date.now() + tokenData.expires_in * 1000,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Fetch user info
|
|
405
|
+
tokens.email = await fetchUserEmail(tokens.access_token);
|
|
406
|
+
|
|
407
|
+
// Setup Code Assist
|
|
408
|
+
tokens.project_id = await setupCodeAssist(tokens.access_token);
|
|
409
|
+
|
|
410
|
+
// Save
|
|
411
|
+
await saveTokens(tokens);
|
|
412
|
+
|
|
413
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
414
|
+
res.end(successPage(tokens.email, tokens.project_id));
|
|
415
|
+
|
|
416
|
+
clearTimeout(timeout);
|
|
417
|
+
server.close();
|
|
418
|
+
resolve(tokens);
|
|
419
|
+
} catch (e: any) {
|
|
420
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
421
|
+
res.end(errorPage(e.message));
|
|
422
|
+
clearTimeout(timeout);
|
|
423
|
+
server.close();
|
|
424
|
+
reject(e);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
server.listen(port, "127.0.0.1", () => {
|
|
429
|
+
console.log(`\n[gemini-oauth] Opening browser for Google login...`);
|
|
430
|
+
console.log(`[gemini-oauth] If browser doesn't open, visit:`);
|
|
431
|
+
console.log(` ${authUrl}\n`);
|
|
432
|
+
openBrowser(authUrl);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Proxy-integrated login (via Fastify routes) ────────────────────────
|
|
438
|
+
|
|
439
|
+
// In-memory pending OAuth state for proxy-integrated login
|
|
440
|
+
let pendingOAuth: {
|
|
441
|
+
state: string;
|
|
442
|
+
verifier: string;
|
|
443
|
+
redirectUri: string;
|
|
444
|
+
} | null = null;
|
|
445
|
+
|
|
446
|
+
/** Build the Google OAuth authorization URL (for proxy-integrated login) */
|
|
447
|
+
export function buildLoginUrl(proxyPort: number): string {
|
|
448
|
+
const redirectUri = `http://127.0.0.1:${proxyPort}/google/callback`;
|
|
449
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
450
|
+
const { verifier, challenge } = generatePKCE();
|
|
451
|
+
|
|
452
|
+
pendingOAuth = { state, verifier, redirectUri };
|
|
453
|
+
|
|
454
|
+
const params = new URLSearchParams({
|
|
455
|
+
client_id: OAUTH_CLIENT_ID,
|
|
456
|
+
redirect_uri: redirectUri,
|
|
457
|
+
response_type: "code",
|
|
458
|
+
scope: OAUTH_SCOPES.join(" "),
|
|
459
|
+
access_type: "offline",
|
|
460
|
+
prompt: "consent",
|
|
461
|
+
state,
|
|
462
|
+
code_challenge_method: "S256",
|
|
463
|
+
code_challenge: challenge,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return `${AUTH_ENDPOINT}?${params}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Handle OAuth callback (for proxy-integrated login) */
|
|
470
|
+
export async function handleOAuthCallback(
|
|
471
|
+
code: string,
|
|
472
|
+
state: string
|
|
473
|
+
): Promise<GoogleTokens> {
|
|
474
|
+
if (!pendingOAuth) {
|
|
475
|
+
throw new Error("No pending OAuth flow. Visit /google/login first.");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (state !== pendingOAuth.state) {
|
|
479
|
+
pendingOAuth = null;
|
|
480
|
+
throw new Error("OAuth state mismatch - possible CSRF attack.");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const { verifier, redirectUri } = pendingOAuth;
|
|
484
|
+
pendingOAuth = null;
|
|
485
|
+
|
|
486
|
+
// Exchange code for tokens
|
|
487
|
+
const tokenResp = await fetch(TOKEN_ENDPOINT, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
490
|
+
body: new URLSearchParams({
|
|
491
|
+
client_id: OAUTH_CLIENT_ID,
|
|
492
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
493
|
+
code,
|
|
494
|
+
code_verifier: verifier,
|
|
495
|
+
grant_type: "authorization_code",
|
|
496
|
+
redirect_uri: redirectUri,
|
|
497
|
+
}),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (!tokenResp.ok) {
|
|
501
|
+
const text = await tokenResp.text();
|
|
502
|
+
throw new Error(`Token exchange failed (${tokenResp.status}): ${text}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const tokenData = (await tokenResp.json()) as any;
|
|
506
|
+
|
|
507
|
+
const tokens: GoogleTokens = {
|
|
508
|
+
access_token: tokenData.access_token,
|
|
509
|
+
refresh_token: tokenData.refresh_token,
|
|
510
|
+
expires_at: Date.now() + tokenData.expires_in * 1000,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Fetch user info
|
|
514
|
+
tokens.email = await fetchUserEmail(tokens.access_token);
|
|
515
|
+
|
|
516
|
+
// Setup Code Assist
|
|
517
|
+
tokens.project_id = await setupCodeAssist(tokens.access_token);
|
|
518
|
+
|
|
519
|
+
// Save
|
|
520
|
+
await saveTokens(tokens);
|
|
521
|
+
|
|
522
|
+
console.log(
|
|
523
|
+
`[gemini-oauth] Login successful! Email: ${tokens.email || "unknown"}, Project: ${tokens.project_id || "none (using standard API)"}`
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
return tokens;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Get login status */
|
|
530
|
+
export async function getLoginStatus(): Promise<{
|
|
531
|
+
loggedIn: boolean;
|
|
532
|
+
email?: string;
|
|
533
|
+
projectId?: string;
|
|
534
|
+
expiresAt?: number;
|
|
535
|
+
mode?: string;
|
|
536
|
+
}> {
|
|
537
|
+
const tokens = await loadTokens();
|
|
538
|
+
if (!tokens) return { loggedIn: false };
|
|
539
|
+
return {
|
|
540
|
+
loggedIn: true,
|
|
541
|
+
email: tokens.email,
|
|
542
|
+
projectId: tokens.project_id,
|
|
543
|
+
expiresAt: tokens.expires_at,
|
|
544
|
+
mode: tokens.project_id ? "code-assist" : "standard-api",
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Logout */
|
|
549
|
+
export async function googleLogout(): Promise<void> {
|
|
550
|
+
try {
|
|
551
|
+
const { unlink } = await import("fs/promises");
|
|
552
|
+
await unlink(AUTH_FILE);
|
|
553
|
+
console.log("[gemini-oauth] Logged out, credentials removed.");
|
|
554
|
+
} catch {
|
|
555
|
+
console.log("[gemini-oauth] Already logged out.");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ── HTML pages ─────────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
function successPage(email?: string, projectId?: string): string {
|
|
562
|
+
return `<!DOCTYPE html>
|
|
563
|
+
<html><head><title>Login Successful</title></head>
|
|
564
|
+
<body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0f172a;">
|
|
565
|
+
<div style="text-align:center;color:#e2e8f0;max-width:500px;">
|
|
566
|
+
<div style="font-size:48px;margin-bottom:16px;">✓</div>
|
|
567
|
+
<h1 style="color:#4ade80;margin:0 0 12px;">Authenticated Successfully</h1>
|
|
568
|
+
<p>Logged in as: <strong>${email || "unknown"}</strong></p>
|
|
569
|
+
${projectId ? `<p>Code Assist Project: <code style="background:#1e293b;padding:2px 8px;border-radius:4px;">${projectId}</code></p>` : `<p style="color:#94a3b8;">No Code Assist project (using standard API)</p>`}
|
|
570
|
+
<p style="color:#64748b;margin-top:24px;">You can close this window and return to your terminal.<br>Use <code style="background:#1e293b;padding:2px 8px;border-radius:4px;">go:gemini-3.1-pro-preview</code> as your model.</p>
|
|
571
|
+
</div>
|
|
572
|
+
</body></html>`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function errorPage(message: string): string {
|
|
576
|
+
return `<!DOCTYPE html>
|
|
577
|
+
<html><head><title>Login Failed</title></head>
|
|
578
|
+
<body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0f172a;">
|
|
579
|
+
<div style="text-align:center;color:#e2e8f0;max-width:500px;">
|
|
580
|
+
<div style="font-size:48px;margin-bottom:16px;">✗</div>
|
|
581
|
+
<h1 style="color:#f87171;margin:0 0 12px;">Authentication Failed</h1>
|
|
582
|
+
<p>${message}</p>
|
|
583
|
+
<p style="color:#64748b;margin-top:24px;">Please try again at <a href="/google/login" style="color:#60a5fa;">/google/login</a></p>
|
|
584
|
+
</div>
|
|
585
|
+
</body></html>`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function loginPage(_proxyPort: number): string {
|
|
589
|
+
return `<!DOCTYPE html>
|
|
590
|
+
<html><head><title>Google Login</title></head>
|
|
591
|
+
<body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0f172a;">
|
|
592
|
+
<div style="text-align:center;color:#e2e8f0;max-width:500px;">
|
|
593
|
+
<h1 style="margin:0 0 12px;">Google Login for Gemini</h1>
|
|
594
|
+
<p style="color:#94a3b8;">Click the button below to authenticate with your Google account.</p>
|
|
595
|
+
<p style="color:#94a3b8;">This uses the same OAuth flow as the official Gemini CLI.</p>
|
|
596
|
+
<a href="/google/login/start" style="display:inline-block;margin-top:24px;padding:12px 32px;background:#4285f4;color:white;text-decoration:none;border-radius:8px;font-size:16px;font-weight:600;">
|
|
597
|
+
Sign in with Google
|
|
598
|
+
</a>
|
|
599
|
+
<p style="color:#475569;margin-top:32px;font-size:14px;">Scopes: cloud-platform, userinfo.email, userinfo.profile</p>
|
|
600
|
+
</div>
|
|
601
|
+
</body></html>`;
|
|
602
|
+
}
|
package/adapters/map.ts
CHANGED
|
@@ -10,6 +10,7 @@ const PROVIDER_PREFIXES: ProviderKey[] = [
|
|
|
10
10
|
"openai",
|
|
11
11
|
"openrouter",
|
|
12
12
|
"gemini",
|
|
13
|
+
"gemini-oauth",
|
|
13
14
|
"glm",
|
|
14
15
|
"anthropic",
|
|
15
16
|
];
|
|
@@ -32,6 +33,15 @@ const MODEL_SHORTCUTS: Record<string, string> = {
|
|
|
32
33
|
opus: "anthropic:claude-opus-4-5-20251101",
|
|
33
34
|
sonnet: "anthropic:claude-sonnet-4-5-20250929",
|
|
34
35
|
haiku: "anthropic:claude-haiku-4-5-20251001",
|
|
36
|
+
// Gemini OAuth shortcuts (Google account login)
|
|
37
|
+
go: "gemini-oauth:gemini-3-pro-preview",
|
|
38
|
+
gp: "gemini-oauth:gemini-3-pro-preview",
|
|
39
|
+
g3: "gemini-oauth:gemini-3-pro-preview",
|
|
40
|
+
"gemini-pro": "gemini-oauth:gemini-3-pro-preview",
|
|
41
|
+
gf: "gemini-oauth:gemini-3-flash-preview",
|
|
42
|
+
"gemini-flash": "gemini-oauth:gemini-3-flash-preview",
|
|
43
|
+
"g25p": "gemini-oauth:gemini-2.5-pro",
|
|
44
|
+
"g25f": "gemini-oauth:gemini-2.5-flash",
|
|
35
45
|
// Add more shortcuts as needed
|
|
36
46
|
};
|
|
37
47
|
|
|
@@ -91,8 +101,8 @@ export function warnIfTools(
|
|
|
91
101
|
provider: ProviderKey,
|
|
92
102
|
): void {
|
|
93
103
|
if (req.tools && req.tools.length > 0) {
|
|
94
|
-
//
|
|
95
|
-
if (provider !== "glm" && provider !== "anthropic") {
|
|
104
|
+
// GLM, Anthropic, and Gemini OAuth support tools natively
|
|
105
|
+
if (provider !== "glm" && provider !== "anthropic" && provider !== "gemini-oauth") {
|
|
96
106
|
console.warn(
|
|
97
107
|
`[proxy] Warning: ${provider} may not fully support Anthropic-style tools. Passing through anyway.`,
|
|
98
108
|
);
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// Gemini OAuth adapter - uses Google OAuth tokens with Code Assist API or standard Generative Language API
|
|
2
|
+
// Supports full tool/function calling and streaming
|
|
3
|
+
|
|
4
|
+
import { FastifyReply } from "fastify";
|
|
5
|
+
import { createParser } from "eventsource-parser";
|
|
6
|
+
import { sendEvent } from "../sse.js";
|
|
7
|
+
import { getAccessToken, loadTokens } from "../google-auth.js";
|
|
8
|
+
import type {
|
|
9
|
+
AnthropicRequest,
|
|
10
|
+
AnthropicMessage,
|
|
11
|
+
AnthropicTool,
|
|
12
|
+
AnthropicContentBlock,
|
|
13
|
+
} from "../types.js";
|
|
14
|
+
import * as crypto from "crypto";
|
|
15
|
+
|
|
16
|
+
// ── Endpoints ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const CA_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
19
|
+
const CA_VERSION = "v1internal";
|
|
20
|
+
const GL_ENDPOINT =
|
|
21
|
+
process.env.GEMINI_BASE_URL ||
|
|
22
|
+
"https://generativelanguage.googleapis.com/v1beta";
|
|
23
|
+
|
|
24
|
+
// ── Format converters: Anthropic → Gemini ──────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Find tool name by tool_use_id in message history */
|
|
27
|
+
function findToolName(messages: AnthropicMessage[], toolUseId: string): string {
|
|
28
|
+
for (const m of messages) {
|
|
29
|
+
if (typeof m.content === "string") continue;
|
|
30
|
+
for (const block of m.content as AnthropicContentBlock[]) {
|
|
31
|
+
if (block.type === "tool_use" && block.id === toolUseId) {
|
|
32
|
+
return block.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return "unknown_tool";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Convert Anthropic content blocks to Gemini parts */
|
|
40
|
+
function toGeminiParts(
|
|
41
|
+
content: AnthropicMessage["content"],
|
|
42
|
+
allMessages: AnthropicMessage[]
|
|
43
|
+
): any[] {
|
|
44
|
+
if (typeof content === "string") {
|
|
45
|
+
return content ? [{ text: content }] : [{ text: " " }];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parts: any[] = [];
|
|
49
|
+
for (const block of content as AnthropicContentBlock[]) {
|
|
50
|
+
if (block.type === "text") {
|
|
51
|
+
if (block.text) parts.push({ text: block.text });
|
|
52
|
+
} else if (block.type === "image") {
|
|
53
|
+
parts.push({
|
|
54
|
+
inlineData: {
|
|
55
|
+
mimeType: block.source.media_type,
|
|
56
|
+
data: block.source.data,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
} else if (block.type === "tool_use") {
|
|
60
|
+
parts.push({
|
|
61
|
+
functionCall: {
|
|
62
|
+
name: block.name,
|
|
63
|
+
args:
|
|
64
|
+
typeof block.input === "string"
|
|
65
|
+
? JSON.parse(block.input)
|
|
66
|
+
: block.input,
|
|
67
|
+
},
|
|
68
|
+
thoughtSignature: "skip_thought_signature_validator",
|
|
69
|
+
});
|
|
70
|
+
} else if (block.type === "tool_result") {
|
|
71
|
+
const functionName = findToolName(allMessages, block.tool_use_id);
|
|
72
|
+
const resultContent =
|
|
73
|
+
typeof block.content === "string"
|
|
74
|
+
? block.content
|
|
75
|
+
: JSON.stringify(block.content);
|
|
76
|
+
parts.push({
|
|
77
|
+
functionResponse: {
|
|
78
|
+
name: functionName,
|
|
79
|
+
response: { content: resultContent },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parts.length > 0 ? parts : [{ text: " " }];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Convert Anthropic messages to Gemini contents array */
|
|
89
|
+
function toGeminiContents(messages: AnthropicMessage[]) {
|
|
90
|
+
// Gemini requires alternating user/model roles
|
|
91
|
+
// Merge consecutive same-role messages
|
|
92
|
+
const merged: { role: string; parts: any[] }[] = [];
|
|
93
|
+
|
|
94
|
+
for (const m of messages) {
|
|
95
|
+
const role = m.role === "assistant" ? "model" : "user";
|
|
96
|
+
const parts = toGeminiParts(m.content, messages);
|
|
97
|
+
|
|
98
|
+
if (merged.length > 0 && merged[merged.length - 1].role === role) {
|
|
99
|
+
// Merge into previous message of same role
|
|
100
|
+
merged[merged.length - 1].parts.push(...parts);
|
|
101
|
+
} else {
|
|
102
|
+
merged.push({ role, parts });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return merged;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whitelist of fields Gemini actually accepts in function declaration schemas */
|
|
110
|
+
const GEMINI_ALLOWED = new Set([
|
|
111
|
+
"type", "properties", "required", "description",
|
|
112
|
+
"enum", "items", "format", "nullable", "title",
|
|
113
|
+
"anyOf", "$ref", "$defs", "$id", "$anchor",
|
|
114
|
+
"minimum", "maximum", "minItems", "maxItems",
|
|
115
|
+
"prefixItems", "additionalProperties", "propertyOrdering",
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
/** Recursively strip JSON Schema fields Gemini doesn't support.
|
|
119
|
+
* `isPropertyMap` = true when we're inside a "properties" object,
|
|
120
|
+
* where keys are user-defined property names (not schema keywords). */
|
|
121
|
+
function sanitizeSchema(obj: any, isPropertyMap = false): any {
|
|
122
|
+
if (obj === null || obj === undefined || typeof obj !== "object") return obj;
|
|
123
|
+
if (Array.isArray(obj)) return obj.map((v) => sanitizeSchema(v, false));
|
|
124
|
+
|
|
125
|
+
const clean: any = {};
|
|
126
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
127
|
+
if (!isPropertyMap && !GEMINI_ALLOWED.has(key)) continue;
|
|
128
|
+
// When key is "properties", its value is a map of name→schema
|
|
129
|
+
clean[key] = sanitizeSchema(val, key === "properties");
|
|
130
|
+
}
|
|
131
|
+
return clean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Convert Anthropic tools to Gemini function declarations */
|
|
135
|
+
function toGeminiTools(tools: AnthropicTool[]) {
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
functionDeclarations: tools.map((t) => ({
|
|
139
|
+
name: t.name,
|
|
140
|
+
description: t.description || "",
|
|
141
|
+
parameters: sanitizeSchema(t.input_schema) || { type: "object", properties: {} },
|
|
142
|
+
})),
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Main adapter ────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
export async function chatGeminiOAuth(
|
|
150
|
+
res: FastifyReply,
|
|
151
|
+
body: AnthropicRequest,
|
|
152
|
+
model: string
|
|
153
|
+
) {
|
|
154
|
+
// Helper to send error as SSE (since headers are already flushed by the gateway)
|
|
155
|
+
function sendSSEError(msg: string) {
|
|
156
|
+
try {
|
|
157
|
+
const id = `msg_${Date.now()}`;
|
|
158
|
+
res.raw.write(`event: message_start\ndata: ${JSON.stringify({ type: "message_start", message: { id, type: "message", role: "assistant", model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } })}\n\n`);
|
|
159
|
+
res.raw.write(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n`);
|
|
160
|
+
res.raw.write(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: `[Gemini OAuth Error] ${msg}` } })}\n\n`);
|
|
161
|
+
res.raw.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: 0 })}\n\n`);
|
|
162
|
+
res.raw.write(`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 0 } })}\n\n`);
|
|
163
|
+
res.raw.write(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
|
|
164
|
+
} catch { /* stream closed */ }
|
|
165
|
+
try { res.raw.end(); } catch {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
return await _chatGeminiOAuthInner(res, body, model);
|
|
170
|
+
} catch (e: any) {
|
|
171
|
+
console.error(`[gemini-oauth] ERROR: ${e.message}`);
|
|
172
|
+
sendSSEError(e.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function _chatGeminiOAuthInner(
|
|
177
|
+
res: FastifyReply,
|
|
178
|
+
body: AnthropicRequest,
|
|
179
|
+
model: string
|
|
180
|
+
) {
|
|
181
|
+
// Get access token (auto-refreshes if expired)
|
|
182
|
+
const accessToken = await getAccessToken();
|
|
183
|
+
const tokens = await loadTokens();
|
|
184
|
+
const projectId = tokens?.project_id;
|
|
185
|
+
|
|
186
|
+
// Build Gemini request
|
|
187
|
+
const contents = toGeminiContents(body.messages);
|
|
188
|
+
|
|
189
|
+
// Generation config matching Gemini CLI defaults for chat
|
|
190
|
+
const generationConfig: any = {
|
|
191
|
+
temperature: body.temperature ?? 1,
|
|
192
|
+
topP: 0.95,
|
|
193
|
+
topK: 64,
|
|
194
|
+
thinkingConfig: { includeThoughts: true },
|
|
195
|
+
};
|
|
196
|
+
if (body.max_tokens !== undefined) {
|
|
197
|
+
generationConfig.maxOutputTokens = body.max_tokens;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Tools
|
|
201
|
+
const tools =
|
|
202
|
+
body.tools && body.tools.length > 0 ? toGeminiTools(body.tools) : undefined;
|
|
203
|
+
|
|
204
|
+
if (tools) {
|
|
205
|
+
console.log(
|
|
206
|
+
`[gemini-oauth] Sending ${body.tools!.length} tools as Gemini function declarations`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let url: string;
|
|
211
|
+
let reqBody: any;
|
|
212
|
+
|
|
213
|
+
if (projectId) {
|
|
214
|
+
// Code Assist API - systemInstruction has a different protobuf schema,
|
|
215
|
+
// so prepend system prompt to first user message instead
|
|
216
|
+
if (body.system) {
|
|
217
|
+
if (contents.length > 0 && contents[0].role === "user") {
|
|
218
|
+
contents[0].parts.unshift({ text: `[System Instructions]\n${body.system}\n[End System Instructions]\n\n` });
|
|
219
|
+
} else {
|
|
220
|
+
contents.unshift({
|
|
221
|
+
role: "user",
|
|
222
|
+
parts: [{ text: `[System Instructions]\n${body.system}\n[End System Instructions]` }],
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
url = `${CA_ENDPOINT}/${CA_VERSION}:streamGenerateContent?alt=sse`;
|
|
228
|
+
reqBody = {
|
|
229
|
+
model,
|
|
230
|
+
project: projectId,
|
|
231
|
+
user_prompt_id: crypto.randomUUID(),
|
|
232
|
+
request: {
|
|
233
|
+
contents,
|
|
234
|
+
generationConfig,
|
|
235
|
+
...(tools && { tools }),
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
console.log(
|
|
239
|
+
`[gemini-oauth] Code Assist API | model="${model}" project="${projectId}"`
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
// Standard Generative Language API - systemInstruction works natively
|
|
243
|
+
const systemInstruction = body.system
|
|
244
|
+
? { role: "user", parts: [{ text: body.system }] }
|
|
245
|
+
: undefined;
|
|
246
|
+
|
|
247
|
+
url = `${GL_ENDPOINT}/models/${encodeURIComponent(model)}:streamGenerateContent?alt=sse`;
|
|
248
|
+
reqBody = {
|
|
249
|
+
contents,
|
|
250
|
+
...(systemInstruction && { systemInstruction }),
|
|
251
|
+
generationConfig,
|
|
252
|
+
...(tools && { tools }),
|
|
253
|
+
};
|
|
254
|
+
console.log(`[gemini-oauth] Standard API | model="${model}"`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Log outgoing request for debugging
|
|
258
|
+
const reqJson = JSON.stringify(reqBody);
|
|
259
|
+
console.log(`[gemini-oauth] REQUEST URL: ${url}`);
|
|
260
|
+
console.log(`[gemini-oauth] REQUEST BODY (${reqJson.length} bytes): ${reqJson.slice(0, 500)}...`);
|
|
261
|
+
|
|
262
|
+
const resp = await fetch(url, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: {
|
|
265
|
+
"Content-Type": "application/json",
|
|
266
|
+
Authorization: `Bearer ${accessToken}`,
|
|
267
|
+
// Required headers matching Gemini CLI client identity
|
|
268
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
269
|
+
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
270
|
+
"Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
|
|
271
|
+
},
|
|
272
|
+
body: reqJson,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (!resp.ok || !resp.body) {
|
|
276
|
+
const text = await safeText(resp);
|
|
277
|
+
console.error(`[gemini-oauth] API error ${resp.status}: ${text}`);
|
|
278
|
+
throw new Error(`Gemini API returned ${resp.status}: ${text.slice(0, 300)}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Stream response and convert to Anthropic SSE format ──────────────
|
|
282
|
+
|
|
283
|
+
const msgId = `msg_${Date.now()}`;
|
|
284
|
+
let contentIndex = 0;
|
|
285
|
+
let hasStartedMessage = false;
|
|
286
|
+
let hasStartedThinking = false;
|
|
287
|
+
let hasStartedContent = false;
|
|
288
|
+
|
|
289
|
+
// Accumulate function calls from streaming chunks
|
|
290
|
+
const pendingToolCalls: { id: string; name: string; args: any }[] = [];
|
|
291
|
+
|
|
292
|
+
function ensureMessageStarted() {
|
|
293
|
+
if (!hasStartedMessage) {
|
|
294
|
+
hasStartedMessage = true;
|
|
295
|
+
sendEvent(res, "message_start", {
|
|
296
|
+
type: "message_start",
|
|
297
|
+
message: {
|
|
298
|
+
id: msgId,
|
|
299
|
+
type: "message",
|
|
300
|
+
role: "assistant",
|
|
301
|
+
model,
|
|
302
|
+
content: [],
|
|
303
|
+
stop_reason: null,
|
|
304
|
+
stop_sequence: null,
|
|
305
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function ensureThinkingBlockStarted() {
|
|
312
|
+
if (!hasStartedThinking) {
|
|
313
|
+
hasStartedThinking = true;
|
|
314
|
+
ensureMessageStarted();
|
|
315
|
+
sendEvent(res, "content_block_start", {
|
|
316
|
+
type: "content_block_start",
|
|
317
|
+
index: contentIndex,
|
|
318
|
+
content_block: { type: "thinking", thinking: "" },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function closeThinkingBlock() {
|
|
324
|
+
if (hasStartedThinking) {
|
|
325
|
+
sendEvent(res, "content_block_stop", {
|
|
326
|
+
type: "content_block_stop",
|
|
327
|
+
index: contentIndex,
|
|
328
|
+
});
|
|
329
|
+
contentIndex++;
|
|
330
|
+
hasStartedThinking = false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function ensureContentBlockStarted() {
|
|
335
|
+
if (!hasStartedContent) {
|
|
336
|
+
closeThinkingBlock();
|
|
337
|
+
hasStartedContent = true;
|
|
338
|
+
ensureMessageStarted();
|
|
339
|
+
sendEvent(res, "content_block_start", {
|
|
340
|
+
type: "content_block_start",
|
|
341
|
+
index: contentIndex,
|
|
342
|
+
content_block: { type: "text", text: "" },
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function closeContentBlock() {
|
|
348
|
+
if (hasStartedContent) {
|
|
349
|
+
sendEvent(res, "content_block_stop", {
|
|
350
|
+
type: "content_block_stop",
|
|
351
|
+
index: contentIndex,
|
|
352
|
+
});
|
|
353
|
+
contentIndex++;
|
|
354
|
+
hasStartedContent = false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const reader = resp.body.getReader();
|
|
359
|
+
const decoder = new TextDecoder();
|
|
360
|
+
const parser = createParser({ onEvent: (event: any) => {
|
|
361
|
+
const data = event.data;
|
|
362
|
+
if (!data) return;
|
|
363
|
+
try {
|
|
364
|
+
const json = JSON.parse(data);
|
|
365
|
+
|
|
366
|
+
// Handle both Code Assist (wrapped) and standard API (unwrapped) responses
|
|
367
|
+
const candidateData = json.response || json;
|
|
368
|
+
const candidate = candidateData?.candidates?.[0];
|
|
369
|
+
if (!candidate?.content?.parts) return;
|
|
370
|
+
|
|
371
|
+
for (const part of candidate.content.parts) {
|
|
372
|
+
// Handle thinking/reasoning (Gemini 2.5+ models)
|
|
373
|
+
if (part.thought === true && part.text) {
|
|
374
|
+
ensureThinkingBlockStarted();
|
|
375
|
+
sendEvent(res, "content_block_delta", {
|
|
376
|
+
type: "content_block_delta",
|
|
377
|
+
index: contentIndex,
|
|
378
|
+
delta: { type: "thinking_delta", thinking: part.text },
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
// Handle regular text
|
|
382
|
+
else if (part.text && part.thought !== true) {
|
|
383
|
+
ensureContentBlockStarted();
|
|
384
|
+
sendEvent(res, "content_block_delta", {
|
|
385
|
+
type: "content_block_delta",
|
|
386
|
+
index: contentIndex,
|
|
387
|
+
delta: { type: "text_delta", text: part.text },
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Handle function calls
|
|
392
|
+
if (part.functionCall) {
|
|
393
|
+
pendingToolCalls.push({
|
|
394
|
+
id: `toolu_${crypto.randomBytes(12).toString("hex")}`,
|
|
395
|
+
name: part.functionCall.name,
|
|
396
|
+
args: part.functionCall.args || {},
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch {
|
|
401
|
+
// ignore parse errors in SSE stream
|
|
402
|
+
}
|
|
403
|
+
}});
|
|
404
|
+
|
|
405
|
+
while (true) {
|
|
406
|
+
const { value, done } = await reader.read();
|
|
407
|
+
if (done) break;
|
|
408
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Finalize: close blocks and emit tool_use if any ──────────────────
|
|
412
|
+
|
|
413
|
+
ensureMessageStarted();
|
|
414
|
+
closeThinkingBlock();
|
|
415
|
+
closeContentBlock();
|
|
416
|
+
|
|
417
|
+
// Emit tool_use content blocks
|
|
418
|
+
if (pendingToolCalls.length > 0) {
|
|
419
|
+
for (const tc of pendingToolCalls) {
|
|
420
|
+
sendEvent(res, "content_block_start", {
|
|
421
|
+
type: "content_block_start",
|
|
422
|
+
index: contentIndex,
|
|
423
|
+
content_block: {
|
|
424
|
+
type: "tool_use",
|
|
425
|
+
id: tc.id,
|
|
426
|
+
name: tc.name,
|
|
427
|
+
input: {},
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
sendEvent(res, "content_block_delta", {
|
|
432
|
+
type: "content_block_delta",
|
|
433
|
+
index: contentIndex,
|
|
434
|
+
delta: {
|
|
435
|
+
type: "input_json_delta",
|
|
436
|
+
partial_json: JSON.stringify(tc.args),
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
sendEvent(res, "content_block_stop", {
|
|
441
|
+
type: "content_block_stop",
|
|
442
|
+
index: contentIndex,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
contentIndex++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Stop reason
|
|
450
|
+
const stopReason = pendingToolCalls.length > 0 ? "tool_use" : "end_turn";
|
|
451
|
+
|
|
452
|
+
sendEvent(res, "message_delta", {
|
|
453
|
+
type: "message_delta",
|
|
454
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
455
|
+
usage: { output_tokens: 0 },
|
|
456
|
+
});
|
|
457
|
+
sendEvent(res, "message_stop", { type: "message_stop" });
|
|
458
|
+
|
|
459
|
+
res.raw.end();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
async function safeText(resp: Response) {
|
|
465
|
+
try {
|
|
466
|
+
return await resp.text();
|
|
467
|
+
} catch {
|
|
468
|
+
return "<no-body>";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
@@ -47,8 +47,7 @@ export async function chatGemini(
|
|
|
47
47
|
|
|
48
48
|
const reader = resp.body.getReader();
|
|
49
49
|
const decoder = new TextDecoder();
|
|
50
|
-
const parser = createParser((event) => {
|
|
51
|
-
if (event.type !== "event") return;
|
|
50
|
+
const parser = createParser({ onEvent: (event: any) => {
|
|
52
51
|
const data = event.data;
|
|
53
52
|
if (!data) return;
|
|
54
53
|
try {
|
|
@@ -62,7 +61,7 @@ export async function chatGemini(
|
|
|
62
61
|
} catch {
|
|
63
62
|
// ignore parse errors
|
|
64
63
|
}
|
|
65
|
-
});
|
|
64
|
+
}});
|
|
66
65
|
|
|
67
66
|
while (true) {
|
|
68
67
|
const { value, done } = await reader.read();
|
|
@@ -52,8 +52,7 @@ export async function chatOpenAI(
|
|
|
52
52
|
|
|
53
53
|
const reader = resp.body.getReader();
|
|
54
54
|
const decoder = new TextDecoder();
|
|
55
|
-
const parser = createParser((event) => {
|
|
56
|
-
if (event.type !== "event") return;
|
|
55
|
+
const parser = createParser({ onEvent: (event: any) => {
|
|
57
56
|
const data = event.data;
|
|
58
57
|
if (!data || data === "[DONE]") return;
|
|
59
58
|
try {
|
|
@@ -63,7 +62,7 @@ export async function chatOpenAI(
|
|
|
63
62
|
} catch {
|
|
64
63
|
// ignore parse errors on keepalives, etc.
|
|
65
64
|
}
|
|
66
|
-
});
|
|
65
|
+
}});
|
|
67
66
|
|
|
68
67
|
while (true) {
|
|
69
68
|
const { value, done } = await reader.read();
|
|
@@ -219,8 +219,7 @@ export async function chatOpenRouter(
|
|
|
219
219
|
|
|
220
220
|
const reader = resp.body.getReader();
|
|
221
221
|
const decoder = new TextDecoder();
|
|
222
|
-
const parser = createParser((event) => {
|
|
223
|
-
if (event.type !== "event") return;
|
|
222
|
+
const parser = createParser({ onEvent: (event: any) => {
|
|
224
223
|
const data = event.data;
|
|
225
224
|
if (!data || data === "[DONE]") return;
|
|
226
225
|
try {
|
|
@@ -268,7 +267,7 @@ export async function chatOpenRouter(
|
|
|
268
267
|
} catch {
|
|
269
268
|
// ignore parse errors
|
|
270
269
|
}
|
|
271
|
-
});
|
|
270
|
+
}});
|
|
272
271
|
|
|
273
272
|
while (true) {
|
|
274
273
|
const { value, done } = await reader.read();
|
package/adapters/types.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type AnthropicRequest = {
|
|
|
28
28
|
system?: string;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export type ProviderKey = "openai" | "openrouter" | "gemini" | "glm" | "anthropic";
|
|
31
|
+
export type ProviderKey = "openai" | "openrouter" | "gemini" | "gemini-oauth" | "glm" | "anthropic";
|
|
32
32
|
|
|
33
33
|
export type ProviderModel = {
|
|
34
34
|
provider: ProviderKey;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-glm",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Cross-platform installer for Claude Code with Z.AI GLM models, multi-provider proxy, and dangerously-skip-permissions shortcuts. Run with: npx claude-glm",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Cross-platform installer for Claude Code with Z.AI GLM models, multi-provider proxy, Gemini OAuth, and dangerously-skip-permissions shortcuts. Run with: npx claude-glm",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
7
7
|
"claude-code",
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"openai",
|
|
11
11
|
"openrouter",
|
|
12
12
|
"gemini",
|
|
13
|
+
"gemini-oauth",
|
|
14
|
+
"google",
|
|
13
15
|
"ai",
|
|
14
16
|
"llm",
|
|
15
17
|
"installer",
|
|
@@ -48,13 +50,14 @@
|
|
|
48
50
|
"node": ">=18.0.0"
|
|
49
51
|
},
|
|
50
52
|
"dependencies": {
|
|
51
|
-
"dotenv": "^16.
|
|
52
|
-
"eventsource-parser": "^
|
|
53
|
-
"fastify": "^4.
|
|
53
|
+
"dotenv": "^16.6.1",
|
|
54
|
+
"eventsource-parser": "^3.0.0",
|
|
55
|
+
"fastify": "^4.29.1"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
56
58
|
"@types/node": "^20.14.0",
|
|
57
|
-
"tsx": "^4.
|
|
59
|
+
"tsx": "^4.21.0",
|
|
58
60
|
"typescript": "^5.6.3"
|
|
59
|
-
}
|
|
61
|
+
},
|
|
62
|
+
"main": "index.js"
|
|
60
63
|
}
|