@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/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 "model":
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: content.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 resolveTextContents(input) {
1770
- if ("contents" in input) {
1771
- return input.contents;
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.systemPrompt) {
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: "system",
1777
- parts: [{ type: "text", text: input.systemPrompt }]
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
- model: "assistant",
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 ?? "application/octet-stream";
1801
- const dataUrl = `data:${mimeType};base64,${part.data}`;
1802
- parts.push({ type: "input_image", image_url: dataUrl, detail: "auto" });
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 === "model";
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
- parts.push({
1847
- type: "input_image",
1848
- image_url: dataUrl,
1849
- detail: "auto"
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 ?? "model", parts: mergedParts } : void 0;
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
- async function generateJson(request) {
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 = providerInfo.provider === "openai" ? {
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
- contents,
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
- ...openAiTextFormat ? { openAiTextFormat } : {},
3084
+ ...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
2620
3085
  signal: request.signal
2621
3086
  });
2622
- for await (const event of call.events) {
2623
- if (event.type === "delta" && event.channel === "response") {
2624
- rawText += event.text;
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
- contents: [{ role: "user", parts }],
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 contents = [{ role: "user", parts: buildInitialPromptParts() }];
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
- contents,
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
- contents.push(result.content);
3368
- contents.push({ role: "user", parts: buildContinuationPromptParts(promptEntries) });
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,