@modular-prompt/driver 0.8.0 → 0.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modular-prompt/driver",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,6 +13,7 @@
13
13
  "files": [
14
14
  "dist",
15
15
  "scripts",
16
+ "skills",
16
17
  "src/mlx-ml/python"
17
18
  ],
18
19
  "dependencies": {
@@ -23,8 +24,8 @@
23
24
  "google-auth-library": "^9.15.1",
24
25
  "js-yaml": "^4.1.0",
25
26
  "openai": "^5.19.1",
26
- "@modular-prompt/core": "0.1.12",
27
- "@modular-prompt/utils": "0.2.3"
27
+ "@modular-prompt/core": "0.1.13",
28
+ "@modular-prompt/utils": "0.2.4"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@eslint/js": "^9.35.0",
@@ -54,7 +55,8 @@
54
55
  "download-model": "node scripts/download-model.js",
55
56
  "lint": "eslint src/**/*.ts",
56
57
  "typecheck": "tsc --noEmit",
57
- "clean": "rm -rf dist tsconfig.tsbuildinfo",
58
+ "copy-skills": "mkdir -p skills/driver-usage && cp ../../skills/driver-usage/SKILL.md skills/driver-usage/SKILL.md",
59
+ "clean": "rm -rf dist skills tsconfig.tsbuildinfo",
58
60
  "postinstall": "node scripts/setup-mlx.js || true",
59
61
  "setup-mlx": "node scripts/setup-mlx.js"
60
62
  }
