opencode-google-auth 0.0.6 → 0.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-google-auth",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "_description_",
5
5
  "keywords": [
6
6
  "opencode-google-auth"
@@ -34,26 +34,25 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@effect/experimental": "^0.58.0",
37
- "@effect/platform": "^0.94.1",
38
- "@effect/platform-bun": "^0.87.0",
39
- "@opencode-ai/plugin": "^1.1.18",
37
+ "@effect/platform": "^0.94.2",
38
+ "@effect/platform-bun": "^0.87.1",
39
+ "@opencode-ai/plugin": "^1.1.36",
40
40
  "arkregex": "^0.0.5",
41
- "effect": "^3.19.14",
41
+ "effect": "^3.19.15",
42
42
  "google-auth-library": "^10.5.0",
43
43
  "open": "^11.0.0",
44
44
  "xdg-basedir": "^5.1.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@ai-sdk/google": "^3.0.7",
48
- "@effect/language-service": "^0.65.0",
49
- "@types/bun": "^1.3.5",
47
+ "@effect/language-service": "^0.72.0",
48
+ "@types/bun": "^1.3.6",
50
49
  "@types/yargs": "^17.0.35",
51
- "@typescript/native-preview": "^7.0.0-dev.20260113.1",
52
- "ai": "^6.0.33",
50
+ "@typescript/native-preview": "^7.0.0-dev.20260126.1",
53
51
  "bumpp": "^10.4.0",
54
- "oxlint": "^1.39.0",
55
- "oxlint-tsgolint": "^0.11.0",
56
- "prettier": "^3.7.4",
52
+ "cloudassist-ai-provider": "^0.0.2",
53
+ "oxlint": "^1.42.0",
54
+ "oxlint-tsgolint": "^0.11.2",
55
+ "prettier": "^3.8.1",
57
56
  "typescript": "^5.9.3",
58
57
  "yargs": "^18.0.0"
59
58
  },
package/src/main.ts CHANGED
@@ -1,23 +1,15 @@
1
+ import { HttpClient } from "@effect/platform"
2
+ import type { Plugin } from "@opencode-ai/plugin"
1
3
  import type {
2
4
  GoogleGenerativeAIProviderOptions,
3
5
  GoogleGenerativeAIProviderSettings,
4
- } from "@ai-sdk/google"
5
- import { HttpClient } from "@effect/platform"
6
- import type { Plugin } from "@opencode-ai/plugin"
6
+ } from "cloudassist-ai-provider"
7
7
  import { Effect } from "effect"
8
8
  import { makeRuntime } from "./lib/runtime"
9
- import {
10
- antigravityConfig,
11
- geminiCliConfig,
12
- ProviderConfig,
13
- } from "./services/config"
9
+ import { geminiCliConfig, ProviderConfig } from "./services/config"
14
10
  import { OAuth } from "./services/oauth"
15
11
  import { Session } from "./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
- import antigravitySpoof from "./antigravity-spoof.txt"
12
+ import type { Credentials, FetchInit, FetchInput, ModelsDev } from "./types"
21
13
 
