tina4-nodejs 3.13.112 → 3.13.114
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/CLAUDE.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +683 -48
- package/packages/core/dist/index.js +686 -48
- package/packages/core/src/aiClient.ts +569 -48
- package/packages/core/src/api.ts +269 -0
- package/packages/core/src/index.ts +3 -3
- package/packages/orm/dist/index.js +683 -48
- package/types/core/src/aiClient.d.ts +158 -5
- package/types/core/src/api.d.ts +102 -0
- package/types/core/src/index.d.ts +3 -3
|
@@ -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
|
-
|
|
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 {
|
|
@@ -41791,6 +42004,8 @@ var init_aiClient = __esm({
|
|
|
41791
42004
|
Ai = class {
|
|
41792
42005
|
static chat(messages, options = {}) {
|
|
41793
42006
|
this.validateMessages(messages);
|
|
42007
|
+
if (options.tools !== void 0) this.validateTools(options.tools);
|
|
42008
|
+
if (options.toolChoice !== void 0) this.validateToolChoice(options.toolChoice);
|
|
41794
42009
|
const config = this.config("chat", options);
|
|
41795
42010
|
const body = this.chatBody(config, messages, options);
|
|
41796
42011
|
const headers = this.headers(config);
|
|
@@ -41818,10 +42033,120 @@ var init_aiClient = __esm({
|
|
|
41818
42033
|
throw new AiParseError("AI provider returned a malformed embedding response");
|
|
41819
42034
|
}
|
|
41820
42035
|
}
|
|
42036
|
+
/**
|
|
42037
|
+
* Validate role + content shape. Content may be a string OR a non-empty
|
|
42038
|
+
* list of {type:'text'|'image'|'tool_result', ...} parts (ADR-0060 +
|
|
42039
|
+
* ADR-0061). The `tool` role is the OpenAI-style tool-result turn
|
|
42040
|
+
* (ADR-0061). Malformed parts fail fast with AiConfigError, never
|
|
42041
|
+
* reaching the wire.
|
|
42042
|
+
*/
|
|
41821
42043
|
static validateMessages(messages) {
|
|
41822
|
-
if (!Array.isArray(messages) || messages.length === 0
|
|
42044
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
41823
42045
|
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
41824
42046
|
}
|
|
42047
|
+
for (const raw of messages) {
|
|
42048
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
42049
|
+
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
42050
|
+
}
|
|
42051
|
+
const message = raw;
|
|
42052
|
+
const role = message.role;
|
|
42053
|
+
if (role === "tool") {
|
|
42054
|
+
if (typeof message.tool_call_id !== "string" || message.tool_call_id.length === 0) {
|
|
42055
|
+
throw new AiConfigError("AI tool message requires a non-empty string 'tool_call_id'");
|
|
42056
|
+
}
|
|
42057
|
+
if (typeof message.content !== "string") {
|
|
42058
|
+
throw new AiConfigError("AI tool message requires a string 'content'");
|
|
42059
|
+
}
|
|
42060
|
+
continue;
|
|
42061
|
+
}
|
|
42062
|
+
if (role !== "system" && role !== "user" && role !== "assistant") {
|
|
42063
|
+
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
42064
|
+
}
|
|
42065
|
+
this.validateContent(message.content);
|
|
42066
|
+
}
|
|
42067
|
+
}
|
|
42068
|
+
static validateContent(content) {
|
|
42069
|
+
if (typeof content === "string") return;
|
|
42070
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
42071
|
+
throw new AiConfigError("AI message content must be a string or a non-empty list of parts");
|
|
42072
|
+
}
|
|
42073
|
+
for (const part of content) {
|
|
42074
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) {
|
|
42075
|
+
throw new AiConfigError("AI content part must be an object with type and text/source");
|
|
42076
|
+
}
|
|
42077
|
+
const record = part;
|
|
42078
|
+
const partType = record.type;
|
|
42079
|
+
if (partType === "text") {
|
|
42080
|
+
if (typeof record.text !== "string") {
|
|
42081
|
+
throw new AiConfigError("AI text content part requires a string 'text' field");
|
|
42082
|
+
}
|
|
42083
|
+
} else if (partType === "image") {
|
|
42084
|
+
if (typeof record.source !== "string" || record.source.length === 0) {
|
|
42085
|
+
throw new AiConfigError("AI image content part requires a non-empty string 'source' field");
|
|
42086
|
+
}
|
|
42087
|
+
if (!record.source.startsWith("data:") && !record.source.startsWith("https://")) {
|
|
42088
|
+
throw new AiConfigError("AI image source must be a data: URI or an https:// URL");
|
|
42089
|
+
}
|
|
42090
|
+
if (record.source.startsWith("data:") && !/^data:[^;,\s]+;base64,[A-Za-z0-9+/=]+$/.test(record.source)) {
|
|
42091
|
+
throw new AiConfigError("AI image data URI must be data:<media_type>;base64,<payload>");
|
|
42092
|
+
}
|
|
42093
|
+
} else if (partType === "tool_result") {
|
|
42094
|
+
if (typeof record.tool_use_id !== "string" || record.tool_use_id.length === 0) {
|
|
42095
|
+
throw new AiConfigError("AI tool_result part requires a non-empty string 'tool_use_id'");
|
|
42096
|
+
}
|
|
42097
|
+
if (typeof record.content !== "string") {
|
|
42098
|
+
throw new AiConfigError("AI tool_result part requires a string 'content'");
|
|
42099
|
+
}
|
|
42100
|
+
} else {
|
|
42101
|
+
throw new AiConfigError(`AI content part has unknown type '${String(partType)}'`);
|
|
42102
|
+
}
|
|
42103
|
+
}
|
|
42104
|
+
}
|
|
42105
|
+
/**
|
|
42106
|
+
* Validate the outbound tool declarations (ADR-0061). Each tool needs a
|
|
42107
|
+
* non-empty `name`, a string `description`, and a JSON-Schema-shaped
|
|
42108
|
+
* `parameters` object. Malformed tools fail fast with AiConfigError,
|
|
42109
|
+
* never reaching the wire.
|
|
42110
|
+
*/
|
|
42111
|
+
static validateTools(tools) {
|
|
42112
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
42113
|
+
throw new AiConfigError("AI tools must be a non-empty list of {name, description, parameters}");
|
|
42114
|
+
}
|
|
42115
|
+
for (const tool of tools) {
|
|
42116
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
|
|
42117
|
+
throw new AiConfigError("AI tool must be an object with name, description, parameters");
|
|
42118
|
+
}
|
|
42119
|
+
const record = tool;
|
|
42120
|
+
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
42121
|
+
throw new AiConfigError("AI tool requires a non-empty string 'name'");
|
|
42122
|
+
}
|
|
42123
|
+
if (typeof record.description !== "string") {
|
|
42124
|
+
throw new AiConfigError("AI tool requires a string 'description'");
|
|
42125
|
+
}
|
|
42126
|
+
if (!record.parameters || typeof record.parameters !== "object" || Array.isArray(record.parameters)) {
|
|
42127
|
+
throw new AiConfigError("AI tool requires a JSON-Schema object 'parameters'");
|
|
42128
|
+
}
|
|
42129
|
+
}
|
|
42130
|
+
}
|
|
42131
|
+
/**
|
|
42132
|
+
* Validate the outbound tool_choice value (ADR-0061). The four accepted
|
|
42133
|
+
* shapes are 'auto', 'none', 'required', and {name: 'x'}.
|
|
42134
|
+
*/
|
|
42135
|
+
static validateToolChoice(choice) {
|
|
42136
|
+
if (typeof choice === "string") {
|
|
42137
|
+
if (choice !== "auto" && choice !== "none" && choice !== "required") {
|
|
42138
|
+
throw new AiConfigError("AI toolChoice string must be 'auto', 'none', or 'required'");
|
|
42139
|
+
}
|
|
42140
|
+
return;
|
|
42141
|
+
}
|
|
42142
|
+
if (choice && typeof choice === "object" && !Array.isArray(choice)) {
|
|
42143
|
+
const record = choice;
|
|
42144
|
+
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
42145
|
+
throw new AiConfigError("AI toolChoice object requires a non-empty string 'name'");
|
|
42146
|
+
}
|
|
42147
|
+
return;
|
|
42148
|
+
}
|
|
42149
|
+
throw new AiConfigError("AI toolChoice must be 'auto'|'none'|'required' or {name: string}");
|
|
41825
42150
|
}
|
|
41826
42151
|
static number(name, fallback, minimum) {
|
|
41827
42152
|
const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
|
|
@@ -41869,18 +42194,136 @@ var init_aiClient = __esm({
|
|
|
41869
42194
|
}
|
|
41870
42195
|
return headers;
|
|
41871
42196
|
}
|
|
42197
|
+
/**
|
|
42198
|
+
* Build the provider-specific request body from a Tina4-shaped message
|
|
42199
|
+
* list plus optional tool declarations (ADR-0060 + ADR-0061).
|
|
42200
|
+
*
|
|
42201
|
+
* Content parts translate per provider:
|
|
42202
|
+
* - OpenAI/local: image → {type:'image_url', image_url:{url}}
|
|
42203
|
+
* - Anthropic: image → {type:'image', source:{type:'base64'|'url', ...}}
|
|
42204
|
+
* String content is preserved verbatim in the OpenAI/local shape and
|
|
42205
|
+
* likewise for Anthropic (both accept a bare string).
|
|
42206
|
+
*
|
|
42207
|
+
* Tool-result turns are normalised to the current provider's expected
|
|
42208
|
+
* shape (either the OpenAI `{role:"tool", tool_call_id, content}` turn or
|
|
42209
|
+
* the Anthropic `{role:"user", content:[{type:"tool_result", ...}]}`
|
|
42210
|
+
* turn), so an agent-loop written against Tina4 never has to fork on
|
|
42211
|
+
* TINA4_AI_PROVIDER (ADR-0061 wire translation).
|
|
42212
|
+
*/
|
|
41872
42213
|
static chatBody(config, messages, options) {
|
|
41873
|
-
const
|
|
42214
|
+
const normalized = this.normalizeMessagesForProvider(messages, config.provider);
|
|
42215
|
+
const body = { model: config.model, messages: normalized, stream: options.stream ?? false };
|
|
41874
42216
|
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
41875
42217
|
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
41876
42218
|
if (config.provider === "anthropic") {
|
|
41877
|
-
const
|
|
41878
|
-
|
|
42219
|
+
const systemParts = [];
|
|
42220
|
+
for (const message of messages) {
|
|
42221
|
+
if (message.role !== "system") continue;
|
|
42222
|
+
const content = message.content;
|
|
42223
|
+
systemParts.push(typeof content === "string" ? content : this.contentToPlainText(content));
|
|
42224
|
+
}
|
|
42225
|
+
body.messages = normalized.filter((message) => message.role !== "system");
|
|
41879
42226
|
body.max_tokens = options.maxTokens ?? 1024;
|
|
41880
|
-
if (
|
|
42227
|
+
if (systemParts.length) body.system = systemParts.join("\n\n");
|
|
41881
42228
|
}
|
|
42229
|
+
this.applyTools(body, config.provider, options);
|
|
41882
42230
|
return body;
|
|
41883
42231
|
}
|
|
42232
|
+
/**
|
|
42233
|
+
* Normalise the Tina4-shaped messages into the provider's on-wire shape.
|
|
42234
|
+
* The `tool` role and the `tool_result` content part are translated
|
|
42235
|
+
* between the OpenAI and Anthropic forms so either input works against
|
|
42236
|
+
* either provider (ADR-0061 return-path table).
|
|
42237
|
+
*/
|
|
42238
|
+
static normalizeMessagesForProvider(messages, provider) {
|
|
42239
|
+
const out = [];
|
|
42240
|
+
for (const message of messages) {
|
|
42241
|
+
if (message.role === "tool") {
|
|
42242
|
+
if (provider === "anthropic") {
|
|
42243
|
+
out.push({
|
|
42244
|
+
role: "user",
|
|
42245
|
+
content: [{ type: "tool_result", tool_use_id: message.tool_call_id, content: message.content }]
|
|
42246
|
+
});
|
|
42247
|
+
} else {
|
|
42248
|
+
out.push({ role: "tool", tool_call_id: message.tool_call_id, content: message.content });
|
|
42249
|
+
}
|
|
42250
|
+
continue;
|
|
42251
|
+
}
|
|
42252
|
+
if (Array.isArray(message.content) && message.content.some((part) => part.type === "tool_result")) {
|
|
42253
|
+
if (provider === "anthropic") {
|
|
42254
|
+
out.push({ role: message.role, content: this.translateContent(message.content, provider) });
|
|
42255
|
+
} else {
|
|
42256
|
+
for (const part of message.content) {
|
|
42257
|
+
if (part.type === "tool_result") {
|
|
42258
|
+
out.push({ role: "tool", tool_call_id: part.tool_use_id, content: part.content });
|
|
42259
|
+
}
|
|
42260
|
+
}
|
|
42261
|
+
}
|
|
42262
|
+
continue;
|
|
42263
|
+
}
|
|
42264
|
+
out.push({ role: message.role, content: this.translateContent(message.content, provider) });
|
|
42265
|
+
}
|
|
42266
|
+
return out;
|
|
42267
|
+
}
|
|
42268
|
+
/**
|
|
42269
|
+
* Attach the outbound `tools` and `tool_choice` (ADR-0061 outbound
|
|
42270
|
+
* translation tables) to the body in place. When toolChoice is 'none'
|
|
42271
|
+
* on Anthropic (Anthropic has no "none" mode) the tools list is omitted
|
|
42272
|
+
* entirely — the model cannot call what it cannot see.
|
|
42273
|
+
*/
|
|
42274
|
+
static applyTools(body, provider, options) {
|
|
42275
|
+
const choice = options.toolChoice;
|
|
42276
|
+
const suppressToolsForAnthropic = provider === "anthropic" && choice === "none";
|
|
42277
|
+
if (options.tools !== void 0 && !suppressToolsForAnthropic) {
|
|
42278
|
+
body.tools = options.tools.map(
|
|
42279
|
+
(tool) => provider === "anthropic" ? { name: tool.name, description: tool.description, input_schema: tool.parameters } : { type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } }
|
|
42280
|
+
);
|
|
42281
|
+
}
|
|
42282
|
+
if (choice === void 0) return;
|
|
42283
|
+
if (provider === "anthropic") {
|
|
42284
|
+
if (choice === "none") return;
|
|
42285
|
+
if (choice === "auto") body.tool_choice = { type: "auto" };
|
|
42286
|
+
else if (choice === "required") body.tool_choice = { type: "any" };
|
|
42287
|
+
else body.tool_choice = { type: "tool", name: choice.name };
|
|
42288
|
+
} else {
|
|
42289
|
+
if (typeof choice === "string") body.tool_choice = choice;
|
|
42290
|
+
else body.tool_choice = { type: "function", function: { name: choice.name } };
|
|
42291
|
+
}
|
|
42292
|
+
}
|
|
42293
|
+
/**
|
|
42294
|
+
* Translate one message content value into the provider's on-wire shape.
|
|
42295
|
+
* A plain string is passed through (both providers accept a string
|
|
42296
|
+
* content). A parts array becomes provider-native content blocks.
|
|
42297
|
+
*/
|
|
42298
|
+
static translateContent(content, provider) {
|
|
42299
|
+
if (typeof content === "string") return content;
|
|
42300
|
+
if (provider === "anthropic") {
|
|
42301
|
+
return content.map((part) => {
|
|
42302
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
42303
|
+
if (part.type === "tool_result") return { type: "tool_result", tool_use_id: part.tool_use_id, content: part.content };
|
|
42304
|
+
if (part.source.startsWith("data:")) {
|
|
42305
|
+
const parsed = this.parseDataUri(part.source);
|
|
42306
|
+
return { type: "image", source: { type: "base64", media_type: parsed.mediaType, data: parsed.data } };
|
|
42307
|
+
}
|
|
42308
|
+
return { type: "image", source: { type: "url", url: part.source } };
|
|
42309
|
+
});
|
|
42310
|
+
}
|
|
42311
|
+
return content.map((part) => {
|
|
42312
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
42313
|
+
if (part.type === "tool_result") {
|
|
42314
|
+
return { type: "text", text: part.content };
|
|
42315
|
+
}
|
|
42316
|
+
return { type: "image_url", image_url: { url: part.source } };
|
|
42317
|
+
});
|
|
42318
|
+
}
|
|
42319
|
+
static parseDataUri(source) {
|
|
42320
|
+
const match = /^data:([^;,\s]+);base64,([A-Za-z0-9+/=]+)$/.exec(source);
|
|
42321
|
+
if (!match) throw new AiConfigError("AI image data URI must be data:<media_type>;base64,<payload>");
|
|
42322
|
+
return { mediaType: match[1], data: match[2] };
|
|
42323
|
+
}
|
|
42324
|
+
static contentToPlainText(parts) {
|
|
42325
|
+
return parts.filter((part) => part.type === "text").map((part) => part.text).join("\n\n");
|
|
42326
|
+
}
|
|
41884
42327
|
static open(config, deadline, headers, body) {
|
|
41885
42328
|
const remainingMs = deadline - performance.now();
|
|
41886
42329
|
if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
|
|
@@ -41980,35 +42423,12 @@ var init_aiClient = __esm({
|
|
|
41980
42423
|
static async chatResponse(config, headers, body) {
|
|
41981
42424
|
return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
|
|
41982
42425
|
}
|
|
41983
|
-
|
|
41984
|
-
|
|
41985
|
-
|
|
41986
|
-
|
|
41987
|
-
|
|
41988
|
-
|
|
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
|
-
}
|
|
42426
|
+
/**
|
|
42427
|
+
* Stream the response through the shared {@link parseSseStream} framer
|
|
42428
|
+
* (ADR-0060 rule 5). Translates each SSE data payload into 0..N
|
|
42429
|
+
* {@link AiEvent}s: text_delta per chunk, tool_call aggregated per
|
|
42430
|
+
* index / block, exactly one done (or error) at the end.
|
|
42431
|
+
*/
|
|
42012
42432
|
static async *streamRequest(config, headers, body) {
|
|
42013
42433
|
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
42014
42434
|
let yielded = false;
|
|
@@ -42027,27 +42447,239 @@ var init_aiClient = __esm({
|
|
|
42027
42447
|
}
|
|
42028
42448
|
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
42029
42449
|
}
|
|
42030
|
-
|
|
42031
|
-
|
|
42032
|
-
|
|
42033
|
-
|
|
42034
|
-
|
|
42035
|
-
|
|
42450
|
+
const response = opened.response;
|
|
42451
|
+
const chunks = this.responseChunks(response);
|
|
42452
|
+
const events = parseSseStream(parseLineStream(chunks));
|
|
42453
|
+
const aggregator = new AggregateState(config.provider);
|
|
42454
|
+
let done = false;
|
|
42455
|
+
try {
|
|
42456
|
+
for await (const sseEvent of events) {
|
|
42457
|
+
for (const emitted of aggregator.consume(sseEvent)) {
|
|
42458
|
+
yielded = true;
|
|
42459
|
+
yield emitted;
|
|
42460
|
+
if (emitted.type === "done" || emitted.type === "error") {
|
|
42461
|
+
done = true;
|
|
42462
|
+
break;
|
|
42463
|
+
}
|
|
42464
|
+
}
|
|
42465
|
+
if (done) break;
|
|
42466
|
+
}
|
|
42467
|
+
} catch (error) {
|
|
42468
|
+
if (yielded) {
|
|
42469
|
+
yielded = true;
|
|
42470
|
+
yield { type: "error", message: error instanceof AiParseError ? "AI provider returned malformed stream data" : `AI transport failed (${error instanceof Error ? error.name : "Error"})` };
|
|
42471
|
+
opened.cleanup();
|
|
42472
|
+
opened = null;
|
|
42473
|
+
return;
|
|
42036
42474
|
}
|
|
42037
|
-
|
|
42038
|
-
yielded = true;
|
|
42039
|
-
yield delta.text;
|
|
42475
|
+
throw error;
|
|
42040
42476
|
}
|
|
42041
42477
|
opened.cleanup();
|
|
42042
42478
|
opened = null;
|
|
42043
|
-
if (
|
|
42044
|
-
|
|
42479
|
+
if (done) return;
|
|
42480
|
+
if (yielded) {
|
|
42481
|
+
yield { type: "error", message: "AI provider stream ended before completion" };
|
|
42482
|
+
return;
|
|
42483
|
+
}
|
|
42484
|
+
throw new AiParseError("AI provider stream ended before completion");
|
|
42045
42485
|
} catch (error) {
|
|
42046
42486
|
opened?.cleanup();
|
|
42047
42487
|
const failure = this.streamError(error);
|
|
42048
|
-
if (
|
|
42488
|
+
if (yielded) {
|
|
42489
|
+
yield { type: "error", message: failure.message };
|
|
42490
|
+
return;
|
|
42491
|
+
}
|
|
42492
|
+
if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || attempt >= config.maxRetries) throw failure;
|
|
42493
|
+
}
|
|
42494
|
+
}
|
|
42495
|
+
}
|
|
42496
|
+
static async *responseChunks(response) {
|
|
42497
|
+
for await (const chunk of response) {
|
|
42498
|
+
yield chunk;
|
|
42499
|
+
}
|
|
42500
|
+
}
|
|
42501
|
+
static streamError(error) {
|
|
42502
|
+
if (error instanceof AiError) return error;
|
|
42503
|
+
if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
|
|
42504
|
+
return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
|
|
42505
|
+
}
|
|
42506
|
+
};
|
|
42507
|
+
AggregateState = class {
|
|
42508
|
+
constructor(provider) {
|
|
42509
|
+
this.provider = provider;
|
|
42510
|
+
}
|
|
42511
|
+
toolBuffers = /* @__PURE__ */ new Map();
|
|
42512
|
+
lastFinishReason = null;
|
|
42513
|
+
lastUsage = null;
|
|
42514
|
+
doneEmitted = false;
|
|
42515
|
+
*consume(event) {
|
|
42516
|
+
const data = event.data;
|
|
42517
|
+
if (data === "[DONE]") {
|
|
42518
|
+
if (this.doneEmitted) return;
|
|
42519
|
+
yield* this.flushRemainingToolCalls();
|
|
42520
|
+
this.doneEmitted = true;
|
|
42521
|
+
yield {
|
|
42522
|
+
type: "done",
|
|
42523
|
+
finishReason: this.lastFinishReason ?? "stop",
|
|
42524
|
+
...this.lastUsage ? { usage: this.lastUsage } : {}
|
|
42525
|
+
};
|
|
42526
|
+
return;
|
|
42527
|
+
}
|
|
42528
|
+
let payload;
|
|
42529
|
+
try {
|
|
42530
|
+
payload = JSON.parse(data);
|
|
42531
|
+
} catch {
|
|
42532
|
+
throw new AiParseError("AI provider returned malformed stream data");
|
|
42533
|
+
}
|
|
42534
|
+
if (this.provider === "anthropic") {
|
|
42535
|
+
yield* this.consumeAnthropic(payload);
|
|
42536
|
+
} else {
|
|
42537
|
+
yield* this.consumeOpenAi(payload);
|
|
42538
|
+
}
|
|
42539
|
+
}
|
|
42540
|
+
*consumeOpenAi(payload) {
|
|
42541
|
+
const choices = payload.choices;
|
|
42542
|
+
if (!Array.isArray(choices) || choices.length === 0) return;
|
|
42543
|
+
const choice = choices[0];
|
|
42544
|
+
const delta = choice.delta ?? {};
|
|
42545
|
+
const content = delta.content;
|
|
42546
|
+
if (typeof content === "string" && content.length > 0) {
|
|
42547
|
+
yield { type: "text_delta", text: content };
|
|
42548
|
+
}
|
|
42549
|
+
const toolCalls = delta.tool_calls;
|
|
42550
|
+
if (Array.isArray(toolCalls)) {
|
|
42551
|
+
for (const call of toolCalls) {
|
|
42552
|
+
const index = typeof call.index === "number" ? String(call.index) : String(this.toolBuffers.size);
|
|
42553
|
+
const idFromCall = typeof call.id === "string" ? call.id : "";
|
|
42554
|
+
const fn = call.function ?? {};
|
|
42555
|
+
const nameFromCall = typeof fn.name === "string" ? fn.name : "";
|
|
42556
|
+
const argsFragment = typeof fn.arguments === "string" ? fn.arguments : "";
|
|
42557
|
+
const existing = this.toolBuffers.get(index) ?? { id: "", name: "", args: "" };
|
|
42558
|
+
if (idFromCall) existing.id = idFromCall;
|
|
42559
|
+
if (nameFromCall) existing.name = nameFromCall;
|
|
42560
|
+
existing.args += argsFragment;
|
|
42561
|
+
this.toolBuffers.set(index, existing);
|
|
42562
|
+
if (existing.name && existing.args) {
|
|
42563
|
+
try {
|
|
42564
|
+
const parsed = JSON.parse(existing.args);
|
|
42565
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
42566
|
+
this.toolBuffers.delete(index);
|
|
42567
|
+
yield { type: "tool_call", id: existing.id || `call_${index}`, name: existing.name, args: parsed };
|
|
42568
|
+
}
|
|
42569
|
+
} catch {
|
|
42570
|
+
}
|
|
42571
|
+
}
|
|
42572
|
+
}
|
|
42573
|
+
}
|
|
42574
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason.length > 0) {
|
|
42575
|
+
this.lastFinishReason = choice.finish_reason;
|
|
42576
|
+
}
|
|
42577
|
+
const usage = payload.usage;
|
|
42578
|
+
if (usage && typeof usage === "object") {
|
|
42579
|
+
const promptTokens = Number(usage.prompt_tokens ?? 0);
|
|
42580
|
+
const completionTokens = Number(usage.completion_tokens ?? 0);
|
|
42581
|
+
const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
|
|
42582
|
+
if (Number.isFinite(promptTokens) && Number.isFinite(completionTokens)) {
|
|
42583
|
+
this.lastUsage = { promptTokens, completionTokens, totalTokens };
|
|
42584
|
+
}
|
|
42585
|
+
}
|
|
42586
|
+
}
|
|
42587
|
+
*consumeAnthropic(payload) {
|
|
42588
|
+
const type2 = payload.type;
|
|
42589
|
+
if (type2 === "content_block_start") {
|
|
42590
|
+
const block = payload.content_block ?? {};
|
|
42591
|
+
if (block.type === "tool_use") {
|
|
42592
|
+
const index = String(payload.index ?? this.toolBuffers.size);
|
|
42593
|
+
const id = typeof block.id === "string" ? block.id : `call_${index}`;
|
|
42594
|
+
const name = typeof block.name === "string" ? block.name : "";
|
|
42595
|
+
this.toolBuffers.set(index, { id, name, args: "" });
|
|
42596
|
+
}
|
|
42597
|
+
return;
|
|
42598
|
+
}
|
|
42599
|
+
if (type2 === "content_block_delta") {
|
|
42600
|
+
const index = String(payload.index ?? 0);
|
|
42601
|
+
const delta = payload.delta ?? {};
|
|
42602
|
+
if (delta.type === "text_delta" && typeof delta.text === "string" && delta.text.length > 0) {
|
|
42603
|
+
yield { type: "text_delta", text: delta.text };
|
|
42604
|
+
return;
|
|
42605
|
+
}
|
|
42606
|
+
if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
42607
|
+
const existing = this.toolBuffers.get(index);
|
|
42608
|
+
if (existing) existing.args += delta.partial_json;
|
|
42609
|
+
}
|
|
42610
|
+
return;
|
|
42611
|
+
}
|
|
42612
|
+
if (type2 === "content_block_stop") {
|
|
42613
|
+
const index = String(payload.index ?? 0);
|
|
42614
|
+
const existing = this.toolBuffers.get(index);
|
|
42615
|
+
if (existing && existing.name) {
|
|
42616
|
+
this.toolBuffers.delete(index);
|
|
42617
|
+
try {
|
|
42618
|
+
const parsed = existing.args ? JSON.parse(existing.args) : {};
|
|
42619
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
42620
|
+
yield { type: "tool_call", id: existing.id, name: existing.name, args: parsed };
|
|
42621
|
+
return;
|
|
42622
|
+
}
|
|
42623
|
+
throw new Error();
|
|
42624
|
+
} catch {
|
|
42625
|
+
throw new AiParseError("AI provider returned malformed tool-call JSON");
|
|
42626
|
+
}
|
|
42627
|
+
}
|
|
42628
|
+
return;
|
|
42629
|
+
}
|
|
42630
|
+
if (type2 === "message_delta") {
|
|
42631
|
+
const delta = payload.delta ?? {};
|
|
42632
|
+
if (typeof delta.stop_reason === "string" && delta.stop_reason.length > 0) {
|
|
42633
|
+
this.lastFinishReason = delta.stop_reason;
|
|
42634
|
+
}
|
|
42635
|
+
const usage = payload.usage ?? {};
|
|
42636
|
+
if (usage.output_tokens !== void 0 || usage.input_tokens !== void 0) {
|
|
42637
|
+
const promptTokens = Number(usage.input_tokens ?? this.lastUsage?.promptTokens ?? 0);
|
|
42638
|
+
const completionTokens = Number(usage.output_tokens ?? this.lastUsage?.completionTokens ?? 0);
|
|
42639
|
+
this.lastUsage = { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens };
|
|
42640
|
+
}
|
|
42641
|
+
return;
|
|
42642
|
+
}
|
|
42643
|
+
if (type2 === "message_stop") {
|
|
42644
|
+
if (this.doneEmitted) return;
|
|
42645
|
+
this.doneEmitted = true;
|
|
42646
|
+
yield {
|
|
42647
|
+
type: "done",
|
|
42648
|
+
finishReason: this.lastFinishReason ?? "end_turn",
|
|
42649
|
+
...this.lastUsage ? { usage: this.lastUsage } : {}
|
|
42650
|
+
};
|
|
42651
|
+
return;
|
|
42652
|
+
}
|
|
42653
|
+
if (type2 === "message_start") {
|
|
42654
|
+
const message = payload.message ?? {};
|
|
42655
|
+
const usage = message.usage ?? {};
|
|
42656
|
+
if (usage.input_tokens !== void 0 || usage.output_tokens !== void 0) {
|
|
42657
|
+
const promptTokens = Number(usage.input_tokens ?? 0);
|
|
42658
|
+
const completionTokens = Number(usage.output_tokens ?? 0);
|
|
42659
|
+
this.lastUsage = { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens };
|
|
42660
|
+
}
|
|
42661
|
+
return;
|
|
42662
|
+
}
|
|
42663
|
+
if (type2 === "error") {
|
|
42664
|
+
const err = payload.error ?? {};
|
|
42665
|
+
throw new AiParseError(typeof err.message === "string" ? err.message : "AI provider signalled a stream error");
|
|
42666
|
+
}
|
|
42667
|
+
}
|
|
42668
|
+
*flushRemainingToolCalls() {
|
|
42669
|
+
for (const [index, buffered] of this.toolBuffers) {
|
|
42670
|
+
if (buffered.name && buffered.args) {
|
|
42671
|
+
try {
|
|
42672
|
+
const parsed = JSON.parse(buffered.args);
|
|
42673
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
42674
|
+
yield { type: "tool_call", id: buffered.id || `call_${index}`, name: buffered.name, args: parsed };
|
|
42675
|
+
continue;
|
|
42676
|
+
}
|
|
42677
|
+
} catch {
|
|
42678
|
+
}
|
|
42679
|
+
throw new AiParseError("AI provider returned malformed tool-call JSON");
|
|
42049
42680
|
}
|
|
42050
42681
|
}
|
|
42682
|
+
this.toolBuffers.clear();
|
|
42051
42683
|
}
|
|
42052
42684
|
};
|
|
42053
42685
|
}
|
|
@@ -43763,6 +44395,7 @@ __export(index_exports, {
|
|
|
43763
44395
|
AiParseError: () => AiParseError,
|
|
43764
44396
|
AiTimeoutError: () => AiTimeoutError,
|
|
43765
44397
|
Api: () => Api,
|
|
44398
|
+
ApiStreamError: () => ApiStreamError,
|
|
43766
44399
|
Auth: () => Auth,
|
|
43767
44400
|
CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
|
|
43768
44401
|
CLOSE_GOING_AWAY: () => CLOSE_GOING_AWAY,
|
|
@@ -43992,7 +44625,9 @@ __export(index_exports, {
|
|
|
43992
44625
|
originAllowed: () => originAllowed,
|
|
43993
44626
|
parseAmqpUrl: () => parseAmqpUrl,
|
|
43994
44627
|
parseFrame: () => parseFrame,
|
|
44628
|
+
parseLineStream: () => parseLineStream,
|
|
43995
44629
|
parseMultipart: () => parseMultipart,
|
|
44630
|
+
parseSseStream: () => parseSseStream,
|
|
43996
44631
|
parseUpgradeHeaders: () => parseUpgradeHeaders,
|
|
43997
44632
|
patch: () => patch,
|
|
43998
44633
|
pidfilePath: () => pidfilePath,
|
|
@@ -44129,6 +44764,7 @@ export {
|
|
|
44129
44764
|
AiParseError,
|
|
44130
44765
|
AiTimeoutError,
|
|
44131
44766
|
Api,
|
|
44767
|
+
ApiStreamError,
|
|
44132
44768
|
Auth,
|
|
44133
44769
|
CANONICAL_SESSION_BACKENDS,
|
|
44134
44770
|
CLOSE_GOING_AWAY,
|
|
@@ -44358,7 +44994,9 @@ export {
|
|
|
44358
44994
|
originAllowed,
|
|
44359
44995
|
parseAmqpUrl,
|
|
44360
44996
|
parseFrame,
|
|
44997
|
+
parseLineStream,
|
|
44361
44998
|
parseMultipart,
|
|
44999
|
+
parseSseStream,
|
|
44362
45000
|
parseUpgradeHeaders,
|
|
44363
45001
|
patch,
|
|
44364
45002
|
pidfilePath,
|