@@ -0,0 +1,432 @@
1
+ ---
2
+ name: driver-usage
3
+ description: modular-promptのドライバー(AIDriver)の使い方ガイド。各ドライバーの初期化、Config、query/streamQuery、ツール定義、構造化出力、AIServiceによるモデル選択を参照する。
4
+ ---
5
+
6
+ # ドライバー使い方ガイド
7
+
8
+ ## ドライバーとは
9
+
10
+ `@modular-prompt/driver` は、コンパイル済みプロンプト(CompiledPrompt)をAIモデルに送信し、結果を受け取るための統一インターフェースを提供する。各AIサービスのAPI差異をドライバー層が吸収するため、プロンプト側のコードを変えずにモデルを切り替えられる。
11
+
12
+ ### 基本的な使い方
13
+
14
+ ```typescript
15
+ import { compile } from '@modular-prompt/core';
16
+ import { OpenAIDriver } from '@modular-prompt/driver';
17
+
18
+ const driver = new OpenAIDriver({ model: 'gpt-4o' });
19
+ const compiled = compile(myModule, context);
20
+
21
+ // 通常クエリ
22
+ const result = await driver.query(compiled);
23
+ console.log(result.content);
24
+
25
+ // ストリーミング
26
+ const { stream, result: resultPromise } = await driver.streamQuery(compiled);
27
+ for await (const chunk of stream) {
28
+ process.stdout.write(chunk);
29
+ }
30
+ const finalResult = await resultPromise;
31
+
32
+ await driver.close();
33
+ ```
34
+
35
+ ## AIDriver インターフェース
36
+
37
+ 全ドライバーが実装する共通インターフェース:
38
+
39
+ ```typescript
40
+ interface AIDriver {
41
+ query(prompt: CompiledPrompt, options?: QueryOptions): Promise<QueryResult>;
42
+ streamQuery(prompt: CompiledPrompt, options?: QueryOptions): Promise<StreamResult>;
43
+ close(): Promise<void>;
44
+ }
45
+ ```
46
+
47
+ ### QueryOptions
48
+
49
+ ```typescript
50
+ interface QueryOptions {
51
+ temperature?: number;
52
+ maxTokens?: number;
53
+ topP?: number;
54
+ stream?: boolean;
55
+ tools?: ToolDefinition[];
56
+ toolChoice?: ToolChoice;
57
+ }
58
+ ```
59
+
60
+ ### QueryResult
61
+
62
+ ```typescript
63
+ interface QueryResult {
64
+ content: string; // テキストレスポンス
65
+ structuredOutput?: unknown; // 構造化出力(schema指定時)
66
+ usage?: {
67
+ promptTokens: number;
68
+ completionTokens: number;
69
+ totalTokens: number;
70
+ };
71
+ toolCalls?: ToolCall[]; // ツール呼び出し
72
+ finishReason?: FinishReason; // 'stop' | 'length' | 'error' | 'tool_calls'
73
+ }
74
+ ```
75
+
76
+ ### StreamResult
77
+
78
+ ```typescript
79
+ interface StreamResult {
80
+ stream: AsyncIterable<string>; // テキストチャンクのストリーム
81
+ result: Promise<QueryResult>; // 最終結果(ストリーム完了後に解決)
82
+ }
83
+ ```
84
+
85
+ ## 各ドライバーのConfig
86
+
87
+ ### OpenAIDriver
88
+
89
+ ```typescript
90
+ import { OpenAIDriver } from '@modular-prompt/driver';
91
+
92
+ const driver = new OpenAIDriver({
93
+ apiKey: process.env.OPENAI_API_KEY, // 環境変数で代替可
94
+ model: 'gpt-4o-mini', // デフォルト: 'gpt-4o-mini'
95
+ baseURL: 'https://...', // カスタムエンドポイント(オプション)
96
+ organization: '...', // Organization ID(オプション)
97
+ defaultOptions: {
98
+ temperature: 0.7,
99
+ maxTokens: 2000,
100
+ frequencyPenalty: 0, // OpenAI固有
101
+ presencePenalty: 0, // OpenAI固有
102
+ stop: ['---'], // 停止シーケンス
103
+ responseFormat: { type: 'json_object' },
104
+ seed: 42
105
+ }
106
+ });
107
+ ```
108
+
109
+ ### AnthropicDriver
110
+
111
+ ```typescript
112
+ import { AnthropicDriver } from '@modular-prompt/driver';
113
+
114
+ const driver = new AnthropicDriver({
115
+ apiKey: process.env.ANTHROPIC_API_KEY, // 環境変数で代替可
116
+ model: 'claude-3-5-sonnet-20241022', // デフォルト
117
+ defaultOptions: {
118
+ maxTokens: 4096,
119
+ temperature: 0.7,
120
+ topK: 40, // Anthropic固有
121
+ stopSequences: ['---']
122
+ }
123
+ });
124
+ ```
125
+
126
+ ### VertexAIDriver
127
+
128
+ ```typescript
129
+ import { VertexAIDriver } from '@modular-prompt/driver';
130
+
131
+ const driver = new VertexAIDriver({
132
+ project: 'my-gcp-project', // 環境変数 GOOGLE_CLOUD_PROJECT で代替可
133
+ location: 'us-central1', // デフォルト: 'us-central1'
134
+ model: 'gemini-2.0-flash-001', // デフォルト
135
+ temperature: 0.05,
136
+ defaultOptions: {
137
+ maxTokens: 1000,
138
+ topP: 0.95,
139
+ topK: 40
140
+ }
141
+ });
142
+ ```
143
+
144
+ Google Cloud認証(ADCまたはサービスアカウント)が必要。
145
+
146
+ ### GoogleGenAIDriver
147
+
148
+ ```typescript
149
+ import { GoogleGenAIDriver } from '@modular-prompt/driver';
150
+
151
+ const driver = new GoogleGenAIDriver({
152
+ apiKey: process.env.GOOGLE_GENAI_API_KEY, // 必須
153
+ model: 'gemini-2.0-flash-exp',
154
+ temperature: 0.7,
155
+ defaultOptions: {
156
+ maxTokens: 2048,
157
+ topP: 0.95,
158
+ topK: 40,
159
+ thinkingConfig: { thinkingLevel: 'HIGH' } // GoogleGenAI固有
160
+ }
161
+ });
162
+ ```
163
+
164
+ APIキーのみで利用可能(Google AI Studioから取得)。
165
+
166
+ ### OllamaDriver
167
+
168
+ ```typescript
169
+ import { OllamaDriver } from '@modular-prompt/driver';
170
+
171
+ const driver = new OllamaDriver({
172
+ baseURL: 'http://localhost:11434/v1', // デフォルト
173
+ model: 'llama3.2' // デフォルト
174
+ });
175
+ ```
176
+
177
+ OpenAI互換APIでローカルLLMにアクセス。
178
+
179
+ ### MlxDriver
180
+
181
+ ```typescript
182
+ import { MlxDriver } from '@modular-prompt/driver';
183
+
184
+ const driver = new MlxDriver({
185
+ model: 'mlx-community/Llama-3.2-3B-Instruct-4bit', // 必須
186
+ defaultOptions: {
187
+ temperature: 0.7,
188
+ maxTokens: 500,
189
+ repetitionPenalty: 1.1, // MLX固有
190
+ repetitionContextSize: 20 // MLX固有
191
+ }
192
+ });
193
+
194
+ // 使用後は必ずclose()(Pythonサブプロセス終了)
195
+ await driver.close();
196
+ ```
197
+
198
+ Apple Silicon専用。Python 3.11以上が必要。
199
+
200
+ ### テスト・デバッグ用ドライバー
201
+
202
+ ```typescript
203
+ import { TestDriver, EchoDriver } from '@modular-prompt/driver';
204
+
205
+ // TestDriver: モックレスポンス
206
+ const testDriver = new TestDriver({
207
+ responses: ['応答1', '応答2'], // キューから順に返す
208
+ delay: 100 // レイテンシのシミュレート(ms)
209
+ });
210
+
211
+ // レスポンスプロバイダ関数
212
+ const testDriver2 = new TestDriver({
213
+ responses: (prompt, options) => {
214
+ if (prompt.metadata?.outputSchema) {
215
+ return JSON.stringify({ result: 'ok' });
216
+ }
217
+ return 'テキスト応答';
218
+ }
219
+ });
220
+
221
+ // EchoDriver: フォーマット済みプロンプトをそのまま返す(AI呼び出しなし)
222
+ const echoDriver = new EchoDriver({
223
+ format: 'debug', // 'text' | 'messages' | 'raw' | 'both' | 'debug'
224
+ includeMetadata: true
225
+ });
226
+ ```
227
+
228
+ ## ツール定義(Function Calling)
229
+
230
+ ### ToolDefinition
231
+
232
+ ```typescript
233
+ const tools: ToolDefinition[] = [
234
+ {
235
+ name: 'get_weather',
236
+ description: '指定都市の天気を取得',
237
+ parameters: {
238
+ type: 'object',
239
+ properties: {
240
+ city: { type: 'string', description: '都市名' },
241
+ unit: { type: 'string', enum: ['celsius', 'fahrenheit'] }
242
+ },
243
+ required: ['city']
244
+ }
245
+ }
246
+ ];
247
+ ```
248
+
249
+ ### ToolChoice
250
+
251
+ ```typescript
252
+ type ToolChoice =
253
+ | 'auto' // モデルが自動判断(デフォルト)
254
+ | 'none' // ツール使用禁止
255
+ | 'required' // 必ず1つ以上のツールを使用
256
+ | { name: string }; // 特定ツールを強制
257
+ ```
258
+
259
+ ### ツール呼び出しの処理
260
+
261
+ ```typescript
262
+ const result = await driver.query(compiled, { tools, toolChoice: 'auto' });
263
+
264
+ if (result.toolCalls) {
265
+ for (const call of result.toolCalls) {
266
+ console.log(call.name); // 関数名
267
+ console.log(call.id); // 呼び出しID
268
+ console.log(call.arguments); // 引数オブジェクト
269
+ }
270
+ }
271
+ ```
272
+
273
+ 対応ドライバー: OpenAI、Anthropic、VertexAI、GoogleGenAI
274
+
275
+ ### ツール結果の返し方(会話ループ)
276
+
277
+ ツール呼び出し結果をモデルに返す会話ループは利用者側で実装する。`QueryOptions.messages` にツール結果を含めて再クエリする。
278
+
279
+ ```typescript
280
+ const result1 = await driver.query(compiled, { tools, toolChoice: 'auto' });
281
+
282
+ if (result1.toolCalls) {
283
+ // ツールを実行して結果を収集
284
+ const toolResults = await Promise.all(
285
+ result1.toolCalls.map(async (tc) => {
286
+ const data = await executeFunction(tc.name, tc.arguments);
287
+ return {
288
+ role: 'tool' as const,
289
+ toolCallId: tc.id,
290
+ name: tc.name,
291
+ kind: 'data' as const, // 'text' | 'data' | 'error'
292
+ value: data
293
+ };
294
+ })
295
+ );
296
+
297
+ // ツール結果を含めて再クエリ
298
+ const result2 = await driver.query(compiled, {
299
+ tools,
300
+ messages: [
301
+ { role: 'assistant', content: result1.content, toolCalls: result1.toolCalls },
302
+ ...toolResults
303
+ ]
304
+ });
305
+ }
306
+ ```
307
+
308
+ ### ToolResultKind
309
+
310
+ ツール結果の種類を示すタグ:
311
+ - `'text'` - プレーンテキスト
312
+ - `'data'` - 構造化データ(オブジェクト等)
313
+ - `'error'` - エラー情報
314
+
315
+ ## 構造化出力
316
+
317
+ プロンプトの `schema` セクションに JSONElement を定義すると、ドライバーが自動的に構造化出力を処理する。
318
+
319
+ ```typescript
320
+ const myModule: PromptModule = {
321
+ objective: ['ユーザー情報を抽出する'],
322
+ schema: [{
323
+ type: 'json',
324
+ content: {
325
+ type: 'object',
326
+ properties: {
327
+ name: { type: 'string' },
328
+ age: { type: 'number' }
329
+ },
330
+ required: ['name', 'age']
331
+ }
332
+ }]
333
+ };
334
+
335
+ const result = await driver.query(compile(myModule, ctx));
336
+ const data = result.structuredOutput as { name: string; age: number };
337
+ ```
338
+
339
+ ドライバーごとの実装方式:
340
+ - **ネイティブサポート**: OpenAI(`response_format`)、VertexAI / GoogleGenAI(`responseSchema`)
341
+ - **JSON抽出型**: Anthropic、MLX(プロンプト指示 + レスポンスからJSON抽出)
342
+
343
+ ## AIService(モデル選択)
344
+
345
+ 複数モデルを登録し、能力(capabilities)ベースで最適なモデルを自動選択する。
346
+
347
+ ### 設定
348
+
349
+ ```typescript
350
+ import { AIService } from '@modular-prompt/driver';
351
+
352
+ const service = new AIService({
353
+ models: [
354
+ {
355
+ model: 'gpt-4o',
356
+ provider: 'openai',
357
+ capabilities: ['streaming', 'japanese', 'tools', 'structured'],
358
+ priority: 10,
359
+ cost: { input: 0.01, output: 0.03 }
360
+ },
361
+ {
362
+ model: 'claude-3-5-sonnet-20241022',
363
+ provider: 'anthropic',
364
+ capabilities: ['streaming', 'japanese', 'tools', 'reasoning'],
365
+ priority: 8
366
+ }
367
+ ],
368
+ drivers: {
369
+ openai: { apiKey: process.env.OPENAI_API_KEY },
370
+ anthropic: { apiKey: process.env.ANTHROPIC_API_KEY }
371
+ },
372
+ defaultOptions: {
373
+ temperature: 0.7,
374
+ maxTokens: 2048
375
+ }
376
+ });
377
+ ```
378
+
379
+ ### ModelSpec
380
+
381
+ ```typescript
382
+ interface ModelSpec {
383
+ model: string;
384
+ provider: DriverProvider;
385
+ capabilities: DriverCapability[];
386
+ priority?: number; // 高いほど優先
387
+ disabled?: boolean; // 無効化フラグ
388
+ maxInputTokens?: number;
389
+ maxOutputTokens?: number;
390
+ maxTotalTokens?: number;
391
+ tokensPerMinute?: number; // TPM制限
392
+ requestsPerMinute?: number; // RPM制限
393
+ cost?: { input: number; output: number };
394
+ metadata?: Record<string, unknown>;
395
+ }
396
+ ```
397
+
398
+ ### DriverCapability(能力フラグ)
399
+
400
+ | 能力 | 説明 |
401
+ |------|------|
402
+ | `streaming` | ストリーミング応答 |
403
+ | `local` | ローカル実行 |
404
+ | `fast` | 高速応答 |
405
+ | `large-context` | 大規模コンテキスト |
406
+ | `multilingual` | 多言語対応 |
407
+ | `japanese` | 日本語特化 |
408
+ | `coding` | コーディング特化 |
409
+ | `reasoning` | 推論・思考特化 |
410
+ | `chat` | チャット特化 |
411
+ | `tools` | ツール使用 |
412
+ | `vision` | 画像認識 |
413
+ | `audio` | 音声処理 |
414
+ | `structured` | 構造化出力 |
415
+ | `json` | JSON出力 |
416
+ | `function-calling` | 関数呼び出し |
417
+
418
+ ### モデル選択
419
+
420
+ ```typescript
421
+ // 能力ベースでドライバーを自動作成
422
+ const driver = await service.createDriverFromCapabilities(
423
+ ['japanese', 'streaming'],
424
+ {
425
+ preferLocal: true, // ローカル優先
426
+ preferProvider: 'anthropic', // 特定プロバイダー優先
427
+ excludeProviders: ['openai'],
428
+ preferFast: true, // 高速優先
429
+ lenient: true // 条件緩和モード(条件を後ろから減らして再検索)
430
+ }
431
+ );
432
+ ```
@@ -124,7 +124,15 @@ def get_special_tokens(tokenizer):
124
124
  "scratchpad": ("<|scratchpad|>", "<|/scratchpad|>"),
