simple-ai-sdk 1.0.15 → 1.0.17

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.
@@ -3,6 +3,10 @@ import type { Message, JSONValue } from "../shared/index.js";
3
3
  export interface IStreamManager {
4
4
  subscribe(listener: StreamManagerListener): () => void;
5
5
  getSnapshot(): StreamManagerState;
6
+ /**
7
+ * セッション情報を読み取り専用で取得する。
8
+ * - 状態参照のためのユーティリティ(副作用は発生しない)。
9
+ */
6
10
  getSession(sessionId: string): ChatSession | undefined;
7
11
  initSession(sessionId: string, initialMessages?: Message[]): void;
8
12
  /**
@@ -11,10 +15,6 @@ export interface IStreamManager {
11
15
  * - コンポーネントがアンマウントされても保持され、完了時に呼ばれる。
12
16
  */
13
17
  setOnFinishHandler(sessionId: string, handler: ((messages: Message[]) => void) | undefined): void;
14
- /**
15
- * セッション単位で最新の onMetadata ハンドラを登録する。
16
- */
17
- setOnMetadataHandler(sessionId: string, handler: ((metadata: Record<string, JSONValue>) => void) | undefined): void;
18
18
  /**
19
19
  * セッション単位で最新の onError ハンドラを登録する。
20
20
  */
@@ -32,7 +32,6 @@ export interface IStreamManager {
32
32
  * レンダリング中に呼ばれる可能性があるため、通知は行わない。
33
33
  */
34
34
  touchSession(sessionId: string): void;
35
- updateSession(sessionId: string, updates: Partial<ChatSession>): void;
36
35
  streamChat(sessionId: string, messages: Message[], options: {
37
36
  api: string;
38
37
  body?: Record<string, JSONValue>;
@@ -61,20 +60,17 @@ declare class StreamManager implements IStreamManager {
61
60
  private rafFallbackTimer;
62
61
  private getRAF;
63
62
  private getCAF;
64
- private pendingCloneSessions;
65
63
  private maxSessions;
66
64
  private debug;
67
65
  private ttlMs?;
68
66
  private sweepTimer;
69
67
  private activeSubscribers;
70
68
  private latestOnFinish;
71
- private latestOnMetadata;
72
69
  private latestOnError;
73
70
  private streamIds;
74
71
  constructor(options?: StreamManagerOptions);
75
72
  subscribe(listener: StreamManagerListener): () => void;
76
73
  setOnFinishHandler(sessionId: string, handler: ((messages: Message[]) => void) | undefined): void;
77
- setOnMetadataHandler(sessionId: string, handler: ((metadata: Record<string, JSONValue>) => void) | undefined): void;
78
74
  setOnErrorHandler(sessionId: string, handler: ((error: Error) => void) | undefined): void;
79
75
  getSnapshot(): StreamManagerState;
80
76
  private notify;
@@ -83,9 +79,22 @@ declare class StreamManager implements IStreamManager {
83
79
  initSession(sessionId: string, initialMessages?: Message[]): void;
84
80
  retainSession(sessionId: string): void;
85
81
  releaseSession(sessionId: string): void;
82
+ /**
83
+ * LRU に従いセッションを1件削除する。
84
+ * - 第一候補: 非ストリーミング(ready/error/aborted)かつ購読者なし
85
+ * - 第二候補: 非ストリーミング(購読者有無は問わない)
86
+ * - 進行中(submitted/streaming)は破棄しない(ユーザー操作中断の回避)
87
+ * @returns 削除が行われた場合は true、候補が無い場合は false
88
+ */
86
89
  private removeOldestSession;
87
90
  private logSessionInfo;
88
- updateSession(sessionId: string, updates: Partial<ChatSession>, immediate?: boolean): void;
91
+ /**
92
+ * セッションの部分更新(内部専用)
93
+ * - 参照の安定性を保ちながら、Map の再生成により変更を通知する
94
+ * - important: messages を更新した場合、messagesVersion をインクリメント
95
+ * - immediate=true の場合、rAF をバイパスして即時通知(ステータス変更等のUX向上)
96
+ */
97
+ private updateSession;
89
98
  streamChat(sessionId: string, messages: Message[], options: {
90
99
  api: string;
91
100
  body?: Record<string, JSONValue>;
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,20 @@ class StreamManager {
12
12
  rafFallbackTimer = null;
13
13
  // Typed accessors for rAF/cAF without relying on DOM types
14
14
  getRAF() {
15
+ // If the page is hidden, avoid rAF because it can be throttled heavily
16
+ try {
17
+ if (typeof window !== "undefined" &&
18
+ typeof document !== "undefined" &&
19
+ // visibilityState can be missing in some environments
20
+ typeof document.visibilityState ===
21
+ "string" &&
22
+ document.visibilityState === "hidden") {
23
+ return undefined;
24
+ }
25
+ }
26
+ catch {
27
+ // ignore detection errors and fall through
28
+ }
15
29
  const g = globalThis;
16
30
  return typeof g.requestAnimationFrame === "function"
17
31
  ? g.requestAnimationFrame
@@ -23,9 +37,6 @@ class StreamManager {
23
37
  ? g.cancelAnimationFrame
24
38
  : undefined;
25
39
  }
26
- // 1フレーム内での messages 配列クローンをセッション単位で一度に抑制するためのフラグ
27
- // 意図: チャンクが高頻度で到着するケースで、配列コピーをフレームあたり1回に抑える
28
- pendingCloneSessions = new Set();
29
40
  maxSessions;
30
41
  debug;
31
42
  ttlMs;
@@ -34,8 +45,6 @@ class StreamManager {
34
45
  activeSubscribers = new Map();
35
46
  // セッション単位で「最新の onFinish」を保持(常に最新を呼びたい要件に対応)
36
47
  latestOnFinish = new Map();
37
- // セッション単位で「最新の onMetadata」を保持
38
- latestOnMetadata = new Map();
39
48
  // セッション単位で「最新の onError」を保持
40
49
  latestOnError = new Map();
41
50
  // 意図: 各セッションの最新ストリームIDを保持し、古いストリームの遅延イベントを無視する
@@ -72,14 +81,6 @@ class StreamManager {
72
81
  this.latestOnFinish.delete(sessionId);
73
82
  }
74
83
  }
75
- setOnMetadataHandler(sessionId, handler) {
76
- if (handler) {
77
- this.latestOnMetadata.set(sessionId, handler);
78
- }
79
- else {
80
- this.latestOnMetadata.delete(sessionId);
81
- }
82
- }
83
84
  setOnErrorHandler(sessionId, handler) {
84
85
  if (handler) {
85
86
  this.latestOnError.set(sessionId, handler);
@@ -121,9 +122,16 @@ class StreamManager {
121
122
  this.rafFallbackTimer = null;
122
123
  }
123
124
  this.rafScheduled = false;
124
- this.listeners.forEach((listener) => listener());
125
- // 即時通知時はクローン抑制フラグをクリア(次フレームに持ち越さない)
126
- this.pendingCloneSessions.clear();
125
+ this.listeners.forEach((listener) => {
126
+ try {
127
+ listener();
128
+ }
129
+ catch (err) {
130
+ // 重要: 1つの購読者の例外で全体の通知が止まらないように防御
131
+ if (this.debug)
132
+ console.warn("[SM] listener error (immediate)", err);
133
+ }
134
+ });
127
135
  }
128
136
  else {
129
137
  // ストリーミング中の頻繁な更新は rAF で 1 フレームにまとめる
@@ -135,9 +143,15 @@ class StreamManager {
135
143
  this.rafId = raf(() => {
136
144
  this.rafId = null;
137
145
  this.rafScheduled = false;
138
- this.listeners.forEach((listener) => listener());
139
- // フレームの終わりでクローン抑制フラグをクリア
140
- this.pendingCloneSessions.clear();
146
+ this.listeners.forEach((listener) => {
147
+ try {
148
+ listener();
149
+ }
150
+ catch (err) {
151
+ if (this.debug)
152
+ console.warn("[SM] listener error (rAF)", err);
153
+ }
154
+ });
141
155
  });
142
156
  }
143
157
  else {
@@ -145,20 +159,23 @@ class StreamManager {
145
159
  this.rafFallbackTimer = setTimeout(() => {
146
160
  this.rafFallbackTimer = null;
147
161
  this.rafScheduled = false;
148
- this.listeners.forEach((listener) => listener());
149
- // Fallbackでもフレーム相当のタイミングでクリア
150
- this.pendingCloneSessions.clear();
162
+ this.listeners.forEach((listener) => {
163
+ try {
164
+ listener();
165
+ }
166
+ catch (err) {
167
+ if (this.debug)
168
+ console.warn("[SM] listener error (timer)", err);
169
+ }
170
+ });
151
171
  }, 16);
152
172
  }
153
173
  }
154
174
  }
155
175
  getSession(sessionId) {
156
- const session = this.state.sessions.get(sessionId);
157
- if (session) {
158
- // Update last accessed time when getting a session
159
- session.lastAccessed = Date.now();
160
- }
161
- return session;
176
+ // 読み取り専用: lastAccessed を更新しない
177
+ // (アクセス時間の更新は touchSession() 側で明示的に実施するポリシー)
178
+ return this.state.sessions.get(sessionId);
162
179
  }
163
180
  // LRU 用に最終アクセス時刻のみを更新する(通知はしない)
164
181
  touchSession(sessionId) {
@@ -171,7 +188,8 @@ class StreamManager {
171
188
  const existingSession = this.state.sessions.get(sessionId);
172
189
  // streaming中のセッションが存在する場合は、initialMessagesで上書きしない
173
190
  if (existingSession) {
174
- if (existingSession.status === "streaming" || existingSession.status === "submitted") {
191
+ if (existingSession.status === "streaming" ||
192
+ existingSession.status === "submitted") {
175
193
  if (this.debug) {
176
194
  console.log(`[SM] Skip initialMessages for streaming session: ${sessionId}`);
177
195
  }
@@ -182,11 +200,20 @@ class StreamManager {
182
200
  }
183
201
  // Check if we need to remove old sessions
184
202
  if (this.state.sessions.size >= this.maxSessions) {
185
- this.removeOldestSession();
203
+ // 進行中(submitted/streaming)のセッションは破棄しないポリシー
204
+ // それ以外の候補が見つからない場合は新規セッションの生成を見送る
205
+ const removed = this.removeOldestSession();
206
+ if (!removed) {
207
+ if (this.debug) {
208
+ console.warn(`[SM] Reached maxSessions; no eligible session to evict. Skipping init for: ${sessionId}`);
209
+ }
210
+ return;
211
+ }
186
212
  }
187
213
  this.state.sessions.set(sessionId, {
188
214
  id: sessionId,
189
215
  messages: initialMessages,
216
+ metadata: [],
190
217
  messagesVersion: 0,
191
218
  status: "ready",
192
219
  error: null,
@@ -196,7 +223,7 @@ class StreamManager {
196
223
  if (this.debug) {
197
224
  this.logSessionInfo(`Session added: ${sessionId}`);
198
225
  }
199
- this.notify(true); // Immediate for initialization
226
+ this.notify(true); // 即時に通知(初期化はUX上重要)
200
227
  if (this.debug)
201
228
  console.log("[SM] init sessions", this.state.sessions, this.activeSubscribers);
202
229
  }
@@ -207,18 +234,6 @@ class StreamManager {
207
234
  if (this.debug) {
208
235
  console.info(`[SM] retainSession: ${sessionId} -> ${next}`);
209
236
  }
210
- // 修正意図(日本語):
211
- // - 0→1 に購読者数が増えた直後は、これまで抑制されていたストリーミング更新を
212
- // 即座に UI に反映させたいケースがある。
213
- // - そこで、対象セッションが streaming/submitted の場合に限り、即時通知を一度だけ行う。
214
- // (注意: 現在の useChatSession 実装では retain が subscribe より先に呼ばれるため、
215
- // この通知は新規購読者より前に発生することがある。将来の呼び出し順や他の購読者に対しては有効。)
216
- if (next === 1) {
217
- const s = this.state.sessions.get(sessionId);
218
- if (s && (s.status === "streaming" || s.status === "submitted")) {
219
- this.notify(true);
220
- }
221
- }
222
237
  }
223
238
  releaseSession(sessionId) {
224
239
  const prev = this.activeSubscribers.get(sessionId) ?? 0;
@@ -233,6 +248,13 @@ class StreamManager {
233
248
  console.info(`[SM] releaseSession: ${sessionId} -> ${next}`);
234
249
  }
235
250
  }
251
+ /**
252
+ * LRU に従いセッションを1件削除する。
253
+ * - 第一候補: 非ストリーミング(ready/error/aborted)かつ購読者なし
254
+ * - 第二候補: 非ストリーミング(購読者有無は問わない)
255
+ * - 進行中(submitted/streaming)は破棄しない(ユーザー操作中断の回避)
256
+ * @returns 削除が行われた場合は true、候補が無い場合は false
257
+ */
236
258
  removeOldestSession() {
237
259
  let oldestSessionId = null;
238
260
  let oldestTime = Date.now();
@@ -248,10 +270,12 @@ class StreamManager {
248
270
  }
249
271
  }
250
272
  }
251
- // 上で見つからない場合は、状態・購読を問わず最古を探す(上限を強制的に守るため)
273
+ // 上で見つからない場合は、「非ストリーミング限定」で最古を探す
252
274
  if (!oldestSessionId) {
253
275
  for (const [id, session] of this.state.sessions) {
254
- if (session.lastAccessed < oldestTime) {
276
+ if (session.status !== "streaming" &&
277
+ session.status !== "submitted" &&
278
+ session.lastAccessed < oldestTime) {
255
279
  oldestTime = session.lastAccessed;
256
280
  oldestSessionId = id;
257
281
  }
@@ -266,14 +290,15 @@ class StreamManager {
266
290
  this.state.sessions.delete(oldestSessionId);
267
291
  // セッション破棄時はハンドラも破棄
268
292
  this.latestOnFinish.delete(oldestSessionId);
269
- this.latestOnMetadata.delete(oldestSessionId);
270
293
  this.latestOnError.delete(oldestSessionId);
271
294
  // 意図: streamId も破棄してメモリリーク・誤判定を防止
272
295
  this.streamIds.delete(oldestSessionId);
273
296
  if (this.debug) {
274
297
  this.logSessionInfo(`Session removed: ${oldestSessionId} (LRU eviction)`);
275
298
  }
299
+ return true;
276
300
  }
301
+ return false;
277
302
  }
278
303
  logSessionInfo(action) {
279
304
  const sessionInfo = Array.from(this.state.sessions.entries()).map(([id, session]) => ({
@@ -290,6 +315,12 @@ class StreamManager {
290
315
  sessions: sessionInfo,
291
316
  });
292
317
  }
318
+ /**
319
+ * セッションの部分更新(内部専用)
320
+ * - 参照の安定性を保ちながら、Map の再生成により変更を通知する
321
+ * - important: messages を更新した場合、messagesVersion をインクリメント
322
+ * - immediate=true の場合、rAF をバイパスして即時通知(ステータス変更等のUX向上)
323
+ */
293
324
  updateSession(sessionId, updates, immediate = false) {
294
325
  const session = this.state.sessions.get(sessionId);
295
326
  if (session) {
@@ -305,25 +336,25 @@ class StreamManager {
305
336
  // messages が指定されたタイミングでのみバージョンを進める
306
337
  session.messagesVersion = (session.messagesVersion ?? 0) + 1;
307
338
  }
308
- // 再レンダリング抑制: 購読者が0のセッションに対する
309
- // ストリーミング由来の message 更新では通知しない。
310
- // (onFinish/onError等のイベントは immediate=true で通知される)
311
339
  if (immediate) {
312
340
  this.notify(true);
313
341
  }
314
342
  else {
315
- const subCount = this.activeSubscribers.get(sessionId) ?? 0;
316
- if (subCount > 0) {
317
- this.notify(false);
318
- }
319
- else if (this.debug) {
320
- // デバッグ時は抑制されたことをログ
321
- console.info(`[SM] notify suppressed (no subscribers): ${sessionId}`);
322
- }
343
+ // 常に rAF バッチで通知(購読者数による抑制は行わない)
344
+ this.notify(false);
323
345
  }
324
346
  }
325
347
  }
326
348
  async streamChat(sessionId, messages, options) {
349
+ const isObject = (v) => typeof v === "object" && v !== null;
350
+ const isChunkPayload = (v) => isObject(v) &&
351
+ (typeof v.text === "undefined" ||
352
+ typeof v.text === "string") &&
353
+ (typeof v.meta === "undefined" ||
354
+ Array.isArray(v.meta));
355
+ const isErrorPayload = (v) => isObject(v) &&
356
+ (typeof v.message === "undefined" ||
357
+ typeof v.message === "string");
327
358
  const session = this.state.sessions.get(sessionId);
328
359
  if (!session) {
329
360
  if (this.debug)
@@ -350,6 +381,8 @@ class StreamManager {
350
381
  status: "submitted",
351
382
  error: null,
352
383
  abortController,
384
+ // 新規ストリーム開始時にメタデータはリセット(current stream のみ保持)
385
+ metadata: [],
353
386
  }, true); // Immediate for status change
354
387
  try {
355
388
  const requestBody = {
@@ -366,13 +399,13 @@ class StreamManager {
366
399
  });
367
400
  // HTTP レベルのエラーハンドリング:SSE 解析に入る前に早期失敗
368
401
  if (!response.ok) {
369
- let message = `HTTP ${response.status}`;
402
+ let message;
370
403
  try {
371
- const text = await response.text();
372
- message += text ? `: ${text}` : "";
404
+ message = await response.text();
373
405
  }
374
406
  catch (err) {
375
407
  console.warn("[SM] response.text() error", err);
408
+ message = `Unknown error: ${response.status}`;
376
409
  }
377
410
  const err = new Error(message);
378
411
  this.updateSession(sessionId, {
@@ -422,7 +455,7 @@ class StreamManager {
422
455
  if (dataLines.length === 0 && !eventName)
423
456
  return;
424
457
  const dataStr = dataLines.join("\n");
425
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
458
+ // JSON を安全にパース(失敗時は null)
426
459
  let parsed;
427
460
  try {
428
461
  parsed = dataStr ? JSON.parse(dataStr) : null;
@@ -433,57 +466,43 @@ class StreamManager {
433
466
  }
434
467
  switch (eventName) {
435
468
  case "chunk": {
436
- if (parsed && typeof parsed.text === "string") {
469
+ if (parsed && isChunkPayload(parsed)) {
437
470
  const latestId = this.streamIds.get(sessionId);
438
471
  if (latestId !== streamId)
439
472
  return; // 中断済みのストリームからの遅延イベントを破棄
440
- // NOTE: 連結は O(n^2) 化を避ける最適化余地がある(必要ならバッファ化)
441
- assistantMessage.content += parsed.text;
442
- const subCount = this.activeSubscribers.get(sessionId) ?? 0;
443
- if (subCount > 0) {
444
- // フレームあたり1回だけ配列参照を更新
445
- if (!this.pendingCloneSessions.has(sessionId)) {
446
- this.pendingCloneSessions.add(sessionId);
447
- this.updateSession(sessionId, {
448
- messages: [...updatedMessages],
449
- }); // rAFバッチで通知
473
+ // Handle text delta if provided
474
+ if (typeof parsed.text === "string" && parsed.text.length > 0) {
475
+ // NOTE: 連結は O(n^2) 化を避ける最適化余地がある(必要ならバッファ化)
476
+ const wasEmpty = assistantMessage.content.length === 0;
477
+ assistantMessage.content += parsed.text;
478
+ // 最初のテキストチャンクは即時反映して初動レイテンシを下げる
479
+ if (wasEmpty) {
480
+ this.updateSession(sessionId, { messages: [...updatedMessages] }, true);
450
481
  }
451
482
  else {
452
- // 2回目以降は配列複製せず、rAFでの通知に任せる
453
- // ここでは何もしない(不要なノーティファイを避ける)
483
+ // 以降は rAF バッチで配列参照を更新
484
+ this.updateSession(sessionId, {
485
+ messages: [...updatedMessages],
486
+ });
454
487
  }
455
488
  }
456
- else if (this.debug) {
457
- console.info(`[SM] chunk suppressed (no subscribers): ${sessionId}`);
458
- }
459
- }
460
- break;
461
- }
462
- case "fullContent": {
463
- if (parsed && parsed.type === "replace" && typeof parsed.content === "string") {
464
- const latestId = this.streamIds.get(sessionId);
465
- if (latestId !== streamId)
466
- return; // 中断済みのストリームからの遅延イベントを破棄
467
- assistantMessage.content = parsed.content;
468
- this.updateSession(sessionId, {
469
- messages: [...updatedMessages],
470
- }, true); // 即時更新
471
- }
472
- break;
473
- }
474
- case "metadata": {
475
- if (parsed) {
476
- try {
477
- this.latestOnMetadata.get(sessionId)?.(parsed);
478
- }
479
- catch {
480
- console.error("[SM] Failed to call onMetadata handler");
489
+ // Handle bundled metadata if present
490
+ if (Array.isArray(parsed.meta) && parsed.meta.length > 0) {
491
+ const s = this.state.sessions.get(sessionId);
492
+ if (s) {
493
+ // Append new metadata items
494
+ const nextMeta = s.metadata.concat(parsed.meta);
495
+ this.updateSession(sessionId, { metadata: nextMeta });
496
+ }
481
497
  }
482
498
  }
483
499
  break;
484
500
  }
485
501
  case "error": {
486
- const err = new Error(parsed?.message || "Stream error");
502
+ const message = isErrorPayload(parsed) && parsed.message
503
+ ? parsed.message
504
+ : "Stream error";
505
+ const err = new Error(message);
487
506
  try {
488
507
  abortController.abort();
489
508
  }
@@ -517,7 +536,8 @@ class StreamManager {
517
536
  abortController: null,
518
537
  }, true);
519
538
  try {
520
- this.latestOnFinish.get(sessionId)?.((this.state.sessions.get(sessionId)?.messages || []));
539
+ this.latestOnFinish.get(sessionId)?.((this.state.sessions.get(sessionId)?.messages ||
540
+ []));
521
541
  }
522
542
  catch {
523
543
  console.error("[SM] Failed to call onFinish handler");
@@ -541,7 +561,7 @@ class StreamManager {
541
561
  const lines = buffer.split("\n");
542
562
  buffer = lines.pop() || ""; // 末尾の未完成行をバッファに残す
543
563
  for (const line of lines) {
544
- if (line === "") {
564
+ if (line.trim() === "") {
545
565
  // 空行で一つのイベントをフラッシュ
546
566
  flushEvent();
547
567
  eventName = null;
@@ -566,6 +586,19 @@ class StreamManager {
566
586
  if (shouldTerminate)
567
587
  break;
568
588
  }
589
+ // 読み取り終了時の明示クリーンアップ(環境により不要だが安全)
590
+ try {
591
+ await reader.cancel();
592
+ }
593
+ catch {
594
+ /* noop */
595
+ }
596
+ try {
597
+ await response.body?.cancel();
598
+ }
599
+ catch {
600
+ /* noop */
601
+ }
569
602
  // 意図: 正常完了(サーバが done を送らないケース)だけをここで処理。
570
603
  // failed=true(error)の場合は既に状態更新済みなので何もしない。
571
604
  if (!failed && !shouldTerminate) {
@@ -584,10 +617,14 @@ class StreamManager {
584
617
  catch (error) {
585
618
  // 意図: エラーイベントは catch に到達しない(failed フラグで分岐)ため、ここは接続エラーや中断のみ
586
619
  if (isAbortError(error)) {
587
- this.updateSession(sessionId, {
588
- status: "aborted",
589
- abortController: null,
590
- }, true); // Immediate for status change
620
+ // 既に stopStream() 等で aborted 済みであれば二重更新を避ける
621
+ const s = this.state.sessions.get(sessionId);
622
+ if (s?.status !== "aborted") {
623
+ this.updateSession(sessionId, {
624
+ status: "aborted",
625
+ abortController: null,
626
+ }, true); // Immediate for status change
627
+ }
591
628
  }
592
629
  else {
593
630
  const err = error instanceof Error ? error : new Error("Unknown error");
@@ -630,6 +667,9 @@ class StreamManager {
630
667
  performSweep() {
631
668
  if (this.debug)
632
669
  console.info("[SM] Running TTL sweep", this.state.sessions);
670
+ const ttlMs = this.ttlMs;
671
+ if (!ttlMs || ttlMs <= 0)
672
+ return; // TTL 未設定なら何もしない
633
673
  if (this.state.sessions.size === 0)
634
674
  return;
635
675
  const now = Date.now();
@@ -642,7 +682,7 @@ class StreamManager {
642
682
  const subCount = this.activeSubscribers.get(id) ?? 0;
643
683
  if (subCount > 0)
644
684
  continue;
645
- if (now - session.lastAccessed > this.ttlMs) {
685
+ if (now - session.lastAccessed > ttlMs) {
646
686
  // 安全のため abortController が残っていれば中断
647
687
  try {
648
688
  session.abortController?.abort();
@@ -655,7 +695,6 @@ class StreamManager {
655
695
  this.state.sessions.delete(id);
656
696
  // 意図: ハンドラと streamId も破棄してリークを防止
657
697
  this.latestOnFinish.delete(id);
658
- this.latestOnMetadata.delete(id);
659
698
  this.latestOnError.delete(id);
660
699
  this.streamIds.delete(id);
661
700
  removed = true;
@@ -701,10 +740,8 @@ class StreamManager {
701
740
  this.listeners.clear();
702
741
  this.activeSubscribers.clear();
703
742
  this.latestOnFinish.clear();
704
- this.latestOnMetadata.clear();
705
743
  this.latestOnError.clear();
706
744
  this.streamIds.clear();
707
- this.pendingCloneSessions.clear();
708
745
  }
709
746
  }
710
747
  // Export the StreamManager class for custom initialization
@@ -3,6 +3,11 @@ export type ChatStatus = "ready" | "submitted" | "streaming" | "error" | "aborte
3
3
  export type ChatSession = {
4
4
  id: string;
5
5
  messages: Message[];
6
+ /**
7
+ * Accumulated metadata payloads received during the current stream.
8
+ * Newer items append to the end.
9
+ */
10
+ metadata: Array<Record<string, JSONValue>>;
6
11
  /**
7
12
  * messages の内容が更新されるたびにインクリメントされるバージョン番号。
8
13
  * - ストリーミング中は配列参照を再生成しない最適化を行っているため、
@@ -15,19 +20,11 @@ export type ChatSession = {
15
20
  abortController: AbortController | null;
16
21
  lastAccessed: number;
17
22
  };
18
- export type OnChatMetadataCallback<T extends Record<string, JSONValue> = Record<string, JSONValue>> = (metadata: T) => void;
19
23
  export type OnChatErrorCallback = (error: Error) => void;
20
24
  export type OnChatFinishCallback = (messages: Message[]) => void;
21
25
  export type UseChatSessionOptions = {
22
- api?: string;
26
+ api: string;
23
27
  initialMessages?: Message[];
24
- /**
25
- * メタデータ受信時のコールバック
26
- * memo化された関数を渡すこと
27
- * @param metadata 受信したメタデータ
28
- * @returns
29
- */
30
- onMetadata?: OnChatMetadataCallback;
31
28
  /**
32
29
  * エラー発生時のコールバック
33
30
  * memo化された関数を渡すこと
@@ -49,6 +46,10 @@ export type ReloadMessage = (params?: {
49
46
  }) => Promise<void>;
50
47
  export type UseChatSessionReturn = {
51
48
  messages: Message[];
49
+ /**
50
+ * Latest list of metadata payloads received during streaming.
51
+ */
52
+ metadata: Array<Record<string, JSONValue>>;
52
53
  /**
53
54
  * messages の内容更新を検知するためのバージョン番号。
54
55
  * useMemo の依存などに利用できる。
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE7D,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,WAAW,GACX,WAAW,GACX,OAAO,GACP,SAAS,CAAC;AAEd,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAChC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAC7D,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;AAE1B,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAEzD,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAEjE,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAE3D,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,EAAE;IACpC,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpB,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,CACX,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,kBAAkB,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE7D,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,WAAW,GACX,WAAW,GACX,OAAO,GACP,SAAS,CAAC;AAEd,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;OAGG;IACH,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3C;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAEzD,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAEjE,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAE3D,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,EAAE;IACpC,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpB,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;OAEG;IACH,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3C;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,CACX,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,kBAAkB,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC,CAAC"}
@@ -1,3 +1,3 @@
1
1
  import type { UseChatSessionOptions, UseChatSessionReturn } from "./types.js";
2
- export declare function useChatSession(sessionId: string, options?: UseChatSessionOptions): UseChatSessionReturn;
2
+ export declare function useChatSession(sessionId: string, options: UseChatSessionOptions): UseChatSessionReturn;
3
3
  //# sourceMappingURL=useChatSession.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useChatSession.d.ts","sourceRoot":"","sources":["../../src/client/useChatSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAsB,MAAM,YAAY,CAAC;AAKlG,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CA+ItB"}
1
+ {"version":3,"file":"useChatSession.d.ts","sourceRoot":"","sources":["../../src/client/useChatSession.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAKpB,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CAsItB"}
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useSyncExternalStore, useCallback, useRef } from "react";
2
2
  import { useAIStreamManager } from "./context.js";
3
3
  import { generateId } from "../shared/generate-id.js";
4
- export function useChatSession(sessionId, options = {}) {
5
- const { api = "/api/chat", initialMessages = [], onMetadata, onError, onFinish, } = options;
4
+ export function useChatSession(sessionId, options) {
5
+ const { api, initialMessages = [], onError, onFinish } = options;
6
6
  const manager = useAIStreamManager();
7
7
  // セッションIDの変更検知用。初期化時に正しい initialMessages を渡すため、
8
8
  // レンダリング中に参照を更新する(副作用ではないので安全)。
@@ -43,10 +43,6 @@ export function useChatSession(sessionId, options = {}) {
43
43
  // onFinish が undefined の場合は登録解除される
44
44
  manager.setOnFinishHandler(sessionId, onFinish);
45
45
  }, [manager, sessionId, onFinish]);
46
- useEffect(() => {
47
- // onMetadata が undefined の場合は登録解除される
48
- manager.setOnMetadataHandler(sessionId, onMetadata);
49
- }, [manager, sessionId, onMetadata]);
50
46
  useEffect(() => {
51
47
  // onError が undefined の場合は登録解除される
52
48
  manager.setOnErrorHandler(sessionId, onError);
@@ -54,6 +50,7 @@ export function useChatSession(sessionId, options = {}) {
54
50
  const session = state.sessions.get(sessionId) || {
55
51
  id: sessionId,
56
52
  messages: initialMessages,
53
+ metadata: [],
57
54
  messagesVersion: 0,
58
55
  status: "ready",
59
56
  error: null,
@@ -97,6 +94,7 @@ export function useChatSession(sessionId, options = {}) {
97
94
  }, [sessionId, manager]);
98
95
  return {
99
96
  messages: session.messages,
97
+ metadata: session.metadata ?? [],
100
98
  messagesVersion: session.messagesVersion ?? 0,
101
99
  status: session.status,
102
100
  error: session.error,
@@ -1 +1 @@
1
- {"version":3,"file":"createChatHandler.d.ts","sourceRoot":"","sources":["../../src/server/createChatHandler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EAGb,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAKpB,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,iBAAiB,CA2T3E"}
1
+ {"version":3,"file":"createChatHandler.d.ts","sourceRoot":"","sources":["../../src/server/createChatHandler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EAGb,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAKpB,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,iBAAiB,CAyb3E"}
@@ -1,11 +1,33 @@
1
1
  import { isAbortError } from "../shared/error-utils.js";
2
2
  // Implemented for usage inside Hono's `streamText()` callback.
3
3
  export function createChatHandler(options) {
4
- const { providers, transforms = [], throttle, converter, onFinish, onError, messages, timeoutMs = 60000, temperature, maxOutputTokens, } = options;
4
+ const { providers, transforms = [], throttle, onFinish, onError, messages, timeoutMs = 60000, temperature, maxOutputTokens, debug = false, } = options;
5
5
  // Convert single provider config to array for consistent handling
6
6
  const providerConfigs = Array.isArray(providers) ? providers : [providers];
7
- // Store per-request metadata sender
8
- let currentSendMetadata = null;
7
+ // Shared metadata queue for this handler instance.
8
+ // - External sendMetadata() calls enqueue here even before handler starts
9
+ // - During handler execution, queued metadata are bundled into next chunk
10
+ // or flushed immediately as a metadata-only chunk
11
+ const metaQueue = [];
12
+ // Flusher the handler sets when stream is ready; external callers invoke it to flush immediately
13
+ let currentFlushMeta = null;
14
+ // Track previous throttle value for debug logging
15
+ let previousThrottle = null;
16
+ const enqueueMetadata = (meta) => {
17
+ metaQueue.push(meta);
18
+ if (debug) {
19
+ console.log("[cch] Enqueued metadata:", meta, `(queue size: ${metaQueue.length})`);
20
+ }
21
+ // If stream is active, flush immediately as metadata-only chunk
22
+ if (currentFlushMeta) {
23
+ try {
24
+ currentFlushMeta();
25
+ }
26
+ catch {
27
+ // ignore flush error; stream may be closing
28
+ }
29
+ }
30
+ };
9
31
  const handler = async (req, stream) => {
10
32
  // OpenAI SDK のストリーム・タイムアウト・中断制御に関する一時状態
11
33
  let chatStream;
@@ -14,10 +36,12 @@ export function createChatHandler(options) {
14
36
  let abortNotified = false; // onError('aborted') の多重呼び出し防止
15
37
  let currentModel = "";
16
38
  let currentProvider = "openai";
17
- // Content send control for throttling (applied to both chunk and fullContent)
39
+ // Content send control for throttling
18
40
  let lastContentSendTime = 0;
19
41
  // Tracks the accText length at last send. Used to compute delta size when sending chunks.
20
42
  let lastContentSendLength = 0;
43
+ // Track total characters streamed across all provider attempts
44
+ let totalStreamedChars = 0;
21
45
  const sendEvent = async (event, data) => {
22
46
  // 接続が既に中断されている場合は送信しない
23
47
  if (isAborted)
@@ -58,7 +82,10 @@ export function createChatHandler(options) {
58
82
  abortNotified = true;
59
83
  try {
60
84
  // ユーザー中断/接続断をサーバ利用側に通知
61
- onError(new Error("Aborted"), "aborted");
85
+ onError(new Error("Aborted"), {
86
+ reason: "aborted",
87
+ streamedChars: totalStreamedChars,
88
+ });
62
89
  }
63
90
  catch {
64
91
  // ignore handler error
@@ -70,7 +97,9 @@ export function createChatHandler(options) {
70
97
  try {
71
98
  // Also listen to the Request's abort signal defensively
72
99
  if (req?.signal) {
73
- req.signal.addEventListener("abort", handleAbort, { once: true });
100
+ req.signal.addEventListener("abort", handleAbort, {
101
+ once: true,
102
+ });
74
103
  }
75
104
  }
76
105
  catch {
@@ -78,13 +107,33 @@ export function createChatHandler(options) {
78
107
  }
79
108
  try {
80
109
  const startTime = Date.now();
81
- let accText = "";
82
- let usage;
83
- const sendMetadata = (meta) => {
84
- void sendEvent("metadata", meta);
110
+ // Helper to drain queued metadata
111
+ const drainMeta = () => {
112
+ if (metaQueue.length === 0)
113
+ return [];
114
+ const out = metaQueue.splice(0, metaQueue.length);
115
+ return out;
116
+ };
117
+ // Flush metadata-only chunk immediately (text: "")
118
+ const flushMetaOnly = async () => {
119
+ if (isAborted)
120
+ return;
121
+ const metas = drainMeta();
122
+ if (metas.length === 0)
123
+ return;
124
+ if (debug) {
125
+ console.log(`[cch] Flushing ${metas.length} metadata items as meta-only chunk:`, metas);
126
+ }
127
+ await sendEvent("chunk", {
128
+ text: "",
129
+ meta: metas,
130
+ });
131
+ };
132
+ // Expose flusher so external sendMetadata() can flush immediately when possible
133
+ currentFlushMeta = () => {
134
+ // Fire and forget; do not await to avoid blocking caller
135
+ void flushMetaOnly();
85
136
  };
86
- // Set the current sendMetadata for this request
87
- currentSendMetadata = sendMetadata;
88
137
  if (timeoutMs) {
89
138
  // サーバ側で全体の処理時間を制限する。クライアント中断済みであれば何もしない
90
139
  timeoutId = setTimeout(async () => {
@@ -103,19 +152,32 @@ export function createChatHandler(options) {
103
152
  code: "TIMEOUT",
104
153
  });
105
154
  if (onError)
106
- onError(new Error("Request timeout"), "timeout");
155
+ onError(new Error("Request timeout"), {
156
+ reason: "timeout",
157
+ streamedChars: totalStreamedChars,
158
+ });
107
159
  }, timeoutMs);
108
160
  }
109
- // Try each provider config until one succeeds
161
+ // Try each provider end-to-end. If a provider fails before sending
162
+ // any content, fallback to the next. Once content is sent, do not fallback.
163
+ let lastError = null;
110
164
  for (let i = 0; i < providerConfigs.length; i++) {
111
165
  const config = providerConfigs[i];
166
+ if (debug) {
167
+ console.log(`[cch] Trying provider ${i + 1}/${providerConfigs.length}: ${config.provider}/${config.model}`);
168
+ }
169
+ // Local attempt state
170
+ let localAccText = "";
171
+ let localUsage;
172
+ lastContentSendTime = 0;
173
+ lastContentSendLength = 0;
112
174
  // geminiでは"none"を指定することでreasoning_effortを無効化できる
113
175
  let reasoningEffort = config.reasoningEffort ?? "none";
114
176
  if (config.provider === "openai" && reasoningEffort === "none") {
115
177
  reasoningEffort = undefined;
116
178
  }
117
179
  try {
118
- // OpenAI provider
180
+ // Establish stream for this provider
119
181
  chatStream = config.openai.chat.completions.stream({
120
182
  model: config.model,
121
183
  messages,
@@ -127,129 +189,157 @@ export function createChatHandler(options) {
127
189
  });
128
190
  currentModel = config.model;
129
191
  currentProvider = config.provider;
130
- break; // Success, exit retry loop
131
- }
132
- catch (error) {
133
- // Continue to next provider if available
134
- if (i === providerConfigs.length - 1) {
135
- // This was the last provider, rethrow the error
136
- throw error;
192
+ if (debug) {
193
+ console.log(`[cch] Successfully connected to ${config.provider}/${config.model}`);
137
194
  }
138
- }
139
- }
140
- // Ensure chatStream was successfully created
141
- if (!chatStream) {
142
- throw new Error("Failed to create chat stream with any provider");
143
- }
144
- // Stream abort is handled by onAbort above
145
- for await (const chunk of chatStream) {
146
- if (isAborted)
147
- break;
148
- const delta = chunk.choices[0]?.delta;
149
- const finishReason = chunk.choices[0]?.finish_reason;
150
- if (chunk.usage) {
151
- usage = {
152
- prompt_tokens: chunk.usage.prompt_tokens,
153
- completion_tokens: chunk.usage.completion_tokens,
154
- total_tokens: chunk.usage.total_tokens,
155
- reasoning_tokens: chunk.usage.completion_tokens_details?.reasoning_tokens,
156
- };
157
- }
158
- if (delta?.content) {
159
- let text = delta.content;
160
- const ctx = {
161
- accText,
162
- sendMetadata,
163
- };
164
- for (const transform of transforms) {
165
- text = await transform(text, ctx);
195
+ // If there is pending metadata, flush once before first content
196
+ if (debug && metaQueue.length > 0) {
197
+ console.log(`[cch] Found ${metaQueue.length} pre-queued metadata items, flushing...`);
166
198
  }
167
- if (text) {
168
- accText += text;
169
- // Throttle sending for both modes (converter vs chunk)
170
- const now = Date.now();
171
- const timeDiff = now - lastContentSendTime;
172
- const accTextLength = accText.length;
173
- const charDiff = accTextLength - lastContentSendLength;
174
- const throttleSettings = throttle
175
- ? typeof throttle === "function"
176
- ? throttle(accTextLength)
177
- : throttle
178
- : { timeMs: 0, chars: 0 };
179
- const shouldSendContent = lastContentSendTime === 0 || // First content is sent immediately
180
- timeDiff >= throttleSettings.timeMs ||
181
- charDiff >= throttleSettings.chars;
182
- if (shouldSendContent) {
183
- if (converter) {
184
- const fullContent = await converter(accText, ctx);
185
- await sendEvent("fullContent", {
186
- type: "replace",
187
- content: fullContent,
188
- });
199
+ await flushMetaOnly();
200
+ // Consume the stream
201
+ for await (const chunk of chatStream) {
202
+ if (isAborted)
203
+ break;
204
+ const delta = chunk.choices[0]?.delta;
205
+ const finishReason = chunk.choices[0]?.finish_reason;
206
+ if (chunk.usage) {
207
+ localUsage = {
208
+ prompt_tokens: chunk.usage.prompt_tokens,
209
+ completion_tokens: chunk.usage.completion_tokens,
210
+ total_tokens: chunk.usage.total_tokens,
211
+ reasoning_tokens: chunk.usage.completion_tokens_details?.reasoning_tokens,
212
+ };
213
+ }
214
+ if (delta?.content) {
215
+ let text = delta.content;
216
+ const ctx = {
217
+ accText: localAccText,
218
+ // Queue metadata; let sender bundle into next chunk
219
+ sendMetadata: (meta) => enqueueMetadata(meta),
220
+ };
221
+ for (const transform of transforms) {
222
+ text = await transform(text, ctx);
189
223
  }
190
- else {
191
- // Send only the newly added delta since last send
192
- const deltaToSend = accText.slice(lastContentSendLength);
193
- if (deltaToSend) {
194
- await sendEvent("chunk", { text: deltaToSend });
224
+ if (text) {
225
+ localAccText += text;
226
+ // Throttle sending for both modes (converter vs chunk)
227
+ const now = Date.now();
228
+ const timeDiff = now - lastContentSendTime;
229
+ const accTextLength = localAccText.length;
230
+ const charDiff = accTextLength - lastContentSendLength;
231
+ const throttleSettings = throttle
232
+ ? typeof throttle === "function"
233
+ ? throttle(accTextLength)
234
+ : throttle
235
+ : { timeMs: 0, chars: 0 };
236
+ const shouldSendContent = lastContentSendTime === 0 || // First content is sent immediately
237
+ timeDiff >= throttleSettings.timeMs ||
238
+ charDiff >= throttleSettings.chars;
239
+ if (shouldSendContent) {
240
+ // Send only the newly added delta since last send
241
+ const deltaToSend = localAccText.slice(lastContentSendLength);
242
+ if (deltaToSend) {
243
+ const metas = drainMeta();
244
+ if (debug &&
245
+ (!previousThrottle ||
246
+ JSON.stringify(throttleSettings) !==
247
+ JSON.stringify(previousThrottle))) {
248
+ console.log(`[cch] throttle: ${JSON.stringify(throttleSettings)}`);
249
+ previousThrottle = throttleSettings;
250
+ }
251
+ const payload = metas.length
252
+ ? { text: deltaToSend, meta: metas }
253
+ : { text: deltaToSend };
254
+ await sendEvent("chunk", payload);
255
+ }
256
+ lastContentSendTime = now;
257
+ lastContentSendLength = localAccText.length;
258
+ totalStreamedChars = Math.max(totalStreamedChars, lastContentSendLength);
195
259
  }
196
260
  }
197
- lastContentSendTime = now;
198
- lastContentSendLength = accText.length;
261
+ }
262
+ // Check for explicit stream end conditions
263
+ if (finishReason === "stop" || finishReason === "length") {
264
+ // Wait a bit for the final usage chunk if needed
265
+ if (!localUsage && chunk.usage === undefined) {
266
+ // Give it one more iteration to get usage
267
+ continue;
268
+ }
269
+ // If we have usage or this is the second iteration after finish, break
270
+ if (
271
+ // finishReasonの後にusageが来るのでusageを持って終了とする
272
+ localUsage &&
273
+ finishReason) {
274
+ break;
275
+ }
199
276
  }
200
277
  }
201
- }
202
- // Check for explicit stream end conditions
203
- if (finishReason === "stop" || finishReason === "length") {
204
- // Wait a bit for the final usage chunk if needed
205
- if (!usage && chunk.usage === undefined) {
206
- // Give it one more iteration to get usage
207
- continue;
278
+ if (!isAborted) {
279
+ // Send final content if there are unsent updates
280
+ if (localAccText.length > lastContentSendLength) {
281
+ const deltaToSend = localAccText.slice(lastContentSendLength);
282
+ if (deltaToSend) {
283
+ const metas = drainMeta();
284
+ const payload = metas.length
285
+ ? { text: deltaToSend, meta: metas }
286
+ : { text: deltaToSend };
287
+ await sendEvent("chunk", payload);
288
+ totalStreamedChars = Math.max(totalStreamedChars, localAccText.length);
289
+ }
290
+ }
291
+ // If any metadata remain, flush them as a final meta-only chunk
292
+ await flushMetaOnly();
293
+ await sendEvent("done", null);
208
294
  }
209
- // If we have usage or this is the second iteration after finish, break
210
- if (
211
- // finishReasonの後にusageが来るのでusageを持って終了とする
212
- usage &&
213
- finishReason) {
214
- break;
295
+ // 正常終了時もタイマーはクリア(次のattemptへは到達しない)
296
+ clearTimeoutSafe();
297
+ if (onFinish && !isAborted) {
298
+ const finishInfo = {
299
+ model: currentModel,
300
+ provider: currentProvider,
301
+ text: localAccText,
302
+ usage: localUsage,
303
+ durationMs: Date.now() - startTime,
304
+ };
305
+ if (debug) {
306
+ console.log(`[cch] Stream finished:`, {
307
+ model: currentModel,
308
+ provider: currentProvider,
309
+ textLength: localAccText.length,
310
+ usage: localUsage,
311
+ durationMs: finishInfo.durationMs,
312
+ });
313
+ }
314
+ onFinish(finishInfo);
215
315
  }
316
+ // Completed successfully; stop retrying
317
+ return;
216
318
  }
217
- }
218
- if (!isAborted) {
219
- // Send final content if there are unsent updates
220
- if (accText.length > lastContentSendLength) {
221
- const ctx = {
222
- accText,
223
- sendMetadata,
224
- };
225
- if (converter) {
226
- const finalContent = await converter(accText, ctx);
227
- await sendEvent("fullContent", {
228
- type: "replace",
229
- content: finalContent,
230
- });
319
+ catch (error) {
320
+ lastError = error;
321
+ if (debug) {
322
+ console.log(`[cch] Provider ${config.provider}/${config.model} failed during streaming:`, error);
231
323
  }
232
- else {
233
- const deltaToSend = accText.slice(lastContentSendLength);
234
- if (deltaToSend) {
235
- await sendEvent("chunk", { text: deltaToSend });
324
+ // If aborted, do not proceed further
325
+ if (isAbortError(error) || isAborted)
326
+ return;
327
+ // If any content has already been sent from this attempt, do not fallback
328
+ const contentAlreadySent = lastContentSendLength > 0;
329
+ const isLast = i === providerConfigs.length - 1;
330
+ if (!contentAlreadySent && !isLast) {
331
+ if (debug) {
332
+ console.log("[cch] Falling back to next provider...");
236
333
  }
334
+ // try next provider
335
+ continue;
237
336
  }
337
+ // Otherwise rethrow to outer catch
338
+ throw error;
238
339
  }
239
- await sendEvent("done", null);
240
- }
241
- // 正常終了時もタイマーはクリア
242
- clearTimeoutSafe();
243
- if (onFinish && !isAborted) {
244
- const finishInfo = {
245
- model: currentModel,
246
- provider: currentProvider,
247
- text: accText,
248
- usage,
249
- durationMs: Date.now() - startTime,
250
- };
251
- onFinish(finishInfo);
252
340
  }
341
+ // If we get here, all providers failed before sending any content
342
+ throw (lastError ?? new Error("Failed to create chat stream with any provider"));
253
343
  }
254
344
  catch (error) {
255
345
  // 例外経路でもタイマーをクリア
@@ -257,26 +347,26 @@ export function createChatHandler(options) {
257
347
  if (isAbortError(error) || isAborted)
258
348
  return;
259
349
  if (onError)
260
- onError(error, "unknown");
350
+ onError(error, {
351
+ reason: "unknown",
352
+ streamedChars: totalStreamedChars,
353
+ });
261
354
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
262
355
  await sendEvent("error", { message: errorMessage });
263
356
  }
264
357
  finally {
265
- // Clear the current sendMetadata after request completes
266
- currentSendMetadata = null;
358
+ // Disable external flusher after request completes
359
+ currentFlushMeta = null;
360
+ // Clear any remaining metadata for safety
361
+ metaQueue.splice(0, metaQueue.length);
267
362
  }
268
363
  };
269
364
  // Create the sendMetadata function
270
365
  const sendMetadata = (meta) => {
271
- if (currentSendMetadata) {
272
- currentSendMetadata(meta);
273
- }
366
+ enqueueMetadata(meta);
274
367
  };
275
- // Create the result object with both handler and sendMetadata
276
- // Also make it callable for backward compatibility
277
- const result = Object.assign(handler, {
368
+ return {
278
369
  handler,
279
370
  sendMetadata,
280
- });
281
- return result;
371
+ };
282
372
  }
@@ -30,27 +30,31 @@ export type ServerEmitTools = {
30
30
  sendMetadata: (meta: Record<string, JSONValue>) => void;
31
31
  };
32
32
  export type ChatHandler = (req: Request, stream: StreamingApi) => Promise<void>;
33
- export type ChatHandlerResult = ChatHandler & {
33
+ export type ChatHandlerResult = {
34
34
  handler: ChatHandler;
35
35
  sendMetadata: (meta: Record<string, JSONValue>) => void;
36
36
  };
37
37
  export type StreamTransform = (chunk: string, ctx: StreamTransformContext) => string | Promise<string>;
38
- export type ContentConverter = (accumulatedText: string, ctx: StreamTransformContext) => string | Promise<string>;
39
38
  export type ThrottleSettings = {
40
39
  timeMs: number;
41
40
  chars: number;
42
41
  };
43
42
  export type ErrorReason = "unknown" | "timeout" | "aborted";
43
+ export type StreamHandlerOnError = (err: unknown, details: {
44
+ reason: ErrorReason;
45
+ streamedChars: number;
46
+ }) => void;
47
+ export type StreamHandlerOnFinish = (info: FinishInfo) => void;
44
48
  export type ServerOptions = {
45
49
  providers: AIProviderConfig | AIProviderConfig[];
46
50
  transforms?: StreamTransform[];
47
51
  throttle?: ThrottleSettings | ((accTextLength: number) => ThrottleSettings);
48
- converter?: ContentConverter;
49
- onFinish?: (info: FinishInfo) => void;
50
- onError?: (err: unknown, reason: ErrorReason) => void;
52
+ onFinish?: StreamHandlerOnFinish;
53
+ onError?: StreamHandlerOnError;
51
54
  maxOutputTokens?: number;
52
55
  timeoutMs?: number;
53
56
  temperature?: number;
54
57
  messages: Message[];
58
+ debug?: boolean;
55
59
  };
56
60
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,15 @@
1
1
  import type { JSONValue } from "./types.js";
2
- export type SSEEventType = "chunk" | "fullContent" | "metadata" | "error" | "done";
2
+ export type SSEEventType = "chunk" | "error" | "done";
3
3
  export type SSEChunkData = {
4
4
  text: string;
5
- };
6
- export type SSEFullContentData = {
7
- type: "replace";
8
- content: string;
9
- };
10
- export type SSEMetadataData = {
11
- [k: string]: JSONValue;
5
+ /**
6
+ * Optional metadata bundled with this chunk. When present, represents
7
+ * one or more metadata payloads that occurred since the previous
8
+ * emitted chunk.
9
+ */
10
+ meta?: Array<{
11
+ [k: string]: JSONValue;
12
+ }>;
12
13
  };
13
14
  export type SSEErrorData = {
14
15
  message: string;
@@ -18,12 +19,6 @@ export type SSEDoneData = null;
18
19
  export type SSEEvent = {
19
20
  event: "chunk";
20
21
  data: SSEChunkData;
21
- } | {
22
- event: "fullContent";
23
- data: SSEFullContentData;
24
- } | {
25
- event: "metadata";
26
- data: SSEMetadataData;
27
22
  } | {
28
23
  event: "error";
29
24
  data: SSEErrorData;
@@ -1 +1 @@
1
- {"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/shared/wire.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,YAAY,GACpB,OAAO,GACP,aAAa,GACb,UAAU,GACV,OAAO,GACP,MAAM,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC;AAE/B,MAAM,MAAM,QAAQ,GAChB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAA;CAAE,GAClD;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,eAAe,CAAA;CAAE,GAC5C;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC"}
1
+ {"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/shared/wire.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,YAAY,GACpB,OAAO,GACP,OAAO,GACP,MAAM,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC;AAE/B,MAAM,MAAM,QAAQ,GAChB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-ai-sdk",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "private": false,
5
5
  "description": "Simple AI SDK for Hono / React19+ / OpenAI",
6
6
  "type": "module",