kiku-agent 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +69 -12
  2. package/package.json +2 -1
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/modules/evaluation/runner.js +4 -14
  18. package/server/agent/eval/modules/evaluation/scoring.js +15 -3
  19. package/server/agent/eval/modules/server/evalApi.js +5 -5
  20. package/server/agent/eval/modules/ui/evalUi.js +7 -4
  21. package/server/agent/eventBus.js +1 -0
  22. package/server/agent/ink/main.jsx +196 -191
  23. package/server/agent/jobManager.js +11 -1
  24. package/server/agent/lanDiscovery.js +721 -23
  25. package/server/agent/llmClient.js +317 -71
  26. package/server/agent/lmStudioWakeup.js +517 -65
  27. package/server/agent/locationService.js +797 -0
  28. package/server/agent/mcpQuizData.js +509 -0
  29. package/server/agent/memory/contextAllocator.js +19 -7
  30. package/server/agent/memory.js +3 -1
  31. package/server/agent/modelCompatibility.js +352 -0
  32. package/server/agent/modelDefaults.js +1 -0
  33. package/server/agent/modelManager.js +46 -2
  34. package/server/agent/newsWatch.js +9 -3
  35. package/server/agent/processManager.js +229 -0
  36. package/server/agent/prompts.js +174 -8
  37. package/server/agent/routingInterceptor.js +9 -1
  38. package/server/agent/sanitizer.js +41 -6
  39. package/server/agent/searchQueryPlanner.js +202 -0
  40. package/server/agent/session.js +59 -4
  41. package/server/agent/tests/agentFollowUps.test.js +128 -2
  42. package/server/agent/tests/agentMusicControl.test.js +245 -0
  43. package/server/agent/tests/apiRouter.test.js +1 -1
  44. package/server/agent/tests/autoContext.test.js +176 -0
  45. package/server/agent/tests/bilibiliEmbed.test.js +45 -0
  46. package/server/agent/tests/chatHardening.test.js +121 -0
  47. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  48. package/server/agent/tests/clarifyingQuestions.test.js +33 -0
  49. package/server/agent/tests/cliAutomation.test.js +38 -1
  50. package/server/agent/tests/cloudProviders.test.js +91 -3
  51. package/server/agent/tests/commands.test.js +92 -3
  52. package/server/agent/tests/contextIntelligence.test.js +50 -3
  53. package/server/agent/tests/cpuMetrics.test.js +101 -0
  54. package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
  55. package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
  56. package/server/agent/tests/engineService.test.js +436 -0
  57. package/server/agent/tests/engineUnload.test.js +94 -0
  58. package/server/agent/tests/envCommand.test.js +69 -0
  59. package/server/agent/tests/evalMetrics.test.js +8 -0
  60. package/server/agent/tests/fetchApi.test.js +99 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +2 -0
  62. package/server/agent/tests/lanDiscovery.test.js +5 -0
  63. package/server/agent/tests/lanInterfaces.test.js +510 -0
  64. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  65. package/server/agent/tests/locationTool.test.js +278 -0
  66. package/server/agent/tests/memoryRoute.test.js +173 -0
  67. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  68. package/server/agent/tests/modelCompatibility.test.js +148 -0
  69. package/server/agent/tests/modelManager.test.js +32 -0
  70. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +4 -2
  72. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  73. package/server/agent/tests/openDomainSearch.test.js +121 -0
  74. package/server/agent/tests/processManager.test.js +144 -0
  75. package/server/agent/tests/providerPersistence.test.js +10 -5
  76. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  77. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  78. package/server/agent/tests/securityAuditRemediation.test.js +16 -9
  79. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  80. package/server/agent/tests/securityHardening.test.js +4 -4
  81. package/server/agent/tests/sessionAndCopy.test.js +39 -4
  82. package/server/agent/tests/sidebarControls.test.js +34 -0
  83. package/server/agent/tests/statefulMemory.test.js +8 -1
  84. package/server/agent/tests/textProtocolLoop.test.js +71 -0
  85. package/server/agent/tests/textToolParsing.test.js +48 -0
  86. package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
  87. package/server/agent/tests/unifiedEndpoints.test.js +13 -6
  88. package/server/agent/tests/v110Features.test.js +1 -1
  89. package/server/agent/tests/vramEstimator.test.js +75 -0
  90. package/server/agent/tests/webChatDocs.test.js +199 -0
  91. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  92. package/server/agent/tests/webChatInspection.test.js +256 -5
  93. package/server/agent/tests/webChatMcp.test.js +94 -0
  94. package/server/agent/tests/webExtract.test.js +23 -0
  95. package/server/agent/tests/webSources.test.js +46 -12
  96. package/server/agent/textToolProtocol.js +173 -46
  97. package/server/agent/toolRouter.js +318 -44
  98. package/server/agent/tools.js +877 -96
  99. package/server/agent/ui.js +39 -34
  100. package/server/agent/vramService.js +35 -54
  101. package/server/agent/webSearch.js +441 -162
  102. package/server/agent/websearch/orchestrator.js +15 -5
  103. package/server/agent/websearch/resilience/cache.js +9 -2
  104. package/server/agent/websearch/security/ssrf.js +41 -10
  105. package/server/agent/youtubeAuth.js +5 -20
  106. package/server/apiRouter.js +5558 -4422
  107. package/server/mcp/data/discography.js +2 -2
  108. package/server/mcp/examples/antigravity.example.json +17 -0
  109. package/server/mcp/handler.js +8 -4
  110. package/server/mcp/security.js +220 -21
  111. package/server/mcp/server.js +2 -1
  112. package/server/mcp/tests/connection.test.js +14 -8
  113. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  114. package/server/mcp/tools.js +7 -1
  115. package/server/supabase_migration_prefix.sql +102 -0
  116. package/server/supabase_schema.sql +18 -18
  117. package/server/web-chat/core/connection.js +179 -218
  118. package/server/web-chat/core/conversationContext.js +9 -2
  119. package/server/web-chat/core/directCatalogEngine.js +52 -28
  120. package/server/web-chat/core/dramaKnowledge.js +33 -22
  121. package/server/web-chat/core/followUpEngine.js +735 -81
  122. package/server/web-chat/core/futureEventGuard.js +5 -1
  123. package/server/web-chat/core/musicControlAction.js +70 -0
  124. package/server/web-chat/core/security.js +32 -21
  125. package/server/web-chat/core/toolInspector.js +6 -6
  126. package/server/web-chat/core/urlGuard.js +45 -0
  127. package/server/web-chat/router.js +204 -157
  128. package/server/web-chat/routes/chatRoute.js +885 -488
  129. package/server/web-chat/routes/engineRoute.js +270 -0
  130. package/server/web-chat/routes/exportRoute.js +1 -1
  131. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  132. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  133. package/server/web-chat/routes/mcpRoute.js +386 -0
  134. package/server/web-chat/routes/memoryRoute.js +253 -0
  135. package/server/web-chat/routes/providerRoute.js +49 -16
  136. package/server/web-chat/routes/staticRoute.js +306 -17
  137. package/server/web-chat/routes/statusRoute.js +194 -73
  138. package/server/web-chat/routes/suggestRoute.js +11 -3
  139. package/server/web-chat/routes/verifyRoute.js +26 -4
  140. package/server/web-chat/server.js +7 -6
  141. package/server/web-chat/ui/docsBuilder.js +3558 -0
  142. package/server/web-chat/ui/htmlBuilder.js +12 -0
  143. package/server/web-chat/ui/scripts/client.js +3401 -1302
  144. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  145. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  146. package/server/web-chat/ui/styles/lan-card.css +165 -0
  147. package/server/web-chat/ui/styles/main.css +4289 -447
  148. package/server/web-chat/ui/sw.js +64 -0
  149. package/server/web-chat/ui/templates/body.html +407 -281
  150. package/src/data/awardsData.js +146 -146
  151. package/src/data/musicVideoData.js +6 -4
  152. package/src/lib/vram/estimator.js +195 -19
