@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/quota-report.ts
CHANGED
|
@@ -4,18 +4,21 @@ import { recursiveGroupIdsSubquery } from "../services/accounts/group-sql";
|
|
|
4
4
|
import { toPgTextArray } from "../services/postgres";
|
|
5
5
|
import { hasBillableAiPricing } from "../shared/ai-costs";
|
|
6
6
|
import { type AiQuotaReport, type AiQuotaReportQuery, AiQuotaReportQuerySchema } from "../shared/ai-quotas";
|
|
7
|
+
import { aiPeopleAtLimit } from "./quota-limits";
|
|
7
8
|
import { aiQuotas } from "./quotas";
|
|
8
9
|
import { readAiSettingsState } from "./settings";
|
|
9
10
|
|
|
10
11
|
/** Resolve display metadata only for administration, never on the inference path. */
|
|
11
12
|
export async function quotaAdminConfig() {
|
|
12
13
|
const config = await aiQuotas.config();
|
|
14
|
+
const limits = await aiPeopleAtLimit();
|
|
13
15
|
const grants = await resolveDisplayNames(config.rules.flatMap((rule) => rule.grants));
|
|
14
16
|
let offset = 0;
|
|
15
17
|
return {
|
|
16
18
|
...config,
|
|
17
19
|
rules: config.rules.map((rule) => ({
|
|
18
20
|
...rule,
|
|
21
|
+
peopleAtLimit: limits.find((limit) => limit.scope === rule.scope)?.people.length ?? 0,
|
|
19
22
|
grants: rule.grants.map(() => {
|
|
20
23
|
const grant = grants[offset++]!;
|
|
21
24
|
return {
|
|
@@ -50,7 +53,7 @@ export async function quotaReport(input: Partial<AiQuotaReportQuery> = {}, now =
|
|
|
50
53
|
page: 1,
|
|
51
54
|
perPage: 25,
|
|
52
55
|
};
|
|
53
|
-
const pricedModels = (await readAiSettingsState()).profiles.filter((p) => hasBillableAiPricing(p.pricing)).map((p) => p.
|
|
56
|
+
const pricedModels = (await readAiSettingsState()).profiles.filter((p) => hasBillableAiPricing(p.pricing)).map((p) => p.modelId);
|
|
54
57
|
const bucket = query.range === "24h" ? "hour" : "day";
|
|
55
58
|
const order = query.sort === "label" ? sql`label` : query.sort === "cost" ? sql`cost` : sql`last_used`;
|
|
56
59
|
const direction = query.direction === "asc" ? sql`ASC` : sql`DESC`;
|
|
@@ -80,7 +83,7 @@ export async function quotaReport(input: Partial<AiQuotaReportQuery> = {}, now =
|
|
|
80
83
|
FROM ai.inference_calls c WHERE c.kind='chat' AND c.started_at<=${now}
|
|
81
84
|
), periods AS MATERIALIZED (
|
|
82
85
|
SELECT * FROM calls WHERE started_at>=${since} AND started_at<${until}
|
|
83
|
-
AND (${query.model}='' OR
|
|
86
|
+
AND (${query.model}='' OR model_id=${query.model})
|
|
84
87
|
), allowances AS MATERIALIZED (
|
|
85
88
|
SELECT a.id,a.type,r.scope,
|
|
86
89
|
CASE WHEN bool_or((g->>'limit') IS NULL) FILTER(WHERE g IS NOT NULL) THEN NULL ELSE COALESCE(max((g->>'limit')::float8),0) END AS amount,
|
|
@@ -95,7 +98,7 @@ export async function quotaReport(input: Partial<AiQuotaReportQuery> = {}, now =
|
|
|
95
98
|
), balances AS MATERIALIZED (
|
|
96
99
|
SELECT a.*,COALESCE(sum(c.cost),0)::float8 AS used,count(c.id) FILTER(WHERE c.unknown)::int AS unknown
|
|
97
100
|
FROM allowances a LEFT JOIN calls c ON c.identity_id=a.id AND c.identity_type=a.type
|
|
98
|
-
AND (a.scope='*' OR c.
|
|
101
|
+
AND (a.scope='*' OR c.model_id=a.scope) AND c.started_at>=a.starts
|
|
99
102
|
AND c.started_at>COALESCE((SELECT max(created_at) FROM ai.cost_resets z
|
|
100
103
|
WHERE COALESCE(z.user_id,z.service_account_id)=a.id AND (z.user_id IS NOT NULL)=(a.type='user') AND z.scope=a.scope),'-infinity'::timestamptz)
|
|
101
104
|
GROUP BY a.id,a.type,a.scope,a.amount,a.starts
|
|
@@ -111,7 +114,7 @@ export async function quotaReport(input: Partial<AiQuotaReportQuery> = {}, now =
|
|
|
111
114
|
SELECT a.id,a.type,a.label,
|
|
112
115
|
CASE WHEN NOT COALESCE((SELECT enabled FROM config),false) THEN 'disabled' ELSE COALESCE(s.status,'unlimited') END AS status,
|
|
113
116
|
COALESCE(s.scopes,0) AS scopes,CASE WHEN s.status='unlimited' OR NOT COALESCE((SELECT enabled FROM config),false) THEN 0 ELSE COALESCE(s.exhausted,0) END AS exhausted,
|
|
114
|
-
(SELECT max(started_at) FROM calls c WHERE c.identity_id=a.id AND c.identity_type=a.type AND (${query.model}='' OR c.
|
|
117
|
+
(SELECT max(started_at) FROM calls c WHERE c.identity_id=a.id AND c.identity_type=a.type AND (${query.model}='' OR c.model_id=${query.model})) AS last_used,
|
|
115
118
|
sum(p.cost)::float8 AS cost,COALESCE(sum(p.input),0)::float8 AS input,COALESCE(sum(p.output),0)::float8 AS output,
|
|
116
119
|
count(p.id)::int AS calls,count(p.id) FILTER(WHERE p.input IS NOT NULL AND p.output IS NOT NULL)::int AS measured,count(p.id) FILTER(WHERE p.estimated)::int AS estimated,count(p.id) FILTER(WHERE p.unknown)::int AS unknown
|
|
117
120
|
FROM identities a LEFT JOIN states s ON s.id=a.id AND s.type=a.type
|
package/src/ai/quotas-migrate.ts
CHANGED
|
@@ -23,6 +23,7 @@ export async function migrateAiQuotas() {
|
|
|
23
23
|
CHECK(cost IS NULL OR cost>=0), CHECK(reserved>=0),
|
|
24
24
|
CHECK(input IS NULL OR input>=0), CHECK(output IS NULL OR output>=0)
|
|
25
25
|
)`;
|
|
26
|
+
await db`ALTER TABLE ai.inference_calls ADD COLUMN IF NOT EXISTS credential_id UUID`;
|
|
26
27
|
await db`ALTER TABLE ai.inference_calls ADD COLUMN IF NOT EXISTS estimated BOOLEAN NOT NULL DEFAULT false`;
|
|
27
28
|
await db`ALTER TABLE ai.inference_calls ADD COLUMN IF NOT EXISTS error TEXT`;
|
|
28
29
|
await db`ALTER TABLE ai.inference_calls ADD COLUMN IF NOT EXISTS cancelled BOOLEAN NOT NULL DEFAULT false`;
|
package/src/ai/quotas.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type AccessSubject, buildAccessPrincipalCondition } from "../server/ser
|
|
|
3
3
|
import { hasBillableAiPricing } from "../shared/ai-costs";
|
|
4
4
|
import { type AiQuotaConfig, AiQuotaConfigSchema, type AiQuotaIdentity, type AiQuotaSnapshot } from "../shared/ai-quotas";
|
|
5
5
|
import { readAiSettingsState } from "./settings";
|
|
6
|
+
import type { AiSettingsState } from "./types";
|
|
6
7
|
|
|
7
8
|
export class AiQuotaError extends Error {
|
|
8
9
|
constructor(
|
|
@@ -43,10 +44,18 @@ export const aiQuotas = {
|
|
|
43
44
|
return { ...config, revision };
|
|
44
45
|
});
|
|
45
46
|
},
|
|
46
|
-
async snapshot(
|
|
47
|
-
|
|
47
|
+
async snapshot(
|
|
48
|
+
subject: AccessSubject,
|
|
49
|
+
model?: string,
|
|
50
|
+
now = new Date(),
|
|
51
|
+
db: SQL = sql,
|
|
52
|
+
context?: { state: AiSettingsState; config: AiQuotaConfig },
|
|
53
|
+
): Promise<AiQuotaSnapshot> {
|
|
54
|
+
const config = context?.config ?? (await aiQuotas.config(db));
|
|
48
55
|
const identity = ids(subject);
|
|
49
|
-
const pricedModels = new Set(
|
|
56
|
+
const pricedModels = new Set(
|
|
57
|
+
(context?.state ?? (await readAiSettingsState())).profiles.filter((p) => hasBillableAiPricing(p.pricing)).map((p) => p.modelId),
|
|
58
|
+
);
|
|
50
59
|
const matches = buildAccessPrincipalCondition({
|
|
51
60
|
subject,
|
|
52
61
|
columns: {
|
|
@@ -76,7 +85,7 @@ export const aiQuotas = {
|
|
|
76
85
|
(c.finished_at IS NOT NULL OR (c.lease_expires_at <= now() OR (c.turn_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM ai.turns t WHERE t.id=c.turn_id AND t.attempt=c.turn_attempt AND t.status='running' AND t.lease_expires_at>now())))))::int AS unknown
|
|
77
86
|
FROM ai.inference_calls c
|
|
78
87
|
WHERE c.kind='chat' AND c.user_id IS NOT DISTINCT FROM ${identity.user}::uuid AND c.service_account_id IS NOT DISTINCT FROM ${identity.service}::uuid
|
|
79
|
-
AND (${rule.scope}='*' OR c.
|
|
88
|
+
AND (${rule.scope}='*' OR c.model_id=${rule.scope})
|
|
80
89
|
AND c.started_at>=${window.from} AND c.started_at<${window.until}
|
|
81
90
|
AND c.started_at>COALESCE((SELECT max(r.created_at) FROM ai.cost_resets r
|
|
82
91
|
WHERE r.user_id IS NOT DISTINCT FROM ${identity.user}::uuid AND r.service_account_id IS NOT DISTINCT FROM ${identity.service}::uuid AND r.scope=${rule.scope}),'-infinity'::timestamptz)`;
|
|
@@ -110,13 +119,15 @@ export const aiQuotas = {
|
|
|
110
119
|
GROUP BY model_profile_id ORDER BY model_profile_id`;
|
|
111
120
|
return { enabled: config.enabled, unit: config.unit ?? "EUR", balances, usage };
|
|
112
121
|
},
|
|
113
|
-
async assertAllowed(subject: AccessSubject,
|
|
122
|
+
async assertAllowed(subject: AccessSubject, variantId: string, providedState?: AiSettingsState) {
|
|
123
|
+
const state = providedState ?? (await readAiSettingsState());
|
|
124
|
+
const profile = state.profiles.find((profile) => profile.id === variantId);
|
|
125
|
+
if (!profile || !hasBillableAiPricing(profile.pricing)) return false;
|
|
126
|
+
const model = profile.modelId;
|
|
114
127
|
// Fast default path: no additional identity/usage queries on unrestricted installations.
|
|
115
|
-
const pricing = (await readAiSettingsState()).profiles.find((profile) => profile.id === model)?.pricing;
|
|
116
|
-
if (!hasBillableAiPricing(pricing)) return false;
|
|
117
128
|
const config = await aiQuotas.config();
|
|
118
129
|
if (!config.enabled || !config.rules.some((r) => r.scope === "*" || r.scope === model)) return false;
|
|
119
|
-
const snapshot = await aiQuotas.snapshot(subject, model);
|
|
130
|
+
const snapshot = await aiQuotas.snapshot(subject, model, undefined, undefined, { state, config });
|
|
120
131
|
for (const b of snapshot.balances) {
|
|
121
132
|
if (b.bypassed || b.limit === null) continue;
|
|
122
133
|
if (b.unknown)
|
|
@@ -26,6 +26,8 @@ import type { AiConversationService, AiRecentResource } from "./types";
|
|
|
26
26
|
export const AiRecentResourceSchema: z.ZodType<AiRecentResource> = z
|
|
27
27
|
.object({
|
|
28
28
|
ref: CloudResourceRefSchema,
|
|
29
|
+
context: LinkedCloudResourceReferenceSchema.shape.context,
|
|
30
|
+
status: LinkedCloudResourceReferenceSchema.shape.status,
|
|
29
31
|
title: LinkedCloudResourceReferenceSchema.shape.title.unwrap(),
|
|
30
32
|
icon: LinkedCloudResourceReferenceSchema.shape.icon,
|
|
31
33
|
href: LinkedCloudResourceReferenceSchema.shape.href,
|
|
@@ -183,6 +185,8 @@ export const listAiRecentResources = async (input: {
|
|
|
183
185
|
ref,
|
|
184
186
|
count,
|
|
185
187
|
title: data.title,
|
|
188
|
+
context: ownRef?.context,
|
|
189
|
+
status: ownRef?.status,
|
|
186
190
|
icon: ownRef?.icon ?? data.icon,
|
|
187
191
|
href,
|
|
188
192
|
preview: recentResourcePreview(ownRef?.preview ?? data.description),
|
|
@@ -229,6 +233,8 @@ export const listAiRecentResources = async (input: {
|
|
|
229
233
|
ref,
|
|
230
234
|
count,
|
|
231
235
|
title: ownRef.title,
|
|
236
|
+
context: ownRef.context,
|
|
237
|
+
status: ownRef.status,
|
|
232
238
|
icon: (ownRef.icon ?? type.icon ?? app.appIcon)?.slice(0, 120),
|
|
233
239
|
href: ownRef.href,
|
|
234
240
|
kind: type.title.slice(0, 120),
|
|
@@ -1,117 +1,44 @@
|
|
|
1
1
|
import { sql } from "bun";
|
|
2
|
-
import { toPgTextArray } from "../services/postgres";
|
|
3
2
|
import { decryptValue, encryptValue } from "../services/settings/crypto";
|
|
4
|
-
import {
|
|
5
|
-
AI_REQUEST_HEADERS_PROVIDER_ERROR,
|
|
6
|
-
AiRequestHeadersSchema,
|
|
7
|
-
patchAiRequestHeaders,
|
|
8
|
-
providerSupportsRequestHeaders,
|
|
9
|
-
} from "../shared/ai-request-options";
|
|
10
|
-
import type { AiModelProfile } from "./types";
|
|
3
|
+
import { AiRequestHeadersSchema, patchAiRequestHeaders } from "../shared/ai-request-options";
|
|
11
4
|
|
|
12
5
|
type SqlClient = typeof sql;
|
|
13
|
-
export type AiRequestHeaderPatch = { profileId: string; patch: unknown };
|
|
14
6
|
|
|
15
|
-
const decryptHeaders = async (
|
|
7
|
+
const decryptHeaders = async (providerId: string, secret: string): Promise<Record<string, string>> => {
|
|
16
8
|
try {
|
|
17
9
|
const parsed = AiRequestHeadersSchema.safeParse(await decryptValue(secret));
|
|
18
10
|
if (parsed.success && Object.values(parsed.data).every((value) => typeof value === "string"))
|
|
19
11
|
return patchAiRequestHeaders({}, parsed.data);
|
|
20
12
|
} catch {}
|
|
21
|
-
console.warn(`[ai] ignoring unreadable request headers for
|
|
13
|
+
console.warn(`[ai] ignoring unreadable request headers for provider ${JSON.stringify(providerId)}`);
|
|
22
14
|
return {};
|
|
23
15
|
};
|
|
24
16
|
|
|
25
17
|
/** Secrets stay server-side, in a separate encrypted row like provider API keys. */
|
|
26
|
-
export const getAiRequestHeaders = async (
|
|
27
|
-
const [row] = await db<{ secret: string }[]>`SELECT secret FROM ai.
|
|
28
|
-
return row ? decryptHeaders(
|
|
18
|
+
export const getAiRequestHeaders = async (providerId: string, db: SqlClient = sql): Promise<Record<string, string>> => {
|
|
19
|
+
const [row] = await db<{ secret: string }[]>`SELECT secret FROM ai.provider_request_headers WHERE provider_id = ${providerId}`;
|
|
20
|
+
return row ? decryptHeaders(providerId, row.secret) : {};
|
|
29
21
|
};
|
|
30
22
|
|
|
31
|
-
export const setAiRequestHeaders = async (
|
|
23
|
+
export const setAiRequestHeaders = async (providerId: string, headers: Record<string, string>, db: SqlClient = sql): Promise<void> => {
|
|
32
24
|
const normalized = patchAiRequestHeaders({}, headers);
|
|
33
25
|
if (!Object.keys(normalized).length) {
|
|
34
|
-
await db`DELETE FROM ai.
|
|
26
|
+
await db`DELETE FROM ai.provider_request_headers WHERE provider_id = ${providerId}`;
|
|
35
27
|
return;
|
|
36
28
|
}
|
|
37
29
|
const encrypted = await encryptValue(normalized);
|
|
38
|
-
await db`INSERT INTO ai.
|
|
39
|
-
ON CONFLICT (
|
|
30
|
+
await db`INSERT INTO ai.provider_request_headers (provider_id, secret) VALUES (${providerId}, ${encrypted})
|
|
31
|
+
ON CONFLICT (provider_id) DO UPDATE SET secret = EXCLUDED.secret, updated_at = now()`;
|
|
40
32
|
};
|
|
41
33
|
|
|
42
|
-
/** Admin reads contain names only. Never serialize header values into
|
|
34
|
+
/** Admin reads contain names only. Never serialize header values into providers. */
|
|
43
35
|
export const listAiRequestHeaderNames = async (db: SqlClient = sql): Promise<Record<string, string[]>> => {
|
|
44
|
-
const rows = await db<{
|
|
36
|
+
const rows = await db<{ provider_id: string; secret: string }[]>`SELECT provider_id, secret FROM ai.provider_request_headers`;
|
|
45
37
|
const names: Record<string, string[]> = {};
|
|
46
38
|
for (const row of rows)
|
|
47
|
-
Object.defineProperty(names, row.
|
|
48
|
-
value: Object.keys(await decryptHeaders(row.
|
|
39
|
+
Object.defineProperty(names, row.provider_id, {
|
|
40
|
+
value: Object.keys(await decryptHeaders(row.provider_id, row.secret)).sort(),
|
|
49
41
|
enumerable: true,
|
|
50
42
|
});
|
|
51
43
|
return names;
|
|
52
44
|
};
|
|
53
|
-
|
|
54
|
-
export const pruneAiRequestHeaders = async (keepProfileIds: readonly string[], db: SqlClient = sql): Promise<void> => {
|
|
55
|
-
if (!keepProfileIds.length) await db`DELETE FROM ai.model_request_headers`;
|
|
56
|
-
else await db`DELETE FROM ai.model_request_headers WHERE profile_id <> ALL(${toPgTextArray([...keepProfileIds])}::text[])`;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/** Follow credentials: changing provider discards stored secrets; omission preserves them on the same provider. */
|
|
60
|
-
export const planAiProfileRequestHeaders = (input: {
|
|
61
|
-
currentProfiles: readonly AiModelProfile[];
|
|
62
|
-
nextProfiles: readonly AiModelProfile[];
|
|
63
|
-
existingNames: Record<string, string[]>;
|
|
64
|
-
submitted: readonly AiRequestHeaderPatch[];
|
|
65
|
-
}): { keepHeaderProfileIds: string[]; patches: { profileId: string; patch: Record<string, string | null> }[]; error?: string } => {
|
|
66
|
-
const current = new Map(input.currentProfiles.map((profile) => [profile.id, profile]));
|
|
67
|
-
const keepHeaderProfileIds = input.nextProfiles
|
|
68
|
-
.filter(
|
|
69
|
-
(profile) =>
|
|
70
|
-
providerSupportsRequestHeaders(profile.provider) &&
|
|
71
|
-
current.get(profile.id)?.provider === profile.provider &&
|
|
72
|
-
!profile.capabilities.includes("transcription"),
|
|
73
|
-
)
|
|
74
|
-
.map((profile) => profile.id);
|
|
75
|
-
const patches: { profileId: string; patch: Record<string, string | null> }[] = [];
|
|
76
|
-
for (const submitted of input.submitted) {
|
|
77
|
-
const profile = input.nextProfiles.find((profile) => profile.id === submitted.profileId);
|
|
78
|
-
const parsed = AiRequestHeadersSchema.safeParse(submitted.patch);
|
|
79
|
-
if (!parsed.success)
|
|
80
|
-
return { keepHeaderProfileIds: [], patches: [], error: `requestHeaders: ${zodHeaderMessage(parsed.error.issues)}` };
|
|
81
|
-
if (!profile || !providerSupportsRequestHeaders(profile.provider))
|
|
82
|
-
return { keepHeaderProfileIds: [], patches: [], error: AI_REQUEST_HEADERS_PROVIDER_ERROR };
|
|
83
|
-
if (profile.capabilities.includes("transcription"))
|
|
84
|
-
return {
|
|
85
|
-
keepHeaderProfileIds: [],
|
|
86
|
-
patches: [],
|
|
87
|
-
error: "requestHeaders: Request settings are not supported on transcription profiles.",
|
|
88
|
-
};
|
|
89
|
-
const oldNames =
|
|
90
|
-
keepHeaderProfileIds.includes(profile.id) && Object.hasOwn(input.existingNames, profile.id) ? input.existingNames[profile.id]! : [];
|
|
91
|
-
try {
|
|
92
|
-
patchAiRequestHeaders(Object.fromEntries(oldNames.map((name) => [name, ""])), parsed.data);
|
|
93
|
-
} catch {
|
|
94
|
-
return {
|
|
95
|
-
keepHeaderProfileIds: [],
|
|
96
|
-
patches: [],
|
|
97
|
-
error: "requestHeaders: At most 32 extra headers are allowed after applying the patch.",
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
patches.push({ profileId: profile.id, patch: parsed.data });
|
|
101
|
-
}
|
|
102
|
-
return { keepHeaderProfileIds, patches };
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const zodHeaderMessage = (issues: readonly { path: readonly PropertyKey[]; message: string }[]) =>
|
|
106
|
-
issues.map((issue) => `${issue.path.map(String).join(".") || "headers"}: ${issue.message}`).join("; ");
|
|
107
|
-
|
|
108
|
-
/** Apply after pruning so a provider change cannot inherit old secrets. Call inside the settings transaction. */
|
|
109
|
-
export const storeAiRequestHeaderPlan = async (
|
|
110
|
-
plan: ReturnType<typeof planAiProfileRequestHeaders>,
|
|
111
|
-
db: SqlClient = sql,
|
|
112
|
-
): Promise<void> => {
|
|
113
|
-
if (plan.error) throw new Error(plan.error);
|
|
114
|
-
await pruneAiRequestHeaders(plan.keepHeaderProfileIds, db);
|
|
115
|
-
for (const { profileId, patch } of plan.patches)
|
|
116
|
-
await setAiRequestHeaders(profileId, patchAiRequestHeaders(await getAiRequestHeaders(profileId, db), patch), db);
|
|
117
|
-
};
|
package/src/ai/resource-refs.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { AiConversationResourceObservation } from "./types";
|
|
|
13
13
|
|
|
14
14
|
const LinkedResourceReferenceSchema = CloudResourceReferenceSchema.extend({
|
|
15
15
|
href: CloudResourceHrefSchema.optional(),
|
|
16
|
-
});
|
|
16
|
+
}).strip();
|
|
17
17
|
|
|
18
18
|
export const addCloudResourceHrefs = <T>(
|
|
19
19
|
value: T,
|
|
@@ -67,12 +67,14 @@ export const collectConversationResourceObservations = (...values: unknown[]): A
|
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const view = CloudResourceViewSchema.safeParse(value);
|
|
70
|
+
const view = CloudResourceViewSchema.strip().safeParse(value);
|
|
71
71
|
if (view.success) {
|
|
72
72
|
const resource: CloudResourceView = view.data;
|
|
73
73
|
observations.set(observationKey(resource.ref), {
|
|
74
74
|
ref: resource.ref,
|
|
75
75
|
title: resource.title,
|
|
76
|
+
context: resource.context,
|
|
77
|
+
status: resource.status,
|
|
76
78
|
preview: resource.preview,
|
|
77
79
|
icon: resource.icon,
|
|
78
80
|
href: resource.links.find((link) => link.rel === "open")?.href,
|
|
@@ -87,6 +89,8 @@ export const collectConversationResourceObservations = (...values: unknown[]): A
|
|
|
87
89
|
...previous,
|
|
88
90
|
ref,
|
|
89
91
|
...(resource.title !== undefined ? { title: resource.title } : {}),
|
|
92
|
+
...(resource.context !== undefined ? { context: resource.context } : {}),
|
|
93
|
+
...(resource.status !== undefined ? { status: resource.status } : {}),
|
|
90
94
|
...(resource.preview !== undefined ? { preview: resource.preview } : {}),
|
|
91
95
|
...(resource.icon !== undefined ? { icon: resource.icon } : {}),
|
|
92
96
|
...(resource.href !== undefined ? { href: resource.href } : {}),
|
package/src/ai/routes.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { coreSettings } from "../services/settings/api";
|
|
|
37
37
|
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
38
38
|
import { readThemeFromCookieHeader } from "../shared/theme";
|
|
39
39
|
import { type AiStoredActionReview, isLegacyActionReview, isUnknownActionOutcome, storedActionReview } from "./action-review";
|
|
40
|
-
import { assistantAiSettingsState,
|
|
40
|
+
import { assistantAiSettingsState, peopleAiModels, resolveAssistantAudioModel, selectAssistantAiModel } from "./assistant-models";
|
|
41
41
|
import { AI_AUDIO_MAX_BYTES } from "./audio-format";
|
|
42
42
|
import { buildAiCapabilityCatalog } from "./capabilities";
|
|
43
43
|
import { checkAiCapability, resolveAiCapabilityActor } from "./capability-execution";
|
|
@@ -254,10 +254,10 @@ const publicMemory = async (userId: string, memory: Awaited<ReturnType<typeof ai
|
|
|
254
254
|
(await publicMemories(userId, [memory]))[0]!;
|
|
255
255
|
|
|
256
256
|
/** Fire-and-forget: remember the model this user actually ran a turn with (preselected for new chats). */
|
|
257
|
-
const rememberLastUsedModel = (actor: RequestActor,
|
|
257
|
+
const rememberLastUsedModel = (actor: RequestActor, model: import("../shared/ai-models").AiModelRequest | undefined): void => {
|
|
258
258
|
const userId = aiPrefsUserId(actor);
|
|
259
|
-
if (!userId || !
|
|
260
|
-
void aiUserPrefs.update(userId, {
|
|
259
|
+
if (!userId || !model) return;
|
|
260
|
+
void aiUserPrefs.update(userId, { selection: model }).catch(() => undefined);
|
|
261
261
|
};
|
|
262
262
|
|
|
263
263
|
const conversationDetail = async (conversation: AiConversation, subject: AccessSubject) => {
|
|
@@ -348,7 +348,7 @@ export const aiRoutes = (() => {
|
|
|
348
348
|
return c.json(await getAiChatQuotas(c.get("accessSubject")));
|
|
349
349
|
})
|
|
350
350
|
.get("/models", async (c) => {
|
|
351
|
-
return respond(c, ok(await
|
|
351
|
+
return respond(c, ok(await peopleAiModels(c.get("accessSubject"))));
|
|
352
352
|
})
|
|
353
353
|
.get("/prefs", async (c) => {
|
|
354
354
|
const ctx = await resolveContext(c);
|
|
@@ -438,12 +438,11 @@ export const aiRoutes = (() => {
|
|
|
438
438
|
const state = await readAiSettingsState();
|
|
439
439
|
let previewProfile: ReturnType<typeof selectAiModelProfile> | null = null;
|
|
440
440
|
if (state.ok && state.enabled) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (preferredId) previewProfile = selectAiModelProfile(state, ctx.modelPolicy, preferredId);
|
|
441
|
+
try {
|
|
442
|
+
previewProfile = selectAiModelProfile(state, ctx.modelPolicy, prefs.selection ?? undefined);
|
|
443
|
+
} catch {
|
|
444
|
+
/* The prompt preview remains available without a usable model. */
|
|
445
|
+
}
|
|
447
446
|
}
|
|
448
447
|
const toolsSupported = Boolean(previewProfile?.capabilities.includes("tools"));
|
|
449
448
|
const availableSkills = toolsSupported
|
|
@@ -1022,7 +1021,7 @@ export const aiRoutes = (() => {
|
|
|
1022
1021
|
locale: getLocale(c),
|
|
1023
1022
|
theme: readThemeFromCookieHeader(c.req.header("cookie")),
|
|
1024
1023
|
timeZone: getTimeZone(c),
|
|
1025
|
-
|
|
1024
|
+
model: body.model ?? project?.defaultModel ?? undefined,
|
|
1026
1025
|
modelPolicy: ctx.modelPolicy,
|
|
1027
1026
|
project: project ?? undefined,
|
|
1028
1027
|
clientToolIds: body.clientToolIds,
|
|
@@ -1043,7 +1042,7 @@ export const aiRoutes = (() => {
|
|
|
1043
1042
|
return respond(c, ok({ queued: true }));
|
|
1044
1043
|
}
|
|
1045
1044
|
const result = await submitAiChatTurn(submission);
|
|
1046
|
-
rememberLastUsedModel(ctx.actor,
|
|
1045
|
+
rememberLastUsedModel(ctx.actor, body.model);
|
|
1047
1046
|
return respond(
|
|
1048
1047
|
c,
|
|
1049
1048
|
ok({
|
|
@@ -1115,9 +1114,9 @@ export const aiRoutes = (() => {
|
|
|
1115
1114
|
if (originalProject && !currentProject) return respond(c, fail(err.notFound("Project")));
|
|
1116
1115
|
|
|
1117
1116
|
try {
|
|
1118
|
-
const
|
|
1117
|
+
const model = await selectAssistantAiModel(
|
|
1119
1118
|
c.get("accessSubject"),
|
|
1120
|
-
body.
|
|
1119
|
+
body.model ?? originalProject?.defaultModel ?? undefined,
|
|
1121
1120
|
ctx.modelPolicy,
|
|
1122
1121
|
);
|
|
1123
1122
|
if ((await prepareConversationForMessageRetry(conversation.id)) === "busy") {
|
|
@@ -1134,7 +1133,7 @@ export const aiRoutes = (() => {
|
|
|
1134
1133
|
locale: getLocale(c),
|
|
1135
1134
|
theme: readThemeFromCookieHeader(c.req.header("cookie")),
|
|
1136
1135
|
timeZone: getTimeZone(c),
|
|
1137
|
-
|
|
1136
|
+
model,
|
|
1138
1137
|
modelPolicy: ctx.modelPolicy,
|
|
1139
1138
|
systemPrompt,
|
|
1140
1139
|
project: originalProject,
|
|
@@ -1158,7 +1157,7 @@ export const aiRoutes = (() => {
|
|
|
1158
1157
|
}
|
|
1159
1158
|
: undefined,
|
|
1160
1159
|
});
|
|
1161
|
-
rememberLastUsedModel(ctx.actor,
|
|
1160
|
+
rememberLastUsedModel(ctx.actor, body.model);
|
|
1162
1161
|
return respond(
|
|
1163
1162
|
c,
|
|
1164
1163
|
ok({
|
|
@@ -1230,7 +1229,7 @@ export const aiRoutes = (() => {
|
|
|
1230
1229
|
const result = await submitAiCompaction({
|
|
1231
1230
|
conversationId: conversation.id,
|
|
1232
1231
|
actor: ctx.actor,
|
|
1233
|
-
|
|
1232
|
+
model: body.model,
|
|
1234
1233
|
modelPolicy: ctx.modelPolicy,
|
|
1235
1234
|
});
|
|
1236
1235
|
return respond(c, ok({ turn: publicTurn(result.turn, conversation.shortId) }), 201);
|
package/src/ai/runtime.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type ApprovalDecisionChannel, approvals } from "../services/approvals";
|
|
|
8
8
|
import { logger } from "../services/logging";
|
|
9
9
|
import { superviseRuntimeTask } from "../services/runtime-lifecycle";
|
|
10
10
|
import { coreSettings } from "../services/settings/api";
|
|
11
|
+
import type { AiModelRequest } from "../shared/ai-models";
|
|
11
12
|
import {
|
|
12
13
|
AI_WEBSITE_APPROVAL_TOOL,
|
|
13
14
|
type AiToolApprovalChangeContext,
|
|
@@ -15,16 +16,17 @@ import {
|
|
|
15
16
|
aiTurnAllowsRememberedApprovals,
|
|
16
17
|
rememberAiToolApproval,
|
|
17
18
|
} from "./approvals";
|
|
18
|
-
import { aiChatAccessSubject,
|
|
19
|
+
import { aiChatAccessSubject, selectAssistantAiModel } from "./assistant-models";
|
|
19
20
|
import { startAiDictationRuntime } from "./dictation-runtime";
|
|
20
21
|
import { AiTurnExecutor, type ExecutorConfig } from "./executor";
|
|
21
22
|
import { canonicalizeAiConversationAttachments, snapshotAiConversationFiles } from "./file-context";
|
|
22
23
|
import type { AiLocalClient, AiLocalContext } from "./local-tool-contracts";
|
|
23
24
|
import { drainQueuedMessages } from "./message-queue";
|
|
25
|
+
import { aiUserPrefs } from "./prefs";
|
|
24
26
|
import { AI_TURN_LEASE_MS } from "./protocol";
|
|
25
27
|
import { aiQuotas } from "./quotas";
|
|
26
28
|
import { parseAiResourceMarker } from "./resource-markers";
|
|
27
|
-
import { isAiVisionModelConfigured } from "./settings";
|
|
29
|
+
import { isAiVisionModelConfigured, readAiSettingsState } from "./settings";
|
|
28
30
|
import { aiConversations } from "./store";
|
|
29
31
|
import { publishAiWireEvent } from "./stream";
|
|
30
32
|
import { aiToolAudit } from "./tool-audit";
|
|
@@ -110,7 +112,7 @@ export type SubmitAiChatTurnInput = {
|
|
|
110
112
|
theme?: "light" | "dark";
|
|
111
113
|
timeZone?: string;
|
|
112
114
|
modelPolicy?: AiModelPolicy;
|
|
113
|
-
|
|
115
|
+
model?: AiModelRequest;
|
|
114
116
|
/** Optional instructions that apply only to this turn. */
|
|
115
117
|
systemPrompt?: string;
|
|
116
118
|
project?: AiChatTurnRunConfig["project"];
|
|
@@ -143,21 +145,43 @@ export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
|
|
|
143
145
|
);
|
|
144
146
|
const canonicalInput = files?.attached.length ? inputMessage.content : input.input;
|
|
145
147
|
const hasImageAttachments = Boolean(files?.attached.some((file) => isAiImageMediaType(file.mediaType)));
|
|
148
|
+
const state = await readAiSettingsState();
|
|
146
149
|
const canInspectAttachedImages =
|
|
147
150
|
input.canInspectAttachedImages ??
|
|
148
|
-
(input.toolSource?.kind === "default" && (await isAiVisionModelConfigured(input.modelPolicy?.allowedDataBoundaries)));
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
input:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
(input.toolSource?.kind === "default" && (await isAiVisionModelConfigured(input.modelPolicy?.allowedDataBoundaries, state)));
|
|
152
|
+
let requested = input.model;
|
|
153
|
+
let previousLabel: string | undefined;
|
|
154
|
+
if (input.assistantChat) {
|
|
155
|
+
const latest = await aiConversations.getLatestTurn({ conversationId: input.conversationId, kind: "chat" });
|
|
156
|
+
const previous = latest ? await aiConversations.getTurnRunConfig({ conversationId: input.conversationId, turnId: latest.id }) : null;
|
|
157
|
+
const inherited = previous?.kind === "chat" ? previous.model : undefined;
|
|
158
|
+
requested ??= inherited;
|
|
159
|
+
if (inherited && "id" in inherited && requested && "id" in requested && requested.id === inherited.id) {
|
|
160
|
+
if (!state.profiles.some((profile) => profile.id === inherited.id)) {
|
|
161
|
+
previousLabel = previous?.kind === "chat" ? (previous.modelLabel ?? inherited.id) : inherited.id;
|
|
162
|
+
const subject = aiChatAccessSubject(input.actor);
|
|
163
|
+
const prefs = subject?.type === "user" ? await aiUserPrefs.get(subject.userId) : null;
|
|
164
|
+
requested = prefs?.selection && "class" in prefs.selection ? prefs.selection : { class: "balanced" };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const model = input.assistantChat
|
|
169
|
+
? await selectAssistantAiModel(aiChatAccessSubject(input.actor), requested, input.modelPolicy, state)
|
|
170
|
+
: requested;
|
|
171
|
+
const { resolved } = await validateAiTurnRequest(
|
|
172
|
+
{
|
|
173
|
+
input: canonicalInput,
|
|
174
|
+
hasImageAttachments,
|
|
175
|
+
canInspectAttachedImages,
|
|
176
|
+
modelPolicy: input.modelPolicy,
|
|
177
|
+
model,
|
|
178
|
+
},
|
|
179
|
+
state,
|
|
180
|
+
);
|
|
159
181
|
const quotaSubject = aiChatAccessSubject(input.actor);
|
|
160
|
-
if (input.assistantChat && quotaSubject) await aiQuotas.assertAllowed(quotaSubject, resolved.profile.id);
|
|
182
|
+
if (input.assistantChat && quotaSubject) await aiQuotas.assertAllowed(quotaSubject, resolved.profile.id, state);
|
|
183
|
+
const modelLabel =
|
|
184
|
+
resolved.profile.label + (resolved.profile.id !== resolved.profile.modelId ? ` · ${resolved.profile.variantLabel}` : "");
|
|
161
185
|
const runConfig: AiChatTurnRunConfig = {
|
|
162
186
|
kind: "chat",
|
|
163
187
|
selectedSkillIds: [
|
|
@@ -177,7 +201,10 @@ export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
|
|
|
177
201
|
...(input.theme ? { theme: input.theme } : {}),
|
|
178
202
|
...(input.timeZone ? { timeZone: input.timeZone } : {}),
|
|
179
203
|
modelPolicy: input.modelPolicy,
|
|
180
|
-
|
|
204
|
+
model,
|
|
205
|
+
modelResolved: resolved.model,
|
|
206
|
+
modelLabel,
|
|
207
|
+
...(previousLabel ? { modelReplacement: { kind: "model_replaced", previousLabel, nextLabel: modelLabel } } : {}),
|
|
181
208
|
systemPrompt: input.systemPrompt,
|
|
182
209
|
project: input.project,
|
|
183
210
|
files,
|
|
@@ -191,7 +218,7 @@ export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
|
|
|
191
218
|
|
|
192
219
|
return {
|
|
193
220
|
conversationId: input.conversationId,
|
|
194
|
-
|
|
221
|
+
modelId: resolved.profile.id,
|
|
195
222
|
runConfig,
|
|
196
223
|
userMessage,
|
|
197
224
|
truncateFromSeq: input.truncateFromSeq,
|
|
@@ -244,7 +271,7 @@ export const deliverAiInterChatMessage = async (input: {
|
|
|
244
271
|
};
|
|
245
272
|
const delivered = await aiConversations.deliverInterChatMessage({
|
|
246
273
|
messageId: input.message.id,
|
|
247
|
-
|
|
274
|
+
modelId: resolved.profile.id,
|
|
248
275
|
runConfig,
|
|
249
276
|
userMessage: { role: "user", content: [{ type: "text", text }] },
|
|
250
277
|
sourceHref: input.sourceHref,
|
|
@@ -257,20 +284,21 @@ export type SubmitAiCompactionInput = {
|
|
|
257
284
|
conversationId: string;
|
|
258
285
|
actor?: RequestActor;
|
|
259
286
|
modelPolicy?: AiModelPolicy;
|
|
260
|
-
|
|
287
|
+
model?: AiModelRequest;
|
|
261
288
|
};
|
|
262
289
|
|
|
263
290
|
export const submitAiCompaction = async (input: SubmitAiCompactionInput): Promise<{ turn: AiTurn }> => {
|
|
264
|
-
const { resolved } = await validateAiTurnRequest({ input: "", modelPolicy: input.modelPolicy,
|
|
291
|
+
const { resolved } = await validateAiTurnRequest({ input: "", modelPolicy: input.modelPolicy, model: input.model });
|
|
265
292
|
const runConfig: AiCompactionTurnRunConfig = {
|
|
266
293
|
kind: "compact",
|
|
267
294
|
actor: input.actor,
|
|
268
295
|
modelPolicy: input.modelPolicy,
|
|
269
|
-
|
|
296
|
+
model: { id: resolved.profile.id },
|
|
297
|
+
modelResolved: resolved.model,
|
|
270
298
|
};
|
|
271
299
|
const turn = await aiConversations.createCompactionTurn({
|
|
272
300
|
conversationId: input.conversationId,
|
|
273
|
-
|
|
301
|
+
modelId: resolved.profile.id,
|
|
274
302
|
runConfig,
|
|
275
303
|
});
|
|
276
304
|
await enqueueAiTurn({ conversationId: input.conversationId, turnId: turn.id });
|