snow-flow 10.0.185 → 10.0.186-dev.682

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.
Files changed (62) hide show
  1. package/bin/index.js.map +9 -9
  2. package/bin/worker.js.map +7 -7
  3. package/mcp/servicenow-unified.js +116 -116
  4. package/package.json +1 -1
  5. package/parsers-config.ts +2 -1
  6. package/src/bun/index.ts +10 -9
  7. package/src/cli/cmd/agent.ts +3 -3
  8. package/src/cli/cmd/auth.ts +46 -0
  9. package/src/cli/cmd/import.ts +2 -2
  10. package/src/cli/cmd/session.ts +9 -12
  11. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +2 -1
  12. package/src/cli/cmd/tui/component/prompt/index.tsx +19 -6
  13. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  14. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  15. package/src/cli/cmd/tui/routes/home.tsx +16 -2
  16. package/src/cli/cmd/tui/routes/session/index.tsx +122 -53
  17. package/src/cli/cmd/tui/routes/session/permission.tsx +9 -1
  18. package/src/cli/cmd/tui/routes/session/sidebar.tsx +9 -1
  19. package/src/cli/cmd/tui/thread.ts +4 -1
  20. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +1 -1
  21. package/src/cli/cmd/tui/util/clipboard.ts +3 -3
  22. package/src/cli/cmd/tui/worker.ts +6 -1
  23. package/src/config/config.ts +28 -0
  24. package/src/context/context-db.ts +437 -0
  25. package/src/format/formatter.ts +14 -5
  26. package/src/global/index.ts +3 -4
  27. package/src/mcp/index.ts +7 -2
  28. package/src/mcp/oauth-callback.ts +7 -15
  29. package/src/mcp/oauth-provider.ts +34 -3
  30. package/src/project/project.ts +8 -4
  31. package/src/provider/models.ts +1 -1
  32. package/src/provider/provider.ts +88 -9
  33. package/src/provider/transform.ts +7 -2
  34. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_capacity_plan.ts +20 -7
  35. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_retrospective.ts +6 -8
  36. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_sprint_manage.ts +46 -28
  37. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_team_manage.ts +53 -41
  38. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_velocity_report.ts +8 -1
  39. package/src/servicenow/servicenow-mcp-unified/tools/automation/snow_schedule_script_job.ts +388 -243
  40. package/src/session/compaction.ts +126 -23
  41. package/src/session/message-v2.ts +33 -10
  42. package/src/session/processor.ts +29 -17
  43. package/src/session/prompt.ts +34 -6
  44. package/src/share/share-next.ts +2 -2
  45. package/src/shell/shell.ts +2 -1
  46. package/src/tool/edit.ts +15 -1
  47. package/src/tool/registry.ts +9 -1
  48. package/src/tool/truncation.ts +17 -0
  49. package/src/tool/websearch.ts +1 -1
  50. package/src/tool/websearch.txt +2 -2
  51. package/src/tool/write.ts +3 -4
  52. package/src/util/filesystem.ts +36 -7
  53. package/src/util/keybind.ts +1 -1
  54. package/src/util/log.ts +8 -5
  55. package/src/util/token.ts +28 -0
  56. package/test/cli/plugin-auth-picker.test.ts +120 -0
  57. package/test/fixture/fixture.ts +3 -0
  58. package/test/mcp/oauth-auto-connect.test.ts +197 -0
  59. package/test/project/project.test.ts +47 -0
  60. package/test/provider/provider.test.ts +2 -0
  61. package/test/provider/transform.test.ts +32 -0
  62. package/test/tool/edit.test.ts +679 -0
@@ -1,19 +1,48 @@
1
- import { realpathSync } from "fs"
2
- import { realpath } from "fs/promises"
3
- import { dirname, join, relative, resolve } from "path"
1
+ import { realpathSync, statSync, mkdirSync } from "fs"
2
+ import { readFile, writeFile, stat as fsStat, realpath, mkdir } from "fs/promises"
3
+ import { dirname, join, relative, resolve as pathResolve } from "path"
4
4
 
