@k2b/cloud 0.43.0 → 0.45.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 +11 -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/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +20 -5
- package/src/access/PrincipalAvatar.tsx +30 -21
- 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/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -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 +10 -6
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +40 -25
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +12 -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/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +58 -47
- package/src/ai/projects.ts +110 -88
- 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 +9 -3
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +47 -48
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- 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 +22 -3
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-skills.ts +4 -1
- 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 +35 -5
- 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/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -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 +6 -1
- 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 +17 -4
- package/src/server/index.ts +2 -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 +31 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +36 -13
- package/src/services/accounts/entities.ts +43 -19
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +19 -14
- package/src/services/accounts/identities.ts +70 -11
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +45 -14
- 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/index.ts +2 -2
- package/src/services/mandates/policy.ts +20 -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 +26 -3
- 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/email-html.ts +1 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +433 -65
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/text-diff.ts +75 -0
- package/src/shared-text/types.ts +24 -5
- 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 +2 -2
- package/src/ssr/CloudShell.island.tsx +44 -7
- package/src/ssr/Layout.tsx +9 -7
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- 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 +26 -31
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +11 -1
- package/src/ssr/profile-actions.ts +3 -6
- 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 +3 -1
- 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/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +134 -34
- 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
|
@@ -32,19 +32,32 @@ import type {
|
|
|
32
32
|
|
|
33
33
|
export const DEFAULT_MAX_LOOP_ITEMS = 100;
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// Callers trim the captured text; no `\s*` around the group keeps matching linear.
|
|
36
|
+
const EXACT_EXPRESSION = /^\$\{\{([^{}]+)\}\}$/;
|
|
36
37
|
|
|
37
38
|
class RuntimeVariableScope implements WorkflowVariableScope {
|
|
38
39
|
readonly #values = new Map<string, WorkflowJsonValue>();
|
|
40
|
+
readonly #unknown = new Set<string>();
|
|
39
41
|
|
|
40
42
|
constructor(
|
|
41
43
|
initial: Record<string, WorkflowJsonValue> = {},
|
|
42
44
|
readonly parent?: RuntimeVariableScope,
|
|
45
|
+
public dependsOnUnknown: boolean = parent?.dependsOnUnknown ?? false,
|
|
43
46
|
) {
|
|
44
47
|
for (const [name, value] of Object.entries(initial)) this.#values.set(name, value);
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
markUnknown(name: string): void {
|
|
51
|
+
this.#values.delete(name);
|
|
52
|
+
this.#unknown.add(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
isUnknown(name: string): boolean {
|
|
56
|
+
return this.#unknown.has(name) || (!this.#values.has(name) && this.parent?.isUnknown(name) === true);
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
get(name: string): WorkflowJsonValue | undefined {
|
|
60
|
+
if (this.#unknown.has(name)) return undefined;
|
|
48
61
|
return this.#values.has(name) ? this.#values.get(name) : this.parent?.get(name);
|
|
49
62
|
}
|
|
50
63
|
|
|
@@ -53,18 +66,18 @@ class RuntimeVariableScope implements WorkflowVariableScope {
|
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
set(name: string, value: WorkflowJsonValue): void {
|
|
69
|
+
this.#unknown.delete(name);
|
|
56
70
|
this.#values.set(name, value);
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
snapshot(): Record<string, WorkflowJsonValue> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
74
|
+
const values = { ...(this.parent?.snapshot() ?? {}), ...Object.fromEntries(this.#values) };
|
|
75
|
+
for (const name of this.#unknown) delete values[name];
|
|
76
|
+
return values;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
child(initial: Record<string, WorkflowJsonValue> = {}): RuntimeVariableScope {
|
|
67
|
-
return new RuntimeVariableScope(initial, this);
|
|
79
|
+
child(initial: Record<string, WorkflowJsonValue> = {}, unknown = this.dependsOnUnknown): RuntimeVariableScope {
|
|
80
|
+
return new RuntimeVariableScope(initial, this, unknown);
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
|
|
@@ -93,11 +106,18 @@ export class WorkflowRetryableStepError extends Error {
|
|
|
93
106
|
}
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
class WorkflowValueError extends Error {
|
|
109
|
+
export class WorkflowValueError extends Error {
|
|
110
|
+
constructor(
|
|
111
|
+
message: string,
|
|
112
|
+
readonly code = "WORKFLOW_VALUE_UNAVAILABLE",
|
|
113
|
+
) {
|
|
114
|
+
super(message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
97
117
|
|
|
98
|
-
class WorkflowValueWaiting extends Error {
|
|
118
|
+
export class WorkflowValueWaiting extends Error {
|
|
99
119
|
constructor(
|
|
100
|
-
reference: string,
|
|
120
|
+
readonly reference: string,
|
|
101
121
|
readonly dependency: WorkflowDependency,
|
|
102
122
|
) {
|
|
103
123
|
super(`workflow reference "${reference}" is waiting`);
|
|
@@ -189,7 +209,10 @@ const resolveLocalReference = (state: RuntimeState, scope: RuntimeVariableScope,
|
|
|
189
209
|
if (rootName === "inputs") root = state.options.invocation.inputs;
|
|
190
210
|
else if (rootName === "bindings") root = state.options.plan.bindings;
|
|
191
211
|
else if (rootName === "context") root = state.options.invocation.context ?? {};
|
|
192
|
-
else
|
|
212
|
+
else {
|
|
213
|
+
if (scope.isUnknown(rootName)) return { state: "waiting", dependency: { kind: "workflow.dry-run-value", key: rootName } };
|
|
214
|
+
root = scope.get(rootName);
|
|
215
|
+
}
|
|
193
216
|
const value = root === undefined || segments.length === 0 ? root : readWorkflowValuePath(root, segments);
|
|
194
217
|
return value === undefined ? { state: "missing" } : { state: "resolved", value };
|
|
195
218
|
};
|
|
@@ -201,7 +224,8 @@ const resolveReference = async (
|
|
|
201
224
|
path: Array<string | number> = [],
|
|
202
225
|
): Promise<WorkflowValueResolution> => {
|
|
203
226
|
const local = () => resolveLocalReference(state, scope, reference);
|
|
204
|
-
|
|
227
|
+
const localValue = local();
|
|
228
|
+
if (localValue.state === "waiting" || !state.options.values) return localValue;
|
|
205
229
|
const resolved = await state.options.values.resolve({
|
|
206
230
|
reference,
|
|
207
231
|
path,
|
|
@@ -297,12 +321,43 @@ const evaluateCondition = async (
|
|
|
297
321
|
if (waiting) throw waiting;
|
|
298
322
|
return false;
|
|
299
323
|
}
|
|
300
|
-
if (condition.operator === "not")
|
|
324
|
+
if (condition.operator === "not") {
|
|
325
|
+
const matched = !(await evaluateCondition(state, scope, condition.condition, [...path, "not"]));
|
|
326
|
+
if (state.options.mode === "dryRun") await emit(state, { type: "condition.evaluated", run: state.run, path, matched });
|
|
327
|
+
return matched;
|
|
328
|
+
}
|
|
301
329
|
if (condition.operator === "exists") {
|
|
302
330
|
const resolved = await resolveReference(state, scope, condition.reference, [...path, "exists"]);
|
|
303
331
|
if (resolved.state === "waiting") throw new WorkflowValueWaiting(condition.reference, resolved.dependency);
|
|
304
332
|
return resolved.state === "resolved" && resolved.value !== null && resolved.value !== "";
|
|
305
333
|
}
|
|
334
|
+
if (
|
|
335
|
+
condition.operator === "greaterThan" ||
|
|
336
|
+
condition.operator === "lessThan" ||
|
|
337
|
+
condition.operator === "atLeast" ||
|
|
338
|
+
condition.operator === "atMost"
|
|
339
|
+
) {
|
|
340
|
+
const numbers: number[] = [];
|
|
341
|
+
for (const [index, operand] of condition.operands.entries()) {
|
|
342
|
+
const expression = typeof operand === "string" ? EXACT_EXPRESSION.exec(operand)?.[1]?.trim() : undefined;
|
|
343
|
+
let value: WorkflowJsonValue;
|
|
344
|
+
if (expression && expression !== "now()") {
|
|
345
|
+
const resolved = await resolveReference(state, scope, expression, [...path, condition.operator, index]);
|
|
346
|
+
if (resolved.state === "waiting") throw new WorkflowValueWaiting(expression, resolved.dependency);
|
|
347
|
+
if (resolved.state === "missing") return false;
|
|
348
|
+
value = resolved.value;
|
|
349
|
+
} else {
|
|
350
|
+
value = await evaluateValue(state, scope, operand, [...path, condition.operator, index]);
|
|
351
|
+
}
|
|
352
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return false;
|
|
353
|
+
numbers.push(value);
|
|
354
|
+
}
|
|
355
|
+
const [left, right] = numbers;
|
|
356
|
+
if (condition.operator === "greaterThan") return left! > right!;
|
|
357
|
+
if (condition.operator === "lessThan") return left! < right!;
|
|
358
|
+
if (condition.operator === "atLeast") return left! >= right!;
|
|
359
|
+
return left! <= right!;
|
|
360
|
+
}
|
|
306
361
|
const left = await evaluateValue(state, scope, condition.operands[0], [...path, condition.operator, 0]);
|
|
307
362
|
const right = await evaluateValue(state, scope, condition.operands[1], [...path, condition.operator, 1]);
|
|
308
363
|
if (condition.operator === "equals") return jsonEqual(left, right);
|
|
@@ -363,7 +418,7 @@ const actionContext = (
|
|
|
363
418
|
};
|
|
364
419
|
return state.options.mode === "execute"
|
|
365
420
|
? ({ ...common, mode: "execute" } as WorkflowExecuteActionContext)
|
|
366
|
-
: ({ ...common, mode: "dryRun" } as WorkflowDryRunActionContext);
|
|
421
|
+
: ({ ...common, mode: "dryRun", dependsOnUnknown: scope.dependsOnUnknown } as WorkflowDryRunActionContext);
|
|
367
422
|
};
|
|
368
423
|
|
|
369
424
|
const invalidRestoration = (state: RuntimeState, step: WorkflowRuntimeStepIdentity, message: string): Flow =>
|
|
@@ -449,11 +504,18 @@ const restoreCompletedIfDescendants = async (
|
|
|
449
504
|
irStep: Extract<WorkflowIrStep, { kind: "if" }>,
|
|
450
505
|
iterationPath: number[],
|
|
451
506
|
persistedTraversal: unknown,
|
|
507
|
+
unknown: boolean,
|
|
452
508
|
): Promise<Flow> => {
|
|
453
509
|
const traversal = persistedIfTraversal(persistedTraversal);
|
|
454
510
|
for (const branch of traversal.branches) {
|
|
455
|
-
const flow = await restoreCompletedSteps(
|
|
456
|
-
|
|
511
|
+
const flow = await restoreCompletedSteps(
|
|
512
|
+
state,
|
|
513
|
+
scope.child({}, scope.dependsOnUnknown || unknown),
|
|
514
|
+
branch === "then" ? irStep.then : irStep.else,
|
|
515
|
+
iterationPath,
|
|
516
|
+
);
|
|
517
|
+
if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
518
|
+
else if (flow.state !== "continue") return flow;
|
|
457
519
|
}
|
|
458
520
|
return { state: "continue" };
|
|
459
521
|
};
|
|
@@ -464,13 +526,15 @@ const restoreCompletedSwitchDescendants = async (
|
|
|
464
526
|
irStep: Extract<WorkflowIrStep, { kind: "switch" }>,
|
|
465
527
|
iterationPath: number[],
|
|
466
528
|
persistedTraversal: unknown,
|
|
529
|
+
unknown: boolean,
|
|
467
530
|
): Promise<Flow> => {
|
|
468
531
|
const traversal = persistedSwitchTraversal(irStep, persistedTraversal);
|
|
469
532
|
for (const branch of traversal.branches) {
|
|
470
533
|
const steps = branch === "default" ? irStep.default : irStep.cases[branch]?.steps;
|
|
471
534
|
if (!steps) throw new WorkflowValueError(`completed switch step references unavailable branch "${branch}"`);
|
|
472
|
-
const flow = await restoreCompletedSteps(state, scope.child(), steps, iterationPath);
|
|
473
|
-
if (flow.state
|
|
535
|
+
const flow = await restoreCompletedSteps(state, scope.child({}, scope.dependsOnUnknown || unknown), steps, iterationPath);
|
|
536
|
+
if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
537
|
+
else if (flow.state !== "continue") return flow;
|
|
474
538
|
}
|
|
475
539
|
return { state: "continue" };
|
|
476
540
|
};
|
|
@@ -481,6 +545,7 @@ const restoreCompletedForEachDescendants = async (
|
|
|
481
545
|
irStep: Extract<WorkflowIrStep, { kind: "forEach" }>,
|
|
482
546
|
iterationPath: number[],
|
|
483
547
|
persistedTraversal: unknown,
|
|
548
|
+
unknown: boolean,
|
|
484
549
|
): Promise<Flow> => {
|
|
485
550
|
const traversal = persistedForEachTraversal(persistedTraversal);
|
|
486
551
|
const limit = loopItemLimit(state);
|
|
@@ -488,11 +553,11 @@ const restoreCompletedForEachDescendants = async (
|
|
|
488
553
|
throw new WorkflowValueError(`completed forEach traversal has ${traversal.items.length} items; limit is ${limit}`);
|
|
489
554
|
}
|
|
490
555
|
for (let index = 0; index < traversal.items.length; index += 1) {
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (flow.state !== "continue") return flow;
|
|
556
|
+
const child = scope.child({ [irStep.alias]: traversal.items[index]! }, scope.dependsOnUnknown || unknown);
|
|
557
|
+
if (unknown) child.markUnknown(irStep.alias);
|
|
558
|
+
const flow = await restoreCompletedSteps(state, child, irStep.steps, [...iterationPath, index]);
|
|
559
|
+
if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
560
|
+
else if (flow.state !== "continue") return flow;
|
|
496
561
|
}
|
|
497
562
|
return { state: "continue" };
|
|
498
563
|
};
|
|
@@ -503,13 +568,14 @@ const restoreCompletedControlDescendants = (
|
|
|
503
568
|
irStep: Exclude<WorkflowIrStep, { kind: "action" }>,
|
|
504
569
|
iterationPath: number[],
|
|
505
570
|
persistedTraversal: WorkflowControlTraversal | undefined,
|
|
571
|
+
unknown = false,
|
|
506
572
|
): Promise<Flow> => {
|
|
507
573
|
if (!persistedTraversal) {
|
|
508
574
|
throw new WorkflowValueError(`completed ${irStep.kind} step is missing its persisted control traversal`);
|
|
509
575
|
}
|
|
510
|
-
if (irStep.kind === "if") return restoreCompletedIfDescendants(state, scope, irStep, iterationPath, persistedTraversal);
|
|
511
|
-
if (irStep.kind === "switch") return restoreCompletedSwitchDescendants(state, scope, irStep, iterationPath, persistedTraversal);
|
|
512
|
-
return restoreCompletedForEachDescendants(state, scope, irStep, iterationPath, persistedTraversal);
|
|
576
|
+
if (irStep.kind === "if") return restoreCompletedIfDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
|
|
577
|
+
if (irStep.kind === "switch") return restoreCompletedSwitchDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
|
|
578
|
+
return restoreCompletedForEachDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
|
|
513
579
|
};
|
|
514
580
|
|
|
515
581
|
const restoreCompletedState = async (
|
|
@@ -524,7 +590,14 @@ const restoreCompletedState = async (
|
|
|
524
590
|
return restoreCompletedControlDescendants(state, scope, irStep, step.iterationPath, restored.outcome.control);
|
|
525
591
|
}
|
|
526
592
|
if (restored.mode === "dryRun" && restored.outcome.state === "planned") {
|
|
527
|
-
return restoreCompletedControlDescendants(
|
|
593
|
+
return restoreCompletedControlDescendants(
|
|
594
|
+
state,
|
|
595
|
+
scope,
|
|
596
|
+
irStep,
|
|
597
|
+
step.iterationPath,
|
|
598
|
+
restored.outcome.control,
|
|
599
|
+
restored.outcome.issues?.some((issue) => issue.step.key === step.key) ?? false,
|
|
600
|
+
);
|
|
528
601
|
}
|
|
529
602
|
return { state: "continue" };
|
|
530
603
|
}
|
|
@@ -539,6 +612,16 @@ const restoreCompletedState = async (
|
|
|
539
612
|
return invalidRestoration(state, step, `planner for action "${irStep.action}" is unavailable while restoring step "${step.key}"`);
|
|
540
613
|
await handler.restoreCompleted?.(actionContext(state, scope, step) as WorkflowDryRunActionContext, irStep, restored.outcome);
|
|
541
614
|
}
|
|
615
|
+
if (restored.mode === "dryRun") {
|
|
616
|
+
const savedName = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
|
|
617
|
+
if (
|
|
618
|
+
typeof savedName === "string" &&
|
|
619
|
+
(restored.outcome.state === "indeterminate" ||
|
|
620
|
+
restored.outcome.state === "unsupported" ||
|
|
621
|
+
(restored.outcome.state === "planned" && restored.outcome.output === undefined && restored.outcome.issues?.length))
|
|
622
|
+
)
|
|
623
|
+
scope.markUnknown(savedName);
|
|
624
|
+
}
|
|
542
625
|
return { state: "continue" };
|
|
543
626
|
};
|
|
544
627
|
|
|
@@ -639,6 +722,15 @@ const evaluateAction = async (
|
|
|
639
722
|
const outcome: WorkflowPlanningOutcome = handler
|
|
640
723
|
? await handler.plan(actionContext(state, scope, step) as WorkflowDryRunActionContext, irStep)
|
|
641
724
|
: { state: "unsupported", reason: `action "${irStep.action}" has no dry-run handler` };
|
|
725
|
+
const savedName = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
|
|
726
|
+
if (
|
|
727
|
+
typeof savedName === "string" &&
|
|
728
|
+
(outcome.state === "indeterminate" ||
|
|
729
|
+
outcome.state === "unsupported" ||
|
|
730
|
+
(outcome.state === "planned" && outcome.output === undefined && outcome.issues?.length))
|
|
731
|
+
) {
|
|
732
|
+
scope.markUnknown(savedName);
|
|
733
|
+
}
|
|
642
734
|
if (outcome.state === "planned" || outcome.state === "terminal") {
|
|
643
735
|
state.effects.push(...outcome.effects);
|
|
644
736
|
state.issues.push(...restoredIssues(state, outcome.issues));
|
|
@@ -729,8 +821,9 @@ const analyzeUnknownBranches = async (
|
|
|
729
821
|
const branches =
|
|
730
822
|
irStep.kind === "if" ? [irStep.then, irStep.else] : [...irStep.cases.map((candidate) => candidate.steps), irStep.default];
|
|
731
823
|
for (const branch of branches) {
|
|
732
|
-
const flow = await runSteps(state, branch, scope.child(), iterationPath);
|
|
824
|
+
const flow = await runSteps(state, branch, scope.child({}, true), iterationPath);
|
|
733
825
|
if (flow.state === "canceled") return flow;
|
|
826
|
+
if (flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
734
827
|
}
|
|
735
828
|
return { state: "continue" };
|
|
736
829
|
};
|
|
@@ -786,10 +879,13 @@ const evaluateControl = async (
|
|
|
786
879
|
if (state.options.mode !== "dryRun") throw new WorkflowValueError(reason);
|
|
787
880
|
state.issues.push({ state: "indeterminate", reason, step: stepIdentity(state, irStep, iterationPath) });
|
|
788
881
|
control = { kind: "forEach", items: [null] };
|
|
789
|
-
|
|
882
|
+
const unknownScope = scope.child({}, true);
|
|
883
|
+
unknownScope.markUnknown(irStep.alias);
|
|
884
|
+
flow = await runSteps(state, irStep.steps, unknownScope, [...iterationPath, 0]);
|
|
790
885
|
if (flow.state === "canceled") {
|
|
791
886
|
return { flow, result: resultForHaltedControl(state, flow, effectsStart, issuesStart) };
|
|
792
887
|
}
|
|
888
|
+
if (flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
793
889
|
flow = { state: "continue" };
|
|
794
890
|
return { flow, result: resultForCompletedControl(state, undefined, control, effectsStart, issuesStart) };
|
|
795
891
|
}
|
|
@@ -842,6 +938,10 @@ const evaluateStep = async (
|
|
|
842
938
|
};
|
|
843
939
|
}
|
|
844
940
|
if (error instanceof WorkflowValueWaiting) {
|
|
941
|
+
if (state.options.mode === "dryRun" && irStep.kind === "action") {
|
|
942
|
+
const name = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
|
|
943
|
+
if (typeof name === "string") scope.markUnknown(name);
|
|
944
|
+
}
|
|
845
945
|
if (state.options.mode === "execute") {
|
|
846
946
|
const outcome: WorkflowStepOutcome = { state: "waiting", dependency: error.dependency };
|
|
847
947
|
return { flow: { state: "waiting", dependency: error.dependency, step }, result: { mode: "execute", outcome } };
|
|
@@ -850,16 +950,16 @@ const evaluateStep = async (
|
|
|
850
950
|
return { flow: { state: "indeterminate", reason: outcome.reason, step }, result: { mode: "dryRun", outcome } };
|
|
851
951
|
}
|
|
852
952
|
if (state.options.mode === "dryRun") {
|
|
953
|
+
if (irStep.kind === "action") {
|
|
954
|
+
const name = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
|
|
955
|
+
if (typeof name === "string") scope.markUnknown(name);
|
|
956
|
+
}
|
|
853
957
|
const outcome: WorkflowPlanningOutcome = { state: "indeterminate", reason: errorMessage(error) };
|
|
854
958
|
return { flow: { state: "indeterminate", reason: outcome.reason, step }, result: { mode: "dryRun", outcome } };
|
|
855
959
|
}
|
|
856
960
|
const outcome: WorkflowStepOutcome = {
|
|
857
961
|
state: "failed",
|
|
858
|
-
error: executionError(
|
|
859
|
-
error instanceof WorkflowValueError ? "WORKFLOW_VALUE_UNAVAILABLE" : "WORKFLOW_ACTION_ERROR",
|
|
860
|
-
errorMessage(error),
|
|
861
|
-
false,
|
|
862
|
-
),
|
|
962
|
+
error: executionError(error instanceof WorkflowValueError ? error.code : "WORKFLOW_ACTION_ERROR", errorMessage(error), false),
|
|
863
963
|
};
|
|
864
964
|
return { flow: { state: "failed", error: outcome.error, step }, result: { mode: "execute", outcome } };
|
|
865
965
|
}
|
|
@@ -78,7 +78,10 @@ type WorkflowActionContextBase<Mode extends WorkflowInvocationMode> = {
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
export type WorkflowExecuteActionContext = WorkflowActionContextBase<"execute">;
|
|
81
|
-
export type WorkflowDryRunActionContext = WorkflowActionContextBase<"dryRun"
|
|
81
|
+
export type WorkflowDryRunActionContext = WorkflowActionContextBase<"dryRun"> & {
|
|
82
|
+
/** The containing branch cannot be selected without an unavailable value. */
|
|
83
|
+
dependsOnUnknown?: boolean;
|
|
84
|
+
};
|
|
82
85
|
|
|
83
86
|
export type WorkflowExecuteActionHandler = {
|
|
84
87
|
execute(context: WorkflowExecuteActionContext, step: WorkflowActionStep): Promise<WorkflowStepOutcome>;
|
|
@@ -120,6 +123,7 @@ export interface WorkflowDryRunActionPort {
|
|
|
120
123
|
* carried a copy of it would be one more thing to keep in step.
|
|
121
124
|
*/
|
|
122
125
|
export type WorkflowTraceEvent =
|
|
126
|
+
| { type: "condition.evaluated"; run: WorkflowRuntimeRunIdentity; path: Array<string | number>; matched: boolean }
|
|
123
127
|
| { type: "run.started"; run: WorkflowRuntimeRunIdentity }
|
|
124
128
|
| { type: "step.started"; step: WorkflowRuntimeStepIdentity }
|
|
125
129
|
| { type: "step.restored"; step: WorkflowRuntimeStepIdentity; restored: WorkflowRestoredStep }
|
|
@@ -434,7 +434,7 @@ export const createWorkflowDryRunPort = (module: DefinedWorkflowModule): Workflo
|
|
|
434
434
|
applySaveAs(ctx, step, (planned.output ?? undefined) as WorkflowJsonValue | undefined);
|
|
435
435
|
return {
|
|
436
436
|
state: "planned" as const,
|
|
437
|
-
output:
|
|
437
|
+
...(planned.outputUnavailable ? {} : { output: planned.output ?? null }),
|
|
438
438
|
effects: [{ action: name, summary: planned.summary, ...(consumes ? { consumes } : {}) } as WorkflowJsonValue],
|
|
439
439
|
// An issue names the step it belongs to, so the dry-run view can point
|
|
440
440
|
// at what could not be determined rather than listing loose strings.
|
|
@@ -145,6 +145,11 @@ export const deleteWorkflowScope = async (scope: { appId: string; scopeId: strin
|
|
|
145
145
|
DELETE FROM workflows.workflow WHERE app_id = ${scope.appId} AND scope_id = ${scope.scopeId} RETURNING id
|
|
146
146
|
`;
|
|
147
147
|
await tx`DELETE FROM workflows.event WHERE app_id = ${scope.appId} AND scope_id = ${scope.scopeId}`;
|
|
148
|
+
const [ai] = await tx<{ present: boolean }[]>`SELECT to_regclass('ai.workflow_scope_budget') IS NOT NULL AS present`;
|
|
149
|
+
if (ai?.present) {
|
|
150
|
+
await tx`DELETE FROM ai.workflow_scope_budget WHERE app_id = ${scope.appId} AND scope_id = ${scope.scopeId}`;
|
|
151
|
+
await tx`DELETE FROM ai.workflow_scope_admission WHERE app_id = ${scope.appId} AND scope_id = ${scope.scopeId}`;
|
|
152
|
+
}
|
|
148
153
|
return rows.length;
|
|
149
154
|
});
|
|
150
155
|
};
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
WorkflowCoordinatorPort,
|
|
25
25
|
WorkflowCoordinatorReleaseState,
|
|
26
26
|
} from "../runtime/coordinator";
|
|
27
|
-
import { WorkflowLeaseLostError } from "../runtime/executor";
|
|
27
|
+
import { WorkflowLeaseLostError, WorkflowRetryableStepError } from "../runtime/executor";
|
|
28
28
|
import type {
|
|
29
29
|
WorkflowHeartbeatOutcome,
|
|
30
30
|
WorkflowRestoredStep,
|
|
@@ -401,7 +401,7 @@ export const finishWorkflowRun = async (
|
|
|
401
401
|
*/
|
|
402
402
|
export const releaseWorkflowRun = async (
|
|
403
403
|
claim: { runId: string; executionGeneration: number; consecutiveFailures?: number },
|
|
404
|
-
options: { backoffMs?: number; db?: SQL } = {},
|
|
404
|
+
options: { backoffMs?: number; db?: SQL; error?: WorkflowJsonValue } = {},
|
|
405
405
|
): Promise<WorkflowCoordinatorReleaseState> => {
|
|
406
406
|
const db = options.db ?? sql;
|
|
407
407
|
const backoffMs =
|
|
@@ -413,6 +413,7 @@ export const releaseWorkflowRun = async (
|
|
|
413
413
|
lease_expires_at = NULL,
|
|
414
414
|
retry_after = now() + ${`${backoffMs} milliseconds`}::interval,
|
|
415
415
|
consecutive_failures = consecutive_failures + 1,
|
|
416
|
+
error = ${options.error ?? null},
|
|
416
417
|
updated_at = now()
|
|
417
418
|
WHERE id = ${claim.runId}
|
|
418
419
|
AND execution_generation = ${claim.executionGeneration}
|
|
@@ -545,7 +546,8 @@ export const createWorkflowCoordinatorPort = (options: {
|
|
|
545
546
|
claim: () => claimWorkflowRun(options),
|
|
546
547
|
renew: (claim) => renewWorkflowRunLease(claim, { leaseMs: options.leaseMs }),
|
|
547
548
|
finish: (claim, result) => finishWorkflowRun(claim, result),
|
|
548
|
-
release: (claim) =>
|
|
549
|
+
release: (claim, error) =>
|
|
550
|
+
releaseWorkflowRun(claim, { ...(error instanceof WorkflowRetryableStepError ? { error: error.executionError } : {}) }),
|
|
549
551
|
});
|
|
550
552
|
|
|
551
553
|
// ─── Scans ───────────────────────────────────────────────────────────────────
|
|
@@ -184,10 +184,15 @@ export const runOneWorkflow = async (options: WorkflowWorkerOptions & { runId?:
|
|
|
184
184
|
const incompatible = await validateCurrentModule(claim, options.module);
|
|
185
185
|
if (incompatible) return incompatible;
|
|
186
186
|
if (claim.consecutiveFailures >= WORKFLOW_RUN_MAX_CONSECUTIVE_FAILURES) {
|
|
187
|
+
const [prior] = await db<{ code: string | null; message: string | null }[]>`
|
|
188
|
+
SELECT error ->> 'code' AS code, error ->> 'message' AS message FROM workflows.run WHERE id = ${claim.runId}::uuid
|
|
189
|
+
`;
|
|
190
|
+
const aiCode = prior?.code?.startsWith("WORKFLOW_AI_") ? prior.code : null;
|
|
187
191
|
const error = {
|
|
188
|
-
code: "WORKFLOW_RETRY_EXHAUSTED",
|
|
189
|
-
message: `
|
|
192
|
+
code: aiCode ?? "WORKFLOW_RETRY_EXHAUSTED",
|
|
193
|
+
message: `${aiCode && prior?.message ? `${prior.message} ` : ""}Workflow failed ${claim.consecutiveFailures} consecutive attempts. Inspect the source or retry after capacity recovers.`,
|
|
190
194
|
retryable: false,
|
|
195
|
+
...(aiCode ? { details: { retryExhausted: true } } : {}),
|
|
191
196
|
};
|
|
192
197
|
await db`
|
|
193
198
|
UPDATE workflows.step_outcome AS s
|
package/src/ai/model-pricing.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
|
-
import { coreSettings } from "../services";
|
|
3
|
-
import { audit } from "../services/audit";
|
|
4
|
-
import { invalidateSettingsCache, set as setSetting } from "../services/settings";
|
|
5
|
-
import { decryptValue } from "../services/settings/crypto";
|
|
6
|
-
import { type AiModelPricing, AiModelPricingSchema } from "../shared/ai-costs";
|
|
7
|
-
import { AiQuotaError } from "./quotas";
|
|
8
|
-
import { parseAiModelProfiles } from "./settings";
|
|
9
|
-
|
|
10
|
-
/** Change prices only, preserving credentials and model access in their own stores. */
|
|
11
|
-
export async function setAiModelPricing(id: string, pricing: AiModelPricing | null, expected: AiModelPricing | null, actorId: string) {
|
|
12
|
-
if (pricing) AiModelPricingSchema.parse(pricing);
|
|
13
|
-
const key = "ai.model_profiles_json";
|
|
14
|
-
const fallback = await coreSettings.get<string>(key);
|
|
15
|
-
const result = await sql.begin(async (db) => {
|
|
16
|
-
const [row] = await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key=${key} FOR UPDATE`;
|
|
17
|
-
const raw = row ? await decryptValue(row.value) : fallback;
|
|
18
|
-
if (typeof raw !== "string") throw new Error("Invalid model configuration.");
|
|
19
|
-
const parsed = parseAiModelProfiles(raw);
|
|
20
|
-
if (parsed.error) throw new Error(parsed.error.message);
|
|
21
|
-
const profile = parsed.profiles.find((profile) => profile.id === id);
|
|
22
|
-
if (!profile) throw new AiQuotaError("quota_conflict", "Model no longer exists.");
|
|
23
|
-
if (profile.capabilities.includes("transcription")) throw new AiQuotaError("quota_conflict", "Audio pricing is not supported.");
|
|
24
|
-
if (JSON.stringify(profile.pricing ?? null) !== JSON.stringify(expected))
|
|
25
|
-
throw new AiQuotaError("quota_conflict", "Model prices changed. Read them again before updating.");
|
|
26
|
-
if (pricing) profile.pricing = pricing;
|
|
27
|
-
else delete profile.pricing;
|
|
28
|
-
await setSetting(key, JSON.stringify(parsed.profiles), db);
|
|
29
|
-
await audit.record(
|
|
30
|
-
{
|
|
31
|
-
action: "ai.model.pricing",
|
|
32
|
-
outcome: "allowed",
|
|
33
|
-
actor: { userId: actorId },
|
|
34
|
-
target: { type: "ai-model", id },
|
|
35
|
-
metadata: { pricing },
|
|
36
|
-
},
|
|
37
|
-
db,
|
|
38
|
-
);
|
|
39
|
-
return { id, pricing: profile.pricing ?? null };
|
|
40
|
-
});
|
|
41
|
-
await invalidateSettingsCache([key]);
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
|
-
import { coreSettings } from "../services";
|
|
3
|
-
import { audit } from "../services/audit";
|
|
4
|
-
import { invalidateSettingsCache, set as setSetting } from "../services/settings";
|
|
5
|
-
import { decryptValue } from "../services/settings/crypto";
|
|
6
|
-
import {
|
|
7
|
-
type AiModelRequestSettings,
|
|
8
|
-
type AiModelRequestSettingsUpdate,
|
|
9
|
-
AiModelRequestSettingsUpdateSchema,
|
|
10
|
-
} from "../shared/ai-model-request-settings";
|
|
11
|
-
import { AiQuotaError } from "./quotas";
|
|
12
|
-
import { getAiRequestHeaders, planAiProfileRequestHeaders, storeAiRequestHeaderPlan } from "./request-headers";
|
|
13
|
-
import { parseAiModelProfiles } from "./settings";
|
|
14
|
-
import type { AiModelProfile } from "./types";
|
|
15
|
-
|
|
16
|
-
const key = "ai.model_profiles_json";
|
|
17
|
-
export class AiModelRequestSettingsInvalid extends Error {}
|
|
18
|
-
|
|
19
|
-
const readSnapshot = async (id: string, fallback: string, db: typeof sql, lock = false) => {
|
|
20
|
-
const rows = lock
|
|
21
|
-
? await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key=${key} FOR UPDATE`
|
|
22
|
-
: await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key=${key}`;
|
|
23
|
-
const raw = rows[0] ? await decryptValue(rows[0].value) : fallback;
|
|
24
|
-
if (typeof raw !== "string") throw new AiModelRequestSettingsInvalid("Invalid model configuration.");
|
|
25
|
-
const parsed = parseAiModelProfiles(raw);
|
|
26
|
-
if (parsed.error) throw new AiModelRequestSettingsInvalid(parsed.error.fields?.[key] ?? parsed.error.message);
|
|
27
|
-
const profile = parsed.profiles.find((profile) => profile.id === id);
|
|
28
|
-
if (!profile) throw new AiQuotaError("quota_conflict", "Model no longer exists.");
|
|
29
|
-
const [headerRow] = await db<{ secret: string }[]>`SELECT secret FROM ai.model_request_headers WHERE profile_id=${id}`;
|
|
30
|
-
// Hash ciphertext, never secret values. Any settings/header change invalidates
|
|
31
|
-
// the snapshot, including replacement of a value under an unchanged name.
|
|
32
|
-
const revision = new Bun.CryptoHasher("sha256").update(JSON.stringify([rows[0]?.value ?? raw, headerRow?.secret ?? null])).digest("hex");
|
|
33
|
-
const requestHeaderNames = Object.keys(await getAiRequestHeaders(id, db)).sort();
|
|
34
|
-
return { profiles: parsed.profiles, profile, settings: present(profile, requestHeaderNames, revision) };
|
|
35
|
-
};
|
|
36
|
-
const present = (profile: AiModelProfile, requestHeaderNames: string[], revision: string): AiModelRequestSettings => ({
|
|
37
|
-
id: profile.id,
|
|
38
|
-
reasoningEffort: profile.reasoningEffort ?? null,
|
|
39
|
-
extraBody: profile.extraBody ?? null,
|
|
40
|
-
requestHeaderNames,
|
|
41
|
-
revision,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
export const getAiModelRequestSettings = async (id: string): Promise<AiModelRequestSettings> =>
|
|
45
|
-
(await readSnapshot(id, await coreSettings.get<string>(key), sql)).settings;
|
|
46
|
-
|
|
47
|
-
/** One transaction changes the non-secret profile and encrypted headers; audit contains names only. */
|
|
48
|
-
export const setAiModelRequestSettings = async (
|
|
49
|
-
id: string,
|
|
50
|
-
input: AiModelRequestSettingsUpdate,
|
|
51
|
-
actorId: string,
|
|
52
|
-
): Promise<AiModelRequestSettings> => {
|
|
53
|
-
const data = AiModelRequestSettingsUpdateSchema.parse(input);
|
|
54
|
-
const fallback = await coreSettings.get<string>(key);
|
|
55
|
-
const result = await sql.begin(async (db) => {
|
|
56
|
-
const current = await readSnapshot(id, fallback, db, true);
|
|
57
|
-
if (current.settings.revision !== data.expected)
|
|
58
|
-
throw new AiQuotaError("quota_conflict", "Model request settings changed. Read them again before updating.");
|
|
59
|
-
const nextProfile = { ...current.profile };
|
|
60
|
-
if (Object.hasOwn(data, "reasoningEffort")) {
|
|
61
|
-
if (data.reasoningEffort) nextProfile.reasoningEffort = data.reasoningEffort;
|
|
62
|
-
else delete nextProfile.reasoningEffort;
|
|
63
|
-
}
|
|
64
|
-
if (Object.hasOwn(data, "extraBody")) {
|
|
65
|
-
if (data.extraBody) nextProfile.extraBody = data.extraBody;
|
|
66
|
-
else delete nextProfile.extraBody;
|
|
67
|
-
}
|
|
68
|
-
const validated = parseAiModelProfiles(JSON.stringify([nextProfile]));
|
|
69
|
-
if (validated.error) throw new AiModelRequestSettingsInvalid(validated.error.fields?.[key] ?? validated.error.message);
|
|
70
|
-
const headers = data.clearRequestHeaders ? {} : await getAiRequestHeaders(id, db);
|
|
71
|
-
const plan = planAiProfileRequestHeaders({
|
|
72
|
-
currentProfiles: [current.profile],
|
|
73
|
-
nextProfiles: [nextProfile],
|
|
74
|
-
existingNames: { [id]: Object.keys(headers) },
|
|
75
|
-
submitted: data.requestHeaders === undefined ? [] : [{ profileId: id, patch: data.requestHeaders }],
|
|
76
|
-
});
|
|
77
|
-
if (plan.error) throw new AiModelRequestSettingsInvalid(plan.error);
|
|
78
|
-
// This endpoint owns one profile only. Never prune the other profiles.
|
|
79
|
-
const keepOthers = current.profiles.filter((profile) => profile.id !== id).map((profile) => profile.id);
|
|
80
|
-
plan.keepHeaderProfileIds = [...keepOthers, ...(data.clearRequestHeaders ? [] : plan.keepHeaderProfileIds)];
|
|
81
|
-
await storeAiRequestHeaderPlan(plan, db);
|
|
82
|
-
await setSetting(key, JSON.stringify(current.profiles.map((profile) => (profile.id === id ? validated.profiles[0] : profile))), db);
|
|
83
|
-
const saved = (await readSnapshot(id, fallback, db)).settings;
|
|
84
|
-
await audit.record(
|
|
85
|
-
{
|
|
86
|
-
action: "ai.model.request-settings",
|
|
87
|
-
outcome: "allowed",
|
|
88
|
-
actor: { userId: actorId },
|
|
89
|
-
target: { type: "ai-model", id },
|
|
90
|
-
metadata: { reasoningEffort: saved.reasoningEffort, extraBody: saved.extraBody, requestHeaderNames: saved.requestHeaderNames },
|
|
91
|
-
},
|
|
92
|
-
db,
|
|
93
|
-
);
|
|
94
|
-
return saved;
|
|
95
|
-
});
|
|
96
|
-
await invalidateSettingsCache([key]);
|
|
97
|
-
return result;
|
|
98
|
-
};
|