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,212 @@
1
+ import {
2
+ HttpClient,
3
+ HttpClientRequest,
4
+ HttpClientResponse,
5
+ } from "@effect/platform"
6
+ import { Data, Effect, pipe, Ref, Schema } from "effect"
7
+ import { OAuth2Client } from "google-auth-library"
8
+ import type { Credentials } from "../../types"
9
+ import { CODE_ASSIST_VERSION, ProviderConfig } from "./config"
10
+ import { OpenCodeContext } from "./opencode"
11
+
12
+ export class SessionError extends Data.TaggedError("SessionError")<{
13
+ readonly reason:
14
+ | "project_fetch"
15
+ | "token_refresh"
16
+ | "no_tokens"
17
+ | "unauthorized"
18
+ readonly message: string
19
+ readonly cause?: unknown
20
+ }> {}
21
+
22
+ export class TokenExpiredError extends Data.TaggedError("TokenExpiredError")<{
23
+ readonly message?: string
24
+ }> {}
25
+
26
+ const CodeAssistTier = Schema.Struct({
27
+ id: Schema.String,
28
+ name: Schema.String,
29
+ description: Schema.String,
30
+ userDefinedCloudaicompanionProject: Schema.Boolean,
31
+ isDefault: Schema.optional(Schema.Boolean),
32
+ })
33
+
34
+ const LoadCodeAssistResponse = Schema.Struct({
35
+ currentTier: CodeAssistTier,
36
+ allowedTiers: Schema.Array(CodeAssistTier),
37
+ cloudaicompanionProject: Schema.String,
38
+ gcpManaged: Schema.Boolean,
39
+ manageSubscriptionUri: Schema.String,
40
+ })
41
+
42
+ export type LoadCodeAssistResponse = typeof LoadCodeAssistResponse.Type
43
+
44
+ export class Session extends Effect.Service<Session>()("Session", {
45
+ effect: Effect.gen(function* () {
46
+ const config = yield* ProviderConfig
47
+ const openCode = yield* OpenCodeContext
48
+ const httpClient = yield* HttpClient.HttpClient
49
+
50
+ const credentialsRef = yield* Ref.make<Credentials | null>(null)
51
+ const projectRef = yield* Ref.make<LoadCodeAssistResponse | null>(null)
52
+
53
+ const endpoint = config.ENDPOINTS.at(0) as string
54
+ const oauthClient = new OAuth2Client({
55
+ clientId: config.CLIENT_ID,
56
+ clientSecret: config.CLIENT_SECRET,
57
+ })
58
+
59
+ const getCredentials = Effect.gen(function* () {
60
+ const current = yield* Ref.get(credentialsRef)
61
+ if (!current) {
62
+ return yield* new SessionError({
63
+ reason: "no_tokens",
64
+ message: "No credentials set",
65
+ })
66
+ }
67
+
68
+ return current
69
+ })
70
+
71
+ const refreshTokens = Effect.gen(function* () {
72
+ const credentials = yield* getCredentials
73
+ oauthClient.setCredentials(credentials)
74
+
75
+ const result = yield* Effect.tryPromise({
76
+ try: () => oauthClient.refreshAccessToken(),
77
+ catch: (cause) =>
78
+ new SessionError({
79
+ reason: "token_refresh",
80
+ message: "Failed to refresh access token",
81
+ cause,
82
+ }),
83
+ })
84
+
85
+ const newCredentials = result.credentials
86
+ yield* Ref.set(credentialsRef, newCredentials as Credentials)
87
+
88
+ const accessToken = newCredentials.access_token
89
+ const refreshToken = newCredentials.refresh_token
90
+ const expiryDate = newCredentials.expiry_date
91
+
92
+ if (accessToken && refreshToken && expiryDate) {
93
+ yield* Effect.promise(() =>
94
+ openCode.client.auth.set({
95
+ path: { id: config.SERVICE_NAME },
96
+ body: {
97
+ type: "oauth",
98
+ access: accessToken,
99
+ refresh: refreshToken,
100
+ expires: expiryDate,
101
+ },
102
+ }),
103
+ )
104
+ }
105
+
106
+ return newCredentials
107
+ })
108
+
109
+ const fetchAttempt = Effect.gen(function* () {
110
+ const credentials = yield* getCredentials
111
+
112
+ const request = yield* HttpClientRequest.post(
113
+ `${endpoint}/${CODE_ASSIST_VERSION}:loadCodeAssist`,
114
+ ).pipe(
115
+ HttpClientRequest.bearerToken(credentials.access_token),
116
+ HttpClientRequest.bodyJson({
117
+ metadata: {
118
+ ideType: "IDE_UNSPECIFIED",
119
+ platform: "PLATFORM_UNSPECIFIED",
120
+ pluginType: "GEMINI",
121
+ },
122
+ }),
123
+ )
124
+
125
+ return yield* pipe(
126
+ httpClient.execute(request),
127
+ Effect.andThen(
128
+ HttpClientResponse.matchStatus({
129
+ "2xx": (res) =>
130
+ HttpClientResponse.schemaBodyJson(LoadCodeAssistResponse)(res),
131
+ 401: () => new TokenExpiredError({ message: "Token expired" }),
132
+ orElse: (response) =>
133
+ new SessionError({
134
+ reason: "project_fetch",
135
+ message: `HTTP error: ${response.status}`,
136
+ }),
137
+ }),
138
+ ),
139
+ )
140
+ })
141
+
142
+ const fetchProject = fetchAttempt.pipe(
143
+ Effect.catchTag("TokenExpiredError", () =>
144
+ pipe(
145
+ Effect.log("Token expired, refreshing..."),
146
+ Effect.flatMap(() => refreshTokens),
147
+ Effect.flatMap(() => fetchAttempt),
148
+ ),
149
+ ),
150
+ Effect.catchAll((error) => {
151
+ if (error instanceof SessionError) {
152
+ return Effect.fail(error)
153
+ }
154
+ return Effect.fail(
155
+ new SessionError({
156
+ reason: "project_fetch",
157
+ message: "Failed to fetch project",
158
+ cause: error,
159
+ }),
160
+ )
161
+ }),
162
+ )
163
+
164
+ const ensureProject = Effect.gen(function* () {
165
+ const cached = yield* Ref.get(projectRef)
166
+ if (cached !== null) {
167
+ return cached
168
+ }
169
+ const project = yield* fetchProject
170
+ yield* Ref.set(projectRef, project)
171
+ return project
172
+ })
173
+
174
+ const getAccessToken = Effect.gen(function* () {
175
+ const currentCreds = yield* Ref.get(credentialsRef)
176
+
177
+ if (!currentCreds?.access_token) {
178
+ return yield* new SessionError({
179
+ reason: "no_tokens",
180
+ message: "No access token available",
181
+ })
182
+ }
183
+
184
+ const buffer = 5 * 60 * 1000
185
+ const isExpired = (currentCreds.expiry_date ?? 0) < Date.now() + buffer
186
+
187
+ let accessToken = currentCreds.access_token
188
+
189
+ if (isExpired) {
190
+ yield* Effect.log("Access token expired, refreshing...")
191
+ const refreshed = yield* refreshTokens
192
+ if (!refreshed.access_token) {
193
+ return yield* new SessionError({
194
+ reason: "token_refresh",
195
+ message: "Refresh did not return access token",
196
+ })
197
+ }
198
+ accessToken = refreshed.access_token
199
+ }
200
+
201
+ yield* ensureProject
202
+ return accessToken
203
+ })
204
+
205
+ return {
206
+ setCredentials: (credentials: Credentials) =>
207
+ Ref.set(credentialsRef, credentials),
208
+ getAccessToken,
209
+ ensureProject,
210
+ }
211
+ }),
212
+ }) {}
package/src/main.ts ADDED
@@ -0,0 +1,273 @@
1
+ import type {
2
+ GoogleGenerativeAIProviderOptions,
3
+ GoogleGenerativeAIProviderSettings,
4
+ } from "@ai-sdk/google"
5
+ import { HttpClient } from "@effect/platform"
6
+ import type { Plugin } from "@opencode-ai/plugin"
7
+ import { Effect } from "effect"
8
+ import { makeRuntime } from "./lib/runtime"
9
+ import {
10
+ antigravityConfig,
11
+ geminiCliConfig,
12
+ ProviderConfig,
13
+ } from "./lib/services/config"
14
+ import { OAuth } from "./lib/services/oauth"
15
+ import { Session } from "./lib/services/session"
16
+ import { transformRequest } from "./transform/request"
17
+ import { transformNonStreamingResponse } from "./transform/response"
18
+ import { transformStreamingResponse } from "./transform/stream"
19
+ import type { Credentials, ModelsDev } from "./types"
20
+
21
+ const fetchModelsDev = Effect.gen(function* () {
22
+ const client = yield* HttpClient.HttpClient
23
+ const response = yield* client.get("https://models.dev/api.json")
24
+ return (yield* response.json) as ModelsDev
25
+ })
26
+
27
+ const customFetch = Effect.fn(function* (
28
+ input: Parameters<typeof fetch>[0],
29
+ init: Parameters<typeof fetch>[1],
30
+ ) {
31
+ const config = yield* ProviderConfig
32
+
33
+ let lastResponse: Response | null = null
34
+
35
+ for (const endpoint of config.ENDPOINTS) {
36
+ const result = yield* transformRequest(input, init, endpoint)
37
+
38
+ const { request, ...loggedBody } = JSON.parse(result.init.body as string)
39
+ const generationConfig = request.generationConfig
40
+
41
+ yield* Effect.log(
42
+ "Transformed request (Omitting request except generationConfig) :",
43
+ result.streaming,
44
+ result.input,
45
+ { ...loggedBody, request: { generationConfig } },
46
+ )
47
+
48
+ const response = yield* Effect.promise(() =>
49
+ fetch(result.input, result.init),
50
+ )
51
+
52
+ // On 429 or 403, try next endpoint
53
+ if (response.status === 429 || response.status === 403) {
54
+ yield* Effect.log(`${response.status} on ${endpoint}, trying next...`)
55
+ lastResponse = response
56
+ continue
57
+ }
58
+
59
+ if (!response.ok) {
60
+ const cloned = response.clone()
61
+ const clonedJson = yield* Effect.promise(() => cloned.json())
62
+
63
+ yield* Effect.log(
64
+ "Received response:",
65
+ cloned.status,
66
+ clonedJson,
67
+ cloned.headers,
68
+ )
69
+ }
70
+
71
+ return result.streaming ?
72
+ transformStreamingResponse(response)
73
+ : yield* Effect.promise(() => transformNonStreamingResponse(response))
74
+ }
75
+
76
+ // All endpoints exhausted with 429
77
+ yield* Effect.logWarning("All endpoints rate limited (429)")
78
+ return lastResponse as Response
79
+ }, Effect.tapDefect(Effect.logError))
80
+
81
+ export const geminiCli: Plugin = async (context) => {
82
+ const runtime = makeRuntime({
83
+ openCodeCtx: context,
84
+ providerConfig: geminiCliConfig(),
85
+ })
86
+
87
+ const config = await runtime.runPromise(
88
+ Effect.gen(function* () {
89
+ const providerConfig = yield* ProviderConfig
90
+ const modelsDev = yield* fetchModelsDev
91
+
92
+ return providerConfig.getConfig(modelsDev)
93
+ }),
94
+ )
95
+
96
+ return {
97
+ config: async (cfg) => {
98
+ cfg.provider ??= {}
99
+ cfg.provider[config.id as string] = config
100
+ },
101
+ auth: {
102
+ provider: config.id as string,
103
+ loader: async (getAuth) => {
104
+ const auth = await getAuth()
105
+ if (auth.type !== "oauth") return {}
106
+
107
+ const credentials: Credentials = {
108
+ access_token: auth.access,
109
+ refresh_token: auth.refresh,
110
+ expiry_date: auth.expires,
111
+ }
112
+
113
+ await runtime.runPromise(
114
+ Effect.gen(function* () {
115
+ const session = yield* Session
116
+ yield* session.setCredentials(credentials)
117
+ }),
118
+ )
119
+
120
+ return {
121
+ apiKey: "",
122
+ fetch: (async (input, init) => {
123
+ const response = await runtime.runPromise(customFetch(input, init))
124
+ return response
125
+ }) as typeof fetch,
126
+ } satisfies GoogleGenerativeAIProviderSettings
127
+ },
128
+ methods: [
129
+ {
130
+ type: "oauth",
131
+ label: "OAuth with Google",
132
+ authorize: async () => {
133
+ const result = await runtime.runPromise(
134
+ Effect.gen(function* () {
135
+ const oauth = yield* OAuth
136
+ return yield* oauth.authenticate
137
+ }),
138
+ )
139
+
140
+ return {
141
+ url: "",
142
+ method: "auto",
143
+ instructions: "You are now authenticated!",
144
+ callback: async () => {
145
+ const accessToken = result.access_token
146
+ const refreshToken = result.refresh_token
147
+ const expiryDate = result.expiry_date
148
+
149
+ if (!accessToken || !refreshToken || !expiryDate) {
150
+ return { type: "failed" }
151
+ }
152
+
153
+ return {
154
+ type: "success",
155
+ provider: config.id as string,
156
+ access: accessToken,
157
+ refresh: refreshToken,
158
+ expires: expiryDate,
159
+ }
160
+ },
161
+ }
162
+ },
163
+ },
164
+ ],
165
+ },
166
+ }
167
+ }
168
+
169
+ export const antigravity: Plugin = async (context) => {
170
+ const runtime = makeRuntime({
171
+ openCodeCtx: context,
172
+ providerConfig: antigravityConfig(),
173
+ })
174
+
175
+ const config = await runtime.runPromise(
176
+ Effect.gen(function* () {
177
+ const providerConfig = yield* ProviderConfig
178
+ const modelsDev = yield* fetchModelsDev
179
+
180
+ return providerConfig.getConfig(modelsDev)
181
+ }),
182
+ )
183
+
184
+ return {
185
+ config: async (cfg) => {
186
+ cfg.provider ??= {}
187
+ cfg.provider[config.id as string] = config
188
+ },
189
+ auth: {
190
+ provider: config.id as string,
191
+ loader: async (getAuth) => {
192
+ const auth = await getAuth()
193
+ if (auth.type !== "oauth") return {}
194
+
195
+ const credentials: Credentials = {
196
+ access_token: auth.access,
197
+ refresh_token: auth.refresh,
198
+ expiry_date: auth.expires,
199
+ }
200
+
201
+ await runtime.runPromise(
202
+ Effect.gen(function* () {
203
+ const session = yield* Session
204
+ yield* session.setCredentials(credentials)
205
+ }),
206
+ )
207
+
208
+ return {
209
+ apiKey: "",
210
+ fetch: (async (input, init) => {
211
+ const response = await runtime.runPromise(customFetch(input, init))
212
+ return response
213
+ }) as typeof fetch,
214
+ } satisfies GoogleGenerativeAIProviderSettings
215
+ },
216
+ methods: [
217
+ {
218
+ type: "oauth",
219
+ label: "OAuth with Google",
220
+ authorize: async () => {
221
+ const result = await runtime.runPromise(
222
+ Effect.gen(function* () {
223
+ const oauth = yield* OAuth
224
+ return yield* oauth.authenticate
225
+ }),
226
+ )
227
+
228
+ return {
229
+ url: "",
230
+ method: "auto",
231
+ instructions: "You are now authenticated!",
232
+ callback: async () => {
233
+ const accessToken = result.access_token
234
+ const refreshToken = result.refresh_token
235
+ const expiryDate = result.expiry_date
236
+
237
+ if (!accessToken || !refreshToken || !expiryDate) {
238
+ return { type: "failed" }
239
+ }
240
+
241
+ return {
242
+ type: "success",
243
+ provider: config.id as string,
244
+ access: accessToken,
245
+ refresh: refreshToken,
246
+ expires: expiryDate,
247
+ }
248
+ },
249
+ }
250
+ },
251
+ },
252
+ ],
253
+ },
254
+ "chat.params": async (input, output) => {
255
+ await runtime.runPromise(
256
+ Effect.log("chat.params event before:", input.model, output.options),
257
+ )
258
+
259
+ if (input.model.providerID === config.id) {
260
+ output.options = {
261
+ ...output.options,
262
+ labels: {
263
+ sessionId: input.sessionID,
264
+ },
265
+ } satisfies GoogleGenerativeAIProviderOptions
266
+ }
267
+
268
+ await runtime.runPromise(
269
+ Effect.log("chat.params event after:", input.model, output.options),
270
+ )
271
+ },
272
+ }
273
+ }
@@ -0,0 +1,198 @@
1
+ {
2
+ "id": "google",
3
+ "env": [
4
+ "GOOGLE_GENERATIVE_AI_API_KEY",
5
+ "GEMINI_API_KEY"
6
+ ],
7
+ "npm": "@ai-sdk/google",
8
+ "name": "Google",
9
+ "doc": "https://ai.google.dev/gemini-api/docs/pricing",
10
+ "models": {
11
+ "gemini-3-flash-preview": {
12
+ "id": "gemini-3-flash-preview",
13
+ "name": "Gemini 3 Flash Preview",
14
+ "family": "gemini-flash",
15
+ "attachment": true,
16
+ "reasoning": true,
17
+ "tool_call": true,
18
+ "structured_output": true,
19
+ "temperature": true,
20
+ "knowledge": "2025-01",
21
+ "release_date": "2025-12-17",
22
+ "last_updated": "2025-12-17",
23
+ "modalities": {
24
+ "input": [
25
+ "text",
26
+ "image",
27
+ "video",
28
+ "audio",
29
+ "pdf"
30
+ ],
31
+ "output": [
32
+ "text"
33
+ ]
34
+ },
35
+ "open_weights": false,
36
+ "cost": {
37
+ "input": 0.5,
38
+ "output": 3,
39
+ "cache_read": 0.05,
40
+ "context_over_200k": {
41
+ "input": 0.5,
42
+ "output": 3,
43
+ "cache_read": 0.05
44
+ }
45
+ },
46
+ "limit": {
47
+ "context": 1048576,
48
+ "output": 65536
49
+ }
50
+ },
51
+ "gemini-3-pro-preview": {
52
+ "id": "gemini-3-pro-preview",
53
+ "name": "Gemini 3 Pro Preview",
54
+ "family": "gemini-pro",
55
+ "attachment": true,
56
+ "reasoning": true,
57
+ "tool_call": true,
58
+ "structured_output": true,
59
+ "temperature": true,
60
+ "knowledge": "2025-01",
61
+ "release_date": "2025-11-18",
62
+ "last_updated": "2025-11-18",
63
+ "modalities": {
64
+ "input": [
65
+ "text",
66
+ "image",
67
+ "video",
68
+ "audio",
69
+ "pdf"
70
+ ],
71
+ "output": [
72
+ "text"
73
+ ]
74
+ },
75
+ "open_weights": false,
76
+ "cost": {
77
+ "input": 2,
78
+ "output": 12,
79
+ "cache_read": 0.2,
80
+ "context_over_200k": {
81
+ "input": 4,
82
+ "output": 18,
83
+ "cache_read": 0.4
84
+ }
85
+ },
86
+ "limit": {
87
+ "context": 1000000,
88
+ "output": 64000
89
+ }
90
+ },
91
+ "gemini-2.5-flash": {
92
+ "id": "gemini-2.5-flash",
93
+ "name": "Gemini 2.5 Flash",
94
+ "family": "gemini-flash",
95
+ "attachment": true,
96
+ "reasoning": true,
97
+ "tool_call": true,
98
+ "structured_output": true,
99
+ "temperature": true,
100
+ "knowledge": "2025-01",
101
+ "release_date": "2025-03-20",
102
+ "last_updated": "2025-06-05",
103
+ "modalities": {
104
+ "input": [
105
+ "text",
106
+ "image",
107
+ "audio",
108
+ "video",
109
+ "pdf"
110
+ ],
111
+ "output": [
112
+ "text"
113
+ ]
114
+ },
115
+ "open_weights": false,
116
+ "cost": {
117
+ "input": 0.3,
118
+ "output": 2.5,
119
+ "cache_read": 0.075,
120
+ "input_audio": 1
121
+ },
122
+ "limit": {
123
+ "context": 1048576,
124
+ "output": 65536
125
+ }
126
+ },
127
+ "gemini-2.5-flash-lite": {
128
+ "id": "gemini-2.5-flash-lite",
129
+ "name": "Gemini 2.5 Flash Lite",
130
+ "family": "gemini-flash-lite",
131
+ "attachment": true,
132
+ "reasoning": true,
133
+ "tool_call": true,
134
+ "structured_output": true,
135
+ "temperature": true,
136
+ "knowledge": "2025-01",
137
+ "release_date": "2025-06-17",
138
+ "last_updated": "2025-06-17",
139
+ "modalities": {
140
+ "input": [
141
+ "text",
142
+ "image",
143
+ "audio",
144
+ "video",
145
+ "pdf"
146
+ ],
147
+ "output": [
148
+ "text"
149
+ ]
150
+ },
151
+ "open_weights": false,
152
+ "cost": {
153
+ "input": 0.1,
154
+ "output": 0.4,
155
+ "cache_read": 0.025
156
+ },
157
+ "limit": {
158
+ "context": 1048576,
159
+ "output": 65536
160
+ }
161
+ },
162
+ "gemini-2.5-pro": {
163
+ "id": "gemini-2.5-pro",
164
+ "name": "Gemini 2.5 Pro",
165
+ "family": "gemini-pro",
166
+ "attachment": true,
167
+ "reasoning": true,
168
+ "tool_call": true,
169
+ "structured_output": true,
170
+ "temperature": true,
171
+ "knowledge": "2025-01",
172
+ "release_date": "2025-03-20",
173
+ "last_updated": "2025-06-05",
174
+ "modalities": {
175
+ "input": [
176
+ "text",
177
+ "image",
178
+ "audio",
179
+ "video",
180
+ "pdf"
181
+ ],
182
+ "output": [
183
+ "text"
184
+ ]
185
+ },
186
+ "open_weights": false,
187
+ "cost": {
188
+ "input": 1.25,
189
+ "output": 10,
190
+ "cache_read": 0.31
191
+ },
192
+ "limit": {
193
+ "context": 1048576,
194
+ "output": 65536
195
+ }
196
+ }
197
+ }
198
+ }