5
5
  export namespace Filesystem {
6
6
  export const exists = (p: string) =>
7
- Bun.file(p)
8
- .stat()
7
+ fsStat(p)
9
8
  .then(() => true)
10
9
  .catch(() => false)
11
10
 
12
11
  export const isDir = (p: string) =>
13
- Bun.file(p)
14
- .stat()
12
+ fsStat(p)
15
13
  .then((s) => s.isDirectory())
16
14
  .catch(() => false)
15
+
16
+ export async function readText(p: string): Promise<string> {
17
+ return readFile(p, "utf-8")
18
+ }
19
+
20
+ export async function readJson<T = unknown>(p: string): Promise<T> {
21
+ const text = await readFile(p, "utf-8")
22
+ return JSON.parse(text) as T
23
+ }
24
+
25
+ export async function write(p: string, content: string | Buffer | Uint8Array): Promise<void> {
26
+ await mkdir(dirname(p), { recursive: true }).catch(() => {})
27
+ await writeFile(p, content)
28
+ }
29
+
30
+ export async function writeJson(p: string, data: unknown): Promise<void> {
31
+ await mkdir(dirname(p), { recursive: true }).catch(() => {})
32
+ await writeFile(p, JSON.stringify(data, null, 2))
33
+ }
34
+
35
+ export async function readBytes(p: string): Promise<Buffer> {
36
+ return readFile(p)
37
+ }
38
+
39
+ export function stat(p: string) {
40
+ return fsStat(p)
41
+ }
42
+
43
+ export function resolve(...segments: string[]): string {
44
+ return pathResolve(...segments)
45
+ }
17
46
  /**
18
47
  * On Windows, normalize a path to its canonical casing using the filesystem.
19
48
  * This is needed because Windows paths are case-insensitive but LSP servers
@@ -23,7 +23,7 @@ export namespace Keybind {
23
23
  */
24
24
  export function fromParsedKey(key: ParsedKey, leader = false): Info {
25
25
  return {
26
- name: key.name,
26
+ name: key.name === " " ? "space" : key.name,
27
27
  ctrl: key.ctrl,
28
28
  meta: key.meta,
29
29
  shift: key.shift,
package/src/util/log.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import path from "path"
2
2
  import fs from "fs/promises"
3
+ import { createWriteStream } from "fs"
3
4
  import { Global } from "../global"
4
5
  import z from "zod"
5
6
 
@@ -63,13 +64,15 @@ export namespace Log {
63
64
  Global.Path.log,
64
65
  options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
65
66
  )
66
- const logfile = Bun.file(logpath)
67
67
  await fs.truncate(logpath).catch(() => {})
68
- const writer = logfile.writer()
68
+ const stream = createWriteStream(logpath, { flags: "a" })
69
69
  write = async (msg: any) => {
70
- const num = writer.write(msg)
71
- writer.flush()
72
- return num
70
+ return new Promise((resolve, reject) => {
71
+ stream.write(msg, (err) => {
72
+ if (err) reject(err)
73
+ else resolve(msg.length)
74
+ })
75
+ })
73
76
  }
74
77
  }
75
78
 
package/src/util/token.ts CHANGED
@@ -1,7 +1,35 @@
1
1
  export namespace Token {
2
2
  const CHARS_PER_TOKEN = 4
3
3
 
4
+ // Provider-aware ratios for more accurate estimation.
5
+ // Measured against actual tokenizer output for mixed code/text.
6
+ const PROVIDER_RATIOS: Record<string, number> = {
7
+ claude: 3.5,
8
+ anthropic: 3.5,
9
+ gpt: 4.0,
10
+ openai: 4.0,
11
+ gemini: 3.8,
12
+ google: 3.8,
13
+ deepseek: 3.7,
14
+ mistral: 3.8,
15
+ default: 3.7,
16
+ }
17
+
4
18
  export function estimate(input: string) {
5
19
  return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN))
6
20
  }
21
+
22
+ export function estimateForProvider(input: string, providerID?: string) {
23
+ const ratio = ratioForProvider(providerID)
24
+ return Math.max(0, Math.round((input || "").length / ratio))
25
+ }
26
+
27
+ function ratioForProvider(providerID?: string): number {
28
+ if (!providerID) return PROVIDER_RATIOS.default
29
+ const lower = providerID.toLowerCase()
30
+ for (const [key, ratio] of Object.entries(PROVIDER_RATIOS)) {
31
+ if (lower.includes(key)) return ratio
32
+ }
33
+ return PROVIDER_RATIOS.default
34
+ }
7
35
  }
