@adaptic/lumic-utils 1.0.24 → 1.0.26
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-CtjgIfRT.js} +3 -3
- package/dist/{apollo-client.client-CyxU1hTu.js.map → apollo-client.client-CtjgIfRT.js.map} +1 -1
- package/dist/{apollo-client.client-9wJcufhf.js → apollo-client.client-Uv0-ZrFd.js} +4 -4
- package/dist/{apollo-client.client-9wJcufhf.js.map → apollo-client.client-Uv0-ZrFd.js.map} +1 -1
- package/dist/{apollo-client.server-CbagxkmK.js → apollo-client.server-DaXmxoBl.js} +3 -3
- package/dist/{apollo-client.server-CbagxkmK.js.map → apollo-client.server-DaXmxoBl.js.map} +1 -1
- package/dist/{apollo-client.server-DB3jLbBx.js → apollo-client.server-JYHXy9ib.js} +3 -3
- package/dist/{apollo-client.server-DB3jLbBx.js.map → apollo-client.server-JYHXy9ib.js.map} +1 -1
- package/dist/{index-B4yVKGNR.js → index-Bu-2kk3p.js} +2 -2
- package/dist/{index-B4yVKGNR.js.map → index-Bu-2kk3p.js.map} +1 -1
- package/dist/{index-KzQOh2uu.js → index-DgFDhFuO.js} +528 -453
- package/dist/{index-KzQOh2uu.js.map → index-DgFDhFuO.js.map} +1 -1
- package/dist/{index-CL79JTWc.js → index-HopiPnjc.js} +2 -2
- package/dist/{index-CL79JTWc.js.map → index-HopiPnjc.js.map} +1 -1
- package/dist/{index-EMQ1uJMh.js → index-z7Y1nTVX.js} +528 -453
- package/dist/{index-EMQ1uJMh.js.map → index-z7Y1nTVX.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 +6 -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
|
@@ -551,19 +551,10 @@ function getModelProvider(model) {
|
|
|
551
551
|
return SUPPORTED_MODELS[model].provider;
|
|
552
552
|
}
|
|
553
553
|
/**
|
|
554
|
-
* Default model tiers per provider for use with LLM_MODEL_MINI/NORMAL/ADVANCED env vars
|
|
555
|
-
*
|
|
556
|
-
* OpenAI `advanced` was reverted from `gpt-5.5` back to `gpt-5.4` on 2026-05-30:
|
|
557
|
-
* the heavy tool-call prompt in adaptic-engine's trading-decision path was
|
|
558
|
-
* timing out even after the engine raised `LLM_CALL_TIMEOUT_MS` to 90s (5+
|
|
559
|
-
* tickers per loop still exceeded the budget). `gpt-5.5`'s 1M-context window
|
|
560
|
-
* is not required on that path — the prompt fits comfortably inside `gpt-5.4`'s
|
|
561
|
-
* envelope and `gpt-5.4` returns inside the original 25-30s p95. `gpt-5.5`
|
|
562
|
-
* remains registered in OPENAI_MODELS and reachable by explicit model id;
|
|
563
|
-
* only the `advanced` tier default is rolled back.
|
|
554
|
+
* Default model tiers per provider for use with LLM_MODEL_MINI/NORMAL/ADVANCED env vars
|
|
564
555
|
*/
|
|
565
556
|
const PROVIDER_DEFAULT_MODELS = {
|
|
566
|
-
openai: { mini: 'gpt-5.4-nano', normal: 'gpt-5.4-mini', advanced: 'gpt-5.
|
|
557
|
+
openai: { mini: 'gpt-5.4-nano', normal: 'gpt-5.4-mini', advanced: 'gpt-5.5' },
|
|
567
558
|
anthropic: { mini: 'claude-haiku-4-5', normal: 'claude-sonnet-4-6', advanced: 'claude-opus-4-7' },
|
|
568
559
|
deepseek: { mini: 'deepseek-v4-flash', normal: 'deepseek-v4-flash', advanced: 'deepseek-v4-pro' },
|
|
569
560
|
kimi: { mini: 'kimi-k2-0905-preview', normal: 'kimi-k2.5', advanced: 'kimi-k2.6' },
|
|
@@ -1517,13 +1508,11 @@ let openai;
|
|
|
1517
1508
|
function initializeOpenAI(apiKey) {
|
|
1518
1509
|
const key = apiKey || getSecrets().openai.apiKey;
|
|
1519
1510
|
if (!key) {
|
|
1520
|
-
throw new Error(
|
|
1511
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
1521
1512
|
}
|
|
1522
1513
|
return new OpenAI({
|
|
1523
1514
|
apiKey: key,
|
|
1524
|
-
defaultHeaders: {
|
|
1525
|
-
"User-Agent": "My Server-side Application (compatible; OpenAI API Client)",
|
|
1526
|
-
},
|
|
1515
|
+
defaultHeaders: { 'User-Agent': 'My Server-side Application (compatible; OpenAI API Client)' },
|
|
1527
1516
|
});
|
|
1528
1517
|
}
|
|
1529
1518
|
/**
|
|
@@ -1541,7 +1530,9 @@ function initializeOpenAI(apiKey) {
|
|
|
1541
1530
|
*/
|
|
1542
1531
|
async function fixJsonWithAI(jsonStr, options) {
|
|
1543
1532
|
// Backward compatibility: handle string apiKey as first positional param
|
|
1544
|
-
const opts = typeof options ===
|
|
1533
|
+
const opts = typeof options === 'string'
|
|
1534
|
+
? { apiKey: options }
|
|
1535
|
+
: (options ?? {});
|
|
1545
1536
|
const apiKey = opts.apiKey;
|
|
1546
1537
|
const maxDepth = opts.maxDepth ?? 2;
|
|
1547
1538
|
const depth = opts._depth ?? 0;
|
|
@@ -1554,18 +1545,18 @@ async function fixJsonWithAI(jsonStr, options) {
|
|
|
1554
1545
|
openai = initializeOpenAI(apiKey);
|
|
1555
1546
|
}
|
|
1556
1547
|
const completion = await openai.chat.completions.create({
|
|
1557
|
-
model:
|
|
1548
|
+
model: 'gpt-5-mini',
|
|
1558
1549
|
messages: [
|
|
1559
1550
|
{
|
|
1560
|
-
role:
|
|
1561
|
-
content:
|
|
1551
|
+
role: 'system',
|
|
1552
|
+
content: 'You are a JSON fixer. Return only valid JSON without any additional text or explanation.'
|
|
1562
1553
|
},
|
|
1563
1554
|
{
|
|
1564
|
-
role:
|
|
1565
|
-
content: `Fix this broken JSON:\n${jsonStr}
|
|
1566
|
-
}
|
|
1555
|
+
role: 'user',
|
|
1556
|
+
content: `Fix this broken JSON:\n${jsonStr}`
|
|
1557
|
+
}
|
|
1567
1558
|
],
|
|
1568
|
-
response_format: { type:
|
|
1559
|
+
response_format: { type: 'json_object' }
|
|
1569
1560
|
});
|
|
1570
1561
|
const fixedJson = completion.choices[0]?.message?.content;
|
|
1571
1562
|
if (fixedJson && isValidJson(fixedJson)) {
|
|
@@ -1579,32 +1570,23 @@ async function fixJsonWithAI(jsonStr, options) {
|
|
|
1579
1570
|
_depth: depth + 1,
|
|
1580
1571
|
});
|
|
1581
1572
|
}
|
|
1582
|
-
throw new JsonParseError(
|
|
1573
|
+
throw new JsonParseError('Failed to fix JSON with AI: returned invalid JSON');
|
|
1583
1574
|
}
|
|
1584
1575
|
catch (err) {
|
|
1585
1576
|
// Re-throw JsonParseError as-is (e.g. max depth exceeded)
|
|
1586
1577
|
if (err instanceof JsonParseError) {
|
|
1587
1578
|
throw err;
|
|
1588
1579
|
}
|
|
1589
|
-
const errorMessage = err instanceof Error ? err.message :
|
|
1580
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
1590
1581
|
throw new JsonParseError(`Error fixing JSON with AI: ${errorMessage}`);
|
|
1591
1582
|
}
|
|
1592
1583
|
}
|
|
1593
1584
|
|
|
1594
1585
|
// llm-utils.ts
|
|
1595
1586
|
function normalizeModelName(model) {
|
|
1596
|
-
return model.replace(/_/g,
|
|
1597
|
-
}
|
|
1598
|
-
const CODE_BLOCK_TYPES = [
|
|
1599
|
-
"javascript",
|
|
1600
|
-
"js",
|
|
1601
|
-
"graphql",
|
|
1602
|
-
"json",
|
|
1603
|
-
"typescript",
|
|
1604
|
-
"python",
|
|
1605
|
-
"markdown",
|
|
1606
|
-
"yaml",
|
|
1607
|
-
];
|
|
1587
|
+
return model.replace(/_/g, '-').toLowerCase();
|
|
1588
|
+
}
|
|
1589
|
+
const CODE_BLOCK_TYPES = ['javascript', 'js', 'graphql', 'json', 'typescript', 'python', 'markdown', 'yaml'];
|
|
1608
1590
|
/**
|
|
1609
1591
|
* Tries to parse JSON from the given content string according to the specified
|
|
1610
1592
|
* response format. If the response format is 'json' or a JSON schema object, it
|
|
@@ -1628,9 +1610,7 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1628
1610
|
if (recursionDepth > maxRetries) {
|
|
1629
1611
|
throw new Error(`Maximum recursion depth (${maxRetries}) exceeded while parsing JSON. AI fixing may have returned broken JSON.`);
|
|
1630
1612
|
}
|
|
1631
|
-
if (responseFormat ===
|
|
1632
|
-
(typeof responseFormat === "object" &&
|
|
1633
|
-
responseFormat?.type === "json_schema")) {
|
|
1613
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat?.type === 'json_schema')) {
|
|
1634
1614
|
let cleanedContent = content.trim();
|
|
1635
1615
|
let detectedType = null;
|
|
1636
1616
|
// Remove code block markers if present
|
|
@@ -1641,15 +1621,24 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1641
1621
|
}
|
|
1642
1622
|
return false;
|
|
1643
1623
|
});
|
|
1644
|
-
if (startsWithCodeBlock && cleanedContent.endsWith(
|
|
1645
|
-
const firstLineEndIndex = cleanedContent.indexOf(
|
|
1624
|
+
if (startsWithCodeBlock && cleanedContent.endsWith('```')) {
|
|
1625
|
+
const firstLineEndIndex = cleanedContent.indexOf('\n');
|
|
1646
1626
|
if (firstLineEndIndex !== -1) {
|
|
1647
1627
|
cleanedContent = cleanedContent.slice(firstLineEndIndex + 1, -3).trim();
|
|
1648
1628
|
getLumicLogger().info(`Code block for type ${detectedType} detected and removed. Cleaned content: ${cleanedContent}`);
|
|
1649
1629
|
}
|
|
1650
1630
|
}
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1631
|
+
// Whether the cleaned content structurally resembles JSON. Used to gate the
|
|
1632
|
+
// lenient fixBrokenJson strategy, which will otherwise coerce arbitrary
|
|
1633
|
+
// prose into a bogus object/array.
|
|
1634
|
+
const inputResemblesJson = cleanedContent.startsWith('{') || cleanedContent.startsWith('[');
|
|
1635
|
+
// Multiple JSON parsing strategies.
|
|
1636
|
+
// Default pipeline: direct parse -> extract from brackets -> remove
|
|
1637
|
+
// leading/trailing text -> GATED fixBrokenJson -> FAIL. fixBrokenJson is
|
|
1638
|
+
// deliberately gated (Strategy 4): its output is accepted only when the
|
|
1639
|
+
// input looked like JSON AND the repaired value round-trips through
|
|
1640
|
+
// JSON.stringify/JSON.parse, so a "successful" repair of non-JSON text is
|
|
1641
|
+
// rejected instead of silently returned as garbage.
|
|
1653
1642
|
const jsonParsingStrategies = [
|
|
1654
1643
|
// Strategy 1: Direct parsing
|
|
1655
1644
|
() => {
|
|
@@ -1675,7 +1664,7 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1675
1664
|
},
|
|
1676
1665
|
// Strategy 3: Remove leading/trailing text and try parsing
|
|
1677
1666
|
() => {
|
|
1678
|
-
const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g,
|
|
1667
|
+
const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g, '');
|
|
1679
1668
|
try {
|
|
1680
1669
|
return JSON.parse(jsonMatch);
|
|
1681
1670
|
}
|
|
@@ -1683,55 +1672,175 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1683
1672
|
return null;
|
|
1684
1673
|
}
|
|
1685
1674
|
},
|
|
1686
|
-
// Strategy 4:
|
|
1675
|
+
// Strategy 4: Gated fixBrokenJson (string manipulation only, no AI)
|
|
1687
1676
|
() => {
|
|
1688
|
-
|
|
1677
|
+
if (!inputResemblesJson) {
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
const fixed = fixBrokenJson(cleanedContent);
|
|
1681
|
+
if (fixed === null) {
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
try {
|
|
1685
|
+
// Round-trip guard: ensures the repaired value is genuinely
|
|
1686
|
+
// JSON-serializable before we trust it.
|
|
1687
|
+
return JSON.parse(JSON.stringify(fixed));
|
|
1688
|
+
}
|
|
1689
|
+
catch {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1689
1692
|
},
|
|
1690
1693
|
];
|
|
1691
1694
|
// Try each parsing strategy
|
|
1692
1695
|
for (const strategy of jsonParsingStrategies) {
|
|
1693
|
-
const result =
|
|
1696
|
+
const result = strategy();
|
|
1694
1697
|
if (result !== null) {
|
|
1695
|
-
return result;
|
|
1698
|
+
return finalizeJsonResult(result, responseFormat);
|
|
1696
1699
|
}
|
|
1697
1700
|
}
|
|
1698
1701
|
// Strategy 5: Use AI fixing (only if explicitly enabled)
|
|
1699
1702
|
if (enableAiFix) {
|
|
1700
|
-
getLumicLogger().warn(
|
|
1703
|
+
getLumicLogger().warn('Using AI-powered JSON fixing. This adds cost and latency. Consider improving JSON generation instead.');
|
|
1704
|
+
let aiFixedJson = null;
|
|
1701
1705
|
try {
|
|
1702
|
-
|
|
1703
|
-
if (aiFixedJson !== null) {
|
|
1704
|
-
// Validate the AI-fixed JSON by attempting to stringify and re-parse it
|
|
1705
|
-
// This ensures it's actually valid JSON and not just something that passed fixJsonWithAI
|
|
1706
|
-
try {
|
|
1707
|
-
const validated = JSON.parse(JSON.stringify(aiFixedJson));
|
|
1708
|
-
return validated;
|
|
1709
|
-
}
|
|
1710
|
-
catch {
|
|
1711
|
-
// AI returned something that looks like JSON but isn't valid
|
|
1712
|
-
// Increment recursion depth and try parsing the AI response
|
|
1713
|
-
getLumicLogger().warn("AI fixing returned invalid JSON, attempting recursive parse...");
|
|
1714
|
-
return parseResponse(JSON.stringify(aiFixedJson), responseFormat, {
|
|
1715
|
-
...options,
|
|
1716
|
-
_recursionDepth: recursionDepth + 1,
|
|
1717
|
-
});
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1706
|
+
aiFixedJson = await fixJsonWithAI(cleanedContent);
|
|
1720
1707
|
}
|
|
1721
1708
|
catch (error) {
|
|
1722
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
1709
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1723
1710
|
getLumicLogger().error(`AI JSON fixing failed: ${errorMessage}`);
|
|
1724
|
-
//
|
|
1711
|
+
// Fall through to the final error below.
|
|
1712
|
+
}
|
|
1713
|
+
if (aiFixedJson !== null) {
|
|
1714
|
+
let validated;
|
|
1715
|
+
try {
|
|
1716
|
+
// Validate the AI-fixed JSON by attempting to stringify and re-parse
|
|
1717
|
+
// it. This ensures it is actually valid JSON, not just something that
|
|
1718
|
+
// passed fixJsonWithAI.
|
|
1719
|
+
validated = JSON.parse(JSON.stringify(aiFixedJson));
|
|
1720
|
+
}
|
|
1721
|
+
catch {
|
|
1722
|
+
// AI returned something that looks like JSON but isn't valid.
|
|
1723
|
+
// Increment recursion depth and try parsing the AI response.
|
|
1724
|
+
getLumicLogger().warn('AI fixing returned invalid JSON, attempting recursive parse...');
|
|
1725
|
+
return parseResponse(JSON.stringify(aiFixedJson), responseFormat, {
|
|
1726
|
+
...options,
|
|
1727
|
+
_recursionDepth: recursionDepth + 1,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
// Schema validation (json_schema format) happens here, OUTSIDE the
|
|
1731
|
+
// JSON.parse try/catch, so a schema mismatch propagates as a thrown
|
|
1732
|
+
// JsonParseError rather than being mistaken for invalid JSON.
|
|
1733
|
+
return finalizeJsonResult(validated, responseFormat);
|
|
1725
1734
|
}
|
|
1726
1735
|
}
|
|
1727
1736
|
// If all strategies fail, throw an error
|
|
1728
1737
|
getLumicLogger().error(`Failed to parse JSON from content: ${cleanedContent}. Original JSON: ${content}`);
|
|
1729
|
-
throw new Error(
|
|
1738
|
+
throw new Error('Unable to parse JSON response');
|
|
1730
1739
|
}
|
|
1731
1740
|
else {
|
|
1732
1741
|
return content;
|
|
1733
1742
|
}
|
|
1734
1743
|
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Reports the JSON type of a runtime value for schema-validation diagnostics.
|
|
1746
|
+
*
|
|
1747
|
+
* @param value The value whose JSON type should be described.
|
|
1748
|
+
* @returns One of `'null' | 'array' | 'object' | 'string' | 'number' | 'boolean' | 'undefined'`.
|
|
1749
|
+
*/
|
|
1750
|
+
function jsonTypeOf(value) {
|
|
1751
|
+
if (value === null)
|
|
1752
|
+
return 'null';
|
|
1753
|
+
if (Array.isArray(value))
|
|
1754
|
+
return 'array';
|
|
1755
|
+
return typeof value;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Extracts a declared primitive `type` string from a property schema, if present.
|
|
1759
|
+
*
|
|
1760
|
+
* @param propertySchema The (untyped) per-property JSON-Schema fragment.
|
|
1761
|
+
* @returns The declared type string, or null when none is declared.
|
|
1762
|
+
*/
|
|
1763
|
+
function extractSchemaType(propertySchema) {
|
|
1764
|
+
if (typeof propertySchema === 'object' && propertySchema !== null && 'type' in propertySchema) {
|
|
1765
|
+
const declared = propertySchema.type;
|
|
1766
|
+
return typeof declared === 'string' ? declared : null;
|
|
1767
|
+
}
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Checks whether a runtime value satisfies a declared JSON-Schema primitive
|
|
1772
|
+
* type. Unknown/unsupported type keywords are treated permissively (they never
|
|
1773
|
+
* cause a rejection), so this validator only ever rejects clear mismatches.
|
|
1774
|
+
*
|
|
1775
|
+
* @param value The runtime value to check.
|
|
1776
|
+
* @param expectedType The declared JSON-Schema type keyword.
|
|
1777
|
+
* @returns True when the value matches (or the keyword is unsupported).
|
|
1778
|
+
*/
|
|
1779
|
+
function valueMatchesJsonType(value, expectedType) {
|
|
1780
|
+
switch (expectedType) {
|
|
1781
|
+
case 'string':
|
|
1782
|
+
return typeof value === 'string';
|
|
1783
|
+
case 'number':
|
|
1784
|
+
return typeof value === 'number';
|
|
1785
|
+
case 'integer':
|
|
1786
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
1787
|
+
case 'boolean':
|
|
1788
|
+
return typeof value === 'boolean';
|
|
1789
|
+
case 'object':
|
|
1790
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1791
|
+
case 'array':
|
|
1792
|
+
return Array.isArray(value);
|
|
1793
|
+
case 'null':
|
|
1794
|
+
return value === null;
|
|
1795
|
+
default:
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Validates a parsed value against a `json_schema` object schema. Enforces that
|
|
1801
|
+
* the value is a plain object, that every `required` property is present, and
|
|
1802
|
+
* that present properties whose schema declares a primitive `type` match it.
|
|
1803
|
+
*
|
|
1804
|
+
* @param value The parsed value to validate.
|
|
1805
|
+
* @param schema The object schema supplied on the response format.
|
|
1806
|
+
* @throws {JsonParseError} When the value violates the schema.
|
|
1807
|
+
*/
|
|
1808
|
+
function validateJsonSchema(value, schema) {
|
|
1809
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1810
|
+
throw new JsonParseError(`JSON schema validation failed: expected an object but received ${jsonTypeOf(value)}.`);
|
|
1811
|
+
}
|
|
1812
|
+
const record = value;
|
|
1813
|
+
const missing = (schema.required ?? []).filter((key) => !(key in record));
|
|
1814
|
+
if (missing.length > 0) {
|
|
1815
|
+
throw new JsonParseError(`JSON schema validation failed: missing required properties: ${missing.join(', ')}.`);
|
|
1816
|
+
}
|
|
1817
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
1818
|
+
if (!(key in record))
|
|
1819
|
+
continue;
|
|
1820
|
+
const expectedType = extractSchemaType(propertySchema);
|
|
1821
|
+
if (expectedType !== null && !valueMatchesJsonType(record[key], expectedType)) {
|
|
1822
|
+
throw new JsonParseError(`JSON schema validation failed: property "${key}" expected type "${expectedType}" but received ${jsonTypeOf(record[key])}.`);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Finalizes a successfully-parsed JSON value. When the caller supplied a
|
|
1828
|
+
* `json_schema` response format, the value is validated against that schema
|
|
1829
|
+
* before being returned so schema violations surface as a thrown
|
|
1830
|
+
* {@link JsonParseError} instead of flowing silently downstream. For plain
|
|
1831
|
+
* `json` formats the value is returned unchanged.
|
|
1832
|
+
*
|
|
1833
|
+
* @param value The parsed value produced by a parsing strategy.
|
|
1834
|
+
* @param responseFormat The originally requested response format.
|
|
1835
|
+
* @returns The value narrowed to `T`.
|
|
1836
|
+
* @throws {JsonParseError} When a supplied json_schema is not satisfied.
|
|
1837
|
+
*/
|
|
1838
|
+
function finalizeJsonResult(value, responseFormat) {
|
|
1839
|
+
if (typeof responseFormat === 'object' && responseFormat?.type === 'json_schema') {
|
|
1840
|
+
validateJsonSchema(value, responseFormat.schema);
|
|
1841
|
+
}
|
|
1842
|
+
return value;
|
|
1843
|
+
}
|
|
1735
1844
|
|
|
1736
1845
|
/**
|
|
1737
1846
|
* Default timeout values (in milliseconds) for external service calls.
|
|
@@ -2003,11 +2112,19 @@ const DEFAULT_RETRY_CONFIG = {
|
|
|
2003
2112
|
* @throws The last error encountered if all retry attempts fail or if a non-retryable error occurs
|
|
2004
2113
|
*/
|
|
2005
2114
|
async function withRetry(fn, config = {}, label = 'unknown') {
|
|
2006
|
-
const { maxRetries, baseDelayMs, maxDelayMs, retryableErrors, circuitBreaker } = {
|
|
2115
|
+
const { maxRetries, baseDelayMs, maxDelayMs, retryableErrors, circuitBreaker, signal } = {
|
|
2007
2116
|
...DEFAULT_RETRY_CONFIG,
|
|
2008
2117
|
...config,
|
|
2009
2118
|
};
|
|
2010
2119
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2120
|
+
// Short-circuit on cancellation BEFORE starting a fresh attempt so
|
|
2121
|
+
// the caller does not pay the cost of issuing a request whose result
|
|
2122
|
+
// they have already discarded.
|
|
2123
|
+
if (signal?.aborted) {
|
|
2124
|
+
throw signal.reason instanceof Error
|
|
2125
|
+
? signal.reason
|
|
2126
|
+
: new Error('Aborted');
|
|
2127
|
+
}
|
|
2011
2128
|
try {
|
|
2012
2129
|
// If a circuit breaker is provided, execute through it
|
|
2013
2130
|
const result = circuitBreaker
|
|
@@ -2020,6 +2137,13 @@ async function withRetry(fn, config = {}, label = 'unknown') {
|
|
|
2020
2137
|
if (error instanceof CircuitBreakerOpenError) {
|
|
2021
2138
|
throw error;
|
|
2022
2139
|
}
|
|
2140
|
+
// Do not retry on cooperative cancellation — the caller has
|
|
2141
|
+
// explicitly asked us to stop.
|
|
2142
|
+
if (signal?.aborted) {
|
|
2143
|
+
throw signal.reason instanceof Error
|
|
2144
|
+
? signal.reason
|
|
2145
|
+
: new Error('Aborted');
|
|
2146
|
+
}
|
|
2023
2147
|
// Don't retry if we've exhausted all attempts
|
|
2024
2148
|
if (attempt === maxRetries) {
|
|
2025
2149
|
throw error;
|
|
@@ -2033,7 +2157,26 @@ async function withRetry(fn, config = {}, label = 'unknown') {
|
|
|
2033
2157
|
const jitter = Math.random() * 1000;
|
|
2034
2158
|
const delay = Math.min(exponentialDelay + jitter, maxDelayMs);
|
|
2035
2159
|
getLumicLogger().warn(`[${label}] Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms`);
|
|
2036
|
-
|
|
2160
|
+
// Sleep but bail early if signal aborts mid-backoff.
|
|
2161
|
+
await new Promise((resolve, reject) => {
|
|
2162
|
+
const timer = setTimeout(() => {
|
|
2163
|
+
if (signal)
|
|
2164
|
+
signal.removeEventListener('abort', onAbort);
|
|
2165
|
+
resolve();
|
|
2166
|
+
}, delay);
|
|
2167
|
+
const onAbort = () => {
|
|
2168
|
+
clearTimeout(timer);
|
|
2169
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error('Aborted'));
|
|
2170
|
+
};
|
|
2171
|
+
if (signal) {
|
|
2172
|
+
if (signal.aborted) {
|
|
2173
|
+
clearTimeout(timer);
|
|
2174
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('Aborted'));
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2037
2180
|
}
|
|
2038
2181
|
}
|
|
2039
2182
|
// This should never be reached, but TypeScript needs it for type safety
|
|
@@ -2320,15 +2463,13 @@ const isRetryableLLMError = (error) => {
|
|
|
2320
2463
|
if (error instanceof Error) {
|
|
2321
2464
|
const message = error.message;
|
|
2322
2465
|
// Retry on rate limits (429)
|
|
2323
|
-
if (message.includes(
|
|
2324
|
-
message.includes("rate limit") ||
|
|
2325
|
-
message.includes("Rate limit")) {
|
|
2466
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
2326
2467
|
return true;
|
|
2327
2468
|
}
|
|
2328
2469
|
// Retry on transient body-corruption 400s. Match the exact OpenAI error
|
|
2329
2470
|
// string to avoid retrying genuine client-side validation 400s (which
|
|
2330
2471
|
// would re-fail forever and waste retry budget).
|
|
2331
|
-
if (message.includes(
|
|
2472
|
+
if (message.includes('could not parse the JSON body of your request')) {
|
|
2332
2473
|
return true;
|
|
2333
2474
|
}
|
|
2334
2475
|
}
|
|
@@ -2397,26 +2538,23 @@ function isReasoningModel(model) {
|
|
|
2397
2538
|
* @throws Error if the response format is invalid or incompatible.
|
|
2398
2539
|
*/
|
|
2399
2540
|
function getResponseFormatOption(responseFormat, normalizedModel) {
|
|
2400
|
-
if (responseFormat ===
|
|
2401
|
-
return { type:
|
|
2541
|
+
if (responseFormat === 'text') {
|
|
2542
|
+
return { type: 'text' };
|
|
2402
2543
|
}
|
|
2403
|
-
if (responseFormat ===
|
|
2404
|
-
return supportsJsonMode(normalizedModel)
|
|
2405
|
-
? { type: "json_object" }
|
|
2406
|
-
: { type: "text" };
|
|
2544
|
+
if (responseFormat === 'json') {
|
|
2545
|
+
return supportsJsonMode(normalizedModel) ? { type: 'json_object' } : { type: 'text' };
|
|
2407
2546
|
}
|
|
2408
|
-
if (typeof responseFormat ===
|
|
2409
|
-
responseFormat.type === "json_schema") {
|
|
2547
|
+
if (typeof responseFormat === 'object' && responseFormat.type === 'json_schema') {
|
|
2410
2548
|
if (!isStructuredOutputCompatibleModel(normalizedModel)) {
|
|
2411
2549
|
throw new Error(`${normalizedModel} is not compatible with structured outputs / json object.`);
|
|
2412
2550
|
}
|
|
2413
2551
|
return {
|
|
2414
|
-
type:
|
|
2552
|
+
type: 'json_schema',
|
|
2415
2553
|
json_schema: {
|
|
2416
|
-
type:
|
|
2554
|
+
type: 'object',
|
|
2417
2555
|
properties: responseFormat.schema.properties,
|
|
2418
2556
|
required: responseFormat.schema.required || [],
|
|
2419
|
-
name:
|
|
2557
|
+
name: 'Response',
|
|
2420
2558
|
},
|
|
2421
2559
|
};
|
|
2422
2560
|
}
|
|
@@ -2475,7 +2613,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2475
2613
|
const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
|
|
2476
2614
|
const apiKey = options.apiKey || getSecrets().openai.apiKey;
|
|
2477
2615
|
if (!apiKey) {
|
|
2478
|
-
throw new Error(
|
|
2616
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
2479
2617
|
}
|
|
2480
2618
|
const openai = new OpenAI({
|
|
2481
2619
|
apiKey: apiKey,
|
|
@@ -2485,7 +2623,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2485
2623
|
// Add developer prompt if present
|
|
2486
2624
|
if (options.developerPrompt && supportsDeveloperPrompt(normalizedModel)) {
|
|
2487
2625
|
messages.push({
|
|
2488
|
-
role:
|
|
2626
|
+
role: 'developer',
|
|
2489
2627
|
content: options.developerPrompt,
|
|
2490
2628
|
});
|
|
2491
2629
|
}
|
|
@@ -2494,15 +2632,15 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2494
2632
|
messages.push(...options.context);
|
|
2495
2633
|
}
|
|
2496
2634
|
// Add user content
|
|
2497
|
-
if (typeof content ===
|
|
2635
|
+
if (typeof content === 'string') {
|
|
2498
2636
|
messages.push({
|
|
2499
|
-
role:
|
|
2637
|
+
role: 'user',
|
|
2500
2638
|
content,
|
|
2501
2639
|
});
|
|
2502
2640
|
}
|
|
2503
2641
|
else {
|
|
2504
2642
|
messages.push({
|
|
2505
|
-
role:
|
|
2643
|
+
role: 'user',
|
|
2506
2644
|
content,
|
|
2507
2645
|
});
|
|
2508
2646
|
}
|
|
@@ -2516,8 +2654,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2516
2654
|
queryOptions.tools = options.tools;
|
|
2517
2655
|
}
|
|
2518
2656
|
// Only include temperature if the model supports it
|
|
2519
|
-
if (options.temperature !== undefined &&
|
|
2520
|
-
supportsTemperature(normalizedModel)) {
|
|
2657
|
+
if (options.temperature !== undefined && supportsTemperature(normalizedModel)) {
|
|
2521
2658
|
queryOptions.temperature = options.temperature;
|
|
2522
2659
|
}
|
|
2523
2660
|
// Only include max_completion_tokens when specified
|
|
@@ -2527,16 +2664,25 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2527
2664
|
// Only set response_format when it's not the default 'text' type
|
|
2528
2665
|
// (sending response_format: {type: 'text'} is redundant and may cause issues)
|
|
2529
2666
|
const responseFormatOption = getResponseFormatOption(responseFormat, normalizedModel);
|
|
2530
|
-
if (responseFormatOption.type !==
|
|
2667
|
+
if (responseFormatOption.type !== 'text') {
|
|
2531
2668
|
queryOptions.response_format = responseFormatOption;
|
|
2532
2669
|
}
|
|
2533
2670
|
let completion;
|
|
2534
2671
|
try {
|
|
2535
|
-
completion = await withRetry(
|
|
2672
|
+
completion = await withRetry(
|
|
2673
|
+
// Pass the AbortSignal into the OpenAI SDK request-options object.
|
|
2674
|
+
// The OpenAI SDK honours `signal` to cancel the in-flight fetch
|
|
2675
|
+
// immediately when it fires — this is what makes caller-driven
|
|
2676
|
+
// cancel-and-restart (e.g. signal-monitoring's materiality
|
|
2677
|
+
// re-trigger path in the engine) actually release the HTTP
|
|
2678
|
+
// connection and response-body buffer rather than just discarding
|
|
2679
|
+
// the resolved value at the engine boundary.
|
|
2680
|
+
() => openai.chat.completions.create(queryOptions, options.signal ? { signal: options.signal } : undefined), {
|
|
2536
2681
|
maxRetries: 3,
|
|
2537
2682
|
baseDelayMs: 2000,
|
|
2538
2683
|
maxDelayMs: 30000,
|
|
2539
2684
|
retryableErrors: isRetryableLLMError,
|
|
2685
|
+
signal: options.signal,
|
|
2540
2686
|
}, `OpenAI:${normalizedModel}`);
|
|
2541
2687
|
}
|
|
2542
2688
|
catch (error) {
|
|
@@ -2548,19 +2694,15 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2548
2694
|
// for production prompts containing sensitive context.
|
|
2549
2695
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2550
2696
|
const totalContentChars = messages.reduce((sum, msg) => {
|
|
2551
|
-
if (typeof msg.content ===
|
|
2697
|
+
if (typeof msg.content === 'string')
|
|
2552
2698
|
return sum + msg.content.length;
|
|
2553
2699
|
if (Array.isArray(msg.content)) {
|
|
2554
|
-
return
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
return s + part.text.length;
|
|
2561
|
-
}
|
|
2562
|
-
return s;
|
|
2563
|
-
}, 0));
|
|
2700
|
+
return sum + msg.content.reduce((s, part) => {
|
|
2701
|
+
if (typeof part === 'object' && part !== null && 'text' in part && typeof part.text === 'string') {
|
|
2702
|
+
return s + part.text.length;
|
|
2703
|
+
}
|
|
2704
|
+
return s;
|
|
2705
|
+
}, 0);
|
|
2564
2706
|
}
|
|
2565
2707
|
return sum;
|
|
2566
2708
|
}, 0);
|
|
@@ -2587,7 +2729,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2587
2729
|
const cachedTokens = completion.usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
2588
2730
|
const response = {
|
|
2589
2731
|
id: completion.id,
|
|
2590
|
-
content: completion.choices[0]?.message?.content ||
|
|
2732
|
+
content: completion.choices[0]?.message?.content || '',
|
|
2591
2733
|
tool_calls: completion.choices[0]?.message?.tool_calls,
|
|
2592
2734
|
usage: {
|
|
2593
2735
|
prompt_tokens: completion.usage?.prompt_tokens ?? 0,
|
|
@@ -2597,7 +2739,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2597
2739
|
},
|
|
2598
2740
|
system_fingerprint: completion.system_fingerprint,
|
|
2599
2741
|
service_tier: options.service_tier,
|
|
2600
|
-
provider:
|
|
2742
|
+
provider: 'openai',
|
|
2601
2743
|
model: normalizedModel,
|
|
2602
2744
|
};
|
|
2603
2745
|
return response;
|
|
@@ -2610,7 +2752,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2610
2752
|
* @param options The options for the LLM call. Defaults to an empty object.
|
|
2611
2753
|
* @return A promise that resolves to the response from the LLM.
|
|
2612
2754
|
*/
|
|
2613
|
-
const makeOpenAIChatCompletionCall = async (content, responseFormat =
|
|
2755
|
+
const makeOpenAIChatCompletionCall = async (content, responseFormat = 'text', options = {}) => {
|
|
2614
2756
|
const mergedOptions = {
|
|
2615
2757
|
...DEFAULT_OPTIONS$1,
|
|
2616
2758
|
...options,
|
|
@@ -2619,7 +2761,7 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2619
2761
|
// Track cost in the global cost tracker. Pass cached tokens through so the
|
|
2620
2762
|
// tracker applies the discounted cached-input rate (typically ~50% of the
|
|
2621
2763
|
// standard input rate) instead of billing every input token at full price.
|
|
2622
|
-
getLLMCostTracker().trackUsage(
|
|
2764
|
+
getLLMCostTracker().trackUsage('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens);
|
|
2623
2765
|
// Handle tool calls differently
|
|
2624
2766
|
if (completion.tool_calls && completion.tool_calls.length > 0) {
|
|
2625
2767
|
const toolCallResponse = {
|
|
@@ -2635,10 +2777,10 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2635
2777
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
2636
2778
|
completion_tokens: completion.usage.completion_tokens,
|
|
2637
2779
|
reasoning_tokens: 0,
|
|
2638
|
-
provider:
|
|
2780
|
+
provider: 'openai',
|
|
2639
2781
|
model: completion.model,
|
|
2640
2782
|
cached_tokens: completion.usage.cached_tokens,
|
|
2641
|
-
cost: calculateCost(
|
|
2783
|
+
cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
2642
2784
|
},
|
|
2643
2785
|
tool_calls: completion.tool_calls,
|
|
2644
2786
|
};
|
|
@@ -2646,7 +2788,7 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2646
2788
|
// Handle regular responses
|
|
2647
2789
|
const parsedResponse = await parseResponse(completion.content, responseFormat);
|
|
2648
2790
|
if (parsedResponse === null) {
|
|
2649
|
-
throw new Error(
|
|
2791
|
+
throw new Error('Failed to parse response');
|
|
2650
2792
|
}
|
|
2651
2793
|
return {
|
|
2652
2794
|
response: parsedResponse,
|
|
@@ -2654,10 +2796,10 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2654
2796
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
2655
2797
|
completion_tokens: completion.usage.completion_tokens,
|
|
2656
2798
|
reasoning_tokens: 0,
|
|
2657
|
-
provider:
|
|
2799
|
+
provider: 'openai',
|
|
2658
2800
|
model: completion.model,
|
|
2659
2801
|
cached_tokens: completion.usage.cached_tokens,
|
|
2660
|
-
cost: calculateCost(
|
|
2802
|
+
cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
2661
2803
|
},
|
|
2662
2804
|
tool_calls: completion.tool_calls,
|
|
2663
2805
|
};
|
|
@@ -2692,44 +2834,45 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2692
2834
|
const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
|
|
2693
2835
|
const apiKey = options.apiKey || getSecrets().openai.apiKey;
|
|
2694
2836
|
if (!apiKey) {
|
|
2695
|
-
throw new Error(
|
|
2837
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
2696
2838
|
}
|
|
2697
2839
|
const openai = new OpenAI({
|
|
2698
2840
|
apiKey: apiKey,
|
|
2699
2841
|
timeout: options.timeout ?? LLM_TIMEOUT_MS,
|
|
2700
2842
|
});
|
|
2701
|
-
// Remove apiKey, model, and
|
|
2702
|
-
const { apiKey: _, model: __, timeout: ___, ...cleanOptions } = options;
|
|
2843
|
+
// Remove apiKey, model, timeout, and signal from options before creating request body
|
|
2844
|
+
const { apiKey: _, model: __, timeout: ___, signal: ____, ...cleanOptions } = options;
|
|
2703
2845
|
const requestBody = {
|
|
2704
2846
|
model: normalizedModel,
|
|
2705
2847
|
input,
|
|
2706
2848
|
...cleanOptions,
|
|
2707
2849
|
};
|
|
2708
2850
|
// Make the API call to the Responses endpoint
|
|
2709
|
-
const response = await withRetry(() => openai.responses.create(requestBody), {
|
|
2851
|
+
const response = await withRetry(() => openai.responses.create(requestBody, options.signal ? { signal: options.signal } : undefined), {
|
|
2710
2852
|
maxRetries: 3,
|
|
2711
2853
|
baseDelayMs: 2000,
|
|
2712
2854
|
maxDelayMs: 30000,
|
|
2713
2855
|
retryableErrors: isRetryableLLMError,
|
|
2856
|
+
signal: options.signal,
|
|
2714
2857
|
}, `OpenAI-Responses:${normalizedModel}`);
|
|
2715
2858
|
// Responses API exposes cached input tokens under `input_tokens_details.cached_tokens`
|
|
2716
2859
|
// (the equivalent of Chat Completions' `prompt_tokens_details.cached_tokens`).
|
|
2717
2860
|
const responsesCachedTokens = response.usage?.input_tokens_details?.cached_tokens || 0;
|
|
2718
2861
|
// Track cost in the global cost tracker
|
|
2719
|
-
getLLMCostTracker().trackUsage(
|
|
2862
|
+
getLLMCostTracker().trackUsage('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens);
|
|
2720
2863
|
// Extract tool calls from the output
|
|
2721
2864
|
const toolCalls = response.output
|
|
2722
|
-
?.filter((item) => item.type ===
|
|
2865
|
+
?.filter((item) => item.type === 'function_call')
|
|
2723
2866
|
.map((toolCall) => ({
|
|
2724
2867
|
id: toolCall.call_id,
|
|
2725
|
-
type:
|
|
2868
|
+
type: 'function',
|
|
2726
2869
|
function: {
|
|
2727
2870
|
name: toolCall.name,
|
|
2728
2871
|
arguments: toolCall.arguments,
|
|
2729
2872
|
},
|
|
2730
2873
|
}));
|
|
2731
2874
|
// Extract code interpreter outputs if present
|
|
2732
|
-
const codeInterpreterCalls = response.output?.filter((item) => item.type ===
|
|
2875
|
+
const codeInterpreterCalls = response.output?.filter((item) => item.type === 'code_interpreter_call');
|
|
2733
2876
|
let codeInterpreterOutputs = undefined;
|
|
2734
2877
|
if (codeInterpreterCalls && codeInterpreterCalls.length > 0) {
|
|
2735
2878
|
// Each code_interpreter_call has an 'outputs' array property
|
|
@@ -2755,34 +2898,32 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2755
2898
|
prompt_tokens: response.usage?.input_tokens || 0,
|
|
2756
2899
|
completion_tokens: response.usage?.output_tokens || 0,
|
|
2757
2900
|
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
2758
|
-
provider:
|
|
2901
|
+
provider: 'openai',
|
|
2759
2902
|
model: normalizedModel,
|
|
2760
2903
|
cached_tokens: responsesCachedTokens,
|
|
2761
|
-
cost: calculateCost(
|
|
2904
|
+
cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens),
|
|
2762
2905
|
},
|
|
2763
2906
|
tool_calls: toolCalls,
|
|
2764
|
-
...(codeInterpreterOutputs
|
|
2765
|
-
? { code_interpreter_outputs: codeInterpreterOutputs }
|
|
2766
|
-
: {}),
|
|
2907
|
+
...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
|
|
2767
2908
|
};
|
|
2768
2909
|
}
|
|
2769
2910
|
// Extract text content from the response output
|
|
2770
2911
|
const textContent = response.output
|
|
2771
|
-
?.filter((item) => item.type ===
|
|
2912
|
+
?.filter((item) => item.type === 'message')
|
|
2772
2913
|
.map((item) => item.content
|
|
2773
|
-
.filter((content) => content.type ===
|
|
2914
|
+
.filter((content) => content.type === 'output_text')
|
|
2774
2915
|
.map((content) => content.text)
|
|
2775
|
-
.join(
|
|
2776
|
-
.join(
|
|
2916
|
+
.join(''))
|
|
2917
|
+
.join('') || '';
|
|
2777
2918
|
// Determine the format for parsing the response
|
|
2778
|
-
let parsingFormat =
|
|
2779
|
-
if (requestBody.text?.format?.type ===
|
|
2780
|
-
parsingFormat =
|
|
2919
|
+
let parsingFormat = 'text';
|
|
2920
|
+
if (requestBody.text?.format?.type === 'json_object') {
|
|
2921
|
+
parsingFormat = 'json';
|
|
2781
2922
|
}
|
|
2782
2923
|
// Handle regular responses
|
|
2783
2924
|
const parsedResponse = await parseResponse(textContent, parsingFormat);
|
|
2784
2925
|
if (parsedResponse === null) {
|
|
2785
|
-
throw new Error(
|
|
2926
|
+
throw new Error('Failed to parse response from Responses API');
|
|
2786
2927
|
}
|
|
2787
2928
|
return {
|
|
2788
2929
|
response: parsedResponse,
|
|
@@ -2790,15 +2931,13 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2790
2931
|
prompt_tokens: response.usage?.input_tokens || 0,
|
|
2791
2932
|
completion_tokens: response.usage?.output_tokens || 0,
|
|
2792
2933
|
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
2793
|
-
provider:
|
|
2934
|
+
provider: 'openai',
|
|
2794
2935
|
model: normalizedModel,
|
|
2795
2936
|
cached_tokens: responsesCachedTokens,
|
|
2796
|
-
cost: calculateCost(
|
|
2937
|
+
cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens),
|
|
2797
2938
|
},
|
|
2798
2939
|
tool_calls: toolCalls,
|
|
2799
|
-
...(codeInterpreterOutputs
|
|
2800
|
-
? { code_interpreter_outputs: codeInterpreterOutputs }
|
|
2801
|
-
: {}),
|
|
2940
|
+
...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
|
|
2802
2941
|
};
|
|
2803
2942
|
};
|
|
2804
2943
|
|
|
@@ -8108,12 +8247,12 @@ function sanitizeObject(obj, sensitiveFields = ['apiKey', 'token', 'secret', 'pa
|
|
|
8108
8247
|
const isRetryableAnthropicError = (error) => {
|
|
8109
8248
|
if (error instanceof Error) {
|
|
8110
8249
|
const message = error.message;
|
|
8111
|
-
if (message.includes(
|
|
8112
|
-
message.includes(
|
|
8113
|
-
message.includes(
|
|
8114
|
-
message.includes(
|
|
8115
|
-
message.includes(
|
|
8116
|
-
message.includes(
|
|
8250
|
+
if (message.includes('429') ||
|
|
8251
|
+
message.includes('500') ||
|
|
8252
|
+
message.includes('503') ||
|
|
8253
|
+
message.includes('529') ||
|
|
8254
|
+
message.includes('rate limit') ||
|
|
8255
|
+
message.includes('overloaded')) {
|
|
8117
8256
|
return true;
|
|
8118
8257
|
}
|
|
8119
8258
|
}
|
|
@@ -8128,11 +8267,8 @@ const isRetryableAnthropicError = (error) => {
|
|
|
8128
8267
|
function translateToolsToAnthropic(tools) {
|
|
8129
8268
|
return tools.map((tool) => ({
|
|
8130
8269
|
name: tool.function.name,
|
|
8131
|
-
description: tool.function.description ||
|
|
8132
|
-
input_schema: (tool.function.parameters || {
|
|
8133
|
-
type: "object",
|
|
8134
|
-
properties: {},
|
|
8135
|
-
}),
|
|
8270
|
+
description: tool.function.description || '',
|
|
8271
|
+
input_schema: (tool.function.parameters || { type: 'object', properties: {} }),
|
|
8136
8272
|
}));
|
|
8137
8273
|
}
|
|
8138
8274
|
/**
|
|
@@ -8145,31 +8281,27 @@ function translateContextToAnthropic(context) {
|
|
|
8145
8281
|
const systemParts = [];
|
|
8146
8282
|
const messages = [];
|
|
8147
8283
|
for (const msg of context) {
|
|
8148
|
-
if (msg.role ===
|
|
8149
|
-
if (typeof msg.content ===
|
|
8284
|
+
if (msg.role === 'system' || msg.role === 'developer') {
|
|
8285
|
+
if (typeof msg.content === 'string') {
|
|
8150
8286
|
systemParts.push(msg.content);
|
|
8151
8287
|
}
|
|
8152
8288
|
continue;
|
|
8153
8289
|
}
|
|
8154
|
-
if (msg.role ===
|
|
8155
|
-
const content = typeof msg.content ===
|
|
8290
|
+
if (msg.role === 'user') {
|
|
8291
|
+
const content = typeof msg.content === 'string'
|
|
8156
8292
|
? msg.content
|
|
8157
8293
|
: JSON.stringify(msg.content);
|
|
8158
|
-
messages.push({ role:
|
|
8294
|
+
messages.push({ role: 'user', content });
|
|
8159
8295
|
continue;
|
|
8160
8296
|
}
|
|
8161
|
-
if (msg.role ===
|
|
8297
|
+
if (msg.role === 'assistant') {
|
|
8162
8298
|
const assistantMsg = msg;
|
|
8163
8299
|
// If the assistant message has tool_calls, translate to Anthropic tool_use blocks
|
|
8164
8300
|
if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
|
|
8165
8301
|
const contentBlocks = [];
|
|
8166
8302
|
// Include text content if present
|
|
8167
|
-
if (typeof assistantMsg.content ===
|
|
8168
|
-
assistantMsg.content
|
|
8169
|
-
contentBlocks.push({
|
|
8170
|
-
type: "text",
|
|
8171
|
-
text: assistantMsg.content,
|
|
8172
|
-
});
|
|
8303
|
+
if (typeof assistantMsg.content === 'string' && assistantMsg.content.trim()) {
|
|
8304
|
+
contentBlocks.push({ type: 'text', text: assistantMsg.content });
|
|
8173
8305
|
}
|
|
8174
8306
|
// Translate each tool_call to a tool_use block
|
|
8175
8307
|
for (const tc of assistantMsg.tool_calls) {
|
|
@@ -8181,36 +8313,32 @@ function translateContextToAnthropic(context) {
|
|
|
8181
8313
|
input = { raw: tc.function.arguments };
|
|
8182
8314
|
}
|
|
8183
8315
|
contentBlocks.push({
|
|
8184
|
-
type:
|
|
8316
|
+
type: 'tool_use',
|
|
8185
8317
|
id: tc.id,
|
|
8186
8318
|
name: tc.function.name,
|
|
8187
8319
|
input,
|
|
8188
8320
|
});
|
|
8189
8321
|
}
|
|
8190
|
-
messages.push({ role:
|
|
8322
|
+
messages.push({ role: 'assistant', content: contentBlocks });
|
|
8191
8323
|
}
|
|
8192
8324
|
else {
|
|
8193
|
-
const content = typeof assistantMsg.content ===
|
|
8325
|
+
const content = typeof assistantMsg.content === 'string'
|
|
8194
8326
|
? assistantMsg.content
|
|
8195
8327
|
: JSON.stringify(assistantMsg.content);
|
|
8196
|
-
messages.push({ role:
|
|
8328
|
+
messages.push({ role: 'assistant', content });
|
|
8197
8329
|
}
|
|
8198
8330
|
continue;
|
|
8199
8331
|
}
|
|
8200
|
-
if (msg.role ===
|
|
8332
|
+
if (msg.role === 'tool') {
|
|
8201
8333
|
// Anthropic expects tool results as user messages with tool_result content blocks
|
|
8202
8334
|
const toolMsg = msg;
|
|
8203
8335
|
messages.push({
|
|
8204
|
-
role:
|
|
8205
|
-
content: [
|
|
8206
|
-
|
|
8207
|
-
type: "tool_result",
|
|
8336
|
+
role: 'user',
|
|
8337
|
+
content: [{
|
|
8338
|
+
type: 'tool_result',
|
|
8208
8339
|
tool_use_id: toolMsg.tool_call_id,
|
|
8209
|
-
content: typeof toolMsg.content ===
|
|
8210
|
-
|
|
8211
|
-
: JSON.stringify(toolMsg.content),
|
|
8212
|
-
},
|
|
8213
|
-
],
|
|
8340
|
+
content: typeof toolMsg.content === 'string' ? toolMsg.content : JSON.stringify(toolMsg.content),
|
|
8341
|
+
}],
|
|
8214
8342
|
});
|
|
8215
8343
|
continue;
|
|
8216
8344
|
}
|
|
@@ -8231,13 +8359,13 @@ function translateContextToAnthropic(context) {
|
|
|
8231
8359
|
merged.push({ role: msg.role, content: toContentBlocks(msg.content) });
|
|
8232
8360
|
}
|
|
8233
8361
|
}
|
|
8234
|
-
return { messages: merged, systemText: systemParts.join(
|
|
8362
|
+
return { messages: merged, systemText: systemParts.join('\n\n') };
|
|
8235
8363
|
}
|
|
8236
8364
|
/** Convert string or content block array to a uniform content block array. */
|
|
8237
8365
|
function toContentBlocks(content) {
|
|
8238
|
-
if (typeof content ===
|
|
8366
|
+
if (typeof content === 'string') {
|
|
8239
8367
|
const textBlock = {
|
|
8240
|
-
type:
|
|
8368
|
+
type: 'text',
|
|
8241
8369
|
text: content,
|
|
8242
8370
|
citations: null,
|
|
8243
8371
|
};
|
|
@@ -8257,11 +8385,11 @@ function toContentBlocks(content) {
|
|
|
8257
8385
|
* @param options The options for the LLM call.
|
|
8258
8386
|
* @return A promise that resolves to the response from the Anthropic API.
|
|
8259
8387
|
*/
|
|
8260
|
-
async function makeAnthropicCall(content, responseFormat =
|
|
8261
|
-
const model = (options.model ||
|
|
8388
|
+
async function makeAnthropicCall(content, responseFormat = 'text', options = {}) {
|
|
8389
|
+
const model = (options.model || 'claude-sonnet-4-6');
|
|
8262
8390
|
const apiKey = options.apiKey || getSecrets().anthropic.apiKey;
|
|
8263
8391
|
if (!apiKey) {
|
|
8264
|
-
throw new Error(
|
|
8392
|
+
throw new Error('Anthropic API key is not provided and ANTHROPIC_API_KEY environment variable is not set');
|
|
8265
8393
|
}
|
|
8266
8394
|
const client = new Anthropic({
|
|
8267
8395
|
apiKey,
|
|
@@ -8273,10 +8401,8 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8273
8401
|
systemParts.push(options.developerPrompt);
|
|
8274
8402
|
}
|
|
8275
8403
|
// If JSON response is requested, instruct the model via system prompt
|
|
8276
|
-
if (responseFormat ===
|
|
8277
|
-
(
|
|
8278
|
-
responseFormat.type === "json_schema")) {
|
|
8279
|
-
systemParts.push("You MUST respond with valid JSON only. No markdown, no code blocks, no explanatory text — just the raw JSON object or array.");
|
|
8404
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) {
|
|
8405
|
+
systemParts.push('You MUST respond with valid JSON only. No markdown, no code blocks, no explanatory text — just the raw JSON object or array.');
|
|
8280
8406
|
}
|
|
8281
8407
|
// Build messages array
|
|
8282
8408
|
const messages = [];
|
|
@@ -8289,7 +8415,7 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8289
8415
|
messages.push(...translated.messages);
|
|
8290
8416
|
}
|
|
8291
8417
|
// Add user content
|
|
8292
|
-
messages.push({ role:
|
|
8418
|
+
messages.push({ role: 'user', content });
|
|
8293
8419
|
// Build request params
|
|
8294
8420
|
const requestParams = {
|
|
8295
8421
|
model,
|
|
@@ -8298,37 +8424,46 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8298
8424
|
};
|
|
8299
8425
|
// Add system prompt if present
|
|
8300
8426
|
if (systemParts.length > 0) {
|
|
8301
|
-
requestParams.system = systemParts.join(
|
|
8302
|
-
}
|
|
8303
|
-
//
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8427
|
+
requestParams.system = systemParts.join('\n\n');
|
|
8428
|
+
}
|
|
8429
|
+
// Sampling controls are intentionally disabled for the Anthropic provider.
|
|
8430
|
+
//
|
|
8431
|
+
// The claude-*-4.x model family rejects any request that specifies BOTH
|
|
8432
|
+
// `temperature` and `top_p` ("`temperature` and `top_p` cannot both be
|
|
8433
|
+
// specified for this model. Please use only one." — HTTP 400), which aborts
|
|
8434
|
+
// the entire caller flow (e.g. the engine's account decision session).
|
|
8435
|
+
// Callers across the org pass both knobs generically, so rather than make
|
|
8436
|
+
// each one provider-aware we drop both here and let Anthropic use its model
|
|
8437
|
+
// defaults. `options.temperature` / `options.top_p` are intentionally not
|
|
8438
|
+
// forwarded. This is the Anthropic adapter only — the OpenAI, DeepSeek, and
|
|
8439
|
+
// OpenAI-compatible adapters still honor `temperature` and `top_p`.
|
|
8310
8440
|
// Translate and add tools if present
|
|
8311
8441
|
if (options.tools && options.tools.length > 0) {
|
|
8312
8442
|
requestParams.tools = translateToolsToAnthropic(options.tools);
|
|
8313
8443
|
}
|
|
8314
8444
|
try {
|
|
8315
|
-
const response = await withRetry(
|
|
8445
|
+
const response = await withRetry(
|
|
8446
|
+
// Pass AbortSignal into the Anthropic SDK request-options object
|
|
8447
|
+
// so caller-driven cancellation interrupts the in-flight HTTP
|
|
8448
|
+
// request immediately. See LLMOptions.signal JSDoc for context.
|
|
8449
|
+
() => client.messages.create(requestParams, options.signal ? { signal: options.signal } : undefined), {
|
|
8316
8450
|
maxRetries: 3,
|
|
8317
8451
|
baseDelayMs: 2000,
|
|
8318
8452
|
maxDelayMs: 30000,
|
|
8319
8453
|
retryableErrors: isRetryableAnthropicError,
|
|
8454
|
+
signal: options.signal,
|
|
8320
8455
|
}, `Anthropic:${model}`);
|
|
8321
8456
|
const inputTokens = response.usage.input_tokens;
|
|
8322
8457
|
const outputTokens = response.usage.output_tokens;
|
|
8323
8458
|
// Track cost
|
|
8324
|
-
getLLMCostTracker().trackUsage(
|
|
8459
|
+
getLLMCostTracker().trackUsage('anthropic', model, inputTokens, outputTokens);
|
|
8325
8460
|
// Extract tool use blocks
|
|
8326
|
-
const toolUseBlocks = response.content.filter((block) => block.type ===
|
|
8461
|
+
const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
|
|
8327
8462
|
if (toolUseBlocks.length > 0) {
|
|
8328
8463
|
// Translate Anthropic tool_use to OpenAI tool_calls format
|
|
8329
8464
|
const toolCalls = toolUseBlocks.map((block) => ({
|
|
8330
8465
|
id: block.id,
|
|
8331
|
-
type:
|
|
8466
|
+
type: 'function',
|
|
8332
8467
|
function: {
|
|
8333
8468
|
name: block.name,
|
|
8334
8469
|
arguments: JSON.stringify(block.input),
|
|
@@ -8347,22 +8482,22 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8347
8482
|
prompt_tokens: inputTokens,
|
|
8348
8483
|
completion_tokens: outputTokens,
|
|
8349
8484
|
reasoning_tokens: 0,
|
|
8350
|
-
provider:
|
|
8485
|
+
provider: 'anthropic',
|
|
8351
8486
|
model,
|
|
8352
|
-
cost: calculateCost(
|
|
8487
|
+
cost: calculateCost('anthropic', model, inputTokens, outputTokens),
|
|
8353
8488
|
},
|
|
8354
8489
|
tool_calls: toolCalls,
|
|
8355
8490
|
};
|
|
8356
8491
|
}
|
|
8357
8492
|
// Extract text content
|
|
8358
8493
|
const textContent = response.content
|
|
8359
|
-
.filter((block) => block.type ===
|
|
8494
|
+
.filter((block) => block.type === 'text')
|
|
8360
8495
|
.map((block) => block.text)
|
|
8361
|
-
.join(
|
|
8496
|
+
.join('');
|
|
8362
8497
|
// Parse response
|
|
8363
8498
|
const parsedResponse = await parseResponse(textContent, responseFormat);
|
|
8364
8499
|
if (parsedResponse === null) {
|
|
8365
|
-
throw new Error(
|
|
8500
|
+
throw new Error('Failed to parse Anthropic response');
|
|
8366
8501
|
}
|
|
8367
8502
|
return {
|
|
8368
8503
|
response: parsedResponse,
|
|
@@ -8370,9 +8505,9 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8370
8505
|
prompt_tokens: inputTokens,
|
|
8371
8506
|
completion_tokens: outputTokens,
|
|
8372
8507
|
reasoning_tokens: 0,
|
|
8373
|
-
provider:
|
|
8508
|
+
provider: 'anthropic',
|
|
8374
8509
|
model,
|
|
8375
|
-
cost: calculateCost(
|
|
8510
|
+
cost: calculateCost('anthropic', model, inputTokens, outputTokens),
|
|
8376
8511
|
},
|
|
8377
8512
|
};
|
|
8378
8513
|
}
|
|
@@ -8394,9 +8529,7 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8394
8529
|
const isRetryableError = (error) => {
|
|
8395
8530
|
if (error instanceof Error) {
|
|
8396
8531
|
const message = error.message;
|
|
8397
|
-
if (message.includes(
|
|
8398
|
-
message.includes("rate limit") ||
|
|
8399
|
-
message.includes("Rate limit")) {
|
|
8532
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
8400
8533
|
return true;
|
|
8401
8534
|
}
|
|
8402
8535
|
}
|
|
@@ -8410,16 +8543,14 @@ function resolveApiKey(provider, optionsApiKey) {
|
|
|
8410
8543
|
if (optionsApiKey)
|
|
8411
8544
|
return optionsApiKey;
|
|
8412
8545
|
const secrets = getSecrets();
|
|
8413
|
-
const pathParts = provider.apiKeySecretPath.split(
|
|
8546
|
+
const pathParts = provider.apiKeySecretPath.split('.');
|
|
8414
8547
|
let current = secrets;
|
|
8415
8548
|
for (const part of pathParts) {
|
|
8416
|
-
if (current === null ||
|
|
8417
|
-
|
|
8418
|
-
typeof current !== "object")
|
|
8419
|
-
return "";
|
|
8549
|
+
if (current === null || current === undefined || typeof current !== 'object')
|
|
8550
|
+
return '';
|
|
8420
8551
|
current = current[part];
|
|
8421
8552
|
}
|
|
8422
|
-
return (typeof current ===
|
|
8553
|
+
return (typeof current === 'string' ? current : '') || '';
|
|
8423
8554
|
}
|
|
8424
8555
|
/**
|
|
8425
8556
|
* Makes a call to an OpenAI-compatible provider (Kimi, Qwen, or any future provider).
|
|
@@ -8433,12 +8564,12 @@ function resolveApiKey(provider, optionsApiKey) {
|
|
|
8433
8564
|
* @param providerName The provider key in OPENAI_COMPATIBLE_PROVIDERS.
|
|
8434
8565
|
* @return A promise that resolves to the response from the provider.
|
|
8435
8566
|
*/
|
|
8436
|
-
async function makeOpenAICompatibleCall(content, responseFormat =
|
|
8567
|
+
async function makeOpenAICompatibleCall(content, responseFormat = 'text', options = {}, providerName) {
|
|
8437
8568
|
const providerConfig = OPENAI_COMPATIBLE_PROVIDERS[providerName];
|
|
8438
8569
|
if (!providerConfig) {
|
|
8439
8570
|
throw new Error(`Unknown OpenAI-compatible provider: ${providerName}`);
|
|
8440
8571
|
}
|
|
8441
|
-
const model = normalizeModelName(options.model ||
|
|
8572
|
+
const model = normalizeModelName(options.model || '');
|
|
8442
8573
|
const apiKey = resolveApiKey(providerConfig, options.apiKey);
|
|
8443
8574
|
if (!apiKey) {
|
|
8444
8575
|
throw new Error(`${providerConfig.name} API key is not provided and ${providerConfig.apiKeyEnvVar} environment variable is not set`);
|
|
@@ -8451,36 +8582,31 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8451
8582
|
const messages = [];
|
|
8452
8583
|
// Add system message with developer prompt if present
|
|
8453
8584
|
if (options.developerPrompt) {
|
|
8454
|
-
messages.push({ role:
|
|
8585
|
+
messages.push({ role: 'system', content: options.developerPrompt });
|
|
8455
8586
|
}
|
|
8456
8587
|
// Add context if present
|
|
8457
8588
|
if (options.context) {
|
|
8458
8589
|
messages.push(...options.context);
|
|
8459
8590
|
}
|
|
8460
8591
|
// Add user content
|
|
8461
|
-
if (typeof content ===
|
|
8462
|
-
messages.push({ role:
|
|
8592
|
+
if (typeof content === 'string') {
|
|
8593
|
+
messages.push({ role: 'user', content });
|
|
8463
8594
|
}
|
|
8464
8595
|
else {
|
|
8465
|
-
messages.push({ role:
|
|
8596
|
+
messages.push({ role: 'user', content });
|
|
8466
8597
|
}
|
|
8467
8598
|
// Determine if the model supports JSON mode
|
|
8468
8599
|
const supportsJson = isValidModel(model) && getModelCapabilities(model).supportsJson;
|
|
8469
8600
|
// If JSON response format, add hint to system prompt
|
|
8470
|
-
if ((responseFormat ===
|
|
8471
|
-
(typeof responseFormat === "object" &&
|
|
8472
|
-
responseFormat.type === "json_schema")) &&
|
|
8601
|
+
if ((responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) &&
|
|
8473
8602
|
supportsJson) {
|
|
8474
|
-
if (!messages.some((m) => m.role ===
|
|
8475
|
-
messages.unshift({
|
|
8476
|
-
role: "system",
|
|
8477
|
-
content: "Please respond in valid JSON format.",
|
|
8478
|
-
});
|
|
8603
|
+
if (!messages.some((m) => m.role === 'system')) {
|
|
8604
|
+
messages.unshift({ role: 'system', content: 'Please respond in valid JSON format.' });
|
|
8479
8605
|
}
|
|
8480
8606
|
else {
|
|
8481
|
-
const systemMsgIndex = messages.findIndex((m) => m.role ===
|
|
8607
|
+
const systemMsgIndex = messages.findIndex((m) => m.role === 'system');
|
|
8482
8608
|
const systemMsg = messages[systemMsgIndex];
|
|
8483
|
-
if (typeof systemMsg.content ===
|
|
8609
|
+
if (typeof systemMsg.content === 'string') {
|
|
8484
8610
|
messages[systemMsgIndex] = {
|
|
8485
8611
|
...systemMsg,
|
|
8486
8612
|
content: `${systemMsg.content} Please respond in valid JSON format.`,
|
|
@@ -8493,8 +8619,8 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8493
8619
|
messages,
|
|
8494
8620
|
};
|
|
8495
8621
|
// Add response format if JSON is supported
|
|
8496
|
-
if (responseFormat !==
|
|
8497
|
-
queryOptions.response_format = { type:
|
|
8622
|
+
if (responseFormat !== 'text' && supportsJson) {
|
|
8623
|
+
queryOptions.response_format = { type: 'json_object' };
|
|
8498
8624
|
}
|
|
8499
8625
|
// Temperature
|
|
8500
8626
|
if (options.temperature !== undefined) {
|
|
@@ -8518,11 +8644,16 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8518
8644
|
queryOptions.tools = options.tools;
|
|
8519
8645
|
}
|
|
8520
8646
|
try {
|
|
8521
|
-
const completion = await withRetry(
|
|
8647
|
+
const completion = await withRetry(
|
|
8648
|
+
// Pass AbortSignal into the SDK request-options object so
|
|
8649
|
+
// caller-driven cancellation interrupts the in-flight HTTP
|
|
8650
|
+
// request immediately. See LLMOptions.signal JSDoc for context.
|
|
8651
|
+
() => openai.chat.completions.create(queryOptions, options.signal ? { signal: options.signal } : undefined), {
|
|
8522
8652
|
maxRetries: 3,
|
|
8523
8653
|
baseDelayMs: 2000,
|
|
8524
8654
|
maxDelayMs: 30000,
|
|
8525
8655
|
retryableErrors: isRetryableError,
|
|
8656
|
+
signal: options.signal,
|
|
8526
8657
|
}, `${providerConfig.name}:${model}`);
|
|
8527
8658
|
const promptTokens = completion.usage?.prompt_tokens || 0;
|
|
8528
8659
|
const completionTokens = completion.usage?.completion_tokens || 0;
|
|
@@ -8552,7 +8683,7 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8552
8683
|
};
|
|
8553
8684
|
}
|
|
8554
8685
|
// Handle regular responses
|
|
8555
|
-
const textContent = completion.choices[0]?.message?.content ||
|
|
8686
|
+
const textContent = completion.choices[0]?.message?.content || '';
|
|
8556
8687
|
const parsedResponse = await parseResponse(textContent, responseFormat);
|
|
8557
8688
|
if (parsedResponse === null) {
|
|
8558
8689
|
throw new Error(`Failed to parse ${providerConfig.name} response`);
|
|
@@ -8844,9 +8975,7 @@ const isRetryableDeepseekError = (error) => {
|
|
|
8844
8975
|
if (error instanceof Error) {
|
|
8845
8976
|
const message = error.message;
|
|
8846
8977
|
// Retry only on rate limits (429)
|
|
8847
|
-
if (message.includes(
|
|
8848
|
-
message.includes("rate limit") ||
|
|
8849
|
-
message.includes("Rate limit")) {
|
|
8978
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
8850
8979
|
return true;
|
|
8851
8980
|
}
|
|
8852
8981
|
}
|
|
@@ -8856,7 +8985,7 @@ const isRetryableDeepseekError = (error) => {
|
|
|
8856
8985
|
* Default options for Deepseek API calls
|
|
8857
8986
|
*/
|
|
8858
8987
|
const DEFAULT_DEEPSEEK_OPTIONS = {
|
|
8859
|
-
defaultModel:
|
|
8988
|
+
defaultModel: 'deepseek-chat',
|
|
8860
8989
|
};
|
|
8861
8990
|
/**
|
|
8862
8991
|
* Checks if the given model is a supported Deepseek model
|
|
@@ -8868,7 +8997,7 @@ const isSupportedDeepseekModel = (model) => {
|
|
|
8868
8997
|
return false;
|
|
8869
8998
|
}
|
|
8870
8999
|
const capabilities = getModelCapabilities(model);
|
|
8871
|
-
return capabilities.provider ===
|
|
9000
|
+
return capabilities.provider === 'deepseek';
|
|
8872
9001
|
};
|
|
8873
9002
|
/**
|
|
8874
9003
|
* Checks if the given Deepseek model supports JSON output format
|
|
@@ -8901,19 +9030,17 @@ const supportsToolCalling = (model) => {
|
|
|
8901
9030
|
*/
|
|
8902
9031
|
function getDeepseekResponseFormatOption(responseFormat, model) {
|
|
8903
9032
|
// Check if the model supports JSON output
|
|
8904
|
-
if (responseFormat !==
|
|
9033
|
+
if (responseFormat !== 'text' && !supportsJsonOutput(model)) {
|
|
8905
9034
|
getLumicLogger().warn(`Model ${model} does not support JSON output. Using text format instead.`);
|
|
8906
|
-
return { type:
|
|
9035
|
+
return { type: 'text' };
|
|
8907
9036
|
}
|
|
8908
|
-
if (responseFormat ===
|
|
8909
|
-
return { type:
|
|
9037
|
+
if (responseFormat === 'text') {
|
|
9038
|
+
return { type: 'text' };
|
|
8910
9039
|
}
|
|
8911
|
-
if (responseFormat ===
|
|
8912
|
-
|
|
8913
|
-
responseFormat.type === "json_schema")) {
|
|
8914
|
-
return { type: "json_object" };
|
|
9040
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) {
|
|
9041
|
+
return { type: 'json_object' };
|
|
8915
9042
|
}
|
|
8916
|
-
return { type:
|
|
9043
|
+
return { type: 'text' };
|
|
8917
9044
|
}
|
|
8918
9045
|
/**
|
|
8919
9046
|
* Creates a completion using the Deepseek API
|
|
@@ -8932,26 +9059,24 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
8932
9059
|
throw new Error(`Unsupported Deepseek model: ${normalizedModel}. Please use 'deepseek-chat' or 'deepseek-reasoner'.`);
|
|
8933
9060
|
}
|
|
8934
9061
|
// Check if tools are requested with a model that doesn't support them
|
|
8935
|
-
if (options.tools &&
|
|
8936
|
-
options.tools.length > 0 &&
|
|
8937
|
-
!supportsToolCalling(normalizedModel)) {
|
|
9062
|
+
if (options.tools && options.tools.length > 0 && !supportsToolCalling(normalizedModel)) {
|
|
8938
9063
|
throw new Error(`Model ${normalizedModel} does not support tool calling.`);
|
|
8939
9064
|
}
|
|
8940
9065
|
const apiKey = options.apiKey || getSecrets().deepseek.apiKey;
|
|
8941
9066
|
if (!apiKey) {
|
|
8942
|
-
throw new Error(
|
|
9067
|
+
throw new Error('Deepseek API key is not provided and DEEPSEEK_API_KEY environment variable is not set');
|
|
8943
9068
|
}
|
|
8944
9069
|
// Initialize OpenAI client with Deepseek API URL
|
|
8945
9070
|
const openai = new OpenAI({
|
|
8946
9071
|
apiKey,
|
|
8947
|
-
baseURL:
|
|
9072
|
+
baseURL: 'https://api.deepseek.com',
|
|
8948
9073
|
timeout: options.timeout ?? LLM_TIMEOUT_MS,
|
|
8949
9074
|
});
|
|
8950
9075
|
const messages = [];
|
|
8951
9076
|
// Add system message with developer prompt if present
|
|
8952
9077
|
if (options.developerPrompt) {
|
|
8953
9078
|
messages.push({
|
|
8954
|
-
role:
|
|
9079
|
+
role: 'system',
|
|
8955
9080
|
content: options.developerPrompt,
|
|
8956
9081
|
});
|
|
8957
9082
|
}
|
|
@@ -8960,35 +9085,33 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
8960
9085
|
messages.push(...options.context);
|
|
8961
9086
|
}
|
|
8962
9087
|
// Add user content
|
|
8963
|
-
if (typeof content ===
|
|
9088
|
+
if (typeof content === 'string') {
|
|
8964
9089
|
messages.push({
|
|
8965
|
-
role:
|
|
9090
|
+
role: 'user',
|
|
8966
9091
|
content,
|
|
8967
9092
|
});
|
|
8968
9093
|
}
|
|
8969
9094
|
else {
|
|
8970
9095
|
messages.push({
|
|
8971
|
-
role:
|
|
9096
|
+
role: 'user',
|
|
8972
9097
|
content,
|
|
8973
9098
|
});
|
|
8974
9099
|
}
|
|
8975
9100
|
// If JSON response format, include a hint in the system prompt
|
|
8976
|
-
if ((responseFormat ===
|
|
8977
|
-
(
|
|
8978
|
-
responseFormat.type === "json_schema")) &&
|
|
8979
|
-
supportsJsonOutput(normalizedModel)) {
|
|
9101
|
+
if ((responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema'))
|
|
9102
|
+
&& supportsJsonOutput(normalizedModel)) {
|
|
8980
9103
|
// If there's no system message yet, add one
|
|
8981
|
-
if (!messages.some(
|
|
9104
|
+
if (!messages.some(m => m.role === 'system')) {
|
|
8982
9105
|
messages.unshift({
|
|
8983
|
-
role:
|
|
8984
|
-
content:
|
|
9106
|
+
role: 'system',
|
|
9107
|
+
content: 'Please respond in valid JSON format.',
|
|
8985
9108
|
});
|
|
8986
9109
|
}
|
|
8987
9110
|
else {
|
|
8988
9111
|
// Append to existing system message
|
|
8989
|
-
const systemMsgIndex = messages.findIndex(
|
|
9112
|
+
const systemMsgIndex = messages.findIndex(m => m.role === 'system');
|
|
8990
9113
|
const systemMsg = messages[systemMsgIndex];
|
|
8991
|
-
if (typeof systemMsg.content ===
|
|
9114
|
+
if (typeof systemMsg.content === 'string') {
|
|
8992
9115
|
messages[systemMsgIndex] = {
|
|
8993
9116
|
...systemMsg,
|
|
8994
9117
|
content: `${systemMsg.content} Please respond in valid JSON format.`,
|
|
@@ -9029,7 +9152,7 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9029
9152
|
0;
|
|
9030
9153
|
return {
|
|
9031
9154
|
id: completion.id,
|
|
9032
|
-
content: completion.choices[0]?.message?.content ||
|
|
9155
|
+
content: completion.choices[0]?.message?.content || '',
|
|
9033
9156
|
tool_calls: completion.choices[0]?.message?.tool_calls,
|
|
9034
9157
|
usage: {
|
|
9035
9158
|
prompt_tokens: completion.usage?.prompt_tokens ?? 0,
|
|
@@ -9038,7 +9161,7 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9038
9161
|
cached_tokens: cachedTokens,
|
|
9039
9162
|
},
|
|
9040
9163
|
system_fingerprint: completion.system_fingerprint,
|
|
9041
|
-
provider:
|
|
9164
|
+
provider: 'deepseek',
|
|
9042
9165
|
model: normalizedModel,
|
|
9043
9166
|
};
|
|
9044
9167
|
}
|
|
@@ -9054,8 +9177,12 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9054
9177
|
* @param responseFormat The format of the response. Defaults to 'json'.
|
|
9055
9178
|
* @param options Configuration options including model ('deepseek-chat' or 'deepseek-reasoner'), tools, and apiKey.
|
|
9056
9179
|
* @return A promise that resolves to the response from the Deepseek API.
|
|
9180
|
+
* @throws {Error} If the requested capability (JSON output / tool calling) is
|
|
9181
|
+
* unsupported by the model, or if the underlying API call fails. This matches
|
|
9182
|
+
* the OpenAI and Anthropic providers, which surface failures by throwing
|
|
9183
|
+
* rather than returning a success-shaped response carrying an error payload.
|
|
9057
9184
|
*/
|
|
9058
|
-
const makeDeepseekCall = async (content, responseFormat =
|
|
9185
|
+
const makeDeepseekCall = async (content, responseFormat = 'json', options = {}) => {
|
|
9059
9186
|
// Set default model if not provided
|
|
9060
9187
|
const mergedOptions = {
|
|
9061
9188
|
...options,
|
|
@@ -9064,51 +9191,21 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9064
9191
|
mergedOptions.model = DEFAULT_DEEPSEEK_OPTIONS.defaultModel;
|
|
9065
9192
|
}
|
|
9066
9193
|
const modelName = normalizeModelName(mergedOptions.model);
|
|
9067
|
-
// Check if the requested response format is compatible with the model
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
error: `Model ${modelName} does not support JSON output format.`,
|
|
9073
|
-
},
|
|
9074
|
-
usage: {
|
|
9075
|
-
prompt_tokens: 0,
|
|
9076
|
-
completion_tokens: 0,
|
|
9077
|
-
reasoning_tokens: 0,
|
|
9078
|
-
provider: "deepseek",
|
|
9079
|
-
model: modelName,
|
|
9080
|
-
cached_tokens: 0,
|
|
9081
|
-
cost: 0,
|
|
9082
|
-
},
|
|
9083
|
-
tool_calls: undefined,
|
|
9084
|
-
};
|
|
9194
|
+
// Check if the requested response format is compatible with the model.
|
|
9195
|
+
// Throw rather than returning a fake-success LLMResponse: a caller that
|
|
9196
|
+
// asked for JSON must not silently receive an { error } payload typed as T.
|
|
9197
|
+
if (responseFormat !== 'text' && !supportsJsonOutput(modelName)) {
|
|
9198
|
+
throw new Error(`Model ${modelName} does not support JSON output format.`);
|
|
9085
9199
|
}
|
|
9086
|
-
// Check if tools are requested with a model that doesn't support them
|
|
9087
|
-
if (mergedOptions.tools &&
|
|
9088
|
-
|
|
9089
|
-
!supportsToolCalling(modelName)) {
|
|
9090
|
-
getLumicLogger().warn(`Model ${modelName} does not support tool calling. Will return error in the response.`);
|
|
9091
|
-
return {
|
|
9092
|
-
response: {
|
|
9093
|
-
error: `Model ${modelName} does not support tool calling.`,
|
|
9094
|
-
},
|
|
9095
|
-
usage: {
|
|
9096
|
-
prompt_tokens: 0,
|
|
9097
|
-
completion_tokens: 0,
|
|
9098
|
-
reasoning_tokens: 0,
|
|
9099
|
-
provider: "deepseek",
|
|
9100
|
-
model: modelName,
|
|
9101
|
-
cached_tokens: 0,
|
|
9102
|
-
cost: 0,
|
|
9103
|
-
},
|
|
9104
|
-
tool_calls: undefined,
|
|
9105
|
-
};
|
|
9200
|
+
// Check if tools are requested with a model that doesn't support them.
|
|
9201
|
+
if (mergedOptions.tools && mergedOptions.tools.length > 0 && !supportsToolCalling(modelName)) {
|
|
9202
|
+
throw new Error(`Model ${modelName} does not support tool calling.`);
|
|
9106
9203
|
}
|
|
9107
9204
|
try {
|
|
9108
9205
|
const completion = await createDeepseekCompletion(content, responseFormat, mergedOptions);
|
|
9109
9206
|
// Track cost in the global cost tracker. Pass cached tokens through so the
|
|
9110
9207
|
// discounted cached-input pricing tier is applied.
|
|
9111
|
-
getLLMCostTracker().trackUsage(
|
|
9208
|
+
getLLMCostTracker().trackUsage('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens);
|
|
9112
9209
|
// Handle tool calls similarly to OpenAI
|
|
9113
9210
|
if (completion.tool_calls && completion.tool_calls.length > 0) {
|
|
9114
9211
|
const toolCallResponse = {
|
|
@@ -9124,10 +9221,10 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9124
9221
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
9125
9222
|
completion_tokens: completion.usage.completion_tokens,
|
|
9126
9223
|
reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
|
|
9127
|
-
provider:
|
|
9224
|
+
provider: 'deepseek',
|
|
9128
9225
|
model: completion.model,
|
|
9129
9226
|
cached_tokens: completion.usage.cached_tokens,
|
|
9130
|
-
cost: calculateCost(
|
|
9227
|
+
cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
9131
9228
|
},
|
|
9132
9229
|
tool_calls: completion.tool_calls,
|
|
9133
9230
|
};
|
|
@@ -9135,7 +9232,7 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9135
9232
|
// Handle regular responses
|
|
9136
9233
|
const parsedResponse = await parseResponse(completion.content, responseFormat);
|
|
9137
9234
|
if (parsedResponse === null) {
|
|
9138
|
-
throw new Error(
|
|
9235
|
+
throw new Error('Failed to parse Deepseek response');
|
|
9139
9236
|
}
|
|
9140
9237
|
return {
|
|
9141
9238
|
response: parsedResponse,
|
|
@@ -9143,32 +9240,22 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9143
9240
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
9144
9241
|
completion_tokens: completion.usage.completion_tokens,
|
|
9145
9242
|
reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
|
|
9146
|
-
provider:
|
|
9243
|
+
provider: 'deepseek',
|
|
9147
9244
|
model: completion.model,
|
|
9148
9245
|
cached_tokens: completion.usage.cached_tokens,
|
|
9149
|
-
cost: calculateCost(
|
|
9246
|
+
cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
9150
9247
|
},
|
|
9151
9248
|
tool_calls: completion.tool_calls,
|
|
9152
9249
|
};
|
|
9153
9250
|
}
|
|
9154
9251
|
catch (error) {
|
|
9155
|
-
//
|
|
9252
|
+
// Mirror the OpenAI/Anthropic providers: log with sanitized context and
|
|
9253
|
+
// rethrow so callers observe the failure. Returning a success-shaped
|
|
9254
|
+
// response with an { error } payload here previously let hard failures
|
|
9255
|
+
// (auth, timeout, rate-limit exhaustion, unparseable output) masquerade as
|
|
9256
|
+
// successful completions downstream.
|
|
9156
9257
|
getLumicLogger().error(`Error in Deepseek API call: ${sanitizeError(error)}`);
|
|
9157
|
-
|
|
9158
|
-
response: {
|
|
9159
|
-
error: sanitizeError(error),
|
|
9160
|
-
},
|
|
9161
|
-
usage: {
|
|
9162
|
-
prompt_tokens: 0,
|
|
9163
|
-
completion_tokens: 0,
|
|
9164
|
-
reasoning_tokens: 0,
|
|
9165
|
-
provider: "deepseek",
|
|
9166
|
-
model: modelName,
|
|
9167
|
-
cached_tokens: 0,
|
|
9168
|
-
cost: 0,
|
|
9169
|
-
},
|
|
9170
|
-
tool_calls: undefined,
|
|
9171
|
-
};
|
|
9258
|
+
throw error;
|
|
9172
9259
|
}
|
|
9173
9260
|
};
|
|
9174
9261
|
|
|
@@ -9271,15 +9358,15 @@ const generateCacheKey = (auth, envVarsCheck = null) => {
|
|
|
9271
9358
|
if (auth) {
|
|
9272
9359
|
return `${auth.AWS_ACCESS_KEY_ID}:${auth.AWS_SECRET_ACCESS_KEY}:${auth.AWS_REGION}`;
|
|
9273
9360
|
}
|
|
9274
|
-
if (envVarsCheck?.source ===
|
|
9361
|
+
if (envVarsCheck?.source === 'lambda') {
|
|
9275
9362
|
// In Lambda, use function name as cache key since it uses IAM role
|
|
9276
9363
|
const secrets = getSecrets();
|
|
9277
|
-
return `lambda:${secrets.aws.lambdaFunctionName ||
|
|
9364
|
+
return `lambda:${secrets.aws.lambdaFunctionName || 'default'}`;
|
|
9278
9365
|
}
|
|
9279
9366
|
if (envVarsCheck?.vars) {
|
|
9280
9367
|
return `${envVarsCheck.vars.accessKeyId}:${envVarsCheck.vars.secretAccessKey}:${envVarsCheck.vars.region}`;
|
|
9281
9368
|
}
|
|
9282
|
-
return
|
|
9369
|
+
return 'default';
|
|
9283
9370
|
};
|
|
9284
9371
|
const validateEnvironmentVars = () => {
|
|
9285
9372
|
const secrets = getSecrets();
|
|
@@ -9292,37 +9379,37 @@ const validateEnvironmentVars = () => {
|
|
|
9292
9379
|
// We're in a Lambda environment
|
|
9293
9380
|
return {
|
|
9294
9381
|
isValid: true,
|
|
9295
|
-
source:
|
|
9296
|
-
vars: { accessKeyId:
|
|
9382
|
+
source: 'lambda',
|
|
9383
|
+
vars: { accessKeyId: '', secretAccessKey: '', region: '' } // No need for explicit credentials in Lambda
|
|
9297
9384
|
};
|
|
9298
9385
|
}
|
|
9299
9386
|
else if (areVarsComplete(secrets.aws.myAccessKeyId, secrets.aws.mySecretAccessKey, secrets.aws.myRegion)) {
|
|
9300
9387
|
return {
|
|
9301
9388
|
isValid: true,
|
|
9302
|
-
source:
|
|
9389
|
+
source: 'my_prefix',
|
|
9303
9390
|
vars: {
|
|
9304
|
-
accessKeyId: secrets.aws.myAccessKeyId
|
|
9305
|
-
secretAccessKey: secrets.aws.mySecretAccessKey
|
|
9306
|
-
region: secrets.aws.myRegion
|
|
9307
|
-
}
|
|
9391
|
+
accessKeyId: secrets.aws.myAccessKeyId,
|
|
9392
|
+
secretAccessKey: secrets.aws.mySecretAccessKey,
|
|
9393
|
+
region: secrets.aws.myRegion
|
|
9394
|
+
}
|
|
9308
9395
|
};
|
|
9309
9396
|
}
|
|
9310
9397
|
else if (areVarsComplete(secrets.aws.accessKeyId, secrets.aws.secretAccessKey, secrets.aws.region)) {
|
|
9311
9398
|
return {
|
|
9312
9399
|
isValid: true,
|
|
9313
|
-
source:
|
|
9400
|
+
source: 'standard',
|
|
9314
9401
|
vars: {
|
|
9315
|
-
accessKeyId: secrets.aws.accessKeyId
|
|
9316
|
-
secretAccessKey: secrets.aws.secretAccessKey
|
|
9317
|
-
region: secrets.aws.region
|
|
9318
|
-
}
|
|
9402
|
+
accessKeyId: secrets.aws.accessKeyId,
|
|
9403
|
+
secretAccessKey: secrets.aws.secretAccessKey,
|
|
9404
|
+
region: secrets.aws.region
|
|
9405
|
+
}
|
|
9319
9406
|
};
|
|
9320
9407
|
}
|
|
9321
9408
|
// If we get here, no complete set of variables was found
|
|
9322
9409
|
return {
|
|
9323
9410
|
isValid: false,
|
|
9324
9411
|
source: null,
|
|
9325
|
-
missingVars: []
|
|
9412
|
+
missingVars: []
|
|
9326
9413
|
};
|
|
9327
9414
|
};
|
|
9328
9415
|
const initialiseAWSClient = (ClientClass, auth = null) => {
|
|
@@ -9331,52 +9418,48 @@ const initialiseAWSClient = (ClientClass, auth = null) => {
|
|
|
9331
9418
|
const requestTimeout = ClientClass === S3Client ? AWS_S3_TIMEOUT_MS : AWS_LAMBDA_TIMEOUT_MS;
|
|
9332
9419
|
// Case 1: Explicit auth provided
|
|
9333
9420
|
if (auth) {
|
|
9334
|
-
if (typeof auth !==
|
|
9335
|
-
throw new Error(
|
|
9421
|
+
if (typeof auth !== 'object' || auth === null) {
|
|
9422
|
+
throw new Error('Auth parameter must be an object');
|
|
9336
9423
|
}
|
|
9337
|
-
const requiredAuthFields = [
|
|
9338
|
-
|
|
9339
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
9340
|
-
"AWS_REGION",
|
|
9341
|
-
];
|
|
9342
|
-
const missingFields = requiredAuthFields.filter((field) => !auth[field]);
|
|
9424
|
+
const requiredAuthFields = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'];
|
|
9425
|
+
const missingFields = requiredAuthFields.filter(field => !auth[field]);
|
|
9343
9426
|
if (missingFields.length > 0) {
|
|
9344
|
-
throw new Error(`Auth object is missing required fields: ${missingFields.join(
|
|
9427
|
+
throw new Error(`Auth object is missing required fields: ${missingFields.join(', ')}`);
|
|
9345
9428
|
}
|
|
9346
9429
|
return new ClientClass({
|
|
9347
9430
|
region: auth.AWS_REGION,
|
|
9348
9431
|
credentials: {
|
|
9349
9432
|
accessKeyId: auth.AWS_ACCESS_KEY_ID,
|
|
9350
|
-
secretAccessKey: auth.AWS_SECRET_ACCESS_KEY
|
|
9433
|
+
secretAccessKey: auth.AWS_SECRET_ACCESS_KEY
|
|
9351
9434
|
},
|
|
9352
9435
|
requestHandler: {
|
|
9353
|
-
requestTimeout
|
|
9354
|
-
}
|
|
9436
|
+
requestTimeout
|
|
9437
|
+
}
|
|
9355
9438
|
});
|
|
9356
9439
|
}
|
|
9357
9440
|
// Case 2: Check environment variables
|
|
9358
9441
|
const envVarsCheck = validateEnvironmentVars();
|
|
9359
9442
|
if (!envVarsCheck.isValid) {
|
|
9360
|
-
throw new Error(
|
|
9443
|
+
throw new Error('No valid AWS credentials found in environment variables');
|
|
9361
9444
|
}
|
|
9362
9445
|
// Case 2a: Lambda execution environment
|
|
9363
|
-
if (envVarsCheck.source ===
|
|
9446
|
+
if (envVarsCheck.source === 'lambda') {
|
|
9364
9447
|
return new ClientClass({
|
|
9365
9448
|
requestHandler: {
|
|
9366
|
-
requestTimeout
|
|
9367
|
-
}
|
|
9449
|
+
requestTimeout
|
|
9450
|
+
}
|
|
9368
9451
|
}); // AWS SDK will automatically use Lambda role credentials
|
|
9369
9452
|
}
|
|
9370
9453
|
// Case 2b: MY_ prefixed vars or standard AWS vars
|
|
9371
9454
|
return new ClientClass({
|
|
9372
|
-
region: envVarsCheck.vars
|
|
9455
|
+
region: envVarsCheck.vars.region,
|
|
9373
9456
|
credentials: {
|
|
9374
|
-
accessKeyId: envVarsCheck.vars
|
|
9375
|
-
secretAccessKey: envVarsCheck.vars
|
|
9457
|
+
accessKeyId: envVarsCheck.vars.accessKeyId,
|
|
9458
|
+
secretAccessKey: envVarsCheck.vars.secretAccessKey
|
|
9376
9459
|
},
|
|
9377
9460
|
requestHandler: {
|
|
9378
|
-
requestTimeout
|
|
9379
|
-
}
|
|
9461
|
+
requestTimeout
|
|
9462
|
+
}
|
|
9380
9463
|
});
|
|
9381
9464
|
}
|
|
9382
9465
|
catch (error) {
|
|
@@ -9389,9 +9472,7 @@ const initialiseS3Client = (auth = null) => {
|
|
|
9389
9472
|
const envVarsCheck = auth ? null : validateEnvironmentVars();
|
|
9390
9473
|
const cacheKey = generateCacheKey(auth, envVarsCheck);
|
|
9391
9474
|
if (s3ClientCache.has(cacheKey)) {
|
|
9392
|
-
|
|
9393
|
-
if (cached)
|
|
9394
|
-
return cached;
|
|
9475
|
+
return s3ClientCache.get(cacheKey);
|
|
9395
9476
|
}
|
|
9396
9477
|
// Create new client and cache it
|
|
9397
9478
|
const client = initialiseAWSClient(S3Client, auth);
|
|
@@ -9403,9 +9484,7 @@ const initialiseLambdaClient = (auth = null) => {
|
|
|
9403
9484
|
const envVarsCheck = auth ? null : validateEnvironmentVars();
|
|
9404
9485
|
const cacheKey = generateCacheKey(auth, envVarsCheck);
|
|
9405
9486
|
if (lambdaClientCache.has(cacheKey)) {
|
|
9406
|
-
|
|
9407
|
-
if (cached)
|
|
9408
|
-
return cached;
|
|
9487
|
+
return lambdaClientCache.get(cacheKey);
|
|
9409
9488
|
}
|
|
9410
9489
|
// Create new client and cache it
|
|
9411
9490
|
const client = initialiseAWSClient(Lambda, auth);
|
|
@@ -9905,17 +9984,15 @@ const isRetryableS3Error = (error) => {
|
|
|
9905
9984
|
if (error instanceof Error) {
|
|
9906
9985
|
const message = error.message;
|
|
9907
9986
|
// Retry on throttling
|
|
9908
|
-
if (message.includes(
|
|
9987
|
+
if (message.includes('SlowDown') || message.includes('RequestTimeout')) {
|
|
9909
9988
|
return true;
|
|
9910
9989
|
}
|
|
9911
9990
|
// Retry on 5xx server errors
|
|
9912
|
-
if (message.includes(
|
|
9913
|
-
message.includes("ServiceUnavailable")) {
|
|
9991
|
+
if (message.includes('InternalError') || message.includes('ServiceUnavailable')) {
|
|
9914
9992
|
return true;
|
|
9915
9993
|
}
|
|
9916
9994
|
// Retry on connection errors
|
|
9917
|
-
if (message.includes(
|
|
9918
|
-
message.includes("NetworkingError")) {
|
|
9995
|
+
if (message.includes('RequestTimeTooSkewed') || message.includes('NetworkingError')) {
|
|
9919
9996
|
return true;
|
|
9920
9997
|
}
|
|
9921
9998
|
}
|
|
@@ -9929,11 +10006,10 @@ const generateRandomHash = (length) => {
|
|
|
9929
10006
|
};
|
|
9930
10007
|
function generateDateTimeStamp() {
|
|
9931
10008
|
const now = new Date();
|
|
9932
|
-
return now
|
|
9933
|
-
.
|
|
9934
|
-
.replace(
|
|
9935
|
-
.replace(
|
|
9936
|
-
.replace(/:/g, "-");
|
|
10009
|
+
return now.toISOString()
|
|
10010
|
+
.replace(/T/, '-')
|
|
10011
|
+
.replace(/\..+/, '')
|
|
10012
|
+
.replace(/:/g, '-');
|
|
9937
10013
|
}
|
|
9938
10014
|
const isValidBucketName = (name) => {
|
|
9939
10015
|
const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
@@ -9964,7 +10040,7 @@ async function uploadFile(s3Client, bucketName, filePath, destinationPath, isPub
|
|
|
9964
10040
|
Bucket: bucketName,
|
|
9965
10041
|
Key: s3Key,
|
|
9966
10042
|
Body: fileContent,
|
|
9967
|
-
ACL: isPublic ?
|
|
10043
|
+
ACL: isPublic ? 'public-read' : undefined
|
|
9968
10044
|
});
|
|
9969
10045
|
await withRetry(() => s3Client.send(command), {
|
|
9970
10046
|
maxRetries: 3,
|
|
@@ -9984,7 +10060,7 @@ async function uploadDirectory(s3Client, bucketName, dirPath, destinationPath, i
|
|
|
9984
10060
|
Bucket: bucketName,
|
|
9985
10061
|
Key: s3Key,
|
|
9986
10062
|
Body: fileContent,
|
|
9987
|
-
ACL: isPublic ?
|
|
10063
|
+
ACL: isPublic ? 'public-read' : undefined
|
|
9988
10064
|
});
|
|
9989
10065
|
await withRetry(() => s3Client.send(command), {
|
|
9990
10066
|
maxRetries: 3,
|
|
@@ -10000,7 +10076,7 @@ async function zipDirectory(sourceDir, outPath) {
|
|
|
10000
10076
|
const zip = new AdmZip();
|
|
10001
10077
|
let fileCount = 0;
|
|
10002
10078
|
try {
|
|
10003
|
-
async function addFilesToZip(currentPath, relativePath =
|
|
10079
|
+
async function addFilesToZip(currentPath, relativePath = '') {
|
|
10004
10080
|
const files = await fs$1.readdir(currentPath, { withFileTypes: true });
|
|
10005
10081
|
for (const file of files) {
|
|
10006
10082
|
const filePath = path__default.join(currentPath, file.name);
|
|
@@ -10043,33 +10119,27 @@ async function downloadFromS3Helper(s3Client, bucketName, s3Path, localPath) {
|
|
|
10043
10119
|
Prefix: s3Path,
|
|
10044
10120
|
ContinuationToken: continuationToken,
|
|
10045
10121
|
});
|
|
10046
|
-
const listResponse =
|
|
10122
|
+
const listResponse = await withRetry(() => s3Client.send(listCommand), {
|
|
10047
10123
|
maxRetries: 3,
|
|
10048
10124
|
baseDelayMs: 1000,
|
|
10049
10125
|
maxDelayMs: 15000,
|
|
10050
10126
|
retryableErrors: isRetryableS3Error,
|
|
10051
|
-
}, `S3:ListObjects:${bucketName}`)
|
|
10127
|
+
}, `S3:ListObjects:${bucketName}`);
|
|
10052
10128
|
const objects = listResponse.Contents || [];
|
|
10053
10129
|
if (objects.length === 0) {
|
|
10054
10130
|
return { fileCount: 0, folderCount: 0, totalSize: 0, fileList: [] };
|
|
10055
10131
|
}
|
|
10056
|
-
if (objects.length === 1 && objects[0].Key?.endsWith(
|
|
10132
|
+
if (objects.length === 1 && objects[0].Key?.endsWith('.zip')) {
|
|
10057
10133
|
// It's a zip file, download and extract it
|
|
10058
10134
|
const zipPath = path__default.join(localPath, path__default.basename(objects[0].Key));
|
|
10059
10135
|
await downloadFile(s3Client, bucketName, objects[0].Key, zipPath);
|
|
10060
|
-
const { totalFiles, totalFolders, totalSize: extractedSize, fileList: extractedFileList
|
|
10136
|
+
const { totalFiles, totalFolders, totalSize: extractedSize, fileList: extractedFileList } = await unzipFile(zipPath, localPath);
|
|
10061
10137
|
await fs$1.unlink(zipPath);
|
|
10062
|
-
return {
|
|
10063
|
-
fileCount: totalFiles,
|
|
10064
|
-
folderCount: totalFolders,
|
|
10065
|
-
totalSize: extractedSize,
|
|
10066
|
-
fileList: extractedFileList,
|
|
10067
|
-
};
|
|
10138
|
+
return { fileCount: totalFiles, folderCount: totalFolders, totalSize: extractedSize, fileList: extractedFileList };
|
|
10068
10139
|
}
|
|
10069
10140
|
else {
|
|
10070
10141
|
// Non-zip files. Download files in batches.
|
|
10071
|
-
for (let i = 0; i < objects.length; i += 1000) {
|
|
10072
|
-
// AWS allows up to 1000 per request
|
|
10142
|
+
for (let i = 0; i < objects.length; i += 1000) { // AWS allows up to 1000 per request
|
|
10073
10143
|
const batch = objects.slice(i, i + 1000);
|
|
10074
10144
|
await Promise.all(batch.map(async (obj) => {
|
|
10075
10145
|
if (!obj.Key)
|
|
@@ -10085,9 +10155,7 @@ async function downloadFromS3Helper(s3Client, bucketName, s3Path, localPath) {
|
|
|
10085
10155
|
}
|
|
10086
10156
|
// Count folders
|
|
10087
10157
|
const relativePath = path__default.relative(localPath, path__default.dirname(localFilePath));
|
|
10088
|
-
if (relativePath &&
|
|
10089
|
-
!relativePath.startsWith("..") &&
|
|
10090
|
-
relativePath !== ".") {
|
|
10158
|
+
if (relativePath && !relativePath.startsWith('..') && relativePath !== '.') {
|
|
10091
10159
|
folderCount++;
|
|
10092
10160
|
}
|
|
10093
10161
|
}));
|
|
@@ -10108,7 +10176,7 @@ async function downloadFile(s3Client, bucketName, s3Key, localFilePath) {
|
|
|
10108
10176
|
await withRetry(async () => {
|
|
10109
10177
|
const { Body } = await s3Client.send(getCommand);
|
|
10110
10178
|
if (!Body)
|
|
10111
|
-
throw new Error(
|
|
10179
|
+
throw new Error('No body returned from S3');
|
|
10112
10180
|
const writeStream = createWriteStream(localFilePath);
|
|
10113
10181
|
await pipeline(Body, writeStream);
|
|
10114
10182
|
}, {
|
|
@@ -10121,14 +10189,27 @@ async function downloadFile(s3Client, bucketName, s3Key, localFilePath) {
|
|
|
10121
10189
|
async function unzipFile(zipPath, destPath) {
|
|
10122
10190
|
const zip = new AdmZip(zipPath);
|
|
10123
10191
|
try {
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10192
|
+
// extractAllToAsync is fire-and-forget: it returns void and reports
|
|
10193
|
+
// failures through its callback. The previous call neither awaited it nor
|
|
10194
|
+
// observed the callback, so getDirectoryStats raced an in-flight (or
|
|
10195
|
+
// failed) extraction and any extraction error was silently dropped. Wrap
|
|
10196
|
+
// it in a Promise so the extraction is fully awaited and errors surface.
|
|
10197
|
+
await new Promise((resolve, reject) => {
|
|
10198
|
+
zip.extractAllToAsync(destPath, true, false, (error) => {
|
|
10199
|
+
if (error) {
|
|
10200
|
+
reject(error);
|
|
10201
|
+
}
|
|
10202
|
+
else {
|
|
10203
|
+
resolve();
|
|
10204
|
+
}
|
|
10205
|
+
});
|
|
10206
|
+
});
|
|
10207
|
+
return await getDirectoryStats(destPath);
|
|
10127
10208
|
}
|
|
10128
10209
|
catch (error) {
|
|
10129
10210
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
10130
10211
|
getLumicLogger().error(`Error unzipping file: ${errorMessage}`);
|
|
10131
|
-
throw error;
|
|
10212
|
+
throw new ZipError(`Failed to extract archive "${zipPath}" to "${destPath}": ${errorMessage}`, error);
|
|
10132
10213
|
}
|
|
10133
10214
|
}
|
|
10134
10215
|
async function getDirectoryStats(dirPath) {
|
|
@@ -10164,19 +10245,17 @@ async function emptyBucket(s3Client, bucketName) {
|
|
|
10164
10245
|
Bucket: bucketName,
|
|
10165
10246
|
ContinuationToken: continuationToken,
|
|
10166
10247
|
});
|
|
10167
|
-
const response =
|
|
10248
|
+
const response = await withRetry(() => s3Client.send(listCommand), {
|
|
10168
10249
|
maxRetries: 3,
|
|
10169
10250
|
baseDelayMs: 1000,
|
|
10170
10251
|
maxDelayMs: 15000,
|
|
10171
10252
|
retryableErrors: isRetryableS3Error,
|
|
10172
|
-
}, `S3:ListObjects:${bucketName}`)
|
|
10253
|
+
}, `S3:ListObjects:${bucketName}`);
|
|
10173
10254
|
const { Contents, IsTruncated, NextContinuationToken } = response;
|
|
10174
10255
|
if (Contents && Contents.length > 0) {
|
|
10175
10256
|
const deleteCommand = new DeleteObjectsCommand({
|
|
10176
10257
|
Bucket: bucketName,
|
|
10177
|
-
Delete: {
|
|
10178
|
-
Objects: Contents.filter((c) => c.Key).map((c) => ({ Key: c.Key })),
|
|
10179
|
-
},
|
|
10258
|
+
Delete: { Objects: Contents.map(({ Key }) => ({ Key: Key })) },
|
|
10180
10259
|
});
|
|
10181
10260
|
await withRetry(() => s3Client.send(deleteCommand), {
|
|
10182
10261
|
maxRetries: 3,
|
|
@@ -23084,11 +23163,11 @@ let poolConfig = DEFAULT_POOL_CONFIG;
|
|
|
23084
23163
|
async function loadApolloModules() {
|
|
23085
23164
|
if (typeof window === "undefined" || process.env.AWS_EXECUTION_ENV) {
|
|
23086
23165
|
// Server-side (or Lambda): load the CommonJS‑based implementation.
|
|
23087
|
-
return (await import('./apollo-client.server-
|
|
23166
|
+
return (await import('./apollo-client.server-DaXmxoBl.js'));
|
|
23088
23167
|
}
|
|
23089
23168
|
else {
|
|
23090
23169
|
// Client-side: load the ESM‑based implementation.
|
|
23091
|
-
return (await import('./apollo-client.client-
|
|
23170
|
+
return (await import('./apollo-client.client-Uv0-ZrFd.js'));
|
|
23092
23171
|
}
|
|
23093
23172
|
}
|
|
23094
23173
|
/**
|
|
@@ -79115,7 +79194,7 @@ const RETRY_CONFIG = {
|
|
|
79115
79194
|
BASE_DELAY: 2000, // Increased base delay to 2 seconds
|
|
79116
79195
|
MAX_DELAY: 64000,
|
|
79117
79196
|
MAX_RETRIES: 5,
|
|
79118
|
-
MAX_RANDOM_DELAY: 1000
|
|
79197
|
+
MAX_RANDOM_DELAY: 1000
|
|
79119
79198
|
};
|
|
79120
79199
|
/**
|
|
79121
79200
|
* Determines if an error is related to Google Sheets API quotas or rate limits.
|
|
@@ -79141,10 +79220,10 @@ const RETRY_CONFIG = {
|
|
|
79141
79220
|
*/
|
|
79142
79221
|
function isQuotaError(error) {
|
|
79143
79222
|
const message = error.message.toLowerCase();
|
|
79144
|
-
return
|
|
79145
|
-
message.includes(
|
|
79146
|
-
message.includes(
|
|
79147
|
-
message.includes(
|
|
79223
|
+
return message.includes('quota exceeded') ||
|
|
79224
|
+
message.includes('rate limit') ||
|
|
79225
|
+
message.includes('429') ||
|
|
79226
|
+
message.includes('too many requests');
|
|
79148
79227
|
}
|
|
79149
79228
|
/**
|
|
79150
79229
|
* Implements exponential backoff retry mechanism for Google Sheets API calls.
|
|
@@ -79203,14 +79282,14 @@ async function withExponentialBackoff(operation) {
|
|
|
79203
79282
|
const exponentialDelay = Math.min(Math.pow(2, retries) * RETRY_CONFIG.BASE_DELAY + randomDelay, RETRY_CONFIG.MAX_DELAY);
|
|
79204
79283
|
logIfDebug(`Quota/Rate limit exceeded. Error: ${error.message}`);
|
|
79205
79284
|
logIfDebug(`Retrying in ${exponentialDelay}ms (attempt ${retries}/${RETRY_CONFIG.MAX_RETRIES})`);
|
|
79206
|
-
await new Promise(
|
|
79285
|
+
await new Promise(resolve => setTimeout(resolve, exponentialDelay));
|
|
79207
79286
|
}
|
|
79208
79287
|
}
|
|
79209
79288
|
}
|
|
79210
79289
|
function checkEnvVars() {
|
|
79211
79290
|
const secrets = getSecrets();
|
|
79212
79291
|
if (!secrets.googleSheets.clientEmail || !secrets.googleSheets.privateKey) {
|
|
79213
|
-
throw new Error(
|
|
79292
|
+
throw new Error('GOOGLE_SHEETS_CLIENT_EMAIL or GOOGLE_SHEETS_PRIVATE_KEY is not defined in environment variables.');
|
|
79214
79293
|
}
|
|
79215
79294
|
}
|
|
79216
79295
|
/**
|
|
@@ -79225,16 +79304,16 @@ async function getAuthClient() {
|
|
|
79225
79304
|
const secrets = getSecrets();
|
|
79226
79305
|
const credentials = {
|
|
79227
79306
|
client_email: secrets.googleSheets.clientEmail,
|
|
79228
|
-
private_key: secrets.googleSheets.privateKey?.replace(/\\n/g,
|
|
79307
|
+
private_key: secrets.googleSheets.privateKey?.replace(/\\n/g, '\n'),
|
|
79229
79308
|
};
|
|
79230
79309
|
const auth = new GoogleAuth({
|
|
79231
79310
|
credentials,
|
|
79232
|
-
scopes: [
|
|
79311
|
+
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
79233
79312
|
});
|
|
79234
79313
|
return auth.getClient();
|
|
79235
79314
|
}
|
|
79236
79315
|
catch (error) {
|
|
79237
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79316
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79238
79317
|
logIfDebug(`Error initializing Google Sheets auth: ${errorMessage}`);
|
|
79239
79318
|
throw new Error(`Failed to initialize Google Sheets auth: ${errorMessage}`);
|
|
79240
79319
|
}
|
|
@@ -79246,7 +79325,7 @@ async function getSheetsClient() {
|
|
|
79246
79325
|
const auth = await getAuthClient();
|
|
79247
79326
|
return new buildExports.sheets_v4.Sheets({
|
|
79248
79327
|
auth,
|
|
79249
|
-
timeout: GOOGLE_SHEETS_TIMEOUT_MS
|
|
79328
|
+
timeout: GOOGLE_SHEETS_TIMEOUT_MS
|
|
79250
79329
|
});
|
|
79251
79330
|
}
|
|
79252
79331
|
/**
|
|
@@ -79279,7 +79358,7 @@ function escapeSheetName(sheetName) {
|
|
|
79279
79358
|
* @param startColumn Optional starting column (defaults to 'A')
|
|
79280
79359
|
* @returns Promise resolving to response with success status and row details
|
|
79281
79360
|
*/
|
|
79282
|
-
async function addRow(config, values, startColumn =
|
|
79361
|
+
async function addRow(config, values, startColumn = 'A') {
|
|
79283
79362
|
try {
|
|
79284
79363
|
const sheets = await getSheetsClient();
|
|
79285
79364
|
const escapedSheetName = escapeSheetName(config.sheetName);
|
|
@@ -79287,7 +79366,7 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79287
79366
|
const response = await withExponentialBackoff(() => sheets.spreadsheets.values.append({
|
|
79288
79367
|
spreadsheetId: config.spreadsheetId,
|
|
79289
79368
|
range,
|
|
79290
|
-
valueInputOption:
|
|
79369
|
+
valueInputOption: 'USER_ENTERED',
|
|
79291
79370
|
requestBody: {
|
|
79292
79371
|
values: [values],
|
|
79293
79372
|
},
|
|
@@ -79295,9 +79374,9 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79295
79374
|
//logIfDebug(`Added row to sheet ${config.sheetName}: ${values.join(', ')}`);
|
|
79296
79375
|
const rowNumber = response.data.updates?.updatedRange
|
|
79297
79376
|
? parseInt(response.data.updates.updatedRange
|
|
79298
|
-
.split(
|
|
79299
|
-
.split(
|
|
79300
|
-
.replace(/[^0-9]/g,
|
|
79377
|
+
.split('!')[1]
|
|
79378
|
+
.split(':')[0]
|
|
79379
|
+
.replace(/[^0-9]/g, ''))
|
|
79301
79380
|
: 0;
|
|
79302
79381
|
return {
|
|
79303
79382
|
success: true,
|
|
@@ -79308,7 +79387,7 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79308
79387
|
};
|
|
79309
79388
|
}
|
|
79310
79389
|
catch (error) {
|
|
79311
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79390
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79312
79391
|
logIfDebug(`Error adding row to sheet: ${errorMessage}`);
|
|
79313
79392
|
return {
|
|
79314
79393
|
success: false,
|
|
@@ -79350,7 +79429,7 @@ async function addSheet(spreadsheetId, sheetTitle) {
|
|
|
79350
79429
|
};
|
|
79351
79430
|
}
|
|
79352
79431
|
catch (error) {
|
|
79353
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79432
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79354
79433
|
logIfDebug(`Error adding sheet: ${errorMessage}`);
|
|
79355
79434
|
return {
|
|
79356
79435
|
success: false,
|
|
@@ -79374,7 +79453,7 @@ async function writeCell(config, cell, value) {
|
|
|
79374
79453
|
await withExponentialBackoff(() => sheets.spreadsheets.values.update({
|
|
79375
79454
|
spreadsheetId: config.spreadsheetId,
|
|
79376
79455
|
range,
|
|
79377
|
-
valueInputOption:
|
|
79456
|
+
valueInputOption: 'USER_ENTERED',
|
|
79378
79457
|
requestBody: {
|
|
79379
79458
|
values: [[value]],
|
|
79380
79459
|
},
|
|
@@ -79388,7 +79467,7 @@ async function writeCell(config, cell, value) {
|
|
|
79388
79467
|
};
|
|
79389
79468
|
}
|
|
79390
79469
|
catch (error) {
|
|
79391
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79470
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79392
79471
|
logIfDebug(`Error writing to cell: ${errorMessage}`);
|
|
79393
79472
|
return {
|
|
79394
79473
|
success: false,
|
|
@@ -79412,11 +79491,8 @@ async function getCell(config, cell) {
|
|
|
79412
79491
|
spreadsheetId: config.spreadsheetId,
|
|
79413
79492
|
range,
|
|
79414
79493
|
}));
|
|
79415
|
-
const
|
|
79416
|
-
|
|
79417
|
-
? null
|
|
79418
|
-
: rawValue;
|
|
79419
|
-
logIfDebug(`Read value from cell ${range}: ${String(value)}`);
|
|
79494
|
+
const value = response.data.values?.[0]?.[0];
|
|
79495
|
+
logIfDebug(`Read value from cell ${range}: ${value}`);
|
|
79420
79496
|
return {
|
|
79421
79497
|
success: true,
|
|
79422
79498
|
data: {
|
|
@@ -79425,7 +79501,7 @@ async function getCell(config, cell) {
|
|
|
79425
79501
|
};
|
|
79426
79502
|
}
|
|
79427
79503
|
catch (error) {
|
|
79428
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79504
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79429
79505
|
logIfDebug(`Error reading from cell: ${errorMessage}`);
|
|
79430
79506
|
return {
|
|
79431
79507
|
success: false,
|
|
@@ -79458,11 +79534,10 @@ function convertA1ToRowCol(a1Notation) {
|
|
|
79458
79534
|
* @returns Padded array of arrays
|
|
79459
79535
|
*/
|
|
79460
79536
|
function padArrays(arrays) {
|
|
79461
|
-
const maxLength = Math.max(...arrays.map(
|
|
79462
|
-
return arrays.map(
|
|
79537
|
+
const maxLength = Math.max(...arrays.map(arr => arr.length));
|
|
79538
|
+
return arrays.map(arr => {
|
|
79463
79539
|
if (arr.length < maxLength) {
|
|
79464
|
-
|
|
79465
|
-
return [...arr, ...padding];
|
|
79540
|
+
return [...arr, ...Array(maxLength - arr.length).fill('')];
|
|
79466
79541
|
}
|
|
79467
79542
|
return arr;
|
|
79468
79543
|
});
|
|
@@ -79474,10 +79549,10 @@ function padArrays(arrays) {
|
|
|
79474
79549
|
* @param startCell Optional starting cell in A1 notation (defaults to 'A1')
|
|
79475
79550
|
* @returns Promise resolving to response with success status and update details
|
|
79476
79551
|
*/
|
|
79477
|
-
async function writeArray(config, data, startCell =
|
|
79552
|
+
async function writeArray(config, data, startCell = 'A1') {
|
|
79478
79553
|
try {
|
|
79479
79554
|
if (!data.length) {
|
|
79480
|
-
throw new Error(
|
|
79555
|
+
throw new Error('Data array is empty');
|
|
79481
79556
|
}
|
|
79482
79557
|
const sheets = await getSheetsClient();
|
|
79483
79558
|
const { row: startRow, column: startCol } = convertA1ToRowCol(startCell);
|
|
@@ -79493,7 +79568,7 @@ async function writeArray(config, data, startCell = "A1") {
|
|
|
79493
79568
|
const response = await withExponentialBackoff(() => sheets.spreadsheets.values.update({
|
|
79494
79569
|
spreadsheetId: config.spreadsheetId,
|
|
79495
79570
|
range,
|
|
79496
|
-
valueInputOption:
|
|
79571
|
+
valueInputOption: 'USER_ENTERED',
|
|
79497
79572
|
requestBody: {
|
|
79498
79573
|
values: paddedData,
|
|
79499
79574
|
},
|
|
@@ -79509,7 +79584,7 @@ async function writeArray(config, data, startCell = "A1") {
|
|
|
79509
79584
|
};
|
|
79510
79585
|
}
|
|
79511
79586
|
catch (error) {
|
|
79512
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79587
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79513
79588
|
logIfDebug(`Error writing array to sheet: ${errorMessage}`);
|
|
79514
79589
|
return {
|
|
79515
79590
|
success: false,
|
|
@@ -81616,4 +81691,4 @@ const lumic = {
|
|
|
81616
81691
|
};
|
|
81617
81692
|
|
|
81618
81693
|
export { GraphQLInterfaceType as $, print as A, getNamedType as B, isInputType as C, isRequiredArgument as D, isNamedType as E, GraphQLError as F, GraphQLNonNull as G, isOutputType as H, isRequiredInputField as I, isCompositeType as J, Kind as K, getNullableType as L, getEnterLeaveForKind as M, isNode as N, OperationTypeNode as O, didYouMean as P, naturalCompare as Q, suggestionList as R, specifiedScalarTypes as S, keyMap as T, isType as U, isNullableType as V, visit as W, visitInParallel as X, keyValMap as Y, assertObjectType as Z, GraphQLScalarType as _, isListType as a, validateGoogleSheetsRange as a$, GraphQLUnionType as a0, GraphQLInputObjectType as a1, assertNullableType as a2, assertInterfaceType as a3, mapValue as a4, isSpecifiedScalarType as a5, isPrintableAsBlockString as a6, printBlockString as a7, BREAK as a8, GRAPHQL_MAX_INT as a9, printSourceLocation as aA, resolveObjMapThunk as aB, resolveReadonlyArrayThunk as aC, valueFromASTUntyped as aD, version$4 as aE, versionInfo as aF, getAugmentedNamespace as aG, isDigit$1 as aH, isNameStart as aI, dedentBlockStringLines as aJ, isNameContinue as aK, setLumicLogger as aL, getLumicLogger as aM, sanitizeForLog as aN, sanitizeError as aO, sanitizeAWSAuth as aP, sanitizeObject as aQ, getSecrets as aR, resetSecrets as aS, requireSecret as aT, withRetry as aU, CircuitBreaker as aV, CircuitBreakerState as aW, CircuitBreakerOpenError as aX, DEFAULT_CIRCUIT_BREAKER_CONFIG as aY, validateSlackChannel as aZ, validateS3Key as a_, GRAPHQL_MIN_INT as aa, GraphQLFloat as ab, GraphQLInt as ac, Location as ad, Token as ae, assertAbstractType as af, assertCompositeType as ag, assertEnumType as ah, assertEnumValueName as ai, assertInputObjectType as aj, assertInputType as ak, assertLeafType as al, assertListType as am, assertNamedType as an, assertNonNullType as ao, assertOutputType as ap, assertScalarType as aq, assertType as ar, assertUnionType as as, assertWrappingType as at, formatError as au, getLocation as av, getVisitFn as aw, isWrappingType as ax, printError as ay, printLocation as az, isAbstractType as b, PDFError as b$, LLMCostTracker as b0, getLLMCostTracker as b1, setLLMCostTracker as b2, resetLLMCostTracker as b3, setMetricsCollector as b4, getMetricsCollector as b5, resetMetricsCollector as b6, withMetrics as b7, generateCorrelationId as b8, getCorrelationId as b9, openAIChatCompletionSchema as bA, openAIImageResponseSchema as bB, validateOpenAIChatCompletion as bC, safeValidateOpenAIChatCompletion as bD, perplexityResponseSchema as bE, validatePerplexityResponse as bF, safeValidatePerplexityResponse as bG, googleSheetsValueRangeSchema as bH, validateGoogleSheetsResponse as bI, safeValidateGoogleSheetsResponse as bJ, s3ListObjectsSchema as bK, s3GetObjectSchema as bL, lambdaInvokeResponseSchema as bM, validateS3ListObjects as bN, safeValidateS3ListObjects as bO, validateLambdaResponse as bP, safeValidateLambdaResponse as bQ, createValidator as bR, createSafeValidator as bS, LumicError as bT, SlackError as bU, LLMError as bV, AWSLambdaError as bW, AWSS3Error as bX, GoogleSheetsError as bY, PerplexityError as bZ, JsonParseError as b_, getCorrelationContext as ba, withCorrelationId as bb, getCorrelationHeaders as bc, TokenBucketRateLimiter as bd, RATE_LIMIT_PROFILES as be, getRateLimiter as bf, resetAllRateLimiters as bg, withRateLimit as bh, checkIntegrationHealth as bi, SUPPORTED_MODELS as bj, isValidModel as bk, getModelCapabilities as bl, getModelProvider as bm, MODEL_ALIASES as bn, OPENAI_COMPATIBLE_PROVIDERS as bo, PROVIDER_DEFAULT_MODELS as bp, LLM_DEFAULT_PROVIDER as bq, LLM_MINI_PROVIDER as br, LLM_NORMAL_PROVIDER as bs, LLM_ADVANCED_PROVIDER as bt, LLM_PROVIDER as bu, LLM_MODEL_MINI as bv, LLM_MODEL_NORMAL as bw, LLM_MODEL_ADVANCED as bx, makeAnthropicCall as by, makeOpenAICompatibleCall as bz, isInterfaceType as c, ZipError as c0, SLACK_TIMEOUT_MS as c1, PERPLEXITY_TIMEOUT_MS as c2, GOOGLE_SHEETS_TIMEOUT_MS as c3, LLM_TIMEOUT_MS as c4, AWS_LAMBDA_TIMEOUT_MS as c5, AWS_S3_TIMEOUT_MS as c6, OPENWEATHER_TIMEOUT_MS as c7, GENERIC_FETCH_TIMEOUT_MS as c8, isObjectType as d, assertName as e, devAssert as f, isObjectLike as g, defineArguments as h, isNonNullType as i, argsToArgsConfig as j, GraphQLBoolean as k, lumic as l, GraphQLString as m, instanceOf as n, inspect as o, isInputObjectType as p, isLeafType as q, isEnumType as r, GraphQLID as s, toObjMap as t, invariant as u, GraphQLObjectType as v, GraphQLEnumType as w, GraphQLList as x, isScalarType as y, isUnionType as z };
|
|
81619
|
-
//# sourceMappingURL=index-
|
|
81694
|
+
//# sourceMappingURL=index-z7Y1nTVX.js.map
|