@k2b/cloud 0.42.0 → 0.44.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/package.json +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
package/src/ai/credentials.ts
CHANGED
|
@@ -1,138 +1,37 @@
|
|
|
1
1
|
import { sql } from "bun";
|
|
2
|
-
import { toPgTextArray } from "../services/postgres";
|
|
3
2
|
import { decryptValue, encryptValue } from "../services/settings/crypto";
|
|
4
|
-
import type { AiRequestHeaderPatch } from "./request-headers";
|
|
5
3
|
|
|
6
4
|
type SqlClient = typeof sql;
|
|
7
5
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* They used to live on the profile inside the `ai.model_profiles_json` setting.
|
|
12
|
-
* Settings are encrypted at rest but a `text` setting is handed to the admin UI
|
|
13
|
-
* in full, so every key travelled to the browser in the page payload — and the
|
|
14
|
-
* form had to send them back to avoid dropping them while editing a label.
|
|
15
|
-
* Only `kind: "secret"` settings are redacted, and the registry is static, so
|
|
16
|
-
* one setting per profile was never an option.
|
|
17
|
-
*
|
|
18
|
-
* These rows are the answer: encrypted with the same `APP_SECRET` helper the
|
|
19
|
-
* settings store uses, addressed by profile id, and never serialised into
|
|
20
|
-
* anything the client receives.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/** The key for a profile, or null when none is stored. */
|
|
24
|
-
export const getAiCredential = async (profileId: string, db: SqlClient = sql): Promise<string | null> => {
|
|
6
|
+
/** Provider keys are encrypted at rest and never returned by an API. */
|
|
7
|
+
export const getAiCredential = async (providerId: string, db: SqlClient = sql): Promise<string | null> => {
|
|
25
8
|
const [row] = await db<{ secret: string }[]>`
|
|
26
|
-
SELECT secret FROM ai.
|
|
9
|
+
SELECT secret FROM ai.provider_credentials WHERE provider_id = ${providerId}
|
|
27
10
|
`;
|
|
28
11
|
if (!row) return null;
|
|
29
12
|
try {
|
|
30
13
|
const value = await decryptValue(row.secret);
|
|
31
14
|
return typeof value === "string" && value.trim() ? value : null;
|
|
32
15
|
} catch {
|
|
33
|
-
console.warn(`[ai] ignoring unreadable provider credential for
|
|
16
|
+
console.warn(`[ai] ignoring unreadable provider credential for provider ${JSON.stringify(providerId)}`);
|
|
34
17
|
return null;
|
|
35
18
|
}
|
|
36
19
|
};
|
|
37
20
|
|
|
38
|
-
/** Store or replace a
|
|
39
|
-
export const setAiCredential = async (
|
|
21
|
+
/** Store or replace a provider's key. */
|
|
22
|
+
export const setAiCredential = async (providerId: string, secret: string, db: SqlClient = sql): Promise<void> => {
|
|
40
23
|
const encrypted = await encryptValue(secret);
|
|
41
24
|
await db`
|
|
42
|
-
INSERT INTO ai.
|
|
43
|
-
VALUES (${
|
|
44
|
-
ON CONFLICT (
|
|
25
|
+
INSERT INTO ai.provider_credentials (provider_id, secret)
|
|
26
|
+
VALUES (${providerId}, ${encrypted})
|
|
27
|
+
ON CONFLICT (provider_id) DO UPDATE
|
|
45
28
|
SET secret = EXCLUDED.secret,
|
|
46
29
|
updated_at = now()
|
|
47
30
|
`;
|
|
48
31
|
};
|
|
49
32
|
|
|
50
|
-
/**
|
|
51
|
-
export const
|
|
52
|
-
const rows = await db<{
|
|
53
|
-
|
|
54
|
-
for (const row of rows) {
|
|
55
|
-
try {
|
|
56
|
-
const value = await decryptValue(row.secret);
|
|
57
|
-
if (typeof value === "string" && value.trim()) usable.push(row.profile_id);
|
|
58
|
-
} catch {
|
|
59
|
-
console.warn(`[ai] ignoring unreadable provider credential for profile ${JSON.stringify(row.profile_id)}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return usable;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Drop keys for profiles that no longer exist.
|
|
67
|
-
*
|
|
68
|
-
* Profiles live in a JSON setting rather than a table, so there is no foreign
|
|
69
|
-
* key to cascade from. Pruning on save keeps a deleted profile from leaving its
|
|
70
|
-
* key behind, and makes recreating an id a clean slate rather than a silent
|
|
71
|
-
* inheritance.
|
|
72
|
-
*/
|
|
73
|
-
export const pruneAiCredentials = async (keepProfileIds: readonly string[], db: SqlClient = sql): Promise<void> => {
|
|
74
|
-
if (keepProfileIds.length === 0) {
|
|
75
|
-
await db`DELETE FROM ai.model_credentials`;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
await db`DELETE FROM ai.model_credentials WHERE profile_id <> ALL(${toPgTextArray([...keepProfileIds])}::text[])`;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Separate provider keys from the profiles blob.
|
|
83
|
-
*
|
|
84
|
-
* Pure on purpose: the admin route and the one-time migration both need the
|
|
85
|
-
* exact same split, and getting it wrong either leaks a key into a setting or
|
|
86
|
-
* drops one on save. `profilesJson` is what may be stored; `credentials` is
|
|
87
|
-
* what must go into the table; `profileIds` is what survives pruning.
|
|
88
|
-
*
|
|
89
|
-
* Unparseable input is handed back untouched — the settings validator owns that
|
|
90
|
-
* error, and rewriting it here would replace the admin's parse message.
|
|
91
|
-
*/
|
|
92
|
-
export const splitAiProfileCredentials = (
|
|
93
|
-
rawValue: unknown,
|
|
94
|
-
): {
|
|
95
|
-
profilesJson: string;
|
|
96
|
-
credentials: Array<{ profileId: string; secret: string }>;
|
|
97
|
-
profileIds: string[];
|
|
98
|
-
requestHeaders: AiRequestHeaderPatch[];
|
|
99
|
-
} | null => {
|
|
100
|
-
if (typeof rawValue !== "string") return null;
|
|
101
|
-
const text = rawValue;
|
|
102
|
-
let parsed: unknown;
|
|
103
|
-
try {
|
|
104
|
-
parsed = JSON.parse(text || "[]");
|
|
105
|
-
} catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
if (!Array.isArray(parsed)) return null;
|
|
109
|
-
|
|
110
|
-
const profiles: unknown[] = [];
|
|
111
|
-
const credentials: Array<{ profileId: string; secret: string }> = [];
|
|
112
|
-
const profileIds: string[] = [];
|
|
113
|
-
const requestHeaders: AiRequestHeaderPatch[] = [];
|
|
114
|
-
|
|
115
|
-
for (const entry of parsed) {
|
|
116
|
-
if (!entry || typeof entry !== "object") {
|
|
117
|
-
profiles.push(entry);
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const {
|
|
121
|
-
apiKey,
|
|
122
|
-
requestHeaders: headerPatch,
|
|
123
|
-
requestHeaderNames: _headerNames,
|
|
124
|
-
credentialSetting: _legacy,
|
|
125
|
-
...profile
|
|
126
|
-
} = entry as Record<string, unknown>;
|
|
127
|
-
const profileId = typeof profile.id === "string" ? profile.id.trim() : "";
|
|
128
|
-
if (profileId) {
|
|
129
|
-
profile.id = profileId;
|
|
130
|
-
profileIds.push(profileId);
|
|
131
|
-
if (headerPatch !== undefined) requestHeaders.push({ profileId, patch: headerPatch });
|
|
132
|
-
if (typeof apiKey === "string" && apiKey.trim()) credentials.push({ profileId, secret: apiKey.trim() });
|
|
133
|
-
}
|
|
134
|
-
profiles.push(profile);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { profilesJson: JSON.stringify(profiles), credentials, profileIds, requestHeaders };
|
|
33
|
+
/** Provider IDs with a stored key; decrypt only the selected provider at execution. */
|
|
34
|
+
export const listAiCredentialProviderIds = async (db: SqlClient = sql): Promise<string[]> => {
|
|
35
|
+
const rows = await db<{ provider_id: string }[]>`SELECT provider_id FROM ai.provider_credentials`;
|
|
36
|
+
return rows.map((row) => row.provider_id);
|
|
138
37
|
};
|
|
@@ -171,6 +171,7 @@ const chartKinds = (limits = true) => {
|
|
|
171
171
|
kind: z.literal("bar"),
|
|
172
172
|
...ChartKindCommon,
|
|
173
173
|
data: list(ChartCategory, 1, CLOUD_AI_CHART_MAX_VALUES),
|
|
174
|
+
xAxis: ChartAxis.pick({ label: true }).strict().optional(),
|
|
174
175
|
yAxis: ChartAxis.optional(),
|
|
175
176
|
colorByBar: z.boolean().optional(),
|
|
176
177
|
showValues: z.boolean().optional(),
|
|
@@ -387,14 +388,14 @@ const flatChartSchema = () => {
|
|
|
387
388
|
.max(CLOUD_AI_CHART_MAX_SERIES)
|
|
388
389
|
.optional()
|
|
389
390
|
.describe("line, scatter: label/data, optional reference role; line: style. Line needs 2 points per series."),
|
|
390
|
-
xAxis: ChartAxis.optional().describe("line, scatter: x axis."),
|
|
391
|
-
yAxis: ChartAxis.optional().describe("bar, line, scatter, boxplot: y axis."),
|
|
391
|
+
xAxis: ChartAxis.optional().describe("line, scatter: x axis. bar: only label, the category axis title."),
|
|
392
|
+
yAxis: ChartAxis.optional().describe("bar, line, scatter, boxplot: y axis; label names the value, format sets units."),
|
|
392
393
|
legend: z
|
|
393
394
|
.boolean()
|
|
394
395
|
.optional()
|
|
395
396
|
.describe("bar (colorByBar), line, scatter, pie, donut: legend; defaults to several series or unlabeled slices."),
|
|
396
397
|
colorByBar: z.boolean().optional().describe("bar: one palette color per bar."),
|
|
397
|
-
showValues: z.boolean().optional().describe("bar, heatmap: print values."),
|
|
398
|
+
showValues: z.boolean().optional().describe("bar: print values, by default when they stay readable. heatmap: print values."),
|
|
398
399
|
area: z.boolean().optional().describe("line, sparkline: fill under the line."),
|
|
399
400
|
smooth: z.boolean().optional().describe("line: smooth curves."),
|
|
400
401
|
showLabels: z.boolean().optional().describe("pie, donut: name slices next to them."),
|
|
@@ -43,7 +43,7 @@ export const isRetryableDictationError = (error: unknown): boolean => {
|
|
|
43
43
|
export const processAiDictation = async (
|
|
44
44
|
id: string,
|
|
45
45
|
signal: AbortSignal,
|
|
46
|
-
run: (input: RunAiTranscriptionInput) => Promise<{ text: string;
|
|
46
|
+
run: (input: RunAiTranscriptionInput) => Promise<{ text: string; modelId: string }> = runAiTranscription,
|
|
47
47
|
): Promise<void> => {
|
|
48
48
|
const token = crypto.randomUUID();
|
|
49
49
|
const [task] = await sql<ClaimedDictation[]>`
|
|
@@ -57,7 +57,7 @@ export const processAiDictation = async (
|
|
|
57
57
|
name: "ai.dictation",
|
|
58
58
|
source: "ai:dictations",
|
|
59
59
|
category: "ai",
|
|
60
|
-
attributes: { dictationId: id, conversationId: task.conversation_id,
|
|
60
|
+
attributes: { dictationId: id, conversationId: task.conversation_id, modelId: task.model_profile_id, attempt: task.attempts },
|
|
61
61
|
});
|
|
62
62
|
let failure: AiTranscriptionError | undefined;
|
|
63
63
|
const canceled = new AbortController();
|
|
@@ -92,7 +92,7 @@ export const processAiDictation = async (
|
|
|
92
92
|
traceParent: span,
|
|
93
93
|
file: new Blob([new Uint8Array(task.source_bytes)]),
|
|
94
94
|
filename: task.source_path,
|
|
95
|
-
|
|
95
|
+
model: { id: task.model_profile_id },
|
|
96
96
|
language: task.language ?? undefined,
|
|
97
97
|
signal: combined,
|
|
98
98
|
attribution: { userId: task.user_id, conversationId: task.conversation_id },
|
|
@@ -109,7 +109,7 @@ export const processAiDictation = async (
|
|
|
109
109
|
log[combined.aborted ? "info" : retry ? "warn" : "error"](failure.message, {
|
|
110
110
|
dictationId: id,
|
|
111
111
|
traceId: span.traceId,
|
|
112
|
-
|
|
112
|
+
modelId: task.model_profile_id,
|
|
113
113
|
attempt: task.attempts,
|
|
114
114
|
retry,
|
|
115
115
|
errorCode: failure.code,
|
package/src/ai/enrich.ts
CHANGED
|
@@ -151,7 +151,7 @@ export const enrichDirtyAiConversations = async (input: {
|
|
|
151
151
|
// Resolve the background model once per run; AI disabled / no model = quiet no-op.
|
|
152
152
|
let resolved: AiResolvedModel;
|
|
153
153
|
try {
|
|
154
|
-
resolved = await (input.deps?.resolveModel ?? resolveAiBackgroundModel)();
|
|
154
|
+
resolved = await (input.deps?.resolveModel ?? resolveAiBackgroundModel)({ class: "fast" });
|
|
155
155
|
} catch (error) {
|
|
156
156
|
log.info("Enrichment skipped: no background model available", {
|
|
157
157
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -224,7 +224,7 @@ export const enrichDirtyAiConversations = async (input: {
|
|
|
224
224
|
conversationId: conversation.id,
|
|
225
225
|
status: "ok",
|
|
226
226
|
trigger,
|
|
227
|
-
|
|
227
|
+
modelId: result.model.id,
|
|
228
228
|
mode: result.structuredMeta.mode,
|
|
229
229
|
durationMs: Date.now() - startedAt,
|
|
230
230
|
titleUpdated: applyTitle,
|
package/src/ai/executor.ts
CHANGED
|
@@ -7,10 +7,11 @@ import { CapabilityActionReviewDataSchema, capabilityActionPolicy } from "../con
|
|
|
7
7
|
import type { AccessSubject, RequestActor } from "../server";
|
|
8
8
|
import { approvalAssurance, approvalScopeFromToolName, approvals } from "../services/approvals";
|
|
9
9
|
import { createHelpReader } from "../services/help";
|
|
10
|
-
import { logger } from "../services/logging";
|
|
10
|
+
import { logger, trace } from "../services/logging";
|
|
11
11
|
import { getMandate } from "../services/mandates";
|
|
12
12
|
import type { MandatePolicyV1 } from "../services/mandates/policy";
|
|
13
13
|
import { coreSettings } from "../services/settings/api";
|
|
14
|
+
import type { AiModelRequest } from "../shared/ai-models";
|
|
14
15
|
import { normalizeLocale } from "../shared/locale";
|
|
15
16
|
import { type AiActionCheck, type AiStoredActionReview, storedActionReview } from "./action-review";
|
|
16
17
|
import {
|
|
@@ -99,6 +100,7 @@ import {
|
|
|
99
100
|
AiTurnFailure,
|
|
100
101
|
type AiTurnFailureInfo,
|
|
101
102
|
type AiTurnFailureReason,
|
|
103
|
+
aiIssueDiagnostic,
|
|
102
104
|
aiTurnFailureFromThrown,
|
|
103
105
|
aiTurnReasonFromThrown,
|
|
104
106
|
rememberProviderErrors,
|
|
@@ -702,7 +704,7 @@ type MaterializedChatConfig = {
|
|
|
702
704
|
tools: AiRuntimeTool[];
|
|
703
705
|
toolApprovalContext?: AiToolApprovalContext;
|
|
704
706
|
modelPolicy: AiChatTurnRunConfig["modelPolicy"];
|
|
705
|
-
|
|
707
|
+
model?: AiModelRequest;
|
|
706
708
|
};
|
|
707
709
|
|
|
708
710
|
const materializeChatConfig = async (config: AiChatTurnRunConfig, signal: AbortSignal, turnId: string): Promise<MaterializedChatConfig> => {
|
|
@@ -722,7 +724,7 @@ const materializeChatConfig = async (config: AiChatTurnRunConfig, signal: AbortS
|
|
|
722
724
|
: [],
|
|
723
725
|
toolApprovalContext: config.toolApprovalContext,
|
|
724
726
|
modelPolicy: config.modelPolicy,
|
|
725
|
-
|
|
727
|
+
model: config.model,
|
|
726
728
|
};
|
|
727
729
|
};
|
|
728
730
|
|
|
@@ -752,7 +754,7 @@ export class AiTurnExecutor {
|
|
|
752
754
|
const runConfig = claim.runConfig;
|
|
753
755
|
if (!runConfig) {
|
|
754
756
|
pipeline.seedBaseline(claim.liveBlocks ?? []);
|
|
755
|
-
await pipeline.emitTurnStarted(claim.turn.
|
|
757
|
+
await pipeline.emitTurnStarted(claim.turn.modelId ?? "");
|
|
756
758
|
await pipeline.emitBaseline();
|
|
757
759
|
await this.finalize(
|
|
758
760
|
conversationId,
|
|
@@ -767,7 +769,7 @@ export class AiTurnExecutor {
|
|
|
767
769
|
|
|
768
770
|
if (runConfig.kind === "compact") {
|
|
769
771
|
pipeline.seedBaseline(claim.liveBlocks ?? []);
|
|
770
|
-
await pipeline.emitTurnStarted(claim.turn.
|
|
772
|
+
await pipeline.emitTurnStarted(claim.turn.modelId ?? "");
|
|
771
773
|
await pipeline.emitBaseline();
|
|
772
774
|
await this.runCompaction(conversationId, turnId, pipeline, runConfig, signal);
|
|
773
775
|
return;
|
|
@@ -778,7 +780,7 @@ export class AiTurnExecutor {
|
|
|
778
780
|
attemptState = await loadChatAttemptState(conversationId, turnId);
|
|
779
781
|
} catch (error) {
|
|
780
782
|
pipeline.seedBaseline(claim.liveBlocks ?? []);
|
|
781
|
-
await pipeline.emitTurnStarted(claim.turn.
|
|
783
|
+
await pipeline.emitTurnStarted(claim.turn.modelId ?? "");
|
|
782
784
|
await pipeline.emitBaseline();
|
|
783
785
|
await this.finalize(
|
|
784
786
|
conversationId,
|
|
@@ -792,15 +794,15 @@ export class AiTurnExecutor {
|
|
|
792
794
|
return;
|
|
793
795
|
}
|
|
794
796
|
pipeline.seedBaseline(rebuildAttemptBaseline(attemptState));
|
|
795
|
-
await pipeline.emitTurnStarted(claim.turn.
|
|
797
|
+
await pipeline.emitTurnStarted(claim.turn.modelId ?? "");
|
|
796
798
|
await pipeline.emitBaseline();
|
|
797
799
|
await this.runChat(conversationId, turnId, claim, runConfig, pipeline, signal, false, attemptState);
|
|
798
800
|
}
|
|
799
801
|
|
|
800
802
|
/**
|
|
801
803
|
* Ends the turn. A failure stores its reason twice: as a code on the turn's last message, which the chat words in
|
|
802
|
-
* the reader's language, and as text in the turn's language for readers without the chat view. The
|
|
803
|
-
*
|
|
804
|
+
* the reader's language, and as text in the turn's language for readers without the chat view. The diagnostic cause,
|
|
805
|
+
* with provider and loop issues reduced to fixed text, goes only to the log. A failed compaction left the chat as it was, so its text names
|
|
804
806
|
* only the reason.
|
|
805
807
|
*/
|
|
806
808
|
private async finalize(
|
|
@@ -888,9 +890,6 @@ export class AiTurnExecutor {
|
|
|
888
890
|
}
|
|
889
891
|
resolvedProjectId = project.id;
|
|
890
892
|
}
|
|
891
|
-
if (isAssistantChatTurn(config) && claim.turn.modelProfileId) {
|
|
892
|
-
await aiModelAccess.assertAllowed(claim.turn.modelProfileId, accessSubjectForActor(material.actor));
|
|
893
|
-
}
|
|
894
893
|
validated = await (this.config.validateTurn ?? validateAiTurnRequest)({
|
|
895
894
|
input: config.input,
|
|
896
895
|
hasImageAttachments: config.files?.attached.some((file) => isAiImageMediaType(file.mediaType)),
|
|
@@ -898,12 +897,10 @@ export class AiTurnExecutor {
|
|
|
898
897
|
material.tools.some((tool) => tool.def.name === "view_image") &&
|
|
899
898
|
(await isAiVisionModelConfigured(material.modelPolicy?.allowedDataBoundaries)),
|
|
900
899
|
modelPolicy: material.modelPolicy,
|
|
901
|
-
|
|
902
|
-
? (claim.turn.modelProfileId ?? material.requestedModelId)
|
|
903
|
-
: material.requestedModelId,
|
|
900
|
+
model: claim.turn.modelId ? { id: claim.turn.modelId } : material.model,
|
|
904
901
|
});
|
|
905
902
|
if (isAssistantChatTurn(config)) {
|
|
906
|
-
await aiModelAccess.assertAllowed(validated.resolved.profile.
|
|
903
|
+
await aiModelAccess.assertAllowed(validated.resolved.profile.modelId, accessSubjectForActor(material.actor));
|
|
907
904
|
}
|
|
908
905
|
} catch (error) {
|
|
909
906
|
signal.removeEventListener("abort", onSignal);
|
|
@@ -919,6 +916,11 @@ export class AiTurnExecutor {
|
|
|
919
916
|
return;
|
|
920
917
|
}
|
|
921
918
|
const { settings, resolved } = validated;
|
|
919
|
+
if (config.modelResolved?.id === resolved.profile.id) resolved.model = config.modelResolved;
|
|
920
|
+
await trace.record({
|
|
921
|
+
event: "model.resolved",
|
|
922
|
+
attributes: { ...resolved.model, resolvedClass: resolved.model.class, turnId, conversationId },
|
|
923
|
+
});
|
|
922
924
|
|
|
923
925
|
const defaultToolSource = config.toolSource?.kind === "default" ? config.toolSource : null;
|
|
924
926
|
const toolActor =
|
|
@@ -1186,7 +1188,7 @@ export class AiTurnExecutor {
|
|
|
1186
1188
|
await pipeline.emitMessage(message);
|
|
1187
1189
|
},
|
|
1188
1190
|
conversationId,
|
|
1189
|
-
|
|
1191
|
+
modelId: resolved.profile.id,
|
|
1190
1192
|
turnId,
|
|
1191
1193
|
leaseOwner: this.config.leaseOwner,
|
|
1192
1194
|
turnInput,
|
|
@@ -1590,7 +1592,9 @@ export class AiTurnExecutor {
|
|
|
1590
1592
|
provider: acceptCanonicalToolNames(
|
|
1591
1593
|
retryTransientProviderErrors(
|
|
1592
1594
|
rememberProviderErrors(
|
|
1593
|
-
answerOpenToolCalls(
|
|
1595
|
+
answerOpenToolCalls(
|
|
1596
|
+
assistantQuotaProvider(resolved.provider, config, quotaSubject, resolved.profile, turnId, conversationId, resolved.model),
|
|
1597
|
+
),
|
|
1594
1598
|
remember,
|
|
1595
1599
|
),
|
|
1596
1600
|
{
|
|
@@ -1658,7 +1662,7 @@ export class AiTurnExecutor {
|
|
|
1658
1662
|
: createCloudCompactFn({
|
|
1659
1663
|
conversationId,
|
|
1660
1664
|
turnId,
|
|
1661
|
-
|
|
1665
|
+
modelId: resolved.profile.id,
|
|
1662
1666
|
additionalInstructions: settings.compactionInstructions,
|
|
1663
1667
|
maxOutputTokens: resolved.profile.maxOutputTokens,
|
|
1664
1668
|
signal: abortController.signal,
|
|
@@ -1885,8 +1889,9 @@ export class AiTurnExecutor {
|
|
|
1885
1889
|
if (!abortController.signal.aborted && isToolRound(event.message)) noteToolRound();
|
|
1886
1890
|
} else if (event.type === "issue") {
|
|
1887
1891
|
if (event.issue.kind === "malformed_tool_call" && event.issue.callId) malformedCallIds.add(event.issue.callId);
|
|
1888
|
-
|
|
1889
|
-
|
|
1892
|
+
const diagnostic = aiIssueDiagnostic(event.issue);
|
|
1893
|
+
lastIssueMessage = diagnostic.message;
|
|
1894
|
+
log.warn("AI turn issue", { conversationId, turnId, ...diagnostic });
|
|
1890
1895
|
} else if (event.type === "loop_end") {
|
|
1891
1896
|
const stoppedAfterFailure =
|
|
1892
1897
|
event.reason === "stop" &&
|
|
@@ -2218,7 +2223,7 @@ export class AiTurnExecutor {
|
|
|
2218
2223
|
validated = await (this.config.validateTurn ?? validateAiTurnRequest)({
|
|
2219
2224
|
input: "",
|
|
2220
2225
|
modelPolicy: config.modelPolicy,
|
|
2221
|
-
|
|
2226
|
+
model: config.model,
|
|
2222
2227
|
});
|
|
2223
2228
|
} catch (error) {
|
|
2224
2229
|
signal.removeEventListener("abort", onSignal);
|
|
@@ -2226,10 +2231,15 @@ export class AiTurnExecutor {
|
|
|
2226
2231
|
return;
|
|
2227
2232
|
}
|
|
2228
2233
|
const { settings, resolved } = validated;
|
|
2234
|
+
if (config.modelResolved?.id === resolved.profile.id) resolved.model = config.modelResolved;
|
|
2235
|
+
await trace.record({
|
|
2236
|
+
event: "model.resolved",
|
|
2237
|
+
attributes: { ...resolved.model, resolvedClass: resolved.model.class, turnId, conversationId },
|
|
2238
|
+
});
|
|
2229
2239
|
|
|
2230
2240
|
const store = aiConversations.createSessionStore({
|
|
2231
2241
|
conversationId,
|
|
2232
|
-
|
|
2242
|
+
modelId: resolved.profile.id,
|
|
2233
2243
|
turnId,
|
|
2234
2244
|
leaseOwner: this.config.leaseOwner,
|
|
2235
2245
|
});
|
|
@@ -2242,6 +2252,7 @@ export class AiTurnExecutor {
|
|
|
2242
2252
|
inferenceProvider(resolved.provider, resolved.profile, {
|
|
2243
2253
|
kind: "background",
|
|
2244
2254
|
task: "chat-compaction",
|
|
2255
|
+
modelRef: resolved.model,
|
|
2245
2256
|
conversationId,
|
|
2246
2257
|
turnId,
|
|
2247
2258
|
appId: "core",
|
|
@@ -2255,7 +2266,7 @@ export class AiTurnExecutor {
|
|
|
2255
2266
|
compact: createCloudCompactFn({
|
|
2256
2267
|
conversationId,
|
|
2257
2268
|
turnId,
|
|
2258
|
-
|
|
2269
|
+
modelId: resolved.profile.id,
|
|
2259
2270
|
additionalInstructions: settings.compactionInstructions,
|
|
2260
2271
|
maxOutputTokens: resolved.profile.maxOutputTokens,
|
|
2261
2272
|
signal: abortController.signal,
|
|
@@ -2274,7 +2285,7 @@ export class AiTurnExecutor {
|
|
|
2274
2285
|
for await (const event of loop as AsyncIterable<CompactEvent>) {
|
|
2275
2286
|
if (event.type === "compaction_start") await pipeline.applyCompaction("running");
|
|
2276
2287
|
else if (event.type === "compaction_end") await pipeline.applyCompaction("completed");
|
|
2277
|
-
else if (event.type === "issue") issueMessage = event.issue.message;
|
|
2288
|
+
else if (event.type === "issue") issueMessage = aiIssueDiagnostic(event.issue).message;
|
|
2278
2289
|
else if (event.type === "loop_end") {
|
|
2279
2290
|
status = event.reason === "stop" ? "completed" : event.reason === "aborted" ? "aborted" : "failed";
|
|
2280
2291
|
await pipeline.applyCompaction(status === "failed" ? "failed" : "completed", event.result);
|
|
@@ -2428,10 +2439,10 @@ class StreamPipeline {
|
|
|
2428
2439
|
await this.maybeSnapshot();
|
|
2429
2440
|
}
|
|
2430
2441
|
|
|
2431
|
-
async emitTurnStarted(
|
|
2442
|
+
async emitTurnStarted(modelId: string): Promise<void> {
|
|
2432
2443
|
const seq = this.nextSeq();
|
|
2433
2444
|
await this.publish(
|
|
2434
|
-
this.envelope({ type: "turn_started" as const, seq,
|
|
2445
|
+
this.envelope({ type: "turn_started" as const, seq, modelId, providerModel: "", blocks: this.blocks }) as AiWireEvent,
|
|
2435
2446
|
);
|
|
2436
2447
|
}
|
|
2437
2448
|
|
package/src/ai/http.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Context } from "hono";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { CapabilityAppIdSchema } from "../contracts/capabilities";
|
|
5
5
|
import { type AuthContext, err, fail, respond } from "../server";
|
|
6
|
+
import { AiModelRequestSchema } from "../shared/ai-models";
|
|
6
7
|
import { aiAttachmentMarker } from "./attachments";
|
|
7
8
|
import { CODE_RUNTIME_TOOL_NAMES } from "./browser-code-contracts";
|
|
8
9
|
import { AI_TURN_ATTACHMENT_MAX_ITEMS } from "./limits";
|
|
@@ -11,13 +12,7 @@ import { AiQuotaError } from "./quotas";
|
|
|
11
12
|
import { AiResourceMarkerSchema } from "./resource-markers";
|
|
12
13
|
import { AI_SHORT_ID_PATTERN } from "./short-id";
|
|
13
14
|
import { AI_SKILL_NAME_MAX_CHARS, AI_SKILL_NAME_PATTERN } from "./skill-format";
|
|
14
|
-
import {
|
|
15
|
-
AI_MESSAGE_FEEDBACK_REASONS,
|
|
16
|
-
type AiClientToolId,
|
|
17
|
-
type AiDraftContentPart,
|
|
18
|
-
type AiSettingsError,
|
|
19
|
-
type AiUserContentPart,
|
|
20
|
-
} from "./types";
|
|
15
|
+
import { AI_MESSAGE_FEEDBACK_REASONS, type AiClientToolId, type AiDraftContentPart, type AiUserContentPart } from "./types";
|
|
21
16
|
import { isAiSettingsError } from "./validate";
|
|
22
17
|
|
|
23
18
|
export const AiDraftContentPartSchema = z.discriminatedUnion("type", [
|
|
@@ -144,9 +139,10 @@ export const AiTurnInputSchema = z
|
|
|
144
139
|
.min(1)
|
|
145
140
|
.max(AI_TURN_ATTACHMENT_MAX_ITEMS * 2 + 1)
|
|
146
141
|
.optional(),
|
|
147
|
-
|
|
142
|
+
model: AiModelRequestSchema.optional(),
|
|
148
143
|
clientToolIds: AiClientToolIdsSchema.optional(),
|
|
149
144
|
})
|
|
145
|
+
.strict()
|
|
150
146
|
.refine((input) => Boolean(input.message?.trim() || input.content?.length), {
|
|
151
147
|
message: "Message or content is required.",
|
|
152
148
|
path: ["message"],
|
|
@@ -166,13 +162,14 @@ export const AiSubmitConversationDraftInputSchema = z
|
|
|
166
162
|
.object({
|
|
167
163
|
queueId: z.uuid().optional(),
|
|
168
164
|
draftRevision: z.number().int().min(1),
|
|
169
|
-
|
|
165
|
+
model: AiModelRequestSchema.optional(),
|
|
170
166
|
clientToolIds: AiClientToolIdsSchema.optional(),
|
|
171
167
|
/** Set by a local client such as cld; the web never sends it. */
|
|
172
168
|
client: AiLocalClientSchema.optional(),
|
|
173
169
|
/** AGENTS.md files and local Skills a local client found; only valid together with client. */
|
|
174
170
|
localContext: AiLocalContextSchema.optional(),
|
|
175
171
|
})
|
|
172
|
+
.strict()
|
|
176
173
|
.refine((input) => !input.localContext || Boolean(input.client), {
|
|
177
174
|
message: "Local context requires a local client.",
|
|
178
175
|
path: ["localContext"],
|
|
@@ -183,9 +180,11 @@ export const AiSteerInputSchema = z.object({
|
|
|
183
180
|
clientRequestId: z.string().trim().min(1).max(120),
|
|
184
181
|
});
|
|
185
182
|
|
|
186
|
-
export const AiCompactionInputSchema = z
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
export const AiCompactionInputSchema = z
|
|
184
|
+
.object({
|
|
185
|
+
model: AiModelRequestSchema.optional(),
|
|
186
|
+
})
|
|
187
|
+
.strict();
|
|
189
188
|
|
|
190
189
|
export type AiCompactionInput = z.infer<typeof AiCompactionInputSchema>;
|
|
191
190
|
|
|
@@ -200,11 +199,12 @@ export const AiMessageRetryInputSchema = z
|
|
|
200
199
|
.min(1)
|
|
201
200
|
.max(AI_TURN_ATTACHMENT_MAX_ITEMS * 2 + 1)
|
|
202
201
|
.optional(),
|
|
203
|
-
|
|
202
|
+
model: AiModelRequestSchema.optional(),
|
|
204
203
|
clientToolIds: AiClientToolIdsSchema.optional(),
|
|
205
204
|
client: AiLocalClientSchema.optional(),
|
|
206
205
|
localContext: AiLocalContextSchema.omit({ selectedSkills: true }).optional(),
|
|
207
206
|
})
|
|
207
|
+
.strict()
|
|
208
208
|
.refine((input) => !input.localContext || Boolean(input.client), {
|
|
209
209
|
message: "Local context requires a local client.",
|
|
210
210
|
path: ["localContext"],
|
|
@@ -268,25 +268,13 @@ export const AiApiErrorSchema = z.object({
|
|
|
268
268
|
|
|
269
269
|
const conflict = (message: string) => ({ code: "CONFLICT" as const, message, status: 409 as const });
|
|
270
270
|
|
|
271
|
-
const aiSettingsServiceError = (error: AiSettingsError) => {
|
|
272
|
-
switch (error.code) {
|
|
273
|
-
case "model_access_denied":
|
|
274
|
-
return err.forbidden(error.message);
|
|
275
|
-
case "invalid_model_profiles":
|
|
276
|
-
case "model_policy_mismatch":
|
|
277
|
-
return err.badInput(error.message);
|
|
278
|
-
case "ai_disabled":
|
|
279
|
-
case "missing_default_model":
|
|
280
|
-
case "default_model_disabled":
|
|
281
|
-
case "missing_provider_credential":
|
|
282
|
-
return conflict(error.message);
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
|
|
286
271
|
export const toAiErrorResponse = (c: Context<AuthContext>, error: unknown) => {
|
|
287
272
|
if (error instanceof AiQuotaError) return c.json({ ok: false, error: error.message, message: error.message, code: error.code }, 429);
|
|
288
273
|
if (isAiSettingsError(error)) {
|
|
289
|
-
return
|
|
274
|
+
return c.json(
|
|
275
|
+
{ ok: false, code: error.aiError.code, message: error.aiError.message, error: error.aiError.message },
|
|
276
|
+
error.aiError.code === "model_policy_mismatch" ? 400 : 409,
|
|
277
|
+
);
|
|
290
278
|
}
|
|
291
279
|
const message = error instanceof Error ? error.message : "AI request failed";
|
|
292
280
|
if (message.includes("idx_ai_turns_one_active")) return respond(c, fail(err.conflict("Running turn")));
|
package/src/ai/index.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
export * from "../shared/ai-models";
|
|
1
2
|
export { type AiAppSkillIssue, aiAppSkills, appSkillsRegistryState, reconcileAppSkills } from "./app-skill-store";
|
|
3
|
+
export {
|
|
4
|
+
type ApplicationAiTaskInput,
|
|
5
|
+
type ApplicationAiTaskKey,
|
|
6
|
+
deleteApplicationAiTasks,
|
|
7
|
+
listUnreleasedApplicationAiTaskSources,
|
|
8
|
+
releaseApplicationAiTask,
|
|
9
|
+
runApplicationAiTask,
|
|
10
|
+
} from "./application-tasks";
|
|
2
11
|
export { type AiApprovalPreferenceRoutes, type AiApprovalPreferenceView, createAiApprovalPreferenceRoutes } from "./approval-routes";
|
|
3
12
|
export {
|
|
4
13
|
AI_WEBSITE_APPROVAL_TOOL,
|
|
@@ -19,7 +28,7 @@ export {
|
|
|
19
28
|
rememberAiToolApproval,
|
|
20
29
|
revokeAiToolApprovalPreference,
|
|
21
30
|
} from "./approvals";
|
|
22
|
-
export { assistantAiSettingsState, listAssistantAiModels,
|
|
31
|
+
export { assistantAiSettingsState, listAssistantAiModels, peopleAiModels, selectAssistantAiModel } from "./assistant-models";
|
|
23
32
|
export { type AiAttachmentRef, aiAttachmentMarker, formatAiFileSize, parseAiAttachmentMarkers } from "./attachments";
|
|
24
33
|
export { CloudAiTranscribeAudioInputSchema, CloudAiTranscribeAudioOutputSchema, createCloudAiTranscribeAudioTool } from "./audio-tool";
|
|
25
34
|
export { AiBackgroundApprovalRequiredError, authorizeBackgroundAction } from "./background-review";
|
|
@@ -65,7 +74,7 @@ export { parseAiSse } from "./client/transport";
|
|
|
65
74
|
export { CODE_CAPABILITY_TOKEN_HEADER, createCodeCapabilityTransport } from "./code-capability-transport";
|
|
66
75
|
export { authorizeCodeExecution } from "./code-execution";
|
|
67
76
|
export { CODE_SOURCE_TOOLS, type CodeSourceToolName } from "./code-source-contracts";
|
|
68
|
-
export {
|
|
77
|
+
export { listAiCredentialProviderIds } from "./credentials";
|
|
69
78
|
export {
|
|
70
79
|
CLOUD_AI_CHART_MAX_SERIES,
|
|
71
80
|
CLOUD_AI_CHART_MAX_VALUES,
|
|
@@ -306,7 +315,6 @@ export {
|
|
|
306
315
|
listAiModels,
|
|
307
316
|
readAiSettingsState,
|
|
308
317
|
resolveAiModel,
|
|
309
|
-
resolveAiSettingsStateFromRaw,
|
|
310
318
|
resolveAiVisionModel,
|
|
311
319
|
selectAiModelProfile,
|
|
312
320
|
toPublicAiSettingsState,
|
|
@@ -365,7 +373,6 @@ export {
|
|
|
365
373
|
streamAiConversationEvents,
|
|
366
374
|
} from "./stream";
|
|
367
375
|
export {
|
|
368
|
-
AI_BACKGROUND_MODEL_SETTING_KEY,
|
|
369
376
|
type RunAiStructuredInput,
|
|
370
377
|
type RunAiStructuredResult,
|
|
371
378
|
resolveAiBackgroundModel,
|
|
@@ -420,7 +427,6 @@ export type {
|
|
|
420
427
|
AiPendingTurnAction,
|
|
421
428
|
AiProjectPromptSnapshot,
|
|
422
429
|
AiProviderId,
|
|
423
|
-
AiPublicModelProfile,
|
|
424
430
|
AiRecentResource,
|
|
425
431
|
AiResolvedModel,
|
|
426
432
|
AiRuntimeTool,
|
|
@@ -27,8 +27,10 @@ export class AiBackgroundAdmissionError extends Error {
|
|
|
27
27
|
export type AiCallContext = {
|
|
28
28
|
kind: "chat" | "background";
|
|
29
29
|
task: string;
|
|
30
|
+
modelRef?: import("../shared/ai-models").AiResolvedModelRef;
|
|
30
31
|
subject?: AccessSubject | null;
|
|
31
32
|
userId?: string;
|
|
33
|
+
credentialId?: string;
|
|
32
34
|
conversationId?: string;
|
|
33
35
|
turnId?: string;
|
|
34
36
|
workflowRunId?: string;
|
|
@@ -91,7 +93,7 @@ export async function beginAiCall(
|
|
|
91
93
|
}
|
|
92
94
|
if (hasBillableAiPricing(profile.pricing) && context.kind === "chat" && context.subject && config.enabled) {
|
|
93
95
|
const now = new Date();
|
|
94
|
-
const snapshot = await aiQuotas.snapshot(context.subject, profile.
|
|
96
|
+
const snapshot = await aiQuotas.snapshot(context.subject, profile.modelId, now, db);
|
|
95
97
|
for (const balance of snapshot.balances) {
|
|
96
98
|
if (balance.bypassed || balance.limit === null) continue;
|
|
97
99
|
if (balance.unknown)
|
|
@@ -109,7 +111,7 @@ export async function beginAiCall(
|
|
|
109
111
|
SELECT (COALESCE(sum(cost),0)+COALESCE(sum(reserved) FILTER(WHERE finished_at IS NULL AND lease_expires_at>now()),0))::text AS committed FROM ai.inference_calls c
|
|
110
112
|
WHERE kind='chat'
|
|
111
113
|
AND user_id IS NOT DISTINCT FROM ${user}::uuid AND service_account_id IS NOT DISTINCT FROM ${service}::uuid
|
|
112
|
-
AND (${balance.scope}='*' OR
|
|
114
|
+
AND (${balance.scope}='*' OR model_id=${balance.scope}) AND started_at>=${window.from} AND started_at<${window.until}
|
|
113
115
|
AND started_at>COALESCE((SELECT max(created_at) FROM ai.cost_resets r
|
|
114
116
|
WHERE r.user_id IS NOT DISTINCT FROM ${user}::uuid AND r.service_account_id IS NOT DISTINCT FROM ${service}::uuid AND r.scope=${balance.scope}),'-infinity'::timestamptz)`;
|
|
115
117
|
const available = aiBudgetUnits(balance.limit) - aiDecimalUnits(pending?.committed ?? "0");
|
|
@@ -145,10 +147,10 @@ export async function beginAiCall(
|
|
|
145
147
|
const id = crypto.randomUUID();
|
|
146
148
|
const userId = context.subject?.type === "user" ? context.subject.userId : context.userId;
|
|
147
149
|
const serviceId = context.subject?.type === "service_account" ? context.subject.serviceAccountId : null;
|
|
148
|
-
await db`INSERT INTO ai.inference_calls(id,user_id,service_account_id,model_profile_id,provider_model,kind,task,
|
|
150
|
+
await db`INSERT INTO ai.inference_calls(id,user_id,service_account_id,credential_id,model_profile_id,model_id,requested_class,resolved_class,provider_model,kind,task,
|
|
149
151
|
turn_id,turn_attempt,conversation_id,workflow_run_id,step_key,trace_id,app_id,workflow_id,workflow_name,workflow_version,pricing,reserved,lease_expires_at)
|
|
150
152
|
VALUES(${id}::uuid,CASE WHEN ${serviceId}::uuid IS NULL THEN COALESCE(${userId ?? null}::uuid,(SELECT created_by_user_id FROM ai.conversations WHERE id=${context.conversationId ?? null}::uuid)) ELSE NULL END,
|
|
151
|
-
${serviceId}::uuid,${profile.id},${profile.model},${context.kind},${context.task},${context.turnId ?? null}::uuid,
|
|
153
|
+
${serviceId}::uuid,${context.credentialId ?? null}::uuid,${profile.id},${profile.modelId},${context.modelRef?.requestedClass ?? null},${context.modelRef?.class ?? null},${profile.model},${context.kind},${context.task},${context.turnId ?? null}::uuid,
|
|
152
154
|
(SELECT attempt FROM ai.turns WHERE id=${context.turnId ?? null}::uuid),${context.conversationId ?? null}::uuid,
|
|
153
155
|
${context.workflowRunId ?? null}::uuid,${context.stepKey ?? null},${context.traceId ?? null},COALESCE(${workflow?.app ?? context.appId ?? null},(SELECT launched_by_app_id FROM ai.conversations WHERE id=${context.conversationId ?? null}::uuid),'core'),${workflow?.id ?? null}::uuid,${workflow?.name ?? null},${workflow?.revision ?? null},
|
|
154
156
|
(${profile.pricing ? JSON.stringify(profile.pricing) : null}::text)::jsonb,${reserved}::numeric,now()+interval '2 minutes')`;
|