@@ -0,0 +1,120 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import { resolvePluginProviders } from "../../src/cli/cmd/auth"
3
+ import type { Hooks } from "@opencode-ai/plugin"
4
+
5
+ function hookWithAuth(provider: string): Hooks {
6
+ return {
7
+ auth: {
8
+ provider,
9
+ methods: [],
10
+ },
11
+ }
12
+ }
13
+
14
+ function hookWithoutAuth(): Hooks {
15
+ return {}
16
+ }
17
+
18
+ describe("resolvePluginProviders", () => {
19
+ test("returns plugin providers not in models.dev", () => {
20
+ const result = resolvePluginProviders({
21
+ hooks: [hookWithAuth("portkey")],
22
+ existingProviders: {},
23
+ disabled: new Set(),
24
+ providerNames: {},
25
+ })
26
+ expect(result).toEqual([{ id: "portkey", name: "portkey" }])
27
+ })
28
+
29
+ test("skips providers already in models.dev", () => {
30
+ const result = resolvePluginProviders({
31
+ hooks: [hookWithAuth("anthropic")],
32
+ existingProviders: { anthropic: {} },
33
+ disabled: new Set(),
34
+ providerNames: {},
35
+ })
36
+ expect(result).toEqual([])
37
+ })
38
+
39
+ test("deduplicates across plugins", () => {
40
+ const result = resolvePluginProviders({
41
+ hooks: [hookWithAuth("portkey"), hookWithAuth("portkey")],
42
+ existingProviders: {},
43
+ disabled: new Set(),
44
+ providerNames: {},
45
+ })
46
+ expect(result).toEqual([{ id: "portkey", name: "portkey" }])
47
+ })
48
+
49
+ test("respects disabled_providers", () => {
50
+ const result = resolvePluginProviders({
51
+ hooks: [hookWithAuth("portkey")],
52
+ existingProviders: {},
53
+ disabled: new Set(["portkey"]),
54
+ providerNames: {},
55
+ })
56
+ expect(result).toEqual([])
57
+ })
58
+
59
+ test("respects enabled_providers when provider is absent", () => {
60
+ const result = resolvePluginProviders({
61
+ hooks: [hookWithAuth("portkey")],
62
+ existingProviders: {},
63
+ disabled: new Set(),
64
+ enabled: new Set(["anthropic"]),
65
+ providerNames: {},
66
+ })
67
+ expect(result).toEqual([])
68
+ })
69
+
70
+ test("includes provider when in enabled set", () => {
71
+ const result = resolvePluginProviders({
72
+ hooks: [hookWithAuth("portkey")],
73
+ existingProviders: {},
74
+ disabled: new Set(),
75
+ enabled: new Set(["portkey"]),
76
+ providerNames: {},
77
+ })
78
+ expect(result).toEqual([{ id: "portkey", name: "portkey" }])
79
+ })
80
+
81
+ test("resolves name from providerNames", () => {
82
+ const result = resolvePluginProviders({
83
+ hooks: [hookWithAuth("portkey")],
84
+ existingProviders: {},
85
+ disabled: new Set(),
86
+ providerNames: { portkey: "Portkey AI" },
87
+ })
88
+ expect(result).toEqual([{ id: "portkey", name: "Portkey AI" }])
89
+ })
90
+
91
+ test("falls back to id when no name configured", () => {
92
+ const result = resolvePluginProviders({
93
+ hooks: [hookWithAuth("portkey")],
94
+ existingProviders: {},
95
+ disabled: new Set(),
96
+ providerNames: {},
97
+ })
98
+ expect(result).toEqual([{ id: "portkey", name: "portkey" }])
99
+ })
100
+
101
+ test("skips hooks without auth", () => {
102
+ const result = resolvePluginProviders({
103
+ hooks: [hookWithoutAuth(), hookWithAuth("portkey"), hookWithoutAuth()],
104
+ existingProviders: {},
105
+ disabled: new Set(),
106
+ providerNames: {},
107
+ })
108
+ expect(result).toEqual([{ id: "portkey", name: "portkey" }])
109
+ })
110
+
111
+ test("returns empty for no hooks", () => {
112
+ const result = resolvePluginProviders({
113
+ hooks: [],
114
+ existingProviders: {},
115
+ disabled: new Set(),
116
+ providerNames: {},
117
+ })
118
+ expect(result).toEqual([])
119
+ })
120
+ })
@@ -20,6 +20,9 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
20
20
  await fs.mkdir(dirpath, { recursive: true })