125
125
  "analysis": ("<|analysis|>", "<|/analysis|>"),
126
126
  "summary": ("<|summary|>", "<|/summary|>"),
127
- "explanation": ("<|explanation|>", "<|/explanation|>")
127
+ "explanation": ("<|explanation|>", "<|/explanation|>"),
128
+
129
+ # tool_call バリエーション(追加)
130
+ "tool_call_explicit": ("<|tool_call_start|>", "<|tool_call_end|>"),
131
+ "tool_call_xml": ("<tool_call>", "</tool_call>"),
132
+ "tool_calls_section": ("<|tool_calls_section_begin|>", "<|tool_calls_section_end|>"),
133
+ "function_call_tags": ("<start_function_call>", "<end_function_call>"),
134
+ "longcat_tool_call": ("<longcat_tool_call>", "</longcat_tool_call>"),
135
+ "minimax_tool_call": ("<minimax:tool_call>", "</minimax:tool_call>"),
128
136
  }
129
137
 
130
138
  # 単体トークン(存在する場合のみ)
@@ -143,7 +151,10 @@ def get_special_tokens(tokenizer):
143
151
  # 一般的なマークダウン風
144
152
  "code_inline": "`",
145
153
  "code_block_start": "```",
146
- "code_block_end": "```"
154
+ "code_block_end": "```",
155
+
156
+ # ツール関連の単体トークン(追加)
157
+ "tool_calls_marker": "[TOOL_CALLS]",
147
158
  }
