@k2b/cloud 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +22 -11
- package/scripts/README.md +10 -0
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +20 -35
- package/scripts/preload.ts +16 -12
- package/scripts/sync-recovery-smoke.ts +22 -6
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +131 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +80 -37
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +18 -151
- package/src/access/PrincipalPicker.tsx +110 -0
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +201 -0
- package/src/ai/browser.ts +4 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +229 -90
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +83 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +25 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +31 -47
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +21 -2
- package/src/ai/chat/tool-groups.ts +58 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +14 -10
- package/src/ai/chat-tasks.ts +297 -43
- package/src/ai/client/controller.ts +165 -39
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +29 -11
- package/src/ai/client/transport.ts +6 -0
- package/src/ai/code-capability-routes.ts +105 -0
- package/src/ai/code-capability-transport.ts +39 -0
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +118 -0
- package/src/ai/code-runtime-tools.ts +179 -0
- package/src/ai/code-source-contracts.ts +270 -0
- package/src/ai/code-source-tools.ts +188 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +99 -3
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +49 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +189 -33
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +27 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +8 -1
- package/src/ai/files-store.ts +212 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +42 -10
- package/src/ai/http.ts +26 -8
- package/src/ai/index.ts +26 -3
- package/src/ai/inference-calls.ts +197 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/live.ts +1 -1
- package/src/ai/markdown-pdf-tool.ts +2 -2
- package/src/ai/memories.ts +30 -27
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +64 -32
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +178 -0
- package/src/ai/migrate.ts +189 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +75 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +18 -43
- package/src/ai/projects.ts +33 -14
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +192 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +241 -11
- package/src/ai/run-timeout.ts +12 -0
- package/src/ai/runtime-tools.ts +7 -5
- package/src/ai/runtime.ts +74 -17
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +58 -0
- package/src/ai/skill-seeds.ts +114 -20
- package/src/ai/skill-tool.ts +21 -5
- package/src/ai/skills-routes.ts +49 -4
- package/src/ai/skills.ts +358 -163
- package/src/ai/store.ts +416 -201
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +12 -1
- package/src/ai/system-prompt.ts +14 -2
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +32 -0
- package/src/ai/todo-tool.ts +19 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +3 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +60 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +80 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth.ts +1 -1
- package/src/api/capabilities.ts +324 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +8 -2
- package/src/api/mcp.ts +45 -34
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +67 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +6 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +116 -35
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +77 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +10 -1
- package/src/cli/admin/instance.ts +2 -2
- package/src/cli/admin/linux.ts +4 -2
- package/src/cli/admin/sync.ts +1 -1
- package/src/cli/capabilities.ts +42 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -51
- package/src/config/index.ts +3 -1
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +75 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +5 -2
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +5 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +18 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -6
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +3 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/app-approval.ts +11 -10
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +8 -5
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/index.ts +15 -0
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +21 -4
- package/src/services/mandates/policy.ts +165 -2
- package/src/services/notifications/batches.ts +125 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +131 -72
- package/src/services/pdf/index.ts +11 -8
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +29 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +26 -35
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +58 -84
- package/src/services/webauthn.ts +1 -1
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +3 -0
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +30 -16
- package/src/ssr/AppLaunchpad.island.tsx +33 -136
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +22 -23
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +11 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +19 -90
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +4 -2
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/notifications/live.ts +0 -50
- package/src/services/session/test-fixture.ts +0 -18
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -2,7 +2,7 @@ import { Hono, type MiddlewareHandler } from "hono";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getCapability } from "../_internal/registry";
|
|
4
4
|
import { CloudResourceRefSchema, cloudResourceRefAppId, resolveCapabilityResourceReader } from "../contracts/capabilities";
|
|
5
|
-
import {
|
|
5
|
+
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
6
6
|
import { type AuthContext, auth, err, fail, ok, rateLimit, respond, v } from "../server";
|
|
7
7
|
import { decodeAiFileContent } from "./files-store";
|
|
8
8
|
import {
|
|
@@ -28,7 +28,7 @@ const ProjectFieldsSchema = z.object({
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const UpdateProjectSchema = ProjectFieldsSchema.partial().refine((input) => Object.keys(input).length > 0, "No changes supplied.");
|
|
31
|
-
const ProjectAccessSchema = z.object({ principal:
|
|
31
|
+
const ProjectAccessSchema = z.object({ principal: AuthenticatedPrincipalSchema, permission: z.enum(["read", "write", "admin"]) });
|
|
32
32
|
const ProjectAccessUpdateSchema = z.object({ permission: z.enum(["read", "write", "admin"]) });
|
|
33
33
|
const KnowledgeSchema = z.object({
|
|
34
34
|
title: z.string().trim().min(1).max(200),
|
|
@@ -70,17 +70,13 @@ type AiProjectsRouteDependencies = {
|
|
|
70
70
|
const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =>
|
|
71
71
|
new Hono<AuthContext>()
|
|
72
72
|
.use(dependencies.limit ?? rateLimit())
|
|
73
|
-
.use("*", dependencies.authenticate ?? auth.requireRole("
|
|
74
|
-
.get("/", async (c) =>
|
|
75
|
-
respond(c, ok({ projects: (await aiProjects.list(c.get("accessSubject") ?? null)).map(publicProject) })),
|
|
76
|
-
)
|
|
73
|
+
.use("*", dependencies.authenticate ?? auth.requireRole("authenticated"))
|
|
74
|
+
.get("/", async (c) => respond(c, ok({ projects: (await aiProjects.list(c.get("accessSubject") ?? null)).map(publicProject) })))
|
|
77
75
|
.post("/", dependencies.authenticate ?? auth.requireRole("authenticated"), v("json", ProjectFieldsSchema), async (c) =>
|
|
78
76
|
respond(
|
|
79
77
|
c,
|
|
80
78
|
ok({
|
|
81
|
-
project: publicProject(
|
|
82
|
-
await aiProjects.create({ subject: c.get("accessSubject"), ...c.req.valid("json") }),
|
|
83
|
-
),
|
|
79
|
+
project: publicProject(await aiProjects.create({ subject: c.get("accessSubject"), ...c.req.valid("json") })),
|
|
84
80
|
}),
|
|
85
81
|
201,
|
|
86
82
|
),
|
|
@@ -108,9 +104,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
|
|
|
108
104
|
})
|
|
109
105
|
.post("/:projectId/access", v("json", ProjectAccessSchema), async (c) => {
|
|
110
106
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "admin");
|
|
111
|
-
const access = project
|
|
112
|
-
? await aiProjects.grantAccess(project.id, c.get("accessSubject") ?? null, c.req.valid("json"))
|
|
113
|
-
: null;
|
|
107
|
+
const access = project ? await aiProjects.grantAccess(project.id, c.get("accessSubject") ?? null, c.req.valid("json")) : null;
|
|
114
108
|
return access ? respond(c, ok({ access }), 201) : notFound(c);
|
|
115
109
|
})
|
|
116
110
|
.patch("/:projectId/access/:accessId", v("json", ProjectAccessUpdateSchema), async (c) => {
|
|
@@ -133,8 +127,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
|
|
|
133
127
|
.delete("/:projectId/access/:accessId", async (c) => {
|
|
134
128
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "admin");
|
|
135
129
|
try {
|
|
136
|
-
return project &&
|
|
137
|
-
(await aiProjects.revokeAccess(project.id, c.req.param("accessId")!, c.get("accessSubject") ?? null))
|
|
130
|
+
return project && (await aiProjects.revokeAccess(project.id, c.req.param("accessId")!, c.get("accessSubject") ?? null))
|
|
138
131
|
? respond(c, ok({ deleted: true }))
|
|
139
132
|
: notFound(c, "Access entry");
|
|
140
133
|
} catch (error) {
|
|
@@ -148,35 +141,27 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
|
|
|
148
141
|
return respond(
|
|
149
142
|
c,
|
|
150
143
|
ok({
|
|
151
|
-
knowledge: (
|
|
152
|
-
|
|
153
|
-
)
|
|
144
|
+
knowledge: (await aiProjects.listKnowledge(project.id, c.get("accessSubject") ?? null, c.req.valid("query").q)).map((item) =>
|
|
145
|
+
publicKnowledge(item, project.shortId),
|
|
146
|
+
),
|
|
154
147
|
}),
|
|
155
148
|
);
|
|
156
149
|
})
|
|
157
150
|
.post("/:projectId/knowledge", v("json", KnowledgeSchema), async (c) => {
|
|
158
151
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
|
|
159
|
-
const knowledge = project
|
|
160
|
-
? await aiProjects.createKnowledge(project.id, c.get("accessSubject") ?? null, c.req.valid("json"))
|
|
161
|
-
: null;
|
|
152
|
+
const knowledge = project ? await aiProjects.createKnowledge(project.id, c.get("accessSubject") ?? null, c.req.valid("json")) : null;
|
|
162
153
|
return knowledge ? respond(c, ok({ knowledge: publicKnowledge(knowledge, project!.shortId) }), 201) : notFound(c);
|
|
163
154
|
})
|
|
164
155
|
.patch("/:projectId/knowledge/:knowledgeId", v("json", UpdateKnowledgeSchema), async (c) => {
|
|
165
156
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
|
|
166
157
|
const knowledge =
|
|
167
158
|
project &&
|
|
168
|
-
(await aiProjects.updateKnowledge(
|
|
169
|
-
project.id,
|
|
170
|
-
c.req.param("knowledgeId")!,
|
|
171
|
-
c.get("accessSubject") ?? null,
|
|
172
|
-
c.req.valid("json"),
|
|
173
|
-
));
|
|
159
|
+
(await aiProjects.updateKnowledge(project.id, c.req.param("knowledgeId")!, c.get("accessSubject") ?? null, c.req.valid("json")));
|
|
174
160
|
return knowledge ? respond(c, ok({ knowledge: publicKnowledge(knowledge, project!.shortId) })) : notFound(c, "Knowledge entry");
|
|
175
161
|
})
|
|
176
162
|
.delete("/:projectId/knowledge/:knowledgeId", async (c) => {
|
|
177
163
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
|
|
178
|
-
return project &&
|
|
179
|
-
(await aiProjects.deleteKnowledge(project.id, c.req.param("knowledgeId")!, c.get("accessSubject") ?? null))
|
|
164
|
+
return project && (await aiProjects.deleteKnowledge(project.id, c.req.param("knowledgeId")!, c.get("accessSubject") ?? null))
|
|
180
165
|
? respond(c, ok({ deleted: true }))
|
|
181
166
|
: notFound(c, "Knowledge entry");
|
|
182
167
|
})
|
|
@@ -186,20 +171,14 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
|
|
|
186
171
|
return respond(
|
|
187
172
|
c,
|
|
188
173
|
ok({
|
|
189
|
-
files: (await aiProjects.listFiles(project.id, c.get("accessSubject") ?? null)).map((file) =>
|
|
190
|
-
publicFile(file, project.shortId),
|
|
191
|
-
),
|
|
174
|
+
files: (await aiProjects.listFiles(project.id, c.get("accessSubject") ?? null)).map((file) => publicFile(file, project.shortId)),
|
|
192
175
|
}),
|
|
193
176
|
);
|
|
194
177
|
})
|
|
195
178
|
.post("/:projectId/files", v("json", ProjectFileSchema), async (c) => {
|
|
196
179
|
const body = c.req.valid("json");
|
|
197
180
|
try {
|
|
198
|
-
const project = await aiProjects.getByShortId(
|
|
199
|
-
c.req.param("projectId")!,
|
|
200
|
-
c.get("accessSubject") ?? null,
|
|
201
|
-
"write",
|
|
202
|
-
);
|
|
181
|
+
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
|
|
203
182
|
const file = project
|
|
204
183
|
? await aiProjects.writeFile(project.id, c.get("accessSubject") ?? null, {
|
|
205
184
|
path: body.path,
|
|
@@ -214,9 +193,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
|
|
|
214
193
|
})
|
|
215
194
|
.get("/:projectId/files/:fileId", async (c) => {
|
|
216
195
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null);
|
|
217
|
-
const file = project
|
|
218
|
-
? await aiProjects.readFile(project.id, c.req.param("fileId")!, c.get("accessSubject") ?? null)
|
|
219
|
-
: null;
|
|
196
|
+
const file = project ? await aiProjects.readFile(project.id, c.req.param("fileId")!, c.get("accessSubject") ?? null) : null;
|
|
220
197
|
if (!file) return notFound(c, "Project file");
|
|
221
198
|
return respond(
|
|
222
199
|
c,
|
|
@@ -229,8 +206,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
|
|
|
229
206
|
})
|
|
230
207
|
.delete("/:projectId/files/:fileId", async (c) => {
|
|
231
208
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
|
|
232
|
-
return project &&
|
|
233
|
-
(await aiProjects.deleteFile(project.id, c.req.param("fileId")!, c.get("accessSubject") ?? null))
|
|
209
|
+
return project && (await aiProjects.deleteFile(project.id, c.req.param("fileId")!, c.get("accessSubject") ?? null))
|
|
234
210
|
? respond(c, ok({ deleted: true }))
|
|
235
211
|
: notFound(c, "Project file");
|
|
236
212
|
})
|
|
@@ -265,8 +241,7 @@ const buildAiProjectsRoutes = (dependencies: AiProjectsRouteDependencies = {}) =
|
|
|
265
241
|
})
|
|
266
242
|
.delete("/:projectId/references/:referenceId", async (c) => {
|
|
267
243
|
const project = await aiProjects.getByShortId(c.req.param("projectId")!, c.get("accessSubject") ?? null, "write");
|
|
268
|
-
return project &&
|
|
269
|
-
(await aiProjects.deleteReference(project.id, c.req.param("referenceId")!, c.get("accessSubject") ?? null))
|
|
244
|
+
return project && (await aiProjects.deleteReference(project.id, c.req.param("referenceId")!, c.get("accessSubject") ?? null))
|
|
270
245
|
? respond(c, ok({ deleted: true }))
|
|
271
246
|
: notFound(c, "Project reference");
|
|
272
247
|
});
|
package/src/ai/projects.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SQL, type SQLQuery, sql } from "bun";
|
|
2
2
|
import type { CloudResourceRef } from "../contracts/capabilities";
|
|
3
|
+
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
3
4
|
import type { AccessSubject } from "../server";
|
|
4
5
|
import {
|
|
5
6
|
buildAccessPrincipalCondition,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
type Principal,
|
|
11
12
|
} from "../server/services/access";
|
|
12
13
|
import { toPgUuidArray } from "../services/postgres";
|
|
14
|
+
import { AiFileVersionConflict, AiFileWriteError, aiFileContentVersion } from "./file-content-version";
|
|
13
15
|
import { mountAiProjectFilePath } from "./file-mount";
|
|
14
16
|
import { withAiShortIdForDb } from "./short-id";
|
|
15
17
|
import type { AiProjectPromptSnapshot } from "./types";
|
|
@@ -168,15 +170,17 @@ const principalForSubject = (subject: AccessSubject): Principal =>
|
|
|
168
170
|
: { type: "service_account", serviceAccountId: subject.serviceAccountId };
|
|
169
171
|
|
|
170
172
|
const accessMatch = (subject: AccessSubject | null): SQLQuery =>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
subject === null
|
|
174
|
+
? sql`FALSE`
|
|
175
|
+
: buildAccessPrincipalCondition({
|
|
176
|
+
subject,
|
|
177
|
+
columns: {
|
|
178
|
+
userId: sql`access.user_id`,
|
|
179
|
+
groupId: sql`access.group_id`,
|
|
180
|
+
serviceAccountId: sql`access.service_account_id`,
|
|
181
|
+
authenticatedOnly: sql`access.authenticated_only`,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
180
184
|
|
|
181
185
|
const permissionFor = async (row: ProjectRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiProjectPermission | "none"> => {
|
|
182
186
|
const match = accessMatch(subject);
|
|
@@ -299,6 +303,7 @@ const createProjectAccess = async (
|
|
|
299
303
|
input: { principal: Principal; permission: AiProjectPermission },
|
|
300
304
|
db: SQL,
|
|
301
305
|
): Promise<AiProjectAccess> => {
|
|
306
|
+
AuthenticatedPrincipalSchema.parse(input.principal);
|
|
302
307
|
const created = await createAccess(input, db);
|
|
303
308
|
if (!created.ok) throw new Error(created.error.message);
|
|
304
309
|
let shortId = "";
|
|
@@ -439,7 +444,6 @@ export const aiProjects = {
|
|
|
439
444
|
JOIN auth.access access ON access.id = project_access.access_id
|
|
440
445
|
WHERE access.permission <> 'none' AND ${match}
|
|
441
446
|
ORDER BY lower(project.name), project.id
|
|
442
|
-
LIMIT 200
|
|
443
447
|
`;
|
|
444
448
|
return (await Promise.all(rows.map((row) => toProject(row, subject)))).filter((project): project is AiProject => Boolean(project));
|
|
445
449
|
},
|
|
@@ -787,24 +791,36 @@ export const aiProjects = {
|
|
|
787
791
|
});
|
|
788
792
|
},
|
|
789
793
|
|
|
790
|
-
async listFiles(projectId: string, subject: AccessSubject | null): Promise<AiProjectFile[]> {
|
|
794
|
+
async listFiles(projectId: string, subject: AccessSubject | null, page?: { after?: string; limit: number }): Promise<AiProjectFile[]> {
|
|
791
795
|
if (!(await requireProject(projectId, subject, "read"))) return [];
|
|
796
|
+
if (page && (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > 1000)) throw new Error("Invalid file page limit");
|
|
792
797
|
return (
|
|
793
798
|
await sql<
|
|
794
799
|
FileRow[]
|
|
795
|
-
>`SELECT id, short_id, project_id, path, media_type, size, updated_at FROM ai.project_files WHERE project_id = ${projectId}::uuid ORDER BY path LIMIT 500`
|
|
800
|
+
>`SELECT id, short_id, project_id, path, media_type, size, updated_at FROM ai.project_files WHERE project_id = ${projectId}::uuid AND path > ${page?.after ?? ""} ORDER BY path LIMIT ${page?.limit ?? 500}`
|
|
796
801
|
).map(toFile);
|
|
797
802
|
},
|
|
798
803
|
|
|
799
804
|
async writeFile(
|
|
800
805
|
projectId: string,
|
|
801
806
|
subject: AccessSubject | null,
|
|
802
|
-
input: { path: string; mediaType: string; bytes: Uint8Array },
|
|
807
|
+
input: { path: string; mediaType: string; bytes: Uint8Array; expectedVersion?: string | null },
|
|
803
808
|
): Promise<AiProjectFile | null> {
|
|
804
809
|
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
805
|
-
if (input.bytes.byteLength > AI_PROJECT_FILE_MAX_BYTES)
|
|
810
|
+
if (input.bytes.byteLength > AI_PROJECT_FILE_MAX_BYTES)
|
|
811
|
+
throw new AiFileWriteError("STORAGE_FULL", "Project file exceeds the size limit; nothing was written.");
|
|
806
812
|
const path = normalizeProjectPath(input.path);
|
|
807
813
|
return sql.begin(async (tx) => {
|
|
814
|
+
const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id=${projectId}::uuid FOR UPDATE`;
|
|
815
|
+
const project = row ? await toProject(row, subject, tx) : null;
|
|
816
|
+
if (!project || !hasPermission(project.permission, "write")) return null;
|
|
817
|
+
if (input.expectedVersion !== undefined) {
|
|
818
|
+
const [existing] = await tx<
|
|
819
|
+
(FileRow & { bytes: Uint8Array })[]
|
|
820
|
+
>`SELECT id,short_id,project_id,path,media_type,size,updated_at,bytes FROM ai.project_files WHERE project_id=${projectId}::uuid AND path=${path}`;
|
|
821
|
+
const version = existing ? aiFileContentVersion({ ...toFile(existing), bytes: existing.bytes }) : null;
|
|
822
|
+
if (version !== input.expectedVersion) throw new AiFileVersionConflict();
|
|
823
|
+
}
|
|
808
824
|
const rows = await withAiShortIdForDb(
|
|
809
825
|
tx,
|
|
810
826
|
"idx_ai_project_files_short_id",
|
|
@@ -851,6 +867,9 @@ export const aiProjects = {
|
|
|
851
867
|
async deleteFile(projectId: string, fileId: string, subject: AccessSubject | null): Promise<boolean> {
|
|
852
868
|
if (!(await requireProject(projectId, subject, "write"))) return false;
|
|
853
869
|
return sql.begin(async (tx) => {
|
|
870
|
+
const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id=${projectId}::uuid FOR UPDATE`;
|
|
871
|
+
const project = row ? await toProject(row, subject, tx) : null;
|
|
872
|
+
if (!project || !hasPermission(project.permission, "write")) return false;
|
|
854
873
|
const rows = await tx<{ id: string }[]>`
|
|
855
874
|
DELETE FROM ai.project_files WHERE short_id = ${fileId} AND project_id = ${projectId}::uuid RETURNING id
|
|
856
875
|
`;
|
package/src/ai/protocol.ts
CHANGED
|
@@ -33,6 +33,8 @@ export type AiTurnBlock =
|
|
|
33
33
|
name: string;
|
|
34
34
|
args?: unknown;
|
|
35
35
|
status: AiToolBlockStatus;
|
|
36
|
+
/** Localized transient host status, without tool payload data. */
|
|
37
|
+
progress?: string;
|
|
36
38
|
result?: unknown;
|
|
37
39
|
isError?: boolean;
|
|
38
40
|
/** Present while status is awaiting_approval. */
|
|
@@ -62,6 +64,7 @@ export type AiWireEvent =
|
|
|
62
64
|
/** Authoritative ordered baseline for this attempt. Optional for older senders. */
|
|
63
65
|
blocks?: AiTurnBlock[];
|
|
64
66
|
})
|
|
67
|
+
| (AiWireEventBase & { type: "message_saved"; message: AiStoredMessage })
|
|
65
68
|
| (AiWireEventBase & { type: "block_set"; block: AiTurnBlock })
|
|
66
69
|
| (AiWireEventBase & { type: "block_delta"; blockId: string; blockKind: "text" | "thinking"; delta: string })
|
|
67
70
|
| (AiWireEventBase & {
|
package/src/ai/provider.ts
CHANGED
|
@@ -10,12 +10,11 @@ const commonOptions = (profile: AiModelProfile, apiKey?: string) => ({
|
|
|
10
10
|
baseURL: profile.baseURL,
|
|
11
11
|
contextWindow: profile.contextWindow,
|
|
12
12
|
temperature: profile.temperature,
|
|
13
|
-
creditsPerInputToken: profile.creditsPerInputToken,
|
|
14
|
-
creditsPerOutputToken: profile.creditsPerOutputToken,
|
|
15
13
|
timeouts: PROVIDER_TIMEOUTS,
|
|
16
14
|
});
|
|
17
15
|
|
|
18
16
|
export const createAiProvider = (profile: AiModelProfile, apiKey?: string): Provider => {
|
|
17
|
+
if (profile.capabilities.includes("transcription")) throw new Error("Transcription profiles cannot be used for chat generation.");
|
|
19
18
|
switch (profile.provider) {
|
|
20
19
|
case "openai":
|
|
21
20
|
return openai(profile.model, commonOptions(profile, apiKey));
|
|
@@ -32,8 +31,6 @@ export const createAiProvider = (profile: AiModelProfile, apiKey?: string): Prov
|
|
|
32
31
|
baseURL: profile.baseURL,
|
|
33
32
|
contextWindow: profile.contextWindow,
|
|
34
33
|
temperature: profile.temperature,
|
|
35
|
-
creditsPerInputToken: profile.creditsPerInputToken,
|
|
36
|
-
creditsPerOutputToken: profile.creditsPerOutputToken,
|
|
37
34
|
timeouts: PROVIDER_TIMEOUTS,
|
|
38
35
|
});
|
|
39
36
|
case "vllm":
|
|
@@ -44,8 +41,6 @@ export const createAiProvider = (profile: AiModelProfile, apiKey?: string): Prov
|
|
|
44
41
|
apiKey,
|
|
45
42
|
contextWindow: profile.contextWindow,
|
|
46
43
|
temperature: profile.temperature,
|
|
47
|
-
creditsPerInputToken: profile.creditsPerInputToken,
|
|
48
|
-
creditsPerOutputToken: profile.creditsPerOutputToken,
|
|
49
44
|
timeouts: PROVIDER_TIMEOUTS,
|
|
50
45
|
compat: {
|
|
51
46
|
toolCallIdPolicy: "passthrough",
|
|
@@ -65,8 +60,6 @@ export const createAiProvider = (profile: AiModelProfile, apiKey?: string): Prov
|
|
|
65
60
|
apiKey,
|
|
66
61
|
contextWindow: profile.contextWindow,
|
|
67
62
|
temperature: profile.temperature,
|
|
68
|
-
creditsPerInputToken: profile.creditsPerInputToken,
|
|
69
|
-
creditsPerOutputToken: profile.creditsPerOutputToken,
|
|
70
63
|
timeouts: PROVIDER_TIMEOUTS,
|
|
71
64
|
});
|
|
72
65
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { estimateTokens, type Provider } from "@k2b/nessi";
|
|
3
|
+
import { sql } from "bun";
|
|
4
|
+
import type { AccessSubject } from "../server/services/access";
|
|
5
|
+
import { logger } from "../services/logging";
|
|
6
|
+
import { isAssistantChatTurn } from "./assistant-models";
|
|
7
|
+
import { AiBackgroundAdmissionError, type AiCallContext, beginAiCall, finishAiCall } from "./inference-calls";
|
|
8
|
+
import type { AiModelProfile } from "./types";
|
|
9
|
+
|
|
10
|
+
const log = logger("ai:quotas");
|
|
11
|
+
|
|
12
|
+
/** One wrapper owns call accounting. complete() can have a separate compaction purpose. */
|
|
13
|
+
const callLifecycle = {
|
|
14
|
+
begin: beginAiCall,
|
|
15
|
+
finish: finishAiCall,
|
|
16
|
+
heartbeat: async (id: string) => {
|
|
17
|
+
await sql`UPDATE ai.inference_calls SET lease_expires_at=now()+interval '2 minutes' WHERE id=${id}::uuid AND finished_at IS NULL`;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export function inferenceProvider(
|
|
21
|
+
provider: Provider,
|
|
22
|
+
profile: AiModelProfile,
|
|
23
|
+
context: AiCallContext,
|
|
24
|
+
completeContext = context,
|
|
25
|
+
lifecycle = callLifecycle,
|
|
26
|
+
): Provider {
|
|
27
|
+
const begin = async (request: Parameters<Provider["complete"]>[0], ctx: AiCallContext) => {
|
|
28
|
+
const messages = request.messages.map((message) =>
|
|
29
|
+
message.role === "user"
|
|
30
|
+
? {
|
|
31
|
+
...message,
|
|
32
|
+
content: message.content.filter((part) => typeof part === "string" || part.type !== "file"),
|
|
33
|
+
}
|
|
34
|
+
: message,
|
|
35
|
+
);
|
|
36
|
+
const inputTokens =
|
|
37
|
+
estimateTokens(messages) +
|
|
38
|
+
Math.ceil(
|
|
39
|
+
((request.systemPrompt?.length ?? 0) +
|
|
40
|
+
JSON.stringify(request.tools ?? []).length +
|
|
41
|
+
JSON.stringify(request.responseFormat ?? {}).length) /
|
|
42
|
+
4,
|
|
43
|
+
);
|
|
44
|
+
// Wait up to one reservation lease before delegating to the caller's retry policy.
|
|
45
|
+
// No provider request or new reservation exists while waiting; cancellation stays active.
|
|
46
|
+
const deadline = Date.now() + 120_000;
|
|
47
|
+
const admit = async () => {
|
|
48
|
+
for (;;) {
|
|
49
|
+
request.signal?.throwIfAborted();
|
|
50
|
+
try {
|
|
51
|
+
return await lifecycle.begin(
|
|
52
|
+
profile,
|
|
53
|
+
ctx,
|
|
54
|
+
inputTokens,
|
|
55
|
+
request.maxOutputTokens ?? profile.maxOutputTokens,
|
|
56
|
+
// Nessi's Anthropic adapter defaults to 1024; other adapters use the model default.
|
|
57
|
+
provider.family === "anthropic" ? 1024 : provider.contextWindow,
|
|
58
|
+
);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!(error instanceof AiBackgroundAdmissionError) || !error.retryable || Date.now() >= deadline) throw error;
|
|
61
|
+
await delay(1_000, undefined, { signal: request.signal });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const call = await admit();
|
|
66
|
+
const heartbeat = setInterval(() => {
|
|
67
|
+
void lifecycle.heartbeat(call.id).catch(() => log.warn("AI accounting heartbeat failed", { callId: call.id }));
|
|
68
|
+
}, 30_000);
|
|
69
|
+
return { ...call, inputTokens, stop: () => clearInterval(heartbeat) };
|
|
70
|
+
};
|
|
71
|
+
const finish = async (id: string, usage: Parameters<typeof finishAiCall>[1], status: "ok" | "failed") => {
|
|
72
|
+
try {
|
|
73
|
+
await lifecycle.finish(id, usage, status);
|
|
74
|
+
} catch {
|
|
75
|
+
log.error("AI cost booking failed", { callId: id, code: "ai_cost_booking_failed" });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
name: provider.name,
|
|
80
|
+
family: provider.family,
|
|
81
|
+
model: provider.model,
|
|
82
|
+
contextWindow: provider.contextWindow,
|
|
83
|
+
capabilities: provider.capabilities,
|
|
84
|
+
complete: async (request) => {
|
|
85
|
+
const call = await begin(request, completeContext);
|
|
86
|
+
let usage: Parameters<typeof finishAiCall>[1];
|
|
87
|
+
let status: "ok" | "failed" = "failed";
|
|
88
|
+
try {
|
|
89
|
+
const result = await provider.complete({ ...request, maxOutputTokens: call.maxOutputTokens });
|
|
90
|
+
if (
|
|
91
|
+
result.usage &&
|
|
92
|
+
[result.usage.input, result.usage.output].every((n) => Number.isSafeInteger(n) && n >= 0) &&
|
|
93
|
+
result.usage.input + result.usage.output > 0
|
|
94
|
+
)
|
|
95
|
+
usage = { input: result.usage.input, output: result.usage.output };
|
|
96
|
+
status = ["error", "aborted", "interrupted"].includes(result.finishReason) ? "failed" : "ok";
|
|
97
|
+
return result;
|
|
98
|
+
} finally {
|
|
99
|
+
call.stop();
|
|
100
|
+
if (!usage && status === "failed") usage = { input: call.inputTokens, output: 0, estimated: true };
|
|
101
|
+
await finish(call.id, usage, status);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
stream: async function* (request) {
|
|
105
|
+
const call = await begin(request, context);
|
|
106
|
+
const id = call.id;
|
|
107
|
+
let usage: { input: number; output: number; estimated?: boolean } | undefined;
|
|
108
|
+
let completed = false;
|
|
109
|
+
let failed = false;
|
|
110
|
+
const outputBlocks = new Map<string, number>();
|
|
111
|
+
let generated = false;
|
|
112
|
+
try {
|
|
113
|
+
for await (const event of provider.stream({ ...request, maxOutputTokens: call.maxOutputTokens })) {
|
|
114
|
+
if (event.type === "issue" && event.issue.kind === "provider_error") failed = true;
|
|
115
|
+
if (event.type === "block_delta") outputBlocks.set(event.blockId, (outputBlocks.get(event.blockId) ?? 0) + event.delta.length);
|
|
116
|
+
if (event.type === "block_end") {
|
|
117
|
+
const block = event.block;
|
|
118
|
+
const size =
|
|
119
|
+
block.type === "text"
|
|
120
|
+
? block.text.length
|
|
121
|
+
: block.type === "thinking"
|
|
122
|
+
? block.thinking.length
|
|
123
|
+
: block.name.length + JSON.stringify(block.args).length;
|
|
124
|
+
outputBlocks.set(event.blockId, size);
|
|
125
|
+
}
|
|
126
|
+
if (event.type === "block_start" || event.type === "block_delta" || event.type === "block_end") generated = true;
|
|
127
|
+
if (event.type === "issue" && event.issue.kind === "provider_error" && event.issue.contextOverflow && !generated && !usage)
|
|
128
|
+
usage = { input: 0, output: 0 };
|
|
129
|
+
if (event.type === "usage") {
|
|
130
|
+
if (event.finishReason === "aborted" || event.finishReason === "interrupted" || event.finishReason === "error") failed = true;
|
|
131
|
+
const { input, output } = event.usage;
|
|
132
|
+
// Some adapters synthesize all-zero usage when the endpoint reports none.
|
|
133
|
+
if (Number.isSafeInteger(input) && Number.isSafeInteger(output) && input >= 0 && output >= 0 && input + output > 0)
|
|
134
|
+
usage = { input, output };
|
|
135
|
+
}
|
|
136
|
+
yield event;
|
|
137
|
+
}
|
|
138
|
+
completed = true;
|
|
139
|
+
} finally {
|
|
140
|
+
call.stop();
|
|
141
|
+
if (id) {
|
|
142
|
+
try {
|
|
143
|
+
// Interrupted adapters commonly omit their final usage event. Charge an
|
|
144
|
+
// explicitly labelled estimate, never invent a measured zero.
|
|
145
|
+
const interrupted = !completed || failed || request.signal?.aborted;
|
|
146
|
+
if (!usage && interrupted) {
|
|
147
|
+
// Encoded files are not text tokens. Their provider-specific costs
|
|
148
|
+
// cannot be reconstructed after interruption.
|
|
149
|
+
const messages = request.messages.map((message) =>
|
|
150
|
+
message.role === "user"
|
|
151
|
+
? {
|
|
152
|
+
...message,
|
|
153
|
+
content: message.content.filter((part) => typeof part === "string" || part.type !== "file"),
|
|
154
|
+
}
|
|
155
|
+
: message,
|
|
156
|
+
);
|
|
157
|
+
const instructionSize =
|
|
158
|
+
(request.systemPrompt?.length ?? 0) +
|
|
159
|
+
JSON.stringify(request.tools ?? []).length +
|
|
160
|
+
JSON.stringify(request.responseFormat ?? {}).length;
|
|
161
|
+
usage = {
|
|
162
|
+
input: estimateTokens(messages) + Math.ceil(instructionSize / 4),
|
|
163
|
+
output: Math.ceil(Array.from(outputBlocks.values()).reduce((sum, size) => sum + size, 0) / 4),
|
|
164
|
+
estimated: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
await finish(id, usage, completed && !failed && !request.signal?.aborted ? "ok" : "failed");
|
|
168
|
+
} catch (error) {
|
|
169
|
+
log.warn("Chat usage booking failed", { code: "quota_booking_failed", turnId: context.turnId, callId: id });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function assistantQuotaProvider(
|
|
178
|
+
provider: Provider,
|
|
179
|
+
config: Parameters<typeof isAssistantChatTurn>[0],
|
|
180
|
+
subject: AccessSubject | null,
|
|
181
|
+
profile: AiModelProfile,
|
|
182
|
+
turnId: string,
|
|
183
|
+
conversationId: string,
|
|
184
|
+
) {
|
|
185
|
+
const common = { subject, turnId, conversationId };
|
|
186
|
+
return inferenceProvider(
|
|
187
|
+
provider,
|
|
188
|
+
profile,
|
|
189
|
+
{ ...common, kind: isAssistantChatTurn(config) ? "chat" : "background", task: isAssistantChatTurn(config) ? "chat" : "agent-turn" },
|
|
190
|
+
{ ...common, kind: "background", task: "chat-compaction" },
|
|
191
|
+
);
|
|
192
|
+
}
|