21
21
  if (options?.git) {
22
22
  await $`git init`.cwd(dirpath).quiet()
23
+ await $`git config core.fsmonitor false`.cwd(dirpath).quiet()
24
+ await $`git config user.email "test@opencode.test"`.cwd(dirpath).quiet()
25
+ await $`git config user.name "Test"`.cwd(dirpath).quiet()
23
26
  await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
24
27
  }
25
28
  if (options?.config) {
@@ -0,0 +1,197 @@
1
+ import { test, expect, mock, beforeEach } from "bun:test"
2
+
3
+ // Mock UnauthorizedError to match the SDK's class
4
+ class MockUnauthorizedError extends Error {
5
+ constructor(message?: string) {
6
+ super(message ?? "Unauthorized")
7
+ this.name = "UnauthorizedError"
8
+ }
9
+ }
10
+
11
+ // Track what options were passed to each transport constructor
12
+ const transportCalls: Array<{
13
+ type: "streamable" | "sse"
14
+ url: string
15
+ options: { authProvider?: unknown }
16
+ }> = []
17
+
18
+ // Controls whether the mock transport simulates a 401 that triggers the SDK
19
+ // auth flow (which calls provider.state()) or a simple UnauthorizedError.
20
+ let simulateAuthFlow = true
21
+
22
+ // Mock the transport constructors to simulate OAuth auto-auth on 401
23
+ mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
24
+ StreamableHTTPClientTransport: class MockStreamableHTTP {
25
+ authProvider: {
26
+ state?: () => Promise<string>
27
+ redirectToAuthorization?: (url: URL) => Promise<void>
28
+ saveCodeVerifier?: (v: string) => Promise<void>
29
+ } | undefined
30
+ constructor(url: URL, options?: { authProvider?: unknown }) {
31
+ this.authProvider = options?.authProvider as typeof this.authProvider
32
+ transportCalls.push({
33
+ type: "streamable",
34
+ url: url.toString(),
35
+ options: options ?? {},
36
+ })
37
+ }
38
+ async start() {
39
+ // Simulate what the real SDK transport does on 401:
40
+ // It calls auth() which eventually calls provider.state(), then
41
+ // provider.redirectToAuthorization(), then throws UnauthorizedError.
42
+ if (simulateAuthFlow && this.authProvider) {
43
+ // The SDK calls provider.state() to get the OAuth state parameter
44
+ if (this.authProvider.state) {
45
+ await this.authProvider.state()
46
+ }
47
+ // The SDK calls saveCodeVerifier before redirecting
48
+ if (this.authProvider.saveCodeVerifier) {
49
+ await this.authProvider.saveCodeVerifier("test-verifier")
50
+ }
51
+ // The SDK calls redirectToAuthorization to redirect the user
52
+ if (this.authProvider.redirectToAuthorization) {
53
+ await this.authProvider.redirectToAuthorization(new URL("https://auth.example.com/authorize?state=test"))
54
+ }
55
+ throw new MockUnauthorizedError()
56
+ }
57
+ throw new MockUnauthorizedError()
58
+ }
59
+ async finishAuth(_code: string) {}
60
+ },
61
+ }))
62
+
63
+ mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
64
+ SSEClientTransport: class MockSSE {
65
+ constructor(url: URL, options?: { authProvider?: unknown }) {
66
+ transportCalls.push({
67
+ type: "sse",
68
+ url: url.toString(),
69
+ options: options ?? {},
70
+ })
71
+ }
72
+ async start() {
73
+ throw new Error("Mock SSE transport cannot connect")
74
+ }
75
+ },
76
+ }))
77
+
78
+ // Mock the MCP SDK Client
79
+ mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
80
+ Client: class MockClient {
81
+ async connect(transport: { start: () => Promise<void> }) {
82
+ await transport.start()
83
+ }
84
+ },
85
+ }))
86
+
87
+ // Mock UnauthorizedError in the auth module so instanceof checks work
88
+ mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({
89
+ UnauthorizedError: MockUnauthorizedError,
90
+ }))
91
+
92
+ beforeEach(() => {
93
+ transportCalls.length = 0
94
+ simulateAuthFlow = true
95
+ })
96
+
97
+ // Import modules after mocking
98
+ const { MCP } = await import("../../src/mcp/index")
99
+ const { Instance } = await import("../../src/project/instance")
100
+ const { tmpdir } = await import("../fixture/fixture")
101
+
102
+ test("first connect to OAuth server shows needs_auth instead of failed", async () => {
103
+ await using tmp = await tmpdir({
104
+ init: async (dir) => {
105
+ await Bun.write(
106
+ `${dir}/opencode.json`,
107
+ JSON.stringify({
108
+ $schema: "https://opencode.ai/config.json",
109
+ mcp: {
110
+ "test-oauth": {
111
+ type: "remote",
112
+ url: "https://example.com/mcp",
113
+ },
114
+ },
115
+ }),
116
+ )
117
+ },
118
+ })
119
+
120
+ await Instance.provide({
121
+ directory: tmp.path,
122
+ fn: async () => {
123
+ const result = await MCP.add("test-oauth", {
124
+ type: "remote",
125
+ url: "https://example.com/mcp",
126
+ })
127
+
128
+ const serverStatus = result.status as Record<string, { status: string; error?: string }>
129
+
130
+ // The server should be detected as needing auth, NOT as failed.
131
+ // Before the fix, provider.state() would throw a plain Error
132
+ // ("No OAuth state saved for MCP server: test-oauth") which was
133
+ // not caught as UnauthorizedError, causing status to be "failed".
134
+ expect(serverStatus["test-oauth"]).toBeDefined()
135
+ expect(serverStatus["test-oauth"].status).toBe("needs_auth")
136
+ },
137
+ })
138
+ })
139
+
140
+ test("state() generates a new state when none is saved", async () => {
141
+ const { McpOAuthProvider } = await import("../../src/mcp/oauth-provider")
142
+ const { McpAuth } = await import("../../src/mcp/auth")
143
+
144
+ await using tmp = await tmpdir()
145
+
146
+ await Instance.provide({
147
+ directory: tmp.path,
148
+ fn: async () => {
149
+ const provider = new McpOAuthProvider(
150
+ "test-state-gen",
151
+ "https://example.com/mcp",
152
+ {},
153
+ { onRedirect: async () => {} },
154
+ )
155
+
156
+ // Ensure no state exists
157
+ const entryBefore = await McpAuth.get("test-state-gen")
158
+ expect(entryBefore?.oauthState).toBeUndefined()
159
+
160
+ // state() should generate and return a new state, not throw
161
+ const state = await provider.state()
162
+ expect(typeof state).toBe("string")
163
+ expect(state.length).toBe(64) // 32 bytes as hex
164
+
165
+ // The generated state should be persisted
166
+ const entryAfter = await McpAuth.get("test-state-gen")
167
+ expect(entryAfter?.oauthState).toBe(state)
168
+ },
169
+ })
170
+ })
171
+
172
+ test("state() returns existing state when one is saved", async () => {
173
+ const { McpOAuthProvider } = await import("../../src/mcp/oauth-provider")
174
+ const { McpAuth } = await import("../../src/mcp/auth")
175
+
176
+ await using tmp = await tmpdir()
177
+
178
+ await Instance.provide({
179
+ directory: tmp.path,
180
+ fn: async () => {
181
+ const provider = new McpOAuthProvider(
182
+ "test-state-existing",
183
+ "https://example.com/mcp",
184
+ {},
185
+ { onRedirect: async () => {} },
186
+ )
187
+
188
+ // Pre-save a state
189
+ const existingState = "pre-saved-state-value"
190
+ await McpAuth.updateOAuthState("test-state-existing", existingState)
191
+
192
+ // state() should return the existing state
193
+ const state = await provider.state()
194
+ expect(state).toBe(existingState)
195
+ },
196
+ })
197
+ })
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test"
2
2
  import { Project } from "../../src/project/project"