148
159
 
149
160
  # ペアトークンの処理
@@ -192,6 +203,24 @@ def detect_tool_call_format(tokenizer):
192
203
  if hasattr(tokenizer, 'init_kwargs'):
193
204
  tool_parser_type = tokenizer.init_kwargs.get('tool_parser_type')
194
205
 
206
+ # 既知パーサーからの逆引き(最優先)
207
+ KNOWN_TOOL_PARSERS = {
208
+ "json_tools": {"call_start": "<tool_call>", "call_end": "</tool_call>"},
209
+ "pythonic": {"call_start": "<|tool_call_start|>", "call_end": "<|tool_call_end|>"},
210
+ "function_gemma": {"call_start": "<start_function_call>", "call_end": "<end_function_call>"},
211
+ "mistral": {"call_start": "[TOOL_CALLS]", "call_end": ""},
212
+ "kimi_k2": {"call_start": "<|tool_calls_section_begin|>", "call_end": "<|tool_calls_section_end|>"},
213
+ "longcat": {"call_start": "<longcat_tool_call>", "call_end": "</longcat_tool_call>"},
214
+ "glm47": {"call_start": "<tool_call>", "call_end": "</tool_call>"},
215
+ "qwen3_coder": {"call_start": "<tool_call>", "call_end": "</tool_call>"},
216
+ "minimax_m2": {"call_start": "<minimax:tool_call>", "call_end": "</minimax:tool_call>"},
217
+ }
218
+
219
+ if tool_parser_type and tool_parser_type in KNOWN_TOOL_PARSERS:
220
+ result = {"tool_parser_type": tool_parser_type}
221
+ result.update(KNOWN_TOOL_PARSERS[tool_parser_type])
222
+ return result
223
+
195
224
  # chat_template テキストを取得
