@oneuptime/common 11.5.7 → 11.5.9

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 (212) hide show
  1. package/Models/DatabaseModels/AIAgentTaskPullRequest.ts +15 -6
  2. package/Models/DatabaseModels/AIRun.ts +68 -0
  3. package/Models/DatabaseModels/AIRunEvent.ts +34 -1
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +215 -0
  5. package/Models/DatabaseModels/Project.ts +25 -0
  6. package/Models/DatabaseModels/StatusPage.ts +42 -0
  7. package/Server/API/AIAgentTaskAPI.ts +38 -12
  8. package/Server/API/AIAgentTaskLogAPI.ts +49 -0
  9. package/Server/API/AIChatAPI.ts +16 -0
  10. package/Server/API/AIReadinessAPI.ts +84 -0
  11. package/Server/API/CodeFixRunAPI.ts +190 -57
  12. package/Server/API/TelemetryExceptionAPI.ts +6 -1
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.ts +41 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.ts +95 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.ts +52 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.ts +30 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.ts +49 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  19. package/Server/Middleware/MasterAdminAuthorization.ts +1 -1
  20. package/Server/Middleware/ProjectAuthorization.ts +1 -1
  21. package/Server/Services/AIRunEventService.ts +15 -1
  22. package/Server/Services/AIRunService.ts +48 -12
  23. package/Server/Services/LlmProviderService.ts +2 -0
  24. package/Server/Services/ProjectService.ts +42 -0
  25. package/Server/Services/StatusPageService.ts +87 -0
  26. package/Server/Services/TelemetryExceptionService.ts +19 -90
  27. package/Server/Types/Markdown.ts +9 -9
  28. package/Server/Types/MarkdownSlugify.ts +47 -0
  29. package/Server/Utils/AI/AIRunPrivacyFilter.ts +106 -0
  30. package/Server/Utils/AI/AIRunTranscript.ts +240 -0
  31. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +9 -0
  32. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +119 -5
  33. package/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.ts +75 -0
  34. package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +235 -0
  35. package/Server/Utils/AI/SRE/Insights/FixRouting.ts +2 -3
  36. package/Server/Utils/AI/Toolbox/CodeTools.ts +883 -0
  37. package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +519 -0
  38. package/Server/Utils/AI/Toolbox/Index.ts +27 -0
  39. package/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.ts +279 -0
  40. package/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.ts +19 -1
  41. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +600 -0
  42. package/Server/Utils/CodeRepository/StackTraceRepoResolver.ts +7 -2
  43. package/Server/Utils/LLM/LLMService.ts +397 -18
  44. package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +4 -11
  45. package/Tests/Models/StatusPageEnableMcpServer.test.ts +125 -0
  46. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +223 -0
  47. package/Tests/Server/Services/TelemetryExceptionAIFixReadiness.test.ts +26 -3
  48. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +12 -0
  49. package/Tests/Server/Types/Markdown.test.ts +106 -0
  50. package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +295 -0
  51. package/Tests/Server/Utils/AI/AIRunTranscript.test.ts +182 -0
  52. package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +411 -0
  53. package/Tests/Server/Utils/AI/CodeTools.test.ts +596 -0
  54. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +475 -0
  55. package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +548 -0
  56. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +142 -0
  57. package/Tests/Server/Utils/AI/ScheduledMaintenanceTools.test.ts +176 -0
  58. package/Tests/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.test.ts +63 -0
  59. package/Tests/Server/Utils/GitHubGetFileContent.test.ts +197 -0
  60. package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +502 -0
  61. package/Tests/Types/AI/AIAgentTaskStatus.test.ts +7 -6
  62. package/Tests/Types/AI/AIChatPageContext.test.ts +0 -0
  63. package/Tests/Types/AI/AIRunStatus.test.ts +42 -0
  64. package/Tests/Types/Log/LogSeverity.test.ts +78 -0
  65. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +167 -0
  66. package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +66 -0
  67. package/Tests/UI/Utils/AIChatExport/ChatWidgetData.test.ts +319 -0
  68. package/Tests/UI/Utils/AIChatExport/ChatWidgetMarkdown.test.ts +188 -0
  69. package/Types/AI/AIAgentTaskStatus.ts +13 -0
  70. package/Types/AI/AIChatPageContext.ts +137 -0
  71. package/Types/AI/AIChatTypes.ts +44 -0
  72. package/Types/AI/AIFixReadiness.ts +46 -0
  73. package/Types/AI/AIRunStatus.ts +11 -0
  74. package/Types/Email/EmailTemplateType.ts +2 -0
  75. package/Types/Log/LogSeverity.ts +61 -0
  76. package/Types/Monitor/SnmpMonitor/SnmpVersion.ts +40 -0
  77. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -2
  78. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +3 -1
  79. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +8 -2
  80. package/UI/Components/EditionLabel/EditionLabel.tsx +821 -331
  81. package/UI/Components/Forms/BasicForm.tsx +26 -8
  82. package/UI/Styles/Theme.css +1314 -0
  83. package/UI/Utils/AIChatExport/ChatWidgetData.ts +316 -0
  84. package/UI/Utils/AIChatExport/ChatWidgetMarkdown.ts +349 -0
  85. package/UI/Utils/AIChatExport/ConversationMarkdown.ts +177 -0
  86. package/UI/Utils/AIChatExport/MarkdownBlocks.ts +327 -0
  87. package/UI/Utils/AIChatExport/MarkdownSafety.ts +215 -0
  88. package/UI/Utils/DownloadFile.ts +54 -0
  89. package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js +15 -7
  90. package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/AIRun.js +69 -0
  92. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  93. package/build/dist/Models/DatabaseModels/AIRunEvent.js +31 -0
  94. package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +225 -0
  96. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/Project.js +27 -0
  98. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/StatusPage.js +43 -0
  100. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  101. package/build/dist/Server/API/AIAgentTaskAPI.js +37 -11
  102. package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
  103. package/build/dist/Server/API/AIAgentTaskLogAPI.js +32 -7
  104. package/build/dist/Server/API/AIAgentTaskLogAPI.js.map +1 -1
  105. package/build/dist/Server/API/AIChatAPI.js +9 -0
  106. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  107. package/build/dist/Server/API/AIReadinessAPI.js +57 -0
  108. package/build/dist/Server/API/AIReadinessAPI.js.map +1 -0
  109. package/build/dist/Server/API/CodeFixRunAPI.js +165 -51
  110. package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
  111. package/build/dist/Server/API/TelemetryExceptionAPI.js +4 -0
  112. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js +36 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js.map +1 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js +80 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js +37 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js +23 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js +22 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js.map +1 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  125. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +1 -1
  126. package/build/dist/Server/Middleware/ProjectAuthorization.js +1 -1
  127. package/build/dist/Server/Services/AIRunEventService.js +8 -0
  128. package/build/dist/Server/Services/AIRunEventService.js.map +1 -1
  129. package/build/dist/Server/Services/AIRunService.js +39 -3
  130. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  131. package/build/dist/Server/Services/LlmProviderService.js +2 -0
  132. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  133. package/build/dist/Server/Services/ProjectService.js +39 -0
  134. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  135. package/build/dist/Server/Services/StatusPageService.js +81 -0
  136. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  137. package/build/dist/Server/Services/TelemetryExceptionService.js +14 -61
  138. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  139. package/build/dist/Server/Types/Markdown.js +8 -8
  140. package/build/dist/Server/Types/Markdown.js.map +1 -1
  141. package/build/dist/Server/Types/MarkdownSlugify.js +47 -0
  142. package/build/dist/Server/Types/MarkdownSlugify.js.map +1 -0
  143. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +82 -0
  144. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -0
  145. package/build/dist/Server/Utils/AI/AIRunTranscript.js +180 -0
  146. package/build/dist/Server/Utils/AI/AIRunTranscript.js.map +1 -0
  147. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +1 -0
  148. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  149. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +103 -5
  150. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  151. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js +55 -0
  152. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js.map +1 -1
  153. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +211 -0
  154. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -0
  155. package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
  156. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +654 -0
  157. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -0
  158. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +389 -0
  159. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -0
  160. package/build/dist/Server/Utils/AI/Toolbox/Index.js +19 -0
  161. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  162. package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js +239 -0
  163. package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js.map +1 -0
  164. package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js +19 -1
  165. package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js.map +1 -1
  166. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +439 -0
  167. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  168. package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js +1 -1
  169. package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js.map +1 -1
  170. package/build/dist/Server/Utils/LLM/LLMService.js +275 -19
  171. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  172. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +2 -11
  173. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
  174. package/build/dist/Types/AI/AIAgentTaskStatus.js +12 -0
  175. package/build/dist/Types/AI/AIAgentTaskStatus.js.map +1 -1
  176. package/build/dist/Types/AI/AIChatPageContext.js +101 -0
  177. package/build/dist/Types/AI/AIChatPageContext.js.map +1 -0
  178. package/build/dist/Types/AI/AIChatTypes.js +2 -0
  179. package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
  180. package/build/dist/Types/AI/AIFixReadiness.js +9 -0
  181. package/build/dist/Types/AI/AIFixReadiness.js.map +1 -0
  182. package/build/dist/Types/AI/AIRunStatus.js +11 -0
  183. package/build/dist/Types/AI/AIRunStatus.js.map +1 -1
  184. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  185. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  186. package/build/dist/Types/Log/LogSeverity.js +56 -0
  187. package/build/dist/Types/Log/LogSeverity.js.map +1 -1
  188. package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js +38 -0
  189. package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js.map +1 -1
  190. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +9 -2
  191. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  192. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +3 -1
  193. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  194. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +9 -2
  195. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  196. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +424 -139
  197. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  198. package/build/dist/UI/Components/Forms/BasicForm.js +24 -6
  199. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  200. package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js +217 -0
  201. package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js.map +1 -0
  202. package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js +234 -0
  203. package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js.map +1 -0
  204. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +111 -0
  205. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -0
  206. package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js +233 -0
  207. package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js.map +1 -0
  208. package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js +134 -0
  209. package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js.map +1 -0
  210. package/build/dist/UI/Utils/DownloadFile.js +41 -0
  211. package/build/dist/UI/Utils/DownloadFile.js.map +1 -0
  212. package/package.json +5 -1
