@oneuptime/common 11.3.26 → 11.3.28

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.
Files changed (241) hide show
  1. package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
  2. package/Models/DatabaseModels/IncidentMember.ts +1 -1
  3. package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
  4. package/Models/DatabaseModels/Index.ts +3 -0
  5. package/Models/DatabaseModels/MigrationFailure.ts +170 -0
  6. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
  7. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
  8. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
  9. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
  10. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
  11. package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
  12. package/Server/API/AlertAPI.ts +1 -1
  13. package/Server/API/IncidentAPI.ts +2 -2
  14. package/Server/API/IncidentEpisodeAPI.ts +1 -1
  15. package/Server/API/LlmProviderAPI.ts +1 -0
  16. package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  22. package/Server/Infrastructure/PostgresDatabase.ts +11 -0
  23. package/Server/Services/AIService.ts +74 -2
  24. package/Server/Services/AnalyticsDatabaseService.ts +10 -1
  25. package/Server/Services/DatabaseService.ts +10 -1
  26. package/Server/Services/IncidentService.ts +1 -1
  27. package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
  28. package/Server/Services/IncomingCallPolicyService.ts +42 -1
  29. package/Server/Services/Index.ts +2 -0
  30. package/Server/Services/MigrationFailureService.ts +9 -0
  31. package/Server/Services/MonitorProbeService.ts +33 -7
  32. package/Server/Services/MonitorService.ts +19 -7
  33. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  34. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
  35. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
  36. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
  37. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
  38. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
  39. package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
  40. package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
  41. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
  42. package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
  43. package/Server/Services/TeamMemberService.ts +6 -0
  44. package/Server/Services/UserNotificationRuleService.ts +13 -42
  45. package/Server/Services/UserOnCallLogService.ts +90 -0
  46. package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
  47. package/Server/Types/Database/QueryUtil.ts +57 -0
  48. package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
  49. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
  50. package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
  51. package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
  52. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
  53. package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
  54. package/Server/Utils/AI/Toolbox/Index.ts +4 -1
  55. package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
  56. package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
  57. package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
  58. package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
  59. package/Server/Utils/LLM/LLMService.ts +110 -11
  60. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
  61. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
  62. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
  63. package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
  64. package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
  65. package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
  66. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
  67. package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
  68. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +78 -0
  69. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -0
  70. package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
  71. package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
  72. package/Tests/Types/Events/Recurring.test.ts +22 -9
  73. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
  74. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
  75. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
  76. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
  77. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
  78. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
  79. package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
  80. package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
  81. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
  82. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
  83. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
  84. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
  85. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
  86. package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
  87. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
  88. package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
  89. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
  90. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
  91. package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
  92. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
  93. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
  94. package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
  95. package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
  96. package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
  97. package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
  98. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
  99. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
  100. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
  101. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
  102. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
  103. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
  104. package/Types/Date.ts +200 -15
  105. package/Types/OnCallDutyPolicy/Layer.ts +790 -260
  106. package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
  107. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
  108. package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
  109. package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
  110. package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
  111. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
  112. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
  114. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
  116. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  118. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
  120. package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
  121. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
  122. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
  124. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  125. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
  126. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  127. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
  128. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
  129. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
  130. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  131. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
  132. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
  133. package/build/dist/Server/API/AlertAPI.js +1 -1
  134. package/build/dist/Server/API/IncidentAPI.js +2 -2
  135. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  136. package/build/dist/Server/API/LlmProviderAPI.js +1 -0
  137. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  138. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  139. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  149. package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
  150. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  151. package/build/dist/Server/Services/AIService.js +53 -2
  152. package/build/dist/Server/Services/AIService.js.map +1 -1
  153. package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
  154. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  155. package/build/dist/Server/Services/DatabaseService.js +10 -1
  156. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  157. package/build/dist/Server/Services/IncidentService.js +1 -1
  158. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
  159. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
  160. package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
  161. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  162. package/build/dist/Server/Services/Index.js +2 -0
  163. package/build/dist/Server/Services/Index.js.map +1 -1
  164. package/build/dist/Server/Services/MigrationFailureService.js +9 -0
  165. package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
  166. package/build/dist/Server/Services/MonitorProbeService.js +33 -7
  167. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  168. package/build/dist/Server/Services/MonitorService.js +12 -5
  169. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  170. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
  171. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  172. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
  173. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  174. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
  175. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  176. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
  177. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  178. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
  179. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  180. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
  181. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  182. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
  183. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  184. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
  185. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
  186. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
  187. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  188. package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
  189. package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
  190. package/build/dist/Server/Services/TeamMemberService.js +6 -0
  191. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  192. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
  193. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  194. package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
  195. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  196. package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
  197. package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
  198. package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
  199. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  200. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
  201. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
  202. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +13 -2
  203. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  204. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
  205. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
  206. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
  207. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
  208. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
  209. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
  210. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +152 -1
  211. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  212. package/build/dist/Server/Utils/AI/Toolbox/Index.js +4 -1
  213. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  214. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
  215. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
  216. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
  217. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
  218. package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
  219. package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
  220. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
  221. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
  222. package/build/dist/Server/Utils/LLM/LLMService.js +81 -5
  223. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  224. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
  225. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  226. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
  227. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  228. package/build/dist/Types/Date.js +153 -15
  229. package/build/dist/Types/Date.js.map +1 -1
  230. package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
  231. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  232. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
  233. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
  234. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
  235. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  236. package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
  237. package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
  238. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  239. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
  240. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
  241. package/package.json +1 -1
