@junando/core 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __exportAll, t as __commonJSMin } from "./rolldown-runtime-BMI-E3GI.js";
2
- import { dedupDuplicate, dedupNew, dedupRedisFailoverTotal, llmInferenceDuration, llmInferenceTotal, notificationsTotal, sqsQueueLag, suppressedClusters, t as metrics_exports } from "./shared/metrics/index.js";
2
+ import { dedupDuplicate, dedupFailoverTotal, dedupNew, llmInferenceDuration, llmInferenceTotal, notificationsTotal, sqsQueueLag, suppressedClusters, t as metrics_exports } from "./shared/metrics/index.js";
3
3
  import { $ as booleanSelector, $t as ServiceException, A as BinaryDecisionDiagram, An as isValidHostLabel, B as NODE_REGION_CONFIG_FILE_OPTIONS, C as isIpAddress, Ct as dateToUtcString, Dt as parseRfc7231DateTime, E as EndpointError, En as normalizeProvider$1, Et as parseRfc3339DateTimeWithOffset, F as init_config$1, Fn as hasOwn, G as NODE_USE_FIPS_ENDPOINT_CONFIG_OPTIONS, H as REGION_ENV_NAME, Ht as NoOpLogger, It as fromBase64, Jt as resolveDefaultRuntimeConfig, Kt as getDefaultExtensionConfiguration, L as resolveDefaultsModeConfig, Ln as getSmithyContext, N as resolveParams, Nn as HttpRequest, Nt as toBase64, O as EndpointCache, On as isValidHostname, Ot as expectUnion, P as config_exports, Pt as fromUtf8, Qt as loadConfigsForDefaultMode, Rt as client_exports$1, Sn as parseUrl, Tt as parseEpochTimestamp, U as REGION_INI_NAME, V as NODE_REGION_CONFIG_OPTIONS, Vt as makeBuilder, Wt as getValueFromTextNode, X as loadConfig$1, Yt as emitWarningIfUnsupportedVersion$1, Z as SelectorType, _ as resolveEndpointConfig, _t as _parseRfc7231DateTime, b as decideEndpoint, bn as init_transport, bt as quoteHeader, c as generateIdempotencyToken, cn as TypeRegistry, ct as NumericValue, d as v4, en as decorateServiceException, ft as splitHeader, g as init_endpoints, gn as deref, gt as _parseRfc3339DateTimeWithOffset, h as getEndpointPlugin, ht as _parseEpochTimestamp, in as Command, jn as HttpResponse, jt as toUtf8, l as init_serde, m as endpoints_exports, mn as getSchemaSerdePlugin, mt as splitEvery, n as init_checksum, nn as createAggregatedClient, on as init_schema, p as sdkStreamMixin, pn as translateTraits, q as NODE_USE_DUALSTACK_ENDPOINT_CONFIG_OPTIONS, r as Sha256Node, rt as calculateBodyLength, s as Uint8ArrayBlobAdapter, sn as schema_exports, t as checksum_exports, tt as ProviderError, u as serde_exports, un as NormalizedSchema, ut as nv, vn as Client, w as customEndpointFunctions, wn as parseQueryString, xt as LazyJsonString, y as resolveEndpoint, z as resolveRegionConfig, zn as require_dist_cjs$17, zt as init_client$1 } from "./checksum-Bbvsykod.js";
4
4
  import { z } from "zod";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import pino from "pino";
7
7
  import { Readable, Writable } from "node:stream";
