@adaptic/lumic-utils 1.0.24 → 1.0.25
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/{apollo-client.client-CyxU1hTu.js → apollo-client.client-D6jG5B1n.js} +3 -3
- package/dist/{apollo-client.client-CyxU1hTu.js.map → apollo-client.client-D6jG5B1n.js.map} +1 -1
- package/dist/{apollo-client.client-9wJcufhf.js → apollo-client.client-DLW-p46Y.js} +4 -4
- package/dist/{apollo-client.client-9wJcufhf.js.map → apollo-client.client-DLW-p46Y.js.map} +1 -1
- package/dist/{apollo-client.server-CbagxkmK.js → apollo-client.server-CXLiLmSz.js} +3 -3
- package/dist/{apollo-client.server-CbagxkmK.js.map → apollo-client.server-CXLiLmSz.js.map} +1 -1
- package/dist/{apollo-client.server-DB3jLbBx.js → apollo-client.server-Cx30LaNh.js} +3 -3
- package/dist/{apollo-client.server-DB3jLbBx.js.map → apollo-client.server-Cx30LaNh.js.map} +1 -1
- package/dist/{index-EMQ1uJMh.js → index-Bvc61lYS.js} +348 -381
- package/dist/{index-EMQ1uJMh.js.map → index-Bvc61lYS.js.map} +1 -1
- package/dist/{index-KzQOh2uu.js → index-ChygDqeG.js} +348 -381
- package/dist/{index-KzQOh2uu.js.map → index-ChygDqeG.js.map} +1 -1
- package/dist/{index-B4yVKGNR.js → index-CqnZ-XiI.js} +2 -2
- package/dist/{index-B4yVKGNR.js.map → index-CqnZ-XiI.js.map} +1 -1
- package/dist/{index-CL79JTWc.js → index-D2TPDKw0.js} +2 -2
- package/dist/{index-CL79JTWc.js.map → index-D2TPDKw0.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/test.cjs +17 -21
- package/dist/test.cjs.map +1 -1
- package/dist/test.mjs +17 -21
- package/dist/test.mjs.map +1 -1
- package/dist/types/functions/get-weather.d.ts +1 -1
- package/dist/types/functions/google-sheets.d.ts +2 -2
- package/dist/types/functions/json-llm-tools.d.ts +1 -1
- package/dist/types/functions/llm-anthropic.d.ts +1 -1
- package/dist/types/functions/llm-deepseek.d.ts +2 -2
- package/dist/types/functions/llm-openai-compatible.d.ts +2 -2
- package/dist/types/functions/llm-openai.d.ts +8 -3
- package/dist/types/functions/llm-utils.d.ts +1 -1
- package/dist/types/test-llm-functions-archive.d.ts +1 -1
- package/dist/types/test.d.ts +1 -1
- package/dist/types/types/openai-types.d.ts +26 -10
- package/dist/types/utils/aws-initialise.d.ts +4 -4
- package/dist/types/utils/retry.d.ts +8 -0
- package/package.json +2 -2
|
@@ -571,19 +571,10 @@ function getModelProvider(model) {
|
|
|
571
571
|
return SUPPORTED_MODELS[model].provider;
|
|
572
572
|
}
|
|
573
573
|
/**
|
|
574
|
-
* Default model tiers per provider for use with LLM_MODEL_MINI/NORMAL/ADVANCED env vars
|
|
575
|
-
*
|
|
576
|
-
* OpenAI `advanced` was reverted from `gpt-5.5` back to `gpt-5.4` on 2026-05-30:
|
|
577
|
-
* the heavy tool-call prompt in adaptic-engine's trading-decision path was
|
|
578
|
-
* timing out even after the engine raised `LLM_CALL_TIMEOUT_MS` to 90s (5+
|
|
579
|
-
* tickers per loop still exceeded the budget). `gpt-5.5`'s 1M-context window
|
|
580
|
-
* is not required on that path — the prompt fits comfortably inside `gpt-5.4`'s
|
|
581
|
-
* envelope and `gpt-5.4` returns inside the original 25-30s p95. `gpt-5.5`
|
|
582
|
-
* remains registered in OPENAI_MODELS and reachable by explicit model id;
|
|
583
|
-
* only the `advanced` tier default is rolled back.
|
|
574
|
+
* Default model tiers per provider for use with LLM_MODEL_MINI/NORMAL/ADVANCED env vars
|
|
584
575
|
*/
|
|
585
576
|
const PROVIDER_DEFAULT_MODELS = {
|
|
586
|
-
openai: { mini: 'gpt-5.4-nano', normal: 'gpt-5.4-mini', advanced: 'gpt-5.
|
|
577
|
+
openai: { mini: 'gpt-5.4-nano', normal: 'gpt-5.4-mini', advanced: 'gpt-5.5' },
|
|
587
578
|
anthropic: { mini: 'claude-haiku-4-5', normal: 'claude-sonnet-4-6', advanced: 'claude-opus-4-7' },
|
|
588
579
|
deepseek: { mini: 'deepseek-v4-flash', normal: 'deepseek-v4-flash', advanced: 'deepseek-v4-pro' },
|
|
589
580
|
kimi: { mini: 'kimi-k2-0905-preview', normal: 'kimi-k2.5', advanced: 'kimi-k2.6' },
|
|
@@ -1537,13 +1528,11 @@ let openai;
|
|
|
1537
1528
|
function initializeOpenAI(apiKey) {
|
|
1538
1529
|
const key = apiKey || getSecrets().openai.apiKey;
|
|
1539
1530
|
if (!key) {
|
|
1540
|
-
throw new Error(
|
|
1531
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
1541
1532
|
}
|
|
1542
1533
|
return new OpenAI({
|
|
1543
1534
|
apiKey: key,
|
|
1544
|
-
defaultHeaders: {
|
|
1545
|
-
"User-Agent": "My Server-side Application (compatible; OpenAI API Client)",
|
|
1546
|
-
},
|
|
1535
|
+
defaultHeaders: { 'User-Agent': 'My Server-side Application (compatible; OpenAI API Client)' },
|
|
1547
1536
|
});
|
|
1548
1537
|
}
|
|
1549
1538
|
/**
|
|
@@ -1561,7 +1550,9 @@ function initializeOpenAI(apiKey) {
|
|
|
1561
1550
|
*/
|
|
1562
1551
|
async function fixJsonWithAI(jsonStr, options) {
|
|
1563
1552
|
// Backward compatibility: handle string apiKey as first positional param
|
|
1564
|
-
const opts = typeof options ===
|
|
1553
|
+
const opts = typeof options === 'string'
|
|
1554
|
+
? { apiKey: options }
|
|
1555
|
+
: (options ?? {});
|
|
1565
1556
|
const apiKey = opts.apiKey;
|
|
1566
1557
|
const maxDepth = opts.maxDepth ?? 2;
|
|
1567
1558
|
const depth = opts._depth ?? 0;
|
|
@@ -1574,18 +1565,18 @@ async function fixJsonWithAI(jsonStr, options) {
|
|
|
1574
1565
|
openai = initializeOpenAI(apiKey);
|
|
1575
1566
|
}
|
|
1576
1567
|
const completion = await openai.chat.completions.create({
|
|
1577
|
-
model:
|
|
1568
|
+
model: 'gpt-5-mini',
|
|
1578
1569
|
messages: [
|
|
1579
1570
|
{
|
|
1580
|
-
role:
|
|
1581
|
-
content:
|
|
1571
|
+
role: 'system',
|
|
1572
|
+
content: 'You are a JSON fixer. Return only valid JSON without any additional text or explanation.'
|
|
1582
1573
|
},
|
|
1583
1574
|
{
|
|
1584
|
-
role:
|
|
1585
|
-
content: `Fix this broken JSON:\n${jsonStr}
|
|
1586
|
-
}
|
|
1575
|
+
role: 'user',
|
|
1576
|
+
content: `Fix this broken JSON:\n${jsonStr}`
|
|
1577
|
+
}
|
|
1587
1578
|
],
|
|
1588
|
-
response_format: { type:
|
|
1579
|
+
response_format: { type: 'json_object' }
|
|
1589
1580
|
});
|
|
1590
1581
|
const fixedJson = completion.choices[0]?.message?.content;
|
|
1591
1582
|
if (fixedJson && isValidJson(fixedJson)) {
|
|
@@ -1599,32 +1590,23 @@ async function fixJsonWithAI(jsonStr, options) {
|
|
|
1599
1590
|
_depth: depth + 1,
|
|
1600
1591
|
});
|
|
1601
1592
|
}
|
|
1602
|
-
throw new JsonParseError(
|
|
1593
|
+
throw new JsonParseError('Failed to fix JSON with AI: returned invalid JSON');
|
|
1603
1594
|
}
|
|
1604
1595
|
catch (err) {
|
|
1605
1596
|
// Re-throw JsonParseError as-is (e.g. max depth exceeded)
|
|
1606
1597
|
if (err instanceof JsonParseError) {
|
|
1607
1598
|
throw err;
|
|
1608
1599
|
}
|
|
1609
|
-
const errorMessage = err instanceof Error ? err.message :
|
|
1600
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
1610
1601
|
throw new JsonParseError(`Error fixing JSON with AI: ${errorMessage}`);
|
|
1611
1602
|
}
|
|
1612
1603
|
}
|
|
1613
1604
|
|
|
1614
1605
|
// llm-utils.ts
|
|
1615
1606
|
function normalizeModelName(model) {
|
|
1616
|
-
return model.replace(/_/g,
|
|
1617
|
-
}
|
|
1618
|
-
const CODE_BLOCK_TYPES = [
|
|
1619
|
-
"javascript",
|
|
1620
|
-
"js",
|
|
1621
|
-
"graphql",
|
|
1622
|
-
"json",
|
|
1623
|
-
"typescript",
|
|
1624
|
-
"python",
|
|
1625
|
-
"markdown",
|
|
1626
|
-
"yaml",
|
|
1627
|
-
];
|
|
1607
|
+
return model.replace(/_/g, '-').toLowerCase();
|
|
1608
|
+
}
|
|
1609
|
+
const CODE_BLOCK_TYPES = ['javascript', 'js', 'graphql', 'json', 'typescript', 'python', 'markdown', 'yaml'];
|
|
1628
1610
|
/**
|
|
1629
1611
|
* Tries to parse JSON from the given content string according to the specified
|
|
1630
1612
|
* response format. If the response format is 'json' or a JSON schema object, it
|
|
@@ -1648,9 +1630,7 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1648
1630
|
if (recursionDepth > maxRetries) {
|
|
1649
1631
|
throw new Error(`Maximum recursion depth (${maxRetries}) exceeded while parsing JSON. AI fixing may have returned broken JSON.`);
|
|
1650
1632
|
}
|
|
1651
|
-
if (responseFormat ===
|
|
1652
|
-
(typeof responseFormat === "object" &&
|
|
1653
|
-
responseFormat?.type === "json_schema")) {
|
|
1633
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat?.type === 'json_schema')) {
|
|
1654
1634
|
let cleanedContent = content.trim();
|
|
1655
1635
|
let detectedType = null;
|
|
1656
1636
|
// Remove code block markers if present
|
|
@@ -1661,8 +1641,8 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1661
1641
|
}
|
|
1662
1642
|
return false;
|
|
1663
1643
|
});
|
|
1664
|
-
if (startsWithCodeBlock && cleanedContent.endsWith(
|
|
1665
|
-
const firstLineEndIndex = cleanedContent.indexOf(
|
|
1644
|
+
if (startsWithCodeBlock && cleanedContent.endsWith('```')) {
|
|
1645
|
+
const firstLineEndIndex = cleanedContent.indexOf('\n');
|
|
1666
1646
|
if (firstLineEndIndex !== -1) {
|
|
1667
1647
|
cleanedContent = cleanedContent.slice(firstLineEndIndex + 1, -3).trim();
|
|
1668
1648
|
getLumicLogger().info(`Code block for type ${detectedType} detected and removed. Cleaned content: ${cleanedContent}`);
|
|
@@ -1695,7 +1675,7 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1695
1675
|
},
|
|
1696
1676
|
// Strategy 3: Remove leading/trailing text and try parsing
|
|
1697
1677
|
() => {
|
|
1698
|
-
const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g,
|
|
1678
|
+
const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g, '');
|
|
1699
1679
|
try {
|
|
1700
1680
|
return JSON.parse(jsonMatch);
|
|
1701
1681
|
}
|
|
@@ -1717,7 +1697,7 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1717
1697
|
}
|
|
1718
1698
|
// Strategy 5: Use AI fixing (only if explicitly enabled)
|
|
1719
1699
|
if (enableAiFix) {
|
|
1720
|
-
getLumicLogger().warn(
|
|
1700
|
+
getLumicLogger().warn('Using AI-powered JSON fixing. This adds cost and latency. Consider improving JSON generation instead.');
|
|
1721
1701
|
try {
|
|
1722
1702
|
const aiFixedJson = await fixJsonWithAI(cleanedContent);
|
|
1723
1703
|
if (aiFixedJson !== null) {
|
|
@@ -1730,7 +1710,7 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1730
1710
|
catch {
|
|
1731
1711
|
// AI returned something that looks like JSON but isn't valid
|
|
1732
1712
|
// Increment recursion depth and try parsing the AI response
|
|
1733
|
-
getLumicLogger().warn(
|
|
1713
|
+
getLumicLogger().warn('AI fixing returned invalid JSON, attempting recursive parse...');
|
|
1734
1714
|
return parseResponse(JSON.stringify(aiFixedJson), responseFormat, {
|
|
1735
1715
|
...options,
|
|
1736
1716
|
_recursionDepth: recursionDepth + 1,
|
|
@@ -1739,14 +1719,14 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1739
1719
|
}
|
|
1740
1720
|
}
|
|
1741
1721
|
catch (error) {
|
|
1742
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
1722
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1743
1723
|
getLumicLogger().error(`AI JSON fixing failed: ${errorMessage}`);
|
|
1744
1724
|
// Continue to final error below
|
|
1745
1725
|
}
|
|
1746
1726
|
}
|
|
1747
1727
|
// If all strategies fail, throw an error
|
|
1748
1728
|
getLumicLogger().error(`Failed to parse JSON from content: ${cleanedContent}. Original JSON: ${content}`);
|
|
1749
|
-
throw new Error(
|
|
1729
|
+
throw new Error('Unable to parse JSON response');
|
|
1750
1730
|
}
|
|
1751
1731
|
else {
|
|
1752
1732
|
return content;
|
|
@@ -2023,11 +2003,19 @@ const DEFAULT_RETRY_CONFIG = {
|
|
|
2023
2003
|
* @throws The last error encountered if all retry attempts fail or if a non-retryable error occurs
|
|
2024
2004
|
*/
|
|
2025
2005
|
async function withRetry(fn, config = {}, label = 'unknown') {
|
|
2026
|
-
const { maxRetries, baseDelayMs, maxDelayMs, retryableErrors, circuitBreaker } = {
|
|
2006
|
+
const { maxRetries, baseDelayMs, maxDelayMs, retryableErrors, circuitBreaker, signal } = {
|
|
2027
2007
|
...DEFAULT_RETRY_CONFIG,
|
|
2028
2008
|
...config,
|
|
2029
2009
|
};
|
|
2030
2010
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2011
|
+
// Short-circuit on cancellation BEFORE starting a fresh attempt so
|
|
2012
|
+
// the caller does not pay the cost of issuing a request whose result
|
|
2013
|
+
// they have already discarded.
|
|
2014
|
+
if (signal?.aborted) {
|
|
2015
|
+
throw signal.reason instanceof Error
|
|
2016
|
+
? signal.reason
|
|
2017
|
+
: new Error('Aborted');
|
|
2018
|
+
}
|
|
2031
2019
|
try {
|
|
2032
2020
|
// If a circuit breaker is provided, execute through it
|
|
2033
2021
|
const result = circuitBreaker
|
|
@@ -2040,6 +2028,13 @@ async function withRetry(fn, config = {}, label = 'unknown') {
|
|
|
2040
2028
|
if (error instanceof CircuitBreakerOpenError) {
|
|
2041
2029
|
throw error;
|
|
2042
2030
|
}
|
|
2031
|
+
// Do not retry on cooperative cancellation — the caller has
|
|
2032
|
+
// explicitly asked us to stop.
|
|
2033
|
+
if (signal?.aborted) {
|
|
2034
|
+
throw signal.reason instanceof Error
|
|
2035
|
+
? signal.reason
|
|
2036
|
+
: new Error('Aborted');
|
|
2037
|
+
}
|
|
2043
2038
|
// Don't retry if we've exhausted all attempts
|
|
2044
2039
|
if (attempt === maxRetries) {
|
|
2045
2040
|
throw error;
|
|
@@ -2053,7 +2048,26 @@ async function withRetry(fn, config = {}, label = 'unknown') {
|
|
|
2053
2048
|
const jitter = Math.random() * 1000;
|
|
2054
2049
|
const delay = Math.min(exponentialDelay + jitter, maxDelayMs);
|
|
2055
2050
|
getLumicLogger().warn(`[${label}] Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms`);
|
|
2056
|
-
|
|
2051
|
+
// Sleep but bail early if signal aborts mid-backoff.
|
|
2052
|
+
await new Promise((resolve, reject) => {
|
|
2053
|
+
const timer = setTimeout(() => {
|
|
2054
|
+
if (signal)
|
|
2055
|
+
signal.removeEventListener('abort', onAbort);
|
|
2056
|
+
resolve();
|
|
2057
|
+
}, delay);
|
|
2058
|
+
const onAbort = () => {
|
|
2059
|
+
clearTimeout(timer);
|
|
2060
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error('Aborted'));
|
|
2061
|
+
};
|
|
2062
|
+
if (signal) {
|
|
2063
|
+
if (signal.aborted) {
|
|
2064
|
+
clearTimeout(timer);
|
|
2065
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('Aborted'));
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2057
2071
|
}
|
|
2058
2072
|
}
|
|
2059
2073
|
// This should never be reached, but TypeScript needs it for type safety
|
|
@@ -2340,15 +2354,13 @@ const isRetryableLLMError = (error) => {
|
|
|
2340
2354
|
if (error instanceof Error) {
|
|
2341
2355
|
const message = error.message;
|
|
2342
2356
|
// Retry on rate limits (429)
|
|
2343
|
-
if (message.includes(
|
|
2344
|
-
message.includes("rate limit") ||
|
|
2345
|
-
message.includes("Rate limit")) {
|
|
2357
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
2346
2358
|
return true;
|
|
2347
2359
|
}
|
|
2348
2360
|
// Retry on transient body-corruption 400s. Match the exact OpenAI error
|
|
2349
2361
|
// string to avoid retrying genuine client-side validation 400s (which
|
|
2350
2362
|
// would re-fail forever and waste retry budget).
|
|
2351
|
-
if (message.includes(
|
|
2363
|
+
if (message.includes('could not parse the JSON body of your request')) {
|
|
2352
2364
|
return true;
|
|
2353
2365
|
}
|
|
2354
2366
|
}
|
|
@@ -2417,26 +2429,23 @@ function isReasoningModel(model) {
|
|
|
2417
2429
|
* @throws Error if the response format is invalid or incompatible.
|
|
2418
2430
|
*/
|
|
2419
2431
|
function getResponseFormatOption(responseFormat, normalizedModel) {
|
|
2420
|
-
if (responseFormat ===
|
|
2421
|
-
return { type:
|
|
2432
|
+
if (responseFormat === 'text') {
|
|
2433
|
+
return { type: 'text' };
|
|
2422
2434
|
}
|
|
2423
|
-
if (responseFormat ===
|
|
2424
|
-
return supportsJsonMode(normalizedModel)
|
|
2425
|
-
? { type: "json_object" }
|
|
2426
|
-
: { type: "text" };
|
|
2435
|
+
if (responseFormat === 'json') {
|
|
2436
|
+
return supportsJsonMode(normalizedModel) ? { type: 'json_object' } : { type: 'text' };
|
|
2427
2437
|
}
|
|
2428
|
-
if (typeof responseFormat ===
|
|
2429
|
-
responseFormat.type === "json_schema") {
|
|
2438
|
+
if (typeof responseFormat === 'object' && responseFormat.type === 'json_schema') {
|
|
2430
2439
|
if (!isStructuredOutputCompatibleModel(normalizedModel)) {
|
|
2431
2440
|
throw new Error(`${normalizedModel} is not compatible with structured outputs / json object.`);
|
|
2432
2441
|
}
|
|
2433
2442
|
return {
|
|
2434
|
-
type:
|
|
2443
|
+
type: 'json_schema',
|
|
2435
2444
|
json_schema: {
|
|
2436
|
-
type:
|
|
2445
|
+
type: 'object',
|
|
2437
2446
|
properties: responseFormat.schema.properties,
|
|
2438
2447
|
required: responseFormat.schema.required || [],
|
|
2439
|
-
name:
|
|
2448
|
+
name: 'Response',
|
|
2440
2449
|
},
|
|
2441
2450
|
};
|
|
2442
2451
|
}
|
|
@@ -2495,7 +2504,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2495
2504
|
const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
|
|
2496
2505
|
const apiKey = options.apiKey || getSecrets().openai.apiKey;
|
|
2497
2506
|
if (!apiKey) {
|
|
2498
|
-
throw new Error(
|
|
2507
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
2499
2508
|
}
|
|
2500
2509
|
const openai = new OpenAI({
|
|
2501
2510
|
apiKey: apiKey,
|
|
@@ -2505,7 +2514,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2505
2514
|
// Add developer prompt if present
|
|
2506
2515
|
if (options.developerPrompt && supportsDeveloperPrompt(normalizedModel)) {
|
|
2507
2516
|
messages.push({
|
|
2508
|
-
role:
|
|
2517
|
+
role: 'developer',
|
|
2509
2518
|
content: options.developerPrompt,
|
|
2510
2519
|
});
|
|
2511
2520
|
}
|
|
@@ -2514,15 +2523,15 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2514
2523
|
messages.push(...options.context);
|
|
2515
2524
|
}
|
|
2516
2525
|
// Add user content
|
|
2517
|
-
if (typeof content ===
|
|
2526
|
+
if (typeof content === 'string') {
|
|
2518
2527
|
messages.push({
|
|
2519
|
-
role:
|
|
2528
|
+
role: 'user',
|
|
2520
2529
|
content,
|
|
2521
2530
|
});
|
|
2522
2531
|
}
|
|
2523
2532
|
else {
|
|
2524
2533
|
messages.push({
|
|
2525
|
-
role:
|
|
2534
|
+
role: 'user',
|
|
2526
2535
|
content,
|
|
2527
2536
|
});
|
|
2528
2537
|
}
|
|
@@ -2536,8 +2545,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2536
2545
|
queryOptions.tools = options.tools;
|
|
2537
2546
|
}
|
|
2538
2547
|
// Only include temperature if the model supports it
|
|
2539
|
-
if (options.temperature !== undefined &&
|
|
2540
|
-
supportsTemperature(normalizedModel)) {
|
|
2548
|
+
if (options.temperature !== undefined && supportsTemperature(normalizedModel)) {
|
|
2541
2549
|
queryOptions.temperature = options.temperature;
|
|
2542
2550
|
}
|
|
2543
2551
|
// Only include max_completion_tokens when specified
|
|
@@ -2547,16 +2555,25 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2547
2555
|
// Only set response_format when it's not the default 'text' type
|
|
2548
2556
|
// (sending response_format: {type: 'text'} is redundant and may cause issues)
|
|
2549
2557
|
const responseFormatOption = getResponseFormatOption(responseFormat, normalizedModel);
|
|
2550
|
-
if (responseFormatOption.type !==
|
|
2558
|
+
if (responseFormatOption.type !== 'text') {
|
|
2551
2559
|
queryOptions.response_format = responseFormatOption;
|
|
2552
2560
|
}
|
|
2553
2561
|
let completion;
|
|
2554
2562
|
try {
|
|
2555
|
-
completion = await withRetry(
|
|
2563
|
+
completion = await withRetry(
|
|
2564
|
+
// Pass the AbortSignal into the OpenAI SDK request-options object.
|
|
2565
|
+
// The OpenAI SDK honours `signal` to cancel the in-flight fetch
|
|
2566
|
+
// immediately when it fires — this is what makes caller-driven
|
|
2567
|
+
// cancel-and-restart (e.g. signal-monitoring's materiality
|
|
2568
|
+
// re-trigger path in the engine) actually release the HTTP
|
|
2569
|
+
// connection and response-body buffer rather than just discarding
|
|
2570
|
+
// the resolved value at the engine boundary.
|
|
2571
|
+
() => openai.chat.completions.create(queryOptions, options.signal ? { signal: options.signal } : undefined), {
|
|
2556
2572
|
maxRetries: 3,
|
|
2557
2573
|
baseDelayMs: 2000,
|
|
2558
2574
|
maxDelayMs: 30000,
|
|
2559
2575
|
retryableErrors: isRetryableLLMError,
|
|
2576
|
+
signal: options.signal,
|
|
2560
2577
|
}, `OpenAI:${normalizedModel}`);
|
|
2561
2578
|
}
|
|
2562
2579
|
catch (error) {
|
|
@@ -2568,19 +2585,15 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2568
2585
|
// for production prompts containing sensitive context.
|
|
2569
2586
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2570
2587
|
const totalContentChars = messages.reduce((sum, msg) => {
|
|
2571
|
-
if (typeof msg.content ===
|
|
2588
|
+
if (typeof msg.content === 'string')
|
|
2572
2589
|
return sum + msg.content.length;
|
|
2573
2590
|
if (Array.isArray(msg.content)) {
|
|
2574
|
-
return
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
return s + part.text.length;
|
|
2581
|
-
}
|
|
2582
|
-
return s;
|
|
2583
|
-
}, 0));
|
|
2591
|
+
return sum + msg.content.reduce((s, part) => {
|
|
2592
|
+
if (typeof part === 'object' && part !== null && 'text' in part && typeof part.text === 'string') {
|
|
2593
|
+
return s + part.text.length;
|
|
2594
|
+
}
|
|
2595
|
+
return s;
|
|
2596
|
+
}, 0);
|
|
2584
2597
|
}
|
|
2585
2598
|
return sum;
|
|
2586
2599
|
}, 0);
|
|
@@ -2607,7 +2620,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2607
2620
|
const cachedTokens = completion.usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
2608
2621
|
const response = {
|
|
2609
2622
|
id: completion.id,
|
|
2610
|
-
content: completion.choices[0]?.message?.content ||
|
|
2623
|
+
content: completion.choices[0]?.message?.content || '',
|
|
2611
2624
|
tool_calls: completion.choices[0]?.message?.tool_calls,
|
|
2612
2625
|
usage: {
|
|
2613
2626
|
prompt_tokens: completion.usage?.prompt_tokens ?? 0,
|
|
@@ -2617,7 +2630,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2617
2630
|
},
|
|
2618
2631
|
system_fingerprint: completion.system_fingerprint,
|
|
2619
2632
|
service_tier: options.service_tier,
|
|
2620
|
-
provider:
|
|
2633
|
+
provider: 'openai',
|
|
2621
2634
|
model: normalizedModel,
|
|
2622
2635
|
};
|
|
2623
2636
|
return response;
|
|
@@ -2630,7 +2643,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2630
2643
|
* @param options The options for the LLM call. Defaults to an empty object.
|
|
2631
2644
|
* @return A promise that resolves to the response from the LLM.
|
|
2632
2645
|
*/
|
|
2633
|
-
const makeOpenAIChatCompletionCall = async (content, responseFormat =
|
|
2646
|
+
const makeOpenAIChatCompletionCall = async (content, responseFormat = 'text', options = {}) => {
|
|
2634
2647
|
const mergedOptions = {
|
|
2635
2648
|
...DEFAULT_OPTIONS$1,
|
|
2636
2649
|
...options,
|
|
@@ -2639,7 +2652,7 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2639
2652
|
// Track cost in the global cost tracker. Pass cached tokens through so the
|
|
2640
2653
|
// tracker applies the discounted cached-input rate (typically ~50% of the
|
|
2641
2654
|
// standard input rate) instead of billing every input token at full price.
|
|
2642
|
-
getLLMCostTracker().trackUsage(
|
|
2655
|
+
getLLMCostTracker().trackUsage('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens);
|
|
2643
2656
|
// Handle tool calls differently
|
|
2644
2657
|
if (completion.tool_calls && completion.tool_calls.length > 0) {
|
|
2645
2658
|
const toolCallResponse = {
|
|
@@ -2655,10 +2668,10 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2655
2668
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
2656
2669
|
completion_tokens: completion.usage.completion_tokens,
|
|
2657
2670
|
reasoning_tokens: 0,
|
|
2658
|
-
provider:
|
|
2671
|
+
provider: 'openai',
|
|
2659
2672
|
model: completion.model,
|
|
2660
2673
|
cached_tokens: completion.usage.cached_tokens,
|
|
2661
|
-
cost: calculateCost(
|
|
2674
|
+
cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
2662
2675
|
},
|
|
2663
2676
|
tool_calls: completion.tool_calls,
|
|
2664
2677
|
};
|
|
@@ -2666,7 +2679,7 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2666
2679
|
// Handle regular responses
|
|
2667
2680
|
const parsedResponse = await parseResponse(completion.content, responseFormat);
|
|
2668
2681
|
if (parsedResponse === null) {
|
|
2669
|
-
throw new Error(
|
|
2682
|
+
throw new Error('Failed to parse response');
|
|
2670
2683
|
}
|
|
2671
2684
|
return {
|
|
2672
2685
|
response: parsedResponse,
|
|
@@ -2674,10 +2687,10 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2674
2687
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
2675
2688
|
completion_tokens: completion.usage.completion_tokens,
|
|
2676
2689
|
reasoning_tokens: 0,
|
|
2677
|
-
provider:
|
|
2690
|
+
provider: 'openai',
|
|
2678
2691
|
model: completion.model,
|
|
2679
2692
|
cached_tokens: completion.usage.cached_tokens,
|
|
2680
|
-
cost: calculateCost(
|
|
2693
|
+
cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
2681
2694
|
},
|
|
2682
2695
|
tool_calls: completion.tool_calls,
|
|
2683
2696
|
};
|
|
@@ -2712,44 +2725,45 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2712
2725
|
const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
|
|
2713
2726
|
const apiKey = options.apiKey || getSecrets().openai.apiKey;
|
|
2714
2727
|
if (!apiKey) {
|
|
2715
|
-
throw new Error(
|
|
2728
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
2716
2729
|
}
|
|
2717
2730
|
const openai = new OpenAI({
|
|
2718
2731
|
apiKey: apiKey,
|
|
2719
2732
|
timeout: options.timeout ?? LLM_TIMEOUT_MS,
|
|
2720
2733
|
});
|
|
2721
|
-
// Remove apiKey, model, and
|
|
2722
|
-
const { apiKey: _, model: __, timeout: ___, ...cleanOptions } = options;
|
|
2734
|
+
// Remove apiKey, model, timeout, and signal from options before creating request body
|
|
2735
|
+
const { apiKey: _, model: __, timeout: ___, signal: ____, ...cleanOptions } = options;
|
|
2723
2736
|
const requestBody = {
|
|
2724
2737
|
model: normalizedModel,
|
|
2725
2738
|
input,
|
|
2726
2739
|
...cleanOptions,
|
|
2727
2740
|
};
|
|
2728
2741
|
// Make the API call to the Responses endpoint
|
|
2729
|
-
const response = await withRetry(() => openai.responses.create(requestBody), {
|
|
2742
|
+
const response = await withRetry(() => openai.responses.create(requestBody, options.signal ? { signal: options.signal } : undefined), {
|
|
2730
2743
|
maxRetries: 3,
|
|
2731
2744
|
baseDelayMs: 2000,
|
|
2732
2745
|
maxDelayMs: 30000,
|
|
2733
2746
|
retryableErrors: isRetryableLLMError,
|
|
2747
|
+
signal: options.signal,
|
|
2734
2748
|
}, `OpenAI-Responses:${normalizedModel}`);
|
|
2735
2749
|
// Responses API exposes cached input tokens under `input_tokens_details.cached_tokens`
|
|
2736
2750
|
// (the equivalent of Chat Completions' `prompt_tokens_details.cached_tokens`).
|
|
2737
2751
|
const responsesCachedTokens = response.usage?.input_tokens_details?.cached_tokens || 0;
|
|
2738
2752
|
// Track cost in the global cost tracker
|
|
2739
|
-
getLLMCostTracker().trackUsage(
|
|
2753
|
+
getLLMCostTracker().trackUsage('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens);
|
|
2740
2754
|
// Extract tool calls from the output
|
|
2741
2755
|
const toolCalls = response.output
|
|
2742
|
-
?.filter((item) => item.type ===
|
|
2756
|
+
?.filter((item) => item.type === 'function_call')
|
|
2743
2757
|
.map((toolCall) => ({
|
|
2744
2758
|
id: toolCall.call_id,
|
|
2745
|
-
type:
|
|
2759
|
+
type: 'function',
|
|
2746
2760
|
function: {
|
|
2747
2761
|
name: toolCall.name,
|
|
2748
2762
|
arguments: toolCall.arguments,
|
|
2749
2763
|
},
|
|
2750
2764
|
}));
|
|
2751
2765
|
// Extract code interpreter outputs if present
|
|
2752
|
-
const codeInterpreterCalls = response.output?.filter((item) => item.type ===
|
|
2766
|
+
const codeInterpreterCalls = response.output?.filter((item) => item.type === 'code_interpreter_call');
|
|
2753
2767
|
let codeInterpreterOutputs = undefined;
|
|
2754
2768
|
if (codeInterpreterCalls && codeInterpreterCalls.length > 0) {
|
|
2755
2769
|
// Each code_interpreter_call has an 'outputs' array property
|
|
@@ -2775,34 +2789,32 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2775
2789
|
prompt_tokens: response.usage?.input_tokens || 0,
|
|
2776
2790
|
completion_tokens: response.usage?.output_tokens || 0,
|
|
2777
2791
|
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
2778
|
-
provider:
|
|
2792
|
+
provider: 'openai',
|
|
2779
2793
|
model: normalizedModel,
|
|
2780
2794
|
cached_tokens: responsesCachedTokens,
|
|
2781
|
-
cost: calculateCost(
|
|
2795
|
+
cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens),
|
|
2782
2796
|
},
|
|
2783
2797
|
tool_calls: toolCalls,
|
|
2784
|
-
...(codeInterpreterOutputs
|
|
2785
|
-
? { code_interpreter_outputs: codeInterpreterOutputs }
|
|
2786
|
-
: {}),
|
|
2798
|
+
...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
|
|
2787
2799
|
};
|
|
2788
2800
|
}
|
|
2789
2801
|
// Extract text content from the response output
|
|
2790
2802
|
const textContent = response.output
|
|
2791
|
-
?.filter((item) => item.type ===
|
|
2803
|
+
?.filter((item) => item.type === 'message')
|
|
2792
2804
|
.map((item) => item.content
|
|
2793
|
-
.filter((content) => content.type ===
|
|
2805
|
+
.filter((content) => content.type === 'output_text')
|
|
2794
2806
|
.map((content) => content.text)
|
|
2795
|
-
.join(
|
|
2796
|
-
.join(
|
|
2807
|
+
.join(''))
|
|
2808
|
+
.join('') || '';
|
|
2797
2809
|
// Determine the format for parsing the response
|
|
2798
|
-
let parsingFormat =
|
|
2799
|
-
if (requestBody.text?.format?.type ===
|
|
2800
|
-
parsingFormat =
|
|
2810
|
+
let parsingFormat = 'text';
|
|
2811
|
+
if (requestBody.text?.format?.type === 'json_object') {
|
|
2812
|
+
parsingFormat = 'json';
|
|
2801
2813
|
}
|
|
2802
2814
|
// Handle regular responses
|
|
2803
2815
|
const parsedResponse = await parseResponse(textContent, parsingFormat);
|
|
2804
2816
|
if (parsedResponse === null) {
|
|
2805
|
-
throw new Error(
|
|
2817
|
+
throw new Error('Failed to parse response from Responses API');
|
|
2806
2818
|
}
|
|
2807
2819
|
return {
|
|
2808
2820
|
response: parsedResponse,
|
|
@@ -2810,15 +2822,13 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2810
2822
|
prompt_tokens: response.usage?.input_tokens || 0,
|
|
2811
2823
|
completion_tokens: response.usage?.output_tokens || 0,
|
|
2812
2824
|
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
2813
|
-
provider:
|
|
2825
|
+
provider: 'openai',
|
|
2814
2826
|
model: normalizedModel,
|
|
2815
2827
|
cached_tokens: responsesCachedTokens,
|
|
2816
|
-
cost: calculateCost(
|
|
2828
|
+
cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens),
|
|
2817
2829
|
},
|
|
2818
2830
|
tool_calls: toolCalls,
|
|
2819
|
-
...(codeInterpreterOutputs
|
|
2820
|
-
? { code_interpreter_outputs: codeInterpreterOutputs }
|
|
2821
|
-
: {}),
|
|
2831
|
+
...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
|
|
2822
2832
|
};
|
|
2823
2833
|
};
|
|
2824
2834
|
|
|
@@ -8128,12 +8138,12 @@ function sanitizeObject(obj, sensitiveFields = ['apiKey', 'token', 'secret', 'pa
|
|
|
8128
8138
|
const isRetryableAnthropicError = (error) => {
|
|
8129
8139
|
if (error instanceof Error) {
|
|
8130
8140
|
const message = error.message;
|
|
8131
|
-
if (message.includes(
|
|
8132
|
-
message.includes(
|
|
8133
|
-
message.includes(
|
|
8134
|
-
message.includes(
|
|
8135
|
-
message.includes(
|
|
8136
|
-
message.includes(
|
|
8141
|
+
if (message.includes('429') ||
|
|
8142
|
+
message.includes('500') ||
|
|
8143
|
+
message.includes('503') ||
|
|
8144
|
+
message.includes('529') ||
|
|
8145
|
+
message.includes('rate limit') ||
|
|
8146
|
+
message.includes('overloaded')) {
|
|
8137
8147
|
return true;
|
|
8138
8148
|
}
|
|
8139
8149
|
}
|
|
@@ -8148,11 +8158,8 @@ const isRetryableAnthropicError = (error) => {
|
|
|
8148
8158
|
function translateToolsToAnthropic(tools) {
|
|
8149
8159
|
return tools.map((tool) => ({
|
|
8150
8160
|
name: tool.function.name,
|
|
8151
|
-
description: tool.function.description ||
|
|
8152
|
-
input_schema: (tool.function.parameters || {
|
|
8153
|
-
type: "object",
|
|
8154
|
-
properties: {},
|
|
8155
|
-
}),
|
|
8161
|
+
description: tool.function.description || '',
|
|
8162
|
+
input_schema: (tool.function.parameters || { type: 'object', properties: {} }),
|
|
8156
8163
|
}));
|
|
8157
8164
|
}
|
|
8158
8165
|
/**
|
|
@@ -8165,31 +8172,27 @@ function translateContextToAnthropic(context) {
|
|
|
8165
8172
|
const systemParts = [];
|
|
8166
8173
|
const messages = [];
|
|
8167
8174
|
for (const msg of context) {
|
|
8168
|
-
if (msg.role ===
|
|
8169
|
-
if (typeof msg.content ===
|
|
8175
|
+
if (msg.role === 'system' || msg.role === 'developer') {
|
|
8176
|
+
if (typeof msg.content === 'string') {
|
|
8170
8177
|
systemParts.push(msg.content);
|
|
8171
8178
|
}
|
|
8172
8179
|
continue;
|
|
8173
8180
|
}
|
|
8174
|
-
if (msg.role ===
|
|
8175
|
-
const content = typeof msg.content ===
|
|
8181
|
+
if (msg.role === 'user') {
|
|
8182
|
+
const content = typeof msg.content === 'string'
|
|
8176
8183
|
? msg.content
|
|
8177
8184
|
: JSON.stringify(msg.content);
|
|
8178
|
-
messages.push({ role:
|
|
8185
|
+
messages.push({ role: 'user', content });
|
|
8179
8186
|
continue;
|
|
8180
8187
|
}
|
|
8181
|
-
if (msg.role ===
|
|
8188
|
+
if (msg.role === 'assistant') {
|
|
8182
8189
|
const assistantMsg = msg;
|
|
8183
8190
|
// If the assistant message has tool_calls, translate to Anthropic tool_use blocks
|
|
8184
8191
|
if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
|
|
8185
8192
|
const contentBlocks = [];
|
|
8186
8193
|
// Include text content if present
|
|
8187
|
-
if (typeof assistantMsg.content ===
|
|
8188
|
-
assistantMsg.content
|
|
8189
|
-
contentBlocks.push({
|
|
8190
|
-
type: "text",
|
|
8191
|
-
text: assistantMsg.content,
|
|
8192
|
-
});
|
|
8194
|
+
if (typeof assistantMsg.content === 'string' && assistantMsg.content.trim()) {
|
|
8195
|
+
contentBlocks.push({ type: 'text', text: assistantMsg.content });
|
|
8193
8196
|
}
|
|
8194
8197
|
// Translate each tool_call to a tool_use block
|
|
8195
8198
|
for (const tc of assistantMsg.tool_calls) {
|
|
@@ -8201,36 +8204,32 @@ function translateContextToAnthropic(context) {
|
|
|
8201
8204
|
input = { raw: tc.function.arguments };
|
|
8202
8205
|
}
|
|
8203
8206
|
contentBlocks.push({
|
|
8204
|
-
type:
|
|
8207
|
+
type: 'tool_use',
|
|
8205
8208
|
id: tc.id,
|
|
8206
8209
|
name: tc.function.name,
|
|
8207
8210
|
input,
|
|
8208
8211
|
});
|
|
8209
8212
|
}
|
|
8210
|
-
messages.push({ role:
|
|
8213
|
+
messages.push({ role: 'assistant', content: contentBlocks });
|
|
8211
8214
|
}
|
|
8212
8215
|
else {
|
|
8213
|
-
const content = typeof assistantMsg.content ===
|
|
8216
|
+
const content = typeof assistantMsg.content === 'string'
|
|
8214
8217
|
? assistantMsg.content
|
|
8215
8218
|
: JSON.stringify(assistantMsg.content);
|
|
8216
|
-
messages.push({ role:
|
|
8219
|
+
messages.push({ role: 'assistant', content });
|
|
8217
8220
|
}
|
|
8218
8221
|
continue;
|
|
8219
8222
|
}
|
|
8220
|
-
if (msg.role ===
|
|
8223
|
+
if (msg.role === 'tool') {
|
|
8221
8224
|
// Anthropic expects tool results as user messages with tool_result content blocks
|
|
8222
8225
|
const toolMsg = msg;
|
|
8223
8226
|
messages.push({
|
|
8224
|
-
role:
|
|
8225
|
-
content: [
|
|
8226
|
-
|
|
8227
|
-
type: "tool_result",
|
|
8227
|
+
role: 'user',
|
|
8228
|
+
content: [{
|
|
8229
|
+
type: 'tool_result',
|
|
8228
8230
|
tool_use_id: toolMsg.tool_call_id,
|
|
8229
|
-
content: typeof toolMsg.content ===
|
|
8230
|
-
|
|
8231
|
-
: JSON.stringify(toolMsg.content),
|
|
8232
|
-
},
|
|
8233
|
-
],
|
|
8231
|
+
content: typeof toolMsg.content === 'string' ? toolMsg.content : JSON.stringify(toolMsg.content),
|
|
8232
|
+
}],
|
|
8234
8233
|
});
|
|
8235
8234
|
continue;
|
|
8236
8235
|
}
|
|
@@ -8251,13 +8250,13 @@ function translateContextToAnthropic(context) {
|
|
|
8251
8250
|
merged.push({ role: msg.role, content: toContentBlocks(msg.content) });
|
|
8252
8251
|
}
|
|
8253
8252
|
}
|
|
8254
|
-
return { messages: merged, systemText: systemParts.join(
|
|
8253
|
+
return { messages: merged, systemText: systemParts.join('\n\n') };
|
|
8255
8254
|
}
|
|
8256
8255
|
/** Convert string or content block array to a uniform content block array. */
|
|
8257
8256
|
function toContentBlocks(content) {
|
|
8258
|
-
if (typeof content ===
|
|
8257
|
+
if (typeof content === 'string') {
|
|
8259
8258
|
const textBlock = {
|
|
8260
|
-
type:
|
|
8259
|
+
type: 'text',
|
|
8261
8260
|
text: content,
|
|
8262
8261
|
citations: null,
|
|
8263
8262
|
};
|
|
@@ -8277,11 +8276,11 @@ function toContentBlocks(content) {
|
|
|
8277
8276
|
* @param options The options for the LLM call.
|
|
8278
8277
|
* @return A promise that resolves to the response from the Anthropic API.
|
|
8279
8278
|
*/
|
|
8280
|
-
async function makeAnthropicCall(content, responseFormat =
|
|
8281
|
-
const model = (options.model ||
|
|
8279
|
+
async function makeAnthropicCall(content, responseFormat = 'text', options = {}) {
|
|
8280
|
+
const model = (options.model || 'claude-sonnet-4-6');
|
|
8282
8281
|
const apiKey = options.apiKey || getSecrets().anthropic.apiKey;
|
|
8283
8282
|
if (!apiKey) {
|
|
8284
|
-
throw new Error(
|
|
8283
|
+
throw new Error('Anthropic API key is not provided and ANTHROPIC_API_KEY environment variable is not set');
|
|
8285
8284
|
}
|
|
8286
8285
|
const client = new Anthropic({
|
|
8287
8286
|
apiKey,
|
|
@@ -8293,10 +8292,8 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8293
8292
|
systemParts.push(options.developerPrompt);
|
|
8294
8293
|
}
|
|
8295
8294
|
// If JSON response is requested, instruct the model via system prompt
|
|
8296
|
-
if (responseFormat ===
|
|
8297
|
-
(
|
|
8298
|
-
responseFormat.type === "json_schema")) {
|
|
8299
|
-
systemParts.push("You MUST respond with valid JSON only. No markdown, no code blocks, no explanatory text — just the raw JSON object or array.");
|
|
8295
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) {
|
|
8296
|
+
systemParts.push('You MUST respond with valid JSON only. No markdown, no code blocks, no explanatory text — just the raw JSON object or array.');
|
|
8300
8297
|
}
|
|
8301
8298
|
// Build messages array
|
|
8302
8299
|
const messages = [];
|
|
@@ -8309,7 +8306,7 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8309
8306
|
messages.push(...translated.messages);
|
|
8310
8307
|
}
|
|
8311
8308
|
// Add user content
|
|
8312
|
-
messages.push({ role:
|
|
8309
|
+
messages.push({ role: 'user', content });
|
|
8313
8310
|
// Build request params
|
|
8314
8311
|
const requestParams = {
|
|
8315
8312
|
model,
|
|
@@ -8318,37 +8315,46 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8318
8315
|
};
|
|
8319
8316
|
// Add system prompt if present
|
|
8320
8317
|
if (systemParts.length > 0) {
|
|
8321
|
-
requestParams.system = systemParts.join(
|
|
8322
|
-
}
|
|
8323
|
-
//
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8318
|
+
requestParams.system = systemParts.join('\n\n');
|
|
8319
|
+
}
|
|
8320
|
+
// Sampling controls are intentionally disabled for the Anthropic provider.
|
|
8321
|
+
//
|
|
8322
|
+
// The claude-*-4.x model family rejects any request that specifies BOTH
|
|
8323
|
+
// `temperature` and `top_p` ("`temperature` and `top_p` cannot both be
|
|
8324
|
+
// specified for this model. Please use only one." — HTTP 400), which aborts
|
|
8325
|
+
// the entire caller flow (e.g. the engine's account decision session).
|
|
8326
|
+
// Callers across the org pass both knobs generically, so rather than make
|
|
8327
|
+
// each one provider-aware we drop both here and let Anthropic use its model
|
|
8328
|
+
// defaults. `options.temperature` / `options.top_p` are intentionally not
|
|
8329
|
+
// forwarded. This is the Anthropic adapter only — the OpenAI, DeepSeek, and
|
|
8330
|
+
// OpenAI-compatible adapters still honor `temperature` and `top_p`.
|
|
8330
8331
|
// Translate and add tools if present
|
|
8331
8332
|
if (options.tools && options.tools.length > 0) {
|
|
8332
8333
|
requestParams.tools = translateToolsToAnthropic(options.tools);
|
|
8333
8334
|
}
|
|
8334
8335
|
try {
|
|
8335
|
-
const response = await withRetry(
|
|
8336
|
+
const response = await withRetry(
|
|
8337
|
+
// Pass AbortSignal into the Anthropic SDK request-options object
|
|
8338
|
+
// so caller-driven cancellation interrupts the in-flight HTTP
|
|
8339
|
+
// request immediately. See LLMOptions.signal JSDoc for context.
|
|
8340
|
+
() => client.messages.create(requestParams, options.signal ? { signal: options.signal } : undefined), {
|
|
8336
8341
|
maxRetries: 3,
|
|
8337
8342
|
baseDelayMs: 2000,
|
|
8338
8343
|
maxDelayMs: 30000,
|
|
8339
8344
|
retryableErrors: isRetryableAnthropicError,
|
|
8345
|
+
signal: options.signal,
|
|
8340
8346
|
}, `Anthropic:${model}`);
|
|
8341
8347
|
const inputTokens = response.usage.input_tokens;
|
|
8342
8348
|
const outputTokens = response.usage.output_tokens;
|
|
8343
8349
|
// Track cost
|
|
8344
|
-
getLLMCostTracker().trackUsage(
|
|
8350
|
+
getLLMCostTracker().trackUsage('anthropic', model, inputTokens, outputTokens);
|
|
8345
8351
|
// Extract tool use blocks
|
|
8346
|
-
const toolUseBlocks = response.content.filter((block) => block.type ===
|
|
8352
|
+
const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
|
|
8347
8353
|
if (toolUseBlocks.length > 0) {
|
|
8348
8354
|
// Translate Anthropic tool_use to OpenAI tool_calls format
|
|
8349
8355
|
const toolCalls = toolUseBlocks.map((block) => ({
|
|
8350
8356
|
id: block.id,
|
|
8351
|
-
type:
|
|
8357
|
+
type: 'function',
|
|
8352
8358
|
function: {
|
|
8353
8359
|
name: block.name,
|
|
8354
8360
|
arguments: JSON.stringify(block.input),
|
|
@@ -8367,22 +8373,22 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8367
8373
|
prompt_tokens: inputTokens,
|
|
8368
8374
|
completion_tokens: outputTokens,
|
|
8369
8375
|
reasoning_tokens: 0,
|
|
8370
|
-
provider:
|
|
8376
|
+
provider: 'anthropic',
|
|
8371
8377
|
model,
|
|
8372
|
-
cost: calculateCost(
|
|
8378
|
+
cost: calculateCost('anthropic', model, inputTokens, outputTokens),
|
|
8373
8379
|
},
|
|
8374
8380
|
tool_calls: toolCalls,
|
|
8375
8381
|
};
|
|
8376
8382
|
}
|
|
8377
8383
|
// Extract text content
|
|
8378
8384
|
const textContent = response.content
|
|
8379
|
-
.filter((block) => block.type ===
|
|
8385
|
+
.filter((block) => block.type === 'text')
|
|
8380
8386
|
.map((block) => block.text)
|
|
8381
|
-
.join(
|
|
8387
|
+
.join('');
|
|
8382
8388
|
// Parse response
|
|
8383
8389
|
const parsedResponse = await parseResponse(textContent, responseFormat);
|
|
8384
8390
|
if (parsedResponse === null) {
|
|
8385
|
-
throw new Error(
|
|
8391
|
+
throw new Error('Failed to parse Anthropic response');
|
|
8386
8392
|
}
|
|
8387
8393
|
return {
|
|
8388
8394
|
response: parsedResponse,
|
|
@@ -8390,9 +8396,9 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8390
8396
|
prompt_tokens: inputTokens,
|
|
8391
8397
|
completion_tokens: outputTokens,
|
|
8392
8398
|
reasoning_tokens: 0,
|
|
8393
|
-
provider:
|
|
8399
|
+
provider: 'anthropic',
|
|
8394
8400
|
model,
|
|
8395
|
-
cost: calculateCost(
|
|
8401
|
+
cost: calculateCost('anthropic', model, inputTokens, outputTokens),
|
|
8396
8402
|
},
|
|
8397
8403
|
};
|
|
8398
8404
|
}
|
|
@@ -8414,9 +8420,7 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8414
8420
|
const isRetryableError = (error) => {
|
|
8415
8421
|
if (error instanceof Error) {
|
|
8416
8422
|
const message = error.message;
|
|
8417
|
-
if (message.includes(
|
|
8418
|
-
message.includes("rate limit") ||
|
|
8419
|
-
message.includes("Rate limit")) {
|
|
8423
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
8420
8424
|
return true;
|
|
8421
8425
|
}
|
|
8422
8426
|
}
|
|
@@ -8430,16 +8434,14 @@ function resolveApiKey(provider, optionsApiKey) {
|
|
|
8430
8434
|
if (optionsApiKey)
|
|
8431
8435
|
return optionsApiKey;
|
|
8432
8436
|
const secrets = getSecrets();
|
|
8433
|
-
const pathParts = provider.apiKeySecretPath.split(
|
|
8437
|
+
const pathParts = provider.apiKeySecretPath.split('.');
|
|
8434
8438
|
let current = secrets;
|
|
8435
8439
|
for (const part of pathParts) {
|
|
8436
|
-
if (current === null ||
|
|
8437
|
-
|
|
8438
|
-
typeof current !== "object")
|
|
8439
|
-
return "";
|
|
8440
|
+
if (current === null || current === undefined || typeof current !== 'object')
|
|
8441
|
+
return '';
|
|
8440
8442
|
current = current[part];
|
|
8441
8443
|
}
|
|
8442
|
-
return (typeof current ===
|
|
8444
|
+
return (typeof current === 'string' ? current : '') || '';
|
|
8443
8445
|
}
|
|
8444
8446
|
/**
|
|
8445
8447
|
* Makes a call to an OpenAI-compatible provider (Kimi, Qwen, or any future provider).
|
|
@@ -8453,12 +8455,12 @@ function resolveApiKey(provider, optionsApiKey) {
|
|
|
8453
8455
|
* @param providerName The provider key in OPENAI_COMPATIBLE_PROVIDERS.
|
|
8454
8456
|
* @return A promise that resolves to the response from the provider.
|
|
8455
8457
|
*/
|
|
8456
|
-
async function makeOpenAICompatibleCall(content, responseFormat =
|
|
8458
|
+
async function makeOpenAICompatibleCall(content, responseFormat = 'text', options = {}, providerName) {
|
|
8457
8459
|
const providerConfig = OPENAI_COMPATIBLE_PROVIDERS[providerName];
|
|
8458
8460
|
if (!providerConfig) {
|
|
8459
8461
|
throw new Error(`Unknown OpenAI-compatible provider: ${providerName}`);
|
|
8460
8462
|
}
|
|
8461
|
-
const model = normalizeModelName(options.model ||
|
|
8463
|
+
const model = normalizeModelName(options.model || '');
|
|
8462
8464
|
const apiKey = resolveApiKey(providerConfig, options.apiKey);
|
|
8463
8465
|
if (!apiKey) {
|
|
8464
8466
|
throw new Error(`${providerConfig.name} API key is not provided and ${providerConfig.apiKeyEnvVar} environment variable is not set`);
|
|
@@ -8471,36 +8473,31 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8471
8473
|
const messages = [];
|
|
8472
8474
|
// Add system message with developer prompt if present
|
|
8473
8475
|
if (options.developerPrompt) {
|
|
8474
|
-
messages.push({ role:
|
|
8476
|
+
messages.push({ role: 'system', content: options.developerPrompt });
|
|
8475
8477
|
}
|
|
8476
8478
|
// Add context if present
|
|
8477
8479
|
if (options.context) {
|
|
8478
8480
|
messages.push(...options.context);
|
|
8479
8481
|
}
|
|
8480
8482
|
// Add user content
|
|
8481
|
-
if (typeof content ===
|
|
8482
|
-
messages.push({ role:
|
|
8483
|
+
if (typeof content === 'string') {
|
|
8484
|
+
messages.push({ role: 'user', content });
|
|
8483
8485
|
}
|
|
8484
8486
|
else {
|
|
8485
|
-
messages.push({ role:
|
|
8487
|
+
messages.push({ role: 'user', content });
|
|
8486
8488
|
}
|
|
8487
8489
|
// Determine if the model supports JSON mode
|
|
8488
8490
|
const supportsJson = isValidModel(model) && getModelCapabilities(model).supportsJson;
|
|
8489
8491
|
// If JSON response format, add hint to system prompt
|
|
8490
|
-
if ((responseFormat ===
|
|
8491
|
-
(typeof responseFormat === "object" &&
|
|
8492
|
-
responseFormat.type === "json_schema")) &&
|
|
8492
|
+
if ((responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) &&
|
|
8493
8493
|
supportsJson) {
|
|
8494
|
-
if (!messages.some((m) => m.role ===
|
|
8495
|
-
messages.unshift({
|
|
8496
|
-
role: "system",
|
|
8497
|
-
content: "Please respond in valid JSON format.",
|
|
8498
|
-
});
|
|
8494
|
+
if (!messages.some((m) => m.role === 'system')) {
|
|
8495
|
+
messages.unshift({ role: 'system', content: 'Please respond in valid JSON format.' });
|
|
8499
8496
|
}
|
|
8500
8497
|
else {
|
|
8501
|
-
const systemMsgIndex = messages.findIndex((m) => m.role ===
|
|
8498
|
+
const systemMsgIndex = messages.findIndex((m) => m.role === 'system');
|
|
8502
8499
|
const systemMsg = messages[systemMsgIndex];
|
|
8503
|
-
if (typeof systemMsg.content ===
|
|
8500
|
+
if (typeof systemMsg.content === 'string') {
|
|
8504
8501
|
messages[systemMsgIndex] = {
|
|
8505
8502
|
...systemMsg,
|
|
8506
8503
|
content: `${systemMsg.content} Please respond in valid JSON format.`,
|
|
@@ -8513,8 +8510,8 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8513
8510
|
messages,
|
|
8514
8511
|
};
|
|
8515
8512
|
// Add response format if JSON is supported
|
|
8516
|
-
if (responseFormat !==
|
|
8517
|
-
queryOptions.response_format = { type:
|
|
8513
|
+
if (responseFormat !== 'text' && supportsJson) {
|
|
8514
|
+
queryOptions.response_format = { type: 'json_object' };
|
|
8518
8515
|
}
|
|
8519
8516
|
// Temperature
|
|
8520
8517
|
if (options.temperature !== undefined) {
|
|
@@ -8538,11 +8535,16 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8538
8535
|
queryOptions.tools = options.tools;
|
|
8539
8536
|
}
|
|
8540
8537
|
try {
|
|
8541
|
-
const completion = await withRetry(
|
|
8538
|
+
const completion = await withRetry(
|
|
8539
|
+
// Pass AbortSignal into the SDK request-options object so
|
|
8540
|
+
// caller-driven cancellation interrupts the in-flight HTTP
|
|
8541
|
+
// request immediately. See LLMOptions.signal JSDoc for context.
|
|
8542
|
+
() => openai.chat.completions.create(queryOptions, options.signal ? { signal: options.signal } : undefined), {
|
|
8542
8543
|
maxRetries: 3,
|
|
8543
8544
|
baseDelayMs: 2000,
|
|
8544
8545
|
maxDelayMs: 30000,
|
|
8545
8546
|
retryableErrors: isRetryableError,
|
|
8547
|
+
signal: options.signal,
|
|
8546
8548
|
}, `${providerConfig.name}:${model}`);
|
|
8547
8549
|
const promptTokens = completion.usage?.prompt_tokens || 0;
|
|
8548
8550
|
const completionTokens = completion.usage?.completion_tokens || 0;
|
|
@@ -8572,7 +8574,7 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8572
8574
|
};
|
|
8573
8575
|
}
|
|
8574
8576
|
// Handle regular responses
|
|
8575
|
-
const textContent = completion.choices[0]?.message?.content ||
|
|
8577
|
+
const textContent = completion.choices[0]?.message?.content || '';
|
|
8576
8578
|
const parsedResponse = await parseResponse(textContent, responseFormat);
|
|
8577
8579
|
if (parsedResponse === null) {
|
|
8578
8580
|
throw new Error(`Failed to parse ${providerConfig.name} response`);
|
|
@@ -8864,9 +8866,7 @@ const isRetryableDeepseekError = (error) => {
|
|
|
8864
8866
|
if (error instanceof Error) {
|
|
8865
8867
|
const message = error.message;
|
|
8866
8868
|
// Retry only on rate limits (429)
|
|
8867
|
-
if (message.includes(
|
|
8868
|
-
message.includes("rate limit") ||
|
|
8869
|
-
message.includes("Rate limit")) {
|
|
8869
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
8870
8870
|
return true;
|
|
8871
8871
|
}
|
|
8872
8872
|
}
|
|
@@ -8876,7 +8876,7 @@ const isRetryableDeepseekError = (error) => {
|
|
|
8876
8876
|
* Default options for Deepseek API calls
|
|
8877
8877
|
*/
|
|
8878
8878
|
const DEFAULT_DEEPSEEK_OPTIONS = {
|
|
8879
|
-
defaultModel:
|
|
8879
|
+
defaultModel: 'deepseek-chat',
|
|
8880
8880
|
};
|
|
8881
8881
|
/**
|
|
8882
8882
|
* Checks if the given model is a supported Deepseek model
|
|
@@ -8888,7 +8888,7 @@ const isSupportedDeepseekModel = (model) => {
|
|
|
8888
8888
|
return false;
|
|
8889
8889
|
}
|
|
8890
8890
|
const capabilities = getModelCapabilities(model);
|
|
8891
|
-
return capabilities.provider ===
|
|
8891
|
+
return capabilities.provider === 'deepseek';
|
|
8892
8892
|
};
|
|
8893
8893
|
/**
|
|
8894
8894
|
* Checks if the given Deepseek model supports JSON output format
|
|
@@ -8921,19 +8921,17 @@ const supportsToolCalling = (model) => {
|
|
|
8921
8921
|
*/
|
|
8922
8922
|
function getDeepseekResponseFormatOption(responseFormat, model) {
|
|
8923
8923
|
// Check if the model supports JSON output
|
|
8924
|
-
if (responseFormat !==
|
|
8924
|
+
if (responseFormat !== 'text' && !supportsJsonOutput(model)) {
|
|
8925
8925
|
getLumicLogger().warn(`Model ${model} does not support JSON output. Using text format instead.`);
|
|
8926
|
-
return { type:
|
|
8926
|
+
return { type: 'text' };
|
|
8927
8927
|
}
|
|
8928
|
-
if (responseFormat ===
|
|
8929
|
-
return { type:
|
|
8928
|
+
if (responseFormat === 'text') {
|
|
8929
|
+
return { type: 'text' };
|
|
8930
8930
|
}
|
|
8931
|
-
if (responseFormat ===
|
|
8932
|
-
|
|
8933
|
-
responseFormat.type === "json_schema")) {
|
|
8934
|
-
return { type: "json_object" };
|
|
8931
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) {
|
|
8932
|
+
return { type: 'json_object' };
|
|
8935
8933
|
}
|
|
8936
|
-
return { type:
|
|
8934
|
+
return { type: 'text' };
|
|
8937
8935
|
}
|
|
8938
8936
|
/**
|
|
8939
8937
|
* Creates a completion using the Deepseek API
|
|
@@ -8952,26 +8950,24 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
8952
8950
|
throw new Error(`Unsupported Deepseek model: ${normalizedModel}. Please use 'deepseek-chat' or 'deepseek-reasoner'.`);
|
|
8953
8951
|
}
|
|
8954
8952
|
// Check if tools are requested with a model that doesn't support them
|
|
8955
|
-
if (options.tools &&
|
|
8956
|
-
options.tools.length > 0 &&
|
|
8957
|
-
!supportsToolCalling(normalizedModel)) {
|
|
8953
|
+
if (options.tools && options.tools.length > 0 && !supportsToolCalling(normalizedModel)) {
|
|
8958
8954
|
throw new Error(`Model ${normalizedModel} does not support tool calling.`);
|
|
8959
8955
|
}
|
|
8960
8956
|
const apiKey = options.apiKey || getSecrets().deepseek.apiKey;
|
|
8961
8957
|
if (!apiKey) {
|
|
8962
|
-
throw new Error(
|
|
8958
|
+
throw new Error('Deepseek API key is not provided and DEEPSEEK_API_KEY environment variable is not set');
|
|
8963
8959
|
}
|
|
8964
8960
|
// Initialize OpenAI client with Deepseek API URL
|
|
8965
8961
|
const openai = new OpenAI({
|
|
8966
8962
|
apiKey,
|
|
8967
|
-
baseURL:
|
|
8963
|
+
baseURL: 'https://api.deepseek.com',
|
|
8968
8964
|
timeout: options.timeout ?? LLM_TIMEOUT_MS,
|
|
8969
8965
|
});
|
|
8970
8966
|
const messages = [];
|
|
8971
8967
|
// Add system message with developer prompt if present
|
|
8972
8968
|
if (options.developerPrompt) {
|
|
8973
8969
|
messages.push({
|
|
8974
|
-
role:
|
|
8970
|
+
role: 'system',
|
|
8975
8971
|
content: options.developerPrompt,
|
|
8976
8972
|
});
|
|
8977
8973
|
}
|
|
@@ -8980,35 +8976,33 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
8980
8976
|
messages.push(...options.context);
|
|
8981
8977
|
}
|
|
8982
8978
|
// Add user content
|
|
8983
|
-
if (typeof content ===
|
|
8979
|
+
if (typeof content === 'string') {
|
|
8984
8980
|
messages.push({
|
|
8985
|
-
role:
|
|
8981
|
+
role: 'user',
|
|
8986
8982
|
content,
|
|
8987
8983
|
});
|
|
8988
8984
|
}
|
|
8989
8985
|
else {
|
|
8990
8986
|
messages.push({
|
|
8991
|
-
role:
|
|
8987
|
+
role: 'user',
|
|
8992
8988
|
content,
|
|
8993
8989
|
});
|
|
8994
8990
|
}
|
|
8995
8991
|
// If JSON response format, include a hint in the system prompt
|
|
8996
|
-
if ((responseFormat ===
|
|
8997
|
-
(
|
|
8998
|
-
responseFormat.type === "json_schema")) &&
|
|
8999
|
-
supportsJsonOutput(normalizedModel)) {
|
|
8992
|
+
if ((responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema'))
|
|
8993
|
+
&& supportsJsonOutput(normalizedModel)) {
|
|
9000
8994
|
// If there's no system message yet, add one
|
|
9001
|
-
if (!messages.some(
|
|
8995
|
+
if (!messages.some(m => m.role === 'system')) {
|
|
9002
8996
|
messages.unshift({
|
|
9003
|
-
role:
|
|
9004
|
-
content:
|
|
8997
|
+
role: 'system',
|
|
8998
|
+
content: 'Please respond in valid JSON format.',
|
|
9005
8999
|
});
|
|
9006
9000
|
}
|
|
9007
9001
|
else {
|
|
9008
9002
|
// Append to existing system message
|
|
9009
|
-
const systemMsgIndex = messages.findIndex(
|
|
9003
|
+
const systemMsgIndex = messages.findIndex(m => m.role === 'system');
|
|
9010
9004
|
const systemMsg = messages[systemMsgIndex];
|
|
9011
|
-
if (typeof systemMsg.content ===
|
|
9005
|
+
if (typeof systemMsg.content === 'string') {
|
|
9012
9006
|
messages[systemMsgIndex] = {
|
|
9013
9007
|
...systemMsg,
|
|
9014
9008
|
content: `${systemMsg.content} Please respond in valid JSON format.`,
|
|
@@ -9049,7 +9043,7 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9049
9043
|
0;
|
|
9050
9044
|
return {
|
|
9051
9045
|
id: completion.id,
|
|
9052
|
-
content: completion.choices[0]?.message?.content ||
|
|
9046
|
+
content: completion.choices[0]?.message?.content || '',
|
|
9053
9047
|
tool_calls: completion.choices[0]?.message?.tool_calls,
|
|
9054
9048
|
usage: {
|
|
9055
9049
|
prompt_tokens: completion.usage?.prompt_tokens ?? 0,
|
|
@@ -9058,7 +9052,7 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9058
9052
|
cached_tokens: cachedTokens,
|
|
9059
9053
|
},
|
|
9060
9054
|
system_fingerprint: completion.system_fingerprint,
|
|
9061
|
-
provider:
|
|
9055
|
+
provider: 'deepseek',
|
|
9062
9056
|
model: normalizedModel,
|
|
9063
9057
|
};
|
|
9064
9058
|
}
|
|
@@ -9075,7 +9069,7 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9075
9069
|
* @param options Configuration options including model ('deepseek-chat' or 'deepseek-reasoner'), tools, and apiKey.
|
|
9076
9070
|
* @return A promise that resolves to the response from the Deepseek API.
|
|
9077
9071
|
*/
|
|
9078
|
-
const makeDeepseekCall = async (content, responseFormat =
|
|
9072
|
+
const makeDeepseekCall = async (content, responseFormat = 'json', options = {}) => {
|
|
9079
9073
|
// Set default model if not provided
|
|
9080
9074
|
const mergedOptions = {
|
|
9081
9075
|
...options,
|
|
@@ -9085,17 +9079,17 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9085
9079
|
}
|
|
9086
9080
|
const modelName = normalizeModelName(mergedOptions.model);
|
|
9087
9081
|
// Check if the requested response format is compatible with the model
|
|
9088
|
-
if (responseFormat !==
|
|
9082
|
+
if (responseFormat !== 'text' && !supportsJsonOutput(modelName)) {
|
|
9089
9083
|
getLumicLogger().warn(`Model ${modelName} does not support JSON output. Will return error in the response.`);
|
|
9090
9084
|
return {
|
|
9091
9085
|
response: {
|
|
9092
|
-
error: `Model ${modelName} does not support JSON output format
|
|
9086
|
+
error: `Model ${modelName} does not support JSON output format.`
|
|
9093
9087
|
},
|
|
9094
9088
|
usage: {
|
|
9095
9089
|
prompt_tokens: 0,
|
|
9096
9090
|
completion_tokens: 0,
|
|
9097
9091
|
reasoning_tokens: 0,
|
|
9098
|
-
provider:
|
|
9092
|
+
provider: 'deepseek',
|
|
9099
9093
|
model: modelName,
|
|
9100
9094
|
cached_tokens: 0,
|
|
9101
9095
|
cost: 0,
|
|
@@ -9104,19 +9098,17 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9104
9098
|
};
|
|
9105
9099
|
}
|
|
9106
9100
|
// Check if tools are requested with a model that doesn't support them
|
|
9107
|
-
if (mergedOptions.tools &&
|
|
9108
|
-
mergedOptions.tools.length > 0 &&
|
|
9109
|
-
!supportsToolCalling(modelName)) {
|
|
9101
|
+
if (mergedOptions.tools && mergedOptions.tools.length > 0 && !supportsToolCalling(modelName)) {
|
|
9110
9102
|
getLumicLogger().warn(`Model ${modelName} does not support tool calling. Will return error in the response.`);
|
|
9111
9103
|
return {
|
|
9112
9104
|
response: {
|
|
9113
|
-
error: `Model ${modelName} does not support tool calling
|
|
9105
|
+
error: `Model ${modelName} does not support tool calling.`
|
|
9114
9106
|
},
|
|
9115
9107
|
usage: {
|
|
9116
9108
|
prompt_tokens: 0,
|
|
9117
9109
|
completion_tokens: 0,
|
|
9118
9110
|
reasoning_tokens: 0,
|
|
9119
|
-
provider:
|
|
9111
|
+
provider: 'deepseek',
|
|
9120
9112
|
model: modelName,
|
|
9121
9113
|
cached_tokens: 0,
|
|
9122
9114
|
cost: 0,
|
|
@@ -9128,7 +9120,7 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9128
9120
|
const completion = await createDeepseekCompletion(content, responseFormat, mergedOptions);
|
|
9129
9121
|
// Track cost in the global cost tracker. Pass cached tokens through so the
|
|
9130
9122
|
// discounted cached-input pricing tier is applied.
|
|
9131
|
-
getLLMCostTracker().trackUsage(
|
|
9123
|
+
getLLMCostTracker().trackUsage('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens);
|
|
9132
9124
|
// Handle tool calls similarly to OpenAI
|
|
9133
9125
|
if (completion.tool_calls && completion.tool_calls.length > 0) {
|
|
9134
9126
|
const toolCallResponse = {
|
|
@@ -9144,10 +9136,10 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9144
9136
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
9145
9137
|
completion_tokens: completion.usage.completion_tokens,
|
|
9146
9138
|
reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
|
|
9147
|
-
provider:
|
|
9139
|
+
provider: 'deepseek',
|
|
9148
9140
|
model: completion.model,
|
|
9149
9141
|
cached_tokens: completion.usage.cached_tokens,
|
|
9150
|
-
cost: calculateCost(
|
|
9142
|
+
cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
9151
9143
|
},
|
|
9152
9144
|
tool_calls: completion.tool_calls,
|
|
9153
9145
|
};
|
|
@@ -9155,7 +9147,7 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9155
9147
|
// Handle regular responses
|
|
9156
9148
|
const parsedResponse = await parseResponse(completion.content, responseFormat);
|
|
9157
9149
|
if (parsedResponse === null) {
|
|
9158
|
-
throw new Error(
|
|
9150
|
+
throw new Error('Failed to parse Deepseek response');
|
|
9159
9151
|
}
|
|
9160
9152
|
return {
|
|
9161
9153
|
response: parsedResponse,
|
|
@@ -9163,10 +9155,10 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9163
9155
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
9164
9156
|
completion_tokens: completion.usage.completion_tokens,
|
|
9165
9157
|
reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
|
|
9166
|
-
provider:
|
|
9158
|
+
provider: 'deepseek',
|
|
9167
9159
|
model: completion.model,
|
|
9168
9160
|
cached_tokens: completion.usage.cached_tokens,
|
|
9169
|
-
cost: calculateCost(
|
|
9161
|
+
cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
9170
9162
|
},
|
|
9171
9163
|
tool_calls: completion.tool_calls,
|
|
9172
9164
|
};
|
|
@@ -9176,13 +9168,13 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9176
9168
|
getLumicLogger().error(`Error in Deepseek API call: ${sanitizeError(error)}`);
|
|
9177
9169
|
return {
|
|
9178
9170
|
response: {
|
|
9179
|
-
error: sanitizeError(error)
|
|
9171
|
+
error: sanitizeError(error)
|
|
9180
9172
|
},
|
|
9181
9173
|
usage: {
|
|
9182
9174
|
prompt_tokens: 0,
|
|
9183
9175
|
completion_tokens: 0,
|
|
9184
9176
|
reasoning_tokens: 0,
|
|
9185
|
-
provider:
|
|
9177
|
+
provider: 'deepseek',
|
|
9186
9178
|
model: modelName,
|
|
9187
9179
|
cached_tokens: 0,
|
|
9188
9180
|
cost: 0,
|
|
@@ -9291,15 +9283,15 @@ const generateCacheKey = (auth, envVarsCheck = null) => {
|
|
|
9291
9283
|
if (auth) {
|
|
9292
9284
|
return `${auth.AWS_ACCESS_KEY_ID}:${auth.AWS_SECRET_ACCESS_KEY}:${auth.AWS_REGION}`;
|
|
9293
9285
|
}
|
|
9294
|
-
if (envVarsCheck?.source ===
|
|
9286
|
+
if (envVarsCheck?.source === 'lambda') {
|
|
9295
9287
|
// In Lambda, use function name as cache key since it uses IAM role
|
|
9296
9288
|
const secrets = getSecrets();
|
|
9297
|
-
return `lambda:${secrets.aws.lambdaFunctionName ||
|
|
9289
|
+
return `lambda:${secrets.aws.lambdaFunctionName || 'default'}`;
|
|
9298
9290
|
}
|
|
9299
9291
|
if (envVarsCheck?.vars) {
|
|
9300
9292
|
return `${envVarsCheck.vars.accessKeyId}:${envVarsCheck.vars.secretAccessKey}:${envVarsCheck.vars.region}`;
|
|
9301
9293
|
}
|
|
9302
|
-
return
|
|
9294
|
+
return 'default';
|
|
9303
9295
|
};
|
|
9304
9296
|
const validateEnvironmentVars = () => {
|
|
9305
9297
|
const secrets = getSecrets();
|
|
@@ -9312,37 +9304,37 @@ const validateEnvironmentVars = () => {
|
|
|
9312
9304
|
// We're in a Lambda environment
|
|
9313
9305
|
return {
|
|
9314
9306
|
isValid: true,
|
|
9315
|
-
source:
|
|
9316
|
-
vars: { accessKeyId:
|
|
9307
|
+
source: 'lambda',
|
|
9308
|
+
vars: { accessKeyId: '', secretAccessKey: '', region: '' } // No need for explicit credentials in Lambda
|
|
9317
9309
|
};
|
|
9318
9310
|
}
|
|
9319
9311
|
else if (areVarsComplete(secrets.aws.myAccessKeyId, secrets.aws.mySecretAccessKey, secrets.aws.myRegion)) {
|
|
9320
9312
|
return {
|
|
9321
9313
|
isValid: true,
|
|
9322
|
-
source:
|
|
9314
|
+
source: 'my_prefix',
|
|
9323
9315
|
vars: {
|
|
9324
|
-
accessKeyId: secrets.aws.myAccessKeyId
|
|
9325
|
-
secretAccessKey: secrets.aws.mySecretAccessKey
|
|
9326
|
-
region: secrets.aws.myRegion
|
|
9327
|
-
}
|
|
9316
|
+
accessKeyId: secrets.aws.myAccessKeyId,
|
|
9317
|
+
secretAccessKey: secrets.aws.mySecretAccessKey,
|
|
9318
|
+
region: secrets.aws.myRegion
|
|
9319
|
+
}
|
|
9328
9320
|
};
|
|
9329
9321
|
}
|
|
9330
9322
|
else if (areVarsComplete(secrets.aws.accessKeyId, secrets.aws.secretAccessKey, secrets.aws.region)) {
|
|
9331
9323
|
return {
|
|
9332
9324
|
isValid: true,
|
|
9333
|
-
source:
|
|
9325
|
+
source: 'standard',
|
|
9334
9326
|
vars: {
|
|
9335
|
-
accessKeyId: secrets.aws.accessKeyId
|
|
9336
|
-
secretAccessKey: secrets.aws.secretAccessKey
|
|
9337
|
-
region: secrets.aws.region
|
|
9338
|
-
}
|
|
9327
|
+
accessKeyId: secrets.aws.accessKeyId,
|
|
9328
|
+
secretAccessKey: secrets.aws.secretAccessKey,
|
|
9329
|
+
region: secrets.aws.region
|
|
9330
|
+
}
|
|
9339
9331
|
};
|
|
9340
9332
|
}
|
|
9341
9333
|
// If we get here, no complete set of variables was found
|
|
9342
9334
|
return {
|
|
9343
9335
|
isValid: false,
|
|
9344
9336
|
source: null,
|
|
9345
|
-
missingVars: []
|
|
9337
|
+
missingVars: []
|
|
9346
9338
|
};
|
|
9347
9339
|
};
|
|
9348
9340
|
const initialiseAWSClient = (ClientClass, auth = null) => {
|
|
@@ -9351,52 +9343,48 @@ const initialiseAWSClient = (ClientClass, auth = null) => {
|
|
|
9351
9343
|
const requestTimeout = ClientClass === clientS3.S3Client ? AWS_S3_TIMEOUT_MS : AWS_LAMBDA_TIMEOUT_MS;
|
|
9352
9344
|
// Case 1: Explicit auth provided
|
|
9353
9345
|
if (auth) {
|
|
9354
|
-
if (typeof auth !==
|
|
9355
|
-
throw new Error(
|
|
9346
|
+
if (typeof auth !== 'object' || auth === null) {
|
|
9347
|
+
throw new Error('Auth parameter must be an object');
|
|
9356
9348
|
}
|
|
9357
|
-
const requiredAuthFields = [
|
|
9358
|
-
|
|
9359
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
9360
|
-
"AWS_REGION",
|
|
9361
|
-
];
|
|
9362
|
-
const missingFields = requiredAuthFields.filter((field) => !auth[field]);
|
|
9349
|
+
const requiredAuthFields = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'];
|
|
9350
|
+
const missingFields = requiredAuthFields.filter(field => !auth[field]);
|
|
9363
9351
|
if (missingFields.length > 0) {
|
|
9364
|
-
throw new Error(`Auth object is missing required fields: ${missingFields.join(
|
|
9352
|
+
throw new Error(`Auth object is missing required fields: ${missingFields.join(', ')}`);
|
|
9365
9353
|
}
|
|
9366
9354
|
return new ClientClass({
|
|
9367
9355
|
region: auth.AWS_REGION,
|
|
9368
9356
|
credentials: {
|
|
9369
9357
|
accessKeyId: auth.AWS_ACCESS_KEY_ID,
|
|
9370
|
-
secretAccessKey: auth.AWS_SECRET_ACCESS_KEY
|
|
9358
|
+
secretAccessKey: auth.AWS_SECRET_ACCESS_KEY
|
|
9371
9359
|
},
|
|
9372
9360
|
requestHandler: {
|
|
9373
|
-
requestTimeout
|
|
9374
|
-
}
|
|
9361
|
+
requestTimeout
|
|
9362
|
+
}
|
|
9375
9363
|
});
|
|
9376
9364
|
}
|
|
9377
9365
|
// Case 2: Check environment variables
|
|
9378
9366
|
const envVarsCheck = validateEnvironmentVars();
|
|
9379
9367
|
if (!envVarsCheck.isValid) {
|
|
9380
|
-
throw new Error(
|
|
9368
|
+
throw new Error('No valid AWS credentials found in environment variables');
|
|
9381
9369
|
}
|
|
9382
9370
|
// Case 2a: Lambda execution environment
|
|
9383
|
-
if (envVarsCheck.source ===
|
|
9371
|
+
if (envVarsCheck.source === 'lambda') {
|
|
9384
9372
|
return new ClientClass({
|
|
9385
9373
|
requestHandler: {
|
|
9386
|
-
requestTimeout
|
|
9387
|
-
}
|
|
9374
|
+
requestTimeout
|
|
9375
|
+
}
|
|
9388
9376
|
}); // AWS SDK will automatically use Lambda role credentials
|
|
9389
9377
|
}
|
|
9390
9378
|
// Case 2b: MY_ prefixed vars or standard AWS vars
|
|
9391
9379
|
return new ClientClass({
|
|
9392
|
-
region: envVarsCheck.vars
|
|
9380
|
+
region: envVarsCheck.vars.region,
|
|
9393
9381
|
credentials: {
|
|
9394
|
-
accessKeyId: envVarsCheck.vars
|
|
9395
|
-
secretAccessKey: envVarsCheck.vars
|
|
9382
|
+
accessKeyId: envVarsCheck.vars.accessKeyId,
|
|
9383
|
+
secretAccessKey: envVarsCheck.vars.secretAccessKey
|
|
9396
9384
|
},
|
|
9397
9385
|
requestHandler: {
|
|
9398
|
-
requestTimeout
|
|
9399
|
-
}
|
|
9386
|
+
requestTimeout
|
|
9387
|
+
}
|
|
9400
9388
|
});
|
|
9401
9389
|
}
|
|
9402
9390
|
catch (error) {
|
|
@@ -9409,9 +9397,7 @@ const initialiseS3Client = (auth = null) => {
|
|
|
9409
9397
|
const envVarsCheck = auth ? null : validateEnvironmentVars();
|
|
9410
9398
|
const cacheKey = generateCacheKey(auth, envVarsCheck);
|
|
9411
9399
|
if (s3ClientCache.has(cacheKey)) {
|
|
9412
|
-
|
|
9413
|
-
if (cached)
|
|
9414
|
-
return cached;
|
|
9400
|
+
return s3ClientCache.get(cacheKey);
|
|
9415
9401
|
}
|
|
9416
9402
|
// Create new client and cache it
|
|
9417
9403
|
const client = initialiseAWSClient(clientS3.S3Client, auth);
|
|
@@ -9423,9 +9409,7 @@ const initialiseLambdaClient = (auth = null) => {
|
|
|
9423
9409
|
const envVarsCheck = auth ? null : validateEnvironmentVars();
|
|
9424
9410
|
const cacheKey = generateCacheKey(auth, envVarsCheck);
|
|
9425
9411
|
if (lambdaClientCache.has(cacheKey)) {
|
|
9426
|
-
|
|
9427
|
-
if (cached)
|
|
9428
|
-
return cached;
|
|
9412
|
+
return lambdaClientCache.get(cacheKey);
|
|
9429
9413
|
}
|
|
9430
9414
|
// Create new client and cache it
|
|
9431
9415
|
const client = initialiseAWSClient(clientLambda.Lambda, auth);
|
|
@@ -9925,17 +9909,15 @@ const isRetryableS3Error = (error) => {
|
|
|
9925
9909
|
if (error instanceof Error) {
|
|
9926
9910
|
const message = error.message;
|
|
9927
9911
|
// Retry on throttling
|
|
9928
|
-
if (message.includes(
|
|
9912
|
+
if (message.includes('SlowDown') || message.includes('RequestTimeout')) {
|
|
9929
9913
|
return true;
|
|
9930
9914
|
}
|
|
9931
9915
|
// Retry on 5xx server errors
|
|
9932
|
-
if (message.includes(
|
|
9933
|
-
message.includes("ServiceUnavailable")) {
|
|
9916
|
+
if (message.includes('InternalError') || message.includes('ServiceUnavailable')) {
|
|
9934
9917
|
return true;
|
|
9935
9918
|
}
|
|
9936
9919
|
// Retry on connection errors
|
|
9937
|
-
if (message.includes(
|
|
9938
|
-
message.includes("NetworkingError")) {
|
|
9920
|
+
if (message.includes('RequestTimeTooSkewed') || message.includes('NetworkingError')) {
|
|
9939
9921
|
return true;
|
|
9940
9922
|
}
|
|
9941
9923
|
}
|
|
@@ -9949,11 +9931,10 @@ const generateRandomHash = (length) => {
|
|
|
9949
9931
|
};
|
|
9950
9932
|
function generateDateTimeStamp() {
|
|
9951
9933
|
const now = new Date();
|
|
9952
|
-
return now
|
|
9953
|
-
.
|
|
9954
|
-
.replace(
|
|
9955
|
-
.replace(
|
|
9956
|
-
.replace(/:/g, "-");
|
|
9934
|
+
return now.toISOString()
|
|
9935
|
+
.replace(/T/, '-')
|
|
9936
|
+
.replace(/\..+/, '')
|
|
9937
|
+
.replace(/:/g, '-');
|
|
9957
9938
|
}
|
|
9958
9939
|
const isValidBucketName = (name) => {
|
|
9959
9940
|
const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
@@ -9984,7 +9965,7 @@ async function uploadFile(s3Client, bucketName, filePath, destinationPath, isPub
|
|
|
9984
9965
|
Bucket: bucketName,
|
|
9985
9966
|
Key: s3Key,
|
|
9986
9967
|
Body: fileContent,
|
|
9987
|
-
ACL: isPublic ?
|
|
9968
|
+
ACL: isPublic ? 'public-read' : undefined
|
|
9988
9969
|
});
|
|
9989
9970
|
await withRetry(() => s3Client.send(command), {
|
|
9990
9971
|
maxRetries: 3,
|
|
@@ -10004,7 +9985,7 @@ async function uploadDirectory(s3Client, bucketName, dirPath, destinationPath, i
|
|
|
10004
9985
|
Bucket: bucketName,
|
|
10005
9986
|
Key: s3Key,
|
|
10006
9987
|
Body: fileContent,
|
|
10007
|
-
ACL: isPublic ?
|
|
9988
|
+
ACL: isPublic ? 'public-read' : undefined
|
|
10008
9989
|
});
|
|
10009
9990
|
await withRetry(() => s3Client.send(command), {
|
|
10010
9991
|
maxRetries: 3,
|
|
@@ -10020,7 +10001,7 @@ async function zipDirectory(sourceDir, outPath) {
|
|
|
10020
10001
|
const zip = new AdmZip();
|
|
10021
10002
|
let fileCount = 0;
|
|
10022
10003
|
try {
|
|
10023
|
-
async function addFilesToZip(currentPath, relativePath =
|
|
10004
|
+
async function addFilesToZip(currentPath, relativePath = '') {
|
|
10024
10005
|
const files = await fs$1.readdir(currentPath, { withFileTypes: true });
|
|
10025
10006
|
for (const file of files) {
|
|
10026
10007
|
const filePath = path$1.join(currentPath, file.name);
|
|
@@ -10063,33 +10044,27 @@ async function downloadFromS3Helper(s3Client, bucketName, s3Path, localPath) {
|
|
|
10063
10044
|
Prefix: s3Path,
|
|
10064
10045
|
ContinuationToken: continuationToken,
|
|
10065
10046
|
});
|
|
10066
|
-
const listResponse =
|
|
10047
|
+
const listResponse = await withRetry(() => s3Client.send(listCommand), {
|
|
10067
10048
|
maxRetries: 3,
|
|
10068
10049
|
baseDelayMs: 1000,
|
|
10069
10050
|
maxDelayMs: 15000,
|
|
10070
10051
|
retryableErrors: isRetryableS3Error,
|
|
10071
|
-
}, `S3:ListObjects:${bucketName}`)
|
|
10052
|
+
}, `S3:ListObjects:${bucketName}`);
|
|
10072
10053
|
const objects = listResponse.Contents || [];
|
|
10073
10054
|
if (objects.length === 0) {
|
|
10074
10055
|
return { fileCount: 0, folderCount: 0, totalSize: 0, fileList: [] };
|
|
10075
10056
|
}
|
|
10076
|
-
if (objects.length === 1 && objects[0].Key?.endsWith(
|
|
10057
|
+
if (objects.length === 1 && objects[0].Key?.endsWith('.zip')) {
|
|
10077
10058
|
// It's a zip file, download and extract it
|
|
10078
10059
|
const zipPath = path$1.join(localPath, path$1.basename(objects[0].Key));
|
|
10079
10060
|
await downloadFile(s3Client, bucketName, objects[0].Key, zipPath);
|
|
10080
|
-
const { totalFiles, totalFolders, totalSize: extractedSize, fileList: extractedFileList
|
|
10061
|
+
const { totalFiles, totalFolders, totalSize: extractedSize, fileList: extractedFileList } = await unzipFile(zipPath, localPath);
|
|
10081
10062
|
await fs$1.unlink(zipPath);
|
|
10082
|
-
return {
|
|
10083
|
-
fileCount: totalFiles,
|
|
10084
|
-
folderCount: totalFolders,
|
|
10085
|
-
totalSize: extractedSize,
|
|
10086
|
-
fileList: extractedFileList,
|
|
10087
|
-
};
|
|
10063
|
+
return { fileCount: totalFiles, folderCount: totalFolders, totalSize: extractedSize, fileList: extractedFileList };
|
|
10088
10064
|
}
|
|
10089
10065
|
else {
|
|
10090
10066
|
// Non-zip files. Download files in batches.
|
|
10091
|
-
for (let i = 0; i < objects.length; i += 1000) {
|
|
10092
|
-
// AWS allows up to 1000 per request
|
|
10067
|
+
for (let i = 0; i < objects.length; i += 1000) { // AWS allows up to 1000 per request
|
|
10093
10068
|
const batch = objects.slice(i, i + 1000);
|
|
10094
10069
|
await Promise.all(batch.map(async (obj) => {
|
|
10095
10070
|
if (!obj.Key)
|
|
@@ -10105,9 +10080,7 @@ async function downloadFromS3Helper(s3Client, bucketName, s3Path, localPath) {
|
|
|
10105
10080
|
}
|
|
10106
10081
|
// Count folders
|
|
10107
10082
|
const relativePath = path$1.relative(localPath, path$1.dirname(localFilePath));
|
|
10108
|
-
if (relativePath &&
|
|
10109
|
-
!relativePath.startsWith("..") &&
|
|
10110
|
-
relativePath !== ".") {
|
|
10083
|
+
if (relativePath && !relativePath.startsWith('..') && relativePath !== '.') {
|
|
10111
10084
|
folderCount++;
|
|
10112
10085
|
}
|
|
10113
10086
|
}));
|
|
@@ -10128,7 +10101,7 @@ async function downloadFile(s3Client, bucketName, s3Key, localFilePath) {
|
|
|
10128
10101
|
await withRetry(async () => {
|
|
10129
10102
|
const { Body } = await s3Client.send(getCommand);
|
|
10130
10103
|
if (!Body)
|
|
10131
|
-
throw new Error(
|
|
10104
|
+
throw new Error('No body returned from S3');
|
|
10132
10105
|
const writeStream = fs.createWriteStream(localFilePath);
|
|
10133
10106
|
await promises.pipeline(Body, writeStream);
|
|
10134
10107
|
}, {
|
|
@@ -10184,19 +10157,17 @@ async function emptyBucket(s3Client, bucketName) {
|
|
|
10184
10157
|
Bucket: bucketName,
|
|
10185
10158
|
ContinuationToken: continuationToken,
|
|
10186
10159
|
});
|
|
10187
|
-
const response =
|
|
10160
|
+
const response = await withRetry(() => s3Client.send(listCommand), {
|
|
10188
10161
|
maxRetries: 3,
|
|
10189
10162
|
baseDelayMs: 1000,
|
|
10190
10163
|
maxDelayMs: 15000,
|
|
10191
10164
|
retryableErrors: isRetryableS3Error,
|
|
10192
|
-
}, `S3:ListObjects:${bucketName}`)
|
|
10165
|
+
}, `S3:ListObjects:${bucketName}`);
|
|
10193
10166
|
const { Contents, IsTruncated, NextContinuationToken } = response;
|
|
10194
10167
|
if (Contents && Contents.length > 0) {
|
|
10195
10168
|
const deleteCommand = new clientS3.DeleteObjectsCommand({
|
|
10196
10169
|
Bucket: bucketName,
|
|
10197
|
-
Delete: {
|
|
10198
|
-
Objects: Contents.filter((c) => c.Key).map((c) => ({ Key: c.Key })),
|
|
10199
|
-
},
|
|
10170
|
+
Delete: { Objects: Contents.map(({ Key }) => ({ Key: Key })) },
|
|
10200
10171
|
});
|
|
10201
10172
|
await withRetry(() => s3Client.send(deleteCommand), {
|
|
10202
10173
|
maxRetries: 3,
|
|
@@ -23104,11 +23075,11 @@ let poolConfig = DEFAULT_POOL_CONFIG;
|
|
|
23104
23075
|
async function loadApolloModules() {
|
|
23105
23076
|
if (typeof window === "undefined" || process.env.AWS_EXECUTION_ENV) {
|
|
23106
23077
|
// Server-side (or Lambda): load the CommonJS‑based implementation.
|
|
23107
|
-
return (await Promise.resolve().then(function () { return require('./apollo-client.server-
|
|
23078
|
+
return (await Promise.resolve().then(function () { return require('./apollo-client.server-Cx30LaNh.js'); }));
|
|
23108
23079
|
}
|
|
23109
23080
|
else {
|
|
23110
23081
|
// Client-side: load the ESM‑based implementation.
|
|
23111
|
-
return (await Promise.resolve().then(function () { return require('./apollo-client.client-
|
|
23082
|
+
return (await Promise.resolve().then(function () { return require('./apollo-client.client-D6jG5B1n.js'); }));
|
|
23112
23083
|
}
|
|
23113
23084
|
}
|
|
23114
23085
|
/**
|
|
@@ -79135,7 +79106,7 @@ const RETRY_CONFIG = {
|
|
|
79135
79106
|
BASE_DELAY: 2000, // Increased base delay to 2 seconds
|
|
79136
79107
|
MAX_DELAY: 64000,
|
|
79137
79108
|
MAX_RETRIES: 5,
|
|
79138
|
-
MAX_RANDOM_DELAY: 1000
|
|
79109
|
+
MAX_RANDOM_DELAY: 1000
|
|
79139
79110
|
};
|
|
79140
79111
|
/**
|
|
79141
79112
|
* Determines if an error is related to Google Sheets API quotas or rate limits.
|
|
@@ -79161,10 +79132,10 @@ const RETRY_CONFIG = {
|
|
|
79161
79132
|
*/
|
|
79162
79133
|
function isQuotaError(error) {
|
|
79163
79134
|
const message = error.message.toLowerCase();
|
|
79164
|
-
return
|
|
79165
|
-
message.includes(
|
|
79166
|
-
message.includes(
|
|
79167
|
-
message.includes(
|
|
79135
|
+
return message.includes('quota exceeded') ||
|
|
79136
|
+
message.includes('rate limit') ||
|
|
79137
|
+
message.includes('429') ||
|
|
79138
|
+
message.includes('too many requests');
|
|
79168
79139
|
}
|
|
79169
79140
|
/**
|
|
79170
79141
|
* Implements exponential backoff retry mechanism for Google Sheets API calls.
|
|
@@ -79223,14 +79194,14 @@ async function withExponentialBackoff(operation) {
|
|
|
79223
79194
|
const exponentialDelay = Math.min(Math.pow(2, retries) * RETRY_CONFIG.BASE_DELAY + randomDelay, RETRY_CONFIG.MAX_DELAY);
|
|
79224
79195
|
logIfDebug(`Quota/Rate limit exceeded. Error: ${error.message}`);
|
|
79225
79196
|
logIfDebug(`Retrying in ${exponentialDelay}ms (attempt ${retries}/${RETRY_CONFIG.MAX_RETRIES})`);
|
|
79226
|
-
await new Promise(
|
|
79197
|
+
await new Promise(resolve => setTimeout(resolve, exponentialDelay));
|
|
79227
79198
|
}
|
|
79228
79199
|
}
|
|
79229
79200
|
}
|
|
79230
79201
|
function checkEnvVars() {
|
|
79231
79202
|
const secrets = getSecrets();
|
|
79232
79203
|
if (!secrets.googleSheets.clientEmail || !secrets.googleSheets.privateKey) {
|
|
79233
|
-
throw new Error(
|
|
79204
|
+
throw new Error('GOOGLE_SHEETS_CLIENT_EMAIL or GOOGLE_SHEETS_PRIVATE_KEY is not defined in environment variables.');
|
|
79234
79205
|
}
|
|
79235
79206
|
}
|
|
79236
79207
|
/**
|
|
@@ -79245,16 +79216,16 @@ async function getAuthClient() {
|
|
|
79245
79216
|
const secrets = getSecrets();
|
|
79246
79217
|
const credentials = {
|
|
79247
79218
|
client_email: secrets.googleSheets.clientEmail,
|
|
79248
|
-
private_key: secrets.googleSheets.privateKey?.replace(/\\n/g,
|
|
79219
|
+
private_key: secrets.googleSheets.privateKey?.replace(/\\n/g, '\n'),
|
|
79249
79220
|
};
|
|
79250
79221
|
const auth = new require$$0$6.GoogleAuth({
|
|
79251
79222
|
credentials,
|
|
79252
|
-
scopes: [
|
|
79223
|
+
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
79253
79224
|
});
|
|
79254
79225
|
return auth.getClient();
|
|
79255
79226
|
}
|
|
79256
79227
|
catch (error) {
|
|
79257
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79228
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79258
79229
|
logIfDebug(`Error initializing Google Sheets auth: ${errorMessage}`);
|
|
79259
79230
|
throw new Error(`Failed to initialize Google Sheets auth: ${errorMessage}`);
|
|
79260
79231
|
}
|
|
@@ -79266,7 +79237,7 @@ async function getSheetsClient() {
|
|
|
79266
79237
|
const auth = await getAuthClient();
|
|
79267
79238
|
return new buildExports.sheets_v4.Sheets({
|
|
79268
79239
|
auth,
|
|
79269
|
-
timeout: GOOGLE_SHEETS_TIMEOUT_MS
|
|
79240
|
+
timeout: GOOGLE_SHEETS_TIMEOUT_MS
|
|
79270
79241
|
});
|
|
79271
79242
|
}
|
|
79272
79243
|
/**
|
|
@@ -79299,7 +79270,7 @@ function escapeSheetName(sheetName) {
|
|
|
79299
79270
|
* @param startColumn Optional starting column (defaults to 'A')
|
|
79300
79271
|
* @returns Promise resolving to response with success status and row details
|
|
79301
79272
|
*/
|
|
79302
|
-
async function addRow(config, values, startColumn =
|
|
79273
|
+
async function addRow(config, values, startColumn = 'A') {
|
|
79303
79274
|
try {
|
|
79304
79275
|
const sheets = await getSheetsClient();
|
|
79305
79276
|
const escapedSheetName = escapeSheetName(config.sheetName);
|
|
@@ -79307,7 +79278,7 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79307
79278
|
const response = await withExponentialBackoff(() => sheets.spreadsheets.values.append({
|
|
79308
79279
|
spreadsheetId: config.spreadsheetId,
|
|
79309
79280
|
range,
|
|
79310
|
-
valueInputOption:
|
|
79281
|
+
valueInputOption: 'USER_ENTERED',
|
|
79311
79282
|
requestBody: {
|
|
79312
79283
|
values: [values],
|
|
79313
79284
|
},
|
|
@@ -79315,9 +79286,9 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79315
79286
|
//logIfDebug(`Added row to sheet ${config.sheetName}: ${values.join(', ')}`);
|
|
79316
79287
|
const rowNumber = response.data.updates?.updatedRange
|
|
79317
79288
|
? parseInt(response.data.updates.updatedRange
|
|
79318
|
-
.split(
|
|
79319
|
-
.split(
|
|
79320
|
-
.replace(/[^0-9]/g,
|
|
79289
|
+
.split('!')[1]
|
|
79290
|
+
.split(':')[0]
|
|
79291
|
+
.replace(/[^0-9]/g, ''))
|
|
79321
79292
|
: 0;
|
|
79322
79293
|
return {
|
|
79323
79294
|
success: true,
|
|
@@ -79328,7 +79299,7 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79328
79299
|
};
|
|
79329
79300
|
}
|
|
79330
79301
|
catch (error) {
|
|
79331
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79302
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79332
79303
|
logIfDebug(`Error adding row to sheet: ${errorMessage}`);
|
|
79333
79304
|
return {
|
|
79334
79305
|
success: false,
|
|
@@ -79370,7 +79341,7 @@ async function addSheet(spreadsheetId, sheetTitle) {
|
|
|
79370
79341
|
};
|
|
79371
79342
|
}
|
|
79372
79343
|
catch (error) {
|
|
79373
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79344
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79374
79345
|
logIfDebug(`Error adding sheet: ${errorMessage}`);
|
|
79375
79346
|
return {
|
|
79376
79347
|
success: false,
|
|
@@ -79394,7 +79365,7 @@ async function writeCell(config, cell, value) {
|
|
|
79394
79365
|
await withExponentialBackoff(() => sheets.spreadsheets.values.update({
|
|
79395
79366
|
spreadsheetId: config.spreadsheetId,
|
|
79396
79367
|
range,
|
|
79397
|
-
valueInputOption:
|
|
79368
|
+
valueInputOption: 'USER_ENTERED',
|
|
79398
79369
|
requestBody: {
|
|
79399
79370
|
values: [[value]],
|
|
79400
79371
|
},
|
|
@@ -79408,7 +79379,7 @@ async function writeCell(config, cell, value) {
|
|
|
79408
79379
|
};
|
|
79409
79380
|
}
|
|
79410
79381
|
catch (error) {
|
|
79411
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79382
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79412
79383
|
logIfDebug(`Error writing to cell: ${errorMessage}`);
|
|
79413
79384
|
return {
|
|
79414
79385
|
success: false,
|
|
@@ -79432,11 +79403,8 @@ async function getCell(config, cell) {
|
|
|
79432
79403
|
spreadsheetId: config.spreadsheetId,
|
|
79433
79404
|
range,
|
|
79434
79405
|
}));
|
|
79435
|
-
const
|
|
79436
|
-
|
|
79437
|
-
? null
|
|
79438
|
-
: rawValue;
|
|
79439
|
-
logIfDebug(`Read value from cell ${range}: ${String(value)}`);
|
|
79406
|
+
const value = response.data.values?.[0]?.[0];
|
|
79407
|
+
logIfDebug(`Read value from cell ${range}: ${value}`);
|
|
79440
79408
|
return {
|
|
79441
79409
|
success: true,
|
|
79442
79410
|
data: {
|
|
@@ -79445,7 +79413,7 @@ async function getCell(config, cell) {
|
|
|
79445
79413
|
};
|
|
79446
79414
|
}
|
|
79447
79415
|
catch (error) {
|
|
79448
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79416
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79449
79417
|
logIfDebug(`Error reading from cell: ${errorMessage}`);
|
|
79450
79418
|
return {
|
|
79451
79419
|
success: false,
|
|
@@ -79478,11 +79446,10 @@ function convertA1ToRowCol(a1Notation) {
|
|
|
79478
79446
|
* @returns Padded array of arrays
|
|
79479
79447
|
*/
|
|
79480
79448
|
function padArrays(arrays) {
|
|
79481
|
-
const maxLength = Math.max(...arrays.map(
|
|
79482
|
-
return arrays.map(
|
|
79449
|
+
const maxLength = Math.max(...arrays.map(arr => arr.length));
|
|
79450
|
+
return arrays.map(arr => {
|
|
79483
79451
|
if (arr.length < maxLength) {
|
|
79484
|
-
|
|
79485
|
-
return [...arr, ...padding];
|
|
79452
|
+
return [...arr, ...Array(maxLength - arr.length).fill('')];
|
|
79486
79453
|
}
|
|
79487
79454
|
return arr;
|
|
79488
79455
|
});
|
|
@@ -79494,10 +79461,10 @@ function padArrays(arrays) {
|
|
|
79494
79461
|
* @param startCell Optional starting cell in A1 notation (defaults to 'A1')
|
|
79495
79462
|
* @returns Promise resolving to response with success status and update details
|
|
79496
79463
|
*/
|
|
79497
|
-
async function writeArray(config, data, startCell =
|
|
79464
|
+
async function writeArray(config, data, startCell = 'A1') {
|
|
79498
79465
|
try {
|
|
79499
79466
|
if (!data.length) {
|
|
79500
|
-
throw new Error(
|
|
79467
|
+
throw new Error('Data array is empty');
|
|
79501
79468
|
}
|
|
79502
79469
|
const sheets = await getSheetsClient();
|
|
79503
79470
|
const { row: startRow, column: startCol } = convertA1ToRowCol(startCell);
|
|
@@ -79513,7 +79480,7 @@ async function writeArray(config, data, startCell = "A1") {
|
|
|
79513
79480
|
const response = await withExponentialBackoff(() => sheets.spreadsheets.values.update({
|
|
79514
79481
|
spreadsheetId: config.spreadsheetId,
|
|
79515
79482
|
range,
|
|
79516
|
-
valueInputOption:
|
|
79483
|
+
valueInputOption: 'USER_ENTERED',
|
|
79517
79484
|
requestBody: {
|
|
79518
79485
|
values: paddedData,
|
|
79519
79486
|
},
|
|
@@ -79529,7 +79496,7 @@ async function writeArray(config, data, startCell = "A1") {
|
|
|
79529
79496
|
};
|
|
79530
79497
|
}
|
|
79531
79498
|
catch (error) {
|
|
79532
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79499
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79533
79500
|
logIfDebug(`Error writing array to sheet: ${errorMessage}`);
|
|
79534
79501
|
return {
|
|
79535
79502
|
success: false,
|
|
@@ -81823,4 +81790,4 @@ exports.withCorrelationId = withCorrelationId;
|
|
|
81823
81790
|
exports.withMetrics = withMetrics;
|
|
81824
81791
|
exports.withRateLimit = withRateLimit;
|
|
81825
81792
|
exports.withRetry = withRetry;
|
|
81826
|
-
//# sourceMappingURL=index-
|
|
81793
|
+
//# sourceMappingURL=index-ChygDqeG.js.map
|