ethagent 3.3.4 → 4.0.0

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 (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -260
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
@@ -1,391 +0,0 @@
1
- import { getKey } from '../storage/secrets.js'
2
- import type { Message, MessageContentBlock, Provider, ProviderCompleteOptions, StreamEvent } from './contracts.js'
3
- import { ProviderError } from './contracts.js'
4
- import { providerErrorFromResponse } from './errors.js'
5
- import { fetchWithRetryStreamEvents } from './retry.js'
6
- import { iterSseFrames } from './sse.js'
7
- import { hasImageBlocks, ImageLoadError, loadImageBlock } from '../utils/images.js'
8
-
9
- export type GeminiToolDefinition = {
10
- name: string
11
- description: string
12
- parameters: {
13
- type: 'object'
14
- properties?: Record<string, unknown>
15
- required?: string[]
16
- }
17
- }
18
-
19
- type GeminiPart = {
20
- text?: string
21
- functionCall?: {
22
- name?: string
23
- args?: Record<string, unknown>
24
- }
25
- }
26
-
27
- type GeminiChunk = {
28
- candidates?: Array<{
29
- content?: {
30
- parts?: GeminiPart[]
31
- }
32
- finishReason?: string
33
- }>
34
- promptFeedback?: {
35
- blockReason?: string
36
- }
37
- usageMetadata?: {
38
- promptTokenCount?: number
39
- candidatesTokenCount?: number
40
- }
41
- }
42
-
43
- type GeminiContentPart =
44
- | { text: string }
45
- | { inlineData: { mimeType: string; data: string } }
46
- | { functionCall: { name: string; args: Record<string, unknown> } }
47
- | { functionResponse: { name: string; response: Record<string, unknown> } }
48
-
49
- type GeminiContent = {
50
- role: 'user' | 'model'
51
- parts: GeminiContentPart[]
52
- }
53
-
54
- type GeminiPayload = {
55
- contents: GeminiContent[]
56
- systemInstruction?: { parts: Array<{ text: string }> }
57
- generationConfig?: { maxOutputTokens?: number }
58
- tools?: Array<{ functionDeclarations: GeminiToolDefinition[] }>
59
- toolConfig?: { functionCallingConfig: { mode: 'AUTO' } }
60
- }
61
-
62
- type DoneStopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'unknown'
63
-
64
- const READ_TIMEOUT_MS = 45_000
65
-
66
- export type GeminiQuotaInfo = {
67
- retryAfterMs?: number
68
- metric?: string
69
- quotaValue?: string
70
- quotaId?: string
71
- }
72
-
73
- export class GeminiProvider implements Provider {
74
- readonly id = 'gemini' as const
75
- readonly model: string
76
- readonly supportsTools: boolean
77
- private readonly tools: GeminiToolDefinition[]
78
-
79
- constructor(opts: { model: string; tools?: GeminiToolDefinition[] }) {
80
- this.model = opts.model
81
- this.tools = opts.tools ?? []
82
- this.supportsTools = this.tools.length > 0
83
- }
84
-
85
- async *complete(
86
- messages: Message[],
87
- signal: AbortSignal,
88
- options: ProviderCompleteOptions = {},
89
- ): AsyncIterable<StreamEvent> {
90
- const rawApiKey = await getKey('gemini')
91
- const apiKey = rawApiKey?.trim()
92
- if (!apiKey) {
93
- const error = new ProviderError('missing API key for gemini (/doctor to verify)')
94
- yield { type: 'error', message: error.message }
95
- return
96
- }
97
- if (hasImageBlocks(messages) && !supportsGeminiImages(this.model)) {
98
- yield { type: 'error', message: `image input is not enabled for ${this.model}` }
99
- return
100
- }
101
-
102
- let payload: GeminiPayload
103
- try {
104
- payload = await buildGeminiPayload(messages, this.tools, options)
105
- } catch (err: unknown) {
106
- if (err instanceof ImageLoadError) {
107
- yield { type: 'error', message: err.message }
108
- return
109
- }
110
- throw err
111
- }
112
- const modelName = this.model.replace(/^models\//, '')
113
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(modelName)}:streamGenerateContent?alt=sse`
114
-
115
- let response: Response
116
- try {
117
- response = yield* fetchWithRetryStreamEvents(url, {
118
- method: 'POST',
119
- headers: {
120
- 'content-type': 'application/json',
121
- accept: 'text/event-stream',
122
- 'x-goog-api-key': apiKey,
123
- },
124
- body: JSON.stringify(payload),
125
- }, { signal })
126
- } catch (err: unknown) {
127
- if (signal.aborted) return
128
- yield { type: 'error', message: (err as Error).message || 'network error' }
129
- return
130
- }
131
-
132
- if (!response.ok) {
133
- const error = await providerErrorFromResponse(this.id, response)
134
- yield { type: 'error', message: error.message }
135
- return
136
- }
137
- if (!response.body) {
138
- yield { type: 'error', message: 'empty response body' }
139
- return
140
- }
141
-
142
- let inputTokens: number | undefined
143
- let outputTokens: number | undefined
144
- let stopReason: DoneStopReason = 'unknown'
145
- let toolCallIndex = 0
146
- let sawToolCall = false
147
-
148
- try {
149
- for await (const frame of iterSseFrames(response.body, signal, READ_TIMEOUT_MS)) {
150
- let parsed: GeminiChunk
151
- try {
152
- parsed = JSON.parse(frame) as GeminiChunk
153
- } catch {
154
- continue
155
- }
156
-
157
- const blockedReason = parsed.promptFeedback?.blockReason
158
- if (blockedReason) {
159
- throw new ProviderError(`prompt blocked: ${blockedReason.toLowerCase()}`)
160
- }
161
-
162
- const candidate = parsed.candidates?.[0]
163
- const parts = candidate?.content?.parts ?? []
164
- for (const part of parts) {
165
- if (part.text) {
166
- yield { type: 'text', delta: part.text }
167
- continue
168
- }
169
- if (part.functionCall?.name) {
170
- const id = `gemini-tool-${toolCallIndex}`
171
- toolCallIndex += 1
172
- sawToolCall = true
173
- const name = part.functionCall.name
174
- const input = part.functionCall.args ?? {}
175
- yield { type: 'tool_use_start', id, name }
176
- yield { type: 'tool_use_stop', id, name, input }
177
- }
178
- }
179
-
180
- if (candidate?.finishReason) {
181
- stopReason = normalizeFinishReason(candidate.finishReason, sawToolCall)
182
- }
183
-
184
- inputTokens = parsed.usageMetadata?.promptTokenCount ?? inputTokens
185
- outputTokens = parsed.usageMetadata?.candidatesTokenCount ?? outputTokens
186
- }
187
- } catch (err: unknown) {
188
- if (signal.aborted) return
189
- yield { type: 'error', message: (err as Error).message || 'stream error' }
190
- return
191
- }
192
-
193
- if (signal.aborted) return
194
- if (sawToolCall) stopReason = 'tool_use'
195
- yield { type: 'done', inputTokens, outputTokens, stopReason }
196
- }
197
- }
198
-
199
- export async function buildGeminiPayload(
200
- messages: Message[],
201
- tools: GeminiToolDefinition[] = [],
202
- options: ProviderCompleteOptions = {},
203
- ): Promise<GeminiPayload> {
204
- const systemParts: string[] = []
205
- const contents: GeminiContent[] = []
206
- const toolUseNamesById = new Map<string, string>()
207
-
208
- for (const message of messages) {
209
- const blocks = normalizeBlocks(message.content)
210
- if (blocks.length === 0) continue
211
-
212
- if (message.role === 'system') {
213
- const systemText = blocks
214
- .filter((block): block is Extract<MessageContentBlock, { type: 'text' }> => block.type === 'text')
215
- .map(block => block.text)
216
- .join('\n\n')
217
- .trim()
218
- if (systemText) systemParts.push(systemText)
219
- continue
220
- }
221
-
222
- if (message.role === 'assistant') {
223
- const parts: GeminiContentPart[] = []
224
- for (const block of blocks) {
225
- if (block.type === 'text') {
226
- parts.push({ text: block.text })
227
- } else if (block.type === 'tool_use') {
228
- toolUseNamesById.set(block.id, block.name)
229
- parts.push({ functionCall: { name: block.name, args: block.input } })
230
- }
231
- }
232
- if (parts.length > 0) contents.push({ role: 'model', parts })
233
- continue
234
- }
235
-
236
- const parts: GeminiContentPart[] = []
237
- for (const block of blocks) {
238
- if (block.type === 'text') {
239
- parts.push({ text: block.text })
240
- } else if (block.type === 'image') {
241
- const loaded = await loadImageBlock(block)
242
- if (!loaded.dataBase64 || !loaded.mimeType) throw new Error(`could not load image: ${block.path}`)
243
- parts.push({ inlineData: { mimeType: loaded.mimeType, data: loaded.dataBase64 } })
244
- } else if (block.type === 'tool_result') {
245
- const name = toolUseNamesById.get(block.toolUseId) ?? 'unknown'
246
- const response: Record<string, unknown> = block.isError
247
- ? { content: block.content, isError: true }
248
- : { content: block.content }
249
- parts.push({ functionResponse: { name, response } })
250
- }
251
- }
252
- if (parts.length > 0) contents.push({ role: 'user', parts })
253
- }
254
-
255
- const payload: GeminiPayload = { contents }
256
- if (systemParts.length > 0) {
257
- payload.systemInstruction = { parts: [{ text: systemParts.join('\n\n') }] }
258
- }
259
- if (options.maxTokens) {
260
- payload.generationConfig = { maxOutputTokens: options.maxTokens }
261
- }
262
- if (tools.length > 0) {
263
- payload.tools = [{ functionDeclarations: tools }]
264
- payload.toolConfig = { functionCallingConfig: { mode: 'AUTO' } }
265
- }
266
- return payload
267
- }
268
-
269
- function normalizeBlocks(content: Message['content']): MessageContentBlock[] {
270
- if (typeof content === 'string') {
271
- const text = content.trim()
272
- return text ? [{ type: 'text', text }] : []
273
- }
274
- return content.filter(block => {
275
- if (block.type === 'text') return block.text.trim().length > 0
276
- return true
277
- })
278
- }
279
-
280
- export function supportsGeminiImages(model: string): boolean {
281
- const normalized = model.toLowerCase()
282
- return normalized.includes('gemini-1.5')
283
- || normalized.includes('gemini-2.0')
284
- || normalized.includes('gemini-2.5')
285
- }
286
-
287
- function normalizeFinishReason(reason: string, sawToolCall: boolean): DoneStopReason {
288
- if (sawToolCall) return 'tool_use'
289
- switch (reason) {
290
- case 'STOP':
291
- return 'end_turn'
292
- case 'MAX_TOKENS':
293
- return 'max_tokens'
294
- case 'STOP_SEQUENCE':
295
- return 'stop_sequence'
296
- default:
297
- return 'unknown'
298
- }
299
- }
300
-
301
- type GeminiQuotaInfoInternal = GeminiQuotaInfo & { kind: 'quota-failure' | 'rate-limit' }
302
-
303
- function readGeminiQuotaInfo(bodyText: string): GeminiQuotaInfoInternal | undefined {
304
- let body: unknown
305
- try {
306
- body = JSON.parse(bodyText)
307
- } catch {
308
- return undefined
309
- }
310
- if (Array.isArray(body)) body = body[0]
311
- if (!body || typeof body !== 'object') return undefined
312
-
313
- const error = (body as { error?: unknown }).error
314
- if (!error || typeof error !== 'object') return undefined
315
-
316
- const details = (error as { details?: unknown }).details
317
- if (!Array.isArray(details)) return undefined
318
-
319
- let retryAfterMs: number | undefined
320
- let metric: string | undefined
321
- let quotaValue: string | undefined
322
- let quotaId: string | undefined
323
- let isQuotaFailure = false
324
-
325
- for (const detail of details) {
326
- if (!detail || typeof detail !== 'object') continue
327
- const type = (detail as { '@type'?: unknown })['@type']
328
- if (typeof type !== 'string') continue
329
-
330
- if (/RetryInfo$/.test(type)) {
331
- const delay = (detail as { retryDelay?: unknown }).retryDelay
332
- const parsed = parseGoogleDurationMs(delay)
333
- if (parsed !== undefined) retryAfterMs = parsed
334
- } else if (/QuotaFailure$/.test(type)) {
335
- isQuotaFailure = true
336
- const violations = (detail as { violations?: unknown }).violations
337
- if (Array.isArray(violations) && violations.length > 0) {
338
- const first = violations[0] as Record<string, unknown> | undefined
339
- if (first) {
340
- const m = first.metric
341
- const qv = first.quotaValue
342
- const qi = first.quotaId
343
- if (typeof m === 'string') metric = m
344
- if (typeof qv === 'string') quotaValue = qv
345
- if (typeof qi === 'string') quotaId = qi
346
- }
347
- }
348
- }
349
- }
350
-
351
- if (!isQuotaFailure && retryAfterMs === undefined) return undefined
352
- return {
353
- kind: isQuotaFailure ? 'quota-failure' : 'rate-limit',
354
- retryAfterMs,
355
- metric,
356
- quotaValue,
357
- quotaId,
358
- }
359
- }
360
-
361
- function parseGoogleDurationMs(value: unknown): number | undefined {
362
- if (typeof value !== 'string') return undefined
363
- const m = /^(\d+(?:\.\d+)?)s$/.exec(value.trim())
364
- if (!m) return undefined
365
- const seconds = Number(m[1])
366
- return Number.isFinite(seconds) ? Math.round(seconds * 1000) : undefined
367
- }
368
-
369
- export async function formatGeminiRateLimitMessage(response: Response): Promise<string | undefined> {
370
- let bodyText = ''
371
- try { bodyText = await response.text() } catch { return undefined }
372
- const info = readGeminiQuotaInfo(bodyText)
373
- if (!info) return undefined
374
- const exhausted = info.kind === 'quota-failure'
375
- const isFreeTier = info.metric ? /free_tier/i.test(info.metric) : false
376
- const parts = [exhausted ? 'gemini quota hit' : 'gemini rate limit']
377
- if (info.quotaValue) {
378
- parts.push(isFreeTier ? `(free-tier cap: ${info.quotaValue})` : `(cap: ${info.quotaValue})`)
379
- } else if (isFreeTier) {
380
- parts.push('(free-tier cap)')
381
- }
382
- if (info.retryAfterMs !== undefined) {
383
- const seconds = Math.ceil(info.retryAfterMs / 1000)
384
- parts.push(`— retry in ~${seconds}s`)
385
- } else if (exhausted && isFreeTier) {
386
- parts.push('— enable billing on the API key\'s project, or /model to switch')
387
- } else if (exhausted) {
388
- parts.push('— /model to switch providers, or wait for the quota window to reset')
389
- }
390
- return parts.join(' ')
391
- }