@hiper2d/ai-agents 0.1.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,4364 @@
1
+ // src/types.ts
2
+ var MESSAGE_ROLE = {
3
+ SYSTEM: "system",
4
+ USER: "user",
5
+ ASSISTANT: "assistant"
6
+ };
7
+ var DEFAULT_LOGGING_CONFIG = {
8
+ agents: {
9
+ enabled: true,
10
+ logSystemPrompt: process.env.LOG_SYSTEM_PROMPT !== "false",
11
+ history: {
12
+ enabled: process.env.LOG_HISTORY !== "false",
13
+ maxCharactersPerMessage: parseInt(process.env.LOG_MAX_HISTORY_CHARS || "1000", 10)
14
+ },
15
+ logCommand: true,
16
+ reply: {
17
+ mode: process.env.LOG_REPLY_MODE === "raw" ? "raw" : "body-only",
18
+ maxReplyChars: parseInt(process.env.LOG_MAX_REPLY_CHARS || "5000", 10),
19
+ maxThinkingChars: parseInt(process.env.LOG_MAX_THINKING_CHARS || "2000", 10),
20
+ includeReasoning: process.env.LOG_INCLUDE_REASONING !== "false",
21
+ includeUsage: process.env.LOG_INCLUDE_USAGE !== "false"
22
+ }
23
+ }
24
+ };
25
+ var BotResponseError = class extends Error {
26
+ details;
27
+ context;
28
+ recoverable;
29
+ /**
30
+ * Model-facing explanation of the rejection, set where the failure is detected and carried
31
+ * through to the consumer's error surface. Used to enrich a user-triggered retry prompt.
32
+ */
33
+ explanation;
34
+ constructor(message, details = "", context = {}, recoverable = true, explanation) {
35
+ super(message);
36
+ this.name = "BotResponseError";
37
+ this.details = details;
38
+ this.context = context;
39
+ this.recoverable = recoverable;
40
+ this.explanation = explanation;
41
+ }
42
+ };
43
+
44
+ // src/logger.ts
45
+ var consoleLogger = {
46
+ debug: (message, args) => console.debug(message, args ?? ""),
47
+ info: (message, args) => console.info(message, args ?? ""),
48
+ warn: (message, args) => console.warn(message, args ?? ""),
49
+ error: (message, args) => console.error(message, args ?? ""),
50
+ agentActivity: (agentName, model, activity) => {
51
+ console.info(`Agent ${activity}: ${agentName} (${model})`);
52
+ }
53
+ };
54
+ var current = consoleLogger;
55
+ function setLlmLogger(replacement) {
56
+ current = replacement;
57
+ }
58
+ var logger = {
59
+ debug: (message, args) => current.debug(message, args),
60
+ info: (message, args) => current.info(message, args),
61
+ warn: (message, args) => current.warn(message, args),
62
+ error: (message, args) => current.error(message, args),
63
+ agentActivity: (agentName, model, activity, data, customConfig) => current.agentActivity(agentName, model, activity, data, customConfig)
64
+ };
65
+
66
+ // src/cache-tier.ts
67
+ var CACHE_TIER_MARKER = "\n<<<CACHE_TIER_BREAK>>>\n";
68
+
69
+ // src/text-utils.ts
70
+ function cleanResponse(response) {
71
+ let cleanResponse2 = response.trim();
72
+ if (cleanResponse2.startsWith("```json")) {
73
+ cleanResponse2 = cleanResponse2.slice(7);
74
+ } else if (cleanResponse2.startsWith("```")) {
75
+ cleanResponse2 = cleanResponse2.slice(3);
76
+ }
77
+ if (cleanResponse2.endsWith("```")) {
78
+ cleanResponse2 = cleanResponse2.slice(0, -3);
79
+ }
80
+ return cleanResponse2.trim();
81
+ }
82
+ function stableHashHex(input) {
83
+ let h1 = 2166136261, h2 = 3421674724;
84
+ for (let i = 0; i < input.length; i++) {
85
+ const c = input.charCodeAt(i);
86
+ h1 = Math.imul(h1 ^ c, 16777619) >>> 0;
87
+ h2 = Math.imul(h2 ^ c, 16777623) >>> 0;
88
+ }
89
+ return h1.toString(16).padStart(8, "0") + h2.toString(16).padStart(8, "0");
90
+ }
91
+
92
+ // src/zod-validate.ts
93
+ function validateResponse(schema, data) {
94
+ return schema.parse(data);
95
+ }
96
+ function safeValidateResponse(schema, data) {
97
+ return schema.safeParse(data);
98
+ }
99
+
100
+ // src/zod-schema-converter.ts
101
+ import { z } from "zod";
102
+ var ZodSchemaConverter = class {
103
+ /**
104
+ * Convert Zod schema to OpenAI-compatible JSON Schema
105
+ */
106
+ static toOpenAIJsonSchema(zodSchema, schemaName) {
107
+ const baseSchema = this.zodToJsonSchema(zodSchema, { strict: true, includeDescription: true });
108
+ return {
109
+ name: schemaName,
110
+ schema: baseSchema,
111
+ strict: true
112
+ };
113
+ }
114
+ /**
115
+ * Convert Zod schema to Google Gemini responseSchema format
116
+ * This follows the official Gemini structured output format
117
+ */
118
+ static toGoogleSchema(zodSchema) {
119
+ return this.convertZodToGoogleType(zodSchema, true);
120
+ }
121
+ /**
122
+ * Internal method to convert Zod types to Google schema format
123
+ */
124
+ static convertZodToGoogleType(zodType, includeDescriptions = false) {
125
+ if (zodType instanceof z.ZodString) {
126
+ const schema2 = { type: "string" };
127
+ if (includeDescriptions && zodType.description) {
128
+ schema2.description = zodType.description;
129
+ }
130
+ return schema2;
131
+ }
132
+ if (zodType instanceof z.ZodNumber) {
133
+ const schema2 = { type: "number" };
134
+ if (includeDescriptions && zodType.description) {
135
+ schema2.description = zodType.description;
136
+ }
137
+ return schema2;
138
+ }
139
+ if (zodType instanceof z.ZodBoolean) {
140
+ const schema2 = { type: "boolean" };
141
+ if (includeDescriptions && zodType.description) {
142
+ schema2.description = zodType.description;
143
+ }
144
+ return schema2;
145
+ }
146
+ if (zodType instanceof z.ZodArray) {
147
+ const schema2 = {
148
+ type: "array",
149
+ items: this.convertZodToGoogleType(zodType.element, includeDescriptions)
150
+ };
151
+ if (includeDescriptions && zodType.description) {
152
+ schema2.description = zodType.description;
153
+ }
154
+ return schema2;
155
+ }
156
+ if (zodType instanceof z.ZodObject) {
157
+ const properties = {};
158
+ const required = [];
159
+ const propertyOrdering = [];
160
+ const shape = zodType.shape;
161
+ for (const [key, value] of Object.entries(shape)) {
162
+ const zodValue = value;
163
+ properties[key] = this.convertZodToGoogleType(zodValue, includeDescriptions);
164
+ propertyOrdering.push(key);
165
+ if (!zodValue.isOptional()) {
166
+ required.push(key);
167
+ }
168
+ }
169
+ const schema2 = {
170
+ type: "object",
171
+ properties,
172
+ propertyOrdering,
173
+ additionalProperties: false
174
+ };
175
+ if (required.length > 0) {
176
+ schema2.required = required;
177
+ }
178
+ if (includeDescriptions && zodType.description) {
179
+ schema2.description = zodType.description;
180
+ }
181
+ return schema2;
182
+ }
183
+ if (zodType instanceof z.ZodOptional) {
184
+ const innerSchema = this.convertZodToGoogleType(zodType._def.innerType, includeDescriptions);
185
+ if (includeDescriptions && zodType.description) {
186
+ innerSchema.description = zodType.description;
187
+ }
188
+ return innerSchema;
189
+ }
190
+ if (zodType instanceof z.ZodNullable) {
191
+ const innerSchema = this.convertZodToGoogleType(zodType._def.innerType, includeDescriptions);
192
+ innerSchema.nullable = true;
193
+ return innerSchema;
194
+ }
195
+ if (zodType instanceof z.ZodEnum) {
196
+ const schema2 = {
197
+ type: "string",
198
+ enum: zodType.options
199
+ };
200
+ if (includeDescriptions && zodType.description) {
201
+ schema2.description = zodType.description;
202
+ }
203
+ return schema2;
204
+ }
205
+ if (zodType instanceof z.ZodLiteral) {
206
+ const value = zodType.value;
207
+ const schema2 = {
208
+ type: typeof value,
209
+ const: value
210
+ };
211
+ if (includeDescriptions && zodType.description) {
212
+ schema2.description = zodType.description;
213
+ }
214
+ return schema2;
215
+ }
216
+ if (zodType instanceof z.ZodUnion) {
217
+ const options = zodType._def.options;
218
+ if (options.length > 0) {
219
+ const schema2 = {
220
+ oneOf: options.map((option) => this.convertZodToGoogleType(option, includeDescriptions))
221
+ };
222
+ if (includeDescriptions && zodType.description) {
223
+ schema2.description = zodType.description;
224
+ }
225
+ return schema2;
226
+ }
227
+ }
228
+ console.warn(`Unsupported Zod type for Google schema: ${zodType.constructor.name}. Falling back to STRING.`);
229
+ const schema = { type: "string" };
230
+ if (includeDescriptions && zodType.description) {
231
+ schema.description = zodType.description;
232
+ }
233
+ return schema;
234
+ }
235
+ /**
236
+ * Convert Zod schema to standard JSON Schema format
237
+ * Public method for external use (e.g., Grok structured outputs)
238
+ */
239
+ static toJsonSchema(zodSchema, options = {}) {
240
+ return this.zodToJsonSchema(zodSchema, options);
241
+ }
242
+ /**
243
+ * Convert Zod schema to Mistral/DeepSeek JSON Schema format
244
+ */
245
+ static toMistralSchema(zodSchema) {
246
+ return this.zodToJsonSchema(zodSchema, {
247
+ strict: true,
248
+ additionalProperties: false
249
+ });
250
+ }
251
+ /**
252
+ * Convert Zod schema to human-readable prompt description for Anthropic
253
+ */
254
+ static toPromptDescription(zodSchema) {
255
+ const jsonSchema = this.zodToJsonSchema(zodSchema, { includeDescription: true });
256
+ const description = this.buildSchemaDescription(jsonSchema, 0);
257
+ return `Your response must be a valid JSON object matching this exact structure:
258
+
259
+ ${description}
260
+
261
+ CRITICAL REQUIREMENTS:
262
+ - Your response must be valid JSON
263
+ - Include all required fields
264
+ - Follow the exact data types specified
265
+ - Do not include any additional fields not specified in the schema
266
+ - IMPORTANT: Fields marked as "string" must be plain text strings, NOT nested objects or arrays. Put all your content into a single string value.`;
267
+ }
268
+ /**
269
+ * Get provider-specific schema format
270
+ */
271
+ static forProvider(zodSchema, provider, schemaName = "response_schema") {
272
+ switch (provider) {
273
+ case "openai":
274
+ return {
275
+ type: "json_schema",
276
+ content: this.toOpenAIJsonSchema(zodSchema, schemaName)
277
+ };
278
+ case "google":
279
+ return {
280
+ type: "google_schema",
281
+ content: this.toGoogleSchema(zodSchema)
282
+ };
283
+ case "mistral":
284
+ case "deepseek":
285
+ return {
286
+ type: "json_schema",
287
+ content: this.toMistralSchema(zodSchema)
288
+ };
289
+ case "anthropic":
290
+ return {
291
+ type: "prompt_description",
292
+ content: this.toPromptDescription(zodSchema)
293
+ };
294
+ case "grok":
295
+ case "kimi":
296
+ return {
297
+ type: "json_schema",
298
+ content: this.zodToJsonSchema(zodSchema, { strict: false })
299
+ };
300
+ default:
301
+ throw new Error(`Unsupported provider: ${provider}`);
302
+ }
303
+ }
304
+ /**
305
+ * Core Zod to JSON Schema conversion
306
+ */
307
+ static zodToJsonSchema(zodSchema, options = {}) {
308
+ const { strict = true, includeDescription = false, additionalProperties } = options;
309
+ const converted = this.convertZodType(zodSchema, includeDescription);
310
+ if (strict && converted.type === "object") {
311
+ return this.makeSchemaStrict(converted, additionalProperties);
312
+ }
313
+ return converted;
314
+ }
315
+ /**
316
+ * Convert individual Zod types to JSON Schema format
317
+ */
318
+ static convertZodType(zodType, includeDescription = false) {
319
+ if (zodType instanceof z.ZodString) {
320
+ const schema = { type: "string" };
321
+ if (includeDescription && zodType.description) {
322
+ schema.description = zodType.description;
323
+ }
324
+ return schema;
325
+ }
326
+ if (zodType instanceof z.ZodNumber) {
327
+ const schema = { type: "number" };
328
+ if (includeDescription && zodType.description) {
329
+ schema.description = zodType.description;
330
+ }
331
+ return schema;
332
+ }
333
+ if (zodType instanceof z.ZodBoolean) {
334
+ const schema = { type: "boolean" };
335
+ if (includeDescription && zodType.description) {
336
+ schema.description = zodType.description;
337
+ }
338
+ return schema;
339
+ }
340
+ if (zodType instanceof z.ZodArray) {
341
+ const schema = {
342
+ type: "array",
343
+ items: this.convertZodType(zodType.element, includeDescription)
344
+ };
345
+ if (includeDescription && zodType.description) {
346
+ schema.description = zodType.description;
347
+ }
348
+ if (zodType._def.minLength !== null) {
349
+ schema.minItems = zodType._def.minLength.value;
350
+ }
351
+ if (zodType._def.maxLength !== null) {
352
+ schema.maxItems = zodType._def.maxLength.value;
353
+ }
354
+ return schema;
355
+ }
356
+ if (zodType instanceof z.ZodObject) {
357
+ const properties = {};
358
+ const required = [];
359
+ const shape = zodType.shape;
360
+ for (const [key, value] of Object.entries(shape)) {
361
+ const zodValue = value;
362
+ properties[key] = this.convertZodType(zodValue, includeDescription);
363
+ if (!zodValue.isOptional()) {
364
+ required.push(key);
365
+ }
366
+ }
367
+ const schema = {
368
+ type: "object",
369
+ properties,
370
+ required
371
+ };
372
+ if (includeDescription && zodType.description) {
373
+ schema.description = zodType.description;
374
+ }
375
+ return schema;
376
+ }
377
+ if (zodType instanceof z.ZodOptional) {
378
+ const innerSchema = this.convertZodType(zodType._def.innerType, includeDescription);
379
+ if (includeDescription && zodType.description) {
380
+ innerSchema.description = zodType.description;
381
+ }
382
+ return innerSchema;
383
+ }
384
+ if (zodType instanceof z.ZodNullable) {
385
+ const innerSchema = this.convertZodType(zodType._def.innerType, includeDescription);
386
+ return {
387
+ ...innerSchema,
388
+ nullable: true
389
+ };
390
+ }
391
+ if (zodType instanceof z.ZodEnum) {
392
+ const schema = {
393
+ type: "string",
394
+ enum: zodType.options
395
+ };
396
+ if (includeDescription && zodType.description) {
397
+ schema.description = zodType.description;
398
+ }
399
+ return schema;
400
+ }
401
+ if (zodType instanceof z.ZodLiteral) {
402
+ const value = zodType.value;
403
+ const schema = {
404
+ type: typeof value,
405
+ const: value
406
+ };
407
+ if (includeDescription && zodType.description) {
408
+ schema.description = zodType.description;
409
+ }
410
+ return schema;
411
+ }
412
+ if (zodType instanceof z.ZodUnion) {
413
+ const options = zodType._def.options;
414
+ return {
415
+ oneOf: options.map((option) => this.convertZodType(option, includeDescription))
416
+ };
417
+ }
418
+ console.warn(`Unsupported Zod type: ${zodType.constructor.name}. Falling back to string.`);
419
+ return { type: "string" };
420
+ }
421
+ /**
422
+ * Recursively add additionalProperties: false to all object types for strict validation
423
+ */
424
+ static makeSchemaStrict(schema, additionalProperties = false) {
425
+ if (typeof schema !== "object" || schema === null) {
426
+ return schema;
427
+ }
428
+ const result = { ...schema };
429
+ if (result.type === "object") {
430
+ result.additionalProperties = additionalProperties;
431
+ }
432
+ if (result.properties) {
433
+ result.properties = Object.fromEntries(
434
+ Object.entries(result.properties).map(([key, prop]) => [
435
+ key,
436
+ this.makeSchemaStrict(prop, additionalProperties)
437
+ ])
438
+ );
439
+ }
440
+ if (result.items) {
441
+ result.items = this.makeSchemaStrict(result.items, additionalProperties);
442
+ }
443
+ if (result.oneOf) {
444
+ result.oneOf = result.oneOf.map((subSchema) => this.makeSchemaStrict(subSchema, additionalProperties));
445
+ }
446
+ if (result.anyOf) {
447
+ result.anyOf = result.anyOf.map((subSchema) => this.makeSchemaStrict(subSchema, additionalProperties));
448
+ }
449
+ if (result.allOf) {
450
+ result.allOf = result.allOf.map((subSchema) => this.makeSchemaStrict(subSchema, additionalProperties));
451
+ }
452
+ return result;
453
+ }
454
+ /**
455
+ * Build human-readable schema description for prompt-based providers
456
+ */
457
+ static buildSchemaDescription(schema, depth = 0) {
458
+ const indent = " ".repeat(depth);
459
+ if (!schema || typeof schema !== "object") {
460
+ return "any";
461
+ }
462
+ if (schema.type === "object") {
463
+ let result = `${indent}{
464
+ `;
465
+ const properties = schema.properties || {};
466
+ const required = schema.required || [];
467
+ const entries = Object.entries(properties);
468
+ for (let i = 0; i < entries.length; i++) {
469
+ const [key, prop] = entries[i];
470
+ const isRequired = required.includes(key);
471
+ const isLast = i === entries.length - 1;
472
+ const typeDesc = this.getTypeDescription(prop, depth + 1);
473
+ const requiredMark = isRequired ? " (required)" : " (optional)";
474
+ const description = prop.description ? ` // ${prop.description}` : "";
475
+ if (prop.type === "object") {
476
+ result += `${indent} "${key}": ${typeDesc}${requiredMark}${description}`;
477
+ } else {
478
+ result += `${indent} "${key}": ${typeDesc}${requiredMark}${description}`;
479
+ }
480
+ if (!isLast) result += ",";
481
+ result += "\n";
482
+ }
483
+ result += `${indent}}`;
484
+ return result;
485
+ }
486
+ return this.getTypeDescription(schema, depth);
487
+ }
488
+ /**
489
+ * Get type description for schema properties
490
+ */
491
+ static getTypeDescription(schema, depth) {
492
+ if (schema.type === "string") {
493
+ if (schema.enum) {
494
+ return `"${schema.enum.join('" | "')}"`;
495
+ }
496
+ return "string";
497
+ }
498
+ if (schema.type === "number") {
499
+ return "number";
500
+ }
501
+ if (schema.type === "boolean") {
502
+ return "boolean";
503
+ }
504
+ if (schema.type === "array") {
505
+ const itemType = this.getTypeDescription(schema.items, depth);
506
+ return `${itemType}[]`;
507
+ }
508
+ if (schema.type === "object") {
509
+ return this.buildInlineObjectDescription(schema, depth);
510
+ }
511
+ if (schema.oneOf) {
512
+ return schema.oneOf.map((s) => this.getTypeDescription(s, depth)).join(" | ");
513
+ }
514
+ return schema?.type || "any";
515
+ }
516
+ /**
517
+ * Build inline object description without leading indentation
518
+ */
519
+ static buildInlineObjectDescription(schema, depth) {
520
+ if (!schema || typeof schema !== "object" || schema.type !== "object") {
521
+ return "any";
522
+ }
523
+ let result = "{\n";
524
+ const properties = schema.properties || {};
525
+ const required = schema.required || [];
526
+ const indent = " ".repeat(depth + 1);
527
+ const entries = Object.entries(properties);
528
+ for (let i = 0; i < entries.length; i++) {
529
+ const [key, prop] = entries[i];
530
+ const isRequired = required.includes(key);
531
+ const isLast = i === entries.length - 1;
532
+ const typeDesc = this.getTypeDescription(prop, depth + 1);
533
+ const requiredMark = isRequired ? " (required)" : " (optional)";
534
+ const description = prop.description ? ` // ${prop.description}` : "";
535
+ result += `${indent}"${key}": ${typeDesc}${requiredMark}${description}`;
536
+ if (!isLast) result += ",";
537
+ result += "\n";
538
+ }
539
+ result += `${" ".repeat(depth)}}`;
540
+ return result;
541
+ }
542
+ };
543
+ function generateSchemaInstructions(zodSchema, provider, schemaName = "response") {
544
+ const providerSchema = ZodSchemaConverter.forProvider(zodSchema, provider, schemaName);
545
+ if (providerSchema.type === "prompt_description") {
546
+ return providerSchema.content;
547
+ }
548
+ return `Your response must be a valid JSON object matching the provided schema. Ensure all required fields are included and data types are correct.`;
549
+ }
550
+ function supportsNativeJsonSchema(provider) {
551
+ return ["openai", "google", "mistral", "deepseek"].includes(provider);
552
+ }
553
+ function needsPromptBasedSchema(provider) {
554
+ return provider === "anthropic";
555
+ }
556
+
557
+ // src/json-response-parser.ts
558
+ function extractFirstJsonObject(text) {
559
+ let searchFrom = 0;
560
+ while (true) {
561
+ const start = text.indexOf("{", searchFrom);
562
+ if (start < 0) return null;
563
+ let depth = 0;
564
+ let inString = false;
565
+ let escaped = false;
566
+ for (let i = start; i < text.length; i++) {
567
+ const ch = text[i];
568
+ if (inString) {
569
+ if (escaped) escaped = false;
570
+ else if (ch === "\\") escaped = true;
571
+ else if (ch === '"') inString = false;
572
+ continue;
573
+ }
574
+ if (ch === '"') inString = true;
575
+ else if (ch === "{") depth++;
576
+ else if (ch === "}") {
577
+ depth--;
578
+ if (depth === 0) {
579
+ try {
580
+ return JSON.parse(text.slice(start, i + 1));
581
+ } catch {
582
+ break;
583
+ }
584
+ }
585
+ }
586
+ }
587
+ searchFrom = start + 1;
588
+ }
589
+ }
590
+ function normalizeNestedReply(value, log) {
591
+ if (value && typeof value === "object" && "reply" in value) {
592
+ const reply = value.reply;
593
+ if (reply && typeof reply === "object") {
594
+ log("Converting nested reply object to string");
595
+ return { ...value, reply: JSON.stringify(reply, null, 2) };
596
+ }
597
+ }
598
+ return value;
599
+ }
600
+ function parseAndValidateLlmJson(rawReply, zodSchema, log = () => {
601
+ }) {
602
+ const cleaned = cleanResponse(rawReply);
603
+ const candidates = [cleaned];
604
+ if (cleaned.startsWith('"') && cleaned.endsWith('"')) {
605
+ candidates.push(cleaned.slice(1, -1).replace(/\\"/g, '"'));
606
+ }
607
+ let parseError = null;
608
+ let zodError = null;
609
+ const tryValidate = (value) => {
610
+ const result = safeValidateResponse(zodSchema, normalizeNestedReply(value, log));
611
+ if (result.success) return { data: result.data };
612
+ zodError = zodError ?? result.error;
613
+ return null;
614
+ };
615
+ for (const candidate of candidates) {
616
+ let parsed;
617
+ try {
618
+ parsed = JSON.parse(candidate);
619
+ } catch (error) {
620
+ parseError = parseError ?? error;
621
+ continue;
622
+ }
623
+ const validated = tryValidate(parsed);
624
+ if (validated) return validated.data;
625
+ }
626
+ for (const candidate of candidates) {
627
+ const extracted = extractFirstJsonObject(candidate);
628
+ if (extracted === null) continue;
629
+ const validated = tryValidate(extracted);
630
+ if (validated) {
631
+ log(`Recovered JSON embedded in prose response (${candidate.length} chars)`);
632
+ return validated.data;
633
+ }
634
+ }
635
+ for (const candidate of candidates) {
636
+ if (!candidate.startsWith('"')) continue;
637
+ for (const rebraced of [`{${candidate}}`, `{${candidate}`]) {
638
+ let parsed;
639
+ try {
640
+ parsed = JSON.parse(rebraced);
641
+ } catch {
642
+ continue;
643
+ }
644
+ const validated = tryValidate(parsed);
645
+ if (validated) {
646
+ log(`Recovered JSON missing outer braces (${candidate.length} chars)`);
647
+ return validated.data;
648
+ }
649
+ }
650
+ }
651
+ const wrapped = safeValidateResponse(zodSchema, { reply: cleaned });
652
+ if (wrapped.success) {
653
+ log(`Wrapped prose response as reply (${cleaned.length} chars)`);
654
+ return wrapped.data;
655
+ }
656
+ if (zodError !== null) {
657
+ log(`Zod validation failed: ${JSON.stringify(zodError.errors)}`);
658
+ throw new Error(`Response validation failed: ${zodError.message}`);
659
+ }
660
+ throw new Error(`Failed to parse JSON response: ${parseError}. First 200 chars: ${cleaned.slice(0, 200)}`);
661
+ }
662
+
663
+ // src/errors.ts
664
+ var ModelError = class extends Error {
665
+ modelType;
666
+ constructor(message, modelType) {
667
+ super(message);
668
+ this.modelType = modelType;
669
+ }
670
+ };
671
+ var ModelOverloadError = class extends ModelError {
672
+ retryable;
673
+ constructor(message, modelType, retryable = true) {
674
+ super(message, modelType);
675
+ this.name = "ModelOverloadError";
676
+ this.retryable = retryable;
677
+ }
678
+ };
679
+ var ModelRateLimitError = class extends ModelError {
680
+ retryAfter;
681
+ // seconds to wait before retrying
682
+ constructor(message, modelType, retryAfter) {
683
+ super(message, modelType);
684
+ this.name = "ModelRateLimitError";
685
+ this.retryAfter = retryAfter;
686
+ }
687
+ };
688
+ var ModelUnavailableError = class extends ModelError {
689
+ reason;
690
+ constructor(message, modelType, reason = "unknown") {
691
+ super(message, modelType);
692
+ this.name = "ModelUnavailableError";
693
+ this.reason = reason;
694
+ }
695
+ };
696
+ var ModelAuthenticationError = class extends ModelError {
697
+ constructor(message, modelType) {
698
+ super(message, modelType);
699
+ this.name = "ModelAuthenticationError";
700
+ }
701
+ };
702
+ var ModelQuotaExceededError = class extends ModelError {
703
+ constructor(message, modelType) {
704
+ super(message, modelType);
705
+ this.name = "ModelQuotaExceededError";
706
+ }
707
+ };
708
+ var ModelRefusalError = class extends ModelError {
709
+ constructor(modelType, message = `${modelType} refused to answer (stop_reason: refusal)`) {
710
+ super(message, modelType);
711
+ this.name = "ModelRefusalError";
712
+ }
713
+ };
714
+
715
+ // src/thinking-utils.ts
716
+ function stripInlineThinking(raw) {
717
+ let thinking = "";
718
+ let text = raw.replace(/<think>([\s\S]*?)<\/think>/g, (_, inner) => {
719
+ thinking += (thinking ? "\n" : "") + inner.trim();
720
+ return "";
721
+ });
722
+ const closeIdx = text.indexOf("</think>");
723
+ if (closeIdx !== -1) {
724
+ const before = text.slice(0, closeIdx).trim();
725
+ if (before) thinking += (thinking ? "\n" : "") + before;
726
+ text = text.slice(closeIdx + "</think>".length);
727
+ }
728
+ const openIdx = text.indexOf("<think>");
729
+ if (openIdx !== -1) {
730
+ const after = text.slice(openIdx);
731
+ const jsonStart = after.indexOf("{");
732
+ thinking += (thinking ? "\n" : "") + (jsonStart === -1 ? after : after.slice(0, jsonStart)).replace("<think>", "").trim();
733
+ text = text.slice(0, openIdx) + (jsonStart === -1 ? "" : after.slice(jsonStart));
734
+ }
735
+ return { text: text.trim(), thinking };
736
+ }
737
+ function mergeThinking(...parts) {
738
+ return parts.filter(Boolean).join("\n");
739
+ }
740
+
741
+ // src/catalog.ts
742
+ var API_KEY_CONSTANTS = {
743
+ OPENAI: "OPENAI_API_KEY",
744
+ ANTHROPIC: "ANTHROPIC_API_KEY",
745
+ GOOGLE: "GOOGLE_API_KEY",
746
+ MISTRAL: "MISTRAL_API_KEY",
747
+ DEEPSEEK: "DEEPSEEK_API_KEY",
748
+ GROK: "GROK_API_KEY",
749
+ MOONSHOT: "MOONSHOT_API_KEY",
750
+ Z_AI: "Z_AI_API_KEY",
751
+ FUGU: "FUGU_API_KEY",
752
+ QWEN: "QWEN_API_KEY",
753
+ MINIMAX: "MINIMAX_API_KEY"
754
+ };
755
+ var SupportedAiKeyNames = {
756
+ [API_KEY_CONSTANTS.OPENAI]: "OpenAI",
757
+ [API_KEY_CONSTANTS.ANTHROPIC]: "Anthropic",
758
+ [API_KEY_CONSTANTS.GOOGLE]: "Google",
759
+ [API_KEY_CONSTANTS.MISTRAL]: "Mistral",
760
+ [API_KEY_CONSTANTS.DEEPSEEK]: "DeepSeek",
761
+ [API_KEY_CONSTANTS.GROK]: "Grok",
762
+ [API_KEY_CONSTANTS.MOONSHOT]: "Moonshot",
763
+ [API_KEY_CONSTANTS.Z_AI]: "Z.AI",
764
+ [API_KEY_CONSTANTS.FUGU]: "Sakana Fugu",
765
+ [API_KEY_CONSTANTS.QWEN]: "Qwen",
766
+ [API_KEY_CONSTANTS.MINIMAX]: "MiniMax"
767
+ };
768
+ var LLM_CONSTANTS = {
769
+ // Thinking-only catalog since 2026-08-05: models whose API offers a thinking toggle used to
770
+ // ship as separate with/without picker entries. The non-thinking variants were retired and
771
+ // the surviving thinking entries took over the plain ids ('claude-opus', 'glm', …).
772
+ // Ids are stable slot names, independent of provider version, so repointing a slot to a
773
+ // newer model doesn't orphan ids persisted by consumers.
774
+ CLAUDE_FABLE: "claude-fable",
775
+ CLAUDE_OPUS: "claude-opus",
776
+ CLAUDE_SONNET: "claude-sonnet",
777
+ CLAUDE_HAIKU: "claude-haiku",
778
+ DEEPSEEK_FLASH: "deepseek-flash",
779
+ DEEPSEEK_PRO: "deepseek-pro",
780
+ // GPT-5.6 family. 'gpt' and 'gpt-mini' are stable picker ids carried over from the
781
+ // GPT-5.5 / GPT-5.4-mini era so existing consumers keep working across the repoint.
782
+ GPT_SOL: "gpt-sol",
783
+ GPT: "gpt",
784
+ GPT_MINI: "gpt-mini",
785
+ GEMINI_PRO: "gemini-pro",
786
+ GEMINI_FLASH: "gemini-flash",
787
+ GEMINI_LITE: "gemini-lite",
788
+ MISTRAL_LARGE: "mistral-large",
789
+ MISTRAL_MEDIUM: "mistral-medium",
790
+ MISTRAL_SMALL: "mistral-small",
791
+ MISTRAL_MAGISTRAL: "mistral-magistral",
792
+ GROK: "grok",
793
+ KIMI: "kimi",
794
+ GLM: "glm",
795
+ GLM_FLASH: "glm-flash",
796
+ FUGU_ULTRA: "fugu-ultra",
797
+ // Qwen (QwenCloud/DashScope). Stable picker ids without the version, matching the gpt/gemini
798
+ // pattern, so future repoints don't orphan persisted ids.
799
+ QWEN_MAX: "qwen-max",
800
+ QWEN_FLASH: "qwen-flash",
801
+ // MiniMax. Single M3 entry; stable id without the version for the same repoint reason.
802
+ MINIMAX: "minimax"
803
+ };
804
+ var DEFAULT_MAX_OUTPUT_TOKENS = 8192;
805
+ var SupportedAiModels = {
806
+ // Claude Fable - frontier reasoning model. Thinking is always on (no non-thinking variant).
807
+ [LLM_CONSTANTS.CLAUDE_FABLE]: {
808
+ displayName: "Claude Fable 5",
809
+ modelApiName: "claude-fable-5",
810
+ apiKeyName: API_KEY_CONSTANTS.ANTHROPIC,
811
+ hasThinking: true,
812
+ reasoningEffort: "high",
813
+ tags: ["expensive"]
814
+ },
815
+ // Claude models — thinking-only entries (non-thinking variants retired 2026-08-05)
816
+ [LLM_CONSTANTS.CLAUDE_OPUS]: {
817
+ displayName: "Claude 5 Opus",
818
+ modelApiName: "claude-opus-5",
819
+ apiKeyName: API_KEY_CONSTANTS.ANTHROPIC,
820
+ hasThinking: true,
821
+ reasoningEffort: "high",
822
+ tags: ["expensive"]
823
+ },
824
+ [LLM_CONSTANTS.CLAUDE_SONNET]: {
825
+ displayName: "Claude 5 Sonnet",
826
+ modelApiName: "claude-sonnet-5",
827
+ apiKeyName: API_KEY_CONSTANTS.ANTHROPIC,
828
+ hasThinking: true,
829
+ reasoningEffort: "high",
830
+ tags: ["expensive"]
831
+ },
832
+ [LLM_CONSTANTS.CLAUDE_HAIKU]: {
833
+ displayName: "Claude 4.5 Haiku",
834
+ modelApiName: "claude-haiku-4-5",
835
+ apiKeyName: API_KEY_CONSTANTS.ANTHROPIC,
836
+ hasThinking: true,
837
+ thinkingBudgetTokens: 1024,
838
+ tags: ["slow", "cheap"]
839
+ },
840
+ // DeepSeek V4 models — thinking-only entries (non-thinking variants retired 2026-08-05).
841
+ // reasoningEffort pinned to 'low' 2026-08-30: at the provider default ('high', no budget
842
+ // knob exists) both models emitted ~8 reasoning tokens per answer token in prod
843
+ // (requestStats 30d: flash p50 8.9s / p90 36s, pro p50 18.9s / p90 56s) and a 15-bot story
844
+ // took 68-105s. Latency tracks reasoning length ~linearly, so effort is the only lever.
845
+ [LLM_CONSTANTS.DEEPSEEK_FLASH]: {
846
+ displayName: "DeepSeek V4 Flash",
847
+ modelApiName: "deepseek-v4-flash",
848
+ apiKeyName: API_KEY_CONSTANTS.DEEPSEEK,
849
+ hasThinking: true,
850
+ reasoningEffort: "low",
851
+ // Reasoning tokens share the output budget, so leave room for both CoT and answer.
852
+ maxOutputTokens: 65536,
853
+ tags: ["cheap"]
854
+ },
855
+ [LLM_CONSTANTS.DEEPSEEK_PRO]: {
856
+ displayName: "DeepSeek V4 Pro",
857
+ modelApiName: "deepseek-v4-pro",
858
+ apiKeyName: API_KEY_CONSTANTS.DEEPSEEK,
859
+ hasThinking: true,
860
+ reasoningEffort: "low",
861
+ // Reasoning tokens share the output budget, so leave room for both CoT and answer.
862
+ maxOutputTokens: 65536,
863
+ tags: ["cheap"]
864
+ },
865
+ // Models with always-on reasoning
866
+ // GPT-5.6 family (promoted July 2026 when the limited preview opened up):
867
+ // sol is the flagship, terra the mainline, luna the cheap tier.
868
+ [LLM_CONSTANTS.GPT_SOL]: {
869
+ displayName: "GPT-5.6 Sol",
870
+ modelApiName: "gpt-5.6-sol",
871
+ apiKeyName: API_KEY_CONSTANTS.OPENAI,
872
+ hasThinking: true,
873
+ temperature: 1,
874
+ tags: ["expensive"]
875
+ },
876
+ [LLM_CONSTANTS.GPT]: {
877
+ displayName: "GPT-5.6 Terra",
878
+ modelApiName: "gpt-5.6-terra",
879
+ apiKeyName: API_KEY_CONSTANTS.OPENAI,
880
+ hasThinking: true,
881
+ temperature: 1,
882
+ tags: ["fast", "expensive"]
883
+ },
884
+ [LLM_CONSTANTS.GPT_MINI]: {
885
+ displayName: "GPT-5.6 Luna",
886
+ modelApiName: "gpt-5.6-luna",
887
+ apiKeyName: API_KEY_CONSTANTS.OPENAI,
888
+ hasThinking: true,
889
+ temperature: 1,
890
+ tags: ["fast", "cheap"]
891
+ },
892
+ // Gemini 3.x reasons via the effort dialect (thinkingLevel). The level is a CEILING on an
893
+ // always-dynamic process — the model still scales actual thinking depth per request within
894
+ // it; "high" is the fully open dynamic range. Levels below are each model's documented
895
+ // default (Pro accepts low|medium|high only — no minimal). This replaced the deprecated
896
+ // 2.5-era thinkingBudget: 1024 (2026-08-06), which HAD been binding — so Flash Lite now
897
+ // thinks noticeably less under its "minimal" default (0.8s/49-token votes vs 4.5s/650
898
+ // budgeted); bump it to 'low' if its output quality visibly drops.
899
+ [LLM_CONSTANTS.GEMINI_PRO]: {
900
+ displayName: "Gemini 3.1 Pro Preview",
901
+ modelApiName: "gemini-3.1-pro-preview",
902
+ apiKeyName: API_KEY_CONSTANTS.GOOGLE,
903
+ hasThinking: true,
904
+ reasoningEffort: "high",
905
+ tags: ["expensive"]
906
+ },
907
+ [LLM_CONSTANTS.GEMINI_FLASH]: {
908
+ // Repointed from gemini-3.6-flash 2026-08-13 (stable picker id, same pattern as gpt).
909
+ // 3.7 rejects thinkingLevel 'minimal' (low|medium|high only), unlike 3.5/3.6.
910
+ displayName: "Gemini 3.7 Flash",
911
+ modelApiName: "gemini-3.7-flash",
912
+ apiKeyName: API_KEY_CONSTANTS.GOOGLE,
913
+ hasThinking: true,
914
+ reasoningEffort: "medium",
915
+ tags: ["fast"]
916
+ },
917
+ [LLM_CONSTANTS.GEMINI_LITE]: {
918
+ displayName: "Gemini 3.5 Flash Lite",
919
+ modelApiName: "gemini-3.5-flash-lite",
920
+ apiKeyName: API_KEY_CONSTANTS.GOOGLE,
921
+ hasThinking: true,
922
+ reasoningEffort: "minimal",
923
+ tags: ["fast", "cheap"]
924
+ },
925
+ // Always-on reasoning (xAI default effort "high", cannot be disabled) — no non-thinking sibling
926
+ [LLM_CONSTANTS.GROK]: {
927
+ displayName: "Grok 4.6",
928
+ modelApiName: "grok-4.6",
929
+ apiKeyName: API_KEY_CONSTANTS.GROK,
930
+ hasThinking: true,
931
+ temperature: 0.7
932
+ },
933
+ // Mistral models
934
+ [LLM_CONSTANTS.MISTRAL_LARGE]: {
935
+ displayName: "Mistral Large 3",
936
+ modelApiName: "mistral-large-latest",
937
+ apiKeyName: API_KEY_CONSTANTS.MISTRAL,
938
+ hasThinking: false,
939
+ tags: ["fast"]
940
+ },
941
+ [LLM_CONSTANTS.MISTRAL_MEDIUM]: {
942
+ displayName: "Mistral Medium 3.5",
943
+ modelApiName: "mistral-medium-3",
944
+ apiKeyName: API_KEY_CONSTANTS.MISTRAL,
945
+ hasThinking: false,
946
+ tags: ["very-fast", "expensive"]
947
+ },
948
+ [LLM_CONSTANTS.MISTRAL_SMALL]: {
949
+ displayName: "Mistral 4 Small",
950
+ modelApiName: "mistral-small-latest",
951
+ apiKeyName: API_KEY_CONSTANTS.MISTRAL,
952
+ hasThinking: false,
953
+ tags: ["very-fast", "cheap"]
954
+ },
955
+ [LLM_CONSTANTS.MISTRAL_MAGISTRAL]: {
956
+ displayName: "Magistral Medium 1.2",
957
+ modelApiName: "magistral-medium-latest",
958
+ apiKeyName: API_KEY_CONSTANTS.MISTRAL,
959
+ hasThinking: true,
960
+ // Measured very-fast (1.6s) because JSON response mode suppresses its thinking
961
+ // (see mistral-agent.ts) — it effectively runs as a non-reasoning model here.
962
+ tags: ["very-fast"]
963
+ },
964
+ // Kimi models. Single always-reasoning entry: K3 reasons by default and the only way to stop
965
+ // it is the undocumented K2-era `thinking: disabled` toggle, which we no longer rely on.
966
+ // K3 always reasons at max effort; ~85-90% of its output tokens are reasoning tokens billed
967
+ // at the output rate, so real per-request cost runs well above the sticker output price.
968
+ [LLM_CONSTANTS.KIMI]: {
969
+ displayName: "Kimi K3",
970
+ modelApiName: "kimi-k3",
971
+ apiKeyName: API_KEY_CONSTANTS.MOONSHOT,
972
+ hasThinking: true,
973
+ // Temperature is omitted from the request: kimi-k3 rejects any value other than 1.
974
+ // Speed samples: 17s (2026-08-04) and 28.9s (2026-08-05) — graded into the >25s tier.
975
+ tags: ["very-slow", "expensive"]
976
+ },
977
+ // Z.AI models — thinking-only entry (non-thinking variant retired 2026-08-05)
978
+ // reasoningEffort MUST be set: GLM-5.3 forces reasoning on and defaults the effort to 'max',
979
+ // and its reasoning tokens count against max_tokens. At 'max' a long-context request can
980
+ // burn the whole 8192 budget on reasoning and return finish_reason 'length' with content ""
981
+ // (prod empty-response incidents + live repro, 2026-08-20). 'high' answered the same test
982
+ // prompt with ~10x fewer reasoning tokens.
983
+ [LLM_CONSTANTS.GLM]: {
984
+ displayName: "GLM-5.3",
985
+ modelApiName: "glm-5.3",
986
+ apiKeyName: API_KEY_CONSTANTS.Z_AI,
987
+ hasThinking: true,
988
+ temperature: 0.7,
989
+ reasoningEffort: "high",
990
+ // Headroom for the shared reasoning+answer budget (like the DeepSeek entries), sized
991
+ // at 2x default rather than DeepSeek's 65536 to bound worst-case latency on a slow model.
992
+ maxOutputTokens: 16384,
993
+ tags: ["slow"]
994
+ },
995
+ // GLM-5.3-Flash (added 2026-08-30): the cheap sibling. Same API contract as GLM-5.3 —
996
+ // thinking cannot be disabled and reasoning_effort takes low|high|max only
997
+ // (docs.z.ai/guides/llm/glm-5.3-flash, /guides/capabilities/thinking), so it gets the same
998
+ // 'high' pin and the same reasoning+answer headroom.
999
+ [LLM_CONSTANTS.GLM_FLASH]: {
1000
+ displayName: "GLM-5.3 Flash",
1001
+ modelApiName: "glm-5.3-flash",
1002
+ apiKeyName: API_KEY_CONSTANTS.Z_AI,
1003
+ hasThinking: true,
1004
+ temperature: 0.7,
1005
+ reasoningEffort: "high",
1006
+ maxOutputTokens: 16384,
1007
+ // Live 2026-08-30 (one sample each): day-2 vote 11.8s, 15-character story 56.2s.
1008
+ tags: ["cheap"]
1009
+ },
1010
+ // Sakana Fugu models — OpenAI-compatible. They reason internally (and bill it as
1011
+ // "orchestration" tokens), but never surface reasoning to us: responses come back with
1012
+ // reasoning_tokens: 0 and no reasoning_content. So hasThinking is false — there's no
1013
+ // thinking content to show and no user-facing thinking toggle. Single entry per model.
1014
+ //
1015
+ // Base `fugu` was RETIRED 2026-08-04. It was carried as a cheap everyday option at an
1016
+ // assumed $1/$3, but reconciling token logs against the Sakana balance showed it actually
1017
+ // bills at fugu-ultra's rates: 592K prompt + 54K completion tokens over Aug 1-3 cost $4.80
1018
+ // real against $0.85 tracked, a 5.7x undercharge. It is a router with no published price,
1019
+ // so the rate is not even guaranteed stable, and its cache hit rate was 9.3% — effectively
1020
+ // zero, since every hit came from a duplicate call seconds apart rather than turn-to-turn
1021
+ // prefix reuse. Ultra costs the same and is predictable.
1022
+ [LLM_CONSTANTS.FUGU_ULTRA]: {
1023
+ displayName: "Sakana Fugu Ultra",
1024
+ modelApiName: "fugu-ultra",
1025
+ apiKeyName: API_KEY_CONSTANTS.FUGU,
1026
+ hasThinking: false,
1027
+ tags: ["extremely-slow", "expensive"]
1028
+ },
1029
+ // Qwen models (QwenCloud, OpenAI-compatible endpoint). Added 2026-08-05 straight into the
1030
+ // thinking-only catalog: their API has an `enable_thinking` toggle, we always send true, and
1031
+ // thinking arrives in `reasoning_content` (verified live against all three, non-streaming).
1032
+ // Speed tags from the 2026-08-05 live measurements (two samples each): plus 17.4s/14.5s,
1033
+ // flash 14.3s/16.4s (both slow); max 30.6s/100.5s — its latency tracks how long it decides
1034
+ // to think (4.2K reasoning tokens on the slow run), hence the budget cap below.
1035
+ [LLM_CONSTANTS.QWEN_MAX]: {
1036
+ displayName: "Qwen3.8 Max",
1037
+ modelApiName: "qwen3.8-max",
1038
+ apiKeyName: API_KEY_CONSTANTS.QWEN,
1039
+ hasThinking: true,
1040
+ temperature: 0.7,
1041
+ // Caps `thinking_budget` to bound the 30–100s latency variance. The same knob works on
1042
+ // the 3.7 models (verified live) — add it to their entries if they ever need taming.
1043
+ thinkingBudgetTokens: 1024,
1044
+ // Capped it measures 25-26s → the >25s tier.
1045
+ tags: ["very-slow"]
1046
+ },
1047
+ // qwen3.8-flash replaced qwen3.7-flash on 2026-08-30 (same 1M context, 128k max output);
1048
+ // qwen3.7-plus was retired the same day — persisted 'qwen-plus' ids resolve to this entry
1049
+ // in consumers' deprecated-id maps. Live 2026-08-30 (one sample each): day-2 vote 13.8s,
1050
+ // 15-character story 26.4s — same bucket as 3.7-flash, so the tags carry over.
1051
+ [LLM_CONSTANTS.QWEN_FLASH]: {
1052
+ displayName: "Qwen3.8 Flash",
1053
+ modelApiName: "qwen3.8-flash",
1054
+ apiKeyName: API_KEY_CONSTANTS.QWEN,
1055
+ hasThinking: true,
1056
+ temperature: 0.7,
1057
+ // Uncapped it swung to 3K reasoning tokens (21s); same cap as its siblings.
1058
+ thinkingBudgetTokens: 1024,
1059
+ tags: ["slow", "cheap"]
1060
+ },
1061
+ // MiniMax M3 (OpenAI-compatible endpoint, 1M context). Thinking-only entry: M3's `thinking`
1062
+ // param defaults to adaptive (it decides per-request how much to think) and can be disabled,
1063
+ // making it hybrid for cost purposes. The agent always sends `reasoning_split: true` so
1064
+ // thinking arrives in `reasoning_content` instead of as `<think>` tags inside the answer.
1065
+ // Note: unlike Qwen, M3 has NO thinking-budget parameter — adaptive is the only throttle.
1066
+ // Speed from the 2026-08-05 live measurement (single sample): 25.3s → the >25s tier.
1067
+ // Temperature: MiniMax range is [0,2], default 1.
1068
+ [LLM_CONSTANTS.MINIMAX]: {
1069
+ displayName: "MiniMax M3",
1070
+ modelApiName: "MiniMax-M3",
1071
+ apiKeyName: API_KEY_CONSTANTS.MINIMAX,
1072
+ hasThinking: true,
1073
+ temperature: 1,
1074
+ tags: ["very-slow", "cheap"]
1075
+ }
1076
+ };
1077
+ function createCatalog(overrides = {}) {
1078
+ const catalog = {};
1079
+ for (const [id, config] of Object.entries(SupportedAiModels)) {
1080
+ catalog[id] = { ...config, ...overrides[id] ?? {} };
1081
+ }
1082
+ for (const [id, config] of Object.entries(overrides)) {
1083
+ if (!catalog[id]) {
1084
+ catalog[id] = config;
1085
+ }
1086
+ }
1087
+ return catalog;
1088
+ }
1089
+ function getModelTags(modelId) {
1090
+ return SupportedAiModels[modelId]?.tags ?? [];
1091
+ }
1092
+ function modelHasTag(modelId, tag) {
1093
+ return getModelTags(modelId).includes(tag);
1094
+ }
1095
+ function modelIsFast(modelId) {
1096
+ return modelHasTag(modelId, "fast") || modelHasTag(modelId, "very-fast");
1097
+ }
1098
+ function getModelDisplayName(modelId) {
1099
+ return SupportedAiModels[modelId]?.displayName ?? modelId;
1100
+ }
1101
+ function getModelProviderName(modelId) {
1102
+ const apiKeyName = SupportedAiModels[modelId]?.apiKeyName;
1103
+ return apiKeyName ? SupportedAiKeyNames[apiKeyName] : void 0;
1104
+ }
1105
+ function getModelConfigByApiName(modelApiName, hasThinking) {
1106
+ const candidates = Object.values(SupportedAiModels).filter((config) => config.modelApiName === modelApiName);
1107
+ if (hasThinking !== void 0) {
1108
+ const exact = candidates.find((config) => config.hasThinking === hasThinking);
1109
+ if (exact) {
1110
+ return exact;
1111
+ }
1112
+ }
1113
+ return candidates[0];
1114
+ }
1115
+ function isInPeakWindow(timestampMs, windowsUtc) {
1116
+ const d = new Date(timestampMs);
1117
+ const hour = d.getUTCHours() + d.getUTCMinutes() / 60;
1118
+ return windowsUtc.some(([start, end]) => hour >= start && hour < end);
1119
+ }
1120
+ function isWeekendAt(timestampMs, utcOffsetHours) {
1121
+ const day = new Date(timestampMs + utcOffsetHours * 36e5).getUTCDay();
1122
+ return day === 0 || day === 6;
1123
+ }
1124
+ function isPeakBilling(timestampMs, peak) {
1125
+ if (peak.weekendOffPeak && isWeekendAt(timestampMs, peak.weekendOffPeak.utcOffsetHours)) {
1126
+ return false;
1127
+ }
1128
+ return isInPeakWindow(timestampMs, peak.windowsUtc);
1129
+ }
1130
+ var DEEPSEEK_PEAK_SCHEDULE = {
1131
+ multiplier: 2,
1132
+ windowsUtc: [[1, 4], [6, 10]],
1133
+ weekendOffPeak: { utcOffsetHours: 8 }
1134
+ };
1135
+ var MODEL_PRICING = {
1136
+ // OpenAI GPT-5.6 models
1137
+ // Sol repriced 2026-08-30 (developers.openai.com/api/docs/pricing): $4/$20 short context,
1138
+ // $8/$30 past the long-context threshold — the same 272k boundary its siblings use.
1139
+ // Cache writes ($5/$10) are not modelled; OpenAI caching is automatic and we only see hits.
1140
+ [SupportedAiModels[LLM_CONSTANTS.GPT_SOL].modelApiName]: {
1141
+ inputPrice: 4,
1142
+ outputPrice: 20,
1143
+ cacheHitPrice: 0.4,
1144
+ extendedContextInputPrice: 8,
1145
+ extendedContextOutputPrice: 30,
1146
+ extendedContextCacheHitPrice: 0.8,
1147
+ extendedContextThresholdTokens: 272e3
1148
+ },
1149
+ [SupportedAiModels[LLM_CONSTANTS.GPT].modelApiName]: {
1150
+ inputPrice: 2,
1151
+ outputPrice: 12,
1152
+ cacheHitPrice: 0.2,
1153
+ extendedContextInputPrice: 4,
1154
+ extendedContextOutputPrice: 18,
1155
+ extendedContextCacheHitPrice: 0.4,
1156
+ extendedContextThresholdTokens: 272e3
1157
+ },
1158
+ [SupportedAiModels[LLM_CONSTANTS.GPT_MINI].modelApiName]: {
1159
+ inputPrice: 0.2,
1160
+ outputPrice: 1.2,
1161
+ cacheHitPrice: 0.02,
1162
+ extendedContextInputPrice: 0.4,
1163
+ extendedContextOutputPrice: 1.8,
1164
+ extendedContextCacheHitPrice: 0.04,
1165
+ extendedContextThresholdTokens: 272e3
1166
+ },
1167
+ // DeepSeek V4 models
1168
+ // Peak-valley pricing landed: these are the new base (off-peak) rates with a 2× surcharge
1169
+ // during UTC 1:00–4:00 and 6:00–10:00, effective provider-side 2026-08-16 16:00 UTC
1170
+ // (api-docs.deepseek.com/quick_start/pricing, fetched 2026-08-13; rates re-confirmed
1171
+ // 2026-08-30). Since 2026-08-23 00:00 Beijing (UTC+8) the surcharge is weekdays-only:
1172
+ // Saturday and Sunday Beijing time bill at the off-peak rate all day (DeepSeek notice email).
1173
+ [SupportedAiModels[LLM_CONSTANTS.DEEPSEEK_FLASH].modelApiName]: {
1174
+ inputPrice: 0.22,
1175
+ outputPrice: 0.66,
1176
+ cacheHitPrice: 7e-3,
1177
+ peakPricing: DEEPSEEK_PEAK_SCHEDULE
1178
+ },
1179
+ [SupportedAiModels[LLM_CONSTANTS.DEEPSEEK_PRO].modelApiName]: {
1180
+ inputPrice: 0.66,
1181
+ outputPrice: 1.98,
1182
+ cacheHitPrice: 0.022,
1183
+ peakPricing: DEEPSEEK_PEAK_SCHEDULE
1184
+ },
1185
+ // Kimi/Moonshot models
1186
+ [SupportedAiModels[LLM_CONSTANTS.KIMI].modelApiName]: {
1187
+ inputPrice: 3,
1188
+ outputPrice: 15,
1189
+ cacheHitPrice: 0.3
1190
+ },
1191
+ // Z.AI models
1192
+ [SupportedAiModels[LLM_CONSTANTS.GLM].modelApiName]: {
1193
+ inputPrice: 1.4,
1194
+ outputPrice: 4.4,
1195
+ cacheHitPrice: 0.26
1196
+ },
1197
+ // GLM-5.3-Flash list rates (docs.z.ai/guides/overview/pricing, 2026-08-30). The page shows a
1198
+ // 50% promo ($0.075 / $0.015 / $0.25) ending 2026-09-09 24:00 UTC+8; we bill the list rate
1199
+ // rather than track a ten-day promo.
1200
+ [SupportedAiModels[LLM_CONSTANTS.GLM_FLASH].modelApiName]: {
1201
+ inputPrice: 0.15,
1202
+ outputPrice: 0.5,
1203
+ cacheHitPrice: 0.03
1204
+ },
1205
+ // Anthropic models
1206
+ [SupportedAiModels[LLM_CONSTANTS.CLAUDE_FABLE].modelApiName]: {
1207
+ // Full 1M context window at standard pricing (no extended-context premium)
1208
+ inputPrice: 10,
1209
+ outputPrice: 50,
1210
+ cacheHitPrice: 1
1211
+ },
1212
+ [SupportedAiModels[LLM_CONSTANTS.CLAUDE_OPUS].modelApiName]: {
1213
+ inputPrice: 5,
1214
+ outputPrice: 25,
1215
+ cacheHitPrice: 0.5
1216
+ },
1217
+ [SupportedAiModels[LLM_CONSTANTS.CLAUDE_SONNET].modelApiName]: {
1218
+ inputPrice: 2,
1219
+ outputPrice: 10,
1220
+ cacheHitPrice: 0.2
1221
+ },
1222
+ [SupportedAiModels[LLM_CONSTANTS.CLAUDE_HAIKU].modelApiName]: {
1223
+ inputPrice: 1,
1224
+ outputPrice: 5,
1225
+ cacheHitPrice: 0.1
1226
+ },
1227
+ // Google models
1228
+ [SupportedAiModels[LLM_CONSTANTS.GEMINI_PRO].modelApiName]: {
1229
+ inputPrice: 2,
1230
+ outputPrice: 12,
1231
+ cacheHitPrice: 0.2,
1232
+ extendedContextInputPrice: 4,
1233
+ extendedContextOutputPrice: 18,
1234
+ extendedContextCacheHitPrice: 0.4,
1235
+ extendedContextThresholdTokens: 2e5
1236
+ },
1237
+ [SupportedAiModels[LLM_CONSTANTS.GEMINI_FLASH].modelApiName]: {
1238
+ // Launch pricing through 2026-12-31; doubles to $1.50/$7.50/$0.15 on 2027-01-01
1239
+ // (ai.google.dev pricing page, fetched 2026-08-13) — ACTION NEEDED then: update these
1240
+ // rates.
1241
+ // Cache storage cost ($0.50 / 1M tokens per hour) is not tracked here — the
1242
+ // schema only models per-token call costs, not time-based storage.
1243
+ inputPrice: 0.75,
1244
+ outputPrice: 3.75,
1245
+ cacheHitPrice: 0.075
1246
+ },
1247
+ [SupportedAiModels[LLM_CONSTANTS.GEMINI_LITE].modelApiName]: {
1248
+ // Cache storage cost ($1.00 / 1M tokens per hour) is not tracked here — the
1249
+ // schema only models per-token call costs, not time-based storage.
1250
+ inputPrice: 0.3,
1251
+ outputPrice: 1.5,
1252
+ cacheHitPrice: 0.025
1253
+ },
1254
+ // Mistral models. Cached tokens bill at 10% of the input price (documented on the
1255
+ // prompt_cache_key param in the API reference; no per-model cached prices published).
1256
+ [SupportedAiModels[LLM_CONSTANTS.MISTRAL_LARGE].modelApiName]: {
1257
+ inputPrice: 0.5,
1258
+ outputPrice: 1.5,
1259
+ cacheHitPrice: 0.05
1260
+ },
1261
+ [SupportedAiModels[LLM_CONSTANTS.MISTRAL_MEDIUM].modelApiName]: {
1262
+ inputPrice: 1.5,
1263
+ outputPrice: 7.5,
1264
+ cacheHitPrice: 0.15
1265
+ },
1266
+ [SupportedAiModels[LLM_CONSTANTS.MISTRAL_SMALL].modelApiName]: {
1267
+ inputPrice: 0.15,
1268
+ outputPrice: 0.6,
1269
+ cacheHitPrice: 0.015
1270
+ },
1271
+ [SupportedAiModels[LLM_CONSTANTS.MISTRAL_MAGISTRAL].modelApiName]: {
1272
+ inputPrice: 2,
1273
+ outputPrice: 5,
1274
+ cacheHitPrice: 0.2
1275
+ },
1276
+ // Grok models. Cached price is per-model on xAI (not a uniform ratio):
1277
+ // grok-4.6 is $0.50/M cached vs $2.00/M input, and all rates double for prompts
1278
+ // >= 200K tokens, per docs.x.ai/developers/models (verified 2026-08-12).
1279
+ [SupportedAiModels[LLM_CONSTANTS.GROK].modelApiName]: {
1280
+ inputPrice: 2,
1281
+ outputPrice: 6,
1282
+ cacheHitPrice: 0.5,
1283
+ extendedContextInputPrice: 4,
1284
+ extendedContextOutputPrice: 12,
1285
+ extendedContextCacheHitPrice: 1,
1286
+ extendedContextThresholdTokens: 2e5
1287
+ },
1288
+ // Sakana Fugu models. Base `fugu` was retired 2026-08-04 — it had no published price and
1289
+ // measured out at these same ultra rates, so it has no pricing entry.
1290
+ // fugu-ultra has published pricing. Above 272K context the rates roughly double.
1291
+ [SupportedAiModels[LLM_CONSTANTS.FUGU_ULTRA].modelApiName]: {
1292
+ inputPrice: 5,
1293
+ outputPrice: 30,
1294
+ cacheHitPrice: 0.5,
1295
+ extendedContextInputPrice: 10,
1296
+ extendedContextOutputPrice: 45,
1297
+ extendedContextCacheHitPrice: 1,
1298
+ extendedContextThresholdTokens: 272e3
1299
+ },
1300
+ // Qwen models. Rates from the official pricing page (qwencloud.com/pricing/api, read
1301
+ // 2026-08-30 — the page is client-rendered, so it was read by eye, not WebFetch):
1302
+ // qwen3.8-max $2/$6 with implicit-cache hits at $0.25; qwen3.8-flash $0.15/$0.47, hits
1303
+ // $0.016. Neither has input-length tiers (the tier column is "-" for both). These
1304
+ // published cached rates supersede the 20%-of-input rule charged before 2026-08-30; we
1305
+ // still don't send explicit cache_control.
1306
+ [SupportedAiModels[LLM_CONSTANTS.QWEN_MAX].modelApiName]: {
1307
+ inputPrice: 2,
1308
+ outputPrice: 6,
1309
+ cacheHitPrice: 0.25
1310
+ },
1311
+ [SupportedAiModels[LLM_CONSTANTS.QWEN_FLASH].modelApiName]: {
1312
+ inputPrice: 0.15,
1313
+ outputPrice: 0.47,
1314
+ cacheHitPrice: 0.016
1315
+ },
1316
+ // MiniMax M3. Rates from platform.minimax.io/docs/guides/pricing-paygo (2026-08-05, USD,
1317
+ // "permanent 50% off" already applied): ≤512k and >512k input tiers. Caching is automatic
1318
+ // (≥512 input tokens), hits reported in prompt_tokens_details.cached_tokens; no write fee
1319
+ // for M3.
1320
+ [SupportedAiModels[LLM_CONSTANTS.MINIMAX].modelApiName]: {
1321
+ inputPrice: 0.3,
1322
+ outputPrice: 1.2,
1323
+ cacheHitPrice: 0.06,
1324
+ extendedContextInputPrice: 0.6,
1325
+ extendedContextOutputPrice: 2.4,
1326
+ extendedContextCacheHitPrice: 0.12,
1327
+ extendedContextThresholdTokens: 512e3
1328
+ }
1329
+ };
1330
+ var HYBRID_THINKING_API_NAMES = /* @__PURE__ */ new Set([
1331
+ SupportedAiModels[LLM_CONSTANTS.CLAUDE_OPUS].modelApiName,
1332
+ SupportedAiModels[LLM_CONSTANTS.CLAUDE_SONNET].modelApiName,
1333
+ SupportedAiModels[LLM_CONSTANTS.CLAUDE_HAIKU].modelApiName,
1334
+ SupportedAiModels[LLM_CONSTANTS.DEEPSEEK_FLASH].modelApiName,
1335
+ SupportedAiModels[LLM_CONSTANTS.DEEPSEEK_PRO].modelApiName,
1336
+ SupportedAiModels[LLM_CONSTANTS.GLM].modelApiName,
1337
+ SupportedAiModels[LLM_CONSTANTS.GLM_FLASH].modelApiName,
1338
+ // Qwen ships thinking-only from day one, but the API's enable_thinking toggle makes these
1339
+ // hybrid by the same definition: we force reasoning on, so they carry the multiplier.
1340
+ SupportedAiModels[LLM_CONSTANTS.QWEN_MAX].modelApiName,
1341
+ SupportedAiModels[LLM_CONSTANTS.QWEN_FLASH].modelApiName,
1342
+ SupportedAiModels[LLM_CONSTANTS.MINIMAX].modelApiName
1343
+ ]);
1344
+ function isHybridThinkingModel(modelApiName) {
1345
+ return HYBRID_THINKING_API_NAMES.has(modelApiName);
1346
+ }
1347
+ function calculateModelCost(modelApiName, inputTokens, outputTokens, options = {}) {
1348
+ const pricing = MODEL_PRICING[modelApiName];
1349
+ if (!pricing) {
1350
+ console.warn(`No pricing information available for model: ${modelApiName}`);
1351
+ return 0;
1352
+ }
1353
+ const divisor = 1e6;
1354
+ const cacheHitTokens = Math.max(0, options.cacheHitTokens ?? 0);
1355
+ const actualCacheHits = Math.min(cacheHitTokens, inputTokens);
1356
+ const uncachedInputTokens = Math.max(0, inputTokens - actualCacheHits);
1357
+ const contextTokens = options.contextTokens ?? options.totalTokens ?? inputTokens;
1358
+ let activeInputPrice = pricing.inputPrice;
1359
+ let activeOutputPrice = pricing.outputPrice;
1360
+ let activeCachePrice = pricing.cacheHitPrice ?? pricing.inputPrice;
1361
+ if (pricing.extendedContextThresholdTokens !== void 0 && contextTokens > pricing.extendedContextThresholdTokens) {
1362
+ activeInputPrice = pricing.extendedContextInputPrice ?? pricing.inputPrice;
1363
+ activeOutputPrice = pricing.extendedContextOutputPrice ?? pricing.outputPrice;
1364
+ activeCachePrice = pricing.extendedContextCacheHitPrice ?? pricing.cacheHitPrice ?? activeInputPrice;
1365
+ } else if (pricing.cacheHitPrice !== void 0) {
1366
+ activeCachePrice = pricing.cacheHitPrice;
1367
+ }
1368
+ if (pricing.peakPricing && isPeakBilling(options.timestamp ?? Date.now(), pricing.peakPricing)) {
1369
+ activeInputPrice *= pricing.peakPricing.multiplier;
1370
+ activeOutputPrice *= pricing.peakPricing.multiplier;
1371
+ activeCachePrice *= pricing.peakPricing.multiplier;
1372
+ }
1373
+ const uncachedInputCost = uncachedInputTokens * activeInputPrice / divisor;
1374
+ const cachedInputCost = actualCacheHits * activeCachePrice / divisor;
1375
+ const outputCost = outputTokens * activeOutputPrice / divisor;
1376
+ return uncachedInputCost + cachedInputCost + outputCost;
1377
+ }
1378
+ function getProviderSignatureFields(aiType, signature) {
1379
+ if (!signature) {
1380
+ return {};
1381
+ }
1382
+ if (aiType.startsWith("claude-")) {
1383
+ return { anthropicThinkingSignature: signature };
1384
+ }
1385
+ if (aiType.startsWith("gemini-")) {
1386
+ return { googleThoughtSignature: signature };
1387
+ }
1388
+ if (aiType.startsWith("grok")) {
1389
+ return { grokEncryptedReasoning: signature };
1390
+ }
1391
+ return {};
1392
+ }
1393
+
1394
+ // src/reasoning-effort.ts
1395
+ var REASONING_EFFORT_SCALE = ["minimal", "low", "medium", "high", "xhigh", "max"];
1396
+ var OPENAI_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
1397
+ var ANTHROPIC_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
1398
+ var GEMINI_REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
1399
+ var GLM_REASONING_EFFORTS = ["low", "high", "max"];
1400
+ var DEEPSEEK_REASONING_EFFORTS = ["low", "high", "max"];
1401
+ var FUGU_REASONING_EFFORTS = ["high", "xhigh"];
1402
+ function clampReasoningEffort(effort, allowed) {
1403
+ const rank = REASONING_EFFORT_SCALE.indexOf(effort);
1404
+ let best = allowed[0];
1405
+ let bestDistance = Infinity;
1406
+ for (const candidate of allowed) {
1407
+ const distance = Math.abs(REASONING_EFFORT_SCALE.indexOf(candidate) - rank);
1408
+ if (distance < bestDistance || distance === bestDistance && REASONING_EFFORT_SCALE.indexOf(candidate) > REASONING_EFFORT_SCALE.indexOf(best)) {
1409
+ best = candidate;
1410
+ bestDistance = distance;
1411
+ }
1412
+ }
1413
+ return best;
1414
+ }
1415
+ var toOpenAIEffort = (effort) => clampReasoningEffort(effort, OPENAI_REASONING_EFFORTS);
1416
+ var toAnthropicEffort = (effort) => clampReasoningEffort(effort, ANTHROPIC_REASONING_EFFORTS);
1417
+ var toGeminiEffort = (effort) => clampReasoningEffort(effort, GEMINI_REASONING_EFFORTS);
1418
+ var toGlmEffort = (effort) => clampReasoningEffort(effort, GLM_REASONING_EFFORTS);
1419
+ var toDeepSeekEffort = (effort) => clampReasoningEffort(effort, DEEPSEEK_REASONING_EFFORTS);
1420
+ var toFuguEffort = (effort) => clampReasoningEffort(effort, FUGU_REASONING_EFFORTS);
1421
+
1422
+ // src/pricing/token-usage-utils.ts
1423
+ function extractTokenUsage(response) {
1424
+ if (!response?.usage) {
1425
+ return null;
1426
+ }
1427
+ const usage = response.usage;
1428
+ const result = {
1429
+ promptTokens: usage.prompt_tokens || 0,
1430
+ completionTokens: usage.completion_tokens || 0,
1431
+ totalTokens: usage.total_tokens || 0
1432
+ };
1433
+ if (usage.prompt_cache_hit_tokens !== void 0) {
1434
+ result.cacheHitTokens = usage.prompt_cache_hit_tokens;
1435
+ } else if (usage.prompt_tokens_details?.cached_tokens !== void 0) {
1436
+ result.cacheHitTokens = usage.prompt_tokens_details.cached_tokens;
1437
+ } else if (usage.cached_tokens !== void 0) {
1438
+ result.cacheHitTokens = usage.cached_tokens;
1439
+ }
1440
+ if (usage.prompt_cache_miss_tokens !== void 0) {
1441
+ result.cacheMissTokens = usage.prompt_cache_miss_tokens;
1442
+ }
1443
+ if (usage.completion_tokens_details?.reasoning_tokens !== void 0) {
1444
+ result.reasoningTokens = usage.completion_tokens_details.reasoning_tokens;
1445
+ }
1446
+ return result;
1447
+ }
1448
+ function calculateCost(modelApiName, inputTokens, outputTokens, options = {}) {
1449
+ return calculateModelCost(modelApiName, inputTokens, outputTokens, options);
1450
+ }
1451
+ function extractUsageAndCalculateCost(modelApiName, response) {
1452
+ const usage = extractTokenUsage(response);
1453
+ if (!usage) {
1454
+ return null;
1455
+ }
1456
+ const cost = calculateCost(modelApiName, usage.promptTokens, usage.completionTokens, {
1457
+ cacheHitTokens: usage.cacheHitTokens || 0,
1458
+ totalTokens: usage.totalTokens
1459
+ });
1460
+ return { usage, cost };
1461
+ }
1462
+ function extractDeepSeekTokenUsage(response) {
1463
+ return extractTokenUsage(response);
1464
+ }
1465
+ function extractOpenAITokenUsage(response) {
1466
+ return extractTokenUsage(response);
1467
+ }
1468
+ function extractKimiTokenUsage(response) {
1469
+ return extractTokenUsage(response);
1470
+ }
1471
+ function extractGrokTokenUsage(response) {
1472
+ return extractTokenUsage(response);
1473
+ }
1474
+ function extractAnthropicTokenUsage(response) {
1475
+ if (!response?.usage) {
1476
+ return null;
1477
+ }
1478
+ const usage = response.usage;
1479
+ return {
1480
+ promptTokens: usage.input_tokens || 0,
1481
+ completionTokens: usage.output_tokens || 0,
1482
+ totalTokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
1483
+ };
1484
+ }
1485
+ function extractGoogleTokenUsage(response) {
1486
+ if (!response?.usageMetadata) {
1487
+ return null;
1488
+ }
1489
+ const usage = response.usageMetadata;
1490
+ const result = {
1491
+ promptTokens: usage.promptTokenCount || 0,
1492
+ completionTokens: usage.candidatesTokenCount || 0,
1493
+ totalTokens: usage.totalTokenCount || 0
1494
+ };
1495
+ if (usage.cachedContentTokenCount !== void 0) {
1496
+ result.cacheHitTokens = usage.cachedContentTokenCount;
1497
+ }
1498
+ return result;
1499
+ }
1500
+ function extractMistralTokenUsage(response) {
1501
+ const usage = response?.usage;
1502
+ if (!usage) {
1503
+ return null;
1504
+ }
1505
+ const result = {
1506
+ promptTokens: usage.promptTokens || 0,
1507
+ completionTokens: usage.completionTokens || 0,
1508
+ totalTokens: usage.totalTokens || 0
1509
+ };
1510
+ if (usage.additionalProperties) {
1511
+ const additionalProps = usage.additionalProperties;
1512
+ if (additionalProps.reasoning_tokens !== void 0) {
1513
+ result.reasoningTokens = additionalProps.reasoning_tokens;
1514
+ } else if (additionalProps.reasoningTokens !== void 0) {
1515
+ result.reasoningTokens = additionalProps.reasoningTokens;
1516
+ } else if (additionalProps.thinking_tokens !== void 0) {
1517
+ result.reasoningTokens = additionalProps.thinking_tokens;
1518
+ }
1519
+ if (additionalProps.prompt_cache_hit_tokens !== void 0) {
1520
+ result.cacheHitTokens = additionalProps.prompt_cache_hit_tokens;
1521
+ } else if (additionalProps.cached_tokens !== void 0) {
1522
+ result.cacheHitTokens = additionalProps.cached_tokens;
1523
+ } else if (additionalProps.prompt_tokens_details?.cached_tokens !== void 0) {
1524
+ result.cacheHitTokens = additionalProps.prompt_tokens_details.cached_tokens;
1525
+ }
1526
+ }
1527
+ return result;
1528
+ }
1529
+
1530
+ // src/pricing/openai-pricing.ts
1531
+ function calculateOpenAICost(model, inputTokens, outputTokens, cacheHitTokens = 0) {
1532
+ return calculateCost(model, inputTokens, outputTokens, { cacheHitTokens });
1533
+ }
1534
+ function extractTokenUsageFromResponse(response) {
1535
+ return extractOpenAITokenUsage(response);
1536
+ }
1537
+
1538
+ // src/pricing/deepseek-pricing.ts
1539
+ function calculateDeepSeekCost(model, inputTokens, outputTokens, cacheHitTokens = 0) {
1540
+ return calculateCost(model, inputTokens, outputTokens, { cacheHitTokens });
1541
+ }
1542
+ function extractTokenUsageFromResponse2(response) {
1543
+ return extractDeepSeekTokenUsage(response);
1544
+ }
1545
+
1546
+ // src/pricing/kimi-pricing.ts
1547
+ function calculateKimiCost(model, inputTokens, outputTokens) {
1548
+ return calculateCost(model, inputTokens, outputTokens);
1549
+ }
1550
+ function extractTokenUsageFromResponse3(response) {
1551
+ return extractKimiTokenUsage(response);
1552
+ }
1553
+
1554
+ // src/pricing/grok-pricing.ts
1555
+ function calculateGrokCost(model, inputTokens, outputTokens, cacheHitTokens = 0) {
1556
+ return calculateCost(model, inputTokens, outputTokens, { cacheHitTokens });
1557
+ }
1558
+ function extractTokenUsageFromResponse4(response) {
1559
+ return extractGrokTokenUsage(response);
1560
+ }
1561
+
1562
+ // src/pricing/anthropic-pricing.ts
1563
+ function calculateAnthropicCost(model, inputTokens, outputTokens, cacheHitTokens = 0) {
1564
+ return calculateCost(model, inputTokens, outputTokens, { cacheHitTokens });
1565
+ }
1566
+ function extractTokenUsageFromResponse5(response) {
1567
+ return extractAnthropicTokenUsage(response);
1568
+ }
1569
+
1570
+ // src/pricing/google-pricing.ts
1571
+ function calculateGoogleCost(model, inputTokens, outputTokens, options = {}) {
1572
+ return calculateCost(model, inputTokens, outputTokens, options);
1573
+ }
1574
+ function extractTokenUsageFromResponse6(response) {
1575
+ return extractGoogleTokenUsage(response);
1576
+ }
1577
+
1578
+ // src/pricing/mistral-pricing.ts
1579
+ function calculateMistralCost(model, inputTokens, outputTokens, cacheHitTokens = 0) {
1580
+ return calculateCost(model, inputTokens, outputTokens, { cacheHitTokens });
1581
+ }
1582
+ function extractTokenUsageFromResponse7(response) {
1583
+ return extractMistralTokenUsage(response);
1584
+ }
1585
+
1586
+ // src/agents/abstract-agent.ts
1587
+ var AbstractAgent = class {
1588
+ name;
1589
+ gameId;
1590
+ userId;
1591
+ /**
1592
+ * Output ceiling sent with every request from this agent. Resolved once from the model's
1593
+ * catalog override, else DEFAULT_MAX_OUTPUT_TOKENS. Callers needing more room raise it
1594
+ * after construction (see story generation), the same way gameId/userId are assigned —
1595
+ * so subclasses must read it when building a request, never snapshot it at construction.
1596
+ */
1597
+ maxOutputTokens;
1598
+ /**
1599
+ * Reasoning-depth knobs, resolved once from the catalog like maxOutputTokens and, like it,
1600
+ * overridable per instance for calls whose profile differs from a turn (story generation
1601
+ * runs deeper). Each provider speaks one dialect — effort (DeepSeek, GLM, Gemini, Claude
1602
+ * adaptive) or a token budget (Qwen, Claude Haiku) — and reads only the field it
1603
+ * understands; the other is ignored. Subclasses read these when building a request.
1604
+ */
1605
+ reasoningEffort;
1606
+ thinkingBudgetTokens;
1607
+ instruction;
1608
+ /**
1609
+ * The instruction split on CACHE_TIER_MARKER: [shared static tier, per-bot tier].
1610
+ * Length 1 when the prompt has no marker (GM prompts, tests). Providers with
1611
+ * explicit cache breakpoints (Anthropic) place one per part; everyone else uses
1612
+ * the joined marker-free `instruction`, whose shared prefix implicit caches match.
1613
+ */
1614
+ instructionParts;
1615
+ temperature;
1616
+ model;
1617
+ enableThinking;
1618
+ agentLoggingConfig;
1619
+ constructor(name, instruction, model, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
1620
+ this.name = name;
1621
+ this.instructionParts = instruction.split(CACHE_TIER_MARKER).filter((part) => part.trim().length > 0);
1622
+ this.instruction = this.instructionParts.join("\n\n");
1623
+ this.temperature = temperature;
1624
+ this.model = model;
1625
+ this.enableThinking = enableThinking;
1626
+ this.agentLoggingConfig = agentLoggingConfig;
1627
+ const modelConfig = getModelConfigByApiName(model);
1628
+ this.maxOutputTokens = modelConfig?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
1629
+ this.reasoningEffort = modelConfig?.reasoningEffort;
1630
+ this.thinkingBudgetTokens = modelConfig?.thinkingBudgetTokens;
1631
+ }
1632
+ /**
1633
+ * Public ask API — template methods that time the provider call and stamp `durationMs`
1634
+ * into the returned TokenUsage. Subclasses implement doAskWithZodSchema/doAskText and
1635
+ * must NOT override these.
1636
+ */
1637
+ async askWithZodSchema(zodSchema, messages) {
1638
+ const startedAt = Date.now();
1639
+ try {
1640
+ const [result, thinking, usage, signature] = await this.doAskWithZodSchema(zodSchema, messages);
1641
+ return [result, thinking, this.stampDuration(usage, startedAt), signature];
1642
+ } catch (error) {
1643
+ this.stampErrorDuration(error, startedAt);
1644
+ throw error;
1645
+ }
1646
+ }
1647
+ async askText(messages) {
1648
+ const startedAt = Date.now();
1649
+ try {
1650
+ const [content, thinking, usage, signature] = await this.doAskText(messages);
1651
+ return [content, thinking, this.stampDuration(usage, startedAt), signature];
1652
+ } catch (error) {
1653
+ this.stampErrorDuration(error, startedAt);
1654
+ throw error;
1655
+ }
1656
+ }
1657
+ stampDuration(usage, startedAt) {
1658
+ return usage ? { ...usage, durationMs: Date.now() - startedAt } : usage;
1659
+ }
1660
+ /** Failed calls carry their duration too — a 35s provider stall that errors is still signal. */
1661
+ stampErrorDuration(error, startedAt) {
1662
+ if (error && typeof error === "object") {
1663
+ error.durationMs = Date.now() - startedAt;
1664
+ }
1665
+ }
1666
+ logger(message) {
1667
+ console.log(`[${this.name} ${this.model}]: ${message}`);
1668
+ }
1669
+ logAsking(messages) {
1670
+ this.logger("==================================================");
1671
+ this.logger(`Asking ${this.name} ${this.model} agent`);
1672
+ this.logger("==================================================");
1673
+ logger.agentActivity(this.name, this.model, "REQUEST", {
1674
+ gameId: this.gameId,
1675
+ userId: this.userId,
1676
+ systemPrompt: this.instruction,
1677
+ history: messages,
1678
+ command: messages.length > 0 ? messages[messages.length - 1].content : void 0
1679
+ }, this.agentLoggingConfig);
1680
+ }
1681
+ logSystemPrompt() {
1682
+ }
1683
+ logMessages(messages) {
1684
+ this.logger(`History for ${this.name}:`);
1685
+ messages.forEach((msg, index) => {
1686
+ const preview = msg.content.length > 1e3 ? msg.content.substring(0, 1e3) + "..." : msg.content;
1687
+ this.logger(` ${index + 1}. [${msg.role}]: ${preview}`);
1688
+ });
1689
+ }
1690
+ logReply(reply, thinking, usage) {
1691
+ const replyStr = typeof reply === "string" ? reply : JSON.stringify(reply);
1692
+ this.logger(`Reply from ${this.name}:`);
1693
+ if (thinking) {
1694
+ const thinkingPreview = thinking.length > 500 ? thinking.substring(0, 500) + "..." : thinking;
1695
+ this.logger(` [thinking]: ${thinkingPreview}`);
1696
+ }
1697
+ const preview = replyStr.length > 1e3 ? replyStr.substring(0, 1e3) + "..." : replyStr;
1698
+ this.logger(` [assistant]: ${preview}`);
1699
+ logger.agentActivity(this.name, this.model, "RESPONSE", {
1700
+ gameId: this.gameId,
1701
+ userId: this.userId,
1702
+ reply,
1703
+ thinking,
1704
+ usage
1705
+ }, this.agentLoggingConfig);
1706
+ }
1707
+ /**
1708
+ * Merges consecutive user messages (e.g. a GM command followed by the detached
1709
+ * reminder postfix) into one, for providers that expect alternating roles — this
1710
+ * reproduces the pre-detachment request shape. ClaudeAgent overrides this to keep
1711
+ * them separate: Anthropic combines consecutive user turns into one turn but keeps
1712
+ * distinct content blocks, which lets its fast cache breakpoint sit on the persisted
1713
+ * command block while the throwaway reminder rides behind it.
1714
+ */
1715
+ prepareMessages(messages) {
1716
+ const result = [];
1717
+ for (const msg of messages) {
1718
+ const prev = result[result.length - 1];
1719
+ if (prev && prev.role === "user" && msg.role === "user") {
1720
+ result[result.length - 1] = { ...prev, content: `${prev.content}
1721
+
1722
+ ${msg.content}` };
1723
+ } else {
1724
+ result.push(msg);
1725
+ }
1726
+ }
1727
+ return result;
1728
+ }
1729
+ };
1730
+
1731
+ // src/agents/gpt-5-agent.ts
1732
+ import OpenAI from "openai";
1733
+ import { z as z2 } from "zod";
1734
+ import { zodTextFormat } from "openai/helpers/zod";
1735
+ var Gpt5Agent = class extends AbstractAgent {
1736
+ client;
1737
+ // Log message templates
1738
+ logTemplates = {
1739
+ error: (name, error) => `Error in ${name} agent: ${error}`
1740
+ };
1741
+ // Error message templates
1742
+ errorMessages = {
1743
+ emptyResponse: "Empty or undefined response from OpenAI API",
1744
+ invalidFormat: "Invalid response format from OpenAI API",
1745
+ apiError: (error) => `Failed to get response from OpenAI API: ${error instanceof Error ? error.message : String(error)}`
1746
+ };
1747
+ constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
1748
+ super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
1749
+ this.client = new OpenAI({
1750
+ apiKey
1751
+ });
1752
+ }
1753
+ /**
1754
+ * Structured output method using Zod with OpenAI's Responses API
1755
+ * This provides better schema handling and runtime validation
1756
+ *
1757
+ * Uses responses.parse for models that support structured outputs
1758
+ */
1759
+ async doAskWithZodSchema(zodSchema, messages) {
1760
+ try {
1761
+ this.logAsking(messages);
1762
+ this.logMessages(messages);
1763
+ const input = [
1764
+ `System: ${this.instruction}`,
1765
+ ...this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
1766
+ ].join("\n\n");
1767
+ let schemaToSend = zodSchema;
1768
+ if (this.enableThinking && zodSchema instanceof z2.ZodObject) {
1769
+ schemaToSend = zodSchema.extend({
1770
+ thinking: z2.string().describe("Your internal chain-of-thought reasoning process used to arrive at the final answer.")
1771
+ });
1772
+ }
1773
+ const response = await this.client.responses.parse({
1774
+ model: this.model,
1775
+ instructions: this.instruction,
1776
+ input,
1777
+ max_output_tokens: this.maxOutputTokens,
1778
+ text: {
1779
+ format: zodTextFormat(schemaToSend, "response_schema")
1780
+ }
1781
+ });
1782
+ if (!response.output_parsed) {
1783
+ this.logger(`Parsing failed. Raw content: ${response.output_text}`);
1784
+ throw new Error(this.errorMessages.invalidFormat);
1785
+ }
1786
+ let reasoningContent = "";
1787
+ if (this.enableThinking && response.output_parsed.thinking) {
1788
+ reasoningContent = response.output_parsed.thinking;
1789
+ }
1790
+ let tokenUsage;
1791
+ if (response.usage) {
1792
+ const cachedTokens = response.usage.input_tokens_details?.cached_tokens ?? 0;
1793
+ const cost = calculateOpenAICost(
1794
+ this.model,
1795
+ response.usage.input_tokens,
1796
+ response.usage.output_tokens,
1797
+ cachedTokens
1798
+ );
1799
+ if (cachedTokens > 0) {
1800
+ this.logger(`\u{1F4BE} Prompt cache: ${cachedTokens} of ${response.usage.input_tokens} input tokens served from cache`);
1801
+ }
1802
+ tokenUsage = {
1803
+ inputTokens: response.usage.input_tokens,
1804
+ outputTokens: response.usage.output_tokens,
1805
+ totalTokens: response.usage.total_tokens || 0,
1806
+ costUSD: cost,
1807
+ ...response.usage.output_tokens_details?.reasoning_tokens ? { reasoningTokens: response.usage.output_tokens_details.reasoning_tokens } : {},
1808
+ ...response.usage.input_tokens_details?.cached_tokens ? { cachedInputTokens: response.usage.input_tokens_details.cached_tokens } : {}
1809
+ };
1810
+ if (response.usage.output_tokens_details?.reasoning_tokens) {
1811
+ const reasoningTokens = response.usage.output_tokens_details.reasoning_tokens;
1812
+ const finalAnswerTokens = tokenUsage.outputTokens - reasoningTokens;
1813
+ this.logger(`Output breakdown: ${reasoningTokens} reasoning tokens, ${finalAnswerTokens} final answer tokens`);
1814
+ }
1815
+ }
1816
+ if (response.output_parsed) {
1817
+ this.logReply(response.output_parsed, reasoningContent, tokenUsage);
1818
+ }
1819
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
1820
+ return [response.output_parsed, reasoningContent, tokenUsage];
1821
+ } catch (error) {
1822
+ this.logger(this.logTemplates.error(this.name, error));
1823
+ throw new Error(this.errorMessages.apiError(error));
1824
+ }
1825
+ }
1826
+ /**
1827
+ * Plain-text ask via the Responses API: no structured-output format, raw output_text.
1828
+ * Note: askWithZodSchema surfaces "thinking" via a schema-injected field; that trick
1829
+ * doesn't apply to plain text, so thinking content is empty here (OpenAI does not
1830
+ * expose chain-of-thought directly).
1831
+ */
1832
+ async doAskText(messages) {
1833
+ try {
1834
+ this.logAsking(messages);
1835
+ this.logMessages(messages);
1836
+ const input = [
1837
+ `System: ${this.instruction}`,
1838
+ ...this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
1839
+ ].join("\n\n");
1840
+ const response = await this.client.responses.create({
1841
+ model: this.model,
1842
+ instructions: this.instruction,
1843
+ input,
1844
+ max_output_tokens: this.maxOutputTokens
1845
+ });
1846
+ const content = response.output_text;
1847
+ if (!content) {
1848
+ throw new Error(this.errorMessages.emptyResponse);
1849
+ }
1850
+ let tokenUsage;
1851
+ if (response.usage) {
1852
+ const cachedTokens = response.usage.input_tokens_details?.cached_tokens ?? 0;
1853
+ const cost = calculateOpenAICost(
1854
+ this.model,
1855
+ response.usage.input_tokens,
1856
+ response.usage.output_tokens,
1857
+ cachedTokens
1858
+ );
1859
+ if (cachedTokens > 0) {
1860
+ this.logger(`\u{1F4BE} Prompt cache: ${cachedTokens} of ${response.usage.input_tokens} input tokens served from cache`);
1861
+ }
1862
+ tokenUsage = {
1863
+ inputTokens: response.usage.input_tokens,
1864
+ outputTokens: response.usage.output_tokens,
1865
+ totalTokens: response.usage.total_tokens || 0,
1866
+ costUSD: cost,
1867
+ ...response.usage.output_tokens_details?.reasoning_tokens ? { reasoningTokens: response.usage.output_tokens_details.reasoning_tokens } : {},
1868
+ ...response.usage.input_tokens_details?.cached_tokens ? { cachedInputTokens: response.usage.input_tokens_details.cached_tokens } : {}
1869
+ };
1870
+ if (response.usage.output_tokens_details?.reasoning_tokens) {
1871
+ const reasoningTokens = response.usage.output_tokens_details.reasoning_tokens;
1872
+ const finalAnswerTokens = tokenUsage.outputTokens - reasoningTokens;
1873
+ this.logger(`Output breakdown: ${reasoningTokens} reasoning tokens, ${finalAnswerTokens} final answer tokens`);
1874
+ }
1875
+ }
1876
+ this.logReply(content, "", tokenUsage);
1877
+ return [content, "", tokenUsage];
1878
+ } catch (error) {
1879
+ this.logger(this.logTemplates.error(this.name, error));
1880
+ throw new Error(this.errorMessages.apiError(error));
1881
+ }
1882
+ }
1883
+ };
1884
+
1885
+ // src/agents/anthropic-agent.ts
1886
+ import { Anthropic } from "@anthropic-ai/sdk";
1887
+ var ClaudeAgent = class extends AbstractAgent {
1888
+ client;
1889
+ // System-prompt breakpoints, one per cache tier (see CACHE_TIER_MARKER):
1890
+ // block 1 — shared static rules, byte-identical across all bots and games with the
1891
+ // same rule set, so one org-level entry serves everyone and ANY bot's call
1892
+ // refreshes its TTL;
1893
+ // block 2 — per-bot identity + game state + summaries, byte-stable from the start of
1894
+ // a game day through the end of its night (deaths/role knowledge/summaries
1895
+ // only change in startNewDay), so every call within a day reads it.
1896
+ // GM prompts have no marker → single block, same behavior as before. Haiku 4.5 needs a
1897
+ // 4096-token cacheable prefix, so tiers below that silently no-op on Haiku — expected.
1898
+ // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
1899
+ // initializer would snapshot the default and silently ignore the override.
1900
+ get defaultParams() {
1901
+ return {
1902
+ max_tokens: this.maxOutputTokens,
1903
+ system: this.instructionParts.map((part) => ({ type: "text", text: part, cache_control: { type: "ephemeral" } })),
1904
+ model: this.model
1905
+ };
1906
+ }
1907
+ // Log message templates
1908
+ logTemplates = {
1909
+ error: (name, error) => `Error in ${name} agent: ${error}`
1910
+ };
1911
+ // Error message templates
1912
+ errorMessages = {
1913
+ emptyResponse: "Empty response from Anthropic API",
1914
+ invalidFormat: "Invalid response format from Anthropic API",
1915
+ apiError: (error) => `Failed to get response from Anthropic API: ${error instanceof Error ? error.message : String(error)}`,
1916
+ unsupportedRole: (role) => `Unsupported role type: ${role}`
1917
+ };
1918
+ constructor(name, instruction, model, apiKey, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
1919
+ super(name, instruction, model, 0.2, enableThinking, agentLoggingConfig);
1920
+ this.client = new Anthropic({
1921
+ apiKey
1922
+ });
1923
+ }
1924
+ /**
1925
+ * Unlike the base class, does NOT merge consecutive user messages: the Messages API
1926
+ * combines consecutive user turns into a single turn while preserving separate content
1927
+ * blocks, so the trailing reminder stays out of the persisted command block and the
1928
+ * fast cache breakpoint (see applyCacheBreakpoint) lands on bytes that repeat.
1929
+ */
1930
+ prepareMessages(messages) {
1931
+ return messages;
1932
+ }
1933
+ convertToAnthropicMessages(messages) {
1934
+ return messages.map((msg) => ({
1935
+ role: this.convertRole(msg.role),
1936
+ content: msg.content
1937
+ }));
1938
+ }
1939
+ /**
1940
+ * Converts messages for thinking-enabled requests.
1941
+ * Assistant messages include thinking blocks ONLY if they have valid signatures.
1942
+ * If a signature is missing, the thinking block is dropped to ensure API validity.
1943
+ */
1944
+ convertToAnthropicMessagesWithThinking(messages) {
1945
+ let assistantMsgCount = 0;
1946
+ let withThinking = 0;
1947
+ let withValidAnthropicSig = 0;
1948
+ let droppedGoogleSig = 0;
1949
+ let droppedNoSig = 0;
1950
+ const result = messages.map((msg) => {
1951
+ const role = this.convertRole(msg.role);
1952
+ if (role === "assistant") {
1953
+ assistantMsgCount++;
1954
+ if (msg.thinking && msg.anthropicThinkingSignature) {
1955
+ withThinking++;
1956
+ withValidAnthropicSig++;
1957
+ const thinkingBlock = {
1958
+ type: "thinking",
1959
+ thinking: msg.thinking,
1960
+ signature: msg.anthropicThinkingSignature
1961
+ };
1962
+ const contentBlocks = [
1963
+ thinkingBlock,
1964
+ { type: "text", text: msg.content }
1965
+ ];
1966
+ return { role, content: contentBlocks };
1967
+ }
1968
+ if (msg.thinking) {
1969
+ withThinking++;
1970
+ if (msg.googleThoughtSignature) {
1971
+ droppedGoogleSig++;
1972
+ } else {
1973
+ droppedNoSig++;
1974
+ }
1975
+ }
1976
+ return { role, content: msg.content };
1977
+ }
1978
+ return { role, content: msg.content };
1979
+ });
1980
+ if (withThinking > 0) {
1981
+ const dropped = droppedGoogleSig + droppedNoSig;
1982
+ let dropReason = "";
1983
+ if (droppedGoogleSig > 0) dropReason += `${droppedGoogleSig} with Google signature`;
1984
+ if (droppedNoSig > 0) dropReason += `${droppedNoSig > 0 && droppedGoogleSig > 0 ? ", " : ""}${droppedNoSig} without signature`;
1985
+ this.logger(`\u{1F4CA} Thinking history: ${assistantMsgCount} assistant msgs, ${withThinking} with thinking, ${withValidAnthropicSig} included, ${dropped} dropped${dropped > 0 ? ` (${dropReason})` : ""}`);
1986
+ }
1987
+ return result;
1988
+ }
1989
+ /**
1990
+ * Breakpoint 2 (fast tier): the last message that will be re-sent byte-identically on
1991
+ * the next request. That is the SECOND-to-last message, not the last one — the final
1992
+ * user message carries unpersisted content (the reminder postfix / schema description)
1993
+ * appended to the GM command, so its bytes never repeat and a breakpoint there would be
1994
+ * a pure 1.25x write tax with no reads. The second-to-last message (the bot's previous
1995
+ * reply, or an earlier flushed block) reappears verbatim next turn, where the moved-
1996
+ * forward breakpoint finds it via the 20-block lookback.
1997
+ *
1998
+ * NOT the top-level auto-caching mode: that mode targets the LAST cacheable block,
1999
+ * which for us is exactly the never-repeated tail — every entry it wrote would be dead.
2000
+ */
2001
+ applyCacheBreakpoint(messages) {
2002
+ if (messages.length < 2) {
2003
+ return;
2004
+ }
2005
+ const anchor = messages[messages.length - 2];
2006
+ if (typeof anchor.content === "string") {
2007
+ if (anchor.content.length > 0) {
2008
+ anchor.content = [{ type: "text", text: anchor.content, cache_control: { type: "ephemeral" } }];
2009
+ }
2010
+ return;
2011
+ }
2012
+ for (let i = anchor.content.length - 1; i >= 0; i--) {
2013
+ const block = anchor.content[i];
2014
+ if (block.type === "text" && block.text.length > 0) {
2015
+ block.cache_control = { type: "ephemeral" };
2016
+ return;
2017
+ }
2018
+ }
2019
+ }
2020
+ /**
2021
+ * Builds TokenUsage from the response. Anthropic's input_tokens EXCLUDES cached tokens
2022
+ * (total prompt = input_tokens + cache_read + cache_creation), unlike the OpenAI-shaped
2023
+ * providers whose prompt_tokens include them — so reconstruct the full prompt size here
2024
+ * before pricing. Cache reads bill at the cacheHitPrice (~0.1x); cache writes bill at
2025
+ * 1.25x input, which MODEL_PRICING doesn't model, so written tokens are priced at the
2026
+ * plain input rate (~20% undercount on the written span only).
2027
+ */
2028
+ buildTokenUsage(usage) {
2029
+ const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
2030
+ const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
2031
+ const uncachedInputTokens = usage.input_tokens || 0;
2032
+ const inputTokens = uncachedInputTokens + cacheReadTokens + cacheWriteTokens;
2033
+ const outputTokens = usage.output_tokens || 0;
2034
+ const cost = calculateAnthropicCost(this.model, inputTokens, outputTokens, cacheReadTokens);
2035
+ if (cacheReadTokens > 0 || cacheWriteTokens > 0) {
2036
+ this.logger(`\u{1F4BE} Prompt cache: ${cacheReadTokens} read, ${cacheWriteTokens} written, ${uncachedInputTokens} uncached`);
2037
+ }
2038
+ return {
2039
+ inputTokens,
2040
+ outputTokens,
2041
+ totalTokens: inputTokens + outputTokens,
2042
+ costUSD: cost,
2043
+ // Cache reads only — writes are a billing premium, not reuse of prior context.
2044
+ ...cacheReadTokens > 0 ? { cachedInputTokens: cacheReadTokens } : {}
2045
+ };
2046
+ }
2047
+ convertRole(role) {
2048
+ if (role === "system" || role === "user") {
2049
+ return "user";
2050
+ }
2051
+ if (role === "assistant") {
2052
+ return "assistant";
2053
+ }
2054
+ throw new Error(this.errorMessages.unsupportedRole(role));
2055
+ }
2056
+ /**
2057
+ * New method using Zod with Anthropic's Claude API
2058
+ * Since Anthropic doesn't support native JSON schemas, we generate prompt descriptions
2059
+ */
2060
+ async doAskWithZodSchema(zodSchema, messages) {
2061
+ const aiMessages = this.prepareMessages(messages);
2062
+ this.logAsking(messages);
2063
+ this.logMessages(messages);
2064
+ try {
2065
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
2066
+ const lastMessage = aiMessages[aiMessages.length - 1];
2067
+ const fullPrompt = `${lastMessage.content}
2068
+
2069
+ ${schemaDescription}`;
2070
+ const messagesWithSchema = [...aiMessages];
2071
+ messagesWithSchema[messagesWithSchema.length - 1] = {
2072
+ ...lastMessage,
2073
+ content: fullPrompt
2074
+ };
2075
+ const canUseThinking = this.enableThinking;
2076
+ const anthropicMessages = canUseThinking ? this.convertToAnthropicMessagesWithThinking(messagesWithSchema) : this.convertToAnthropicMessages(messagesWithSchema);
2077
+ this.applyCacheBreakpoint(anthropicMessages);
2078
+ const params = {
2079
+ ...this.defaultParams,
2080
+ messages: anthropicMessages
2081
+ };
2082
+ const usesAdaptiveThinking = this.model.includes("fable") || this.model.includes("opus") || this.model.includes("sonnet");
2083
+ if (canUseThinking) {
2084
+ if (usesAdaptiveThinking) {
2085
+ params.thinking = { type: "adaptive", display: "summarized" };
2086
+ params.output_config = { effort: toAnthropicEffort(this.reasoningEffort ?? "high") };
2087
+ } else {
2088
+ params.thinking = { type: "enabled", budget_tokens: this.thinkingBudgetTokens ?? 1024 };
2089
+ params.temperature = 1;
2090
+ }
2091
+ } else if (usesAdaptiveThinking) {
2092
+ params.thinking = { type: "disabled" };
2093
+ } else {
2094
+ params.temperature = this.temperature;
2095
+ }
2096
+ let response;
2097
+ try {
2098
+ response = await this.client.messages.create(params);
2099
+ } catch (apiError) {
2100
+ this.logger(this.logTemplates.error(this.name, apiError));
2101
+ throw new Error(this.errorMessages.apiError(apiError));
2102
+ }
2103
+ if (response.stop_reason === "refusal") {
2104
+ throw new ModelRefusalError(this.model);
2105
+ }
2106
+ if (!("content" in response) || !Array.isArray(response.content) || response.content.length === 0) {
2107
+ throw new Error(this.errorMessages.emptyResponse);
2108
+ }
2109
+ let textContent = null;
2110
+ let thinkingContent = "";
2111
+ let anthropicThinkingSignature = "";
2112
+ for (const block of response.content) {
2113
+ if (this.enableThinking && block.type === "thinking" && "thinking" in block) {
2114
+ thinkingContent = block.thinking;
2115
+ if ("signature" in block) {
2116
+ anthropicThinkingSignature = block.signature;
2117
+ }
2118
+ }
2119
+ if ("text" in block && !textContent) {
2120
+ textContent = block.text;
2121
+ }
2122
+ }
2123
+ if (!textContent) {
2124
+ throw new Error(this.errorMessages.invalidFormat);
2125
+ }
2126
+ const parsedData = parseAndValidateLlmJson(textContent, zodSchema, (m) => this.logger(m));
2127
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
2128
+ let tokenUsage;
2129
+ if (response.usage) {
2130
+ tokenUsage = this.buildTokenUsage(response.usage);
2131
+ if (this.enableThinking && thinkingContent) {
2132
+ this.logger(`Thinking enabled: ${thinkingContent.length} characters of thinking content`);
2133
+ this.logger(`Note: Thinking tokens are included in output token count and cost`);
2134
+ }
2135
+ }
2136
+ if (parsedData) {
2137
+ this.logReply(parsedData, thinkingContent || void 0, tokenUsage);
2138
+ }
2139
+ return [parsedData, thinkingContent, tokenUsage, anthropicThinkingSignature || void 0];
2140
+ } catch (error) {
2141
+ if (error instanceof ModelError) {
2142
+ throw error;
2143
+ }
2144
+ const errorDetails = error instanceof Error ? error.message : String(error);
2145
+ const isRecoverable = errorDetails.includes("overloaded_error") || errorDetails.includes("529") || errorDetails.includes("rate_limit");
2146
+ throw new BotResponseError(
2147
+ "Failed to get response from Anthropic API with Zod schema",
2148
+ errorDetails,
2149
+ {
2150
+ model: this.model,
2151
+ agentName: this.name,
2152
+ apiProvider: "Anthropic",
2153
+ schemaType: "zod"
2154
+ },
2155
+ isRecoverable
2156
+ );
2157
+ }
2158
+ }
2159
+ /**
2160
+ * Plain-text ask: same request as askWithZodSchema but without a schema description
2161
+ * appended to the prompt and without JSON parsing. Thinking blocks and signatures
2162
+ * are extracted identically.
2163
+ */
2164
+ async doAskText(messages) {
2165
+ const aiMessages = this.prepareMessages(messages);
2166
+ this.logAsking(messages);
2167
+ this.logMessages(messages);
2168
+ try {
2169
+ const canUseThinking = this.enableThinking;
2170
+ const anthropicMessages = canUseThinking ? this.convertToAnthropicMessagesWithThinking(aiMessages) : this.convertToAnthropicMessages(aiMessages);
2171
+ this.applyCacheBreakpoint(anthropicMessages);
2172
+ const params = {
2173
+ ...this.defaultParams,
2174
+ messages: anthropicMessages
2175
+ };
2176
+ const usesAdaptiveThinking = this.model.includes("fable") || this.model.includes("opus") || this.model.includes("sonnet");
2177
+ if (canUseThinking) {
2178
+ if (usesAdaptiveThinking) {
2179
+ params.thinking = { type: "adaptive", display: "summarized" };
2180
+ params.output_config = { effort: toAnthropicEffort(this.reasoningEffort ?? "high") };
2181
+ } else {
2182
+ params.thinking = { type: "enabled", budget_tokens: this.thinkingBudgetTokens ?? 1024 };
2183
+ params.temperature = 1;
2184
+ }
2185
+ } else if (usesAdaptiveThinking) {
2186
+ params.thinking = { type: "disabled" };
2187
+ } else {
2188
+ params.temperature = this.temperature;
2189
+ }
2190
+ let response;
2191
+ try {
2192
+ response = await this.client.messages.create(params);
2193
+ } catch (apiError) {
2194
+ this.logger(this.logTemplates.error(this.name, apiError));
2195
+ throw new Error(this.errorMessages.apiError(apiError));
2196
+ }
2197
+ if (response.stop_reason === "refusal") {
2198
+ throw new ModelRefusalError(this.model);
2199
+ }
2200
+ if (!("content" in response) || !Array.isArray(response.content) || response.content.length === 0) {
2201
+ throw new Error(this.errorMessages.emptyResponse);
2202
+ }
2203
+ const textParts = [];
2204
+ let thinkingContent = "";
2205
+ let anthropicThinkingSignature = "";
2206
+ for (const block of response.content) {
2207
+ if (this.enableThinking && block.type === "thinking" && "thinking" in block) {
2208
+ thinkingContent = block.thinking;
2209
+ if ("signature" in block) {
2210
+ anthropicThinkingSignature = block.signature;
2211
+ }
2212
+ }
2213
+ if ("text" in block) {
2214
+ textParts.push(block.text);
2215
+ }
2216
+ }
2217
+ const textContent = textParts.join("");
2218
+ if (!textContent) {
2219
+ throw new Error(this.errorMessages.emptyResponse);
2220
+ }
2221
+ let tokenUsage;
2222
+ if (response.usage) {
2223
+ tokenUsage = this.buildTokenUsage(response.usage);
2224
+ if (this.enableThinking && thinkingContent) {
2225
+ this.logger(`Thinking enabled: ${thinkingContent.length} characters of thinking content`);
2226
+ this.logger(`Note: Thinking tokens are included in output token count and cost`);
2227
+ }
2228
+ }
2229
+ this.logReply(textContent, thinkingContent || void 0, tokenUsage);
2230
+ return [textContent, thinkingContent, tokenUsage, anthropicThinkingSignature || void 0];
2231
+ } catch (error) {
2232
+ if (error instanceof ModelError) {
2233
+ throw error;
2234
+ }
2235
+ const errorDetails = error instanceof Error ? error.message : String(error);
2236
+ const isRecoverable = errorDetails.includes("overloaded_error") || errorDetails.includes("529") || errorDetails.includes("rate_limit");
2237
+ throw new BotResponseError(
2238
+ "Failed to get response from Anthropic API",
2239
+ errorDetails,
2240
+ {
2241
+ model: this.model,
2242
+ agentName: this.name,
2243
+ apiProvider: "Anthropic",
2244
+ schemaType: "text"
2245
+ },
2246
+ isRecoverable
2247
+ );
2248
+ }
2249
+ }
2250
+ };
2251
+
2252
+ // src/agents/google-agent.ts
2253
+ import { GoogleGenAI } from "@google/genai";
2254
+ var GoogleAgent = class extends AbstractAgent {
2255
+ client;
2256
+ defaultConfig = {
2257
+ responseMimeType: "application/json"
2258
+ };
2259
+ // Log message templates
2260
+ logTemplates = {
2261
+ error: (name, error) => `Error in ${name} agent: ${error}`
2262
+ };
2263
+ // Error message templates
2264
+ errorMessages = {
2265
+ emptyResponse: "Empty response from Google API - check logs for detailed response info",
2266
+ invalidFormat: "Invalid response format from Google API",
2267
+ apiError: (error) => `Failed to get response from Google API: ${error instanceof Error ? error.message : String(error)}`,
2268
+ unsupportedRole: (role) => `Unsupported role type: ${role}`
2269
+ };
2270
+ constructor(name, instruction, model, apiKey, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
2271
+ super(name, instruction, model, 0.2, enableThinking, agentLoggingConfig);
2272
+ this.client = new GoogleGenAI({
2273
+ apiKey
2274
+ });
2275
+ }
2276
+ convertToContents(rawMessages) {
2277
+ const messages = this.prepareMessages(rawMessages);
2278
+ try {
2279
+ let assistantMsgCount = 0;
2280
+ let withThinking = 0;
2281
+ let withValidGoogleSig = 0;
2282
+ let droppedAnthropicSig = 0;
2283
+ let droppedNoSig = 0;
2284
+ const contents = messages.map((msg) => {
2285
+ const role = this.convertRole(msg.role);
2286
+ const parts = [];
2287
+ if (role === "model") {
2288
+ assistantMsgCount++;
2289
+ if (msg.thinking && msg.googleThoughtSignature) {
2290
+ withThinking++;
2291
+ withValidGoogleSig++;
2292
+ parts.push({
2293
+ text: msg.thinking,
2294
+ thought: true
2295
+ });
2296
+ const responsePart = { text: msg.content };
2297
+ responsePart.thoughtSignature = msg.googleThoughtSignature;
2298
+ parts.push(responsePart);
2299
+ } else {
2300
+ if (msg.thinking) {
2301
+ withThinking++;
2302
+ if (msg.anthropicThinkingSignature) {
2303
+ droppedAnthropicSig++;
2304
+ } else {
2305
+ droppedNoSig++;
2306
+ }
2307
+ }
2308
+ parts.push({ text: msg.content });
2309
+ }
2310
+ } else {
2311
+ parts.push({ text: msg.content });
2312
+ }
2313
+ return {
2314
+ role,
2315
+ parts
2316
+ };
2317
+ });
2318
+ if (withThinking > 0) {
2319
+ const dropped = droppedAnthropicSig + droppedNoSig;
2320
+ let dropReason = "";
2321
+ if (droppedAnthropicSig > 0) dropReason += `${droppedAnthropicSig} with Anthropic signature`;
2322
+ if (droppedNoSig > 0) dropReason += `${droppedNoSig > 0 && droppedAnthropicSig > 0 ? ", " : ""}${droppedNoSig} without signature`;
2323
+ this.logger(`\u{1F4CA} Thinking history: ${assistantMsgCount} assistant msgs, ${withThinking} with thinking, ${withValidGoogleSig} included, ${dropped} dropped${dropped > 0 ? ` (${dropReason})` : ""}`);
2324
+ }
2325
+ return contents;
2326
+ } catch (error) {
2327
+ throw error;
2328
+ }
2329
+ }
2330
+ convertRole(role) {
2331
+ if (role === "assistant") {
2332
+ return "model";
2333
+ }
2334
+ if (role === "user" || role === "system") {
2335
+ return "user";
2336
+ }
2337
+ throw new Error(this.errorMessages.unsupportedRole(role));
2338
+ }
2339
+ calculateCost(inputTokens, outputTokens, totalTokens) {
2340
+ const contextTokens = this.deriveContextTokens(inputTokens, outputTokens, totalTokens);
2341
+ return calculateGoogleCost(this.model, inputTokens, outputTokens, {
2342
+ contextTokens,
2343
+ totalTokens
2344
+ });
2345
+ }
2346
+ calculateCostWithCacheHits(inputTokens, outputTokens, totalTokens, cacheHitTokens) {
2347
+ const contextTokens = this.deriveContextTokens(inputTokens, outputTokens, totalTokens);
2348
+ return calculateGoogleCost(this.model, inputTokens, outputTokens, {
2349
+ contextTokens,
2350
+ totalTokens,
2351
+ cacheHitTokens
2352
+ });
2353
+ }
2354
+ deriveContextTokens(inputTokens, outputTokens, totalTokens) {
2355
+ if (!totalTokens) {
2356
+ return inputTokens;
2357
+ }
2358
+ const promptAndReasoningTokens = Math.max(totalTokens - outputTokens, 0);
2359
+ return Math.max(inputTokens, promptAndReasoningTokens);
2360
+ }
2361
+ /**
2362
+ * New method using Zod with Google's Gemini API
2363
+ * This provides better schema handling and runtime validation
2364
+ */
2365
+ async doAskWithZodSchema(zodSchema, messages) {
2366
+ const contents = this.convertToContents(messages);
2367
+ try {
2368
+ const googleSchema = ZodSchemaConverter.toGoogleSchema(zodSchema);
2369
+ const config = {
2370
+ temperature: this.temperature,
2371
+ responseMimeType: "application/json",
2372
+ responseSchema: googleSchema,
2373
+ maxOutputTokens: this.maxOutputTokens,
2374
+ systemInstruction: this.instruction
2375
+ };
2376
+ if (this.enableThinking) {
2377
+ config.thinkingConfig = {
2378
+ includeThoughts: true,
2379
+ thinkingLevel: toGeminiEffort(this.reasoningEffort ?? "low").toUpperCase()
2380
+ };
2381
+ }
2382
+ this.logAsking(messages);
2383
+ this.logMessages(messages);
2384
+ let response;
2385
+ try {
2386
+ response = await this.client.models.generateContent({
2387
+ model: this.model,
2388
+ contents,
2389
+ config
2390
+ });
2391
+ } catch (apiError) {
2392
+ this.logger(this.logTemplates.error(this.name, apiError));
2393
+ throw new Error(this.errorMessages.apiError(apiError));
2394
+ }
2395
+ let thinkingContent = "";
2396
+ let googleThoughtSignature = "";
2397
+ if (this.enableThinking && response.candidates?.[0]?.content?.parts) {
2398
+ const parts = response.candidates[0].content.parts;
2399
+ const thinkingParts = [];
2400
+ for (const part of parts) {
2401
+ if (part.thought && part.text) {
2402
+ thinkingParts.push(part.text);
2403
+ }
2404
+ if (part.thoughtSignature) {
2405
+ googleThoughtSignature = part.thoughtSignature;
2406
+ } else if (part.thought_signature) {
2407
+ googleThoughtSignature = part.thought_signature;
2408
+ } else if (part.signature) {
2409
+ googleThoughtSignature = part.signature;
2410
+ }
2411
+ }
2412
+ thinkingContent = thinkingParts.join("\n");
2413
+ if (thinkingContent && !googleThoughtSignature) {
2414
+ this.logger(`\u26A0\uFE0F Thinking content received but no signature found in response`);
2415
+ }
2416
+ }
2417
+ const usageMetadata = response.usageMetadata;
2418
+ let tokenUsage;
2419
+ if (usageMetadata) {
2420
+ const inputTokens = usageMetadata.promptTokenCount || 0;
2421
+ const reasoningTokens = usageMetadata.thoughtsTokenCount || 0;
2422
+ const outputTokens = (usageMetadata.candidatesTokenCount || 0) + reasoningTokens;
2423
+ const totalTokens = usageMetadata.totalTokenCount || 0;
2424
+ const cacheHitTokens = usageMetadata.cachedContentTokenCount || 0;
2425
+ const costUSD = this.calculateCostWithCacheHits(inputTokens, outputTokens, totalTokens, cacheHitTokens);
2426
+ tokenUsage = {
2427
+ inputTokens,
2428
+ outputTokens,
2429
+ totalTokens,
2430
+ costUSD,
2431
+ ...reasoningTokens > 0 ? { reasoningTokens } : {},
2432
+ ...cacheHitTokens > 0 ? { cachedInputTokens: cacheHitTokens } : {}
2433
+ };
2434
+ }
2435
+ this.logger(`Zod schema response received - hasText: ${!!response.text}, textLength: ${response.text ? response.text.length : 0}`);
2436
+ if (!response.text) {
2437
+ throw new Error(this.errorMessages.emptyResponse);
2438
+ }
2439
+ const parsedData = parseAndValidateLlmJson(response.text, zodSchema, (m) => this.logger(m));
2440
+ if (parsedData) {
2441
+ this.logReply(parsedData, thinkingContent || void 0, tokenUsage);
2442
+ }
2443
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
2444
+ return [parsedData, thinkingContent, tokenUsage, googleThoughtSignature || void 0];
2445
+ } catch (error) {
2446
+ this.logger(this.logTemplates.error(this.name, error));
2447
+ this.handleGeminiError(error);
2448
+ throw error;
2449
+ }
2450
+ }
2451
+ /**
2452
+ * Plain-text ask: same request as askWithZodSchema but without responseSchema /
2453
+ * responseMimeType, returning the raw text. Thinking parts and thought signatures
2454
+ * are extracted identically.
2455
+ */
2456
+ async doAskText(messages) {
2457
+ const contents = this.convertToContents(messages);
2458
+ try {
2459
+ const config = {
2460
+ temperature: this.temperature,
2461
+ maxOutputTokens: this.maxOutputTokens,
2462
+ systemInstruction: this.instruction
2463
+ };
2464
+ if (this.enableThinking) {
2465
+ config.thinkingConfig = {
2466
+ includeThoughts: true,
2467
+ thinkingLevel: toGeminiEffort(this.reasoningEffort ?? "low").toUpperCase()
2468
+ };
2469
+ }
2470
+ this.logAsking(messages);
2471
+ this.logMessages(messages);
2472
+ let response;
2473
+ try {
2474
+ response = await this.client.models.generateContent({
2475
+ model: this.model,
2476
+ contents,
2477
+ config
2478
+ });
2479
+ } catch (apiError) {
2480
+ this.logger(this.logTemplates.error(this.name, apiError));
2481
+ throw new Error(this.errorMessages.apiError(apiError));
2482
+ }
2483
+ let thinkingContent = "";
2484
+ let googleThoughtSignature = "";
2485
+ if (this.enableThinking && response.candidates?.[0]?.content?.parts) {
2486
+ const parts = response.candidates[0].content.parts;
2487
+ const thinkingParts = [];
2488
+ for (const part of parts) {
2489
+ if (part.thought && part.text) {
2490
+ thinkingParts.push(part.text);
2491
+ }
2492
+ if (part.thoughtSignature) {
2493
+ googleThoughtSignature = part.thoughtSignature;
2494
+ } else if (part.thought_signature) {
2495
+ googleThoughtSignature = part.thought_signature;
2496
+ } else if (part.signature) {
2497
+ googleThoughtSignature = part.signature;
2498
+ }
2499
+ }
2500
+ thinkingContent = thinkingParts.join("\n");
2501
+ if (thinkingContent && !googleThoughtSignature) {
2502
+ this.logger(`\u26A0\uFE0F Thinking content received but no signature found in response`);
2503
+ }
2504
+ }
2505
+ const usageMetadata = response.usageMetadata;
2506
+ let tokenUsage;
2507
+ if (usageMetadata) {
2508
+ const inputTokens = usageMetadata.promptTokenCount || 0;
2509
+ const reasoningTokens = usageMetadata.thoughtsTokenCount || 0;
2510
+ const outputTokens = (usageMetadata.candidatesTokenCount || 0) + reasoningTokens;
2511
+ const totalTokens = usageMetadata.totalTokenCount || 0;
2512
+ const cacheHitTokens = usageMetadata.cachedContentTokenCount || 0;
2513
+ const costUSD = this.calculateCostWithCacheHits(inputTokens, outputTokens, totalTokens, cacheHitTokens);
2514
+ tokenUsage = {
2515
+ inputTokens,
2516
+ outputTokens,
2517
+ totalTokens,
2518
+ costUSD,
2519
+ ...reasoningTokens > 0 ? { reasoningTokens } : {},
2520
+ ...cacheHitTokens > 0 ? { cachedInputTokens: cacheHitTokens } : {}
2521
+ };
2522
+ }
2523
+ this.logger(`Plain text response received - hasText: ${!!response.text}, textLength: ${response.text ? response.text.length : 0}`);
2524
+ if (!response.text) {
2525
+ throw new Error(this.errorMessages.emptyResponse);
2526
+ }
2527
+ this.logReply(response.text, thinkingContent || void 0, tokenUsage);
2528
+ return [response.text, thinkingContent, tokenUsage, googleThoughtSignature || void 0];
2529
+ } catch (error) {
2530
+ this.logger(this.logTemplates.error(this.name, error));
2531
+ this.handleGeminiError(error);
2532
+ throw error;
2533
+ }
2534
+ }
2535
+ /**
2536
+ * Handles Gemini API errors and throws appropriate specific exceptions
2537
+ * @param error - The error to handle
2538
+ */
2539
+ handleGeminiError(error) {
2540
+ let errorMessage = "";
2541
+ let errorCode;
2542
+ let errorStatus = "";
2543
+ if (error && typeof error === "object") {
2544
+ if ("message" in error) {
2545
+ errorMessage = String(error.message);
2546
+ }
2547
+ try {
2548
+ const parsed = JSON.parse(errorMessage);
2549
+ if (parsed.error) {
2550
+ errorMessage = parsed.error.message || errorMessage;
2551
+ errorCode = parsed.error.code;
2552
+ errorStatus = parsed.error.status;
2553
+ }
2554
+ } catch {
2555
+ }
2556
+ } else if (typeof error === "string") {
2557
+ try {
2558
+ const parsed = JSON.parse(error);
2559
+ if (parsed.error) {
2560
+ errorMessage = parsed.error.message || error;
2561
+ errorCode = parsed.error.code;
2562
+ errorStatus = parsed.error.status;
2563
+ }
2564
+ } catch {
2565
+ errorMessage = error;
2566
+ }
2567
+ }
2568
+ if (errorCode === 503 || errorStatus === "UNAVAILABLE" || errorMessage.includes("model is overloaded") || errorMessage.includes("overloaded")) {
2569
+ throw new ModelOverloadError(
2570
+ errorMessage || "Model is currently overloaded. Please try again later.",
2571
+ "Gemini"
2572
+ );
2573
+ }
2574
+ if (errorCode === 429 || errorMessage.includes("rate limit") || errorMessage.includes("quota")) {
2575
+ throw new ModelRateLimitError(
2576
+ errorMessage || "Rate limit exceeded for Gemini model.",
2577
+ "Gemini"
2578
+ );
2579
+ }
2580
+ if (errorCode === 401 || errorCode === 403 || errorMessage.includes("authentication") || errorMessage.includes("unauthorized")) {
2581
+ throw new ModelAuthenticationError(
2582
+ errorMessage || "Authentication failed for Gemini model.",
2583
+ "Gemini"
2584
+ );
2585
+ }
2586
+ if (errorMessage.includes("quota exceeded") || errorMessage.includes("billing")) {
2587
+ throw new ModelQuotaExceededError(
2588
+ errorMessage || "Quota exceeded for Gemini model.",
2589
+ "Gemini"
2590
+ );
2591
+ }
2592
+ if (errorCode && errorCode >= 500) {
2593
+ throw new ModelUnavailableError(
2594
+ errorMessage || "Gemini model is temporarily unavailable.",
2595
+ "Gemini",
2596
+ "server_error"
2597
+ );
2598
+ }
2599
+ }
2600
+ };
2601
+
2602
+ // src/agents/mistral-agent.ts
2603
+ import { Mistral } from "@mistralai/mistralai";
2604
+ import { HTTPClient } from "@mistralai/mistralai/lib/http";
2605
+ var MistralAgent = class extends AbstractAgent {
2606
+ client;
2607
+ // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
2608
+ // initializer would snapshot the default and silently ignore the override.
2609
+ get defaultParams() {
2610
+ return {
2611
+ model: this.model,
2612
+ maxTokens: this.maxOutputTokens,
2613
+ temperature: this.temperature
2614
+ };
2615
+ }
2616
+ // Log message templates
2617
+ logTemplates = {
2618
+ error: (name, error) => `Error in ${name} agent: ${error}`
2619
+ };
2620
+ // Error message templates
2621
+ errorMessages = {
2622
+ emptyResponse: "Empty or undefined response from Mistral API",
2623
+ invalidFormat: "Invalid response format from Mistral API",
2624
+ apiError: (error) => `Failed to get response from Mistral API: ${error instanceof Error ? error.message : String(error)}`
2625
+ };
2626
+ constructor(name, instruction, model, apiKey, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
2627
+ super(name, instruction, model, 0.7, enableThinking, agentLoggingConfig);
2628
+ const promptCacheKey = stableHashHex(`${name}
2629
+ ${instruction}`);
2630
+ const httpClient = new HTTPClient();
2631
+ httpClient.addHook("beforeRequest", async (request) => {
2632
+ try {
2633
+ if (request.method === "POST" && new URL(request.url).pathname.endsWith("/chat/completions")) {
2634
+ const body = await request.clone().text();
2635
+ const json = JSON.parse(body);
2636
+ json.prompt_cache_key = promptCacheKey;
2637
+ return new Request(request.url, {
2638
+ method: request.method,
2639
+ headers: request.headers,
2640
+ body: JSON.stringify(json)
2641
+ });
2642
+ }
2643
+ } catch {
2644
+ }
2645
+ return request;
2646
+ });
2647
+ this.client = new Mistral({ apiKey, httpClient });
2648
+ }
2649
+ convertToMistralMessages(messages) {
2650
+ return this.prepareMessages(messages).map((msg) => ({
2651
+ role: msg.role === "developer" ? "system" : msg.role,
2652
+ content: msg.content
2653
+ }));
2654
+ }
2655
+ processReply(response) {
2656
+ const message = response?.choices?.[0]?.message;
2657
+ if (!message || !message.content) {
2658
+ throw new Error(this.errorMessages.emptyResponse);
2659
+ }
2660
+ let reply = message.content;
2661
+ if (Array.isArray(reply)) {
2662
+ const { content, thinking } = this.processStructuredReply(reply);
2663
+ if (this.enableThinking && thinking) {
2664
+ this.logger(`Thinking content: ${thinking.length} characters of reasoning`);
2665
+ }
2666
+ return [cleanResponse(content), thinking, this.extractTokenUsage(response)];
2667
+ }
2668
+ return [cleanResponse(reply), "", this.extractTokenUsage(response)];
2669
+ }
2670
+ processStructuredReply(reply) {
2671
+ let content = "";
2672
+ let thinking = "";
2673
+ for (const chunk of reply) {
2674
+ if (typeof chunk === "object" && chunk !== null && "type" in chunk) {
2675
+ if (chunk.type === "thinking" && "thinking" in chunk) {
2676
+ const thinkingArray = chunk.thinking;
2677
+ thinking = thinkingArray.filter((item) => item?.type === "text" && item?.text).map((item) => item.text).join("");
2678
+ } else if (chunk.type === "text" && "text" in chunk) {
2679
+ content = chunk.text;
2680
+ }
2681
+ }
2682
+ }
2683
+ return { content, thinking };
2684
+ }
2685
+ extractTokenUsage(response) {
2686
+ const usage = extractMistralTokenUsage(response);
2687
+ if (!usage) return void 0;
2688
+ this.logger(`MISTRAL_CACHE_CALIBRATION raw usage: ${JSON.stringify(response?.usage)}`);
2689
+ if (usage.reasoningTokens && usage.reasoningTokens > 0) {
2690
+ this.logger(`\u{1F9E0} Reasoning tokens used: ${usage.reasoningTokens}`);
2691
+ }
2692
+ if (usage.cacheHitTokens && usage.cacheHitTokens > 0) {
2693
+ this.logger(`\u{1F4BE} Prompt cache: ${usage.cacheHitTokens} of ${usage.promptTokens} input tokens served from cache`);
2694
+ }
2695
+ const costUSD = calculateCost(this.model, usage.promptTokens, usage.completionTokens, {
2696
+ totalTokens: usage.totalTokens,
2697
+ cacheHitTokens: usage.cacheHitTokens || 0
2698
+ });
2699
+ return {
2700
+ inputTokens: usage.promptTokens,
2701
+ outputTokens: usage.completionTokens,
2702
+ totalTokens: usage.totalTokens,
2703
+ costUSD,
2704
+ // Omitted when absent so we never hand Firestore an undefined value.
2705
+ ...usage.reasoningTokens ? { reasoningTokens: usage.reasoningTokens } : {},
2706
+ ...usage.cacheHitTokens ? { cachedInputTokens: usage.cacheHitTokens } : {}
2707
+ };
2708
+ }
2709
+ /**
2710
+ * New method using Zod with Mistral API
2711
+ * This provides better schema handling and runtime validation
2712
+ *
2713
+ * Uses Mistral Custom Structured Outputs (responseFormat json_schema), which
2714
+ * enforces the response shape server-side and is more reliable than plain JSON
2715
+ * mode. The human-readable schema description is still appended to the last
2716
+ * message because the enforced schema omits field descriptions/semantics.
2717
+ */
2718
+ async doAskWithZodSchema(zodSchema, messages) {
2719
+ try {
2720
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
2721
+ const convertedMessages = this.convertToMistralMessages(messages);
2722
+ if (convertedMessages.length > 0) {
2723
+ const lastMessage = convertedMessages[convertedMessages.length - 1];
2724
+ if (lastMessage && lastMessage.content) {
2725
+ lastMessage.content += `
2726
+
2727
+ Your response must be a valid JSON object matching this schema:
2728
+ ${schemaDescription}`;
2729
+ }
2730
+ } else {
2731
+ convertedMessages.push({
2732
+ role: "user",
2733
+ content: `Please respond with a valid JSON object matching this schema:
2734
+ ${schemaDescription}`
2735
+ });
2736
+ }
2737
+ const systemMessage = {
2738
+ role: MESSAGE_ROLE.SYSTEM,
2739
+ content: this.instruction
2740
+ };
2741
+ const allMessages = [systemMessage, ...convertedMessages];
2742
+ const requestParams = {
2743
+ ...this.defaultParams,
2744
+ messages: allMessages,
2745
+ responseFormat: {
2746
+ type: "json_schema",
2747
+ jsonSchema: {
2748
+ name: "response_schema",
2749
+ schemaDefinition: ZodSchemaConverter.toMistralSchema(zodSchema),
2750
+ strict: true
2751
+ }
2752
+ }
2753
+ };
2754
+ this.logAsking(messages);
2755
+ this.logMessages(messages);
2756
+ let response;
2757
+ try {
2758
+ response = await this.client.chat.complete(requestParams);
2759
+ } catch (apiError) {
2760
+ this.logger(this.logTemplates.error(this.name, apiError));
2761
+ throw new Error(this.errorMessages.apiError(apiError));
2762
+ }
2763
+ if (!response || !response.choices || response.choices.length === 0) {
2764
+ throw new Error(this.errorMessages.emptyResponse);
2765
+ }
2766
+ const choice = response.choices[0];
2767
+ const content = choice.message?.content;
2768
+ if (!content) {
2769
+ throw new Error(this.errorMessages.invalidFormat);
2770
+ }
2771
+ let responseText;
2772
+ let thinkingContent = "";
2773
+ if (Array.isArray(content)) {
2774
+ const { content: extractedContent, thinking } = this.processStructuredReply(content);
2775
+ responseText = extractedContent;
2776
+ thinkingContent = thinking;
2777
+ } else if (typeof content === "string") {
2778
+ responseText = content;
2779
+ } else {
2780
+ responseText = JSON.stringify(content);
2781
+ }
2782
+ const parsedData = parseAndValidateLlmJson(responseText, zodSchema, (m) => this.logger(m));
2783
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
2784
+ const tokenUsage = this.extractTokenUsage(response);
2785
+ if (parsedData) {
2786
+ this.logReply(parsedData, thinkingContent || void 0, tokenUsage);
2787
+ }
2788
+ return [parsedData, thinkingContent, tokenUsage];
2789
+ } catch (error) {
2790
+ this.logger(this.logTemplates.error(this.name, error));
2791
+ throw new Error(this.errorMessages.apiError(error));
2792
+ }
2793
+ }
2794
+ /**
2795
+ * Plain-text ask: no schema appended, no responseFormat. Note that Magistral
2796
+ * reasoning models only return thinking traces when responseFormat is NOT
2797
+ * json_object, so unlike askWithZodSchema this path can surface thinking content.
2798
+ */
2799
+ async doAskText(messages) {
2800
+ try {
2801
+ const convertedMessages = this.convertToMistralMessages(messages);
2802
+ const systemMessage = {
2803
+ role: MESSAGE_ROLE.SYSTEM,
2804
+ content: this.instruction
2805
+ };
2806
+ const requestParams = {
2807
+ ...this.defaultParams,
2808
+ messages: [systemMessage, ...convertedMessages]
2809
+ };
2810
+ this.logAsking(messages);
2811
+ this.logMessages(messages);
2812
+ let response;
2813
+ try {
2814
+ response = await this.client.chat.complete(requestParams);
2815
+ } catch (apiError) {
2816
+ this.logger(this.logTemplates.error(this.name, apiError));
2817
+ throw new Error(this.errorMessages.apiError(apiError));
2818
+ }
2819
+ const [content, thinkingContent, tokenUsage] = this.processReply(response);
2820
+ if (!content) {
2821
+ throw new Error(this.errorMessages.emptyResponse);
2822
+ }
2823
+ this.logReply(content, thinkingContent || void 0, tokenUsage);
2824
+ return [content, thinkingContent, tokenUsage];
2825
+ } catch (error) {
2826
+ this.logger(this.logTemplates.error(this.name, error));
2827
+ throw new Error(this.errorMessages.apiError(error));
2828
+ }
2829
+ }
2830
+ };
2831
+
2832
+ // src/agents/deepseek-v2-agent.ts
2833
+ import OpenAI2 from "openai";
2834
+ var DeepSeekV2Agent = class extends AbstractAgent {
2835
+ client;
2836
+ // Log message templates
2837
+ logTemplates = {
2838
+ error: (name, error) => `Error in ${name} agent: ${error}`,
2839
+ switchingModel: (from, to) => `Switching from ${from} to ${to} for thinking mode`
2840
+ };
2841
+ // Error message templates
2842
+ errorMessages = {
2843
+ emptyResponse: "Empty or undefined response from DeepSeek API",
2844
+ invalidFormat: "Invalid response format from DeepSeek API",
2845
+ apiError: (error) => `Failed to get response from DeepSeek API: ${error instanceof Error ? error.message : String(error)}`
2846
+ };
2847
+ constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
2848
+ super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
2849
+ this.client = new OpenAI2({
2850
+ baseURL: "https://api.deepseek.com",
2851
+ apiKey
2852
+ });
2853
+ }
2854
+ convertToOpenAIMessages(messages) {
2855
+ const preparedMessages = this.prepareMessages(messages);
2856
+ return preparedMessages.map((msg) => ({
2857
+ role: msg.role === "developer" ? "system" : msg.role === "assistant" ? "assistant" : "user",
2858
+ content: msg.content
2859
+ }));
2860
+ }
2861
+ addSystemInstruction(messages) {
2862
+ if (messages.length === 0 || messages[0].role !== "system") {
2863
+ return [
2864
+ { role: "system", content: this.instruction },
2865
+ ...messages
2866
+ ];
2867
+ }
2868
+ const updatedMessages = [...messages];
2869
+ updatedMessages[0] = {
2870
+ ...updatedMessages[0],
2871
+ content: `${this.instruction}
2872
+
2873
+ ${updatedMessages[0].content}`
2874
+ };
2875
+ return updatedMessages;
2876
+ }
2877
+ /**
2878
+ * Thinking params for the request body. DeepSeek V4 toggles thinking with a top-level
2879
+ * `thinking: { type }` (the docs' `extra_body` is a Python-SDK wrapper; openai-node has no
2880
+ * such thing and sends the key literally, where the API ignores it — probed 2026-08-30:
2881
+ * `extra_body: {thinking: {type: 'disabled'}}` still reasoned, top-level `thinking` did
2882
+ * not). Thinking is on by default, so the flag matters only for turning it off.
2883
+ * `reasoning_effort` takes low|high|max (default high, no budget parameter exists); it is
2884
+ * the instance field (catalog default, per-call override) and is only sent when set.
2885
+ */
2886
+ thinkingParams() {
2887
+ if (!this.enableThinking) {
2888
+ return { thinking: { type: "disabled" } };
2889
+ }
2890
+ const effort = this.reasoningEffort;
2891
+ return {
2892
+ thinking: { type: "enabled" },
2893
+ ...effort ? { reasoning_effort: toDeepSeekEffort(effort) } : {}
2894
+ };
2895
+ }
2896
+ /**
2897
+ * New method using Zod with DeepSeek API
2898
+ * This provides better schema handling and runtime validation
2899
+ *
2900
+ * DeepSeek V4 uses thinking toggle via extra_body. JSON mode (response_format
2901
+ * json_object) is supported with or without thinking, so we always request it.
2902
+ * Thinking additionally surfaces reasoning via reasoning_content.
2903
+ */
2904
+ async doAskWithZodSchema(zodSchema, messages) {
2905
+ try {
2906
+ const input = this.convertToOpenAIMessages(messages);
2907
+ this.logAsking(messages);
2908
+ this.logMessages(messages);
2909
+ let modifiedInput = [...input];
2910
+ const requestParams = {
2911
+ model: this.model,
2912
+ messages: this.addSystemInstruction(modifiedInput),
2913
+ max_tokens: this.maxOutputTokens,
2914
+ ...this.enableThinking ? {} : { temperature: this.temperature }
2915
+ };
2916
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
2917
+ const lastMessage = modifiedInput[modifiedInput.length - 1];
2918
+ if (lastMessage && lastMessage.role === "user") {
2919
+ modifiedInput[modifiedInput.length - 1] = {
2920
+ ...lastMessage,
2921
+ content: `${lastMessage.content}
2922
+
2923
+ Your response must be a valid JSON object matching this schema:
2924
+ ${schemaDescription}`
2925
+ };
2926
+ requestParams.messages = this.addSystemInstruction(modifiedInput);
2927
+ }
2928
+ requestParams.response_format = {
2929
+ type: "json_object"
2930
+ };
2931
+ Object.assign(requestParams, this.thinkingParams());
2932
+ let response;
2933
+ try {
2934
+ response = await this.client.chat.completions.create(requestParams);
2935
+ } catch (apiError) {
2936
+ this.logger(this.logTemplates.error(this.name, apiError));
2937
+ throw new Error(this.errorMessages.apiError(apiError));
2938
+ }
2939
+ let thinkingContent = "";
2940
+ if (this.enableThinking && response.choices[0]?.message) {
2941
+ const reasoning = response.choices[0].message.reasoning_content;
2942
+ if (reasoning) {
2943
+ thinkingContent = reasoning;
2944
+ }
2945
+ }
2946
+ const rawContent = response.choices[0]?.message?.content;
2947
+ if (!rawContent) {
2948
+ throw new Error(this.errorMessages.emptyResponse);
2949
+ }
2950
+ const { text: content, thinking: inlineThinking } = stripInlineThinking(rawContent);
2951
+ thinkingContent = mergeThinking(thinkingContent, inlineThinking);
2952
+ if (!content) {
2953
+ throw new Error(this.errorMessages.emptyResponse);
2954
+ }
2955
+ const parsedData = parseAndValidateLlmJson(content, zodSchema, (m) => this.logger(m));
2956
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
2957
+ const usageResult = extractUsageAndCalculateCost(this.model, response);
2958
+ let tokenUsage;
2959
+ if (usageResult) {
2960
+ tokenUsage = {
2961
+ inputTokens: usageResult.usage.promptTokens,
2962
+ outputTokens: usageResult.usage.completionTokens,
2963
+ totalTokens: usageResult.usage.totalTokens,
2964
+ costUSD: usageResult.cost,
2965
+ ...usageResult.usage.cacheHitTokens !== void 0 ? { cachedInputTokens: usageResult.usage.cacheHitTokens } : {},
2966
+ // Omitted when absent so we never hand Firestore an undefined value.
2967
+ ...usageResult.usage.reasoningTokens ? { reasoningTokens: usageResult.usage.reasoningTokens } : {}
2968
+ };
2969
+ }
2970
+ if (parsedData) {
2971
+ this.logReply(parsedData, thinkingContent || void 0, tokenUsage);
2972
+ }
2973
+ return [parsedData, thinkingContent, tokenUsage];
2974
+ } catch (error) {
2975
+ this.logger(this.logTemplates.error(this.name, error));
2976
+ throw new Error(this.errorMessages.apiError(error));
2977
+ }
2978
+ }
2979
+ /**
2980
+ * Plain-text ask: same request structure as askWithZodSchema but without JSON mode
2981
+ * or a schema appended to the prompt. The raw response string is returned as-is.
2982
+ */
2983
+ async doAskText(messages) {
2984
+ try {
2985
+ const input = this.convertToOpenAIMessages(messages);
2986
+ this.logAsking(messages);
2987
+ this.logMessages(messages);
2988
+ const requestParams = {
2989
+ model: this.model,
2990
+ messages: this.addSystemInstruction(input),
2991
+ max_tokens: this.maxOutputTokens,
2992
+ ...this.enableThinking ? {} : { temperature: this.temperature }
2993
+ };
2994
+ Object.assign(requestParams, this.thinkingParams());
2995
+ let response;
2996
+ try {
2997
+ response = await this.client.chat.completions.create(requestParams);
2998
+ } catch (apiError) {
2999
+ this.logger(this.logTemplates.error(this.name, apiError));
3000
+ throw new Error(this.errorMessages.apiError(apiError));
3001
+ }
3002
+ let thinkingContent = "";
3003
+ if (this.enableThinking && response.choices[0]?.message) {
3004
+ const reasoning = response.choices[0].message.reasoning_content;
3005
+ if (reasoning) {
3006
+ thinkingContent = reasoning;
3007
+ }
3008
+ }
3009
+ const rawContent = response.choices[0]?.message?.content;
3010
+ if (!rawContent) {
3011
+ throw new Error(this.errorMessages.emptyResponse);
3012
+ }
3013
+ const { text: content, thinking: inlineThinking } = stripInlineThinking(rawContent);
3014
+ thinkingContent = mergeThinking(thinkingContent, inlineThinking);
3015
+ if (!content) {
3016
+ throw new Error(this.errorMessages.emptyResponse);
3017
+ }
3018
+ const usageResult = extractUsageAndCalculateCost(this.model, response);
3019
+ let tokenUsage;
3020
+ if (usageResult) {
3021
+ tokenUsage = {
3022
+ inputTokens: usageResult.usage.promptTokens,
3023
+ outputTokens: usageResult.usage.completionTokens,
3024
+ totalTokens: usageResult.usage.totalTokens,
3025
+ costUSD: usageResult.cost,
3026
+ ...usageResult.usage.cacheHitTokens !== void 0 ? { cachedInputTokens: usageResult.usage.cacheHitTokens } : {},
3027
+ // Omitted when absent so we never hand Firestore an undefined value.
3028
+ ...usageResult.usage.reasoningTokens ? { reasoningTokens: usageResult.usage.reasoningTokens } : {}
3029
+ };
3030
+ }
3031
+ this.logReply(content, thinkingContent || void 0, tokenUsage);
3032
+ return [content, thinkingContent, tokenUsage];
3033
+ } catch (error) {
3034
+ this.logger(this.logTemplates.error(this.name, error));
3035
+ throw new Error(this.errorMessages.apiError(error));
3036
+ }
3037
+ }
3038
+ };
3039
+
3040
+ // src/agents/grok-agent.ts
3041
+ import { OpenAI as OpenAI3 } from "openai";
3042
+ var GrokAgent = class extends AbstractAgent {
3043
+ client;
3044
+ // Log message templates
3045
+ logTemplates = {
3046
+ error: (name, error) => `Error in ${name} agent: ${error}`
3047
+ };
3048
+ // Error message templates
3049
+ errorMessages = {
3050
+ emptyResponse: "Empty or undefined response from Grok API",
3051
+ invalidFormat: "Invalid response format from Grok API",
3052
+ apiError: (error) => `Failed to get response from Grok API: ${error instanceof Error ? error.message : String(error)}`
3053
+ };
3054
+ constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
3055
+ super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
3056
+ const convId = stableHashHex(`${name}
3057
+ ${instruction}`);
3058
+ this.client = new OpenAI3({
3059
+ apiKey,
3060
+ baseURL: "https://api.x.ai/v1",
3061
+ timeout: 12e5,
3062
+ defaultHeaders: { "x-grok-conv-id": convId }
3063
+ });
3064
+ }
3065
+ /**
3066
+ * Structured output implementation for Grok using json_object mode with prompt
3067
+ * augmentation — more reliable than json_schema on OpenAI-compatible endpoints.
3068
+ */
3069
+ async doAskWithZodSchema(zodSchema, messages) {
3070
+ try {
3071
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
3072
+ const input = this.buildResponsesInput(this.prepareMessages(messages));
3073
+ const lastMessage = input[input.length - 1];
3074
+ if (lastMessage && typeof lastMessage.content === "string") {
3075
+ lastMessage.content += `
3076
+
3077
+ Your response must be a valid JSON object matching this schema:
3078
+ ${schemaDescription}`;
3079
+ }
3080
+ this.logAsking(messages);
3081
+ this.logMessages(messages);
3082
+ const response = await this.createResponse(input, true);
3083
+ const { text, reasoningSummary, encryptedReasoning } = this.extractResponseParts(response);
3084
+ if (!text) {
3085
+ throw new Error(this.errorMessages.emptyResponse);
3086
+ }
3087
+ this.logger(`Grok Agent - Found reasoning summary: ${!!reasoningSummary}, encrypted reasoning: ${!!encryptedReasoning}`);
3088
+ const parsedData = parseAndValidateLlmJson(text, zodSchema, (m) => this.logger(m));
3089
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
3090
+ const tokenUsage = this.extractTokenUsage(response);
3091
+ if (parsedData) {
3092
+ this.logReply(parsedData, reasoningSummary, tokenUsage);
3093
+ }
3094
+ return [parsedData, reasoningSummary, tokenUsage, encryptedReasoning];
3095
+ } catch (error) {
3096
+ this.logger(this.logTemplates.error(this.name, error));
3097
+ throw new Error(this.errorMessages.apiError(error));
3098
+ }
3099
+ }
3100
+ /**
3101
+ * Plain-text ask: no JSON mode and no schema appended to the prompt.
3102
+ * Reasoning extraction and token accounting are identical to askWithZodSchema.
3103
+ */
3104
+ async doAskText(messages) {
3105
+ try {
3106
+ const input = this.buildResponsesInput(this.prepareMessages(messages));
3107
+ this.logAsking(messages);
3108
+ this.logMessages(messages);
3109
+ const response = await this.createResponse(input, false);
3110
+ const { text, reasoningSummary, encryptedReasoning } = this.extractResponseParts(response);
3111
+ if (!text) {
3112
+ throw new Error(this.errorMessages.emptyResponse);
3113
+ }
3114
+ const tokenUsage = this.extractTokenUsage(response);
3115
+ this.logReply(text, reasoningSummary, tokenUsage);
3116
+ return [text, reasoningSummary, tokenUsage, encryptedReasoning];
3117
+ } catch (error) {
3118
+ this.logger(this.logTemplates.error(this.name, error));
3119
+ throw new Error(this.errorMessages.apiError(error));
3120
+ }
3121
+ }
3122
+ createResponse(input, jsonMode) {
3123
+ return this.client.responses.create({
3124
+ model: this.model,
3125
+ temperature: this.temperature,
3126
+ input,
3127
+ // Reasoning bills against the output budget on top of the visible answer, so this
3128
+ // has to cover both. Raise it with a catalog `maxOutputTokens` override if Grok
3129
+ // ever starts truncating — measured turns peak far below the shared default.
3130
+ max_output_tokens: this.maxOutputTokens,
3131
+ // We manage conversation state ourselves; encrypted reasoning is only
3132
+ // returned for unstored responses.
3133
+ store: false,
3134
+ include: ["reasoning.encrypted_content"],
3135
+ ...jsonMode ? { text: { format: { type: "json_object" } } } : {}
3136
+ });
3137
+ }
3138
+ /**
3139
+ * Converts game history to Responses API input items. The system instruction is
3140
+ * merged into the leading system message; assistant messages carrying stored
3141
+ * encrypted reasoning get their reasoning items replayed right before them.
3142
+ */
3143
+ buildResponsesInput(messages) {
3144
+ const input = [];
3145
+ for (const msg of messages) {
3146
+ if (msg.role === "assistant" && msg.grokEncryptedReasoning) {
3147
+ try {
3148
+ const reasoningItems = JSON.parse(msg.grokEncryptedReasoning);
3149
+ if (Array.isArray(reasoningItems)) {
3150
+ input.push(...reasoningItems);
3151
+ }
3152
+ } catch {
3153
+ this.logger(`Failed to parse stored encrypted reasoning, replaying message without it`);
3154
+ }
3155
+ }
3156
+ input.push({ role: msg.role, content: msg.content });
3157
+ }
3158
+ if (input.length > 0 && input[0].role !== "system") {
3159
+ input.unshift({ role: "system", content: this.instruction });
3160
+ } else if (input.length > 0 && input[0].role === "system") {
3161
+ input[0].content = `${this.instruction}
3162
+
3163
+ ${input[0].content}`;
3164
+ }
3165
+ return input;
3166
+ }
3167
+ /**
3168
+ * Walks the response output items: reasoning items yield the human-readable summary
3169
+ * plus the encrypted items (serialized for storage/replay); message items yield text.
3170
+ */
3171
+ extractResponseParts(response) {
3172
+ const textParts = [];
3173
+ const summaryParts = [];
3174
+ const encryptedItems = [];
3175
+ for (const item of response?.output ?? []) {
3176
+ if (!item) {
3177
+ continue;
3178
+ }
3179
+ if (item.type === "reasoning") {
3180
+ for (const summary of item.summary ?? []) {
3181
+ if (typeof summary?.text === "string" && summary.text) {
3182
+ summaryParts.push(summary.text);
3183
+ }
3184
+ }
3185
+ if (item.encrypted_content) {
3186
+ encryptedItems.push(item);
3187
+ }
3188
+ } else if (item.type === "message") {
3189
+ for (const part of item.content ?? []) {
3190
+ if (part?.type === "output_text" && typeof part.text === "string") {
3191
+ textParts.push(part.text);
3192
+ }
3193
+ }
3194
+ }
3195
+ }
3196
+ return {
3197
+ text: textParts.join("\n").trim(),
3198
+ reasoningSummary: summaryParts.join("\n").trim(),
3199
+ encryptedReasoning: encryptedItems.length > 0 ? JSON.stringify(encryptedItems) : void 0
3200
+ };
3201
+ }
3202
+ extractTokenUsage(response) {
3203
+ const usage = response?.usage;
3204
+ if (!usage) {
3205
+ return void 0;
3206
+ }
3207
+ const inputTokens = usage.input_tokens || 0;
3208
+ const outputTokens = usage.output_tokens || 0;
3209
+ const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0;
3210
+ const cachedTokens = usage.input_tokens_details?.cached_tokens || 0;
3211
+ const cost = calculateGrokCost(this.model, inputTokens, outputTokens, cachedTokens);
3212
+ if (reasoningTokens > 0) {
3213
+ this.logger(`Output breakdown: ${reasoningTokens} reasoning tokens, ${outputTokens - reasoningTokens} final answer tokens, ${outputTokens} total output tokens`);
3214
+ }
3215
+ if (cachedTokens > 0) {
3216
+ this.logger(`Input breakdown: ${cachedTokens} cached tokens of ${inputTokens} input tokens`);
3217
+ }
3218
+ return {
3219
+ inputTokens,
3220
+ outputTokens,
3221
+ totalTokens: inputTokens + outputTokens,
3222
+ costUSD: cost,
3223
+ // Omitted when zero so we never hand Firestore an undefined value.
3224
+ ...reasoningTokens > 0 ? { reasoningTokens } : {},
3225
+ ...cachedTokens > 0 ? { cachedInputTokens: cachedTokens } : {}
3226
+ };
3227
+ }
3228
+ };
3229
+
3230
+ // src/agents/kimi-agent.ts
3231
+ import { OpenAI as OpenAI4 } from "openai";
3232
+ var KimiAgent = class extends AbstractAgent {
3233
+ client;
3234
+ // kimi-k3 rejects any temperature other than 1, so we never send the field.
3235
+ // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
3236
+ // initializer would snapshot the default and silently ignore the override.
3237
+ get defaultParams() {
3238
+ return {
3239
+ model: this.model,
3240
+ stream: false,
3241
+ max_tokens: this.maxOutputTokens,
3242
+ // Moonshot's only accepted level; "max" is not in the OpenAI SDK's ReasoningEffort union.
3243
+ reasoning_effort: "max"
3244
+ };
3245
+ }
3246
+ // Log message templates
3247
+ logTemplates = {
3248
+ error: (name, error) => `Error in ${name} agent: ${error}`
3249
+ };
3250
+ // Error message templates
3251
+ errorMessages = {
3252
+ emptyResponse: "Empty or undefined response from Kimi API",
3253
+ invalidFormat: "Invalid response format from Kimi API",
3254
+ apiError: (error) => `Failed to get response from Kimi API: ${error instanceof Error ? error.message : String(error)}`
3255
+ };
3256
+ constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
3257
+ super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
3258
+ this.client = new OpenAI4({
3259
+ apiKey,
3260
+ baseURL: "https://api.moonshot.ai/v1"
3261
+ });
3262
+ }
3263
+ convertToOpenAIMessages(messages) {
3264
+ return messages.map((msg) => ({
3265
+ role: msg.role,
3266
+ content: msg.content
3267
+ }));
3268
+ }
3269
+ extractThinkingAndUsage(completion) {
3270
+ let thinkingContent = "";
3271
+ const message = completion.choices[0]?.message;
3272
+ if (message?.reasoning_content) {
3273
+ thinkingContent = message.reasoning_content;
3274
+ this.logger(`Captured reasoning_content (${thinkingContent.length} characters)`);
3275
+ }
3276
+ let tokenUsage;
3277
+ const usageResult = extractUsageAndCalculateCost(this.model, completion);
3278
+ if (usageResult) {
3279
+ const reasoningTokens = usageResult.usage.reasoningTokens;
3280
+ tokenUsage = {
3281
+ inputTokens: usageResult.usage.promptTokens,
3282
+ outputTokens: usageResult.usage.completionTokens,
3283
+ totalTokens: usageResult.usage.totalTokens,
3284
+ costUSD: usageResult.cost,
3285
+ ...usageResult.usage.cacheHitTokens !== void 0 ? { cachedInputTokens: usageResult.usage.cacheHitTokens } : {},
3286
+ // Only present when reasoning ran; the key is omitted otherwise so we never
3287
+ // hand Firestore an undefined value.
3288
+ ...reasoningTokens ? { reasoningTokens } : {}
3289
+ };
3290
+ if (reasoningTokens) {
3291
+ const finalAnswerTokens = Math.max(0, tokenUsage.outputTokens - reasoningTokens);
3292
+ this.logger(
3293
+ `Output breakdown: ${reasoningTokens} reasoning tokens, ${finalAnswerTokens} final answer tokens`
3294
+ );
3295
+ }
3296
+ }
3297
+ return { thinkingContent, tokenUsage };
3298
+ }
3299
+ /**
3300
+ * New method using Zod with Kimi/Moonshot AI API
3301
+ * This provides better schema handling and runtime validation
3302
+ *
3303
+ * Kimi/Moonshot AI API is OpenAI-compatible, so we try JSON mode first,
3304
+ * and fall back to prompt-based schema if not supported
3305
+ */
3306
+ async doAskWithZodSchema(zodSchema, messages) {
3307
+ try {
3308
+ const preparedMessages = this.prepareMessages(messages);
3309
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
3310
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
3311
+ openAIMessages.unshift({
3312
+ role: "system",
3313
+ content: this.instruction
3314
+ });
3315
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
3316
+ openAIMessages[0].content = `${this.instruction}
3317
+
3318
+ ${openAIMessages[0].content}`;
3319
+ }
3320
+ this.logAsking(messages);
3321
+ this.logMessages(messages);
3322
+ try {
3323
+ const kimiSchema = ZodSchemaConverter.toOpenAIJsonSchema(zodSchema, "response_schema");
3324
+ let completion;
3325
+ try {
3326
+ const params = {
3327
+ ...this.defaultParams,
3328
+ messages: openAIMessages,
3329
+ response_format: {
3330
+ type: "json_schema",
3331
+ json_schema: kimiSchema
3332
+ }
3333
+ };
3334
+ completion = await this.client.chat.completions.create(params);
3335
+ } catch (apiError) {
3336
+ this.logger(this.logTemplates.error(this.name, apiError));
3337
+ throw new Error(this.errorMessages.apiError(apiError));
3338
+ }
3339
+ const rawReply = completion.choices[0]?.message?.content;
3340
+ if (!rawReply) {
3341
+ throw new Error(this.errorMessages.emptyResponse);
3342
+ }
3343
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
3344
+ if (!reply) {
3345
+ throw new Error(this.errorMessages.emptyResponse);
3346
+ }
3347
+ const parsedData = parseAndValidateLlmJson(reply, zodSchema, (m) => this.logger(m));
3348
+ this.logger(`\u2705 Response validated successfully with Zod schema (JSON mode)`);
3349
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
3350
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
3351
+ if (parsedData) {
3352
+ this.logReply(parsedData, thinkingContent, tokenUsage);
3353
+ }
3354
+ return [parsedData, thinkingContent, tokenUsage];
3355
+ } catch (jsonModeError) {
3356
+ this.logger(`JSON mode failed, falling back to prompt-based schema: ${jsonModeError}`);
3357
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
3358
+ const lastMessage = openAIMessages[openAIMessages.length - 1];
3359
+ if (lastMessage) {
3360
+ lastMessage.content += `
3361
+
3362
+ Your response must be a valid JSON object matching this schema:
3363
+ ${schemaDescription}`;
3364
+ }
3365
+ let completion;
3366
+ try {
3367
+ const params = {
3368
+ ...this.defaultParams,
3369
+ messages: openAIMessages
3370
+ };
3371
+ completion = await this.client.chat.completions.create(params);
3372
+ } catch (apiError) {
3373
+ this.logger(this.logTemplates.error(this.name, apiError));
3374
+ throw new Error(this.errorMessages.apiError(apiError));
3375
+ }
3376
+ const rawReply = completion.choices[0]?.message?.content;
3377
+ if (!rawReply) {
3378
+ throw new Error(this.errorMessages.emptyResponse);
3379
+ }
3380
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
3381
+ if (!reply) {
3382
+ throw new Error(this.errorMessages.emptyResponse);
3383
+ }
3384
+ const parsedData = parseAndValidateLlmJson(reply, zodSchema, (m) => this.logger(m));
3385
+ this.logger(`\u2705 Response validated successfully with Zod schema (prompt mode)`);
3386
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
3387
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
3388
+ if (parsedData) {
3389
+ this.logReply(parsedData, thinkingContent, tokenUsage);
3390
+ }
3391
+ return [parsedData, thinkingContent, tokenUsage];
3392
+ }
3393
+ } catch (error) {
3394
+ this.logger(this.logTemplates.error(this.name, error));
3395
+ throw new Error(this.errorMessages.apiError(error));
3396
+ }
3397
+ }
3398
+ /**
3399
+ * Plain-text ask: no JSON mode (and therefore no prompt-based schema fallback).
3400
+ * Thinking toggle and reasoning_content extraction are identical to askWithZodSchema.
3401
+ */
3402
+ async doAskText(messages) {
3403
+ try {
3404
+ const preparedMessages = this.prepareMessages(messages);
3405
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
3406
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
3407
+ openAIMessages.unshift({
3408
+ role: "system",
3409
+ content: this.instruction
3410
+ });
3411
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
3412
+ openAIMessages[0].content = `${this.instruction}
3413
+
3414
+ ${openAIMessages[0].content}`;
3415
+ }
3416
+ this.logAsking(messages);
3417
+ this.logMessages(messages);
3418
+ let completion;
3419
+ try {
3420
+ const params = {
3421
+ ...this.defaultParams,
3422
+ messages: openAIMessages
3423
+ };
3424
+ completion = await this.client.chat.completions.create(params);
3425
+ } catch (apiError) {
3426
+ this.logger(this.logTemplates.error(this.name, apiError));
3427
+ throw new Error(this.errorMessages.apiError(apiError));
3428
+ }
3429
+ const rawReply = completion.choices[0]?.message?.content;
3430
+ if (!rawReply) {
3431
+ throw new Error(this.errorMessages.emptyResponse);
3432
+ }
3433
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
3434
+ if (!reply) {
3435
+ throw new Error(this.errorMessages.emptyResponse);
3436
+ }
3437
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
3438
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
3439
+ this.logReply(reply, thinkingContent, tokenUsage);
3440
+ return [reply, thinkingContent, tokenUsage];
3441
+ } catch (error) {
3442
+ this.logger(this.logTemplates.error(this.name, error));
3443
+ throw new Error(this.errorMessages.apiError(error));
3444
+ }
3445
+ }
3446
+ };
3447
+
3448
+ // src/agents/glm-agent.ts
3449
+ import { OpenAI as OpenAI5 } from "openai";
3450
+ var GlmAgent = class extends AbstractAgent {
3451
+ client;
3452
+ // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
3453
+ // initializer would snapshot the default and silently ignore the override.
3454
+ // `reasoning_effort` is re-declared as string: the OpenAI SDK's union lacks Z.AI's 'max'.
3455
+ get defaultParams() {
3456
+ return {
3457
+ model: this.model,
3458
+ temperature: this.temperature,
3459
+ stream: false,
3460
+ max_tokens: this.maxOutputTokens,
3461
+ thinking: { type: "enabled" },
3462
+ reasoning_effort: toGlmEffort(this.reasoningEffort ?? "high")
3463
+ };
3464
+ }
3465
+ logTemplates = {
3466
+ error: (name, error) => `Error in ${name} agent: ${error}`
3467
+ };
3468
+ errorMessages = {
3469
+ emptyResponse: (finishReason) => `Empty or undefined response from Z.AI API (finish_reason: ${finishReason ?? "unknown"})`,
3470
+ invalidFormat: "Invalid response format from Z.AI API",
3471
+ apiError: (error) => `Failed to get response from Z.AI API: ${error instanceof Error ? error.message : String(error)}`
3472
+ };
3473
+ constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
3474
+ super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
3475
+ this.client = new OpenAI5({
3476
+ apiKey,
3477
+ baseURL: "https://api.z.ai/api/paas/v4/"
3478
+ });
3479
+ }
3480
+ convertToOpenAIMessages(messages) {
3481
+ return messages.map((msg) => ({
3482
+ role: msg.role,
3483
+ content: msg.content
3484
+ }));
3485
+ }
3486
+ extractThinkingAndUsage(completion) {
3487
+ let thinkingContent = "";
3488
+ const message = completion.choices[0]?.message;
3489
+ if (this.enableThinking && message?.reasoning_content) {
3490
+ thinkingContent = message.reasoning_content;
3491
+ this.logger(`Captured reasoning_content (${thinkingContent.length} characters)`);
3492
+ }
3493
+ let tokenUsage;
3494
+ const usageResult = extractUsageAndCalculateCost(this.model, completion);
3495
+ if (usageResult) {
3496
+ tokenUsage = {
3497
+ inputTokens: usageResult.usage.promptTokens,
3498
+ outputTokens: usageResult.usage.completionTokens,
3499
+ totalTokens: usageResult.usage.totalTokens,
3500
+ costUSD: usageResult.cost,
3501
+ ...usageResult.usage.cacheHitTokens !== void 0 ? { cachedInputTokens: usageResult.usage.cacheHitTokens } : {}
3502
+ };
3503
+ if (this.enableThinking && usageResult.usage.reasoningTokens) {
3504
+ const reasoningTokens = usageResult.usage.reasoningTokens;
3505
+ const finalAnswerTokens = Math.max(0, tokenUsage.outputTokens - reasoningTokens);
3506
+ this.logger(
3507
+ `Output breakdown: ${reasoningTokens} reasoning tokens, ${finalAnswerTokens} final answer tokens`
3508
+ );
3509
+ }
3510
+ }
3511
+ return { thinkingContent, tokenUsage };
3512
+ }
3513
+ /**
3514
+ * Robust schema-aware coercion of a model reply.
3515
+ * Order: strict JSON parse → embedded {…} extraction → wrap-as-reply (BotAnswer-shaped schemas).
3516
+ * Returns the validated value or throws.
3517
+ */
3518
+ parseAndValidate(rawReply, zodSchema) {
3519
+ return parseAndValidateLlmJson(rawReply, zodSchema, (m) => this.logger(m));
3520
+ }
3521
+ async doAskWithZodSchema(zodSchema, messages) {
3522
+ try {
3523
+ const preparedMessages = this.prepareMessages(messages);
3524
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
3525
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
3526
+ openAIMessages.unshift({
3527
+ role: "system",
3528
+ content: this.instruction
3529
+ });
3530
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
3531
+ openAIMessages[0].content = `${this.instruction}
3532
+
3533
+ ${openAIMessages[0].content}`;
3534
+ }
3535
+ this.logAsking(messages);
3536
+ this.logMessages(messages);
3537
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
3538
+ const lastMessage = openAIMessages[openAIMessages.length - 1];
3539
+ if (lastMessage) {
3540
+ lastMessage.content += `
3541
+
3542
+ IMPORTANT: Respond with ONLY a valid JSON object matching this schema. Do NOT write narration, roleplay actions, asterisks, or commentary outside the JSON. Output the JSON object and nothing else.
3543
+ ${schemaDescription}`;
3544
+ }
3545
+ let completion;
3546
+ try {
3547
+ const params = {
3548
+ ...this.defaultParams,
3549
+ messages: openAIMessages,
3550
+ response_format: { type: "json_object" }
3551
+ };
3552
+ completion = await this.client.chat.completions.create(params);
3553
+ } catch (apiError) {
3554
+ this.logger(this.logTemplates.error(this.name, apiError));
3555
+ throw new Error(this.errorMessages.apiError(apiError));
3556
+ }
3557
+ const rawReply = completion.choices[0]?.message?.content;
3558
+ if (!rawReply) {
3559
+ throw new Error(this.errorMessages.emptyResponse(completion.choices[0]?.finish_reason));
3560
+ }
3561
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
3562
+ if (!reply) {
3563
+ throw new Error(this.errorMessages.emptyResponse(completion.choices[0]?.finish_reason));
3564
+ }
3565
+ const validated = this.parseAndValidate(reply, zodSchema);
3566
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
3567
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
3568
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
3569
+ if (validated) {
3570
+ this.logReply(validated, thinkingContent, tokenUsage);
3571
+ }
3572
+ return [validated, thinkingContent, tokenUsage];
3573
+ } catch (error) {
3574
+ this.logger(this.logTemplates.error(this.name, error));
3575
+ throw new Error(this.errorMessages.apiError(error));
3576
+ }
3577
+ }
3578
+ /**
3579
+ * Plain-text ask: no JSON mode and no schema appended to the prompt.
3580
+ * Thinking toggle and reasoning_content extraction are identical to askWithZodSchema.
3581
+ */
3582
+ async doAskText(messages) {
3583
+ try {
3584
+ const preparedMessages = this.prepareMessages(messages);
3585
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
3586
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
3587
+ openAIMessages.unshift({
3588
+ role: "system",
3589
+ content: this.instruction
3590
+ });
3591
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
3592
+ openAIMessages[0].content = `${this.instruction}
3593
+
3594
+ ${openAIMessages[0].content}`;
3595
+ }
3596
+ this.logAsking(messages);
3597
+ this.logMessages(messages);
3598
+ let completion;
3599
+ try {
3600
+ const params = {
3601
+ ...this.defaultParams,
3602
+ messages: openAIMessages
3603
+ };
3604
+ completion = await this.client.chat.completions.create(params);
3605
+ } catch (apiError) {
3606
+ this.logger(this.logTemplates.error(this.name, apiError));
3607
+ throw new Error(this.errorMessages.apiError(apiError));
3608
+ }
3609
+ const rawReply = completion.choices[0]?.message?.content;
3610
+ if (!rawReply) {
3611
+ throw new Error(this.errorMessages.emptyResponse(completion.choices[0]?.finish_reason));
3612
+ }
3613
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
3614
+ if (!reply) {
3615
+ throw new Error(this.errorMessages.emptyResponse(completion.choices[0]?.finish_reason));
3616
+ }
3617
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
3618
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
3619
+ this.logReply(reply, thinkingContent, tokenUsage);
3620
+ return [reply, thinkingContent, tokenUsage];
3621
+ } catch (error) {
3622
+ this.logger(this.logTemplates.error(this.name, error));
3623
+ throw new Error(this.errorMessages.apiError(error));
3624
+ }
3625
+ }
3626
+ };
3627
+
3628
+ // src/agents/fugu-agent.ts
3629
+ import { OpenAI as OpenAI6 } from "openai";
3630
+ var FuguAgent = class extends AbstractAgent {
3631
+ client;
3632
+ // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
3633
+ // initializer would snapshot the default and silently ignore the override.
3634
+ get defaultParams() {
3635
+ return {
3636
+ model: this.model,
3637
+ stream: false,
3638
+ // Caps visible output only. Server-side orchestration/reasoning tokens are
3639
+ // separate and unaffected by this.
3640
+ max_tokens: this.maxOutputTokens
3641
+ };
3642
+ }
3643
+ logTemplates = {
3644
+ error: (name, error) => `Error in ${name} agent: ${error}`
3645
+ };
3646
+ errorMessages = {
3647
+ emptyResponse: "Empty or undefined response from Sakana Fugu API",
3648
+ invalidFormat: "Invalid response format from Sakana Fugu API",
3649
+ apiError: (error) => `Failed to get response from Sakana Fugu API: ${error instanceof Error ? error.message : String(error)}`
3650
+ };
3651
+ constructor(name, instruction, model, apiKey, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
3652
+ super(name, instruction, model, 1, enableThinking, agentLoggingConfig);
3653
+ this.client = new OpenAI6({
3654
+ apiKey,
3655
+ baseURL: "https://api.sakana.ai/v1",
3656
+ timeout: 12e5
3657
+ });
3658
+ }
3659
+ convertToOpenAIMessages(messages) {
3660
+ return messages.map((msg) => ({
3661
+ role: msg.role,
3662
+ content: msg.content
3663
+ }));
3664
+ }
3665
+ // ───────────────────────────────────────────────────────────────────────────────────────
3666
+ // TEMPORARY (cost calibration). Base `fugu` is a dynamic router with no published per-token
3667
+ // price and Sakana returns NO cost field in the response — only token counts. Crucially the
3668
+ // response also reports "orchestration tokens" (billed at input/output rates per Sakana's
3669
+ // pricing page) that our standard TokenUsage drops. This logs the full raw breakdown to
3670
+ // BetterStack under a distinctive tag so we can sum real tokens per game and, combined with
3671
+ // the Sakana billing dashboard total, derive the true per-token rate. REMOVE AFTER CALIBRATION.
3672
+ logRawUsageForCalibration(completion) {
3673
+ const usage = completion?.usage;
3674
+ if (!usage) return;
3675
+ logger.info("FUGU_COST_CALIBRATION", {
3676
+ tag: "FUGU_COST_CALIBRATION",
3677
+ model: this.model,
3678
+ agentName: this.name,
3679
+ gameId: this.gameId,
3680
+ userId: this.userId,
3681
+ promptTokens: usage.prompt_tokens ?? 0,
3682
+ completionTokens: usage.completion_tokens ?? 0,
3683
+ totalTokens: usage.total_tokens ?? 0,
3684
+ cachedTokens: usage.prompt_tokens_details?.cached_tokens ?? 0,
3685
+ orchestrationInputTokens: usage.prompt_tokens_details?.orchestration_input_tokens ?? 0,
3686
+ orchestrationInputCachedTokens: usage.prompt_tokens_details?.orchestration_input_cached_tokens ?? 0,
3687
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? 0,
3688
+ orchestrationOutputTokens: usage.completion_tokens_details?.orchestration_output_tokens ?? 0,
3689
+ rawUsage: usage
3690
+ });
3691
+ }
3692
+ extractThinkingAndUsage(completion) {
3693
+ let thinkingContent = "";
3694
+ const message = completion.choices[0]?.message;
3695
+ if (message?.reasoning_content) {
3696
+ thinkingContent = message.reasoning_content;
3697
+ this.logger(`Captured reasoning_content (${thinkingContent.length} characters)`);
3698
+ }
3699
+ let tokenUsage;
3700
+ const usageResult = extractUsageAndCalculateCost(this.model, completion);
3701
+ if (usageResult) {
3702
+ tokenUsage = {
3703
+ inputTokens: usageResult.usage.promptTokens,
3704
+ outputTokens: usageResult.usage.completionTokens,
3705
+ totalTokens: usageResult.usage.totalTokens,
3706
+ costUSD: usageResult.cost,
3707
+ ...usageResult.usage.cacheHitTokens !== void 0 ? { cachedInputTokens: usageResult.usage.cacheHitTokens } : {}
3708
+ };
3709
+ if (usageResult.usage.reasoningTokens) {
3710
+ const reasoningTokens = usageResult.usage.reasoningTokens;
3711
+ const finalAnswerTokens = Math.max(0, tokenUsage.outputTokens - reasoningTokens);
3712
+ this.logger(
3713
+ `Output breakdown: ${reasoningTokens} reasoning tokens, ${finalAnswerTokens} final answer tokens`
3714
+ );
3715
+ }
3716
+ }
3717
+ return { thinkingContent, tokenUsage };
3718
+ }
3719
+ prependSystemInstruction(openAIMessages) {
3720
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
3721
+ openAIMessages.unshift({ role: "system", content: this.instruction });
3722
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
3723
+ openAIMessages[0].content = `${this.instruction}
3724
+
3725
+ ${openAIMessages[0].content}`;
3726
+ }
3727
+ }
3728
+ async doAskWithZodSchema(zodSchema, messages) {
3729
+ try {
3730
+ const preparedMessages = this.prepareMessages(messages);
3731
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
3732
+ this.prependSystemInstruction(openAIMessages);
3733
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
3734
+ const lastMessage = openAIMessages[openAIMessages.length - 1];
3735
+ if (lastMessage) {
3736
+ lastMessage.content += `
3737
+
3738
+ IMPORTANT: Respond with ONLY a valid JSON object matching this schema. Do NOT write narration, roleplay actions, asterisks, or commentary outside the JSON. Output the JSON object and nothing else.
3739
+ ${schemaDescription}`;
3740
+ }
3741
+ this.logAsking(messages);
3742
+ this.logMessages(messages);
3743
+ let completion;
3744
+ try {
3745
+ const params = {
3746
+ ...this.defaultParams,
3747
+ messages: openAIMessages,
3748
+ response_format: { type: "json_object" }
3749
+ };
3750
+ completion = await this.client.chat.completions.create(params);
3751
+ } catch (apiError) {
3752
+ this.logger(this.logTemplates.error(this.name, apiError));
3753
+ throw new Error(this.errorMessages.apiError(apiError));
3754
+ }
3755
+ this.logRawUsageForCalibration(completion);
3756
+ const rawReply = completion.choices[0]?.message?.content;
3757
+ if (!rawReply) {
3758
+ throw new Error(this.errorMessages.emptyResponse);
3759
+ }
3760
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
3761
+ if (!reply) {
3762
+ throw new Error(this.errorMessages.emptyResponse);
3763
+ }
3764
+ const validated = parseAndValidateLlmJson(reply, zodSchema, (m) => this.logger(m));
3765
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
3766
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
3767
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
3768
+ if (validated) {
3769
+ this.logReply(validated, thinkingContent, tokenUsage);
3770
+ }
3771
+ return [validated, thinkingContent, tokenUsage];
3772
+ } catch (error) {
3773
+ this.logger(this.logTemplates.error(this.name, error));
3774
+ throw new Error(this.errorMessages.apiError(error));
3775
+ }
3776
+ }
3777
+ /**
3778
+ * Plain-text ask: no schema appended to the prompt. Reasoning extraction and token
3779
+ * accounting are identical to askWithZodSchema.
3780
+ */
3781
+ async doAskText(messages) {
3782
+ try {
3783
+ const preparedMessages = this.prepareMessages(messages);
3784
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
3785
+ this.prependSystemInstruction(openAIMessages);
3786
+ this.logAsking(messages);
3787
+ this.logMessages(messages);
3788
+ let completion;
3789
+ try {
3790
+ const params = {
3791
+ ...this.defaultParams,
3792
+ messages: openAIMessages
3793
+ };
3794
+ completion = await this.client.chat.completions.create(params);
3795
+ } catch (apiError) {
3796
+ this.logger(this.logTemplates.error(this.name, apiError));
3797
+ throw new Error(this.errorMessages.apiError(apiError));
3798
+ }
3799
+ this.logRawUsageForCalibration(completion);
3800
+ const rawReply = completion.choices[0]?.message?.content;
3801
+ if (!rawReply) {
3802
+ throw new Error(this.errorMessages.emptyResponse);
3803
+ }
3804
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
3805
+ if (!reply) {
3806
+ throw new Error(this.errorMessages.emptyResponse);
3807
+ }
3808
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
3809
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
3810
+ this.logReply(reply, thinkingContent, tokenUsage);
3811
+ return [reply, thinkingContent, tokenUsage];
3812
+ } catch (error) {
3813
+ this.logger(this.logTemplates.error(this.name, error));
3814
+ throw new Error(this.errorMessages.apiError(error));
3815
+ }
3816
+ }
3817
+ };
3818
+
3819
+ // src/agents/qwen-agent.ts
3820
+ import { OpenAI as OpenAI7 } from "openai";
3821
+ var QwenAgent = class extends AbstractAgent {
3822
+ client;
3823
+ // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
3824
+ // initializer would snapshot the default and silently ignore the override.
3825
+ get defaultParams() {
3826
+ return {
3827
+ model: this.model,
3828
+ temperature: this.temperature,
3829
+ stream: false,
3830
+ // Reasoning tokens share the completion budget on Qwen, so this has to leave room
3831
+ // for both CoT and answer — too small cuts the JSON mid-object.
3832
+ max_tokens: this.maxOutputTokens
3833
+ };
3834
+ }
3835
+ logTemplates = {
3836
+ error: (name, error) => `Error in ${name} agent: ${error}`
3837
+ };
3838
+ errorMessages = {
3839
+ emptyResponse: "Empty or undefined response from Qwen API",
3840
+ invalidFormat: "Invalid response format from Qwen API",
3841
+ apiError: (error) => `Failed to get response from Qwen API: ${error instanceof Error ? error.message : String(error)}`
3842
+ };
3843
+ constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
3844
+ super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
3845
+ this.client = new OpenAI7({
3846
+ apiKey,
3847
+ baseURL: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
3848
+ });
3849
+ }
3850
+ /**
3851
+ * Thinking params for the request body. `thinking_budget` caps reasoning length and is only
3852
+ * sent when the instance has one (catalog default, or a per-call override like story
3853
+ * generation); without it the model thinks at the provider default, and qwen3.8-max's
3854
+ * latency then swings 30–100s.
3855
+ *
3856
+ * `reasoning_effort` is deliberately NOT sent. Probed live 2026-08-30 on qwen3.8-flash and
3857
+ * qwen3.8-max: every value low..max is accepted, but reasoning length doesn't track it
3858
+ * (max: low → 1,686 reasoning tokens / 44s, high → 226 / 7s, xhigh → 1,102 / 30s), while
3859
+ * thinking_budget bounds it reliably (≤340 at 1024). The docs also call the two mutually
3860
+ * exclusive on qwen3.8-max. So on Qwen the budget IS the effort knob; `reasoningEffort`
3861
+ * on this agent is ignored.
3862
+ */
3863
+ thinkingParams() {
3864
+ const budget = this.thinkingBudgetTokens;
3865
+ return {
3866
+ enable_thinking: this.enableThinking,
3867
+ ...this.enableThinking && budget !== void 0 ? { thinking_budget: budget } : {}
3868
+ };
3869
+ }
3870
+ convertToOpenAIMessages(messages) {
3871
+ return messages.map((msg) => ({
3872
+ role: msg.role,
3873
+ content: msg.content
3874
+ }));
3875
+ }
3876
+ extractThinkingAndUsage(completion) {
3877
+ let thinkingContent = "";
3878
+ const message = completion.choices[0]?.message;
3879
+ if (this.enableThinking && message?.reasoning_content) {
3880
+ thinkingContent = message.reasoning_content;
3881
+ this.logger(`Captured reasoning_content (${thinkingContent.length} characters)`);
3882
+ }
3883
+ let tokenUsage;
3884
+ const usageResult = extractUsageAndCalculateCost(this.model, completion);
3885
+ if (usageResult) {
3886
+ tokenUsage = {
3887
+ inputTokens: usageResult.usage.promptTokens,
3888
+ outputTokens: usageResult.usage.completionTokens,
3889
+ totalTokens: usageResult.usage.totalTokens,
3890
+ costUSD: usageResult.cost,
3891
+ ...usageResult.usage.cacheHitTokens !== void 0 ? { cachedInputTokens: usageResult.usage.cacheHitTokens } : {}
3892
+ };
3893
+ if (this.enableThinking && usageResult.usage.reasoningTokens) {
3894
+ const reasoningTokens = usageResult.usage.reasoningTokens;
3895
+ const finalAnswerTokens = Math.max(0, tokenUsage.outputTokens - reasoningTokens);
3896
+ this.logger(
3897
+ `Output breakdown: ${reasoningTokens} reasoning tokens, ${finalAnswerTokens} final answer tokens`
3898
+ );
3899
+ }
3900
+ }
3901
+ return { thinkingContent, tokenUsage };
3902
+ }
3903
+ /**
3904
+ * Robust schema-aware coercion of a model reply.
3905
+ * Order: strict JSON parse → embedded {…} extraction → wrap-as-reply (BotAnswer-shaped schemas).
3906
+ * Returns the validated value or throws.
3907
+ */
3908
+ parseAndValidate(rawReply, zodSchema) {
3909
+ return parseAndValidateLlmJson(rawReply, zodSchema, (m) => this.logger(m));
3910
+ }
3911
+ async doAskWithZodSchema(zodSchema, messages) {
3912
+ try {
3913
+ const preparedMessages = this.prepareMessages(messages);
3914
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
3915
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
3916
+ openAIMessages.unshift({
3917
+ role: "system",
3918
+ content: this.instruction
3919
+ });
3920
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
3921
+ openAIMessages[0].content = `${this.instruction}
3922
+
3923
+ ${openAIMessages[0].content}`;
3924
+ }
3925
+ this.logAsking(messages);
3926
+ this.logMessages(messages);
3927
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
3928
+ const lastMessage = openAIMessages[openAIMessages.length - 1];
3929
+ if (lastMessage) {
3930
+ lastMessage.content += `
3931
+
3932
+ IMPORTANT: Respond with ONLY a valid JSON object matching this schema. Do NOT write narration, roleplay actions, asterisks, or commentary outside the JSON. Output the JSON object and nothing else.
3933
+ ${schemaDescription}`;
3934
+ }
3935
+ let completion;
3936
+ try {
3937
+ const params = {
3938
+ ...this.defaultParams,
3939
+ messages: openAIMessages,
3940
+ ...this.thinkingParams()
3941
+ };
3942
+ completion = await this.client.chat.completions.create(params);
3943
+ } catch (apiError) {
3944
+ this.logger(this.logTemplates.error(this.name, apiError));
3945
+ throw new Error(this.errorMessages.apiError(apiError));
3946
+ }
3947
+ const rawReply = completion.choices[0]?.message?.content;
3948
+ if (!rawReply) {
3949
+ throw new Error(this.errorMessages.emptyResponse);
3950
+ }
3951
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
3952
+ if (!reply) {
3953
+ throw new Error(this.errorMessages.emptyResponse);
3954
+ }
3955
+ const validated = this.parseAndValidate(reply, zodSchema);
3956
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
3957
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
3958
+ const thinkingContent = [reasoningContent, inlineThinking].filter(Boolean).join("\n");
3959
+ if (validated) {
3960
+ this.logReply(validated, thinkingContent, tokenUsage);
3961
+ }
3962
+ return [validated, thinkingContent, tokenUsage];
3963
+ } catch (error) {
3964
+ this.logger(this.logTemplates.error(this.name, error));
3965
+ throw new Error(this.errorMessages.apiError(error));
3966
+ }
3967
+ }
3968
+ /**
3969
+ * Plain-text ask: no schema appended to the prompt.
3970
+ * Thinking toggle and reasoning_content extraction are identical to askWithZodSchema.
3971
+ */
3972
+ async doAskText(messages) {
3973
+ try {
3974
+ const preparedMessages = this.prepareMessages(messages);
3975
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
3976
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
3977
+ openAIMessages.unshift({
3978
+ role: "system",
3979
+ content: this.instruction
3980
+ });
3981
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
3982
+ openAIMessages[0].content = `${this.instruction}
3983
+
3984
+ ${openAIMessages[0].content}`;
3985
+ }
3986
+ this.logAsking(messages);
3987
+ this.logMessages(messages);
3988
+ let completion;
3989
+ try {
3990
+ const params = {
3991
+ ...this.defaultParams,
3992
+ messages: openAIMessages,
3993
+ ...this.thinkingParams()
3994
+ };
3995
+ completion = await this.client.chat.completions.create(params);
3996
+ } catch (apiError) {
3997
+ this.logger(this.logTemplates.error(this.name, apiError));
3998
+ throw new Error(this.errorMessages.apiError(apiError));
3999
+ }
4000
+ const rawReply = completion.choices[0]?.message?.content;
4001
+ if (!rawReply) {
4002
+ throw new Error(this.errorMessages.emptyResponse);
4003
+ }
4004
+ const { text: reply, thinking: inlineThinking } = stripInlineThinking(rawReply);
4005
+ if (!reply) {
4006
+ throw new Error(this.errorMessages.emptyResponse);
4007
+ }
4008
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
4009
+ const thinkingContent = [reasoningContent, inlineThinking].filter(Boolean).join("\n");
4010
+ this.logReply(reply, thinkingContent, tokenUsage);
4011
+ return [reply, thinkingContent, tokenUsage];
4012
+ } catch (error) {
4013
+ this.logger(this.logTemplates.error(this.name, error));
4014
+ throw new Error(this.errorMessages.apiError(error));
4015
+ }
4016
+ }
4017
+ };
4018
+
4019
+ // src/agents/minimax-agent.ts
4020
+ import { OpenAI as OpenAI8 } from "openai";
4021
+ var MiniMaxAgent = class extends AbstractAgent {
4022
+ client;
4023
+ // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
4024
+ // initializer would snapshot the default and silently ignore the override.
4025
+ get defaultParams() {
4026
+ return {
4027
+ model: this.model,
4028
+ temperature: this.temperature,
4029
+ stream: false,
4030
+ // MiniMax deprecates max_tokens in favor of max_completion_tokens (M3 max is 512K,
4031
+ // far above anything a turn needs).
4032
+ max_completion_tokens: this.maxOutputTokens
4033
+ };
4034
+ }
4035
+ logTemplates = {
4036
+ error: (name, error) => `Error in ${name} agent: ${error}`
4037
+ };
4038
+ errorMessages = {
4039
+ emptyResponse: "Empty or undefined response from MiniMax API",
4040
+ invalidFormat: "Invalid response format from MiniMax API",
4041
+ apiError: (error) => `Failed to get response from MiniMax API: ${error instanceof Error ? error.message : String(error)}`
4042
+ };
4043
+ constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
4044
+ super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
4045
+ this.client = new OpenAI8({
4046
+ apiKey,
4047
+ baseURL: "https://api.minimax.io/v1"
4048
+ });
4049
+ }
4050
+ thinkingParams() {
4051
+ return {
4052
+ thinking: { type: this.enableThinking ? "adaptive" : "disabled" },
4053
+ reasoning_split: true
4054
+ };
4055
+ }
4056
+ convertToOpenAIMessages(messages) {
4057
+ return messages.map((msg) => ({
4058
+ role: msg.role,
4059
+ content: msg.content
4060
+ }));
4061
+ }
4062
+ extractThinkingAndUsage(completion) {
4063
+ let thinkingContent = "";
4064
+ const message = completion.choices[0]?.message;
4065
+ if (this.enableThinking && message?.reasoning_content) {
4066
+ thinkingContent = message.reasoning_content;
4067
+ this.logger(`Captured reasoning_content (${thinkingContent.length} characters)`);
4068
+ }
4069
+ let tokenUsage;
4070
+ const usageResult = extractUsageAndCalculateCost(this.model, completion);
4071
+ if (usageResult) {
4072
+ tokenUsage = {
4073
+ inputTokens: usageResult.usage.promptTokens,
4074
+ outputTokens: usageResult.usage.completionTokens,
4075
+ totalTokens: usageResult.usage.totalTokens,
4076
+ costUSD: usageResult.cost,
4077
+ ...usageResult.usage.cacheHitTokens !== void 0 ? { cachedInputTokens: usageResult.usage.cacheHitTokens } : {}
4078
+ };
4079
+ if (this.enableThinking && usageResult.usage.reasoningTokens) {
4080
+ const reasoningTokens = usageResult.usage.reasoningTokens;
4081
+ const finalAnswerTokens = Math.max(0, tokenUsage.outputTokens - reasoningTokens);
4082
+ this.logger(
4083
+ `Output breakdown: ${reasoningTokens} reasoning tokens, ${finalAnswerTokens} final answer tokens`
4084
+ );
4085
+ }
4086
+ }
4087
+ return { thinkingContent, tokenUsage };
4088
+ }
4089
+ /**
4090
+ * Robust schema-aware coercion of a model reply.
4091
+ * Order: strict JSON parse → embedded {…} extraction → wrap-as-reply (BotAnswer-shaped schemas).
4092
+ * Returns the validated value or throws.
4093
+ */
4094
+ parseAndValidate(rawReply, zodSchema) {
4095
+ return parseAndValidateLlmJson(rawReply, zodSchema, (m) => this.logger(m));
4096
+ }
4097
+ async doAskWithZodSchema(zodSchema, messages) {
4098
+ try {
4099
+ const preparedMessages = this.prepareMessages(messages);
4100
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
4101
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
4102
+ openAIMessages.unshift({
4103
+ role: "system",
4104
+ content: this.instruction
4105
+ });
4106
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
4107
+ openAIMessages[0].content = `${this.instruction}
4108
+
4109
+ ${openAIMessages[0].content}`;
4110
+ }
4111
+ this.logAsking(messages);
4112
+ this.logMessages(messages);
4113
+ const schemaDescription = ZodSchemaConverter.toPromptDescription(zodSchema);
4114
+ const lastMessage = openAIMessages[openAIMessages.length - 1];
4115
+ if (lastMessage) {
4116
+ lastMessage.content += `
4117
+
4118
+ IMPORTANT: Respond with ONLY a valid JSON object matching this schema. Do NOT write narration, roleplay actions, asterisks, or commentary outside the JSON. Output the JSON object and nothing else.
4119
+ ${schemaDescription}`;
4120
+ }
4121
+ let completion;
4122
+ try {
4123
+ const params = {
4124
+ ...this.defaultParams,
4125
+ messages: openAIMessages,
4126
+ ...this.thinkingParams()
4127
+ };
4128
+ completion = await this.client.chat.completions.create(params);
4129
+ } catch (apiError) {
4130
+ this.logger(this.logTemplates.error(this.name, apiError));
4131
+ throw new Error(this.errorMessages.apiError(apiError));
4132
+ }
4133
+ const reply = completion.choices[0]?.message?.content;
4134
+ if (!reply) {
4135
+ throw new Error(this.errorMessages.emptyResponse);
4136
+ }
4137
+ const { text: cleanReply, thinking: inlineThinking } = stripInlineThinking(reply);
4138
+ const validated = this.parseAndValidate(cleanReply, zodSchema);
4139
+ this.logger(`\u2705 Response validated successfully with Zod schema`);
4140
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
4141
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
4142
+ if (validated) {
4143
+ this.logReply(validated, thinkingContent, tokenUsage);
4144
+ }
4145
+ return [validated, thinkingContent, tokenUsage];
4146
+ } catch (error) {
4147
+ this.logger(this.logTemplates.error(this.name, error));
4148
+ throw new Error(this.errorMessages.apiError(error));
4149
+ }
4150
+ }
4151
+ /**
4152
+ * Plain-text ask: no schema appended to the prompt.
4153
+ * Thinking handling and reasoning_content extraction are identical to askWithZodSchema.
4154
+ */
4155
+ async doAskText(messages) {
4156
+ try {
4157
+ const preparedMessages = this.prepareMessages(messages);
4158
+ const openAIMessages = this.convertToOpenAIMessages(preparedMessages);
4159
+ if (openAIMessages.length > 0 && openAIMessages[0].role !== "system") {
4160
+ openAIMessages.unshift({
4161
+ role: "system",
4162
+ content: this.instruction
4163
+ });
4164
+ } else if (openAIMessages.length > 0 && openAIMessages[0].role === "system") {
4165
+ openAIMessages[0].content = `${this.instruction}
4166
+
4167
+ ${openAIMessages[0].content}`;
4168
+ }
4169
+ this.logAsking(messages);
4170
+ this.logMessages(messages);
4171
+ let completion;
4172
+ try {
4173
+ const params = {
4174
+ ...this.defaultParams,
4175
+ messages: openAIMessages,
4176
+ ...this.thinkingParams()
4177
+ };
4178
+ completion = await this.client.chat.completions.create(params);
4179
+ } catch (apiError) {
4180
+ this.logger(this.logTemplates.error(this.name, apiError));
4181
+ throw new Error(this.errorMessages.apiError(apiError));
4182
+ }
4183
+ const reply = completion.choices[0]?.message?.content;
4184
+ if (!reply) {
4185
+ throw new Error(this.errorMessages.emptyResponse);
4186
+ }
4187
+ const { text: cleanReply, thinking: inlineThinking } = stripInlineThinking(reply);
4188
+ if (!cleanReply) {
4189
+ throw new Error(this.errorMessages.emptyResponse);
4190
+ }
4191
+ const { thinkingContent: reasoningContent, tokenUsage } = this.extractThinkingAndUsage(completion);
4192
+ const thinkingContent = mergeThinking(reasoningContent, inlineThinking);
4193
+ this.logReply(cleanReply, thinkingContent, tokenUsage);
4194
+ return [cleanReply, thinkingContent, tokenUsage];
4195
+ } catch (error) {
4196
+ this.logger(this.logTemplates.error(this.name, error));
4197
+ throw new Error(this.errorMessages.apiError(error));
4198
+ }
4199
+ }
4200
+ };
4201
+
4202
+ // src/agents/agent-factory.ts
4203
+ var AgentFactory = class {
4204
+ static createAgent(name, instruction, llmType, apiKeys, enableThinking = false) {
4205
+ const modelName = this.validateLlmTypeAndGet(llmType);
4206
+ const model = SupportedAiModels[modelName];
4207
+ const apiKeyName = model.apiKeyName;
4208
+ const key = apiKeys[apiKeyName];
4209
+ const shouldEnableThinking = model.hasThinking;
4210
+ switch (modelName) {
4211
+ // Claude models — thinking-only since 2026-08-05
4212
+ case LLM_CONSTANTS.CLAUDE_FABLE:
4213
+ case LLM_CONSTANTS.CLAUDE_OPUS:
4214
+ case LLM_CONSTANTS.CLAUDE_SONNET:
4215
+ case LLM_CONSTANTS.CLAUDE_HAIKU:
4216
+ return new ClaudeAgent(name, instruction, model.modelApiName, key, shouldEnableThinking);
4217
+ // Always-on reasoning models
4218
+ case LLM_CONSTANTS.GPT_SOL:
4219
+ case LLM_CONSTANTS.GPT:
4220
+ case LLM_CONSTANTS.GPT_MINI:
4221
+ return new Gpt5Agent(name, instruction, model.modelApiName, key, model.temperature, shouldEnableThinking);
4222
+ case LLM_CONSTANTS.GEMINI_PRO:
4223
+ case LLM_CONSTANTS.GEMINI_FLASH:
4224
+ case LLM_CONSTANTS.GEMINI_LITE:
4225
+ return new GoogleAgent(name, instruction, model.modelApiName, key, shouldEnableThinking);
4226
+ case LLM_CONSTANTS.GROK:
4227
+ return new GrokAgent(name, instruction, model.modelApiName, key, model.temperature, shouldEnableThinking);
4228
+ // DeepSeek V4 models — thinking-only since 2026-08-05
4229
+ case LLM_CONSTANTS.DEEPSEEK_FLASH:
4230
+ case LLM_CONSTANTS.DEEPSEEK_PRO:
4231
+ return new DeepSeekV2Agent(name, instruction, model.modelApiName, key, model.temperature ?? 0, shouldEnableThinking);
4232
+ // Mistral models
4233
+ case LLM_CONSTANTS.MISTRAL_MEDIUM:
4234
+ case LLM_CONSTANTS.MISTRAL_SMALL:
4235
+ case LLM_CONSTANTS.MISTRAL_LARGE:
4236
+ case LLM_CONSTANTS.MISTRAL_MAGISTRAL:
4237
+ return new MistralAgent(name, instruction, model.modelApiName, key, shouldEnableThinking);
4238
+ case LLM_CONSTANTS.KIMI:
4239
+ return new KimiAgent(name, instruction, model.modelApiName, key, 0, shouldEnableThinking);
4240
+ // Z.AI models — thinking-only since 2026-08-05
4241
+ case LLM_CONSTANTS.GLM:
4242
+ case LLM_CONSTANTS.GLM_FLASH:
4243
+ return new GlmAgent(name, instruction, model.modelApiName, key, model.temperature, shouldEnableThinking);
4244
+ // Sakana Fugu models — always-on reasoning, no temperature (ignored by the model)
4245
+ case LLM_CONSTANTS.FUGU_ULTRA:
4246
+ return new FuguAgent(name, instruction, model.modelApiName, key, shouldEnableThinking);
4247
+ // Qwen models — thinking-only (enable_thinking always sent)
4248
+ case LLM_CONSTANTS.QWEN_MAX:
4249
+ case LLM_CONSTANTS.QWEN_FLASH:
4250
+ return new QwenAgent(name, instruction, model.modelApiName, key, model.temperature, shouldEnableThinking);
4251
+ // MiniMax M3 — adaptive thinking (the model decides per-request)
4252
+ case LLM_CONSTANTS.MINIMAX:
4253
+ return new MiniMaxAgent(name, instruction, model.modelApiName, key, model.temperature, shouldEnableThinking);
4254
+ default:
4255
+ throw new Error(`Unknown Key: ${modelName}`);
4256
+ }
4257
+ }
4258
+ static validateLlmTypeAndGet(llmType) {
4259
+ const llmValues = Object.values(LLM_CONSTANTS);
4260
+ if (!llmValues.includes(llmType)) {
4261
+ throw new Error(`Invalid llmType: ${llmType}`);
4262
+ }
4263
+ return llmType;
4264
+ }
4265
+ };
4266
+ export {
4267
+ ANTHROPIC_REASONING_EFFORTS,
4268
+ API_KEY_CONSTANTS,
4269
+ AbstractAgent,
4270
+ AgentFactory,
4271
+ BotResponseError,
4272
+ CACHE_TIER_MARKER,
4273
+ ClaudeAgent,
4274
+ DEEPSEEK_PEAK_SCHEDULE,
4275
+ DEEPSEEK_REASONING_EFFORTS,
4276
+ DEFAULT_LOGGING_CONFIG,
4277
+ DEFAULT_MAX_OUTPUT_TOKENS,
4278
+ DeepSeekV2Agent,
4279
+ FUGU_REASONING_EFFORTS,
4280
+ FuguAgent,
4281
+ GEMINI_REASONING_EFFORTS,
4282
+ GLM_REASONING_EFFORTS,
4283
+ GlmAgent,
4284
+ GoogleAgent,
4285
+ Gpt5Agent,
4286
+ GrokAgent,
4287
+ KimiAgent,
4288
+ LLM_CONSTANTS,
4289
+ MESSAGE_ROLE,
4290
+ MODEL_PRICING,
4291
+ MiniMaxAgent,
4292
+ MistralAgent,
4293
+ ModelAuthenticationError,
4294
+ ModelError,
4295
+ ModelOverloadError,
4296
+ ModelQuotaExceededError,
4297
+ ModelRateLimitError,
4298
+ ModelRefusalError,
4299
+ ModelUnavailableError,
4300
+ OPENAI_REASONING_EFFORTS,
4301
+ QwenAgent,
4302
+ REASONING_EFFORT_SCALE,
4303
+ SupportedAiKeyNames,
4304
+ SupportedAiModels,
4305
+ ZodSchemaConverter,
4306
+ calculateAnthropicCost,
4307
+ calculateCost,
4308
+ calculateDeepSeekCost,
4309
+ calculateGoogleCost,
4310
+ calculateGrokCost,
4311
+ calculateKimiCost,
4312
+ calculateMistralCost,
4313
+ calculateModelCost,
4314
+ calculateOpenAICost,
4315
+ clampReasoningEffort,
4316
+ cleanResponse,
4317
+ createCatalog,
4318
+ extractAnthropicTokenUsage,
4319
+ extractTokenUsageFromResponse5 as extractAnthropicTokenUsageFromResponse,
4320
+ extractDeepSeekTokenUsage,
4321
+ extractTokenUsageFromResponse2 as extractDeepSeekTokenUsageFromResponse,
4322
+ extractFirstJsonObject,
4323
+ extractGoogleTokenUsage,
4324
+ extractTokenUsageFromResponse6 as extractGoogleTokenUsageFromResponse,
4325
+ extractGrokTokenUsage,
4326
+ extractTokenUsageFromResponse4 as extractGrokTokenUsageFromResponse,
4327
+ extractKimiTokenUsage,
4328
+ extractTokenUsageFromResponse3 as extractKimiTokenUsageFromResponse,
4329
+ extractMistralTokenUsage,
4330
+ extractTokenUsageFromResponse7 as extractMistralTokenUsageFromResponse,
4331
+ extractOpenAITokenUsage,
4332
+ extractTokenUsageFromResponse as extractOpenAITokenUsageFromResponse,
4333
+ extractTokenUsage,
4334
+ extractUsageAndCalculateCost,
4335
+ generateSchemaInstructions,
4336
+ getModelConfigByApiName,
4337
+ getModelDisplayName,
4338
+ getModelProviderName,
4339
+ getModelTags,
4340
+ getProviderSignatureFields,
4341
+ isHybridThinkingModel,
4342
+ isInPeakWindow,
4343
+ isPeakBilling,
4344
+ isWeekendAt,
4345
+ logger,
4346
+ mergeThinking,
4347
+ modelHasTag,
4348
+ modelIsFast,
4349
+ needsPromptBasedSchema,
4350
+ parseAndValidateLlmJson,
4351
+ safeValidateResponse,
4352
+ setLlmLogger,
4353
+ stableHashHex,
4354
+ stripInlineThinking,
4355
+ supportsNativeJsonSchema,
4356
+ toAnthropicEffort,
4357
+ toDeepSeekEffort,
4358
+ toFuguEffort,
4359
+ toGeminiEffort,
4360
+ toGlmEffort,
4361
+ toOpenAIEffort,
4362
+ validateResponse
4363
+ };
4364
+ //# sourceMappingURL=index.mjs.map