3
+ import { Filesystem } from "../../src/util/filesystem"
3
4
  import { Log } from "../../src/util/log"
4
5
  import { Storage } from "../../src/storage/storage"
5
6
  import { $ } from "bun"
@@ -68,6 +69,52 @@ describe("Project.fromDirectory with worktrees", () => {
68
69
  await $`git worktree remove ${worktreePath}`.cwd(tmp.path).quiet()
69
70
  })
70
71
 
72
+ test("worktree should share project ID with main repo", async () => {
73
+ const p = Project
74
+ await using tmp = await tmpdir({ git: true })
75
+
76
+ const { project: main } = await p.fromDirectory(tmp.path)
77
+
78
+ const worktreePath = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt-shared")
79
+ try {
80
+ await $`git worktree add ${worktreePath} -b shared-${Date.now()}`.cwd(tmp.path).quiet()
81
+
82
+ const { project: wt } = await p.fromDirectory(worktreePath)
83
+
84
+ expect(wt.id).toBe(main.id)
85
+
86
+ // Cache should live in the common .git dir, not the worktree's .git file
87
+ const cache = path.join(tmp.path, ".git", "opencode")
88
+ const exists = await Filesystem.exists(cache)
89
+ expect(exists).toBe(true)
90
+ } finally {
91
+ await $`git worktree remove ${worktreePath}`
92
+ .cwd(tmp.path)
93
+ .quiet()
94
+ .catch(() => {})
95
+ }
96
+ })
97
+
98
+ test("separate clones of the same repo should share project ID", async () => {
99
+ const p = Project
100
+ await using tmp = await tmpdir({ git: true })
101
+
102
+ // Create a bare remote, push, then clone into a second directory
103
+ const bare = tmp.path + "-bare"
104
+ const clone = tmp.path + "-clone"
105
+ try {
106
+ await $`git clone --bare ${tmp.path} ${bare}`.quiet()
107
+ await $`git clone ${bare} ${clone}`.quiet()
108
+
109
+ const { project: a } = await p.fromDirectory(tmp.path)
110
+ const { project: b } = await p.fromDirectory(clone)
111
+
112
+ expect(b.id).toBe(a.id)
113
+ } finally {
114
+ await $`rm -rf ${bare} ${clone}`.quiet().nothrow()
115
+ }
116
+ })
117
+
71
118
  test("should accumulate multiple worktrees in sandboxes", async () => {
72
119
  await using tmp = await tmpdir({ git: true })
73
120
 
@@ -281,6 +281,7 @@ test("env variable takes precedence, config merges options", async () => {
281
281
  anthropic: {
282
282
  options: {
283
283
  timeout: 60000,
284
+ chunkTimeout: 15000,
284
285
  },
285
286
  },
286
287
  },
@@ -298,6 +299,7 @@ test("env variable takes precedence, config merges options", async () => {
298
299
  expect(providers["anthropic"]).toBeDefined()
299
300
  // Config options should be merged
300
301
  expect(providers["anthropic"].options.timeout).toBe(60000)
302
+ expect(providers["anthropic"].options.chunkTimeout).toBe(15000)
301
303
  },
302
304
  })
303
305
  })
