okengine 0.22.0 → 0.23.1
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 +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.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/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- 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 +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- 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/auth/tenants.ts +17 -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/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/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- 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-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- 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.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-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.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-D9twj8Xy.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-CsHWlVPU.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-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.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-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.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-ki_ishmX.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 +6 -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/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.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.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- 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/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- 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/mcp-protocol.ts +5 -6
- 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 +1187 -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/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- 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 +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- 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/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -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
|
+
}
|
|
@@ -6,6 +6,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
6
6
|
import { withAbortSignal } from "../kernel/abort-scope.ts";
|
|
7
7
|
import { createFx, createFxContext } from "../kernel/fx.ts";
|
|
8
8
|
import { ai, createAiRuntime } from "../elements/ai.ts";
|
|
9
|
+
import { openAnthropic } from "./ai-anthropic.ts";
|
|
9
10
|
import { openOpenaiCompatible } from "./ai-openai-compatible.ts";
|
|
10
11
|
import { mockAiDriver } from "./ai-mock.ts";
|
|
11
12
|
|
|
@@ -44,6 +45,29 @@ describe("openai-compatible SSE stream", () => {
|
|
|
44
45
|
if (chunk.text) parts.push(chunk.text);
|
|
45
46
|
}
|
|
46
47
|
expect(parts.join("")).toBe("Hello");
|
|
48
|
+
const bodies: unknown[] = [];
|
|
49
|
+
const formatFetch = Object.assign(
|
|
50
|
+
async (_input: string | URL | Request, init?: RequestInit) => {
|
|
51
|
+
bodies.push(JSON.parse(String(init?.body)));
|
|
52
|
+
return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
53
|
+
},
|
|
54
|
+
{ preconnect: () => {} },
|
|
55
|
+
) as typeof fetch;
|
|
56
|
+
const formatted = await openOpenaiCompatible({
|
|
57
|
+
apiKey: "sk-test",
|
|
58
|
+
baseUrl: "https://example.test/v1",
|
|
59
|
+
fetch: formatFetch,
|
|
60
|
+
});
|
|
61
|
+
for await (const _chunk of formatted.stream!({
|
|
62
|
+
messages: [{ role: "user", content: "hi" }],
|
|
63
|
+
responseFormat: {
|
|
64
|
+
type: "json_schema",
|
|
65
|
+
json_schema: { name: "out", schema: { type: "object" } },
|
|
66
|
+
},
|
|
67
|
+
})) {
|
|
68
|
+
// drain
|
|
69
|
+
}
|
|
70
|
+
expect(bodies[0]).toMatchObject({ response_format: { type: "json_object" } });
|
|
47
71
|
|
|
48
72
|
const ac = new AbortController();
|
|
49
73
|
const iter = client.stream!({
|
|
@@ -58,6 +82,44 @@ describe("openai-compatible SSE stream", () => {
|
|
|
58
82
|
});
|
|
59
83
|
});
|
|
60
84
|
|
|
85
|
+
describe("anthropic messages stream", () => {
|
|
86
|
+
test("text deltas, input_json_delta, and message_delta usage", async () => {
|
|
87
|
+
const sse = [
|
|
88
|
+
'data: {"type":"message_start","message":{"usage":{"input_tokens":4,"output_tokens":1}}}',
|
|
89
|
+
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Ok"}}',
|
|
90
|
+
'data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_1","name":"orders.get","input":{}}}',
|
|
91
|
+
'data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\\"id\\":"}}',
|
|
92
|
+
'data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\\"14\\"}"}}',
|
|
93
|
+
'data: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":6}}',
|
|
94
|
+
'data: {"type":"message_stop"}',
|
|
95
|
+
"",
|
|
96
|
+
].join("\n");
|
|
97
|
+
const fetchFn = Object.assign(
|
|
98
|
+
async () =>
|
|
99
|
+
new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } }),
|
|
100
|
+
{ preconnect: () => {} },
|
|
101
|
+
) as typeof fetch;
|
|
102
|
+
const client = await openAnthropic({
|
|
103
|
+
apiKey: "sk-test",
|
|
104
|
+
fetch: fetchFn,
|
|
105
|
+
});
|
|
106
|
+
const texts: string[] = [];
|
|
107
|
+
const args: string[] = [];
|
|
108
|
+
let usage: { inputTokens?: number; outputTokens?: number } | undefined;
|
|
109
|
+
for await (const chunk of client.stream!({
|
|
110
|
+
messages: [{ role: "user", content: "status" }],
|
|
111
|
+
tools: [{ name: "orders.get" }],
|
|
112
|
+
})) {
|
|
113
|
+
if (chunk.text) texts.push(chunk.text);
|
|
114
|
+
if (chunk.toolCall?.argumentsDelta) args.push(chunk.toolCall.argumentsDelta);
|
|
115
|
+
if (chunk.usage) usage = chunk.usage;
|
|
116
|
+
}
|
|
117
|
+
expect(texts.join("")).toBe("Ok");
|
|
118
|
+
expect(args.join("")).toBe('{"id":"14"}');
|
|
119
|
+
expect(usage).toEqual({ inputTokens: 4, outputTokens: 6 });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
61
123
|
describe("fx.stream uses ambient AbortSignal", () => {
|
|
62
124
|
test("race abort cancels in-flight stream fetch", async () => {
|
|
63
125
|
let sawAbort = false;
|
package/src/drivers/ai-types.ts
CHANGED
|
@@ -66,10 +66,30 @@ export interface AiCompleteResult {
|
|
|
66
66
|
readonly external?: DriverExternal;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** One tool-call fragment inside a model stream. */
|
|
70
|
+
export interface AiStreamToolCallDelta {
|
|
71
|
+
/** Provider index for this call within the turn. */
|
|
72
|
+
readonly index: number;
|
|
73
|
+
readonly id?: string;
|
|
74
|
+
readonly name?: string;
|
|
75
|
+
/** Partial JSON arguments. Empty when the call is only announced. */
|
|
76
|
+
readonly argumentsDelta?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
/** One streamed token / delta from a model. */
|
|
70
80
|
export interface AiStreamChunk {
|
|
71
81
|
readonly text: string;
|
|
72
82
|
readonly done?: boolean;
|
|
83
|
+
readonly toolCall?: AiStreamToolCallDelta;
|
|
84
|
+
readonly usage?: {
|
|
85
|
+
readonly inputTokens?: number;
|
|
86
|
+
readonly outputTokens?: number;
|
|
87
|
+
readonly cost?: number;
|
|
88
|
+
};
|
|
89
|
+
/** Provider payload. Present on the terminal chunk when the driver has one. */
|
|
90
|
+
readonly raw?: unknown;
|
|
91
|
+
/** Egress identity, matching {@link AiCompleteResult.external}. */
|
|
92
|
+
readonly external?: DriverExternal;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
/** Embedding request. */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
7
|
|
|
8
|
-
import type
|
|
8
|
+
import { createJournal, type JournalRun } from "../kernel/journal.ts";
|
|
9
9
|
import { createPostgresJournalFake, createPostgresJournalStore } from "./journal-postgres.ts";
|
|
10
10
|
|
|
11
11
|
function seedRun(patch: Partial<JournalRun> & { id: string }): JournalRun {
|
|
@@ -174,4 +174,25 @@ describe("postgres JournalStore (fake)", () => {
|
|
|
174
174
|
expect(row?.leaseExpiresAt).toBeUndefined();
|
|
175
175
|
await store.close();
|
|
176
176
|
});
|
|
177
|
+
|
|
178
|
+
test("step writes stay linear as the journal grows", async () => {
|
|
179
|
+
const store = await createPostgresJournalStore({ sql: createPostgresJournalFake() });
|
|
180
|
+
const journal = createJournal({ store, now: () => 1, codeVersion: "v1" });
|
|
181
|
+
const session = await journal.start("long");
|
|
182
|
+
const steps = 40;
|
|
183
|
+
const payload = "x".repeat(200);
|
|
184
|
+
for (let i = 0; i < steps; i++) {
|
|
185
|
+
await session.step(`s${i}`, () => payload);
|
|
186
|
+
}
|
|
187
|
+
const perStep = JSON.stringify({
|
|
188
|
+
kind: "step",
|
|
189
|
+
name: "s0",
|
|
190
|
+
value: payload,
|
|
191
|
+
at: 1,
|
|
192
|
+
}).length;
|
|
193
|
+
expect(store.writeBytes).toBeLessThan(steps * (perStep + 80) * 3);
|
|
194
|
+
const row = await store.get(session.runId);
|
|
195
|
+
expect(row?.entries).toHaveLength(steps);
|
|
196
|
+
await store.close();
|
|
197
|
+
});
|
|
177
198
|
});
|