@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
@@ -1,5 +1,6 @@
1
1
  import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
2
2
  import HTTPResponse from "../../../Types/API/HTTPResponse";
3
+ import Headers from "../../../Types/API/Headers";
3
4
  import URL from "../../../Types/API/URL";
4
5
  import { JSONArray, JSONObject } from "../../../Types/JSON";
5
6
  import JSONFunctions from "../../../Types/JSONFunctions";
@@ -85,6 +86,32 @@ export interface LLMProviderConfig {
85
86
  modelName?: string;
86
87
  }
87
88
 
89
+ /**
90
+ * Which spelling of the output-token cap an OpenAI-style endpoint accepts.
91
+ *
92
+ * - Default: send the legacy `max_tokens` unless additionalParams opted into
93
+ * `max_completion_tokens`. This is the historical behavior.
94
+ * - MaxCompletionTokens: the model rejects `max_tokens` (reasoning models).
95
+ * - MaxTokens: the model rejects `max_completion_tokens` (older models and
96
+ * Azure API versions before 2024-08-01-preview).
97
+ */
98
+ enum TokenLimitParam {
99
+ Default = "Default",
100
+ MaxCompletionTokens = "MaxCompletionTokens",
101
+ MaxTokens = "MaxTokens",
102
+ }
103
+
104
+ /**
105
+ * Per-endpoint compatibility adjustments for the OpenAI chat-completions wire
106
+ * format. Model families disagree about which generation parameters they
107
+ * accept, so a request may need reshaping before a given deployment takes it.
108
+ */
109
+ interface OpenAIRequestAdaptation {
110
+ tokenLimitParam: TokenLimitParam;
111
+ /** Generation params this model rejects outright, so we omit them. */
112
+ unsupportedParams: Set<string>;
113
+ }
114
+
88
115
  export default class LLMService {
89
116
  /*
90
117
  * Provider-level parameters allowed on protected, unattended completions.
@@ -116,6 +143,145 @@ export default class LLMService {
116
143
  "verbosity",
117
144
  ]);
118
145
 
146
+ /*
147
+ * Generation params that reasoning models (o-series, gpt-5) reject outright.
148
+ * When a provider names one of these in an error, we drop it and retry
149
+ * rather than failing the whole completion. Only params that merely tune
150
+ * sampling are droppable — never anything that changes what the model is
151
+ * asked to do.
152
+ */
153
+ private static readonly DROPPABLE_UNSUPPORTED_PARAMETERS: Set<string> =
154
+ new Set([
155
+ "frequency_penalty",
156
+ "logit_bias",
157
+ "logprobs",
158
+ "presence_penalty",
159
+ "temperature",
160
+ "top_logprobs",
161
+ "top_p",
162
+ ]);
163
+
164
+ /*
165
+ * Reasoning models require `max_completion_tokens` and reject the legacy
166
+ * `max_tokens`. Detecting them by name lets the first request succeed
167
+ * instead of burning a round trip on a rejected one.
168
+ *
169
+ * On Azure this is only a hint: the `model` field there is the *deployment*
170
+ * name, which the operator chooses freely ("dumbo_o3", "prod-fast"), so it
171
+ * may not resemble the underlying model at all. A wrong guess in either
172
+ * direction is corrected by the error-driven retry in
173
+ * postOpenAIChatCompletion, which is what actually makes this correct.
174
+ */
175
+ private static readonly REASONING_MODEL_NAME_REGEX: RegExp =
176
+ /^(?:o\d+(?:[-_.]|$)|gpt-?5|codex-mini)/i;
177
+
178
+ /*
179
+ * Backstop only. A provider names one offending parameter per response, so
180
+ * the worst legitimate chain is both token spellings plus one drop for each
181
+ * droppable param. The loop already terminates on its own — every rule
182
+ * refuses to re-suggest a fix it has applied — so this just stops a
183
+ * misbehaving endpoint from looping forever. Sizing it below the reachable
184
+ * chain would fail requests the next attempt would have fixed.
185
+ */
186
+ private static readonly MAX_REQUEST_ADAPTATION_ATTEMPTS: number =
187
+ 2 + LLMService.DROPPABLE_UNSUPPORTED_PARAMETERS.size;
188
+
189
+ /*
190
+ * Remembers what a given endpoint actually accepted, so only the first
191
+ * completion per provider pays for the discovery. API.post retries 4xx, so
192
+ * a rejected request costs several full prompt uploads plus backoff —
193
+ * re-learning that on every call would be expensive on hot paths like the
194
+ * AI agent loop.
195
+ *
196
+ * Keyed by provider + base URL + model, so editing any of them re-probes.
197
+ * Entries also expire: what a deployment accepts is not fixed forever — the
198
+ * operator can repoint a deployment at a different model, upgrade the Azure
199
+ * api-version, or change reasoning_effort (gpt-5.1 accepts temperature when
200
+ * it is "none"). Without expiry we would keep dropping a parameter the model
201
+ * has since started accepting.
202
+ */
203
+ private static readonly requestAdaptationCache: Map<
204
+ string,
205
+ { adaptation: OpenAIRequestAdaptation; learnedAt: number }
206
+ > = new Map();
207
+
208
+ private static readonly MAX_ADAPTATION_CACHE_ENTRIES: number = 500;
209
+
210
+ private static readonly ADAPTATION_CACHE_TTL_IN_MS: number = 60 * 60 * 1000;
211
+
212
+ private static getAdaptationCacheKey(
213
+ config: LLMProviderConfig,
214
+ modelName: string,
215
+ ): string {
216
+ return `${config.llmType}|${config.baseUrl || ""}|${modelName}`;
217
+ }
218
+
219
+ private static getInitialRequestAdaptation(
220
+ config: LLMProviderConfig,
221
+ modelName: string,
222
+ ): OpenAIRequestAdaptation {
223
+ const key: string = this.getAdaptationCacheKey(config, modelName);
224
+ const cached:
225
+ | { adaptation: OpenAIRequestAdaptation; learnedAt: number }
226
+ | undefined = this.requestAdaptationCache.get(key);
227
+
228
+ if (cached) {
229
+ if (Date.now() - cached.learnedAt < this.ADAPTATION_CACHE_TTL_IN_MS) {
230
+ return {
231
+ tokenLimitParam: cached.adaptation.tokenLimitParam,
232
+ unsupportedParams: new Set(cached.adaptation.unsupportedParams),
233
+ };
234
+ }
235
+
236
+ this.requestAdaptationCache.delete(key);
237
+ }
238
+
239
+ return {
240
+ tokenLimitParam: this.REASONING_MODEL_NAME_REGEX.test(modelName)
241
+ ? TokenLimitParam.MaxCompletionTokens
242
+ : TokenLimitParam.Default,
243
+ unsupportedParams: new Set(),
244
+ };
245
+ }
246
+
247
+ private static cacheRequestAdaptation(
248
+ config: LLMProviderConfig,
249
+ modelName: string,
250
+ adaptation: OpenAIRequestAdaptation,
251
+ ): void {
252
+ const key: string = this.getAdaptationCacheKey(config, modelName);
253
+
254
+ // Bound the cache; Map iterates in insertion order, so this drops the oldest.
255
+ if (
256
+ !this.requestAdaptationCache.has(key) &&
257
+ this.requestAdaptationCache.size >= this.MAX_ADAPTATION_CACHE_ENTRIES
258
+ ) {
259
+ const oldestKey: string | undefined = this.requestAdaptationCache
260
+ .keys()
261
+ .next().value;
262
+
263
+ if (oldestKey !== undefined) {
264
+ this.requestAdaptationCache.delete(oldestKey);
265
+ }
266
+ }
267
+
268
+ this.requestAdaptationCache.set(key, {
269
+ adaptation: {
270
+ tokenLimitParam: adaptation.tokenLimitParam,
271
+ unsupportedParams: new Set(adaptation.unsupportedParams),
272
+ },
273
+ learnedAt: Date.now(),
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Test seam: provider compatibility is remembered process-wide, so tests
279
+ * must be able to start from a clean slate.
280
+ */
281
+ public static clearRequestAdaptationCache(): void {
282
+ this.requestAdaptationCache.clear();
283
+ }
284
+
119
285
  @CaptureSpan()
120
286
  public static async getCompletion(
121
287
  request: LLMCompletionRequest,
@@ -268,6 +434,7 @@ export default class LLMService {
268
434
  private static buildOpenAIRequestBody(
269
435
  modelName: string,
270
436
  request: LLMCompletionRequest,
437
+ adaptation: OpenAIRequestAdaptation,
271
438
  ): JSONObject {
272
439
  const data: JSONObject = {
273
440
  model: modelName,
@@ -346,9 +513,229 @@ export default class LLMService {
346
513
  delete data["max_tokens"];
347
514
  }
348
515
 
516
+ /*
517
+ * Applied last so it overrides every other source: this is what the
518
+ * endpoint has actually told us (or the model name strongly implies) it
519
+ * will accept, and a request it rejects is worth nothing.
520
+ */
521
+ this.applyRequestAdaptation(data, adaptation);
522
+
349
523
  return data;
350
524
  }
351
525
 
526
+ private static applyRequestAdaptation(
527
+ data: JSONObject,
528
+ adaptation: OpenAIRequestAdaptation,
529
+ ): void {
530
+ if (adaptation.tokenLimitParam === TokenLimitParam.MaxCompletionTokens) {
531
+ const tokenLimit: unknown =
532
+ data["max_tokens"] ?? data["max_completion_tokens"];
533
+
534
+ delete data["max_tokens"];
535
+
536
+ if (tokenLimit !== undefined) {
537
+ data["max_completion_tokens"] = tokenLimit as number;
538
+ }
539
+ } else if (adaptation.tokenLimitParam === TokenLimitParam.MaxTokens) {
540
+ const tokenLimit: unknown =
541
+ data["max_completion_tokens"] ?? data["max_tokens"];
542
+
543
+ delete data["max_completion_tokens"];
544
+
545
+ if (tokenLimit !== undefined) {
546
+ data["max_tokens"] = tokenLimit as number;
547
+ }
548
+ }
549
+
550
+ for (const unsupportedParam of adaptation.unsupportedParams) {
551
+ delete data[unsupportedParam];
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Work out how to reshape a request that the provider rejected on
557
+ * parameter-compatibility grounds. Returns undefined when the error is
558
+ * something else (bad key, unknown model, rate limit) or when we have
559
+ * already applied the only remedy we have — the caller then surfaces the
560
+ * provider's error as-is.
561
+ */
562
+ private static getRequestAdaptationForError(
563
+ response: HTTPErrorResponse,
564
+ adaptation: OpenAIRequestAdaptation,
565
+ attemptedTokenLimitParams: Set<TokenLimitParam>,
566
+ ): OpenAIRequestAdaptation | undefined {
567
+ const error: JSONObject | undefined = (response.data as JSONObject)?.[
568
+ "error"
569
+ ] as JSONObject | undefined;
570
+
571
+ if (!error) {
572
+ return undefined;
573
+ }
574
+
575
+ const param: string = (error["param"] as string) || "";
576
+ const code: string = (error["code"] as string) || "";
577
+ const message: string = (error["message"] as string) || "";
578
+
579
+ /*
580
+ * "Unsupported parameter: 'max_tokens' is not supported with this model.
581
+ * Use 'max_completion_tokens' instead." — every o-series and gpt-5 model.
582
+ *
583
+ * The code/message check matters: a too-large or wrong-type value is also
584
+ * reported with param "max_tokens", and renaming the parameter would not
585
+ * fix it. It would only replace the provider's real complaint with a
586
+ * confusing one about a parameter the caller never set.
587
+ */
588
+ if (
589
+ param === "max_tokens" &&
590
+ (code === "unsupported_parameter" ||
591
+ message.includes("max_completion_tokens")) &&
592
+ !attemptedTokenLimitParams.has(TokenLimitParam.MaxCompletionTokens)
593
+ ) {
594
+ return {
595
+ ...adaptation,
596
+ tokenLimitParam: TokenLimitParam.MaxCompletionTokens,
597
+ };
598
+ }
599
+
600
+ /*
601
+ * The reverse: older models and Azure API versions before
602
+ * 2024-08-01-preview reject max_completion_tokens. This one reports param
603
+ * and code as null, so the message is the only signal. Matching
604
+ * "argument" rather than "argument supplied" also catches the plural the
605
+ * endpoint uses when more than one parameter is unrecognized.
606
+ *
607
+ * Both spellings are tried at most once each. A deployment that rejects
608
+ * both (a reasoning model behind an API version too old to know the new
609
+ * parameter) has no working request, so we stop and let the provider's
610
+ * own error explain it rather than flip-flopping until the attempt cap.
611
+ */
612
+ if (
613
+ (message.includes("Unrecognized request argument") ||
614
+ (param === "max_completion_tokens" &&
615
+ code === "unsupported_parameter")) &&
616
+ message.includes("max_completion_tokens") &&
617
+ !attemptedTokenLimitParams.has(TokenLimitParam.MaxTokens)
618
+ ) {
619
+ return {
620
+ ...adaptation,
621
+ tokenLimitParam: TokenLimitParam.MaxTokens,
622
+ };
623
+ }
624
+
625
+ /*
626
+ * Reasoning models reject temperature, top_p and the penalty/logprob
627
+ * params. Both codes appear in the wild for these:
628
+ * unsupported_parameter ("is not supported with this model") and
629
+ * unsupported_value ("does not support 0 with this model").
630
+ */
631
+ if (
632
+ (code === "unsupported_parameter" || code === "unsupported_value") &&
633
+ this.DROPPABLE_UNSUPPORTED_PARAMETERS.has(param) &&
634
+ !adaptation.unsupportedParams.has(param)
635
+ ) {
636
+ return {
637
+ ...adaptation,
638
+ unsupportedParams: new Set(adaptation.unsupportedParams).add(param),
639
+ };
640
+ }
641
+
642
+ return undefined;
643
+ }
644
+
645
+ /**
646
+ * POST an OpenAI-style chat completion, reshaping and retrying when the
647
+ * endpoint rejects a generation parameter it does not support.
648
+ *
649
+ * This is error-driven rather than model-driven on purpose. Azure sends the
650
+ * deployment name in `model`, and operators name deployments whatever they
651
+ * like, so there is no reliable way to know up front whether a deployment is
652
+ * a reasoning model. The provider itself is the only authority, and it names
653
+ * the offending parameter in the 400 — one parameter per response, hence the
654
+ * loop.
655
+ */
656
+ private static async postOpenAIChatCompletion(data: {
657
+ requestUrl: string;
658
+ headers: Headers;
659
+ config: LLMProviderConfig;
660
+ modelName: string;
661
+ request: LLMCompletionRequest;
662
+ }): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
663
+ let adaptation: OpenAIRequestAdaptation = this.getInitialRequestAdaptation(
664
+ data.config,
665
+ data.modelName,
666
+ );
667
+
668
+ /*
669
+ * Which spellings have actually gone out on the wire. Read off the built
670
+ * body rather than the adaptation, because TokenLimitParam.Default is a
671
+ * sentinel meaning "whatever the body already carried" — recording it
672
+ * would mark neither spelling as tried and let the loop resend a
673
+ * byte-identical request.
674
+ */
675
+ const attemptedTokenLimitParams: Set<TokenLimitParam> = new Set();
676
+
677
+ const post: () => Promise<
678
+ HTTPResponse<JSONObject> | HTTPErrorResponse
679
+ > = (): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> => {
680
+ const body: JSONObject = this.buildOpenAIRequestBody(
681
+ data.modelName,
682
+ data.request,
683
+ adaptation,
684
+ );
685
+
686
+ if (body["max_completion_tokens"] !== undefined) {
687
+ attemptedTokenLimitParams.add(TokenLimitParam.MaxCompletionTokens);
688
+ } else if (body["max_tokens"] !== undefined) {
689
+ attemptedTokenLimitParams.add(TokenLimitParam.MaxTokens);
690
+ }
691
+
692
+ return API.post<JSONObject>({
693
+ url: URL.fromString(data.requestUrl),
694
+ data: body,
695
+ headers: data.headers,
696
+ options: {
697
+ retries: data.request.requestRetries ?? 2,
698
+ exponentialBackoff: true,
699
+ timeout: data.request.requestTimeoutInMs ?? 120000,
700
+ },
701
+ });
702
+ };
703
+
704
+ let response: HTTPResponse<JSONObject> | HTTPErrorResponse = await post();
705
+
706
+ for (
707
+ let attempt: number = 0;
708
+ attempt < this.MAX_REQUEST_ADAPTATION_ATTEMPTS &&
709
+ response instanceof HTTPErrorResponse;
710
+ attempt++
711
+ ) {
712
+ const nextAdaptation: OpenAIRequestAdaptation | undefined =
713
+ this.getRequestAdaptationForError(
714
+ response,
715
+ adaptation,
716
+ attemptedTokenLimitParams,
717
+ );
718
+
719
+ if (!nextAdaptation) {
720
+ return response;
721
+ }
722
+
723
+ adaptation = nextAdaptation;
724
+
725
+ logger.debug(
726
+ `${data.config.llmType} rejected a generation parameter for model ${data.modelName}. Retrying with a request adapted to what the model accepts.`,
727
+ );
728
+
729
+ response = await post();
730
+ }
731
+
732
+ if (!(response instanceof HTTPErrorResponse)) {
733
+ this.cacheRequestAdaptation(data.config, data.modelName, adaptation);
734
+ }
735
+
736
+ return response;
737
+ }
738
+
352
739
  private static parseOpenAIResponse(
353
740
  jsonData: JSONObject,
354
741
  providerName: string,
@@ -545,11 +932,8 @@ export default class LLMService {
545
932
  const modelName: string =
546
933
  config.modelName || defaultModels[config.llmType] || "gpt-4o";
547
934
  const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
548
- await API.post<JSONObject>({
549
- url: URL.fromString(
550
- this.buildOpenAICompatibleChatCompletionsUrl(baseUrl),
551
- ),
552
- data: this.buildOpenAIRequestBody(modelName, request),
935
+ await this.postOpenAIChatCompletion({
936
+ requestUrl: this.buildOpenAICompatibleChatCompletionsUrl(baseUrl),
553
937
  headers: {
554
938
  "Content-Type": "application/json",
555
939
  /*
@@ -561,11 +945,9 @@ export default class LLMService {
561
945
  ? { Authorization: `Bearer ${config.apiKey}` }
562
946
  : {}),
563
947
  },
564
- options: {
565
- retries: request.requestRetries ?? 2,
566
- exponentialBackoff: true,
567
- timeout: request.requestTimeoutInMs ?? 120000,
568
- },
948
+ config: config,
949
+ modelName: modelName,
950
+ request: request,
569
951
  });
570
952
 
571
953
  const logAttributes: LogAttributes = {
@@ -632,18 +1014,15 @@ export default class LLMService {
632
1014
  );
633
1015
 
634
1016
  const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
635
- await API.post<JSONObject>({
636
- url: URL.fromString(requestUrl),
637
- data: this.buildOpenAIRequestBody(modelName, request),
1017
+ await this.postOpenAIChatCompletion({
1018
+ requestUrl: requestUrl,
638
1019
  headers: {
639
1020
  "api-key": config.apiKey,
640
1021
  "Content-Type": "application/json",
641
1022
  },
642
- options: {
643
- retries: request.requestRetries ?? 2,
644
- exponentialBackoff: true,
645
- timeout: request.requestTimeoutInMs ?? 120000,
646
- },
1023
+ config: config,
1024
+ modelName: modelName,
1025
+ request: request,
647
1026
  });
648
1027
 
649
1028
  const logAttributes: LogAttributes = {
@@ -4,7 +4,9 @@ import MonitorSteps from "../../../Types/Monitor/MonitorSteps";
4
4
  import QueryHelper from "../../Types/Database/QueryHelper";
5
5
  import LIMIT_MAX from "../../../Types/Database/LimitMax";
6
6
  import MonitorType from "../../../Types/Monitor/MonitorType";
7
- import SnmpVersion from "../../../Types/Monitor/SnmpMonitor/SnmpVersion";
7
+ import SnmpVersion, {
8
+ SnmpVersionUtil,
9
+ } from "../../../Types/Monitor/SnmpMonitor/SnmpVersion";
8
10
  import SnmpV3Auth from "../../../Types/Monitor/SnmpMonitor/SnmpV3Auth";
9
11
  import SnmpSecurityLevel from "../../../Types/Monitor/SnmpMonitor/SnmpSecurityLevel";
10
12
  import SnmpAuthProtocol from "../../../Types/Monitor/SnmpMonitor/SnmpAuthProtocol";
@@ -162,16 +164,7 @@ export default class NetworkDeviceHydrationUtil {
162
164
 
163
165
  // Tolerates both enum values ("2c") and enum keys ("V2c") in stored config.
164
166
  private static parseSnmpVersion(value: string | undefined): SnmpVersion {
165
- switch ((value || "").toLowerCase()) {
166
- case "1":
167
- case "v1":
168
- return SnmpVersion.V1;
169
- case "3":
170
- case "v3":
171
- return SnmpVersion.V3;
172
- default:
173
- return SnmpVersion.V2c;
174
- }
167
+ return SnmpVersionUtil.parse(value);
175
168
  }
176
169
 
177
170
  /*
@@ -0,0 +1,125 @@
1
+ /**
2
+ * StatusPage.enableMcpServer column contract.
3
+ *
4
+ * The feature's headline promise is "MCP stays on unless the owner turns it
5
+ * off". Three separate mechanisms carry that promise, and they are easy to
6
+ * confuse:
7
+ * - NEW rows get true from the Postgres column default, declared by
8
+ * @Column({ default: true }) and stored in TypeORM's metadata
9
+ * - EXISTING rows got true from the migration's NOT NULL DEFAULT true
10
+ * - @TableColumn({ defaultValue: true }) is documentation only — it feeds the
11
+ * generated API schema and form metadata, and defaults nothing at runtime
12
+ *
13
+ * All three are pinned below, because a one-word edit to any of them silently
14
+ * flips the default for everyone. Asserting only the @TableColumn metadata
15
+ * would be a guard that catches nothing: flipping @Column to default: false
16
+ * leaves that assertion green while every new status page ships with MCP off.
17
+ *
18
+ * isDefaultValueColumn is pinned too: it keeps the column optional on create,
19
+ * where required: true would make DatabaseService reject creates that omit it.
20
+ */
21
+
22
+ import StatusPage from "../../Models/DatabaseModels/StatusPage";
23
+ import { TableColumnMetadata } from "../../Types/Database/TableColumn";
24
+ import TableColumnType from "../../Types/Database/TableColumnType";
25
+ import Permission from "../../Types/Permission";
26
+ import { describe, expect, test } from "@jest/globals";
27
+ import { getMetadataArgsStorage } from "typeorm";
28
+ import { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
29
+ import fs from "fs";
30
+ import path from "path";
31
+
32
+ const COLUMN: string = "enableMcpServer";
33
+
34
+ const MIGRATION_PATH: string = path.join(
35
+ __dirname,
36
+ "..",
37
+ "..",
38
+ "Server",
39
+ "Infrastructure",
40
+ "Postgres",
41
+ "SchemaMigrations",
42
+ "1784137457184-AddEnableMcpServerToStatusPage.ts",
43
+ );
44
+
45
+ function metadata(): TableColumnMetadata {
46
+ return new StatusPage().getTableColumnMetadata(COLUMN);
47
+ }
48
+
49
+ function typeOrmColumn(): ColumnMetadataArgs | undefined {
50
+ return getMetadataArgsStorage().columns.find((column: ColumnMetadataArgs) => {
51
+ return column.target === StatusPage && column.propertyName === COLUMN;
52
+ });
53
+ }
54
+
55
+ describe("StatusPage.enableMcpServer", () => {
56
+ test("exists as a boolean column", () => {
57
+ expect(metadata()).toBeDefined();
58
+ expect(metadata().type).toBe(TableColumnType.Boolean);
59
+ });
60
+
61
+ test("a newly created status page has MCP enabled", () => {
62
+ /*
63
+ * This is the assertion that actually protects new status pages: the
64
+ * Postgres column default. Nothing else populates the column when a create
65
+ * omits it.
66
+ */
67
+ expect(typeOrmColumn()).toBeDefined();
68
+ expect(typeOrmColumn()?.options.default).toBe(true);
69
+ });
70
+
71
+ test("every pre-existing status page was backfilled as enabled", () => {
72
+ /*
73
+ * The migration is the only thing that decided the value for status pages
74
+ * that existed before this feature. Because isMcpServerEnabled queries
75
+ * `enableMcpServer: true`, losing this default would silently gate every
76
+ * status page created before the migration ran.
77
+ */
78
+ const migration: string = fs.readFileSync(MIGRATION_PATH, "utf8");
79
+
80
+ expect(migration).toContain('ALTER TABLE "StatusPage"');
81
+ expect(migration).toContain(
82
+ `ADD "${COLUMN}" boolean NOT NULL DEFAULT true`,
83
+ );
84
+ });
85
+
86
+ test("is documented as defaulting to enabled", () => {
87
+ /*
88
+ * Feeds the generated API schema and form metadata. Documentation only —
89
+ * see the tests above for the defaults that actually apply.
90
+ */
91
+ expect(metadata().defaultValue).toBe(true);
92
+ });
93
+
94
+ test("is a default-value column, so create may omit it", () => {
95
+ expect(new StatusPage().isDefaultValueColumn(COLUMN)).toBe(true);
96
+ });
97
+
98
+ test("is not required, so existing create calls keep working", () => {
99
+ expect(metadata().required).toBeFalsy();
100
+ });
101
+
102
+ test("is readable by status page viewers", () => {
103
+ const accessControl: Array<Permission> | undefined =
104
+ new StatusPage().getColumnAccessControlFor(COLUMN)?.read;
105
+
106
+ expect(accessControl).toContain(Permission.ProjectMember);
107
+ expect(accessControl).toContain(Permission.StatusPageAdmin);
108
+ });
109
+
110
+ test("is editable by status page editors", () => {
111
+ const accessControl: Array<Permission> | undefined =
112
+ new StatusPage().getColumnAccessControlFor(COLUMN)?.update;
113
+
114
+ expect(accessControl).toContain(Permission.EditProjectStatusPage);
115
+ expect(accessControl).toContain(Permission.StatusPageAdmin);
116
+ });
117
+
118
+ test("is not editable by a read-only viewer", () => {
119
+ const accessControl: Array<Permission> | undefined =
120
+ new StatusPage().getColumnAccessControlFor(COLUMN)?.update;
121
+
122
+ expect(accessControl).not.toContain(Permission.Viewer);
123
+ expect(accessControl).not.toContain(Permission.StatusPageViewer);
124
+ });
125
+ });