okengine 0.21.1 → 0.23.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 (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -71,21 +71,21 @@ describe("ImagesConfig nesting — recipe coverage", () => {
71
71
  image: "rustfs/rustfs:1.0.0",
72
72
  },
73
73
  {
74
- images: { store: { index: "getmeili/meilisearch:v1.53" } },
74
+ images: { store: { index: "getmeili/meilisearch:v1.54" } },
75
75
  role: "store.index",
76
- image: "getmeili/meilisearch:v1.53",
76
+ image: "getmeili/meilisearch:v1.54",
77
77
  },
78
78
  // channel.email
79
79
  {
80
- images: { channel: { email: "axllent/mailpit:v1.31.1" } },
80
+ images: { channel: { email: "axllent/mailpit:v1.31.2" } },
81
81
  role: "channel.email",
82
- image: "axllent/mailpit:v1.31.1",
82
+ image: "axllent/mailpit:v1.31.2",
83
83
  },
84
84
  // ai role: no compose recipes (BYO OKE_AI_URL) — not covered here
85
85
  {
86
- images: { pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57" },
86
+ images: { pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59" },
87
87
  role: "pgdog",
88
- image: "ghcr.io/pgdogdev/pgdog:v0.1.57",
88
+ image: "ghcr.io/pgdogdev/pgdog:v0.1.59",
89
89
  },
90
90
  { images: { proxy: "caddy:2-alpine" }, role: "proxy", image: "caddy:2-alpine" },
91
91
  { images: { proxy: "traefik:v3.1" }, role: "proxy", image: "traefik:v3.1" },
@@ -108,11 +108,11 @@ describe("ImagesConfig nesting — compose output parity", () => {
108
108
  test("nested config derives byte-identical output to its flat-shape equivalent", () => {
109
109
  const flatImages: Readonly<Record<string, string>> = {
110
110
  "store.sql": "postgres:18-alpine",
111
- pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
111
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
112
112
  "store.kv": "redis:8-alpine",
113
113
  "store.files": "rustfs/rustfs:1.0.0",
114
- "store.index": "getmeili/meilisearch:v1.53",
115
- "channel.email": "axllent/mailpit:v1.31.1",
114
+ "store.index": "getmeili/meilisearch:v1.54",
115
+ "channel.email": "axllent/mailpit:v1.31.2",
116
116
  };
117
117
 
118
118
  const nestedImages: ImagesConfig = {
@@ -120,10 +120,10 @@ describe("ImagesConfig nesting — compose output parity", () => {
120
120
  sql: "postgres:18-alpine",
121
121
  kv: "redis:8-alpine",
122
122
  files: "rustfs/rustfs:1.0.0",
123
- index: "getmeili/meilisearch:v1.53",
123
+ index: "getmeili/meilisearch:v1.54",
124
124
  },
125
- channel: { email: "axllent/mailpit:v1.31.1" },
126
- pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
125
+ channel: { email: "axllent/mailpit:v1.31.2" },
126
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
127
127
  };
128
128
 
129
129
  // Same nested→flat set, written in a different field order — proves the
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Dragonfly image recipe — Redis-wire-compatible, multi-threaded alternative.
2
+ * Dragonfly image recipe — default `store.kv` pin.
3
3
  *
4
- * Peer of Redis / Valkey via `images["store.kv"]` (driver id stays `redis`).
4
+ * Redis-wire-compatible and multi-threaded. Peer of Redis / Valkey via
5
+ * `images["store.kv"]` (driver id stays `redis`).
5
6
  * Official image: `docker.dragonflydb.io/dragonflydb/dragonfly`.
6
7
  */
7
8
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Redis image recipe — default `store.kv` pin (`redis:*`).
2
+ * Redis image recipe — opt-in `store.kv` pin (`redis:*`).
3
3
  *
4
4
  * Valkey and Dragonfly are separate recipes — same `redis` driver /
5
5
  * `redis://` URL, different binaries. Pin via `images["store.kv"]`.
@@ -163,7 +163,7 @@ describe("deriveInfrastructure instanceId", () => {
163
163
  const n = instancePortOffset(id);
164
164
  const result = deriveInfrastructure({
165
165
  images: {
166
- "channel.email": "axllent/mailpit:v1.31.1",
166
+ "channel.email": "axllent/mailpit:v1.31.2",
167
167
  "store.files": "rustfs/rustfs:1.0.0",
168
168
  },
169
169
  app: `dev-${id}`,
@@ -11,8 +11,11 @@ import type {
11
11
  AiDriver,
12
12
  AiModelClient,
13
13
  AiOpenOptions,
14
+ AiStreamChunk,
15
+ AiToolCall,
14
16
  } from "./ai-types.ts";
15
17
  import { hostFromUrl } from "./external.ts";
18
+ import { preconnectFetch } from "./ai-preconnect.ts";
16
19
 
17
20
  const DEFAULT_BASE = "https://api.anthropic.com";
18
21
  const ANTHROPIC_VERSION = "2023-06-01";
@@ -30,10 +33,7 @@ export async function openAnthropic(options: AiOpenOptions = {}): Promise<AiMode
30
33
  const model = options.model ?? "claude-sonnet-4-20250514";
31
34
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
32
35
  const fetchFn = options.fetch ?? globalThis.fetch;
33
- const preconnect = (fetchFn as { preconnect?: (href: string) => void }).preconnect;
34
- if (typeof preconnect === "function") {
35
- preconnect(baseUrl);
36
- }
36
+ preconnectFetch(fetchFn, baseUrl);
37
37
  const host = hostFromUrl(baseUrl);
38
38
  const external = host
39
39
  ? {
@@ -48,21 +48,10 @@ export async function openAnthropic(options: AiOpenOptions = {}): Promise<AiMode
48
48
  model,
49
49
  async complete(opts: AiCompleteOptions): Promise<AiCompleteResult> {
50
50
  const resolvedModel = opts.model ?? model;
51
- const { system, messages } = splitSystem(opts.messages);
52
51
  const res = await fetchFn(`${baseUrl}/v1/messages`, {
53
52
  method: "POST",
54
- headers: {
55
- "content-type": "application/json",
56
- "x-api-key": apiKey,
57
- "anthropic-version": ANTHROPIC_VERSION,
58
- },
59
- body: JSON.stringify({
60
- model: resolvedModel,
61
- max_tokens: opts.maxTokens ?? 1024,
62
- ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
63
- ...(system !== undefined ? { system } : {}),
64
- messages,
65
- }),
53
+ headers: anthropicHeaders(apiKey),
54
+ body: JSON.stringify(anthropicBody(resolvedModel, opts)),
66
55
  ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
67
56
  });
68
57
  const raw = (await res.json().catch(() => ({}))) as AnthropicMessagesResponse;
@@ -71,11 +60,13 @@ export async function openAnthropic(options: AiOpenOptions = {}): Promise<AiMode
71
60
  throw new Error(`anthropic: ${msg}`);
72
61
  }
73
62
  const text = textFromContent(raw.content);
63
+ const toolCalls = toolCallsFromContent(raw.content);
74
64
  return {
75
65
  text,
76
66
  raw,
77
67
  model: raw.model ?? resolvedModel,
78
68
  driverId: "anthropic",
69
+ ...(toolCalls !== undefined ? { toolCalls } : {}),
79
70
  usage: {
80
71
  inputTokens: raw.usage?.input_tokens,
81
72
  outputTokens: raw.usage?.output_tokens,
@@ -83,6 +74,21 @@ export async function openAnthropic(options: AiOpenOptions = {}): Promise<AiMode
83
74
  ...(external !== undefined ? { external } : {}),
84
75
  };
85
76
  },
77
+ async *stream(opts: AiCompleteOptions): AsyncIterable<AiStreamChunk> {
78
+ const resolvedModel = opts.model ?? model;
79
+ const res = await fetchFn(`${baseUrl}/v1/messages`, {
80
+ method: "POST",
81
+ headers: anthropicHeaders(apiKey),
82
+ body: JSON.stringify({ ...anthropicBody(resolvedModel, opts), stream: true }),
83
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
84
+ });
85
+ if (!res.ok) {
86
+ const raw = (await res.json().catch(() => ({}))) as AnthropicMessagesResponse;
87
+ const msg = raw.error?.message ?? `anthropic HTTP ${res.status}`;
88
+ throw new Error(`anthropic: ${msg}`);
89
+ }
90
+ yield* readAnthropicSse(res, opts.signal);
91
+ },
86
92
  };
87
93
  }
88
94
 
@@ -92,9 +98,17 @@ export const anthropicAiDriver: AiDriver = {
92
98
  open: openAnthropic,
93
99
  };
94
100
 
101
+ interface AnthropicBlock {
102
+ readonly type?: string;
103
+ readonly text?: string;
104
+ readonly id?: string;
105
+ readonly name?: string;
106
+ readonly input?: unknown;
107
+ }
108
+
95
109
  interface AnthropicMessagesResponse {
96
110
  readonly model?: string;
97
- readonly content?: readonly { readonly type?: string; readonly text?: string }[];
111
+ readonly content?: readonly AnthropicBlock[];
98
112
  readonly usage?: {
99
113
  readonly input_tokens?: number;
100
114
  readonly output_tokens?: number;
@@ -102,19 +116,70 @@ interface AnthropicMessagesResponse {
102
116
  readonly error?: { readonly message?: string };
103
117
  }
104
118
 
119
+ function anthropicHeaders(apiKey: string): Record<string, string> {
120
+ return {
121
+ "content-type": "application/json",
122
+ "x-api-key": apiKey,
123
+ "anthropic-version": ANTHROPIC_VERSION,
124
+ };
125
+ }
126
+
127
+ function anthropicBody(model: string, opts: AiCompleteOptions): Record<string, unknown> {
128
+ const { system, messages } = splitSystem(opts.messages);
129
+ return {
130
+ model,
131
+ max_tokens: opts.maxTokens ?? 1024,
132
+ ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
133
+ ...(system !== undefined ? { system } : {}),
134
+ messages,
135
+ ...(opts.tools !== undefined && opts.tools.length > 0
136
+ ? {
137
+ tools: opts.tools.map((tool) => ({
138
+ name: tool.name,
139
+ ...(tool.description !== undefined ? { description: tool.description } : {}),
140
+ input_schema: tool.parameters ?? { type: "object", properties: {} },
141
+ })),
142
+ }
143
+ : {}),
144
+ };
145
+ }
146
+
105
147
  function splitSystem(messages: AiCompleteOptions["messages"]): {
106
148
  system?: string;
107
- messages: Array<{ role: "user" | "assistant"; content: string }>;
149
+ messages: Array<{ role: "user" | "assistant"; content: unknown }>;
108
150
  } {
109
151
  const systemParts: string[] = [];
110
- const out: Array<{ role: "user" | "assistant"; content: string }> = [];
152
+ const out: Array<{ role: "user" | "assistant"; content: unknown }> = [];
111
153
  for (const m of messages) {
112
154
  if (m.role === "system") {
113
155
  systemParts.push(m.content);
114
156
  continue;
115
157
  }
116
158
  if (m.role === "tool") {
117
- out.push({ role: "user", content: m.content });
159
+ out.push({
160
+ role: "user",
161
+ content: [
162
+ {
163
+ type: "tool_result",
164
+ tool_use_id: m.toolCallId ?? "",
165
+ content: m.content,
166
+ },
167
+ ],
168
+ });
169
+ continue;
170
+ }
171
+ if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
172
+ const blocks: unknown[] = [];
173
+ if (m.content) blocks.push({ type: "text", text: m.content });
174
+ for (const call of m.toolCalls) {
175
+ blocks.push({
176
+ type: "tool_use",
177
+ id: call.id,
178
+ name: call.name,
179
+ input: call.arguments ?? {},
180
+ });
181
+ }
182
+ out.push({ role: "assistant", content: blocks });
118
183
  continue;
119
184
  }
120
185
  out.push({ role: m.role, content: m.content });
@@ -132,3 +197,129 @@ function textFromContent(content: AnthropicMessagesResponse["content"]): string
132
197
  .map((b) => b.text!)
133
198
  .join("");
134
199
  }
200
+
201
+ function toolCallsFromContent(
202
+ content: AnthropicMessagesResponse["content"],
203
+ ): readonly AiToolCall[] | undefined {
204
+ const calls = (content ?? []).filter((block) => block.type === "tool_use" && block.name);
205
+ if (calls.length === 0) return undefined;
206
+ return calls.map((block, index) => ({
207
+ id: block.id ?? `toolu_${index}`,
208
+ name: block.name ?? "",
209
+ arguments: block.input ?? {},
210
+ }));
211
+ }
212
+
213
+ /**
214
+ * Parse Anthropic Messages SSE into text and tool-call chunks.
215
+ *
216
+ * @param res - Streaming response
217
+ * @param signal - Optional abort
218
+ */
219
+ async function* readAnthropicSse(
220
+ res: Response,
221
+ signal?: AbortSignal,
222
+ ): AsyncGenerator<AiStreamChunk> {
223
+ const stream = (res as Response & { textStream?: () => AsyncIterable<string> }).textStream;
224
+ if (typeof stream !== "function") {
225
+ throw new Error("anthropic: Response.textStream is required (Bun >= 1.4.2)");
226
+ }
227
+ let buffer = "";
228
+ let inputTokens: number | undefined;
229
+ let outputTokens: number | undefined;
230
+ let stopped = false;
231
+ for await (const piece of stream.call(res)) {
232
+ if (signal?.aborted) {
233
+ const err = new Error("This operation was aborted");
234
+ err.name = "AbortError";
235
+ throw err;
236
+ }
237
+ buffer += piece;
238
+ const lines = buffer.split("\n");
239
+ buffer = lines.pop() ?? "";
240
+ for (const line of lines) {
241
+ const trimmed = line.trim();
242
+ if (!trimmed.startsWith("data:")) continue;
243
+ const data = trimmed.slice(5).trim();
244
+ if (!data || data === "[DONE]") continue;
245
+ let event: {
246
+ type?: string;
247
+ index?: number;
248
+ content_block?: AnthropicBlock;
249
+ delta?: {
250
+ type?: string;
251
+ text?: string;
252
+ partial_json?: string;
253
+ stop_reason?: string;
254
+ };
255
+ message?: { usage?: { input_tokens?: number; output_tokens?: number } };
256
+ usage?: { output_tokens?: number; input_tokens?: number };
257
+ };
258
+ try {
259
+ event = JSON.parse(data) as typeof event;
260
+ } catch {
261
+ continue;
262
+ }
263
+ if (event.type === "error") {
264
+ const message =
265
+ typeof (event as { error?: { message?: string } }).error?.message === "string"
266
+ ? (event as { error?: { message?: string } }).error?.message
267
+ : "anthropic stream error";
268
+ throw new Error(`anthropic: ${message}`);
269
+ }
270
+ if (event.type === "message_stop") {
271
+ stopped = true;
272
+ continue;
273
+ }
274
+ if (event.type === "message_start") {
275
+ inputTokens = event.message?.usage?.input_tokens ?? inputTokens;
276
+ continue;
277
+ }
278
+ if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
279
+ yield {
280
+ text: "",
281
+ toolCall: {
282
+ index: event.index ?? 0,
283
+ ...(event.content_block.id !== undefined ? { id: event.content_block.id } : {}),
284
+ ...(event.content_block.name !== undefined ? { name: event.content_block.name } : {}),
285
+ argumentsDelta: "",
286
+ },
287
+ };
288
+ continue;
289
+ }
290
+ if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
291
+ if (event.delta.text) yield { text: event.delta.text };
292
+ continue;
293
+ }
294
+ if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta") {
295
+ yield {
296
+ text: "",
297
+ toolCall: {
298
+ index: event.index ?? 0,
299
+ ...(event.delta.partial_json !== undefined
300
+ ? { argumentsDelta: event.delta.partial_json }
301
+ : {}),
302
+ },
303
+ };
304
+ continue;
305
+ }
306
+ if (event.type === "message_delta") {
307
+ outputTokens = event.usage?.output_tokens ?? outputTokens;
308
+ inputTokens = event.usage?.input_tokens ?? inputTokens;
309
+ }
310
+ }
311
+ }
312
+ if (!stopped) throw new Error("anthropic: stream ended without message_stop");
313
+ yield {
314
+ text: "",
315
+ done: true,
316
+ ...(inputTokens !== undefined || outputTokens !== undefined
317
+ ? {
318
+ usage: {
319
+ ...(inputTokens !== undefined ? { inputTokens } : {}),
320
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
321
+ },
322
+ }
323
+ : {}),
324
+ };
325
+ }
@@ -97,6 +97,39 @@ export const mockAiDriver: AiDriver = {
97
97
  break;
98
98
  }
99
99
  }
100
+ if (
101
+ payload &&
102
+ typeof payload === "object" &&
103
+ payload !== null &&
104
+ "__toolCalls" in payload
105
+ ) {
106
+ const script = payload as { __toolCalls: readonly AiToolCall[]; text?: string };
107
+ const text = script.text ?? "";
108
+ const size = Math.max(1, Math.ceil(text.length / 2) || 1);
109
+ for (let i = 0; i < text.length; i += size) {
110
+ yield { text: text.slice(i, i + size) };
111
+ }
112
+ for (const [index, tc] of script.__toolCalls.entries()) {
113
+ const args =
114
+ typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments ?? {});
115
+ yield {
116
+ text: "",
117
+ toolCall: { index, id: tc.id, name: tc.name, argumentsDelta: "" },
118
+ };
119
+ const mid = Math.max(1, Math.ceil(args.length / 2));
120
+ yield { text: "", toolCall: { index, argumentsDelta: args.slice(0, mid) } };
121
+ if (mid < args.length) {
122
+ yield { text: "", toolCall: { index, argumentsDelta: args.slice(mid) } };
123
+ }
124
+ }
125
+ yield {
126
+ text: "",
127
+ done: true,
128
+ raw: payload,
129
+ usage: { inputTokens: last.length, outputTokens: text.length, cost: 0 },
130
+ };
131
+ return;
132
+ }
100
133
  const text = typeof payload === "string" ? payload : JSON.stringify(payload);
101
134
  const size = Math.max(1, Math.ceil(text.length / 3));
102
135
  for (let i = 0; i < text.length; i += size) {
@@ -107,7 +140,7 @@ export const mockAiDriver: AiDriver = {
107
140
  }
108
141
  yield { text: text.slice(i, i + size) };
109
142
  }
110
- yield { text: "", done: true };
143
+ yield { text: "", done: true, raw: payload };
111
144
  },
112
145
  async embed(opts: AiEmbedOptions): Promise<AiEmbedResult> {
113
146
  const inputs = Array.isArray(opts.input) ? opts.input : [opts.input];
@@ -20,6 +20,7 @@ import type {
20
20
  } from "./ai-types.ts";
21
21
  import type { DriverExternal } from "./external.ts";
22
22
  import { hostFromUrl } from "./external.ts";
23
+ import { preconnectFetch } from "./ai-preconnect.ts";
23
24
 
24
25
  /** Default OpenAI cloud base — apiKey is required for this origin. */
25
26
  export const OPENAI_COMPAT_DEFAULT_BASE = "https://api.openai.com/v1";
@@ -151,9 +152,23 @@ export async function openOpenaiCompatible(options: AiOpenOptions = {}): Promise
151
152
  model: resolvedModel,
152
153
  messages: opts.messages.map(serializeMessage),
153
154
  stream: true,
155
+ stream_options: { include_usage: true },
154
156
  };
155
157
  if (opts.temperature !== undefined) body.temperature = opts.temperature;
156
158
  if (opts.maxTokens !== undefined) body.max_tokens = opts.maxTokens;
159
+ if (opts.responseFormat !== undefined) {
160
+ body.response_format = wireOpenaiResponseFormat(opts.responseFormat);
161
+ }
162
+ if (opts.tools !== undefined && opts.tools.length > 0) {
163
+ body.tools = opts.tools.map((t) => ({
164
+ type: "function",
165
+ function: {
166
+ name: t.name,
167
+ ...(t.description !== undefined ? { description: t.description } : {}),
168
+ ...(t.parameters !== undefined ? { parameters: t.parameters } : {}),
169
+ },
170
+ }));
171
+ }
157
172
 
158
173
  const res = await fetchFn(`${baseUrl}/chat/completions`, {
159
174
  method: "POST",
@@ -166,7 +181,7 @@ export async function openOpenaiCompatible(options: AiOpenOptions = {}): Promise
166
181
  const msg = raw.error?.message ?? `openai-compatible HTTP ${res.status}`;
167
182
  throwHttp(`openai-compatible: ${msg}`, res.status);
168
183
  }
169
- yield* readOpenaiSse(res, opts.signal);
184
+ yield* readOpenaiSse(res, opts.signal, externalBase);
170
185
  },
171
186
  async embed(opts: AiEmbedOptions): Promise<AiEmbedResult> {
172
187
  const resolvedModel = opts.model ?? model;
@@ -290,8 +305,13 @@ function parseToolCalls(
290
305
  * @param res - Streaming response
291
306
  * @param signal - Optional abort
292
307
  */
293
- async function* readOpenaiSse(res: Response, signal?: AbortSignal): AsyncGenerator<AiStreamChunk> {
308
+ async function* readOpenaiSse(
309
+ res: Response,
310
+ signal?: AbortSignal,
311
+ external?: AiStreamChunk["external"],
312
+ ): AsyncGenerator<AiStreamChunk> {
294
313
  let buffer = "";
314
+ let closed = false;
295
315
  for await (const piece of responseTextStream(res)) {
296
316
  if (signal?.aborted) throw abortAsError(signal.reason);
297
317
  buffer += piece;
@@ -302,30 +322,64 @@ async function* readOpenaiSse(res: Response, signal?: AbortSignal): AsyncGenerat
302
322
  if (!trimmed.startsWith("data:")) continue;
303
323
  const data = trimmed.slice(5).trim();
304
324
  if (data === "[DONE]") {
305
- yield { text: "", done: true };
325
+ yield { text: "", done: true, ...(external !== undefined ? { external } : {}) };
306
326
  return;
307
327
  }
308
328
  try {
309
329
  const chunk = JSON.parse(data) as {
310
330
  choices?: readonly {
311
- delta?: { content?: string | null };
331
+ delta?: {
332
+ content?: string | null;
333
+ tool_calls?: readonly {
334
+ index?: number;
335
+ id?: string;
336
+ function?: { name?: string; arguments?: string };
337
+ }[];
338
+ };
312
339
  finish_reason?: string | null;
313
340
  }[];
341
+ usage?: { prompt_tokens?: number; completion_tokens?: number };
314
342
  };
315
- const delta = chunk.choices?.[0]?.delta?.content;
316
- if (typeof delta === "string" && delta.length > 0) {
317
- yield { text: delta };
343
+ if (chunk.choices?.[0]?.finish_reason) closed = true;
344
+ const delta = chunk.choices?.[0]?.delta;
345
+ const content = delta?.content;
346
+ if (typeof content === "string" && content.length > 0) {
347
+ yield { text: content };
348
+ }
349
+ for (const call of delta?.tool_calls ?? []) {
350
+ const args = call.function?.arguments;
351
+ yield {
352
+ text: "",
353
+ toolCall: {
354
+ index: call.index ?? 0,
355
+ ...(call.id !== undefined ? { id: call.id } : {}),
356
+ ...(call.function?.name !== undefined ? { name: call.function.name } : {}),
357
+ ...(typeof args === "string" ? { argumentsDelta: args } : {}),
358
+ },
359
+ };
318
360
  }
319
- if (chunk.choices?.[0]?.finish_reason) {
320
- yield { text: "", done: true };
321
- return;
361
+ if (chunk.usage) {
362
+ yield {
363
+ text: "",
364
+ usage: {
365
+ ...(chunk.usage.prompt_tokens !== undefined
366
+ ? { inputTokens: chunk.usage.prompt_tokens }
367
+ : {}),
368
+ ...(chunk.usage.completion_tokens !== undefined
369
+ ? { outputTokens: chunk.usage.completion_tokens }
370
+ : {}),
371
+ },
372
+ };
322
373
  }
323
- } catch {
324
- // ignore malformed SSE lines
374
+ } catch (err) {
375
+ if (err instanceof Error && err.message.startsWith("openai-compatible:")) throw err;
325
376
  }
326
377
  }
327
378
  }
328
- yield { text: "", done: true };
379
+ if (!closed) {
380
+ throw new Error("openai-compatible: stream ended without [DONE] or finish_reason");
381
+ }
382
+ yield { text: "", done: true, ...(external !== undefined ? { external } : {}) };
329
383
  }
330
384
 
331
385
  /**
@@ -341,19 +395,6 @@ function responseTextStream(res: Response): AsyncIterable<string> {
341
395
  return stream.call(res);
342
396
  }
343
397
 
344
- /**
345
- * Warm DNS+TCP+TLS for a cloud origin. No-op when `fetch` is a test stub.
346
- *
347
- * @param fetchFn - Fetch implementation
348
- * @param url - Provider base URL
349
- */
350
- function preconnectFetch(fetchFn: typeof fetch, url: string): void {
351
- const preconnect = (fetchFn as { preconnect?: (href: string) => void }).preconnect;
352
- if (typeof preconnect === "function") {
353
- preconnect(url);
354
- }
355
- }
356
-
357
398
  function abortAsError(reason?: unknown): Error {
358
399
  if (reason instanceof Error) return reason;
359
400
  const err = new Error(reason !== undefined ? String(reason) : "This operation was aborted");
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Best-effort `fetch.preconnect` for AI HTTP drivers.
3
+ *
4
+ * Bun throws `Invalid port` when the URL uses an implicit 80/443. A failed
5
+ * preconnect must not fail the request and must not log.
6
+ *
7
+ * @param fetchFn - Injected or global fetch
8
+ * @param url - Origin or base URL to warm
9
+ */
10
+ export function preconnectFetch(fetchFn: typeof fetch, url: string): void {
11
+ const preconnect = (fetchFn as { preconnect?: (href: string) => void }).preconnect;
12
+ if (typeof preconnect !== "function") return;
13
+ try {
14
+ preconnect(url);
15
+ } catch {
16
+ // Bun rejects default ports. The request still runs.
17
+ }
18
+ }