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.
@@ -21,10 +21,52 @@ export interface ChatResponse {
21
21
  finishReason: string | null;
22
22
  raw: Record<string, unknown>;
23
23
  }
24
+ /**
25
+ * A multimodal content part. `text` carries plain UTF-8 prose; `image`
26
+ * carries a `data:<media_type>;base64,<payload>` URI or an https:// URL
27
+ * (the client translates to each provider's shape). ADR-0060.
28
+ */
29
+ export type ContentPart = {
30
+ type: "text";
31
+ text: string;
32
+ } | {
33
+ type: "image";
34
+ source: string;
35
+ };
36
+ /** The value a caller may pass for `message.content`. ADR-0060. */
37
+ export type AiMessageContent = string | ContentPart[];
24
38
  export interface AiMessage {
25
39
  role: "system" | "user" | "assistant";
26
- content: string;
40
+ content: AiMessageContent;
27
41
  }
42
+ /**
43
+ * One event yielded by `Ai.chat(stream: true)`. The four variants
44
+ * discriminated by `type`. Text deltas arrive per chunk (typewriter UX);
45
+ * `tool_call` fires once per call, aggregated from provider fragments;
46
+ * `done` fires exactly once after all deltas; `error` replaces `done` on
47
+ * mid-stream failure. ADR-0060.
48
+ */
49
+ export type AiEvent = {
50
+ type: "text_delta";
51
+ text: string;
52
+ } | {
53
+ type: "tool_call";
54
+ id: string;
55
+ name: string;
56
+ args: Record<string, unknown>;
57
+ } | {
58
+ type: "done";
59
+ finishReason: string;
60
+ usage?: {
61
+ promptTokens: number;
62
+ completionTokens: number;
63
+ totalTokens: number;
64
+ };
65
+ } | {
66
+ type: "error";
67
+ message: string;
68
+ code?: string;
69
+ };
28
70
  export interface AiChatOptions {
29
71
  model?: string;
30
72
  temperature?: number;
@@ -41,26 +83,53 @@ export interface AiEmbedOptions {
41
83
  export declare class Ai {
42
84
  static chat(messages: AiMessage[], options: AiChatOptions & {
43
85
  stream: true;
44
- }): AsyncGenerator<string>;
86
+ }): AsyncGenerator<AiEvent>;
45
87
  static chat(messages: AiMessage[], options?: AiChatOptions & {
46
88
  stream?: false;
47
89
  }): Promise<ChatResponse>;
48
90
  static complete(prompt: string, options?: Omit<AiChatOptions, "stream">): Promise<string>;
49
91
  static embed(textOrTexts: string | string[], options?: AiEmbedOptions): Promise<number[] | number[][]>;
92
+ /**
93
+ * Validate role + content shape. Content may be a string OR a non-empty
94
+ * list of {type:'text'|'image', ...} parts (ADR-0060). Malformed parts
95
+ * fail fast with AiConfigError, never reaching the wire.
96
+ */
50
97
  private static validateMessages;
98
+ private static validateContent;
51
99
  private static number;
52
100
  private static config;
53
101
  private static endpoint;
54
102
  private static headers;
103
+ /**
104
+ * Build the provider-specific request body from a Tina4-shaped message
105
+ * list. Multimodal parts are translated per provider (ADR-0060):
106
+ * - OpenAI/local: {type:'image_url', image_url:{url}}
107
+ * - Anthropic: {type:'image', source:{type:'base64'|'url', ...}}
108
+ * String content is preserved verbatim in the OpenAI/local shape and
109
+ * likewise for Anthropic (both accept a bare string).
110
+ */
55
111
  private static chatBody;
112
+ /**
113
+ * Translate one message content value into the provider's on-wire shape.
114
+ * A plain string is passed through (both providers accept a string
115
+ * content). A parts array becomes provider-native content blocks.
116
+ */
117
+ private static translateContent;
118
+ private static parseDataUri;
119
+ private static contentToPlainText;
56
120
  private static open;
57
121
  private static readBody;
58
122
  private static retryDelay;
59
123
  private static requestJson;
60
124
  private static normalizeChat;
61
125
  private static chatResponse;
62
- private static streamDelta;
63
- private static streamData;
64
- private static streamError;
126
+ /**
127
+ * Stream the response through the shared {@link parseSseStream} framer
128
+ * (ADR-0060 rule 5). Translates each SSE data payload into 0..N
129
+ * {@link AiEvent}s: text_delta per chunk, tool_call aggregated per
130
+ * index / block, exactly one done (or error) at the end.
131
+ */
65
132
  private static streamRequest;
133
+ private static responseChunks;
134
+ private static streamError;
66
135
  }