8
+ import { DeleteItemCommand, DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
9
+ import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
10
+ import { GoogleGenerativeAI, GoogleGenerativeAIAbortError } from "@google/generative-ai";
8
11
  import * as Breaker from "opossum";
9
12
  import { readFileSync } from "node:fs";
10
13
  import { YAMLParseError, parse } from "yaml";
@@ -39,6 +42,7 @@ let LLMProviderType = /* @__PURE__ */ function(LLMProviderType) {
39
42
  LLMProviderType["Claude"] = "claude";
40
43
  LLMProviderType["OpenRouter"] = "openrouter";
41
44
  LLMProviderType["Qwen"] = "qwen";
45
+ LLMProviderType["Bedrock"] = "bedrock";
42
46
  return LLMProviderType;
43
47
  }({});
44
48
  const HTTP_TIMEOUT_MS = Object.freeze({
@@ -71,7 +75,8 @@ const LLM_FALLBACK_DEFAULTS = Object.freeze({
71
75
  const LLM_MODELS = Object.freeze({
72
76
  Gemini: "gemini-2.0-flash",
73
77
  Claude: "claude-haiku-4-5",
74
- OpenRouter: "qwen/qwen-2.5-72b-instruct"
78
+ OpenRouter: "qwen/qwen-2.5-72b-instruct",
79
+ Bedrock: "us.amazon.nova-lite-v1:0"
75
80
  });
76
81
  const SLACK_API_URL = "https://slack.com/api/chat.postMessage";
77
82
  const ROLLBACK_ACTION_ID = "trigger_rollback";
@@ -808,6 +813,7 @@ var ProcessIncidentUseCase = class {
808
813
  latencyMs: Date.now() - notifyStartMs
809
814
  });
810
815
  builder.set("error", toErrorSection(err));
816
+ await this.releaseDedupClaim(cluster.fingerprint);
811
817
  this.emit(builder, Outcome.Error, clusterStartMs);
812
818
  throw err;
813
819
  }
@@ -820,6 +826,22 @@ var ProcessIncidentUseCase = class {
820
826
  }
821
827
  }
822
828
  /**
829
+ * Releases the dedup claim so an SQS retry is not discarded as a duplicate.
830
+ * Concurrency: safe without a lock — sqs.adapter.ts sets messageGroupId to
831
+ * the fingerprint, so SQS FIFO serialises delivery per fingerprint and two
832
+ * runs for the same fingerprint are never in flight simultaneously.
833
+ */
834
+ async releaseDedupClaim(fingerprint) {
835
+ try {
836
+ await this.deps.dedup.reset(fingerprint);
837
+ } catch (err) {
838
+ this.deps.logger.warn({
839
+ err,
840
+ fingerprint
841
+ }, "Failed to release dedup claim; retry may be suppressed");
842
+ }
843
+ }
844
+ /**
823
845
  * Flushes the builder into a final event, applies tail sampling, redacts
824
846
  * PII, and emits the single canonical log line for the cluster.
825
847
  */
@@ -832,7 +854,7 @@ var ProcessIncidentUseCase = class {
832
854
  };
833
855
  //#endregion
834
856
  //#region src/infrastructure/dedup/redis-dedup.adapter.ts
835
- const logger$6 = createLogger();
857
+ const logger$8 = createLogger();
836
858
  var RedisDeduplicationStore = class {
837
859
  redis;
838
860
  keyPrefix = "junando:dedup:";
@@ -847,11 +869,11 @@ var RedisDeduplicationStore = class {
847
869
  };
848
870
  } catch (err) {
849
871
  const message = err instanceof Error ? err.message : String(err);
850
- logger$6.warn({
872
+ logger$8.warn({
851
873
  err,
852
874
  fingerprint
853
875
  }, "Redis dedup check failed, failing open");
854
- dedupRedisFailoverTotal.inc();
876
+ dedupFailoverTotal.inc();
855
877
  return {
856
878
  isNew: true,
857
879
  ttlSeconds,
@@ -886,6 +908,63 @@ var InMemoryDeduplicationStore = class {
886
908
  }
887
909
  };
888
910
  //#endregion
911
+ //#region src/infrastructure/dedup/dynamodb-dedup.adapter.ts
912
+ const logger$7 = createLogger();
913
+ const CONDITION_EXPRESSION = "attribute_not_exists(fingerprint) OR expiresAt < :now";
914
+ var DynamoDBDeduplicationStore = class {
915
+ tableName;
916
+ region;
917
+ client = null;
918
+ constructor(tableName, region) {
919
+ this.tableName = tableName;
920
+ this.region = region;
921
+ }
922
+ getClient() {
923
+ if (!this.client) this.client = new DynamoDBClient(this.region ? { region: this.region } : {});
924
+ return this.client;
925
+ }
926
+ async isNew(fingerprint, ttlSeconds) {
927
+ const nowSec = Math.floor(Date.now() / 1e3);
928
+ try {
929
+ await this.getClient().send(new PutItemCommand({
930
+ TableName: this.tableName,
931
+ Item: {
932
+ fingerprint: { S: fingerprint },
933
+ expiresAt: { N: String(nowSec + ttlSeconds) }
934
+ },
935
+ ConditionExpression: CONDITION_EXPRESSION,
936
+ ExpressionAttributeValues: { ":now": { N: String(nowSec) } }
937
+ }));
938
+ return {
939
+ isNew: true,
940
+ ttlSeconds
941
+ };
942
+ } catch (err) {
943
+ if (err instanceof Error && err.name === "ConditionalCheckFailedException") return {
944
+ isNew: false,
945
+ ttlSeconds
946
+ };
947
+ const message = err instanceof Error ? err.message : String(err);
948
+ logger$7.warn({
949
+ err,
950
+ fingerprint
951
+ }, "DynamoDB dedup check failed, failing open");
952
+ dedupFailoverTotal.inc();
953
+ return {
954
+ isNew: true,
955
+ ttlSeconds,
956
+ error: message
957
+ };
958
+ }
959
+ }
960
+ async reset(fingerprint) {
961
+ await this.getClient().send(new DeleteItemCommand({
962
+ TableName: this.tableName,
963
+ Key: { fingerprint: { S: fingerprint } }
964
+ }));
965
+ }
966
+ };
967
+ //#endregion
889
968
  //#region src/infrastructure/indexer/opensearch.adapter.ts
890
969
  var OpenSearchIndexer = class {
891
970
  endpoint;
@@ -917,30 +996,8 @@ var InMemoryIndexer = class {
917
996
  }
918
997
  };
919
998
  //#endregion
920
- //#region src/infrastructure/llm/llm.adapter.ts
921
- const logger$5 = createLogger();
922
- /** Provider name reported by MockLLMProvider results. */
923
- const MOCK_PROVIDER_NAME = "mock";
924
- /**
925
- * Schema for OpenRouter API response validation.
926
- * Ensures type safety at the external boundary.
927
- */
928
- const OpenRouterResponseSchema = z.object({
929
- id: z.string().optional(),
930
- choices: z.array(z.object({
931
- index: z.number(),
932
- message: z.object({
933
- role: z.string(),
934
- content: z.string().optional()
935
- }),
936
- finish_reason: z.string().optional()
937
- })),
938
- usage: z.object({
939
- prompt_tokens: z.number().optional(),
940
- completion_tokens: z.number().optional(),
941
- total_tokens: z.number().optional()
942
- }).optional()
943
- });
999
+ //#region src/infrastructure/llm/shared.ts
1000
+ const logger$6 = createLogger();
944
1001
  const SYSTEM_PROMPT = `You are a senior Site Reliability Engineer.
945
1002
  Respond ONLY with raw JSON, no markdown, no text before or after:
946
1003
  {"probable_cause":"string","impacted_services":["string"],"recommended_steps":["string"],"urgency_level":"low|medium|high|critical","requires_rollback":true|false}`;
@@ -954,11 +1011,6 @@ const RE_URGENCY_LEVEL = /"urgency_level"\s*:\s*"([^"]+)"/;
954
1011
  const RE_REQUIRES_ROLLBACK = /"requires_rollback"\s*:\s*(true|false)/;
955
1012
  const RE_RECOMMENDED_STEPS = /"recommended_steps"\s*:\s*\[([^\]]+)\]/;
956
1013
  const RE_IMPACTED_SERVICES = /"impacted_services"\s*:\s*\[([^\]]+)\]/;
957
- const BREAKER_OPTIONS = {
958
- timeout: CIRCUIT_BREAKER.Timeout,
959
- errorThresholdPercentage: CIRCUIT_BREAKER.ErrorThresholdPercentage,
960
- resetTimeout: CIRCUIT_BREAKER.ResetTimeoutMs
961
- };
962
1014
  /**
963
1015
  * Builds the user-facing prompt sent to the LLM for analysis.
964
1016
  * Includes cluster summary and trace count for context.
@@ -980,7 +1032,7 @@ function parseAnalysis(raw, correlationId) {
980
1032
  stage1Succeeded = true;
981
1033
  return analysis;
982
1034
  } catch {}
983
- if (!stage1Succeeded) logger$5.warn({
1035
+ if (!stage1Succeeded) logger$6.warn({
984
1036
  rawResponse: raw.slice(0, 500),
985
1037
  correlationId
986
1038
  }, "llm:parse:failed");
@@ -1001,13 +1053,13 @@ function parseAnalysis(raw, correlationId) {
1001
1053
  requires_rollback: rollbackMatch?.[1] === "true"
1002
1054
  };
1003
1055
  const parsed = LLMAnalysisSchema.parse(analysis);
1004
- logger$5.warn({
1056
+ logger$6.warn({
1005
1057
  matchedFields: ["probable_cause", "urgency_level"],
1006
1058
  correlationId
1007
1059
  }, "llm:parse:partial");
1008
1060
  return parsed;
1009
1061
  }
1010
- logger$5.warn({ correlationId }, "llm:parse:unusable");
1062
+ logger$6.warn({ correlationId }, "llm:parse:unusable");
1011
1063
  return null;
1012
1064
  }
1013
1065
  /**
@@ -1017,7 +1069,7 @@ function parseAnalysis(raw, correlationId) {
1017
1069
  */
1018
1070
  function parseLlmText(raw, correlationId) {
1019
1071
  if (raw.trim() === "") {
1020
- logger$5.warn({ correlationId }, "llm:parse:empty");
1072
+ logger$6.warn({ correlationId }, "llm:parse:empty");
1021
1073
  return {
1022
1074
  analysis: null,
1023
1075
  degradedReason: "empty_response"
@@ -1029,49 +1081,79 @@ function parseLlmText(raw, correlationId) {
1029
1081
  degradedReason: "unparseable_response"
1030
1082
  };
1031
1083
  }
1084
+ //#endregion
1085
+ //#region src/infrastructure/llm/bedrock.provider.ts
1032
1086
  /**
1033
- * Gemini LLM provider using Google Generative AI SDK.
1034
- * Wrapped with circuit breaker for resilience.
1087
+ * Recognized transient Bedrock error names, mapped to their degradedReason.
1088
+ * Any other error name (or non-Error rejection) is rethrown by the caller.
1035
1089
  */
1036
- var GeminiProvider = class {
1037
- apiKey;
1090
+ const BEDROCK_AVAILABILITY_ERRORS = /* @__PURE__ */ new Map([
1091
+ ["ThrottlingException", "provider_unavailable"],
1092
+ ["ServiceUnavailableException", "provider_unavailable"],
1093
+ ["InternalServerException", "provider_unavailable"],
1094
+ ["ModelTimeoutException", "timeout"],
1095
+ ["AbortError", "timeout"]
1096
+ ]);
1097
+ function classifyBedrockError(error) {
1098
+ if (!(error instanceof Error)) return void 0;
1099
+ return BEDROCK_AVAILABILITY_ERRORS.get(error.name);
1100
+ }
1101
+ /**
1102
+ * Bedrock LLM provider using AWS Bedrock Runtime's Converse API.
1103
+ * No circuit breaker — Bedrock's own throttling/timeout errors are
1104
+ * classified directly into a degradedReason.
1105
+ */
1106
+ var BedrockProvider = class {
1038
1107
  model;
1039
- breaker;
1040
- constructor(apiKey, model = LLM_MODELS.Gemini) {
1041
- this.apiKey = apiKey;
1108
+ client = null;
1109
+ constructor(model = LLM_MODELS.Bedrock) {
1042
1110
  this.model = model;
1043
- this.breaker = new Breaker.default(this.analyzeRaw.bind(this), BREAKER_OPTIONS);
1111
+ }
1112
+ getClient() {
1113
+ if (!this.client) this.client = new BedrockRuntimeClient({});
1114
+ return this.client;
1044
1115
  }
1045
1116
  async analyze(cluster, traces) {
1046
1117
  const startMs = Date.now();
1047
- return {
1048
- ...await this.analyzeWithBreaker(cluster, traces),
1049
- provider: "gemini",
1050
- model: this.model,
1051
- latencyMs: Date.now() - startMs
1052
- };
1053
- }
1054
- async analyzeWithBreaker(cluster, traces) {
1118
+ const abortController = new AbortController();
1119
+ const timeoutHandle = setTimeout(() => abortController.abort(), HTTP_TIMEOUT_MS.LLM);
1055
1120
  try {
1056
- return await this.breaker.fire(cluster, traces);
1057
- } catch {
1058
- return this.analyzeRaw(cluster, traces);
1121
+ const response = await this.getClient().send(new ConverseCommand({
1122
+ modelId: this.model,
1123
+ system: [{ text: SYSTEM_PROMPT }],
1124
+ messages: [{
1125
+ role: "user",
1126
+ content: [{ text: buildUserPrompt(cluster, traces) }]
1127
+ }],
1128
+ inferenceConfig: { maxTokens: LLM_MAX_TOKENS }
1129
+ }), { abortSignal: abortController.signal });
1130
+ return {
1131
+ ...parseLlmText(response.output?.message?.content?.find((b) => b.text !== void 0)?.text ?? ""),
1132
+ provider: "bedrock",
1133
+ model: this.model,
1134
+ latencyMs: Date.now() - startMs,
1135
+ promptTokens: response.usage?.inputTokens ?? 0,
1136
+ completionTokens: response.usage?.outputTokens ?? 0
1137
+ };
1138
+ } catch (error) {
1139
+ const degradedReason = classifyBedrockError(error);
1140
+ if (degradedReason === void 0) throw error;
1141
+ return {
1142
+ analysis: null,
1143
+ degradedReason,
1144
+ provider: "bedrock",
1145
+ model: this.model,
1146
+ latencyMs: Date.now() - startMs,
1147
+ promptTokens: 0,
1148
+ completionTokens: 0
1149
+ };
1150
+ } finally {
1151
+ clearTimeout(timeoutHandle);
1059
1152
  }
1060
1153
  }
1061
- async analyzeRaw(cluster, traces) {
1062
- const { GoogleGenerativeAI } = await import("@google/generative-ai");
1063
- const result = await new GoogleGenerativeAI(this.apiKey).getGenerativeModel({
1064
- model: this.model,
1065
- systemInstruction: SYSTEM_PROMPT
1066
- }).generateContent(buildUserPrompt(cluster, traces));
1067
- const usage = result.response.usageMetadata;
1068
- return {
1069
- ...parseLlmText(result.response.text()),
1070
- promptTokens: usage?.promptTokenCount ?? 0,
1071
- completionTokens: usage?.candidatesTokenCount ?? 0
1072
- };
1073
- }
1074
1154
  };
1155
+ //#endregion
1156
+ //#region src/infrastructure/llm/claude.provider.ts
1075
1157
  /**
1076
1158
  * Claude LLM provider using Anthropic SDK.
1077
1159
  * Supports Claude Haiku and other models.
@@ -1105,30 +1187,108 @@ var ClaudeProvider = class {
1105
1187
  };
1106
1188
  }
1107
1189
  };
1190
+ //#endregion
1191
+ //#region src/infrastructure/llm/gemini.provider.ts
1192
+ const OPOSSUM_TIMEOUT_CODE = "ETIMEDOUT";
1193
+ const OPOSSUM_OPEN_BREAKER_CODE = "EOPENBREAKER";
1194
+ const UNDICI_CONNECT_TIMEOUT_CODE = "UND_ERR_CONNECT_TIMEOUT";
1195
+ function isErrorLike(value) {
1196
+ return typeof value === "object" && value !== null;
1197
+ }
1198
+ function hasTimeoutCode(error) {
1199
+ return error.code === OPOSSUM_TIMEOUT_CODE || error.code === UNDICI_CONNECT_TIMEOUT_CODE;
1200
+ }
1201
+ function isStandardFetchTimeout(error) {
1202
+ return error instanceof DOMException && error.name === "TimeoutError";
1203
+ }
1204
+ function classifyGeminiAvailabilityError(error) {
1205
+ if (!isErrorLike(error)) return void 0;
1206
+ if (error.code === OPOSSUM_OPEN_BREAKER_CODE) return "circuit_breaker_open";
1207
+ if (hasTimeoutCode(error)) return "timeout";
1208
+ if (error instanceof GoogleGenerativeAIAbortError) return "timeout";
1209
+ if (isStandardFetchTimeout(error)) return "timeout";
1210
+ if (isErrorLike(error.cause) && (hasTimeoutCode(error.cause) || isStandardFetchTimeout(error.cause))) return "timeout";
1211
+ }
1212
+ function degradedGeminiResult(degradedReason) {
1213
+ return {
1214
+ analysis: null,
1215
+ degradedReason,
1216
+ promptTokens: 0,
1217
+ completionTokens: 0
1218
+ };
1219
+ }
1220
+ const BREAKER_OPTIONS = {
1221
+ timeout: CIRCUIT_BREAKER.Timeout,
1222
+ errorThresholdPercentage: CIRCUIT_BREAKER.ErrorThresholdPercentage,
1223
+ resetTimeout: CIRCUIT_BREAKER.ResetTimeoutMs
1224
+ };
1108
1225
  /**
1109
- * Mock LLM provider for testing and local development.
1110
- * Returns deterministic responses without external API calls.
1226
+ * Gemini LLM provider using Google Generative AI SDK.
1227
+ * Wrapped with circuit breaker for resilience.
1111
1228
  */
1112
- var MockLLMProvider = class {
1113
- callLog = [];
1114
- async analyze(cluster, _traces) {
1115
- this.callLog.push({ cluster });
1229
+ var GeminiProvider = class {
1230
+ apiKey;
1231
+ model;
1232
+ breaker;
1233
+ constructor(apiKey, model = LLM_MODELS.Gemini) {
1234
+ this.apiKey = apiKey;
1235
+ this.model = model;
1236
+ this.breaker = new Breaker.default(this.analyzeRaw.bind(this), BREAKER_OPTIONS);
1237
+ }
1238
+ async analyze(cluster, traces) {
1239
+ const startMs = Date.now();
1116
1240
  return {
1117
- analysis: {
1118
- probable_cause: `Mock: ${cluster.alertType} on ${cluster.serviceName}`,
1119
- impacted_services: [cluster.serviceName],
1120
- recommended_steps: ["Check the logs", "Verify the deployment"],
1121
- urgency_level: "high",
1122
- requires_rollback: false
1123
- },
1124
- provider: MOCK_PROVIDER_NAME,
1125
- model: MOCK_PROVIDER_NAME,
1126
- latencyMs: 0,
1127
- promptTokens: 0,
1128
- completionTokens: 0
1241
+ ...await this.analyzeWithBreaker(cluster, traces),
1242
+ provider: "gemini",
1243
+ model: this.model,
1244
+ latencyMs: Date.now() - startMs
1245
+ };
1246
+ }
1247
+ async analyzeWithBreaker(cluster, traces) {
1248
+ try {
1249
+ return await this.breaker.fire(cluster, traces);
1250
+ } catch (error) {
1251
+ const degradedReason = classifyGeminiAvailabilityError(error);
1252
+ if (degradedReason !== void 0) return degradedGeminiResult(degradedReason);
1253
+ throw error;
1254
+ }
1255
+ }
1256
+ async analyzeRaw(cluster, traces) {
1257
+ const result = await new GoogleGenerativeAI(this.apiKey).getGenerativeModel({
1258
+ model: this.model,
1259
+ systemInstruction: SYSTEM_PROMPT
1260
+ }).generateContent(buildUserPrompt(cluster, traces));
1261
+ const usage = result.response.usageMetadata;
1262
+ return {
1263
+ ...parseLlmText(result.response.text()),
1264
+ promptTokens: usage?.promptTokenCount ?? 0,
1265
+ completionTokens: usage?.candidatesTokenCount ?? 0
1129
1266
  };
1130
1267
  }
1131
1268
  };
1269
+ //#endregion
1270
+ //#region src/infrastructure/llm/openrouter.provider.ts
1271
+ const logger$5 = createLogger();
1272
+ /**
1273
+ * Schema for OpenRouter API response validation.
1274
+ * Ensures type safety at the external boundary.
1275
+ */
1276
+ const OpenRouterResponseSchema = z.object({
1277
+ id: z.string().optional(),
1278
+ choices: z.array(z.object({
1279
+ index: z.number(),
1280
+ message: z.object({
1281
+ role: z.string(),
1282
+ content: z.string().optional()
1283
+ }),
1284
+ finish_reason: z.string().optional()
1285
+ })),
1286
+ usage: z.object({
1287
+ prompt_tokens: z.number().optional(),
1288
+ completion_tokens: z.number().optional(),
1289
+ total_tokens: z.number().optional()
1290
+ }).optional()
1291
+ });
1132
1292
  /**
1133
1293
  * OpenRouter LLM provider using OpenAI-compatible API.
1134
1294
  * Supports various open models (Qwen, etc.) via OpenRouter gateway.
@@ -1292,6 +1452,8 @@ var OpenRouterProvider = class {
1292
1452
  throw new Error("OpenRouter API exhausted all models");
1293
1453
  }
1294
1454
  };
1455
+ //#endregion
1456
+ //#region src/infrastructure/llm/factory.ts
1295
1457
  /**
1296
1458
  * Registry mapping provider names to their factory functions.
1297
1459
  * Used by createLLMProvider to instantiate the appropriate LLM client.
@@ -1300,7 +1462,8 @@ const LLM_PROVIDER_REGISTRY = /* @__PURE__ */ new Map([
1300
1462
  ["gemini", (apiKey, model) => new GeminiProvider(apiKey, model)],
1301
1463
  ["claude", (apiKey, model) => new ClaudeProvider(apiKey, model)],
1302
1464
  ["openrouter", (apiKey, model, options) => new OpenRouterProvider(apiKey, model, options?.fallbackModels, options?.fallbackTimeoutMs, "openrouter")],
1303
- ["qwen", (apiKey, model, options) => new OpenRouterProvider(apiKey, model, options?.fallbackModels, options?.fallbackTimeoutMs, "qwen")]
1465
+ ["qwen", (apiKey, model, options) => new OpenRouterProvider(apiKey, model, options?.fallbackModels, options?.fallbackTimeoutMs, "qwen")],
1466
+ ["bedrock", (_apiKey, model) => new BedrockProvider(model)]
1304
1467
  ]);
1305
1468
  function createLLMProvider(provider, apiKey, model, options) {
1306
1469
  const factory = LLM_PROVIDER_REGISTRY.get(provider);
@@ -1308,9 +1471,37 @@ function createLLMProvider(provider, apiKey, model, options) {
1308
1471
  const supported = Array.from(LLM_PROVIDER_REGISTRY.keys()).join(", ");
1309
1472
  throw new Error(`Unknown LLM_PROVIDER: "${provider}". Supported: ${supported}`);
1310
1473
  }
1311
- return factory(apiKey, model, options);
1474
+ return factory(apiKey ?? "", model, options);
1312
1475
  }
1313
1476
  //#endregion
1477
+ //#region src/infrastructure/llm/mock.provider.ts
1478
+ /** Provider name reported by MockLLMProvider results. */
1479
+ const MOCK_PROVIDER_NAME = "mock";
1480
+ /**
1481
+ * Mock LLM provider for testing and local development.
1482
+ * Returns deterministic responses without external API calls.
1483
+ */
1484
+ var MockLLMProvider = class {
1485
+ callLog = [];
1486
+ async analyze(cluster, _traces) {
1487
+ this.callLog.push({ cluster });
1488
+ return {
1489
+ analysis: {
1490
+ probable_cause: `Mock: ${cluster.alertType} on ${cluster.serviceName}`,
1491
+ impacted_services: [cluster.serviceName],
1492
+ recommended_steps: ["Check the logs", "Verify the deployment"],
1493
+ urgency_level: "high",
1494
+ requires_rollback: false
1495
+ },
1496
+ provider: MOCK_PROVIDER_NAME,
1497
+ model: MOCK_PROVIDER_NAME,
1498
+ latencyMs: 0,
1499
+ promptTokens: 0,
1500
+ completionTokens: 0
1501
+ };
1502
+ }
1503
+ };
1504
+ //#endregion
1314
1505
  //#region src/shared/factory-registry.ts
1315
1506
  var FactoryRegistry = class {
1316
1507
  _factories = /* @__PURE__ */ new Map();
@@ -35621,6 +35812,20 @@ function parseBooleanEnv(value) {
35621
35812
  const normalized = value.trim().toLowerCase();
35622
35813
  return normalized === "true" || normalized === "1";
35623
35814
  }
35815
+ function llmApiKeyRequired(llmProvider, llmApiKey) {
35816
+ return llmProvider !== "bedrock" && !llmApiKey;
35817
+ }
35818
+ /**
35819
+ * Resolves the effective LLM_MODEL. An explicit LLM_MODEL always wins, for any
35820
+ * provider. BEDROCK_DEFAULT_MODEL (set by the CDK stack from the deployment
35821
+ * region — see junando-stack.ts) is used ONLY as a fallback when the provider
35822
+ * is bedrock, so a Bedrock-specific value can never leak into another
35823
+ * provider's model override.
35824
+ */
35825
+ function resolveLlmModel(llmProvider, llmModel, bedrockDefaultModel) {
35826
+ if (llmModel) return llmModel;
35827
+ return llmProvider === "bedrock" ? bedrockDefaultModel : void 0;
35828
+ }
35624
35829
  function parseOptionalStringArray(value) {
35625
35830
  if (value === void 0 || value === "") return void 0;
35626
35831
  return value.split(",").map((s) => s.trim()).filter(Boolean);
@@ -35659,6 +35864,8 @@ async function loadSecretsFromSSM() {
35659
35864
  const key = param.Name.replace(`${prefix}/`, "").replaceAll("-", "_").toUpperCase();
35660
35865
  process.env[key] = param.Value;
35661
35866
  }
35867
+ const missing = result.InvalidParameters ?? [];
35868
+ if (missing.length > 0) createLogger().warn({ missingParameters: missing }, "SSM parameters not found");
35662
35869
  } catch (err) {
35663
35870
  createLogger().error({ err }, "Failed to load SSM parameters");
35664
35871
  }
@@ -35668,9 +35875,10 @@ const ConfigSchema = z.object({
35668
35875
  "gemini",
35669
35876
  "claude",
35670
35877
  "openrouter",
35671
- "qwen"
35878
+ "qwen",
35879
+ "bedrock"
35672
35880
  ]),
35673
- llmApiKey: z.string().min(1),
35881
+ llmApiKey: z.string().min(1).optional(),
35674
35882
  llmModel: z.string().optional().transform((v) => v === "" ? void 0 : v),
35675
35883
  notifierType: z.enum(["slack", "teams"]).default("slack"),
35676
35884
  slackBotToken: z.string().startsWith("xoxb-").optional(),
@@ -35680,7 +35888,9 @@ const ConfigSchema = z.object({
35680
35888
  rollbackActionAllowedSlackUserIds: z.array(z.string()).optional(),
35681
35889
  teamsWebhookUrl: z.string().url().optional(),
35682
35890
  lokiUrl: z.string().optional().transform((v) => v === "" ? void 0 : v),
35683
- redisUrl: z.string().url(),
35891
+ dedupStore: z.enum(["dynamodb", "redis"]).default("dynamodb"),
35892
+ dedupTableName: z.string().min(1).optional(),
35893
+ redisUrl: z.string().url().optional(),
35684
35894
  sqsQueueUrl: z.string().url().optional().or(z.literal("")),
35685
35895
  dedupTtlSeconds: z.coerce.number().int().positive().default(300),
35686
35896
  clusterWindowMs: z.coerce.number().int().positive().default(12e4),
@@ -35741,13 +35951,28 @@ const ConfigSchema = z.object({
35741
35951
  });
35742
35952
  }
35743
35953
  }
35954
+ if (data.dedupStore === "dynamodb" && !data.dedupTableName) ctx.addIssue({
35955
+ code: z.ZodIssueCode.custom,
35956
+ path: ["dedupTableName"],
35957
+ message: "[dedupStore: dynamodb] DEDUP_TABLE_NAME is required"
35958
+ });
35959
+ if (data.dedupStore === "redis" && !data.redisUrl) ctx.addIssue({
35960
+ code: z.ZodIssueCode.custom,
35961
+ path: ["redisUrl"],
35962
+ message: "[dedupStore: redis] REDIS_URL is required"
35963
+ });
35964
+ if (llmApiKeyRequired(data.llmProvider, data.llmApiKey)) ctx.addIssue({
35965
+ code: z.ZodIssueCode.custom,
35966
+ path: ["llmApiKey"],
35967
+ message: `[llmProvider: ${data.llmProvider}] LLM_API_KEY is required`
35968
+ });
35744
35969
  });
35745
35970
  async function loadConfig() {
35746
35971
  await loadSecretsFromSSM();
35747
35972
  const result = ConfigSchema.safeParse({
35748
35973
  llmProvider: process.env["LLM_PROVIDER"],
35749
35974
  llmApiKey: process.env["LLM_API_KEY"],
35750
- llmModel: process.env["LLM_MODEL"],
35975
+ llmModel: resolveLlmModel(process.env["LLM_PROVIDER"], process.env["LLM_MODEL"], process.env["BEDROCK_DEFAULT_MODEL"]),
35751
35976
  notifierType: process.env["NOTIFIER_TYPE"],
35752
35977
  slackBotToken: process.env["SLACK_BOT_TOKEN"],
35753
35978
  slackSigningSecret: process.env["SLACK_SIGNING_SECRET"],
@@ -35755,6 +35980,8 @@ async function loadConfig() {
35755
35980
  teamsWebhookUrl: process.env["TEAMS_WEBHOOK_URL"],
35756
35981
  lokiUrl: process.env["LOKI_URL"],
35757
35982
  redisUrl: process.env["REDIS_URL"],
35983
+ dedupStore: process.env["DEDUP_STORE"],
35984
+ dedupTableName: process.env["DEDUP_TABLE_NAME"],
35758
35985
  sqsQueueUrl: process.env["SQS_QUEUE_URL"],
35759
35986
  dedupTtlSeconds: process.env["DEDUP_TTL_SECONDS"],
35760
35987
  clusterWindowMs: process.env["CLUSTER_WINDOW_MS"],
@@ -35768,11 +35995,14 @@ async function loadConfig() {
35768
35995
  });
35769
35996
  if (!result.success) {
35770
35997
  const errorMessages = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
35998
+ const rawLlmProvider = process.env["LLM_PROVIDER"];
35999
+ const rawLlmApiKey = process.env["LLM_API_KEY"];
36000
+ if (!errorMessages.some((m) => m.startsWith("llmApiKey")) && llmApiKeyRequired(rawLlmProvider, rawLlmApiKey)) errorMessages.push(`llmApiKey: [llmProvider: ${rawLlmProvider ?? "(unset)"}] LLM_API_KEY is required`);
35771
36001
  throw new Error(`Invalid configuration:\n - ${errorMessages.join("\n - ")}`);
35772
36002
  }
35773
36003
  return result.data;
35774
36004
  }
35775
36005
  //#endregion
35776
- export { ALERT_TYPE_LABELS, AlertClusterSchema, AlertStatusSchema, AlertType, AlertmanagerPayloadSchema, CIRCUIT_BREAKER, ChannelRegistry, ClaudeProvider, ClusteringService, Component, ConsoleNotifier, DEDUP_TTL_MS_MULTIPLIER, DEV_SERVER_PORT, FactoryRegistry, Fingerprint, GeminiProvider, HOUR_MS, HTTP_TIMEOUT_MS, InMemoryAlertQueue, InMemoryDeduplicationStore, InMemoryIndexer, IncidentSchema, LLMAnalysisSchema, LLMProviderType, LLM_FALLBACK_DEFAULTS, LLM_MAX_TOKENS, LLM_MODELS, LokiTraceRepository, MockLLMProvider, MockTraceRepository, NoopRollbackActionHandler, NormalizedAlertSchema, NotifyOutcome, OpenSearchIndexer, Outcome, PAYLOAD_DEFAULTS, ProcessIncidentUseCase, RATE_LIMITER, REDIS_KEY_PREFIX, ROLLBACK_ACTION_ID, RedisDeduplicationStore, RoutingNotifier, RuleActionSchema, RuleActionType, RuleConditionSchema, RuleConfigurationSchema, RuleEngine, RuleEvaluationPhase, RuleSchema, RuleSectionSchema, SLACK_API_URL, SQSAlertQueue, SamplingDecision, SeverityLevel, SlackNotifier, Stage, TEAMS_WEBHOOK_TIMEOUT_MS, TeamsNotifier, TeamsNotifierError, URGENCY_EMOJI, UrgencyLevelSchema, WEBHOOK_DEFAULTS, WideEventBuilder, compileCondition, createLLMProvider, createLogger, createNotifier, createRollbackActionHandler, dispatchActions, flushLoki, loadConfig, metrics_exports as metrics, normalizePayload, parseRuleConfig, reinitLogger, startSqsLagPoller };
36006
+ export { ALERT_TYPE_LABELS, AlertClusterSchema, AlertStatusSchema, AlertType, AlertmanagerPayloadSchema, BedrockProvider, CIRCUIT_BREAKER, ChannelRegistry, ClaudeProvider, ClusteringService, Component, ConsoleNotifier, DEDUP_TTL_MS_MULTIPLIER, DEV_SERVER_PORT, DynamoDBDeduplicationStore, FactoryRegistry, Fingerprint, GeminiProvider, HOUR_MS, HTTP_TIMEOUT_MS, InMemoryAlertQueue, InMemoryDeduplicationStore, InMemoryIndexer, IncidentSchema, LLMAnalysisSchema, LLMProviderType, LLM_FALLBACK_DEFAULTS, LLM_MAX_TOKENS, LLM_MODELS, LokiTraceRepository, MockLLMProvider, MockTraceRepository, NoopRollbackActionHandler, NormalizedAlertSchema, NotifyOutcome, OpenRouterProvider, OpenSearchIndexer, Outcome, PAYLOAD_DEFAULTS, ProcessIncidentUseCase, RATE_LIMITER, REDIS_KEY_PREFIX, ROLLBACK_ACTION_ID, RedisDeduplicationStore, RoutingNotifier, RuleActionSchema, RuleActionType, RuleConditionSchema, RuleConfigurationSchema, RuleEngine, RuleEvaluationPhase, RuleSchema, RuleSectionSchema, SLACK_API_URL, SQSAlertQueue, SamplingDecision, SeverityLevel, SlackNotifier, Stage, TEAMS_WEBHOOK_TIMEOUT_MS, TeamsNotifier, TeamsNotifierError, URGENCY_EMOJI, UrgencyLevelSchema, WEBHOOK_DEFAULTS, WideEventBuilder, compileCondition, createLLMProvider, createLogger, createNotifier, createRollbackActionHandler, dispatchActions, flushLoki, loadConfig, metrics_exports as metrics, normalizePayload, parseRuleConfig, reinitLogger, startSqsLagPoller };
35777
36007
 
35778
36008
  //# sourceMappingURL=index.js.map