196
225
  template = getattr(tokenizer, 'chat_template', None)
197
226
  if not template and hasattr(tokenizer, 'init_kwargs'):
@@ -207,21 +236,36 @@ def detect_tool_call_format(tokenizer):
207
236
 
208
237
  # テンプレートテキストからデリミタを抽出
209
238
  if template:
210
- # tool_call タグの検出(<tool_call>, <|tool_call|> 等)
211
- call_match = re.search(r'(<\|?tool_call\|?>)\s*\\n.*?(<\/?\|?tool_call\|?>|<\|?/tool_call\|?>)', template)
212
- if call_match:
213
- result["call_start"] = call_match.group(1)
214
- result["call_end"] = call_match.group(2)
215
- else:
216
- # フォールバック: tool_call を含む開閉タグペアを探す
217
- tags = re.findall(r'<[|/]?tool_call[|]?>', template)
218
- if len(tags) >= 2:
219
- # 開タグと閉タグを分離
220
- open_tags = [t for t in tags if '/' not in t]
221
- close_tags = [t for t in tags if '/' in t]
222
- if open_tags and close_tags:
223
- result["call_start"] = open_tags[0]
224
- result["call_end"] = close_tags[0]
239
+ # 複数のtool_call関連パターンを順に試行
240
+ tool_call_patterns = [
241
+ # <tool_call>...</tool_call>, <|tool_call|>...<|/tool_call|>
242
+ (r'<\|?tool_call\|?>', r'</?\|?tool_call\|?>|<\|?/tool_call\|?>'),
243
+ # <|tool_call_start|>...<|tool_call_end|>
244
+ (r'<\|tool_call_start\|>', r'<\|tool_call_end\|>'),
245
+ # <start_function_call>...<end_function_call>
246
+ (r'<start_function_call>', r'<end_function_call>'),
247
+ # <|tool_calls_section_begin|>...<|tool_calls_section_end|>
248
+ (r'<\|tool_calls_section_begin\|>', r'<\|tool_calls_section_end\|>'),
249
+ # <longcat_tool_call>...</longcat_tool_call>
250
+ (r'<longcat_tool_call>', r'</longcat_tool_call>'),
251
+ # <minimax:tool_call>...</minimax:tool_call>
252
+ (r'<minimax:tool_call>', r'</minimax:tool_call>'),
253
+ ]
254
+
255
+ for start_pattern, end_pattern in tool_call_patterns:
256
+ start_match = re.search(start_pattern, template)
257
+ end_match = re.search(end_pattern, template)
258
+ if start_match and end_match:
259
+ result["call_start"] = start_match.group(0)
260
+ result["call_end"] = end_match.group(0)
261
+ break
262
+
263
+ # Mistral特殊ケース
264
+ if "call_start" not in result:
265
+ mistral_match = re.search(r'\[TOOL_CALLS\]', template)
266
+ if mistral_match:
267
+ result["call_start"] = "[TOOL_CALLS]"
268
+ result["call_end"] = ""
225
269
 
226
270
  # tool_response タグの検出
227
271
  resp_tags = re.findall(r'<[|/]?tool_response[|]?>', template)
@@ -325,6 +369,20 @@ def get_capabilities(tokenizer):
325
369
  "features": get_tokenizer_features(tokenizer)
326
370
  }
327
371
 
372
+ # tool_call_formatの情報をspecial_tokensに反映(補完)
373
+ features = capabilities.get("features", {})
374
+ chat_template = features.get("chat_template")
375
+ if chat_template:
376
+ tcf = chat_template.get("tool_call_format")
377
+ if tcf and tcf.get("call_start") and "tool_call" not in capabilities["special_tokens"]:
378
+ call_start = tcf["call_start"]
379
+ call_end = tcf.get("call_end", "")
380
+ if call_end: # ペアがある場合のみ
381
+ capabilities["special_tokens"]["tool_call"] = {
382
+ "start": {"text": call_start, "id": -1},
383
+ "end": {"text": call_end, "id": -1}
384
+ }
385
+
328
386
  # チャット制約を検出して追加
329
387
  chat_restrictions = detect_chat_restrictions(tokenizer)
330
388
  if chat_restrictions: