@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
|
@@ -571,19 +571,10 @@ function getModelProvider(model) {
|
|
|
571
571
|
return SUPPORTED_MODELS[model].provider;
|
|
572
572
|
}
|
|
573
573
|
/**
|
|
574
|
-
* Default model tiers per provider for use with LLM_MODEL_MINI/NORMAL/ADVANCED env vars
|
|
575
|
-
*
|
|
576
|
-
* OpenAI `advanced` was reverted from `gpt-5.5` back to `gpt-5.4` on 2026-05-30:
|
|
577
|
-
* the heavy tool-call prompt in adaptic-engine's trading-decision path was
|
|
578
|
-
* timing out even after the engine raised `LLM_CALL_TIMEOUT_MS` to 90s (5+
|
|
579
|
-
* tickers per loop still exceeded the budget). `gpt-5.5`'s 1M-context window
|
|
580
|
-
* is not required on that path — the prompt fits comfortably inside `gpt-5.4`'s
|
|
581
|
-
* envelope and `gpt-5.4` returns inside the original 25-30s p95. `gpt-5.5`
|
|
582
|
-
* remains registered in OPENAI_MODELS and reachable by explicit model id;
|
|
583
|
-
* only the `advanced` tier default is rolled back.
|
|
574
|
+
* Default model tiers per provider for use with LLM_MODEL_MINI/NORMAL/ADVANCED env vars
|
|
584
575
|
*/
|
|
585
576
|
const PROVIDER_DEFAULT_MODELS = {
|
|
586
|
-
openai: { mini: 'gpt-5.4-nano', normal: 'gpt-5.4-mini', advanced: 'gpt-5.
|
|
577
|
+
openai: { mini: 'gpt-5.4-nano', normal: 'gpt-5.4-mini', advanced: 'gpt-5.5' },
|
|
587
578
|
anthropic: { mini: 'claude-haiku-4-5', normal: 'claude-sonnet-4-6', advanced: 'claude-opus-4-7' },
|
|
588
579
|
deepseek: { mini: 'deepseek-v4-flash', normal: 'deepseek-v4-flash', advanced: 'deepseek-v4-pro' },
|
|
589
580
|
kimi: { mini: 'kimi-k2-0905-preview', normal: 'kimi-k2.5', advanced: 'kimi-k2.6' },
|
|
@@ -1537,13 +1528,11 @@ let openai;
|
|
|
1537
1528
|
function initializeOpenAI(apiKey) {
|
|
1538
1529
|
const key = apiKey || getSecrets().openai.apiKey;
|
|
1539
1530
|
if (!key) {
|
|
1540
|
-
throw new Error(
|
|
1531
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
1541
1532
|
}
|
|
1542
1533
|
return new OpenAI({
|
|
1543
1534
|
apiKey: key,
|
|
1544
|
-
defaultHeaders: {
|
|
1545
|
-
"User-Agent": "My Server-side Application (compatible; OpenAI API Client)",
|
|
1546
|
-
},
|
|
1535
|
+
defaultHeaders: { 'User-Agent': 'My Server-side Application (compatible; OpenAI API Client)' },
|
|
1547
1536
|
});
|
|
1548
1537
|
}
|
|
1549
1538
|
/**
|
|
@@ -1561,7 +1550,9 @@ function initializeOpenAI(apiKey) {
|
|
|
1561
1550
|
*/
|
|
1562
1551
|
async function fixJsonWithAI(jsonStr, options) {
|
|
1563
1552
|
// Backward compatibility: handle string apiKey as first positional param
|
|
1564
|
-
const opts = typeof options ===
|
|
1553
|
+
const opts = typeof options === 'string'
|
|
1554
|
+
? { apiKey: options }
|
|
1555
|
+
: (options ?? {});
|
|
1565
1556
|
const apiKey = opts.apiKey;
|
|
1566
1557
|
const maxDepth = opts.maxDepth ?? 2;
|
|
1567
1558
|
const depth = opts._depth ?? 0;
|
|
@@ -1574,18 +1565,18 @@ async function fixJsonWithAI(jsonStr, options) {
|
|
|
1574
1565
|
openai = initializeOpenAI(apiKey);
|
|
1575
1566
|
}
|
|
1576
1567
|
const completion = await openai.chat.completions.create({
|
|
1577
|
-
model:
|
|
1568
|
+
model: 'gpt-5-mini',
|
|
1578
1569
|
messages: [
|
|
1579
1570
|
{
|
|
1580
|
-
role:
|
|
1581
|
-
content:
|
|
1571
|
+
role: 'system',
|
|
1572
|
+
content: 'You are a JSON fixer. Return only valid JSON without any additional text or explanation.'
|
|
1582
1573
|
},
|
|
1583
1574
|
{
|
|
1584
|
-
role:
|
|
1585
|
-
content: `Fix this broken JSON:\n${jsonStr}
|
|
1586
|
-
}
|
|
1575
|
+
role: 'user',
|
|
1576
|
+
content: `Fix this broken JSON:\n${jsonStr}`
|
|
1577
|
+
}
|
|
1587
1578
|
],
|
|
1588
|
-
response_format: { type:
|
|
1579
|
+
response_format: { type: 'json_object' }
|
|
1589
1580
|
});
|
|
1590
1581
|
const fixedJson = completion.choices[0]?.message?.content;
|
|
1591
1582
|
if (fixedJson && isValidJson(fixedJson)) {
|
|
@@ -1599,32 +1590,23 @@ async function fixJsonWithAI(jsonStr, options) {
|
|
|
1599
1590
|
_depth: depth + 1,
|
|
1600
1591
|
});
|
|
1601
1592
|
}
|
|
1602
|
-
throw new JsonParseError(
|
|
1593
|
+
throw new JsonParseError('Failed to fix JSON with AI: returned invalid JSON');
|
|
1603
1594
|
}
|
|
1604
1595
|
catch (err) {
|
|
1605
1596
|
// Re-throw JsonParseError as-is (e.g. max depth exceeded)
|
|
1606
1597
|
if (err instanceof JsonParseError) {
|
|
1607
1598
|
throw err;
|
|
1608
1599
|
}
|
|
1609
|
-
const errorMessage = err instanceof Error ? err.message :
|
|
1600
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
1610
1601
|
throw new JsonParseError(`Error fixing JSON with AI: ${errorMessage}`);
|
|
1611
1602
|
}
|
|
1612
1603
|
}
|
|
1613
1604
|
|
|
1614
1605
|
// llm-utils.ts
|
|
1615
1606
|
function normalizeModelName(model) {
|
|
1616
|
-
return model.replace(/_/g,
|
|
1617
|
-
}
|
|
1618
|
-
const CODE_BLOCK_TYPES = [
|
|
1619
|
-
"javascript",
|
|
1620
|
-
"js",
|
|
1621
|
-
"graphql",
|
|
1622
|
-
"json",
|
|
1623
|
-
"typescript",
|
|
1624
|
-
"python",
|
|
1625
|
-
"markdown",
|
|
1626
|
-
"yaml",
|
|
1627
|
-
];
|
|
1607
|
+
return model.replace(/_/g, '-').toLowerCase();
|
|
1608
|
+
}
|
|
1609
|
+
const CODE_BLOCK_TYPES = ['javascript', 'js', 'graphql', 'json', 'typescript', 'python', 'markdown', 'yaml'];
|
|
1628
1610
|
/**
|
|
1629
1611
|
* Tries to parse JSON from the given content string according to the specified
|
|
1630
1612
|
* response format. If the response format is 'json' or a JSON schema object, it
|
|
@@ -1648,9 +1630,7 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1648
1630
|
if (recursionDepth > maxRetries) {
|
|
1649
1631
|
throw new Error(`Maximum recursion depth (${maxRetries}) exceeded while parsing JSON. AI fixing may have returned broken JSON.`);
|
|
1650
1632
|
}
|
|
1651
|
-
if (responseFormat ===
|
|
1652
|
-
(typeof responseFormat === "object" &&
|
|
1653
|
-
responseFormat?.type === "json_schema")) {
|
|
1633
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat?.type === 'json_schema')) {
|
|
1654
1634
|
let cleanedContent = content.trim();
|
|
1655
1635
|
let detectedType = null;
|
|
1656
1636
|
// Remove code block markers if present
|
|
@@ -1661,15 +1641,24 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1661
1641
|
}
|
|
1662
1642
|
return false;
|
|
1663
1643
|
});
|
|
1664
|
-
if (startsWithCodeBlock && cleanedContent.endsWith(
|
|
1665
|
-
const firstLineEndIndex = cleanedContent.indexOf(
|
|
1644
|
+
if (startsWithCodeBlock && cleanedContent.endsWith('```')) {
|
|
1645
|
+
const firstLineEndIndex = cleanedContent.indexOf('\n');
|
|
1666
1646
|
if (firstLineEndIndex !== -1) {
|
|
1667
1647
|
cleanedContent = cleanedContent.slice(firstLineEndIndex + 1, -3).trim();
|
|
1668
1648
|
getLumicLogger().info(`Code block for type ${detectedType} detected and removed. Cleaned content: ${cleanedContent}`);
|
|
1669
1649
|
}
|
|
1670
1650
|
}
|
|
1671
|
-
//
|
|
1672
|
-
//
|
|
1651
|
+
// Whether the cleaned content structurally resembles JSON. Used to gate the
|
|
1652
|
+
// lenient fixBrokenJson strategy, which will otherwise coerce arbitrary
|
|
1653
|
+
// prose into a bogus object/array.
|
|
1654
|
+
const inputResemblesJson = cleanedContent.startsWith('{') || cleanedContent.startsWith('[');
|
|
1655
|
+
// Multiple JSON parsing strategies.
|
|
1656
|
+
// Default pipeline: direct parse -> extract from brackets -> remove
|
|
1657
|
+
// leading/trailing text -> GATED fixBrokenJson -> FAIL. fixBrokenJson is
|
|
1658
|
+
// deliberately gated (Strategy 4): its output is accepted only when the
|
|
1659
|
+
// input looked like JSON AND the repaired value round-trips through
|
|
1660
|
+
// JSON.stringify/JSON.parse, so a "successful" repair of non-JSON text is
|
|
1661
|
+
// rejected instead of silently returned as garbage.
|
|
1673
1662
|
const jsonParsingStrategies = [
|
|
1674
1663
|
// Strategy 1: Direct parsing
|
|
1675
1664
|
() => {
|
|
@@ -1695,7 +1684,7 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1695
1684
|
},
|
|
1696
1685
|
// Strategy 3: Remove leading/trailing text and try parsing
|
|
1697
1686
|
() => {
|
|
1698
|
-
const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g,
|
|
1687
|
+
const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g, '');
|
|
1699
1688
|
try {
|
|
1700
1689
|
return JSON.parse(jsonMatch);
|
|
1701
1690
|
}
|
|
@@ -1703,55 +1692,175 @@ async function parseResponse(content, responseFormat, options = {}) {
|
|
|
1703
1692
|
return null;
|
|
1704
1693
|
}
|
|
1705
1694
|
},
|
|
1706
|
-
// Strategy 4:
|
|
1695
|
+
// Strategy 4: Gated fixBrokenJson (string manipulation only, no AI)
|
|
1707
1696
|
() => {
|
|
1708
|
-
|
|
1697
|
+
if (!inputResemblesJson) {
|
|
1698
|
+
return null;
|
|
1699
|
+
}
|
|
1700
|
+
const fixed = fixBrokenJson(cleanedContent);
|
|
1701
|
+
if (fixed === null) {
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
// Round-trip guard: ensures the repaired value is genuinely
|
|
1706
|
+
// JSON-serializable before we trust it.
|
|
1707
|
+
return JSON.parse(JSON.stringify(fixed));
|
|
1708
|
+
}
|
|
1709
|
+
catch {
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1709
1712
|
},
|
|
1710
1713
|
];
|
|
1711
1714
|
// Try each parsing strategy
|
|
1712
1715
|
for (const strategy of jsonParsingStrategies) {
|
|
1713
|
-
const result =
|
|
1716
|
+
const result = strategy();
|
|
1714
1717
|
if (result !== null) {
|
|
1715
|
-
return result;
|
|
1718
|
+
return finalizeJsonResult(result, responseFormat);
|
|
1716
1719
|
}
|
|
1717
1720
|
}
|
|
1718
1721
|
// Strategy 5: Use AI fixing (only if explicitly enabled)
|
|
1719
1722
|
if (enableAiFix) {
|
|
1720
|
-
getLumicLogger().warn(
|
|
1723
|
+
getLumicLogger().warn('Using AI-powered JSON fixing. This adds cost and latency. Consider improving JSON generation instead.');
|
|
1724
|
+
let aiFixedJson = null;
|
|
1721
1725
|
try {
|
|
1722
|
-
|
|
1723
|
-
if (aiFixedJson !== null) {
|
|
1724
|
-
// Validate the AI-fixed JSON by attempting to stringify and re-parse it
|
|
1725
|
-
// This ensures it's actually valid JSON and not just something that passed fixJsonWithAI
|
|
1726
|
-
try {
|
|
1727
|
-
const validated = JSON.parse(JSON.stringify(aiFixedJson));
|
|
1728
|
-
return validated;
|
|
1729
|
-
}
|
|
1730
|
-
catch {
|
|
1731
|
-
// AI returned something that looks like JSON but isn't valid
|
|
1732
|
-
// Increment recursion depth and try parsing the AI response
|
|
1733
|
-
getLumicLogger().warn("AI fixing returned invalid JSON, attempting recursive parse...");
|
|
1734
|
-
return parseResponse(JSON.stringify(aiFixedJson), responseFormat, {
|
|
1735
|
-
...options,
|
|
1736
|
-
_recursionDepth: recursionDepth + 1,
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1726
|
+
aiFixedJson = await fixJsonWithAI(cleanedContent);
|
|
1740
1727
|
}
|
|
1741
1728
|
catch (error) {
|
|
1742
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
1729
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1743
1730
|
getLumicLogger().error(`AI JSON fixing failed: ${errorMessage}`);
|
|
1744
|
-
//
|
|
1731
|
+
// Fall through to the final error below.
|
|
1732
|
+
}
|
|
1733
|
+
if (aiFixedJson !== null) {
|
|
1734
|
+
let validated;
|
|
1735
|
+
try {
|
|
1736
|
+
// Validate the AI-fixed JSON by attempting to stringify and re-parse
|
|
1737
|
+
// it. This ensures it is actually valid JSON, not just something that
|
|
1738
|
+
// passed fixJsonWithAI.
|
|
1739
|
+
validated = JSON.parse(JSON.stringify(aiFixedJson));
|
|
1740
|
+
}
|
|
1741
|
+
catch {
|
|
1742
|
+
// AI returned something that looks like JSON but isn't valid.
|
|
1743
|
+
// Increment recursion depth and try parsing the AI response.
|
|
1744
|
+
getLumicLogger().warn('AI fixing returned invalid JSON, attempting recursive parse...');
|
|
1745
|
+
return parseResponse(JSON.stringify(aiFixedJson), responseFormat, {
|
|
1746
|
+
...options,
|
|
1747
|
+
_recursionDepth: recursionDepth + 1,
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
// Schema validation (json_schema format) happens here, OUTSIDE the
|
|
1751
|
+
// JSON.parse try/catch, so a schema mismatch propagates as a thrown
|
|
1752
|
+
// JsonParseError rather than being mistaken for invalid JSON.
|
|
1753
|
+
return finalizeJsonResult(validated, responseFormat);
|
|
1745
1754
|
}
|
|
1746
1755
|
}
|
|
1747
1756
|
// If all strategies fail, throw an error
|
|
1748
1757
|
getLumicLogger().error(`Failed to parse JSON from content: ${cleanedContent}. Original JSON: ${content}`);
|
|
1749
|
-
throw new Error(
|
|
1758
|
+
throw new Error('Unable to parse JSON response');
|
|
1750
1759
|
}
|
|
1751
1760
|
else {
|
|
1752
1761
|
return content;
|
|
1753
1762
|
}
|
|
1754
1763
|
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Reports the JSON type of a runtime value for schema-validation diagnostics.
|
|
1766
|
+
*
|
|
1767
|
+
* @param value The value whose JSON type should be described.
|
|
1768
|
+
* @returns One of `'null' | 'array' | 'object' | 'string' | 'number' | 'boolean' | 'undefined'`.
|
|
1769
|
+
*/
|
|
1770
|
+
function jsonTypeOf(value) {
|
|
1771
|
+
if (value === null)
|
|
1772
|
+
return 'null';
|
|
1773
|
+
if (Array.isArray(value))
|
|
1774
|
+
return 'array';
|
|
1775
|
+
return typeof value;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Extracts a declared primitive `type` string from a property schema, if present.
|
|
1779
|
+
*
|
|
1780
|
+
* @param propertySchema The (untyped) per-property JSON-Schema fragment.
|
|
1781
|
+
* @returns The declared type string, or null when none is declared.
|
|
1782
|
+
*/
|
|
1783
|
+
function extractSchemaType(propertySchema) {
|
|
1784
|
+
if (typeof propertySchema === 'object' && propertySchema !== null && 'type' in propertySchema) {
|
|
1785
|
+
const declared = propertySchema.type;
|
|
1786
|
+
return typeof declared === 'string' ? declared : null;
|
|
1787
|
+
}
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Checks whether a runtime value satisfies a declared JSON-Schema primitive
|
|
1792
|
+
* type. Unknown/unsupported type keywords are treated permissively (they never
|
|
1793
|
+
* cause a rejection), so this validator only ever rejects clear mismatches.
|
|
1794
|
+
*
|
|
1795
|
+
* @param value The runtime value to check.
|
|
1796
|
+
* @param expectedType The declared JSON-Schema type keyword.
|
|
1797
|
+
* @returns True when the value matches (or the keyword is unsupported).
|
|
1798
|
+
*/
|
|
1799
|
+
function valueMatchesJsonType(value, expectedType) {
|
|
1800
|
+
switch (expectedType) {
|
|
1801
|
+
case 'string':
|
|
1802
|
+
return typeof value === 'string';
|
|
1803
|
+
case 'number':
|
|
1804
|
+
return typeof value === 'number';
|
|
1805
|
+
case 'integer':
|
|
1806
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
1807
|
+
case 'boolean':
|
|
1808
|
+
return typeof value === 'boolean';
|
|
1809
|
+
case 'object':
|
|
1810
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1811
|
+
case 'array':
|
|
1812
|
+
return Array.isArray(value);
|
|
1813
|
+
case 'null':
|
|
1814
|
+
return value === null;
|
|
1815
|
+
default:
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Validates a parsed value against a `json_schema` object schema. Enforces that
|
|
1821
|
+
* the value is a plain object, that every `required` property is present, and
|
|
1822
|
+
* that present properties whose schema declares a primitive `type` match it.
|
|
1823
|
+
*
|
|
1824
|
+
* @param value The parsed value to validate.
|
|
1825
|
+
* @param schema The object schema supplied on the response format.
|
|
1826
|
+
* @throws {JsonParseError} When the value violates the schema.
|
|
1827
|
+
*/
|
|
1828
|
+
function validateJsonSchema(value, schema) {
|
|
1829
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1830
|
+
throw new JsonParseError(`JSON schema validation failed: expected an object but received ${jsonTypeOf(value)}.`);
|
|
1831
|
+
}
|
|
1832
|
+
const record = value;
|
|
1833
|
+
const missing = (schema.required ?? []).filter((key) => !(key in record));
|
|
1834
|
+
if (missing.length > 0) {
|
|
1835
|
+
throw new JsonParseError(`JSON schema validation failed: missing required properties: ${missing.join(', ')}.`);
|
|
1836
|
+
}
|
|
1837
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
1838
|
+
if (!(key in record))
|
|
1839
|
+
continue;
|
|
1840
|
+
const expectedType = extractSchemaType(propertySchema);
|
|
1841
|
+
if (expectedType !== null && !valueMatchesJsonType(record[key], expectedType)) {
|
|
1842
|
+
throw new JsonParseError(`JSON schema validation failed: property "${key}" expected type "${expectedType}" but received ${jsonTypeOf(record[key])}.`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Finalizes a successfully-parsed JSON value. When the caller supplied a
|
|
1848
|
+
* `json_schema` response format, the value is validated against that schema
|
|
1849
|
+
* before being returned so schema violations surface as a thrown
|
|
1850
|
+
* {@link JsonParseError} instead of flowing silently downstream. For plain
|
|
1851
|
+
* `json` formats the value is returned unchanged.
|
|
1852
|
+
*
|
|
1853
|
+
* @param value The parsed value produced by a parsing strategy.
|
|
1854
|
+
* @param responseFormat The originally requested response format.
|
|
1855
|
+
* @returns The value narrowed to `T`.
|
|
1856
|
+
* @throws {JsonParseError} When a supplied json_schema is not satisfied.
|
|
1857
|
+
*/
|
|
1858
|
+
function finalizeJsonResult(value, responseFormat) {
|
|
1859
|
+
if (typeof responseFormat === 'object' && responseFormat?.type === 'json_schema') {
|
|
1860
|
+
validateJsonSchema(value, responseFormat.schema);
|
|
1861
|
+
}
|
|
1862
|
+
return value;
|
|
1863
|
+
}
|
|
1755
1864
|
|
|
1756
1865
|
/**
|
|
1757
1866
|
* Default timeout values (in milliseconds) for external service calls.
|
|
@@ -2023,11 +2132,19 @@ const DEFAULT_RETRY_CONFIG = {
|
|
|
2023
2132
|
* @throws The last error encountered if all retry attempts fail or if a non-retryable error occurs
|
|
2024
2133
|
*/
|
|
2025
2134
|
async function withRetry(fn, config = {}, label = 'unknown') {
|
|
2026
|
-
const { maxRetries, baseDelayMs, maxDelayMs, retryableErrors, circuitBreaker } = {
|
|
2135
|
+
const { maxRetries, baseDelayMs, maxDelayMs, retryableErrors, circuitBreaker, signal } = {
|
|
2027
2136
|
...DEFAULT_RETRY_CONFIG,
|
|
2028
2137
|
...config,
|
|
2029
2138
|
};
|
|
2030
2139
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2140
|
+
// Short-circuit on cancellation BEFORE starting a fresh attempt so
|
|
2141
|
+
// the caller does not pay the cost of issuing a request whose result
|
|
2142
|
+
// they have already discarded.
|
|
2143
|
+
if (signal?.aborted) {
|
|
2144
|
+
throw signal.reason instanceof Error
|
|
2145
|
+
? signal.reason
|
|
2146
|
+
: new Error('Aborted');
|
|
2147
|
+
}
|
|
2031
2148
|
try {
|
|
2032
2149
|
// If a circuit breaker is provided, execute through it
|
|
2033
2150
|
const result = circuitBreaker
|
|
@@ -2040,6 +2157,13 @@ async function withRetry(fn, config = {}, label = 'unknown') {
|
|
|
2040
2157
|
if (error instanceof CircuitBreakerOpenError) {
|
|
2041
2158
|
throw error;
|
|
2042
2159
|
}
|
|
2160
|
+
// Do not retry on cooperative cancellation — the caller has
|
|
2161
|
+
// explicitly asked us to stop.
|
|
2162
|
+
if (signal?.aborted) {
|
|
2163
|
+
throw signal.reason instanceof Error
|
|
2164
|
+
? signal.reason
|
|
2165
|
+
: new Error('Aborted');
|
|
2166
|
+
}
|
|
2043
2167
|
// Don't retry if we've exhausted all attempts
|
|
2044
2168
|
if (attempt === maxRetries) {
|
|
2045
2169
|
throw error;
|
|
@@ -2053,7 +2177,26 @@ async function withRetry(fn, config = {}, label = 'unknown') {
|
|
|
2053
2177
|
const jitter = Math.random() * 1000;
|
|
2054
2178
|
const delay = Math.min(exponentialDelay + jitter, maxDelayMs);
|
|
2055
2179
|
getLumicLogger().warn(`[${label}] Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms`);
|
|
2056
|
-
|
|
2180
|
+
// Sleep but bail early if signal aborts mid-backoff.
|
|
2181
|
+
await new Promise((resolve, reject) => {
|
|
2182
|
+
const timer = setTimeout(() => {
|
|
2183
|
+
if (signal)
|
|
2184
|
+
signal.removeEventListener('abort', onAbort);
|
|
2185
|
+
resolve();
|
|
2186
|
+
}, delay);
|
|
2187
|
+
const onAbort = () => {
|
|
2188
|
+
clearTimeout(timer);
|
|
2189
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error('Aborted'));
|
|
2190
|
+
};
|
|
2191
|
+
if (signal) {
|
|
2192
|
+
if (signal.aborted) {
|
|
2193
|
+
clearTimeout(timer);
|
|
2194
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('Aborted'));
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2057
2200
|
}
|
|
2058
2201
|
}
|
|
2059
2202
|
// This should never be reached, but TypeScript needs it for type safety
|
|
@@ -2340,15 +2483,13 @@ const isRetryableLLMError = (error) => {
|
|
|
2340
2483
|
if (error instanceof Error) {
|
|
2341
2484
|
const message = error.message;
|
|
2342
2485
|
// Retry on rate limits (429)
|
|
2343
|
-
if (message.includes(
|
|
2344
|
-
message.includes("rate limit") ||
|
|
2345
|
-
message.includes("Rate limit")) {
|
|
2486
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
2346
2487
|
return true;
|
|
2347
2488
|
}
|
|
2348
2489
|
// Retry on transient body-corruption 400s. Match the exact OpenAI error
|
|
2349
2490
|
// string to avoid retrying genuine client-side validation 400s (which
|
|
2350
2491
|
// would re-fail forever and waste retry budget).
|
|
2351
|
-
if (message.includes(
|
|
2492
|
+
if (message.includes('could not parse the JSON body of your request')) {
|
|
2352
2493
|
return true;
|
|
2353
2494
|
}
|
|
2354
2495
|
}
|
|
@@ -2417,26 +2558,23 @@ function isReasoningModel(model) {
|
|
|
2417
2558
|
* @throws Error if the response format is invalid or incompatible.
|
|
2418
2559
|
*/
|
|
2419
2560
|
function getResponseFormatOption(responseFormat, normalizedModel) {
|
|
2420
|
-
if (responseFormat ===
|
|
2421
|
-
return { type:
|
|
2561
|
+
if (responseFormat === 'text') {
|
|
2562
|
+
return { type: 'text' };
|
|
2422
2563
|
}
|
|
2423
|
-
if (responseFormat ===
|
|
2424
|
-
return supportsJsonMode(normalizedModel)
|
|
2425
|
-
? { type: "json_object" }
|
|
2426
|
-
: { type: "text" };
|
|
2564
|
+
if (responseFormat === 'json') {
|
|
2565
|
+
return supportsJsonMode(normalizedModel) ? { type: 'json_object' } : { type: 'text' };
|
|
2427
2566
|
}
|
|
2428
|
-
if (typeof responseFormat ===
|
|
2429
|
-
responseFormat.type === "json_schema") {
|
|
2567
|
+
if (typeof responseFormat === 'object' && responseFormat.type === 'json_schema') {
|
|
2430
2568
|
if (!isStructuredOutputCompatibleModel(normalizedModel)) {
|
|
2431
2569
|
throw new Error(`${normalizedModel} is not compatible with structured outputs / json object.`);
|
|
2432
2570
|
}
|
|
2433
2571
|
return {
|
|
2434
|
-
type:
|
|
2572
|
+
type: 'json_schema',
|
|
2435
2573
|
json_schema: {
|
|
2436
|
-
type:
|
|
2574
|
+
type: 'object',
|
|
2437
2575
|
properties: responseFormat.schema.properties,
|
|
2438
2576
|
required: responseFormat.schema.required || [],
|
|
2439
|
-
name:
|
|
2577
|
+
name: 'Response',
|
|
2440
2578
|
},
|
|
2441
2579
|
};
|
|
2442
2580
|
}
|
|
@@ -2495,7 +2633,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2495
2633
|
const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
|
|
2496
2634
|
const apiKey = options.apiKey || getSecrets().openai.apiKey;
|
|
2497
2635
|
if (!apiKey) {
|
|
2498
|
-
throw new Error(
|
|
2636
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
2499
2637
|
}
|
|
2500
2638
|
const openai = new OpenAI({
|
|
2501
2639
|
apiKey: apiKey,
|
|
@@ -2505,7 +2643,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2505
2643
|
// Add developer prompt if present
|
|
2506
2644
|
if (options.developerPrompt && supportsDeveloperPrompt(normalizedModel)) {
|
|
2507
2645
|
messages.push({
|
|
2508
|
-
role:
|
|
2646
|
+
role: 'developer',
|
|
2509
2647
|
content: options.developerPrompt,
|
|
2510
2648
|
});
|
|
2511
2649
|
}
|
|
@@ -2514,15 +2652,15 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2514
2652
|
messages.push(...options.context);
|
|
2515
2653
|
}
|
|
2516
2654
|
// Add user content
|
|
2517
|
-
if (typeof content ===
|
|
2655
|
+
if (typeof content === 'string') {
|
|
2518
2656
|
messages.push({
|
|
2519
|
-
role:
|
|
2657
|
+
role: 'user',
|
|
2520
2658
|
content,
|
|
2521
2659
|
});
|
|
2522
2660
|
}
|
|
2523
2661
|
else {
|
|
2524
2662
|
messages.push({
|
|
2525
|
-
role:
|
|
2663
|
+
role: 'user',
|
|
2526
2664
|
content,
|
|
2527
2665
|
});
|
|
2528
2666
|
}
|
|
@@ -2536,8 +2674,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2536
2674
|
queryOptions.tools = options.tools;
|
|
2537
2675
|
}
|
|
2538
2676
|
// Only include temperature if the model supports it
|
|
2539
|
-
if (options.temperature !== undefined &&
|
|
2540
|
-
supportsTemperature(normalizedModel)) {
|
|
2677
|
+
if (options.temperature !== undefined && supportsTemperature(normalizedModel)) {
|
|
2541
2678
|
queryOptions.temperature = options.temperature;
|
|
2542
2679
|
}
|
|
2543
2680
|
// Only include max_completion_tokens when specified
|
|
@@ -2547,16 +2684,25 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2547
2684
|
// Only set response_format when it's not the default 'text' type
|
|
2548
2685
|
// (sending response_format: {type: 'text'} is redundant and may cause issues)
|
|
2549
2686
|
const responseFormatOption = getResponseFormatOption(responseFormat, normalizedModel);
|
|
2550
|
-
if (responseFormatOption.type !==
|
|
2687
|
+
if (responseFormatOption.type !== 'text') {
|
|
2551
2688
|
queryOptions.response_format = responseFormatOption;
|
|
2552
2689
|
}
|
|
2553
2690
|
let completion;
|
|
2554
2691
|
try {
|
|
2555
|
-
completion = await withRetry(
|
|
2692
|
+
completion = await withRetry(
|
|
2693
|
+
// Pass the AbortSignal into the OpenAI SDK request-options object.
|
|
2694
|
+
// The OpenAI SDK honours `signal` to cancel the in-flight fetch
|
|
2695
|
+
// immediately when it fires — this is what makes caller-driven
|
|
2696
|
+
// cancel-and-restart (e.g. signal-monitoring's materiality
|
|
2697
|
+
// re-trigger path in the engine) actually release the HTTP
|
|
2698
|
+
// connection and response-body buffer rather than just discarding
|
|
2699
|
+
// the resolved value at the engine boundary.
|
|
2700
|
+
() => openai.chat.completions.create(queryOptions, options.signal ? { signal: options.signal } : undefined), {
|
|
2556
2701
|
maxRetries: 3,
|
|
2557
2702
|
baseDelayMs: 2000,
|
|
2558
2703
|
maxDelayMs: 30000,
|
|
2559
2704
|
retryableErrors: isRetryableLLMError,
|
|
2705
|
+
signal: options.signal,
|
|
2560
2706
|
}, `OpenAI:${normalizedModel}`);
|
|
2561
2707
|
}
|
|
2562
2708
|
catch (error) {
|
|
@@ -2568,19 +2714,15 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2568
2714
|
// for production prompts containing sensitive context.
|
|
2569
2715
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2570
2716
|
const totalContentChars = messages.reduce((sum, msg) => {
|
|
2571
|
-
if (typeof msg.content ===
|
|
2717
|
+
if (typeof msg.content === 'string')
|
|
2572
2718
|
return sum + msg.content.length;
|
|
2573
2719
|
if (Array.isArray(msg.content)) {
|
|
2574
|
-
return
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
return s + part.text.length;
|
|
2581
|
-
}
|
|
2582
|
-
return s;
|
|
2583
|
-
}, 0));
|
|
2720
|
+
return sum + msg.content.reduce((s, part) => {
|
|
2721
|
+
if (typeof part === 'object' && part !== null && 'text' in part && typeof part.text === 'string') {
|
|
2722
|
+
return s + part.text.length;
|
|
2723
|
+
}
|
|
2724
|
+
return s;
|
|
2725
|
+
}, 0);
|
|
2584
2726
|
}
|
|
2585
2727
|
return sum;
|
|
2586
2728
|
}, 0);
|
|
@@ -2607,7 +2749,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2607
2749
|
const cachedTokens = completion.usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
2608
2750
|
const response = {
|
|
2609
2751
|
id: completion.id,
|
|
2610
|
-
content: completion.choices[0]?.message?.content ||
|
|
2752
|
+
content: completion.choices[0]?.message?.content || '',
|
|
2611
2753
|
tool_calls: completion.choices[0]?.message?.tool_calls,
|
|
2612
2754
|
usage: {
|
|
2613
2755
|
prompt_tokens: completion.usage?.prompt_tokens ?? 0,
|
|
@@ -2617,7 +2759,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2617
2759
|
},
|
|
2618
2760
|
system_fingerprint: completion.system_fingerprint,
|
|
2619
2761
|
service_tier: options.service_tier,
|
|
2620
|
-
provider:
|
|
2762
|
+
provider: 'openai',
|
|
2621
2763
|
model: normalizedModel,
|
|
2622
2764
|
};
|
|
2623
2765
|
return response;
|
|
@@ -2630,7 +2772,7 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
|
|
|
2630
2772
|
* @param options The options for the LLM call. Defaults to an empty object.
|
|
2631
2773
|
* @return A promise that resolves to the response from the LLM.
|
|
2632
2774
|
*/
|
|
2633
|
-
const makeOpenAIChatCompletionCall = async (content, responseFormat =
|
|
2775
|
+
const makeOpenAIChatCompletionCall = async (content, responseFormat = 'text', options = {}) => {
|
|
2634
2776
|
const mergedOptions = {
|
|
2635
2777
|
...DEFAULT_OPTIONS$1,
|
|
2636
2778
|
...options,
|
|
@@ -2639,7 +2781,7 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2639
2781
|
// Track cost in the global cost tracker. Pass cached tokens through so the
|
|
2640
2782
|
// tracker applies the discounted cached-input rate (typically ~50% of the
|
|
2641
2783
|
// standard input rate) instead of billing every input token at full price.
|
|
2642
|
-
getLLMCostTracker().trackUsage(
|
|
2784
|
+
getLLMCostTracker().trackUsage('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens);
|
|
2643
2785
|
// Handle tool calls differently
|
|
2644
2786
|
if (completion.tool_calls && completion.tool_calls.length > 0) {
|
|
2645
2787
|
const toolCallResponse = {
|
|
@@ -2655,10 +2797,10 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2655
2797
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
2656
2798
|
completion_tokens: completion.usage.completion_tokens,
|
|
2657
2799
|
reasoning_tokens: 0,
|
|
2658
|
-
provider:
|
|
2800
|
+
provider: 'openai',
|
|
2659
2801
|
model: completion.model,
|
|
2660
2802
|
cached_tokens: completion.usage.cached_tokens,
|
|
2661
|
-
cost: calculateCost(
|
|
2803
|
+
cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
2662
2804
|
},
|
|
2663
2805
|
tool_calls: completion.tool_calls,
|
|
2664
2806
|
};
|
|
@@ -2666,7 +2808,7 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2666
2808
|
// Handle regular responses
|
|
2667
2809
|
const parsedResponse = await parseResponse(completion.content, responseFormat);
|
|
2668
2810
|
if (parsedResponse === null) {
|
|
2669
|
-
throw new Error(
|
|
2811
|
+
throw new Error('Failed to parse response');
|
|
2670
2812
|
}
|
|
2671
2813
|
return {
|
|
2672
2814
|
response: parsedResponse,
|
|
@@ -2674,10 +2816,10 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = "text", op
|
|
|
2674
2816
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
2675
2817
|
completion_tokens: completion.usage.completion_tokens,
|
|
2676
2818
|
reasoning_tokens: 0,
|
|
2677
|
-
provider:
|
|
2819
|
+
provider: 'openai',
|
|
2678
2820
|
model: completion.model,
|
|
2679
2821
|
cached_tokens: completion.usage.cached_tokens,
|
|
2680
|
-
cost: calculateCost(
|
|
2822
|
+
cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
2681
2823
|
},
|
|
2682
2824
|
tool_calls: completion.tool_calls,
|
|
2683
2825
|
};
|
|
@@ -2712,44 +2854,45 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2712
2854
|
const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
|
|
2713
2855
|
const apiKey = options.apiKey || getSecrets().openai.apiKey;
|
|
2714
2856
|
if (!apiKey) {
|
|
2715
|
-
throw new Error(
|
|
2857
|
+
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
2716
2858
|
}
|
|
2717
2859
|
const openai = new OpenAI({
|
|
2718
2860
|
apiKey: apiKey,
|
|
2719
2861
|
timeout: options.timeout ?? LLM_TIMEOUT_MS,
|
|
2720
2862
|
});
|
|
2721
|
-
// Remove apiKey, model, and
|
|
2722
|
-
const { apiKey: _, model: __, timeout: ___, ...cleanOptions } = options;
|
|
2863
|
+
// Remove apiKey, model, timeout, and signal from options before creating request body
|
|
2864
|
+
const { apiKey: _, model: __, timeout: ___, signal: ____, ...cleanOptions } = options;
|
|
2723
2865
|
const requestBody = {
|
|
2724
2866
|
model: normalizedModel,
|
|
2725
2867
|
input,
|
|
2726
2868
|
...cleanOptions,
|
|
2727
2869
|
};
|
|
2728
2870
|
// Make the API call to the Responses endpoint
|
|
2729
|
-
const response = await withRetry(() => openai.responses.create(requestBody), {
|
|
2871
|
+
const response = await withRetry(() => openai.responses.create(requestBody, options.signal ? { signal: options.signal } : undefined), {
|
|
2730
2872
|
maxRetries: 3,
|
|
2731
2873
|
baseDelayMs: 2000,
|
|
2732
2874
|
maxDelayMs: 30000,
|
|
2733
2875
|
retryableErrors: isRetryableLLMError,
|
|
2876
|
+
signal: options.signal,
|
|
2734
2877
|
}, `OpenAI-Responses:${normalizedModel}`);
|
|
2735
2878
|
// Responses API exposes cached input tokens under `input_tokens_details.cached_tokens`
|
|
2736
2879
|
// (the equivalent of Chat Completions' `prompt_tokens_details.cached_tokens`).
|
|
2737
2880
|
const responsesCachedTokens = response.usage?.input_tokens_details?.cached_tokens || 0;
|
|
2738
2881
|
// Track cost in the global cost tracker
|
|
2739
|
-
getLLMCostTracker().trackUsage(
|
|
2882
|
+
getLLMCostTracker().trackUsage('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens);
|
|
2740
2883
|
// Extract tool calls from the output
|
|
2741
2884
|
const toolCalls = response.output
|
|
2742
|
-
?.filter((item) => item.type ===
|
|
2885
|
+
?.filter((item) => item.type === 'function_call')
|
|
2743
2886
|
.map((toolCall) => ({
|
|
2744
2887
|
id: toolCall.call_id,
|
|
2745
|
-
type:
|
|
2888
|
+
type: 'function',
|
|
2746
2889
|
function: {
|
|
2747
2890
|
name: toolCall.name,
|
|
2748
2891
|
arguments: toolCall.arguments,
|
|
2749
2892
|
},
|
|
2750
2893
|
}));
|
|
2751
2894
|
// Extract code interpreter outputs if present
|
|
2752
|
-
const codeInterpreterCalls = response.output?.filter((item) => item.type ===
|
|
2895
|
+
const codeInterpreterCalls = response.output?.filter((item) => item.type === 'code_interpreter_call');
|
|
2753
2896
|
let codeInterpreterOutputs = undefined;
|
|
2754
2897
|
if (codeInterpreterCalls && codeInterpreterCalls.length > 0) {
|
|
2755
2898
|
// Each code_interpreter_call has an 'outputs' array property
|
|
@@ -2775,34 +2918,32 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2775
2918
|
prompt_tokens: response.usage?.input_tokens || 0,
|
|
2776
2919
|
completion_tokens: response.usage?.output_tokens || 0,
|
|
2777
2920
|
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
2778
|
-
provider:
|
|
2921
|
+
provider: 'openai',
|
|
2779
2922
|
model: normalizedModel,
|
|
2780
2923
|
cached_tokens: responsesCachedTokens,
|
|
2781
|
-
cost: calculateCost(
|
|
2924
|
+
cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens),
|
|
2782
2925
|
},
|
|
2783
2926
|
tool_calls: toolCalls,
|
|
2784
|
-
...(codeInterpreterOutputs
|
|
2785
|
-
? { code_interpreter_outputs: codeInterpreterOutputs }
|
|
2786
|
-
: {}),
|
|
2927
|
+
...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
|
|
2787
2928
|
};
|
|
2788
2929
|
}
|
|
2789
2930
|
// Extract text content from the response output
|
|
2790
2931
|
const textContent = response.output
|
|
2791
|
-
?.filter((item) => item.type ===
|
|
2932
|
+
?.filter((item) => item.type === 'message')
|
|
2792
2933
|
.map((item) => item.content
|
|
2793
|
-
.filter((content) => content.type ===
|
|
2934
|
+
.filter((content) => content.type === 'output_text')
|
|
2794
2935
|
.map((content) => content.text)
|
|
2795
|
-
.join(
|
|
2796
|
-
.join(
|
|
2936
|
+
.join(''))
|
|
2937
|
+
.join('') || '';
|
|
2797
2938
|
// Determine the format for parsing the response
|
|
2798
|
-
let parsingFormat =
|
|
2799
|
-
if (requestBody.text?.format?.type ===
|
|
2800
|
-
parsingFormat =
|
|
2939
|
+
let parsingFormat = 'text';
|
|
2940
|
+
if (requestBody.text?.format?.type === 'json_object') {
|
|
2941
|
+
parsingFormat = 'json';
|
|
2801
2942
|
}
|
|
2802
2943
|
// Handle regular responses
|
|
2803
2944
|
const parsedResponse = await parseResponse(textContent, parsingFormat);
|
|
2804
2945
|
if (parsedResponse === null) {
|
|
2805
|
-
throw new Error(
|
|
2946
|
+
throw new Error('Failed to parse response from Responses API');
|
|
2806
2947
|
}
|
|
2807
2948
|
return {
|
|
2808
2949
|
response: parsedResponse,
|
|
@@ -2810,15 +2951,13 @@ const makeResponsesAPICall = async (input, options = {}) => {
|
|
|
2810
2951
|
prompt_tokens: response.usage?.input_tokens || 0,
|
|
2811
2952
|
completion_tokens: response.usage?.output_tokens || 0,
|
|
2812
2953
|
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
2813
|
-
provider:
|
|
2954
|
+
provider: 'openai',
|
|
2814
2955
|
model: normalizedModel,
|
|
2815
2956
|
cached_tokens: responsesCachedTokens,
|
|
2816
|
-
cost: calculateCost(
|
|
2957
|
+
cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens),
|
|
2817
2958
|
},
|
|
2818
2959
|
tool_calls: toolCalls,
|
|
2819
|
-
...(codeInterpreterOutputs
|
|
2820
|
-
? { code_interpreter_outputs: codeInterpreterOutputs }
|
|
2821
|
-
: {}),
|
|
2960
|
+
...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
|
|
2822
2961
|
};
|
|
2823
2962
|
};
|
|
2824
2963
|
|
|
@@ -8128,12 +8267,12 @@ function sanitizeObject(obj, sensitiveFields = ['apiKey', 'token', 'secret', 'pa
|
|
|
8128
8267
|
const isRetryableAnthropicError = (error) => {
|
|
8129
8268
|
if (error instanceof Error) {
|
|
8130
8269
|
const message = error.message;
|
|
8131
|
-
if (message.includes(
|
|
8132
|
-
message.includes(
|
|
8133
|
-
message.includes(
|
|
8134
|
-
message.includes(
|
|
8135
|
-
message.includes(
|
|
8136
|
-
message.includes(
|
|
8270
|
+
if (message.includes('429') ||
|
|
8271
|
+
message.includes('500') ||
|
|
8272
|
+
message.includes('503') ||
|
|
8273
|
+
message.includes('529') ||
|
|
8274
|
+
message.includes('rate limit') ||
|
|
8275
|
+
message.includes('overloaded')) {
|
|
8137
8276
|
return true;
|
|
8138
8277
|
}
|
|
8139
8278
|
}
|
|
@@ -8148,11 +8287,8 @@ const isRetryableAnthropicError = (error) => {
|
|
|
8148
8287
|
function translateToolsToAnthropic(tools) {
|
|
8149
8288
|
return tools.map((tool) => ({
|
|
8150
8289
|
name: tool.function.name,
|
|
8151
|
-
description: tool.function.description ||
|
|
8152
|
-
input_schema: (tool.function.parameters || {
|
|
8153
|
-
type: "object",
|
|
8154
|
-
properties: {},
|
|
8155
|
-
}),
|
|
8290
|
+
description: tool.function.description || '',
|
|
8291
|
+
input_schema: (tool.function.parameters || { type: 'object', properties: {} }),
|
|
8156
8292
|
}));
|
|
8157
8293
|
}
|
|
8158
8294
|
/**
|
|
@@ -8165,31 +8301,27 @@ function translateContextToAnthropic(context) {
|
|
|
8165
8301
|
const systemParts = [];
|
|
8166
8302
|
const messages = [];
|
|
8167
8303
|
for (const msg of context) {
|
|
8168
|
-
if (msg.role ===
|
|
8169
|
-
if (typeof msg.content ===
|
|
8304
|
+
if (msg.role === 'system' || msg.role === 'developer') {
|
|
8305
|
+
if (typeof msg.content === 'string') {
|
|
8170
8306
|
systemParts.push(msg.content);
|
|
8171
8307
|
}
|
|
8172
8308
|
continue;
|
|
8173
8309
|
}
|
|
8174
|
-
if (msg.role ===
|
|
8175
|
-
const content = typeof msg.content ===
|
|
8310
|
+
if (msg.role === 'user') {
|
|
8311
|
+
const content = typeof msg.content === 'string'
|
|
8176
8312
|
? msg.content
|
|
8177
8313
|
: JSON.stringify(msg.content);
|
|
8178
|
-
messages.push({ role:
|
|
8314
|
+
messages.push({ role: 'user', content });
|
|
8179
8315
|
continue;
|
|
8180
8316
|
}
|
|
8181
|
-
if (msg.role ===
|
|
8317
|
+
if (msg.role === 'assistant') {
|
|
8182
8318
|
const assistantMsg = msg;
|
|
8183
8319
|
// If the assistant message has tool_calls, translate to Anthropic tool_use blocks
|
|
8184
8320
|
if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
|
|
8185
8321
|
const contentBlocks = [];
|
|
8186
8322
|
// Include text content if present
|
|
8187
|
-
if (typeof assistantMsg.content ===
|
|
8188
|
-
assistantMsg.content
|
|
8189
|
-
contentBlocks.push({
|
|
8190
|
-
type: "text",
|
|
8191
|
-
text: assistantMsg.content,
|
|
8192
|
-
});
|
|
8323
|
+
if (typeof assistantMsg.content === 'string' && assistantMsg.content.trim()) {
|
|
8324
|
+
contentBlocks.push({ type: 'text', text: assistantMsg.content });
|
|
8193
8325
|
}
|
|
8194
8326
|
// Translate each tool_call to a tool_use block
|
|
8195
8327
|
for (const tc of assistantMsg.tool_calls) {
|
|
@@ -8201,36 +8333,32 @@ function translateContextToAnthropic(context) {
|
|
|
8201
8333
|
input = { raw: tc.function.arguments };
|
|
8202
8334
|
}
|
|
8203
8335
|
contentBlocks.push({
|
|
8204
|
-
type:
|
|
8336
|
+
type: 'tool_use',
|
|
8205
8337
|
id: tc.id,
|
|
8206
8338
|
name: tc.function.name,
|
|
8207
8339
|
input,
|
|
8208
8340
|
});
|
|
8209
8341
|
}
|
|
8210
|
-
messages.push({ role:
|
|
8342
|
+
messages.push({ role: 'assistant', content: contentBlocks });
|
|
8211
8343
|
}
|
|
8212
8344
|
else {
|
|
8213
|
-
const content = typeof assistantMsg.content ===
|
|
8345
|
+
const content = typeof assistantMsg.content === 'string'
|
|
8214
8346
|
? assistantMsg.content
|
|
8215
8347
|
: JSON.stringify(assistantMsg.content);
|
|
8216
|
-
messages.push({ role:
|
|
8348
|
+
messages.push({ role: 'assistant', content });
|
|
8217
8349
|
}
|
|
8218
8350
|
continue;
|
|
8219
8351
|
}
|
|
8220
|
-
if (msg.role ===
|
|
8352
|
+
if (msg.role === 'tool') {
|
|
8221
8353
|
// Anthropic expects tool results as user messages with tool_result content blocks
|
|
8222
8354
|
const toolMsg = msg;
|
|
8223
8355
|
messages.push({
|
|
8224
|
-
role:
|
|
8225
|
-
content: [
|
|
8226
|
-
|
|
8227
|
-
type: "tool_result",
|
|
8356
|
+
role: 'user',
|
|
8357
|
+
content: [{
|
|
8358
|
+
type: 'tool_result',
|
|
8228
8359
|
tool_use_id: toolMsg.tool_call_id,
|
|
8229
|
-
content: typeof toolMsg.content ===
|
|
8230
|
-
|
|
8231
|
-
: JSON.stringify(toolMsg.content),
|
|
8232
|
-
},
|
|
8233
|
-
],
|
|
8360
|
+
content: typeof toolMsg.content === 'string' ? toolMsg.content : JSON.stringify(toolMsg.content),
|
|
8361
|
+
}],
|
|
8234
8362
|
});
|
|
8235
8363
|
continue;
|
|
8236
8364
|
}
|
|
@@ -8251,13 +8379,13 @@ function translateContextToAnthropic(context) {
|
|
|
8251
8379
|
merged.push({ role: msg.role, content: toContentBlocks(msg.content) });
|
|
8252
8380
|
}
|
|
8253
8381
|
}
|
|
8254
|
-
return { messages: merged, systemText: systemParts.join(
|
|
8382
|
+
return { messages: merged, systemText: systemParts.join('\n\n') };
|
|
8255
8383
|
}
|
|
8256
8384
|
/** Convert string or content block array to a uniform content block array. */
|
|
8257
8385
|
function toContentBlocks(content) {
|
|
8258
|
-
if (typeof content ===
|
|
8386
|
+
if (typeof content === 'string') {
|
|
8259
8387
|
const textBlock = {
|
|
8260
|
-
type:
|
|
8388
|
+
type: 'text',
|
|
8261
8389
|
text: content,
|
|
8262
8390
|
citations: null,
|
|
8263
8391
|
};
|
|
@@ -8277,11 +8405,11 @@ function toContentBlocks(content) {
|
|
|
8277
8405
|
* @param options The options for the LLM call.
|
|
8278
8406
|
* @return A promise that resolves to the response from the Anthropic API.
|
|
8279
8407
|
*/
|
|
8280
|
-
async function makeAnthropicCall(content, responseFormat =
|
|
8281
|
-
const model = (options.model ||
|
|
8408
|
+
async function makeAnthropicCall(content, responseFormat = 'text', options = {}) {
|
|
8409
|
+
const model = (options.model || 'claude-sonnet-4-6');
|
|
8282
8410
|
const apiKey = options.apiKey || getSecrets().anthropic.apiKey;
|
|
8283
8411
|
if (!apiKey) {
|
|
8284
|
-
throw new Error(
|
|
8412
|
+
throw new Error('Anthropic API key is not provided and ANTHROPIC_API_KEY environment variable is not set');
|
|
8285
8413
|
}
|
|
8286
8414
|
const client = new Anthropic({
|
|
8287
8415
|
apiKey,
|
|
@@ -8293,10 +8421,8 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8293
8421
|
systemParts.push(options.developerPrompt);
|
|
8294
8422
|
}
|
|
8295
8423
|
// If JSON response is requested, instruct the model via system prompt
|
|
8296
|
-
if (responseFormat ===
|
|
8297
|
-
(
|
|
8298
|
-
responseFormat.type === "json_schema")) {
|
|
8299
|
-
systemParts.push("You MUST respond with valid JSON only. No markdown, no code blocks, no explanatory text — just the raw JSON object or array.");
|
|
8424
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) {
|
|
8425
|
+
systemParts.push('You MUST respond with valid JSON only. No markdown, no code blocks, no explanatory text — just the raw JSON object or array.');
|
|
8300
8426
|
}
|
|
8301
8427
|
// Build messages array
|
|
8302
8428
|
const messages = [];
|
|
@@ -8309,7 +8435,7 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8309
8435
|
messages.push(...translated.messages);
|
|
8310
8436
|
}
|
|
8311
8437
|
// Add user content
|
|
8312
|
-
messages.push({ role:
|
|
8438
|
+
messages.push({ role: 'user', content });
|
|
8313
8439
|
// Build request params
|
|
8314
8440
|
const requestParams = {
|
|
8315
8441
|
model,
|
|
@@ -8318,37 +8444,46 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8318
8444
|
};
|
|
8319
8445
|
// Add system prompt if present
|
|
8320
8446
|
if (systemParts.length > 0) {
|
|
8321
|
-
requestParams.system = systemParts.join(
|
|
8322
|
-
}
|
|
8323
|
-
//
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8447
|
+
requestParams.system = systemParts.join('\n\n');
|
|
8448
|
+
}
|
|
8449
|
+
// Sampling controls are intentionally disabled for the Anthropic provider.
|
|
8450
|
+
//
|
|
8451
|
+
// The claude-*-4.x model family rejects any request that specifies BOTH
|
|
8452
|
+
// `temperature` and `top_p` ("`temperature` and `top_p` cannot both be
|
|
8453
|
+
// specified for this model. Please use only one." — HTTP 400), which aborts
|
|
8454
|
+
// the entire caller flow (e.g. the engine's account decision session).
|
|
8455
|
+
// Callers across the org pass both knobs generically, so rather than make
|
|
8456
|
+
// each one provider-aware we drop both here and let Anthropic use its model
|
|
8457
|
+
// defaults. `options.temperature` / `options.top_p` are intentionally not
|
|
8458
|
+
// forwarded. This is the Anthropic adapter only — the OpenAI, DeepSeek, and
|
|
8459
|
+
// OpenAI-compatible adapters still honor `temperature` and `top_p`.
|
|
8330
8460
|
// Translate and add tools if present
|
|
8331
8461
|
if (options.tools && options.tools.length > 0) {
|
|
8332
8462
|
requestParams.tools = translateToolsToAnthropic(options.tools);
|
|
8333
8463
|
}
|
|
8334
8464
|
try {
|
|
8335
|
-
const response = await withRetry(
|
|
8465
|
+
const response = await withRetry(
|
|
8466
|
+
// Pass AbortSignal into the Anthropic SDK request-options object
|
|
8467
|
+
// so caller-driven cancellation interrupts the in-flight HTTP
|
|
8468
|
+
// request immediately. See LLMOptions.signal JSDoc for context.
|
|
8469
|
+
() => client.messages.create(requestParams, options.signal ? { signal: options.signal } : undefined), {
|
|
8336
8470
|
maxRetries: 3,
|
|
8337
8471
|
baseDelayMs: 2000,
|
|
8338
8472
|
maxDelayMs: 30000,
|
|
8339
8473
|
retryableErrors: isRetryableAnthropicError,
|
|
8474
|
+
signal: options.signal,
|
|
8340
8475
|
}, `Anthropic:${model}`);
|
|
8341
8476
|
const inputTokens = response.usage.input_tokens;
|
|
8342
8477
|
const outputTokens = response.usage.output_tokens;
|
|
8343
8478
|
// Track cost
|
|
8344
|
-
getLLMCostTracker().trackUsage(
|
|
8479
|
+
getLLMCostTracker().trackUsage('anthropic', model, inputTokens, outputTokens);
|
|
8345
8480
|
// Extract tool use blocks
|
|
8346
|
-
const toolUseBlocks = response.content.filter((block) => block.type ===
|
|
8481
|
+
const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
|
|
8347
8482
|
if (toolUseBlocks.length > 0) {
|
|
8348
8483
|
// Translate Anthropic tool_use to OpenAI tool_calls format
|
|
8349
8484
|
const toolCalls = toolUseBlocks.map((block) => ({
|
|
8350
8485
|
id: block.id,
|
|
8351
|
-
type:
|
|
8486
|
+
type: 'function',
|
|
8352
8487
|
function: {
|
|
8353
8488
|
name: block.name,
|
|
8354
8489
|
arguments: JSON.stringify(block.input),
|
|
@@ -8367,22 +8502,22 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8367
8502
|
prompt_tokens: inputTokens,
|
|
8368
8503
|
completion_tokens: outputTokens,
|
|
8369
8504
|
reasoning_tokens: 0,
|
|
8370
|
-
provider:
|
|
8505
|
+
provider: 'anthropic',
|
|
8371
8506
|
model,
|
|
8372
|
-
cost: calculateCost(
|
|
8507
|
+
cost: calculateCost('anthropic', model, inputTokens, outputTokens),
|
|
8373
8508
|
},
|
|
8374
8509
|
tool_calls: toolCalls,
|
|
8375
8510
|
};
|
|
8376
8511
|
}
|
|
8377
8512
|
// Extract text content
|
|
8378
8513
|
const textContent = response.content
|
|
8379
|
-
.filter((block) => block.type ===
|
|
8514
|
+
.filter((block) => block.type === 'text')
|
|
8380
8515
|
.map((block) => block.text)
|
|
8381
|
-
.join(
|
|
8516
|
+
.join('');
|
|
8382
8517
|
// Parse response
|
|
8383
8518
|
const parsedResponse = await parseResponse(textContent, responseFormat);
|
|
8384
8519
|
if (parsedResponse === null) {
|
|
8385
|
-
throw new Error(
|
|
8520
|
+
throw new Error('Failed to parse Anthropic response');
|
|
8386
8521
|
}
|
|
8387
8522
|
return {
|
|
8388
8523
|
response: parsedResponse,
|
|
@@ -8390,9 +8525,9 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8390
8525
|
prompt_tokens: inputTokens,
|
|
8391
8526
|
completion_tokens: outputTokens,
|
|
8392
8527
|
reasoning_tokens: 0,
|
|
8393
|
-
provider:
|
|
8528
|
+
provider: 'anthropic',
|
|
8394
8529
|
model,
|
|
8395
|
-
cost: calculateCost(
|
|
8530
|
+
cost: calculateCost('anthropic', model, inputTokens, outputTokens),
|
|
8396
8531
|
},
|
|
8397
8532
|
};
|
|
8398
8533
|
}
|
|
@@ -8414,9 +8549,7 @@ async function makeAnthropicCall(content, responseFormat = "text", options = {})
|
|
|
8414
8549
|
const isRetryableError = (error) => {
|
|
8415
8550
|
if (error instanceof Error) {
|
|
8416
8551
|
const message = error.message;
|
|
8417
|
-
if (message.includes(
|
|
8418
|
-
message.includes("rate limit") ||
|
|
8419
|
-
message.includes("Rate limit")) {
|
|
8552
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
8420
8553
|
return true;
|
|
8421
8554
|
}
|
|
8422
8555
|
}
|
|
@@ -8430,16 +8563,14 @@ function resolveApiKey(provider, optionsApiKey) {
|
|
|
8430
8563
|
if (optionsApiKey)
|
|
8431
8564
|
return optionsApiKey;
|
|
8432
8565
|
const secrets = getSecrets();
|
|
8433
|
-
const pathParts = provider.apiKeySecretPath.split(
|
|
8566
|
+
const pathParts = provider.apiKeySecretPath.split('.');
|
|
8434
8567
|
let current = secrets;
|
|
8435
8568
|
for (const part of pathParts) {
|
|
8436
|
-
if (current === null ||
|
|
8437
|
-
|
|
8438
|
-
typeof current !== "object")
|
|
8439
|
-
return "";
|
|
8569
|
+
if (current === null || current === undefined || typeof current !== 'object')
|
|
8570
|
+
return '';
|
|
8440
8571
|
current = current[part];
|
|
8441
8572
|
}
|
|
8442
|
-
return (typeof current ===
|
|
8573
|
+
return (typeof current === 'string' ? current : '') || '';
|
|
8443
8574
|
}
|
|
8444
8575
|
/**
|
|
8445
8576
|
* Makes a call to an OpenAI-compatible provider (Kimi, Qwen, or any future provider).
|
|
@@ -8453,12 +8584,12 @@ function resolveApiKey(provider, optionsApiKey) {
|
|
|
8453
8584
|
* @param providerName The provider key in OPENAI_COMPATIBLE_PROVIDERS.
|
|
8454
8585
|
* @return A promise that resolves to the response from the provider.
|
|
8455
8586
|
*/
|
|
8456
|
-
async function makeOpenAICompatibleCall(content, responseFormat =
|
|
8587
|
+
async function makeOpenAICompatibleCall(content, responseFormat = 'text', options = {}, providerName) {
|
|
8457
8588
|
const providerConfig = OPENAI_COMPATIBLE_PROVIDERS[providerName];
|
|
8458
8589
|
if (!providerConfig) {
|
|
8459
8590
|
throw new Error(`Unknown OpenAI-compatible provider: ${providerName}`);
|
|
8460
8591
|
}
|
|
8461
|
-
const model = normalizeModelName(options.model ||
|
|
8592
|
+
const model = normalizeModelName(options.model || '');
|
|
8462
8593
|
const apiKey = resolveApiKey(providerConfig, options.apiKey);
|
|
8463
8594
|
if (!apiKey) {
|
|
8464
8595
|
throw new Error(`${providerConfig.name} API key is not provided and ${providerConfig.apiKeyEnvVar} environment variable is not set`);
|
|
@@ -8471,36 +8602,31 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8471
8602
|
const messages = [];
|
|
8472
8603
|
// Add system message with developer prompt if present
|
|
8473
8604
|
if (options.developerPrompt) {
|
|
8474
|
-
messages.push({ role:
|
|
8605
|
+
messages.push({ role: 'system', content: options.developerPrompt });
|
|
8475
8606
|
}
|
|
8476
8607
|
// Add context if present
|
|
8477
8608
|
if (options.context) {
|
|
8478
8609
|
messages.push(...options.context);
|
|
8479
8610
|
}
|
|
8480
8611
|
// Add user content
|
|
8481
|
-
if (typeof content ===
|
|
8482
|
-
messages.push({ role:
|
|
8612
|
+
if (typeof content === 'string') {
|
|
8613
|
+
messages.push({ role: 'user', content });
|
|
8483
8614
|
}
|
|
8484
8615
|
else {
|
|
8485
|
-
messages.push({ role:
|
|
8616
|
+
messages.push({ role: 'user', content });
|
|
8486
8617
|
}
|
|
8487
8618
|
// Determine if the model supports JSON mode
|
|
8488
8619
|
const supportsJson = isValidModel(model) && getModelCapabilities(model).supportsJson;
|
|
8489
8620
|
// If JSON response format, add hint to system prompt
|
|
8490
|
-
if ((responseFormat ===
|
|
8491
|
-
(typeof responseFormat === "object" &&
|
|
8492
|
-
responseFormat.type === "json_schema")) &&
|
|
8621
|
+
if ((responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) &&
|
|
8493
8622
|
supportsJson) {
|
|
8494
|
-
if (!messages.some((m) => m.role ===
|
|
8495
|
-
messages.unshift({
|
|
8496
|
-
role: "system",
|
|
8497
|
-
content: "Please respond in valid JSON format.",
|
|
8498
|
-
});
|
|
8623
|
+
if (!messages.some((m) => m.role === 'system')) {
|
|
8624
|
+
messages.unshift({ role: 'system', content: 'Please respond in valid JSON format.' });
|
|
8499
8625
|
}
|
|
8500
8626
|
else {
|
|
8501
|
-
const systemMsgIndex = messages.findIndex((m) => m.role ===
|
|
8627
|
+
const systemMsgIndex = messages.findIndex((m) => m.role === 'system');
|
|
8502
8628
|
const systemMsg = messages[systemMsgIndex];
|
|
8503
|
-
if (typeof systemMsg.content ===
|
|
8629
|
+
if (typeof systemMsg.content === 'string') {
|
|
8504
8630
|
messages[systemMsgIndex] = {
|
|
8505
8631
|
...systemMsg,
|
|
8506
8632
|
content: `${systemMsg.content} Please respond in valid JSON format.`,
|
|
@@ -8513,8 +8639,8 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8513
8639
|
messages,
|
|
8514
8640
|
};
|
|
8515
8641
|
// Add response format if JSON is supported
|
|
8516
|
-
if (responseFormat !==
|
|
8517
|
-
queryOptions.response_format = { type:
|
|
8642
|
+
if (responseFormat !== 'text' && supportsJson) {
|
|
8643
|
+
queryOptions.response_format = { type: 'json_object' };
|
|
8518
8644
|
}
|
|
8519
8645
|
// Temperature
|
|
8520
8646
|
if (options.temperature !== undefined) {
|
|
@@ -8538,11 +8664,16 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8538
8664
|
queryOptions.tools = options.tools;
|
|
8539
8665
|
}
|
|
8540
8666
|
try {
|
|
8541
|
-
const completion = await withRetry(
|
|
8667
|
+
const completion = await withRetry(
|
|
8668
|
+
// Pass AbortSignal into the SDK request-options object so
|
|
8669
|
+
// caller-driven cancellation interrupts the in-flight HTTP
|
|
8670
|
+
// request immediately. See LLMOptions.signal JSDoc for context.
|
|
8671
|
+
() => openai.chat.completions.create(queryOptions, options.signal ? { signal: options.signal } : undefined), {
|
|
8542
8672
|
maxRetries: 3,
|
|
8543
8673
|
baseDelayMs: 2000,
|
|
8544
8674
|
maxDelayMs: 30000,
|
|
8545
8675
|
retryableErrors: isRetryableError,
|
|
8676
|
+
signal: options.signal,
|
|
8546
8677
|
}, `${providerConfig.name}:${model}`);
|
|
8547
8678
|
const promptTokens = completion.usage?.prompt_tokens || 0;
|
|
8548
8679
|
const completionTokens = completion.usage?.completion_tokens || 0;
|
|
@@ -8572,7 +8703,7 @@ async function makeOpenAICompatibleCall(content, responseFormat = "text", option
|
|
|
8572
8703
|
};
|
|
8573
8704
|
}
|
|
8574
8705
|
// Handle regular responses
|
|
8575
|
-
const textContent = completion.choices[0]?.message?.content ||
|
|
8706
|
+
const textContent = completion.choices[0]?.message?.content || '';
|
|
8576
8707
|
const parsedResponse = await parseResponse(textContent, responseFormat);
|
|
8577
8708
|
if (parsedResponse === null) {
|
|
8578
8709
|
throw new Error(`Failed to parse ${providerConfig.name} response`);
|
|
@@ -8864,9 +8995,7 @@ const isRetryableDeepseekError = (error) => {
|
|
|
8864
8995
|
if (error instanceof Error) {
|
|
8865
8996
|
const message = error.message;
|
|
8866
8997
|
// Retry only on rate limits (429)
|
|
8867
|
-
if (message.includes(
|
|
8868
|
-
message.includes("rate limit") ||
|
|
8869
|
-
message.includes("Rate limit")) {
|
|
8998
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
|
|
8870
8999
|
return true;
|
|
8871
9000
|
}
|
|
8872
9001
|
}
|
|
@@ -8876,7 +9005,7 @@ const isRetryableDeepseekError = (error) => {
|
|
|
8876
9005
|
* Default options for Deepseek API calls
|
|
8877
9006
|
*/
|
|
8878
9007
|
const DEFAULT_DEEPSEEK_OPTIONS = {
|
|
8879
|
-
defaultModel:
|
|
9008
|
+
defaultModel: 'deepseek-chat',
|
|
8880
9009
|
};
|
|
8881
9010
|
/**
|
|
8882
9011
|
* Checks if the given model is a supported Deepseek model
|
|
@@ -8888,7 +9017,7 @@ const isSupportedDeepseekModel = (model) => {
|
|
|
8888
9017
|
return false;
|
|
8889
9018
|
}
|
|
8890
9019
|
const capabilities = getModelCapabilities(model);
|
|
8891
|
-
return capabilities.provider ===
|
|
9020
|
+
return capabilities.provider === 'deepseek';
|
|
8892
9021
|
};
|
|
8893
9022
|
/**
|
|
8894
9023
|
* Checks if the given Deepseek model supports JSON output format
|
|
@@ -8921,19 +9050,17 @@ const supportsToolCalling = (model) => {
|
|
|
8921
9050
|
*/
|
|
8922
9051
|
function getDeepseekResponseFormatOption(responseFormat, model) {
|
|
8923
9052
|
// Check if the model supports JSON output
|
|
8924
|
-
if (responseFormat !==
|
|
9053
|
+
if (responseFormat !== 'text' && !supportsJsonOutput(model)) {
|
|
8925
9054
|
getLumicLogger().warn(`Model ${model} does not support JSON output. Using text format instead.`);
|
|
8926
|
-
return { type:
|
|
9055
|
+
return { type: 'text' };
|
|
8927
9056
|
}
|
|
8928
|
-
if (responseFormat ===
|
|
8929
|
-
return { type:
|
|
9057
|
+
if (responseFormat === 'text') {
|
|
9058
|
+
return { type: 'text' };
|
|
8930
9059
|
}
|
|
8931
|
-
if (responseFormat ===
|
|
8932
|
-
|
|
8933
|
-
responseFormat.type === "json_schema")) {
|
|
8934
|
-
return { type: "json_object" };
|
|
9060
|
+
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) {
|
|
9061
|
+
return { type: 'json_object' };
|
|
8935
9062
|
}
|
|
8936
|
-
return { type:
|
|
9063
|
+
return { type: 'text' };
|
|
8937
9064
|
}
|
|
8938
9065
|
/**
|
|
8939
9066
|
* Creates a completion using the Deepseek API
|
|
@@ -8952,26 +9079,24 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
8952
9079
|
throw new Error(`Unsupported Deepseek model: ${normalizedModel}. Please use 'deepseek-chat' or 'deepseek-reasoner'.`);
|
|
8953
9080
|
}
|
|
8954
9081
|
// Check if tools are requested with a model that doesn't support them
|
|
8955
|
-
if (options.tools &&
|
|
8956
|
-
options.tools.length > 0 &&
|
|
8957
|
-
!supportsToolCalling(normalizedModel)) {
|
|
9082
|
+
if (options.tools && options.tools.length > 0 && !supportsToolCalling(normalizedModel)) {
|
|
8958
9083
|
throw new Error(`Model ${normalizedModel} does not support tool calling.`);
|
|
8959
9084
|
}
|
|
8960
9085
|
const apiKey = options.apiKey || getSecrets().deepseek.apiKey;
|
|
8961
9086
|
if (!apiKey) {
|
|
8962
|
-
throw new Error(
|
|
9087
|
+
throw new Error('Deepseek API key is not provided and DEEPSEEK_API_KEY environment variable is not set');
|
|
8963
9088
|
}
|
|
8964
9089
|
// Initialize OpenAI client with Deepseek API URL
|
|
8965
9090
|
const openai = new OpenAI({
|
|
8966
9091
|
apiKey,
|
|
8967
|
-
baseURL:
|
|
9092
|
+
baseURL: 'https://api.deepseek.com',
|
|
8968
9093
|
timeout: options.timeout ?? LLM_TIMEOUT_MS,
|
|
8969
9094
|
});
|
|
8970
9095
|
const messages = [];
|
|
8971
9096
|
// Add system message with developer prompt if present
|
|
8972
9097
|
if (options.developerPrompt) {
|
|
8973
9098
|
messages.push({
|
|
8974
|
-
role:
|
|
9099
|
+
role: 'system',
|
|
8975
9100
|
content: options.developerPrompt,
|
|
8976
9101
|
});
|
|
8977
9102
|
}
|
|
@@ -8980,35 +9105,33 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
8980
9105
|
messages.push(...options.context);
|
|
8981
9106
|
}
|
|
8982
9107
|
// Add user content
|
|
8983
|
-
if (typeof content ===
|
|
9108
|
+
if (typeof content === 'string') {
|
|
8984
9109
|
messages.push({
|
|
8985
|
-
role:
|
|
9110
|
+
role: 'user',
|
|
8986
9111
|
content,
|
|
8987
9112
|
});
|
|
8988
9113
|
}
|
|
8989
9114
|
else {
|
|
8990
9115
|
messages.push({
|
|
8991
|
-
role:
|
|
9116
|
+
role: 'user',
|
|
8992
9117
|
content,
|
|
8993
9118
|
});
|
|
8994
9119
|
}
|
|
8995
9120
|
// If JSON response format, include a hint in the system prompt
|
|
8996
|
-
if ((responseFormat ===
|
|
8997
|
-
(
|
|
8998
|
-
responseFormat.type === "json_schema")) &&
|
|
8999
|
-
supportsJsonOutput(normalizedModel)) {
|
|
9121
|
+
if ((responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema'))
|
|
9122
|
+
&& supportsJsonOutput(normalizedModel)) {
|
|
9000
9123
|
// If there's no system message yet, add one
|
|
9001
|
-
if (!messages.some(
|
|
9124
|
+
if (!messages.some(m => m.role === 'system')) {
|
|
9002
9125
|
messages.unshift({
|
|
9003
|
-
role:
|
|
9004
|
-
content:
|
|
9126
|
+
role: 'system',
|
|
9127
|
+
content: 'Please respond in valid JSON format.',
|
|
9005
9128
|
});
|
|
9006
9129
|
}
|
|
9007
9130
|
else {
|
|
9008
9131
|
// Append to existing system message
|
|
9009
|
-
const systemMsgIndex = messages.findIndex(
|
|
9132
|
+
const systemMsgIndex = messages.findIndex(m => m.role === 'system');
|
|
9010
9133
|
const systemMsg = messages[systemMsgIndex];
|
|
9011
|
-
if (typeof systemMsg.content ===
|
|
9134
|
+
if (typeof systemMsg.content === 'string') {
|
|
9012
9135
|
messages[systemMsgIndex] = {
|
|
9013
9136
|
...systemMsg,
|
|
9014
9137
|
content: `${systemMsg.content} Please respond in valid JSON format.`,
|
|
@@ -9049,7 +9172,7 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9049
9172
|
0;
|
|
9050
9173
|
return {
|
|
9051
9174
|
id: completion.id,
|
|
9052
|
-
content: completion.choices[0]?.message?.content ||
|
|
9175
|
+
content: completion.choices[0]?.message?.content || '',
|
|
9053
9176
|
tool_calls: completion.choices[0]?.message?.tool_calls,
|
|
9054
9177
|
usage: {
|
|
9055
9178
|
prompt_tokens: completion.usage?.prompt_tokens ?? 0,
|
|
@@ -9058,7 +9181,7 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9058
9181
|
cached_tokens: cachedTokens,
|
|
9059
9182
|
},
|
|
9060
9183
|
system_fingerprint: completion.system_fingerprint,
|
|
9061
|
-
provider:
|
|
9184
|
+
provider: 'deepseek',
|
|
9062
9185
|
model: normalizedModel,
|
|
9063
9186
|
};
|
|
9064
9187
|
}
|
|
@@ -9074,8 +9197,12 @@ async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
|
9074
9197
|
* @param responseFormat The format of the response. Defaults to 'json'.
|
|
9075
9198
|
* @param options Configuration options including model ('deepseek-chat' or 'deepseek-reasoner'), tools, and apiKey.
|
|
9076
9199
|
* @return A promise that resolves to the response from the Deepseek API.
|
|
9200
|
+
* @throws {Error} If the requested capability (JSON output / tool calling) is
|
|
9201
|
+
* unsupported by the model, or if the underlying API call fails. This matches
|
|
9202
|
+
* the OpenAI and Anthropic providers, which surface failures by throwing
|
|
9203
|
+
* rather than returning a success-shaped response carrying an error payload.
|
|
9077
9204
|
*/
|
|
9078
|
-
const makeDeepseekCall = async (content, responseFormat =
|
|
9205
|
+
const makeDeepseekCall = async (content, responseFormat = 'json', options = {}) => {
|
|
9079
9206
|
// Set default model if not provided
|
|
9080
9207
|
const mergedOptions = {
|
|
9081
9208
|
...options,
|
|
@@ -9084,51 +9211,21 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9084
9211
|
mergedOptions.model = DEFAULT_DEEPSEEK_OPTIONS.defaultModel;
|
|
9085
9212
|
}
|
|
9086
9213
|
const modelName = normalizeModelName(mergedOptions.model);
|
|
9087
|
-
// Check if the requested response format is compatible with the model
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
error: `Model ${modelName} does not support JSON output format.`,
|
|
9093
|
-
},
|
|
9094
|
-
usage: {
|
|
9095
|
-
prompt_tokens: 0,
|
|
9096
|
-
completion_tokens: 0,
|
|
9097
|
-
reasoning_tokens: 0,
|
|
9098
|
-
provider: "deepseek",
|
|
9099
|
-
model: modelName,
|
|
9100
|
-
cached_tokens: 0,
|
|
9101
|
-
cost: 0,
|
|
9102
|
-
},
|
|
9103
|
-
tool_calls: undefined,
|
|
9104
|
-
};
|
|
9214
|
+
// Check if the requested response format is compatible with the model.
|
|
9215
|
+
// Throw rather than returning a fake-success LLMResponse: a caller that
|
|
9216
|
+
// asked for JSON must not silently receive an { error } payload typed as T.
|
|
9217
|
+
if (responseFormat !== 'text' && !supportsJsonOutput(modelName)) {
|
|
9218
|
+
throw new Error(`Model ${modelName} does not support JSON output format.`);
|
|
9105
9219
|
}
|
|
9106
|
-
// Check if tools are requested with a model that doesn't support them
|
|
9107
|
-
if (mergedOptions.tools &&
|
|
9108
|
-
|
|
9109
|
-
!supportsToolCalling(modelName)) {
|
|
9110
|
-
getLumicLogger().warn(`Model ${modelName} does not support tool calling. Will return error in the response.`);
|
|
9111
|
-
return {
|
|
9112
|
-
response: {
|
|
9113
|
-
error: `Model ${modelName} does not support tool calling.`,
|
|
9114
|
-
},
|
|
9115
|
-
usage: {
|
|
9116
|
-
prompt_tokens: 0,
|
|
9117
|
-
completion_tokens: 0,
|
|
9118
|
-
reasoning_tokens: 0,
|
|
9119
|
-
provider: "deepseek",
|
|
9120
|
-
model: modelName,
|
|
9121
|
-
cached_tokens: 0,
|
|
9122
|
-
cost: 0,
|
|
9123
|
-
},
|
|
9124
|
-
tool_calls: undefined,
|
|
9125
|
-
};
|
|
9220
|
+
// Check if tools are requested with a model that doesn't support them.
|
|
9221
|
+
if (mergedOptions.tools && mergedOptions.tools.length > 0 && !supportsToolCalling(modelName)) {
|
|
9222
|
+
throw new Error(`Model ${modelName} does not support tool calling.`);
|
|
9126
9223
|
}
|
|
9127
9224
|
try {
|
|
9128
9225
|
const completion = await createDeepseekCompletion(content, responseFormat, mergedOptions);
|
|
9129
9226
|
// Track cost in the global cost tracker. Pass cached tokens through so the
|
|
9130
9227
|
// discounted cached-input pricing tier is applied.
|
|
9131
|
-
getLLMCostTracker().trackUsage(
|
|
9228
|
+
getLLMCostTracker().trackUsage('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens);
|
|
9132
9229
|
// Handle tool calls similarly to OpenAI
|
|
9133
9230
|
if (completion.tool_calls && completion.tool_calls.length > 0) {
|
|
9134
9231
|
const toolCallResponse = {
|
|
@@ -9144,10 +9241,10 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9144
9241
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
9145
9242
|
completion_tokens: completion.usage.completion_tokens,
|
|
9146
9243
|
reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
|
|
9147
|
-
provider:
|
|
9244
|
+
provider: 'deepseek',
|
|
9148
9245
|
model: completion.model,
|
|
9149
9246
|
cached_tokens: completion.usage.cached_tokens,
|
|
9150
|
-
cost: calculateCost(
|
|
9247
|
+
cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
9151
9248
|
},
|
|
9152
9249
|
tool_calls: completion.tool_calls,
|
|
9153
9250
|
};
|
|
@@ -9155,7 +9252,7 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9155
9252
|
// Handle regular responses
|
|
9156
9253
|
const parsedResponse = await parseResponse(completion.content, responseFormat);
|
|
9157
9254
|
if (parsedResponse === null) {
|
|
9158
|
-
throw new Error(
|
|
9255
|
+
throw new Error('Failed to parse Deepseek response');
|
|
9159
9256
|
}
|
|
9160
9257
|
return {
|
|
9161
9258
|
response: parsedResponse,
|
|
@@ -9163,32 +9260,22 @@ const makeDeepseekCall = async (content, responseFormat = "json", options = {})
|
|
|
9163
9260
|
prompt_tokens: completion.usage.prompt_tokens,
|
|
9164
9261
|
completion_tokens: completion.usage.completion_tokens,
|
|
9165
9262
|
reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
|
|
9166
|
-
provider:
|
|
9263
|
+
provider: 'deepseek',
|
|
9167
9264
|
model: completion.model,
|
|
9168
9265
|
cached_tokens: completion.usage.cached_tokens,
|
|
9169
|
-
cost: calculateCost(
|
|
9266
|
+
cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
|
|
9170
9267
|
},
|
|
9171
9268
|
tool_calls: completion.tool_calls,
|
|
9172
9269
|
};
|
|
9173
9270
|
}
|
|
9174
9271
|
catch (error) {
|
|
9175
|
-
//
|
|
9272
|
+
// Mirror the OpenAI/Anthropic providers: log with sanitized context and
|
|
9273
|
+
// rethrow so callers observe the failure. Returning a success-shaped
|
|
9274
|
+
// response with an { error } payload here previously let hard failures
|
|
9275
|
+
// (auth, timeout, rate-limit exhaustion, unparseable output) masquerade as
|
|
9276
|
+
// successful completions downstream.
|
|
9176
9277
|
getLumicLogger().error(`Error in Deepseek API call: ${sanitizeError(error)}`);
|
|
9177
|
-
|
|
9178
|
-
response: {
|
|
9179
|
-
error: sanitizeError(error),
|
|
9180
|
-
},
|
|
9181
|
-
usage: {
|
|
9182
|
-
prompt_tokens: 0,
|
|
9183
|
-
completion_tokens: 0,
|
|
9184
|
-
reasoning_tokens: 0,
|
|
9185
|
-
provider: "deepseek",
|
|
9186
|
-
model: modelName,
|
|
9187
|
-
cached_tokens: 0,
|
|
9188
|
-
cost: 0,
|
|
9189
|
-
},
|
|
9190
|
-
tool_calls: undefined,
|
|
9191
|
-
};
|
|
9278
|
+
throw error;
|
|
9192
9279
|
}
|
|
9193
9280
|
};
|
|
9194
9281
|
|
|
@@ -9291,15 +9378,15 @@ const generateCacheKey = (auth, envVarsCheck = null) => {
|
|
|
9291
9378
|
if (auth) {
|
|
9292
9379
|
return `${auth.AWS_ACCESS_KEY_ID}:${auth.AWS_SECRET_ACCESS_KEY}:${auth.AWS_REGION}`;
|
|
9293
9380
|
}
|
|
9294
|
-
if (envVarsCheck?.source ===
|
|
9381
|
+
if (envVarsCheck?.source === 'lambda') {
|
|
9295
9382
|
// In Lambda, use function name as cache key since it uses IAM role
|
|
9296
9383
|
const secrets = getSecrets();
|
|
9297
|
-
return `lambda:${secrets.aws.lambdaFunctionName ||
|
|
9384
|
+
return `lambda:${secrets.aws.lambdaFunctionName || 'default'}`;
|
|
9298
9385
|
}
|
|
9299
9386
|
if (envVarsCheck?.vars) {
|
|
9300
9387
|
return `${envVarsCheck.vars.accessKeyId}:${envVarsCheck.vars.secretAccessKey}:${envVarsCheck.vars.region}`;
|
|
9301
9388
|
}
|
|
9302
|
-
return
|
|
9389
|
+
return 'default';
|
|
9303
9390
|
};
|
|
9304
9391
|
const validateEnvironmentVars = () => {
|
|
9305
9392
|
const secrets = getSecrets();
|
|
@@ -9312,37 +9399,37 @@ const validateEnvironmentVars = () => {
|
|
|
9312
9399
|
// We're in a Lambda environment
|
|
9313
9400
|
return {
|
|
9314
9401
|
isValid: true,
|
|
9315
|
-
source:
|
|
9316
|
-
vars: { accessKeyId:
|
|
9402
|
+
source: 'lambda',
|
|
9403
|
+
vars: { accessKeyId: '', secretAccessKey: '', region: '' } // No need for explicit credentials in Lambda
|
|
9317
9404
|
};
|
|
9318
9405
|
}
|
|
9319
9406
|
else if (areVarsComplete(secrets.aws.myAccessKeyId, secrets.aws.mySecretAccessKey, secrets.aws.myRegion)) {
|
|
9320
9407
|
return {
|
|
9321
9408
|
isValid: true,
|
|
9322
|
-
source:
|
|
9409
|
+
source: 'my_prefix',
|
|
9323
9410
|
vars: {
|
|
9324
|
-
accessKeyId: secrets.aws.myAccessKeyId
|
|
9325
|
-
secretAccessKey: secrets.aws.mySecretAccessKey
|
|
9326
|
-
region: secrets.aws.myRegion
|
|
9327
|
-
}
|
|
9411
|
+
accessKeyId: secrets.aws.myAccessKeyId,
|
|
9412
|
+
secretAccessKey: secrets.aws.mySecretAccessKey,
|
|
9413
|
+
region: secrets.aws.myRegion
|
|
9414
|
+
}
|
|
9328
9415
|
};
|
|
9329
9416
|
}
|
|
9330
9417
|
else if (areVarsComplete(secrets.aws.accessKeyId, secrets.aws.secretAccessKey, secrets.aws.region)) {
|
|
9331
9418
|
return {
|
|
9332
9419
|
isValid: true,
|
|
9333
|
-
source:
|
|
9420
|
+
source: 'standard',
|
|
9334
9421
|
vars: {
|
|
9335
|
-
accessKeyId: secrets.aws.accessKeyId
|
|
9336
|
-
secretAccessKey: secrets.aws.secretAccessKey
|
|
9337
|
-
region: secrets.aws.region
|
|
9338
|
-
}
|
|
9422
|
+
accessKeyId: secrets.aws.accessKeyId,
|
|
9423
|
+
secretAccessKey: secrets.aws.secretAccessKey,
|
|
9424
|
+
region: secrets.aws.region
|
|
9425
|
+
}
|
|
9339
9426
|
};
|
|
9340
9427
|
}
|
|
9341
9428
|
// If we get here, no complete set of variables was found
|
|
9342
9429
|
return {
|
|
9343
9430
|
isValid: false,
|
|
9344
9431
|
source: null,
|
|
9345
|
-
missingVars: []
|
|
9432
|
+
missingVars: []
|
|
9346
9433
|
};
|
|
9347
9434
|
};
|
|
9348
9435
|
const initialiseAWSClient = (ClientClass, auth = null) => {
|
|
@@ -9351,52 +9438,48 @@ const initialiseAWSClient = (ClientClass, auth = null) => {
|
|
|
9351
9438
|
const requestTimeout = ClientClass === clientS3.S3Client ? AWS_S3_TIMEOUT_MS : AWS_LAMBDA_TIMEOUT_MS;
|
|
9352
9439
|
// Case 1: Explicit auth provided
|
|
9353
9440
|
if (auth) {
|
|
9354
|
-
if (typeof auth !==
|
|
9355
|
-
throw new Error(
|
|
9441
|
+
if (typeof auth !== 'object' || auth === null) {
|
|
9442
|
+
throw new Error('Auth parameter must be an object');
|
|
9356
9443
|
}
|
|
9357
|
-
const requiredAuthFields = [
|
|
9358
|
-
|
|
9359
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
9360
|
-
"AWS_REGION",
|
|
9361
|
-
];
|
|
9362
|
-
const missingFields = requiredAuthFields.filter((field) => !auth[field]);
|
|
9444
|
+
const requiredAuthFields = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'];
|
|
9445
|
+
const missingFields = requiredAuthFields.filter(field => !auth[field]);
|
|
9363
9446
|
if (missingFields.length > 0) {
|
|
9364
|
-
throw new Error(`Auth object is missing required fields: ${missingFields.join(
|
|
9447
|
+
throw new Error(`Auth object is missing required fields: ${missingFields.join(', ')}`);
|
|
9365
9448
|
}
|
|
9366
9449
|
return new ClientClass({
|
|
9367
9450
|
region: auth.AWS_REGION,
|
|
9368
9451
|
credentials: {
|
|
9369
9452
|
accessKeyId: auth.AWS_ACCESS_KEY_ID,
|
|
9370
|
-
secretAccessKey: auth.AWS_SECRET_ACCESS_KEY
|
|
9453
|
+
secretAccessKey: auth.AWS_SECRET_ACCESS_KEY
|
|
9371
9454
|
},
|
|
9372
9455
|
requestHandler: {
|
|
9373
|
-
requestTimeout
|
|
9374
|
-
}
|
|
9456
|
+
requestTimeout
|
|
9457
|
+
}
|
|
9375
9458
|
});
|
|
9376
9459
|
}
|
|
9377
9460
|
// Case 2: Check environment variables
|
|
9378
9461
|
const envVarsCheck = validateEnvironmentVars();
|
|
9379
9462
|
if (!envVarsCheck.isValid) {
|
|
9380
|
-
throw new Error(
|
|
9463
|
+
throw new Error('No valid AWS credentials found in environment variables');
|
|
9381
9464
|
}
|
|
9382
9465
|
// Case 2a: Lambda execution environment
|
|
9383
|
-
if (envVarsCheck.source ===
|
|
9466
|
+
if (envVarsCheck.source === 'lambda') {
|
|
9384
9467
|
return new ClientClass({
|
|
9385
9468
|
requestHandler: {
|
|
9386
|
-
requestTimeout
|
|
9387
|
-
}
|
|
9469
|
+
requestTimeout
|
|
9470
|
+
}
|
|
9388
9471
|
}); // AWS SDK will automatically use Lambda role credentials
|
|
9389
9472
|
}
|
|
9390
9473
|
// Case 2b: MY_ prefixed vars or standard AWS vars
|
|
9391
9474
|
return new ClientClass({
|
|
9392
|
-
region: envVarsCheck.vars
|
|
9475
|
+
region: envVarsCheck.vars.region,
|
|
9393
9476
|
credentials: {
|
|
9394
|
-
accessKeyId: envVarsCheck.vars
|
|
9395
|
-
secretAccessKey: envVarsCheck.vars
|
|
9477
|
+
accessKeyId: envVarsCheck.vars.accessKeyId,
|
|
9478
|
+
secretAccessKey: envVarsCheck.vars.secretAccessKey
|
|
9396
9479
|
},
|
|
9397
9480
|
requestHandler: {
|
|
9398
|
-
requestTimeout
|
|
9399
|
-
}
|
|
9481
|
+
requestTimeout
|
|
9482
|
+
}
|
|
9400
9483
|
});
|
|
9401
9484
|
}
|
|
9402
9485
|
catch (error) {
|
|
@@ -9409,9 +9492,7 @@ const initialiseS3Client = (auth = null) => {
|
|
|
9409
9492
|
const envVarsCheck = auth ? null : validateEnvironmentVars();
|
|
9410
9493
|
const cacheKey = generateCacheKey(auth, envVarsCheck);
|
|
9411
9494
|
if (s3ClientCache.has(cacheKey)) {
|
|
9412
|
-
|
|
9413
|
-
if (cached)
|
|
9414
|
-
return cached;
|
|
9495
|
+
return s3ClientCache.get(cacheKey);
|
|
9415
9496
|
}
|
|
9416
9497
|
// Create new client and cache it
|
|
9417
9498
|
const client = initialiseAWSClient(clientS3.S3Client, auth);
|
|
@@ -9423,9 +9504,7 @@ const initialiseLambdaClient = (auth = null) => {
|
|
|
9423
9504
|
const envVarsCheck = auth ? null : validateEnvironmentVars();
|
|
9424
9505
|
const cacheKey = generateCacheKey(auth, envVarsCheck);
|
|
9425
9506
|
if (lambdaClientCache.has(cacheKey)) {
|
|
9426
|
-
|
|
9427
|
-
if (cached)
|
|
9428
|
-
return cached;
|
|
9507
|
+
return lambdaClientCache.get(cacheKey);
|
|
9429
9508
|
}
|
|
9430
9509
|
// Create new client and cache it
|
|
9431
9510
|
const client = initialiseAWSClient(clientLambda.Lambda, auth);
|
|
@@ -9925,17 +10004,15 @@ const isRetryableS3Error = (error) => {
|
|
|
9925
10004
|
if (error instanceof Error) {
|
|
9926
10005
|
const message = error.message;
|
|
9927
10006
|
// Retry on throttling
|
|
9928
|
-
if (message.includes(
|
|
10007
|
+
if (message.includes('SlowDown') || message.includes('RequestTimeout')) {
|
|
9929
10008
|
return true;
|
|
9930
10009
|
}
|
|
9931
10010
|
// Retry on 5xx server errors
|
|
9932
|
-
if (message.includes(
|
|
9933
|
-
message.includes("ServiceUnavailable")) {
|
|
10011
|
+
if (message.includes('InternalError') || message.includes('ServiceUnavailable')) {
|
|
9934
10012
|
return true;
|
|
9935
10013
|
}
|
|
9936
10014
|
// Retry on connection errors
|
|
9937
|
-
if (message.includes(
|
|
9938
|
-
message.includes("NetworkingError")) {
|
|
10015
|
+
if (message.includes('RequestTimeTooSkewed') || message.includes('NetworkingError')) {
|
|
9939
10016
|
return true;
|
|
9940
10017
|
}
|
|
9941
10018
|
}
|
|
@@ -9949,11 +10026,10 @@ const generateRandomHash = (length) => {
|
|
|
9949
10026
|
};
|
|
9950
10027
|
function generateDateTimeStamp() {
|
|
9951
10028
|
const now = new Date();
|
|
9952
|
-
return now
|
|
9953
|
-
.
|
|
9954
|
-
.replace(
|
|
9955
|
-
.replace(
|
|
9956
|
-
.replace(/:/g, "-");
|
|
10029
|
+
return now.toISOString()
|
|
10030
|
+
.replace(/T/, '-')
|
|
10031
|
+
.replace(/\..+/, '')
|
|
10032
|
+
.replace(/:/g, '-');
|
|
9957
10033
|
}
|
|
9958
10034
|
const isValidBucketName = (name) => {
|
|
9959
10035
|
const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
@@ -9984,7 +10060,7 @@ async function uploadFile(s3Client, bucketName, filePath, destinationPath, isPub
|
|
|
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,
|
|
@@ -10004,7 +10080,7 @@ async function uploadDirectory(s3Client, bucketName, dirPath, destinationPath, i
|
|
|
10004
10080
|
Bucket: bucketName,
|
|
10005
10081
|
Key: s3Key,
|
|
10006
10082
|
Body: fileContent,
|
|
10007
|
-
ACL: isPublic ?
|
|
10083
|
+
ACL: isPublic ? 'public-read' : undefined
|
|
10008
10084
|
});
|
|
10009
10085
|
await withRetry(() => s3Client.send(command), {
|
|
10010
10086
|
maxRetries: 3,
|
|
@@ -10020,7 +10096,7 @@ async function zipDirectory(sourceDir, outPath) {
|
|
|
10020
10096
|
const zip = new AdmZip();
|
|
10021
10097
|
let fileCount = 0;
|
|
10022
10098
|
try {
|
|
10023
|
-
async function addFilesToZip(currentPath, relativePath =
|
|
10099
|
+
async function addFilesToZip(currentPath, relativePath = '') {
|
|
10024
10100
|
const files = await fs$1.readdir(currentPath, { withFileTypes: true });
|
|
10025
10101
|
for (const file of files) {
|
|
10026
10102
|
const filePath = path$1.join(currentPath, file.name);
|
|
@@ -10063,33 +10139,27 @@ async function downloadFromS3Helper(s3Client, bucketName, s3Path, localPath) {
|
|
|
10063
10139
|
Prefix: s3Path,
|
|
10064
10140
|
ContinuationToken: continuationToken,
|
|
10065
10141
|
});
|
|
10066
|
-
const listResponse =
|
|
10142
|
+
const listResponse = await withRetry(() => s3Client.send(listCommand), {
|
|
10067
10143
|
maxRetries: 3,
|
|
10068
10144
|
baseDelayMs: 1000,
|
|
10069
10145
|
maxDelayMs: 15000,
|
|
10070
10146
|
retryableErrors: isRetryableS3Error,
|
|
10071
|
-
}, `S3:ListObjects:${bucketName}`)
|
|
10147
|
+
}, `S3:ListObjects:${bucketName}`);
|
|
10072
10148
|
const objects = listResponse.Contents || [];
|
|
10073
10149
|
if (objects.length === 0) {
|
|
10074
10150
|
return { fileCount: 0, folderCount: 0, totalSize: 0, fileList: [] };
|
|
10075
10151
|
}
|
|
10076
|
-
if (objects.length === 1 && objects[0].Key?.endsWith(
|
|
10152
|
+
if (objects.length === 1 && objects[0].Key?.endsWith('.zip')) {
|
|
10077
10153
|
// It's a zip file, download and extract it
|
|
10078
10154
|
const zipPath = path$1.join(localPath, path$1.basename(objects[0].Key));
|
|
10079
10155
|
await downloadFile(s3Client, bucketName, objects[0].Key, zipPath);
|
|
10080
|
-
const { totalFiles, totalFolders, totalSize: extractedSize, fileList: extractedFileList
|
|
10156
|
+
const { totalFiles, totalFolders, totalSize: extractedSize, fileList: extractedFileList } = await unzipFile(zipPath, localPath);
|
|
10081
10157
|
await fs$1.unlink(zipPath);
|
|
10082
|
-
return {
|
|
10083
|
-
fileCount: totalFiles,
|
|
10084
|
-
folderCount: totalFolders,
|
|
10085
|
-
totalSize: extractedSize,
|
|
10086
|
-
fileList: extractedFileList,
|
|
10087
|
-
};
|
|
10158
|
+
return { fileCount: totalFiles, folderCount: totalFolders, totalSize: extractedSize, fileList: extractedFileList };
|
|
10088
10159
|
}
|
|
10089
10160
|
else {
|
|
10090
10161
|
// Non-zip files. Download files in batches.
|
|
10091
|
-
for (let i = 0; i < objects.length; i += 1000) {
|
|
10092
|
-
// AWS allows up to 1000 per request
|
|
10162
|
+
for (let i = 0; i < objects.length; i += 1000) { // AWS allows up to 1000 per request
|
|
10093
10163
|
const batch = objects.slice(i, i + 1000);
|
|
10094
10164
|
await Promise.all(batch.map(async (obj) => {
|
|
10095
10165
|
if (!obj.Key)
|
|
@@ -10105,9 +10175,7 @@ async function downloadFromS3Helper(s3Client, bucketName, s3Path, localPath) {
|
|
|
10105
10175
|
}
|
|
10106
10176
|
// Count folders
|
|
10107
10177
|
const relativePath = path$1.relative(localPath, path$1.dirname(localFilePath));
|
|
10108
|
-
if (relativePath &&
|
|
10109
|
-
!relativePath.startsWith("..") &&
|
|
10110
|
-
relativePath !== ".") {
|
|
10178
|
+
if (relativePath && !relativePath.startsWith('..') && relativePath !== '.') {
|
|
10111
10179
|
folderCount++;
|
|
10112
10180
|
}
|
|
10113
10181
|
}));
|
|
@@ -10128,7 +10196,7 @@ async function downloadFile(s3Client, bucketName, s3Key, localFilePath) {
|
|
|
10128
10196
|
await withRetry(async () => {
|
|
10129
10197
|
const { Body } = await s3Client.send(getCommand);
|
|
10130
10198
|
if (!Body)
|
|
10131
|
-
throw new Error(
|
|
10199
|
+
throw new Error('No body returned from S3');
|
|
10132
10200
|
const writeStream = fs.createWriteStream(localFilePath);
|
|
10133
10201
|
await promises.pipeline(Body, writeStream);
|
|
10134
10202
|
}, {
|
|
@@ -10141,14 +10209,27 @@ async function downloadFile(s3Client, bucketName, s3Key, localFilePath) {
|
|
|
10141
10209
|
async function unzipFile(zipPath, destPath) {
|
|
10142
10210
|
const zip = new AdmZip(zipPath);
|
|
10143
10211
|
try {
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10212
|
+
// extractAllToAsync is fire-and-forget: it returns void and reports
|
|
10213
|
+
// failures through its callback. The previous call neither awaited it nor
|
|
10214
|
+
// observed the callback, so getDirectoryStats raced an in-flight (or
|
|
10215
|
+
// failed) extraction and any extraction error was silently dropped. Wrap
|
|
10216
|
+
// it in a Promise so the extraction is fully awaited and errors surface.
|
|
10217
|
+
await new Promise((resolve, reject) => {
|
|
10218
|
+
zip.extractAllToAsync(destPath, true, false, (error) => {
|
|
10219
|
+
if (error) {
|
|
10220
|
+
reject(error);
|
|
10221
|
+
}
|
|
10222
|
+
else {
|
|
10223
|
+
resolve();
|
|
10224
|
+
}
|
|
10225
|
+
});
|
|
10226
|
+
});
|
|
10227
|
+
return await getDirectoryStats(destPath);
|
|
10147
10228
|
}
|
|
10148
10229
|
catch (error) {
|
|
10149
10230
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
10150
10231
|
getLumicLogger().error(`Error unzipping file: ${errorMessage}`);
|
|
10151
|
-
throw error;
|
|
10232
|
+
throw new ZipError(`Failed to extract archive "${zipPath}" to "${destPath}": ${errorMessage}`, error);
|
|
10152
10233
|
}
|
|
10153
10234
|
}
|
|
10154
10235
|
async function getDirectoryStats(dirPath) {
|
|
@@ -10184,19 +10265,17 @@ async function emptyBucket(s3Client, bucketName) {
|
|
|
10184
10265
|
Bucket: bucketName,
|
|
10185
10266
|
ContinuationToken: continuationToken,
|
|
10186
10267
|
});
|
|
10187
|
-
const response =
|
|
10268
|
+
const response = await withRetry(() => s3Client.send(listCommand), {
|
|
10188
10269
|
maxRetries: 3,
|
|
10189
10270
|
baseDelayMs: 1000,
|
|
10190
10271
|
maxDelayMs: 15000,
|
|
10191
10272
|
retryableErrors: isRetryableS3Error,
|
|
10192
|
-
}, `S3:ListObjects:${bucketName}`)
|
|
10273
|
+
}, `S3:ListObjects:${bucketName}`);
|
|
10193
10274
|
const { Contents, IsTruncated, NextContinuationToken } = response;
|
|
10194
10275
|
if (Contents && Contents.length > 0) {
|
|
10195
10276
|
const deleteCommand = new clientS3.DeleteObjectsCommand({
|
|
10196
10277
|
Bucket: bucketName,
|
|
10197
|
-
Delete: {
|
|
10198
|
-
Objects: Contents.filter((c) => c.Key).map((c) => ({ Key: c.Key })),
|
|
10199
|
-
},
|
|
10278
|
+
Delete: { Objects: Contents.map(({ Key }) => ({ Key: Key })) },
|
|
10200
10279
|
});
|
|
10201
10280
|
await withRetry(() => s3Client.send(deleteCommand), {
|
|
10202
10281
|
maxRetries: 3,
|
|
@@ -23104,11 +23183,11 @@ let poolConfig = DEFAULT_POOL_CONFIG;
|
|
|
23104
23183
|
async function loadApolloModules() {
|
|
23105
23184
|
if (typeof window === "undefined" || process.env.AWS_EXECUTION_ENV) {
|
|
23106
23185
|
// Server-side (or Lambda): load the CommonJS‑based implementation.
|
|
23107
|
-
return (await Promise.resolve().then(function () { return require('./apollo-client.server-
|
|
23186
|
+
return (await Promise.resolve().then(function () { return require('./apollo-client.server-JYHXy9ib.js'); }));
|
|
23108
23187
|
}
|
|
23109
23188
|
else {
|
|
23110
23189
|
// Client-side: load the ESM‑based implementation.
|
|
23111
|
-
return (await Promise.resolve().then(function () { return require('./apollo-client.client-
|
|
23190
|
+
return (await Promise.resolve().then(function () { return require('./apollo-client.client-CtjgIfRT.js'); }));
|
|
23112
23191
|
}
|
|
23113
23192
|
}
|
|
23114
23193
|
/**
|
|
@@ -79135,7 +79214,7 @@ const RETRY_CONFIG = {
|
|
|
79135
79214
|
BASE_DELAY: 2000, // Increased base delay to 2 seconds
|
|
79136
79215
|
MAX_DELAY: 64000,
|
|
79137
79216
|
MAX_RETRIES: 5,
|
|
79138
|
-
MAX_RANDOM_DELAY: 1000
|
|
79217
|
+
MAX_RANDOM_DELAY: 1000
|
|
79139
79218
|
};
|
|
79140
79219
|
/**
|
|
79141
79220
|
* Determines if an error is related to Google Sheets API quotas or rate limits.
|
|
@@ -79161,10 +79240,10 @@ const RETRY_CONFIG = {
|
|
|
79161
79240
|
*/
|
|
79162
79241
|
function isQuotaError(error) {
|
|
79163
79242
|
const message = error.message.toLowerCase();
|
|
79164
|
-
return
|
|
79165
|
-
message.includes(
|
|
79166
|
-
message.includes(
|
|
79167
|
-
message.includes(
|
|
79243
|
+
return message.includes('quota exceeded') ||
|
|
79244
|
+
message.includes('rate limit') ||
|
|
79245
|
+
message.includes('429') ||
|
|
79246
|
+
message.includes('too many requests');
|
|
79168
79247
|
}
|
|
79169
79248
|
/**
|
|
79170
79249
|
* Implements exponential backoff retry mechanism for Google Sheets API calls.
|
|
@@ -79223,14 +79302,14 @@ async function withExponentialBackoff(operation) {
|
|
|
79223
79302
|
const exponentialDelay = Math.min(Math.pow(2, retries) * RETRY_CONFIG.BASE_DELAY + randomDelay, RETRY_CONFIG.MAX_DELAY);
|
|
79224
79303
|
logIfDebug(`Quota/Rate limit exceeded. Error: ${error.message}`);
|
|
79225
79304
|
logIfDebug(`Retrying in ${exponentialDelay}ms (attempt ${retries}/${RETRY_CONFIG.MAX_RETRIES})`);
|
|
79226
|
-
await new Promise(
|
|
79305
|
+
await new Promise(resolve => setTimeout(resolve, exponentialDelay));
|
|
79227
79306
|
}
|
|
79228
79307
|
}
|
|
79229
79308
|
}
|
|
79230
79309
|
function checkEnvVars() {
|
|
79231
79310
|
const secrets = getSecrets();
|
|
79232
79311
|
if (!secrets.googleSheets.clientEmail || !secrets.googleSheets.privateKey) {
|
|
79233
|
-
throw new Error(
|
|
79312
|
+
throw new Error('GOOGLE_SHEETS_CLIENT_EMAIL or GOOGLE_SHEETS_PRIVATE_KEY is not defined in environment variables.');
|
|
79234
79313
|
}
|
|
79235
79314
|
}
|
|
79236
79315
|
/**
|
|
@@ -79245,16 +79324,16 @@ async function getAuthClient() {
|
|
|
79245
79324
|
const secrets = getSecrets();
|
|
79246
79325
|
const credentials = {
|
|
79247
79326
|
client_email: secrets.googleSheets.clientEmail,
|
|
79248
|
-
private_key: secrets.googleSheets.privateKey?.replace(/\\n/g,
|
|
79327
|
+
private_key: secrets.googleSheets.privateKey?.replace(/\\n/g, '\n'),
|
|
79249
79328
|
};
|
|
79250
79329
|
const auth = new require$$0$6.GoogleAuth({
|
|
79251
79330
|
credentials,
|
|
79252
|
-
scopes: [
|
|
79331
|
+
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
79253
79332
|
});
|
|
79254
79333
|
return auth.getClient();
|
|
79255
79334
|
}
|
|
79256
79335
|
catch (error) {
|
|
79257
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79336
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79258
79337
|
logIfDebug(`Error initializing Google Sheets auth: ${errorMessage}`);
|
|
79259
79338
|
throw new Error(`Failed to initialize Google Sheets auth: ${errorMessage}`);
|
|
79260
79339
|
}
|
|
@@ -79266,7 +79345,7 @@ async function getSheetsClient() {
|
|
|
79266
79345
|
const auth = await getAuthClient();
|
|
79267
79346
|
return new buildExports.sheets_v4.Sheets({
|
|
79268
79347
|
auth,
|
|
79269
|
-
timeout: GOOGLE_SHEETS_TIMEOUT_MS
|
|
79348
|
+
timeout: GOOGLE_SHEETS_TIMEOUT_MS
|
|
79270
79349
|
});
|
|
79271
79350
|
}
|
|
79272
79351
|
/**
|
|
@@ -79299,7 +79378,7 @@ function escapeSheetName(sheetName) {
|
|
|
79299
79378
|
* @param startColumn Optional starting column (defaults to 'A')
|
|
79300
79379
|
* @returns Promise resolving to response with success status and row details
|
|
79301
79380
|
*/
|
|
79302
|
-
async function addRow(config, values, startColumn =
|
|
79381
|
+
async function addRow(config, values, startColumn = 'A') {
|
|
79303
79382
|
try {
|
|
79304
79383
|
const sheets = await getSheetsClient();
|
|
79305
79384
|
const escapedSheetName = escapeSheetName(config.sheetName);
|
|
@@ -79307,7 +79386,7 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79307
79386
|
const response = await withExponentialBackoff(() => sheets.spreadsheets.values.append({
|
|
79308
79387
|
spreadsheetId: config.spreadsheetId,
|
|
79309
79388
|
range,
|
|
79310
|
-
valueInputOption:
|
|
79389
|
+
valueInputOption: 'USER_ENTERED',
|
|
79311
79390
|
requestBody: {
|
|
79312
79391
|
values: [values],
|
|
79313
79392
|
},
|
|
@@ -79315,9 +79394,9 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79315
79394
|
//logIfDebug(`Added row to sheet ${config.sheetName}: ${values.join(', ')}`);
|
|
79316
79395
|
const rowNumber = response.data.updates?.updatedRange
|
|
79317
79396
|
? parseInt(response.data.updates.updatedRange
|
|
79318
|
-
.split(
|
|
79319
|
-
.split(
|
|
79320
|
-
.replace(/[^0-9]/g,
|
|
79397
|
+
.split('!')[1]
|
|
79398
|
+
.split(':')[0]
|
|
79399
|
+
.replace(/[^0-9]/g, ''))
|
|
79321
79400
|
: 0;
|
|
79322
79401
|
return {
|
|
79323
79402
|
success: true,
|
|
@@ -79328,7 +79407,7 @@ async function addRow(config, values, startColumn = "A") {
|
|
|
79328
79407
|
};
|
|
79329
79408
|
}
|
|
79330
79409
|
catch (error) {
|
|
79331
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79410
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79332
79411
|
logIfDebug(`Error adding row to sheet: ${errorMessage}`);
|
|
79333
79412
|
return {
|
|
79334
79413
|
success: false,
|
|
@@ -79370,7 +79449,7 @@ async function addSheet(spreadsheetId, sheetTitle) {
|
|
|
79370
79449
|
};
|
|
79371
79450
|
}
|
|
79372
79451
|
catch (error) {
|
|
79373
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79452
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79374
79453
|
logIfDebug(`Error adding sheet: ${errorMessage}`);
|
|
79375
79454
|
return {
|
|
79376
79455
|
success: false,
|
|
@@ -79394,7 +79473,7 @@ async function writeCell(config, cell, value) {
|
|
|
79394
79473
|
await withExponentialBackoff(() => sheets.spreadsheets.values.update({
|
|
79395
79474
|
spreadsheetId: config.spreadsheetId,
|
|
79396
79475
|
range,
|
|
79397
|
-
valueInputOption:
|
|
79476
|
+
valueInputOption: 'USER_ENTERED',
|
|
79398
79477
|
requestBody: {
|
|
79399
79478
|
values: [[value]],
|
|
79400
79479
|
},
|
|
@@ -79408,7 +79487,7 @@ async function writeCell(config, cell, value) {
|
|
|
79408
79487
|
};
|
|
79409
79488
|
}
|
|
79410
79489
|
catch (error) {
|
|
79411
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79490
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79412
79491
|
logIfDebug(`Error writing to cell: ${errorMessage}`);
|
|
79413
79492
|
return {
|
|
79414
79493
|
success: false,
|
|
@@ -79432,11 +79511,8 @@ async function getCell(config, cell) {
|
|
|
79432
79511
|
spreadsheetId: config.spreadsheetId,
|
|
79433
79512
|
range,
|
|
79434
79513
|
}));
|
|
79435
|
-
const
|
|
79436
|
-
|
|
79437
|
-
? null
|
|
79438
|
-
: rawValue;
|
|
79439
|
-
logIfDebug(`Read value from cell ${range}: ${String(value)}`);
|
|
79514
|
+
const value = response.data.values?.[0]?.[0];
|
|
79515
|
+
logIfDebug(`Read value from cell ${range}: ${value}`);
|
|
79440
79516
|
return {
|
|
79441
79517
|
success: true,
|
|
79442
79518
|
data: {
|
|
@@ -79445,7 +79521,7 @@ async function getCell(config, cell) {
|
|
|
79445
79521
|
};
|
|
79446
79522
|
}
|
|
79447
79523
|
catch (error) {
|
|
79448
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79524
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79449
79525
|
logIfDebug(`Error reading from cell: ${errorMessage}`);
|
|
79450
79526
|
return {
|
|
79451
79527
|
success: false,
|
|
@@ -79478,11 +79554,10 @@ function convertA1ToRowCol(a1Notation) {
|
|
|
79478
79554
|
* @returns Padded array of arrays
|
|
79479
79555
|
*/
|
|
79480
79556
|
function padArrays(arrays) {
|
|
79481
|
-
const maxLength = Math.max(...arrays.map(
|
|
79482
|
-
return arrays.map(
|
|
79557
|
+
const maxLength = Math.max(...arrays.map(arr => arr.length));
|
|
79558
|
+
return arrays.map(arr => {
|
|
79483
79559
|
if (arr.length < maxLength) {
|
|
79484
|
-
|
|
79485
|
-
return [...arr, ...padding];
|
|
79560
|
+
return [...arr, ...Array(maxLength - arr.length).fill('')];
|
|
79486
79561
|
}
|
|
79487
79562
|
return arr;
|
|
79488
79563
|
});
|
|
@@ -79494,10 +79569,10 @@ function padArrays(arrays) {
|
|
|
79494
79569
|
* @param startCell Optional starting cell in A1 notation (defaults to 'A1')
|
|
79495
79570
|
* @returns Promise resolving to response with success status and update details
|
|
79496
79571
|
*/
|
|
79497
|
-
async function writeArray(config, data, startCell =
|
|
79572
|
+
async function writeArray(config, data, startCell = 'A1') {
|
|
79498
79573
|
try {
|
|
79499
79574
|
if (!data.length) {
|
|
79500
|
-
throw new Error(
|
|
79575
|
+
throw new Error('Data array is empty');
|
|
79501
79576
|
}
|
|
79502
79577
|
const sheets = await getSheetsClient();
|
|
79503
79578
|
const { row: startRow, column: startCol } = convertA1ToRowCol(startCell);
|
|
@@ -79513,7 +79588,7 @@ async function writeArray(config, data, startCell = "A1") {
|
|
|
79513
79588
|
const response = await withExponentialBackoff(() => sheets.spreadsheets.values.update({
|
|
79514
79589
|
spreadsheetId: config.spreadsheetId,
|
|
79515
79590
|
range,
|
|
79516
|
-
valueInputOption:
|
|
79591
|
+
valueInputOption: 'USER_ENTERED',
|
|
79517
79592
|
requestBody: {
|
|
79518
79593
|
values: paddedData,
|
|
79519
79594
|
},
|
|
@@ -79529,7 +79604,7 @@ async function writeArray(config, data, startCell = "A1") {
|
|
|
79529
79604
|
};
|
|
79530
79605
|
}
|
|
79531
79606
|
catch (error) {
|
|
79532
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
79607
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
79533
79608
|
logIfDebug(`Error writing array to sheet: ${errorMessage}`);
|
|
79534
79609
|
return {
|
|
79535
79610
|
success: false,
|
|
@@ -81823,4 +81898,4 @@ exports.withCorrelationId = withCorrelationId;
|
|
|
81823
81898
|
exports.withMetrics = withMetrics;
|
|
81824
81899
|
exports.withRateLimit = withRateLimit;
|
|
81825
81900
|
exports.withRetry = withRetry;
|
|
81826
|
-
//# sourceMappingURL=index-
|
|
81901
|
+
//# sourceMappingURL=index-DgFDhFuO.js.map
|