palaryn 0.4.8 → 0.4.10

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.
@@ -17,6 +17,7 @@ import { PlanTier } from '../types/subscription';
17
17
  import { MODEL_PRICING, resolveModelPricing } from '../budget/model-pricing';
18
18
  import { PolicyEngine } from '../policy/engine';
19
19
  import { ToolCall } from '../types/tool-call';
20
+ import { detectLlmProvider, llmFetchWithFallback, LlmProvider } from '../llm-utils';
20
21
 
21
22
  /** Strip HTML tags from user-provided display strings to prevent stored XSS. */
22
23
  function stripHtmlTags(input: string): string {
@@ -1239,18 +1240,9 @@ Input: "Allow GET to example.com, block DELETE everywhere"
1239
1240
  Output: [{"name":"allow-get-example","description":"Allow GET requests to example.com","effect":"ALLOW","priority":10,"conditions":{"methods":["GET"],"domains":["example.com"]}},{"name":"block-delete-all","description":"Block all DELETE operations","effect":"DENY","priority":5,"conditions":{"capabilities":["delete"],"methods":["DELETE"]}}]`;
1240
1241
 
1241
1242
  try {
1242
- const controller = new AbortController();
1243
- const timeout = setTimeout(() => controller.abort(), 15000);
1244
-
1245
- // Detect provider from API key prefix
1246
- const isOpenAI = apiKey.startsWith('sk-proj-') || apiKey.startsWith('sk-');
1247
- const llmUrl = isOpenAI
1248
- ? 'https://api.openai.com/v1/chat/completions'
1249
- : 'https://api.anthropic.com/v1/messages';
1250
- const llmHeaders: Record<string, string> = isOpenAI
1251
- ? { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }
1252
- : { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
1253
- const llmBody = isOpenAI
1243
+ const fallbackKey = process.env.PALARYN_LLM_FALLBACK_KEY;
1244
+
1245
+ const buildBody = (provider: LlmProvider) => provider.isOpenAI
1254
1246
  ? JSON.stringify({
1255
1247
  model: 'gpt-4.1-mini',
1256
1248
  max_tokens: 1024,
@@ -1266,24 +1258,20 @@ Output: [{"name":"allow-get-example","description":"Allow GET requests to exampl
1266
1258
  messages: [{ role: 'user', content: trimmed }],
1267
1259
  });
1268
1260
 
1269
- const llmRes = await fetch(llmUrl, {
1270
- method: 'POST',
1271
- headers: llmHeaders,
1272
- body: llmBody,
1273
- signal: controller.signal,
1261
+ const { response: llmRes, provider, usedFallback } = await llmFetchWithFallback({
1262
+ primaryKey: apiKey,
1263
+ fallbackKey,
1264
+ buildBody,
1265
+ timeoutMs: 15000,
1266
+ tag: '[generate-rule]',
1274
1267
  });
1275
1268
 
1276
- clearTimeout(timeout);
1277
-
1278
- if (!llmRes.ok) {
1279
- const errBody = await llmRes.text();
1280
- console.error('[generate-rule] LLM API error:', llmRes.status, errBody);
1281
- res.status(502).json({ error: 'Failed to generate rule. LLM API returned an error.' });
1282
- return;
1269
+ if (usedFallback) {
1270
+ console.log(`[generate-rule] Used fallback provider (${provider.isOpenAI ? 'OpenAI' : 'Anthropic'})`);
1283
1271
  }
1284
1272
 
1285
1273
  const llmData = await llmRes.json() as any;
1286
- const text = isOpenAI
1274
+ const text = provider.isOpenAI
1287
1275
  ? (llmData.choices?.[0]?.message?.content || '')
1288
1276
  : (llmData.content?.[0]?.text || '');
1289
1277
 
@@ -1326,8 +1314,9 @@ Output: [{"name":"allow-get-example","description":"Allow GET requests to exampl
1326
1314
  res.status(504).json({ error: 'Rule generation timed out. Please try again.' });
1327
1315
  return;
1328
1316
  }
1329
- console.error('[generate-rule] Unexpected error:', err);
1330
- res.status(500).json({ error: 'Failed to generate rule. Please try again.' });
1317
+ const errMsg = err instanceof Error ? err.message : String(err);
1318
+ console.error('[generate-rule] Unexpected error:', errMsg);
1319
+ res.status(502).json({ error: 'Failed to generate rule. LLM API returned an error.' });
1331
1320
  }
1332
1321
  });
1333
1322
 
@@ -1457,13 +1446,7 @@ ${current_policy ? `\nThe user has an existing policy. Refine it based on their
1457
1446
  res.flushHeaders();
1458
1447
 
1459
1448
  try {
1460
- const isOpenAI = apiKey.startsWith('sk-proj-') || apiKey.startsWith('sk-');
1461
- const llmUrl = isOpenAI
1462
- ? 'https://api.openai.com/v1/chat/completions'
1463
- : 'https://api.anthropic.com/v1/messages';
1464
- const llmHeaders: Record<string, string> = isOpenAI
1465
- ? { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }
1466
- : { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
1449
+ const fallbackKey = process.env.PALARYN_LLM_FALLBACK_KEY;
1467
1450
 
1468
1451
  // Build message array for multi-turn context
1469
1452
  const chatMessages = messages.map((m: { role: string; content: string }) => ({
@@ -1471,7 +1454,7 @@ ${current_policy ? `\nThe user has an existing policy. Refine it based on their
1471
1454
  content: m.content.slice(0, 2000),
1472
1455
  }));
1473
1456
 
1474
- const llmBody = isOpenAI
1457
+ const buildBody = (provider: LlmProvider) => provider.isOpenAI
1475
1458
  ? JSON.stringify({
1476
1459
  model: 'gpt-4.1-mini',
1477
1460
  max_tokens: 2048,
@@ -1489,32 +1472,24 @@ ${current_policy ? `\nThe user has an existing policy. Refine it based on their
1489
1472
  messages: chatMessages,
1490
1473
  });
1491
1474
 
1492
- const controller = new AbortController();
1493
- const timeout = setTimeout(() => controller.abort(), 60000);
1494
-
1495
- // Handle client disconnect
1496
- req.on('close', () => {
1497
- controller.abort();
1498
- clearTimeout(timeout);
1499
- });
1475
+ const clientAbort = new AbortController();
1476
+ req.on('close', () => clientAbort.abort());
1500
1477
 
1501
- const llmRes = await fetch(llmUrl, {
1502
- method: 'POST',
1503
- headers: llmHeaders,
1504
- body: llmBody,
1505
- signal: controller.signal,
1478
+ const { response: llmRes, provider, usedFallback } = await llmFetchWithFallback({
1479
+ primaryKey: apiKey,
1480
+ fallbackKey,
1481
+ buildBody,
1482
+ timeoutMs: 15000,
1483
+ signal: clientAbort.signal,
1484
+ tag: '[policy-chat]',
1506
1485
  });
1507
1486
 
1508
- clearTimeout(timeout);
1509
-
1510
- if (!llmRes.ok) {
1511
- const errBody = await llmRes.text();
1512
- console.error('[policy-chat] LLM API error:', llmRes.status, errBody);
1513
- res.write(`event: error\ndata: ${JSON.stringify({ error: 'LLM API returned an error.' })}\n\n`);
1514
- res.end();
1515
- return;
1487
+ if (usedFallback) {
1488
+ console.log(`[policy-chat] Used fallback provider (${provider.isOpenAI ? 'OpenAI' : 'Anthropic'})`);
1516
1489
  }
1517
1490
 
1491
+ const isOpenAI = provider.isOpenAI;
1492
+
1518
1493
  if (!llmRes.body) {
1519
1494
  res.write(`event: error\ndata: ${JSON.stringify({ error: 'No response stream from LLM.' })}\n\n`);
1520
1495
  res.end();