@stonerzju/opencode 1.2.17 → 1.2.19

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 (261) hide show
  1. package/bin/opencode +29 -157
  2. package/package.json +29 -29
  3. package/src/acp/agent.ts +4 -4
  4. package/src/acp/session.ts +1 -1
  5. package/src/agent/agent.ts +3 -3
  6. package/src/bun/index.ts +2 -2
  7. package/src/cli/cmd/acp.ts +3 -3
  8. package/src/cli/cmd/debug/file.ts +1 -1
  9. package/src/cli/cmd/github.ts +2 -2
  10. package/src/cli/cmd/pr.ts +1 -1
  11. package/src/cli/cmd/tui/app.tsx +24 -24
  12. package/src/cli/cmd/tui/attach.ts +3 -3
  13. package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
  14. package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
  15. package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
  16. package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
  17. package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
  18. package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
  19. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
  20. package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
  21. package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
  22. package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
  23. package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
  24. package/src/cli/cmd/tui/component/logo.tsx +2 -2
  25. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
  26. package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
  27. package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
  28. package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
  29. package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
  30. package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
  31. package/src/cli/cmd/tui/component/tips.tsx +1 -1
  32. package/src/cli/cmd/tui/context/directory.ts +1 -1
  33. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  34. package/src/cli/cmd/tui/context/keybind.tsx +2 -2
  35. package/src/cli/cmd/tui/context/kv.tsx +2 -2
  36. package/src/cli/cmd/tui/context/local.tsx +6 -6
  37. package/src/cli/cmd/tui/context/sync.tsx +4 -4
  38. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  39. package/src/cli/cmd/tui/context/theme.tsx +2 -2
  40. package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
  41. package/src/cli/cmd/tui/event.ts +2 -2
  42. package/src/cli/cmd/tui/routes/home.tsx +6 -6
  43. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
  44. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
  45. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
  46. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
  47. package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
  48. package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
  49. package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
  50. package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
  51. package/src/cli/cmd/tui/thread.ts +9 -9
  52. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
  53. package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
  54. package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
  55. package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
  56. package/src/cli/cmd/tui/ui/toast.tsx +1 -1
  57. package/src/cli/cmd/tui/util/editor.ts +3 -3
  58. package/src/cli/cmd/tui/util/transcript.ts +1 -1
  59. package/src/cli/cmd/tui/worker.ts +10 -10
  60. package/src/cli/error.ts +1 -1
  61. package/src/cli/ui.ts +1 -1
  62. package/src/cli/upgrade.ts +4 -4
  63. package/src/command/index.ts +1 -1
  64. package/src/config/config.ts +10 -10
  65. package/src/config/markdown.ts +1 -1
  66. package/src/config/migrate-tui-config.ts +5 -5
  67. package/src/config/paths.ts +4 -4
  68. package/src/config/tui.ts +4 -4
  69. package/src/control/control.sql.ts +1 -1
  70. package/src/control/index.ts +1 -1
  71. package/src/control-plane/adaptors/worktree.ts +1 -1
  72. package/src/control-plane/session-proxy-middleware.ts +1 -1
  73. package/src/control-plane/workspace.sql.ts +1 -1
  74. package/src/control-plane/workspace.ts +7 -7
  75. package/src/file/index.ts +1 -1
  76. package/src/file/ripgrep.ts +2 -2
  77. package/src/file/watcher.ts +5 -5
  78. package/src/format/formatter.ts +1 -1
  79. package/src/ide/index.ts +3 -3
  80. package/src/index.ts +1 -1
  81. package/src/installation/index.ts +3 -3
  82. package/src/lsp/client.ts +3 -3
  83. package/src/lsp/index.ts +3 -3
  84. package/src/mcp/index.ts +4 -4
  85. package/src/permission/index.ts +2 -2
  86. package/src/permission/next.ts +10 -10
  87. package/src/plugin/codex.ts +1 -1
  88. package/src/plugin/copilot.ts +2 -2
  89. package/src/plugin/index.ts +1 -1
  90. package/src/project/bootstrap.ts +2 -2
  91. package/src/project/instance.ts +4 -4
  92. package/src/project/project.sql.ts +1 -1
  93. package/src/project/project.ts +5 -5
  94. package/src/project/state.ts +1 -1
  95. package/src/project/vcs.ts +4 -4
  96. package/src/provider/auth.ts +4 -4
  97. package/src/provider/error.ts +1 -1
  98. package/src/provider/models-snapshot.ts +2 -0
  99. package/src/provider/models.ts +1 -1
  100. package/src/provider/provider.ts +2 -2
  101. package/src/provider/transform.ts +2 -2
  102. package/src/pty/index.ts +5 -5
  103. package/src/question/index.ts +5 -5
  104. package/src/server/event.ts +1 -1
  105. package/src/server/mdns.ts +1 -1
  106. package/src/server/routes/global.ts +3 -3
  107. package/src/server/routes/permission.ts +1 -1
  108. package/src/server/routes/pty.ts +1 -1
  109. package/src/server/routes/session.ts +4 -4
  110. package/src/server/routes/tui.ts +1 -1
  111. package/src/server/server.ts +3 -3
  112. package/src/session/compaction.ts +7 -7
  113. package/src/session/index.ts +10 -10
  114. package/src/session/instruction.ts +1 -1
  115. package/src/session/llm.ts +11 -11
  116. package/src/session/message-v2.ts +10 -10
  117. package/src/session/message.ts +1 -1
  118. package/src/session/processor.ts +10 -10
  119. package/src/session/prompt.ts +8 -8
  120. package/src/session/retry.ts +2 -2
  121. package/src/session/revert.ts +1 -1
  122. package/src/session/session.sql.ts +3 -3
  123. package/src/session/status.ts +3 -3
  124. package/src/session/summary.ts +5 -5
  125. package/src/session/system.ts +1 -1
  126. package/src/session/todo.ts +2 -2
  127. package/src/share/share-next.ts +7 -7
  128. package/src/share/share.sql.ts +1 -1
  129. package/src/shell/shell.ts +3 -3
  130. package/src/skill/skill.ts +6 -6
  131. package/src/storage/db.ts +1 -1
  132. package/src/storage/storage.ts +1 -1
  133. package/src/tool/bash.ts +6 -6
  134. package/src/tool/edit.ts +1 -1
  135. package/src/tool/registry.ts +2 -2
  136. package/src/tool/skill.ts +1 -1
  137. package/src/tool/task.ts +3 -3
  138. package/src/util/array.ts +10 -0
  139. package/src/util/binary.ts +41 -0
  140. package/src/util/encode.ts +51 -0
  141. package/src/util/error.ts +54 -0
  142. package/src/util/identifier.ts +48 -0
  143. package/src/util/lazy.ts +4 -16
  144. package/src/util/path.ts +37 -0
  145. package/src/util/retry.ts +41 -0
  146. package/src/util/slug.ts +74 -0
  147. package/src/worktree/index.ts +3 -3
  148. package/AGENTS.md +0 -10
  149. package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
  150. package/Dockerfile +0 -18
  151. package/README.md +0 -15
  152. package/bunfig.toml +0 -7
  153. package/drizzle.config.ts +0 -10
  154. package/script/build.ts +0 -224
  155. package/script/check-migrations.ts +0 -16
  156. package/script/postinstall.mjs +0 -131
  157. package/script/publish.ts +0 -181
  158. package/script/schema.ts +0 -63
  159. package/script/seed-e2e.ts +0 -50
  160. package/sst-env.d.ts +0 -10
  161. package/test/AGENTS.md +0 -81
  162. package/test/acp/agent-interface.test.ts +0 -51
  163. package/test/acp/event-subscription.test.ts +0 -683
  164. package/test/agent/agent.test.ts +0 -689
  165. package/test/bun.test.ts +0 -53
  166. package/test/cli/github-action.test.ts +0 -197
  167. package/test/cli/github-remote.test.ts +0 -80
  168. package/test/cli/import.test.ts +0 -38
  169. package/test/cli/plugin-auth-picker.test.ts +0 -120
  170. package/test/cli/tui/transcript.test.ts +0 -322
  171. package/test/config/agent-color.test.ts +0 -71
  172. package/test/config/config.test.ts +0 -1886
  173. package/test/config/fixtures/empty-frontmatter.md +0 -4
  174. package/test/config/fixtures/frontmatter.md +0 -28
  175. package/test/config/fixtures/markdown-header.md +0 -11
  176. package/test/config/fixtures/no-frontmatter.md +0 -1
  177. package/test/config/fixtures/weird-model-id.md +0 -13
  178. package/test/config/markdown.test.ts +0 -228
  179. package/test/config/tui.test.ts +0 -510
  180. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  181. package/test/control-plane/sse.test.ts +0 -56
  182. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  183. package/test/control-plane/workspace-sync.test.ts +0 -97
  184. package/test/file/ignore.test.ts +0 -10
  185. package/test/file/index.test.ts +0 -394
  186. package/test/file/path-traversal.test.ts +0 -198
  187. package/test/file/ripgrep.test.ts +0 -39
  188. package/test/file/time.test.ts +0 -361
  189. package/test/fixture/db.ts +0 -11
  190. package/test/fixture/fixture.ts +0 -45
  191. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  192. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  193. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  194. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  195. package/test/fixture/skills/index.json +0 -6
  196. package/test/ide/ide.test.ts +0 -82
  197. package/test/keybind.test.ts +0 -421
  198. package/test/lsp/client.test.ts +0 -95
  199. package/test/mcp/headers.test.ts +0 -153
  200. package/test/mcp/oauth-browser.test.ts +0 -249
  201. package/test/memory/abort-leak.test.ts +0 -136
  202. package/test/patch/patch.test.ts +0 -348
  203. package/test/permission/arity.test.ts +0 -33
  204. package/test/permission/next.test.ts +0 -689
  205. package/test/permission-task.test.ts +0 -319
  206. package/test/plugin/auth-override.test.ts +0 -44
  207. package/test/plugin/codex.test.ts +0 -123
  208. package/test/preload.ts +0 -80
  209. package/test/project/project.test.ts +0 -348
  210. package/test/project/worktree-remove.test.ts +0 -65
  211. package/test/provider/amazon-bedrock.test.ts +0 -446
  212. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  213. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  214. package/test/provider/gitlab-duo.test.ts +0 -262
  215. package/test/provider/provider.test.ts +0 -2220
  216. package/test/provider/transform.test.ts +0 -2353
  217. package/test/pty/pty-output-isolation.test.ts +0 -140
  218. package/test/question/question.test.ts +0 -300
  219. package/test/scheduler.test.ts +0 -73
  220. package/test/server/global-session-list.test.ts +0 -89
  221. package/test/server/session-list.test.ts +0 -90
  222. package/test/server/session-select.test.ts +0 -78
  223. package/test/session/compaction.test.ts +0 -423
  224. package/test/session/instruction.test.ts +0 -170
  225. package/test/session/llm.test.ts +0 -667
  226. package/test/session/message-v2.test.ts +0 -924
  227. package/test/session/prompt.test.ts +0 -211
  228. package/test/session/retry.test.ts +0 -188
  229. package/test/session/revert-compact.test.ts +0 -285
  230. package/test/session/session.test.ts +0 -71
  231. package/test/session/structured-output-integration.test.ts +0 -233
  232. package/test/session/structured-output.test.ts +0 -385
  233. package/test/skill/discovery.test.ts +0 -110
  234. package/test/skill/skill.test.ts +0 -388
  235. package/test/snapshot/snapshot.test.ts +0 -1180
  236. package/test/storage/json-migration.test.ts +0 -846
  237. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  238. package/test/tool/apply_patch.test.ts +0 -566
  239. package/test/tool/bash.test.ts +0 -402
  240. package/test/tool/edit.test.ts +0 -496
  241. package/test/tool/external-directory.test.ts +0 -127
  242. package/test/tool/fixtures/large-image.png +0 -0
  243. package/test/tool/fixtures/models-api.json +0 -38413
  244. package/test/tool/grep.test.ts +0 -110
  245. package/test/tool/question.test.ts +0 -107
  246. package/test/tool/read.test.ts +0 -504
  247. package/test/tool/registry.test.ts +0 -122
  248. package/test/tool/skill.test.ts +0 -112
  249. package/test/tool/truncation.test.ts +0 -160
  250. package/test/tool/webfetch.test.ts +0 -100
  251. package/test/tool/write.test.ts +0 -348
  252. package/test/util/filesystem.test.ts +0 -443
  253. package/test/util/format.test.ts +0 -59
  254. package/test/util/glob.test.ts +0 -164
  255. package/test/util/iife.test.ts +0 -36
  256. package/test/util/lazy.test.ts +0 -50
  257. package/test/util/lock.test.ts +0 -72
  258. package/test/util/process.test.ts +0 -59
  259. package/test/util/timeout.test.ts +0 -21
  260. package/test/util/wildcard.test.ts +0 -90
  261. package/tsconfig.json +0 -16