@@ -1,7 +1,10 @@
1
- import { config, CLOUD_PROVIDERS, normalizeProviderModel, matchModelIdentifier, extractModelQuantization, savePersistedProviderKey, savePersistedActiveProvider, savePersistedProviderModel, getPersistedActiveProvider, getProviderBaseUrl, savePersistedProviderBaseUrl, setActiveProvider } from './config.js';
1
+ import { config, CLOUD_PROVIDERS, normalizeProviderModel, matchModelIdentifier, extractModelQuantization, savePersistedProviderKey, savePersistedActiveProvider, savePersistedProviderModel, getPersistedActiveProvider, getProviderBaseUrl, savePersistedProviderBaseUrl, setActiveProvider, getPersistedInferenceSettings } from './config.js';
2
2
  import { parseApiErrorText } from './errors.js';
3
3
  import { estimateTokens } from './telemetry.js';
4
- import { estimateTokenCount } from './contextCompactor.js';
4
+ import { estimateTokenCount, prepareChatMessagesForDispatch } from './contextCompactor.js';
5
+ import { isIpAddress, isPrivateIPv4, isPrivateIPv6 } from './sanitizer.js';
6
+ import { applyModelRequestOptions, normalizeAssistantMessage, StreamContentNormalizer } from './modelCompatibility.js';
7
+ import { toAnthropicMessages, toAnthropicTools, normalizeAnthropicResponse } from './cloudAdapters.js';
5
8
 
