simple-ai-sdk 1.0.45 → 1.0.47
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/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +24 -0
- package/dist/client/manager.d.ts.map +1 -1
- package/dist/client/manager.js +1 -1
- package/dist/server/createChatHandler.d.ts.map +1 -1
- package/dist/server/createChatHandler.js +12 -1
- package/dist/server/types.d.ts +22 -4
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shared/types.d.ts +6 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/wire.d.ts +3 -2
- package/dist/shared/wire.d.ts.map +1 -1
- package/dist/spec.md +649 -0
- package/package.json +5 -2
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const usage = `Usage:
|
|
6
|
+
simple-ai-sdk spec
|
|
7
|
+
`;
|
|
8
|
+
async function main() {
|
|
9
|
+
const command = process.argv[2];
|
|
10
|
+
if (command !== "spec") {
|
|
11
|
+
process.stdout.write(usage);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const specPath = join(currentDir, "spec.md");
|
|
17
|
+
const spec = await readFile(specPath, "utf8");
|
|
18
|
+
process.stdout.write(spec);
|
|
19
|
+
}
|
|
20
|
+
void main().catch((error) => {
|
|
21
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
22
|
+
process.stderr.write(`Failed to read spec.md: ${message}\n`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,oBAAoB,EAEpB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/client/manager.ts"],"names":[],"mappings":"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"}
|
package/dist/client/manager.js
CHANGED
|
@@ -879,7 +879,7 @@ const isChunkPayload = (v) => isObject(v) &&
|
|
|
879
879
|
typeof v.text === "string") &&
|
|
880
880
|
(typeof v.meta === "undefined" ||
|
|
881
881
|
Array.isArray(v.meta));
|
|
882
|
-
// Note:
|
|
882
|
+
// Note: keep runtime permissive for backward compatibility.
|
|
883
883
|
const isErrorPayload = (v) => isObject(v) &&
|
|
884
884
|
(typeof v.message === "undefined" ||
|
|
885
885
|
typeof v.message === "string");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createChatHandler.d.ts","sourceRoot":"","sources":["../../src/server/createChatHandler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"createChatHandler.d.ts","sourceRoot":"","sources":["../../src/server/createChatHandler.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,aAAa,EAGb,iBAAiB,EAIlB,MAAM,YAAY,CAAC;AAMpB,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,iBAAiB,CAwgB3E"}
|
|
@@ -2,7 +2,7 @@ import { isAbortError } from "../shared/error-utils.js";
|
|
|
2
2
|
import { normalizeFinishReason } from "./server-utils.js";
|
|
3
3
|
// Implemented for usage inside Hono's `streamText()` callback.
|
|
4
4
|
export function createChatHandler(options) {
|
|
5
|
-
const { providers, transforms = [], throttle, onFinish, onError, messages, timeoutMs = 60000, maxOutputTokens, debug = false, userId, } = options;
|
|
5
|
+
const { providers, transforms = [], throttle, onFinish, onFirstToken, onError, messages, timeoutMs = 60000, maxOutputTokens, debug = false, userId, } = options;
|
|
6
6
|
// Convert single provider config to array for consistent handling
|
|
7
7
|
const providerConfigs = Array.isArray(providers) ? providers : [providers];
|
|
8
8
|
// Shared metadata queue for this handler instance.
|
|
@@ -59,6 +59,7 @@ export function createChatHandler(options) {
|
|
|
59
59
|
let currentModel = "";
|
|
60
60
|
let currentProvider = "openai";
|
|
61
61
|
let overallFinishReason = null;
|
|
62
|
+
let firstTokenNotified = false;
|
|
62
63
|
// Content send control for throttling
|
|
63
64
|
let lastContentSendTime = 0;
|
|
64
65
|
// Tracks the accText length at last send. Used to compute delta size when sending chunks.
|
|
@@ -261,6 +262,16 @@ export function createChatHandler(options) {
|
|
|
261
262
|
};
|
|
262
263
|
}
|
|
263
264
|
if (delta?.content) {
|
|
265
|
+
if (!firstTokenNotified && onFirstToken) {
|
|
266
|
+
firstTokenNotified = true;
|
|
267
|
+
const ttftMs = Date.now() - startTime;
|
|
268
|
+
void Promise.resolve(onFirstToken({ ttftMs })).catch((e) => {
|
|
269
|
+
// onFirstToken failure is not fatal; do not block streaming
|
|
270
|
+
if (debug) {
|
|
271
|
+
console.error("[cch] onFirstToken error:", e);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
264
275
|
let text = delta.content;
|
|
265
276
|
const ctx = {
|
|
266
277
|
accText: localAccText,
|
package/dist/server/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenAI } from "openai";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CustomDataValue, Message, SessionMetadata } from "../shared/index.js";
|
|
3
3
|
import type { StreamingApi } from "hono/utils/stream.js";
|
|
4
4
|
export type HonoStreamType = StreamingApi;
|
|
5
5
|
/**
|
|
@@ -38,17 +38,17 @@ export type FinishInfo = {
|
|
|
38
38
|
export type StreamTransformContext = {
|
|
39
39
|
accText: string;
|
|
40
40
|
sendMetadata: (meta: SessionMetadata) => void;
|
|
41
|
-
sendCustomData: (data:
|
|
41
|
+
sendCustomData: (data: CustomDataValue) => void;
|
|
42
42
|
};
|
|
43
43
|
export type ServerEmitTools = {
|
|
44
44
|
sendMetadata: (meta: SessionMetadata) => void;
|
|
45
|
-
sendCustomData: (data:
|
|
45
|
+
sendCustomData: (data: CustomDataValue) => void;
|
|
46
46
|
};
|
|
47
47
|
export type ChatHandler = (req: Request, stream: StreamingApi) => Promise<void>;
|
|
48
48
|
export type ChatHandlerResult = {
|
|
49
49
|
handler: ChatHandler;
|
|
50
50
|
sendMetadata: (meta: SessionMetadata) => void;
|
|
51
|
-
sendCustomData: (data:
|
|
51
|
+
sendCustomData: (data: CustomDataValue) => void;
|
|
52
52
|
};
|
|
53
53
|
export type StreamTransform = (chunk: string, ctx: StreamTransformContext) => string | Promise<string>;
|
|
54
54
|
export type ThrottleSettings = {
|
|
@@ -64,12 +64,30 @@ export type StreamHandlerOnError = (err: unknown, details: {
|
|
|
64
64
|
streamedChars: number;
|
|
65
65
|
}) => string;
|
|
66
66
|
export type StreamHandlerOnFinish = (info: FinishInfo) => void | Promise<void>;
|
|
67
|
+
export type StreamHandlerOnFirstToken = (info: {
|
|
68
|
+
/**
|
|
69
|
+
* リクエスト処理を開始してから、
|
|
70
|
+
* プロバイダーから最初の「空でない content トークン」を受信するまでの経過時間(ミリ秒)
|
|
71
|
+
* - 計測開始点は `createChatHandler` が返す `handler()` の実行開始時点
|
|
72
|
+
* - `handler()` 呼び出し前に行った処理(認証・DB・JSON parse など)は含まれない
|
|
73
|
+
*/
|
|
74
|
+
ttftMs: number;
|
|
75
|
+
}) => void | Promise<void>;
|
|
67
76
|
export type ServerOptions = {
|
|
68
77
|
providers: AIProviderConfig | AIProviderConfig[];
|
|
69
78
|
userId: string;
|
|
70
79
|
transforms?: StreamTransform[];
|
|
71
80
|
throttle?: ThrottleSettings;
|
|
72
81
|
onFinish?: StreamHandlerOnFinish;
|
|
82
|
+
/**
|
|
83
|
+
* Called once when the first non-empty content token is received.
|
|
84
|
+
*
|
|
85
|
+
* Performance note:
|
|
86
|
+
* - This callback is fired at most once per request.
|
|
87
|
+
* - The handler does NOT await this callback to avoid delaying text streaming.
|
|
88
|
+
* - Keep work lightweight; offload heavy tasks (DB writes, etc.) asynchronously.
|
|
89
|
+
*/
|
|
90
|
+
onFirstToken?: StreamHandlerOnFirstToken;
|
|
73
91
|
onError: StreamHandlerOnError;
|
|
74
92
|
maxOutputTokens: undefined | number;
|
|
75
93
|
timeoutMs?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -3,10 +3,14 @@ export type Message = {
|
|
|
3
3
|
id: string;
|
|
4
4
|
role: Role;
|
|
5
5
|
content: string;
|
|
6
|
-
customData?:
|
|
6
|
+
customData?: CustomDataValue;
|
|
7
7
|
};
|
|
8
|
-
export type
|
|
8
|
+
export type JSONPrimitive = null | boolean | number | string;
|
|
9
|
+
export type JSONObject = {
|
|
9
10
|
[k: string]: JSONValue;
|
|
10
11
|
};
|
|
12
|
+
export type JSONArray = JSONValue[];
|
|
13
|
+
export type JSONValue = JSONPrimitive | JSONArray | JSONObject;
|
|
14
|
+
export type CustomDataValue = JSONArray | JSONObject;
|
|
11
15
|
export type SessionMetadata = Record<string, JSONValue>;
|
|
12
16
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEnD,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEnD,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,eAAe,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC7D,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AACpD,MAAM,MAAM,SAAS,GAAG,SAAS,EAAE,CAAC;AAEpC,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC;AAE/D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,UAAU,CAAC;AAErD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC"}
|
package/dist/shared/wire.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JSONValue } from "./types.js";
|
|
1
|
+
import type { CustomDataValue, JSONValue } from "./types.js";
|
|
2
2
|
export type SSEEventType = "chunk" | "error" | "done";
|
|
3
3
|
export type SSEChunkData = {
|
|
4
4
|
text: string;
|
|
@@ -13,8 +13,9 @@ export type SSEChunkData = {
|
|
|
13
13
|
/**
|
|
14
14
|
* Optional custom data to attach to the current assistant message.
|
|
15
15
|
* When sent multiple times, the last value wins (overwrites previous).
|
|
16
|
+
* Primitive values are intentionally excluded at the type level.
|
|
16
17
|
*/
|
|
17
|
-
customData?:
|
|
18
|
+
customData?: CustomDataValue;
|
|
18
19
|
};
|
|
19
20
|
export type SSEErrorData = {
|
|
20
21
|
message: string;
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/shared/wire.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE7D,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;IACzC;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,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/dist/spec.md
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
# simple-ai-sdk — React + TypeScript 向けストリーミング AI SDK(クライアント & サーバ)
|
|
2
|
+
- 個人用の非公開ライブラリなので、後方互換性は保証しなくてOK
|
|
3
|
+
|
|
4
|
+
OpenAI/Gemini(OpenAI 互換エンドポイント)を **OpenAI 公式 SDK** 経由で呼び出し、サーバで受けたストリーミング結果を **SSE (Server‑Sent Events)** に統一して React に中継する最小構成。
|
|
5
|
+
- クライアント: React 19+ 専用フック / 並列ストリーム / ルーティング耐性
|
|
6
|
+
- サーバ: typescript前提。開発時にはHonoを使う
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 目的 / スコープ
|
|
12
|
+
|
|
13
|
+
- OpenAI / Google Gemini を **同一コードパス** でストリーミング。
|
|
14
|
+
- サーバ側で **チャンク毎のテキスト変換(伏字・句読点揃え・文単位フラッシュ等)** が可能。
|
|
15
|
+
- サーバは **onFinish** で使用トークン数や全文を受け取り、ロギングや課金連携に利用可能。
|
|
16
|
+
- クライアントは **`useChatSession()`** フックで messages と状態を一元管理。**複数セッション並列**に扱える。
|
|
17
|
+
- **ページ遷移(SPA 内)ではストリームを止めない**。タブを閉じた/完全リロード時は特別な対応は行わない(下記参照)。
|
|
18
|
+
|
|
19
|
+
> **NOTE**: 「タブを閉じた / リロード」時の継続は **行いません**(仕様)。アプリ要件上必要なら Service Worker / SharedWorker 拡張は別途検討してください。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## パッケージ構成(提案)
|
|
24
|
+
|
|
25
|
+
単一リポジトリ内に **client** / **server** / **shared** を配置します。パッケージ名は例示です。
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
./
|
|
29
|
+
├─ client/ # React 19+ 向けフックとユーティリティ
|
|
30
|
+
│ ├─ useChatSession.ts
|
|
31
|
+
│ ├─ context.tsx # SimpleAIProvider(並列・ルーティング耐性の要)
|
|
32
|
+
│ ├─ manager.ts # 外部ストア(StreamManager)
|
|
33
|
+
│ └─ types.ts
|
|
34
|
+
├─ server/ # フレームワーク非依存ハンドラ
|
|
35
|
+
│ ├─ createChatHandler.ts
|
|
36
|
+
│ └─ types.ts
|
|
37
|
+
├─ shared/
|
|
38
|
+
│ ├─ types.ts # Message 型等の共有
|
|
39
|
+
│ └─ wire.ts # SSE のイベント種別とワイヤプロトコル
|
|
40
|
+
└─ README.md
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- npm 公開時のエントリ例:
|
|
44
|
+
- `"exports": { "./client": "./client/index.js", "./server": "./server/index.js" }`
|
|
45
|
+
- `type: module`(ESM)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## インストール
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# サーバ側
|
|
53
|
+
npm i openai hono # hono は任意(例では使用)
|
|
54
|
+
# クライアント側(React 19+ 前提)
|
|
55
|
+
npm i react
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> TODO: パッケージ名・エクスポートポリシー(monorepo か単一 pkg か)を確定。
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 共有データモデル(`shared/types.ts`)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export type Role = 'user' | 'assistant' | 'system'
|
|
66
|
+
|
|
67
|
+
export type Message = {
|
|
68
|
+
id: string
|
|
69
|
+
role: Role
|
|
70
|
+
content: string
|
|
71
|
+
customData?: CustomDataValue // アシスタントメッセージに紐づくカスタムデータ(非プリミティブのみ)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type JSONPrimitive = null | boolean | number | string
|
|
75
|
+
export type JSONObject = { [k: string]: JSONValue }
|
|
76
|
+
export type JSONArray = JSONValue[]
|
|
77
|
+
export type JSONValue = JSONPrimitive | JSONArray | JSONObject
|
|
78
|
+
export type CustomDataValue = JSONArray | JSONObject
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ワイヤプロトコル(SSE)
|
|
84
|
+
|
|
85
|
+
- **エンドポイント**: `POST /api/chat`(任意)
|
|
86
|
+
- **リクエスト**: `application/json`
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
- **レスポンス**: `text/event-stream`(Honoの`streamText`によって自動的に設定)。`data:` には JSON を入れます。
|
|
90
|
+
- `event: chunk` → `{ text: string, meta?: Array<{ [k: string]: JSONValue }>, customData?: CustomDataValue }`
|
|
91
|
+
- `meta` は、そのチャンク時点までにサーバが送信したメタデータ(0個以上)を配列で同梱
|
|
92
|
+
- `customData` は、現在ストリーミング中の assistant メッセージに紐づけるカスタムデータ(配列/オブジェクトのみ)。複数回送信された場合は最後の値で上書き(last write wins)
|
|
93
|
+
- 非プリミティブ制約は型レベルの厳格化であり、ランタイムでは後方互換のため受信処理を許容する
|
|
94
|
+
- テキスト更新が無い場合でも、`{ text: "", meta: [...], customData: ... }` のような「メタのみチャンク」を送ることがあります
|
|
95
|
+
- `event: error` → `{ message: string, code?: string }`
|
|
96
|
+
- `event: done` → `{ finished_reason: "length" | "stop" | "content_filter" | null }`
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## サーバ API(`server`)
|
|
102
|
+
|
|
103
|
+
### 依存
|
|
104
|
+
- `openai`(公式 SDK)
|
|
105
|
+
- `hono`(streamText APIを使用)
|
|
106
|
+
|
|
107
|
+
### `createChatHandler(opts)`
|
|
108
|
+
Honoの`streamText`と連携するためのハンドラ群を返します。返り値は `handler` / `sendMetadata` / `sendCustomData` を持つオブジェクトです。SSE でクライアントに中継します。
|
|
109
|
+
|
|
110
|
+
メタデータは個別SSEイベントではなく、`chunk` イベントの `meta` 配列に同梱されます。`sendMetadata` はストリーム開始前に呼んでも安全で、開始後に最初のタイミングでメタのみチャンクとしてフラッシュされます。
|
|
111
|
+
`onFirstToken` を指定すると、最初の非空 `delta.content` を受信した時点で 1 回だけ呼び出されます(`onFinish` より前)。ストリーミング遅延を避けるため、ハンドラ側は `onFirstToken` を await しません。
|
|
112
|
+
|
|
113
|
+
**返り値の型**:
|
|
114
|
+
```ts
|
|
115
|
+
type ChatHandlerResult = {
|
|
116
|
+
handler: (req: Request, stream: HonoStreamType) => Promise<void>
|
|
117
|
+
/**
|
|
118
|
+
* メタデータを送信キューに積む。ストリーム中なら次の `chunk.meta` に同梱、
|
|
119
|
+
* 直後に必要ならメタのみチャンクとして即時フラッシュされる。
|
|
120
|
+
*/
|
|
121
|
+
sendMetadata: (meta: SessionMetadata) => void
|
|
122
|
+
/**
|
|
123
|
+
* 現在ストリーミング中の assistant メッセージにカスタムデータを紐づける。
|
|
124
|
+
* 複数回呼び出した場合は最後の値で上書き(last write wins)。
|
|
125
|
+
* ストリーム中なら次の `chunk.customData` として配送される。
|
|
126
|
+
*/
|
|
127
|
+
sendCustomData: (data: CustomDataValue) => void
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import OpenAI from 'openai'
|
|
133
|
+
import { createChatHandler } from 'simple-ai-sdk/server'
|
|
134
|
+
|
|
135
|
+
// 単一プロバイダー設定
|
|
136
|
+
const { handler } = createChatHandler({
|
|
137
|
+
providers: {
|
|
138
|
+
openai: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
|
|
139
|
+
provider: 'openai',
|
|
140
|
+
model: 'gpt-4.1-nano',
|
|
141
|
+
},
|
|
142
|
+
userId: 'user-123',
|
|
143
|
+
transforms: [/* redact(), sentenceAggregator() など */],
|
|
144
|
+
throttle: { ...}, // throttleの設定
|
|
145
|
+
onFinish(info) {
|
|
146
|
+
console.log('usage:', info.usage)
|
|
147
|
+
},
|
|
148
|
+
onError(err, details) {
|
|
149
|
+
console.error(details.reason, err)
|
|
150
|
+
return "An error occurred" // クライアントに送信されるメッセージ
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// フォールバック設定(複数プロバイダー)
|
|
155
|
+
const handlerWithFallback = createChatHandler({
|
|
156
|
+
providers: [
|
|
157
|
+
{
|
|
158
|
+
openai: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
|
|
159
|
+
provider: 'openai',
|
|
160
|
+
model: 'gpt-4',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
openai: new OpenAI({ apiKey: process.env.OPENAI_API_KEY_BACKUP }),
|
|
164
|
+
provider: 'openai',
|
|
165
|
+
model: 'gpt-3.5-turbo',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
openai: new OpenAI({
|
|
169
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
170
|
+
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
171
|
+
}),
|
|
172
|
+
provider: 'gemini',
|
|
173
|
+
model: 'gemini-2.0-flash',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
userId: 'user-123',
|
|
177
|
+
onFinish(info) {
|
|
178
|
+
console.log(`Completed with ${info.provider}/${info.model}`, info.usage)
|
|
179
|
+
},
|
|
180
|
+
onError(err, details) {
|
|
181
|
+
console.error('All providers failed:', details.reason, err)
|
|
182
|
+
return "Service temporarily unavailable" // クライアントに送信されるメッセージ
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// 例1: handler を使う最小構成
|
|
187
|
+
app.post('/api/chat', (c) => streamText(c, async (stream) => {
|
|
188
|
+
await handler(c.req.raw, stream)
|
|
189
|
+
}))
|
|
190
|
+
|
|
191
|
+
// 例2: 新しい使い方(分割代入で sendMetadata を取得)
|
|
192
|
+
app.post('/api/chat', (c) => streamText(c, async (stream) => {
|
|
193
|
+
const { handler, sendMetadata } = createChatHandler({ /* options */ })
|
|
194
|
+
|
|
195
|
+
// リクエスト開始時にメタデータを投入(ストリーム開始時に flush される)
|
|
196
|
+
sendMetadata({ phase: 'start', timestamp: Date.now() })
|
|
197
|
+
|
|
198
|
+
const promise = handler(c.req.raw, stream)
|
|
199
|
+
|
|
200
|
+
// 任意のタイミングでメタデータを送信(次の chunk.meta かメタのみチャンクで配送)
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
sendMetadata({ phase: 'processing', progress: 0.5 })
|
|
203
|
+
}, 1000)
|
|
204
|
+
|
|
205
|
+
await promise
|
|
206
|
+
}))
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Gemini(OpenAI 互換)設定例
|
|
210
|
+
|
|
211
|
+
Google Gemini は OpenAI 公式 SDK をそのまま使い、`baseURL` に OpenAI 互換エンドポイントを指定して利用できます。
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import OpenAI from 'openai'
|
|
215
|
+
import { createChatHandler } from 'simple-ai-sdk/server'
|
|
216
|
+
|
|
217
|
+
const handler = createChatHandler({
|
|
218
|
+
providers: {
|
|
219
|
+
openai: new OpenAI({
|
|
220
|
+
apiKey: process.env.GEMINI_API_KEY, // 例: 環境変数に設定
|
|
221
|
+
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
222
|
+
}),
|
|
223
|
+
provider: 'gemini',
|
|
224
|
+
// 例: Gemini のモデル ID を指定(実環境に合わせて変更)
|
|
225
|
+
model: 'gemini-1.5-pro',
|
|
226
|
+
},
|
|
227
|
+
userId: 'user-123',
|
|
228
|
+
onError(err, details) {
|
|
229
|
+
console.error(err, details)
|
|
230
|
+
return "An error occurred"
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
最小サンプル(OpenAI SDK インスタンスのみ)
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import OpenAI from 'openai'
|
|
239
|
+
|
|
240
|
+
const openai = new OpenAI({
|
|
241
|
+
apiKey: 'GEMINI_API_KEY',
|
|
242
|
+
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### `ProviderConfig`
|
|
247
|
+
|
|
248
|
+
プロバイダー設定をグループ化した型です。フォールバック時に一緒に切り替わります。
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
export type ProviderConfig = {
|
|
252
|
+
openai: OpenAI // OpenAI SDK インスタンス(必須)
|
|
253
|
+
provider: Provider // 'openai' | 'gemini'
|
|
254
|
+
model: string // 使用するモデル名
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### `ServerOptions`
|
|
259
|
+
|
|
260
|
+
| オプション | 型 | 必須 | 説明 |
|
|
261
|
+
|---|---|---|---|
|
|
262
|
+
| `providers` | `ProviderConfig \| ProviderConfig[]` | ✅ | プロバイダー設定。単一または配列で指定。配列の場合、エラー時に順番にフォールバック試行。 |
|
|
263
|
+
| `userId` | `string` | ✅ | 不正検知用ユーザーID。`provider: 'openai'` のとき `safety_identifier` として送信。 |
|
|
264
|
+
| `transforms` | `Transform[]` | 任意 | チャンク毎のテキスト変換。複数可、上から順に適用。 |
|
|
265
|
+
| `throttle` | `ThrottleSettings` | 任意 | スロットリング設定。最初のチャンクは常に即送信。 |
|
|
266
|
+
| `onFinish` | `(info: FinishInfo) => void \| Promise<void>` | 任意 | ストリーム完了時。使用トークンや全文、処理時間を受け取る。`done` イベント送信前に `await` されるため、非同期処理(DB保存、`sendCustomData` 呼び出し等)を行える。失敗しても `done` は送信される。 |
|
|
267
|
+
| `onFirstToken` | `(info: { ttftMs: number }) => void \| Promise<void>` | 任意 | 最初の非空トークン受信時に 1 回だけ呼ばれる。`ttftMs` は「リクエスト開始から最初の非空 `delta.content` 受信まで」の時間(ms)。ハンドラは await しないため、重い処理は非同期にオフロードすること。 |
|
|
268
|
+
| `onError` | `(err: unknown, details: { reason: "unknown" \| "timeout" \| "aborted", streamedChars: number }) => string` | ✅ | エラー時。返り値の文字列がクライアントへのエラーメッセージとして送信される。サーバー内部エラーを隠蔽し、安全なメッセージのみクライアントに返すために使用。 |
|
|
269
|
+
| `timeoutMs` | `number` | 任意 | タイムアウト秒数。超過時にストリームを中断。 |
|
|
270
|
+
|
|
271
|
+
#### `FinishInfo`
|
|
272
|
+
```ts
|
|
273
|
+
export type FinishInfo = {
|
|
274
|
+
model: string
|
|
275
|
+
provider: 'openai' | 'gemini'
|
|
276
|
+
text: string
|
|
277
|
+
usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number; [k: string]: JSONValue }
|
|
278
|
+
durationMs: number
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
> 使用トークンは、SDK のストリーム最終チャンクで `usage` が提供される場合のみ取得できます。互換レイヤや一部モデルでは `undefined` の可能性があります。**TODO:** Gemini での `usage` 提供差分を要確認。
|
|
283
|
+
|
|
284
|
+
#### Transform API
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
export type StreamTransformContext = {
|
|
288
|
+
accText: string // これまでクライアントへ流した合計テキスト
|
|
289
|
+
sendMetadata: (meta: SessionMetadata) => void // 次の chunk.meta に同梱(必要ならメタのみ即時フラッシュ)
|
|
290
|
+
sendCustomData: (data: CustomDataValue) => void // 現在の assistant メッセージにカスタムデータを紐づける(配列/オブジェクトのみ)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export type StreamTransform = (chunk: string, ctx: StreamTransformContext) => string | Promise<string>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- 各チャンクの `delta.content` に対して順次適用され、戻り値がクライアントに送られます。
|
|
297
|
+
- **文アグリゲータ**のようにチャンクを内部バッファに貯めたい場合、戻り値を `''` にして、最終フラッシュをハンドラ側が終了直前に呼び出す実装とします。
|
|
298
|
+
|
|
299
|
+
##### サンプル
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
// 伏字
|
|
303
|
+
export const redact: Transform = (s) => s.replace(/(NGワード)/g, '***')
|
|
304
|
+
|
|
305
|
+
// 文単位で出力
|
|
306
|
+
export const sentenceAggregator = (): Transform => {
|
|
307
|
+
let buf = ''
|
|
308
|
+
return (s, ctx) => {
|
|
309
|
+
buf += s
|
|
310
|
+
const parts = buf.split(/(?<=。)|\n\n/)
|
|
311
|
+
buf = parts.pop() ?? ''
|
|
312
|
+
const out = parts.join('')
|
|
313
|
+
if (out) ctx.sendMetadata({ flushed: out.length }) // 任意のメタ送信(chunk.meta)
|
|
314
|
+
return out
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### sendCustomData の使用例
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
// onFinish 内で LLM の回答を元に追加データを取得し、メッセージに紐づける例
|
|
323
|
+
app.post('/api/chat', (c) => {
|
|
324
|
+
return streamText(c, async (stream) => {
|
|
325
|
+
const { handler, sendCustomData } = createChatHandler({
|
|
326
|
+
providers: { /* ... */ },
|
|
327
|
+
userId: 'user-123',
|
|
328
|
+
onFinish: async (info) => {
|
|
329
|
+
// LLM の回答テキストを元に DB から関連データを取得
|
|
330
|
+
const relatedItems = await db.findRelated(info.text)
|
|
331
|
+
// assistant メッセージにカスタムデータを紐づける
|
|
332
|
+
sendCustomData({ sources: relatedItems, generatedAt: Date.now() })
|
|
333
|
+
},
|
|
334
|
+
onError(err, details) {
|
|
335
|
+
return "エラーが発生しました"
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
await handler(c.req.raw, stream)
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
クライアント側では `message.customData` でアクセス可能:
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
{messages.map((message) => (
|
|
347
|
+
<div key={message.id}>
|
|
348
|
+
<p>{message.content}</p>
|
|
349
|
+
{message.customData && (
|
|
350
|
+
<pre>{JSON.stringify(message.customData, null, 2)}</pre>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
))}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## クライアント API(`client`)
|
|
359
|
+
|
|
360
|
+
### 設計方針
|
|
361
|
+
- **StreamManager(外部ストア)**に実ネットワーク処理を持たせ、React ライフサイクルから分離。
|
|
362
|
+
- React では **Context + `useSyncExternalStore`** で購読。コンポーネントがアンマウントしてもストリームは継続。
|
|
363
|
+
- **フレーム単位バッチ(rAF)**:ストリーミング中の更新は requestAnimationFrame で1フレームにまとめて通知。初回は即時反映、以降はフレーム内でバッチ処理。
|
|
364
|
+
- **購読者0の最適化**:`activeSubscribers` が 0 のセッションに対する「ストリーミング由来の message 更新」では再レンダリング通知を抑制します。`onFinish`/`onError` などのイベント(ステータス変更)は通知されます。
|
|
365
|
+
- **参照の更新ポリシー**:ストリーミング中の各チャンクで `messages` の配列参照を新規にします。これにより、`useMemo([messages])` や `useEffect([messages])` といった参照依存でも確実に再評価されます。
|
|
366
|
+
- さらに、`messages` 内容更新のたびにインクリメントされる `messagesVersion` も返します。重い処理の再計算制御に利用できます(任意)。
|
|
367
|
+
- 0→1 の購読遷移時は、セッションが `streaming/submitted` の場合に一度だけ即時通知します(UX 改善のためのベストエフォート)。
|
|
368
|
+
- **購読のライフサイクル**:`useChatSession` は `useSyncExternalStore` の subscribe 内で `retainSession`(マウント)/ `releaseSession`(アンマウント)を同期的に行い、購読者数と通知抑制の整合を保証します。
|
|
369
|
+
|
|
370
|
+
### `SimpleAIProvider` / `useChatSession(sessionId, options)`
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
// ルートに配置(Next.js App Router なら app/layout.tsx)
|
|
374
|
+
<SimpleAIProvider>{children}</SimpleAIProvider>
|
|
375
|
+
|
|
376
|
+
// オプション
|
|
377
|
+
// - maxSessions: セッション数上限(デフォルト: 5)。超過時は LRU で破棄(ストリーミング中は優先的に除外)
|
|
378
|
+
// - debug: マネージャ内部ログの有効化
|
|
379
|
+
// - ttlMs: 購読者が 0 かつ ready/error のセッションを TTL 経過後に自動掃除(ブラウザ環境のみ定期実行)
|
|
380
|
+
<SimpleAIProvider maxSessions={10} debug ttlMs={60_000}>{children}</SimpleAIProvider>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
// 取得できる値
|
|
385
|
+
export type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error' | 'aborted'
|
|
386
|
+
|
|
387
|
+
type UseChatSessionReturn = {
|
|
388
|
+
messages: Message[]
|
|
389
|
+
/**
|
|
390
|
+
* 受信済みのメタデータ一覧。新しいものが末尾に追加される。
|
|
391
|
+
*/
|
|
392
|
+
metadata: Array<SessionMetadata>
|
|
393
|
+
/**
|
|
394
|
+
* messages の内容更新を検知するためのバージョン番号。
|
|
395
|
+
* useMemo の依存などに利用可能(任意)。
|
|
396
|
+
*/
|
|
397
|
+
messagesVersion: number
|
|
398
|
+
status: ChatStatus
|
|
399
|
+
error: Error | null
|
|
400
|
+
/**
|
|
401
|
+
* API を呼ばずに現在のセッションの messages を即時上書きする。
|
|
402
|
+
* 内部的に messagesVersion がインクリメントされる。
|
|
403
|
+
*/
|
|
404
|
+
setMessages: (messages: Message[], options?: { resetStatus?: boolean }) => void
|
|
405
|
+
sendMessage: (content: string, params?: { body?: Record<string, JSONValue>; dropFromMessageId?: string }) => Promise<void>
|
|
406
|
+
reload: (params?: { body?: Record<string, JSONValue> }) => Promise<void>
|
|
407
|
+
stop: () => void
|
|
408
|
+
stopAll: () => void
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
const chatA = useChatSession('session-A', {
|
|
414
|
+
api: '/api/chat',
|
|
415
|
+
initialMessages: [{ id: 'usr-1', role: 'user', content: 'You are helpful.' }],
|
|
416
|
+
onError(err) { console.error('A err', err) },
|
|
417
|
+
onFinish(messages, details) {
|
|
418
|
+
console.log('A finished', messages.length, details.finishedReason)
|
|
419
|
+
console.log('metadata:', details.metadata)
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
const chatB = useChatSession('session-B', { api: '/api/chat' })
|
|
424
|
+
|
|
425
|
+
// 2 つのセッションを並列実行可能
|
|
426
|
+
await Promise.all([
|
|
427
|
+
chatA.sendMessage('こんにちは!'),
|
|
428
|
+
chatB.sendMessage('Hello!'),
|
|
429
|
+
])
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### ふるまい
|
|
433
|
+
- `sendMessage` は **新規 user メッセージを push** した上で、**全 messages** をサーバへ送ります。`params.dropFromMessageId` を指定すると、そのid以降(そのIDを含む)のメッセージを破棄してから新規メッセージを追加します。
|
|
434
|
+
- `reload` は **最後の user 以降** を削除して再送します(再試行・プロンプト修正に有用)。
|
|
435
|
+
- `stop` は現在のストリームを中断します。
|
|
436
|
+
- `stopAll` はアクティブな全ストリーム(submitted/streaming 状態)を中断します。ページ離脱やグローバルなキャンセルボタンで利用できます。
|
|
437
|
+
- `setMessages` は **API 呼び出しを行わず**、現在のセッションの `messages` を即時上書きします。UI に即時反映され、内部で `messagesVersion` がインクリメントされます。
|
|
438
|
+
- 通常は `status` を変更しません(例: ローカルでの編集や一時的な整形結果の反映に使用)。
|
|
439
|
+
- `options.resetStatus` を指定すると `status/error/finishedReason/nextAssistantMessageId` を初期化します(`submitted/streaming` 中は無視)。
|
|
440
|
+
- **SPA 内のページ遷移ではストリーム継続**。Provider 配下から外れても、再マウント時に最新スナップショットを購読し直します。
|
|
441
|
+
- **セッション数制限**: メモリ使用量を抑えるため、最大セッション数を制限(デフォルト: 5)。上限に達すると LRU 方式で最も古いセッションを自動削除。ストリーミング中のセッションは削除対象から優先的に除外。
|
|
442
|
+
- **メモ化のコツ**: ストリーミング中は各チャンクで `messages` 参照が更新されるため、`useMemo(..., [messages])` で確実に再評価されます。より厳密な制御が必要な場合は `messagesVersion` を依存に加えることもできます。
|
|
443
|
+
- **prefetch の挙動**: マウント前(購読 0)のチャンクは通知を抑制しますが、内部状態は更新され、マウント後の最初のスナップショットで最新内容を取得できます。購読開始直後(0→1)で `streaming/submitted` の場合は即時通知を1回行います。
|
|
444
|
+
|
|
445
|
+
> **タブを閉じた / リロードの扱い**: 特に対策は行いません。ブラウザプロセス終了により接続は切断されます(仕様)。
|
|
446
|
+
|
|
447
|
+
#### onFinish/onError の扱い(クライアント)
|
|
448
|
+
- セッション単位で保持: `useChatSession(..., { onFinish })` のハンドラは、変更のたびにマネージャへ登録し、セッションごとに「最新のみ」を保持します。
|
|
449
|
+
- 完了時は常に最新を呼ぶ: ストリーム完了時は常に最新の `onFinish` が呼ばれます。コンポーネントがアンマウント済み(購読 0)でも呼ばれます。
|
|
450
|
+
- `onFinish` の `details.metadata` は、そのストリーム中に受信したメタデータの最新スナップショットです。
|
|
451
|
+
- 複数購読時の優先: 同一 `sessionId` を複数コンポーネントが使用する場合、最後に登録された `onFinish` が有効です。
|
|
452
|
+
- `onError` は per-call: `sendMessage`/`reload` 呼び出し時に渡されたハンドラが呼ばれます(最新保持は行いません)。
|
|
453
|
+
|
|
454
|
+
### ページ遷移前のセッション事前準備(`usePrefetchChatSession`)
|
|
455
|
+
|
|
456
|
+
ページ遷移前にセッションを事前に初期化し、オプションでストリーミングを開始することで、遷移後の応答時間を短縮できます。
|
|
457
|
+
|
|
458
|
+
#### 基本的な使用方法
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
import { usePrefetchChatSession } from 'simple-ai-sdk/client'
|
|
462
|
+
|
|
463
|
+
function PageA() {
|
|
464
|
+
const prefetch = usePrefetchChatSession()
|
|
465
|
+
|
|
466
|
+
const handleNavigate = () => {
|
|
467
|
+
// セッション初期化とストリーミング開始(完了を待たない)
|
|
468
|
+
prefetch('chat-session-1', {
|
|
469
|
+
api: '/api/chat',
|
|
470
|
+
streamMessageContent: 'Hello AI!' // 即座に送信される
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
// 即座にページBへ遷移
|
|
474
|
+
router.push('/page-b')
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function PageB() {
|
|
479
|
+
// すでにストリーミング中のセッションを取得
|
|
480
|
+
const chat = useChatSession('chat-session-1')
|
|
481
|
+
// chat.messagesにはストリーミング中の内容が含まれる
|
|
482
|
+
// chat.metadataにはこれまで受信したメタデータが含まれる
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### オプション
|
|
487
|
+
|
|
488
|
+
| オプション | 型 | 必須 | 説明 |
|
|
489
|
+
|---|---|---|---|
|
|
490
|
+
| `api` | `string` | ✅ | APIエンドポイント |
|
|
491
|
+
| `streamMessageContent` | `string` | 任意 | 指定すると即座にユーザーメッセージとして送信され、ストリーミングが開始される |
|
|
492
|
+
| `body` | `Record<string, JSONValue>` | 任意 | 追加のリクエストボディ |
|
|
493
|
+
|
|
494
|
+
#### 動作の特徴
|
|
495
|
+
|
|
496
|
+
- **非同期完了を待たない**: ストリーミングはバックグラウンドで開始され、関数は即座に返る
|
|
497
|
+
- **セッション購読者カウントに影響しない**: `retainSession`/`releaseSession`を呼ばないため、既存のライフサイクル管理と干渉しない
|
|
498
|
+
- **既存セッションとの共存**: 同一sessionIdのセッションが既に存在する場合は初期化をスキップ
|
|
499
|
+
- **エラーハンドリング**: バックグラウンドエラーはコンソールに出力され、遷移後のページで適切にハンドリング可能
|
|
500
|
+
|
|
501
|
+
#### 使用シーン
|
|
502
|
+
|
|
503
|
+
- **SPA内でのページ遷移**: ユーザーの操作に先立ってAIレスポンスの準備を開始
|
|
504
|
+
- **ウィザード形式のUI**: 次のステップで必要なAI処理を事前開始
|
|
505
|
+
- **プリロード戦略**: ユーザーが次に実行する可能性の高いアクションを予測して事前準備
|
|
506
|
+
|
|
507
|
+
### ローディング中メッセージ判定(`useLoadingMessageId`)
|
|
508
|
+
|
|
509
|
+
ストリーミング中・送信直後のメッセージだけにアニメーションを付けたい場合のフック。
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
import { useChatSession } from "simple-ai-sdk/client/useChatSession";
|
|
513
|
+
import { useLoadingMessageId } from "simple-ai-sdk/client/utils/useLoadingMessageId";
|
|
514
|
+
|
|
515
|
+
const session = useChatSession("s1", { api: "/api/chat" });
|
|
516
|
+
const loadingId = useLoadingMessageId(session, { holdMs: 750 });
|
|
517
|
+
|
|
518
|
+
// message.id === loadingId のときだけスケルトン/バウンス等を表示する
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- `status === "submitted"` のときは `nextAssistantMessageId` を返す。
|
|
522
|
+
- `status === "streaming"` のときは最新の assistant メッセージ ID を返す。
|
|
523
|
+
- ストリーミングが終わってもデフォルト 750ms (`holdMs` で変更可) は最後の ID を維持し、アニメーションが即座に消えないようにする。
|
|
524
|
+
- `index.ts` からは再エクスポートしない。`simple-ai-sdk/client/utils/useLoadingMessageId` から直接 import する。
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## クイックスタート
|
|
529
|
+
|
|
530
|
+
### サーバ(Hono)
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
// server.ts
|
|
534
|
+
import { Hono } from 'hono'
|
|
535
|
+
import { streamText } from 'hono/streaming'
|
|
536
|
+
import OpenAI from 'openai'
|
|
537
|
+
import { createChatHandler } from 'simple-ai-sdk/server'
|
|
538
|
+
import { redact, sentenceAggregator } from 'simple-ai-sdk/server/transforms'
|
|
539
|
+
import { marked } from 'marked'
|
|
540
|
+
|
|
541
|
+
const app = new Hono()
|
|
542
|
+
|
|
543
|
+
// 通常のテキストストリーミング
|
|
544
|
+
app.post('/api/chat', (c) => {
|
|
545
|
+
return streamText(c, async (stream) => {
|
|
546
|
+
const { handler, sendMetadata } = createChatHandler({
|
|
547
|
+
providers: {
|
|
548
|
+
openai: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
|
|
549
|
+
provider: 'openai',
|
|
550
|
+
model: 'gpt-4o-mini',
|
|
551
|
+
},
|
|
552
|
+
transforms: [redact, sentenceAggregator()],
|
|
553
|
+
onFinish(info) {
|
|
554
|
+
console.log('tokens:', info.usage)
|
|
555
|
+
},
|
|
556
|
+
onError(err, details) {
|
|
557
|
+
console.error(err, details)
|
|
558
|
+
return "An error occurred"
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
// ストリーム開始時にメタデータを送信
|
|
563
|
+
sendMetadata({ phase: 'start', timestamp: Date.now() })
|
|
564
|
+
|
|
565
|
+
await handler(c.req.raw, stream)
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
export default app
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### クライアント(React 19+)
|
|
573
|
+
|
|
574
|
+
```tsx
|
|
575
|
+
import { SimpleAIProvider } from 'simple-ai-sdk/client/context'
|
|
576
|
+
import { useChatSession } from 'simple-ai-sdk/client/useChatSession'
|
|
577
|
+
|
|
578
|
+
export default function App() {
|
|
579
|
+
return (
|
|
580
|
+
<SimpleAIProvider>
|
|
581
|
+
<DualChats />
|
|
582
|
+
</SimpleAIProvider>
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function DualChats() {
|
|
587
|
+
const a = useChatSession('A', { api: '/api/chat' })
|
|
588
|
+
const b = useChatSession('B', { api: '/api/chat' })
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
|
592
|
+
<Panel title="A" s={a} />
|
|
593
|
+
<Panel title="B" s={b} />
|
|
594
|
+
</div>
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function Panel({ title, s }: { title: string; s: ReturnType<typeof useChatSession> }) {
|
|
599
|
+
return (
|
|
600
|
+
<div>
|
|
601
|
+
<h3>{title}</h3>
|
|
602
|
+
<div style={{ minHeight: 120, border: '1px solid #ddd', padding: 8 }}>
|
|
603
|
+
{s.messages.map(m => <p key={m.id}><b>{m.role}:</b> {m.content}</p>)}
|
|
604
|
+
</div>
|
|
605
|
+
<button onClick={() => s.sendMessage('Hello')} disabled={s.status === 'streaming'}>Send</button>
|
|
606
|
+
<button onClick={() => s.stop()}>Stop</button>
|
|
607
|
+
<button onClick={() => s.reload()}>Reload</button>
|
|
608
|
+
<div>Status: {s.status}{s.error && ` (${s.error.message})`}</div>
|
|
609
|
+
</div>
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## セキュリティ & 運用ノート
|
|
617
|
+
|
|
618
|
+
- **API キーはサーバ側のみ**に保持。クライアントへ渡さない。
|
|
619
|
+
- **CORS/キャッシュ**: SSE は中間プロキシで圧縮されると切れることがあるため `Cache-Control: no-cache, no-transform` を推奨。
|
|
620
|
+
- **ログ/課金**: `onFinish` で `usage` と全文を受け取り、監査や課金算定に利用。
|
|
621
|
+
- **GC(掃除)**: セッション購読者が 0 の場合、`done` 後に TTL で破棄する設計が望ましい。
|
|
622
|
+
- **エラーハンドリング**: 各種エラーに対する適切なハンドリングを実装すること。
|
|
623
|
+
- **Timeout**: オプションでタイムアウト秒数を設定し、超過時にストリームを中断する
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## よくある拡張
|
|
628
|
+
|
|
629
|
+
- **プロバイダーフォールバック**: `providers` を配列で指定することで、エラー時に自動的に次のプロバイダーへフォールバック。OpenAI障害時にGeminiへの切り替えなどが可能。
|
|
630
|
+
- **Gemini 互換**: `provider: 'gemini'` で OpenAI SDK の `baseURL` に Google の OpenAI 互換エンドポイントを設定して動作可能。
|
|
631
|
+
- **メタデータ連携**:
|
|
632
|
+
- Transform 内: `StreamTransformContext.sendMetadata()` でメタデータを送信(次の `chunk.meta` に同梱。必要に応じてメタのみチャンクで即時配送)
|
|
633
|
+
- ハンドラ外: `createChatHandler` の返り値から `sendMetadata` を取得して、リクエストの任意タイミングでメタデータ送信(開始時、進捗通知など)。ストリーム開始前に呼んだ場合も、開始直後にフラッシュされる。
|
|
634
|
+
- **複雑な出力**: `chunk` に `type` を導入してツールコールや引用ハイライトなどを段階的に配信。
|
|
635
|
+
|
|
636
|
+
> TODO: 今後`onStart`/`onChunk` といった追加ライフサイクルフックの導入を検討する
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## 既知の制約
|
|
641
|
+
|
|
642
|
+
- `usage` はモデル/ベンダや SDK のバージョンにより **ストリーム内で未提供** の場合があります(`onFinish.usage` が `undefined`)。
|
|
643
|
+
- タブ閉鎖・リロードでの接続維持は **行いません**(仕様)。必要なら別パッケージで実装予定。
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## ライセンス
|
|
648
|
+
|
|
649
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-ai-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.47",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Simple AI SDK for Hono / React19+ / OpenAI",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"simple-ai-sdk": "./dist/cli.js"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
"./client": {
|
|
11
14
|
"types": "./dist/client/index.d.ts",
|
|
@@ -52,7 +55,7 @@
|
|
|
52
55
|
"author": "catnose <hello@catnose.me> (https://catnose.me)",
|
|
53
56
|
"license": "MIT",
|
|
54
57
|
"scripts": {
|
|
55
|
-
"build": "rm -rf dist && tsc",
|
|
58
|
+
"build": "rm -rf dist && tsc && node ./scripts/copy-spec.mjs",
|
|
56
59
|
"dev": "tsc --watch",
|
|
57
60
|
"typecheck": "tsc --noEmit",
|
|
58
61
|
"lint": "eslint .",
|