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.
- package/AGENTS.md +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +37 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- 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.
|
|
74
|
+
images: { store: { index: "getmeili/meilisearch:v1.54" } },
|
|
75
75
|
role: "store.index",
|
|
76
|
-
image: "getmeili/meilisearch:v1.
|
|
76
|
+
image: "getmeili/meilisearch:v1.54",
|
|
77
77
|
},
|
|
78
78
|
// channel.email
|
|
79
79
|
{
|
|
80
|
-
images: { channel: { email: "axllent/mailpit:v1.31.
|
|
80
|
+
images: { channel: { email: "axllent/mailpit:v1.31.2" } },
|
|
81
81
|
role: "channel.email",
|
|
82
|
-
image: "axllent/mailpit:v1.31.
|
|
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.
|
|
86
|
+
images: { pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59" },
|
|
87
87
|
role: "pgdog",
|
|
88
|
-
image: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
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.
|
|
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.
|
|
115
|
-
"channel.email": "axllent/mailpit:v1.31.
|
|
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.
|
|
123
|
+
index: "getmeili/meilisearch:v1.54",
|
|
124
124
|
},
|
|
125
|
-
channel: { email: "axllent/mailpit:v1.31.
|
|
126
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
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 —
|
|
2
|
+
* Dragonfly image recipe — default `store.kv` pin.
|
|
3
3
|
*
|
|
4
|
-
* Peer of Redis / Valkey via
|
|
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
|
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
149
|
+
messages: Array<{ role: "user" | "assistant"; content: unknown }>;
|
|
108
150
|
} {
|
|
109
151
|
const systemParts: string[] = [];
|
|
110
|
-
const out: Array<{ role: "user" | "assistant"; content:
|
|
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({
|
|
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
|
+
}
|
package/src/drivers/ai-mock.ts
CHANGED
|
@@ -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(
|
|
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?: {
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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.
|
|
320
|
-
yield {
|
|
321
|
-
|
|
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
|
-
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (err instanceof Error && err.message.startsWith("openai-compatible:")) throw err;
|
|
325
376
|
}
|
|
326
377
|
}
|
|
327
378
|
}
|
|
328
|
-
|
|
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
|
+
}
|