@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.cjs
CHANGED
|
@@ -51,6 +51,7 @@ __export(index_exports, {
|
|
|
51
51
|
refreshChatGptOauthToken: () => refreshChatGptOauthToken,
|
|
52
52
|
runToolLoop: () => runToolLoop,
|
|
53
53
|
sanitisePartForLogging: () => sanitisePartForLogging,
|
|
54
|
+
streamJson: () => streamJson,
|
|
54
55
|
streamText: () => streamText,
|
|
55
56
|
stripCodexCitationMarkers: () => stripCodexCitationMarkers,
|
|
56
57
|
toGeminiJsonSchema: () => toGeminiJsonSchema,
|
|
@@ -1575,10 +1576,13 @@ function convertGooglePartsToLlmParts(parts) {
|
|
|
1575
1576
|
function assertLlmRole(value) {
|
|
1576
1577
|
switch (value) {
|
|
1577
1578
|
case "user":
|
|
1578
|
-
case "
|
|
1579
|
+
case "assistant":
|
|
1579
1580
|
case "system":
|
|
1581
|
+
case "developer":
|
|
1580
1582
|
case "tool":
|
|
1581
1583
|
return value;
|
|
1584
|
+
case "model":
|
|
1585
|
+
return "assistant";
|
|
1582
1586
|
default:
|
|
1583
1587
|
throw new Error(`Unsupported LLM role: ${String(value)}`);
|
|
1584
1588
|
}
|
|
@@ -1608,8 +1612,9 @@ function toGeminiPart(part) {
|
|
|
1608
1612
|
}
|
|
1609
1613
|
}
|
|
1610
1614
|
function convertLlmContentToGeminiContent(content) {
|
|
1615
|
+
const role = content.role === "assistant" ? "model" : "user";
|
|
1611
1616
|
return {
|
|
1612
|
-
role
|
|
1617
|
+
role,
|
|
1613
1618
|
parts: content.parts.map(toGeminiPart)
|
|
1614
1619
|
};
|
|
1615
1620
|
}
|
|
@@ -1655,6 +1660,20 @@ function isInlineImageMime(mimeType) {
|
|
|
1655
1660
|
}
|
|
1656
1661
|
return mimeType.startsWith("image/");
|
|
1657
1662
|
}
|
|
1663
|
+
function guessInlineDataFilename(mimeType) {
|
|
1664
|
+
switch (mimeType) {
|
|
1665
|
+
case "application/pdf":
|
|
1666
|
+
return "document.pdf";
|
|
1667
|
+
case "application/json":
|
|
1668
|
+
return "data.json";
|
|
1669
|
+
case "text/markdown":
|
|
1670
|
+
return "document.md";
|
|
1671
|
+
case "text/plain":
|
|
1672
|
+
return "document.txt";
|
|
1673
|
+
default:
|
|
1674
|
+
return "attachment.bin";
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1658
1677
|
function mergeConsecutiveTextParts(parts) {
|
|
1659
1678
|
if (parts.length === 0) {
|
|
1660
1679
|
return [];
|
|
@@ -1766,28 +1785,350 @@ function parseJsonFromLlmText(rawText) {
|
|
|
1766
1785
|
const repairedText = escapeNewlinesInStrings(cleanedText);
|
|
1767
1786
|
return JSON.parse(repairedText);
|
|
1768
1787
|
}
|
|
1769
|
-
function
|
|
1770
|
-
|
|
1771
|
-
|
|
1788
|
+
function parsePartialJsonFromLlmText(rawText) {
|
|
1789
|
+
const jsonStart = extractJsonStartText(rawText);
|
|
1790
|
+
if (!jsonStart) {
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
try {
|
|
1794
|
+
return parsePartialJson(jsonStart);
|
|
1795
|
+
} catch {
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function extractJsonStartText(rawText) {
|
|
1800
|
+
let text = rawText.trimStart();
|
|
1801
|
+
if (text.startsWith("```")) {
|
|
1802
|
+
text = text.replace(/^```[a-zA-Z0-9_-]*\s*\n?/, "");
|
|
1803
|
+
}
|
|
1804
|
+
const objIndex = text.indexOf("{");
|
|
1805
|
+
const arrIndex = text.indexOf("[");
|
|
1806
|
+
let start = -1;
|
|
1807
|
+
if (objIndex !== -1 && arrIndex !== -1) {
|
|
1808
|
+
start = Math.min(objIndex, arrIndex);
|
|
1809
|
+
} else {
|
|
1810
|
+
start = objIndex !== -1 ? objIndex : arrIndex;
|
|
1811
|
+
}
|
|
1812
|
+
if (start === -1) {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
return text.slice(start);
|
|
1816
|
+
}
|
|
1817
|
+
function parsePartialJson(text) {
|
|
1818
|
+
let i = 0;
|
|
1819
|
+
const len = text.length;
|
|
1820
|
+
const isWhitespace = (char) => char === " " || char === "\n" || char === "\r" || char === " ";
|
|
1821
|
+
const skipWhitespace = () => {
|
|
1822
|
+
while (i < len && isWhitespace(text[i] ?? "")) {
|
|
1823
|
+
i += 1;
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
const parseString = () => {
|
|
1827
|
+
if (text[i] !== '"') {
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
i += 1;
|
|
1831
|
+
let value = "";
|
|
1832
|
+
while (i < len) {
|
|
1833
|
+
const ch = text[i] ?? "";
|
|
1834
|
+
if (ch === '"') {
|
|
1835
|
+
i += 1;
|
|
1836
|
+
return { value, complete: true };
|
|
1837
|
+
}
|
|
1838
|
+
if (ch === "\\") {
|
|
1839
|
+
if (i + 1 >= len) {
|
|
1840
|
+
return { value, complete: false };
|
|
1841
|
+
}
|
|
1842
|
+
const esc = text[i + 1] ?? "";
|
|
1843
|
+
switch (esc) {
|
|
1844
|
+
case '"':
|
|
1845
|
+
case "\\":
|
|
1846
|
+
case "/":
|
|
1847
|
+
value += esc;
|
|
1848
|
+
i += 2;
|
|
1849
|
+
continue;
|
|
1850
|
+
case "b":
|
|
1851
|
+
value += "\b";
|
|
1852
|
+
i += 2;
|
|
1853
|
+
continue;
|
|
1854
|
+
case "f":
|
|
1855
|
+
value += "\f";
|
|
1856
|
+
i += 2;
|
|
1857
|
+
continue;
|
|
1858
|
+
case "n":
|
|
1859
|
+
value += "\n";
|
|
1860
|
+
i += 2;
|
|
1861
|
+
continue;
|
|
1862
|
+
case "r":
|
|
1863
|
+
value += "\r";
|
|
1864
|
+
i += 2;
|
|
1865
|
+
continue;
|
|
1866
|
+
case "t":
|
|
1867
|
+
value += " ";
|
|
1868
|
+
i += 2;
|
|
1869
|
+
continue;
|
|
1870
|
+
case "u": {
|
|
1871
|
+
if (i + 5 >= len) {
|
|
1872
|
+
return { value, complete: false };
|
|
1873
|
+
}
|
|
1874
|
+
const hex = text.slice(i + 2, i + 6);
|
|
1875
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(hex)) {
|
|
1876
|
+
value += "u";
|
|
1877
|
+
i += 2;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
value += String.fromCharCode(Number.parseInt(hex, 16));
|
|
1881
|
+
i += 6;
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
default:
|
|
1885
|
+
value += esc;
|
|
1886
|
+
i += 2;
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
value += ch;
|
|
1891
|
+
i += 1;
|
|
1892
|
+
}
|
|
1893
|
+
return { value, complete: false };
|
|
1894
|
+
};
|
|
1895
|
+
const parseNumber = () => {
|
|
1896
|
+
const start = i;
|
|
1897
|
+
while (i < len) {
|
|
1898
|
+
const ch = text[i] ?? "";
|
|
1899
|
+
if (isWhitespace(ch) || ch === "," || ch === "}" || ch === "]") {
|
|
1900
|
+
break;
|
|
1901
|
+
}
|
|
1902
|
+
i += 1;
|
|
1903
|
+
}
|
|
1904
|
+
const raw = text.slice(start, i);
|
|
1905
|
+
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/u.test(raw)) {
|
|
1906
|
+
i = start;
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
return { value: Number(raw), complete: true };
|
|
1910
|
+
};
|
|
1911
|
+
const parseLiteral = () => {
|
|
1912
|
+
if (text.startsWith("true", i)) {
|
|
1913
|
+
i += 4;
|
|
1914
|
+
return { value: true, complete: true };
|
|
1915
|
+
}
|
|
1916
|
+
if (text.startsWith("false", i)) {
|
|
1917
|
+
i += 5;
|
|
1918
|
+
return { value: false, complete: true };
|
|
1919
|
+
}
|
|
1920
|
+
if (text.startsWith("null", i)) {
|
|
1921
|
+
i += 4;
|
|
1922
|
+
return { value: null, complete: true };
|
|
1923
|
+
}
|
|
1924
|
+
return null;
|
|
1925
|
+
};
|
|
1926
|
+
skipWhitespace();
|
|
1927
|
+
const first = text[i];
|
|
1928
|
+
if (first !== "{" && first !== "[") {
|
|
1929
|
+
return null;
|
|
1772
1930
|
}
|
|
1931
|
+
const root = first === "{" ? {} : [];
|
|
1932
|
+
const stack = first === "{" ? [{ type: "object", value: root, state: "keyOrEnd" }] : [{ type: "array", value: root, state: "valueOrEnd" }];
|
|
1933
|
+
i += 1;
|
|
1934
|
+
while (stack.length > 0) {
|
|
1935
|
+
skipWhitespace();
|
|
1936
|
+
if (i >= len) {
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
const ctx = stack[stack.length - 1];
|
|
1940
|
+
if (!ctx) {
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
const ch = text[i] ?? "";
|
|
1944
|
+
if (ctx.type === "object") {
|
|
1945
|
+
if (ctx.state === "keyOrEnd") {
|
|
1946
|
+
if (ch === "}") {
|
|
1947
|
+
i += 1;
|
|
1948
|
+
stack.pop();
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
if (ch === ",") {
|
|
1952
|
+
i += 1;
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
if (ch !== '"') {
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
const key = parseString();
|
|
1959
|
+
if (!key) {
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
if (!key.complete) {
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
ctx.key = key.value;
|
|
1966
|
+
ctx.state = "colon";
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
if (ctx.state === "colon") {
|
|
1970
|
+
if (ch === ":") {
|
|
1971
|
+
i += 1;
|
|
1972
|
+
ctx.state = "value";
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
if (ctx.state === "value") {
|
|
1978
|
+
if (ch === "}") {
|
|
1979
|
+
i += 1;
|
|
1980
|
+
ctx.key = void 0;
|
|
1981
|
+
stack.pop();
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (ch === ",") {
|
|
1985
|
+
i += 1;
|
|
1986
|
+
ctx.key = void 0;
|
|
1987
|
+
ctx.state = "keyOrEnd";
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
const key = ctx.key;
|
|
1991
|
+
if (!key) {
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1994
|
+
if (ch === "{" || ch === "[") {
|
|
1995
|
+
const container = ch === "{" ? {} : [];
|
|
1996
|
+
ctx.value[key] = container;
|
|
1997
|
+
ctx.key = void 0;
|
|
1998
|
+
ctx.state = "commaOrEnd";
|
|
1999
|
+
stack.push(
|
|
2000
|
+
ch === "{" ? { type: "object", value: container, state: "keyOrEnd" } : { type: "array", value: container, state: "valueOrEnd" }
|
|
2001
|
+
);
|
|
2002
|
+
i += 1;
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
let primitive = null;
|
|
2006
|
+
if (ch === '"') {
|
|
2007
|
+
primitive = parseString();
|
|
2008
|
+
} else if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
2009
|
+
primitive = parseNumber();
|
|
2010
|
+
} else {
|
|
2011
|
+
primitive = parseLiteral();
|
|
2012
|
+
}
|
|
2013
|
+
if (!primitive) {
|
|
2014
|
+
break;
|
|
2015
|
+
}
|
|
2016
|
+
ctx.value[key] = primitive.value;
|
|
2017
|
+
ctx.key = void 0;
|
|
2018
|
+
ctx.state = "commaOrEnd";
|
|
2019
|
+
if (!primitive.complete) {
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
if (ctx.state === "commaOrEnd") {
|
|
2025
|
+
if (ch === ",") {
|
|
2026
|
+
i += 1;
|
|
2027
|
+
ctx.state = "keyOrEnd";
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (ch === "}") {
|
|
2031
|
+
i += 1;
|
|
2032
|
+
stack.pop();
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
} else {
|
|
2038
|
+
if (ctx.state === "valueOrEnd") {
|
|
2039
|
+
if (ch === "]") {
|
|
2040
|
+
i += 1;
|
|
2041
|
+
stack.pop();
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
if (ch === ",") {
|
|
2045
|
+
i += 1;
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
if (ch === "{" || ch === "[") {
|
|
2049
|
+
const container = ch === "{" ? {} : [];
|
|
2050
|
+
ctx.value.push(container);
|
|
2051
|
+
ctx.state = "commaOrEnd";
|
|
2052
|
+
stack.push(
|
|
2053
|
+
ch === "{" ? { type: "object", value: container, state: "keyOrEnd" } : { type: "array", value: container, state: "valueOrEnd" }
|
|
2054
|
+
);
|
|
2055
|
+
i += 1;
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
let primitive = null;
|
|
2059
|
+
if (ch === '"') {
|
|
2060
|
+
primitive = parseString();
|
|
2061
|
+
} else if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
2062
|
+
primitive = parseNumber();
|
|
2063
|
+
} else {
|
|
2064
|
+
primitive = parseLiteral();
|
|
2065
|
+
}
|
|
2066
|
+
if (!primitive) {
|
|
2067
|
+
break;
|
|
2068
|
+
}
|
|
2069
|
+
ctx.value.push(primitive.value);
|
|
2070
|
+
ctx.state = "commaOrEnd";
|
|
2071
|
+
if (!primitive.complete) {
|
|
2072
|
+
break;
|
|
2073
|
+
}
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
if (ctx.state === "commaOrEnd") {
|
|
2077
|
+
if (ch === ",") {
|
|
2078
|
+
i += 1;
|
|
2079
|
+
ctx.state = "valueOrEnd";
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
if (ch === "]") {
|
|
2083
|
+
i += 1;
|
|
2084
|
+
stack.pop();
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
break;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return root;
|
|
2092
|
+
}
|
|
2093
|
+
function resolveTextContents(input) {
|
|
1773
2094
|
const contents = [];
|
|
1774
|
-
if (input.
|
|
2095
|
+
if (input.instructions) {
|
|
2096
|
+
const instructions = input.instructions.trim();
|
|
2097
|
+
if (instructions.length > 0) {
|
|
2098
|
+
contents.push({
|
|
2099
|
+
role: "system",
|
|
2100
|
+
parts: [{ type: "text", text: instructions }]
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (typeof input.input === "string") {
|
|
1775
2105
|
contents.push({
|
|
1776
|
-
role: "
|
|
1777
|
-
parts: [{ type: "text", text: input.
|
|
2106
|
+
role: "user",
|
|
2107
|
+
parts: [{ type: "text", text: input.input }]
|
|
2108
|
+
});
|
|
2109
|
+
return contents;
|
|
2110
|
+
}
|
|
2111
|
+
for (const message of input.input) {
|
|
2112
|
+
const parts = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
|
|
2113
|
+
contents.push({
|
|
2114
|
+
role: message.role,
|
|
2115
|
+
parts: parts.map(
|
|
2116
|
+
(part) => part.type === "text" ? {
|
|
2117
|
+
type: "text",
|
|
2118
|
+
text: part.text,
|
|
2119
|
+
thought: "thought" in part && part.thought === true ? true : void 0
|
|
2120
|
+
} : { type: "inlineData", data: part.data, mimeType: part.mimeType }
|
|
2121
|
+
)
|
|
1778
2122
|
});
|
|
1779
2123
|
}
|
|
1780
|
-
contents.push({
|
|
1781
|
-
role: "user",
|
|
1782
|
-
parts: [{ type: "text", text: input.prompt }]
|
|
1783
|
-
});
|
|
1784
2124
|
return contents;
|
|
1785
2125
|
}
|
|
1786
2126
|
function toOpenAiInput(contents) {
|
|
1787
2127
|
const OPENAI_ROLE_FROM_LLM = {
|
|
1788
2128
|
user: "user",
|
|
1789
|
-
|
|
2129
|
+
assistant: "assistant",
|
|
1790
2130
|
system: "system",
|
|
2131
|
+
developer: "developer",
|
|
1791
2132
|
tool: "assistant"
|
|
1792
2133
|
};
|
|
1793
2134
|
return contents.map((content) => {
|
|
@@ -1797,9 +2138,18 @@ function toOpenAiInput(contents) {
|
|
|
1797
2138
|
parts.push({ type: "input_text", text: part.text });
|
|
1798
2139
|
continue;
|
|
1799
2140
|
}
|
|
1800
|
-
const mimeType = part.mimeType
|
|
1801
|
-
|
|
1802
|
-
|
|
2141
|
+
const mimeType = part.mimeType;
|
|
2142
|
+
if (isInlineImageMime(mimeType)) {
|
|
2143
|
+
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
2144
|
+
parts.push({ type: "input_image", image_url: dataUrl, detail: "auto" });
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
const fileData = decodeInlineDataBuffer(part.data).toString("base64");
|
|
2148
|
+
parts.push({
|
|
2149
|
+
type: "input_file",
|
|
2150
|
+
filename: guessInlineDataFilename(mimeType),
|
|
2151
|
+
file_data: fileData
|
|
2152
|
+
});
|
|
1803
2153
|
}
|
|
1804
2154
|
if (parts.length === 1 && parts[0]?.type === "input_text" && typeof parts[0].text === "string") {
|
|
1805
2155
|
return {
|
|
@@ -1817,7 +2167,7 @@ function toChatGptInput(contents) {
|
|
|
1817
2167
|
const instructionsParts = [];
|
|
1818
2168
|
const input = [];
|
|
1819
2169
|
for (const content of contents) {
|
|
1820
|
-
if (content.role === "system") {
|
|
2170
|
+
if (content.role === "system" || content.role === "developer") {
|
|
1821
2171
|
for (const part of content.parts) {
|
|
1822
2172
|
if (part.type === "text") {
|
|
1823
2173
|
instructionsParts.push(part.text);
|
|
@@ -1825,7 +2175,7 @@ function toChatGptInput(contents) {
|
|
|
1825
2175
|
}
|
|
1826
2176
|
continue;
|
|
1827
2177
|
}
|
|
1828
|
-
const isAssistant = content.role === "
|
|
2178
|
+
const isAssistant = content.role === "assistant" || content.role === "tool";
|
|
1829
2179
|
const parts = [];
|
|
1830
2180
|
for (const part of content.parts) {
|
|
1831
2181
|
if (part.type === "text") {
|
|
@@ -1835,19 +2185,29 @@ function toChatGptInput(contents) {
|
|
|
1835
2185
|
});
|
|
1836
2186
|
continue;
|
|
1837
2187
|
}
|
|
1838
|
-
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
1839
|
-
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
1840
2188
|
if (isAssistant) {
|
|
2189
|
+
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
1841
2190
|
parts.push({
|
|
1842
2191
|
type: "output_text",
|
|
1843
|
-
text: `[image:${mimeType}]`
|
|
2192
|
+
text: isInlineImageMime(part.mimeType) ? `[image:${mimeType}]` : `[file:${mimeType}]`
|
|
1844
2193
|
});
|
|
1845
2194
|
} else {
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2195
|
+
if (isInlineImageMime(part.mimeType)) {
|
|
2196
|
+
const mimeType = part.mimeType ?? "application/octet-stream";
|
|
2197
|
+
const dataUrl = `data:${mimeType};base64,${part.data}`;
|
|
2198
|
+
parts.push({
|
|
2199
|
+
type: "input_image",
|
|
2200
|
+
image_url: dataUrl,
|
|
2201
|
+
detail: "auto"
|
|
2202
|
+
});
|
|
2203
|
+
} else {
|
|
2204
|
+
const fileData = decodeInlineDataBuffer(part.data).toString("base64");
|
|
2205
|
+
parts.push({
|
|
2206
|
+
type: "input_file",
|
|
2207
|
+
filename: guessInlineDataFilename(part.mimeType),
|
|
2208
|
+
file_data: fileData
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
1851
2211
|
}
|
|
1852
2212
|
}
|
|
1853
2213
|
if (parts.length === 0) {
|
|
@@ -2535,7 +2895,7 @@ async function runTextCall(params) {
|
|
|
2535
2895
|
});
|
|
2536
2896
|
}
|
|
2537
2897
|
const mergedParts = mergeConsecutiveTextParts(responseParts);
|
|
2538
|
-
const content = mergedParts.length > 0 ? { role: responseRole ?? "
|
|
2898
|
+
const content = mergedParts.length > 0 ? { role: responseRole ?? "assistant", parts: mergedParts } : void 0;
|
|
2539
2899
|
const { text, thoughts } = extractTextByChannel(content);
|
|
2540
2900
|
const costUsd = estimateCallCostUsd({
|
|
2541
2901
|
modelId: modelVersion,
|
|
@@ -2585,8 +2945,7 @@ async function generateText(request) {
|
|
|
2585
2945
|
}
|
|
2586
2946
|
return await call.result;
|
|
2587
2947
|
}
|
|
2588
|
-
|
|
2589
|
-
const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
|
|
2948
|
+
function buildJsonSchemaConfig(request) {
|
|
2590
2949
|
const schemaName = (request.openAiSchemaName ?? "llm-response").trim() || "llm-response";
|
|
2591
2950
|
const providerInfo = resolveProvider(request.model);
|
|
2592
2951
|
const isOpenAiVariant = providerInfo.provider === "openai" || providerInfo.provider === "chatgpt";
|
|
@@ -2598,31 +2957,143 @@ async function generateJson(request) {
|
|
|
2598
2957
|
if (isOpenAiVariant && !isJsonSchemaObject(responseJsonSchema)) {
|
|
2599
2958
|
throw new Error("OpenAI structured outputs require a JSON object schema at the root.");
|
|
2600
2959
|
}
|
|
2601
|
-
const openAiTextFormat =
|
|
2960
|
+
const openAiTextFormat = isOpenAiVariant ? {
|
|
2602
2961
|
type: "json_schema",
|
|
2603
2962
|
name: schemaName,
|
|
2604
2963
|
strict: true,
|
|
2605
2964
|
schema: normalizeOpenAiSchema(responseJsonSchema)
|
|
2606
2965
|
} : void 0;
|
|
2966
|
+
return { providerInfo, responseJsonSchema, openAiTextFormat };
|
|
2967
|
+
}
|
|
2968
|
+
function streamJson(request) {
|
|
2969
|
+
const queue = createAsyncQueue();
|
|
2970
|
+
const abortController = new AbortController();
|
|
2971
|
+
const resolveAbortSignal = () => {
|
|
2972
|
+
if (!request.signal) {
|
|
2973
|
+
return abortController.signal;
|
|
2974
|
+
}
|
|
2975
|
+
if (request.signal.aborted) {
|
|
2976
|
+
abortController.abort(request.signal.reason);
|
|
2977
|
+
} else {
|
|
2978
|
+
request.signal.addEventListener(
|
|
2979
|
+
"abort",
|
|
2980
|
+
() => abortController.abort(request.signal?.reason),
|
|
2981
|
+
{
|
|
2982
|
+
once: true
|
|
2983
|
+
}
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
return abortController.signal;
|
|
2987
|
+
};
|
|
2988
|
+
const result = (async () => {
|
|
2989
|
+
const signal = resolveAbortSignal();
|
|
2990
|
+
const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
|
|
2991
|
+
const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
|
|
2992
|
+
const streamMode = request.streamMode ?? "partial";
|
|
2993
|
+
const failures = [];
|
|
2994
|
+
let openAiTextFormatForAttempt = openAiTextFormat;
|
|
2995
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2996
|
+
let rawText = "";
|
|
2997
|
+
let lastPartial = "";
|
|
2998
|
+
try {
|
|
2999
|
+
const call = streamText({
|
|
3000
|
+
model: request.model,
|
|
3001
|
+
input: request.input,
|
|
3002
|
+
instructions: request.instructions,
|
|
3003
|
+
tools: request.tools,
|
|
3004
|
+
responseMimeType: request.responseMimeType ?? "application/json",
|
|
3005
|
+
responseJsonSchema,
|
|
3006
|
+
openAiReasoningEffort: request.openAiReasoningEffort,
|
|
3007
|
+
...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
|
|
3008
|
+
signal
|
|
3009
|
+
});
|
|
3010
|
+
try {
|
|
3011
|
+
for await (const event of call.events) {
|
|
3012
|
+
queue.push(event);
|
|
3013
|
+
if (event.type === "delta" && event.channel === "response") {
|
|
3014
|
+
rawText += event.text;
|
|
3015
|
+
if (streamMode === "partial") {
|
|
3016
|
+
const partial = parsePartialJsonFromLlmText(rawText);
|
|
3017
|
+
if (partial !== null) {
|
|
3018
|
+
const serialized = JSON.stringify(partial);
|
|
3019
|
+
if (serialized !== lastPartial) {
|
|
3020
|
+
lastPartial = serialized;
|
|
3021
|
+
queue.push({
|
|
3022
|
+
type: "json",
|
|
3023
|
+
stage: "partial",
|
|
3024
|
+
value: partial
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
} catch (streamError) {
|
|
3032
|
+
await call.result.catch(() => void 0);
|
|
3033
|
+
throw streamError;
|
|
3034
|
+
}
|
|
3035
|
+
const result2 = await call.result;
|
|
3036
|
+
rawText = rawText || result2.text;
|
|
3037
|
+
const cleanedText = normalizeJsonText(rawText);
|
|
3038
|
+
const repairedText = escapeNewlinesInStrings(cleanedText);
|
|
3039
|
+
const payload = JSON.parse(repairedText);
|
|
3040
|
+
const normalized = typeof request.normalizeJson === "function" ? request.normalizeJson(payload) : payload;
|
|
3041
|
+
const parsed = request.schema.parse(normalized);
|
|
3042
|
+
queue.push({ type: "json", stage: "final", value: parsed });
|
|
3043
|
+
queue.close();
|
|
3044
|
+
return { value: parsed, rawText, result: result2 };
|
|
3045
|
+
} catch (error) {
|
|
3046
|
+
const handled = error instanceof Error ? error : new Error(String(error));
|
|
3047
|
+
failures.push({ attempt, rawText, error: handled });
|
|
3048
|
+
if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
|
|
3049
|
+
openAiTextFormatForAttempt = void 0;
|
|
3050
|
+
}
|
|
3051
|
+
if (attempt >= maxAttempts) {
|
|
3052
|
+
throw new LlmJsonCallError(`LLM JSON call failed after ${attempt} attempt(s)`, failures);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
throw new LlmJsonCallError("LLM JSON call failed", failures);
|
|
3057
|
+
})().catch((error) => {
|
|
3058
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3059
|
+
queue.fail(err);
|
|
3060
|
+
throw err;
|
|
3061
|
+
});
|
|
3062
|
+
return {
|
|
3063
|
+
events: queue.iterable,
|
|
3064
|
+
result,
|
|
3065
|
+
abort: () => abortController.abort()
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
async function generateJson(request) {
|
|
3069
|
+
const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
|
|
3070
|
+
const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
|
|
3071
|
+
let openAiTextFormatForAttempt = openAiTextFormat;
|
|
2607
3072
|
const failures = [];
|
|
2608
3073
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2609
3074
|
let rawText = "";
|
|
2610
3075
|
try {
|
|
2611
|
-
const contents = resolveTextContents(request);
|
|
2612
3076
|
const call = streamText({
|
|
2613
3077
|
model: request.model,
|
|
2614
|
-
|
|
3078
|
+
input: request.input,
|
|
3079
|
+
instructions: request.instructions,
|
|
2615
3080
|
tools: request.tools,
|
|
2616
3081
|
responseMimeType: request.responseMimeType ?? "application/json",
|
|
2617
3082
|
responseJsonSchema,
|
|
2618
3083
|
openAiReasoningEffort: request.openAiReasoningEffort,
|
|
2619
|
-
...
|
|
3084
|
+
...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
|
|
2620
3085
|
signal: request.signal
|
|
2621
3086
|
});
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
3087
|
+
try {
|
|
3088
|
+
for await (const event of call.events) {
|
|
3089
|
+
request.onEvent?.(event);
|
|
3090
|
+
if (event.type === "delta" && event.channel === "response") {
|
|
3091
|
+
rawText += event.text;
|
|
3092
|
+
}
|
|
2625
3093
|
}
|
|
3094
|
+
} catch (streamError) {
|
|
3095
|
+
await call.result.catch(() => void 0);
|
|
3096
|
+
throw streamError;
|
|
2626
3097
|
}
|
|
2627
3098
|
const result = await call.result;
|
|
2628
3099
|
rawText = rawText || result.text;
|
|
@@ -2635,6 +3106,9 @@ async function generateJson(request) {
|
|
|
2635
3106
|
} catch (error) {
|
|
2636
3107
|
const handled = error instanceof Error ? error : new Error(String(error));
|
|
2637
3108
|
failures.push({ attempt, rawText, error: handled });
|
|
3109
|
+
if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
|
|
3110
|
+
openAiTextFormatForAttempt = void 0;
|
|
3111
|
+
}
|
|
2638
3112
|
if (attempt >= maxAttempts) {
|
|
2639
3113
|
throw new LlmJsonCallError(`LLM JSON call failed after ${attempt} attempt(s)`, failures);
|
|
2640
3114
|
}
|
|
@@ -3220,7 +3694,7 @@ async function gradeGeneratedImage(params) {
|
|
|
3220
3694
|
];
|
|
3221
3695
|
const { value } = await generateJson({
|
|
3222
3696
|
model: params.model,
|
|
3223
|
-
|
|
3697
|
+
input: [{ role: "user", content: parts }],
|
|
3224
3698
|
schema: IMAGE_GRADE_SCHEMA
|
|
3225
3699
|
});
|
|
3226
3700
|
return value;
|
|
@@ -3297,7 +3771,7 @@ async function generateImages(request) {
|
|
|
3297
3771
|
lines.push(`\\nPlease make all ${pending.length} remaining images.`);
|
|
3298
3772
|
return [{ type: "text", text: lines.join("\\n") }];
|
|
3299
3773
|
};
|
|
3300
|
-
const
|
|
3774
|
+
const inputMessages = [{ role: "user", content: buildInitialPromptParts() }];
|
|
3301
3775
|
const orderedEntries = [...promptEntries];
|
|
3302
3776
|
const resolvedImages = /* @__PURE__ */ new Map();
|
|
3303
3777
|
const removeResolvedEntries = (resolved) => {
|
|
@@ -3317,7 +3791,7 @@ async function generateImages(request) {
|
|
|
3317
3791
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
3318
3792
|
const result = await generateText({
|
|
3319
3793
|
model: request.model,
|
|
3320
|
-
|
|
3794
|
+
input: inputMessages,
|
|
3321
3795
|
responseModalities: ["IMAGE", "TEXT"],
|
|
3322
3796
|
imageAspectRatio: request.imageAspectRatio,
|
|
3323
3797
|
imageSize: request.imageSize ?? "2K"
|
|
@@ -3364,8 +3838,11 @@ async function generateImages(request) {
|
|
|
3364
3838
|
if (promptEntries.length === 0) {
|
|
3365
3839
|
break;
|
|
3366
3840
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3841
|
+
inputMessages.push({
|
|
3842
|
+
role: "assistant",
|
|
3843
|
+
content: result.content.parts
|
|
3844
|
+
});
|
|
3845
|
+
inputMessages.push({ role: "user", content: buildContinuationPromptParts(promptEntries) });
|
|
3369
3846
|
}
|
|
3370
3847
|
const orderedImages = [];
|
|
3371
3848
|
for (const entry of orderedEntries) {
|
|
@@ -3462,6 +3939,7 @@ ${lines}`;
|
|
|
3462
3939
|
refreshChatGptOauthToken,
|
|
3463
3940
|
runToolLoop,
|
|
3464
3941
|
sanitisePartForLogging,
|
|
3942
|
+
streamJson,
|
|
3465
3943
|
streamText,
|
|
3466
3944
|
stripCodexCitationMarkers,
|
|
3467
3945
|
toGeminiJsonSchema,
|