@@ -49,6 +49,13 @@ export interface LLMUsage {
49
49
  promptTokens: number;
50
50
  completionTokens: number;
51
51
  totalTokens: number;
52
+ /*
53
+ * Prompt-caching breakdown, when the provider reports it. cachedInputTokens
54
+ * are input tokens served from cache (billed at a large discount);
55
+ * cacheCreationTokens are input tokens written to the cache on this call.
56
+ */
57
+ cachedInputTokens?: number | undefined;
58
+ cacheCreationTokens?: number | undefined;
52
59
  }
53
60
 
54
61
  export interface LLMCompletionResponse {
@@ -237,6 +244,17 @@ export default class LLMService {
237
244
  const toolCalls: Array<LLMToolCall> | undefined =
238
245
  this.parseOpenAIToolCalls(message);
239
246
 
247
+ /*
248
+ * OpenAI (and Azure OpenAI) automatically cache a stable prompt prefix
249
+ * once it is long enough and report the cache hit under
250
+ * prompt_tokens_details.cached_tokens — surface it for cost visibility.
251
+ */
252
+ const cachedTokens: number | undefined = usage
253
+ ? ((usage["prompt_tokens_details"] as JSONObject | undefined)?.[
254
+ "cached_tokens"
255
+ ] as number | undefined)
256
+ : undefined;
257
+
240
258
  return {
241
259
  content: (message["content"] as string) || "",
242
260
  toolCalls: toolCalls,
@@ -246,6 +264,7 @@ export default class LLMService {
246
264
  promptTokens: usage["prompt_tokens"] as number,
247
265
  completionTokens: usage["completion_tokens"] as number,
248
266
  totalTokens: usage["total_tokens"] as number,
267
+ cachedInputTokens: cachedTokens || undefined,
249
268
  }
250
269
  : undefined,
251
270
  };
@@ -565,6 +584,32 @@ export default class LLMService {
565
584
  continue;
566
585
  }
567
586
 
587
+ /*
588
+ * Anthropic requires strictly alternating user/assistant turns and
589
+ * returns a 400 on two consecutive same-role messages. The agent loop
590
+ * can legitimately emit back-to-back user turns (e.g. a tool_result
591
+ * user message immediately followed by the "budget exhausted, answer
592
+ * now" nudge), so coalesce a run of same-role messages into one instead
593
+ * of failing the whole request.
594
+ */
595
+ const previousMessage: JSONObject | undefined =
596
+ anthropicMessages[anthropicMessages.length - 1];
597
+
598
+ if (previousMessage && previousMessage["role"] === msg.role) {
599
+ if (typeof previousMessage["content"] === "string") {
600
+ previousMessage["content"] =
601
+ `${previousMessage["content"] as string}\n\n${msg.content}`;
602
+ continue;
603
+ }
604
+ if (Array.isArray(previousMessage["content"])) {
605
+ (previousMessage["content"] as Array<JSONObject>).push({
606
+ type: "text",
607
+ text: msg.content,
608
+ });
609
+ continue;
610
+ }
611
+ }
612
+
568
613
  anthropicMessages.push({
569
614
  role: msg.role,
570
615
  content: msg.content,
@@ -602,18 +647,43 @@ export default class LLMService {
602
647
  max_tokens: request.maxTokens || LLMService.ANTHROPIC_DEFAULT_MAX_TOKENS,
603
648
  };
604
649
 
650
+ /*
651
+ * Prompt caching. The system prompt and tool definitions are the large,
652
+ * stable prefix re-sent on every turn of the agent loop, so caching them is
653
+ * the biggest single cost + latency lever (cached input is billed at ~10%).
654
+ * An ephemeral cache_control breakpoint on the system block and on the LAST
655
+ * tool caches the whole system + tools prefix. cache_control is GA under
656
+ * anthropic-version 2023-06-01, so no beta header is required.
657
+ */
605
658
  if (systemMessage) {
606
- requestData["system"] = systemMessage;
659
+ requestData["system"] = [
660
+ {
661
+ type: "text",
662
+ text: systemMessage,
663
+ cache_control: { type: "ephemeral" },
664
+ },
665
+ ];
607
666
  }
608
667
 
609
668
  if (request.tools && request.tools.length > 0) {
610
- requestData["tools"] = request.tools.map((tool: LLMToolDefinition) => {
611
- return {
612
- name: tool.name,
613
- description: tool.description,
614
- input_schema: tool.inputSchema,
615
- };
616
- });
669
+ const anthropicTools: Array<JSONObject> = request.tools.map(
670
+ (tool: LLMToolDefinition) => {
671
+ return {
672
+ name: tool.name,
673
+ description: tool.description,
674
+ input_schema: tool.inputSchema,
675
+ };
676
+ },
677
+ );
678
+
679
+ // A breakpoint on the last tool caches the entire tools block before it.
680
+ const lastTool: JSONObject | undefined =
681
+ anthropicTools[anthropicTools.length - 1];
682
+ if (lastTool) {
683
+ lastTool["cache_control"] = { type: "ephemeral" };
684
+ }
685
+
686
+ requestData["tools"] = anthropicTools;
617
687
  }
618
688
 
619
689
  const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
@@ -685,10 +755,21 @@ export default class LLMService {
685
755
  stopReason: jsonData["stop_reason"] === "tool_use" ? "tool_use" : "stop",
686
756
  usage: usage
687
757
  ? {
688
- promptTokens: usage["input_tokens"] as number,
689
- completionTokens: usage["output_tokens"] as number,
758
+ promptTokens: (usage["input_tokens"] as number) || 0,
759
+ completionTokens: (usage["output_tokens"] as number) || 0,
760
+ /*
761
+ * Anthropic reports cache-read and cache-creation input tokens
762
+ * separately from input_tokens. Surface them and fold them into
763
+ * totalTokens so the logged total reflects the full billable input.
764
+ */
765
+ cachedInputTokens:
766
+ (usage["cache_read_input_tokens"] as number) || undefined,
767
+ cacheCreationTokens:
768
+ (usage["cache_creation_input_tokens"] as number) || undefined,
690
769
  totalTokens:
691
770
  ((usage["input_tokens"] as number) || 0) +
771
+ ((usage["cache_read_input_tokens"] as number) || 0) +
772
+ ((usage["cache_creation_input_tokens"] as number) || 0) +
692
773
  ((usage["output_tokens"] as number) || 0),
693
774
  }
694
775
  : undefined,
@@ -812,11 +893,29 @@ export default class LLMService {
812
893
  });
813
894
  }
814
895
 
896
+ /*
897
+ * Ollama reports token counts on the final /api/chat response as
898
+ * prompt_eval_count (input) and eval_count (output). Populate usage from
899
+ * them so LlmLog, the AI dashboards and (costed self-hosted Ollama)
900
+ * billing are not silently blind to token spend.
901
+ */
902
+ const ollamaPromptTokens: number =
903
+ (jsonData["prompt_eval_count"] as number) || 0;
904
+ const ollamaCompletionTokens: number =
905
+ (jsonData["eval_count"] as number) || 0;
906
+
815
907
  return {
816
908
  content: (message["content"] as string) || "",
817
909
  toolCalls: toolCalls,
818
910
  stopReason: toolCalls && toolCalls.length > 0 ? "tool_use" : "stop",
819
- usage: undefined, // Ollama doesn't provide token usage in the same way
911
+ usage:
912
+ ollamaPromptTokens || ollamaCompletionTokens
913
+ ? {
914
+ promptTokens: ollamaPromptTokens,
915
+ completionTokens: ollamaCompletionTokens,
916
+ totalTokens: ollamaPromptTokens + ollamaCompletionTokens,
917
+ }
918
+ : undefined,
820
919
  };
821
920
  }
822
921
  }
@@ -141,31 +141,34 @@ function parseUnixNanoToDate(
141
141
  value: string | number | undefined,
142
142
  context: string,
143
143
  ): Date {
144
- let numericValue: number = OneUptimeDate.getCurrentDateAsUnixNano();
145
-
146
144
  if (value !== undefined && value !== null) {
147
145
  try {
148
146
  if (typeof value === "string") {
149
- const parsed: number = Number.parseFloat(value);
150
- if (isNaN(parsed)) {
147
+ const trimmed: string = value.trim();
148
+ if (trimmed === "" || isNaN(Number(trimmed))) {
151
149
  throw new Error(`Invalid timestamp string: ${value}`);
152
150
  }
153
- numericValue = parsed;
154
- } else if (typeof value === "number") {
151
+ /*
152
+ * Hand the raw nanosecond string to fromUnixNano so it can divide
153
+ * with BigInt — nanosecond epochs exceed Number.MAX_SAFE_INTEGER, so
154
+ * parsing to a float here would drop up to a full millisecond.
155
+ */
156
+ return OneUptimeDate.fromUnixNano(trimmed);
157
+ }
158
+ if (typeof value === "number") {
155
159
  if (!Number.isFinite(value)) {
156
160
  throw new Error(`Invalid timestamp number: ${value}`);
157
161
  }
158
- numericValue = value;
162
+ return OneUptimeDate.fromUnixNano(value);
159
163
  }
160
164
  } catch (error) {
161
165
  logger.warn(
162
166
  `Error processing ${context}: ${error instanceof Error ? error.message : String(error)}, using current time`,
163
167
  );
164
- numericValue = OneUptimeDate.getCurrentDateAsUnixNano();
165
168
  }
166
169
  }
167
170
 
168
- return OneUptimeDate.fromUnixNano(numericValue);
171
+ return OneUptimeDate.getCurrentDate();
169
172
  }
170
173
 
171
174
  // Same trim-or-null read contract as OtelIngestBaseService.getStringAttribute.
@@ -242,31 +242,34 @@ function parseUnixNanoToDate(
242
242
  value: string | number | undefined,
243
243
  context: string,
244
244
  ): Date {
245
- let numericValue: number = OneUptimeDate.getCurrentDateAsUnixNano();
246
-
247
245
  if (value !== undefined && value !== null) {
248
246
  try {
249
247
  if (typeof value === "string") {
250
- const parsed: number = Number.parseFloat(value);
251
- if (isNaN(parsed)) {
248
+ const trimmed: string = value.trim();
249
+ if (trimmed === "" || isNaN(Number(trimmed))) {
252
250
  throw new Error(`Invalid timestamp string: ${value}`);
253
251
  }
254
- numericValue = parsed;
255
- } else if (typeof value === "number") {
252
+ /*
253
+ * Hand the raw nanosecond string to fromUnixNano so it can divide
254
+ * with BigInt — nanosecond epochs exceed Number.MAX_SAFE_INTEGER, so
255
+ * parsing to a float here would drop up to a full millisecond.
256
+ */
257
+ return OneUptimeDate.fromUnixNano(trimmed);
258
+ }
259
+ if (typeof value === "number") {
256
260
  if (!Number.isFinite(value)) {
257
261
  throw new Error(`Invalid timestamp number: ${value}`);
258
262
  }
259
- numericValue = value;
263
+ return OneUptimeDate.fromUnixNano(value);
260
264
  }
261
265
  } catch (error) {
262
266
  logger.warn(
263
267
  `Error processing ${context}: ${error instanceof Error ? error.message : String(error)}, using current time`,
264
268
  );
265
- numericValue = OneUptimeDate.getCurrentDateAsUnixNano();
266
269
  }
267
270
  }
268
271
 
269
- return OneUptimeDate.fromUnixNano(numericValue);
272
+ return OneUptimeDate.getCurrentDate();
270
273
  }
271
274
 
272
275
  // Same trim-or-null read contract as OtelIngestBaseService.getStringAttribute.
@@ -234,14 +234,6 @@ describe("AnalyticsDatabaseService", () => {
234
234
  jest.spyOn(logger, "error").mockImplementation(() => {
235
235
  return undefined!;
236
236
  });
237
-
238
- /*
239
- * The base getException method is async and throws without await in the
240
- * catch block, creating unhandled rejections. Override to throw synchronously.
241
- */
242
- (service as any).getException = (error: Error): never => {
243
- throw error;
244
- };
245
237
  });
246
238
 
247
239
  afterEach(() => {
@@ -0,0 +1,233 @@
1
+ import OnCallDutyPolicyScheduleLayerUserService from "../../../Server/Services/OnCallDutyPolicyScheduleLayerUserService";
2
+ import Model from "../../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import logger from "../../../Server/Utils/Logger";
5
+ import { describe, expect, test, afterEach } from "@jest/globals";
6
+
7
+ /*
8
+ * OnCallDutyPolicyScheduleLayerUserService.onBeforeUpdate re-sequences the
9
+ * `order` of the other rows in a schedule layer when a user is dragged to a new
10
+ * position. Audit finding L3: the "moving down" branch (newOrder > currentOrder)
11
+ * decremented EVERY row with `order <= newOrder`, including rows ABOVE the moved
12
+ * user. Dragging a user downward therefore drove the top row's order to 0 (and
13
+ * negative after repeated down-drags) and opened a gap at 1, breaking the
14
+ * 1-based contiguous invariant that create-default (count+1) and delete
15
+ * re-sequencing depend on.
16
+ *
17
+ * The fix adds the missing lower bound `resource.order > currentOrder`, so only
18
+ * rows strictly between the old and new position shift — mirroring the
19
+ * double-bounded "moving up" branch (`order >= newOrder && order < currentOrder`).
20
+ *
21
+ * These tests stub the three persistence helpers onBeforeUpdate calls
22
+ * (findOneBy -> moved row's current order + layer id; findBy -> every row in the
23
+ * layer; updateOneBy -> the per-row shift) and CAPTURE each updateOneBy so we can
24
+ * assert exactly which rows moved and to what order. No Postgres involved.
25
+ */
26
+
27
+ const service: any = OnCallDutyPolicyScheduleLayerUserService as any;
28
+
29
+ // A schedule layer id shared by every row in these single-layer scenarios.
30
+ const LAYER_ID: ObjectID = new ObjectID("layer1");
31
+
32
+ const USER_A: ObjectID = new ObjectID("A");
33
+ const USER_B: ObjectID = new ObjectID("B");
34
+ const USER_C: ObjectID = new ObjectID("C");
35
+
36
+ // A captured updateOneBy shift: which row and what order it was set to.
37
+ interface CapturedShift {
38
+ id: string;
39
+ order: number;
40
+ }
41
+
42
+ // Build a fake persisted row for the layer.
43
+ function row(id: ObjectID, order: number): Model {
44
+ return {
45
+ _id: id,
46
+ order,
47
+ onCallDutyPolicyScheduleLayerId: LAYER_ID,
48
+ } as unknown as Model;
49
+ }
50
+
51
+ // Build the UpdateBy passed to onBeforeUpdate for moving `movedId` to `newOrder`.
52
+ function updateBy(movedId: ObjectID, newOrder: number): any {
53
+ return {
54
+ data: { order: newOrder },
55
+ query: { _id: movedId },
56
+ props: {},
57
+ } as any;
58
+ }
59
+
60
+ /*
61
+ * Stub findOneBy (moved row lookup), findBy (all rows in the layer) and
62
+ * updateOneBy (record the shift). Returns the array captured shifts are pushed
63
+ * into so each test can inspect exactly what moved.
64
+ */
65
+ function stubService(
66
+ movedRow: Model,
67
+ layerRows: Array<Model>,
68
+ ): Array<CapturedShift> {
69
+ const captured: Array<CapturedShift> = [];
70
+
71
+ jest.spyOn(service, "findOneBy").mockResolvedValue(movedRow as never);
72
+
73
+ jest.spyOn(service, "findBy").mockResolvedValue(layerRows as never);
74
+
75
+ jest
76
+ .spyOn(service, "updateOneBy")
77
+ .mockImplementation((...args: Array<unknown>): Promise<void> => {
78
+ const updateArg: {
79
+ query: { _id: ObjectID };
80
+ data: { order: number };
81
+ } = args[0] as {
82
+ query: { _id: ObjectID };
83
+ data: { order: number };
84
+ };
85
+ captured.push({
86
+ id: updateArg.query._id.toString(),
87
+ order: updateArg.data.order,
88
+ });
89
+ return Promise.resolve();
90
+ });
91
+
92
+ return captured;
93
+ }
94
+
95
+ describe("OnCallDutyPolicyScheduleLayerUserService.onBeforeUpdate reorder", () => {
96
+ afterEach(() => {
97
+ jest.restoreAllMocks();
98
+ });
99
+
100
+ /*
101
+ * ----------------------------------------------------------------------- *
102
+ * 1. MOVE DOWN B: order 2 -> 3. Only C (3 -> 2) shifts; A is untouched.
103
+ * -----------------------------------------------------------------------
104
+ */
105
+ test("moving B down (2 -> 3) decrements ONLY C and never touches A", async () => {
106
+ const captured: Array<CapturedShift> = stubService(row(USER_B, 2), [
107
+ row(USER_A, 1),
108
+ row(USER_B, 2),
109
+ row(USER_C, 3),
110
+ ]);
111
+
112
+ await service.onBeforeUpdate(updateBy(USER_B, 3));
113
+
114
+ // Exactly one row shifted, and it was C going from 3 down to 2.
115
+ expect(captured).toHaveLength(1);
116
+ expect(captured[0]).toEqual({ id: USER_C.toString(), order: 2 });
117
+
118
+ // A (the top row) must never be decremented — the regression.
119
+ const aShifts: Array<CapturedShift> = captured.filter(
120
+ (c: CapturedShift): boolean => {
121
+ return c.id === USER_A.toString();
122
+ },
123
+ );
124
+ expect(aShifts).toHaveLength(0);
125
+
126
+ /*
127
+ * After the shift, with the outer update setting B = 3, the resulting layer
128
+ * is contiguous {A:1, C:2, B:3}.
129
+ */
130
+ const finalOrders: Record<string, number> = {
131
+ [USER_A.toString()]: 1, // untouched
132
+ [USER_C.toString()]: 2, // shifted
133
+ [USER_B.toString()]: 3, // set by the outer update
134
+ };
135
+ expect(finalOrders[USER_A.toString()]).toBe(1);
136
+ expect(finalOrders[USER_C.toString()]).toBe(2);
137
+ expect(finalOrders[USER_B.toString()]).toBe(3);
138
+ });
139
+
140
+ /*
141
+ * ----------------------------------------------------------------------- *
142
+ * 2. MOVE UP C: order 3 -> 1. A (1 -> 2) and B (2 -> 3) increment; C untouched.
143
+ * -----------------------------------------------------------------------
144
+ */
145
+ test("moving C up (3 -> 1) increments ONLY A and B", async () => {
146
+ const captured: Array<CapturedShift> = stubService(row(USER_C, 3), [
147
+ row(USER_A, 1),
148
+ row(USER_B, 2),
149
+ row(USER_C, 3),
150
+ ]);
151
+
152
+ await service.onBeforeUpdate(updateBy(USER_C, 1));
153
+
154
+ expect(captured).toHaveLength(2);
155
+
156
+ const byId: Record<string, number> = {};
157
+ for (const shift of captured) {
158
+ byId[shift.id] = shift.order;
159
+ }
160
+
161
+ expect(byId[USER_A.toString()]).toBe(2);
162
+ expect(byId[USER_B.toString()]).toBe(3);
163
+ // C itself is not part of the shift loop; the outer update sets it to 1.
164
+ expect(byId[USER_C.toString()]).toBeUndefined();
165
+
166
+ // Contiguous {C:1, A:2, B:3}.
167
+ const finalOrders: Record<string, number> = {
168
+ [USER_C.toString()]: 1,
169
+ [USER_A.toString()]: 2,
170
+ [USER_B.toString()]: 3,
171
+ };
172
+ expect(Object.values(finalOrders).sort()).toEqual([1, 2, 3]);
173
+ });
174
+
175
+ /*
176
+ * ----------------------------------------------------------------------- *
177
+ * 3. MOVE DOWN A: order 1 -> 3 (dragging the TOP row all the way down).
178
+ * Only B (2 -> 1) and C (3 -> 2) decrement; A is not touched by the loop.
179
+ * -----------------------------------------------------------------------
180
+ */
181
+ test("moving the top row A down (1 -> 3) decrements ONLY B and C, no row hits 0", async () => {
182
+ const captured: Array<CapturedShift> = stubService(row(USER_A, 1), [
183
+ row(USER_A, 1),
184
+ row(USER_B, 2),
185
+ row(USER_C, 3),
186
+ ]);
187
+
188
+ await service.onBeforeUpdate(updateBy(USER_A, 3));
189
+
190
+ expect(captured).toHaveLength(2);
191
+
192
+ const byId: Record<string, number> = {};
193
+ for (const shift of captured) {
194
+ byId[shift.id] = shift.order;
195
+ }
196
+
197
+ expect(byId[USER_B.toString()]).toBe(1);
198
+ expect(byId[USER_C.toString()]).toBe(2);
199
+ // A is the moved row itself — the shift loop must not decrement it.
200
+ expect(byId[USER_A.toString()]).toBeUndefined();
201
+
202
+ // Nothing collapsed to 0 or negative.
203
+ for (const shift of captured) {
204
+ expect(shift.order).toBeGreaterThan(0);
205
+ }
206
+ });
207
+
208
+ /*
209
+ * ----------------------------------------------------------------------- *
210
+ * 4. Regression: in a move-down, NO captured shift may set an order <= 0.
211
+ * Before the fix, dragging B down drove A's order to 0. This asserts the
212
+ * invariant across the whole move-down code path.
213
+ * -----------------------------------------------------------------------
214
+ */
215
+ test("regression: no move-down shift ever sets order <= 0", async () => {
216
+ const captured: Array<CapturedShift> = stubService(row(USER_B, 2), [
217
+ row(USER_A, 1),
218
+ row(USER_B, 2),
219
+ row(USER_C, 3),
220
+ ]);
221
+
222
+ await service.onBeforeUpdate(updateBy(USER_B, 3));
223
+
224
+ for (const shift of captured) {
225
+ expect(shift.order).toBeGreaterThan(0);
226
+ }
227
+ });
228
+ });
229
+
230
+ // Keep any incidental logger.error noise out of the test output.
231
+ jest.spyOn(logger, "error").mockImplementation((): void => {
232
+ return undefined;
233
+ });