@oneuptime/common 11.3.24 → 11.3.26
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/Models/DatabaseModels/AIConversation.ts +322 -0
- package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
- package/Models/DatabaseModels/AIRun.ts +584 -0
- package/Models/DatabaseModels/AIRunEvent.ts +443 -0
- package/Models/DatabaseModels/Index.ts +8 -0
- package/Models/DatabaseModels/LlmLog.ts +26 -0
- package/Models/DatabaseModels/LlmProvider.ts +2 -2
- package/Server/API/AIChatAPI.ts +693 -0
- package/Server/API/LlmProviderAPI.ts +169 -0
- package/Server/API/MicrosoftTeamsAPI.ts +5 -0
- package/Server/API/SlackAPI.ts +663 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Services/AIConversationMessageService.ts +39 -0
- package/Server/Services/AIConversationService.ts +63 -0
- package/Server/Services/AIRunEventService.ts +39 -0
- package/Server/Services/AIRunService.ts +39 -0
- package/Server/Services/AIService.ts +53 -31
- package/Server/Services/DatabaseService.ts +19 -0
- package/Server/Services/LlmProviderService.ts +110 -0
- package/Server/Services/ProjectService.ts +24 -0
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
- package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1054 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
- package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
- package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
- package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
- package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +204 -0
- package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
- package/Server/Utils/AI/Toolbox/Index.ts +228 -0
- package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
- package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
- package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +257 -0
- package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
- package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
- package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
- package/Server/Utils/LLM/LLMService.ts +502 -92
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
- package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
- package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
- package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
- package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +308 -0
- package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
- package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +155 -0
- package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
- package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
- package/Types/AI/AIChatMessageRole.ts +6 -0
- package/Types/AI/AIChatMessageStatus.ts +32 -0
- package/Types/AI/AIChatPermissionMode.ts +69 -0
- package/Types/AI/AIChatTypes.ts +231 -0
- package/Types/AI/AIRunEventType.ts +17 -0
- package/Types/AI/AIRunStatus.ts +28 -0
- package/Types/AI/AIRunType.ts +6 -0
- package/Types/LLM/LlmType.ts +6 -0
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
- package/UI/Components/Page/Page.tsx +14 -0
- package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
- package/UI/Utils/TestLLMProvider.ts +59 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +8 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
- package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
- package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +498 -0
- package/build/dist/Server/API/AIChatAPI.js.map +1 -0
- package/build/dist/Server/API/LlmProviderAPI.js +107 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +442 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
- package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
- package/build/dist/Server/Services/AIConversationService.js +42 -0
- package/build/dist/Server/Services/AIConversationService.js.map +1 -0
- package/build/dist/Server/Services/AIRunEventService.js +34 -0
- package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
- package/build/dist/Server/Services/AIRunService.js +34 -0
- package/build/dist/Server/Services/AIRunService.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +31 -23
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +108 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +745 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +167 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +150 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +188 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +372 -88
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
- package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
- package/build/dist/Types/AI/AIChatTypes.js +74 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
- package/build/dist/Types/AI/AIRunEventType.js +18 -0
- package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
- package/build/dist/Types/AI/AIRunStatus.js +26 -0
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
- package/build/dist/Types/AI/AIRunType.js +7 -0
- package/build/dist/Types/AI/AIRunType.js.map +1 -0
- package/build/dist/Types/LLM/LlmType.js +6 -0
- package/build/dist/Types/LLM/LlmType.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +3 -1
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
- package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
- package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
- package/package.json +1 -1
|
@@ -9,6 +9,8 @@ import LLMService, {
|
|
|
9
9
|
LLMProviderConfig,
|
|
10
10
|
LLMCompletionResponse,
|
|
11
11
|
LLMMessage,
|
|
12
|
+
LLMToolCall,
|
|
13
|
+
LLMToolDefinition,
|
|
12
14
|
} from "../Utils/LLM/LLMService";
|
|
13
15
|
import LlmProvider from "../../Models/DatabaseModels/LlmProvider";
|
|
14
16
|
import LlmLog from "../../Models/DatabaseModels/LlmLog";
|
|
@@ -20,18 +22,32 @@ import logger, { LogAttributes } from "../Utils/Logger";
|
|
|
20
22
|
|
|
21
23
|
export interface AILogRequest {
|
|
22
24
|
projectId: ObjectID;
|
|
23
|
-
userId?: ObjectID;
|
|
25
|
+
userId?: ObjectID | undefined;
|
|
24
26
|
feature: string; // e.g., "IncidentPostmortem", "IncidentNote"
|
|
25
27
|
incidentId?: ObjectID;
|
|
26
28
|
alertId?: ObjectID;
|
|
27
29
|
scheduledMaintenanceId?: ObjectID;
|
|
30
|
+
aiRunId?: ObjectID;
|
|
31
|
+
/*
|
|
32
|
+
* When set, use this specific provider (validated against the project) rather
|
|
33
|
+
* than the project default. Powers the in-chat provider/model switcher.
|
|
34
|
+
*/
|
|
35
|
+
llmProviderId?: ObjectID | undefined;
|
|
28
36
|
messages: Array<LLMMessage>;
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
tools?: Array<LLMToolDefinition> | undefined;
|
|
38
|
+
maxTokens?: number | undefined;
|
|
39
|
+
temperature?: number | undefined;
|
|
40
|
+
/*
|
|
41
|
+
* When false, prompt/response previews are NOT persisted to LlmLog.
|
|
42
|
+
* Use for features whose content is private to a single user (e.g. AI
|
|
43
|
+
* chat) — LlmLog is readable by all project members.
|
|
44
|
+
*/
|
|
45
|
+
storeContentPreviews?: boolean | undefined;
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
export interface AILogResponse {
|
|
34
49
|
content: string;
|
|
50
|
+
toolCalls?: Array<LLMToolCall> | undefined;
|
|
35
51
|
llmLog: LlmLog;
|
|
36
52
|
}
|
|
37
53
|
|
|
@@ -46,9 +62,12 @@ export class Service extends BaseService {
|
|
|
46
62
|
): Promise<AILogResponse> {
|
|
47
63
|
const startTime: Date = new Date();
|
|
48
64
|
|
|
49
|
-
// Get LLM provider for the project
|
|
65
|
+
// Get LLM provider for the project (honoring an explicit per-chat choice).
|
|
50
66
|
const llmProvider: LlmProvider | null =
|
|
51
|
-
await LlmProviderService.
|
|
67
|
+
await LlmProviderService.getProviderForChat({
|
|
68
|
+
projectId: request.projectId,
|
|
69
|
+
llmProviderId: request.llmProviderId,
|
|
70
|
+
});
|
|
52
71
|
|
|
53
72
|
if (!llmProvider) {
|
|
54
73
|
throw new BadDataException(
|
|
@@ -67,12 +86,18 @@ export class Service extends BaseService {
|
|
|
67
86
|
logEntry.projectId = request.projectId;
|
|
68
87
|
logEntry.isGlobalProvider = llmProvider.isGlobalLlm || false;
|
|
69
88
|
logEntry.feature = request.feature;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
89
|
+
|
|
90
|
+
const storeContentPreviews: boolean =
|
|
91
|
+
request.storeContentPreviews !== false;
|
|
92
|
+
|
|
93
|
+
logEntry.requestPrompt = storeContentPreviews
|
|
94
|
+
? request.messages
|
|
95
|
+
.map((m: LLMMessage) => {
|
|
96
|
+
return m.content;
|
|
97
|
+
})
|
|
98
|
+
.join("\n")
|
|
99
|
+
.substring(0, 5000) // Store first 5000 chars
|
|
100
|
+
: "[Redacted — this content is private to the requesting user]";
|
|
76
101
|
logEntry.requestStartedAt = startTime;
|
|
77
102
|
|
|
78
103
|
// Set optional fields only if they have values
|
|
@@ -100,6 +125,9 @@ export class Service extends BaseService {
|
|
|
100
125
|
if (request.scheduledMaintenanceId) {
|
|
101
126
|
logEntry.scheduledMaintenanceId = request.scheduledMaintenanceId;
|
|
102
127
|
}
|
|
128
|
+
if (request.aiRunId) {
|
|
129
|
+
logEntry.aiRunId = request.aiRunId;
|
|
130
|
+
}
|
|
103
131
|
|
|
104
132
|
// Check if billing should apply
|
|
105
133
|
const shouldBill: boolean =
|
|
@@ -153,6 +181,8 @@ export class Service extends BaseService {
|
|
|
153
181
|
llmProviderConfig: llmConfig,
|
|
154
182
|
messages: request.messages,
|
|
155
183
|
temperature: request.temperature ?? 0.7,
|
|
184
|
+
maxTokens: request.maxTokens,
|
|
185
|
+
tools: request.tools,
|
|
156
186
|
});
|
|
157
187
|
|
|
158
188
|
const endTime: Date = new Date();
|
|
@@ -160,7 +190,9 @@ export class Service extends BaseService {
|
|
|
160
190
|
// Update log with success info
|
|
161
191
|
logEntry.status = LlmLogStatus.Success;
|
|
162
192
|
logEntry.totalTokens = response.usage?.totalTokens || 0;
|
|
163
|
-
logEntry.responsePreview =
|
|
193
|
+
logEntry.responsePreview = storeContentPreviews
|
|
194
|
+
? response.content.substring(0, 2000) // Store first 2000 chars
|
|
195
|
+
: "[Redacted — this content is private to the requesting user]";
|
|
164
196
|
logEntry.requestCompletedAt = endTime;
|
|
165
197
|
logEntry.durationMs = endTime.getTime() - startTime.getTime();
|
|
166
198
|
|
|
@@ -176,27 +208,16 @@ export class Service extends BaseService {
|
|
|
176
208
|
|
|
177
209
|
// Deduct from project balance
|
|
178
210
|
if (totalCost > 0) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
/*
|
|
212
|
+
* Atomic decrement — concurrent LLM calls within and across chat
|
|
213
|
+
* turns must not lose each other's deductions (a read-modify-write
|
|
214
|
+
* here silently forgave overlapping spend).
|
|
215
|
+
*/
|
|
216
|
+
await ProjectService.deductAiBalanceInUSDCents({
|
|
217
|
+
projectId: request.projectId,
|
|
218
|
+
amountInUSDCents: totalCost,
|
|
183
219
|
});
|
|
184
220
|
|
|
185
|
-
if (project) {
|
|
186
|
-
const newBalance: number = Math.max(
|
|
187
|
-
0,
|
|
188
|
-
(project.aiCurrentBalanceInUSDCents || 0) - totalCost,
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
await ProjectService.updateOneById({
|
|
192
|
-
id: request.projectId,
|
|
193
|
-
data: {
|
|
194
|
-
aiCurrentBalanceInUSDCents: newBalance,
|
|
195
|
-
},
|
|
196
|
-
props: { isRoot: true },
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
221
|
// Check if auto-recharge is needed (do this async, don't wait)
|
|
201
222
|
AIBillingService.rechargeIfBalanceIsLow(request.projectId).catch(
|
|
202
223
|
(err: Error) => {
|
|
@@ -221,6 +242,7 @@ export class Service extends BaseService {
|
|
|
221
242
|
|
|
222
243
|
return {
|
|
223
244
|
content: response.content,
|
|
245
|
+
toolCalls: response.toolCalls,
|
|
224
246
|
llmLog: savedLog,
|
|
225
247
|
};
|
|
226
248
|
} catch (error) {
|
|
@@ -2106,6 +2106,25 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
|
2106
2106
|
);
|
|
2107
2107
|
}
|
|
2108
2108
|
|
|
2109
|
+
/*
|
|
2110
|
+
* Atomically subtract `value` from a numeric column in a single UPDATE
|
|
2111
|
+
* (SET col = col - value) so concurrent callers never lose each other's
|
|
2112
|
+
* writes the way a read-modify-write would. The column can go negative;
|
|
2113
|
+
* callers that gate on a non-negative balance simply reject the next
|
|
2114
|
+
* request rather than silently forgiving the overage.
|
|
2115
|
+
*/
|
|
2116
|
+
protected async atomicDecrementColumnValueBy(data: {
|
|
2117
|
+
id: ObjectID;
|
|
2118
|
+
columnName: keyof TBaseModel;
|
|
2119
|
+
value: number;
|
|
2120
|
+
}): Promise<void> {
|
|
2121
|
+
await this.getRepository().decrement(
|
|
2122
|
+
{ _id: data.id.toString() } as any,
|
|
2123
|
+
data.columnName as string,
|
|
2124
|
+
data.value,
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2109
2128
|
@CaptureSpan()
|
|
2110
2129
|
public async searchBy({
|
|
2111
2130
|
skip,
|
|
@@ -6,6 +6,7 @@ import ObjectID from "../../Types/ObjectID";
|
|
|
6
6
|
import UpdateBy from "../Types/Database/UpdateBy";
|
|
7
7
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
8
8
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
9
|
+
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
9
10
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
10
11
|
|
|
11
12
|
export class Service extends DatabaseService<Model> {
|
|
@@ -153,6 +154,115 @@ export class Service extends DatabaseService<Model> {
|
|
|
153
154
|
|
|
154
155
|
return null;
|
|
155
156
|
}
|
|
157
|
+
|
|
158
|
+
/*
|
|
159
|
+
* Resolve the provider to use for a chat turn. When the user has explicitly
|
|
160
|
+
* chosen a provider (llmProviderId), use it — but only if it is actually
|
|
161
|
+
* usable by this project: either a global provider, or one owned by the
|
|
162
|
+
* project. Otherwise fall back to the project default / global provider.
|
|
163
|
+
* A stale id (e.g. the chosen provider was deleted) also falls back, so a
|
|
164
|
+
* conversation never breaks because its provider went away.
|
|
165
|
+
*/
|
|
166
|
+
@CaptureSpan()
|
|
167
|
+
public async getProviderForChat(data: {
|
|
168
|
+
projectId: ObjectID;
|
|
169
|
+
llmProviderId?: ObjectID | undefined;
|
|
170
|
+
}): Promise<Model | null> {
|
|
171
|
+
if (data.llmProviderId) {
|
|
172
|
+
const provider: Model | null = await this.findOneBy({
|
|
173
|
+
query: {
|
|
174
|
+
_id: data.llmProviderId.toString(),
|
|
175
|
+
},
|
|
176
|
+
select: {
|
|
177
|
+
_id: true,
|
|
178
|
+
name: true,
|
|
179
|
+
llmType: true,
|
|
180
|
+
apiKey: true,
|
|
181
|
+
baseUrl: true,
|
|
182
|
+
modelName: true,
|
|
183
|
+
isGlobalLlm: true,
|
|
184
|
+
projectId: true,
|
|
185
|
+
costPerMillionTokensInUSDCents: true,
|
|
186
|
+
},
|
|
187
|
+
props: {
|
|
188
|
+
isRoot: true,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (provider) {
|
|
193
|
+
const isGlobal: boolean = provider.isGlobalLlm === true;
|
|
194
|
+
const belongsToProject: boolean =
|
|
195
|
+
provider.projectId?.toString() === data.projectId.toString();
|
|
196
|
+
|
|
197
|
+
if (isGlobal || belongsToProject) {
|
|
198
|
+
return provider;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Fall through to default resolution when the id is invalid/inaccessible.
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this.getLLMProviderForProject(data.projectId);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/*
|
|
208
|
+
* The providers a project member can pick from in the chat provider switcher:
|
|
209
|
+
* every provider configured for the project plus every global provider.
|
|
210
|
+
* Secrets (apiKey) are never selected here — this only feeds the picker UI.
|
|
211
|
+
*/
|
|
212
|
+
@CaptureSpan()
|
|
213
|
+
public async getSelectableProvidersForProject(
|
|
214
|
+
projectId: ObjectID,
|
|
215
|
+
): Promise<Array<Model>> {
|
|
216
|
+
const [projectProviders, globalProviders]: [Array<Model>, Array<Model>] =
|
|
217
|
+
await Promise.all([
|
|
218
|
+
this.findBy({
|
|
219
|
+
query: {
|
|
220
|
+
projectId: projectId,
|
|
221
|
+
},
|
|
222
|
+
select: {
|
|
223
|
+
_id: true,
|
|
224
|
+
name: true,
|
|
225
|
+
llmType: true,
|
|
226
|
+
modelName: true,
|
|
227
|
+
isDefault: true,
|
|
228
|
+
isGlobalLlm: true,
|
|
229
|
+
},
|
|
230
|
+
sort: {
|
|
231
|
+
isDefault: SortOrder.Descending,
|
|
232
|
+
name: SortOrder.Ascending,
|
|
233
|
+
},
|
|
234
|
+
skip: 0,
|
|
235
|
+
limit: LIMIT_MAX,
|
|
236
|
+
props: {
|
|
237
|
+
isRoot: true,
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
this.findBy({
|
|
241
|
+
query: {
|
|
242
|
+
projectId: QueryHelper.isNull(),
|
|
243
|
+
isGlobalLlm: true,
|
|
244
|
+
},
|
|
245
|
+
select: {
|
|
246
|
+
_id: true,
|
|
247
|
+
name: true,
|
|
248
|
+
llmType: true,
|
|
249
|
+
modelName: true,
|
|
250
|
+
isDefault: true,
|
|
251
|
+
isGlobalLlm: true,
|
|
252
|
+
},
|
|
253
|
+
sort: {
|
|
254
|
+
name: SortOrder.Ascending,
|
|
255
|
+
},
|
|
256
|
+
skip: 0,
|
|
257
|
+
limit: LIMIT_MAX,
|
|
258
|
+
props: {
|
|
259
|
+
isRoot: true,
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
return [...projectProviders, ...globalProviders];
|
|
265
|
+
}
|
|
156
266
|
}
|
|
157
267
|
|
|
158
268
|
export default new Service();
|
|
@@ -1915,6 +1915,30 @@ export class ProjectService extends DatabaseService<Model> {
|
|
|
1915
1915
|
);
|
|
1916
1916
|
}
|
|
1917
1917
|
|
|
1918
|
+
@CaptureSpan()
|
|
1919
|
+
/*
|
|
1920
|
+
* Atomically deduct AI spend from the project's balance. A single
|
|
1921
|
+
* SET aiCurrentBalanceInUSDCents = aiCurrentBalanceInUSDCents - amount
|
|
1922
|
+
* so concurrent AI runs (many LLM calls per turn, several turns per
|
|
1923
|
+
* project) can't clobber each other's deductions the way a
|
|
1924
|
+
* read-then-write would. No-op for non-positive amounts.
|
|
1925
|
+
*/
|
|
1926
|
+
@CaptureSpan()
|
|
1927
|
+
public async deductAiBalanceInUSDCents(data: {
|
|
1928
|
+
projectId: ObjectID;
|
|
1929
|
+
amountInUSDCents: number;
|
|
1930
|
+
}): Promise<void> {
|
|
1931
|
+
if (!data.amountInUSDCents || data.amountInUSDCents <= 0) {
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
await this.atomicDecrementColumnValueBy({
|
|
1936
|
+
id: data.projectId,
|
|
1937
|
+
columnName: "aiCurrentBalanceInUSDCents",
|
|
1938
|
+
value: data.amountInUSDCents,
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1918
1942
|
@CaptureSpan()
|
|
1919
1943
|
public async incrementAndGetIncidentCounter(
|
|
1920
1944
|
projectId: ObjectID,
|
|
@@ -703,18 +703,31 @@ export default class ModelPermission {
|
|
|
703
703
|
* scale to telemetry volume; one Postgres roundtrip + one indexed
|
|
704
704
|
* predicate does. See Internal/Docs/PermissionsSimplification.md.
|
|
705
705
|
*/
|
|
706
|
-
|
|
706
|
+
/*
|
|
707
|
+
* Resolves the owned-scope decision for an analytics model without touching
|
|
708
|
+
* a query. Returns `null` when no ownership filter applies (root/master,
|
|
709
|
+
* creates, models without @OwnedThrough, no applicable permission rows, or
|
|
710
|
+
* an unrestricted grant) — i.e. the caller has full access. Otherwise
|
|
711
|
+
* returns the FK column plus the exact set of parent resource IDs the user
|
|
712
|
+
* may read (possibly empty, meaning "access to nothing").
|
|
713
|
+
*
|
|
714
|
+
* This is the single source of truth for owned-scope resolution:
|
|
715
|
+
* addOwnedScopeToQuery injects it into a ClickHouse query, and
|
|
716
|
+
* getAccessibleServiceIdsForAnalyticsModel exposes it to callers that build
|
|
717
|
+
* their own aggregation SQL (e.g. the AI toolbox), so the two can never
|
|
718
|
+
* drift apart.
|
|
719
|
+
*/
|
|
720
|
+
private static async resolveOwnedScope<TBaseModel extends BaseModel>(
|
|
707
721
|
modelType: { new (): TBaseModel },
|
|
708
|
-
query: Query<TBaseModel>,
|
|
709
722
|
props: DatabaseCommonInteractionProps,
|
|
710
723
|
type: DatabaseRequestType,
|
|
711
|
-
): Promise<
|
|
724
|
+
): Promise<{ fkColumn: string; allowedResourceIds: Set<string> } | null> {
|
|
712
725
|
if (props.isRoot || props.isMasterAdmin) {
|
|
713
|
-
return
|
|
726
|
+
return null;
|
|
714
727
|
}
|
|
715
728
|
|
|
716
729
|
if (type === DatabaseRequestType.Create) {
|
|
717
|
-
return
|
|
730
|
+
return null;
|
|
718
731
|
}
|
|
719
732
|
|
|
720
733
|
/*
|
|
@@ -725,7 +738,7 @@ export default class ModelPermission {
|
|
|
725
738
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
726
739
|
const model: any = new modelType();
|
|
727
740
|
if (!model.ownedThrough) {
|
|
728
|
-
return
|
|
741
|
+
return null;
|
|
729
742
|
}
|
|
730
743
|
|
|
731
744
|
const modelPermissions: Array<Permission> = this.getModelPermissions(
|
|
@@ -748,7 +761,7 @@ export default class ModelPermission {
|
|
|
748
761
|
);
|
|
749
762
|
|
|
750
763
|
if (applicableRows.length === 0) {
|
|
751
|
-
return
|
|
764
|
+
return null;
|
|
752
765
|
}
|
|
753
766
|
|
|
754
767
|
/*
|
|
@@ -771,7 +784,7 @@ export default class ModelPermission {
|
|
|
771
784
|
},
|
|
772
785
|
);
|
|
773
786
|
if (hasUnrestrictedGrant) {
|
|
774
|
-
return
|
|
787
|
+
return null;
|
|
775
788
|
}
|
|
776
789
|
|
|
777
790
|
const allowedResourceIds: Set<string> = new Set<string>();
|
|
@@ -831,7 +844,59 @@ export default class ModelPermission {
|
|
|
831
844
|
allowedResourceIds.add(props.tenantId.toString());
|
|
832
845
|
}
|
|
833
846
|
|
|
834
|
-
|
|
847
|
+
return { fkColumn: model.ownedThrough.fkColumn, allowedResourceIds };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/*
|
|
851
|
+
* The set of parent resource (Service) IDs the user may read for an analytics
|
|
852
|
+
* model, for callers that bypass findBy() and build aggregation SQL directly
|
|
853
|
+
* (e.g. the AI toolbox's log_histogram / query_traces, which call the
|
|
854
|
+
* aggregation services with a raw projectId). Pass the result as the
|
|
855
|
+
* aggregation's `serviceIds` filter.
|
|
856
|
+
*
|
|
857
|
+
* `null` means no ownership restriction applies — do NOT constrain (the user
|
|
858
|
+
* has project-wide access). A returned array (even empty) means the user is
|
|
859
|
+
* label/owned-restricted: constrain to exactly these IDs, and treat an empty
|
|
860
|
+
* array as "no accessible services" (match nothing), NEVER as "no filter".
|
|
861
|
+
*/
|
|
862
|
+
public static async getAccessibleServiceIdsForAnalyticsModel<
|
|
863
|
+
TBaseModel extends BaseModel,
|
|
864
|
+
>(
|
|
865
|
+
modelType: { new (): TBaseModel },
|
|
866
|
+
props: DatabaseCommonInteractionProps,
|
|
867
|
+
type: DatabaseRequestType,
|
|
868
|
+
): Promise<Array<ObjectID> | null> {
|
|
869
|
+
const scope: {
|
|
870
|
+
fkColumn: string;
|
|
871
|
+
allowedResourceIds: Set<string>;
|
|
872
|
+
} | null = await this.resolveOwnedScope(modelType, props, type);
|
|
873
|
+
|
|
874
|
+
if (!scope) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return Array.from(scope.allowedResourceIds).map((id: string) => {
|
|
879
|
+
return new ObjectID(id);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private static async addOwnedScopeToQuery<TBaseModel extends BaseModel>(
|
|
884
|
+
modelType: { new (): TBaseModel },
|
|
885
|
+
query: Query<TBaseModel>,
|
|
886
|
+
props: DatabaseCommonInteractionProps,
|
|
887
|
+
type: DatabaseRequestType,
|
|
888
|
+
): Promise<Query<TBaseModel>> {
|
|
889
|
+
const scope: {
|
|
890
|
+
fkColumn: string;
|
|
891
|
+
allowedResourceIds: Set<string>;
|
|
892
|
+
} | null = await this.resolveOwnedScope(modelType, props, type);
|
|
893
|
+
|
|
894
|
+
if (!scope) {
|
|
895
|
+
return query;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const fkColumn: string = scope.fkColumn;
|
|
899
|
+
const allowedResourceIds: Set<string> = scope.allowedResourceIds;
|
|
835
900
|
const sentinelNoMatch: Array<string> = [
|
|
836
901
|
ObjectID.getZeroObjectID().toString(),
|
|
837
902
|
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
2
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* AI conversations (and their messages, runs and run events) are personal to
|
|
6
|
+
* the user who created them. This filter pins every non-root query to the
|
|
7
|
+
* requesting user so one project member can never read another member's
|
|
8
|
+
* conversations — including through the auto-generated list/count endpoints.
|
|
9
|
+
*/
|
|
10
|
+
export function pinQueryToRequestingUser<TQuery>(
|
|
11
|
+
query: TQuery,
|
|
12
|
+
props: DatabaseCommonInteractionProps,
|
|
13
|
+
userColumnName: string,
|
|
14
|
+
): TQuery {
|
|
15
|
+
if (props.isRoot || props.isMasterAdmin) {
|
|
16
|
+
return query;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!props.userId) {
|
|
20
|
+
throw new NotAuthorizedException(
|
|
21
|
+
"AI conversations are personal and can only be accessed by the user who created them.",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
(query as Record<string, unknown>)[userColumnName] = props.userId;
|
|
26
|
+
|
|
27
|
+
return query;
|
|
28
|
+
}
|