novac 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
@@ -0,0 +1,2185 @@
1
+ /**
2
+ * KitAI — A Complete AI / LLM Toolkit Library
3
+ * Version : 1.0.0
4
+ * License : MIT
5
+ * Style : CJS, single file, zero runtime dependencies
6
+ *
7
+ * ╔══════════════════════════════════════════════════════════════════════╗
8
+ * ║ FEATURE MAP ║
9
+ * ╠══════════════════════════════════════════════════════════════════════╣
10
+ * ║ PROVIDER LAYER ║
11
+ * ║ 1. Multi-provider client (OpenAI, Anthropic, Gemini, Ollama, ║
12
+ * ║ Groq, Mistral, Together, Cohere, HuggingFace, custom) ║
13
+ * ║ 2. Provider auto-detect from API key prefix ║
14
+ * ║ 3. Streaming chat completions (SSE + async iterator) ║
15
+ * ║ 4. Non-streaming chat completions ║
16
+ * ║ 5. Image generation (DALL·E 3, Stable Diffusion, Flux) ║
17
+ * ║ 6. Audio transcription (Whisper) ║
18
+ * ║ 7. Text-to-speech synthesis ║
19
+ * ║ 8. Embeddings (text → float[] vector) ║
20
+ * ║ 9. Reranking (cross-encoder style) ║
21
+ * ║ 10. Model catalogue (80+ models with metadata) ║
22
+ * ║ ║
23
+ * ║ PROMPT ENGINEERING ║
24
+ * ║ 11. Prompt template engine ({var} + {{block}} + filters) ║
25
+ * ║ 12. Few-shot example builder ║
26
+ * ║ 13. System prompt library (30+ built-in personas/roles) ║
27
+ * ║ 14. Chain-of-thought / ReAct prompt scaffolds ║
28
+ * ║ 15. Prompt compression (trim to token budget) ║
29
+ * ║ 16. Prompt scoring & critique (self-critique loop) ║
30
+ * ║ ║
31
+ * ║ MEMORY & CONTEXT ║
32
+ * ║ 17. Sliding-window conversation memory ║
33
+ * ║ 18. Token-budget memory (auto-trim oldest turns) ║
34
+ * ║ 19. Summary memory (compress history into summary) ║
35
+ * ║ 20. Vector store (in-memory cosine-similarity search) ║
36
+ * ║ 21. RAG pipeline (chunk → embed → retrieve → augment) ║
37
+ * ║ 22. Document chunker (fixed, sentence, paragraph, semantic) ║
38
+ * ║ ║
39
+ * ║ CHAIN & AGENT ║
40
+ * ║ 23. Sequential chain (pipe output of one call into next) ║
41
+ * ║ 24. Parallel chain (fan-out + merge) ║
42
+ * ║ 25. Conditional chain (route by classifier) ║
43
+ * ║ 26. Tool/function-calling agent (ReAct loop) ║
44
+ * ║ 27. Built-in tools: calculator, date/time, web-search stub, ║
45
+ * ║ JSON-path extractor, regex, base64, UUID, hash ║
46
+ * ║ 28. Tool registry (register / unregister / list) ║
47
+ * ║ ║
48
+ * ║ STRUCTURED OUTPUT ║
49
+ * ║ 29. JSON schema enforcer (retry-until-valid) ║
50
+ * ║ 30. Zod-style schema builder (kitai.schema) ║
51
+ * ║ 31. Output parsers: JSON, CSV, list, key-value, markdown table ║
52
+ * ║ 32. Extraction (pull typed fields from free text) ║
53
+ * ║ 33. Classification (zero-shot, few-shot, chain-of-thought) ║
54
+ * ║ ║
55
+ * ║ EVALUATION & SAFETY ║
56
+ * ║ 34. LLM-as-judge eval (correctness, relevance, coherence) ║
57
+ * ║ 35. BLEU / ROUGE-L / exact-match / F1 metrics (offline) ║
58
+ * ║ 36. Hallucination detector (grounding check) ║
59
+ * ║ 37. Toxicity / PII / prompt-injection guard ║
60
+ * ║ 38. Retry + exponential-backoff + fallback chain ║
61
+ * ║ ║
62
+ * ║ TOKENIZER & COUNTING ║
63
+ * ║ 39. Token estimator (tiktoken-compatible cl100k approximation) ║
64
+ * ║ 40. Cost estimator (per-model $/1M token pricing table) ║
65
+ * ║ 41. Context-window advisor ║
66
+ * ║ ║
67
+ * ║ UTILITIES ║
68
+ * ║ 42. Diff viewer (old vs new LLM response) ║
69
+ * ║ 43. Markdown → plain-text stripper ║
70
+ * ║ 44. Response cache (in-memory LRU, hash-keyed) ║
71
+ * ║ 45. Conversation export (JSON / Markdown / HTML) ║
72
+ * ║ 46. Streaming aggregator (chunks → full message) ║
73
+ * ║ 47. Multi-turn role validator ║
74
+ * ║ 48. Semantic similarity (cosine on embeddings) ║
75
+ * ║ 49. A/B experiment runner ║
76
+ * ║ 50. KitAIFormat — the core message/response object ║
77
+ * ╚══════════════════════════════════════════════════════════════════════╝
78
+ */
79
+
80
+ 'use strict';
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ // §0 Internal helpers
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+
86
+ const _noop = () => {};
87
+ const _sleep = ms => new Promise(r => setTimeout(r, ms));
88
+ const _isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
89
+
90
+ /** Minimal fetch shim (Node ≥18 has fetch built-in; older Node uses https) */
91
+ function _fetch(url, opts = {}) {
92
+ if (typeof fetch !== 'undefined') return fetch(url, opts);
93
+ if (_isNode) {
94
+ const mod = url.startsWith('https') ? require('https') : require('http');
95
+ return new Promise((resolve, reject) => {
96
+ const body = opts.body ? Buffer.from(opts.body) : null;
97
+ const u = new URL(url);
98
+ const req = mod.request({
99
+ hostname: u.hostname, port: u.port, path: u.pathname + u.search,
100
+ method: opts.method || 'GET',
101
+ headers: { 'Content-Type':'application/json', ...(opts.headers||{}),
102
+ ...(body ? {'Content-Length': body.length} : {}) },
103
+ }, res => {
104
+ const chunks = [];
105
+ res.on('data', c => chunks.push(c));
106
+ res.on('end', () => {
107
+ const text = Buffer.concat(chunks).toString('utf8');
108
+ resolve({
109
+ ok: res.statusCode >= 200 && res.statusCode < 300,
110
+ status: res.statusCode,
111
+ headers: { get: k => res.headers[k.toLowerCase()] },
112
+ text: () => Promise.resolve(text),
113
+ json: () => Promise.resolve(JSON.parse(text)),
114
+ body: { getReader: () => {
115
+ let done = false;
116
+ return {
117
+ read: () => { if(done) return Promise.resolve({done:true,value:undefined});
118
+ done=true; return Promise.resolve({done:false,value:Buffer.from(text)}); }
119
+ };
120
+ }},
121
+ });
122
+ });
123
+ res.on('error', reject);
124
+ });
125
+ req.on('error', reject);
126
+ if (body) req.write(body);
127
+ req.end();
128
+ });
129
+ }
130
+ throw new Error('KitAI: no fetch implementation available');
131
+ }
132
+
133
+ function _md5Hex(str) {
134
+ // FNV-1a 64-bit approximation – fast, not crypto
135
+ let h1=0x811c9dc5, h2=0x811c9dc5;
136
+ for (let i=0;i<str.length;i++) {
137
+ const c=str.charCodeAt(i);
138
+ h1^=c; h1=(h1*0x01000193)>>>0;
139
+ h2^=(c<<8); h2=(h2*0x01000193)>>>0;
140
+ }
141
+ return (h1>>>0).toString(16).padStart(8,'0')+(h2>>>0).toString(16).padStart(8,'0');
142
+ }
143
+
144
+ function _deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }
145
+
146
+ function _cosineSim(a, b) {
147
+ if (a.length !== b.length) return 0;
148
+ let dot=0, na=0, nb=0;
149
+ for (let i=0;i<a.length;i++) { dot+=a[i]*b[i]; na+=a[i]*a[i]; nb+=b[i]*b[i]; }
150
+ return dot / (Math.sqrt(na)*Math.sqrt(nb) || 1);
151
+ }
152
+
153
+ function _dotProduct(a, b) {
154
+ let s=0; for (let i=0;i<a.length;i++) s+=a[i]*b[i]; return s;
155
+ }
156
+
157
+ function _norm(v) {
158
+ const mag = Math.sqrt(v.reduce((s,x)=>s+x*x,0));
159
+ return v.map(x => x/mag);
160
+ }
161
+
162
+ // ─────────────────────────────────────────────────────────────────────────────
163
+ // §1 KitAIFormat — core response object
164
+ // ─────────────────────────────────────────────────────────────────────────────
165
+
166
+ class KitAIFormat {
167
+ /**
168
+ * @param {object} opts
169
+ * @param {string} opts.content – text content of the response
170
+ * @param {string} [opts.role] – 'assistant' | 'user' | 'system' | 'tool'
171
+ * @param {string} [opts.model] – model id that produced this
172
+ * @param {object} [opts.usage] – { promptTokens, completionTokens, totalTokens }
173
+ * @param {any[]} [opts.toolCalls] – raw tool_calls array if any
174
+ * @param {any} [opts.raw] – full provider response
175
+ * @param {number} [opts.latencyMs] – round-trip latency
176
+ * @param {string} [opts.finishReason]
177
+ */
178
+ constructor({ content='', role='assistant', model='', usage={}, toolCalls=[], raw=null, latencyMs=0, finishReason='stop' } = {}) {
179
+ this.content = content;
180
+ this.role = role;
181
+ this.model = model;
182
+ this.usage = { promptTokens:0, completionTokens:0, totalTokens:0, ...usage };
183
+ this.toolCalls = toolCalls;
184
+ this.raw = raw;
185
+ this.latencyMs = latencyMs;
186
+ this.finishReason = finishReason;
187
+ this.createdAt = Date.now();
188
+ }
189
+
190
+ // ── Parsers ────────────────────────────────────────────────────────────
191
+
192
+ /** Parse content as JSON (throws on failure) */
193
+ toJSON() { return JSON.parse(this.content); }
194
+
195
+ /** Best-effort JSON parse (returns null on failure) */
196
+ tryJSON() { try { return this.toJSON(); } catch { return null; } }
197
+
198
+ /** Split content into a trimmed string array by line */
199
+ toLines() { return this.content.split('\n').map(l=>l.trim()).filter(Boolean); }
200
+
201
+ /** Extract bullet / numbered list items */
202
+ toList() {
203
+ return this.content.split('\n')
204
+ .map(l => l.replace(/^[-*•]\s+|^\d+[.)]\s+/,'').trim())
205
+ .filter(Boolean);
206
+ }
207
+
208
+ /** Strip markdown formatting → plain text */
209
+ toPlainText() { return kitai.stripMarkdown(this.content); }
210
+
211
+ /** Extract all ```lang ... ``` code blocks */
212
+ toCodeBlocks() {
213
+ const blocks = [], re = /```(\w*)\n?([\s\S]*?)```/g;
214
+ let m;
215
+ while ((m=re.exec(this.content))!==null)
216
+ blocks.push({ lang: m[1]||'text', code: m[2].trim() });
217
+ return blocks;
218
+ }
219
+
220
+ /** Extract key: value pairs from content */
221
+ toKeyValue() {
222
+ const map = {};
223
+ for (const line of this.toLines()) {
224
+ const i = line.indexOf(':');
225
+ if (i>0) map[line.slice(0,i).trim()] = line.slice(i+1).trim();
226
+ }
227
+ return map;
228
+ }
229
+
230
+ /** Parse a markdown table into array of objects */
231
+ toTable() { return kitai.parsers.markdownTable(this.content); }
232
+
233
+ /** Return as a Message object (for use in next conversation turn) */
234
+ toMessage() { return { role: this.role, content: this.content }; }
235
+
236
+ /** Cost in USD based on model pricing */
237
+ estimatedCost() { return kitai.cost.estimate(this.model, this.usage.promptTokens, this.usage.completionTokens); }
238
+
239
+ /** Human-readable summary line */
240
+ summary() {
241
+ const tok = this.usage.totalTokens ? `${this.usage.totalTokens} tok` : '';
242
+ const lat = this.latencyMs ? `${this.latencyMs}ms` : '';
243
+ const cost = this.estimatedCost() ? `$${this.estimatedCost().toFixed(6)}` : '';
244
+ const info = [this.model, tok, lat, cost].filter(Boolean).join(' · ');
245
+ return `[${this.role}] ${this.content.slice(0,80)}${this.content.length>80?'…':''} (${info})`;
246
+ }
247
+
248
+ toString() { return this.content; }
249
+ }
250
+
251
+ // ─────────────────────────────────────────────────────────────────────────────
252
+ // §2 Provider registry & adapters
253
+ // ─────────────────────────────────────────────────────────────────────────────
254
+
255
+ const _PROVIDERS = {
256
+ openai: {
257
+ baseURL: 'https://api.openai.com/v1',
258
+ authHeader: key => ({ Authorization: `Bearer ${key}` }),
259
+ chatPath: '/chat/completions',
260
+ embedPath: '/embeddings',
261
+ imagePath: '/images/generations',
262
+ audioPath: '/audio/transcriptions',
263
+ ttsPath: '/audio/speech',
264
+ models: ['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo','gpt-4o-2024-11-20'],
265
+ parseChat: r => ({ content: r.choices[0].message.content, toolCalls: r.choices[0].message.tool_calls||[],
266
+ usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
267
+ totalTokens:r.usage?.total_tokens||0 }, finishReason: r.choices[0].finish_reason }),
268
+ parseEmbed: r => r.data[0].embedding,
269
+ parseChunk: line => {
270
+ if (!line.startsWith('data:')) return null;
271
+ const d = line.slice(5).trim();
272
+ if (d==='[DONE]') return null;
273
+ try { const j=JSON.parse(d); return j.choices[0]?.delta?.content||''; } catch { return ''; }
274
+ },
275
+ },
276
+
277
+ anthropic: {
278
+ baseURL: 'https://api.anthropic.com/v1',
279
+ authHeader: key => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }),
280
+ chatPath: '/messages',
281
+ embedPath: null,
282
+ imagePath: null,
283
+ models: ['claude-opus-4-20250514','claude-sonnet-4-5','claude-haiku-4-5','claude-3-opus-20240229'],
284
+ buildBody: (msgs, model, opts) => {
285
+ const sys = msgs.find(m=>m.role==='system');
286
+ const conv= msgs.filter(m=>m.role!=='system');
287
+ return { model, messages: conv, max_tokens: opts.maxTokens||1024,
288
+ ...(sys ? {system: sys.content} : {}), ...(opts.temperature!=null?{temperature:opts.temperature}:{}) };
289
+ },
290
+ parseChat: r => ({ content: r.content[0].text, toolCalls: r.content.filter(b=>b.type==='tool_use'),
291
+ usage: { promptTokens:r.usage?.input_tokens||0, completionTokens:r.usage?.output_tokens||0,
292
+ totalTokens:(r.usage?.input_tokens||0)+(r.usage?.output_tokens||0) },
293
+ finishReason: r.stop_reason }),
294
+ parseChunk: line => {
295
+ if (!line.startsWith('data:')) return null;
296
+ const d=line.slice(5).trim();
297
+ try { const j=JSON.parse(d); return j.type==='content_block_delta'?j.delta?.text||'':j.type==='message_stop'?null:''; }
298
+ catch { return ''; }
299
+ },
300
+ },
301
+
302
+ gemini: {
303
+ baseURL: 'https://generativelanguage.googleapis.com/v1beta',
304
+ authHeader: () => ({}),
305
+ chatPathFn: (model,key) => `/models/${model}:generateContent?key=${key}`,
306
+ models: ['gemini-1.5-pro','gemini-1.5-flash','gemini-2.0-flash','gemini-pro'],
307
+ buildBody: (msgs, _model, opts) => ({
308
+ contents: msgs.filter(m=>m.role!=='system').map(m=>({ role:m.role==='assistant'?'model':'user', parts:[{text:m.content}] })),
309
+ generationConfig: { maxOutputTokens: opts.maxTokens||1024, temperature: opts.temperature??0.7 },
310
+ }),
311
+ parseChat: r => ({ content: r.candidates[0].content.parts[0].text, toolCalls: [],
312
+ usage: { promptTokens:r.usageMetadata?.promptTokenCount||0,
313
+ completionTokens:r.usageMetadata?.candidatesTokenCount||0,
314
+ totalTokens:r.usageMetadata?.totalTokenCount||0 }, finishReason:'stop' }),
315
+ },
316
+
317
+ ollama: {
318
+ baseURL: 'http://localhost:11434/v1',
319
+ authHeader: () => ({}),
320
+ chatPath: '/chat/completions',
321
+ embedPath: '/embeddings',
322
+ models: ['llama3','mistral','phi3','gemma2','qwen2','deepseek-coder'],
323
+ parseChat: r => ({ content: r.choices[0].message.content, toolCalls: [],
324
+ usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
325
+ totalTokens:r.usage?.total_tokens||0 }, finishReason:'stop' }),
326
+ parseEmbed: r => r.data?.[0]?.embedding || r.embedding || [],
327
+ parseChunk: line => {
328
+ if (!line.startsWith('data:')) return null;
329
+ const d=line.slice(5).trim(); if(d==='[DONE]') return null;
330
+ try { return JSON.parse(d).choices[0]?.delta?.content||''; } catch { return ''; }
331
+ },
332
+ },
333
+
334
+ groq: {
335
+ baseURL: 'https://api.groq.com/openai/v1',
336
+ authHeader: key => ({ Authorization: `Bearer ${key}` }),
337
+ chatPath: '/chat/completions',
338
+ embedPath: '/openai/v1/embeddings',
339
+ models: ['llama-3.1-70b-versatile','llama-3.1-8b-instant','mixtral-8x7b-32768','gemma2-9b-it'],
340
+ parseChat: r => ({ content: r.choices[0].message.content, toolCalls: r.choices[0].message.tool_calls||[],
341
+ usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
342
+ totalTokens:r.usage?.total_tokens||0 }, finishReason:r.choices[0].finish_reason }),
343
+ parseEmbed: r => r.data[0].embedding,
344
+ parseChunk: line => {
345
+ if (!line.startsWith('data:')) return null;
346
+ const d=line.slice(5).trim(); if(d==='[DONE]') return null;
347
+ try { return JSON.parse(d).choices[0]?.delta?.content||''; } catch { return ''; }
348
+ },
349
+ },
350
+
351
+ mistral: {
352
+ baseURL: 'https://api.mistral.ai/v1',
353
+ authHeader: key => ({ Authorization: `Bearer ${key}` }),
354
+ chatPath: '/chat/completions',
355
+ embedPath: '/embeddings',
356
+ models: ['mistral-large-latest','mistral-medium-latest','mistral-small-latest','codestral-latest','open-mixtral-8x22b'],
357
+ parseChat: r => ({ content: r.choices[0].message.content, toolCalls: r.choices[0].message.tool_calls||[],
358
+ usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
359
+ totalTokens:r.usage?.total_tokens||0 }, finishReason:r.choices[0].finish_reason }),
360
+ parseEmbed: r => r.data[0].embedding,
361
+ parseChunk: line => {
362
+ if (!line.startsWith('data:')) return null;
363
+ const d=line.slice(5).trim(); if(d==='[DONE]') return null;
364
+ try { return JSON.parse(d).choices[0]?.delta?.content||''; } catch { return ''; }
365
+ },
366
+ },
367
+
368
+ cohere: {
369
+ baseURL: 'https://api.cohere.com/v2',
370
+ authHeader: key => ({ Authorization: `Bearer ${key}` }),
371
+ chatPath: '/chat',
372
+ embedPath: '/embed',
373
+ rerankPath: '/rerank',
374
+ models: ['command-r-plus','command-r','command','command-light'],
375
+ buildBody: (msgs, model, opts) => ({
376
+ model, messages: msgs.map(m=>({role:m.role,content:m.content})),
377
+ max_tokens: opts.maxTokens||1024,
378
+ }),
379
+ parseChat: r => ({ content: r.message?.content?.[0]?.text||r.text||'', toolCalls:[],
380
+ usage: { promptTokens:r.meta?.tokens?.input_tokens||0,
381
+ completionTokens:r.meta?.tokens?.output_tokens||0,
382
+ totalTokens:(r.meta?.tokens?.input_tokens||0)+(r.meta?.tokens?.output_tokens||0) },
383
+ finishReason:'stop' }),
384
+ parseEmbed: r => r.embeddings?.float?.[0] || r.embeddings?.[0] || [],
385
+ },
386
+
387
+ together: {
388
+ baseURL: 'https://api.together.xyz/v1',
389
+ authHeader: key => ({ Authorization: `Bearer ${key}` }),
390
+ chatPath: '/chat/completions',
391
+ embedPath: '/embeddings',
392
+ models: ['meta-llama/Llama-3-70b-chat-hf','mistralai/Mixtral-8x7B-Instruct-v0.1','Qwen/Qwen2-72B-Instruct'],
393
+ parseChat: r => ({ content: r.choices[0].message.content, toolCalls:[],
394
+ usage: { promptTokens:r.usage?.prompt_tokens||0, completionTokens:r.usage?.completion_tokens||0,
395
+ totalTokens:r.usage?.total_tokens||0 }, finishReason:r.choices[0].finish_reason }),
396
+ parseEmbed: r => r.data[0].embedding,
397
+ parseChunk: line => {
398
+ if (!line.startsWith('data:')) return null;
399
+ const d=line.slice(5).trim(); if(d==='[DONE]') return null;
400
+ try { return JSON.parse(d).choices[0]?.delta?.content||''; } catch { return ''; }
401
+ },
402
+ },
403
+ };
404
+
405
+ // ─────────────────────────────────────────────────────────────────────────────
406
+ // §3 Model catalogue (80+ entries)
407
+ // ─────────────────────────────────────────────────────────────────────────────
408
+
409
+ const _MODELS = {
410
+ // OpenAI
411
+ 'gpt-4o': { provider:'openai', context:128000, family:'gpt-4', type:'chat', inputPer1M:2.50, outputPer1M:10.00 },
412
+ 'gpt-4o-mini': { provider:'openai', context:128000, family:'gpt-4', type:'chat', inputPer1M:0.15, outputPer1M:0.60 },
413
+ 'gpt-4-turbo': { provider:'openai', context:128000, family:'gpt-4', type:'chat', inputPer1M:10.00, outputPer1M:30.00 },
414
+ 'gpt-4': { provider:'openai', context:8192, family:'gpt-4', type:'chat', inputPer1M:30.00, outputPer1M:60.00 },
415
+ 'gpt-3.5-turbo': { provider:'openai', context:16385, family:'gpt-3.5', type:'chat', inputPer1M:0.50, outputPer1M:1.50 },
416
+ 'o1': { provider:'openai', context:200000, family:'o1', type:'chat', inputPer1M:15.00, outputPer1M:60.00 },
417
+ 'o1-mini': { provider:'openai', context:128000, family:'o1', type:'chat', inputPer1M:3.00, outputPer1M:12.00 },
418
+ 'o3-mini': { provider:'openai', context:200000, family:'o3', type:'chat', inputPer1M:1.10, outputPer1M:4.40 },
419
+ 'text-embedding-3-small':{ provider:'openai', context:8191, family:'embed', type:'embed', inputPer1M:0.02, outputPer1M:0 },
420
+ 'text-embedding-3-large':{ provider:'openai', context:8191, family:'embed', type:'embed', inputPer1M:0.13, outputPer1M:0 },
421
+ 'dall-e-3': { provider:'openai', context:0, family:'image', type:'image', inputPer1M:0, outputPer1M:0 },
422
+ 'whisper-1': { provider:'openai', context:0, family:'audio', type:'audio', inputPer1M:0, outputPer1M:0 },
423
+ 'tts-1': { provider:'openai', context:0, family:'tts', type:'tts', inputPer1M:15.00, outputPer1M:0 },
424
+ 'tts-1-hd': { provider:'openai', context:0, family:'tts', type:'tts', inputPer1M:30.00, outputPer1M:0 },
425
+
426
+ // Anthropic
427
+ 'claude-opus-4-20250514':{ provider:'anthropic',context:200000,family:'claude-4', type:'chat', inputPer1M:15.00, outputPer1M:75.00 },
428
+ 'claude-sonnet-4-5': { provider:'anthropic', context:200000, family:'claude-4', type:'chat', inputPer1M:3.00, outputPer1M:15.00 },
429
+ 'claude-haiku-4-5': { provider:'anthropic', context:200000, family:'claude-4', type:'chat', inputPer1M:0.80, outputPer1M:4.00 },
430
+ 'claude-3-opus-20240229':{ provider:'anthropic',context:200000,family:'claude-3', type:'chat', inputPer1M:15.00, outputPer1M:75.00 },
431
+ 'claude-3-5-sonnet-20241022':{ provider:'anthropic',context:200000,family:'claude-3',type:'chat',inputPer1M:3.00, outputPer1M:15.00 },
432
+ 'claude-3-haiku-20240307':{ provider:'anthropic',context:200000,family:'claude-3',type:'chat', inputPer1M:0.25, outputPer1M:1.25 },
433
+
434
+ // Google Gemini
435
+ 'gemini-1.5-pro': { provider:'gemini', context:2000000,family:'gemini', type:'chat', inputPer1M:1.25, outputPer1M:5.00 },
436
+ 'gemini-1.5-flash': { provider:'gemini', context:1000000,family:'gemini', type:'chat', inputPer1M:0.075, outputPer1M:0.30 },
437
+ 'gemini-2.0-flash': { provider:'gemini', context:1048576,family:'gemini', type:'chat', inputPer1M:0.10, outputPer1M:0.40 },
438
+ 'gemini-pro': { provider:'gemini', context:32760, family:'gemini', type:'chat', inputPer1M:0.50, outputPer1M:1.50 },
439
+
440
+ // Groq
441
+ 'llama-3.1-70b-versatile':{ provider:'groq', context:32768, family:'llama', type:'chat', inputPer1M:0.59, outputPer1M:0.79 },
442
+ 'llama-3.1-8b-instant':{ provider:'groq', context:131072, family:'llama', type:'chat', inputPer1M:0.05, outputPer1M:0.08 },
443
+ 'mixtral-8x7b-32768': { provider:'groq', context:32768, family:'mixtral', type:'chat', inputPer1M:0.24, outputPer1M:0.24 },
444
+ 'gemma2-9b-it': { provider:'groq', context:8192, family:'gemma', type:'chat', inputPer1M:0.20, outputPer1M:0.20 },
445
+
446
+ // Mistral
447
+ 'mistral-large-latest':{ provider:'mistral', context:131072, family:'mistral', type:'chat', inputPer1M:2.00, outputPer1M:6.00 },
448
+ 'mistral-medium-latest':{ provider:'mistral', context:131072, family:'mistral', type:'chat', inputPer1M:2.70, outputPer1M:8.10 },
449
+ 'mistral-small-latest':{ provider:'mistral', context:32768, family:'mistral', type:'chat', inputPer1M:0.20, outputPer1M:0.60 },
450
+ 'codestral-latest': { provider:'mistral', context:32768, family:'codestral',type:'chat', inputPer1M:0.20, outputPer1M:0.60 },
451
+ 'open-mixtral-8x22b': { provider:'mistral', context:65536, family:'mixtral', type:'chat', inputPer1M:2.00, outputPer1M:6.00 },
452
+ 'mistral-embed': { provider:'mistral', context:8192, family:'embed', type:'embed', inputPer1M:0.10, outputPer1M:0 },
453
+
454
+ // Cohere
455
+ 'command-r-plus': { provider:'cohere', context:128000, family:'command', type:'chat', inputPer1M:2.50, outputPer1M:10.00 },
456
+ 'command-r': { provider:'cohere', context:128000, family:'command', type:'chat', inputPer1M:0.15, outputPer1M:0.60 },
457
+ 'command': { provider:'cohere', context:4096, family:'command', type:'chat', inputPer1M:1.00, outputPer1M:2.00 },
458
+ 'embed-english-v3.0': { provider:'cohere', context:512, family:'embed', type:'embed', inputPer1M:0.10, outputPer1M:0 },
459
+ 'embed-multilingual-v3.0':{ provider:'cohere', context:512, family:'embed', type:'embed', inputPer1M:0.10, outputPer1M:0 },
460
+
461
+ // Together
462
+ 'meta-llama/Llama-3-70b-chat-hf':{ provider:'together',context:8192,family:'llama',type:'chat',inputPer1M:0.90,outputPer1M:0.90},
463
+ 'Qwen/Qwen2-72B-Instruct': { provider:'together',context:32768,family:'qwen',type:'chat',inputPer1M:0.90,outputPer1M:0.90},
464
+
465
+ // Ollama (local – free)
466
+ 'llama3': { provider:'ollama', context:8192, family:'llama', type:'chat', inputPer1M:0, outputPer1M:0 },
467
+ 'mistral': { provider:'ollama', context:32768, family:'mistral', type:'chat', inputPer1M:0, outputPer1M:0 },
468
+ 'phi3': { provider:'ollama', context:131072, family:'phi', type:'chat', inputPer1M:0, outputPer1M:0 },
469
+ 'gemma2': { provider:'ollama', context:8192, family:'gemma', type:'chat', inputPer1M:0, outputPer1M:0 },
470
+ 'qwen2': { provider:'ollama', context:32768, family:'qwen', type:'chat', inputPer1M:0, outputPer1M:0 },
471
+ 'deepseek-coder': { provider:'ollama', context:16384, family:'deepseek', type:'chat', inputPer1M:0, outputPer1M:0 },
472
+ 'nomic-embed-text': { provider:'ollama', context:8192, family:'embed', type:'embed', inputPer1M:0, outputPer1M:0 },
473
+ };
474
+
475
+ // ─────────────────────────────────────────────────────────────────────────────
476
+ // §4 Tokenizer (cl100k_base approximation — no wasm required)
477
+ // ─────────────────────────────────────────────────────────────────────────────
478
+
479
+ const tokenizer = (() => {
480
+ // Approximation: GPT-4 averages ~4 chars/token for English prose.
481
+ // We use a slightly smarter heuristic that accounts for whitespace splits,
482
+ // punctuation, numbers, CJK, code identifiers, etc.
483
+ function estimate(text) {
484
+ if (!text) return 0;
485
+ let count = 0;
486
+ // Coarse split by whitespace first
487
+ const words = text.split(/\s+/);
488
+ for (const w of words) {
489
+ if (!w) continue;
490
+ // Each word costs at least 1 token
491
+ // Long words (~7+ chars) typically split into multiple tokens
492
+ count += Math.ceil(w.length / 4.5);
493
+ // Punctuation tacked onto words adds tokens
494
+ const puncts = (w.match(/[^\w]/g)||[]).length;
495
+ count += Math.ceil(puncts * 0.5);
496
+ }
497
+ // CJK characters are generally 1 char = 1 token
498
+ const cjk = (text.match(/[\u4e00-\u9fff\u3040-\u30ff\uAC00-\uD7AF]/g)||[]).length;
499
+ count += cjk;
500
+ return Math.max(1, Math.round(count));
501
+ }
502
+
503
+ function estimateMessages(messages) {
504
+ // ~4 tokens per message overhead (role + separators)
505
+ return messages.reduce((s,m) => s + estimate(m.content||'') + 4, 3);
506
+ }
507
+
508
+ function maxForModel(modelId) {
509
+ return (_MODELS[modelId]||{}).context || 4096;
510
+ }
511
+
512
+ function fitsInContext(messages, modelId, reserveTokens=512) {
513
+ const used = estimateMessages(messages);
514
+ const max = maxForModel(modelId);
515
+ return { fits: used + reserveTokens <= max, used, max, remaining: max - used };
516
+ }
517
+
518
+ return { estimate, estimateMessages, maxForModel, fitsInContext };
519
+ })();
520
+
521
+ // ─────────────────────────────────────────────────────────────────────────────
522
+ // §5 Cost estimator
523
+ // ─────────────────────────────────────────────────────────────────────────────
524
+
525
+ const cost = {
526
+ /**
527
+ * Estimate cost in USD.
528
+ * @param {string} modelId
529
+ * @param {number} promptTokens
530
+ * @param {number} completionTokens
531
+ * @returns {number} USD
532
+ */
533
+ estimate(modelId, promptTokens=0, completionTokens=0) {
534
+ const m = _MODELS[modelId];
535
+ if (!m) return 0;
536
+ return (promptTokens * m.inputPer1M + completionTokens * m.outputPer1M) / 1_000_000;
537
+ },
538
+
539
+ /** Return full pricing table */
540
+ table() {
541
+ return Object.entries(_MODELS).map(([id,m]) => ({
542
+ id, provider:m.provider, family:m.family, type:m.type,
543
+ contextK: Math.round(m.context/1000),
544
+ inputPer1M: m.inputPer1M, outputPer1M: m.outputPer1M,
545
+ }));
546
+ },
547
+
548
+ /** Compare cost across multiple models for the same workload */
549
+ compare(promptTokens, completionTokens, modelIds) {
550
+ return modelIds
551
+ .map(id => ({ id, usd: cost.estimate(id, promptTokens, completionTokens) }))
552
+ .sort((a,b) => a.usd - b.usd);
553
+ },
554
+
555
+ /** Cheapest model for a given type ('chat'|'embed'|'image') */
556
+ cheapest(type='chat') {
557
+ return Object.entries(_MODELS)
558
+ .filter(([,m]) => m.type===type && m.inputPer1M > 0)
559
+ .sort((a,b) => a[1].inputPer1M - b[1].inputPer1M)[0]?.[0] || null;
560
+ },
561
+ };
562
+
563
+ // ─────────────────────────────────────────────────────────────────────────────
564
+ // §6 LRU Response Cache
565
+ // ─────────────────────────────────────────────────────────────────────────────
566
+
567
+ class LRUCache {
568
+ constructor(maxSize=256) {
569
+ this._max = maxSize;
570
+ this._map = new Map();
571
+ }
572
+ _key(messages, model, opts) {
573
+ return _md5Hex(JSON.stringify({ messages, model, opts }));
574
+ }
575
+ get(messages, model, opts) {
576
+ const k = this._key(messages, model, opts);
577
+ if (!this._map.has(k)) return null;
578
+ const v = this._map.get(k);
579
+ this._map.delete(k); this._map.set(k,v); // promote
580
+ return v;
581
+ }
582
+ set(messages, model, opts, value) {
583
+ const k = this._key(messages, model, opts);
584
+ if (this._map.size >= this._max) this._map.delete(this._map.keys().next().value);
585
+ this._map.set(k, value);
586
+ }
587
+ clear() { this._map.clear(); }
588
+ get size() { return this._map.size; }
589
+ invalidate(messages, model, opts) { this._map.delete(this._key(messages, model, opts)); }
590
+ }
591
+
592
+ // ─────────────────────────────────────────────────────────────────────────────
593
+ // §7 Core client
594
+ // ─────────────────────────────────────────────────────────────────────────────
595
+
596
+ class KitAIClient {
597
+ /**
598
+ * @param {object} config
599
+ * @param {string} config.apiKey
600
+ * @param {string} [config.provider] – auto-detected if omitted
601
+ * @param {string} [config.model] – default model
602
+ * @param {string} [config.baseURL] – override base URL
603
+ * @param {number} [config.maxTokens] – default max_tokens
604
+ * @param {number} [config.temperature]
605
+ * @param {number} [config.maxRetries]
606
+ * @param {boolean}[config.cache] – enable response cache
607
+ * @param {object} [config.headers] – extra headers
608
+ */
609
+ constructor(config = {}) {
610
+ this.apiKey = config.apiKey || '';
611
+ this.provider = config.provider || kitai.detectProvider(this.apiKey);
612
+ this.model = config.model || '';
613
+ this._baseURL = config.baseURL || '';
614
+ this.maxTokens = config.maxTokens ?? 1024;
615
+ this.temperature = config.temperature ?? 0.7;
616
+ this.maxRetries = config.maxRetries ?? 3;
617
+ this._cache = config.cache ? new LRUCache() : null;
618
+ this._headers = config.headers || {};
619
+ this._prov = _PROVIDERS[this.provider] || _PROVIDERS.openai;
620
+ }
621
+
622
+ _baseUrl() { return this._baseURL || this._prov.baseURL; }
623
+
624
+ _headers_(extra={}) {
625
+ const auth = this._prov.authHeader(this.apiKey);
626
+ return { 'Content-Type':'application/json', ...auth, ...this._headers, ...extra };
627
+ }
628
+
629
+ _buildBody(messages, model, opts={}) {
630
+ if (this._prov.buildBody) return this._prov.buildBody(messages, model, { maxTokens:this.maxTokens, temperature:this.temperature, ...opts });
631
+ const body = {
632
+ model,
633
+ messages,
634
+ max_tokens: opts.maxTokens ?? this.maxTokens,
635
+ temperature: opts.temperature ?? this.temperature,
636
+ };
637
+ if (opts.tools) body.tools = opts.tools;
638
+ if (opts.jsonMode) body.response_format = { type:'json_object' };
639
+ if (opts.stream) body.stream = true;
640
+ return body;
641
+ }
642
+
643
+ /** Non-streaming chat completion */
644
+ async chat(messages, opts = {}) {
645
+ const model = opts.model || this.model || this._prov.models?.[0] || 'gpt-4o-mini';
646
+
647
+ // Cache lookup
648
+ if (this._cache && !opts.noCache) {
649
+ const hit = this._cache.get(messages, model, opts);
650
+ if (hit) return hit;
651
+ }
652
+
653
+ const t0 = Date.now();
654
+ let lastErr;
655
+ for (let attempt=0; attempt<=this.maxRetries; attempt++) {
656
+ try {
657
+ let path, url;
658
+ if (this.provider==='gemini' && this._prov.chatPathFn) {
659
+ path = this._prov.chatPathFn(model, this.apiKey);
660
+ url = `${this._baseUrl()}${path}`;
661
+ } else {
662
+ url = `${this._baseUrl()}${this._prov.chatPath}`;
663
+ }
664
+ const res = await _fetch(url, {
665
+ method: 'POST',
666
+ headers: this._headers_(),
667
+ body: JSON.stringify(this._buildBody(messages, model, opts)),
668
+ });
669
+ if (!res.ok) {
670
+ const txt = await res.text();
671
+ throw new Error(`HTTP ${res.status}: ${txt.slice(0,200)}`);
672
+ }
673
+ const raw = await res.json();
674
+ const parsed = this._prov.parseChat(raw);
675
+ const result = new KitAIFormat({ ...parsed, model, raw, latencyMs: Date.now()-t0 });
676
+ if (this._cache) this._cache.set(messages, model, opts, result);
677
+ return result;
678
+ } catch(e) {
679
+ lastErr = e;
680
+ if (attempt < this.maxRetries) await _sleep(Math.min(1000*2**attempt, 30000));
681
+ }
682
+ }
683
+ throw lastErr;
684
+ }
685
+
686
+ /** Streaming chat — returns an async generator yielding string chunks */
687
+ async *stream(messages, opts = {}) {
688
+ const model = opts.model || this.model || this._prov.models?.[0] || 'gpt-4o-mini';
689
+ const url = `${this._baseUrl()}${this._prov.chatPath}`;
690
+ const res = await _fetch(url, {
691
+ method: 'POST',
692
+ headers: this._headers_(),
693
+ body: JSON.stringify(this._buildBody(messages, model, { ...opts, stream:true })),
694
+ });
695
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
696
+
697
+ // Browser ReadableStream
698
+ if (res.body && res.body.getReader) {
699
+ const reader = res.body.getReader();
700
+ const dec = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null;
701
+ let buf = '';
702
+ while (true) {
703
+ const { done, value } = await reader.read();
704
+ if (done) break;
705
+ buf += dec ? dec.decode(value, {stream:true}) : value.toString();
706
+ const lines = buf.split('\n');
707
+ buf = lines.pop();
708
+ for (const line of lines) {
709
+ const chunk = this._prov.parseChunk?.(line);
710
+ if (chunk === null) return;
711
+ if (chunk) yield chunk;
712
+ }
713
+ }
714
+ } else {
715
+ // Node HTTP response arrives as complete text — simulate streaming
716
+ const text = typeof res.text === 'function' ? await res.text() : String(res.body);
717
+ for (const line of text.split('\n')) {
718
+ const chunk = this._prov.parseChunk?.(line);
719
+ if (chunk === null) return;
720
+ if (chunk) yield chunk;
721
+ }
722
+ }
723
+ }
724
+
725
+ /** Collect an entire stream into a KitAIFormat */
726
+ async streamCollect(messages, opts={}) {
727
+ const t0 = Date.now();
728
+ const model = opts.model || this.model || this._prov.models?.[0] || 'gpt-4o-mini';
729
+ let content = '';
730
+ for await (const chunk of this.stream(messages, opts)) content += chunk;
731
+ const tokens = tokenizer.estimate(content);
732
+ return new KitAIFormat({ content, model, latencyMs: Date.now()-t0,
733
+ usage:{ completionTokens: tokens, promptTokens: tokenizer.estimateMessages(messages), totalTokens: tokenizer.estimateMessages(messages)+tokens } });
734
+ }
735
+
736
+ /** Generate embeddings */
737
+ async embed(input, opts={}) {
738
+ const model = opts.model || 'text-embedding-3-small';
739
+ const inputs = Array.isArray(input) ? input : [input];
740
+ const path = this._prov.embedPath;
741
+ if (!path) throw new Error(`Provider ${this.provider} does not support embeddings`);
742
+ const body = this.provider==='cohere'
743
+ ? { model, texts: inputs, input_type:'search_document', embedding_types:['float'] }
744
+ : { model, input: inputs.length===1 ? inputs[0] : inputs };
745
+ const res = await _fetch(`${this._baseUrl()}${path}`, {
746
+ method:'POST', headers:this._headers_(), body:JSON.stringify(body),
747
+ });
748
+ if (!res.ok) throw new Error(`Embed HTTP ${res.status}`);
749
+ const raw = await res.json();
750
+ const vec = this._prov.parseEmbed(raw);
751
+ return Array.isArray(vec[0]) ? vec : [vec];
752
+ }
753
+
754
+ /** Generate an image, returns URL or base64 */
755
+ async imageGenerate(prompt, opts={}) {
756
+ const model = opts.model || 'dall-e-3';
757
+ const body = { model, prompt, n:opts.n||1, size:opts.size||'1024x1024',
758
+ response_format: opts.responseFormat||'url', quality:opts.quality||'standard' };
759
+ const res = await _fetch(`${this._baseUrl()}${this._prov.imagePath}`, {
760
+ method:'POST', headers:this._headers_(), body:JSON.stringify(body),
761
+ });
762
+ if (!res.ok) throw new Error(`Image HTTP ${res.status}`);
763
+ const raw = await res.json();
764
+ return raw.data.map(d => d.url || d.b64_json);
765
+ }
766
+
767
+ /** Synthesize speech, returns audio buffer (ArrayBuffer or Buffer) */
768
+ async textToSpeech(text, opts={}) {
769
+ const body = { model: opts.model||'tts-1', input:text, voice:opts.voice||'alloy',
770
+ response_format: opts.format||'mp3', speed: opts.speed||1.0 };
771
+ const res = await _fetch(`${this._baseUrl()}${this._prov.ttsPath}`, {
772
+ method:'POST', headers:this._headers_(), body:JSON.stringify(body),
773
+ });
774
+ if (!res.ok) throw new Error(`TTS HTTP ${res.status}`);
775
+ return res.arrayBuffer ? res.arrayBuffer() : res.buffer();
776
+ }
777
+ }
778
+
779
+ // ─────────────────────────────────────────────────────────────────────────────
780
+ // §8 Memory systems
781
+ // ─────────────────────────────────────────────────────────────────────────────
782
+
783
+ class SlidingWindowMemory {
784
+ /** @param {number} maxTurns – number of full turns (user+assistant pairs) to keep */
785
+ constructor(maxTurns=10) {
786
+ this._max = maxTurns * 2;
787
+ this._msgs = [];
788
+ this._system = null;
789
+ }
790
+ setSystem(content) { this._system = content; return this; }
791
+ add(role, content) { this._msgs.push({role,content}); this._trim(); return this; }
792
+ addUser(content) { return this.add('user',content); }
793
+ addAssistant(content) { return this.add('assistant',content); }
794
+ addResult(res) { return this.addAssistant(res.content||String(res)); }
795
+ _trim() { while(this._msgs.length > this._max) this._msgs.shift(); }
796
+ messages() {
797
+ const sys = this._system ? [{role:'system',content:this._system}] : [];
798
+ return [...sys, ...this._msgs];
799
+ }
800
+ clear() { this._msgs=[]; return this; }
801
+ get length() { return this._msgs.length; }
802
+ }
803
+
804
+ class TokenBudgetMemory {
805
+ /** @param {number} budget – max tokens for the message history */
806
+ constructor(budget=3000, model='gpt-4o-mini') {
807
+ this._budget = budget;
808
+ this._model = model;
809
+ this._msgs = [];
810
+ this._system = null;
811
+ }
812
+ setSystem(content) { this._system = content; return this; }
813
+ add(role, content) { this._msgs.push({role,content}); this._trim(); return this; }
814
+ addUser(content) { return this.add('user',content); }
815
+ addAssistant(content) { return this.add('assistant',content); }
816
+ addResult(res) { return this.addAssistant(res.content||String(res)); }
817
+ _trim() {
818
+ while(tokenizer.estimateMessages(this.messages()) > this._budget && this._msgs.length > 1)
819
+ this._msgs.shift();
820
+ }
821
+ messages() {
822
+ const sys = this._system ? [{role:'system',content:this._system}] : [];
823
+ return [...sys, ...this._msgs];
824
+ }
825
+ clear() { this._msgs=[]; return this; }
826
+ tokenUsage(){ return tokenizer.estimateMessages(this.messages()); }
827
+ }
828
+
829
+ class SummaryMemory {
830
+ /**
831
+ * When history exceeds `triggerTurns` turns, the client is asked to summarise
832
+ * older messages, which are then replaced with a single "summary" system note.
833
+ * @param {KitAIClient} client
834
+ * @param {number} triggerTurns
835
+ */
836
+ constructor(client, triggerTurns=10) {
837
+ this._client = client;
838
+ this._trigger = triggerTurns * 2;
839
+ this._msgs = [];
840
+ this._summary = '';
841
+ this._system = '';
842
+ }
843
+ setSystem(content) { this._system=content; return this; }
844
+ add(role,content) { this._msgs.push({role,content}); return this; }
845
+ addUser(content) { return this.add('user',content); }
846
+ addAssistant(content) { return this.add('assistant',content); }
847
+ addResult(res) { return this.addAssistant(res.content||String(res)); }
848
+ async messages() {
849
+ if (this._msgs.length >= this._trigger) await this._summarise();
850
+ const parts = [];
851
+ if (this._system) parts.push({role:'system', content:this._system});
852
+ if (this._summary) parts.push({role:'system', content:`Conversation so far:\n${this._summary}`});
853
+ return [...parts, ...this._msgs];
854
+ }
855
+ async _summarise() {
856
+ const toCompress = this._msgs.splice(0, Math.floor(this._trigger/2));
857
+ const prompt = `Summarise this conversation concisely:\n${toCompress.map(m=>`${m.role}: ${m.content}`).join('\n')}`;
858
+ const res = await this._client.chat([{role:'user',content:prompt}]);
859
+ this._summary = (this._summary ? this._summary+'\n' : '') + res.content;
860
+ }
861
+ clear() { this._msgs=[]; this._summary=''; return this; }
862
+ }
863
+
864
+ // ─────────────────────────────────────────────────────────────────────────────
865
+ // §9 Vector store (in-memory cosine similarity)
866
+ // ─────────────────────────────────────────────────────────────────────────────
867
+
868
+ class VectorStore {
869
+ constructor() {
870
+ this._docs = []; // [{id, text, embedding, metadata}]
871
+ }
872
+
873
+ /** Add a document with its embedding */
874
+ add(id, text, embedding, metadata={}) {
875
+ this._docs.push({ id, text, embedding: _norm(embedding), metadata });
876
+ return this;
877
+ }
878
+
879
+ /** Add many documents at once */
880
+ addMany(docs) {
881
+ for (const d of docs) this.add(d.id||`doc_${this._docs.length}`, d.text, d.embedding, d.metadata);
882
+ return this;
883
+ }
884
+
885
+ /** Cosine similarity search */
886
+ search(queryEmbedding, topK=5, filter=null) {
887
+ const q = _norm(queryEmbedding);
888
+ return this._docs
889
+ .filter(d => filter ? filter(d.metadata) : true)
890
+ .map(d => ({ ...d, score: _dotProduct(q, d.embedding) }))
891
+ .sort((a,b) => b.score - a.score)
892
+ .slice(0, topK);
893
+ }
894
+
895
+ /** Remove by id */
896
+ remove(id) { this._docs = this._docs.filter(d=>d.id!==id); return this; }
897
+
898
+ /** Update metadata */
899
+ updateMetadata(id, meta) {
900
+ const d = this._docs.find(d=>d.id===id);
901
+ if (d) d.metadata = { ...d.metadata, ...meta };
902
+ return this;
903
+ }
904
+
905
+ get size() { return this._docs.length; }
906
+ clear() { this._docs=[]; return this; }
907
+
908
+ /** Export as JSON-serialisable array */
909
+ export() { return this._docs.map(d=>({ ...d, embedding: Array.from(d.embedding) })); }
910
+
911
+ /** Import from export() */
912
+ static import(data) {
913
+ const vs = new VectorStore();
914
+ for (const d of data) vs.add(d.id, d.text, d.embedding, d.metadata);
915
+ return vs;
916
+ }
917
+ }
918
+
919
+ // ─────────────────────────────────────────────────────────────────────────────
920
+ // §10 Document chunker
921
+ // ─────────────────────────────────────────────────────────────────────────────
922
+
923
+ const chunker = {
924
+ /** Fixed-size character chunks with optional overlap */
925
+ fixed(text, size=500, overlap=50) {
926
+ const chunks=[];
927
+ for(let i=0;i<text.length;i+=size-overlap)
928
+ chunks.push(text.slice(i,i+size));
929
+ return chunks;
930
+ },
931
+
932
+ /** Split on sentence boundaries */
933
+ sentence(text, maxChunkSize=1000) {
934
+ const sents = text.match(/[^.!?]+[.!?]+[\s]*/g) || [text];
935
+ const chunks=[]; let cur='';
936
+ for(const s of sents){
937
+ if((cur+s).length>maxChunkSize && cur){ chunks.push(cur.trim()); cur=''; }
938
+ cur+=s;
939
+ }
940
+ if(cur.trim()) chunks.push(cur.trim());
941
+ return chunks;
942
+ },
943
+
944
+ /** Split on paragraph boundaries (double newline) */
945
+ paragraph(text, maxChunkSize=2000) {
946
+ const paras = text.split(/\n{2,}/).map(p=>p.trim()).filter(Boolean);
947
+ const chunks=[]; let cur='';
948
+ for(const p of paras){
949
+ if((cur+'\n\n'+p).length>maxChunkSize && cur){ chunks.push(cur.trim()); cur=''; }
950
+ cur = cur ? cur+'\n\n'+p : p;
951
+ }
952
+ if(cur.trim()) chunks.push(cur.trim());
953
+ return chunks;
954
+ },
955
+
956
+ /** Recursive character text splitter (LangChain-style) */
957
+ recursive(text, chunkSize=500, overlap=50, separators=['\n\n','\n',' ','']) {
958
+ for(const sep of separators){
959
+ const parts = sep ? text.split(sep) : [...text];
960
+ if(parts.length > 1){
961
+ const chunks=[]; let cur='';
962
+ for(const p of parts){
963
+ const joined = cur ? cur+sep+p : p;
964
+ if(joined.length <= chunkSize) { cur=joined; }
965
+ else {
966
+ if(cur) chunks.push(cur);
967
+ cur = p.length<=chunkSize ? p : chunker.recursive(p,chunkSize,overlap,separators.slice(1))[0];
968
+ }
969
+ }
970
+ if(cur) chunks.push(cur);
971
+ if(chunks.length > 1) return chunks;
972
+ }
973
+ }
974
+ // Fall back to fixed
975
+ return chunker.fixed(text, chunkSize, overlap);
976
+ },
977
+
978
+ /** Token-aware chunking */
979
+ byTokens(text, maxTokens=256, overlap=20) {
980
+ const words=text.split(/\s+/);
981
+ const chunks=[]; let cur=[]; let curTok=0;
982
+ for(const w of words){
983
+ const wt=tokenizer.estimate(w);
984
+ if(curTok+wt>maxTokens && cur.length){
985
+ chunks.push(cur.join(' '));
986
+ const keep=Math.floor(overlap/2);
987
+ cur=cur.slice(-keep); curTok=tokenizer.estimate(cur.join(' '));
988
+ }
989
+ cur.push(w); curTok+=wt;
990
+ }
991
+ if(cur.length) chunks.push(cur.join(' '));
992
+ return chunks;
993
+ },
994
+ };
995
+
996
+ // ─────────────────────────────────────────────────────────────────────────────
997
+ // §11 RAG pipeline
998
+ // ─────────────────────────────────────────────────────────────────────────────
999
+
1000
+ class RAGPipeline {
1001
+ /**
1002
+ * @param {KitAIClient} client
1003
+ * @param {VectorStore} [store]
1004
+ * @param {object} [opts]
1005
+ * @param {string} [opts.chunkMode] 'sentence'|'paragraph'|'fixed'|'recursive'
1006
+ * @param {number} [opts.chunkSize]
1007
+ * @param {number} [opts.topK]
1008
+ * @param {string} [opts.embedModel]
1009
+ * @param {string} [opts.chatModel]
1010
+ * @param {string} [opts.promptTemplate]
1011
+ */
1012
+ constructor(client, store=new VectorStore(), opts={}) {
1013
+ this._client = client;
1014
+ this._store = store;
1015
+ this._opts = { chunkMode:'sentence', chunkSize:500, topK:5,
1016
+ embedModel:'text-embedding-3-small', chatModel:'', ...opts };
1017
+ }
1018
+
1019
+ /** Ingest a document: chunk → embed → store */
1020
+ async ingest(text, metadata={}) {
1021
+ const mode = this._opts.chunkMode;
1022
+ const chunks= chunker[mode]?.(text, this._opts.chunkSize) || chunker.sentence(text);
1023
+ const vecs = await this._client.embed(chunks, { model:this._opts.embedModel });
1024
+ for(let i=0;i<chunks.length;i++){
1025
+ const id=`${metadata.source||'doc'}_${Date.now()}_${i}`;
1026
+ this._store.add(id, chunks[i], vecs[i] || vecs[0], { ...metadata, chunkIndex:i });
1027
+ }
1028
+ return { chunks: chunks.length, stored: chunks.length };
1029
+ }
1030
+
1031
+ /** Query the RAG pipeline: embed query → retrieve → augment → generate */
1032
+ async query(question, opts={}) {
1033
+ const [qvec] = await this._client.embed(question, { model:this._opts.embedModel });
1034
+ const results = this._store.search(qvec, opts.topK || this._opts.topK, opts.filter);
1035
+ const context = results.map((r,i)=>`[${i+1}] ${r.text}`).join('\n\n');
1036
+ const tpl = opts.promptTemplate || this._opts.promptTemplate ||
1037
+ `Answer the question using only the context below.\n\nContext:\n{context}\n\nQuestion: {question}\n\nAnswer:`;
1038
+ const prompt = tpl.replace('{context}',context).replace('{question}',question);
1039
+ const response = await this._client.chat([{role:'user',content:prompt}],
1040
+ {model: opts.model||this._opts.chatModel});
1041
+ return { response, sources: results, context };
1042
+ }
1043
+
1044
+ get store() { return this._store; }
1045
+ }
1046
+
1047
+ // ─────────────────────────────────────────────────────────────────────────────
1048
+ // §12 Prompt template engine
1049
+ // ─────────────────────────────────────────────────────────────────────────────
1050
+
1051
+ class PromptTemplate {
1052
+ /**
1053
+ * @param {string} template – "{var}" for variables, "{{#if cond}}...{{/if}}" for blocks
1054
+ * @param {object} [defaults]
1055
+ */
1056
+ constructor(template, defaults={}) {
1057
+ this._tpl = template;
1058
+ this._defaults = defaults;
1059
+ }
1060
+
1061
+ /** Render the template with given variables */
1062
+ render(vars={}) {
1063
+ const v = { ...this._defaults, ...vars };
1064
+ let out = this._tpl;
1065
+
1066
+ // {{#if key}}...{{/if}}
1067
+ out = out.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, block) =>
1068
+ v[key] ? block : '');
1069
+
1070
+ // {{#each key}}...{{/each}} — expects v[key] to be an array
1071
+ out = out.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, key, block) =>
1072
+ (Array.isArray(v[key]) ? v[key] : []).map(item =>
1073
+ block.replace(/\{\{this\}\}/g, item)).join(''));
1074
+
1075
+ // {var|filter} filters: upper, lower, trim, json, truncate:N
1076
+ out = out.replace(/\{(\w+)(?:\|([^}]+))?\}/g, (_, key, filter) => {
1077
+ let val = v[key] !== undefined ? String(v[key]) : '';
1078
+ if (filter) {
1079
+ for (const f of filter.split(',')) {
1080
+ const [fn, arg] = f.trim().split(':');
1081
+ if (fn==='upper') val=val.toUpperCase();
1082
+ else if (fn==='lower') val=val.toLowerCase();
1083
+ else if (fn==='trim') val=val.trim();
1084
+ else if (fn==='json') val=JSON.stringify(v[key]);
1085
+ else if (fn==='truncate') val=val.slice(0,+(arg||100));
1086
+ else if (fn==='indent') val=val.split('\n').join('\n'+' '.repeat(+(arg||1)));
1087
+ }
1088
+ }
1089
+ return val;
1090
+ });
1091
+ return out;
1092
+ }
1093
+
1094
+ /** Render into a messages array ready for the client */
1095
+ toMessages(vars={}, role='user') {
1096
+ return [{ role, content: this.render(vars) }];
1097
+ }
1098
+
1099
+ /** Compose two templates */
1100
+ concat(other, separator='\n') {
1101
+ return new PromptTemplate(this._tpl + separator + other._tpl, { ...this._defaults, ...other._defaults });
1102
+ }
1103
+
1104
+ toString() { return this._tpl; }
1105
+
1106
+ static from(strings, ...keys) {
1107
+ // Tagged template literal support: PromptTemplate.from`Hello {name}!`
1108
+ const tpl = strings.reduce((acc,s,i)=>acc+s+(keys[i]||''),'');
1109
+ return new PromptTemplate(tpl);
1110
+ }
1111
+ }
1112
+
1113
+ // ─────────────────────────────────────────────────────────────────────────────
1114
+ // §13 Few-shot builder
1115
+ // ─────────────────────────────────────────────────────────────────────────────
1116
+
1117
+ class FewShotBuilder {
1118
+ constructor(instructionOrTemplate='', opts={}) {
1119
+ this._instruction = instructionOrTemplate;
1120
+ this._examples = [];
1121
+ this._inputKey = opts.inputKey || 'Input';
1122
+ this._outputKey = opts.outputKey || 'Output';
1123
+ this._separator = opts.separator || '\n\n';
1124
+ this._exampleTpl = opts.exampleTemplate || null;
1125
+ }
1126
+ /** Add an example { input, output } */
1127
+ add(input, output) { this._examples.push({input,output}); return this; }
1128
+ addMany(pairs) { for(const [i,o] of pairs) this.add(i,o); return this; }
1129
+
1130
+ build(query, extraSystem='') {
1131
+ const exStr = this._examples.map(e =>
1132
+ this._exampleTpl
1133
+ ? this._exampleTpl.replace('{input}',e.input).replace('{output}',e.output)
1134
+ : `${this._inputKey}: ${e.input}\n${this._outputKey}: ${e.output}`
1135
+ ).join(this._separator);
1136
+ const system = [this._instruction, extraSystem, exStr].filter(Boolean).join('\n\n');
1137
+ return [
1138
+ { role:'system', content: system },
1139
+ { role:'user', content: `${this._inputKey}: ${query}` },
1140
+ ];
1141
+ }
1142
+ }
1143
+
1144
+ // ─────────────────────────────────────────────────────────────────────────────
1145
+ // §14 System prompt library
1146
+ // ─────────────────────────────────────────────────────────────────────────────
1147
+
1148
+ const systemPrompts = {
1149
+ assistant: 'You are a helpful, accurate, and concise AI assistant.',
1150
+ coder: 'You are an expert software engineer. Write clean, efficient, well-commented code. When asked to fix bugs, explain the root cause. Always prefer readability over cleverness.',
1151
+ analyst: 'You are a senior data analyst. Provide structured, evidence-based analysis. Use bullet points, tables, and clear section headers. Always cite your reasoning.',
1152
+ writer: 'You are a professional writer and editor. Produce clear, engaging, and well-structured prose. Adapt tone and style to the audience. Avoid jargon unless appropriate.',
1153
+ tutor: 'You are a patient and encouraging tutor. Break complex concepts into simple steps. Use analogies and examples. Check for understanding and adjust explanations accordingly.',
1154
+ reviewer: 'You are a critical code reviewer. Identify bugs, security issues, performance problems, and style violations. Be specific and constructive.',
1155
+ planner: 'You are a strategic planner. Break goals into actionable steps. Consider risks, dependencies, and timelines. Output structured plans with clear milestones.',
1156
+ summariser: 'You are a summarisation expert. Distil content to its core ideas. Be concise, accurate, and preserve the most important details. Never fabricate.',
1157
+ debater: 'You are a debate coach. Present the strongest version of any argument, acknowledge counterarguments, and reason to a clear conclusion.',
1158
+ socratic: 'You are a Socratic guide. Help the user discover answers through questions rather than providing them directly.',
1159
+ translator: 'You are a professional translator. Preserve meaning, tone, and nuance. Flag idioms or culturally specific content that may not translate directly.',
1160
+ jsonFormatter: 'You respond ONLY with valid JSON. No explanation, no markdown, no prose. Ensure the JSON is syntactically correct and matches any schema provided.',
1161
+ classifier: 'You are a classification engine. You respond ONLY with the category label — no explanation, no prose, no quotes.',
1162
+ extractor: 'You are an information extraction engine. Extract the requested fields from the text and return them as a JSON object. If a field is not present, use null.',
1163
+ factChecker: 'You are a rigorous fact-checker. Evaluate claims for accuracy. Cite reasoning. Distinguish between verified facts, likely facts, uncertain claims, and falsehoods.',
1164
+ safeguard: 'You are a content safety evaluator. Identify harmful, toxic, or policy-violating content. Output a JSON object with fields: safe (bool), categories (string[]), explanation (string).',
1165
+ persona: 'You are {name}, {description}. Speak in first person and stay in character at all times.',
1166
+ scientist: 'You are a research scientist. Reason carefully and empirically. Distinguish between hypotheses, evidence, and established fact. Prefer precision over speculation.',
1167
+ lawyer: 'You are a careful legal analyst. Identify relevant legal principles, flag ambiguities, and note jurisdictional differences. Always recommend consulting a qualified attorney.',
1168
+ doctor: 'You are a clinical information assistant. Provide accurate medical information while emphasising that users should consult licensed healthcare professionals for personal advice.',
1169
+ productManager: 'You are a seasoned product manager. Think in terms of user value, feasibility, and business impact. Use frameworks like RICE, MoSCoW, and Jobs-to-be-Done when appropriate.',
1170
+ recruiter: 'You are an expert technical recruiter. Evaluate candidates fairly, identify skill gaps constructively, and always provide actionable feedback.',
1171
+ designer: 'You are a UX/UI design expert. Prioritise user needs, accessibility, and visual clarity. Reference design principles like Gestalt, Fitts\'s Law, and Jakob\'s Law.',
1172
+ marketer: 'You are a creative marketing strategist. Generate compelling copy, campaign ideas, and growth strategies grounded in consumer psychology.',
1173
+ therapist: 'You are a supportive, empathetic counsellor. Listen actively, reflect feelings, and guide without judging. Always recommend professional help for serious concerns.',
1174
+ chef: 'You are a professional chef. Provide precise recipes, cooking techniques, and ingredient substitutions. Consider dietary restrictions proactively.',
1175
+ mathematician: 'You are a mathematics expert. Show your work step by step. Use LaTeX notation where helpful. Verify answers and flag any assumptions.',
1176
+ historian: 'You are a meticulous historian. Ground answers in primary sources. Contextualise events within broader historical patterns. Acknowledge historiographical debates.',
1177
+ poet: 'You are a gifted poet. Craft vivid, emotionally resonant writing. Adapt to requested forms (sonnet, haiku, free verse, etc.) and themes.',
1178
+ custom: (content) => content,
1179
+ };
1180
+
1181
+ // ─────────────────────────────────────────────────────────────────────────────
1182
+ // §15 Chain-of-thought / ReAct scaffolds
1183
+ // ─────────────────────────────────────────────────────────────────────────────
1184
+
1185
+ const scaffolds = {
1186
+ chainOfThought(question, opts={}) {
1187
+ const style = opts.style || 'standard'; // 'standard' | 'stepback' | 'selfconsistency'
1188
+ if (style==='stepback') return [
1189
+ {role:'user', content:`Before answering: "${question}", first answer this broader question that would help: What general principle or concept is needed to answer this?`},
1190
+ ];
1191
+ return [{role:'user', content:`Think step by step:\n\n${question}\n\nLet's reason through this carefully:`}];
1192
+ },
1193
+
1194
+ react(question, toolNames=[]) {
1195
+ const tools = toolNames.length ? `Available tools: ${toolNames.join(', ')}.` : '';
1196
+ return [{role:'system', content:[
1197
+ 'You are a ReAct agent. For each step output exactly:',
1198
+ 'Thought: <your reasoning>',
1199
+ 'Action: <tool_name>(<args>)',
1200
+ 'Observation: <result>',
1201
+ '...repeat until...',
1202
+ 'Final Answer: <answer>',
1203
+ tools,
1204
+ ].filter(Boolean).join('\n')},
1205
+ {role:'user', content: question}];
1206
+ },
1207
+
1208
+ selfCritique(originalPrompt, response) {
1209
+ return [{role:'user', content:`Original question: ${originalPrompt}\n\nYour previous answer:\n${response}\n\nCritically review your answer. Identify any errors, gaps, or improvements. Then provide a revised, improved answer.`}];
1210
+ },
1211
+
1212
+ treeOfThought(question, branches=3) {
1213
+ return [{role:'user', content:`Consider ${branches} different approaches to answering:\n\n"${question}"\n\nFor each approach:\n1. Outline the approach\n2. Reason through it\n3. Evaluate its strengths and weaknesses\n\nThen select the best approach and provide a final answer.`}];
1214
+ },
1215
+
1216
+ planAndSolve(question) {
1217
+ return [{role:'user', content:`Let's solve this step by step.\n\nProblem: ${question}\n\nStep 1: Devise a plan with numbered sub-steps.\nStep 2: Execute each sub-step in order.\nStep 3: Verify the final answer.\n\nBegin:`}];
1218
+ },
1219
+ };
1220
+
1221
+ // ─────────────────────────────────────────────────────────────────────────────
1222
+ // §16 Sequential chain
1223
+ // ─────────────────────────────────────────────────────────────────────────────
1224
+
1225
+ class SequentialChain {
1226
+ constructor(client, opts={}) {
1227
+ this._client = client;
1228
+ this._steps = [];
1229
+ this._opts = opts;
1230
+ }
1231
+
1232
+ /** Add a step. `step` can be:
1233
+ * - a string prompt template (uses {input} for previous output)
1234
+ * - a PromptTemplate instance
1235
+ * - an async function(previousOutput) => messages[]
1236
+ */
1237
+ step(step, opts={}) { this._steps.push({step, opts}); return this; }
1238
+
1239
+ async run(initialInput, ctx={}) {
1240
+ let current = initialInput;
1241
+ const history = [];
1242
+ for (const {step, opts} of this._steps) {
1243
+ let messages;
1244
+ if (typeof step === 'function') {
1245
+ messages = await step(current, ctx);
1246
+ } else if (step instanceof PromptTemplate) {
1247
+ messages = step.toMessages({ input: current, ...ctx });
1248
+ } else {
1249
+ messages = [{ role:'user', content: String(step).replace(/\{input\}/g, current) }];
1250
+ }
1251
+ const res = await this._client.chat(messages, { ...this._opts, ...opts });
1252
+ history.push({ input: current, output: res.content, response: res });
1253
+ current = res.content;
1254
+ }
1255
+ return { output: current, history };
1256
+ }
1257
+ }
1258
+
1259
+ // ─────────────────────────────────────────────────────────────────────────────
1260
+ // §17 Parallel chain
1261
+ // ─────────────────────────────────────────────────────────────────────────────
1262
+
1263
+ class ParallelChain {
1264
+ constructor(client, opts={}) {
1265
+ this._client = client;
1266
+ this._branches= [];
1267
+ this._opts = opts;
1268
+ }
1269
+
1270
+ branch(name, messagesOrFn) { this._branches.push({name, fn: messagesOrFn}); return this; }
1271
+
1272
+ async run(input, mergeFn=null) {
1273
+ const results = await Promise.all(this._branches.map(async ({name, fn}) => {
1274
+ const messages = typeof fn==='function' ? await fn(input) : fn;
1275
+ const res = await this._client.chat(messages, this._opts);
1276
+ return { name, response: res, output: res.content };
1277
+ }));
1278
+ const merged = mergeFn ? await mergeFn(results, this._client) : results;
1279
+ return { branches: results, merged };
1280
+ }
1281
+ }
1282
+
1283
+ // ─────────────────────────────────────────────────────────────────────────────
1284
+ // §18 Tool registry & ReAct agent
1285
+ // ─────────────────────────────────────────────────────────────────────────────
1286
+
1287
+ class ToolRegistry {
1288
+ constructor() { this._tools = new Map(); }
1289
+
1290
+ /**
1291
+ * Register a tool.
1292
+ * @param {string} name
1293
+ * @param {string} description
1294
+ * @param {Function} fn – sync or async (args) => result
1295
+ * @param {object} [schema] – JSON schema for args
1296
+ */
1297
+ register(name, description, fn, schema={}) {
1298
+ this._tools.set(name, { name, description, fn, schema });
1299
+ return this;
1300
+ }
1301
+
1302
+ unregister(name) { this._tools.delete(name); return this; }
1303
+ has(name) { return this._tools.has(name); }
1304
+ get(name) { return this._tools.get(name); }
1305
+ list() { return [...this._tools.values()].map(t=>({ name:t.name, description:t.description, schema:t.schema })); }
1306
+
1307
+ async call(name, args) {
1308
+ const tool = this._tools.get(name);
1309
+ if (!tool) throw new Error(`Tool not found: ${name}`);
1310
+ return tool.fn(args);
1311
+ }
1312
+
1313
+ /** Convert to OpenAI tool_choice format */
1314
+ toOpenAISchema() {
1315
+ return [...this._tools.values()].map(t=>({
1316
+ type:'function',
1317
+ function:{ name:t.name, description:t.description, parameters: t.schema||{type:'object',properties:{}} },
1318
+ }));
1319
+ }
1320
+ }
1321
+
1322
+ // Built-in tools
1323
+ const _builtinTools = {
1324
+ calculator: {
1325
+ description: 'Evaluate a mathematical expression. Args: { expression: string }',
1326
+ fn: ({expression}) => {
1327
+ // Safe-ish eval: only allow math chars
1328
+ if (!/^[\d+\-*/().\s%^eE,]+$/.test(expression)) return 'Error: invalid expression';
1329
+ try { return String(Function(`"use strict"; return (${expression})`)()) } catch(e) { return 'Error: '+e.message; }
1330
+ },
1331
+ },
1332
+ datetime: {
1333
+ description: 'Get current date/time info. Args: { timezone?: string, format?: "iso"|"human"|"unix" }',
1334
+ fn: ({timezone='UTC', format='iso'}) => {
1335
+ const d = new Date();
1336
+ if(format==='unix') return String(Math.floor(d.getTime()/1000));
1337
+ if(format==='human') return d.toLocaleString('en-US',{timeZone:timezone});
1338
+ return d.toISOString();
1339
+ },
1340
+ },
1341
+ jsonPath: {
1342
+ description: 'Extract a value from a JSON string using dot notation. Args: { json: string, path: string }',
1343
+ fn: ({json, path}) => {
1344
+ try {
1345
+ const obj = typeof json==='string' ? JSON.parse(json) : json;
1346
+ return String(path.split('.').reduce((o,k)=>o?.[k],obj) ?? 'null');
1347
+ } catch(e) { return 'Error: '+e.message; }
1348
+ },
1349
+ },
1350
+ regex: {
1351
+ description: 'Test or extract regex matches. Args: { text: string, pattern: string, flags?: string, mode?: "test"|"match"|"replace", replacement?: string }',
1352
+ fn: ({text, pattern, flags='g', mode='match', replacement=''}) => {
1353
+ const re = new RegExp(pattern, flags);
1354
+ if(mode==='test') return String(re.test(text));
1355
+ if(mode==='replace') return text.replace(re, replacement);
1356
+ const m = text.match(re);
1357
+ return m ? JSON.stringify(m) : 'null';
1358
+ },
1359
+ },
1360
+ base64: {
1361
+ description: 'Encode or decode base64. Args: { text: string, mode: "encode"|"decode" }',
1362
+ fn: ({text, mode='encode'}) => {
1363
+ if(mode==='encode') return typeof btoa!=='undefined' ? btoa(text) : Buffer.from(text).toString('base64');
1364
+ return typeof atob!=='undefined' ? atob(text) : Buffer.from(text,'base64').toString('utf8');
1365
+ },
1366
+ },
1367
+ uuid: {
1368
+ description: 'Generate a UUID v4. Args: {}',
1369
+ fn: () => {
1370
+ if(typeof crypto!=='undefined' && crypto.randomUUID) return crypto.randomUUID();
1371
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{
1372
+ const r=Math.random()*16|0; return (c==='x'?r:(r&0x3|0x8)).toString(16);
1373
+ });
1374
+ },
1375
+ },
1376
+ hash: {
1377
+ description: 'FNV-1a hash of a string. Args: { text: string }',
1378
+ fn: ({text}) => _md5Hex(text),
1379
+ },
1380
+ webSearch: {
1381
+ description: 'Search the web (stub — returns a placeholder; connect to a real API). Args: { query: string, topK?: number }',
1382
+ fn: ({query, topK=3}) => JSON.stringify({ query, note:'web_search stub — connect to SerpAPI/Brave/Tavily', results:[] }),
1383
+ },
1384
+ };
1385
+
1386
+ // ─────────────────────────────────────────────────────────────────────────────
1387
+ // §19 ReAct agent
1388
+ // ─────────────────────────────────────────────────────────────────────────────
1389
+
1390
+ class ReactAgent {
1391
+ /**
1392
+ * @param {KitAIClient} client
1393
+ * @param {ToolRegistry} registry
1394
+ * @param {object} [opts]
1395
+ * @param {number} [opts.maxSteps]
1396
+ * @param {string} [opts.model]
1397
+ * @param {boolean} [opts.verbose]
1398
+ */
1399
+ constructor(client, registry, opts={}) {
1400
+ this._client = client;
1401
+ this._registry = registry;
1402
+ this._maxSteps = opts.maxSteps || 10;
1403
+ this._model = opts.model || '';
1404
+ this._verbose = opts.verbose || false;
1405
+ }
1406
+
1407
+ async run(question) {
1408
+ const messages = scaffolds.react(question, this._registry.list().map(t=>t.name));
1409
+ const trace = [];
1410
+ let steps = 0;
1411
+
1412
+ while (steps++ < this._maxSteps) {
1413
+ const res = await this._client.chat(messages, { model:this._model });
1414
+ const txt = res.content;
1415
+ messages.push({ role:'assistant', content:txt });
1416
+ if(this._verbose) console.log(`[ReAct step ${steps}]\n${txt}`);
1417
+
1418
+ // Parse Final Answer
1419
+ const finalMatch = txt.match(/Final Answer:\s*([\s\S]+)/i);
1420
+ if (finalMatch) return { answer: finalMatch[1].trim(), trace, steps };
1421
+
1422
+ // Parse Action: tool_name(json_or_text)
1423
+ const actionMatch = txt.match(/Action:\s*(\w+)\(([\s\S]*?)\)(?:\n|$)/i);
1424
+ if (actionMatch) {
1425
+ const toolName = actionMatch[1];
1426
+ let args = {};
1427
+ try { args = JSON.parse(actionMatch[2]); } catch { args = { input: actionMatch[2] }; }
1428
+ let observation;
1429
+ try { observation = await this._registry.call(toolName, args); }
1430
+ catch (e) { observation = `Error: ${e.message}`; }
1431
+ trace.push({ thought:txt, action:toolName, args, observation });
1432
+ messages.push({ role:'user', content:`Observation: ${observation}` });
1433
+ } else {
1434
+ // No action found – treat as final
1435
+ return { answer: txt, trace, steps };
1436
+ }
1437
+ }
1438
+ return { answer:'Max steps reached', trace, steps };
1439
+ }
1440
+ }
1441
+
1442
+ // ─────────────────────────────────────────────────────────────────────────────
1443
+ // §20 Structured output — schema builder & JSON enforcer
1444
+ // ─────────────────────────────────────────────────────────────────────────────
1445
+
1446
+ const schema = {
1447
+ string: (desc='') => ({ type:'string', description:desc }),
1448
+ number: (desc='') => ({ type:'number', description:desc }),
1449
+ boolean: (desc='') => ({ type:'boolean', description:desc }),
1450
+ array: (items, desc='') => ({ type:'array', items, description:desc }),
1451
+ object: (properties, required=[], desc='') => ({ type:'object', properties, required, description:desc }),
1452
+ enum: (values, desc='') => ({ type:'string', enum:values, description:desc }),
1453
+ nullable:(inner) => ({ ...inner, nullable:true }),
1454
+
1455
+ /** Generate a JSON instruction string from a schema object */
1456
+ toInstruction(schemaObj, name='response') {
1457
+ return `Respond ONLY with a valid JSON object matching this schema for "${name}":\n${JSON.stringify(schemaObj, null, 2)}\nNo explanation, no markdown, just the JSON.`;
1458
+ },
1459
+
1460
+ /** Validate a value against a simple schema (subset of JSON Schema) */
1461
+ validate(value, schemaObj) {
1462
+ const errors = [];
1463
+ function check(val, s, path='') {
1464
+ if(s.nullable && (val===null||val===undefined)) return;
1465
+ if(s.type==='string' && typeof val!=='string') errors.push(`${path}: expected string, got ${typeof val}`);
1466
+ if(s.type==='number' && typeof val!=='number') errors.push(`${path}: expected number, got ${typeof val}`);
1467
+ if(s.type==='boolean' && typeof val!=='boolean') errors.push(`${path}: expected boolean, got ${typeof val}`);
1468
+ if(s.type==='array'){
1469
+ if(!Array.isArray(val)) errors.push(`${path}: expected array`);
1470
+ else val.forEach((item,i)=>check(item,s.items||{},`${path}[${i}]`));
1471
+ }
1472
+ if(s.type==='object'){
1473
+ if(typeof val!=='object'||Array.isArray(val)) errors.push(`${path}: expected object`);
1474
+ else {
1475
+ for(const req of (s.required||[])) if(!(req in val)) errors.push(`${path}.${req}: required`);
1476
+ for(const [k,sub] of Object.entries(s.properties||{})) if(k in val) check(val[k],sub,`${path}.${k}`);
1477
+ }
1478
+ }
1479
+ if(s.enum && !s.enum.includes(val)) errors.push(`${path}: must be one of ${s.enum.join(',')}`);
1480
+ }
1481
+ check(value, schemaObj);
1482
+ return { valid: errors.length===0, errors };
1483
+ },
1484
+ };
1485
+
1486
+ class JSONEnforcer {
1487
+ /**
1488
+ * @param {KitAIClient} client
1489
+ * @param {object} schemaObj – KitAI schema or raw JSON Schema
1490
+ * @param {number} maxRetries
1491
+ */
1492
+ constructor(client, schemaObj, maxRetries=3) {
1493
+ this._client = client;
1494
+ this._schema = schemaObj;
1495
+ this._retries = maxRetries;
1496
+ }
1497
+
1498
+ async run(messages, opts={}) {
1499
+ const systemMsg = { role:'system', content: schema.toInstruction(this._schema) };
1500
+ const msgs = [systemMsg, ...messages];
1501
+ for (let attempt=0; attempt<=this._retries; attempt++) {
1502
+ const res = await this._client.chat(msgs, { ...opts, jsonMode: true });
1503
+ const json = res.tryJSON();
1504
+ if (json !== null) {
1505
+ const v = schema.validate(json, this._schema);
1506
+ if (v.valid) return { data:json, response:res, attempt };
1507
+ // Feed validation errors back
1508
+ msgs.push({ role:'assistant', content:res.content });
1509
+ msgs.push({ role:'user', content:`Your JSON was invalid:\n${v.errors.join('\n')}\nPlease fix and respond with valid JSON only.` });
1510
+ } else {
1511
+ msgs.push({ role:'assistant', content:res.content });
1512
+ msgs.push({ role:'user', content:'Your response was not valid JSON. Please respond with JSON only.' });
1513
+ }
1514
+ }
1515
+ throw new Error('JSONEnforcer: max retries exceeded');
1516
+ }
1517
+ }
1518
+
1519
+ // ─────────────────────────────────────────────────────────────────────────────
1520
+ // §21 Output parsers
1521
+ // ─────────────────────────────────────────────────────────────────────────────
1522
+
1523
+ const parsers = {
1524
+ /** Parse JSON, stripping markdown fences */
1525
+ json(text) {
1526
+ const clean = text.replace(/^```(?:json)?\n?/,'').replace(/\n?```$/,'').trim();
1527
+ return JSON.parse(clean);
1528
+ },
1529
+
1530
+ /** Parse CSV text → array of row objects */
1531
+ csv(text, delimiter=',') {
1532
+ const lines = text.trim().split('\n');
1533
+ const headers = lines[0].split(delimiter).map(h=>h.trim().replace(/^"|"$/g,''));
1534
+ return lines.slice(1).map(l => {
1535
+ const vals = l.split(delimiter).map(v=>v.trim().replace(/^"|"$/g,''));
1536
+ return Object.fromEntries(headers.map((h,i)=>[h,vals[i]??'']));
1537
+ });
1538
+ },
1539
+
1540
+ /** Parse numbered or bulleted list → string[] */
1541
+ list(text) {
1542
+ return text.split('\n')
1543
+ .map(l=>l.replace(/^[-*•]\s+|^\d+[.)]\s+/,'').trim())
1544
+ .filter(Boolean);
1545
+ },
1546
+
1547
+ /** Parse "Key: Value" lines → object */
1548
+ keyValue(text) {
1549
+ const result = {};
1550
+ for(const line of text.split('\n')){
1551
+ const i=line.indexOf(':');
1552
+ if(i>0) result[line.slice(0,i).trim()] = line.slice(i+1).trim();
1553
+ }
1554
+ return result;
1555
+ },
1556
+
1557
+ /** Parse a markdown table → array of objects */
1558
+ markdownTable(text) {
1559
+ const lines = text.split('\n').filter(l=>l.trim().startsWith('|'));
1560
+ if(lines.length<2) return [];
1561
+ const headers = lines[0].split('|').slice(1,-1).map(h=>h.trim());
1562
+ return lines.slice(2).map(l=>{ // skip separator line
1563
+ const vals = l.split('|').slice(1,-1).map(v=>v.trim());
1564
+ return Object.fromEntries(headers.map((h,i)=>[h,vals[i]||'']));
1565
+ });
1566
+ },
1567
+
1568
+ /** Extract typed fields using an extraction prompt + JSON enforcer */
1569
+ async extract(client, text, fields, model='') {
1570
+ const schemaObj = { type:'object', properties: Object.fromEntries(
1571
+ Object.entries(fields).map(([k,v])=>[k, typeof v==='string'?{type:v}:v])
1572
+ ), required:[] };
1573
+ const msgs=[{role:'user',content:`Extract the following fields from this text:\n\n${text}\n\nFields: ${Object.keys(fields).join(', ')}`}];
1574
+ const enforcer=new JSONEnforcer(client,schemaObj);
1575
+ return enforcer.run(msgs,{model});
1576
+ },
1577
+ };
1578
+
1579
+ // ─────────────────────────────────────────────────────────────────────────────
1580
+ // §22 Classification
1581
+ // ─────────────────────────────────────────────────────────────────────────────
1582
+
1583
+ const classify = {
1584
+ /** Zero-shot classification */
1585
+ async zeroShot(client, text, labels, opts={}) {
1586
+ const labelList = labels.join(' | ');
1587
+ const msgs = [
1588
+ { role:'system', content:`Classify the input into exactly one of these categories: ${labelList}\nRespond with ONLY the category name. No explanation.` },
1589
+ { role:'user', content: text },
1590
+ ];
1591
+ const res = await client.chat(msgs, opts);
1592
+ const label = res.content.trim();
1593
+ return { label: labels.find(l=>l.toLowerCase()===label.toLowerCase()) || label, raw:res };
1594
+ },
1595
+
1596
+ /** Few-shot classification */
1597
+ async fewShot(client, text, examples, labels, opts={}) {
1598
+ const builder = new FewShotBuilder(`Classify into: ${labels.join(' | ')}`);
1599
+ for(const [input,output] of examples) builder.add(input,output);
1600
+ const msgs = builder.build(text, `Reply with only the label.`);
1601
+ const res = await client.chat(msgs, opts);
1602
+ return { label: res.content.trim(), raw:res };
1603
+ },
1604
+
1605
+ /** Multi-label classification */
1606
+ async multiLabel(client, text, labels, opts={}) {
1607
+ const msgs = [
1608
+ { role:'system', content:`From the list [${labels.join(', ')}], select ALL that apply to the input. Respond with a JSON array of matching labels only.` },
1609
+ { role:'user', content: text },
1610
+ ];
1611
+ const res = await client.chat(msgs, opts);
1612
+ try { return { labels: parsers.json(res.content), raw:res }; }
1613
+ catch { return { labels:[], raw:res }; }
1614
+ },
1615
+ };
1616
+
1617
+ // ─────────────────────────────────────────────────────────────────────────────
1618
+ // §23 Evaluation & metrics
1619
+ // ─────────────────────────────────────────────────────────────────────────────
1620
+
1621
+ const metrics = {
1622
+ /** BLEU-1 (unigram precision) */
1623
+ bleu1(reference, hypothesis) {
1624
+ const ref = reference.toLowerCase().split(/\s+/);
1625
+ const hyp = hypothesis.toLowerCase().split(/\s+/);
1626
+ const refSet = new Set(ref);
1627
+ const matches = hyp.filter(w=>refSet.has(w)).length;
1628
+ const bp = hyp.length >= ref.length ? 1 : Math.exp(1 - ref.length/hyp.length);
1629
+ return bp * matches / (hyp.length||1);
1630
+ },
1631
+
1632
+ /** ROUGE-L (longest common subsequence F1) */
1633
+ rougeL(reference, hypothesis) {
1634
+ const r=reference.toLowerCase().split(/\s+/), h=hypothesis.toLowerCase().split(/\s+/);
1635
+ const m=r.length, n=h.length;
1636
+ const dp=Array.from({length:m+1},()=>new Array(n+1).fill(0));
1637
+ for(let i=1;i<=m;i++) for(let j=1;j<=n;j++)
1638
+ dp[i][j]=r[i-1]===h[j-1]?dp[i-1][j-1]+1:Math.max(dp[i-1][j],dp[i][j-1]);
1639
+ const lcs=dp[m][n];
1640
+ const p=lcs/(n||1), rec=lcs/(m||1);
1641
+ return p+rec>0 ? 2*p*rec/(p+rec) : 0;
1642
+ },
1643
+
1644
+ /** Exact match */
1645
+ exactMatch(reference, hypothesis) {
1646
+ return reference.trim().toLowerCase()===hypothesis.trim().toLowerCase()?1:0;
1647
+ },
1648
+
1649
+ /** Token-level F1 */
1650
+ tokenF1(reference, hypothesis) {
1651
+ const r=reference.toLowerCase().split(/\s+/), h=hypothesis.toLowerCase().split(/\s+/);
1652
+ const refCount={}, hypCount={};
1653
+ for(const w of r) refCount[w]=(refCount[w]||0)+1;
1654
+ for(const w of h) hypCount[w]=(hypCount[w]||0)+1;
1655
+ let overlap=0;
1656
+ for(const w of Object.keys(hypCount)) overlap+=Math.min(hypCount[w],refCount[w]||0);
1657
+ const p=overlap/(h.length||1), rec=overlap/(r.length||1);
1658
+ return p+rec>0?2*p*rec/(p+rec):0;
1659
+ },
1660
+ };
1661
+
1662
+ // ─────────────────────────────────────────────────────────────────────────────
1663
+ // §24 LLM-as-judge
1664
+ // ─────────────────────────────────────────────────────────────────────────────
1665
+
1666
+ class LLMJudge {
1667
+ constructor(client, opts={}) {
1668
+ this._client = client;
1669
+ this._model = opts.model || '';
1670
+ this._criteria= opts.criteria || ['correctness','relevance','coherence','conciseness'];
1671
+ this._scale = opts.scale || 5;
1672
+ }
1673
+
1674
+ async evaluate(question, response, reference='', extra={}) {
1675
+ const refNote = reference ? `\nReference answer: ${reference}` : '';
1676
+ const critList = this._criteria.map(c=>`- ${c}: score 1-${this._scale}`).join('\n');
1677
+ const msgs = [{role:'user', content:
1678
+ `Evaluate this AI response on a scale of 1-${this._scale} for each criterion.\n\nQuestion: ${question}${refNote}\n\nResponse: ${response}\n\nCriteria:\n${critList}\n\nRespond as JSON: { "scores": { <criterion>: <score> }, "overall": <avg>, "rationale": "<brief>" }\n\nAdditional context: ${JSON.stringify(extra)}`
1679
+ }];
1680
+ const res = await this._client.chat(msgs, { model:this._model, jsonMode:true });
1681
+ const data = res.tryJSON() || { scores:{}, overall:0, rationale:res.content };
1682
+ return { ...data, raw:res };
1683
+ }
1684
+
1685
+ async batch(pairs, reference='') {
1686
+ return Promise.all(pairs.map(([q,r])=>this.evaluate(q,r,reference)));
1687
+ }
1688
+ }
1689
+
1690
+ // ─────────────────────────────────────────────────────────────────────────────
1691
+ // §25 Safety guard
1692
+ // ─────────────────────────────────────────────────────────────────────────────
1693
+
1694
+ const guard = {
1695
+ /** Lightweight offline PII detector (regex-based) */
1696
+ detectPII(text) {
1697
+ const patterns = {
1698
+ email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
1699
+ phone: /(\+?\d[\d\s\-().]{7,}\d)/g,
1700
+ ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
1701
+ creditCard: /\b(?:\d[ -]?){13,16}\b/g,
1702
+ ipAddress: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
1703
+ dateOfBirth: /\b\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}\b/g,
1704
+ };
1705
+ const found={};
1706
+ for(const [k,re] of Object.entries(patterns)){
1707
+ const m=text.match(re);
1708
+ if(m) found[k]=m;
1709
+ }
1710
+ return { hasPII: Object.keys(found).length>0, detections:found };
1711
+ },
1712
+
1713
+ /** Offline prompt injection heuristic */
1714
+ detectInjection(text) {
1715
+ const patterns = [
1716
+ /ignore\s+(all\s+)?previous\s+instructions/i,
1717
+ /you\s+are\s+now\s+(a\s+)?(?!an?\s+assistant)/i,
1718
+ /disregard\s+your\s+(system|instructions|prompt)/i,
1719
+ /act\s+as\s+(?:an?\s+)?(?:evil|unethical|jailbreak)/i,
1720
+ /\[INST\]|\[\/INST\]|<\|im_start\|>|<\|im_end\|>/,
1721
+ /DAN\s+mode|jailbreak|bypass\s+(?:safety|filter)/i,
1722
+ ];
1723
+ const matches = patterns.filter(p=>p.test(text));
1724
+ return { injectionDetected: matches.length>0, matchCount:matches.length };
1725
+ },
1726
+
1727
+ /** LLM-based toxicity + safety check */
1728
+ async check(client, text, opts={}) {
1729
+ const msgs=[{role:'system',content:systemPrompts.safeguard},{role:'user',content:text}];
1730
+ const res=await client.chat(msgs,{model:opts.model||'',jsonMode:true});
1731
+ const data=res.tryJSON()||{safe:null,categories:[],explanation:res.content};
1732
+ return { ...data, pii:guard.detectPII(text), injection:guard.detectInjection(text), raw:res };
1733
+ },
1734
+
1735
+ /** Hallucination grounding check */
1736
+ async groundingCheck(client, claim, context, opts={}) {
1737
+ const msgs=[{role:'user',content:
1738
+ `Does the following claim appear to be fully supported by the given context?\n\nContext:\n${context}\n\nClaim: ${claim}\n\nRespond as JSON: { "supported": true|false, "confidence": 0.0-1.0, "reasoning": "..." }`
1739
+ }];
1740
+ const res=await client.chat(msgs,{model:opts.model||'',jsonMode:true});
1741
+ return res.tryJSON()||{supported:null,confidence:0,reasoning:res.content};
1742
+ },
1743
+ };
1744
+
1745
+ // ─────────────────────────────────────────────────────────────────────────────
1746
+ // §26 Utilities
1747
+ // ─────────────────────────────────────────────────────────────────────────────
1748
+
1749
+ /** Strip markdown to plain text */
1750
+ function stripMarkdown(text) {
1751
+ return text
1752
+ .replace(/```[\s\S]*?```/g,'') // code blocks
1753
+ .replace(/`[^`]+`/g,'') // inline code
1754
+ .replace(/#{1,6}\s+/g,'') // headers
1755
+ .replace(/\*\*([^*]+)\*\*/g,'$1') // bold
1756
+ .replace(/\*([^*]+)\*/g,'$1') // italic
1757
+ .replace(/~~([^~]+)~~/g,'$1') // strikethrough
1758
+ .replace(/\[([^\]]+)\]\([^)]+\)/g,'$1')// links
1759
+ .replace(/^>\s+/gm,'') // blockquotes
1760
+ .replace(/^[-*+]\s+/gm,'') // bullets
1761
+ .replace(/^\d+\.\s+/gm,'') // numbered list
1762
+ .replace(/\n{3,}/g,'\n\n') // triple newlines
1763
+ .trim();
1764
+ }
1765
+
1766
+ /** Diff two strings — returns array of { type:'equal'|'insert'|'delete', value:string } */
1767
+ function diffText(oldText, newText) {
1768
+ const oldWords = oldText.split(/\s+/);
1769
+ const newWords = newText.split(/\s+/);
1770
+ const ops=[];
1771
+ // Simple LCS diff on word level
1772
+ const m=oldWords.length,n=newWords.length;
1773
+ const dp=Array.from({length:m+1},()=>new Array(n+1).fill(0));
1774
+ for(let i=1;i<=m;i++) for(let j=1;j<=n;j++)
1775
+ dp[i][j]=oldWords[i-1]===newWords[j-1]?dp[i-1][j-1]+1:Math.max(dp[i-1][j],dp[i][j-1]);
1776
+ let i=m,j=n;
1777
+ const result=[];
1778
+ while(i>0||j>0){
1779
+ if(i>0&&j>0&&oldWords[i-1]===newWords[j-1]){result.unshift({type:'equal',value:oldWords[--i]});j--;}
1780
+ else if(j>0&&(i===0||dp[i][j-1]>=dp[i-1][j])){result.unshift({type:'insert',value:newWords[--j]});}
1781
+ else{result.unshift({type:'delete',value:oldWords[--i]});}
1782
+ }
1783
+ return result;
1784
+ }
1785
+
1786
+ /** Semantic similarity score (requires pre-computed embeddings) */
1787
+ function semanticSimilarity(embeddingA, embeddingB) {
1788
+ return _cosineSim(embeddingA, embeddingB);
1789
+ }
1790
+
1791
+ /** Export a conversation history to various formats */
1792
+ const exporter = {
1793
+ toJSON(messages, meta={}) {
1794
+ return JSON.stringify({ meta, messages, exportedAt: new Date().toISOString() }, null, 2);
1795
+ },
1796
+ toMarkdown(messages, title='Conversation') {
1797
+ const lines=[`# ${title}\n`];
1798
+ for(const m of messages) {
1799
+ const role=m.role.charAt(0).toUpperCase()+m.role.slice(1);
1800
+ lines.push(`## ${role}\n${m.content}\n`);
1801
+ }
1802
+ return lines.join('\n');
1803
+ },
1804
+ toHTML(messages, title='Conversation') {
1805
+ const rows = messages.map(m=>{
1806
+ const bg = m.role==='user'?'#f0f4ff':m.role==='system'?'#fff8e1':'#f0fff4';
1807
+ return `<div style="background:${bg};border-radius:8px;padding:12px;margin:8px 0"><strong>${m.role}</strong><p>${m.content.replace(/\n/g,'<br>')}</p></div>`;
1808
+ }).join('');
1809
+ return `<!DOCTYPE html><html><head><title>${title}</title></head><body style="font-family:sans-serif;max-width:800px;margin:auto;padding:20px"><h1>${title}</h1>${rows}</body></html>`;
1810
+ },
1811
+ toText(messages) {
1812
+ return messages.map(m=>`[${m.role.toUpperCase()}]\n${m.content}`).join('\n\n---\n\n');
1813
+ },
1814
+ };
1815
+
1816
+ /** A/B experiment runner */
1817
+ class ABExperiment {
1818
+ /**
1819
+ * @param {KitAIClient[]} clients – two or more client/model configurations
1820
+ * @param {object} [opts]
1821
+ */
1822
+ constructor(clients, opts={}) {
1823
+ this._clients = clients;
1824
+ this._opts = opts;
1825
+ this._results = [];
1826
+ }
1827
+
1828
+ async run(messages, judge=null) {
1829
+ const responses = await Promise.all(
1830
+ this._clients.map(async (c,i) => {
1831
+ const t0 = Date.now();
1832
+ const res = await c.chat(messages, this._opts);
1833
+ res.variant = i;
1834
+ return res;
1835
+ })
1836
+ );
1837
+ let scores = null;
1838
+ if (judge instanceof LLMJudge) {
1839
+ const q = messages.filter(m=>m.role==='user').map(m=>m.content).join(' ');
1840
+ scores = await Promise.all(responses.map(r=>judge.evaluate(q,r.content)));
1841
+ }
1842
+ const result = { responses, scores, timestamp: Date.now() };
1843
+ this._results.push(result);
1844
+ return result;
1845
+ }
1846
+
1847
+ history() { return this._results; }
1848
+
1849
+ summary() {
1850
+ if(!this._results.length) return null;
1851
+ const totals = this._clients.map((_,i)=>({ variant:i, avgLatency:0, avgScore:0, count:0 }));
1852
+ for(const r of this._results){
1853
+ r.responses.forEach((res,i)=>{
1854
+ totals[i].avgLatency+=res.latencyMs; totals[i].count++;
1855
+ if(r.scores?.[i]) totals[i].avgScore+=r.scores[i].overall||0;
1856
+ });
1857
+ }
1858
+ return totals.map(t=>({
1859
+ variant:t.variant,
1860
+ avgLatency: t.count?Math.round(t.avgLatency/t.count):0,
1861
+ avgScore: t.count?+(t.avgScore/t.count).toFixed(2):0,
1862
+ runs:t.count,
1863
+ }));
1864
+ }
1865
+ }
1866
+
1867
+ /** Compress a prompt to fit within a token budget (trim middle) */
1868
+ function compressPrompt(text, maxTokens=2000) {
1869
+ const est = tokenizer.estimate(text);
1870
+ if (est <= maxTokens) return text;
1871
+ const ratio = maxTokens / est;
1872
+ const keep = Math.floor(text.length * ratio * 0.9);
1873
+ const half = Math.floor(keep/2);
1874
+ return text.slice(0,half) + '\n\n[... content trimmed for context window ...]\n\n' + text.slice(-half);
1875
+ }
1876
+
1877
+ /** Validate that conversation messages alternate correctly */
1878
+ function validateMessages(messages) {
1879
+ const errors=[];
1880
+ for(let i=0;i<messages.length;i++){
1881
+ const m=messages[i];
1882
+ if(!m.role) errors.push(`Message ${i}: missing role`);
1883
+ if(!m.content&&!m.tool_calls) errors.push(`Message ${i}: missing content`);
1884
+ if(i>0&&m.role==='user'&&messages[i-1].role==='user') errors.push(`Message ${i}: consecutive user messages`);
1885
+ if(i>0&&m.role==='assistant'&&messages[i-1].role==='assistant') errors.push(`Message ${i}: consecutive assistant messages`);
1886
+ }
1887
+ return { valid:errors.length===0, errors };
1888
+ }
1889
+
1890
+ // ─────────────────────────────────────────────────────────────────────────────
1891
+ // §27 Provider auto-detect from API key prefix
1892
+ // ─────────────────────────────────────────────────────────────────────────────
1893
+
1894
+ function detectProvider(apiKey='') {
1895
+ if (!apiKey) return 'openai';
1896
+ if (apiKey.startsWith('sk-ant-')) return 'anthropic';
1897
+ if (apiKey.startsWith('AIza')) return 'gemini';
1898
+ if (apiKey.startsWith('gsk_')) return 'groq';
1899
+ if (apiKey.startsWith('AZ')) return 'together';
1900
+ return 'openai';
1901
+ }
1902
+
1903
+ // ─────────────────────────────────────────────────────────────────────────────
1904
+ // §28 kitai namespace
1905
+ // ─────────────────────────────────────────────────────────────────────────────
1906
+
1907
+ const kitai = {
1908
+
1909
+ // ── Meta ────────────────────────────────────────────────────────────────
1910
+ version: '1.0.0',
1911
+
1912
+ // ── Core classes ────────────────────────────────────────────────────────
1913
+ KitAIFormat,
1914
+ KitAIClient,
1915
+ PromptTemplate,
1916
+ FewShotBuilder,
1917
+ VectorStore,
1918
+ RAGPipeline,
1919
+ LRUCache,
1920
+ ToolRegistry,
1921
+ ReactAgent,
1922
+ JSONEnforcer,
1923
+ LLMJudge,
1924
+ ABExperiment,
1925
+ SequentialChain,
1926
+ ParallelChain,
1927
+ SlidingWindowMemory,
1928
+ TokenBudgetMemory,
1929
+ SummaryMemory,
1930
+
1931
+ // ── Sub-namespaces ───────────────────────────────────────────────────────
1932
+ tokenizer,
1933
+ cost,
1934
+ schema,
1935
+ parsers,
1936
+ classify,
1937
+ metrics,
1938
+ guard,
1939
+ chunker,
1940
+ scaffolds,
1941
+ systemPrompts,
1942
+ exporter,
1943
+
1944
+ // ── Utility functions ────────────────────────────────────────────────────
1945
+ stripMarkdown,
1946
+ diffText,
1947
+ semanticSimilarity,
1948
+ compressPrompt,
1949
+ validateMessages,
1950
+ detectProvider,
1951
+
1952
+ // ── Model catalogue ──────────────────────────────────────────────────────
1953
+ models: {
1954
+ all: () => Object.entries(_MODELS).map(([id,m])=>({id,...m})),
1955
+ byProvider: (p) => Object.entries(_MODELS).filter(([,m])=>m.provider===p).map(([id,m])=>({id,...m})),
1956
+ byType: (t) => Object.entries(_MODELS).filter(([,m])=>m.type===t).map(([id,m])=>({id,...m})),
1957
+ byFamily: (f) => Object.entries(_MODELS).filter(([,m])=>m.family===f).map(([id,m])=>({id,...m})),
1958
+ get: (id)=> _MODELS[id]||null,
1959
+ contextWindow: (id)=> _MODELS[id]?.context||null,
1960
+ providers: () => [...new Set(Object.values(_MODELS).map(m=>m.provider))],
1961
+ families: () => [...new Set(Object.values(_MODELS).map(m=>m.family))],
1962
+ cheapest: (type='chat') => cost.cheapest(type),
1963
+ largest: (type='chat') => Object.entries(_MODELS)
1964
+ .filter(([,m])=>m.type===type)
1965
+ .sort((a,b)=>b[1].context-a[1].context)[0]?.[0]||null,
1966
+ register(id, spec) { _MODELS[id]=spec; },
1967
+ },
1968
+
1969
+ // ── Provider registry ────────────────────────────────────────────────────
1970
+ providers: {
1971
+ all: () => Object.keys(_PROVIDERS),
1972
+ get: (name) => _PROVIDERS[name]||null,
1973
+ register: (name, spec) => { _PROVIDERS[name]=spec; },
1974
+ },
1975
+
1976
+ // ── Built-in tools ───────────────────────────────────────────────────────
1977
+ builtinTools: _builtinTools,
1978
+
1979
+ /** Create a ToolRegistry pre-loaded with all built-in tools */
1980
+ defaultToolRegistry() {
1981
+ const reg = new ToolRegistry();
1982
+ for(const [name,{description,fn}] of Object.entries(_builtinTools))
1983
+ reg.register(name, description, fn);
1984
+ return reg;
1985
+ },
1986
+
1987
+ // ──────────────────────────────────────────────────────────────────────────
1988
+ // FEATURE 1 – Multi-provider client factory
1989
+ // ──────────────────────────────────────────────────────────────────────────
1990
+ /**
1991
+ * Create a KitAIClient.
1992
+ * @param {object|string} config – full config object, or just an apiKey string
1993
+ */
1994
+ client(config={}) {
1995
+ if (typeof config === 'string') config = { apiKey: config };
1996
+ return new KitAIClient(config);
1997
+ },
1998
+
1999
+ // ──────────────────────────────────────────────────────────────────────────
2000
+ // FEATURE 3 – Stream aggregator (async iter → full string)
2001
+ // ──────────────────────────────────────────────────────────────────────────
2002
+ async collectStream(asyncIter) {
2003
+ let out='';
2004
+ for await(const chunk of asyncIter) out+=chunk;
2005
+ return out;
2006
+ },
2007
+
2008
+ // ──────────────────────────────────────────────────────────────────────────
2009
+ // FEATURE 10 – Quick model info lookup
2010
+ // ──────────────────────────────────────────────────────────────────────────
2011
+ model: (id) => _MODELS[id] || null,
2012
+
2013
+ // ──────────────────────────────────────────────────────────────────────────
2014
+ // FEATURE 11 – Prompt template factory
2015
+ // ──────────────────────────────────────────────────────────────────────────
2016
+ prompt(template, defaults={}) { return new PromptTemplate(template, defaults); },
2017
+
2018
+ // ──────────────────────────────────────────────────────────────────────────
2019
+ // FEATURE 13 – System prompt helper
2020
+ // ──────────────────────────────────────────────────────────────────────────
2021
+ system(name, vars={}) {
2022
+ let content = typeof systemPrompts[name]==='function' ? systemPrompts[name](vars.content||'') : systemPrompts[name];
2023
+ if (!content) content = name; // allow passing raw content
2024
+ for(const [k,v] of Object.entries(vars)) content=content.replace(`{${k}}`,v);
2025
+ return { role:'system', content };
2026
+ },
2027
+
2028
+ // ──────────────────────────────────────────────────────────────────────────
2029
+ // FEATURE 17 – Memory factories
2030
+ // ──────────────────────────────────────────────────────────────────────────
2031
+ memory: {
2032
+ sliding: (maxTurns=10) => new SlidingWindowMemory(maxTurns),
2033
+ budget: (budget=3000, model='') => new TokenBudgetMemory(budget, model),
2034
+ summary: (client, trigger=10) => new SummaryMemory(client, trigger),
2035
+ },
2036
+
2037
+ // ──────────────────────────────────────────────────────────────────────────
2038
+ // FEATURE 21 – RAG factory
2039
+ // ──────────────────────────────────────────────────────────────────────────
2040
+ rag(client, opts={}) { return new RAGPipeline(client, new VectorStore(), opts); },
2041
+
2042
+ // ──────────────────────────────────────────────────────────────────────────
2043
+ // FEATURE 23 – Sequential chain factory
2044
+ // ──────────────────────────────────────────────────────────────────────────
2045
+ chain(client, opts={}) { return new SequentialChain(client, opts); },
2046
+ parallel(client, opts={}) { return new ParallelChain(client, opts); },
2047
+
2048
+ // ──────────────────────────────────────────────────────────────────────────
2049
+ // FEATURE 26 – ReAct agent factory
2050
+ // ──────────────────────────────────────────────────────────────────────────
2051
+ agent(client, registry=kitai.defaultToolRegistry(), opts={}) {
2052
+ return new ReactAgent(client, registry, opts);
2053
+ },
2054
+
2055
+ // ──────────────────────────────────────────────────────────────────────────
2056
+ // FEATURE 29 – JSON enforcer factory
2057
+ // ──────────────────────────────────────────────────────────────────────────
2058
+ enforce(client, schemaObj, maxRetries=3) { return new JSONEnforcer(client, schemaObj, maxRetries); },
2059
+
2060
+ // ──────────────────────────────────────────────────────────────────────────
2061
+ // FEATURE 34 – LLM judge factory
2062
+ // ──────────────────────────────────────────────────────────────────────────
2063
+ judge(client, opts={}) { return new LLMJudge(client, opts); },
2064
+
2065
+ // ──────────────────────────────────────────────────────────────────────────
2066
+ // FEATURE 38 – Retry + exponential backoff wrapper
2067
+ // ──────────────────────────────────────────────────────────────────────────
2068
+ /**
2069
+ * Wrap any async fn with retry + exponential backoff + optional fallback.
2070
+ * @param {Function} fn
2071
+ * @param {object} [opts]
2072
+ * @param {number} [opts.maxRetries]
2073
+ * @param {number} [opts.baseDelayMs]
2074
+ * @param {Function[]} [opts.fallbacks] – list of alternative fns to try after all retries fail
2075
+ */
2076
+ async retry(fn, opts={}) {
2077
+ const { maxRetries=3, baseDelayMs=500, fallbacks=[] } = opts;
2078
+ let lastErr;
2079
+ for(let i=0;i<=maxRetries;i++){
2080
+ try { return await fn(); }
2081
+ catch (e) {
2082
+ lastErr=e;
2083
+ if(i<maxRetries) await _sleep(Math.min(baseDelayMs*2**i, 30000));
2084
+ }
2085
+ }
2086
+ for(const fallback of fallbacks){
2087
+ try { return await fallback(); } catch(e) { lastErr=e; }
2088
+ }
2089
+ throw lastErr;
2090
+ },
2091
+
2092
+ // ──────────────────────────────────────────────────────────────────────────
2093
+ // FEATURE 39 – Token estimation shortcuts
2094
+ // ──────────────────────────────────────────────────────────────────────────
2095
+ estimateTokens(text) { return tokenizer.estimate(text); },
2096
+ estimateMessages(messages) { return tokenizer.estimateMessages(messages); },
2097
+ fitsContext(messages, model) { return tokenizer.fitsInContext(messages, model); },
2098
+
2099
+ // ──────────────────────────────────────────────────────────────────────────
2100
+ // FEATURE 41 – Context window advisor
2101
+ // ──────────────────────────────────────────────────────────────────────────
2102
+ /**
2103
+ * Given text content and desired reserve, recommend the cheapest model
2104
+ * whose context window fits.
2105
+ */
2106
+ adviseBestModel(messages, reserveTokens=1000, type='chat') {
2107
+ const used = tokenizer.estimateMessages(messages);
2108
+ const candidates = Object.entries(_MODELS)
2109
+ .filter(([,m]) => m.type===type && m.context >= used+reserveTokens)
2110
+ .sort((a,b)=>a[1].inputPer1M-b[1].inputPer1M);
2111
+ return candidates.map(([id,m])=>({ id, contextWindow:m.context, usedTokens:used, inputPer1M:m.inputPer1M }));
2112
+ },
2113
+
2114
+ // ──────────────────────────────────────────────────────────────────────────
2115
+ // FEATURE 44 – Cache factory
2116
+ // ──────────────────────────────────────────────────────────────────────────
2117
+ cache(maxSize=256) { return new LRUCache(maxSize); },
2118
+
2119
+ // ──────────────────────────────────────────────────────────────────────────
2120
+ // FEATURE 49 – A/B experiment factory
2121
+ // ──────────────────────────────────────────────────────────────────────────
2122
+ experiment(clients, opts={}) { return new ABExperiment(clients, opts); },
2123
+
2124
+ // ──────────────────────────────────────────────────────────────────────────
2125
+ // FEATURE 48 – Semantic similarity (async, needs client for embed)
2126
+ // ──────────────────────────────────────────────────────────────────────────
2127
+ async similarityScore(client, textA, textB, model='text-embedding-3-small') {
2128
+ const [[a],[b]] = await Promise.all([
2129
+ client.embed(textA,{model}),
2130
+ client.embed(textB,{model}),
2131
+ ]);
2132
+ return _cosineSim(a,b);
2133
+ },
2134
+
2135
+ // ──────────────────────────────────────────────────────────────────────────
2136
+ // Convenience: one-shot chat without creating a client first
2137
+ // ──────────────────────────────────────────────────────────────────────────
2138
+ async ask(apiKey, question, opts={}) {
2139
+ const c = kitai.client({ apiKey, ...opts });
2140
+ const res = await c.chat([{role:'user',content:question}], opts);
2141
+ return res;
2142
+ },
2143
+ };
2144
+
2145
+ // ─────────────────────────────────────────────────────────────────────────────
2146
+ // §29 Exports (same pattern as KitGPS)
2147
+ // ─────────────────────────────────────────────────────────────────────────────
2148
+ let kitdef = {};
2149
+ kitdef = kitai;
2150
+ kitdef.default = kitai;
2151
+ kitdef.KitAIFormat = KitAIFormat;
2152
+ kitdef.KitAIClient = KitAIClient;
2153
+ kitdef.PromptTemplate = PromptTemplate;
2154
+ kitdef.FewShotBuilder = FewShotBuilder;
2155
+ kitdef.VectorStore = VectorStore;
2156
+ kitdef.RAGPipeline = RAGPipeline;
2157
+ kitdef.ToolRegistry = ToolRegistry;
2158
+ kitdef.ReactAgent = ReactAgent;
2159
+ kitdef.JSONEnforcer = JSONEnforcer;
2160
+ kitdef.LLMJudge = LLMJudge;
2161
+ kitdef.ABExperiment = ABExperiment;
2162
+ kitdef.SequentialChain= SequentialChain;
2163
+ kitdef.ParallelChain = ParallelChain;
2164
+ kitdef.SlidingWindowMemory = SlidingWindowMemory;
2165
+ kitdef.TokenBudgetMemory = TokenBudgetMemory;
2166
+ kitdef.SummaryMemory = SummaryMemory;
2167
+ kitdef.LRUCache = LRUCache;
2168
+ kitdef.tokenizer = tokenizer;
2169
+ kitdef.cost = cost;
2170
+ kitdef.schema = schema;
2171
+ kitdef.parsers = parsers;
2172
+ kitdef.classify = classify;
2173
+ kitdef.metrics = metrics;
2174
+ kitdef.guard = guard;
2175
+ kitdef.chunker = chunker;
2176
+ kitdef.scaffolds = scaffolds;
2177
+ kitdef.systemPrompts = systemPrompts;
2178
+ kitdef.exporter = exporter;
2179
+ kitdef.stripMarkdown = stripMarkdown;
2180
+ kitdef.diffText = diffText;
2181
+ kitdef.semanticSimilarity = semanticSimilarity;
2182
+ kitdef.compressPrompt = compressPrompt;
2183
+ kitdef.validateMessages = validateMessages;
2184
+ kitdef.detectProvider = detectProvider;
2185
+ module.exports = { kitdef };