@@ -4,6 +4,38 @@ export interface ApiResult {
4
4
  headers: Record<string, string>;
5
5
  error: string | null;
6
6
  }
7
+ /**
8
+ * Options for the streaming primitives ({@link Api.streamBytes},
9
+ * {@link Api.streamLines}, {@link Api.streamSse}). All fields optional.
10
+ * `timeout` bounds the WHOLE stream (headers + body), matching
11
+ * `TINA4_API_TIMEOUT`; `connectTimeout` bounds only the connection +
12
+ * headers-arrival phase, matching `TINA4_API_CONNECT_TIMEOUT`.
13
+ */
14
+ export interface StreamOptions {
15
+ method?: string;
16
+ body?: unknown;
17
+ headers?: Record<string, string>;
18
+ contentType?: string;
19
+ timeout?: number;
20
+ connectTimeout?: number;
21
+ }
22
+ /**
23
+ * One SSE event yielded by {@link Api.streamSse}. `data` is always present
24
+ * (multi-line `data:` fields are concatenated with `\n`). `event`, `id`,
25
+ * `retry` are set only when the corresponding SSE field appeared. `retry`
26
+ * is a number (milliseconds) per the SSE spec.
27
+ */
28
+ export interface SseEvent {
29
+ data: string;
30
+ event?: string;
31
+ id?: string;
32
+ retry?: number;
33
+ }
34
+ /** Raised by the streaming primitives on a non-2xx status. */
35
+ export declare class ApiStreamError extends Error {
36
+ readonly status: number | null;
37
+ constructor(message: string, status?: number | null);
38
+ }
7
39
  /**
8
40
  * Result of {@link Api.download}. There is no `body` field — the response
9
41
  * body went to disk. `path` is the destination on success and `null` on any
@@ -90,6 +122,28 @@ export interface ApiOptions {
90
122
  */
91
123
  cookies?: boolean;
92
124
  }
