opencode-google-auth 0.0.5 → 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.5",
3
+ "version": "0.0.7",
4
4
  "description": "_description_",
5
5
  "keywords": [
6
6
  "opencode-google-auth"
@@ -28,32 +28,31 @@
28
28
  "scripts": {
29
29
  "deploy": "bun build ./src/main.ts --target bun --outfile .opencode/plugin/opencode-google-auth.js",
30
30
  "format": "prettier --write .",
31
- "lint": "oxlint",
31
+ "lint": "oxlint --type-aware",
32
32
  "release": "bumpp && npm publish",
33
33
  "typecheck": "tsgo"
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)
26
+ yield* Effect.log("Trying endpoint", endpoint)
27
+ yield* Effect.log("Input", input)
28
+ yield* Effect.log("Init", init)
38
29
 
39
- const { request, ...loggedBody } = JSON.parse(result.init.body as string)
40
- const generationConfig = request.generationConfig
30
+ const [finalInput, finalInit] = config.requestTransform?.(input, init) ?? [
31
+ input,
32
+ init,
33
+ ]
41
34
 
42
- yield* Effect.log(
43
- "Transformed request (Omitting request except generationConfig) :",
44
- result.streaming,
45
- result.input,
46
- { ...loggedBody, request: { generationConfig } },
47
- )
48
-
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,7 +99,7 @@ export const geminiCli: Plugin = async (context) => {
124
99
  )
125
100
 
126
101
  return {
127
- apiKey: "",
102
+ apiKey: auth.access,
128
103
  fetch: (async (input, init) => {
129
104
  const response = await runtime.runPromise(customFetch(input, init))
130
105
  return response
@@ -169,6 +144,30 @@ export const geminiCli: Plugin = async (context) => {
169
144
  },
170
145
  ],
171
146
  },
147
+ "chat.params": async (input, output) => {
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
+ }),
158
+ )
159
+
160
+ if (config.id === input.model.providerID) {
161
+ output.options = {
162
+ ...output.options,
163
+ projectId: options.projectId,
164
+ } satisfies GoogleGenerativeAIProviderOptions
165
+ }
166
+
167
+ await runtime.runPromise(
168
+ Effect.log("chat.params", config.id, input.model, output),
169
+ )
170
+ },
172
171
  }
173
172
  }
174
173
 
@@ -183,7 +182,10 @@ export const geminiCli: Plugin = async (context) => {
183
182
  // const providerConfig = yield* ProviderConfig
184
183
  // const modelsDev = yield* fetchModelsDev
185
184
 
186
- // return providerConfig.getConfig(modelsDev)
185
+ // const config = providerConfig.getConfig(modelsDev)
186
+ // yield* Effect.log("Config initialized", config.id)
187
+
188
+ // return config
187
189
  // }),
188
190
  // )
189
191
 
@@ -212,7 +214,7 @@ export const geminiCli: Plugin = async (context) => {
212
214
  // )
213
215
 
214
216
  // return {
215
- // apiKey: "",
217
+ // apiKey: auth.access,
216
218
  // fetch: (async (input, init) => {
217
219
  // const response = await runtime.runPromise(customFetch(input, init))
218
220
  // return response
@@ -257,26 +259,37 @@ export const geminiCli: Plugin = async (context) => {
257
259
  // },
258
260
  // ],
259
261
  // },
260
- // "experimental.chat.system.transform": async (_input, output) => {
261
- // // THIS IS REQUIRED OTHERWISE YOU'LL GET 429 OR 403 FOR SOME GODDAMN REASON
262
+ // "experimental.chat.system.transform": async (input, output) => {
262
263
  // output.system.unshift(antigravitySpoof)
263
264
  // },
264
265
  // "chat.params": async (input, output) => {
265
- // await runtime.runPromise(
266
- // Effect.log("chat.params event before:", input.model, output.options),
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
+ // }),
267
278
  // )
268
279
 
269
- // if (input.model.providerID === config.id) {
280
+ // if (config.id === input.model.providerID) {
270
281
  // output.options = {
271
282
  // ...output.options,
272
- // labels: {
273
- // sessionId: input.sessionID,
274
- // },
283
+ // userAgent: "antigravity",
284
+ // requestType: "agent",
285
+ // requestId: `agent-${requestId}`,
286
+ // sessionId: input.sessionID,
287
+ // projectId: options.projectId,
275
288
  // } satisfies GoogleGenerativeAIProviderOptions
276
289
  // }
277
290
 
278
291
  // await runtime.runPromise(
279
- // Effect.log("chat.params event after:", input.model, output.options),
292
+ // Effect.log("chat.params", config.id, input.model, output),
280
293
  // )
281
294
  // },
282
295
  // }
@@ -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<
@@ -247,64 +244,16 @@ export const antigravityConfig = (): ProviderConfigShape => ({
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
- }
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)
276
255
  }
277
256
 
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
- innerRequest.toolConfig = {
284
- functionCallingConfig: {
285
- mode: "VALIDATED",
286
- },
287
- }
288
-
289
- if (isThinking) {
290
- headers.set("anthropic-beta", "interleaved-thinking-2025-05-14")
291
- }
292
- }
293
-
294
- if (sessionId) {
295
- innerRequest.sessionId = sessionId
296
- }
297
-
298
- return {
299
- headers,
300
- url,
301
- body: {
302
- requestType: "agent",
303
- userAgent: "antigravity",
304
- requestId: `agent-${crypto.randomUUID()}`,
305
- ...body,
306
- },
307
- }
308
- }),
309
- skipRequestTransform: true,
257
+ return [_input, { ...init, headers }]
258
+ },
310
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
- }