@@ -0,0 +1,548 @@
1
+ import API from "../../../../Utils/API";
2
+ import logger from "../../../../Server/Utils/Logger";
3
+ import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
4
+ import HTTPResponse from "../../../../Types/API/HTTPResponse";
5
+ import { JSONObject } from "../../../../Types/JSON";
6
+ import LlmType from "../../../../Types/LLM/LlmType";
7
+ import LLMService, {
8
+ LLMProviderConfig,
9
+ } from "../../../../Server/Utils/LLM/LLMService";
10
+ import {
11
+ afterEach,
12
+ beforeEach,
13
+ describe,
14
+ expect,
15
+ jest,
16
+ test,
17
+ } from "@jest/globals";
18
+
19
+ type PostSpy = ReturnType<typeof jest.spyOn>;
20
+
21
+ const SUCCESS_BODY: JSONObject = {
22
+ choices: [{ message: { content: "OK" } }],
23
+ usage: { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 },
24
+ };
25
+
26
+ function successResponse(): HTTPResponse<JSONObject> {
27
+ return { jsonData: SUCCESS_BODY } as unknown as HTTPResponse<JSONObject>;
28
+ }
29
+
30
+ /** The verbatim 400 an o-series / gpt-5 deployment returns for max_tokens. */
31
+ function maxTokensUnsupportedResponse(): HTTPErrorResponse {
32
+ return new HTTPErrorResponse(
33
+ 400,
34
+ {
35
+ error: {
36
+ message:
37
+ "Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.",
38
+ type: "invalid_request_error",
39
+ param: "max_tokens",
40
+ code: "unsupported_parameter",
41
+ },
42
+ },
43
+ {},
44
+ );
45
+ }
46
+
47
+ /** Older models / Azure api-versions before 2024-08-01-preview. param and code are null. */
48
+ function maxCompletionTokensUnrecognizedResponse(): HTTPErrorResponse {
49
+ return new HTTPErrorResponse(
50
+ 400,
51
+ {
52
+ error: {
53
+ message:
54
+ "Unrecognized request argument supplied: max_completion_tokens",
55
+ type: "invalid_request_error",
56
+ param: null,
57
+ code: null,
58
+ },
59
+ },
60
+ {},
61
+ );
62
+ }
63
+
64
+ function temperatureUnsupportedResponse(): HTTPErrorResponse {
65
+ return new HTTPErrorResponse(
66
+ 400,
67
+ {
68
+ error: {
69
+ message:
70
+ "Unsupported value: 'temperature' does not support 0 with this model. Only the default (1) value is supported.",
71
+ type: "invalid_request_error",
72
+ param: "temperature",
73
+ code: "unsupported_value",
74
+ },
75
+ },
76
+ {},
77
+ );
78
+ }
79
+
80
+ const AZURE_CONFIG: LLMProviderConfig = {
81
+ llmType: LlmType.AzureOpenAI,
82
+ apiKey: "azure-key",
83
+ baseUrl: "https://example.openai.azure.com/openai/deployments/my-deployment",
84
+ // Azure sends the *deployment* name, which need not resemble the model.
85
+ modelName: "my-deployment",
86
+ };
87
+
88
+ function bodyOf(spy: PostSpy, callIndex: number): JSONObject {
89
+ return (spy.mock.calls[callIndex]![0] as { data: JSONObject }).data;
90
+ }
91
+
92
+ beforeEach(() => {
93
+ LLMService.clearRequestAdaptationCache();
94
+ jest.spyOn(logger, "debug").mockImplementation((): void => {});
95
+ jest.spyOn(logger, "error").mockImplementation((): void => {});
96
+ });
97
+
98
+ afterEach(() => {
99
+ jest.restoreAllMocks();
100
+ });
101
+
102
+ describe("LLMService — Azure OpenAI max_tokens compatibility", () => {
103
+ test("retries with max_completion_tokens when the deployment rejects max_tokens", async () => {
104
+ const spy: PostSpy = jest
105
+ .spyOn(API, "post")
106
+ .mockResolvedValueOnce(maxTokensUnsupportedResponse())
107
+ .mockResolvedValueOnce(successResponse()) as PostSpy;
108
+
109
+ const response: { content: string } = await LLMService.getCompletion({
110
+ llmProviderConfig: AZURE_CONFIG,
111
+ messages: [{ role: "user", content: "Reply with the word: OK" }],
112
+ maxTokens: 16,
113
+ });
114
+
115
+ expect(response.content).toBe("OK");
116
+ expect(spy).toHaveBeenCalledTimes(2);
117
+
118
+ // First attempt used the legacy spelling...
119
+ expect(bodyOf(spy, 0)["max_tokens"]).toBe(16);
120
+ expect(bodyOf(spy, 0)["max_completion_tokens"]).toBeUndefined();
121
+
122
+ // ...the retry swapped it, preserving the cap and dropping the legacy key.
123
+ expect(bodyOf(spy, 1)["max_completion_tokens"]).toBe(16);
124
+ expect(bodyOf(spy, 1)["max_tokens"]).toBeUndefined();
125
+ });
126
+
127
+ test("drops temperature when the deployment rejects it, after the token swap", async () => {
128
+ const spy: PostSpy = jest
129
+ .spyOn(API, "post")
130
+ .mockResolvedValueOnce(maxTokensUnsupportedResponse())
131
+ .mockResolvedValueOnce(temperatureUnsupportedResponse())
132
+ .mockResolvedValueOnce(successResponse()) as PostSpy;
133
+
134
+ const response: { content: string } = await LLMService.getCompletion({
135
+ llmProviderConfig: AZURE_CONFIG,
136
+ messages: [{ role: "user", content: "hi" }],
137
+ temperature: 0,
138
+ maxTokens: 16,
139
+ });
140
+
141
+ expect(response.content).toBe("OK");
142
+ expect(spy).toHaveBeenCalledTimes(3);
143
+
144
+ // Errors arrive one param at a time, so the fixes accumulate.
145
+ expect(bodyOf(spy, 1)["max_completion_tokens"]).toBe(16);
146
+ expect(bodyOf(spy, 1)["temperature"]).toBe(0);
147
+
148
+ expect(bodyOf(spy, 2)["max_completion_tokens"]).toBe(16);
149
+ expect(bodyOf(spy, 2)["temperature"]).toBeUndefined();
150
+ });
151
+
152
+ test("remembers what the deployment accepted so later calls do not re-probe", async () => {
153
+ const spy: PostSpy = jest
154
+ .spyOn(API, "post")
155
+ .mockResolvedValueOnce(maxTokensUnsupportedResponse())
156
+ .mockResolvedValue(successResponse()) as PostSpy;
157
+
158
+ await LLMService.getCompletion({
159
+ llmProviderConfig: AZURE_CONFIG,
160
+ messages: [{ role: "user", content: "hi" }],
161
+ maxTokens: 16,
162
+ });
163
+ expect(spy).toHaveBeenCalledTimes(2);
164
+
165
+ await LLMService.getCompletion({
166
+ llmProviderConfig: AZURE_CONFIG,
167
+ messages: [{ role: "user", content: "hi again" }],
168
+ maxTokens: 32,
169
+ });
170
+
171
+ // One extra call only: the second completion got the shape right first try.
172
+ expect(spy).toHaveBeenCalledTimes(3);
173
+ expect(bodyOf(spy, 2)["max_completion_tokens"]).toBe(32);
174
+ expect(bodyOf(spy, 2)["max_tokens"]).toBeUndefined();
175
+ });
176
+
177
+ test("re-probes when the model name changes", async () => {
178
+ const spy: PostSpy = jest
179
+ .spyOn(API, "post")
180
+ .mockResolvedValueOnce(maxTokensUnsupportedResponse())
181
+ .mockResolvedValue(successResponse()) as PostSpy;
182
+
183
+ await LLMService.getCompletion({
184
+ llmProviderConfig: AZURE_CONFIG,
185
+ messages: [{ role: "user", content: "hi" }],
186
+ maxTokens: 16,
187
+ });
188
+ expect(spy).toHaveBeenCalledTimes(2);
189
+
190
+ await LLMService.getCompletion({
191
+ llmProviderConfig: { ...AZURE_CONFIG, modelName: "other-deployment" },
192
+ messages: [{ role: "user", content: "hi" }],
193
+ maxTokens: 16,
194
+ });
195
+
196
+ // A different deployment is a different model: back to the legacy default.
197
+ expect(bodyOf(spy, 2)["max_tokens"]).toBe(16);
198
+ });
199
+ });
200
+
201
+ describe("LLMService — reasoning model detection by name", () => {
202
+ test.each([
203
+ ["gpt-5", true],
204
+ ["gpt-5-mini", true],
205
+ ["gpt5-mini", true],
206
+ ["o1", true],
207
+ ["o1-mini", true],
208
+ ["o3-mini", true],
209
+ ["o4-mini", true],
210
+ ["codex-mini", true],
211
+ ["gpt-4o", false],
212
+ ["gpt-4o-mini", false],
213
+ ["gpt-4.1", false],
214
+ ["gpt-4-turbo", false],
215
+ ["gpt-35-turbo", false],
216
+ ])(
217
+ "%s sends max_completion_tokens up front: %s",
218
+ async (modelName: string, expectsMaxCompletionTokens: boolean) => {
219
+ const spy: PostSpy = jest
220
+ .spyOn(API, "post")
221
+ .mockResolvedValue(successResponse()) as PostSpy;
222
+
223
+ await LLMService.getCompletion({
224
+ llmProviderConfig: {
225
+ llmType: LlmType.OpenAI,
226
+ apiKey: "test-key",
227
+ modelName: modelName,
228
+ },
229
+ messages: [{ role: "user", content: "hi" }],
230
+ maxTokens: 16,
231
+ });
232
+
233
+ // Known reasoning models skip the wasted round trip entirely.
234
+ expect(spy).toHaveBeenCalledTimes(1);
235
+ const body: JSONObject = bodyOf(spy, 0);
236
+
237
+ if (expectsMaxCompletionTokens) {
238
+ expect(body["max_completion_tokens"]).toBe(16);
239
+ expect(body["max_tokens"]).toBeUndefined();
240
+ } else {
241
+ expect(body["max_tokens"]).toBe(16);
242
+ expect(body["max_completion_tokens"]).toBeUndefined();
243
+ }
244
+ },
245
+ );
246
+ });
247
+
248
+ describe("LLMService — legacy models that reject max_completion_tokens", () => {
249
+ test("falls back to max_tokens when the endpoint does not recognize max_completion_tokens", async () => {
250
+ const spy: PostSpy = jest
251
+ .spyOn(API, "post")
252
+ .mockResolvedValueOnce(maxCompletionTokensUnrecognizedResponse())
253
+ .mockResolvedValueOnce(successResponse()) as PostSpy;
254
+
255
+ // A gpt-5-named deployment behind an endpoint that only knows max_tokens.
256
+ const response: { content: string } = await LLMService.getCompletion({
257
+ llmProviderConfig: { ...AZURE_CONFIG, modelName: "gpt-5-mini" },
258
+ messages: [{ role: "user", content: "hi" }],
259
+ maxTokens: 16,
260
+ });
261
+
262
+ expect(response.content).toBe("OK");
263
+ expect(bodyOf(spy, 0)["max_completion_tokens"]).toBe(16);
264
+ expect(bodyOf(spy, 1)["max_tokens"]).toBe(16);
265
+ expect(bodyOf(spy, 1)["max_completion_tokens"]).toBeUndefined();
266
+ });
267
+
268
+ test("does not flip-flop when the endpoint rejects both spellings", async () => {
269
+ const spy: PostSpy = jest
270
+ .spyOn(API, "post")
271
+ .mockResolvedValueOnce(maxCompletionTokensUnrecognizedResponse())
272
+ .mockResolvedValue(maxTokensUnsupportedResponse()) as PostSpy;
273
+
274
+ await expect(
275
+ LLMService.getCompletion({
276
+ llmProviderConfig: { ...AZURE_CONFIG, modelName: "gpt-5-mini" },
277
+ messages: [{ role: "user", content: "hi" }],
278
+ maxTokens: 16,
279
+ }),
280
+ ).rejects.toThrow("Azure OpenAI API error");
281
+
282
+ // Each spelling is tried exactly once, then the provider error surfaces.
283
+ expect(spy).toHaveBeenCalledTimes(2);
284
+ });
285
+
286
+ test("does not flip-flop when the name heuristic misses and additionalParams chose the new spelling", async () => {
287
+ const spy: PostSpy = jest
288
+ .spyOn(API, "post")
289
+ .mockResolvedValueOnce(maxCompletionTokensUnrecognizedResponse())
290
+ .mockResolvedValue(maxTokensUnsupportedResponse()) as PostSpy;
291
+
292
+ /*
293
+ * modelName does not match the heuristic, so the adaptation starts at the
294
+ * Default sentinel while additionalParams puts max_completion_tokens on
295
+ * the wire. Both spellings must still be tried at most once each.
296
+ */
297
+ await expect(
298
+ LLMService.getCompletion({
299
+ llmProviderConfig: { ...AZURE_CONFIG, modelName: "prod" },
300
+ messages: [{ role: "user", content: "hi" }],
301
+ maxTokens: 16,
302
+ additionalParams: { max_completion_tokens: 4000 },
303
+ }),
304
+ ).rejects.toThrow("Azure OpenAI API error");
305
+
306
+ expect(spy).toHaveBeenCalledTimes(2);
307
+ expect(bodyOf(spy, 0)["max_completion_tokens"]).toBeDefined();
308
+ expect(bodyOf(spy, 1)["max_tokens"]).toBeDefined();
309
+ });
310
+
311
+ test("adapts a chain longer than four params rather than giving up short", async () => {
312
+ const reject: (param: string) => HTTPErrorResponse = (
313
+ param: string,
314
+ ): HTTPErrorResponse => {
315
+ return new HTTPErrorResponse(
316
+ 400,
317
+ {
318
+ error: {
319
+ message: `Unsupported parameter: '${param}' is not supported with this model.`,
320
+ type: "invalid_request_error",
321
+ param: param,
322
+ code: "unsupported_parameter",
323
+ },
324
+ },
325
+ {},
326
+ );
327
+ };
328
+
329
+ const spy: PostSpy = jest
330
+ .spyOn(API, "post")
331
+ .mockResolvedValueOnce(maxTokensUnsupportedResponse())
332
+ .mockResolvedValueOnce(reject("temperature"))
333
+ .mockResolvedValueOnce(reject("top_p"))
334
+ .mockResolvedValueOnce(reject("frequency_penalty"))
335
+ .mockResolvedValueOnce(reject("presence_penalty"))
336
+ .mockResolvedValueOnce(successResponse()) as PostSpy;
337
+
338
+ // Five reshapes are needed; the attempt cap must not cut this short.
339
+ const response: { content: string } = await LLMService.getCompletion({
340
+ llmProviderConfig: { ...AZURE_CONFIG, modelName: "prod" },
341
+ messages: [{ role: "user", content: "hi" }],
342
+ temperature: 0,
343
+ maxTokens: 16,
344
+ additionalParams: {
345
+ top_p: 0.9,
346
+ frequency_penalty: 0.2,
347
+ presence_penalty: 0.2,
348
+ },
349
+ });
350
+
351
+ expect(response.content).toBe("OK");
352
+ expect(spy).toHaveBeenCalledTimes(6);
353
+
354
+ const finalBody: JSONObject = bodyOf(spy, 5);
355
+ expect(finalBody["max_completion_tokens"]).toBe(16);
356
+ expect(finalBody["temperature"]).toBeUndefined();
357
+ expect(finalBody["top_p"]).toBeUndefined();
358
+ expect(finalBody["frequency_penalty"]).toBeUndefined();
359
+ expect(finalBody["presence_penalty"]).toBeUndefined();
360
+ });
361
+
362
+ test("remembers dropped params, not just the token spelling", async () => {
363
+ const spy: PostSpy = jest
364
+ .spyOn(API, "post")
365
+ .mockResolvedValueOnce(maxTokensUnsupportedResponse())
366
+ .mockResolvedValueOnce(temperatureUnsupportedResponse())
367
+ .mockResolvedValue(successResponse()) as PostSpy;
368
+
369
+ await LLMService.getCompletion({
370
+ llmProviderConfig: AZURE_CONFIG,
371
+ messages: [{ role: "user", content: "hi" }],
372
+ temperature: 0,
373
+ maxTokens: 16,
374
+ });
375
+ expect(spy).toHaveBeenCalledTimes(3);
376
+
377
+ await LLMService.getCompletion({
378
+ llmProviderConfig: AZURE_CONFIG,
379
+ messages: [{ role: "user", content: "again" }],
380
+ temperature: 0,
381
+ maxTokens: 16,
382
+ });
383
+
384
+ // Both halves of what was learned must survive: the spelling and the drop.
385
+ expect(spy).toHaveBeenCalledTimes(4);
386
+ expect(bodyOf(spy, 3)["max_completion_tokens"]).toBe(16);
387
+ expect(bodyOf(spy, 3)["temperature"]).toBeUndefined();
388
+ });
389
+
390
+ test("keeps two Azure resources that share a deployment name apart", async () => {
391
+ const spy: PostSpy = jest
392
+ .spyOn(API, "post")
393
+ .mockResolvedValueOnce(maxTokensUnsupportedResponse())
394
+ .mockResolvedValue(successResponse()) as PostSpy;
395
+
396
+ await LLMService.getCompletion({
397
+ llmProviderConfig: AZURE_CONFIG,
398
+ messages: [{ role: "user", content: "hi" }],
399
+ maxTokens: 16,
400
+ });
401
+ expect(spy).toHaveBeenCalledTimes(2);
402
+
403
+ await LLMService.getCompletion({
404
+ llmProviderConfig: {
405
+ ...AZURE_CONFIG,
406
+ baseUrl:
407
+ "https://other.openai.azure.com/openai/deployments/my-deployment",
408
+ },
409
+ messages: [{ role: "user", content: "hi" }],
410
+ maxTokens: 16,
411
+ });
412
+
413
+ /*
414
+ * Same deployment name, different resource: what one accepts says nothing
415
+ * about the other, so it must re-probe.
416
+ */
417
+ expect(bodyOf(spy, 2)["max_tokens"]).toBe(16);
418
+ });
419
+ });
420
+
421
+ describe("LLMService — errors that are not parameter-compatibility problems", () => {
422
+ test("a max_tokens value error surfaces as-is instead of triggering a rename", async () => {
423
+ const spy: PostSpy = jest.spyOn(API, "post").mockResolvedValue(
424
+ new HTTPErrorResponse(
425
+ 400,
426
+ {
427
+ error: {
428
+ message:
429
+ "max_tokens is too large: 200000. This model supports at most 16384 completion tokens.",
430
+ type: "invalid_request_error",
431
+ param: "max_tokens",
432
+ code: null,
433
+ },
434
+ },
435
+ {},
436
+ ),
437
+ ) as PostSpy;
438
+
439
+ await expect(
440
+ LLMService.getCompletion({
441
+ llmProviderConfig: {
442
+ llmType: LlmType.OpenAI,
443
+ apiKey: "test-key",
444
+ modelName: "gpt-4o",
445
+ },
446
+ messages: [{ role: "user", content: "hi" }],
447
+ maxTokens: 200000,
448
+ }),
449
+ /*
450
+ * Renaming the param would not fix a too-large value; it would only
451
+ * bury the real complaint under one about a param the caller never set.
452
+ */
453
+ ).rejects.toThrow("max_tokens is too large");
454
+
455
+ expect(spy).toHaveBeenCalledTimes(1);
456
+ });
457
+
458
+ test("the plural unrecognized-argument wording still triggers the fallback", async () => {
459
+ const spy: PostSpy = jest
460
+ .spyOn(API, "post")
461
+ .mockResolvedValueOnce(
462
+ new HTTPErrorResponse(
463
+ 400,
464
+ {
465
+ error: {
466
+ message:
467
+ "Unrecognized request arguments supplied: max_completion_tokens, reasoning_effort",
468
+ type: "invalid_request_error",
469
+ param: null,
470
+ code: null,
471
+ },
472
+ },
473
+ {},
474
+ ),
475
+ )
476
+ .mockResolvedValueOnce(successResponse()) as PostSpy;
477
+
478
+ const response: { content: string } = await LLMService.getCompletion({
479
+ llmProviderConfig: { ...AZURE_CONFIG, modelName: "gpt-5-mini" },
480
+ messages: [{ role: "user", content: "hi" }],
481
+ maxTokens: 16,
482
+ });
483
+
484
+ expect(response.content).toBe("OK");
485
+ expect(bodyOf(spy, 1)["max_tokens"]).toBe(16);
486
+ });
487
+
488
+ test("an auth failure is surfaced without any retry", async () => {
489
+ const spy: PostSpy = jest.spyOn(API, "post").mockResolvedValue(
490
+ new HTTPErrorResponse(
491
+ 401,
492
+ {
493
+ error: {
494
+ message: "Incorrect API key provided.",
495
+ type: "invalid_request_error",
496
+ param: null,
497
+ code: "invalid_api_key",
498
+ },
499
+ },
500
+ {},
501
+ ),
502
+ ) as PostSpy;
503
+
504
+ await expect(
505
+ LLMService.getCompletion({
506
+ llmProviderConfig: AZURE_CONFIG,
507
+ messages: [{ role: "user", content: "hi" }],
508
+ maxTokens: 16,
509
+ }),
510
+ ).rejects.toThrow("Azure OpenAI API error");
511
+
512
+ expect(spy).toHaveBeenCalledTimes(1);
513
+ });
514
+
515
+ test("a failed request is not cached as a working shape", async () => {
516
+ jest
517
+ .spyOn(API, "post")
518
+ .mockResolvedValueOnce(maxTokensUnsupportedResponse())
519
+ .mockResolvedValueOnce(
520
+ new HTTPErrorResponse(500, { error: { message: "upstream boom" } }, {}),
521
+ );
522
+
523
+ await expect(
524
+ LLMService.getCompletion({
525
+ llmProviderConfig: AZURE_CONFIG,
526
+ messages: [{ role: "user", content: "hi" }],
527
+ maxTokens: 16,
528
+ }),
529
+ ).rejects.toThrow("Azure OpenAI API error");
530
+
531
+ jest.restoreAllMocks();
532
+ jest.spyOn(logger, "debug").mockImplementation((): void => {});
533
+ jest.spyOn(logger, "error").mockImplementation((): void => {});
534
+
535
+ const spy: PostSpy = jest
536
+ .spyOn(API, "post")
537
+ .mockResolvedValue(successResponse()) as PostSpy;
538
+
539
+ await LLMService.getCompletion({
540
+ llmProviderConfig: AZURE_CONFIG,
541
+ messages: [{ role: "user", content: "hi" }],
542
+ maxTokens: 16,
543
+ });
544
+
545
+ // The swap was never confirmed to work, so it must not be remembered.
546
+ expect(bodyOf(spy, 0)["max_tokens"]).toBe(16);
547
+ });
548
+ });
@@ -0,0 +1,142 @@
1
+ import {
2
+ buildObservabilityChatSystemPrompt,
3
+ buildPageContextSection,
4
+ } from "../../../../Server/Utils/AI/Chat/ObservabilityChatPrompt";
5
+ import AIChatPageContextType, {
6
+ AIChatPageContext,
7
+ } from "../../../../Types/AI/AIChatPageContext";
8
+ import AIChatPermissionMode from "../../../../Types/AI/AIChatPermissionMode";
9
+ import { describe, expect, test } from "@jest/globals";
10
+
11
+ const ENTITY_ID: string = "0f10509d-6656-4a08-b957-235fd4e8c52e";
12
+ const TRACE_ID: string = "4bf92f3577b34da6a3ce929d0e0e4736";
13
+
14
+ describe("buildPageContextSection", () => {
15
+ test("returns an empty string without a context", () => {
16
+ expect(buildPageContextSection(undefined)).toBe("");
17
+ });
18
+
19
+ /*
20
+ * Each entity type must name the tool (and its id argument) that fetches
21
+ * the entity — that pairing is what lets the model resolve "this incident"
22
+ * without guessing.
23
+ */
24
+ test.each<[AIChatPageContextType, string]>([
25
+ [
26
+ AIChatPageContextType.Incident,
27
+ `query_incidents using incidentId="${ENTITY_ID}"`,
28
+ ],
29
+ [AIChatPageContextType.Alert, `query_alerts using alertId="${ENTITY_ID}"`],
30
+ [
31
+ AIChatPageContextType.Monitor,
32
+ `query_monitors using monitorId="${ENTITY_ID}"`,
33
+ ],
34
+ [AIChatPageContextType.TelemetryService, `serviceId="${ENTITY_ID}"`],
35
+ [
36
+ AIChatPageContextType.Exception,
37
+ `find_code_for_exception with exceptionId="${ENTITY_ID}"`,
38
+ ],
39
+ ])(
40
+ "%s guidance names the fetch tool and id",
41
+ (type: AIChatPageContextType, expected: string) => {
42
+ const section: string = buildPageContextSection({
43
+ type: type,
44
+ entityId: ENTITY_ID,
45
+ });
46
+
47
+ expect(section).toContain("## Current page context");
48
+ expect(section).toContain(expected);
49
+ },
50
+ );
51
+
52
+ test("trace guidance uses get_trace with the hex trace id", () => {
53
+ const section: string = buildPageContextSection({
54
+ type: AIChatPageContextType.Trace,
55
+ entityId: TRACE_ID,
56
+ });
57
+
58
+ expect(section).toContain(`get_trace using traceId="${TRACE_ID}"`);
59
+ });
60
+
61
+ test("scheduled maintenance guidance names query_scheduled_maintenance", () => {
62
+ const section: string = buildPageContextSection({
63
+ type: AIChatPageContextType.ScheduledMaintenanceEvent,
64
+ entityId: ENTITY_ID,
65
+ });
66
+
67
+ expect(section).toContain(
68
+ `query_scheduled_maintenance using scheduledMaintenanceId="${ENTITY_ID}"`,
69
+ );
70
+ });
71
+
72
+ test("the entity title is included in quotes when present", () => {
73
+ const section: string = buildPageContextSection({
74
+ type: AIChatPageContextType.Incident,
75
+ entityId: ENTITY_ID,
76
+ entityTitle: "#42 Payment API down",
77
+ });
78
+
79
+ expect(section).toContain('titled "#42 Payment API down"');
80
+ });
81
+
82
+ test.each<[AIChatPageContextType, string]>([
83
+ [AIChatPageContextType.IncidentsList, "query_incidents"],
84
+ [AIChatPageContextType.AlertsList, "query_alerts"],
85
+ [AIChatPageContextType.MonitorsList, "query_monitors"],
86
+ [
87
+ AIChatPageContextType.ScheduledMaintenanceList,
88
+ "query_scheduled_maintenance",
89
+ ],
90
+ [AIChatPageContextType.LogsExplorer, "log_histogram"],
91
+ [AIChatPageContextType.TracesExplorer, "query_traces"],
92
+ [AIChatPageContextType.MetricsExplorer, "query_metrics"],
93
+ [AIChatPageContextType.ExceptionsList, "top_exceptions"],
94
+ ])(
95
+ "area context %s names its primary tool",
96
+ (type: AIChatPageContextType, expectedTool: string) => {
97
+ const context: AIChatPageContext = { type: type };
98
+ const section: string = buildPageContextSection(context);
99
+
100
+ expect(section).toContain("## Current page context");
101
+ expect(section).toContain(expectedTool);
102
+ },
103
+ );
104
+
105
+ test("every section reminds the model that context is not evidence", () => {
106
+ const section: string = buildPageContextSection({
107
+ type: AIChatPageContextType.Incident,
108
+ entityId: ENTITY_ID,
109
+ });
110
+
111
+ expect(section).toContain("It is not evidence");
112
+ });
113
+ });
114
+
115
+ describe("buildObservabilityChatSystemPrompt with page context", () => {
116
+ test("omits the section when no context is passed", () => {
117
+ const prompt: string = buildObservabilityChatSystemPrompt({
118
+ currentTime: new Date("2026-07-16T00:00:00Z"),
119
+ permissionMode: AIChatPermissionMode.AskForApproval,
120
+ });
121
+
122
+ expect(prompt).not.toContain("## Current page context");
123
+ });
124
+
125
+ test("includes the section without displacing the hard rules", () => {
126
+ const prompt: string = buildObservabilityChatSystemPrompt({
127
+ currentTime: new Date("2026-07-16T00:00:00Z"),
128
+ permissionMode: AIChatPermissionMode.AskForApproval,
129
+ pageContext: {
130
+ type: AIChatPageContextType.Incident,
131
+ entityId: ENTITY_ID,
132
+ entityTitle: "Payment API down",
133
+ },
134
+ });
135
+
136
+ expect(prompt).toContain("## Current page context");
137
+ expect(prompt).toContain(`incidentId="${ENTITY_ID}"`);
138
+ // The binding trust rules survive untouched.
139
+ expect(prompt).toContain("## Hard rules");
140
+ expect(prompt).toContain("Cite your sources.");
141
+ });
142
+ });