125
+ /**
126
+ * Split an async byte iterable into UTF-8 lines. Handles LF and CRLF; a
127
+ * multibyte codepoint that lands across a chunk boundary buffers across the
128
+ * split (TextDecoder({stream: true})). A trailing line without a terminator
129
+ * yields on EOF.
130
+ *
131
+ * Exported so {@link Api} instance methods AND `Ai.chat` streaming share
132
+ * one framer — ADR-0060's "no duplicate framing code" rule.
133
+ */
134
+ export declare function parseLineStream(chunks: AsyncIterable<Uint8Array>): AsyncGenerator<string>;
135
+ /**
136
+ * Parse SSE (Server-Sent Events) framing from a line iterable. Yields
137
+ * one {@link SseEvent} per event boundary (blank line) or on EOF for a
138
+ * final trailing event. `:` comment lines are ignored. Fields are
139
+ * `data` (multi-line concatenated with `\n`), `event`, `id`, `retry`.
140
+ *
141
+ * Follows the WHATWG SSE parsing algorithm closely enough for every LLM
142
+ * provider (OpenAI, Anthropic, local): one leading space after the colon
143
+ * is stripped, unknown fields are ignored, malformed `retry:` values are
144
+ * ignored.
145
+ */
146
+ export declare function parseSseStream(lines: AsyncIterable<string>): AsyncGenerator<SseEvent>;
93
147
  export declare class Api {
94
148
  private baseUrl;
95
149
  private headers;
@@ -209,6 +263,54 @@ export declare class Api {
209
263
  * not written on error.
210
264
  */
211
265
  download(path: string, destPath: string, params?: Record<string, string>): Promise<DownloadResult>;
266
+ /**
267
+ * Stream a response body as raw bytes. Yields the chunks the transport
268
+ * delivers, in order, never buffered whole. Ends cleanly on EOF and
269
+ * throws on a transport failure or a non-2xx status (body drained
270
+ * first). No JSON decoding, no line splitting, no framing —
271
+ * {@link streamLines} and {@link streamSse} build on this primitive.
272
+ *
273
+ * Closing the iterator before EOF (a `break` out of a `for await`)
274
+ * destroys the underlying socket, so a caller who takes only the
275
+ * first few chunks never leaks the connection.
276
+ *
277
+ * `opts.timeout` bounds the whole stream duration (default
278
+ * `TINA4_API_TIMEOUT` or the client `timeout`); `opts.connectTimeout`
279
+ * bounds just the connect + headers phase (default
280
+ * `TINA4_API_CONNECT_TIMEOUT` or 10s).
281
+ */
282
+ streamBytes(path: string, opts?: StreamOptions): AsyncGenerator<Uint8Array>;
283
+ /**
284
+ * Stream the response body as UTF-8 lines. Splits on LF or CRLF;
285
+ * buffers a multibyte codepoint that lands across a chunk boundary;
286
+ * yields a trailing line without a terminator on EOF. Built on
287
+ * {@link streamBytes} plus the shared {@link parseLineStream}.
288
+ */
289
+ streamLines(path: string, opts?: StreamOptions): AsyncGenerator<string>;
290
+ /**
291
+ * Stream the response as SSE (Server-Sent Events). Yields one
292
+ * {@link SseEvent} per event boundary (blank line) or on EOF for a
293
+ * trailing event. `data:[DONE]` is delivered as an ordinary event
294
+ * with `data === "[DONE]"` and the iterator ends on the next EOF.
295
+ * Built on {@link streamLines} plus the shared {@link parseSseStream}.
296
+ */
297
+ streamSse(path: string, opts?: StreamOptions): AsyncGenerator<SseEvent>;
298
+ /**
299
+ * Resolve a stream duration from (in order): explicit `opts` field,
300
+ * the named env var, then the fallback. Zero disables. A non-numeric
301
+ * or negative env value warns via a fallback rather than throwing —
302
+ * a bad env var must not brick every streaming call.
303
+ */
304
+ private streamSeconds;
305
+ /**
306
+ * Open a streaming HTTP request. Returns the raw
307
+ * {@link http.IncomingMessage} once headers arrive. Redirects are NOT
308
+ * followed on streams (a caller who needs a redirect should do a
309
+ * regular GET first). Connect phase is bounded by `connectSec`;
310
+ * body-phase timeout is applied by the caller (streamBytes) via
311
+ * `res.destroy()`.
312
+ */
313
+ private openStreamRequest;
212
314
  private buildUrl;
213
315
  /**
214
316
  * Build the request headers (default User-Agent + auth + cookie jar +
@@ -39,8 +39,8 @@ export { ServiceRunner, Tina4Service, matchCronField, matchesCron } from "./serv
39
39
  export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from "./service.js";
40
40
  export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, sweep, createBackend, _resetBackend } from "./cache.js";
41
41
  export type { ResponseCacheConfig, CacheBackend } from "./cache.js";
42
- export { Api } from "./api.js";
43
- export type { ApiResult, ApiOptions, ApiTransport, DownloadResult, UploadOptions } from "./api.js";
42
+ export { Api, ApiStreamError, parseLineStream, parseSseStream } from "./api.js";
43
+ export type { ApiResult, ApiOptions, ApiTransport, DownloadResult, UploadOptions, StreamOptions, SseEvent } from "./api.js";
44
44
  export { Context, defaultContext, existingContext, fts5Supported, _sharedContexts } from "./context/index.js";
45
45
  export type { SearchHit } from "./context/index.js";
46
46
  export { Events } from "./events.js";
@@ -58,7 +58,7 @@ export type { AiTool } from "./ai.js";
58
58
  export { Sso, SSO, SsoError } from "./sso.js";
59
59
  export type { SsoOptions } from "./sso.js";
60
60
  export { Ai, AiError, AiConfigError, AiHTTPError, AiTimeoutError, AiParseError } from "./aiClient.js";
61
- export type { ChatResponse, AiMessage, AiChatOptions, AiEmbedOptions } from "./aiClient.js";
61
+ export type { ChatResponse, AiMessage, AiChatOptions, AiEmbedOptions, AiEvent, ContentPart, AiMessageContent } from "./aiClient.js";
62
62
  export type { ImapMessage, ImapFullMessage, ImapAttachment } from "./messenger.js";
63
63
  export { LiteBackend } from "./queueBackends/liteBackend.js";
64
64
  export { RabbitMQBackend, parseAmqpUrl } from "./queueBackends/rabbitmqBackend.js";