@k2b/cloud 0.43.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 +7 -2
- 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 +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- 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/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/PermissionEditor.tsx +9 -3
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- 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/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/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 +31 -22
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +5 -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.ts +26 -9
- 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 +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- 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 +21 -44
- 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 +27 -22
- 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 +16 -0
- 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/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 +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- 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/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- 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 +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -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 +1 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +6 -0
- package/src/services/accounts/entities.ts +14 -3
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +62 -10
- package/src/services/accounts/users.ts +7 -6
- package/src/services/app-access.ts +25 -7
- 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/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -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/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +71 -43
- 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 +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +8 -6
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +25 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +2 -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/settings.ts
CHANGED
|
@@ -1,627 +1,140 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { coreSettings } from "../services";
|
|
3
|
-
import { stripImageDataUrlMetadata } from "../services/image-metadata";
|
|
4
|
-
import { AiModelPricingSchema } from "../shared/ai-costs";
|
|
5
2
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "../shared/ai-
|
|
12
|
-
import {
|
|
3
|
+
type AiModelRequest,
|
|
4
|
+
type AiResolvedModelRef,
|
|
5
|
+
aiModelClassFallbackChain,
|
|
6
|
+
providerRequiresCredential,
|
|
7
|
+
providerSupportsCredential,
|
|
8
|
+
} from "../shared/ai-models";
|
|
9
|
+
import { providerSupportsRequestHeaders } from "../shared/ai-request-options";
|
|
10
|
+
import { getAiCredential, listAiCredentialProviderIds } from "./credentials";
|
|
13
11
|
import { AI_FIRECRAWL_API_KEY_SETTING_KEY } from "./firecrawl-tools";
|
|
12
|
+
import { aiModelCatalog, readAiModelProfiles } from "./model-catalog";
|
|
14
13
|
import { createAiProvider } from "./provider";
|
|
15
14
|
import { getAiRequestHeaders } from "./request-headers";
|
|
16
|
-
import {
|
|
17
|
-
AI_DATA_BOUNDARIES,
|
|
18
|
-
AI_MODEL_CAPABILITIES,
|
|
19
|
-
type AiDataBoundary,
|
|
20
|
-
type AiModelCapability,
|
|
21
|
-
type AiModelPolicy,
|
|
22
|
-
type AiModelProfile,
|
|
23
|
-
type AiProviderId,
|
|
24
|
-
type AiPublicModelProfile,
|
|
25
|
-
type AiResolvedModel,
|
|
26
|
-
type AiSettingsError,
|
|
27
|
-
type AiSettingsState,
|
|
28
|
-
} from "./types";
|
|
29
|
-
|
|
30
|
-
const PROVIDERS = ["openai", "openrouter", "anthropic", "mistral", "gemini", "ollama", "vllm", "openai-compatible"] as const;
|
|
31
|
-
const LEGACY_DATA_BOUNDARIES = ["local", "internal"] as const;
|
|
32
|
-
const DATA_BOUNDARY_INPUTS = [...AI_DATA_BOUNDARIES, ...LEGACY_DATA_BOUNDARIES] as const;
|
|
33
|
-
const HttpUrlSchema = z
|
|
34
|
-
.string()
|
|
35
|
-
.trim()
|
|
36
|
-
.url()
|
|
37
|
-
.refine((value) => {
|
|
38
|
-
const protocol = new URL(value).protocol;
|
|
39
|
-
return protocol === "http:" || protocol === "https:";
|
|
40
|
-
}, "Provider endpoints must use HTTP or HTTPS.");
|
|
41
|
-
|
|
42
|
-
export const providerRequiresCredential = (provider: AiProviderId): boolean =>
|
|
43
|
-
provider === "openai" || provider === "openrouter" || provider === "anthropic" || provider === "mistral" || provider === "gemini";
|
|
44
|
-
|
|
45
|
-
export const providerSupportsCredential = (provider: AiProviderId): boolean =>
|
|
46
|
-
providerRequiresCredential(provider) || provider === "vllm" || provider === "openai-compatible";
|
|
47
|
-
|
|
48
|
-
const defaultDataBoundary = (provider: AiProviderId): AiDataBoundary =>
|
|
49
|
-
provider === "ollama" || provider === "vllm" || provider === "openai-compatible" ? "private" : "hosted";
|
|
50
|
-
|
|
51
|
-
const normalizeDataBoundary = (boundary: (typeof DATA_BOUNDARY_INPUTS)[number] | undefined, provider: AiProviderId): AiDataBoundary => {
|
|
52
|
-
if (boundary === "hosted") return "hosted";
|
|
53
|
-
if (boundary === "private" || boundary === "local" || boundary === "internal") return "private";
|
|
54
|
-
return defaultDataBoundary(provider);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const isModelCapability = (value: string): value is AiModelCapability => AI_MODEL_CAPABILITIES.some((capability) => capability === value);
|
|
58
|
-
|
|
59
|
-
// Keep in sync with the ai.max_tool_result_chars default in core-settings.ts.
|
|
60
|
-
// This is an operator ceiling. The runtime derives the actual per-result
|
|
61
|
-
// budget from the selected model's context window.
|
|
62
|
-
const DEFAULT_MAX_TOOL_RESULT_CHARS = 2_000_000;
|
|
63
|
-
|
|
64
|
-
const normalizeMaxToolResultChars = (value: unknown): number => {
|
|
65
|
-
const numeric = Number(value);
|
|
66
|
-
if (!Number.isFinite(numeric) || numeric <= 0) return DEFAULT_MAX_TOOL_RESULT_CHARS;
|
|
67
|
-
return Math.floor(numeric);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const normalizeCapabilities = (values: string[] | undefined): AiModelCapability[] => {
|
|
71
|
-
if (!values) return ["streaming"];
|
|
72
|
-
const capabilities = values.filter(isModelCapability);
|
|
73
|
-
return [...new Set(capabilities)];
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const ModelProfileSchema = z
|
|
77
|
-
.object({
|
|
78
|
-
id: z
|
|
79
|
-
.string()
|
|
80
|
-
.trim()
|
|
81
|
-
.min(1)
|
|
82
|
-
.regex(/^[a-z0-9][a-z0-9._-]*$/),
|
|
83
|
-
label: z.string().trim().min(1),
|
|
84
|
-
provider: z.enum(PROVIDERS),
|
|
85
|
-
model: z.string().trim().min(1),
|
|
86
|
-
enabled: z.boolean().default(true),
|
|
87
|
-
// Small logo shown in the admin card and the composer model picker. Hard cap:
|
|
88
|
-
// it ships to every user via the public model list (64x64 webp ≈ a few KB).
|
|
89
|
-
image: z
|
|
90
|
-
.string()
|
|
91
|
-
.trim()
|
|
92
|
-
.regex(/^data:image\//)
|
|
93
|
-
.max(28_000)
|
|
94
|
-
.optional(),
|
|
95
|
-
// Legacy/advanced metadata is tolerated but no longer used for model selection.
|
|
96
|
-
tags: z.array(z.string()).optional(),
|
|
97
|
-
capabilities: z.array(z.string()).optional(),
|
|
98
|
-
dataBoundary: z.enum(DATA_BOUNDARY_INPUTS).optional(),
|
|
99
|
-
// Legacy name accepted for stored profiles created before dataBoundary.
|
|
100
|
-
dataPolicy: z.enum(DATA_BOUNDARY_INPUTS).optional(),
|
|
101
|
-
baseURL: HttpUrlSchema.optional(),
|
|
102
|
-
contextWindow: z.number().int().positive().optional(),
|
|
103
|
-
temperature: z.number().min(0).max(2).optional(),
|
|
104
|
-
maxOutputTokens: z.number().int().positive().optional(),
|
|
105
|
-
reasoningEffort: AiReasoningEffortSchema,
|
|
106
|
-
extraBody: AiExtraBodySchema.optional(),
|
|
107
|
-
requestHeaders: AiRequestHeadersSchema.optional(),
|
|
108
|
-
maxLoadedTools: z.number().int().optional(),
|
|
109
|
-
maxToolRounds: z.number().int().optional(),
|
|
110
|
-
pricing: AiModelPricingSchema.optional(),
|
|
111
|
-
})
|
|
112
|
-
.superRefine((profile, ctx) => {
|
|
113
|
-
if (profile.requestHeaders !== undefined && !providerSupportsRequestHeaders(profile.provider))
|
|
114
|
-
ctx.addIssue({ code: "custom", path: ["requestHeaders"], message: AI_REQUEST_HEADERS_PROVIDER_ERROR });
|
|
115
|
-
if (profile.capabilities?.includes("transcription")) {
|
|
116
|
-
for (const key of ["reasoningEffort", "extraBody", "requestHeaders"] as const)
|
|
117
|
-
if (profile[key] !== undefined)
|
|
118
|
-
ctx.addIssue({ code: "custom", path: [key], message: "Request settings are not supported on transcription profiles." });
|
|
119
|
-
if (profile.pricing) ctx.addIssue({ code: "custom", path: ["pricing"], message: "Audio pricing is not supported." });
|
|
120
|
-
if (profile.capabilities.some((capability) => capability !== "transcription")) {
|
|
121
|
-
ctx.addIssue({ code: "custom", path: ["capabilities"], message: "Audio transcription cannot be combined with chat capabilities." });
|
|
122
|
-
}
|
|
123
|
-
if (profile.provider !== "openai" && profile.provider !== "openai-compatible") {
|
|
124
|
-
ctx.addIssue({
|
|
125
|
-
code: "custom",
|
|
126
|
-
path: ["provider"],
|
|
127
|
-
message: "Audio transcription requires OpenAI or an OpenAI-compatible endpoint.",
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (profile.provider === "openai-compatible" && !profile.baseURL) {
|
|
132
|
-
ctx.addIssue({ code: "custom", path: ["baseURL"], message: "OpenAI-compatible profiles require a base URL." });
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const profileToPublic = (profile: AiModelProfile): AiPublicModelProfile => ({
|
|
137
|
-
id: profile.id,
|
|
138
|
-
label: profile.label,
|
|
139
|
-
provider: profile.provider,
|
|
140
|
-
model: profile.model,
|
|
141
|
-
image: profile.image,
|
|
142
|
-
capabilities: profile.capabilities,
|
|
143
|
-
dataBoundary: profile.dataBoundary,
|
|
144
|
-
contextWindow: profile.contextWindow,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const normalizeProfile = (raw: z.infer<typeof ModelProfileSchema>): AiModelProfile => {
|
|
148
|
-
const { requestHeaders: _requestHeaders, capabilities, dataBoundary, dataPolicy: legacyDataPolicy, tags: _legacyTags, ...profile } = raw;
|
|
149
|
-
return {
|
|
150
|
-
...profile,
|
|
151
|
-
capabilities: normalizeCapabilities(capabilities),
|
|
152
|
-
dataBoundary: normalizeDataBoundary(dataBoundary ?? legacyDataPolicy, raw.provider),
|
|
153
|
-
};
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
/** Sanitize every submitted model logo, including unchanged images from stored profiles. */
|
|
157
|
-
export const prepareAiModelProfileImages = (rawJson: string): string => {
|
|
158
|
-
const raw: unknown = JSON.parse(rawJson);
|
|
159
|
-
if (!Array.isArray(raw)) return rawJson;
|
|
160
|
-
return JSON.stringify(
|
|
161
|
-
raw.map((profile: unknown) => {
|
|
162
|
-
if (typeof profile !== "object" || profile === null || !("image" in profile) || typeof profile.image !== "string") return profile;
|
|
163
|
-
return { ...profile, image: stripImageDataUrlMetadata(profile.image) };
|
|
164
|
-
}),
|
|
165
|
-
);
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
export const parseAiModelProfiles = (rawJson: string): { profiles: AiModelProfile[]; error?: AiSettingsError } => {
|
|
169
|
-
let raw: unknown;
|
|
170
|
-
try {
|
|
171
|
-
raw = rawJson.trim() ? JSON.parse(rawJson) : [];
|
|
172
|
-
} catch (error) {
|
|
173
|
-
return {
|
|
174
|
-
profiles: [],
|
|
175
|
-
error: {
|
|
176
|
-
code: "invalid_model_profiles",
|
|
177
|
-
message: "AI model profiles must be valid JSON.",
|
|
178
|
-
fields: { "ai.model_profiles_json": error instanceof Error ? error.message : "Invalid JSON" },
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const parsed = z.array(ModelProfileSchema).safeParse(raw);
|
|
184
|
-
if (!parsed.success) {
|
|
185
|
-
return {
|
|
186
|
-
profiles: [],
|
|
187
|
-
error: {
|
|
188
|
-
code: "invalid_model_profiles",
|
|
189
|
-
message: "AI model profiles do not match the expected shape.",
|
|
190
|
-
fields: { "ai.model_profiles_json": z.prettifyError(parsed.error) },
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const seen = new Set<string>();
|
|
196
|
-
const duplicate = parsed.data.find((profile) => {
|
|
197
|
-
if (seen.has(profile.id)) return true;
|
|
198
|
-
seen.add(profile.id);
|
|
199
|
-
return false;
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
if (duplicate) {
|
|
203
|
-
return {
|
|
204
|
-
profiles: [],
|
|
205
|
-
error: {
|
|
206
|
-
code: "invalid_model_profiles",
|
|
207
|
-
message: `Duplicate AI model profile id "${duplicate.id}".`,
|
|
208
|
-
fields: { "ai.model_profiles_json": `Duplicate model profile id "${duplicate.id}".` },
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const profiles = parsed.data.map(normalizeProfile);
|
|
214
|
-
return { profiles };
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
export type AiModelReferenceSetting =
|
|
218
|
-
| "ai.default_model_id"
|
|
219
|
-
| "ai.background_model_id"
|
|
220
|
-
| "ai.workflow_model_id"
|
|
221
|
-
| "ai.vision_model_id"
|
|
222
|
-
| "ai.audio_model_id";
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* One reason why a prospective AI configuration cannot be saved. Codes and
|
|
226
|
-
* profile ids are stable and locale-free; the admin route turns them into
|
|
227
|
-
* request-localized messages.
|
|
228
|
-
*/
|
|
229
|
-
export type AiSettingsIssue =
|
|
230
|
-
| { code: "model_required"; setting: AiModelReferenceSetting }
|
|
231
|
-
| {
|
|
232
|
-
code: "model_profile_missing" | "model_profile_disabled" | "model_profile_incompatible";
|
|
233
|
-
setting: AiModelReferenceSetting;
|
|
234
|
-
profileId: string;
|
|
235
|
-
}
|
|
236
|
-
| { code: "provider_credential_missing"; setting: "ai.model_profiles_json"; profileId: string };
|
|
237
|
-
|
|
238
|
-
export const validateAiSettingsConfiguration = (input: {
|
|
239
|
-
enabled: boolean;
|
|
240
|
-
defaultModelId: string;
|
|
241
|
-
backgroundModelId: string;
|
|
242
|
-
visionModelId?: string;
|
|
243
|
-
audioModelId?: string;
|
|
244
|
-
workflowModelId: string;
|
|
245
|
-
profiles: readonly AiModelProfile[];
|
|
246
|
-
credentialProfileIds: readonly string[];
|
|
247
|
-
}): AiSettingsIssue[] => {
|
|
248
|
-
if (!input.enabled) return [];
|
|
249
|
-
|
|
250
|
-
const issues: AiSettingsIssue[] = [];
|
|
251
|
-
const isChatModel = (profile: AiModelProfile) => !profile.capabilities.includes("transcription");
|
|
252
|
-
const checkReference = (
|
|
253
|
-
setting: AiModelReferenceSetting,
|
|
254
|
-
profileId: string | undefined,
|
|
255
|
-
fits: (profile: AiModelProfile) => boolean,
|
|
256
|
-
required = false,
|
|
257
|
-
) => {
|
|
258
|
-
if (!profileId) {
|
|
259
|
-
if (required) issues.push({ code: "model_required", setting });
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const profile = input.profiles.find((item) => item.id === profileId);
|
|
263
|
-
if (!profile) issues.push({ code: "model_profile_missing", setting, profileId });
|
|
264
|
-
else if (!profile.enabled) issues.push({ code: "model_profile_disabled", setting, profileId });
|
|
265
|
-
else if (!fits(profile)) issues.push({ code: "model_profile_incompatible", setting, profileId });
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
checkReference("ai.default_model_id", input.defaultModelId, isChatModel, true);
|
|
269
|
-
checkReference("ai.background_model_id", input.backgroundModelId, isChatModel);
|
|
270
|
-
checkReference("ai.workflow_model_id", input.workflowModelId, isChatModel);
|
|
271
|
-
checkReference("ai.vision_model_id", input.visionModelId, (profile) => profile.capabilities.includes("vision"));
|
|
272
|
-
checkReference("ai.audio_model_id", input.audioModelId, (profile) => profile.capabilities.includes("transcription"));
|
|
273
|
-
|
|
274
|
-
const configured = new Set(input.credentialProfileIds);
|
|
275
|
-
for (const profile of input.profiles) {
|
|
276
|
-
if (profile.enabled && providerRequiresCredential(profile.provider) && !configured.has(profile.id)) {
|
|
277
|
-
issues.push({ code: "provider_credential_missing", setting: "ai.model_profiles_json", profileId: profile.id });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return issues;
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
export const planAiProfileCredentials = (input: {
|
|
285
|
-
currentProfiles: readonly AiModelProfile[];
|
|
286
|
-
nextProfiles: readonly AiModelProfile[];
|
|
287
|
-
existingCredentialProfileIds: readonly string[];
|
|
288
|
-
submittedCredentialProfileIds: readonly string[];
|
|
289
|
-
}): { keepCredentialProfileIds: string[]; unsupportedCredentialProfileId?: string } => {
|
|
290
|
-
const nextProfilesById = new Map(input.nextProfiles.map((profile) => [profile.id, profile]));
|
|
291
|
-
const unsupportedCredentialProfileId = input.submittedCredentialProfileIds.find((profileId) => {
|
|
292
|
-
const profile = nextProfilesById.get(profileId);
|
|
293
|
-
return !profile || !providerSupportsCredential(profile.provider);
|
|
294
|
-
});
|
|
295
|
-
if (unsupportedCredentialProfileId) return { keepCredentialProfileIds: [], unsupportedCredentialProfileId };
|
|
296
|
-
|
|
297
|
-
const existingCredentials = new Set(input.existingCredentialProfileIds);
|
|
298
|
-
const submittedCredentials = new Set(input.submittedCredentialProfileIds);
|
|
299
|
-
const currentProfilesById = new Map(input.currentProfiles.map((profile) => [profile.id, profile]));
|
|
300
|
-
const keepCredentialProfileIds = input.nextProfiles
|
|
301
|
-
.filter((profile) => {
|
|
302
|
-
if (!providerSupportsCredential(profile.provider)) return false;
|
|
303
|
-
if (submittedCredentials.has(profile.id)) return true;
|
|
304
|
-
const current = currentProfilesById.get(profile.id);
|
|
305
|
-
return current?.provider === profile.provider && existingCredentials.has(profile.id);
|
|
306
|
-
})
|
|
307
|
-
.map((profile) => profile.id);
|
|
308
|
-
|
|
309
|
-
return { keepCredentialProfileIds };
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
export const resolveAiSettingsStateFromRaw = async (input: {
|
|
313
|
-
enabled: boolean;
|
|
314
|
-
defaultModelId: string;
|
|
315
|
-
visionModelId?: string;
|
|
316
|
-
profilesJson: string;
|
|
317
|
-
globalInstructions?: string;
|
|
318
|
-
localClientInstructions?: string;
|
|
319
|
-
compactionInstructions?: string;
|
|
320
|
-
maxToolResultChars?: unknown;
|
|
321
|
-
firecrawlApiKey?: string;
|
|
322
|
-
credentialProfileIds?: readonly string[];
|
|
323
|
-
readCredential?: (profileId: string) => Promise<string | null | undefined>;
|
|
324
|
-
}): Promise<AiSettingsState> => {
|
|
325
|
-
const parsed = parseAiModelProfiles(input.profilesJson ?? "[]");
|
|
326
|
-
const baseState = {
|
|
327
|
-
enabled: Boolean(input.enabled),
|
|
328
|
-
defaultModelId: input.defaultModelId ?? "",
|
|
329
|
-
visionModelId: input.visionModelId ?? "",
|
|
330
|
-
globalInstructions: input.globalInstructions ?? "",
|
|
331
|
-
localClientInstructions: input.localClientInstructions ?? "",
|
|
332
|
-
compactionInstructions: input.compactionInstructions ?? "",
|
|
333
|
-
maxToolResultChars: normalizeMaxToolResultChars(input.maxToolResultChars),
|
|
334
|
-
firecrawlConfigured: Boolean(input.firecrawlApiKey?.trim()),
|
|
335
|
-
};
|
|
336
|
-
if (parsed.error) {
|
|
337
|
-
return {
|
|
338
|
-
ok: false,
|
|
339
|
-
...baseState,
|
|
340
|
-
profiles: parsed.profiles,
|
|
341
|
-
error: parsed.error,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (!input.enabled) {
|
|
346
|
-
return {
|
|
347
|
-
ok: true,
|
|
348
|
-
...baseState,
|
|
349
|
-
enabled: false,
|
|
350
|
-
profiles: parsed.profiles,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const defaultProfile = parsed.profiles.find((profile) => profile.id === input.defaultModelId);
|
|
355
|
-
if (!input.defaultModelId || !defaultProfile) {
|
|
356
|
-
return {
|
|
357
|
-
ok: false,
|
|
358
|
-
...baseState,
|
|
359
|
-
enabled: true,
|
|
360
|
-
profiles: parsed.profiles,
|
|
361
|
-
error: {
|
|
362
|
-
code: "missing_default_model",
|
|
363
|
-
message: "AI is enabled but no valid default model profile is configured.",
|
|
364
|
-
fields: { "ai.default_model_id": "Choose an enabled model profile id." },
|
|
365
|
-
},
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!defaultProfile.enabled || defaultProfile.capabilities.includes("transcription")) {
|
|
370
|
-
return {
|
|
371
|
-
ok: false,
|
|
372
|
-
...baseState,
|
|
373
|
-
enabled: true,
|
|
374
|
-
profiles: parsed.profiles,
|
|
375
|
-
error: {
|
|
376
|
-
code: "default_model_disabled",
|
|
377
|
-
message: `Default AI model "${input.defaultModelId}" must be an enabled text model.`,
|
|
378
|
-
fields: { "ai.default_model_id": "Choose an enabled model profile id." },
|
|
379
|
-
},
|
|
380
|
-
};
|
|
381
|
-
}
|
|
15
|
+
import type { AiDataBoundary, AiModelPolicy, AiModelProfile, AiResolvedModel, AiSettingsError, AiSettingsState } from "./types";
|
|
382
16
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
try {
|
|
387
|
-
credentialConfigured = Boolean((await input.readCredential(defaultProfile.id))?.trim());
|
|
388
|
-
} catch {
|
|
389
|
-
credentialConfigured = false;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (!credentialConfigured) {
|
|
393
|
-
return {
|
|
394
|
-
ok: false,
|
|
395
|
-
...baseState,
|
|
396
|
-
enabled: true,
|
|
397
|
-
profiles: parsed.profiles,
|
|
398
|
-
error: {
|
|
399
|
-
code: "missing_provider_credential",
|
|
400
|
-
message: `Default AI model "${input.defaultModelId}" is missing provider credentials.`,
|
|
401
|
-
fields: { "ai.model_profiles_json": "Enter the provider API key on the default model profile." },
|
|
402
|
-
},
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return {
|
|
408
|
-
ok: true,
|
|
409
|
-
...baseState,
|
|
410
|
-
enabled: true,
|
|
411
|
-
profiles: parsed.profiles,
|
|
412
|
-
};
|
|
17
|
+
export { providerRequiresCredential, providerSupportsCredential };
|
|
18
|
+
export const aiSettingsFailure = (code: AiSettingsError["code"], message: string): never => {
|
|
19
|
+
throw Object.assign(new Error(message), { aiError: { code, message } satisfies AiSettingsError });
|
|
413
20
|
};
|
|
414
|
-
|
|
415
|
-
const readAiSettingsSnapshot = async (): Promise<{ state: AiSettingsState; credentialProfileIds: string[] }> => {
|
|
21
|
+
export const readAiSettingsState = async (): Promise<AiSettingsState> => {
|
|
416
22
|
const [
|
|
417
23
|
enabled,
|
|
418
|
-
defaultModelId,
|
|
419
|
-
visionModelId,
|
|
420
|
-
profilesJson,
|
|
421
24
|
globalInstructions,
|
|
422
25
|
localClientInstructions,
|
|
423
26
|
compactionInstructions,
|
|
424
27
|
maxToolResultChars,
|
|
425
28
|
firecrawlApiKey,
|
|
29
|
+
profiles,
|
|
30
|
+
assignments,
|
|
426
31
|
] = await Promise.all([
|
|
427
32
|
coreSettings.get<boolean>("ai.enabled"),
|
|
428
|
-
coreSettings.get<string>("ai.default_model_id"),
|
|
429
|
-
coreSettings.get<string>("ai.vision_model_id"),
|
|
430
|
-
coreSettings.get<string>("ai.model_profiles_json"),
|
|
431
33
|
coreSettings.get<string>("ai.global_instructions"),
|
|
432
34
|
coreSettings.get<string>("ai.local_client_instructions"),
|
|
433
35
|
coreSettings.get<string>("ai.compaction_instructions"),
|
|
434
36
|
coreSettings.get<number>("ai.max_tool_result_chars"),
|
|
435
37
|
coreSettings.get<string>(AI_FIRECRAWL_API_KEY_SETTING_KEY),
|
|
38
|
+
readAiModelProfiles(),
|
|
39
|
+
aiModelCatalog.assignments(),
|
|
436
40
|
]);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const defaultProfile = parsed.profiles.find((profile) => profile.id === defaultModelId);
|
|
440
|
-
const needsCredentials =
|
|
441
|
-
Boolean(enabled) &&
|
|
442
|
-
!parsed.error &&
|
|
443
|
-
Boolean(defaultProfile?.enabled) &&
|
|
444
|
-
parsed.profiles.some((profile) => profile.enabled && providerRequiresCredential(profile.provider));
|
|
445
|
-
const credentialProfileIds = needsCredentials ? await listAiCredentialProfileIds() : [];
|
|
446
|
-
|
|
447
|
-
const state = await resolveAiSettingsStateFromRaw({
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
448
43
|
enabled: Boolean(enabled),
|
|
449
|
-
|
|
450
|
-
visionModelId: visionModelId ?? "",
|
|
451
|
-
profilesJson: profilesJson ?? "[]",
|
|
44
|
+
assignments,
|
|
452
45
|
globalInstructions: globalInstructions ?? "",
|
|
453
46
|
localClientInstructions: localClientInstructions ?? "",
|
|
454
47
|
compactionInstructions: compactionInstructions ?? "",
|
|
455
|
-
maxToolResultChars,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
return { state, credentialProfileIds };
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
export const readAiSettingsState = async (): Promise<AiSettingsState> => (await readAiSettingsSnapshot()).state;
|
|
463
|
-
|
|
464
|
-
const hasAll = <T extends string>(values: readonly T[], required: readonly T[] | undefined): boolean =>
|
|
465
|
-
!required || required.every((requiredValue) => values.includes(requiredValue));
|
|
466
|
-
|
|
467
|
-
const matchesPolicy = (profile: AiModelProfile, policy: AiModelPolicy): boolean => {
|
|
468
|
-
if (!profile.enabled) return false;
|
|
469
|
-
if (profile.capabilities.includes("transcription") !== Boolean(policy.requiredCapabilities?.includes("transcription"))) return false;
|
|
470
|
-
if ("allowedModelIds" in policy && policy.allowedModelIds && !policy.allowedModelIds.includes(profile.id)) return false;
|
|
471
|
-
if (policy.allowedDataBoundaries && !policy.allowedDataBoundaries.includes(profile.dataBoundary)) return false;
|
|
472
|
-
return hasAll(profile.capabilities, policy.requiredCapabilities);
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
const resolvePolicyModelId = (state: Extract<AiSettingsState, { ok: true }>, policy: AiModelPolicy, requestedModelId?: string): string => {
|
|
476
|
-
if (policy.kind === "locked") return policy.modelId;
|
|
477
|
-
if (policy.kind === "selectable") return requestedModelId ?? policy.defaultModelId ?? state.defaultModelId;
|
|
478
|
-
return state.defaultModelId;
|
|
48
|
+
maxToolResultChars: maxToolResultChars > 0 ? maxToolResultChars : 2_000_000,
|
|
49
|
+
firecrawlConfigured: Boolean(firecrawlApiKey?.trim()),
|
|
50
|
+
profiles,
|
|
51
|
+
};
|
|
479
52
|
};
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
policy
|
|
484
|
-
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
message: `AI model "${modelId}" is unavailable or not allowed for this chat or data boundary. An administrator can enable a compatible model permitted by the application policy in the AI settings.`,
|
|
497
|
-
fields: { modelProfileId: "Choose an allowed enabled model profile." },
|
|
498
|
-
} satisfies AiSettingsError,
|
|
499
|
-
},
|
|
500
|
-
);
|
|
53
|
+
export const matchesAiModelPolicy = (profile: AiModelProfile, policy: AiModelPolicy): boolean =>
|
|
54
|
+
profile.enabled &&
|
|
55
|
+
profile.capabilities.includes("transcription") === Boolean(policy.requiredCapabilities?.includes("transcription")) &&
|
|
56
|
+
(!("allowedModelIds" in policy) || !policy.allowedModelIds || policy.allowedModelIds.includes(profile.id)) &&
|
|
57
|
+
(!policy.allowedDataBoundaries || policy.allowedDataBoundaries.includes(profile.dataBoundary)) &&
|
|
58
|
+
(!policy.requiredCapabilities || policy.requiredCapabilities.every((capability) => profile.capabilities.includes(capability)));
|
|
59
|
+
|
|
60
|
+
export const resolveAiModelRef = (
|
|
61
|
+
state: Pick<AiSettingsState, "enabled" | "assignments">,
|
|
62
|
+
request: AiModelRequest = { class: "balanced" },
|
|
63
|
+
): AiResolvedModelRef => {
|
|
64
|
+
if (!state.enabled) return aiSettingsFailure("ai_disabled", "AI is disabled.");
|
|
65
|
+
if ("id" in request) return { id: request.id };
|
|
66
|
+
for (const modelClass of [request.class, ...aiModelClassFallbackChain(request.class)]) {
|
|
67
|
+
const assignment = state.assignments.find((assignment) => assignment.slot === modelClass);
|
|
68
|
+
if (assignment?.variantId) return { requestedClass: request.class, class: modelClass, id: assignment.variantId };
|
|
501
69
|
}
|
|
70
|
+
return aiSettingsFailure("ai_no_model", "No AI model class is assigned.");
|
|
71
|
+
};
|
|
72
|
+
const policyRequest = (policy: AiModelPolicy, request?: AiModelRequest): AiModelRequest | undefined =>
|
|
73
|
+
policy.kind === "locked" ? { id: policy.modelId } : request;
|
|
74
|
+
export const selectAiModelProfile = (state: AiSettingsState, policy: AiModelPolicy, request?: AiModelRequest): AiModelProfile => {
|
|
75
|
+
const ref = resolveAiModelRef(state, policyRequest(policy, request));
|
|
76
|
+
const profile = state.profiles.find((candidate) => candidate.id === ref.id);
|
|
77
|
+
if (!profile || !matchesAiModelPolicy(profile, policy))
|
|
78
|
+
return aiSettingsFailure("model_policy_mismatch", "The AI model is missing, disabled, or outside the permitted policy.");
|
|
502
79
|
return profile;
|
|
503
80
|
};
|
|
504
|
-
|
|
505
|
-
|
|
81
|
+
export const resolveAiModelFromState = async (
|
|
82
|
+
state: AiSettingsState,
|
|
506
83
|
policy: AiModelPolicy = { kind: "platform-default" },
|
|
507
|
-
|
|
84
|
+
request?: AiModelRequest,
|
|
508
85
|
): Promise<AiResolvedModel> => {
|
|
509
|
-
const
|
|
510
|
-
|
|
86
|
+
const model = resolveAiModelRef(state, policyRequest(policy, request));
|
|
87
|
+
const profile = selectAiModelProfile(state, policy, request);
|
|
88
|
+
const credential = providerSupportsCredential(profile.provider) ? await getAiCredential(profile.providerId) : null;
|
|
89
|
+
if (providerRequiresCredential(profile.provider) && !credential?.trim())
|
|
90
|
+
return aiSettingsFailure("model_policy_mismatch", "The AI provider has no usable credential.");
|
|
91
|
+
const headers = providerSupportsRequestHeaders(profile.provider) ? await getAiRequestHeaders(profile.providerId) : {};
|
|
92
|
+
return { model, profile, provider: createAiProvider(profile, credential?.trim() || undefined, headers) };
|
|
511
93
|
};
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
requiredCapabilities: ["vision"],
|
|
517
|
-
allowedDataBoundaries,
|
|
518
|
-
});
|
|
519
|
-
|
|
94
|
+
export const resolveAiModel = async (
|
|
95
|
+
policy: AiModelPolicy = { kind: "platform-default" },
|
|
96
|
+
request?: AiModelRequest,
|
|
97
|
+
): Promise<AiResolvedModel> => resolveAiModelFromState(await readAiSettingsState(), policy, request);
|
|
520
98
|
export const resolveAiVisionModel = async (allowedDataBoundaries?: AiDataBoundary[]): Promise<AiResolvedModel> => {
|
|
521
99
|
const state = await readAiSettingsState();
|
|
522
|
-
if (!state.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const modelId = (state.visionModelId ?? "").trim();
|
|
532
|
-
if (!modelId)
|
|
533
|
-
throw new Error(
|
|
534
|
-
"Image inspection is not set up: no vision model is configured. An administrator can configure a vision model in the AI settings.",
|
|
535
|
-
);
|
|
536
|
-
return resolveAiModelFromState(state, visionModelPolicy(modelId, allowedDataBoundaries));
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
export const isAiVisionModelConfigured = async (allowedDataBoundaries?: AiDataBoundary[]): Promise<boolean> => {
|
|
540
|
-
const { state, credentialProfileIds } = await readAiSettingsSnapshot();
|
|
541
|
-
if (!state.ok || !state.enabled || !state.visionModelId?.trim()) return false;
|
|
100
|
+
if (!state.enabled) return aiSettingsFailure("ai_disabled", "AI is disabled.");
|
|
101
|
+
const id = state.assignments.find((assignment) => assignment.slot === "vision")?.variantId;
|
|
102
|
+
if (!id) return aiSettingsFailure("model_policy_mismatch", "No image inspection model is assigned.");
|
|
103
|
+
return resolveAiModelFromState(state, { kind: "locked", modelId: id, requiredCapabilities: ["vision"], allowedDataBoundaries });
|
|
104
|
+
};
|
|
105
|
+
export const isAiVisionModelConfigured = async (
|
|
106
|
+
allowedDataBoundaries?: AiDataBoundary[],
|
|
107
|
+
providedState?: AiSettingsState,
|
|
108
|
+
): Promise<boolean> => {
|
|
542
109
|
try {
|
|
543
|
-
const
|
|
544
|
-
|
|
110
|
+
const state = providedState ?? (await readAiSettingsState());
|
|
111
|
+
const id = state.assignments.find((assignment) => assignment.slot === "vision")?.variantId;
|
|
112
|
+
if (!id) return false;
|
|
113
|
+
const profile = selectAiModelProfile(state, { kind: "locked", modelId: id, requiredCapabilities: ["vision"], allowedDataBoundaries });
|
|
114
|
+
return !providerRequiresCredential(profile.provider) || (await listAiCredentialProviderIds()).includes(profile.providerId);
|
|
545
115
|
} catch {
|
|
546
116
|
return false;
|
|
547
117
|
}
|
|
548
118
|
};
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
code: "ai_disabled",
|
|
560
|
-
message: "AI is disabled. An administrator can enable AI in the AI settings.",
|
|
561
|
-
} satisfies AiSettingsError,
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const profile = selectAiModelProfile(state, policy, requestedModelId);
|
|
566
|
-
|
|
567
|
-
const credential = providerSupportsCredential(profile.provider) ? await getAiCredential(profile.id) : null;
|
|
568
|
-
if (providerRequiresCredential(profile.provider) && !credential?.trim()) {
|
|
569
|
-
throw Object.assign(
|
|
570
|
-
new Error(
|
|
571
|
-
`AI model "${profile.id}" is missing provider credentials. An administrator can add the provider API key in the AI settings.`,
|
|
572
|
-
),
|
|
573
|
-
{
|
|
574
|
-
aiError: {
|
|
575
|
-
code: "missing_provider_credential",
|
|
576
|
-
message: `AI model "${profile.id}" is missing provider credentials. An administrator can add the provider API key in the AI settings.`,
|
|
577
|
-
fields: { "ai.model_profiles_json": "Enter the provider API key on this model profile." },
|
|
578
|
-
} satisfies AiSettingsError,
|
|
579
|
-
},
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const headers = providerSupportsRequestHeaders(profile.provider) ? await getAiRequestHeaders(profile.id) : {};
|
|
584
|
-
return { profile, provider: createAiProvider(profile, credential?.trim() || undefined, headers) };
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
const isUsableProfile = (profile: AiModelProfile, credentialProfileIds: ReadonlySet<string>): boolean =>
|
|
588
|
-
!providerRequiresCredential(profile.provider) || credentialProfileIds.has(profile.id);
|
|
589
|
-
|
|
590
|
-
export const listAiModels = async (policy: AiModelPolicy = { kind: "selectable" }): Promise<AiPublicModelProfile[]> => {
|
|
591
|
-
const { state, credentialProfileIds } = await readAiSettingsSnapshot();
|
|
592
|
-
if (!state.ok || !state.enabled) return [];
|
|
593
|
-
const configured = new Set(credentialProfileIds);
|
|
594
|
-
return state.profiles.filter((profile) => matchesPolicy(profile, policy) && isUsableProfile(profile, configured)).map(profileToPublic);
|
|
119
|
+
export const listAiModels = async (
|
|
120
|
+
policy: AiModelPolicy = { kind: "selectable" },
|
|
121
|
+
providedState?: AiSettingsState,
|
|
122
|
+
): Promise<AiModelProfile[]> => {
|
|
123
|
+
const [state, credentials] = await Promise.all([providedState ?? readAiSettingsState(), listAiCredentialProviderIds()]);
|
|
124
|
+
if (!state.enabled) return [];
|
|
125
|
+
return state.profiles.filter(
|
|
126
|
+
(profile) =>
|
|
127
|
+
matchesAiModelPolicy(profile, policy) && (!providerRequiresCredential(profile.provider) || credentials.includes(profile.providerId)),
|
|
128
|
+
);
|
|
595
129
|
};
|
|
596
|
-
|
|
597
130
|
export const toPublicAiSettingsState = async (allowedDataBoundaries?: AiDataBoundary[]) => {
|
|
598
|
-
const
|
|
599
|
-
const configured = new Set(credentialProfileIds);
|
|
600
|
-
let visionModelConfigured = false;
|
|
601
|
-
if (state.ok && state.enabled && state.visionModelId?.trim()) {
|
|
602
|
-
try {
|
|
603
|
-
const profile = selectAiModelProfile(state, visionModelPolicy(state.visionModelId, allowedDataBoundaries));
|
|
604
|
-
visionModelConfigured = isUsableProfile(profile, configured);
|
|
605
|
-
} catch {
|
|
606
|
-
// The configured auxiliary model is outside this application's policy.
|
|
607
|
-
}
|
|
608
|
-
}
|
|
131
|
+
const state = await readAiSettingsState();
|
|
609
132
|
return {
|
|
610
|
-
ok:
|
|
133
|
+
ok: true,
|
|
611
134
|
enabled: state.enabled,
|
|
612
|
-
|
|
613
|
-
visionModelConfigured,
|
|
614
|
-
error: state.ok ? null : state.error,
|
|
135
|
+
visionModelConfigured: await isAiVisionModelConfigured(allowedDataBoundaries),
|
|
615
136
|
firecrawlConfigured: state.firecrawlConfigured,
|
|
616
|
-
|
|
617
|
-
state.ok && state.enabled
|
|
618
|
-
? state.profiles
|
|
619
|
-
.filter(
|
|
620
|
-
(profile) => matchesPolicy(profile, { kind: "selectable", allowedDataBoundaries }) && isUsableProfile(profile, configured),
|
|
621
|
-
)
|
|
622
|
-
.map(profileToPublic)
|
|
623
|
-
: [],
|
|
137
|
+
error: null,
|
|
624
138
|
};
|
|
625
139
|
};
|
|
626
|
-
|
|
627
|
-
export type { AiDataBoundary, AiModelCapability, AiModelPolicy, AiModelProfile };
|
|
140
|
+
export type { AiDataBoundary, AiModelCapability, AiModelPolicy, AiModelProfile } from "./types";
|