@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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/index.d.mts +1191 -0
- package/dist/index.d.ts +1191 -0
- package/dist/index.js +4496 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4364 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +77 -0
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
|