@volcengine/ark-runtime 1.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.
Files changed (123) hide show
  1. package/LICENSE.txt +202 -0
  2. package/README.md +104 -0
  3. package/dist/cjs/index.js +1717 -0
  4. package/dist/esm/client.d.ts +97 -0
  5. package/dist/esm/client.d.ts.map +1 -0
  6. package/dist/esm/config.d.ts +46 -0
  7. package/dist/esm/config.d.ts.map +1 -0
  8. package/dist/esm/encryption/encrypt-chat.d.ts +24 -0
  9. package/dist/esm/encryption/encrypt-chat.d.ts.map +1 -0
  10. package/dist/esm/encryption/index.d.ts +4 -0
  11. package/dist/esm/encryption/index.d.ts.map +1 -0
  12. package/dist/esm/encryption/key-agreement.d.ts +73 -0
  13. package/dist/esm/encryption/key-agreement.d.ts.map +1 -0
  14. package/dist/esm/index.d.ts +11 -0
  15. package/dist/esm/index.d.ts.map +1 -0
  16. package/dist/esm/index.mjs +1476 -0
  17. package/dist/esm/rslib-runtime.mjs +37 -0
  18. package/dist/esm/types/bot.d.ts +109 -0
  19. package/dist/esm/types/bot.d.ts.map +1 -0
  20. package/dist/esm/types/chat-completion.d.ts +167 -0
  21. package/dist/esm/types/chat-completion.d.ts.map +1 -0
  22. package/dist/esm/types/common.d.ts +29 -0
  23. package/dist/esm/types/common.d.ts.map +1 -0
  24. package/dist/esm/types/content-generation.d.ts +118 -0
  25. package/dist/esm/types/content-generation.d.ts.map +1 -0
  26. package/dist/esm/types/context.d.ts +49 -0
  27. package/dist/esm/types/context.d.ts.map +1 -0
  28. package/dist/esm/types/embeddings.d.ts +44 -0
  29. package/dist/esm/types/embeddings.d.ts.map +1 -0
  30. package/dist/esm/types/error.d.ts +45 -0
  31. package/dist/esm/types/error.d.ts.map +1 -0
  32. package/dist/esm/types/file.d.ts +66 -0
  33. package/dist/esm/types/file.d.ts.map +1 -0
  34. package/dist/esm/types/http-request-error.d.ts +13 -0
  35. package/dist/esm/types/http-request-error.d.ts.map +1 -0
  36. package/dist/esm/types/images.d.ts +78 -0
  37. package/dist/esm/types/images.d.ts.map +1 -0
  38. package/dist/esm/types/index.d.ts +13 -0
  39. package/dist/esm/types/index.d.ts.map +1 -0
  40. package/dist/esm/types/multimodal-embedding.d.ts +56 -0
  41. package/dist/esm/types/multimodal-embedding.d.ts.map +1 -0
  42. package/dist/esm/types/responses/enums.d.ts +38 -0
  43. package/dist/esm/types/responses/enums.d.ts.map +1 -0
  44. package/dist/esm/types/responses/helpers.d.ts +22 -0
  45. package/dist/esm/types/responses/helpers.d.ts.map +1 -0
  46. package/dist/esm/types/responses/index.d.ts +4 -0
  47. package/dist/esm/types/responses/index.d.ts.map +1 -0
  48. package/dist/esm/types/responses/types.d.ts +906 -0
  49. package/dist/esm/types/responses/types.d.ts.map +1 -0
  50. package/dist/esm/types/tokenization.d.ts +22 -0
  51. package/dist/esm/types/tokenization.d.ts.map +1 -0
  52. package/dist/esm/utils/breaker-provider.d.ts +9 -0
  53. package/dist/esm/utils/breaker-provider.d.ts.map +1 -0
  54. package/dist/esm/utils/breaker.d.ts +28 -0
  55. package/dist/esm/utils/breaker.d.ts.map +1 -0
  56. package/dist/esm/utils/normalize.d.ts +51 -0
  57. package/dist/esm/utils/normalize.d.ts.map +1 -0
  58. package/dist/esm/utils/request-builder.d.ts +15 -0
  59. package/dist/esm/utils/request-builder.d.ts.map +1 -0
  60. package/dist/esm/utils/request-id.d.ts +5 -0
  61. package/dist/esm/utils/request-id.d.ts.map +1 -0
  62. package/dist/esm/utils/retry.d.ts +11 -0
  63. package/dist/esm/utils/retry.d.ts.map +1 -0
  64. package/dist/esm/utils/sse-decoder.d.ts +23 -0
  65. package/dist/esm/utils/sse-decoder.d.ts.map +1 -0
  66. package/dist/esm/utils/stream-reader.d.ts +67 -0
  67. package/dist/esm/utils/stream-reader.d.ts.map +1 -0
  68. package/dist/tsconfig.tsbuildinfo +1 -0
  69. package/example/README.md +118 -0
  70. package/example/batch-chat.ts +64 -0
  71. package/example/bot-chat.ts +66 -0
  72. package/example/chat-completion-function-call.ts +141 -0
  73. package/example/chat-completion-reasoning.ts +64 -0
  74. package/example/chat-completion-vision.ts +70 -0
  75. package/example/chat-completion.ts +62 -0
  76. package/example/content-generation.ts +70 -0
  77. package/example/context.ts +69 -0
  78. package/example/embeddings.ts +31 -0
  79. package/example/file-upload.ts +53 -0
  80. package/example/images.ts +74 -0
  81. package/example/list-input-items.ts +34 -0
  82. package/example/multimodal-embeddings.ts +36 -0
  83. package/example/responses/basic.ts +75 -0
  84. package/example/responses/doubao-app.ts +53 -0
  85. package/example/responses/mcp.ts +66 -0
  86. package/example/responses/streaming.ts +45 -0
  87. package/example/responses/video.ts +74 -0
  88. package/example/responses/web-search.ts +52 -0
  89. package/example/structured-outputs.ts +71 -0
  90. package/example/tokenization.ts +30 -0
  91. package/package.json +47 -0
  92. package/src/client.ts +1199 -0
  93. package/src/config.ts +68 -0
  94. package/src/encryption/encrypt-chat.ts +146 -0
  95. package/src/encryption/index.ts +21 -0
  96. package/src/encryption/key-agreement.ts +270 -0
  97. package/src/index.ts +10 -0
  98. package/src/types/ark.d.ts +9 -0
  99. package/src/types/bot.ts +127 -0
  100. package/src/types/chat-completion.ts +228 -0
  101. package/src/types/common.ts +37 -0
  102. package/src/types/content-generation.ts +135 -0
  103. package/src/types/context.ts +59 -0
  104. package/src/types/embeddings.ts +74 -0
  105. package/src/types/error.ts +93 -0
  106. package/src/types/file.ts +76 -0
  107. package/src/types/http-request-error.ts +34 -0
  108. package/src/types/images.ts +102 -0
  109. package/src/types/index.ts +12 -0
  110. package/src/types/multimodal-embedding.ts +67 -0
  111. package/src/types/responses/enums.ts +163 -0
  112. package/src/types/responses/helpers.ts +67 -0
  113. package/src/types/responses/index.ts +3 -0
  114. package/src/types/responses/types.ts +1335 -0
  115. package/src/types/tokenization.ts +24 -0
  116. package/src/utils/breaker-provider.ts +17 -0
  117. package/src/utils/breaker.ts +56 -0
  118. package/src/utils/normalize.ts +154 -0
  119. package/src/utils/request-builder.ts +51 -0
  120. package/src/utils/request-id.ts +17 -0
  121. package/src/utils/retry.ts +76 -0
  122. package/src/utils/sse-decoder.ts +140 -0
  123. package/src/utils/stream-reader.ts +270 -0