6
9
  function computeRetryDelay(attempt, headers) {
7
10
  const retryAfter = Number(headers?.get?.('retry-after'));
@@ -11,6 +14,22 @@ function computeRetryDelay(attempt, headers) {
11
14
  return Math.min(8000, 1200 * 2 ** attempt) + Math.floor(Math.random() * 400);
12
15
  }
13
16
 
17
+ function isLocalInferenceEndpoint(baseUrl) {
18
+ let hostname = '';
19
+ try {
20
+ hostname = new URL(baseUrl).hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
21
+ } catch {
22
+ return false;
23
+ }
24
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname.endsWith('.local')) return true;
25
+ if (!isIpAddress(hostname)) return false;
26
+ return isPrivateIPv4(hostname) || isPrivateIPv6(hostname);
27
+ }
28
+
29
+ function parseAnthropicArgs(value) {
30
+ try { return JSON.parse(value || '{}'); } catch { return {}; }
31
+ }
32
+
14
33
  export class LLMClient {
15
34
  constructor(options = {}) {
16
35
  const providerId = (options.provider || getPersistedActiveProvider() || config.activeProvider || 'lmstudio').toLowerCase();
@@ -25,6 +44,7 @@ export class LLMClient {
25
44
  this.apiKey = options.apiKey !== undefined
26
45
  ? options.apiKey
27
46
  : (providerDef.apiKey !== undefined ? providerDef.apiKey : (providerDef.isLocal ? (config.lmStudio.apiKey || 'lm-studio') : ''));
47
+ this.modelExplicitlySet = Boolean(options.model);
28
48
  this.model = normalizeProviderModel(
29
49
  providerId,
30
50
  options.model || providerDef.defaultModel || config.lmStudio.model || 'google/gemma-4-e4b'
@@ -73,6 +93,7 @@ export class LLMClient {
73
93
  setModel(model) {
74
94
  const trimmed = String(model || '').trim();
75
95
  if (!trimmed) throw new Error('A model ID is required.');
96
+ this.modelExplicitlySet = true;
76
97
  this.model = normalizeProviderModel(this.provider, trimmed);
77
98
  savePersistedProviderModel(this.provider, this.model);
78
99
  return this.model;
@@ -102,7 +123,9 @@ export class LLMClient {
102
123
  ...this.extraHeaders,
103
124
  };
104
125
 
105
- if (this.apiKey) {
126
+ if (this.provider === 'anthropic') {
127
+ if (this.apiKey) headers['x-api-key'] = this.apiKey;
128
+ } else if (this.apiKey) {
106
129
  headers['Authorization'] = `Bearer ${this.apiKey}`;
107
130
  if (this.provider === 'gemini') {
108
131
  headers['x-goog-api-key'] = this.apiKey;
@@ -111,6 +134,8 @@ export class LLMClient {
111
134
  headers['Authorization'] = 'Bearer lm-studio';
112
135
  } else if (this.provider === 'vllm') {
113
136
  headers['Authorization'] = 'Bearer token-kiku';
137
+ } else if (this.provider === 'ollama') {
138
+ headers['Authorization'] = this.apiKey ? `Bearer ${this.apiKey}` : 'Bearer ollama';
114
139
  }
115
140
 
116
141
  return headers;
@@ -120,7 +145,20 @@ export class LLMClient {
120
145
  const def = CLOUD_PROVIDERS[this.provider] || {};
121
146
  const startTime = Date.now();
122
147
 
123
- if (!def.isLocal && !this.apiKey) {
148
+ if (this.provider === 'custom' && !this.baseUrl) {
149
+ return {
150
+ connected: false,
151
+ provider: this.provider,
152
+ providerName: this.providerName,
153
+ error: `No base URL configured for ${this.providerName}. Set LLM_BASE_URL in .env or apply a discovered server in /settings`,
154
+ models: [],
155
+ latencyMs: 0
156
+ };
157
+ }
158
+
159
+ const keylessLocalEndpoint = this.provider === 'custom' && isLocalInferenceEndpoint(this.baseUrl);
160
+
161
+ if (!def.isLocal && !this.apiKey && !keylessLocalEndpoint) {
124
162
  return {
125
163
  connected: false,
126
164
  provider: this.provider,
@@ -130,6 +168,21 @@ export class LLMClient {
130
168
  latencyMs: 0
131
169
  };
132
170
  }
171
+ if (this.provider === 'anthropic') {
172
+ return {
173
+ connected: true,
174
+ provider: this.provider,
175
+ providerName: this.providerName,
176
+ baseUrl: this.baseUrl,
177
+ models: [this.model],
178
+ loadedModels: [this.model],
179
+ activeModel: this.model,
180
+ activeLoadedModel: this.model,
181
+ isModelAvailable: true,
182
+ isModelLoaded: true,
183
+ latencyMs: 0,
184
+ };
185
+ }
133
186
  try {
134
187
  const controller = new AbortController();
135
188
  const timeoutMs = process.env.NODE_ENV === 'test' || process.execArgv.includes('--test') ? 800 : 8000;
@@ -185,13 +238,38 @@ export class LLMClient {
185
238
  .map(m => m.id || m.name)
186
239
  .filter(Boolean);
187
240
 
188
- const activeLoaded = loadedModels.length > 0 ? loadedModels[0] : null;
241
+ let activeLoaded = loadedModels.length > 0 ? loadedModels[0] : null;
242
+
243
+ if (this.provider === 'lmstudio') {
244
+ try {
245
+ const rootUrl = String(this.baseUrl || '').replace(/\/v1\/?$/, '').replace(/\/+$/, '');
246
+ const v0Res = await fetch(`${rootUrl}/api/v0/models`, {
247
+ method: 'GET',
248
+ headers: this._buildHeaders(),
249
+ signal: controller.signal
250
+ });
251
+ if (v0Res.ok) {
252
+ const v0Data = await v0Res.json();
253
+ const v0List = Array.isArray(v0Data?.data) ? v0Data.data : [];
254
+ const v0Loaded = v0List
255
+ .filter(m => m && typeof m === 'object' && m.state === 'loaded')
256
+ .map(m => m.id)
257
+ .filter(Boolean);
258
+ if (v0Loaded.length > 0) {
259
+ activeLoaded = v0Loaded[0];
260
+ for (const id of v0Loaded) {
261
+ if (!loadedModels.includes(id)) loadedModels.push(id);
262
+ }
263
+ } else if (v0List.length > 0 && loadedModels.length === 0) {
264
+ activeLoaded = null;
265
+ }
266
+ }
267
+ } catch {}
268
+ }
189
269
 
190
-
191
270
  const exactMatch = models.find(m => m === this.model);
192
271
  const fuzzyMatch = models.find(m => matchModelIdentifier(this.model, m));
193
272
 
194
-
195
273
  const modelIsGemmaOrUnset = !this.model || /gemma/i.test(this.model);
196
274
  const matchedGemma = modelIsGemmaOrUnset
197
275
  ? ((activeLoaded && /gemma/i.test(activeLoaded))
@@ -205,37 +283,53 @@ export class LLMClient {
205
283
  )))
206
284
  : null;
207
285
 
208
- const resolvedActive = exactMatch || (activeLoaded ? activeLoaded : (fuzzyMatch || matchedGemma || this.model));
286
+ const isDefaultGemma = !this.model || this.model === 'google/gemma-4-e4b' || this.model === 'gemma-4-e4b';
287
+ const activeLoadedMatches = Boolean(activeLoaded && (
288
+ matchModelIdentifier(this.model, activeLoaded) ||
289
+ (isDefaultGemma && /gemma/i.test(activeLoaded))
290
+ ));
291
+
292
+ const resolvedActive = exactMatch || (activeLoadedMatches ? activeLoaded : (activeLoaded || fuzzyMatch || matchedGemma || this.model));
209
293
 
210
-
211
294
  if (this.provider === 'lmstudio' && models.length > 0) {
212
295
  if (exactMatch) {
213
296
  this.model = exactMatch;
214
- } else if (activeLoaded && (this.model === 'google/gemma-4-e4b' || matchModelIdentifier(this.model, activeLoaded))) {
297
+ } else if (activeLoadedMatches) {
215
298
  this.model = activeLoaded;
216
- } else if (fuzzyMatch) {
299
+ } else if (fuzzyMatch && matchModelIdentifier(this.model, fuzzyMatch)) {
217
300
  this.model = fuzzyMatch;
218
- } else if (matchedGemma && this.model === 'google/gemma-4-e4b') {
301
+ } else if (matchedGemma && isDefaultGemma) {
219
302
  this.model = matchedGemma;
220
303
  }
221
304
  }
222
305
 
223
306
  const isAvailable = models.length === 0 ? true : (
224
- models.includes(this.model) ||
225
- models.some(m => matchModelIdentifier(this.model, m)) ||
226
- Boolean(activeLoaded && models.includes(activeLoaded))
307
+ exactMatch ? true : (
308
+ activeLoadedMatches ? true : (
309
+ (fuzzyMatch && matchModelIdentifier(this.model, fuzzyMatch)) ? true : (
310
+ this.modelExplicitlySet ? false : Boolean(activeLoaded && models.includes(activeLoaded))
311
+ )
312
+ )
313
+ )
227
314
  );
228
315
 
316
+ const isLocal = this.provider === 'lmstudio' || this.provider === 'vllm' || this.provider === 'ollama';
317
+ const effectiveLoadedModels = loadedModels.length > 0
318
+ ? loadedModels
319
+ : (isLocal ? [] : (models.length > 0 ? [models[0]] : []));
320
+ const isModelLoaded = isLocal ? Boolean(activeLoaded) : (models.length > 0);
321
+
229
322
  return {
230
323
  connected: true,
231
324
  provider: this.provider,
232
325
  providerName: this.providerName,
233
326
  baseUrl: this.baseUrl,
234
327
  models,
235
- loadedModels: loadedModels.length > 0 ? loadedModels : (models.length > 0 ? [models[0]] : []),
328
+ loadedModels: effectiveLoadedModels,
236
329
  activeLoadedModel: activeLoaded,
237
330
  activeModel: resolvedActive,
238
331
  isModelAvailable: isAvailable,
332
+ isModelLoaded,
239
333
  latencyMs
240
334
  };
241
335
  } catch (err) {
@@ -262,7 +356,7 @@ export class LLMClient {
262
356
  }
263
357
 
264
358
  _isLegacyReasoningModel() {
265
- return /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
359
+ return /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner') || /deepseek.*r1/i.test(this.model);
266
360
  }
267
361
 
268
362
 
@@ -270,47 +364,174 @@ export class LLMClient {
270
364
  return this._isLegacyReasoningModel();
271
365
  }
272
366
 
367
+ async _chatAnthropic({ messages, tools, temperature, maxTokens, stream, signal, onToken, onThought }) {
368
+ const converted = toAnthropicMessages(messages);
369
+ const payload = {
370
+ model: this.model,
371
+ messages: converted.messages,
372
+ max_tokens: maxTokens,
373
+ stream,
374
+ };
375
+ if (converted.system) payload.system = converted.system;
376
+ if (temperature !== undefined && temperature !== null) payload.temperature = temperature;
377
+ const anthropicTools = toAnthropicTools(tools);
378
+ if (anthropicTools.length > 0) {
379
+ payload.tools = anthropicTools;
380
+ payload.tool_choice = { type: 'auto' };
381
+ }
382
+
383
+ const response = await fetch(`${this.baseUrl}/v1/messages`, {
384
+ method: 'POST',
385
+ headers: this._buildHeaders(),
386
+ body: JSON.stringify(payload),
387
+ signal,
388
+ });
389
+ if (!response.ok) {
390
+ const errorText = await response.text();
391
+ const error = new Error(`${this.providerName} error (${response.status}): ${parseApiErrorText(errorText)}`);
392
+ error.status = response.status;
393
+ throw error;
394
+ }
395
+ if (!stream) return normalizeAnthropicResponse(await response.json());
396
+
397
+ const reader = response.body.getReader();
398
+ const decoder = new TextDecoder();
399
+ let buffer = '';
400
+ let content = '';
401
+ let reasoning = '';
402
+ const toolCalls = [];
403
+ const processEvent = (eventText) => {
404
+ const dataLine = eventText.split('\n').find(line => line.startsWith('data:'));
405
+ if (!dataLine) return;
406
+ let event;
407
+ try { event = JSON.parse(dataLine.replace(/^data:\s*/, '')); } catch { return; }
408
+ const block = event.content_block;
409
+ if (event.type === 'content_block_start' && block?.type === 'tool_use') {
410
+ toolCalls[event.index || toolCalls.length] = {
411
+ id: block.id || `call_${Date.now()}`,
412
+ type: 'function',
413
+ function: { name: block.name || '', arguments: '' },
414
+ };
415
+ }
416
+ const delta = event.delta;
417
+ if (!delta) return;
418
+ if (delta.type === 'text_delta') {
419
+ content += delta.text || '';
420
+ if (onToken) onToken(delta.text || '');
421
+ } else if (delta.type === 'thinking_delta') {
422
+ reasoning += delta.thinking || '';
423
+ if (onThought) onThought(delta.thinking || '');
424
+ } else if (delta.type === 'input_json_delta') {
425
+ const call = toolCalls[event.index || 0];
426
+ if (call) call.function.arguments += delta.partial_json || '';
427
+ }
428
+ };
429
+ while (true) {
430
+ const { done, value } = await reader.read();
431
+ if (done) break;
432
+ buffer += decoder.decode(value, { stream: true });
433
+ const events = buffer.split('\n\n');
434
+ buffer = events.pop() || '';
435
+ for (const event of events) processEvent(event);
436
+ }
437
+ if (buffer.trim()) processEvent(buffer);
438
+ return normalizeAnthropicResponse({ content: [
439
+ ...(content ? [{ type: 'text', text: content }] : []),
440
+ ...(reasoning ? [{ type: 'thinking', thinking: reasoning }] : []),
441
+ ...toolCalls.filter(Boolean).map(call => ({ type: 'tool_use', id: call.id, name: call.function.name, input: parseAnthropicArgs(call.function.arguments) })),
442
+ ] });
443
+ }
444
+
273
445
  async chat(firstArg, secondArg = {}) {
274
- let messages, tools, temperature, maxTokens, stream, signal, onToken, onThought;
446
+ let messages, tools, temperature, topP, minP, repeatPenalty, maxTokens, stream, signal, onToken, onThought, contextLimit, thinkingMode;
447
+ const inferenceSettings = typeof getPersistedInferenceSettings === 'function' ? getPersistedInferenceSettings() : {};
275
448
  if (Array.isArray(firstArg)) {
276
449
  messages = firstArg;
277
450
  ({
278
451
  tools = null,
279
452
  temperature = 0.3,
453
+ topP = null,
454
+ minP = inferenceSettings.minP ?? 0.08,
455
+ repeatPenalty = inferenceSettings.repeatPenalty ?? 1.10,
280
456
  maxTokens = 8192,
281
457
  stream = true,
282
458
  signal = null,
283
459
  onToken = null,
284
- onThought = null
460
+ onThought = null,
461
+ contextLimit = undefined,
462
+ thinkingMode = null
285
463
  } = secondArg || {});
286
464
  } else {
287
465
  ({
288
466
  messages,
289
467
  tools = null,
290
468
  temperature = 0.3,
469
+ topP = null,
470
+ minP = inferenceSettings.minP ?? 0.08,
471
+ repeatPenalty = inferenceSettings.repeatPenalty ?? 1.10,
291
472
  maxTokens = 8192,
292
473
  stream = true,
293
474
  signal = null,
294
475
  onToken = null,
295
- onThought = null
476
+ onThought = null,
477
+ contextLimit = undefined,
478
+ thinkingMode = null
296
479
  } = firstArg || {});
297
480
  }
481
+ const dispatch = prepareChatMessagesForDispatch(messages, {
482
+ tools,
483
+ maxContextTokens: Number(contextLimit) || Number(process.env.KIKU_CONTEXT_LIMIT) || 32768,
484
+ generationReserve: maxTokens,
485
+ pruneAt: 0.85,
486
+ });
487
+ messages = dispatch.messages;
488
+ tools = dispatch.tools;
489
+ this.lastContextBudget = dispatch.budget;
490
+ if (this.provider === 'anthropic') {
491
+ return this._chatAnthropic({ messages, tools, temperature, maxTokens, stream, signal, onToken, onThought });
492
+ }
298
493
  const isLegacyReasoningModel = this._isLegacyReasoningModel();
299
494
  const usesCompletionTokenLimit = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
300
495
  const rejectsTemperature = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
301
496
 
302
497
  this.toolsDisabledReason = null;
303
498
 
499
+
500
+ const sanitizedMessages = messages.map(m => {
501
+ if (m && m.role === 'assistant' && m.reasoning_content) {
502
+ const { reasoning_content, ...rest } = m;
503
+ return rest;
504
+ }
505
+ return m;
506
+ });
507
+
304
508
  const buildPayload = (includeTools = true, includeTemp = true) => {
305
509
  const p = {
306
510
  model: this.model,
307
- messages,
511
+ messages: sanitizedMessages,
308
512
  stream
309
513
  };
310
514
 
311
515
  if (!rejectsTemperature && includeTemp && temperature !== undefined && temperature !== null) {
312
516
  p.temperature = temperature;
313
517
  }
518
+ const customTemp = temperature !== undefined && temperature !== null && Number(temperature) !== 0.3;
519
+ if (!rejectsTemperature && topP !== undefined && topP !== null && !customTemp) {
520
+ const tp = Number(topP);
521
+ if (Number.isFinite(tp)) p.top_p = Math.min(1, Math.max(0.01, tp));
522
+ }
523
+
524
+ const isLocalOrCompatible = this.provider === 'lmstudio' || this.provider === 'vllm' || this.provider === 'custom' || isLocalInferenceEndpoint(this.baseUrl);
525
+ if (!rejectsTemperature && !isLegacyReasoningModel && isLocalOrCompatible) {
526
+ if (minP !== undefined && minP !== null) {
527
+ const mp = Number(minP);
528
+ if (Number.isFinite(mp) && mp > 0 && mp <= 1) p.min_p = mp;
529
+ }
530
+ if (repeatPenalty !== undefined && repeatPenalty !== null) {
531
+ const rp = Number(repeatPenalty);
532
+ if (Number.isFinite(rp) && rp >= 1 && rp <= 2) p.repeat_penalty = rp;
533
+ }
534
+ }
314
535
 
315
536
  if (usesCompletionTokenLimit) {
316
537
  p.max_completion_tokens = maxTokens;
@@ -331,7 +552,11 @@ export class LLMClient {
331
552
  p.stream_options = { include_usage: true };
332
553
  }
333
554
 
334
- return p;
555
+ return applyModelRequestOptions(p, {
556
+ provider: this.provider,
557
+ model: this.model,
558
+ thinkingMode,
559
+ });
335
560
  };
336
561
 
337
562
  let payload = buildPayload(true, true);
@@ -381,6 +606,10 @@ export class LLMClient {
381
606
  } else if (lowerErr.includes('temperature') && payload.temperature !== undefined) {
382
607
  payload = buildPayload(Boolean(payload.tools), false);
383
608
  retryNeeded = true;
609
+ } else if ((lowerErr.includes('min_p') || lowerErr.includes('repeat_penalty') || lowerErr.includes('extra') || lowerErr.includes('additional properties')) && (payload.min_p !== undefined || payload.repeat_penalty !== undefined)) {
610
+ delete payload.min_p;
611
+ delete payload.repeat_penalty;
612
+ retryNeeded = true;
384
613
  } else if (lowerErr.includes('max_tokens') && payload.max_tokens !== undefined) {
385
614
  delete payload.max_tokens;
386
615
  payload.max_completion_tokens = maxTokens;
@@ -442,11 +671,8 @@ export class LLMClient {
442
671
  const data = await res.json();
443
672
  clearTimeout(timeoutId);
444
673
  if (signal) signal.removeEventListener('abort', abortHandler);
445
- const msg = data.choices && data.choices[0] ? data.choices[0].message : null;
674
+ const msg = data.choices && data.choices[0] ? normalizeAssistantMessage(data.choices[0].message || {}) : null;
446
675
  if (msg) {
447
- if (!msg.reasoning_content && (msg.reasoning || msg.thinking)) {
448
- msg.reasoning_content = msg.reasoning || msg.thinking;
449
- }
450
676
  if (data.usage) {
451
677
  msg.usage = {
452
678
  prompt_tokens: data.usage.prompt_tokens || 0,
@@ -476,6 +702,56 @@ export class LLMClient {
476
702
  let buffer = '';
477
703
  let reportedUsage = null;
478
704
  let malformedChunks = 0;
705
+ const contentNormalizer = new StreamContentNormalizer();
706
+ const processSseLine = line => {
707
+ const trimmed = line.trim();
708
+ if (!trimmed || !trimmed.startsWith('data:')) return;
709
+ const jsonStr = trimmed.replace(/^data:\s*/, '');
710
+ if (jsonStr === '[DONE]') return;
711
+
712
+ try {
713
+ const parsed = JSON.parse(jsonStr);
714
+ if (parsed.usage) reportedUsage = parsed.usage;
715
+ const delta = parsed.choices?.[0]?.delta;
716
+ if (!delta) return;
717
+
718
+ const reasoning = delta.reasoning_content || delta.reasoning || delta.thinking;
719
+ if (reasoning) {
720
+ fullReasoning += reasoning;
721
+ if (onThought) onThought(reasoning);
722
+ }
723
+
724
+ if (delta.content) {
725
+ const contentParts = contentNormalizer.push(delta.content);
726
+ if (contentParts.reasoning) {
727
+ fullReasoning += contentParts.reasoning;
728
+ if (onThought) onThought(contentParts.reasoning);
729
+ }
730
+ if (contentParts.answer) {
731
+ fullContent += contentParts.answer;
732
+ if (onToken) onToken(contentParts.answer);
733
+ }
734
+ }
735
+
736
+ const streamedToolCalls = delta.tool_calls || (delta.function_call ? [{ index: 0, function: delta.function_call }] : null);
737
+ if (streamedToolCalls) {
738
+ for (const tc of streamedToolCalls) {
739
+ const idx = tc.index ?? 0;
740
+ if (!toolCalls[idx]) {
741
+ toolCalls[idx] = {
742
+ id: tc.id || `call_${Date.now()}_${idx}`,
743
+ type: 'function',
744
+ function: { name: '', arguments: '' }
745
+ };
746
+ }
747
+ if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
748
+ if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
749
+ }
750
+ }
751
+ } catch {
752
+ malformedChunks++;
753
+ }
754
+ };
479
755
 
480
756
  while (true) {
481
757
  const { done, value } = await reader.read();
@@ -486,50 +762,19 @@ export class LLMClient {
486
762
  buffer += decoder.decode(value, { stream: true });
487
763
  const lines = buffer.split('\n');
488
764
  buffer = lines.pop() || '';
765
+ for (const line of lines) processSseLine(line);
766
+ }
489
767
 
490
- for (const line of lines) {
491
- const trimmed = line.trim();
492
- if (!trimmed || !trimmed.startsWith('data:')) continue;
493
- const jsonStr = trimmed.replace(/^data:\s*/, '');
494
- if (jsonStr === '[DONE]') continue;
495
-
496
- try {
497
- const parsed = JSON.parse(jsonStr);
498
- if (parsed.usage) {
499
- reportedUsage = parsed.usage;
500
- }
501
- const delta = parsed.choices?.[0]?.delta;
502
- if (!delta) continue;
503
-
504
- const reasoning = delta.reasoning_content || delta.reasoning || delta.thinking;
505
- if (reasoning) {
506
- fullReasoning += reasoning;
507
- if (onThought) onThought(reasoning);
508
- }
509
-
510
- if (delta.content) {
511
- fullContent += delta.content;
512
- if (onToken) onToken(delta.content);
513
- }
768
+ if (buffer.trim()) processSseLine(buffer);
514
769
 
515
- if (delta.tool_calls) {
516
- for (const tc of delta.tool_calls) {
517
- const idx = tc.index ?? 0;
518
- if (!toolCalls[idx]) {
519
- toolCalls[idx] = {
520
- id: tc.id || `call_${Date.now()}_${idx}`,
521
- type: 'function',
522
- function: { name: '', arguments: '' }
523
- };
524
- }
525
- if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
526
- if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
527
- }
528
- }
529
- } catch {
530
- malformedChunks++;
531
- }
532
- }
770
+ const finalContentParts = contentNormalizer.flush();
771
+ if (finalContentParts.reasoning) {
772
+ fullReasoning += finalContentParts.reasoning;
773
+ if (onThought) onThought(finalContentParts.reasoning);
774
+ }
775
+ if (finalContentParts.answer) {
776
+ fullContent += finalContentParts.answer;
777
+ if (onToken) onToken(finalContentParts.answer);
533
778
  }
534
779
 
535
780
  clearTimeout(timeoutId);
@@ -555,9 +800,10 @@ export class LLMClient {
555
800
 
556
801
  const totalTokens = reportedUsage?.total_tokens ?? (promptTokens + completionTokens);
557
802
 
558
- const resultMessage = {
803
+ const rawToolContent = finalContentParts.toolCallBuffer || '';
804
+ const resultMessage = normalizeAssistantMessage({
559
805
  role: 'assistant',
560
- content: fullContent,
806
+ content: fullContent + (rawToolContent ? `\n${rawToolContent}` : ''),
561
807
  reasoning_content: fullReasoning,
562
808
  usage: {
563
809
  prompt_tokens: promptTokens,
@@ -565,7 +811,7 @@ export class LLMClient {
565
811
  reasoning_tokens: reasoningTokens,
566
812
  total_tokens: totalTokens
567
813
  }
568
- };
814
+ });
569
815
 
570
816
  if (toolCalls.length > 0) {
571
817
  resultMessage.tool_calls = toolCalls.filter(Boolean);