tina4-nodejs 3.13.112 → 3.13.113

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.
@@ -38784,11 +38784,100 @@ function buildMultipartBody(boundary, fieldName, filename, fileContent, contentT
38784
38784
  parts.push(Buffer.from(delimiter2 + "--" + crlf, "utf-8"));
38785
38785
  return Buffer.concat(parts);
38786
38786
  }
38787
- var RETRY_STATUSES, DOWNLOAD_CHUNK_SIZE, MAX_REDIRECTS, STRIP_ON_CROSS_ORIGIN, MIME_BY_EXT, Api;
38787
+ async function* parseLineStream(chunks) {
38788
+ const decoder = new TextDecoder("utf-8");
38789
+ let buffer = "";
38790
+ for await (const chunk of chunks) {
38791
+ buffer += decoder.decode(chunk, { stream: true });
38792
+ let idx;
38793
+ while ((idx = buffer.indexOf("\n")) >= 0) {
38794
+ let line = buffer.slice(0, idx);
38795
+ buffer = buffer.slice(idx + 1);
38796
+ if (line.endsWith("\r")) {
38797
+ line = line.slice(0, -1);
38798
+ }
38799
+ yield line;
38800
+ }
38801
+ }
38802
+ buffer += decoder.decode();
38803
+ if (buffer.length > 0) {
38804
+ if (buffer.endsWith("\r")) {
38805
+ buffer = buffer.slice(0, -1);
38806
+ }
38807
+ yield buffer;
38808
+ }
38809
+ }
38810
+ async function* parseSseStream(lines) {
38811
+ let dataParts = [];
38812
+ let event;
38813
+ let id;
38814
+ let retry;
38815
+ let has = false;
38816
+ const emit = () => {
38817
+ if (!has) return null;
38818
+ const ev = { data: dataParts.join("\n") };
38819
+ if (event !== void 0) ev.event = event;
38820
+ if (id !== void 0) ev.id = id;
38821
+ if (retry !== void 0) ev.retry = retry;
38822
+ return ev;
38823
+ };
38824
+ const reset2 = () => {
38825
+ dataParts = [];
38826
+ event = void 0;
38827
+ id = void 0;
38828
+ retry = void 0;
38829
+ has = false;
38830
+ };
38831
+ for await (const line of lines) {
38832
+ if (line === "") {
38833
+ const ev = emit();
38834
+ if (ev) yield ev;
38835
+ reset2();
38836
+ continue;
38837
+ }
38838
+ if (line.startsWith(":")) continue;
38839
+ const colon = line.indexOf(":");
38840
+ const field = colon < 0 ? line : line.slice(0, colon);
38841
+ let value = colon < 0 ? "" : line.slice(colon + 1);
38842
+ if (value.startsWith(" ")) value = value.slice(1);
38843
+ switch (field) {
38844
+ case "data":
38845
+ dataParts.push(value);
38846
+ has = true;
38847
+ break;
38848
+ case "event":
38849
+ event = value;
38850
+ has = true;
38851
+ break;
38852
+ case "id":
38853
+ id = value;
38854
+ has = true;
38855
+ break;
38856
+ case "retry": {
38857
+ const parsed = Number(value);
38858
+ if (Number.isFinite(parsed) && parsed >= 0) {
38859
+ retry = parsed;
38860
+ has = true;
38861
+ }
38862
+ break;
38863
+ }
38864
+ }
38865
+ }
38866
+ const trailing = emit();
38867
+ if (trailing) yield trailing;
38868
+ }
38869
+ var ApiStreamError, RETRY_STATUSES, DOWNLOAD_CHUNK_SIZE, MAX_REDIRECTS, STRIP_ON_CROSS_ORIGIN, MIME_BY_EXT, Api;
38788
38870
  var init_api = __esm({
38789
38871
  "src/api.ts"() {
38790
38872
  "use strict";
38791
38873
  init_version();
38874
+ ApiStreamError = class extends Error {
38875
+ constructor(message, status2 = null) {
38876
+ super(message);
38877
+ this.status = status2;
38878
+ this.name = "ApiStreamError";
38879
+ }
38880
+ };
38792
38881
  RETRY_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
38793
38882
  DOWNLOAD_CHUNK_SIZE = 64 * 1024;
38794
38883
  MAX_REDIRECTS = 10;
@@ -39107,7 +39196,130 @@ var init_api = __esm({
39107
39196
  }
39108
39197
  return { http_code: code, headers: respHeaders, error: null, path: destPath };
39109
39198
  }
39199
+ /**
39200
+ * Stream a response body as raw bytes. Yields the chunks the transport
39201
+ * delivers, in order, never buffered whole. Ends cleanly on EOF and
39202
+ * throws on a transport failure or a non-2xx status (body drained
39203
+ * first). No JSON decoding, no line splitting, no framing —
39204
+ * {@link streamLines} and {@link streamSse} build on this primitive.
39205
+ *
39206
+ * Closing the iterator before EOF (a `break` out of a `for await`)
39207
+ * destroys the underlying socket, so a caller who takes only the
39208
+ * first few chunks never leaks the connection.
39209
+ *
39210
+ * `opts.timeout` bounds the whole stream duration (default
39211
+ * `TINA4_API_TIMEOUT` or the client `timeout`); `opts.connectTimeout`
39212
+ * bounds just the connect + headers phase (default
39213
+ * `TINA4_API_CONNECT_TIMEOUT` or 10s).
39214
+ */
39215
+ async *streamBytes(path8, opts = {}) {
39216
+ const url = this.buildUrl(path8);
39217
+ const method = (opts.method ?? "GET").toUpperCase();
39218
+ const contentType = opts.contentType ?? "application/json";
39219
+ const { headers, data } = this.buildRequest(method, contentType, opts.body, opts.headers);
39220
+ const totalSec = this.streamSeconds(opts.timeout, "TINA4_API_TIMEOUT", this.timeout);
39221
+ const connectSec = this.streamSeconds(opts.connectTimeout, "TINA4_API_CONNECT_TIMEOUT", 10);
39222
+ const opened = await this.openStreamRequest(method, url, headers, data, connectSec);
39223
+ const res = opened.res;
39224
+ const status2 = res.statusCode ?? 0;
39225
+ this.storeCookies(res.headers["set-cookie"]);
39226
+ if (status2 < 200 || status2 >= 300) {
39227
+ res.resume();
39228
+ throw new ApiStreamError(`stream failed with HTTP ${status2}`, status2);
39229
+ }
39230
+ let totalTimer = null;
39231
+ if (totalSec > 0) {
39232
+ totalTimer = setTimeout(() => {
39233
+ res.destroy(new ApiStreamError(`stream total timeout after ${totalSec}s`, null));
39234
+ }, totalSec * 1e3);
39235
+ }
39236
+ try {
39237
+ for await (const chunk of res) {
39238
+ yield chunk;
39239
+ }
39240
+ } finally {
39241
+ if (totalTimer) clearTimeout(totalTimer);
39242
+ if (!res.destroyed) res.destroy();
39243
+ }
39244
+ }
39245
+ /**
39246
+ * Stream the response body as UTF-8 lines. Splits on LF or CRLF;
39247
+ * buffers a multibyte codepoint that lands across a chunk boundary;
39248
+ * yields a trailing line without a terminator on EOF. Built on
39249
+ * {@link streamBytes} plus the shared {@link parseLineStream}.
39250
+ */
39251
+ async *streamLines(path8, opts = {}) {
39252
+ yield* parseLineStream(this.streamBytes(path8, opts));
39253
+ }
39254
+ /**
39255
+ * Stream the response as SSE (Server-Sent Events). Yields one
39256
+ * {@link SseEvent} per event boundary (blank line) or on EOF for a
39257
+ * trailing event. `data:[DONE]` is delivered as an ordinary event
39258
+ * with `data === "[DONE]"` and the iterator ends on the next EOF.
39259
+ * Built on {@link streamLines} plus the shared {@link parseSseStream}.
39260
+ */
39261
+ async *streamSse(path8, opts = {}) {
39262
+ yield* parseSseStream(this.streamLines(path8, opts));
39263
+ }
39110
39264
  // ── Internal helpers ──────────────────────────────────────────────
39265
+ /**
39266
+ * Resolve a stream duration from (in order): explicit `opts` field,
39267
+ * the named env var, then the fallback. Zero disables. A non-numeric
39268
+ * or negative env value warns via a fallback rather than throwing —
39269
+ * a bad env var must not brick every streaming call.
39270
+ */
39271
+ streamSeconds(explicit, envName, fallback) {
39272
+ if (explicit !== void 0) {
39273
+ return Number.isFinite(explicit) && explicit >= 0 ? Number(explicit) : fallback;
39274
+ }
39275
+ const raw = process.env[envName];
39276
+ if (raw === void 0) return fallback;
39277
+ const n = Number(raw);
39278
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
39279
+ }
39280
+ /**
39281
+ * Open a streaming HTTP request. Returns the raw
39282
+ * {@link http.IncomingMessage} once headers arrive. Redirects are NOT
39283
+ * followed on streams (a caller who needs a redirect should do a
39284
+ * regular GET first). Connect phase is bounded by `connectSec`;
39285
+ * body-phase timeout is applied by the caller (streamBytes) via
39286
+ * `res.destroy()`.
39287
+ */
39288
+ openStreamRequest(method, url, headers, data, connectSec) {
39289
+ return new Promise((resolve20, reject) => {
39290
+ let parsed;
39291
+ try {
39292
+ parsed = new URL2(url);
39293
+ } catch (err) {
39294
+ reject(err instanceof Error ? err : new Error(String(err)));
39295
+ return;
39296
+ }
39297
+ const isHttps = parsed.protocol === "https:";
39298
+ const protocolModule = isHttps ? https : http;
39299
+ const options = {
39300
+ hostname: parsed.hostname,
39301
+ port: parsed.port || (isHttps ? 443 : 80),
39302
+ path: parsed.pathname + parsed.search,
39303
+ method,
39304
+ headers,
39305
+ timeout: connectSec > 0 ? connectSec * 1e3 : void 0
39306
+ };
39307
+ if (isHttps && this.ignoreSsl) {
39308
+ options.rejectUnauthorized = false;
39309
+ }
39310
+ const req2 = protocolModule.request(options, (res) => {
39311
+ resolve20({ res });
39312
+ });
39313
+ req2.on("timeout", () => {
39314
+ req2.destroy(new ApiStreamError(`stream connect timeout after ${connectSec}s`, null));
39315
+ });
39316
+ req2.on("error", (err) => {
39317
+ reject(err);
39318
+ });
39319
+ if (data) req2.write(data);
39320
+ req2.end();
39321
+ });
39322
+ }
39111
39323
  buildUrl(path8) {
39112
39324
  if (path8.startsWith("http://") || path8.startsWith("https://")) {
39113
39325
  return path8;
@@ -41770,10 +41982,11 @@ export default class User {
41770
41982
  // src/aiClient.ts
41771
41983
  import http2 from "node:http";
41772
41984
  import https2 from "node:https";
41773
- var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai;
41985
+ var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai, AggregateState;
41774
41986
  var init_aiClient = __esm({
41775
41987
  "src/aiClient.ts"() {
41776
41988
  "use strict";
41989
+ init_api();
41777
41990
  AiError = class extends Error {
41778
41991
  };
41779
41992
  AiConfigError = class extends AiError {
@@ -41818,10 +42031,51 @@ var init_aiClient = __esm({
41818
42031
  throw new AiParseError("AI provider returned a malformed embedding response");
41819
42032
  }
41820
42033
  }
42034
+ /**
42035
+ * Validate role + content shape. Content may be a string OR a non-empty
42036
+ * list of {type:'text'|'image', ...} parts (ADR-0060). Malformed parts
42037
+ * fail fast with AiConfigError, never reaching the wire.
42038
+ */
41821
42039
  static validateMessages(messages) {
41822
- if (!Array.isArray(messages) || messages.length === 0 || !messages.every((message) => message && ["system", "user", "assistant"].includes(message.role) && typeof message.content === "string")) {
42040
+ if (!Array.isArray(messages) || messages.length === 0) {
41823
42041
  throw new AiConfigError("AI messages must contain supported roles and string content");
41824
42042
  }
42043
+ for (const message of messages) {
42044
+ if (!message || !["system", "user", "assistant"].includes(message.role)) {
42045
+ throw new AiConfigError("AI messages must contain supported roles and string content");
42046
+ }
42047
+ this.validateContent(message.content);
42048
+ }
42049
+ }
42050
+ static validateContent(content) {
42051
+ if (typeof content === "string") return;
42052
+ if (!Array.isArray(content) || content.length === 0) {
42053
+ throw new AiConfigError("AI message content must be a string or a non-empty list of parts");
42054
+ }
42055
+ for (const part of content) {
42056
+ if (!part || typeof part !== "object" || Array.isArray(part)) {
42057
+ throw new AiConfigError("AI content part must be an object with type and text/source");
42058
+ }
42059
+ const record = part;
42060
+ const partType = record.type;
42061
+ if (partType === "text") {
42062
+ if (typeof record.text !== "string") {
42063
+ throw new AiConfigError("AI text content part requires a string 'text' field");
42064
+ }
42065
+ } else if (partType === "image") {
42066
+ if (typeof record.source !== "string" || record.source.length === 0) {
42067
+ throw new AiConfigError("AI image content part requires a non-empty string 'source' field");
42068
+ }
42069
+ if (!record.source.startsWith("data:") && !record.source.startsWith("https://")) {
42070
+ throw new AiConfigError("AI image source must be a data: URI or an https:// URL");
42071
+ }
42072
+ if (record.source.startsWith("data:") && !/^data:[^;,\s]+;base64,[A-Za-z0-9+/=]+$/.test(record.source)) {
42073
+ throw new AiConfigError("AI image data URI must be data:<media_type>;base64,<payload>");
42074
+ }
42075
+ } else {
42076
+ throw new AiConfigError(`AI content part has unknown type '${String(partType)}'`);
42077
+ }
42078
+ }
41825
42079
  }
41826
42080
  static number(name, fallback, minimum) {
41827
42081
  const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
@@ -41869,18 +42123,57 @@ var init_aiClient = __esm({
41869
42123
  }
41870
42124
  return headers;
41871
42125
  }
42126
+ /**
42127
+ * Build the provider-specific request body from a Tina4-shaped message
42128
+ * list. Multimodal parts are translated per provider (ADR-0060):
42129
+ * - OpenAI/local: {type:'image_url', image_url:{url}}
42130
+ * - Anthropic: {type:'image', source:{type:'base64'|'url', ...}}
42131
+ * String content is preserved verbatim in the OpenAI/local shape and
42132
+ * likewise for Anthropic (both accept a bare string).
42133
+ */
41872
42134
  static chatBody(config, messages, options) {
41873
- const body = { model: config.model, messages, stream: options.stream ?? false };
42135
+ const translate = (list) => list.map((message) => ({ role: message.role, content: this.translateContent(message.content, config.provider) }));
42136
+ const body = { model: config.model, messages: translate(messages), stream: options.stream ?? false };
41874
42137
  if (options.temperature !== void 0) body.temperature = options.temperature;
41875
42138
  if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
41876
42139
  if (config.provider === "anthropic") {
41877
- const system = messages.filter((message) => message.role === "system").map((message) => message.content);
41878
- body.messages = messages.filter((message) => message.role !== "system");
42140
+ const systemParts = messages.filter((message) => message.role === "system").map((message) => typeof message.content === "string" ? message.content : this.contentToPlainText(message.content));
42141
+ body.messages = translate(messages.filter((message) => message.role !== "system"));
41879
42142
  body.max_tokens = options.maxTokens ?? 1024;
41880
- if (system.length) body.system = system.join("\n\n");
42143
+ if (systemParts.length) body.system = systemParts.join("\n\n");
41881
42144
  }
41882
42145
  return body;
41883
42146
  }
42147
+ /**
42148
+ * Translate one message content value into the provider's on-wire shape.
42149
+ * A plain string is passed through (both providers accept a string
42150
+ * content). A parts array becomes provider-native content blocks.
42151
+ */
42152
+ static translateContent(content, provider) {
42153
+ if (typeof content === "string") return content;
42154
+ if (provider === "anthropic") {
42155
+ return content.map((part) => {
42156
+ if (part.type === "text") return { type: "text", text: part.text };
42157
+ if (part.source.startsWith("data:")) {
42158
+ const parsed = this.parseDataUri(part.source);
42159
+ return { type: "image", source: { type: "base64", media_type: parsed.mediaType, data: parsed.data } };
42160
+ }
42161
+ return { type: "image", source: { type: "url", url: part.source } };
42162
+ });
42163
+ }
42164
+ return content.map((part) => {
42165
+ if (part.type === "text") return { type: "text", text: part.text };
42166
+ return { type: "image_url", image_url: { url: part.source } };
42167
+ });
42168
+ }
42169
+ static parseDataUri(source) {
42170
+ const match = /^data:([^;,\s]+);base64,([A-Za-z0-9+/=]+)$/.exec(source);
42171
+ if (!match) throw new AiConfigError("AI image data URI must be data:<media_type>;base64,<payload>");
42172
+ return { mediaType: match[1], data: match[2] };
42173
+ }
42174
+ static contentToPlainText(parts) {
42175
+ return parts.filter((part) => part.type === "text").map((part) => part.text).join("\n\n");
42176
+ }
41884
42177
  static open(config, deadline, headers, body) {
41885
42178
  const remainingMs = deadline - performance.now();
41886
42179
  if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
@@ -41980,35 +42273,12 @@ var init_aiClient = __esm({
41980
42273
  static async chatResponse(config, headers, body) {
41981
42274
  return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
41982
42275
  }
41983
- static streamDelta(provider, data) {
41984
- if (data === "[DONE]") return { completed: true };
41985
- let event;
41986
- try {
41987
- event = JSON.parse(data);
41988
- } catch {
41989
- throw new AiParseError("AI provider returned malformed stream data");
41990
- }
41991
- const text = provider === "anthropic" ? event.type === "content_block_delta" ? event.delta?.text : void 0 : event.choices?.[0]?.delta?.content;
41992
- if (text !== void 0 && text !== null && typeof text !== "string") throw new AiParseError("AI provider returned malformed stream data");
41993
- return { completed: false, text };
41994
- }
41995
- static async *streamData(response) {
41996
- let buffer = "";
41997
- for await (const chunk of response) {
41998
- buffer += Buffer.from(chunk).toString("utf8");
41999
- let newline;
42000
- while ((newline = buffer.indexOf("\n")) >= 0) {
42001
- const line = buffer.slice(0, newline).trim();
42002
- buffer = buffer.slice(newline + 1);
42003
- if (line.startsWith("data:")) yield line.slice(5).trim();
42004
- }
42005
- }
42006
- }
42007
- static streamError(error) {
42008
- if (error instanceof AiError) return error;
42009
- if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
42010
- return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
42011
- }
42276
+ /**
42277
+ * Stream the response through the shared {@link parseSseStream} framer
42278
+ * (ADR-0060 rule 5). Translates each SSE data payload into 0..N
42279
+ * {@link AiEvent}s: text_delta per chunk, tool_call aggregated per
42280
+ * index / block, exactly one done (or error) at the end.
42281
+ */
42012
42282
  static async *streamRequest(config, headers, body) {
42013
42283
  const deadline = performance.now() + config.totalTimeout * 1e3;
42014
42284
  let yielded = false;
@@ -42027,27 +42297,239 @@ var init_aiClient = __esm({
42027
42297
  }
42028
42298
  throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
42029
42299
  }
42030
- let completed = false;
42031
- for await (const data of this.streamData(opened.response)) {
42032
- const delta = this.streamDelta(config.provider, data);
42033
- if (delta.completed) {
42034
- completed = true;
42035
- break;
42300
+ const response = opened.response;
42301
+ const chunks = this.responseChunks(response);
42302
+ const events = parseSseStream(parseLineStream(chunks));
42303
+ const aggregator = new AggregateState(config.provider);
42304
+ let done = false;
42305
+ try {
42306
+ for await (const sseEvent of events) {
42307
+ for (const emitted of aggregator.consume(sseEvent)) {
42308
+ yielded = true;
42309
+ yield emitted;
42310
+ if (emitted.type === "done" || emitted.type === "error") {
42311
+ done = true;
42312
+ break;
42313
+ }
42314
+ }
42315
+ if (done) break;
42316
+ }
42317
+ } catch (error) {
42318
+ if (yielded) {
42319
+ yielded = true;
42320
+ yield { type: "error", message: error instanceof AiParseError ? "AI provider returned malformed stream data" : `AI transport failed (${error instanceof Error ? error.name : "Error"})` };
42321
+ opened.cleanup();
42322
+ opened = null;
42323
+ return;
42036
42324
  }
42037
- if (delta.text === void 0) continue;
42038
- yielded = true;
42039
- yield delta.text;
42325
+ throw error;
42040
42326
  }
42041
42327
  opened.cleanup();
42042
42328
  opened = null;
42043
- if (completed) return;
42044
- throw new AiParseError("AI provider stream ended before [DONE]");
42329
+ if (done) return;
42330
+ if (yielded) {
42331
+ yield { type: "error", message: "AI provider stream ended before completion" };
42332
+ return;
42333
+ }
42334
+ throw new AiParseError("AI provider stream ended before completion");
42045
42335
  } catch (error) {
42046
42336
  opened?.cleanup();
42047
42337
  const failure = this.streamError(error);
42048
- if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || yielded || attempt >= config.maxRetries) throw failure;
42338
+ if (yielded) {
42339
+ yield { type: "error", message: failure.message };
42340
+ return;
42341
+ }
42342
+ if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || attempt >= config.maxRetries) throw failure;
42343
+ }
42344
+ }
42345
+ }
42346
+ static async *responseChunks(response) {
42347
+ for await (const chunk of response) {
42348
+ yield chunk;
42349
+ }
42350
+ }
42351
+ static streamError(error) {
42352
+ if (error instanceof AiError) return error;
42353
+ if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
42354
+ return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
42355
+ }
42356
+ };
42357
+ AggregateState = class {
42358
+ constructor(provider) {
42359
+ this.provider = provider;
42360
+ }
42361
+ toolBuffers = /* @__PURE__ */ new Map();
42362
+ lastFinishReason = null;
42363
+ lastUsage = null;
42364
+ doneEmitted = false;
42365
+ *consume(event) {
42366
+ const data = event.data;
42367
+ if (data === "[DONE]") {
42368
+ if (this.doneEmitted) return;
42369
+ yield* this.flushRemainingToolCalls();
42370
+ this.doneEmitted = true;
42371
+ yield {
42372
+ type: "done",
42373
+ finishReason: this.lastFinishReason ?? "stop",
42374
+ ...this.lastUsage ? { usage: this.lastUsage } : {}
42375
+ };
42376
+ return;
42377
+ }
42378
+ let payload;
42379
+ try {
42380
+ payload = JSON.parse(data);
42381
+ } catch {
42382
+ throw new AiParseError("AI provider returned malformed stream data");
42383
+ }
42384
+ if (this.provider === "anthropic") {
42385
+ yield* this.consumeAnthropic(payload);
42386
+ } else {
42387
+ yield* this.consumeOpenAi(payload);
42388
+ }
42389
+ }
42390
+ *consumeOpenAi(payload) {
42391
+ const choices = payload.choices;
42392
+ if (!Array.isArray(choices) || choices.length === 0) return;
42393
+ const choice = choices[0];
42394
+ const delta = choice.delta ?? {};
42395
+ const content = delta.content;
42396
+ if (typeof content === "string" && content.length > 0) {
42397
+ yield { type: "text_delta", text: content };
42398
+ }
42399
+ const toolCalls = delta.tool_calls;
42400
+ if (Array.isArray(toolCalls)) {
42401
+ for (const call of toolCalls) {
42402
+ const index = typeof call.index === "number" ? String(call.index) : String(this.toolBuffers.size);
42403
+ const idFromCall = typeof call.id === "string" ? call.id : "";
42404
+ const fn = call.function ?? {};
42405
+ const nameFromCall = typeof fn.name === "string" ? fn.name : "";
42406
+ const argsFragment = typeof fn.arguments === "string" ? fn.arguments : "";
42407
+ const existing = this.toolBuffers.get(index) ?? { id: "", name: "", args: "" };
42408
+ if (idFromCall) existing.id = idFromCall;
42409
+ if (nameFromCall) existing.name = nameFromCall;
42410
+ existing.args += argsFragment;
42411
+ this.toolBuffers.set(index, existing);
42412
+ if (existing.name && existing.args) {
42413
+ try {
42414
+ const parsed = JSON.parse(existing.args);
42415
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
42416
+ this.toolBuffers.delete(index);
42417
+ yield { type: "tool_call", id: existing.id || `call_${index}`, name: existing.name, args: parsed };
42418
+ }
42419
+ } catch {
42420
+ }
42421
+ }
42422
+ }
42423
+ }
42424
+ if (typeof choice.finish_reason === "string" && choice.finish_reason.length > 0) {
42425
+ this.lastFinishReason = choice.finish_reason;
42426
+ }
42427
+ const usage = payload.usage;
42428
+ if (usage && typeof usage === "object") {
42429
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
42430
+ const completionTokens = Number(usage.completion_tokens ?? 0);
42431
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
42432
+ if (Number.isFinite(promptTokens) && Number.isFinite(completionTokens)) {
42433
+ this.lastUsage = { promptTokens, completionTokens, totalTokens };
42434
+ }
42435
+ }
42436
+ }
42437
+ *consumeAnthropic(payload) {
42438
+ const type2 = payload.type;
42439
+ if (type2 === "content_block_start") {
42440
+ const block = payload.content_block ?? {};
42441
+ if (block.type === "tool_use") {
42442
+ const index = String(payload.index ?? this.toolBuffers.size);
42443
+ const id = typeof block.id === "string" ? block.id : `call_${index}`;
42444
+ const name = typeof block.name === "string" ? block.name : "";
42445
+ this.toolBuffers.set(index, { id, name, args: "" });
42446
+ }
42447
+ return;
42448
+ }
42449
+ if (type2 === "content_block_delta") {
42450
+ const index = String(payload.index ?? 0);
42451
+ const delta = payload.delta ?? {};
42452
+ if (delta.type === "text_delta" && typeof delta.text === "string" && delta.text.length > 0) {
42453
+ yield { type: "text_delta", text: delta.text };
42454
+ return;
42455
+ }
42456
+ if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
42457
+ const existing = this.toolBuffers.get(index);
42458
+ if (existing) existing.args += delta.partial_json;
42459
+ }
42460
+ return;
42461
+ }
42462
+ if (type2 === "content_block_stop") {
42463
+ const index = String(payload.index ?? 0);
42464
+ const existing = this.toolBuffers.get(index);
42465
+ if (existing && existing.name) {
42466
+ this.toolBuffers.delete(index);
42467
+ try {
42468
+ const parsed = existing.args ? JSON.parse(existing.args) : {};
42469
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
42470
+ yield { type: "tool_call", id: existing.id, name: existing.name, args: parsed };
42471
+ return;
42472
+ }
42473
+ throw new Error();
42474
+ } catch {
42475
+ throw new AiParseError("AI provider returned malformed tool-call JSON");
42476
+ }
42477
+ }
42478
+ return;
42479
+ }
42480
+ if (type2 === "message_delta") {
42481
+ const delta = payload.delta ?? {};
42482
+ if (typeof delta.stop_reason === "string" && delta.stop_reason.length > 0) {
42483
+ this.lastFinishReason = delta.stop_reason;
42484
+ }
42485
+ const usage = payload.usage ?? {};
42486
+ if (usage.output_tokens !== void 0 || usage.input_tokens !== void 0) {
42487
+ const promptTokens = Number(usage.input_tokens ?? this.lastUsage?.promptTokens ?? 0);
42488
+ const completionTokens = Number(usage.output_tokens ?? this.lastUsage?.completionTokens ?? 0);
42489
+ this.lastUsage = { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens };
42490
+ }
42491
+ return;
42492
+ }
42493
+ if (type2 === "message_stop") {
42494
+ if (this.doneEmitted) return;
42495
+ this.doneEmitted = true;
42496
+ yield {
42497
+ type: "done",
42498
+ finishReason: this.lastFinishReason ?? "end_turn",
42499
+ ...this.lastUsage ? { usage: this.lastUsage } : {}
42500
+ };
42501
+ return;
42502
+ }
42503
+ if (type2 === "message_start") {
42504
+ const message = payload.message ?? {};
42505
+ const usage = message.usage ?? {};
42506
+ if (usage.input_tokens !== void 0 || usage.output_tokens !== void 0) {
42507
+ const promptTokens = Number(usage.input_tokens ?? 0);
42508
+ const completionTokens = Number(usage.output_tokens ?? 0);
42509
+ this.lastUsage = { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens };
42510
+ }
42511
+ return;
42512
+ }
42513
+ if (type2 === "error") {
42514
+ const err = payload.error ?? {};
42515
+ throw new AiParseError(typeof err.message === "string" ? err.message : "AI provider signalled a stream error");
42516
+ }
42517
+ }
42518
+ *flushRemainingToolCalls() {
42519
+ for (const [index, buffered] of this.toolBuffers) {
42520
+ if (buffered.name && buffered.args) {
42521
+ try {
42522
+ const parsed = JSON.parse(buffered.args);
42523
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
42524
+ yield { type: "tool_call", id: buffered.id || `call_${index}`, name: buffered.name, args: parsed };
42525
+ continue;
42526
+ }
42527
+ } catch {
42528
+ }
42529
+ throw new AiParseError("AI provider returned malformed tool-call JSON");
42049
42530
  }
42050
42531
  }
42532
+ this.toolBuffers.clear();
42051
42533
  }
42052
42534
  };
42053
42535
  }
@@ -43763,6 +44245,7 @@ __export(index_exports, {
43763
44245
  AiParseError: () => AiParseError,
43764
44246
  AiTimeoutError: () => AiTimeoutError,
43765
44247
  Api: () => Api,
44248
+ ApiStreamError: () => ApiStreamError,
43766
44249
  Auth: () => Auth,
43767
44250
  CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
43768
44251
  CLOSE_GOING_AWAY: () => CLOSE_GOING_AWAY,
@@ -43992,7 +44475,9 @@ __export(index_exports, {
43992
44475
  originAllowed: () => originAllowed,
43993
44476
  parseAmqpUrl: () => parseAmqpUrl,
43994
44477
  parseFrame: () => parseFrame,
44478
+ parseLineStream: () => parseLineStream,
43995
44479
  parseMultipart: () => parseMultipart,
44480
+ parseSseStream: () => parseSseStream,
43996
44481
  parseUpgradeHeaders: () => parseUpgradeHeaders,
43997
44482
  patch: () => patch,
43998
44483
  pidfilePath: () => pidfilePath,
@@ -44129,6 +44614,7 @@ export {
44129
44614
  AiParseError,
44130
44615
  AiTimeoutError,
44131
44616
  Api,
44617
+ ApiStreamError,
44132
44618
  Auth,
44133
44619
  CANONICAL_SESSION_BACKENDS,
44134
44620
  CLOSE_GOING_AWAY,
@@ -44358,7 +44844,9 @@ export {
44358
44844
  originAllowed,
44359
44845
  parseAmqpUrl,
44360
44846
  parseFrame,
44847
+ parseLineStream,
44361
44848
  parseMultipart,
44849
+ parseSseStream,
44362
44850
  parseUpgradeHeaders,
44363
44851
  patch,
44364
44852
  pidfilePath,