opencode-google-auth 0.0.2 → 0.0.4

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.2",
3
+ "version": "0.0.4",
4
4
  "description": "_description_",
5
5
  "keywords": [
6
6
  "opencode-google-auth"
@@ -26,9 +26,7 @@
26
26
  "src"
27
27
  ],
28
28
  "scripts": {
29
- "prebuild": "bun ./scripts/fetch-models.ts",
30
- "predeploy": "bun run build",
31
- "deploy": "bun ./scripts/deploy.ts",
29
+ "deploy": "bun build ./src/main.ts --target bun --outfile .opencode/plugin/opencode-google-auth.js",
32
30
  "format": "prettier --write .",
33
31
  "lint": "oxlint",
34
32
  "release": "bumpp && npm publish",
@@ -38,19 +36,21 @@
38
36
  "@effect/experimental": "^0.58.0",
39
37
  "@effect/platform": "^0.94.1",
40
38
  "@effect/platform-bun": "^0.87.0",
41
- "@opencode-ai/plugin": "^1.1.15",
39
+ "@opencode-ai/plugin": "^1.1.18",
42
40
  "arkregex": "^0.0.5",
43
41
  "effect": "^3.19.14",
44
- "google-auth-library": "^10.5.0"
42
+ "google-auth-library": "^10.5.0",
43
+ "open": "^11.0.0",
44
+ "xdg-basedir": "^5.1.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@ai-sdk/google": "^3.0.7",
48
- "@effect/language-service": "^0.64.1",
48
+ "@effect/language-service": "^0.65.0",
49
49
  "@types/bun": "^1.3.5",
50
50
  "@types/yargs": "^17.0.35",
51
- "@typescript/native-preview": "^7.0.0-dev.20260112.1",
52
- "ai": "^6.0.30",
53
- "bumpp": "^10.3.2",
51
+ "@typescript/native-preview": "^7.0.0-dev.20260113.1",
52
+ "ai": "^6.0.33",
53
+ "bumpp": "^10.4.0",
54
54
  "oxlint": "^1.39.0",
55
55
  "oxlint-tsgolint": "^0.11.0",
56
56
  "prettier": "^3.7.4",
@@ -0,0 +1 @@
1
+ You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**
package/src/lib/logger.ts CHANGED
@@ -2,8 +2,9 @@ import { PlatformLogger } from "@effect/platform"
2
2
  import { Effect, Inspectable, Logger, LogLevel, pipe } from "effect"
3
3
  import path from "node:path"
4
4
  import type { OpenCodeLogLevel } from "../types"
5
- import { ProviderConfig } from "../services/config"
5
+ import { PLUGIN_NAME, ProviderConfig } from "../services/config"
6
6
  import { OpenCodeContext } from "../services/opencode"
7
+ import { xdgData } from "xdg-basedir"
7
8
 
8
9
  const makeOpenCodeLogger = Effect.gen(function* () {
9
10
  const openCode = yield* OpenCodeContext
@@ -32,11 +33,13 @@ const makeOpenCodeLogger = Effect.gen(function* () {
32
33
  })
33
34
  })
34
35
 
36
+ const LOG_DIR = xdgData ? path.join(xdgData, "opencode") : import.meta.dir
37
+
35
38
  export const combinedLogger = Effect.gen(function* () {
36
39
  const openCodeLogger = yield* makeOpenCodeLogger
37
40
  const fileLogger = yield* pipe(
38
41
  Logger.jsonLogger,
39
- PlatformLogger.toFile(path.join(import.meta.dir, "plugin.log")),
42
+ PlatformLogger.toFile(path.join(LOG_DIR, `${PLUGIN_NAME}.log`)),
40
43
  )
41
44
 
42
45
  return Logger.zip(openCodeLogger, fileLogger)
package/src/main.ts CHANGED
@@ -17,6 +17,7 @@ import { transformRequest } from "./transform/request"
17
17
  import { transformNonStreamingResponse } from "./transform/response"
18
18
  import { transformStreamingResponse } from "./transform/stream"
19
19
  import type { Credentials, ModelsDev } from "./types"
20
+ import antigravitySpoof from "./antigravity-spoof.txt"
20
21
 
21
22
  const fetchModelsDev = Effect.gen(function* () {
22
23
  const client = yield* HttpClient.HttpClient
@@ -251,6 +252,10 @@ export const antigravity: Plugin = async (context) => {
251
252
  },
252
253
  ],
253
254
  },
255
+ "experimental.chat.system.transform": async (_input, output) => {
256
+ // THIS IS REQUIRED OTHERWISE YOU'LL GET 429 OR 403 FOR SOME GODDAMN REASON
257
+ output.system.unshift(antigravitySpoof)
258
+ },
254
259
  "chat.params": async (input, output) => {
255
260
  await runtime.runPromise(
256
261
  Effect.log("chat.params event before:", input.model, output.options),
@@ -40,8 +40,8 @@ export class ProviderConfig extends Context.Tag("ProviderConfig")<
40
40
  ProviderConfigShape
41
41
  >() {}
42
42
 
43
+ export const PLUGIN_NAME = "opencode-google-auth"
43
44
  export const CODE_ASSIST_VERSION = "v1internal"
44
-
45
45
  export const CLIENT_METADATA = {
46
46
  ideType: "IDE_UNSPECIFIED",
47
47
  platform: "PLATFORM_UNSPECIFIED",
@@ -49,8 +49,8 @@ export const CLIENT_METADATA = {
49
49
  } as const
50
50
 
51
51
  export const GEMINI_CLI_MODELS = [
52
- "gemini-2.5-pro",
53
- "gemini-2.5-flash",
52
+ // "gemini-2.5-pro",
53
+ // "gemini-2.5-flash",
54
54
  "gemini-2.5-flash-lite",
55
55
  "gemini-3-pro-preview",
56
56
  "gemini-3-flash-preview",
@@ -148,7 +148,12 @@ export const antigravityConfig = (): ProviderConfigShape => ({
148
148
  "claude-opus-4-5@20251101"
149
149
  ] as OpenCodeModel
150
150
 
151
- const models: Record<string, OpenCodeModel> = {
151
+ const models: Record<
152
+ string,
153
+ OpenCodeModel & {
154
+ variants?: Record<string, Record<string, unknown>>
155
+ }
156
+ > = {
152
157
  "gemini-3-flash": {
153
158
  ...geminiFlash,
154
159
  id: "gemini-3-flash",
@@ -157,7 +162,6 @@ export const antigravityConfig = (): ProviderConfigShape => ({
157
162
  ...geminiPro,
158
163
  id: "gemini-3-pro-low",
159
164
  name: "Gemini 3 Pro (Low)",
160
- temperature: false,
161
165
  options: {
162
166
  thinkingConfig: {
163
167
  thinkingLevel: "low",
@@ -188,12 +192,12 @@ export const antigravityConfig = (): ProviderConfigShape => ({
188
192
  "claude-sonnet-4-5-thinking": {
189
193
  ...claudeSonnet,
190
194
  id: "claude-sonnet-4-5-thinking",
191
- name: "Claude Sonnet 4.5 (Reasoning)",
195
+ name: "Claude Sonnet 4.5 (Thinking)",
192
196
  },
193
197
  "claude-opus-4-5-thinking": {
194
198
  ...claudeOpus,
195
199
  id: "claude-opus-4-5-thinking",
196
- name: "Claude Opus 4.5 (Reasoning)",
200
+ name: "Claude Opus 4.5 (Thinking)",
197
201
  },
198
202
  }
199
203
 
@@ -201,7 +205,7 @@ export const antigravityConfig = (): ProviderConfigShape => ({
201
205
  ...googleProvider,
202
206
  id: antigravityConfig().SERVICE_NAME,
203
207
  name: antigravityConfig().DISPLAY_NAME,
204
- api: antigravityConfig().ENDPOINTS.at(2) as string,
208
+ api: antigravityConfig().ENDPOINTS.at(0) as string,
205
209
  models,
206
210
  }
207
211
  },
@@ -238,6 +242,24 @@ export const antigravityConfig = (): ProviderConfigShape => ({
238
242
  | Record<string, unknown>
239
243
  | undefined
240
244
 
245
+ const tools = request.tools as
246
+ | Array<{ functionDeclarations?: Array<{ parameters?: unknown }> }>
247
+ | undefined
248
+ if (tools && Array.isArray(tools)) {
249
+ for (const tool of tools) {
250
+ if (
251
+ tool.functionDeclarations
252
+ && Array.isArray(tool.functionDeclarations)
253
+ ) {
254
+ for (const func of tool.functionDeclarations) {
255
+ if (!func.parameters) {
256
+ func.parameters = { type: "object", properties: {} }
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+
241
263
  innerRequest.toolConfig = {
242
264
  functionCallingConfig: {
243
265
  mode: "VALIDATED",
@@ -278,101 +300,18 @@ export const antigravityConfig = (): ProviderConfigShape => ({
278
300
  }
279
301
 
280
302
  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"
303
+ innerRequest.sessionId = sessionId
354
304
  }
355
305
 
356
306
  return {
357
307
  headers,
358
308
  url,
359
309
  body: {
360
- ...body,
361
310
  requestType: "agent",
362
311
  userAgent: "antigravity",
363
312
  requestId: `agent-${crypto.randomUUID()}`,
313
+ ...body,
364
314
  },
365
315
  }
366
316
  }),
367
317
  })
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
- }
@@ -16,6 +16,7 @@ import { Data, Deferred, Effect, Fiber, Schema } from "effect"
16
16
  import { OAuth2Client } from "google-auth-library"
17
17
  import type { BunServeOptions } from "../types"
18
18
  import { ProviderConfig } from "./config"
19
+ import open from "open"
19
20
 
20
21
  export class OAuthError extends Data.TaggedError("OAuthError")<{
21
22
  readonly reason: "browser" | "callback" | "state_mismatch" | "token_exchange"
@@ -69,7 +70,17 @@ class OAuth extends Effect.Service<OAuth>()("OAuth", {
69
70
  scope: config.SCOPES as unknown as string[],
70
71
  prompt: "consent",
71
72
  })
73
+
72
74
  yield* Effect.log(`OAuth2 authorization URL: ${authUrl}`)
75
+ yield* Effect.tryPromise({
76
+ try: () => open(authUrl),
77
+ catch: (cause) =>
78
+ new OAuthError({
79
+ reason: "browser",
80
+ message: "Failed to open browser",
81
+ cause,
82
+ }),
83
+ })
73
84
 
74
85
  const serverFiber = yield* HttpRouter.empty.pipe(
75
86
  HttpRouter.get(
@@ -49,8 +49,7 @@ export const transformRequest = Effect.fn("transformRequest")(function* (
49
49
  // Transform headers
50
50
  const headers = new Headers(init?.headers)
51
51
  headers.delete("x-api-key")
52
- // headers.delete("x-goog-api-key")
53
- headers.set("x-opencode-tools-debug", "1")
52
+ headers.delete("x-goog-api-key")
54
53
  headers.set("Authorization", `Bearer ${accessToken}`)
55
54
 
56
55
  for (const [key, value] of Object.entries(config.HEADERS)) {
@@ -69,8 +68,8 @@ export const transformRequest = Effect.fn("transformRequest")(function* (
69
68
  )
70
69
 
71
70
  const wrappedBody = {
72
- project: projectId,
73
71
  model,
72
+ project: projectId,
74
73
  request: parsedBody ?? {},
75
74
  }
76
75
 
package/src/models.json DELETED
@@ -1,155 +0,0 @@
1
- {
2
- "id": "google",
3
- "env": ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"],
4
- "npm": "@ai-sdk/google",
5
- "name": "Google",
6
- "doc": "https://ai.google.dev/gemini-api/docs/pricing",
7
- "models": {
8
- "gemini-3-flash-preview": {
9
- "id": "gemini-3-flash-preview",
10
- "name": "Gemini 3 Flash Preview",
11
- "family": "gemini-flash",
12
- "attachment": true,
13
- "reasoning": true,
14
- "tool_call": true,
15
- "structured_output": true,
16
- "temperature": true,
17
- "knowledge": "2025-01",
18
- "release_date": "2025-12-17",
19
- "last_updated": "2025-12-17",
20
- "modalities": {
21
- "input": ["text", "image", "video", "audio", "pdf"],
22
- "output": ["text"]
23
- },
24
- "open_weights": false,
25
- "cost": {
26
- "input": 0.5,
27
- "output": 3,
28
- "cache_read": 0.05,
29
- "context_over_200k": {
30
- "input": 0.5,
31
- "output": 3,
32
- "cache_read": 0.05
33
- }
34
- },
35
- "limit": {
36
- "context": 1048576,
37
- "output": 65536
38
- }
39
- },
40
- "gemini-3-pro-preview": {
41
- "id": "gemini-3-pro-preview",
42
- "name": "Gemini 3 Pro Preview",
43
- "family": "gemini-pro",
44
- "attachment": true,
45
- "reasoning": true,
46
- "tool_call": true,
47
- "structured_output": true,
48
- "temperature": true,
49
- "knowledge": "2025-01",
50
- "release_date": "2025-11-18",
51
- "last_updated": "2025-11-18",
52
- "modalities": {
53
- "input": ["text", "image", "video", "audio", "pdf"],
54
- "output": ["text"]
55
- },
56
- "open_weights": false,
57
- "cost": {
58
- "input": 2,
59
- "output": 12,
60
- "cache_read": 0.2,
61
- "context_over_200k": {
62
- "input": 4,
63
- "output": 18,
64
- "cache_read": 0.4
65
- }
66
- },
67
- "limit": {
68
- "context": 1000000,
69
- "output": 64000
70
- }
71
- },
72
- "gemini-2.5-flash": {
73
- "id": "gemini-2.5-flash",
74
- "name": "Gemini 2.5 Flash",
75
- "family": "gemini-flash",
76
- "attachment": true,
77
- "reasoning": true,
78
- "tool_call": true,
79
- "structured_output": true,
80
- "temperature": true,
81
- "knowledge": "2025-01",
82
- "release_date": "2025-03-20",
83
- "last_updated": "2025-06-05",
84
- "modalities": {
85
- "input": ["text", "image", "audio", "video", "pdf"],
86
- "output": ["text"]
87
- },
88
- "open_weights": false,
89
- "cost": {
90
- "input": 0.3,
91
- "output": 2.5,
92
- "cache_read": 0.075,
93
- "input_audio": 1
94
- },
95
- "limit": {
96
- "context": 1048576,
97
- "output": 65536
98
- }
99
- },
100
- "gemini-2.5-flash-lite": {
101
- "id": "gemini-2.5-flash-lite",
102
- "name": "Gemini 2.5 Flash Lite",
103
- "family": "gemini-flash-lite",
104
- "attachment": true,
105
- "reasoning": true,
106
- "tool_call": true,
107
- "structured_output": true,
108
- "temperature": true,
109
- "knowledge": "2025-01",
110
- "release_date": "2025-06-17",
111
- "last_updated": "2025-06-17",
112
- "modalities": {
113
- "input": ["text", "image", "audio", "video", "pdf"],
114
- "output": ["text"]
115
- },
116
- "open_weights": false,
117
- "cost": {
118
- "input": 0.1,
119
- "output": 0.4,
120
- "cache_read": 0.025
121
- },
122
- "limit": {
123
- "context": 1048576,
124
- "output": 65536
125
- }
126
- },
127
- "gemini-2.5-pro": {
128
- "id": "gemini-2.5-pro",
129
- "name": "Gemini 2.5 Pro",
130
- "family": "gemini-pro",
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-03-20",
138
- "last_updated": "2025-06-05",
139
- "modalities": {
140
- "input": ["text", "image", "audio", "video", "pdf"],
141
- "output": ["text"]
142
- },
143
- "open_weights": false,
144
- "cost": {
145
- "input": 1.25,
146
- "output": 10,
147
- "cache_read": 0.31
148
- },
149
- "limit": {
150
- "context": 1048576,
151
- "output": 65536
152
- }
153
- }
154
- }
155
- }
@@ -1,194 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { transformRequest } from "../transform/request"
3
- import { antigravityConfig, ProviderConfig } from "./config"
4
- import { Session } from "./session"
5
- import { Effect, Layer, pipe } from "effect"
6
- import { generateText } from "ai"
7
- import { createGoogleGenerativeAI } from "@ai-sdk/google"
8
-
9
- describe("Antigravity transformRequest", () => {
10
- const baseParams = {
11
- accessToken: "test-token",
12
- projectId: "test-project-123",
13
- }
14
-
15
- const config = antigravityConfig()
16
-
17
- const MockSession = Layer.succeed(
18
- Session,
19
- Session.of({
20
- getAccessToken: Effect.succeed(baseParams.accessToken),
21
- ensureProject: Effect.succeed({
22
- cloudaicompanionProject: baseParams.projectId,
23
- currentTier: {
24
- id: "free",
25
- name: "Free",
26
- description: "",
27
- userDefinedCloudaicompanionProject: false,
28
- userDefinedProjectId: "test-project-123",
29
- },
30
- allowedTiers: [],
31
- gcpManaged: false,
32
- manageSubscriptionUri: "",
33
- }),
34
- setCredentials: () => Effect.void,
35
- } as unknown as Session),
36
- ).pipe(Layer.provideMerge(Layer.succeed(ProviderConfig, config)))
37
-
38
- const setupTest = () => {
39
- let capturedBody: unknown = null
40
-
41
- const mockFetch = async (input: string | Request, init?: RequestInit) => {
42
- const result = await pipe(
43
- transformRequest(
44
- input,
45
- init as unknown as Parameters<typeof fetch>[1],
46
- "https://example.com",
47
- ),
48
- Effect.provide(MockSession),
49
- Effect.runPromise,
50
- )
51
- capturedBody = JSON.parse(result.init.body as string)
52
-
53
- return new Response(
54
- JSON.stringify({
55
- candidates: [
56
- {
57
- content: {
58
- parts: [{ text: "Hello" }],
59
- role: "model",
60
- },
61
- finishReason: "STOP",
62
- },
63
- ],
64
- usageMetadata: {
65
- promptTokenCount: 1,
66
- candidatesTokenCount: 1,
67
- totalTokenCount: 2,
68
- },
69
- }),
70
- {
71
- status: 200,
72
- headers: { "Content-Type": "application/json" },
73
- },
74
- )
75
- }
76
-
77
- const google = createGoogleGenerativeAI({
78
- apiKey: "test-key",
79
- fetch: mockFetch as typeof fetch,
80
- })
81
-
82
- return {
83
- google,
84
- getCapturedBody: () => capturedBody as Record<string, unknown>,
85
- }
86
- }
87
-
88
- it("propagates sessionId from labels and cleans up labels", async () => {
89
- const { google, getCapturedBody } = setupTest()
90
-
91
- await generateText({
92
- model: google("gemini-1.5-flash"),
93
- prompt: "Hello",
94
- providerOptions: {
95
- google: {
96
- labels: {
97
- sessionId: "test-session-id",
98
- otherLabel: "keep-me",
99
- },
100
- },
101
- },
102
- })
103
-
104
- const body = getCapturedBody()
105
- const innerRequest = body.request as Record<string, unknown>
106
- const labels = innerRequest.labels as Record<string, unknown>
107
-
108
- // sessionId should be moved to innerRequest and transformed
109
- // The actual format is `-${uuid}:${model}:${project}:seed-${hashedSession}`
110
- expect(innerRequest.sessionId).not.toBe("test-session-id")
111
- expect(innerRequest.sessionId).toBeString()
112
- expect(
113
- (innerRequest.sessionId as string).includes("gemini-1.5-flash"),
114
- ).toBe(true)
115
- expect(
116
- (innerRequest.sessionId as string).includes("test-project-123"),
117
- ).toBe(true)
118
-
119
- // labels should be cleaned up (sessionId removed)
120
- expect(labels.sessionId).toBeUndefined()
121
- expect(labels.otherLabel).toBe("keep-me")
122
-
123
- // Metadata should be present
124
- expect(body.requestType).toBe("agent")
125
- expect(body.userAgent).toBe("antigravity")
126
- expect(body.requestId).toBeDefined()
127
- })
128
-
129
- it("removes labels object if it becomes empty after sessionId extraction", async () => {
130
- const { google, getCapturedBody } = setupTest()
131
-
132
- await generateText({
133
- model: google("gemini-1.5-flash"),
134
- prompt: "Hello",
135
- providerOptions: {
136
- google: {
137
- labels: {
138
- sessionId: "test-session-id",
139
- },
140
- },
141
- },
142
- })
143
-
144
- const body = getCapturedBody()
145
- const innerRequest = body.request as Record<string, unknown>
146
-
147
- expect(innerRequest.sessionId).not.toBe("test-session-id")
148
- expect(innerRequest.sessionId).toBeString()
149
- expect(innerRequest.labels).toBeUndefined()
150
- })
151
-
152
- it("replaces todoread tool definition with placeholder schema", async () => {
153
- const { google, getCapturedBody } = setupTest()
154
-
155
- await generateText({
156
- model: google("gemini-1.5-flash"),
157
- prompt: "Hello",
158
- tools: {
159
- todoread: {
160
- description: "Use this tool to read your todo list",
161
- parameters: { type: "object", properties: {} },
162
- },
163
- },
164
- })
165
-
166
- const body = getCapturedBody()
167
- const innerRequest = body.request as Record<string, unknown>
168
- const tools = innerRequest.tools as Array<Record<string, unknown>>
169
- const firstTool = tools[0]
170
- expect(firstTool).toBeDefined()
171
- if (!firstTool) throw new Error("firstTool is undefined")
172
- const functionDeclarations = firstTool.functionDeclarations as Array<
173
- Record<string, unknown>
174
- >
175
- const todoread = functionDeclarations.find((f) => f.name === "todoread")
176
-
177
- expect(todoread).toBeDefined()
178
- expect(todoread).toEqual({
179
- name: "todoread",
180
- description: "Use this tool to read your todo list",
181
- parameters: {
182
- type: "object",
183
- properties: {
184
- _placeholder: {
185
- type: "boolean",
186
- description: "Placeholder. Always pass true.",
187
- },
188
- },
189
- required: ["_placeholder"],
190
- additionalProperties: false,
191
- },
192
- })
193
- })
194
- })