@@ -1,924 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { APICallError } from "ai"
3
- import { MessageV2 } from "../../src/session/message-v2"
4
- import type { Provider } from "../../src/provider/provider"
5
-
6
- const sessionID = "session"
7
- const model: Provider.Model = {
8
- id: "test-model",
9
- providerID: "test",
10
- api: {
11
- id: "test-model",
12
- url: "https://example.com",
13
- npm: "@ai-sdk/openai",
14
- },
15
- name: "Test Model",
16
- capabilities: {
17
- temperature: true,
18
- reasoning: false,
19
- attachment: false,
20
- toolcall: true,
21
- input: {
22
- text: true,
23
- audio: false,
24
- image: false,
25
- video: false,
26
- pdf: false,
27
- },
28
- output: {
29
- text: true,
30
- audio: false,
31
- image: false,
32
- video: false,
33
- pdf: false,
34
- },
35
- interleaved: false,
36
- },
37
- cost: {
38
- input: 0,
39
- output: 0,
40
- cache: {
41
- read: 0,
42
- write: 0,
43
- },
44
- },
45
- limit: {
46
- context: 0,
47
- input: 0,
48
- output: 0,
49
- },
50
- status: "active",
51
- options: {},
52
- headers: {},
53
- release_date: "2026-01-01",
54
- }
55
-
56
- function userInfo(id: string): MessageV2.User {
57
- return {
58
- id,
59
- sessionID,
60
- role: "user",
61
- time: { created: 0 },
62
- agent: "user",
63
- model: { providerID: "test", modelID: "test" },
64
- tools: {},
65
- mode: "",
66
- } as unknown as MessageV2.User
67
- }
68
-
69
- function assistantInfo(
70
- id: string,
71
- parentID: string,
72
- error?: MessageV2.Assistant["error"],
73
- meta?: { providerID: string; modelID: string },
74
- ): MessageV2.Assistant {
75
- const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
76
- return {
77
- id,
78
- sessionID,
79
- role: "assistant",
80
- time: { created: 0 },
81
- error,
82
- parentID,
83
- modelID: infoModel.modelID,
84
- providerID: infoModel.providerID,
85
- mode: "",
86
- agent: "agent",
87
- path: { cwd: "/", root: "/" },
88
- cost: 0,
89
- tokens: {
90
- input: 0,
91
- output: 0,
92
- reasoning: 0,
93
- cache: { read: 0, write: 0 },
94
- },
95
- } as unknown as MessageV2.Assistant
96
- }
97
-
98
- function basePart(messageID: string, id: string) {
99
- return {
100
- id,
101
- sessionID,
102
- messageID,
103
- }
104
- }
105
-
106
- describe("session.message-v2.toModelMessage", () => {
107
- test("filters out messages with no parts", () => {
108
- const input: MessageV2.WithParts[] = [
109
- {
110
- info: userInfo("m-empty"),
111
- parts: [],
112
- },
113
- {
114
- info: userInfo("m-user"),
115
- parts: [
116
- {
117
- ...basePart("m-user", "p1"),
118
- type: "text",
119
- text: "hello",
120
- },
121
- ] as MessageV2.Part[],
122
- },
123
- ]
124
-
125
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
126
- {
127
- role: "user",
128
- content: [{ type: "text", text: "hello" }],
129
- },
130
- ])
131
- })
132
-
133
- test("filters out messages with only ignored parts", () => {
134
- const messageID = "m-user"
135
-
136
- const input: MessageV2.WithParts[] = [
137
- {
138
- info: userInfo(messageID),
139
- parts: [
140
- {
141
- ...basePart(messageID, "p1"),
142
- type: "text",
143
- text: "ignored",
144
- ignored: true,
145
- },
146
- ] as MessageV2.Part[],
147
- },
148
- ]
149
-
150
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
151
- })
152
-
153
- test("includes synthetic text parts", () => {
154
- const messageID = "m-user"
155
-
156
- const input: MessageV2.WithParts[] = [
157
- {
158
- info: userInfo(messageID),
159
- parts: [
160
- {
161
- ...basePart(messageID, "p1"),
162
- type: "text",
163
- text: "hello",
164
- synthetic: true,
165
- },
166
- ] as MessageV2.Part[],
167
- },
168
- {
169
- info: assistantInfo("m-assistant", messageID),
170
- parts: [
171
- {
172
- ...basePart("m-assistant", "a1"),
173
- type: "text",
174
- text: "assistant",
175
- synthetic: true,
176
- },
177
- ] as MessageV2.Part[],
178
- },
179
- ]
180
-
181
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
182
- {
183
- role: "user",
184
- content: [{ type: "text", text: "hello" }],
185
- },
186
- {
187
- role: "assistant",
188
- content: [{ type: "text", text: "assistant" }],
189
- },
190
- ])
191
- })
192
-
193
- test("converts user text/file parts and injects compaction/subtask prompts", () => {
194
- const messageID = "m-user"
195
-
196
- const input: MessageV2.WithParts[] = [
197
- {
198
- info: userInfo(messageID),
199
- parts: [
200
- {
201
- ...basePart(messageID, "p1"),
202
- type: "text",
203
- text: "hello",
204
- },
205
- {
206
- ...basePart(messageID, "p2"),
207
- type: "text",
208
- text: "ignored",
209
- ignored: true,
210
- },
211
- {
212
- ...basePart(messageID, "p3"),
213
- type: "file",
214
- mime: "image/png",
215
- filename: "img.png",
216
- url: "https://example.com/img.png",
217
- },
218
- {
219
- ...basePart(messageID, "p4"),
220
- type: "file",
221
- mime: "text/plain",
222
- filename: "note.txt",
223
- url: "https://example.com/note.txt",
224
- },
225
- {
226
- ...basePart(messageID, "p5"),
227
- type: "file",
228
- mime: "application/x-directory",
229
- filename: "dir",
230
- url: "https://example.com/dir",
231
- },
232
- {
233
- ...basePart(messageID, "p6"),
234
- type: "compaction",
235
- auto: true,
236
- },
237
- {
238
- ...basePart(messageID, "p7"),
239
- type: "subtask",
240
- prompt: "prompt",
241
- description: "desc",
242
- agent: "agent",
243
- },
244
- ] as MessageV2.Part[],
245
- },
246
- ]
247
-
248
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
249
- {
250
- role: "user",
251
- content: [
252
- { type: "text", text: "hello" },
253
- {
254
- type: "file",
255
- mediaType: "image/png",
256
- filename: "img.png",
257
- data: "https://example.com/img.png",
258
- },
259
- { type: "text", text: "What did we do so far?" },
260
- { type: "text", text: "The following tool was executed by the user" },
261
- ],
262
- },
263
- ])
264
- })
265
-
266
- test("converts assistant tool completion into tool-call + tool-result messages with attachments", () => {
267
- const userID = "m-user"
268
- const assistantID = "m-assistant"
269
-
270
- const input: MessageV2.WithParts[] = [
271
- {
272
- info: userInfo(userID),
273
- parts: [
274
- {
275
- ...basePart(userID, "u1"),
276
- type: "text",
277
- text: "run tool",
278
- },
279
- ] as MessageV2.Part[],
280
- },
281
- {
282
- info: assistantInfo(assistantID, userID),
283
- parts: [
284
- {
285
- ...basePart(assistantID, "a1"),
286
- type: "text",
287
- text: "done",
288
- metadata: { openai: { assistant: "meta" } },
289
- },
290
- {
291
- ...basePart(assistantID, "a2"),
292
- type: "tool",
293
- callID: "call-1",
294
- tool: "bash",
295
- state: {
296
- status: "completed",
297
- input: { cmd: "ls" },
298
- output: "ok",
299
- title: "Bash",
300
- metadata: {},
301
- time: { start: 0, end: 1 },
302
- attachments: [
303
- {
304
- ...basePart(assistantID, "file-1"),
305
- type: "file",
306
- mime: "image/png",
307
- filename: "attachment.png",
308
- url: "data:image/png;base64,Zm9v",
309
- },
310
- ],
311
- },
312
- metadata: { openai: { tool: "meta" } },
313
- },
314
- ] as MessageV2.Part[],
315
- },
316
- ]
317
-
318
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
319
- {
320
- role: "user",
321
- content: [{ type: "text", text: "run tool" }],
322
- },
323
- {
324
- role: "assistant",
325
- content: [
326
- { type: "text", text: "done", providerOptions: { openai: { assistant: "meta" } } },
327
- {
328
- type: "tool-call",
329
- toolCallId: "call-1",
330
- toolName: "bash",
331
- input: { cmd: "ls" },
332
- providerExecuted: undefined,
333
- providerOptions: { openai: { tool: "meta" } },
334
- },
335
- ],
336
- },
337
- {
338
- role: "tool",
339
- content: [
340
- {
341
- type: "tool-result",
342
- toolCallId: "call-1",
343
- toolName: "bash",
344
- output: {
345
- type: "content",
346
- value: [
347
- { type: "text", text: "ok" },
348
- { type: "media", mediaType: "image/png", data: "Zm9v" },
349
- ],
350
- },
351
- providerOptions: { openai: { tool: "meta" } },
352
- },
353
- ],
354
- },
355
- ])
356
- })
357
-
358
- test("omits provider metadata when assistant model differs", () => {
359
- const userID = "m-user"
360
- const assistantID = "m-assistant"
361
-
362
- const input: MessageV2.WithParts[] = [
363
- {
364
- info: userInfo(userID),
365
- parts: [
366
- {
367
- ...basePart(userID, "u1"),
368
- type: "text",
369
- text: "run tool",
370
- },
371
- ] as MessageV2.Part[],
372
- },
373
- {
374
- info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
375
- parts: [
376
- {
377
- ...basePart(assistantID, "a1"),
378
- type: "text",
379
- text: "done",
380
- metadata: { openai: { assistant: "meta" } },
381
- },
382
- {
383
- ...basePart(assistantID, "a2"),
384
- type: "tool",
385
- callID: "call-1",
386
- tool: "bash",
387
- state: {
388
- status: "completed",
389
- input: { cmd: "ls" },
390
- output: "ok",
391
- title: "Bash",
392
- metadata: {},
393
- time: { start: 0, end: 1 },
394
- },
395
- metadata: { openai: { tool: "meta" } },
396
- },
397
- ] as MessageV2.Part[],
398
- },
399
- ]
400
-
401
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
402
- {
403
- role: "user",
404
- content: [{ type: "text", text: "run tool" }],
405
- },
406
- {
407
- role: "assistant",
408
- content: [
409
- { type: "text", text: "done" },
410
- {
411
- type: "tool-call",
412
- toolCallId: "call-1",
413
- toolName: "bash",
414
- input: { cmd: "ls" },
415
- providerExecuted: undefined,
416
- },
417
- ],
418
- },
419
- {
420
- role: "tool",
421
- content: [
422
- {
423
- type: "tool-result",
424
- toolCallId: "call-1",
425
- toolName: "bash",
426
- output: { type: "text", value: "ok" },
427
- },
428
- ],
429
- },
430
- ])
431
- })
432
-
433
- test("replaces compacted tool output with placeholder", () => {
434
- const userID = "m-user"
435
- const assistantID = "m-assistant"
436
-
437
- const input: MessageV2.WithParts[] = [
438
- {
439
- info: userInfo(userID),
440
- parts: [
441
- {
442
- ...basePart(userID, "u1"),
443
- type: "text",
444
- text: "run tool",
445
- },
446
- ] as MessageV2.Part[],
447
- },
448
- {
449
- info: assistantInfo(assistantID, userID),
450
- parts: [
451
- {
452
- ...basePart(assistantID, "a1"),
453
- type: "tool",
454
- callID: "call-1",
455
- tool: "bash",
456
- state: {
457
- status: "completed",
458
- input: { cmd: "ls" },
459
- output: "this should be cleared",
460
- title: "Bash",
461
- metadata: {},
462
- time: { start: 0, end: 1, compacted: 1 },
463
- },
464
- },
465
- ] as MessageV2.Part[],
466
- },
467
- ]
468
-
469
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
470
- {
471
- role: "user",
472
- content: [{ type: "text", text: "run tool" }],
473
- },
474
- {
475
- role: "assistant",
476
- content: [
477
- {
478
- type: "tool-call",
479
- toolCallId: "call-1",
480
- toolName: "bash",
481
- input: { cmd: "ls" },
482
- providerExecuted: undefined,
483
- },
484
- ],
485
- },
486
- {
487
- role: "tool",
488
- content: [
489
- {
490
- type: "tool-result",
491
- toolCallId: "call-1",
492
- toolName: "bash",
493
- output: { type: "text", value: "[Old tool result content cleared]" },
494
- },
495
- ],
496
- },
497
- ])
498
- })
499
-
500
- test("converts assistant tool error into error-text tool result", () => {
501
- const userID = "m-user"
502
- const assistantID = "m-assistant"
503
-
504
- const input: MessageV2.WithParts[] = [
505
- {
506
- info: userInfo(userID),
507
- parts: [
508
- {
509
- ...basePart(userID, "u1"),
510
- type: "text",
511
- text: "run tool",
512
- },
513
- ] as MessageV2.Part[],
514
- },
515
- {
516
- info: assistantInfo(assistantID, userID),
517
- parts: [
518
- {
519
- ...basePart(assistantID, "a1"),
520
- type: "tool",
521
- callID: "call-1",
522
- tool: "bash",
523
- state: {
524
- status: "error",
525
- input: { cmd: "ls" },
526
- error: "nope",
527
- time: { start: 0, end: 1 },
528
- metadata: {},
529
- },
530
- metadata: { openai: { tool: "meta" } },
531
- },
532
- ] as MessageV2.Part[],
533
- },
534
- ]
535
-
536
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
537
- {
538
- role: "user",
539
- content: [{ type: "text", text: "run tool" }],
540
- },
541
- {
542
- role: "assistant",
543
- content: [
544
- {
545
- type: "tool-call",
546
- toolCallId: "call-1",
547
- toolName: "bash",
548
- input: { cmd: "ls" },
549
- providerExecuted: undefined,
550
- providerOptions: { openai: { tool: "meta" } },
551
- },
552
- ],
553
- },
554
- {
555
- role: "tool",
556
- content: [
557
- {
558
- type: "tool-result",
559
- toolCallId: "call-1",
560
- toolName: "bash",
561
- output: { type: "error-text", value: "nope" },
562
- providerOptions: { openai: { tool: "meta" } },
563
- },
564
- ],
565
- },
566
- ])
567
- })
568
-
569
- test("filters assistant messages with non-abort errors", () => {
570
- const assistantID = "m-assistant"
571
-
572
- const input: MessageV2.WithParts[] = [
573
- {
574
- info: assistantInfo(
575
- assistantID,
576
- "m-parent",
577
- new MessageV2.APIError({ message: "boom", isRetryable: true }).toObject() as MessageV2.APIError,
578
- ),
579
- parts: [
580
- {
581
- ...basePart(assistantID, "a1"),
582
- type: "text",
583
- text: "should not render",
584
- },
585
- ] as MessageV2.Part[],
586
- },
587
- ]
588
-
589
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
590
- })
591
-
592
- test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => {
593
- const assistantID1 = "m-assistant-1"
594
- const assistantID2 = "m-assistant-2"
595
-
596
- const aborted = new MessageV2.AbortedError({ message: "aborted" }).toObject() as MessageV2.Assistant["error"]
597
-
598
- const input: MessageV2.WithParts[] = [
599
- {
600
- info: assistantInfo(assistantID1, "m-parent", aborted),
601
- parts: [
602
- {
603
- ...basePart(assistantID1, "a1"),
604
- type: "reasoning",
605
- text: "thinking",
606
- time: { start: 0 },
607
- },
608
- {
609
- ...basePart(assistantID1, "a2"),
610
- type: "text",
611
- text: "partial answer",
612
- },
613
- ] as MessageV2.Part[],
614
- },
615
- {
616
- info: assistantInfo(assistantID2, "m-parent", aborted),
617
- parts: [
618
- {
619
- ...basePart(assistantID2, "b1"),
620
- type: "step-start",
621
- },
622
- {
623
- ...basePart(assistantID2, "b2"),
624
- type: "reasoning",
625
- text: "thinking",
626
- time: { start: 0 },
627
- },
628
- ] as MessageV2.Part[],
629
- },
630
- ]
631
-
632
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
633
- {
634
- role: "assistant",
635
- content: [
636
- { type: "reasoning", text: "thinking", providerOptions: undefined },
637
- { type: "text", text: "partial answer" },
638
- ],
639
- },
640
- ])
641
- })
642
-
643
- test("splits assistant messages on step-start boundaries", () => {
644
- const assistantID = "m-assistant"
645
-
646
- const input: MessageV2.WithParts[] = [
647
- {
648
- info: assistantInfo(assistantID, "m-parent"),
649
- parts: [
650
- {
651
- ...basePart(assistantID, "p1"),
652
- type: "text",
653
- text: "first",
654
- },
655
- {
656
- ...basePart(assistantID, "p2"),
657
- type: "step-start",
658
- },
659
- {
660
- ...basePart(assistantID, "p3"),
661
- type: "text",
662
- text: "second",
663
- },
664
- ] as MessageV2.Part[],
665
- },
666
- ]
667
-
668
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
669
- {
670
- role: "assistant",
671
- content: [{ type: "text", text: "first" }],
672
- },
673
- {
674
- role: "assistant",
675
- content: [{ type: "text", text: "second" }],
676
- },
677
- ])
678
- })
679
-
680
- test("drops messages that only contain step-start parts", () => {
681
- const assistantID = "m-assistant"
682
-
683
- const input: MessageV2.WithParts[] = [
684
- {
685
- info: assistantInfo(assistantID, "m-parent"),
686
- parts: [
687
- {
688
- ...basePart(assistantID, "p1"),
689
- type: "step-start",
690
- },
691
- ] as MessageV2.Part[],
692
- },
693
- ]
694
-
695
- expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
696
- })
697
-
698
- test("converts pending/running tool calls to error results to prevent dangling tool_use", () => {
699
- const userID = "m-user"
700
- const assistantID = "m-assistant"
701
-
702
- const input: MessageV2.WithParts[] = [
703
- {
704
- info: userInfo(userID),
705
- parts: [
706
- {
707
- ...basePart(userID, "u1"),
708
- type: "text",
709
- text: "run tool",
710
- },
711
- ] as MessageV2.Part[],
712
- },
713
- {
714
- info: assistantInfo(assistantID, userID),
715
- parts: [
716
- {
717
- ...basePart(assistantID, "a1"),
718
- type: "tool",
719
- callID: "call-pending",
720
- tool: "bash",
721
- state: {
722
- status: "pending",
723
- input: { cmd: "ls" },
724
- raw: "",
725
- },
726
- },
727
- {
728
- ...basePart(assistantID, "a2"),
729
- type: "tool",
730
- callID: "call-running",
731
- tool: "read",
732
- state: {
733
- status: "running",
734
- input: { path: "/tmp" },
735
- time: { start: 0 },
736
- },
737
- },
738
- ] as MessageV2.Part[],
739
- },
740
- ]
741
-
742
- const result = MessageV2.toModelMessages(input, model)
743
-
744
- expect(result).toStrictEqual([
745
- {
746
- role: "user",
747
- content: [{ type: "text", text: "run tool" }],
748
- },
749
- {
750
- role: "assistant",
751
- content: [
752
- {
753
- type: "tool-call",
754
- toolCallId: "call-pending",
755
- toolName: "bash",
756
- input: { cmd: "ls" },
757
- providerExecuted: undefined,
758
- },
759
- {
760
- type: "tool-call",
761
- toolCallId: "call-running",
762
- toolName: "read",
763
- input: { path: "/tmp" },
764
- providerExecuted: undefined,
765
- },
766
- ],
767
- },
768
- {
769
- role: "tool",
770
- content: [
771
- {
772
- type: "tool-result",
773
- toolCallId: "call-pending",
774
- toolName: "bash",
775
- output: { type: "error-text", value: "[Tool execution was interrupted]" },
776
- },
777
- {
778
- type: "tool-result",
779
- toolCallId: "call-running",
780
- toolName: "read",
781
- output: { type: "error-text", value: "[Tool execution was interrupted]" },
782
- },
783
- ],
784
- },
785
- ])
786
- })
787
- })
788
-
789
- describe("session.message-v2.fromError", () => {
790
- test("serializes context_length_exceeded as ContextOverflowError", () => {
791
- const input = {
792
- type: "error",
793
- error: {
794
- code: "context_length_exceeded",
795
- },
796
- }
797
- const result = MessageV2.fromError(input, { providerID: "test" })
798
-
799
- expect(result).toStrictEqual({
800
- name: "ContextOverflowError",
801
- data: {
802
- message: "Input exceeds context window of this model",
803
- responseBody: JSON.stringify(input),
804
- },
805
- })
806
- })
807
-
808
- test("serializes response error codes", () => {
809
- const cases = [
810
- {
811
- code: "insufficient_quota",
812
- message: "Quota exceeded. Check your plan and billing details.",
813
- },
814
- {
815
- code: "usage_not_included",
816
- message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
817
- },
818
- {
819
- code: "invalid_prompt",
820
- message: "Invalid prompt from test",
821
- },
822
- ]
823
-
824
- cases.forEach((item) => {
825
- const input = {
826
- type: "error",
827
- error: {
828
- code: item.code,
829
- message: item.code === "invalid_prompt" ? item.message : undefined,
830
- },
831
- }
832
- const result = MessageV2.fromError(input, { providerID: "test" })
833
-
834
- expect(result).toStrictEqual({
835
- name: "APIError",
836
- data: {
837
- message: item.message,
838
- isRetryable: false,
839
- responseBody: JSON.stringify(input),
840
- },
841
- })
842
- })
843
- })
844
-
845
- test("maps github-copilot 403 to reauth guidance", () => {
846
- const error = new APICallError({
847
- message: "forbidden",
848
- url: "https://api.githubcopilot.com/v1/chat/completions",
849
- requestBodyValues: {},
850
- statusCode: 403,
851
- responseHeaders: { "content-type": "application/json" },
852
- responseBody: '{"error":"forbidden"}',
853
- isRetryable: false,
854
- })
855
-
856
- const result = MessageV2.fromError(error, { providerID: "github-copilot" })
857
-
858
- expect(result).toStrictEqual({
859
- name: "APIError",
860
- data: {
861
- message:
862
- "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode.",
863
- statusCode: 403,
864
- isRetryable: false,
865
- responseHeaders: { "content-type": "application/json" },
866
- responseBody: '{"error":"forbidden"}',
867
- metadata: {
868
- url: "https://api.githubcopilot.com/v1/chat/completions",
869
- },
870
- },
871
- })
872
- })
873
-
874
- test("detects context overflow from APICallError provider messages", () => {
875
- const cases = [
876
- "prompt is too long: 213462 tokens > 200000 maximum",
877
- "Your input exceeds the context window of this model",
878
- "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
879
- "Please reduce the length of the messages or completion",
880
- "400 status code (no body)",
881
- "413 status code (no body)",
882
- ]
883
-
884
- cases.forEach((message) => {
885
- const error = new APICallError({
886
- message,
887
- url: "https://example.com",
888
- requestBodyValues: {},
889
- statusCode: 400,
890
- responseHeaders: { "content-type": "application/json" },
891
- isRetryable: false,
892
- })
893
- const result = MessageV2.fromError(error, { providerID: "test" })
894
- expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true)
895
- })
896
- })
897
-
898
- test("does not classify 429 no body as context overflow", () => {
899
- const result = MessageV2.fromError(
900
- new APICallError({
901
- message: "429 status code (no body)",
902
- url: "https://example.com",
903
- requestBodyValues: {},
904
- statusCode: 429,
905
- responseHeaders: { "content-type": "application/json" },
906
- isRetryable: false,
907
- }),
908
- { providerID: "test" },
909
- )
910
- expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(false)
911
- expect(MessageV2.APIError.isInstance(result)).toBe(true)
912
- })
913
-
914
- test("serializes unknown inputs", () => {
915
- const result = MessageV2.fromError(123, { providerID: "test" })
916
-
917
- expect(result).toStrictEqual({
918
- name: "UnknownError",
919
- data: {
920
- message: "123",
921
- },
922
- })
923
- })
924
- })