22
14
  const fetchModelsDev = Effect.gen(function* () {
23
15
  const client = yield* HttpClient.HttpClient
@@ -25,34 +17,25 @@ const fetchModelsDev = Effect.gen(function* () {
25
17
  return (yield* response.json) as ModelsDev
26
18
  })
27
19
 
28
- const customFetch = Effect.fn(function* (
29
- input: Parameters<typeof fetch>[0],
30
- init: Parameters<typeof fetch>[1],
31
- ) {
20
+ const customFetch = Effect.fn(function* (input: FetchInput, init: FetchInit) {
32
21
  const config = yield* ProviderConfig
33
22
 
34
23
  let lastResponse: Response | null = null
35
24
 
36
25
  for (const endpoint of config.ENDPOINTS) {
37
- const result = yield* transformRequest(input, init, endpoint)
38
-
39
- const { request, ...loggedBody } = JSON.parse(result.init.body as string)
40
- const generationConfig = request.generationConfig
26
+ yield* Effect.log("Trying endpoint", endpoint)
27
+ yield* Effect.log("Input", input)
28
+ yield* Effect.log("Init", init)
41
29
 
42
- yield* Effect.log(
43
- "Transformed request (Omitting request except generationConfig) :",
44
- result.streaming,
45
- result.input,
46
- { ...loggedBody, request: { generationConfig } },
47
- )
30
+ const [finalInput, finalInit] = config.requestTransform?.(input, init) ?? [
31
+ input,
32
+ init,
33
+ ]
48
34
 
49
- const response = yield* Effect.promise(() =>
50
- fetch(result.input, result.init),
51
- )
35
+ const response = yield* Effect.promise(() => fetch(finalInput, finalInit))
52
36
 
53
37
  // On 429 or 403, try next endpoint
54
38
  if (response.status === 429 || response.status === 403) {
55
- yield* Effect.log(`${response.status} on ${endpoint}, trying next...`)
56
39
  lastResponse = response
57
40
  continue
58
41
  }
@@ -61,7 +44,7 @@ const customFetch = Effect.fn(function* (
61
44
  const cloned = response.clone()
62
45
  const clonedJson = yield* Effect.promise(() => cloned.json())
63
46
 
64
- yield* Effect.log(
47
+ yield* Effect.logWarning(
65
48
  "Received response:",
66
49
  cloned.status,
67
50
  clonedJson,
@@ -69,18 +52,10 @@ const customFetch = Effect.fn(function* (
69
52
  )
70
53
  }
71
54
 
72
- if (config.skipRequestTransform) {
73
- yield* Effect.log("Skipping response transformation")
74
- return response
75
- }
76
-
77
- return result.streaming ?
78
- transformStreamingResponse(response)
79
- : yield* Effect.promise(() => transformNonStreamingResponse(response))
55
+ return response
80
56
  }
81
57
 
82
- // All endpoints exhausted with 429
83
- yield* Effect.logWarning("All endpoints rate limited (429)")
58
+ yield* Effect.logError("All endpoints are rate limited (429)")
84
59
  return lastResponse as Response
85
60
  }, Effect.tapDefect(Effect.logError))
86
61
 
@@ -124,95 +99,7 @@ export const geminiCli: Plugin = async (context) => {
124
99
  )
125
100
 
126
101
  return {
127
- apiKey: "",
128
- fetch: (async (input, init) => {
129
- const response = await runtime.runPromise(customFetch(input, init))
130
- return response
131
- }) as typeof fetch,
132
- } satisfies GoogleGenerativeAIProviderSettings
133
- },
134
- methods: [
135
- {
136
- type: "oauth",
137
- label: "OAuth with Google",
138
- authorize: async () => {
139
- const result = await runtime.runPromise(
140
- Effect.gen(function* () {
141
- const oauth = yield* OAuth
142
- return yield* oauth.authenticate
143
- }),
144
- )
145
-
146
- return {
147
- url: "",
148
- method: "auto",
149
- instructions: "You are now authenticated!",
150
- callback: async () => {
151
- const accessToken = result.access_token
152
- const refreshToken = result.refresh_token
153
- const expiryDate = result.expiry_date
154
-
155
- if (!accessToken || !refreshToken || !expiryDate) {
156
- return { type: "failed" }
157
- }
158
-
159
- return {
160
- type: "success",
161
- provider: config.id as string,
162
- access: accessToken,
163
- refresh: refreshToken,
164
- expires: expiryDate,
165
- }
166
- },
167
- }
168
- },
169
- },
170
- ],
171
- },
172
- }
173
- }
174
-
175
- export const antigravity: Plugin = async (context) => {
176
- const runtime = makeRuntime({
177
- openCodeCtx: context,
178
- providerConfig: antigravityConfig(),
179
- })
180
-
181
- const config = await runtime.runPromise(
182
- Effect.gen(function* () {
183
- const providerConfig = yield* ProviderConfig
184
- const modelsDev = yield* fetchModelsDev
185
-
186
- return providerConfig.getConfig(modelsDev)
187
- }),
188
- )
189
-
190
- return {
191
- config: async (cfg) => {
192
- cfg.provider ??= {}
193
- cfg.provider[config.id as string] = config
194
- },
195
- auth: {
196
- provider: config.id as string,
197
- loader: async (getAuth) => {
198
- const auth = await getAuth()
199
- if (auth.type !== "oauth") return {}
200
-
201
- const credentials: Credentials = {
202
- access_token: auth.access,
203
- refresh_token: auth.refresh,
204
- expiry_date: auth.expires,
205
- }
206
-
207
- await runtime.runPromise(
208
- Effect.gen(function* () {
209
- const session = yield* Session
210
- yield* session.setCredentials(credentials)
211
- }),
212
- )
213
-
214
- return {
215
- apiKey: "",
102
+ apiKey: auth.access,
216
103
  fetch: (async (input, init) => {
217
104
  const response = await runtime.runPromise(customFetch(input, init))
218
105
  return response
@@ -257,27 +144,153 @@ export const antigravity: Plugin = async (context) => {
257
144
  },
258
145
  ],
259
146
  },
260
- "experimental.chat.system.transform": async (_input, output) => {
261
- // THIS IS REQUIRED OTHERWISE YOU'LL GET 429 OR 403 FOR SOME GODDAMN REASON
262
- output.system.unshift(antigravitySpoof)
263
- },
264
147
  "chat.params": async (input, output) => {
265
- await runtime.runPromise(
266
- Effect.log("chat.params event before:", input.model, output.options),
148
+ const options = await runtime.runPromise(
149
+ Effect.gen(function* () {
150
+ const session = yield* Session
151
+ const project = yield* session.ensureProject
152
+ const projectId = project.cloudaicompanionProject
153
+
154
+ return {
155
+ projectId,
156
+ }
157
+ }),
267
158
  )
268
159
 
269
- if (input.model.providerID === config.id) {
160
+ if (config.id === input.model.providerID) {
270
161
  output.options = {
271
162
  ...output.options,
272
- labels: {
273
- sessionId: input.sessionID,
274
- },
163
+ projectId: options.projectId,
275
164
  } satisfies GoogleGenerativeAIProviderOptions
276
165
  }
277
166
 
278
167
  await runtime.runPromise(
279
- Effect.log("chat.params event after:", input.model, output.options),
168
+ Effect.log("chat.params", config.id, input.model, output),
280
169
  )
281
170
  },
282
171
  }
283
172
  }
173
+
174
+ // export const antigravity: Plugin = async (context) => {
175
+ // const runtime = makeRuntime({
176
+ // openCodeCtx: context,
177
+ // providerConfig: antigravityConfig(),
178
+ // })
179
+
180
+ // const config = await runtime.runPromise(
181
+ // Effect.gen(function* () {
182
+ // const providerConfig = yield* ProviderConfig
183
+ // const modelsDev = yield* fetchModelsDev
184
+
185
+ // const config = providerConfig.getConfig(modelsDev)
186
+ // yield* Effect.log("Config initialized", config.id)
187
+
188
+ // return config
189
+ // }),
190
+ // )
191
+
192
+ // return {
193
+ // config: async (cfg) => {
194
+ // cfg.provider ??= {}
195
+ // cfg.provider[config.id as string] = config
196
+ // },
197
+ // auth: {
198
+ // provider: config.id as string,
199
+ // loader: async (getAuth) => {
200
+ // const auth = await getAuth()
201
+ // if (auth.type !== "oauth") return {}
202
+
203
+ // const credentials: Credentials = {
204
+ // access_token: auth.access,
205
+ // refresh_token: auth.refresh,
206
+ // expiry_date: auth.expires,
207
+ // }
208
+
209
+ // await runtime.runPromise(
210
+ // Effect.gen(function* () {
211
+ // const session = yield* Session
212
+ // yield* session.setCredentials(credentials)
213
+ // }),
214
+ // )
215
+
216
+ // return {
217
+ // apiKey: auth.access,
218
+ // fetch: (async (input, init) => {
219
+ // const response = await runtime.runPromise(customFetch(input, init))
220
+ // return response
221
+ // }) as typeof fetch,
222
+ // } satisfies GoogleGenerativeAIProviderSettings
223
+ // },
224
+ // methods: [
225
+ // {
226
+ // type: "oauth",
227
+ // label: "OAuth with Google",
228
+ // authorize: async () => {
229
+ // const result = await runtime.runPromise(
230
+ // Effect.gen(function* () {
231
+ // const oauth = yield* OAuth
232
+ // return yield* oauth.authenticate
233
+ // }),
234
+ // )
235
+
236
+ // return {
237
+ // url: "",
238
+ // method: "auto",
239
+ // instructions: "You are now authenticated!",
240
+ // callback: async () => {
241
+ // const accessToken = result.access_token
242
+ // const refreshToken = result.refresh_token
243
+ // const expiryDate = result.expiry_date
244
+
245
+ // if (!accessToken || !refreshToken || !expiryDate) {
246
+ // return { type: "failed" }
247
+ // }
248
+
249
+ // return {
250
+ // type: "success",
251
+ // provider: config.id as string,
252
+ // access: accessToken,
253
+ // refresh: refreshToken,
254
+ // expires: expiryDate,
255
+ // }
256
+ // },
257
+ // }
258
+ // },
259
+ // },
260
+ // ],
261
+ // },
262
+ // "experimental.chat.system.transform": async (input, output) => {
263
+ // output.system.unshift(antigravitySpoof)
264
+ // },
265
+ // "chat.params": async (input, output) => {
266
+ // const requestId = crypto.randomUUID()
267
+
268
+ // const options = await runtime.runPromise(
269
+ // Effect.gen(function* () {
270
+ // const session = yield* Session
271
+ // const project = yield* session.ensureProject
272
+ // const projectId = project.cloudaicompanionProject
273
+
274
+ // return {
275
+ // projectId,
276
+ // }
277
+ // }),
278
+ // )
279
+
280
+ // if (config.id === input.model.providerID) {
281
+ // output.options = {
282
+ // ...output.options,
283
+ // userAgent: "antigravity",
284
+ // requestType: "agent",
285
+ // requestId: `agent-${requestId}`,
286
+ // sessionId: input.sessionID,
287
+ // projectId: options.projectId,
288
+ // } satisfies GoogleGenerativeAIProviderOptions
289
+ // }
290
+
291
+ // await runtime.runPromise(
292
+ // Effect.log("chat.params", config.id, input.model, output),
293
+ // )
294
+ // },
295
+ // }
296
+ // }
@@ -1,6 +1,8 @@
1
1
  import type { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"
2
- import { Context, Effect, pipe } from "effect"
2
+ import { Context, pipe } from "effect"
3
3
  import type {
4
+ FetchInit,
5
+ FetchInput,
4
6
  ModelsDev,
5
7
  OpenCodeModel,
6
8
  OpenCodeProvider,
@@ -28,12 +30,10 @@ export interface ProviderConfigShape {
28
30
  readonly CLIENT_ID: string
29
31
  readonly CLIENT_SECRET: string
30
32
  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
- readonly skipRequestTransform?: boolean
33
+ readonly requestTransform?: (
34
+ input: FetchInput,
35
+ init: FetchInit,
36
+ ) => [FetchInput, FetchInit]
37
37
  }
38
38
 
39
39
  export class ProviderConfig extends Context.Tag("ProviderConfig")<
@@ -100,11 +100,10 @@ export const geminiCliConfig = (): ProviderConfigShape => ({
100
100
  ...provider,
101
101
  id: geminiCliConfig().SERVICE_NAME,
102
102
  name: geminiCliConfig().DISPLAY_NAME,
103
- api: geminiCliConfig().ENDPOINTS.at(0) as string,
103
+ npm: "cloudassist-ai-provider",
104
104
  models: filteredModels as Record<string, OpenCodeModel>,
105
105
  }
106
106
  },
107
- skipRequestTransform: false,
108
107
  })
109
108
 
110
109
  export const antigravityConfig = (): ProviderConfigShape => ({
@@ -133,9 +132,7 @@ export const antigravityConfig = (): ProviderConfigShape => ({
133
132
  },
134
133
  getConfig: (modelsDev) => {
135
134
  const googleProvider = modelsDev.google as Provider
136
- const googleVertextProvider = modelsDev[
137
- "google-vertex-anthropic"
138
- ] as Provider
135
+ const googleVertextProvider = modelsDev.anthropic as Provider
139
136
 
140
137
  const geminiFlash = googleProvider.models[
141
138
  "gemini-3-flash-preview"
@@ -144,10 +141,10 @@ export const antigravityConfig = (): ProviderConfigShape => ({
144
141
  "gemini-3-pro-preview"
145
142
  ] as OpenCodeModel
146
143
  const claudeSonnet = googleVertextProvider.models[
147
- "claude-sonnet-4-5@20250929"
144
+ "claude-sonnet-4-5"
148
145
  ] as OpenCodeModel
149
146
  const claudeOpus = googleVertextProvider.models[
150
- "claude-opus-4-5@20251101"
147
+ "claude-opus-4-5"
151
148
  ] as OpenCodeModel
152
149
 
153
150
  const models: Record<
@@ -181,149 +178,82 @@ export const antigravityConfig = (): ProviderConfigShape => ({
181
178
  },
182
179
  } satisfies GoogleGenerativeAIProviderOptions,
183
180
  },
184
- // "claude-sonnet-4-5": {
185
- // ...claudeSonnet,
186
- // id: "claude-sonnet-4-5",
187
- // reasoning: false,
188
- // options: {
189
- // thinkingConfig: {
190
- // includeThoughts: false,
191
- // },
192
- // } satisfies GoogleGenerativeAIProviderOptions,
193
- // },
194
- // "claude-sonnet-4-5-thinking": {
195
- // ...claudeSonnet,
196
- // id: "claude-sonnet-4-5-thinking",
197
- // name: "Claude Sonnet 4.5 (Thinking)",
198
- // options: {
199
- // thinkingConfig: {
200
- // includeThoughts: true,
201
- // thinkingBudget: 31999,
202
- // },
203
- // } satisfies GoogleGenerativeAIProviderOptions,
204
- // variants: {
205
- // high: {
206
- // thinkingConfig: {
207
- // includeThoughts: true,
208
- // thinkingBudget: 16000,
209
- // },
210
- // } satisfies GoogleGenerativeAIProviderOptions,
211
- // max: {
212
- // thinkingConfig: {
213
- // includeThoughts: true,
214
- // thinkingBudget: 31999,
215
- // },
216
- // } satisfies GoogleGenerativeAIProviderOptions,
217
- // },
218
- // },
219
- // "claude-opus-4-5-thinking": {
220
- // ...claudeOpus,
221
- // id: "claude-opus-4-5-thinking",
222
- // name: "Claude Opus 4.5 (Thinking)",
223
- // options: {
224
- // thinkingConfig: {
225
- // includeThoughts: true,
226
- // thinkingBudget: 31999,
227
- // },
228
- // } satisfies GoogleGenerativeAIProviderOptions,
229
- // variants: {
230
- // high: {
231
- // thinkingConfig: {
232
- // includeThoughts: true,
233
- // thinkingBudget: 16000,
234
- // },
235
- // } satisfies GoogleGenerativeAIProviderOptions,
236
- // max: {
237
- // thinkingConfig: {
238
- // includeThoughts: true,
239
- // thinkingBudget: 31999,
240
- // },
241
- // } satisfies GoogleGenerativeAIProviderOptions,
242
- // },
243
- // },
181
+ "claude-sonnet-4-5": {
182
+ ...claudeSonnet,
183
+ id: "claude-sonnet-4-5",
184
+ reasoning: false,
185
+ options: {
186
+ thinkingConfig: {
187
+ includeThoughts: false,
188
+ },
189
+ } satisfies GoogleGenerativeAIProviderOptions,
190
+ },
191
+ "claude-sonnet-4-5-thinking": {
192
+ ...claudeSonnet,
193
+ id: "claude-sonnet-4-5-thinking",
194
+ name: "Claude Sonnet 4.5 (Thinking)",
195
+ options: {
196
+ thinkingConfig: {
197
+ includeThoughts: true,
198
+ thinkingBudget: 31999,
199
+ },
200
+ } satisfies GoogleGenerativeAIProviderOptions,
201
+ variants: {
202
+ high: {
203
+ thinkingConfig: {
204
+ includeThoughts: true,
205
+ thinkingBudget: 16000,
206
+ },
207
+ } satisfies GoogleGenerativeAIProviderOptions,
208
+ max: {
209
+ thinkingConfig: {
210
+ includeThoughts: true,
211
+ thinkingBudget: 31999,
212
+ },
213
+ } satisfies GoogleGenerativeAIProviderOptions,
214
+ },
215
+ },
216
+ "claude-opus-4-5-thinking": {
217
+ ...claudeOpus,
218
+ id: "claude-opus-4-5-thinking",
219
+ name: "Claude Opus 4.5 (Thinking)",
220
+ options: {
221
+ thinkingConfig: {
222
+ includeThoughts: true,
223
+ thinkingBudget: 31999,
224
+ },
225
+ } satisfies GoogleGenerativeAIProviderOptions,
226
+ variants: {
227
+ high: {
228
+ thinkingConfig: {
229
+ includeThoughts: true,
230
+ thinkingBudget: 16000,
231
+ },
232
+ } satisfies GoogleGenerativeAIProviderOptions,
233
+ max: {
234
+ thinkingConfig: {
235
+ includeThoughts: true,
236
+ thinkingBudget: 31999,
237
+ },
238
+ } satisfies GoogleGenerativeAIProviderOptions,
239
+ },
240
+ },
244
241
  }
245
242
 
246
243
  return {
247
244
  ...googleProvider,
248
245
  id: antigravityConfig().SERVICE_NAME,
249
246
  name: antigravityConfig().DISPLAY_NAME,
250
- api: antigravityConfig().ENDPOINTS.at(0) as string,
251
- npm: "google-antigravity-ai-provider",
247
+ npm: "cloudassist-ai-provider",
252
248
  models,
253
249
  }
254
250
  },
255
- transformRequest: Effect.fn(function* (context) {
256
- yield* Effect.log(
257
- "Transforming request for: ",
258
- antigravityConfig().SERVICE_NAME,
259
- )
260
-
261
- const { body, headers, url } = context
262
- const innerRequest = body.request as Record<string, unknown>
263
-
264
- let sessionId: string | undefined
265
- if (
266
- innerRequest.labels
267
- && typeof innerRequest.labels === "object"
268
- && "sessionId" in innerRequest.labels
269
- ) {
270
- const labels = innerRequest.labels as Record<string, unknown>
271
- sessionId = labels.sessionId as string
272
- delete labels.sessionId
273
- if (Object.keys(labels).length === 0) {
274
- delete innerRequest.labels
275
- }
276
- }
277
-
278
- // Handle thinkingConfig for Claude models
279
- const isClaude = body.model.toLowerCase().includes("claude")
280
- const isThinking = body.model.toLowerCase().includes("thinking")
281
-
282
- if (isClaude && body.request && typeof body.request === "object") {
283
- const request = body.request as Record<string, unknown>
284
- const tools = request.tools as
285
- | Array<{ functionDeclarations?: Array<{ parameters?: unknown }> }>
286
- | undefined
287
- if (tools && Array.isArray(tools)) {
288
- for (const tool of tools) {
289
- if (
290
- tool.functionDeclarations
291
- && Array.isArray(tool.functionDeclarations)
292
- ) {
293
- for (const func of tool.functionDeclarations) {
294
- if (!func.parameters) {
295
- func.parameters = { type: "object", properties: {} }
296
- }
297
- }
298
- }
299
- }
300
- }
301
-
302
- innerRequest.toolConfig = {
303
- functionCallingConfig: {
304
- mode: "VALIDATED",
305
- },
306
- }
307
-
308
- if (isThinking) {
309
- headers.set("anthropic-beta", "interleaved-thinking-2025-05-14")
310
- }
311
- }
312
-
313
- if (sessionId) {
314
- innerRequest.sessionId = sessionId
251
+ requestTransform: (_input, init) => {
252
+ const headers = new Headers(init?.headers)
253
+ for (const [key, value] of Object.entries(antigravityConfig().HEADERS)) {
254
+ headers.set(key, value)
315
255
  }
316
256
 
317
- return {
318
- headers,
319
- url,
320
- body: {
321
- requestType: "agent",
322
- userAgent: "antigravity",
323
- requestId: `agent-${crypto.randomUUID()}`,
324
- ...body,
325
- },
326
- }
327
- }),
328
- skipRequestTransform: true,
257
+ return [_input, { ...init, headers }]
258
+ },
329
259
  })
package/src/types.ts CHANGED
@@ -17,6 +17,9 @@ export type OpenCodeLogLevel = NonNullable<
17
17
 
18
18
  export type BunServeOptions = Partial<Bun.Serve.Options<undefined, never>>
19
19
 
20
+ export type FetchInput = Parameters<typeof fetch>[0]
21
+ export type FetchInit = Parameters<typeof fetch>[1]
22
+
20
23
  /**
21
24
  * Subset of google-auth-library Credentials type.
22
25
  *
@@ -1,176 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { transformRequest } from "./request"
3
- import { geminiCliConfig, ProviderConfig } from "../services/config"
4
- import { Session } from "../services/session"
5
- import { Effect, Layer, pipe } from "effect"
6
-
7
- describe("transformRequest", () => {
8
- const baseParams = {
9
- accessToken: "test-token",
10
- projectId: "test-project-123",
11
- }
12
-
13
- const config = geminiCliConfig()
14
-
15
- const MockSession = Layer.succeed(
16
- Session,
17
- Session.of({
18
- getAccessToken: Effect.succeed(baseParams.accessToken),
19
- ensureProject: Effect.succeed({
20
- cloudaicompanionProject: baseParams.projectId,
21
- currentTier: {
22
- id: "free",
23
- name: "Free",
24
- description: "",
25
- userDefinedCloudaicompanionProject: false,
26
- },
27
- allowedTiers: [],
28
- gcpManaged: false,
29
- manageSubscriptionUri: "",
30
- }),
31
- setCredentials: () => Effect.void,
32
- } as unknown as Session),
33
- ).pipe(Layer.provideMerge(Layer.succeed(ProviderConfig, config)))
34
-
35
- const runTransform = (
36
- input: Parameters<typeof fetch>[0],
37
- init: Parameters<typeof fetch>[1],
38
- endpoint = "https://example.com",
39
- ) =>
40
- pipe(
41
- transformRequest(input, init, endpoint),
42
- Effect.provide(MockSession),
43
- Effect.runPromise,
44
- )
45
-
46
- describe("URL transformation", () => {
47
- it("transforms /v1beta/models/{model}:{action} to /v1internal:{action}", async () => {
48
- const result = await runTransform(
49
- "https://example.com/v1beta/models/gemini-2.5-pro:generateContent",
50
- undefined,
51
- )
52
-
53
- expect(result.input).toContain("/v1internal:generateContent")
54
- expect(result.streaming).toBe(false)
55
- })
56
-
57
- it("detects streaming requests", async () => {
58
- const result = await runTransform(
59
- "https://example.com/v1beta/models/gemini-2.5-pro:streamGenerateContent",
60
- undefined,
61
- )
62
-
63
- expect(result.input).toContain("/v1internal:streamGenerateContent")
64
- expect(result.input).toContain("alt=sse")
65
- expect(result.streaming).toBe(true)
66
- })
67
-
68
- it("passes through non-matching URLs", async () => {
69
- const url = "https://example.com/some/other/path"
70
- const result = await runTransform(url, undefined)
71
-
72
- expect(result.input).toBe(url)
73
- expect(result.streaming).toBe(false)
74
- })
75
- })
76
-
77
- describe("header transformation", () => {
78
- it("sets Authorization header", async () => {
79
- const result = await runTransform(
80
- "https://example.com/v1beta/models/gemini-2.5-pro:generateContent",
81
- {},
82
- )
83
-
84
- const headers = result.init.headers as Headers
85
- expect(headers.get("Authorization")).toBe("Bearer test-token")
86
- })
87
-
88
- it("removes x-api-key header", async () => {
89
- const result = await runTransform(
90
- "https://example.com/v1beta/models/gemini-2.5-pro:generateContent",
91
- {
92
- headers: {
93
- "x-api-key": "some-key",
94
- "x-goog-api-key": "another-key",
95
- },
96
- },
97
- )
98
-
99
- const headers = result.init.headers as Headers
100
- expect(headers.get("x-api-key")).toBeNull()
101
- expect(headers.get("x-goog-api-key")).toBeNull()
102
- })
103
-
104
- it("sets Accept header for streaming", async () => {
105
- const result = await runTransform(
106
- "https://example.com/v1beta/models/gemini-2.5-pro:streamGenerateContent",
107
- {},
108
- )
109
-
110
- const headers = result.init.headers as Headers
111
- expect(headers.get("Accept")).toBe("text/event-stream")
112
- })
113
-
114
- it("applies provider-specific headers", async () => {
115
- const result = await runTransform(
116
- "https://example.com/v1beta/models/gemini-2.5-pro:generateContent",
117
- {},
118
- )
119
-
120
- const headers = result.init.headers as Headers
121
- expect(headers.get("User-Agent")).toBe(
122
- config.HEADERS["User-Agent"] ?? null,
123
- )
124
- expect(headers.get("X-Goog-Api-Client")).toBe(
125
- config.HEADERS["X-Goog-Api-Client"] ?? null,
126
- )
127
- })
128
- })
129
-
130
- describe("body transformation", () => {
131
- it("wraps request body with project and model", async () => {
132
- const originalBody = JSON.stringify({ contents: [{ text: "Hello" }] })
133
-
134
- const result = await runTransform(
135
- "https://example.com/v1beta/models/gemini-2.5-pro:generateContent",
136
- { body: originalBody },
137
- )
138
-
139
- const body = JSON.parse(result.init.body as string)
140
- expect(body.project).toBe("test-project-123")
141
- expect(body.model).toBe("gemini-2.5-pro")
142
- expect(body.request).toEqual({ contents: [{ text: "Hello" }] })
143
- })
144
-
145
- it("preserves body if not JSON", async () => {
146
- const originalBody = "not-json"
147
-
148
- const result = await runTransform(
149
- "https://example.com/v1beta/models/gemini-2.5-pro:generateContent",
150
- { body: originalBody },
151
- )
152
-
153
- expect(result.init.body).toBe("not-json")
154
- })
155
- })
156
-
157
- describe("input types", () => {
158
- it("handles URL object", async () => {
159
- const url = new URL(
160
- "https://example.com/v1beta/models/gemini-2.5-pro:generateContent",
161
- )
162
- const result = await runTransform(url, undefined)
163
-
164
- expect(result.input).toContain("/v1internal:generateContent")
165
- })
166
-
167
- it("handles Request object", async () => {
168
- const request = new Request(
169
- "https://example.com/v1beta/models/gemini-2.5-pro:generateContent",
170
- )
171
- const result = await runTransform(request, undefined)
172
-
173
- expect(result.input).toContain("/v1internal:generateContent")
174
- })
175
- })
176
- })
@@ -1,101 +0,0 @@
1
- import { regex } from "arkregex"
2
- import { Effect, pipe } from "effect"
3
- import {
4
- CODE_ASSIST_VERSION,
5
- ProviderConfig,
6
- type RequestContext,
7
- } from "../services/config"
8
- import { Session } from "../services/session"
9
-
10
- const STREAM_ACTION = "streamGenerateContent"
11
- const PATH_PATTERN = regex("/models/(?<model>[^:]+):(?<action>\\w+)")
12
-
13
- export const transformRequest = Effect.fn("transformRequest")(function* (
14
- input: Parameters<typeof fetch>[0],
15
- init: Parameters<typeof fetch>[1],
16
- endpoint: string,
17
- ) {
18
- const config = yield* ProviderConfig
19
- const session = yield* Session
20
- const accessToken = yield* session.getAccessToken
21
- const project = yield* session.ensureProject
22
- const projectId = project.cloudaicompanionProject
23
-
24
- const url = new URL(input instanceof Request ? input.url : input)
25
-
26
- // Rewrite the URL to use the specified endpoint
27
- const endpointUrl = new URL(endpoint)
28
- url.protocol = endpointUrl.protocol
29
- url.host = endpointUrl.host
30
-
31
- const match = PATH_PATTERN.exec(url.pathname)
32
- if (!match) {
33
- return {
34
- input: url.toString(),
35
- init: init ?? {},
36
- streaming: false,
37
- }
38
- }
39
-
40
- const { model, action } = match.groups
41
- const streaming = action === STREAM_ACTION
42
-
43
- // Transform URL to internal endpoint
44
- url.pathname = `/${CODE_ASSIST_VERSION}:${action}`
45
- if (streaming) {
46
- url.searchParams.set("alt", "sse")
47
- }
48
-
49
- // Transform headers
50
- const headers = new Headers(init?.headers)
51
- headers.delete("x-api-key")
52
- headers.delete("x-goog-api-key")
53
- headers.set("Authorization", `Bearer ${accessToken}`)
54
-
55
- for (const [key, value] of Object.entries(config.HEADERS)) {
56
- headers.set(key, value)
57
- }
58
-
59
- if (streaming) {
60
- headers.set("Accept", "text/event-stream")
61
- }
62
-
63
- // Wrap and transform request
64
- const isJson = typeof init?.body === "string"
65
- const parsedBody = yield* pipe(
66
- Effect.try(() => (isJson ? JSON.parse(init.body as string) : null)),
67
- Effect.orElseSucceed(() => null),
68
- )
69
-
70
- const wrappedBody = {
71
- model,
72
- project: projectId,
73
- request: parsedBody ?? {},
74
- }
75
-
76
- const {
77
- body: transformedBody,
78
- headers: finalHeaders,
79
- url: finalUrl,
80
- } =
81
- config.transformRequest ?
82
- yield* config.transformRequest({
83
- body: wrappedBody,
84
- headers,
85
- url,
86
- } satisfies RequestContext)
87
- : { body: wrappedBody, headers, url }
88
-
89
- const finalBody =
90
- isJson && parsedBody ? JSON.stringify(transformedBody) : init?.body
91
-
92
- return {
93
- input: finalUrl.toString(),
94
- init: {
95
- ...init,
96
- headers: finalHeaders,
97
- body: finalBody,
98
- },
99
- streaming,
100
- }
101
- })
@@ -1,75 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { transformNonStreamingResponse } from "./response"
3
-
4
- describe("transformNonStreamingResponse", () => {
5
- it("unwraps { response: X } to X", async () => {
6
- const original = new Response(
7
- JSON.stringify({ response: { text: "Hello" } }),
8
- { headers: { "content-type": "application/json" } },
9
- )
10
-
11
- const result = await transformNonStreamingResponse(original)
12
- const body = await result.json()
13
-
14
- expect(body).toEqual({ text: "Hello" })
15
- })
16
-
17
- it("passes through response without wrapper", async () => {
18
- const original = new Response(JSON.stringify({ text: "Hello" }), {
19
- headers: { "content-type": "application/json" },
20
- })
21
-
22
- const result = await transformNonStreamingResponse(original)
23
- const body = await result.json()
24
-
25
- expect(body).toEqual({ text: "Hello" })
26
- })
27
-
28
- it("passes through non-JSON responses", async () => {
29
- const original = new Response("plain text", {
30
- headers: { "content-type": "text/plain" },
31
- })
32
-
33
- const result = await transformNonStreamingResponse(original)
34
- const body = await result.text()
35
-
36
- expect(body).toBe("plain text")
37
- })
38
-
39
- it("preserves status code", async () => {
40
- const original = new Response(JSON.stringify({ response: {} }), {
41
- status: 201,
42
- statusText: "Created",
43
- headers: { "content-type": "application/json" },
44
- })
45
-
46
- const result = await transformNonStreamingResponse(original)
47
-
48
- expect(result.status).toBe(201)
49
- expect(result.statusText).toBe("Created")
50
- })
51
-
52
- it("preserves headers", async () => {
53
- const original = new Response(JSON.stringify({ response: {} }), {
54
- headers: {
55
- "content-type": "application/json",
56
- "x-custom-header": "value",
57
- },
58
- })
59
-
60
- const result = await transformNonStreamingResponse(original)
61
-
62
- expect(result.headers.get("x-custom-header")).toBe("value")
63
- })
64
-
65
- it("handles malformed JSON gracefully", async () => {
66
- const original = new Response("not-json{", {
67
- headers: { "content-type": "application/json" },
68
- })
69
-
70
- const result = await transformNonStreamingResponse(original)
71
- const body = await result.text()
72
-
73
- expect(body).toBe("not-json{")
74
- })
75
- })
@@ -1,27 +0,0 @@
1
- export const transformNonStreamingResponse = async (
2
- response: Response,
3
- ): Promise<Response> => {
4
- const contentType = response.headers.get("content-type")
5
-
6
- if (!contentType?.includes("application/json")) {
7
- return response
8
- }
9
-
10
- try {
11
- const cloned = response.clone()
12
- const parsed = (await cloned.json()) as { response?: unknown }
13
-
14
- if (parsed.response !== undefined) {
15
- const { response: responseData, ...rest } = parsed
16
- return new Response(JSON.stringify({ ...rest, ...responseData }), {
17
- status: response.status,
18
- statusText: response.statusText,
19
- headers: response.headers,
20
- })
21
- }
22
- } catch {
23
- // Return original if parse fails
24
- }
25
-
26
- return response
27
- }
@@ -1,108 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { transformStreamingResponse } from "./stream"
3
-
4
- describe("transformStreamingResponse", () => {
5
- const createSSEStream = (events: string[]) => {
6
- const encoder = new TextEncoder()
7
- return new ReadableStream({
8
- start(controller) {
9
- for (const event of events) {
10
- controller.enqueue(encoder.encode(event))
11
- }
12
- controller.close()
13
- },
14
- })
15
- }
16
-
17
- it("unwraps response wrapper from SSE events", async () => {
18
- const stream = createSSEStream([
19
- 'data: {"response":{"text":"Hello"}}\n\n',
20
- 'data: {"response":{"text":" World"}}\n\n',
21
- ])
22
-
23
- const response = new Response(stream, {
24
- headers: { "content-type": "text/event-stream" },
25
- })
26
-
27
- const result = await transformStreamingResponse(response)
28
- const text = await result.text()
29
-
30
- expect(text).toContain('data: {"text":"Hello"}')
31
- expect(text).toContain('data: {"text":" World"}')
32
- })
33
-
34
- it("handles responses without wrapper", async () => {
35
- const stream = createSSEStream(['data: {"text":"Direct"}\n\n'])
36
-
37
- const response = new Response(stream, {
38
- headers: { "content-type": "text/event-stream" },
39
- })
40
-
41
- const result = await transformStreamingResponse(response)
42
- const text = await result.text()
43
-
44
- expect(text).toContain('data: {"text":"Direct"}')
45
- })
46
-
47
- it("returns original response if no body", async () => {
48
- const response = new Response(null, { status: 204 })
49
-
50
- const result = await transformStreamingResponse(response)
51
-
52
- expect(result.status).toBe(204)
53
- })
54
-
55
- it("preserves status and headers", async () => {
56
- const stream = createSSEStream(['data: {"response":{}}\n\n'])
57
-
58
- const response = new Response(stream, {
59
- status: 200,
60
- statusText: "OK",
61
- headers: {
62
- "content-type": "text/event-stream",
63
- "x-custom": "value",
64
- },
65
- })
66
-
67
- const result = await transformStreamingResponse(response)
68
-
69
- expect(result.status).toBe(200)
70
- expect(result.headers.get("x-custom")).toBe("value")
71
- })
72
-
73
- it("handles chunked SSE data", async () => {
74
- // Simulate data split across chunks
75
- const stream = createSSEStream([
76
- 'data: {"resp',
77
- 'onse":{"part":"1"}}\n\ndata: {"response":{"part":"2"}}\n\n',
78
- ])
79
-
80
- const response = new Response(stream, {
81
- headers: { "content-type": "text/event-stream" },
82
- })
83
-
84
- const result = await transformStreamingResponse(response)
85
- const text = await result.text()
86
-
87
- expect(text).toContain('data: {"part":"1"}')
88
- expect(text).toContain('data: {"part":"2"}')
89
- })
90
-
91
- it("filters out non-data lines", async () => {
92
- const stream = createSSEStream([
93
- ": comment\n",
94
- "event: message\n",
95
- 'data: {"response":{"valid":"data"}}\n\n',
96
- ])
97
-
98
- const response = new Response(stream, {
99
- headers: { "content-type": "text/event-stream" },
100
- })
101
-
102
- const result = await transformStreamingResponse(response)
103
- const text = await result.text()
104
-
105
- expect(text).toContain('data: {"valid":"data"}')
106
- expect(text).not.toContain("comment")
107
- })
108
- })
@@ -1,61 +0,0 @@
1
- import {
2
- encoder,
3
- type Event,
4
- makeChannel,
5
- Retry,
6
- } from "@effect/experimental/Sse"
7
- import { pipe, Stream } from "effect"
8
-
9
- const parseAndMerge = (event: Event): string => {
10
- if (!event.data) {
11
- return encoder.write(event)
12
- }
13
-
14
- try {
15
- const parsed = JSON.parse(event.data) as {
16
- response?: Record<string, unknown>
17
- }
18
- if (parsed.response) {
19
- const { response, ...rest } = parsed
20
- return encoder.write({
21
- ...event,
22
- data: JSON.stringify({ ...rest, ...response }),
23
- })
24
- }
25
-
26
- return encoder.write(event)
27
- } catch {
28
- return encoder.write(event)
29
- }
30
- }
31
-
32
- const parseSSE = (body: ReadableStream<Uint8Array>) =>
33
- pipe(
34
- Stream.fromReadableStream(
35
- () => body,
36
- (error) => error,
37
- ),
38
- Stream.decodeText,
39
- Stream.pipeThroughChannel(makeChannel()),
40
- Stream.map((event) =>
41
- Retry.is(event) ? encoder.write(event) : parseAndMerge(event),
42
- ),
43
- Stream.encodeText,
44
- )
45
-
46
- export const transformStreamingResponse = (response: Response) => {
47
- if (!response.body) {
48
- return response
49
- }
50
-
51
- const transformed = parseSSE(response.body)
52
- const readable = Stream.toReadableStream(
53
- transformed as Stream.Stream<Uint8Array, never, never>,
54
- )
55
-
56
- return new Response(readable, {
57
- status: response.status,
58
- statusText: response.statusText,
59
- headers: response.headers,
60
- })
61
- }