opencode-google-auth 0.0.1

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.
@@ -0,0 +1,378 @@
1
+ import type { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"
2
+ import { Context, Effect, pipe } from "effect"
3
+ import type {
4
+ ModelsDev,
5
+ OpenCodeModel,
6
+ OpenCodeProvider,
7
+ Provider,
8
+ } from "../../types"
9
+
10
+ export interface WrappedBody {
11
+ readonly project: string
12
+ readonly request: unknown
13
+ readonly model: string
14
+ }
15
+
16
+ export interface RequestContext {
17
+ readonly body: WrappedBody
18
+ readonly headers: Headers
19
+ readonly url: URL
20
+ }
21
+
22
+ export interface ProviderConfigShape {
23
+ readonly SERVICE_NAME: string
24
+ readonly DISPLAY_NAME: string
25
+ readonly ENDPOINTS: readonly string[]
26
+ readonly HEADERS: Readonly<Record<string, string>>
27
+ readonly SCOPES: readonly string[]
28
+ readonly CLIENT_ID: string
29
+ readonly CLIENT_SECRET: string
30
+ readonly getConfig: (modelsDev: ModelsDev) => OpenCodeProvider
31
+ readonly transformRequest?: (context: RequestContext) => Effect.Effect<{
32
+ body: Record<string, unknown>
33
+ headers: Headers
34
+ url: URL
35
+ }>
36
+ }
37
+
38
+ export class ProviderConfig extends Context.Tag("ProviderConfig")<
39
+ ProviderConfig,
40
+ ProviderConfigShape
41
+ >() {}
42
+
43
+ export const CODE_ASSIST_VERSION = "v1internal"
44
+
45
+ export const CLIENT_METADATA = {
46
+ ideType: "IDE_UNSPECIFIED",
47
+ platform: "PLATFORM_UNSPECIFIED",
48
+ pluginType: "GEMINI",
49
+ } as const
50
+
51
+ export const GEMINI_CLI_MODELS = [
52
+ "gemini-2.5-pro",
53
+ "gemini-2.5-flash",
54
+ "gemini-2.5-flash-lite",
55
+ "gemini-3-pro-preview",
56
+ "gemini-3-flash-preview",
57
+ ] as const
58
+
59
+ export const ANTIGRAVITY_MODELS = [
60
+ "gemini-3-flash",
61
+ "gemini-3-pro-low",
62
+ "gemini-3-pro-high",
63
+ "claude-sonnet-4-5",
64
+ "claude-sonnet-4-5-thinking",
65
+ "claude-opus-4-5-thinking",
66
+ ] as const
67
+
68
+ export const geminiCliConfig = (): ProviderConfigShape => ({
69
+ SERVICE_NAME: "gemini-cli",
70
+ DISPLAY_NAME: "Gemini CLI",
71
+ CLIENT_ID:
72
+ "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
73
+ CLIENT_SECRET: "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",
74
+ SCOPES: [
75
+ "https://www.googleapis.com/auth/cloud-platform",
76
+ "https://www.googleapis.com/auth/userinfo.email",
77
+ "https://www.googleapis.com/auth/userinfo.profile",
78
+ ],
79
+ ENDPOINTS: ["https://cloudcode-pa.googleapis.com"],
80
+ HEADERS: {
81
+ "User-Agent": "google-api-nodejs-client/9.15.1",
82
+ "X-Goog-Api-Client": "gl-node/22.17.0",
83
+ "Client-Metadata":
84
+ "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
85
+ },
86
+ getConfig: (modelsDev) => {
87
+ const provider = modelsDev.google as Provider
88
+ const filteredModels = pipe(
89
+ provider.models,
90
+ (models) => Object.entries(models),
91
+ (entries) =>
92
+ entries.filter(([key]) =>
93
+ (GEMINI_CLI_MODELS as readonly string[]).includes(key),
94
+ ),
95
+ (filtered) => Object.fromEntries(filtered),
96
+ )
97
+
98
+ return {
99
+ ...provider,
100
+ id: geminiCliConfig().SERVICE_NAME,
101
+ name: geminiCliConfig().DISPLAY_NAME,
102
+ api: geminiCliConfig().ENDPOINTS.at(0) as string,
103
+ models: filteredModels as Record<string, OpenCodeModel>,
104
+ }
105
+ },
106
+ })
107
+
108
+ export const antigravityConfig = (): ProviderConfigShape => ({
109
+ SERVICE_NAME: "antigravity",
110
+ DISPLAY_NAME: "Antigravity",
111
+ CLIENT_ID:
112
+ "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
113
+ CLIENT_SECRET: "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",
114
+ SCOPES: [
115
+ "https://www.googleapis.com/auth/cloud-platform",
116
+ "https://www.googleapis.com/auth/userinfo.email",
117
+ "https://www.googleapis.com/auth/userinfo.profile",
118
+ "https://www.googleapis.com/auth/cclog",
119
+ "https://www.googleapis.com/auth/experimentsandconfigs",
120
+ ],
121
+ ENDPOINTS: [
122
+ "https://daily-cloudcode-pa.sandbox.googleapis.com",
123
+ "https://autopush-cloudcode-pa.sandbox.googleapis.com",
124
+ "https://cloudcode-pa.googleapis.com",
125
+ ],
126
+ HEADERS: {
127
+ "User-Agent": "antigravity/1.11.5 windows/amd64",
128
+ "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
129
+ "Client-Metadata":
130
+ '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
131
+ },
132
+ getConfig: (modelsDev) => {
133
+ const googleProvider = modelsDev.google as Provider
134
+ const googleVertextProvider = modelsDev[
135
+ "google-vertex-anthropic"
136
+ ] as Provider
137
+
138
+ const geminiFlash = googleProvider.models[
139
+ "gemini-3-flash-preview"
140
+ ] as OpenCodeModel
141
+ const geminiPro = googleProvider.models[
142
+ "gemini-3-pro-preview"
143
+ ] as OpenCodeModel
144
+ const claudeSonnet = googleVertextProvider.models[
145
+ "claude-sonnet-4-5@20250929"
146
+ ] as OpenCodeModel
147
+ const claudeOpus = googleVertextProvider.models[
148
+ "claude-opus-4-5@20251101"
149
+ ] as OpenCodeModel
150
+
151
+ const models: Record<string, OpenCodeModel> = {
152
+ "gemini-3-flash": {
153
+ ...geminiFlash,
154
+ id: "gemini-3-flash",
155
+ },
156
+ "gemini-3-pro-low": {
157
+ ...geminiPro,
158
+ id: "gemini-3-pro-low",
159
+ name: "Gemini 3 Pro (Low)",
160
+ temperature: false,
161
+ options: {
162
+ thinkingConfig: {
163
+ thinkingLevel: "low",
164
+ },
165
+ } satisfies GoogleGenerativeAIProviderOptions,
166
+ },
167
+ "gemini-3-pro-high": {
168
+ ...geminiPro,
169
+ id: "gemini-3-pro-high",
170
+ name: "Gemini 3 Pro (High)",
171
+ temperature: false,
172
+ options: {
173
+ thinkingConfig: {
174
+ thinkingLevel: "high",
175
+ },
176
+ } satisfies GoogleGenerativeAIProviderOptions,
177
+ },
178
+ "claude-sonnet-4-5": {
179
+ ...claudeSonnet,
180
+ id: "claude-sonnet-4-5",
181
+ reasoning: false,
182
+ options: {
183
+ thinkingConfig: {
184
+ includeThoughts: false,
185
+ },
186
+ } satisfies GoogleGenerativeAIProviderOptions,
187
+ },
188
+ "claude-sonnet-4-5-thinking": {
189
+ ...claudeSonnet,
190
+ id: "claude-sonnet-4-5-thinking",
191
+ name: "Claude Sonnet 4.5 (Reasoning)",
192
+ },
193
+ "claude-opus-4-5-thinking": {
194
+ ...claudeOpus,
195
+ id: "claude-opus-4-5-thinking",
196
+ name: "Claude Opus 4.5 (Reasoning)",
197
+ },
198
+ }
199
+
200
+ return {
201
+ ...googleProvider,
202
+ id: antigravityConfig().SERVICE_NAME,
203
+ name: antigravityConfig().DISPLAY_NAME,
204
+ api: antigravityConfig().ENDPOINTS.at(2) as string,
205
+ models,
206
+ }
207
+ },
208
+ transformRequest: Effect.fn(function* (context) {
209
+ yield* Effect.log(
210
+ "Transforming request for: ",
211
+ antigravityConfig().SERVICE_NAME,
212
+ )
213
+
214
+ const { body, headers, url } = context
215
+ const innerRequest = body.request as Record<string, unknown>
216
+
217
+ let sessionId: string | undefined
218
+ if (
219
+ innerRequest.labels
220
+ && typeof innerRequest.labels === "object"
221
+ && "sessionId" in innerRequest.labels
222
+ ) {
223
+ const labels = innerRequest.labels as Record<string, unknown>
224
+ sessionId = labels.sessionId as string
225
+ delete labels.sessionId
226
+ if (Object.keys(labels).length === 0) {
227
+ delete innerRequest.labels
228
+ }
229
+ }
230
+
231
+ // Handle thinkingConfig for Claude models
232
+ const isClaude = body.model.toLowerCase().includes("claude")
233
+ const isThinking = body.model.toLowerCase().includes("thinking")
234
+
235
+ if (isClaude && body.request && typeof body.request === "object") {
236
+ const request = body.request as Record<string, unknown>
237
+ const generationConfig = request.generationConfig as
238
+ | Record<string, unknown>
239
+ | undefined
240
+
241
+ innerRequest.toolConfig = {
242
+ functionCallingConfig: {
243
+ mode: "VALIDATED",
244
+ },
245
+ }
246
+
247
+ // For non-thinking Claude, remove thinkingConfig entirely
248
+ if (!isThinking && generationConfig?.thinkingConfig) {
249
+ delete generationConfig.thinkingConfig
250
+ }
251
+
252
+ // For thinking Claude, convert camelCase to snake_case and add default budget
253
+ if (isThinking && generationConfig?.thinkingConfig) {
254
+ const thinkingConfig = generationConfig.thinkingConfig as Record<
255
+ string,
256
+ unknown
257
+ >
258
+
259
+ if (thinkingConfig.includeThoughts !== undefined) {
260
+ thinkingConfig.include_thoughts = thinkingConfig.includeThoughts
261
+ delete thinkingConfig.includeThoughts
262
+ }
263
+
264
+ if (thinkingConfig.thinkingBudget !== undefined) {
265
+ thinkingConfig.thinking_budget = thinkingConfig.thinkingBudget
266
+ delete thinkingConfig.thinkingBudget
267
+ }
268
+
269
+ // Add default thinking_budget if not present (required for Claude thinking)
270
+ if (thinkingConfig.thinking_budget === undefined) {
271
+ thinkingConfig.thinking_budget = 32768 // Default to high tier
272
+ }
273
+ }
274
+
275
+ if (isThinking) {
276
+ headers.set("anthropic-beta", "interleaved-thinking-2025-05-14")
277
+ }
278
+ }
279
+
280
+ if (sessionId) {
281
+ const hashedSession = yield* Effect.promise(() => hash(sessionId))
282
+
283
+ const finalSessionId = [
284
+ `-${crypto.randomUUID()}`,
285
+ body.model,
286
+ body.project,
287
+ `seed-${hashedSession}`,
288
+ ].join(":")
289
+
290
+ innerRequest.sessionId = finalSessionId
291
+ }
292
+
293
+ if (innerRequest.tools && Array.isArray(innerRequest.tools)) {
294
+ const tools = innerRequest.tools as Array<Record<string, unknown>>
295
+ for (const tool of tools) {
296
+ if (
297
+ tool.functionDeclarations
298
+ && Array.isArray(tool.functionDeclarations)
299
+ ) {
300
+ const functionDeclarations = tool.functionDeclarations as Array<
301
+ Record<string, unknown>
302
+ >
303
+ for (let i = 0; i < functionDeclarations.length; i++) {
304
+ const declaration = functionDeclarations[i]
305
+ if (declaration && declaration.name === "todoread") {
306
+ functionDeclarations[i] = {
307
+ ...functionDeclarations[i],
308
+ parameters: {
309
+ type: "object",
310
+ properties: {
311
+ _placeholder: {
312
+ type: "boolean",
313
+ description: "Placeholder. Always pass true.",
314
+ },
315
+ },
316
+ required: ["_placeholder"],
317
+ additionalProperties: false,
318
+ },
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ // if (
327
+ // innerRequest.systemInstruction
328
+ // && typeof innerRequest.systemInstruction === "object"
329
+ // ) {
330
+ // const systemInstruction = innerRequest.systemInstruction as Record<
331
+ // string,
332
+ // unknown
333
+ // >
334
+
335
+ // if (systemInstruction.parts && Array.isArray(systemInstruction.parts)) {
336
+ // let parts = systemInstruction.parts as Array<{ text: string }>
337
+
338
+ // parts.unshift({
339
+ // text: "You are Antigravity, a powerful agentic AI coding assistant designed by the Google DeepMind team working on Advanced Agentic Coding.",
340
+ // })
341
+ // }
342
+ // }
343
+
344
+ if (
345
+ innerRequest.systemInstruction
346
+ && typeof innerRequest.systemInstruction === "object"
347
+ ) {
348
+ const systemInstruction = innerRequest.systemInstruction as Record<
349
+ string,
350
+ unknown
351
+ >
352
+
353
+ systemInstruction.role = "user"
354
+ }
355
+
356
+ return {
357
+ headers,
358
+ url,
359
+ body: {
360
+ ...body,
361
+ requestType: "agent",
362
+ userAgent: "antigravity",
363
+ requestId: `agent-${crypto.randomUUID()}`,
364
+ },
365
+ }
366
+ }),
367
+ })
368
+
369
+ async function hash(str: string) {
370
+ const encoder = new TextEncoder()
371
+ const data = encoder.encode(str)
372
+
373
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data)
374
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
375
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
376
+
377
+ return hashHex.slice(0, 16)
378
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * OAuth service
3
+ *
4
+ * Handles initial OAuth authentication flow only.
5
+ * Token refresh is handled by the Session service.
6
+ */
7
+
8
+ import {
9
+ HttpRouter,
10
+ HttpServer,
11
+ HttpServerRequest,
12
+ HttpServerResponse,
13
+ } from "@effect/platform"
14
+ import { BunHttpServer } from "@effect/platform-bun"
15
+ import { Data, Deferred, Effect, Fiber, Schema } from "effect"
16
+ import { OAuth2Client } from "google-auth-library"
17
+ import type { BunServeOptions } from "../../types"
18
+ import { ProviderConfig } from "./config"
19
+
20
+ export class OAuthError extends Data.TaggedError("OAuthError")<{
21
+ readonly reason: "browser" | "callback" | "state_mismatch" | "token_exchange"
22
+ readonly message: string
23
+ readonly cause?: unknown
24
+ }> {}
25
+
26
+ const SuccessParamsSchema = Schema.Struct({
27
+ code: Schema.String,
28
+ state: Schema.String,
29
+ })
30
+
31
+ const FailureParamsSchema = Schema.Struct({
32
+ error: Schema.String,
33
+ error_description: Schema.optional(Schema.String),
34
+ state: Schema.optional(Schema.String),
35
+ })
36
+
37
+ const isFailureParams = Schema.is(FailureParamsSchema)
38
+
39
+ const ParamsSchema = Schema.Union(SuccessParamsSchema, FailureParamsSchema)
40
+
41
+ class OAuth extends Effect.Service<OAuth>()("OAuth", {
42
+ effect: Effect.gen(function* () {
43
+ const config = yield* ProviderConfig
44
+
45
+ const client = new OAuth2Client({
46
+ clientId: config.CLIENT_ID,
47
+ clientSecret: config.CLIENT_SECRET,
48
+ })
49
+ const serverOptions: BunServeOptions = { port: 0 }
50
+ const ServerLive = BunHttpServer.layerServer(serverOptions)
51
+
52
+ const authenticate = Effect.gen(function* () {
53
+ yield* HttpServer.logAddress
54
+
55
+ const deferredParams = yield* Deferred.make<
56
+ typeof SuccessParamsSchema.Type,
57
+ OAuthError
58
+ >()
59
+
60
+ const redirectUri = yield* HttpServer.addressFormattedWith((address) =>
61
+ Effect.succeed(`${address}/oauth2callback`),
62
+ )
63
+ const state = crypto.randomUUID()
64
+
65
+ const authUrl = client.generateAuthUrl({
66
+ state,
67
+ redirect_uri: redirectUri,
68
+ access_type: "offline",
69
+ scope: config.SCOPES as unknown as string[],
70
+ prompt: "consent",
71
+ })
72
+ yield* Effect.log(`OAuth2 authorization URL: ${authUrl}`)
73
+
74
+ const serverFiber = yield* HttpRouter.empty.pipe(
75
+ HttpRouter.get(
76
+ "/oauth2callback",
77
+ Effect.gen(function* () {
78
+ const params =
79
+ yield* HttpServerRequest.schemaSearchParams(ParamsSchema)
80
+
81
+ if (isFailureParams(params)) {
82
+ yield* Deferred.fail(
83
+ deferredParams,
84
+ new OAuthError({
85
+ reason: "callback",
86
+ message: `${params.error} - ${params.error_description ?? "No additional details provided"}`,
87
+ }),
88
+ )
89
+ } else {
90
+ yield* Deferred.succeed(deferredParams, params)
91
+ }
92
+
93
+ return yield* HttpServerResponse.text("You may now close this tab.")
94
+ }).pipe(Effect.tapError(Effect.logError)),
95
+ ),
96
+ HttpServer.serveEffect(),
97
+ Effect.fork,
98
+ )
99
+
100
+ yield* Effect.log("Started OAuth2 callback server")
101
+
102
+ const search = yield* Deferred.await(deferredParams)
103
+ yield* Effect.log("Received OAuth2 callback with params", search)
104
+
105
+ yield* Fiber.interrupt(serverFiber)
106
+
107
+ if (state !== search.state) {
108
+ return yield* new OAuthError({
109
+ reason: "state_mismatch",
110
+ message: "Invalid state parameter. Possible CSRF attack.",
111
+ })
112
+ }
113
+
114
+ const result = yield* Effect.tryPromise({
115
+ try: () =>
116
+ client.getToken({
117
+ code: search.code,
118
+ redirect_uri: redirectUri,
119
+ }),
120
+ catch: (cause) =>
121
+ new OAuthError({
122
+ reason: "token_exchange",
123
+ message: "Failed to exchange authorization code for tokens",
124
+ cause,
125
+ }),
126
+ })
127
+
128
+ return result.tokens
129
+ }).pipe(Effect.provide(ServerLive), Effect.scoped)
130
+
131
+ return { authenticate }
132
+ }),
133
+ }) {}
134
+
135
+ export { OAuth }
@@ -0,0 +1,7 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin"
2
+ import { Context } from "effect"
3
+
4
+ export class OpenCodeContext extends Context.Tag("OpenCodeContext")<
5
+ OpenCodeContext,
6
+ PluginInput
7
+ >() {}