@@ -596,6 +596,38 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
596
596
  expect(result[0].content[1]).toEqual({ type: "text", text: "Result" })
597
597
  })
598
598
 
599
+ test("filters empty content for bedrock provider", () => {
600
+ const bedrockModel = {
601
+ ...anthropicModel,
602
+ id: "amazon-bedrock/anthropic.claude-opus-4-6",
603
+ providerID: "amazon-bedrock",
604
+ api: {
605
+ id: "anthropic.claude-opus-4-6",
606
+ url: "https://bedrock-runtime.us-east-1.amazonaws.com",
607
+ npm: "@ai-sdk/amazon-bedrock",
608
+ },
609
+ }
610
+
611
+ const msgs = [
612
+ { role: "user", content: "Hello" },
613
+ { role: "assistant", content: "" },
614
+ {
615
+ role: "assistant",
616
+ content: [
617
+ { type: "text", text: "" },
618
+ { type: "text", text: "Answer" },
619
+ ],
620
+ },
621
+ ] as any[]
622
+
623
+ const result = ProviderTransform.message(msgs, bedrockModel, {})
624
+
625
+ expect(result).toHaveLength(2)
626
+ expect(result[0].content).toBe("Hello")
627
+ expect(result[1].content).toHaveLength(1)
628
+ expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" })
629
+ })
630
+
599
631
  test("does not filter for non-anthropic providers", () => {
600
632
  const openaiModel = {
601
633
  ...anthropicModel,