@raybase/core 0.0.1 → 0.3.3
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/LICENSE +21 -0
- package/dist/.admin-build/AdminApp.css +2 -0
- package/dist/.admin-build/AdminApp.js +77 -0
- package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
- package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
- package/dist/actions.d.ts +347 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +661 -0
- package/dist/actions.js.map +1 -0
- package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
- package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
- package/dist/admin-boot.d.ts +94 -0
- package/dist/admin-boot.d.ts.map +1 -0
- package/dist/admin-boot.js +139 -0
- package/dist/admin-boot.js.map +1 -0
- package/dist/admin-docs-content.d.ts +95 -0
- package/dist/admin-docs-content.d.ts.map +1 -0
- package/dist/admin-docs-content.js +463 -0
- package/dist/admin-docs-content.js.map +1 -0
- package/dist/admin-docs-ui.d.ts +11 -0
- package/dist/admin-docs-ui.d.ts.map +1 -0
- package/dist/admin-docs-ui.js +23 -0
- package/dist/admin-docs-ui.js.map +1 -0
- package/dist/admin-kit.d.ts +29 -0
- package/dist/admin-kit.js +1557 -0
- package/dist/admin-role.d.ts +8 -0
- package/dist/admin-role.d.ts.map +1 -0
- package/dist/admin-role.js +22 -0
- package/dist/admin-role.js.map +1 -0
- package/dist/admin-ui.d.ts +3 -0
- package/dist/admin-ui.d.ts.map +1 -0
- package/dist/admin-ui.js +16 -0
- package/dist/admin-ui.js.map +1 -0
- package/dist/agent-ai-chat.d.ts +127 -0
- package/dist/agent-ai-chat.d.ts.map +1 -0
- package/dist/agent-ai-chat.js +5975 -0
- package/dist/agent-ai-chat.js.map +1 -0
- package/dist/agent-chapter-archive.d.ts +214 -0
- package/dist/agent-chapter-archive.d.ts.map +1 -0
- package/dist/agent-chapter-archive.js +696 -0
- package/dist/agent-chapter-archive.js.map +1 -0
- package/dist/agent-chapter-search.d.ts +116 -0
- package/dist/agent-chapter-search.d.ts.map +1 -0
- package/dist/agent-chapter-search.js +605 -0
- package/dist/agent-chapter-search.js.map +1 -0
- package/dist/agent-chapter-tools.d.ts +45 -0
- package/dist/agent-chapter-tools.d.ts.map +1 -0
- package/dist/agent-chapter-tools.js +344 -0
- package/dist/agent-chapter-tools.js.map +1 -0
- package/dist/agent-chapters.d.ts +228 -0
- package/dist/agent-chapters.d.ts.map +1 -0
- package/dist/agent-chapters.js +737 -0
- package/dist/agent-chapters.js.map +1 -0
- package/dist/agent-data-parts.d.ts +167 -0
- package/dist/agent-data-parts.d.ts.map +1 -0
- package/dist/agent-data-parts.js +68 -0
- package/dist/agent-data-parts.js.map +1 -0
- package/dist/agent-errors.d.ts +41 -0
- package/dist/agent-errors.d.ts.map +1 -0
- package/dist/agent-errors.js +191 -0
- package/dist/agent-errors.js.map +1 -0
- package/dist/agent-follow-up-store.d.ts +273 -0
- package/dist/agent-follow-up-store.d.ts.map +1 -0
- package/dist/agent-follow-up-store.js +703 -0
- package/dist/agent-follow-up-store.js.map +1 -0
- package/dist/agent-follow-ups.d.ts +314 -0
- package/dist/agent-follow-ups.d.ts.map +1 -0
- package/dist/agent-follow-ups.js +840 -0
- package/dist/agent-follow-ups.js.map +1 -0
- package/dist/agent-memory.d.ts +323 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +1270 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/agent-message-parts.d.ts +58 -0
- package/dist/agent-message-parts.d.ts.map +1 -0
- package/dist/agent-message-parts.js +163 -0
- package/dist/agent-message-parts.js.map +1 -0
- package/dist/agent-remote-mcp-credentials.d.ts +154 -0
- package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
- package/dist/agent-remote-mcp-credentials.js +287 -0
- package/dist/agent-remote-mcp-credentials.js.map +1 -0
- package/dist/agent-remote-mcp-crypto.d.ts +32 -0
- package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
- package/dist/agent-remote-mcp-crypto.js +101 -0
- package/dist/agent-remote-mcp-crypto.js.map +1 -0
- package/dist/agent-remote-mcp-install.d.ts +299 -0
- package/dist/agent-remote-mcp-install.d.ts.map +1 -0
- package/dist/agent-remote-mcp-install.js +785 -0
- package/dist/agent-remote-mcp-install.js.map +1 -0
- package/dist/agent-remote-mcp-oauth.d.ts +73 -0
- package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
- package/dist/agent-remote-mcp-oauth.js +344 -0
- package/dist/agent-remote-mcp-oauth.js.map +1 -0
- package/dist/agent-remote-mcp-transport.d.ts +46 -0
- package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
- package/dist/agent-remote-mcp-transport.js +225 -0
- package/dist/agent-remote-mcp-transport.js.map +1 -0
- package/dist/agent-remote-mcp.d.ts +147 -0
- package/dist/agent-remote-mcp.d.ts.map +1 -0
- package/dist/agent-remote-mcp.js +681 -0
- package/dist/agent-remote-mcp.js.map +1 -0
- package/dist/agent-room-redaction.d.ts +66 -0
- package/dist/agent-room-redaction.d.ts.map +1 -0
- package/dist/agent-room-redaction.js +264 -0
- package/dist/agent-room-redaction.js.map +1 -0
- package/dist/agent-rooms.d.ts +209 -0
- package/dist/agent-rooms.d.ts.map +1 -0
- package/dist/agent-rooms.js +244 -0
- package/dist/agent-rooms.js.map +1 -0
- package/dist/agent-runtime.d.ts +541 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +4260 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent-skill-archive.d.ts +20 -0
- package/dist/agent-skill-archive.d.ts.map +1 -0
- package/dist/agent-skill-archive.js +316 -0
- package/dist/agent-skill-archive.js.map +1 -0
- package/dist/agent-skill-discovery.d.ts +96 -0
- package/dist/agent-skill-discovery.d.ts.map +1 -0
- package/dist/agent-skill-discovery.js +464 -0
- package/dist/agent-skill-discovery.js.map +1 -0
- package/dist/agent-skill-format.d.ts +57 -0
- package/dist/agent-skill-format.d.ts.map +1 -0
- package/dist/agent-skill-format.js +238 -0
- package/dist/agent-skill-format.js.map +1 -0
- package/dist/agent-skill-import.d.ts +64 -0
- package/dist/agent-skill-import.d.ts.map +1 -0
- package/dist/agent-skill-import.js +295 -0
- package/dist/agent-skill-import.js.map +1 -0
- package/dist/agent-skill-publish.d.ts +51 -0
- package/dist/agent-skill-publish.d.ts.map +1 -0
- package/dist/agent-skill-publish.js +136 -0
- package/dist/agent-skill-publish.js.map +1 -0
- package/dist/agent-skill-store.d.ts +252 -0
- package/dist/agent-skill-store.d.ts.map +1 -0
- package/dist/agent-skill-store.js +514 -0
- package/dist/agent-skill-store.js.map +1 -0
- package/dist/agent-skills.d.ts +85 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/agent-skills.js +291 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/agent-transcript-pager.d.ts +77 -0
- package/dist/agent-transcript-pager.d.ts.map +1 -0
- package/dist/agent-transcript-pager.js +210 -0
- package/dist/agent-transcript-pager.js.map +1 -0
- package/dist/agent-user-data-parts.d.ts +20 -0
- package/dist/agent-user-data-parts.d.ts.map +1 -0
- package/dist/agent-user-data-parts.js +161 -0
- package/dist/agent-user-data-parts.js.map +1 -0
- package/dist/agent-user-transcript.d.ts +46 -0
- package/dist/agent-user-transcript.d.ts.map +1 -0
- package/dist/agent-user-transcript.js +106 -0
- package/dist/agent-user-transcript.js.map +1 -0
- package/dist/agent-voice-notes.d.ts +15 -0
- package/dist/agent-voice-notes.d.ts.map +1 -0
- package/dist/agent-voice-notes.js +17 -0
- package/dist/agent-voice-notes.js.map +1 -0
- package/dist/agent-wakes.d.ts +213 -0
- package/dist/agent-wakes.d.ts.map +1 -0
- package/dist/agent-wakes.js +534 -0
- package/dist/agent-wakes.js.map +1 -0
- package/dist/agent.d.ts +1780 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +3123 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai.d.ts +583 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1858 -0
- package/dist/ai.js.map +1 -0
- package/dist/app-manifest.d.ts +255 -0
- package/dist/app-manifest.d.ts.map +1 -0
- package/dist/app-manifest.js +372 -0
- package/dist/app-manifest.js.map +1 -0
- package/dist/app-users.d.ts +30 -0
- package/dist/app-users.d.ts.map +1 -0
- package/dist/app-users.js +91 -0
- package/dist/app-users.js.map +1 -0
- package/dist/audit-archive.d.ts +85 -0
- package/dist/audit-archive.d.ts.map +1 -0
- package/dist/audit-archive.js +178 -0
- package/dist/audit-archive.js.map +1 -0
- package/dist/audit.d.ts +54 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +95 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth-logger.d.ts +28 -0
- package/dist/auth-logger.d.ts.map +1 -0
- package/dist/auth-logger.js +53 -0
- package/dist/auth-logger.js.map +1 -0
- package/dist/auth-password-secret.d.ts +59 -0
- package/dist/auth-password-secret.d.ts.map +1 -0
- package/dist/auth-password-secret.js +409 -0
- package/dist/auth-password-secret.js.map +1 -0
- package/dist/auth-schema.d.ts +120 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +285 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/auth.d.ts +321 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +1562 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup-archive.d.ts +257 -0
- package/dist/backup-archive.d.ts.map +1 -0
- package/dist/backup-archive.js +2068 -0
- package/dist/backup-archive.js.map +1 -0
- package/dist/backup-crypto.d.ts +313 -0
- package/dist/backup-crypto.d.ts.map +1 -0
- package/dist/backup-crypto.js +693 -0
- package/dist/backup-crypto.js.map +1 -0
- package/dist/backup-format.d.ts +200 -0
- package/dist/backup-format.d.ts.map +1 -0
- package/dist/backup-format.js +1247 -0
- package/dist/backup-format.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +35 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +83 -0
- package/dist/clock.js.map +1 -0
- package/dist/data-client.d.ts +187 -0
- package/dist/data-client.d.ts.map +1 -0
- package/dist/data-client.js +590 -0
- package/dist/data-client.js.map +1 -0
- package/dist/data-secrets.d.ts +164 -0
- package/dist/data-secrets.d.ts.map +1 -0
- package/dist/data-secrets.js +383 -0
- package/dist/data-secrets.js.map +1 -0
- package/dist/db.d.ts +453 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +324 -0
- package/dist/db.js.map +1 -0
- package/dist/dev-activity.d.ts +48 -0
- package/dist/dev-activity.d.ts.map +1 -0
- package/dist/dev-activity.js +50 -0
- package/dist/dev-activity.js.map +1 -0
- package/dist/do.d.ts +429 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +4526 -0
- package/dist/do.js.map +1 -0
- package/dist/email.d.ts +137 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +527 -0
- package/dist/email.js.map +1 -0
- package/dist/generated/admin-assets.d.ts +16 -0
- package/dist/generated/admin-assets.d.ts.map +1 -0
- package/dist/generated/admin-assets.js +16 -0
- package/dist/generated/admin-assets.js.map +1 -0
- package/dist/generated/admin-docs-assets.d.ts +3 -0
- package/dist/generated/admin-docs-assets.d.ts.map +1 -0
- package/dist/generated/admin-docs-assets.js +3 -0
- package/dist/generated/admin-docs-assets.js.map +1 -0
- package/dist/handoff-consumer.d.ts +79 -0
- package/dist/handoff-consumer.d.ts.map +1 -0
- package/dist/handoff-consumer.js +300 -0
- package/dist/handoff-consumer.js.map +1 -0
- package/dist/handoff.d.ts +70 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +228 -0
- package/dist/handoff.js.map +1 -0
- package/dist/host-auth.d.ts +47 -0
- package/dist/host-auth.d.ts.map +1 -0
- package/dist/host-auth.js +59 -0
- package/dist/host-auth.js.map +1 -0
- package/dist/host-user.d.ts +15 -0
- package/dist/host-user.d.ts.map +1 -0
- package/dist/host-user.js +23 -0
- package/dist/host-user.js.map +1 -0
- package/dist/html.d.ts +35 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +634 -0
- package/dist/html.js.map +1 -0
- package/dist/http-error.d.ts +13 -0
- package/dist/http-error.d.ts.map +1 -0
- package/dist/http-error.js +15 -0
- package/dist/http-error.js.map +1 -0
- package/dist/imports.d.ts +219 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +571 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/inference-tape.d.ts +64 -0
- package/dist/inference-tape.d.ts.map +1 -0
- package/dist/inference-tape.js +278 -0
- package/dist/inference-tape.js.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/jobs.d.ts +796 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +538 -0
- package/dist/jobs.js.map +1 -0
- package/dist/json-schema.d.ts +91 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +884 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/knowledge.d.ts +33 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +348 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/local-dev-chrome.d.ts +29 -0
- package/dist/local-dev-chrome.d.ts.map +1 -0
- package/dist/local-dev-chrome.js +514 -0
- package/dist/local-dev-chrome.js.map +1 -0
- package/dist/local-dev-personas.d.ts +15 -0
- package/dist/local-dev-personas.d.ts.map +1 -0
- package/dist/local-dev-personas.js +46 -0
- package/dist/local-dev-personas.js.map +1 -0
- package/dist/mcp.d.ts +567 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2480 -0
- package/dist/mcp.js.map +1 -0
- package/dist/modules.d.ts +145 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +164 -0
- package/dist/modules.js.map +1 -0
- package/dist/notification-client.d.ts +94 -0
- package/dist/notification-client.d.ts.map +1 -0
- package/dist/notification-client.js +127 -0
- package/dist/notification-client.js.map +1 -0
- package/dist/notifications.d.ts +447 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +1625 -0
- package/dist/notifications.js.map +1 -0
- package/dist/operator-data-query.d.ts +117 -0
- package/dist/operator-data-query.d.ts.map +1 -0
- package/dist/operator-data-query.js +431 -0
- package/dist/operator-data-query.js.map +1 -0
- package/dist/operator-mcp.d.ts +63 -0
- package/dist/operator-mcp.d.ts.map +1 -0
- package/dist/operator-mcp.js +2828 -0
- package/dist/operator-mcp.js.map +1 -0
- package/dist/outbound-https.d.ts +24 -0
- package/dist/outbound-https.d.ts.map +1 -0
- package/dist/outbound-https.js +172 -0
- package/dist/outbound-https.js.map +1 -0
- package/dist/pitr.d.ts +60 -0
- package/dist/pitr.d.ts.map +1 -0
- package/dist/pitr.js +94 -0
- package/dist/pitr.js.map +1 -0
- package/dist/platform.d.ts +199 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +196 -0
- package/dist/platform.js.map +1 -0
- package/dist/policies.d.ts +115 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +158 -0
- package/dist/policies.js.map +1 -0
- package/dist/principal.d.ts +143 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +174 -0
- package/dist/principal.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +578 -0
- package/dist/realtime.js.map +1 -0
- package/dist/retrieval.d.ts +263 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +602 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-error-client.d.ts +10 -0
- package/dist/runtime-error-client.d.ts.map +1 -0
- package/dist/runtime-error-client.js +70 -0
- package/dist/runtime-error-client.js.map +1 -0
- package/dist/runtime-errors.d.ts +180 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +572 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/schema.d.ts +434 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +232 -0
- package/dist/schema.js.map +1 -0
- package/dist/search.d.ts +216 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +785 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +191 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +478 -0
- package/dist/settings.js.map +1 -0
- package/dist/sql.d.ts +7 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +30 -0
- package/dist/sql.js.map +1 -0
- package/dist/storage-client.d.ts +69 -0
- package/dist/storage-client.d.ts.map +1 -0
- package/dist/storage-client.js +469 -0
- package/dist/storage-client.js.map +1 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +82 -0
- package/dist/storage.js.map +1 -0
- package/dist/subject-domains.d.ts +15 -0
- package/dist/subject-domains.d.ts.map +1 -0
- package/dist/subject-domains.js +38 -0
- package/dist/subject-domains.js.map +1 -0
- package/dist/support.d.ts +132 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +483 -0
- package/dist/support.js.map +1 -0
- package/dist/system-tables.d.ts +12 -0
- package/dist/system-tables.d.ts.map +1 -0
- package/dist/system-tables.js +225 -0
- package/dist/system-tables.js.map +1 -0
- package/dist/test.d.ts +57 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +454 -0
- package/dist/test.js.map +1 -0
- package/dist/vite.d.ts +82 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +82 -0
- package/dist/vite.js.map +1 -0
- package/dist/webpush-crypto.d.ts +39 -0
- package/dist/webpush-crypto.d.ts.map +1 -0
- package/dist/webpush-crypto.js +213 -0
- package/dist/webpush-crypto.js.map +1 -0
- package/dist/webpush-private.d.ts +11 -0
- package/dist/webpush-private.d.ts.map +1 -0
- package/dist/webpush-private.js +11 -0
- package/dist/webpush-private.js.map +1 -0
- package/dist/webpush.d.ts +86 -0
- package/dist/webpush.d.ts.map +1 -0
- package/dist/webpush.js +375 -0
- package/dist/webpush.js.map +1 -0
- package/dist/worker.d.ts +185 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +13021 -0
- package/dist/worker.js.map +1 -0
- package/package.json +161 -8
- package/README.md +0 -3
package/dist/ai.js
ADDED
|
@@ -0,0 +1,1858 @@
|
|
|
1
|
+
import { dataStub } from "./db.js";
|
|
2
|
+
import { sha256Hex, stableJson } from "./internal.js";
|
|
3
|
+
import { jsonSchemaFor, parseWithSchema, restoreStrictModelValue, strictModelSchema } from "./json-schema.js";
|
|
4
|
+
import { id, integer, longText, raybaseSystemTable, text } from "./schema.js";
|
|
5
|
+
import { nowIso, randomId } from "./sql.js";
|
|
6
|
+
import { createInferenceTape, tapeChatStream, withoutInferenceTapeEnv } from "./inference-tape.js";
|
|
7
|
+
export const raybaseRecommendedModelsManifestUrl = "https://raybase.dev/models/recommended.json";
|
|
8
|
+
export const defaultAiModel = "";
|
|
9
|
+
export const defaultEmbeddingModel = "@cf/baai/bge-base-en-v1.5";
|
|
10
|
+
export const defaultAiLimits = {
|
|
11
|
+
// perUserDailyRequests, perUserDailyOutputTokens, and perAppDailyRequests are
|
|
12
|
+
// deliberately absent: unset means unlimited. Public (unauthenticated) traffic
|
|
13
|
+
// keeps a default cap because that surface is an abuse question, not a spend one.
|
|
14
|
+
perPublicDailyRequests: 200,
|
|
15
|
+
perPublicDailyOutputTokens: 60_000,
|
|
16
|
+
maxToolCallsPerTurn: 6,
|
|
17
|
+
maxLoopStepsPerTurn: 8,
|
|
18
|
+
maxConversationMessages: 40
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Deadline for a single model call. A provider request that hangs otherwise
|
|
22
|
+
* parks the whole agent turn: the turn's abort signal is only consulted
|
|
23
|
+
* between loop steps, so neither a member's cancel nor the runtime can cut a
|
|
24
|
+
* request already in flight. Measured healthy agent calls finish far inside
|
|
25
|
+
* this (drafts up to ~21s, delivery gates and chapter closes ~9s), so the
|
|
26
|
+
* bound never touches honest traffic.
|
|
27
|
+
*/
|
|
28
|
+
export const defaultModelCallTimeoutMs = 120_000;
|
|
29
|
+
export const defaultAiDefaults = {
|
|
30
|
+
model: defaultAiModel,
|
|
31
|
+
embeddingModel: defaultEmbeddingModel,
|
|
32
|
+
maxInputChars: 20_000,
|
|
33
|
+
maxOutputTokens: 1_200,
|
|
34
|
+
modelCallTimeoutMs: defaultModelCallTimeoutMs,
|
|
35
|
+
modelParams: {}
|
|
36
|
+
};
|
|
37
|
+
export class RaybaseAiModelNotConfiguredError extends Error {
|
|
38
|
+
constructor(source = "AI model") {
|
|
39
|
+
super(`${source} is not configured. Set the defaultAiModel setting in Raybase admin or provide appConfig.ai.defaults.model.`);
|
|
40
|
+
this.name = "RaybaseAiModelNotConfiguredError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function chatMessageText(message) {
|
|
44
|
+
if (typeof message.content === "string")
|
|
45
|
+
return message.content;
|
|
46
|
+
return message.content
|
|
47
|
+
.filter((part) => part.type === "text")
|
|
48
|
+
.map((part) => part.text)
|
|
49
|
+
.join("");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Tool definitions in the form a strict provider accepts.
|
|
53
|
+
*
|
|
54
|
+
* Every object closed with every property required, optionality spelled as a
|
|
55
|
+
* null union, open records carried as JSON in a string. `restoreStrictToolCalls`
|
|
56
|
+
* is the other half and is not optional: the arguments that come back carry the
|
|
57
|
+
* padding, and an action's own validator refuses it.
|
|
58
|
+
*
|
|
59
|
+
* Core applies both around every `ai.chat` and `ai.chatStream` call, so declared
|
|
60
|
+
* Zod and JSON-Schema inputs work on a strict lane without an app doing
|
|
61
|
+
* anything. A custom inference provider that builds its own request — an app
|
|
62
|
+
* registering `appConfig.ai.provider` — uses this pair rather than rebuilding it.
|
|
63
|
+
*/
|
|
64
|
+
export function strictChatTools(tools) {
|
|
65
|
+
if (!tools?.length)
|
|
66
|
+
return tools;
|
|
67
|
+
return tools.map((tool) => {
|
|
68
|
+
const parameters = tool.parameters === undefined
|
|
69
|
+
? { type: "object", properties: {} }
|
|
70
|
+
: tool.parameters;
|
|
71
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters))
|
|
72
|
+
return tool;
|
|
73
|
+
return { ...tool, parameters: strictModelSchema(parameters) };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Tool-call arguments with the strict padding undone, restored against the tool
|
|
78
|
+
* schemas *as they were declared* — before `strictChatTools` saw them.
|
|
79
|
+
*/
|
|
80
|
+
export function restoreStrictToolCalls(calls, tools) {
|
|
81
|
+
if (!calls?.length)
|
|
82
|
+
return calls ?? [];
|
|
83
|
+
if (!tools?.length)
|
|
84
|
+
return calls;
|
|
85
|
+
const schemas = new Map(tools.map((tool) => [tool.name, tool.parameters]));
|
|
86
|
+
return calls.map((call) => {
|
|
87
|
+
const schema = schemas.get(call.name);
|
|
88
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
89
|
+
return call;
|
|
90
|
+
return { ...call, arguments: restoreStrictModelValue(call.arguments, schema) };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The first complete JSON object in a reply.
|
|
95
|
+
*
|
|
96
|
+
* Models put prose before the JSON and, less often, after it. Scanning from the
|
|
97
|
+
* first `{` to the last `}` swallows both into one unparseable span; this walks
|
|
98
|
+
* brace depth with string and escape handling, mirroring the boundary a
|
|
99
|
+
* streaming JSON decoder would find, and returns the first object that parses.
|
|
100
|
+
*/
|
|
101
|
+
export function firstJsonObject(text) {
|
|
102
|
+
for (let start = text.indexOf("{"); start >= 0; start = text.indexOf("{", start + 1)) {
|
|
103
|
+
let depth = 0;
|
|
104
|
+
let quoted = false;
|
|
105
|
+
let escaped = false;
|
|
106
|
+
for (let index = start; index < text.length; index += 1) {
|
|
107
|
+
const character = text[index];
|
|
108
|
+
if (quoted) {
|
|
109
|
+
if (escaped)
|
|
110
|
+
escaped = false;
|
|
111
|
+
else if (character === "\\")
|
|
112
|
+
escaped = true;
|
|
113
|
+
else if (character === '"')
|
|
114
|
+
quoted = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (character === '"')
|
|
118
|
+
quoted = true;
|
|
119
|
+
else if (character === "{")
|
|
120
|
+
depth += 1;
|
|
121
|
+
else if (character === "}" && --depth === 0) {
|
|
122
|
+
try {
|
|
123
|
+
const value = JSON.parse(text.slice(start, index + 1));
|
|
124
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The one input-size measure. Every budget check — the agent turn pre-flight,
|
|
136
|
+
* the in-loop re-measure, and the AI layer's own guard — must count the same
|
|
137
|
+
* request the same way, or a turn passes one check and fails the next on the
|
|
138
|
+
* same bytes. Callers that measure ahead of a call pass exactly what they will
|
|
139
|
+
* send.
|
|
140
|
+
*
|
|
141
|
+
* Image and attachment content parts still count as zero: chatMessageText
|
|
142
|
+
* keeps text parts only, and a provider bills their bytes as tokens on a
|
|
143
|
+
* different scale than characters.
|
|
144
|
+
*/
|
|
145
|
+
export function chatRequestInputChars(input) {
|
|
146
|
+
return jsonCharLength({
|
|
147
|
+
messages: input.messages.map((message) => ({ ...message, content: chatMessageText(message) })),
|
|
148
|
+
tools: input.tools,
|
|
149
|
+
metadata: input.metadata,
|
|
150
|
+
responseFormat: input.responseFormat
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export class RaybaseInferenceProviderError extends Error {
|
|
154
|
+
code;
|
|
155
|
+
health;
|
|
156
|
+
retryAt;
|
|
157
|
+
actionHref;
|
|
158
|
+
constructor(input) {
|
|
159
|
+
super(input.message ?? "The selected AI provider is unavailable.");
|
|
160
|
+
this.name = "RaybaseInferenceProviderError";
|
|
161
|
+
this.code = input.code;
|
|
162
|
+
this.health = input.health;
|
|
163
|
+
this.retryAt = input.retryAt;
|
|
164
|
+
this.actionHref = input.actionHref;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function safeInferenceProviderError(error) {
|
|
168
|
+
if (error instanceof RaybaseInferenceProviderError) {
|
|
169
|
+
const code = /^[a-z0-9][a-z0-9_.:-]{0,79}$/.test(error.code) ? error.code : "provider_error";
|
|
170
|
+
const retryAt = error.retryAt && Number.isFinite(new Date(error.retryAt).getTime())
|
|
171
|
+
? new Date(error.retryAt).toISOString()
|
|
172
|
+
: undefined;
|
|
173
|
+
const actionHref = error.actionHref?.startsWith("/") && error.actionHref.length <= 256
|
|
174
|
+
? error.actionHref
|
|
175
|
+
: undefined;
|
|
176
|
+
return {
|
|
177
|
+
code,
|
|
178
|
+
...(error.health ? { health: error.health } : {}),
|
|
179
|
+
...(retryAt ? { retryAt } : {}),
|
|
180
|
+
...(actionHref ? { actionHref } : {}),
|
|
181
|
+
message: "The selected AI provider is unavailable."
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
code: "provider_error",
|
|
186
|
+
message: "The selected AI provider failed."
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function safeInferenceProviderErrorMessage(error) {
|
|
190
|
+
return JSON.stringify(safeInferenceProviderError(error));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* A structured call that produced nothing usable: no JSON in the reply, or JSON
|
|
194
|
+
* the caller's own schema refused. It carries the raw text and the validation
|
|
195
|
+
* issues, because a caller who cannot see what the model actually said has no
|
|
196
|
+
* way to tell a prompt problem from a schema problem.
|
|
197
|
+
*/
|
|
198
|
+
export class RaybaseAIObjectError extends Error {
|
|
199
|
+
raw;
|
|
200
|
+
issues;
|
|
201
|
+
constructor(message, input) {
|
|
202
|
+
super(message, input.cause === undefined ? undefined : { cause: input.cause });
|
|
203
|
+
this.name = "RaybaseAIObjectError";
|
|
204
|
+
this.raw = input.raw;
|
|
205
|
+
this.issues = input.issues ?? [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* `ai.object` for any client that can `chat`: the request-scoped `RaybaseAI`,
|
|
210
|
+
* the commit-scoped variant, and anything wrapping either of them with its own
|
|
211
|
+
* accounting. Kept as a free function so a wrapper delegates in one line rather
|
|
212
|
+
* than reimplementing the parse-and-validate half.
|
|
213
|
+
*/
|
|
214
|
+
export function raybaseObjectCall(chat) {
|
|
215
|
+
return async (input, options) => {
|
|
216
|
+
const messages = objectRequestMessages(input);
|
|
217
|
+
const declared = jsonSchemaFor(input.schema);
|
|
218
|
+
const responseFormat = {
|
|
219
|
+
type: "json_schema",
|
|
220
|
+
name: objectSchemaName(input.schemaName),
|
|
221
|
+
schema: strictModelSchema(declared),
|
|
222
|
+
strict: true,
|
|
223
|
+
...(input.schemaDescription ? { description: input.schemaDescription } : {})
|
|
224
|
+
};
|
|
225
|
+
const attempts = input.retry === false ? 1 : 2;
|
|
226
|
+
let raw = "";
|
|
227
|
+
let issues = [];
|
|
228
|
+
let failure;
|
|
229
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
230
|
+
const result = await chat({
|
|
231
|
+
model: input.model ?? "",
|
|
232
|
+
messages: attempt === 0 ? messages : [...messages, ...objectRetryMessages(raw, issues)],
|
|
233
|
+
responseFormat,
|
|
234
|
+
...(input.maxOutputTokens !== undefined ? { maxOutputTokens: input.maxOutputTokens } : {}),
|
|
235
|
+
...(input.modelParams ? { modelParams: input.modelParams } : {}),
|
|
236
|
+
...(input.providerParams ? { providerParams: input.providerParams } : {}),
|
|
237
|
+
...(input.metadata ? { metadata: input.metadata } : {})
|
|
238
|
+
}, options);
|
|
239
|
+
raw = result.text;
|
|
240
|
+
const parsed = firstJsonObject(raw);
|
|
241
|
+
if (!parsed) {
|
|
242
|
+
issues = [{ message: "The reply contained no complete JSON object." }];
|
|
243
|
+
failure = undefined;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
// The wire form travels back too: nulls where the caller wrote an
|
|
248
|
+
// optional, JSON in a string where it wrote a record.
|
|
249
|
+
return parseWithSchema(input.schema, restoreStrictModelValue(parsed, declared), "ai.object");
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
failure = error;
|
|
253
|
+
issues = objectValidationIssues(error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw new RaybaseAIObjectError(issues.length === 1 && typeof issues[0]?.message === "string"
|
|
257
|
+
? `Structured AI reply was rejected: ${issues[0].message}`
|
|
258
|
+
: "Structured AI reply did not validate against its schema", { raw, issues, cause: failure });
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function objectRequestMessages(input) {
|
|
262
|
+
const messages = input.messages?.length
|
|
263
|
+
? input.messages
|
|
264
|
+
: input.prompt
|
|
265
|
+
? [{ role: "user", content: input.prompt }]
|
|
266
|
+
: [];
|
|
267
|
+
if (!messages.length)
|
|
268
|
+
throw new Error("ai.object requires messages or a prompt");
|
|
269
|
+
return messages;
|
|
270
|
+
}
|
|
271
|
+
function objectSchemaName(name) {
|
|
272
|
+
const trimmed = name?.trim();
|
|
273
|
+
if (!trimmed)
|
|
274
|
+
return "response";
|
|
275
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(trimmed))
|
|
276
|
+
throw new Error(`ai.object schemaName ${trimmed} must be 1-64 characters of letters, digits, underscores or dashes`);
|
|
277
|
+
return trimmed;
|
|
278
|
+
}
|
|
279
|
+
function objectRetryMessages(raw, issues) {
|
|
280
|
+
return [
|
|
281
|
+
{ role: "assistant", content: raw },
|
|
282
|
+
{
|
|
283
|
+
role: "user",
|
|
284
|
+
content: `That reply did not validate against the required schema: ${objectIssueText(issues)}. Reply again with only the JSON object, corrected.`
|
|
285
|
+
}
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
function objectIssueText(issues) {
|
|
289
|
+
const described = issues.map((issue) => {
|
|
290
|
+
const record = issue;
|
|
291
|
+
const path = Array.isArray(record?.path) && record.path.length ? `${record.path.join(".")}: ` : "";
|
|
292
|
+
return `${path}${typeof record?.message === "string" ? record.message : String(issue)}`;
|
|
293
|
+
});
|
|
294
|
+
return described.join("; ").slice(0, 800) || "no details";
|
|
295
|
+
}
|
|
296
|
+
function objectValidationIssues(error) {
|
|
297
|
+
const issues = error?.issues;
|
|
298
|
+
if (Array.isArray(issues) && issues.length)
|
|
299
|
+
return issues;
|
|
300
|
+
return [{ message: error instanceof Error ? error.message : String(error) }];
|
|
301
|
+
}
|
|
302
|
+
export const raybaseAiSystemTables = [
|
|
303
|
+
raybaseSystemTable("_ai_usage_events", {
|
|
304
|
+
id: id(),
|
|
305
|
+
userId: text(),
|
|
306
|
+
principalKind: text(),
|
|
307
|
+
publicScopeHash: text(),
|
|
308
|
+
serviceName: text(),
|
|
309
|
+
agent: text(),
|
|
310
|
+
conversationId: text(),
|
|
311
|
+
model: text({ required: true }),
|
|
312
|
+
requestKind: text({ required: true }),
|
|
313
|
+
role: text(),
|
|
314
|
+
gatewayId: text(),
|
|
315
|
+
gatewayLogId: text(),
|
|
316
|
+
inputChars: integer({ required: true }),
|
|
317
|
+
floorChars: integer(),
|
|
318
|
+
tailChars: integer(),
|
|
319
|
+
outputChars: integer({ required: true }),
|
|
320
|
+
estimatedInputTokens: integer({ required: true }),
|
|
321
|
+
estimatedOutputTokens: integer({ required: true }),
|
|
322
|
+
estimatedNeurons: integer(),
|
|
323
|
+
modelParams: longText(),
|
|
324
|
+
metadata: longText(),
|
|
325
|
+
status: text({ required: true }),
|
|
326
|
+
error: longText(),
|
|
327
|
+
durationMs: integer({ required: true }),
|
|
328
|
+
createdAt: text({ required: true })
|
|
329
|
+
}, { indexes: [["userId", "createdAt"], ["serviceName", "createdAt"], ["agent", "createdAt"], ["conversationId"]] }),
|
|
330
|
+
raybaseSystemTable("_ai_budget_windows", {
|
|
331
|
+
id: id(),
|
|
332
|
+
scope: text({ required: true }),
|
|
333
|
+
scopeId: text({ required: true }),
|
|
334
|
+
windowStart: text({ required: true }),
|
|
335
|
+
windowEnd: text({ required: true }),
|
|
336
|
+
requestCount: integer({ required: true }),
|
|
337
|
+
estimatedOutputTokens: integer({ required: true }),
|
|
338
|
+
estimatedNeurons: integer(),
|
|
339
|
+
updatedAt: text({ required: true })
|
|
340
|
+
}, { indexes: [["scope", "scopeId", "windowStart"], ["windowEnd"]] }),
|
|
341
|
+
raybaseSystemTable("_ai_action_events", {
|
|
342
|
+
id: id(),
|
|
343
|
+
userId: text({ required: true }),
|
|
344
|
+
agent: text({ required: true }),
|
|
345
|
+
conversationId: text(),
|
|
346
|
+
actionName: text({ required: true }),
|
|
347
|
+
inputHash: text(),
|
|
348
|
+
status: text({ required: true }),
|
|
349
|
+
error: longText(),
|
|
350
|
+
createdAt: text({ required: true })
|
|
351
|
+
}, { indexes: [["userId", "createdAt"], ["agent", "createdAt"], ["conversationId"], ["actionName"]] }),
|
|
352
|
+
raybaseSystemTable("_ai_embedding_chunks", {
|
|
353
|
+
id: id(),
|
|
354
|
+
indexName: text({ required: true }),
|
|
355
|
+
backend: text({ required: true }),
|
|
356
|
+
physicalIndex: text(),
|
|
357
|
+
namespace: text(),
|
|
358
|
+
vectorId: text({ required: true }),
|
|
359
|
+
sourceType: text({ required: true }),
|
|
360
|
+
sourceTable: text(),
|
|
361
|
+
sourceId: text({ required: true }),
|
|
362
|
+
sourceVersion: text(),
|
|
363
|
+
chunkKey: text({ required: true }),
|
|
364
|
+
chunkOrdinal: integer({ required: true }),
|
|
365
|
+
embeddingModel: text({ required: true }),
|
|
366
|
+
dimensions: integer({ required: true }),
|
|
367
|
+
metric: text(),
|
|
368
|
+
textHash: text({ required: true }),
|
|
369
|
+
title: text(),
|
|
370
|
+
url: text(),
|
|
371
|
+
metadata: longText(),
|
|
372
|
+
vectorBase64: longText(),
|
|
373
|
+
status: text({ required: true }),
|
|
374
|
+
mutationId: text(),
|
|
375
|
+
lastError: longText(),
|
|
376
|
+
indexedAt: text(),
|
|
377
|
+
updatedAt: text({ required: true })
|
|
378
|
+
}, {
|
|
379
|
+
indexes: [
|
|
380
|
+
["indexName", "backend", "sourceType", "sourceTable", "sourceId"],
|
|
381
|
+
["backend", "physicalIndex", "namespace", "vectorId"],
|
|
382
|
+
["status", "updatedAt"],
|
|
383
|
+
["textHash"],
|
|
384
|
+
["sourceType", "sourceTable", "sourceId", "chunkKey"]
|
|
385
|
+
]
|
|
386
|
+
}),
|
|
387
|
+
raybaseSystemTable("_ai_retrieval_reindex_runs", {
|
|
388
|
+
id: id(),
|
|
389
|
+
indexName: text({ required: true }),
|
|
390
|
+
backend: text({ required: true }),
|
|
391
|
+
status: text({ required: true }),
|
|
392
|
+
actorId: text(),
|
|
393
|
+
actorKind: text(),
|
|
394
|
+
namespace: text(),
|
|
395
|
+
totalSources: integer({ required: true }),
|
|
396
|
+
totalChunks: integer({ required: true }),
|
|
397
|
+
upsertedChunks: integer({ required: true }),
|
|
398
|
+
deletedChunks: integer({ required: true }),
|
|
399
|
+
skippedChunks: integer({ required: true }),
|
|
400
|
+
errorCount: integer({ required: true }),
|
|
401
|
+
startedAt: text({ required: true }),
|
|
402
|
+
completedAt: text(),
|
|
403
|
+
summaryJson: longText()
|
|
404
|
+
}, { indexes: [["indexName", "startedAt"], ["backend", "status"], ["namespace", "startedAt"]] })
|
|
405
|
+
];
|
|
406
|
+
export function aiEndpoint(config) {
|
|
407
|
+
const endpoint = config.ai?.endpoint || "/api/ai";
|
|
408
|
+
return endpoint.startsWith("/") ? endpoint.replace(/\/$/, "") || "/api/ai" : `/${endpoint.replace(/\/$/, "")}`;
|
|
409
|
+
}
|
|
410
|
+
export function normalizeAiLimits(config) {
|
|
411
|
+
return { ...defaultAiLimits, ...(config.ai?.limits ?? {}) };
|
|
412
|
+
}
|
|
413
|
+
export function normalizeAiDefaults(config) {
|
|
414
|
+
return {
|
|
415
|
+
...defaultAiDefaults,
|
|
416
|
+
...(config.ai?.defaults ?? {}),
|
|
417
|
+
modelParams: normalizeModelParams(config.ai?.defaults?.modelParams ?? {})
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
export async function fetchRecommendedAiModelsManifest(url = raybaseRecommendedModelsManifestUrl) {
|
|
421
|
+
const response = await fetch(url, {
|
|
422
|
+
headers: { accept: "application/json" }
|
|
423
|
+
});
|
|
424
|
+
if (!response.ok)
|
|
425
|
+
return null;
|
|
426
|
+
const value = await response.json();
|
|
427
|
+
if (!value || typeof value !== "object")
|
|
428
|
+
return null;
|
|
429
|
+
const manifest = value;
|
|
430
|
+
if (manifest.schemaVersion !== 1)
|
|
431
|
+
return null;
|
|
432
|
+
return manifest;
|
|
433
|
+
}
|
|
434
|
+
export function recommendedDefaultAiModel(manifest) {
|
|
435
|
+
const model = manifest?.defaults?.guide?.model ?? manifest?.defaults?.chat?.model;
|
|
436
|
+
return typeof model === "string" && model.trim() ? model.trim() : null;
|
|
437
|
+
}
|
|
438
|
+
export function requireConfiguredAiModel(model, source = "AI model") {
|
|
439
|
+
const resolved = typeof model === "string" ? model.trim() : "";
|
|
440
|
+
if (resolved)
|
|
441
|
+
return resolved;
|
|
442
|
+
throw new RaybaseAiModelNotConfiguredError(source);
|
|
443
|
+
}
|
|
444
|
+
export function aiGatewayId(config) {
|
|
445
|
+
return config.ai?.gateway?.id ?? null;
|
|
446
|
+
}
|
|
447
|
+
export function buildAiGatewayOptions(input) {
|
|
448
|
+
const gatewayId = effectiveAiGatewayId(input.config, input.env);
|
|
449
|
+
if (!gatewayId)
|
|
450
|
+
return {};
|
|
451
|
+
const gateway = {
|
|
452
|
+
id: gatewayId,
|
|
453
|
+
cache: input.env?.RAYBASE_ENV === "local" && input.env.RAYBASE_AI_GATEWAY_CACHE !== undefined
|
|
454
|
+
? parseAiGatewayCache(input.env.RAYBASE_AI_GATEWAY_CACHE)
|
|
455
|
+
: input.config.ai?.gateway?.cache,
|
|
456
|
+
metadata: {
|
|
457
|
+
raybaseApp: input.config.appName,
|
|
458
|
+
raybaseAgent: input.agent ?? null,
|
|
459
|
+
raybasePrincipalKind: input.principalKind ?? (input.publicId ? "public" : "user"),
|
|
460
|
+
raybaseUser: input.userId ?? null,
|
|
461
|
+
raybasePublicPrincipal: input.publicId ?? null,
|
|
462
|
+
raybaseService: input.serviceName ?? null,
|
|
463
|
+
raybaseRequest: input.requestId ?? null,
|
|
464
|
+
raybaseSurface: input.surface ?? null,
|
|
465
|
+
raybaseEnv: input.env?.RAYBASE_ENV === "local" ? "local" : "production",
|
|
466
|
+
...(input.metadata ?? {})
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
return { gateway };
|
|
470
|
+
}
|
|
471
|
+
function effectiveAiGatewayProviderAuthorization(config, env) {
|
|
472
|
+
const secretName = config.ai?.gateway?.authorizationSecretName?.trim();
|
|
473
|
+
if (!secretName || !env)
|
|
474
|
+
return null;
|
|
475
|
+
const value = env[secretName];
|
|
476
|
+
return typeof value === "string" && value.trim() ? `Bearer ${value.trim()}` : null;
|
|
477
|
+
}
|
|
478
|
+
function effectiveAiGatewayRequestAuthorization(env) {
|
|
479
|
+
const value = env?.RAYBASE_AI_GATEWAY_AUTH?.trim();
|
|
480
|
+
if (!value)
|
|
481
|
+
return null;
|
|
482
|
+
return /^Bearer\s+/i.test(value) ? value : `Bearer ${value}`;
|
|
483
|
+
}
|
|
484
|
+
export function aiGatewayAuthorizationConfig(config) {
|
|
485
|
+
return {
|
|
486
|
+
authorizationSecretName: config.ai?.gateway?.authorizationSecretName?.trim() || undefined,
|
|
487
|
+
storeKeyName: config.ai?.gateway?.storeKeyName?.trim() || undefined
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function parseAiGatewayCache(value) {
|
|
491
|
+
const normalized = value.trim().toLowerCase();
|
|
492
|
+
if (!normalized)
|
|
493
|
+
return undefined;
|
|
494
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
495
|
+
return true;
|
|
496
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
497
|
+
return false;
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
function effectiveAiGatewayId(config, env) {
|
|
501
|
+
if (env?.RAYBASE_ENV === "local" && env.RAYBASE_AI_GATEWAY_ID !== undefined) {
|
|
502
|
+
return env.RAYBASE_AI_GATEWAY_ID.trim() || null;
|
|
503
|
+
}
|
|
504
|
+
return aiGatewayId(config);
|
|
505
|
+
}
|
|
506
|
+
function normalizeAiPrincipal(options) {
|
|
507
|
+
if (options.principal)
|
|
508
|
+
return options.principal;
|
|
509
|
+
if (options.user)
|
|
510
|
+
return { kind: "user", user: options.user };
|
|
511
|
+
throw new Error("createRaybaseAI requires a signed-in user or explicit principal");
|
|
512
|
+
}
|
|
513
|
+
function usagePrincipalFields(principal) {
|
|
514
|
+
switch (principal.kind) {
|
|
515
|
+
case "user":
|
|
516
|
+
return { principalKind: "user", userId: principal.user.id, publicScopeHash: null, serviceName: null };
|
|
517
|
+
case "public":
|
|
518
|
+
return { principalKind: "public", userId: null, publicScopeHash: principal.id, serviceName: null };
|
|
519
|
+
case "service":
|
|
520
|
+
return { principalKind: "service", userId: null, publicScopeHash: null, serviceName: principal.service };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function usagePromptSplitFields(split) {
|
|
524
|
+
if (!split)
|
|
525
|
+
return { floorChars: null, tailChars: null };
|
|
526
|
+
return {
|
|
527
|
+
floorChars: Math.max(0, Math.round(split.floorChars)),
|
|
528
|
+
tailChars: Math.max(0, Math.round(split.tailChars))
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
export function createRaybaseAI(options) {
|
|
532
|
+
const workersProvider = options.env.AI ? createCloudflareWorkersAIInferenceProvider() : undefined;
|
|
533
|
+
const cloudflareProvider = options.env.AI ? createCloudflareInferenceProvider() : undefined;
|
|
534
|
+
const principal = normalizeAiPrincipal(options);
|
|
535
|
+
const defaults = normalizeAiDefaults(options.config);
|
|
536
|
+
const limits = normalizeAiLimits(options.config);
|
|
537
|
+
const requestId = options.requestId ?? options.request?.headers.get("cf-ray") ?? randomId("ai_req");
|
|
538
|
+
const baseSignal = options.signal ?? options.request?.signal;
|
|
539
|
+
const inferenceTape = createInferenceTape(options.env);
|
|
540
|
+
const providerEnv = withoutInferenceTapeEnv(options.env);
|
|
541
|
+
let providerPromise;
|
|
542
|
+
const resolveProvider = () => {
|
|
543
|
+
providerPromise ??= Promise.resolve(options.providerFactory
|
|
544
|
+
? options.providerFactory({
|
|
545
|
+
env: providerEnv,
|
|
546
|
+
request: options.request,
|
|
547
|
+
config: options.config,
|
|
548
|
+
control: options.control,
|
|
549
|
+
principal,
|
|
550
|
+
organizationId: options.organizationId,
|
|
551
|
+
requestId,
|
|
552
|
+
surface: options.surface ?? "http",
|
|
553
|
+
signal: baseSignal,
|
|
554
|
+
moduleOptions: options.config.modules
|
|
555
|
+
})
|
|
556
|
+
: options.provider ?? cloudflareProvider).then((provider) => localAiProvider(options.env, options.config, provider));
|
|
557
|
+
return providerPromise;
|
|
558
|
+
};
|
|
559
|
+
const resolveEmbeddingProvider = async () => {
|
|
560
|
+
const selected = await resolveProvider();
|
|
561
|
+
if (selected?.embed)
|
|
562
|
+
return selected;
|
|
563
|
+
return localAiProvider(options.env, options.config, workersProvider);
|
|
564
|
+
};
|
|
565
|
+
const claimBudget = options.claimBudget ??
|
|
566
|
+
((input) => {
|
|
567
|
+
return dataStub(options.env).claimAiBudget(input);
|
|
568
|
+
});
|
|
569
|
+
async function beforeCall(input) {
|
|
570
|
+
const maxInputChars = input.maxInputChars ?? defaults.maxInputChars;
|
|
571
|
+
if (input.inputChars > maxInputChars) {
|
|
572
|
+
throw new Error(`AI input is ${input.inputChars} characters, exceeding maxInputChars ${maxInputChars}`);
|
|
573
|
+
}
|
|
574
|
+
if (input.budgetMode === "system-exempt")
|
|
575
|
+
return;
|
|
576
|
+
const overhead = input.budgetMode === "system-overhead" || principal.kind === "service";
|
|
577
|
+
const claim = await claimBudget({
|
|
578
|
+
now: nowIso(),
|
|
579
|
+
principalKind: overhead ? undefined : principal.kind,
|
|
580
|
+
userId: overhead ? null : principal.kind === "user" ? principal.user.id : null,
|
|
581
|
+
publicScopeId: overhead ? null : principal.kind === "public" ? principal.id : null,
|
|
582
|
+
appScopeId: "app",
|
|
583
|
+
requestCount: 1,
|
|
584
|
+
estimatedOutputTokens: Math.max(0, input.estimatedOutputTokens),
|
|
585
|
+
estimatedNeurons: null,
|
|
586
|
+
perUserDailyRequests: overhead ? null : limits.perUserDailyRequests ?? null,
|
|
587
|
+
perUserDailyOutputTokens: overhead ? null : limits.perUserDailyOutputTokens ?? null,
|
|
588
|
+
perAppDailyRequests: limits.perAppDailyRequests ?? null,
|
|
589
|
+
perPublicDailyRequests: overhead ? null : limits.perPublicDailyRequests,
|
|
590
|
+
perPublicDailyOutputTokens: overhead ? null : limits.perPublicDailyOutputTokens
|
|
591
|
+
});
|
|
592
|
+
if (!claim.ok) {
|
|
593
|
+
throw new Error(`AI budget exhausted for ${claim.scope}: ${claim.reason}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function record(event) {
|
|
597
|
+
await recordAiUsageEvent(options.control, {
|
|
598
|
+
...event,
|
|
599
|
+
metadata: options.usageMetadata ?? null
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function providerContext(input) {
|
|
603
|
+
const gateway = buildAiGatewayOptions({
|
|
604
|
+
config: options.config,
|
|
605
|
+
env: providerEnv,
|
|
606
|
+
agent: input.agent ?? options.agent,
|
|
607
|
+
userId: principal.kind === "user" ? principal.user.id : undefined,
|
|
608
|
+
principalKind: principal.kind,
|
|
609
|
+
publicId: principal.kind === "public" ? principal.id : undefined,
|
|
610
|
+
serviceName: principal.kind === "service" ? principal.service : undefined,
|
|
611
|
+
requestId,
|
|
612
|
+
surface: options.surface ?? "http",
|
|
613
|
+
metadata: input.metadata
|
|
614
|
+
});
|
|
615
|
+
return {
|
|
616
|
+
env: providerEnv,
|
|
617
|
+
request: options.request,
|
|
618
|
+
config: options.config,
|
|
619
|
+
principal,
|
|
620
|
+
organizationId: options.organizationId,
|
|
621
|
+
surface: options.surface ?? "http",
|
|
622
|
+
agent: input.agent ?? options.agent,
|
|
623
|
+
conversationId: input.conversationId ?? options.conversationId,
|
|
624
|
+
requestId,
|
|
625
|
+
gatewayOptions: { ...gateway, ...(input.gateway ?? {}) },
|
|
626
|
+
metadata: input.metadata,
|
|
627
|
+
signal: input.signal ?? baseSignal
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function tapeCorrelation(input) {
|
|
631
|
+
return {
|
|
632
|
+
...(input.agent ?? options.agent ? { agent: input.agent ?? options.agent } : {}),
|
|
633
|
+
...(input.conversationId ?? options.conversationId
|
|
634
|
+
? { conversationId: input.conversationId ?? options.conversationId }
|
|
635
|
+
: {}),
|
|
636
|
+
requestId,
|
|
637
|
+
...(options.turnKey ? { turnKey: options.turnKey } : {}),
|
|
638
|
+
surface: options.surface ?? "http",
|
|
639
|
+
...(options.organizationId ? { organizationId: options.organizationId } : {}),
|
|
640
|
+
...(effectiveAiGatewayId(options.config, options.env)
|
|
641
|
+
? { gatewayId: effectiveAiGatewayId(options.config, options.env) }
|
|
642
|
+
: {})
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const client = {
|
|
646
|
+
async chat(input, chatOptions = {}) {
|
|
647
|
+
const provider = await resolveProvider();
|
|
648
|
+
if (!provider)
|
|
649
|
+
throw new Error("Raybase AI is enabled in appConfig but no AI provider is available");
|
|
650
|
+
const model = requireConfiguredAiModel(input.model || options.defaultModel || defaults.model);
|
|
651
|
+
const inputChars = chatRequestInputChars(input);
|
|
652
|
+
const promptSplit = usagePromptSplitFields(chatOptions.promptSplit);
|
|
653
|
+
const maxOutputTokens = input.maxOutputTokens ?? chatOptions.maxOutputTokens ?? defaults.maxOutputTokens;
|
|
654
|
+
const requestKind = chatOptions.requestKind ?? "chat";
|
|
655
|
+
const role = aiUsageRole(chatOptions.metadata ?? input.metadata);
|
|
656
|
+
const modelParams = effectiveModelParams(defaults.modelParams, {
|
|
657
|
+
modelParams: { ...(chatOptions.modelParams ?? {}), ...(input.modelParams ?? {}) }
|
|
658
|
+
});
|
|
659
|
+
await beforeCall({
|
|
660
|
+
agent: chatOptions.agent,
|
|
661
|
+
requestKind,
|
|
662
|
+
budgetMode: chatOptions.budgetMode,
|
|
663
|
+
inputChars,
|
|
664
|
+
estimatedOutputTokens: maxOutputTokens,
|
|
665
|
+
maxInputChars: chatOptions.maxInputChars
|
|
666
|
+
});
|
|
667
|
+
const request = {
|
|
668
|
+
...input,
|
|
669
|
+
model,
|
|
670
|
+
maxOutputTokens,
|
|
671
|
+
modelParams,
|
|
672
|
+
...(input.tools ? { tools: strictChatTools(input.tools) } : {})
|
|
673
|
+
};
|
|
674
|
+
const tapeCall = await inferenceTape.begin({
|
|
675
|
+
operation: "chat",
|
|
676
|
+
provider: provider.id ?? "unknown",
|
|
677
|
+
requestKind,
|
|
678
|
+
roleOrLane: role ?? requestKind,
|
|
679
|
+
request,
|
|
680
|
+
...(chatOptions.promptBandDiagnostics
|
|
681
|
+
? { diagnostics: { promptBands: chatOptions.promptBandDiagnostics } }
|
|
682
|
+
: {}),
|
|
683
|
+
correlation: tapeCorrelation({ agent: chatOptions.agent, conversationId: chatOptions.conversationId })
|
|
684
|
+
});
|
|
685
|
+
const started = Date.now();
|
|
686
|
+
try {
|
|
687
|
+
const result = await provider.chat(request, providerContext({
|
|
688
|
+
agent: chatOptions.agent,
|
|
689
|
+
conversationId: chatOptions.conversationId,
|
|
690
|
+
gateway: chatOptions.gateway,
|
|
691
|
+
metadata: chatOptions.metadata ?? input.metadata,
|
|
692
|
+
signal: chatOptions.signal
|
|
693
|
+
}));
|
|
694
|
+
await tapeCall.completed(result);
|
|
695
|
+
await record({
|
|
696
|
+
...usagePrincipalFields(principal),
|
|
697
|
+
agent: chatOptions.agent ?? options.agent ?? null,
|
|
698
|
+
conversationId: chatOptions.conversationId ?? options.conversationId ?? null,
|
|
699
|
+
model,
|
|
700
|
+
requestKind,
|
|
701
|
+
role,
|
|
702
|
+
gatewayId: effectiveAiGatewayId(options.config, options.env),
|
|
703
|
+
gatewayLogId: result.provider?.requestId ?? null,
|
|
704
|
+
inputChars,
|
|
705
|
+
...promptSplit,
|
|
706
|
+
outputChars: result.text.length,
|
|
707
|
+
estimatedInputTokens: result.usage?.inputTokens ?? estimateTokensFromChars(inputChars),
|
|
708
|
+
estimatedOutputTokens: result.usage?.outputTokens ?? estimateTokensFromChars(result.text.length),
|
|
709
|
+
estimatedNeurons: null,
|
|
710
|
+
modelParams,
|
|
711
|
+
status: "ok",
|
|
712
|
+
durationMs: Date.now() - started
|
|
713
|
+
});
|
|
714
|
+
return {
|
|
715
|
+
...result,
|
|
716
|
+
// The padding the strict form required, undone against the tools as
|
|
717
|
+
// the caller declared them, before anything parses these arguments.
|
|
718
|
+
toolCalls: restoreStrictToolCalls(result.toolCalls, input.tools),
|
|
719
|
+
provider: { id: result.provider?.id ?? provider.id, requestId: result.provider?.requestId, raw: result.provider?.raw }
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
await tapeCall.error(error);
|
|
724
|
+
await record({
|
|
725
|
+
...usagePrincipalFields(principal),
|
|
726
|
+
agent: chatOptions.agent ?? options.agent ?? null,
|
|
727
|
+
conversationId: chatOptions.conversationId ?? options.conversationId ?? null,
|
|
728
|
+
model,
|
|
729
|
+
requestKind,
|
|
730
|
+
role,
|
|
731
|
+
gatewayId: effectiveAiGatewayId(options.config, options.env),
|
|
732
|
+
inputChars,
|
|
733
|
+
...promptSplit,
|
|
734
|
+
outputChars: 0,
|
|
735
|
+
estimatedInputTokens: estimateTokensFromChars(inputChars),
|
|
736
|
+
estimatedOutputTokens: 0,
|
|
737
|
+
modelParams,
|
|
738
|
+
status: "error",
|
|
739
|
+
error: safeInferenceProviderErrorMessage(error),
|
|
740
|
+
durationMs: Date.now() - started
|
|
741
|
+
});
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
async chatStream(input, streamOptions = {}) {
|
|
746
|
+
const provider = await resolveProvider();
|
|
747
|
+
if (!provider)
|
|
748
|
+
throw new Error("Raybase AI is enabled in appConfig but no AI provider is available");
|
|
749
|
+
const model = requireConfiguredAiModel(input.model || options.defaultModel || defaults.model);
|
|
750
|
+
const inputChars = chatRequestInputChars(input);
|
|
751
|
+
const promptSplit = usagePromptSplitFields(streamOptions.promptSplit);
|
|
752
|
+
const maxOutputTokens = input.maxOutputTokens ?? streamOptions.maxOutputTokens ?? defaults.maxOutputTokens;
|
|
753
|
+
const requestKind = streamOptions.requestKind ?? "chat";
|
|
754
|
+
const role = aiUsageRole(streamOptions.metadata ?? input.metadata);
|
|
755
|
+
const modelParams = effectiveModelParams(defaults.modelParams, {
|
|
756
|
+
modelParams: { ...(streamOptions.modelParams ?? {}), ...(input.modelParams ?? {}) }
|
|
757
|
+
});
|
|
758
|
+
await beforeCall({
|
|
759
|
+
agent: streamOptions.agent,
|
|
760
|
+
requestKind,
|
|
761
|
+
budgetMode: streamOptions.budgetMode,
|
|
762
|
+
inputChars,
|
|
763
|
+
estimatedOutputTokens: maxOutputTokens,
|
|
764
|
+
maxInputChars: streamOptions.maxInputChars
|
|
765
|
+
});
|
|
766
|
+
const request = {
|
|
767
|
+
...input,
|
|
768
|
+
model,
|
|
769
|
+
maxOutputTokens,
|
|
770
|
+
modelParams,
|
|
771
|
+
...(input.tools ? { tools: strictChatTools(input.tools) } : {})
|
|
772
|
+
};
|
|
773
|
+
const tapeCall = await inferenceTape.begin({
|
|
774
|
+
operation: "chatStream",
|
|
775
|
+
provider: provider.id ?? "unknown",
|
|
776
|
+
requestKind,
|
|
777
|
+
roleOrLane: role ?? requestKind,
|
|
778
|
+
request,
|
|
779
|
+
...(streamOptions.promptBandDiagnostics
|
|
780
|
+
? { diagnostics: { promptBands: streamOptions.promptBandDiagnostics } }
|
|
781
|
+
: {}),
|
|
782
|
+
correlation: tapeCorrelation({ agent: streamOptions.agent, conversationId: streamOptions.conversationId })
|
|
783
|
+
});
|
|
784
|
+
const started = Date.now();
|
|
785
|
+
try {
|
|
786
|
+
const source = provider.chatStream
|
|
787
|
+
? await provider.chatStream(request, providerContext({
|
|
788
|
+
agent: streamOptions.agent,
|
|
789
|
+
conversationId: streamOptions.conversationId,
|
|
790
|
+
gateway: streamOptions.gateway,
|
|
791
|
+
metadata: streamOptions.metadata ?? input.metadata,
|
|
792
|
+
signal: streamOptions.signal
|
|
793
|
+
}))
|
|
794
|
+
: chatResultToStream(await provider.chat(request, providerContext({
|
|
795
|
+
agent: streamOptions.agent,
|
|
796
|
+
conversationId: streamOptions.conversationId,
|
|
797
|
+
gateway: streamOptions.gateway,
|
|
798
|
+
metadata: streamOptions.metadata ?? input.metadata,
|
|
799
|
+
signal: streamOptions.signal
|
|
800
|
+
})));
|
|
801
|
+
return recordChatStreamUsage(restoreStrictToolCallStream(tapeChatStream(source, tapeCall), input.tools), {
|
|
802
|
+
record,
|
|
803
|
+
principal,
|
|
804
|
+
agent: streamOptions.agent ?? options.agent ?? null,
|
|
805
|
+
conversationId: streamOptions.conversationId ?? options.conversationId ?? null,
|
|
806
|
+
model,
|
|
807
|
+
requestKind,
|
|
808
|
+
role,
|
|
809
|
+
gatewayId: effectiveAiGatewayId(options.config, options.env),
|
|
810
|
+
inputChars,
|
|
811
|
+
promptSplit,
|
|
812
|
+
modelParams,
|
|
813
|
+
started,
|
|
814
|
+
maxOutputTokens
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
await tapeCall.error(error);
|
|
819
|
+
await record({
|
|
820
|
+
...usagePrincipalFields(principal),
|
|
821
|
+
agent: streamOptions.agent ?? options.agent ?? null,
|
|
822
|
+
conversationId: streamOptions.conversationId ?? options.conversationId ?? null,
|
|
823
|
+
model,
|
|
824
|
+
requestKind,
|
|
825
|
+
role,
|
|
826
|
+
gatewayId: effectiveAiGatewayId(options.config, options.env),
|
|
827
|
+
inputChars,
|
|
828
|
+
...promptSplit,
|
|
829
|
+
outputChars: 0,
|
|
830
|
+
estimatedInputTokens: estimateTokensFromChars(inputChars),
|
|
831
|
+
estimatedOutputTokens: 0,
|
|
832
|
+
modelParams,
|
|
833
|
+
status: "error",
|
|
834
|
+
error: safeInferenceProviderErrorMessage(error),
|
|
835
|
+
durationMs: Date.now() - started
|
|
836
|
+
});
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
async embed(input, embedOptions = {}) {
|
|
841
|
+
const embeddingProvider = await resolveEmbeddingProvider();
|
|
842
|
+
if (!embeddingProvider?.embed)
|
|
843
|
+
throw new Error("Raybase AI embedding provider is unavailable");
|
|
844
|
+
const model = requireConfiguredAiModel(embedOptions.model ?? defaults.embeddingModel, "AI embedding model");
|
|
845
|
+
const inputChars = Array.isArray(input) ? input.join("\n").length : input.length;
|
|
846
|
+
const role = aiUsageRole(embedOptions.metadata);
|
|
847
|
+
const maxInputChars = embedOptions.maxInputChars ?? defaults.maxInputChars;
|
|
848
|
+
if (inputChars > maxInputChars)
|
|
849
|
+
throw new Error(`AI embedding input exceeds maxInputChars ${maxInputChars}`);
|
|
850
|
+
await beforeCall({
|
|
851
|
+
agent: embedOptions.agent,
|
|
852
|
+
requestKind: "embedding",
|
|
853
|
+
budgetMode: embedOptions.budgetMode,
|
|
854
|
+
inputChars,
|
|
855
|
+
estimatedOutputTokens: 0,
|
|
856
|
+
maxInputChars
|
|
857
|
+
});
|
|
858
|
+
const request = { model, input, metadata: embedOptions.metadata };
|
|
859
|
+
const tapeCall = await inferenceTape.begin({
|
|
860
|
+
operation: "embed",
|
|
861
|
+
provider: embeddingProvider.id ?? "unknown",
|
|
862
|
+
requestKind: "embedding",
|
|
863
|
+
roleOrLane: role ?? "embedding",
|
|
864
|
+
request,
|
|
865
|
+
correlation: tapeCorrelation({ agent: embedOptions.agent, conversationId: embedOptions.conversationId })
|
|
866
|
+
});
|
|
867
|
+
const started = Date.now();
|
|
868
|
+
try {
|
|
869
|
+
const result = await embeddingProvider.embed(request, providerContext({
|
|
870
|
+
agent: embedOptions.agent,
|
|
871
|
+
conversationId: embedOptions.conversationId,
|
|
872
|
+
gateway: embedOptions.gateway,
|
|
873
|
+
metadata: embedOptions.metadata,
|
|
874
|
+
signal: embedOptions.signal
|
|
875
|
+
}));
|
|
876
|
+
await tapeCall.completed(result);
|
|
877
|
+
await recordAiEmbeddingUsage({
|
|
878
|
+
record,
|
|
879
|
+
principal,
|
|
880
|
+
agent: embedOptions.agent ?? options.agent ?? null,
|
|
881
|
+
conversationId: embedOptions.conversationId ?? options.conversationId ?? null,
|
|
882
|
+
model,
|
|
883
|
+
gatewayId: effectiveAiGatewayId(options.config, options.env),
|
|
884
|
+
raw: result.provider?.raw,
|
|
885
|
+
inputChars,
|
|
886
|
+
role,
|
|
887
|
+
started
|
|
888
|
+
});
|
|
889
|
+
return result.embeddings;
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
await tapeCall.error(error);
|
|
893
|
+
await record({
|
|
894
|
+
...usagePrincipalFields(principal),
|
|
895
|
+
agent: embedOptions.agent ?? options.agent ?? null,
|
|
896
|
+
conversationId: embedOptions.conversationId ?? options.conversationId ?? null,
|
|
897
|
+
model,
|
|
898
|
+
requestKind: "embedding",
|
|
899
|
+
role,
|
|
900
|
+
gatewayId: effectiveAiGatewayId(options.config, options.env),
|
|
901
|
+
inputChars,
|
|
902
|
+
outputChars: 0,
|
|
903
|
+
estimatedInputTokens: estimateTokensFromChars(inputChars),
|
|
904
|
+
estimatedOutputTokens: 0,
|
|
905
|
+
status: "error",
|
|
906
|
+
error: safeInferenceProviderErrorMessage(error),
|
|
907
|
+
durationMs: Date.now() - started
|
|
908
|
+
});
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
object: (input, objectOptions) => raybaseObjectCall((request, callOptions) => client.chat(request, callOptions))(input, objectOptions)
|
|
913
|
+
};
|
|
914
|
+
return client;
|
|
915
|
+
}
|
|
916
|
+
/** Tool calls in a stream, restored the same way `chat` restores its own. */
|
|
917
|
+
async function* restoreStrictToolCallStream(source, tools) {
|
|
918
|
+
if (!tools?.length) {
|
|
919
|
+
yield* source;
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
for await (const event of source) {
|
|
923
|
+
if (event.type === "tool-call") {
|
|
924
|
+
yield { ...event, toolCall: restoreStrictToolCalls([event.toolCall], tools)[0] };
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (event.type === "done" && event.result?.toolCalls?.length) {
|
|
928
|
+
yield { ...event, result: { ...event.result, toolCalls: restoreStrictToolCalls(event.result.toolCalls, tools) } };
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
yield event;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
export const disabledRaybaseAI = {
|
|
935
|
+
async chat() {
|
|
936
|
+
throw new Error("Raybase AI is not available in this context");
|
|
937
|
+
},
|
|
938
|
+
async chatStream() {
|
|
939
|
+
throw new Error("Raybase AI is not available in this context");
|
|
940
|
+
},
|
|
941
|
+
async object() {
|
|
942
|
+
throw new Error("Raybase AI is not available in this context");
|
|
943
|
+
},
|
|
944
|
+
async embed() {
|
|
945
|
+
throw new Error("Raybase AI is not available in this context");
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
async function recordAiEmbeddingUsage(input) {
|
|
949
|
+
await input.record({
|
|
950
|
+
...usagePrincipalFields(input.principal),
|
|
951
|
+
agent: input.agent,
|
|
952
|
+
conversationId: input.conversationId,
|
|
953
|
+
model: input.model,
|
|
954
|
+
requestKind: "embedding",
|
|
955
|
+
role: input.role,
|
|
956
|
+
gatewayId: input.gatewayId,
|
|
957
|
+
gatewayLogId: gatewayLogId(input.raw),
|
|
958
|
+
inputChars: input.inputChars,
|
|
959
|
+
outputChars: 0,
|
|
960
|
+
estimatedInputTokens: estimateTokensFromChars(input.inputChars),
|
|
961
|
+
estimatedOutputTokens: 0,
|
|
962
|
+
estimatedNeurons: null,
|
|
963
|
+
status: "ok",
|
|
964
|
+
durationMs: Date.now() - input.started
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
function recordChatStreamUsage(source, input) {
|
|
968
|
+
return (async function* streamWithUsage() {
|
|
969
|
+
let outputText = "";
|
|
970
|
+
let doneResult;
|
|
971
|
+
try {
|
|
972
|
+
for await (const event of source) {
|
|
973
|
+
if (event.type === "text-delta")
|
|
974
|
+
outputText += event.text;
|
|
975
|
+
if (event.type === "done")
|
|
976
|
+
doneResult = event.result;
|
|
977
|
+
yield event;
|
|
978
|
+
}
|
|
979
|
+
const recordedText = typeof doneResult?.text === "string" ? doneResult.text : outputText;
|
|
980
|
+
await input.record({
|
|
981
|
+
...usagePrincipalFields(input.principal),
|
|
982
|
+
agent: input.agent,
|
|
983
|
+
conversationId: input.conversationId,
|
|
984
|
+
model: input.model,
|
|
985
|
+
requestKind: input.requestKind,
|
|
986
|
+
role: input.role,
|
|
987
|
+
gatewayId: input.gatewayId,
|
|
988
|
+
gatewayLogId: doneResult?.provider?.requestId ?? null,
|
|
989
|
+
inputChars: input.inputChars,
|
|
990
|
+
...input.promptSplit,
|
|
991
|
+
outputChars: recordedText.length,
|
|
992
|
+
estimatedInputTokens: doneResult?.usage?.inputTokens ?? estimateTokensFromChars(input.inputChars),
|
|
993
|
+
estimatedOutputTokens: doneResult?.usage?.outputTokens ?? estimateTokensFromChars(recordedText.length || input.maxOutputTokens),
|
|
994
|
+
estimatedNeurons: null,
|
|
995
|
+
modelParams: input.modelParams,
|
|
996
|
+
status: "ok",
|
|
997
|
+
durationMs: Date.now() - input.started
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
await input.record({
|
|
1002
|
+
...usagePrincipalFields(input.principal),
|
|
1003
|
+
agent: input.agent,
|
|
1004
|
+
conversationId: input.conversationId,
|
|
1005
|
+
model: input.model,
|
|
1006
|
+
requestKind: input.requestKind,
|
|
1007
|
+
role: input.role,
|
|
1008
|
+
gatewayId: input.gatewayId,
|
|
1009
|
+
inputChars: input.inputChars,
|
|
1010
|
+
...input.promptSplit,
|
|
1011
|
+
outputChars: 0,
|
|
1012
|
+
estimatedInputTokens: estimateTokensFromChars(input.inputChars),
|
|
1013
|
+
estimatedOutputTokens: 0,
|
|
1014
|
+
modelParams: input.modelParams,
|
|
1015
|
+
status: "error",
|
|
1016
|
+
error: safeInferenceProviderErrorMessage(error),
|
|
1017
|
+
durationMs: Date.now() - input.started
|
|
1018
|
+
});
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1021
|
+
})();
|
|
1022
|
+
}
|
|
1023
|
+
async function* chatResultToStream(result) {
|
|
1024
|
+
if (result.text)
|
|
1025
|
+
yield { type: "text-delta", text: result.text };
|
|
1026
|
+
for (const toolCall of result.toolCalls)
|
|
1027
|
+
yield { type: "tool-call", toolCall };
|
|
1028
|
+
yield { type: "done", result };
|
|
1029
|
+
}
|
|
1030
|
+
export function createCloudflareInferenceProvider() {
|
|
1031
|
+
const workers = createCloudflareWorkersAIInferenceProvider();
|
|
1032
|
+
const gateway = createCloudflareAiGatewayInferenceProvider();
|
|
1033
|
+
return {
|
|
1034
|
+
id: "cloudflare",
|
|
1035
|
+
async chat(input, context) {
|
|
1036
|
+
return shouldUseProviderNativeAiGateway(input.model, context) ? gateway.chat(input, context) : workers.chat(input, context);
|
|
1037
|
+
},
|
|
1038
|
+
async chatStream(input, context) {
|
|
1039
|
+
return shouldUseProviderNativeAiGateway(input.model, context) ? gateway.chatStream(input, context) : workers.chatStream(input, context);
|
|
1040
|
+
},
|
|
1041
|
+
async embed(input, context) {
|
|
1042
|
+
return workers.embed(input, context);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
export function createCloudflareAiGatewayInferenceProvider() {
|
|
1047
|
+
return {
|
|
1048
|
+
id: "cloudflare-ai-gateway",
|
|
1049
|
+
async chat(input, context) {
|
|
1050
|
+
const response = await fetch(await aiGatewayChatCompletionsUrl(input.model, context), {
|
|
1051
|
+
method: "POST",
|
|
1052
|
+
headers: aiGatewayProviderHeaders(input.model, context, false),
|
|
1053
|
+
body: stableJson(aiGatewayChatPayload(input)),
|
|
1054
|
+
signal: context.signal
|
|
1055
|
+
});
|
|
1056
|
+
const raw = await parseAiGatewayJsonResponse(response);
|
|
1057
|
+
const result = normalizeWorkersAiChatResult(raw, "cloudflare-ai-gateway");
|
|
1058
|
+
return {
|
|
1059
|
+
...result,
|
|
1060
|
+
provider: {
|
|
1061
|
+
id: "cloudflare-ai-gateway",
|
|
1062
|
+
requestId: gatewayLogIdFromHeaders(response.headers) ?? result.provider?.requestId,
|
|
1063
|
+
raw
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
},
|
|
1067
|
+
async chatStream(input, context) {
|
|
1068
|
+
const response = await fetch(await aiGatewayChatCompletionsUrl(input.model, context), {
|
|
1069
|
+
method: "POST",
|
|
1070
|
+
headers: aiGatewayProviderHeaders(input.model, context, true),
|
|
1071
|
+
body: stableJson(aiGatewayChatPayload(input, true)),
|
|
1072
|
+
signal: context.signal
|
|
1073
|
+
});
|
|
1074
|
+
if (!response.ok)
|
|
1075
|
+
await throwAiGatewayResponseError(response);
|
|
1076
|
+
return aiGatewayRawStreamEvents(response);
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
export function createCloudflareWorkersAIInferenceProvider() {
|
|
1081
|
+
return {
|
|
1082
|
+
id: "workers-ai",
|
|
1083
|
+
async chat(input, context) {
|
|
1084
|
+
const raw = await requireWorkersAI(context.env).run(input.model, workersAiChatPayload(input), context.gatewayOptions);
|
|
1085
|
+
return normalizeWorkersAiChatResult(raw, "workers-ai");
|
|
1086
|
+
},
|
|
1087
|
+
async chatStream(input, context) {
|
|
1088
|
+
const raw = await requireWorkersAI(context.env).run(input.model, workersAiChatPayload(input, true), context.gatewayOptions);
|
|
1089
|
+
return workersAiRawStreamEvents(raw);
|
|
1090
|
+
},
|
|
1091
|
+
async embed(input, context) {
|
|
1092
|
+
const raw = await requireWorkersAI(context.env).run(input.model, { text: input.input }, context.gatewayOptions);
|
|
1093
|
+
return {
|
|
1094
|
+
embeddings: parseEmbeddingResult(raw),
|
|
1095
|
+
provider: { id: "workers-ai", requestId: gatewayLogId(raw) ?? undefined, raw }
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
export const raybaseTranscriptionModel = "@cf/openai/whisper-large-v3-turbo";
|
|
1101
|
+
export async function transcribeAudioWithWorkersAI(env, input) {
|
|
1102
|
+
const result = await requireWorkersAI(env).run(raybaseTranscriptionModel, {
|
|
1103
|
+
audio: input.audioBase64
|
|
1104
|
+
});
|
|
1105
|
+
const transcript = result && typeof result === "object" && typeof result.text === "string"
|
|
1106
|
+
? result.text.trim()
|
|
1107
|
+
: "";
|
|
1108
|
+
return { transcript };
|
|
1109
|
+
}
|
|
1110
|
+
function requireWorkersAI(env) {
|
|
1111
|
+
if (!env.AI)
|
|
1112
|
+
throw new Error("Workers AI binding is unavailable");
|
|
1113
|
+
return env.AI;
|
|
1114
|
+
}
|
|
1115
|
+
function workersAiChatPayload(input, stream = false) {
|
|
1116
|
+
const payload = {
|
|
1117
|
+
messages: input.messages.map(workersAiChatMessage),
|
|
1118
|
+
...(input.maxOutputTokens !== undefined ? { max_tokens: input.maxOutputTokens } : {}),
|
|
1119
|
+
...(input.modelParams ?? {}),
|
|
1120
|
+
...chatProviderParams(input.providerParams)
|
|
1121
|
+
};
|
|
1122
|
+
if (input.tools?.length)
|
|
1123
|
+
payload.tools = input.tools.map(workersAiTool);
|
|
1124
|
+
if (input.responseFormat)
|
|
1125
|
+
payload.response_format = chatResponseFormatPayload(input.responseFormat);
|
|
1126
|
+
if (stream)
|
|
1127
|
+
payload.stream = true;
|
|
1128
|
+
return payload;
|
|
1129
|
+
}
|
|
1130
|
+
function chatProviderParams(params) {
|
|
1131
|
+
if (!params?.verbosity)
|
|
1132
|
+
return {};
|
|
1133
|
+
return { verbosity: params.verbosity };
|
|
1134
|
+
}
|
|
1135
|
+
function chatResponseFormatPayload(format) {
|
|
1136
|
+
if (format.type === "json_object")
|
|
1137
|
+
return { type: "json_object" };
|
|
1138
|
+
return {
|
|
1139
|
+
type: "json_schema",
|
|
1140
|
+
json_schema: {
|
|
1141
|
+
name: format.name,
|
|
1142
|
+
schema: format.schema,
|
|
1143
|
+
...(format.description ? { description: format.description } : {}),
|
|
1144
|
+
...(format.strict !== undefined ? { strict: format.strict } : {})
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function workersAiChatMessage(message) {
|
|
1149
|
+
if (message.role === "tool")
|
|
1150
|
+
return { role: "tool", tool_call_id: message.toolCallId, content: message.content };
|
|
1151
|
+
const content = message.role === "user" && Array.isArray(message.content)
|
|
1152
|
+
? message.content.map(workersAiChatContentPart)
|
|
1153
|
+
: message.content;
|
|
1154
|
+
const output = { role: message.role, content };
|
|
1155
|
+
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
1156
|
+
output.tool_calls = message.toolCalls.map((call) => ({
|
|
1157
|
+
id: call.id,
|
|
1158
|
+
type: "function",
|
|
1159
|
+
function: { name: call.name, arguments: stableJson(call.arguments ?? {}) }
|
|
1160
|
+
}));
|
|
1161
|
+
}
|
|
1162
|
+
return output;
|
|
1163
|
+
}
|
|
1164
|
+
function workersAiChatContentPart(part) {
|
|
1165
|
+
if (part.type === "text")
|
|
1166
|
+
return { type: "text", text: part.text };
|
|
1167
|
+
const url = part.type === "image"
|
|
1168
|
+
? `data:${part.mediaType};base64,${part.data}`
|
|
1169
|
+
: part.url;
|
|
1170
|
+
return { type: "image_url", image_url: { url } };
|
|
1171
|
+
}
|
|
1172
|
+
function workersAiTool(tool) {
|
|
1173
|
+
return {
|
|
1174
|
+
type: "function",
|
|
1175
|
+
function: {
|
|
1176
|
+
name: tool.name,
|
|
1177
|
+
description: tool.description ?? "",
|
|
1178
|
+
parameters: tool.parameters ?? { type: "object", properties: {} }
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
function shouldUseProviderNativeAiGateway(model, context) {
|
|
1183
|
+
if (model.startsWith("@cf/"))
|
|
1184
|
+
return false;
|
|
1185
|
+
if (!effectiveAiGatewayId(context.config, context.env))
|
|
1186
|
+
return false;
|
|
1187
|
+
return Boolean(context.config.ai?.gateway?.authorizationSecretName?.trim() ||
|
|
1188
|
+
context.config.ai?.gateway?.storeKeyName?.trim() ||
|
|
1189
|
+
context.env.RAYBASE_AI_GATEWAY_AUTH?.trim());
|
|
1190
|
+
}
|
|
1191
|
+
async function aiGatewayChatCompletionsUrl(model, context) {
|
|
1192
|
+
const gatewayId = effectiveAiGatewayId(context.config, context.env);
|
|
1193
|
+
if (!gatewayId)
|
|
1194
|
+
throw new Error("AI Gateway id is required for provider-native AI Gateway requests");
|
|
1195
|
+
const provider = aiGatewayProviderSlug(model);
|
|
1196
|
+
const gateway = context.env.AI?.gateway?.(gatewayId);
|
|
1197
|
+
if (!gateway?.getUrl)
|
|
1198
|
+
throw new Error("AI Gateway provider-native requests require env.AI.gateway().getUrl()");
|
|
1199
|
+
const baseUrl = (await gateway.getUrl(provider)).replace(/\/+$/, "");
|
|
1200
|
+
return `${baseUrl}${aiGatewayChatCompletionsPath(provider)}`;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* The OpenAI-compatible chat path under a provider's AI Gateway base. The
|
|
1204
|
+
* gateway appends what follows the provider slug to the provider's own API
|
|
1205
|
+
* root, so the path depends on where each provider mounts its compatible
|
|
1206
|
+
* endpoint: OpenAI-shaped APIs serve `/chat/completions` at their `/v1`
|
|
1207
|
+
* root, while Anthropic's compatibility layer lives at `/v1/chat/completions`
|
|
1208
|
+
* under `api.anthropic.com`.
|
|
1209
|
+
*/
|
|
1210
|
+
export function aiGatewayChatCompletionsPath(provider) {
|
|
1211
|
+
return provider === "anthropic" ? "/v1/chat/completions" : "/chat/completions";
|
|
1212
|
+
}
|
|
1213
|
+
function aiGatewayProviderSlug(model) {
|
|
1214
|
+
const slash = model.indexOf("/");
|
|
1215
|
+
const provider = slash === -1 ? "" : model.slice(0, slash);
|
|
1216
|
+
if (!provider || provider.startsWith("@"))
|
|
1217
|
+
throw new Error(`AI Gateway provider-native model must be provider/model, received ${model}`);
|
|
1218
|
+
return provider;
|
|
1219
|
+
}
|
|
1220
|
+
function aiGatewayProviderModel(model) {
|
|
1221
|
+
const slash = model.indexOf("/");
|
|
1222
|
+
return slash === -1 ? model : model.slice(slash + 1);
|
|
1223
|
+
}
|
|
1224
|
+
function aiGatewayProviderHeaders(model, context, stream) {
|
|
1225
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1226
|
+
if (stream)
|
|
1227
|
+
headers.set("Accept", "text/event-stream");
|
|
1228
|
+
const gatewayAuthorization = effectiveAiGatewayRequestAuthorization(context.env);
|
|
1229
|
+
if (gatewayAuthorization)
|
|
1230
|
+
headers.set("cf-aig-authorization", gatewayAuthorization);
|
|
1231
|
+
const providerAuthorization = effectiveAiGatewayProviderAuthorization(context.config, context.env);
|
|
1232
|
+
if (providerAuthorization) {
|
|
1233
|
+
headers.set("Authorization", providerAuthorization);
|
|
1234
|
+
// Anthropic reads its key from x-api-key; the gateway only recognises a
|
|
1235
|
+
// request-supplied Anthropic credential there, and the compatibility
|
|
1236
|
+
// endpoint accepts either header.
|
|
1237
|
+
if (aiGatewayProviderSlug(model) === "anthropic")
|
|
1238
|
+
headers.set("x-api-key", providerAuthorization.replace(/^Bearer\s+/i, ""));
|
|
1239
|
+
}
|
|
1240
|
+
const storeKeyName = context.config.ai?.gateway?.storeKeyName?.trim();
|
|
1241
|
+
if (storeKeyName && storeKeyName !== "default")
|
|
1242
|
+
headers.set("cf-aig-byok-alias", storeKeyName);
|
|
1243
|
+
headers.set("cf-aig-metadata", stableJson(aiGatewayMetadata(context)));
|
|
1244
|
+
headers.set("x-raybase-model-provider", aiGatewayProviderSlug(model));
|
|
1245
|
+
return headers;
|
|
1246
|
+
}
|
|
1247
|
+
function aiGatewayMetadata(context) {
|
|
1248
|
+
const gateway = asRecord(asRecord(context.gatewayOptions).gateway);
|
|
1249
|
+
return {
|
|
1250
|
+
...asRecord(gateway.metadata),
|
|
1251
|
+
...(context.metadata ?? {})
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function aiGatewayChatPayload(input, stream = false) {
|
|
1255
|
+
const providerModel = aiGatewayProviderModel(input.model);
|
|
1256
|
+
return {
|
|
1257
|
+
...workersAiChatPayload(input, stream),
|
|
1258
|
+
model: providerModel
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
async function parseAiGatewayJsonResponse(response) {
|
|
1262
|
+
const text = await response.text();
|
|
1263
|
+
let parsed = text;
|
|
1264
|
+
if (text.trim()) {
|
|
1265
|
+
try {
|
|
1266
|
+
parsed = JSON.parse(text);
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
parsed = text;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (!response.ok) {
|
|
1273
|
+
const message = typeof parsed === "string" ? parsed : stableJson(parsed);
|
|
1274
|
+
throw new Error(`AI Gateway request failed with ${response.status}: ${message}`);
|
|
1275
|
+
}
|
|
1276
|
+
return parsed;
|
|
1277
|
+
}
|
|
1278
|
+
async function throwAiGatewayResponseError(response) {
|
|
1279
|
+
const body = await response.text();
|
|
1280
|
+
throw new Error(`AI Gateway request failed with ${response.status}: ${body}`);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Tool calls in an OpenAI-shaped chat stream arrive as fragments: the first
|
|
1284
|
+
* delta for an index carries the id and name with empty arguments, later
|
|
1285
|
+
* deltas append argument text, and the finish chunk closes them. Fragments
|
|
1286
|
+
* are gathered here per index and released as one call each at the finish;
|
|
1287
|
+
* a delta that already carries parsed (non-string) arguments is complete and
|
|
1288
|
+
* released at once, which is how Workers AI streams them.
|
|
1289
|
+
*/
|
|
1290
|
+
export class AiStreamToolCallAssembler {
|
|
1291
|
+
pending = new Map();
|
|
1292
|
+
/** Returns the calls completed by this chunk; fragments stay pending. */
|
|
1293
|
+
collect(raw) {
|
|
1294
|
+
const record = asRecord(raw);
|
|
1295
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
1296
|
+
const first = asRecord(choices[0]);
|
|
1297
|
+
const delta = asRecord(first.delta);
|
|
1298
|
+
const fragments = Array.isArray(delta.tool_calls) ? delta.tool_calls : Array.isArray(delta.toolCalls) ? delta.toolCalls : null;
|
|
1299
|
+
const finished = typeof first.finish_reason === "string" || typeof first.finishReason === "string";
|
|
1300
|
+
if (!fragments) {
|
|
1301
|
+
const complete = extractProviderToolCalls(raw);
|
|
1302
|
+
return finished ? [...complete, ...this.flush()] : complete;
|
|
1303
|
+
}
|
|
1304
|
+
const complete = [];
|
|
1305
|
+
fragments.forEach((entry, position) => {
|
|
1306
|
+
const call = asRecord(entry);
|
|
1307
|
+
const fn = asRecord(call.function);
|
|
1308
|
+
const args = call.arguments ?? fn.arguments ?? call.args ?? call.input;
|
|
1309
|
+
if (args !== undefined && typeof args !== "string") {
|
|
1310
|
+
complete.push(...extractProviderToolCalls({ tool_calls: [entry] }));
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const index = typeof call.index === "number" ? call.index : position;
|
|
1314
|
+
const current = this.pending.get(index) ?? { id: "", name: "", arguments: "" };
|
|
1315
|
+
if (typeof call.id === "string" && call.id)
|
|
1316
|
+
current.id = call.id;
|
|
1317
|
+
const name = String(call.name ?? fn.name ?? "");
|
|
1318
|
+
if (name)
|
|
1319
|
+
current.name = name;
|
|
1320
|
+
if (typeof args === "string")
|
|
1321
|
+
current.arguments += args;
|
|
1322
|
+
this.pending.set(index, current);
|
|
1323
|
+
});
|
|
1324
|
+
return finished ? [...complete, ...this.flush()] : complete;
|
|
1325
|
+
}
|
|
1326
|
+
/** Releases every pending call, for the finish chunk and the end of the stream. */
|
|
1327
|
+
flush() {
|
|
1328
|
+
const calls = [...this.pending.entries()]
|
|
1329
|
+
.sort(([left], [right]) => left - right)
|
|
1330
|
+
.map(([index, call]) => {
|
|
1331
|
+
if (!call.name)
|
|
1332
|
+
throw new Error(`Malformed streamed tool call at index ${index}: missing name`);
|
|
1333
|
+
return {
|
|
1334
|
+
id: call.id || `tool_call_${index + 1}_${randomId("call")}`,
|
|
1335
|
+
name: call.name,
|
|
1336
|
+
arguments: parseProviderToolArguments(call.arguments.trim() || "{}")
|
|
1337
|
+
};
|
|
1338
|
+
});
|
|
1339
|
+
this.pending.clear();
|
|
1340
|
+
return calls;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
async function* aiGatewayRawStreamEvents(response) {
|
|
1344
|
+
const stream = coerceReadableStream(response);
|
|
1345
|
+
const reader = stream.getReader();
|
|
1346
|
+
const decoder = new TextDecoder();
|
|
1347
|
+
const requestId = gatewayLogIdFromHeaders(response.headers) ?? undefined;
|
|
1348
|
+
const assembler = new AiStreamToolCallAssembler();
|
|
1349
|
+
let buffered = "";
|
|
1350
|
+
let outputText = "";
|
|
1351
|
+
let sawDone = false;
|
|
1352
|
+
const done = () => ({ type: "done", result: { text: outputText, provider: { id: "cloudflare-ai-gateway", requestId } } });
|
|
1353
|
+
try {
|
|
1354
|
+
while (true) {
|
|
1355
|
+
const { value, done: finished } = await reader.read();
|
|
1356
|
+
if (finished)
|
|
1357
|
+
break;
|
|
1358
|
+
buffered += decoder.decode(value, { stream: true });
|
|
1359
|
+
const splitAt = buffered.lastIndexOf("\n\n");
|
|
1360
|
+
if (splitAt === -1)
|
|
1361
|
+
continue;
|
|
1362
|
+
const ready = buffered.slice(0, splitAt + 2);
|
|
1363
|
+
buffered = buffered.slice(splitAt + 2);
|
|
1364
|
+
for (const event of parseWorkersAiSseText(ready)) {
|
|
1365
|
+
if (event.text) {
|
|
1366
|
+
outputText += event.text;
|
|
1367
|
+
yield { type: "text-delta", text: event.text };
|
|
1368
|
+
}
|
|
1369
|
+
if (event.done) {
|
|
1370
|
+
for (const toolCall of assembler.flush())
|
|
1371
|
+
yield { type: "tool-call", toolCall };
|
|
1372
|
+
sawDone = true;
|
|
1373
|
+
yield done();
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
for (const toolCall of assembler.collect(event.raw))
|
|
1377
|
+
yield { type: "tool-call", toolCall };
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
buffered += decoder.decode();
|
|
1381
|
+
if (buffered.trim()) {
|
|
1382
|
+
for (const event of parseWorkersAiSseText(buffered)) {
|
|
1383
|
+
if (event.text) {
|
|
1384
|
+
outputText += event.text;
|
|
1385
|
+
yield { type: "text-delta", text: event.text };
|
|
1386
|
+
}
|
|
1387
|
+
if (event.done) {
|
|
1388
|
+
for (const toolCall of assembler.flush())
|
|
1389
|
+
yield { type: "tool-call", toolCall };
|
|
1390
|
+
sawDone = true;
|
|
1391
|
+
yield done();
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
for (const toolCall of assembler.collect(event.raw))
|
|
1395
|
+
yield { type: "tool-call", toolCall };
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (!sawDone) {
|
|
1399
|
+
for (const toolCall of assembler.flush())
|
|
1400
|
+
yield { type: "tool-call", toolCall };
|
|
1401
|
+
yield done();
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
finally {
|
|
1405
|
+
reader.releaseLock();
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function normalizeWorkersAiChatResult(raw, providerId) {
|
|
1409
|
+
const text = modelResultText(raw);
|
|
1410
|
+
const toolCalls = extractProviderToolCalls(raw);
|
|
1411
|
+
return {
|
|
1412
|
+
text,
|
|
1413
|
+
toolCalls,
|
|
1414
|
+
finishReason: normalizeFinishReason(raw, toolCalls),
|
|
1415
|
+
usage: normalizeProviderUsage(raw),
|
|
1416
|
+
provider: { id: providerId, requestId: gatewayLogId(raw) ?? undefined, raw }
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
async function* workersAiRawStreamEvents(raw) {
|
|
1420
|
+
if (!(raw instanceof ReadableStream) && !(raw instanceof Response) && typeof raw !== "string") {
|
|
1421
|
+
yield* chatResultToStream(normalizeWorkersAiChatResult(raw, "workers-ai"));
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const stream = coerceReadableStream(raw);
|
|
1425
|
+
const reader = stream.getReader();
|
|
1426
|
+
const decoder = new TextDecoder();
|
|
1427
|
+
let buffered = "";
|
|
1428
|
+
let sawDone = false;
|
|
1429
|
+
try {
|
|
1430
|
+
while (true) {
|
|
1431
|
+
const { value, done } = await reader.read();
|
|
1432
|
+
if (done)
|
|
1433
|
+
break;
|
|
1434
|
+
buffered += decoder.decode(value, { stream: true });
|
|
1435
|
+
const splitAt = buffered.lastIndexOf("\n\n");
|
|
1436
|
+
if (splitAt === -1)
|
|
1437
|
+
continue;
|
|
1438
|
+
const ready = buffered.slice(0, splitAt + 2);
|
|
1439
|
+
buffered = buffered.slice(splitAt + 2);
|
|
1440
|
+
for (const event of parseWorkersAiSseText(ready)) {
|
|
1441
|
+
if (event.text)
|
|
1442
|
+
yield { type: "text-delta", text: event.text };
|
|
1443
|
+
for (const toolCall of extractProviderToolCalls(event.raw))
|
|
1444
|
+
yield { type: "tool-call", toolCall };
|
|
1445
|
+
if (event.done) {
|
|
1446
|
+
sawDone = true;
|
|
1447
|
+
yield { type: "done" };
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
buffered += decoder.decode();
|
|
1452
|
+
if (buffered.trim()) {
|
|
1453
|
+
const events = parseWorkersAiSseText(buffered);
|
|
1454
|
+
if (events.length) {
|
|
1455
|
+
for (const event of events) {
|
|
1456
|
+
if (event.text)
|
|
1457
|
+
yield { type: "text-delta", text: event.text };
|
|
1458
|
+
for (const toolCall of extractProviderToolCalls(event.raw))
|
|
1459
|
+
yield { type: "tool-call", toolCall };
|
|
1460
|
+
if (event.done) {
|
|
1461
|
+
sawDone = true;
|
|
1462
|
+
yield { type: "done" };
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
let parsed = buffered;
|
|
1468
|
+
try {
|
|
1469
|
+
parsed = JSON.parse(buffered);
|
|
1470
|
+
}
|
|
1471
|
+
catch {
|
|
1472
|
+
parsed = buffered;
|
|
1473
|
+
}
|
|
1474
|
+
const result = normalizeWorkersAiChatResult(parsed, "workers-ai");
|
|
1475
|
+
if (result.text)
|
|
1476
|
+
yield { type: "text-delta", text: result.text };
|
|
1477
|
+
for (const toolCall of result.toolCalls)
|
|
1478
|
+
yield { type: "tool-call", toolCall };
|
|
1479
|
+
sawDone = true;
|
|
1480
|
+
yield { type: "done", result };
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (!sawDone)
|
|
1484
|
+
yield { type: "done" };
|
|
1485
|
+
}
|
|
1486
|
+
finally {
|
|
1487
|
+
reader.releaseLock();
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function extractProviderToolCalls(result) {
|
|
1491
|
+
const record = asRecord(result);
|
|
1492
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
1493
|
+
const firstMessage = asRecord(asRecord(choices[0]).message);
|
|
1494
|
+
const firstDelta = asRecord(asRecord(choices[0]).delta);
|
|
1495
|
+
const candidates = [record.tool_calls, record.toolCalls, firstMessage.tool_calls, firstMessage.toolCalls, firstDelta.tool_calls, firstDelta.toolCalls]
|
|
1496
|
+
.find(Array.isArray);
|
|
1497
|
+
return (candidates ?? []).map((raw, index) => {
|
|
1498
|
+
const call = asRecord(raw);
|
|
1499
|
+
const fn = asRecord(call.function);
|
|
1500
|
+
const name = String(call.name ?? fn.name ?? "");
|
|
1501
|
+
if (!name)
|
|
1502
|
+
throw new Error(`Malformed tool call at index ${index}: missing name`);
|
|
1503
|
+
return {
|
|
1504
|
+
id: String(call.id ?? `tool_call_${index + 1}_${randomId("call")}`),
|
|
1505
|
+
name,
|
|
1506
|
+
arguments: parseProviderToolArguments(call.arguments ?? fn.arguments ?? call.args ?? call.input ?? {})
|
|
1507
|
+
};
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
function parseProviderToolArguments(value) {
|
|
1511
|
+
if (typeof value !== "string")
|
|
1512
|
+
return value ?? {};
|
|
1513
|
+
try {
|
|
1514
|
+
return JSON.parse(value);
|
|
1515
|
+
}
|
|
1516
|
+
catch {
|
|
1517
|
+
throw new Error("Malformed tool call arguments JSON");
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function normalizeFinishReason(raw, toolCalls) {
|
|
1521
|
+
const record = asRecord(raw);
|
|
1522
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
1523
|
+
const first = asRecord(choices[0]);
|
|
1524
|
+
const reason = String(record.finish_reason ?? record.finishReason ?? first.finish_reason ?? first.finishReason ?? "").toLowerCase();
|
|
1525
|
+
if (toolCalls.length || reason === "tool_calls" || reason === "tool-calls")
|
|
1526
|
+
return "tool-calls";
|
|
1527
|
+
if (reason === "stop")
|
|
1528
|
+
return "stop";
|
|
1529
|
+
if (reason === "length" || reason === "max_tokens")
|
|
1530
|
+
return "length";
|
|
1531
|
+
if (reason === "error")
|
|
1532
|
+
return "error";
|
|
1533
|
+
return "unknown";
|
|
1534
|
+
}
|
|
1535
|
+
function normalizeProviderUsage(raw) {
|
|
1536
|
+
const usage = asRecord(asRecord(raw).usage);
|
|
1537
|
+
const inputTokens = numericUsage(usage.prompt_tokens ?? usage.input_tokens ?? usage.inputTokens);
|
|
1538
|
+
const outputTokens = numericUsage(usage.completion_tokens ?? usage.output_tokens ?? usage.outputTokens);
|
|
1539
|
+
return inputTokens === undefined && outputTokens === undefined ? undefined : { inputTokens, outputTokens };
|
|
1540
|
+
}
|
|
1541
|
+
function numericUsage(value) {
|
|
1542
|
+
const numeric = Number(value);
|
|
1543
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : undefined;
|
|
1544
|
+
}
|
|
1545
|
+
function localAiProvider(env, config, liveProvider) {
|
|
1546
|
+
if (env.RAYBASE_ENV !== "local")
|
|
1547
|
+
return liveProvider;
|
|
1548
|
+
const mode = normalizeLocalAiMode(env.RAYBASE_AI_MODE);
|
|
1549
|
+
if (mode === "mock")
|
|
1550
|
+
return mockRaybaseAIProvider(config, env.RAYBASE_AI_MOCK_RESPONSE);
|
|
1551
|
+
if (mode === "disabled")
|
|
1552
|
+
return localDisabledRaybaseAIProvider;
|
|
1553
|
+
return liveProvider;
|
|
1554
|
+
}
|
|
1555
|
+
function normalizeLocalAiMode(value) {
|
|
1556
|
+
const mode = value?.trim().toLowerCase();
|
|
1557
|
+
if (mode === "live" || mode === "mock" || mode === "disabled")
|
|
1558
|
+
return mode;
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
export function mockRaybaseAIProvider(config, responseOverride) {
|
|
1562
|
+
const defaults = normalizeAiDefaults(config);
|
|
1563
|
+
return {
|
|
1564
|
+
id: "raybase-local-mock",
|
|
1565
|
+
async chat(input) {
|
|
1566
|
+
const text = responseOverride?.trim() || "Raybase local mock AI response.";
|
|
1567
|
+
return {
|
|
1568
|
+
text,
|
|
1569
|
+
toolCalls: [],
|
|
1570
|
+
finishReason: "stop",
|
|
1571
|
+
provider: { id: "raybase-local-mock", raw: { input } }
|
|
1572
|
+
};
|
|
1573
|
+
},
|
|
1574
|
+
async chatStream(input) {
|
|
1575
|
+
const text = responseOverride?.trim() || "Raybase local mock AI response.";
|
|
1576
|
+
return chatResultToStream({
|
|
1577
|
+
text,
|
|
1578
|
+
toolCalls: [],
|
|
1579
|
+
finishReason: "stop",
|
|
1580
|
+
provider: { id: "raybase-local-mock", raw: { input } }
|
|
1581
|
+
});
|
|
1582
|
+
},
|
|
1583
|
+
async embed(input) {
|
|
1584
|
+
const dimensions = embeddingDimensionsForModelName(defaults.embeddingModel);
|
|
1585
|
+
const values = Array.isArray(input.input) ? input.input : [input.input];
|
|
1586
|
+
return {
|
|
1587
|
+
embeddings: values.map((value) => deterministicUnitVector(String(value), dimensions)),
|
|
1588
|
+
provider: { id: "raybase-local-mock" }
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
const localDisabledRaybaseAIProvider = {
|
|
1594
|
+
id: "raybase-local-disabled",
|
|
1595
|
+
async chat() {
|
|
1596
|
+
throw new Error("Raybase AI is disabled for local development. Set RAYBASE_AI_MODE=mock or run `raybase dev --ai live` to use Workers AI.");
|
|
1597
|
+
},
|
|
1598
|
+
async chatStream() {
|
|
1599
|
+
throw new Error("Raybase AI is disabled for local development. Set RAYBASE_AI_MODE=mock or run `raybase dev --ai live` to use Workers AI.");
|
|
1600
|
+
},
|
|
1601
|
+
async embed() {
|
|
1602
|
+
throw new Error("Raybase AI is disabled for local development. Set RAYBASE_AI_MODE=mock or run `raybase dev --ai live` to use Workers AI.");
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
function deterministicUnitVector(input, dimensions) {
|
|
1606
|
+
const vector = [];
|
|
1607
|
+
let state = deterministicHash(input || "raybase-local-mock");
|
|
1608
|
+
let sumSquares = 0;
|
|
1609
|
+
for (let index = 0; index < dimensions; index += 1) {
|
|
1610
|
+
state = (1664525 * state + 1013904223) >>> 0;
|
|
1611
|
+
const value = ((state / 0xffffffff) * 2) - 1;
|
|
1612
|
+
vector.push(value);
|
|
1613
|
+
sumSquares += value * value;
|
|
1614
|
+
}
|
|
1615
|
+
const norm = Math.sqrt(sumSquares) || 1;
|
|
1616
|
+
return vector.map((value) => value / norm);
|
|
1617
|
+
}
|
|
1618
|
+
function deterministicHash(value) {
|
|
1619
|
+
let hash = 2166136261;
|
|
1620
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1621
|
+
hash ^= value.charCodeAt(index);
|
|
1622
|
+
hash = Math.imul(hash, 16777619);
|
|
1623
|
+
}
|
|
1624
|
+
return hash >>> 0 || 1;
|
|
1625
|
+
}
|
|
1626
|
+
function embeddingDimensionsForModelName(model) {
|
|
1627
|
+
if (model === "@cf/baai/bge-small-en-v1.5")
|
|
1628
|
+
return 384;
|
|
1629
|
+
if (model === "@cf/baai/bge-large-en-v1.5")
|
|
1630
|
+
return 1024;
|
|
1631
|
+
return 768;
|
|
1632
|
+
}
|
|
1633
|
+
function effectiveModelParams(defaults, options) {
|
|
1634
|
+
return normalizeModelParams({
|
|
1635
|
+
...defaults,
|
|
1636
|
+
...(options.modelParams ?? {})
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
function normalizeModelParams(params) {
|
|
1640
|
+
const output = {};
|
|
1641
|
+
const temperature = boundedModelParam(params.temperature, "temperature");
|
|
1642
|
+
const topP = boundedModelParam(params.top_p, "top_p");
|
|
1643
|
+
if (temperature !== undefined)
|
|
1644
|
+
output.temperature = temperature;
|
|
1645
|
+
if (topP !== undefined)
|
|
1646
|
+
output.top_p = topP;
|
|
1647
|
+
return output;
|
|
1648
|
+
}
|
|
1649
|
+
function boundedModelParam(value, name) {
|
|
1650
|
+
if (value === undefined || value === null)
|
|
1651
|
+
return undefined;
|
|
1652
|
+
const numeric = Number(value);
|
|
1653
|
+
if (!Number.isFinite(numeric) || numeric < 0 || numeric > 2) {
|
|
1654
|
+
throw new Error(`AI model parameter ${name} must be a number between 0 and 2`);
|
|
1655
|
+
}
|
|
1656
|
+
return numeric;
|
|
1657
|
+
}
|
|
1658
|
+
export async function recordAiUsageEvent(db, event) {
|
|
1659
|
+
await db.run(`INSERT INTO "_ai_usage_events"
|
|
1660
|
+
("id", "userId", "principalKind", "publicScopeHash", "serviceName", "agent", "conversationId", "model", "requestKind", "role", "gatewayId", "gatewayLogId",
|
|
1661
|
+
"inputChars", "floorChars", "tailChars", "outputChars", "estimatedInputTokens", "estimatedOutputTokens", "estimatedNeurons", "modelParams", "metadata",
|
|
1662
|
+
"status", "error", "durationMs", "createdAt")
|
|
1663
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1664
|
+
randomId("ai_usage"),
|
|
1665
|
+
event.userId ?? null,
|
|
1666
|
+
event.principalKind ?? "user",
|
|
1667
|
+
event.publicScopeHash ?? null,
|
|
1668
|
+
event.serviceName ?? null,
|
|
1669
|
+
event.agent ?? null,
|
|
1670
|
+
event.conversationId ?? null,
|
|
1671
|
+
event.model,
|
|
1672
|
+
event.requestKind,
|
|
1673
|
+
event.role ?? null,
|
|
1674
|
+
event.gatewayId ?? null,
|
|
1675
|
+
event.gatewayLogId ?? null,
|
|
1676
|
+
event.inputChars,
|
|
1677
|
+
event.floorChars ?? null,
|
|
1678
|
+
event.tailChars ?? null,
|
|
1679
|
+
event.outputChars,
|
|
1680
|
+
event.estimatedInputTokens,
|
|
1681
|
+
event.estimatedOutputTokens,
|
|
1682
|
+
event.estimatedNeurons ?? null,
|
|
1683
|
+
event.modelParams && Object.keys(event.modelParams).length ? stableJson(event.modelParams) : null,
|
|
1684
|
+
event.metadata && Object.keys(event.metadata).length ? stableJson(event.metadata) : null,
|
|
1685
|
+
event.status,
|
|
1686
|
+
event.error ?? null,
|
|
1687
|
+
event.durationMs,
|
|
1688
|
+
nowIso()
|
|
1689
|
+
]);
|
|
1690
|
+
}
|
|
1691
|
+
export async function recordAiActionEvent(db, input) {
|
|
1692
|
+
await db.run(`INSERT INTO "_ai_action_events" ("id", "userId", "agent", "conversationId", "actionName", "inputHash", "status", "error", "createdAt")
|
|
1693
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1694
|
+
randomId("ai_action"),
|
|
1695
|
+
input.userId,
|
|
1696
|
+
input.agent,
|
|
1697
|
+
input.conversationId ?? null,
|
|
1698
|
+
input.actionName,
|
|
1699
|
+
input.actionInput === undefined ? null : await sha256Hex(stableJson(input.actionInput)),
|
|
1700
|
+
input.status,
|
|
1701
|
+
input.error ?? null,
|
|
1702
|
+
nowIso()
|
|
1703
|
+
]);
|
|
1704
|
+
}
|
|
1705
|
+
export function formatSseEvent(event, data) {
|
|
1706
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
1707
|
+
}
|
|
1708
|
+
export function parseWorkersAiSseText(text) {
|
|
1709
|
+
const events = [];
|
|
1710
|
+
let current = [];
|
|
1711
|
+
const flush = () => {
|
|
1712
|
+
if (!current.length)
|
|
1713
|
+
return;
|
|
1714
|
+
const data = current.join("\n").trim();
|
|
1715
|
+
current = [];
|
|
1716
|
+
if (!data)
|
|
1717
|
+
return;
|
|
1718
|
+
if (data === "[DONE]") {
|
|
1719
|
+
events.push({ raw: "[DONE]", done: true });
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
try {
|
|
1723
|
+
const parsed = JSON.parse(data);
|
|
1724
|
+
events.push({ raw: parsed, text: modelResultText(parsed) || undefined });
|
|
1725
|
+
}
|
|
1726
|
+
catch {
|
|
1727
|
+
events.push({ raw: data, text: data });
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1731
|
+
if (!line.trim()) {
|
|
1732
|
+
flush();
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
if (line.startsWith("data:"))
|
|
1736
|
+
current.push(line.slice(5).trimStart());
|
|
1737
|
+
}
|
|
1738
|
+
flush();
|
|
1739
|
+
return events;
|
|
1740
|
+
}
|
|
1741
|
+
export function modelResultText(value) {
|
|
1742
|
+
if (typeof value === "string")
|
|
1743
|
+
return value;
|
|
1744
|
+
const record = asRecord(value);
|
|
1745
|
+
if (typeof record.response === "string")
|
|
1746
|
+
return record.response;
|
|
1747
|
+
if (typeof record.text === "string")
|
|
1748
|
+
return record.text;
|
|
1749
|
+
if (typeof record.content === "string")
|
|
1750
|
+
return record.content;
|
|
1751
|
+
const output = record.output;
|
|
1752
|
+
if (typeof output === "string")
|
|
1753
|
+
return output;
|
|
1754
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
1755
|
+
const first = asRecord(choices[0]);
|
|
1756
|
+
const message = asRecord(first.message);
|
|
1757
|
+
const delta = asRecord(first.delta);
|
|
1758
|
+
if (typeof message.content === "string")
|
|
1759
|
+
return message.content;
|
|
1760
|
+
if (typeof delta.content === "string")
|
|
1761
|
+
return delta.content;
|
|
1762
|
+
if (typeof first.text === "string")
|
|
1763
|
+
return first.text;
|
|
1764
|
+
return "";
|
|
1765
|
+
}
|
|
1766
|
+
export function jsonCharLength(value) {
|
|
1767
|
+
if (typeof value === "string")
|
|
1768
|
+
return value.length;
|
|
1769
|
+
return stableJson(value).length;
|
|
1770
|
+
}
|
|
1771
|
+
export function estimateTokensFromChars(chars) {
|
|
1772
|
+
return Math.max(0, Math.ceil(chars / 4));
|
|
1773
|
+
}
|
|
1774
|
+
export function encodeFloatVector(vector) {
|
|
1775
|
+
const floats = new Float32Array(vector);
|
|
1776
|
+
const bytes = new Uint8Array(floats.buffer);
|
|
1777
|
+
let binary = "";
|
|
1778
|
+
for (const byte of bytes)
|
|
1779
|
+
binary += String.fromCharCode(byte);
|
|
1780
|
+
if (typeof btoa === "function")
|
|
1781
|
+
return btoa(binary);
|
|
1782
|
+
throw new Error("Base64 encoding is unavailable in this runtime");
|
|
1783
|
+
}
|
|
1784
|
+
export function decodeFloatVector(base64) {
|
|
1785
|
+
if (typeof atob !== "function")
|
|
1786
|
+
throw new Error("Base64 decoding is unavailable in this runtime");
|
|
1787
|
+
const binary = atob(base64);
|
|
1788
|
+
const bytes = new Uint8Array(binary.length);
|
|
1789
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
1790
|
+
bytes[index] = binary.charCodeAt(index);
|
|
1791
|
+
return Array.from(new Float32Array(bytes.buffer));
|
|
1792
|
+
}
|
|
1793
|
+
export function cosineSimilarity(left, right) {
|
|
1794
|
+
const length = Math.min(left.length, right.length);
|
|
1795
|
+
let dot = 0;
|
|
1796
|
+
let leftNorm = 0;
|
|
1797
|
+
let rightNorm = 0;
|
|
1798
|
+
for (let index = 0; index < length; index += 1) {
|
|
1799
|
+
const a = left[index] ?? 0;
|
|
1800
|
+
const b = right[index] ?? 0;
|
|
1801
|
+
dot += a * b;
|
|
1802
|
+
leftNorm += a * a;
|
|
1803
|
+
rightNorm += b * b;
|
|
1804
|
+
}
|
|
1805
|
+
if (!leftNorm || !rightNorm)
|
|
1806
|
+
return 0;
|
|
1807
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
1808
|
+
}
|
|
1809
|
+
function parseEmbeddingResult(raw) {
|
|
1810
|
+
if (Array.isArray(raw) && Array.isArray(raw[0]))
|
|
1811
|
+
return raw;
|
|
1812
|
+
const record = asRecord(raw);
|
|
1813
|
+
const data = record.data;
|
|
1814
|
+
if (Array.isArray(data)) {
|
|
1815
|
+
if (Array.isArray(data[0]))
|
|
1816
|
+
return data;
|
|
1817
|
+
if (data.every((item) => asRecord(item).embedding))
|
|
1818
|
+
return data.map((item) => asRecord(item).embedding);
|
|
1819
|
+
}
|
|
1820
|
+
if (Array.isArray(record.embeddings))
|
|
1821
|
+
return record.embeddings;
|
|
1822
|
+
if (Array.isArray(record.embedding))
|
|
1823
|
+
return [record.embedding];
|
|
1824
|
+
throw new Error("Workers AI embedding response did not include vectors");
|
|
1825
|
+
}
|
|
1826
|
+
function coerceReadableStream(raw) {
|
|
1827
|
+
if (raw instanceof ReadableStream)
|
|
1828
|
+
return raw;
|
|
1829
|
+
if (raw instanceof Response && raw.body)
|
|
1830
|
+
return raw.body;
|
|
1831
|
+
const text = typeof raw === "string" ? raw : stableJson(raw);
|
|
1832
|
+
return new ReadableStream({
|
|
1833
|
+
start(controller) {
|
|
1834
|
+
controller.enqueue(new TextEncoder().encode(text));
|
|
1835
|
+
controller.close();
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
function gatewayLogId(raw) {
|
|
1840
|
+
const record = asRecord(raw);
|
|
1841
|
+
return typeof record.gatewayLogId === "string" ? record.gatewayLogId : typeof record.logId === "string" ? record.logId : null;
|
|
1842
|
+
}
|
|
1843
|
+
function gatewayLogIdFromHeaders(headers) {
|
|
1844
|
+
return headers.get("cf-aig-log-id") ?? headers.get("cf-ai-gateway-log-id") ?? headers.get("x-ai-gateway-log-id");
|
|
1845
|
+
}
|
|
1846
|
+
function asRecord(value) {
|
|
1847
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1848
|
+
}
|
|
1849
|
+
function aiUsageRole(metadata) {
|
|
1850
|
+
const record = asRecord(metadata);
|
|
1851
|
+
for (const key of ["role", "raybaseComposingSurface"]) {
|
|
1852
|
+
const value = record[key];
|
|
1853
|
+
if (typeof value === "string" && value.trim())
|
|
1854
|
+
return value.trim();
|
|
1855
|
+
}
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
//# sourceMappingURL=ai.js.map
|