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