@ljoukov/llm 0.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -12
- package/dist/index.cjs +519 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -6
- package/dist/index.d.ts +53 -6
- package/dist/index.js +518 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,10 +1518,13 @@ function convertGooglePartsToLlmParts(parts) {
|
|
|
1518
1518
|
function assertLlmRole(value) {
|
|
1519
1519
|
switch (value) {
|
|
1520
1520
|
case "user":
|
|
1521
|
-
case "
|
|
1521
|
+
case "assistant":
|
|
1522
1522
|
case "system":
|
|
1523
|
+
case "developer":
|
|
1523
1524
|
case "tool":
|
|
1524
1525
|
return value;
|
|
1526
|
+
case "model":
|
|
1527
|
+
return "assistant";
|
|
1525
1528
|
default:
|
|
1526
1529
|
throw new Error(`Unsupported LLM role: ${String(value)}`);
|
|
1527
1530
|
}
|
|
@@ -1551,8 +1554,9 @@ function toGeminiPart(part) {
|
|
|
1551
1554
|
}
|
|
1552
1555
|
}
|
|
1553
1556
|
function convertLlmContentToGeminiContent(content) {
|
|
1557
|
+
const role = content.role === "assistant" ? "model" : "user";
|
|
1554
1558
|
return {
|
|
1555
|
-
role
|
|
1559
|
+
role,
|
|
1556
1560
|
parts: content.parts.map(toGeminiPart)
|
|
1557
1561
|
};
|
|
1558
1562
|
}
|
|
@@ -1598,6 +1602,20 @@ function isInlineImageMime(mimeType) {
|
|
|
1598
1602
|
}
|
|
1599
1603
|
return mimeType.startsWith("image/");
|
|
1600
1604
|
}
|
|
1605
|
+
function guessInlineDataFilename(mimeType) {
|
|
1606
|
+
switch (mimeType) {
|
|
1607
|
+
case "application/pdf":
|
|
1608
|
+
return "document.pdf";
|
|
1609
|
+
case "application/json":
|
|
1610
|
+
return "data.json";
|
|
1611
|
+
case "text/markdown":
|
|
1612
|
+
return "document.md";
|
|
1613
|
+
case "text/plain":
|
|
1614
|
+
return "document.txt";
|
|
1615
|
+
default:
|
|
1616
|
+
return "attachment.bin";
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1601
1619
|
function mergeConsecutiveTextParts(parts) {
|
|
1602
1620
|
if (parts.length === 0) {
|
|
1603
1621
|
return [];
|
|
@@ -1709,28 +1727,350 @@ function parseJsonFromLlmText(rawText) {
|
|
|
1709
1727
|
const repairedText = escapeNewlinesInStrings(cleanedText);
|
|
1710
1728
|
return JSON.parse(repairedText);
|
|
1711
1729
|
}
|
|
1712
|
-
function
|
|
1713
|
-
|
|
1714
|
-
|
|
1730
|
+
function parsePartialJsonFromLlmText(rawText) {
|
|
1731
|
+
const jsonStart = extractJsonStartText(rawText);
|
|
1732
|
+
if (!jsonStart) {
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
try {
|
|
1736
|
+
return parsePartialJson(jsonStart);
|
|
1737
|
+
} catch {
|
|
1738
|
+
return null;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
function extractJsonStartText(rawText) {
|
|
1742
|
+
let text = rawText.trimStart();
|
|
1743
|
+
if (text.startsWith("```")) {
|
|
1744
|
+
text = text.replace(/^```[a-zA-Z0-9_-]*\s*\n?/, "");
|
|
1745
|
+
}
|
|
1746
|
+
const objIndex = text.indexOf("{");
|
|
1747
|
+
const arrIndex = text.indexOf("[");
|
|
1748
|
+
let start = -1;
|
|
1749
|
+
if (objIndex !== -1 && arrIndex !== -1) {
|
|
1750
|
+
start = Math.min(objIndex, arrIndex);
|
|
1751
|
+
} else {
|
|
1752
|
+
start = objIndex !== -1 ? objIndex : arrIndex;
|
|
1753
|
+
}
|
|
1754
|
+
if (start === -1) {
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
return text.slice(start);
|
|
1758
|
+
}
|
|
1759
|
+
function parsePartialJson(text) {
|
|
1760
|
+
let i = 0;
|
|
1761
|
+
const len = text.length;
|
|
1762
|
+
const isWhitespace = (char) => char === " " || char === "\n" || char === "\r" || char === " ";
|
|
1763
|
+
const skipWhitespace = () => {
|
|
1764
|
+
while (i < len && isWhitespace(text[i] ?? "")) {
|
|
1765
|
+
i += 1;
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
const parseString = () => {
|
|
1769
|
+
if (text[i] !== '"') {
|
|
1770
|
+
return null;
|
|
1771
|
+
}
|
|
1772
|
+
i += 1;
|
|
1773
|
+
let value = "";
|
|
1774
|
+
while (i < len) {
|
|
1775
|
+
const ch = text[i] ?? "";
|
|
1776
|
+
if (ch === '"') {
|
|
1777
|
+
i += 1;
|
|
1778
|
+
return { value, complete: true };
|
|
1779
|
+
}
|
|
1780
|
+
if (ch === "\\") {
|
|
1781
|
+
if (i + 1 >= len) {
|
|
1782
|
+
return { value, complete: false };
|
|
1783
|
+
}
|
|
1784
|
+
const esc = text[i + 1] ?? "";
|
|
1785
|
+
switch (esc) {
|
|
1786
|
+
case '"':
|
|
1787
|
+
case "\\":
|
|
1788
|
+
case "/":
|
|
1789
|
+
value += esc;
|
|
1790
|
+
i += 2;
|
|
1791
|
+
continue;
|
|
1792
|
+
case "b":
|
|
1793
|
+
value += "\b";
|
|
1794
|
+
i += 2;
|
|
1795
|
+
continue;
|
|
1796
|
+
case "f":
|
|
1797
|
+
value += "\f";
|
|
1798
|
+
i += 2;
|
|
1799
|
+
continue;
|
|
1800
|
+
case "n":
|
|
1801
|
+
value += "\n";
|
|
1802
|
+
i += 2;
|
|
1803
|
+
continue;
|
|
1804
|
+
case "r":
|
|
1805
|
+
value += "\r";
|
|
1806
|
+
i += 2;
|
|
1807
|
+
continue;
|
|
1808
|
+
case "t":
|
|
1809
|
+
value += " ";
|
|
1810
|
+
i += 2;
|
|
1811
|
+
continue;
|
|
1812
|
+
case "u": {
|
|
1813
|
+
if (i + 5 >= len) {
|
|
1814
|
+
return { value, complete: false };
|
|
1815
|
+
}
|
|
1816
|
+
const hex = text.slice(i + 2, i + 6);
|
|
1817
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(hex)) {
|
|
1818
|
+
value += "u";
|
|
1819
|
+
i += 2;
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
value += String.fromCharCode(Number.parseInt(hex, 16));
|
|
1823
|
+
i += 6;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
default:
|
|
1827
|
+
value += esc;
|
|
1828
|
+
i += 2;
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
value += ch;
|
|
1833
|
+
i += 1;
|
|
1834
|
+
}
|
|
1835
|
+
return { value, complete: false };
|
|
1836
|
+
};
|
|
1837
|
+
const parseNumber = () => {
|
|
1838
|
+
const start = i;
|
|
1839
|
+
while (i < len) {
|
|
1840
|
+
const ch = text[i] ?? "";
|
|
1841
|
+
if (isWhitespace(ch) || ch === "," || ch === "}" || ch === "]") {
|
|
1842
|
+
break;
|
|
1843
|
+
}
|
|
1844
|
+
i += 1;
|
|
1845
|
+
}
|
|
1846
|
+
const raw = text.slice(start, i);
|
|
1847
|
+
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/u.test(raw)) {
|
|
1848
|
+
i = start;
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
return { value: Number(raw), complete: true };
|
|
1852
|
+
};
|
|
1853
|
+
const parseLiteral = () => {
|
|
1854
|
+
if (text.startsWith("true", i)) {
|
|
1855
|
+
i += 4;
|
|
1856
|
+
return { value: true, complete: true };
|
|
1857
|
+
}
|
|
1858
|
+
if (text.startsWith("false", i)) {
|
|
1859
|
+
i += 5;
|
|
1860
|
+
return { value: false, complete: true };
|
|
1861
|
+
}
|
|
1862
|
+
if (text.startsWith("null", i)) {
|
|
1863
|
+
i += 4;
|
|
1864
|
+
return { value: null, complete: true };
|
|
1865
|
+
}
|
|
1866
|
+
return null;
|
|
1867
|
+
};
|
|
1868
|
+
skipWhitespace();
|
|
1869
|
+
const first = text[i];
|
|
1870
|
+
if (first !== "{" && first !== "[") {
|
|
1871
|
+
return null;
|
|
1715
1872
|
}
|
|
1873
|
+
const root = first === "{" ? {} : [];
|
|
1874
|
+
const stack = first === "{" ? [{ type: "object", value: root, state: "keyOrEnd" }] : [{ type: "array", value: root, state: "valueOrEnd" }];
|
|
1875
|
+
i += 1;
|
|
1876
|
+
while (stack.length > 0) {
|
|
1877
|
+
skipWhitespace();
|
|
1878
|
+
if (i >= len) {
|
|
1879
|
+
break;
|
|
1880
|
+
}
|
|
1881
|
+
const ctx = stack[stack.length - 1];
|
|
1882
|
+
if (!ctx) {
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
const ch = text[i] ?? "";
|
|
1886
|
+
if (ctx.type === "object") {
|
|
1887
|
+
if (ctx.state === "keyOrEnd") {
|
|
1888
|
+
if (ch === "}") {
|
|
1889
|
+
i += 1;
|
|
1890
|
+
stack.pop();
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
if (ch === ",") {
|
|
1894
|
+
i += 1;
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (ch !== '"') {
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1900
|
+
const key = parseString();
|
|
1901
|
+
if (!key) {
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
if (!key.complete) {
|
|
1905
|
+
break;
|
|
1906
|
+
}
|
|
1907
|
+
ctx.key = key.value;
|
|
1908
|
+
ctx.state = "colon";
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
if (ctx.state === "colon") {
|
|
1912
|
+
if (ch === ":") {
|
|
1913
|
+
i += 1;
|
|
1914
|
+
ctx.state = "value";
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
if (ctx.state === "value") {
|
|
1920
|
+
if (ch === "}") {
|
|
1921
|
+
i += 1;
|
|
1922
|
+
ctx.key = void 0;
|
|
1923
|
+
stack.pop();
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
if (ch === ",") {
|
|
1927
|
+
i += 1;
|
|
1928
|
+
ctx.key = void 0;
|
|
1929
|
+
ctx.state = "keyOrEnd";
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
const key = ctx.key;
|
|
1933
|
+
if (!key) {
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
if (ch === "{" || ch === "[") {
|
|
1937
|
+
const container = ch === "{" ? {} : [];
|
|
1938
|
+
ctx.value[key] = container;
|
|
1939
|
+
ctx.key = void 0;
|
|
1940
|
+
ctx.state = "commaOrEnd";
|
|
1941
|
+
stack.push(
|
|
1942
|
+
ch === "{" ? { type: "object", value: container, state: "keyOrEnd" } : { type: "array", value: container, state: "valueOrEnd" }
|
|
1943
|
+
);
|
|
1944
|
+
i += 1;
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
let primitive = null;
|
|
1948
|
+
if (ch === '"') {
|
|
1949
|
+
primitive = parseString();
|
|
1950
|
+
} else if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
1951
|
+
primitive = parseNumber();
|
|
1952
|
+
} else {
|
|
1953
|
+
primitive = parseLiteral();
|
|
1954
|
+
}
|
|
1955
|
+
if (!primitive) {
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
ctx.value[key] = primitive.value;
|
|
1959
|
+
ctx.key = void 0;
|
|
1960
|
+
ctx.state = "commaOrEnd";
|
|
1961
|
+
if (!primitive.complete) {
|
|
1962
|
+
break;
|
|
1963
|
+
}
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
if (ctx.state === "commaOrEnd") {
|
|
1967
|
+
if (ch === ",") {
|
|
1968
|
+
i += 1;
|
|
1969
|
+
ctx.state = "keyOrEnd";
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
if (ch === "}") {
|
|
1973
|
+
i += 1;
|
|
1974
|
+
stack.pop();
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
} else {
|
|
1980
|
+
if (ctx.state === "valueOrEnd") {
|
|
1981
|
+
if (ch === "]") {
|
|
1982
|
+
i += 1;
|
|
1983
|
+
stack.pop();
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
if (ch === ",") {
|
|
1987
|
+
i += 1;
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
if (ch === "{" || ch === "[") {
|
|
1991
|
+
const container = ch === "{" ? {} : [];
|
|
1992
|
+
ctx.value.push(container);
|
|
1993
|
+
ctx.state = "commaOrEnd";
|
|
1994
|
+
stack.push(
|
|
1995
|
+
ch === "{" ? { type: "object", value: container, state: "keyOrEnd" } : { type: "array", value: container, state: "valueOrEnd" }
|
|
1996
|
+
);
|
|
1997
|
+
i += 1;
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
let primitive = null;
|
|
2001
|
+
if (ch === '"') {
|
|
2002
|
+
primitive = parseString();
|
|
2003
|
+
} else if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
2004
|
+
primitive = parseNumber();
|
|
2005
|
+
} else {
|
|
2006
|
+
primitive = parseLiteral();
|
|
2007
|
+
}
|
|
2008
|
+
if (!primitive) {
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
ctx.value.push(primitive.value);
|
|
2012
|
+
ctx.state = "commaOrEnd";
|
|
2013
|
+
if (!primitive.complete) {
|
|
2014
|
+
break;
|
|
2015
|
+
}
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
if (ctx.state === "commaOrEnd") {
|
|
2019
|
+
if (ch === ",") {
|
|
2020
|
+
i += 1;
|
|
2021
|
+
ctx.state = "valueOrEnd";
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
if (ch === "]") {
|
|
2025
|
+
i += 1;
|
|
2026
|
+
stack.pop();
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return root;
|
|
2034
|
+
}
|
|
2035
|
+
function resolveTextContents(input) {
|
|
1716
2036
|
const contents = [];
|
|
1717
|
-
if (input.
|
|
2037
|
+
if (input.instructions) {
|
|
2038
|
+
const instructions = input.instructions.trim();
|
|
2039
|
+
if (instructions.length > 0) {
|
|
2040
|
+
contents.push({
|
|
2041
|
+
role: "system",
|
|
2042
|
+
parts: [{ type: "text", text: instructions }]
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
if (typeof input.input === "string") {
|
|
1718
2047
|
contents.push({
|
|
1719
|
-
role: "
|
|
1720
|
-
parts: [{ type: "text", text: input.
|
|
2048
|
+
role: "user",
|
|
2049
|
+
parts: [{ type: "text", text: input.input }]
|
|
2050
|
+
});
|
|
2051
|
+
return contents;
|
|
2052
|
+
}
|
|
2053
|
+
for (const message of input.input) {
|
|
2054
|
+
const parts = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
|
|
2055
|
+
contents.push({
|
|
2056
|
+
role: message.role,
|
|
2057
|
+
parts: parts.map(
|
|
2058
|
+
(part) => part.type === "text" ? {
|
|
2059
|
+
type: "text",
|
|
2060
|
+
text: part.text,
|
|
2061
|
+
thought: "thought" in part && part.thought === true ? true : void 0
|
|
2062
|
+
} : { type: "inlineData", data: part.data, mimeType: part.mimeType }
|
|
2063
|
+
)
|
|
1721
2064
|
});
|
|
1722
2065
|
}
|
|
1723
|
-
contents.push({
|
|
1724
|
-
role: "user",
|
|
1725
|
-
parts: [{ type: "text", text: input.prompt }]
|
|
1726
|
-
});
|
|
1727
2066
|
return contents;
|
|
1728
2067
|
}
|
|
1729
2068
|
function toOpenAiInput(contents) {
|
|
1730
2069
|
const OPENAI_ROLE_FROM_LLM = {
|
|
1731
2070
|
user: "user",
|
|
1732
|
-
|
|
2071
|
+
assistant: "assistant",
|
|
1733
2072
|
system: "system",
|
|
2073
|
+
developer: "developer",
|
|
1734
2074
|
tool: "assistant"
|
|
1735
2075
|
};
|
|
1736
2076
|
return contents.map((content) => {
|
|
@@ -1740,9 +2080,18 @@ function toOpenAiInput(contents) {
|
|
|
1740
2080
|
parts.push({ type: "input_text", text: part.text });
|
|
1741
2081
|
continue;
|
|
1742
2082
|
}
|
|
1743
|
-
const mimeType = part.mimeType
|
|
1744
|
-
|
|
1745
|
-
|
|
2083
|
+
const mimeType = part.mimeType;
|
|
2084
|
+
if (isInlineImageMime(mimeType)) {
|
|
2085
|
+
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
2086
|
+
parts.push({ type: "input_image", image_url: dataUrl, detail: "auto" });
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
const fileData = decodeInlineDataBuffer(part.data).toString("base64");
|
|
2090
|
+
parts.push({
|
|
2091
|
+
type: "input_file",
|
|
2092
|
+
filename: guessInlineDataFilename(mimeType),
|
|
2093
|
+
file_data: fileData
|
|
2094
|
+
});
|
|
1746
2095
|
}
|
|
1747
2096
|
if (parts.length === 1 && parts[0]?.type === "input_text" && typeof parts[0].text === "string") {
|
|
1748
2097
|
return {
|
|
@@ -1760,7 +2109,7 @@ function toChatGptInput(contents) {
|
|
|
1760
2109
|
const instructionsParts = [];
|
|
1761
2110
|
const input = [];
|
|
1762
2111
|
for (const content of contents) {
|
|
1763
|
-
if (content.role === "system") {
|
|
2112
|
+
if (content.role === "system" || content.role === "developer") {
|
|
1764
2113
|
for (const part of content.parts) {
|
|
1765
2114
|
if (part.type === "text") {
|
|
1766
2115
|
instructionsParts.push(part.text);
|
|
@@ -1768,7 +2117,7 @@ function toChatGptInput(contents) {
|
|
|
1768
2117
|
}
|
|
1769
2118
|
continue;
|
|
1770
2119
|
}
|
|
1771
|
-
const isAssistant = content.role === "
|
|
2120
|
+
const isAssistant = content.role === "assistant" || content.role === "tool";
|
|
1772
2121
|
const parts = [];
|
|
1773
2122
|
for (const part of content.parts) {
|
|
1774
2123
|
if (part.type === "text") {
|
|
@@ -1778,19 +2127,29 @@ function toChatGptInput(contents) {
|
|
|
1778
2127
|
});
|
|
1779
2128
|
continue;
|
|
1780
2129
|
}
|
|
1781
|
-
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
1782
|
-
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
1783
2130
|
if (isAssistant) {
|
|
2131
|
+
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
1784
2132
|
parts.push({
|
|
1785
2133
|
type: "output_text",
|
|
1786
|
-
text: `[image:${mimeType}]`
|
|
2134
|
+
text: isInlineImageMime(part.mimeType) ? `[image:${mimeType}]` : `[file:${mimeType}]`
|
|
1787
2135
|
});
|
|
1788
2136
|
} else {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2137
|
+
if (isInlineImageMime(part.mimeType)) {
|
|
2138
|
+
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
2139
|
+
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
2140
|
+
parts.push({
|
|
2141
|
+
type: "input_image",
|
|
2142
|
+
image_url: dataUrl,
|
|
2143
|
+
detail: "auto"
|
|
2144
|
+
});
|
|
2145
|
+
} else {
|
|
2146
|
+
const fileData = decodeInlineDataBuffer(part.data).toString("base64");
|
|
2147
|
+
parts.push({
|
|
2148
|
+
type: "input_file",
|
|
2149
|
+
filename: guessInlineDataFilename(part.mimeType),
|
|
2150
|
+
file_data: fileData
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
1794
2153
|
}
|
|
1795
2154
|
}
|
|
1796
2155
|
if (parts.length === 0) {
|
|
@@ -2478,7 +2837,7 @@ async function runTextCall(params) {
|
|
|
2478
2837
|
});
|
|
2479
2838
|
}
|
|
2480
2839
|
const mergedParts = mergeConsecutiveTextParts(responseParts);
|
|
2481
|
-
const content = mergedParts.length > 0 ? { role: responseRole ?? "
|
|
2840
|
+
const content = mergedParts.length > 0 ? { role: responseRole ?? "assistant", parts: mergedParts } : void 0;
|
|
2482
2841
|
const { text, thoughts } = extractTextByChannel(content);
|
|
2483
2842
|
const costUsd = estimateCallCostUsd({
|
|
2484
2843
|
modelId: modelVersion,
|
|
@@ -2528,8 +2887,7 @@ async function generateText(request) {
|
|
|
2528
2887
|
}
|
|
2529
2888
|
return await call.result;
|
|
2530
2889
|
}
|
|
2531
|
-
|
|
2532
|
-
const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
|
|
2890
|
+
function buildJsonSchemaConfig(request) {
|
|
2533
2891
|
const schemaName = (request.openAiSchemaName ?? "llm-response").trim() || "llm-response";
|
|
2534
2892
|
const providerInfo = resolveProvider(request.model);
|
|
2535
2893
|
const isOpenAiVariant = providerInfo.provider === "openai" || providerInfo.provider === "chatgpt";
|
|
@@ -2541,31 +2899,143 @@ async function generateJson(request) {
|
|
|
2541
2899
|
if (isOpenAiVariant && !isJsonSchemaObject(responseJsonSchema)) {
|
|
2542
2900
|
throw new Error("OpenAI structured outputs require a JSON object schema at the root.");
|
|
2543
2901
|
}
|
|
2544
|
-
const openAiTextFormat =
|
|
2902
|
+
const openAiTextFormat = isOpenAiVariant ? {
|
|
2545
2903
|
type: "json_schema",
|
|
2546
2904
|
name: schemaName,
|
|
2547
2905
|
strict: true,
|
|
2548
2906
|
schema: normalizeOpenAiSchema(responseJsonSchema)
|
|
2549
2907
|
} : void 0;
|
|
2908
|
+
return { providerInfo, responseJsonSchema, openAiTextFormat };
|
|
2909
|
+
}
|
|
2910
|
+
function streamJson(request) {
|
|
2911
|
+
const queue = createAsyncQueue();
|
|
2912
|
+
const abortController = new AbortController();
|
|
2913
|
+
const resolveAbortSignal = () => {
|
|
2914
|
+
if (!request.signal) {
|
|
2915
|
+
return abortController.signal;
|
|
2916
|
+
}
|
|
2917
|
+
if (request.signal.aborted) {
|
|
2918
|
+
abortController.abort(request.signal.reason);
|
|
2919
|
+
} else {
|
|
2920
|
+
request.signal.addEventListener(
|
|
2921
|
+
"abort",
|
|
2922
|
+
() => abortController.abort(request.signal?.reason),
|
|
2923
|
+
{
|
|
2924
|
+
once: true
|
|
2925
|
+
}
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
return abortController.signal;
|
|
2929
|
+
};
|
|
2930
|
+
const result = (async () => {
|
|
2931
|
+
const signal = resolveAbortSignal();
|
|
2932
|
+
const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
|
|
2933
|
+
const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
|
|
2934
|
+
const streamMode = request.streamMode ?? "partial";
|
|
2935
|
+
const failures = [];
|
|
2936
|
+
let openAiTextFormatForAttempt = openAiTextFormat;
|
|
2937
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2938
|
+
let rawText = "";
|
|
2939
|
+
let lastPartial = "";
|
|
2940
|
+
try {
|
|
2941
|
+
const call = streamText({
|
|
2942
|
+
model: request.model,
|
|
2943
|
+
input: request.input,
|
|
2944
|
+
instructions: request.instructions,
|
|
2945
|
+
tools: request.tools,
|
|
2946
|
+
responseMimeType: request.responseMimeType ?? "application/json",
|
|
2947
|
+
responseJsonSchema,
|
|
2948
|
+
openAiReasoningEffort: request.openAiReasoningEffort,
|
|
2949
|
+
...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
|
|
2950
|
+
signal
|
|
2951
|
+
});
|
|
2952
|
+
try {
|
|
2953
|
+
for await (const event of call.events) {
|
|
2954
|
+
queue.push(event);
|
|
2955
|
+
if (event.type === "delta" && event.channel === "response") {
|
|
2956
|
+
rawText += event.text;
|
|
2957
|
+
if (streamMode === "partial") {
|
|
2958
|
+
const partial = parsePartialJsonFromLlmText(rawText);
|
|
2959
|
+
if (partial !== null) {
|
|
2960
|
+
const serialized = JSON.stringify(partial);
|
|
2961
|
+
if (serialized !== lastPartial) {
|
|
2962
|
+
lastPartial = serialized;
|
|
2963
|
+
queue.push({
|
|
2964
|
+
type: "json",
|
|
2965
|
+
stage: "partial",
|
|
2966
|
+
value: partial
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
} catch (streamError) {
|
|
2974
|
+
await call.result.catch(() => void 0);
|
|
2975
|
+
throw streamError;
|
|
2976
|
+
}
|
|
2977
|
+
const result2 = await call.result;
|
|
2978
|
+
rawText = rawText || result2.text;
|
|
2979
|
+
const cleanedText = normalizeJsonText(rawText);
|
|
2980
|
+
const repairedText = escapeNewlinesInStrings(cleanedText);
|
|
2981
|
+
const payload = JSON.parse(repairedText);
|
|
2982
|
+
const normalized = typeof request.normalizeJson === "function" ? request.normalizeJson(payload) : payload;
|
|
2983
|
+
const parsed = request.schema.parse(normalized);
|
|
2984
|
+
queue.push({ type: "json", stage: "final", value: parsed });
|
|
2985
|
+
queue.close();
|
|
2986
|
+
return { value: parsed, rawText, result: result2 };
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
const handled = error instanceof Error ? error : new Error(String(error));
|
|
2989
|
+
failures.push({ attempt, rawText, error: handled });
|
|
2990
|
+
if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
|
|
2991
|
+
openAiTextFormatForAttempt = void 0;
|
|
2992
|
+
}
|
|
2993
|
+
if (attempt >= maxAttempts) {
|
|
2994
|
+
throw new LlmJsonCallError(`LLM JSON call failed after ${attempt} attempt(s)`, failures);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
throw new LlmJsonCallError("LLM JSON call failed", failures);
|
|
2999
|
+
})().catch((error) => {
|
|
3000
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3001
|
+
queue.fail(err);
|
|
3002
|
+
throw err;
|
|
3003
|
+
});
|
|
3004
|
+
return {
|
|
3005
|
+
events: queue.iterable,
|
|
3006
|
+
result,
|
|
3007
|
+
abort: () => abortController.abort()
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
async function generateJson(request) {
|
|
3011
|
+
const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
|
|
3012
|
+
const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
|
|
3013
|
+
let openAiTextFormatForAttempt = openAiTextFormat;
|
|
2550
3014
|
const failures = [];
|
|
2551
3015
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2552
3016
|
let rawText = "";
|
|
2553
3017
|
try {
|
|
2554
|
-
const contents = resolveTextContents(request);
|
|
2555
3018
|
const call = streamText({
|
|
2556
3019
|
model: request.model,
|
|
2557
|
-
|
|
3020
|
+
input: request.input,
|
|
3021
|
+
instructions: request.instructions,
|
|
2558
3022
|
tools: request.tools,
|
|
2559
3023
|
responseMimeType: request.responseMimeType ?? "application/json",
|
|
2560
3024
|
responseJsonSchema,
|
|
2561
3025
|
openAiReasoningEffort: request.openAiReasoningEffort,
|
|
2562
|
-
...
|
|
3026
|
+
...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
|
|
2563
3027
|
signal: request.signal
|
|
2564
3028
|
});
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
3029
|
+
try {
|
|
3030
|
+
for await (const event of call.events) {
|
|
3031
|
+
request.onEvent?.(event);
|
|
3032
|
+
if (event.type === "delta" && event.channel === "response") {
|
|
3033
|
+
rawText += event.text;
|
|
3034
|
+
}
|
|
2568
3035
|
}
|
|
3036
|
+
} catch (streamError) {
|
|
3037
|
+
await call.result.catch(() => void 0);
|
|
3038
|
+
throw streamError;
|
|
2569
3039
|
}
|
|
2570
3040
|
const result = await call.result;
|
|
2571
3041
|
rawText = rawText || result.text;
|
|
@@ -2578,6 +3048,9 @@ async function generateJson(request) {
|
|
|
2578
3048
|
} catch (error) {
|
|
2579
3049
|
const handled = error instanceof Error ? error : new Error(String(error));
|
|
2580
3050
|
failures.push({ attempt, rawText, error: handled });
|
|
3051
|
+
if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
|
|
3052
|
+
openAiTextFormatForAttempt = void 0;
|
|
3053
|
+
}
|
|
2581
3054
|
if (attempt >= maxAttempts) {
|
|
2582
3055
|
throw new LlmJsonCallError(`LLM JSON call failed after ${attempt} attempt(s)`, failures);
|
|
2583
3056
|
}
|
|
@@ -3163,7 +3636,7 @@ async function gradeGeneratedImage(params) {
|
|
|
3163
3636
|
];
|
|
3164
3637
|
const { value } = await generateJson({
|
|
3165
3638
|
model: params.model,
|
|
3166
|
-
|
|
3639
|
+
input: [{ role: "user", content: parts }],
|
|
3167
3640
|
schema: IMAGE_GRADE_SCHEMA
|
|
3168
3641
|
});
|
|
3169
3642
|
return value;
|
|
@@ -3240,7 +3713,7 @@ async function generateImages(request) {
|
|
|
3240
3713
|
lines.push(`\\nPlease make all ${pending.length} remaining images.`);
|
|
3241
3714
|
return [{ type: "text", text: lines.join("\\n") }];
|
|
3242
3715
|
};
|
|
3243
|
-
const
|
|
3716
|
+
const inputMessages = [{ role: "user", content: buildInitialPromptParts() }];
|
|
3244
3717
|
const orderedEntries = [...promptEntries];
|
|
3245
3718
|
const resolvedImages = /* @__PURE__ */ new Map();
|
|
3246
3719
|
const removeResolvedEntries = (resolved) => {
|
|
@@ -3260,7 +3733,7 @@ async function generateImages(request) {
|
|
|
3260
3733
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
3261
3734
|
const result = await generateText({
|
|
3262
3735
|
model: request.model,
|
|
3263
|
-
|
|
3736
|
+
input: inputMessages,
|
|
3264
3737
|
responseModalities: ["IMAGE", "TEXT"],
|
|
3265
3738
|
imageAspectRatio: request.imageAspectRatio,
|
|
3266
3739
|
imageSize: request.imageSize ?? "2K"
|
|
@@ -3307,8 +3780,11 @@ async function generateImages(request) {
|
|
|
3307
3780
|
if (promptEntries.length === 0) {
|
|
3308
3781
|
break;
|
|
3309
3782
|
}
|
|
3310
|
-
|
|
3311
|
-
|
|
3783
|
+
inputMessages.push({
|
|
3784
|
+
role: "assistant",
|
|
3785
|
+
content: result.content.parts
|
|
3786
|
+
});
|
|
3787
|
+
inputMessages.push({ role: "user", content: buildContinuationPromptParts(promptEntries) });
|
|
3312
3788
|
}
|
|
3313
3789
|
const orderedImages = [];
|
|
3314
3790
|
for (const entry of orderedEntries) {
|
|
@@ -3404,6 +3880,7 @@ export {
|
|
|
3404
3880
|
refreshChatGptOauthToken,
|
|
3405
3881
|
runToolLoop,
|
|
3406
3882
|
sanitisePartForLogging,
|
|
3883
|
+
streamJson,
|
|
3407
3884
|
streamText,
|
|
3408
3885
|
stripCodexCitationMarkers,
|
|
3409
3886
|
toGeminiJsonSchema,
|