@@ -0,0 +1,24 @@
1
+ import type { HttpHeaders } from "./common";
2
+
3
+ export interface TokenizationRequest {
4
+ text: string | string[];
5
+ model: string;
6
+ user?: string;
7
+ }
8
+
9
+ export interface Tokenization {
10
+ index: number;
11
+ object: string;
12
+ total_tokens: number;
13
+ token_ids: number[];
14
+ offset_mapping: number[][];
15
+ }
16
+
17
+ export interface TokenizationResponse {
18
+ id: string;
19
+ created: number;
20
+ model: string;
21
+ object: string;
22
+ data: Tokenization[];
23
+ headers?: HttpHeaders;
24
+ }
@@ -0,0 +1,17 @@
1
+ import { Breaker } from "./breaker";
2
+
3
+ /**
4
+ * Maintains a per-model Breaker instance.
5
+ */
6
+ export class ModelBreakerProvider {
7
+ private breakers = new Map<string, Breaker>();
8
+
9
+ getOrCreate(model: string): Breaker {
10
+ let breaker = this.breakers.get(model);
11
+ if (!breaker) {
12
+ breaker = new Breaker();
13
+ this.breakers.set(model, breaker);
14
+ }
15
+ return breaker;
16
+ }
17
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Circuit breaker with slow-start strategy.
3
+ *
4
+ * After reset(durationMs):
5
+ * - No requests allowed during the cooldown period
6
+ * - After cooldown: allow 1, then 2, 4, 8... requests (exponential)
7
+ * - After 10s past cooldown: allow all
8
+ */
9
+ export class Breaker {
10
+ private allowTime: number; // timestamp (ms)
11
+ private waiters = new Map<string, number>(); // id → index
12
+ private nextIndex = 0;
13
+
14
+ constructor() {
15
+ this.allowTime = Date.now();
16
+ }
17
+
18
+ /**
19
+ * Check if a request with given waitIndex is allowed through.
20
+ */
21
+ private allow(waitIndex: number): boolean {
22
+ const elapsed = (Date.now() - this.allowTime) / 1000;
23
+ if (elapsed <= 0) return false;
24
+ if (elapsed > 10) return true;
25
+ return waitIndex < Math.pow(2, elapsed);
26
+ }
27
+
28
+ private getAllowedDelayMs(): number {
29
+ const delay = this.allowTime - Date.now();
30
+ return delay < 1000 ? 1000 : delay;
31
+ }
32
+
33
+ /**
34
+ * Reset the breaker — block all requests for `durationMs`.
35
+ */
36
+ reset(durationMs: number): void {
37
+ this.allowTime = Date.now() + durationMs;
38
+ }
39
+
40
+ /**
41
+ * Wait until the breaker allows this request through.
42
+ */
43
+ async wait(): Promise<void> {
44
+ const id = String(this.nextIndex++);
45
+ const idx = this.waiters.size;
46
+ this.waiters.set(id, idx);
47
+
48
+ try {
49
+ while (!this.allow(idx)) {
50
+ await new Promise((r) => setTimeout(r, this.getAllowedDelayMs()));
51
+ }
52
+ } finally {
53
+ this.waiters.delete(id);
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Response normalization utilities.
3
+ *
4
+ * The Go SDK serializes responses through Go structs, which means:
5
+ * - Fields WITHOUT `omitempty` are always present (even when zero/null)
6
+ * - Fields WITH `omitempty` are omitted when zero/null/empty
7
+ *
8
+ * The Node.js SDK receives raw JSON from the server (via Axios / JSON.parse),
9
+ * so fields may or may not be present depending on what the server returns.
10
+ *
11
+ * These functions align Node.js responses to match Go SDK serialization behavior.
12
+ */
13
+
14
+ import type {
15
+ ChatCompletionResponse,
16
+ ChatCompletionStreamResponse,
17
+ ChatCompletionChoice,
18
+ ChatCompletionStreamChoice,
19
+ } from "../types/chat-completion";
20
+ import type { EmbeddingResponse } from "../types/embeddings";
21
+ import type { Usage } from "../types/common";
22
+
23
+ // ─── Chat Completion (non-stream) ────────────────────────────
24
+
25
+ /**
26
+ * Normalize a ChatCompletionResponse to match Go SDK output.
27
+ *
28
+ * Go struct tags:
29
+ * ChatCompletionMessage.Name *string `json:"name"` → always present
30
+ * ChatCompletionChoice.LogProbs *LogProbs `json:"logprobs,omitempty"` → omitted when nil
31
+ * ChatCompletionChoice.FinishReason FinishReason `json:"finish_reason"` → always present (custom MarshalJSON: empty → null)
32
+ */
33
+ export function normalizeChatCompletionResponse(
34
+ resp: ChatCompletionResponse,
35
+ ): ChatCompletionResponse {
36
+ if (resp.choices) {
37
+ for (const choice of resp.choices) {
38
+ normalizeChatCompletionChoice(choice);
39
+ }
40
+ }
41
+ return resp;
42
+ }
43
+
44
+ function normalizeChatCompletionChoice(choice: ChatCompletionChoice): void {
45
+ // Go: Name *string `json:"name"` — no omitempty, always serialized
46
+ if (choice.message && !("name" in choice.message)) {
47
+ (choice.message as any).name = null;
48
+ }
49
+
50
+ // Go: LogProbs *LogProbs `json:"logprobs,omitempty"` — omitted when nil
51
+ if ("logprobs" in choice && (choice.logprobs === null || choice.logprobs === undefined)) {
52
+ delete (choice as any).logprobs;
53
+ }
54
+
55
+ // Go: FinishReason `json:"finish_reason"` — always present, custom MarshalJSON
56
+ // (already present in server response, but ensure it exists)
57
+ if (!("finish_reason" in choice)) {
58
+ (choice as any).finish_reason = null;
59
+ }
60
+ }
61
+
62
+ // ─── Chat Completion Stream ──────────────────────────────────
63
+
64
+ /**
65
+ * Normalize a ChatCompletionStreamResponse chunk to match Go SDK output.
66
+ *
67
+ * Go struct tags:
68
+ * ChatCompletionStreamChoiceDelta.Content string `json:"content,omitempty"` → omitted when ""
69
+ * ChatCompletionStreamChoiceDelta.Role string `json:"role,omitempty"` → omitted when ""
70
+ * ChatCompletionStreamChoice.FinishReason FinishReason `json:"finish_reason"` → always present (null when empty)
71
+ * ChatCompletionStreamChoice.LogProbs *LogProbs `json:"logprobs,omitempty"` → omitted when nil
72
+ * ChatCompletionStreamResponse.Usage *Usage `json:"usage,omitempty"` → omitted when nil
73
+ */
74
+ export function normalizeChatCompletionStreamChunk(
75
+ chunk: ChatCompletionStreamResponse,
76
+ ): ChatCompletionStreamResponse {
77
+ if (chunk.choices) {
78
+ for (const choice of chunk.choices) {
79
+ normalizeStreamChoice(choice);
80
+ }
81
+ }
82
+
83
+ // Go: Usage *Usage `json:"usage,omitempty"` — omitted when nil
84
+ if ("usage" in chunk && (chunk.usage === null || chunk.usage === undefined)) {
85
+ delete (chunk as any).usage;
86
+ }
87
+
88
+ return chunk;
89
+ }
90
+
91
+ function normalizeStreamChoice(choice: ChatCompletionStreamChoice): void {
92
+ // Go: FinishReason `json:"finish_reason"` — no omitempty, custom MarshalJSON outputs null for empty
93
+ if (!("finish_reason" in choice)) {
94
+ (choice as any).finish_reason = null;
95
+ }
96
+
97
+ // Go: LogProbs *LogProbs `json:"logprobs,omitempty"` — omitted when nil
98
+ if ("logprobs" in choice && (choice.logprobs === null || choice.logprobs === undefined)) {
99
+ delete (choice as any).logprobs;
100
+ }
101
+
102
+ if (choice.delta) {
103
+ // Go: Content string `json:"content,omitempty"` — omitted when empty string
104
+ if ("content" in choice.delta && choice.delta.content === "") {
105
+ delete (choice.delta as any).content;
106
+ }
107
+
108
+ // Go: Role string `json:"role,omitempty"` — omitted when empty string
109
+ if ("role" in choice.delta && choice.delta.role === "") {
110
+ delete (choice.delta as any).role;
111
+ }
112
+ }
113
+ }
114
+
115
+ // ─── Embeddings ──────────────────────────────────────────────
116
+
117
+ /**
118
+ * Normalize an EmbeddingResponse to match Go SDK output.
119
+ *
120
+ * Go struct tags (Usage):
121
+ * PromptTokens int `json:"prompt_tokens"` → always present
122
+ * CompletionTokens int `json:"completion_tokens"` → always present (even 0)
123
+ * TotalTokens int `json:"total_tokens"` → always present
124
+ * PromptTokensDetails PromptTokensDetail `json:"prompt_tokens_details"` → always present (even zero struct)
125
+ * CompletionTokensDetails CompletionTokensDetails `json:"completion_tokens_details"` → always present (even zero struct)
126
+ */
127
+ export function normalizeEmbeddingResponse(
128
+ resp: EmbeddingResponse,
129
+ ): EmbeddingResponse {
130
+ if (resp.usage) {
131
+ normalizeUsage(resp.usage);
132
+ }
133
+ return resp;
134
+ }
135
+
136
+ /**
137
+ * Ensure Usage has all fields that Go always serializes (no omitempty).
138
+ */
139
+ export function normalizeUsage(usage: Usage): void {
140
+ // Go: CompletionTokens int `json:"completion_tokens"` — no omitempty
141
+ if (usage.completion_tokens === undefined || usage.completion_tokens === null) {
142
+ usage.completion_tokens = 0;
143
+ }
144
+
145
+ // Go: PromptTokensDetails PromptTokensDetail `json:"prompt_tokens_details"` — no omitempty
146
+ if (!usage.prompt_tokens_details) {
147
+ usage.prompt_tokens_details = { cached_tokens: 0 };
148
+ }
149
+
150
+ // Go: CompletionTokensDetails CompletionTokensDetails `json:"completion_tokens_details"` — no omitempty
151
+ if (!usage.completion_tokens_details) {
152
+ usage.completion_tokens_details = { reasoning_tokens: 0 };
153
+ }
154
+ }
@@ -0,0 +1,51 @@
1
+ import type { AxiosRequestConfig } from "axios";
2
+ import { genRequestId } from "./request-id";
3
+ import { ClientRequestHeader } from "../types/common";
4
+
5
+ export interface RequestOptions {
6
+ body?: unknown;
7
+ contentType?: string;
8
+ projectName?: string;
9
+ customHeaders?: Record<string, string>;
10
+ query?: Record<string, string>;
11
+ signal?: AbortSignal;
12
+ stream?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Build an AxiosRequestConfig from method, url, and request options.
17
+ */
18
+ export function buildRequest(
19
+ method: string,
20
+ url: string,
21
+ authHeader: Record<string, string>,
22
+ opts: RequestOptions = {},
23
+ ): AxiosRequestConfig {
24
+ const headers: Record<string, string> = {
25
+ [ClientRequestHeader]: genRequestId(),
26
+ "Content-Type": opts.contentType ?? "application/json",
27
+ ...authHeader,
28
+ ...opts.customHeaders,
29
+ };
30
+
31
+ const config: AxiosRequestConfig = {
32
+ method: method as any,
33
+ url,
34
+ headers,
35
+ signal: opts.signal,
36
+ };
37
+
38
+ if (opts.body !== undefined) {
39
+ config.data = opts.body;
40
+ }
41
+
42
+ if (opts.query) {
43
+ config.params = opts.query;
44
+ }
45
+
46
+ if (opts.stream) {
47
+ config.responseType = "stream";
48
+ }
49
+
50
+ return config;
51
+ }
@@ -0,0 +1,17 @@
1
+ import { randomBytes } from "crypto";
2
+
3
+ /**
4
+ * Generate a request ID in the format: YYYYMMDDHHmmss + 20 hex digits.
5
+ */
6
+ export function genRequestId(): string {
7
+ const now = new Date();
8
+ const ts =
9
+ now.getFullYear().toString() +
10
+ String(now.getMonth() + 1).padStart(2, "0") +
11
+ String(now.getDate()).padStart(2, "0") +
12
+ String(now.getHours()).padStart(2, "0") +
13
+ String(now.getMinutes()).padStart(2, "0") +
14
+ String(now.getSeconds()).padStart(2, "0");
15
+ const hex = randomBytes(10).toString("hex");
16
+ return ts + hex;
17
+ }
@@ -0,0 +1,76 @@
1
+ export interface RetryPolicy {
2
+ maxAttempts: number;
3
+ initialBackoffMs: number;
4
+ maxBackoffMs: number;
5
+ }
6
+
7
+ /**
8
+ * Calculate retry delay using ExponentialWithRandomJitter strategy.
9
+ * Algorithm aligned with sdk-core calculateRetryDelay
10
+ * (packages/sdk-core/src/utils/retry.ts).
11
+ */
12
+ function calculateRetryDelay(
13
+ attemptNumber: number,
14
+ minRetryDelay: number,
15
+ maxRetryDelay: number,
16
+ ): number {
17
+ const base = Math.min(
18
+ minRetryDelay * Math.pow(2, attemptNumber - 1),
19
+ maxRetryDelay,
20
+ );
21
+ return Math.floor(Math.min(maxRetryDelay, base + Math.random() * base));
22
+ }
23
+
24
+ /**
25
+ * Retry a function with exponential backoff and jitter.
26
+ * Respects AbortSignal for cancellation.
27
+ */
28
+ export async function retry<T>(
29
+ policy: RetryPolicy,
30
+ fn: () => Promise<T>,
31
+ isRetryable: (err: unknown) => boolean,
32
+ signal?: AbortSignal,
33
+ ): Promise<T> {
34
+ let lastError: unknown;
35
+ for (let attempt = 0; attempt <= policy.maxAttempts; attempt++) {
36
+ try {
37
+ return await fn();
38
+ } catch (err) {
39
+ lastError = err;
40
+
41
+ if (!isRetryable(err)) {
42
+ throw err;
43
+ }
44
+ if (attempt === policy.maxAttempts) {
45
+ break;
46
+ }
47
+
48
+ const delayMs = calculateRetryDelay(
49
+ attempt + 1,
50
+ policy.initialBackoffMs,
51
+ policy.maxBackoffMs,
52
+ );
53
+
54
+ await sleep(delayMs, signal);
55
+ }
56
+ }
57
+ throw lastError;
58
+ }
59
+
60
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
61
+ return new Promise<void>((resolve, reject) => {
62
+ if (signal?.aborted) {
63
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
64
+ return;
65
+ }
66
+ const timer = setTimeout(resolve, ms);
67
+ signal?.addEventListener(
68
+ "abort",
69
+ () => {
70
+ clearTimeout(timer);
71
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
72
+ },
73
+ { once: true },
74
+ );
75
+ });
76
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * SSE event parsed from a text/event-stream.
3
+ */
4
+ export interface SSEEvent {
5
+ event: string;
6
+ data: string;
7
+ }
8
+
9
+ /**
10
+ * EventStreamDecoder parses a text/event-stream from a Node readable stream.
11
+ * Yields SSEEvent objects via the async iterator protocol.
12
+ */
13
+ export class EventStreamDecoder {
14
+ private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
15
+ private decoder = new TextDecoder();
16
+ private buffer = "";
17
+
18
+ constructor(
19
+ private stream:
20
+ | ReadableStream<Uint8Array>
21
+ | NodeJS.ReadableStream,
22
+ ) {}
23
+
24
+ async *[Symbol.asyncIterator](): AsyncIterator<SSEEvent> {
25
+ const lines = this.readLines();
26
+ let event = "";
27
+ let dataLines: string[] = [];
28
+
29
+ for await (const line of lines) {
30
+ // Empty line → dispatch event
31
+ if (line === "") {
32
+ if (dataLines.length > 0) {
33
+ yield {
34
+ event,
35
+ data: dataLines.join("\n"),
36
+ };
37
+ }
38
+ event = "";
39
+ dataLines = [];
40
+ continue;
41
+ }
42
+
43
+ // Comment line
44
+ if (line.startsWith(":")) {
45
+ continue;
46
+ }
47
+
48
+ const colonIdx = line.indexOf(":");
49
+ let field: string;
50
+ let value: string;
51
+
52
+ if (colonIdx === -1) {
53
+ field = line;
54
+ value = "";
55
+ } else {
56
+ field = line.slice(0, colonIdx);
57
+ value = line.slice(colonIdx + 1);
58
+ // Remove optional leading space after colon
59
+ if (value.startsWith(" ")) {
60
+ value = value.slice(1);
61
+ }
62
+ }
63
+
64
+ switch (field) {
65
+ case "event":
66
+ event = value;
67
+ break;
68
+ case "data":
69
+ dataLines.push(value);
70
+ break;
71
+ // id, retry etc. — ignored for our use case
72
+ }
73
+ }
74
+
75
+ // Flush remaining
76
+ if (dataLines.length > 0) {
77
+ yield { event, data: dataLines.join("\n") };
78
+ }
79
+ }
80
+
81
+ private async *readLines(): AsyncGenerator<string> {
82
+ // Handle both Web ReadableStream and Node ReadableStream
83
+ const chunks = this.iterateStream();
84
+
85
+ for await (const chunk of chunks) {
86
+ this.buffer += typeof chunk === "string"
87
+ ? chunk
88
+ : this.decoder.decode(chunk, { stream: true });
89
+
90
+ const lines = this.buffer.split("\n");
91
+ // Keep the last partial line in the buffer
92
+ this.buffer = lines.pop() ?? "";
93
+
94
+ for (const line of lines) {
95
+ // Remove trailing \r if present
96
+ yield line.endsWith("\r") ? line.slice(0, -1) : line;
97
+ }
98
+ }
99
+
100
+ // Flush remaining buffer
101
+ if (this.buffer.length > 0) {
102
+ yield this.buffer;
103
+ this.buffer = "";
104
+ }
105
+ }
106
+
107
+ private async *iterateStream(): AsyncGenerator<Uint8Array | string> {
108
+ const stream = this.stream as any;
109
+
110
+ // Web ReadableStream
111
+ if (typeof stream.getReader === "function") {
112
+ this.reader = stream.getReader();
113
+ try {
114
+ while (true) {
115
+ const { done, value } = await this.reader!.read();
116
+ if (done) break;
117
+ if (value) yield value;
118
+ }
119
+ } finally {
120
+ this.reader!.releaseLock();
121
+ this.reader = null;
122
+ }
123
+ return;
124
+ }
125
+
126
+ // Node ReadableStream (async iterable)
127
+ if (Symbol.asyncIterator in stream) {
128
+ for await (const chunk of stream) {
129
+ yield chunk;
130
+ }
131
+ return;
132
+ }
133
+
134
+ throw new Error("Unsupported stream type");
135
+ }
136
+
137
+ cancel(): void {
138
+ this.reader?.cancel();
139
+ }
140
+ }