@orxataguy/tyr 1.0.31 → 1.0.32
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/README.md +30 -0
- package/package.json +1 -1
- package/src/core/Container.ts +4 -1
- package/src/core/Kernel.ts +2 -0
- package/src/core/sys/chat.ts +73 -0
- package/src/core/sys/help.ts +1 -0
- package/src/index.ts +1 -0
- package/src/lib/AIContextManager.ts +1004 -27
- package/src/lib/AIVendorManager.ts +200 -9
- package/src/lib/ChatManager.ts +880 -0
- package/src/lib/PromptTemplateManager.ts +105 -1
- package/src/lib/TokenManager.ts +30 -2
|
@@ -17,11 +17,15 @@ export type AIRole = 'system' | 'user' | 'assistant';
|
|
|
17
17
|
* formato (en Gemini, por ejemplo, es un id sintético generado por este manager, ya que la
|
|
18
18
|
* API de Gemini no da ids reales para las function calls).
|
|
19
19
|
* - 'tool_result': resultado de haber ejecutado una herramienta, para devolvérselo al modelo.
|
|
20
|
+
* - 'image': imagen adjunta (p.ej. un attachment de chat). `data` es el contenido en base64 SIN
|
|
21
|
+
* el prefijo `data:...;base64,`; `mediaType` es el MIME type (p.ej. 'image/png'). Solo válido
|
|
22
|
+
* en mensajes de rol 'user' — los vendors no devuelven imágenes en sus respuestas.
|
|
20
23
|
*/
|
|
21
24
|
export type AIContentBlock =
|
|
22
25
|
| { type: 'text'; text: string }
|
|
23
26
|
| { type: 'tool_use'; id: string; name: string; input: any }
|
|
24
|
-
| { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean }
|
|
27
|
+
| { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean }
|
|
28
|
+
| { type: 'image'; mediaType: string; data: string };
|
|
25
29
|
|
|
26
30
|
export interface AIMessage {
|
|
27
31
|
role: AIRole;
|
|
@@ -37,6 +41,24 @@ export interface AITool {
|
|
|
37
41
|
input_schema: any;
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Nivel de "esfuerzo de razonamiento" (extended thinking) pedido al modelo, independiente del
|
|
46
|
+
* vendor concreto. Se traduce a `thinking.budget_tokens` en Anthropic y a `reasoning_effort` en
|
|
47
|
+
* los modelos de OpenAI que lo soportan (serie 'o'); Gemini no tiene un equivalente público en
|
|
48
|
+
* este momento y el valor se ignora silenciosamente para ese vendor.
|
|
49
|
+
*/
|
|
50
|
+
export type ThinkingEffort = 'none' | 'low' | 'medium' | 'high' | 'max';
|
|
51
|
+
|
|
52
|
+
/** Prioridad de una tarea, usada por resolvePriority()/completeWithPriority() para elegir modelo
|
|
53
|
+
* y esfuerzo de razonamiento sin que el llamador tenga que conocer nombres de modelo concretos. */
|
|
54
|
+
export type TaskPriority =
|
|
55
|
+
| 'baja-prioridad'
|
|
56
|
+
| 'baja-media-prioridad'
|
|
57
|
+
| 'media-prioridad'
|
|
58
|
+
| 'media-alta-prioridad'
|
|
59
|
+
| 'alta-prioridad'
|
|
60
|
+
| 'muy-alta-prioridad';
|
|
61
|
+
|
|
40
62
|
export interface AICompletionOptions {
|
|
41
63
|
vendor?: AIVendor;
|
|
42
64
|
model?: string;
|
|
@@ -46,6 +68,9 @@ export interface AICompletionOptions {
|
|
|
46
68
|
/** Herramientas disponibles para que el modelo las invoque. Solo soportado en complete(),
|
|
47
69
|
* no en stream() — ver el guard al inicio de stream(). */
|
|
48
70
|
tools?: AITool[];
|
|
71
|
+
/** Nivel de extended thinking / reasoning effort. Ver ThinkingEffort. 'none' (o ausente)
|
|
72
|
+
* desactiva el thinking explícitamente. */
|
|
73
|
+
thinking?: ThinkingEffort;
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
export interface AICompletionResult {
|
|
@@ -72,6 +97,7 @@ interface VendorConfig {
|
|
|
72
97
|
temperature: number;
|
|
73
98
|
maxTokens: number;
|
|
74
99
|
maxRetries: number;
|
|
100
|
+
thinking: ThinkingEffort;
|
|
75
101
|
}
|
|
76
102
|
|
|
77
103
|
interface VendorRequest {
|
|
@@ -97,6 +123,58 @@ const DEFAULT_MAX_TOKENS = 4096;
|
|
|
97
123
|
const DEFAULT_MAX_RETRIES = 3;
|
|
98
124
|
const RETRY_BASE_DELAY_MS = 500;
|
|
99
125
|
|
|
126
|
+
// --- Dynamic Model Routing ----------------------------------------------------------------------
|
|
127
|
+
//
|
|
128
|
+
// Three cost/capability tiers per vendor. Each is overridable via env var (AI_MODEL_CHEAP /
|
|
129
|
+
// AI_MODEL_BALANCED / AI_MODEL_FLAGSHIP) so operators can point a tier at a newer model without
|
|
130
|
+
// touching code. 'balanced' matches DEFAULT_MODELS on purpose — it's the vendor's normal default.
|
|
131
|
+
type ModelTier = 'cheap' | 'balanced' | 'flagship';
|
|
132
|
+
|
|
133
|
+
const MODEL_TIERS: Record<AIVendor, Record<ModelTier, string>> = {
|
|
134
|
+
anthropic: { cheap: 'claude-haiku-4-5', balanced: 'claude-sonnet-5', flagship: 'claude-opus-4-1' },
|
|
135
|
+
openai: { cheap: 'gpt-4o-mini', balanced: 'gpt-4o', flagship: 'gpt-4o' },
|
|
136
|
+
gemini: { cheap: 'gemini-2.5-flash', balanced: 'gemini-2.5-pro', flagship: 'gemini-2.5-pro' },
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const MODEL_TIER_ENV: Record<ModelTier, string> = {
|
|
140
|
+
cheap: 'AI_MODEL_CHEAP',
|
|
141
|
+
balanced: 'AI_MODEL_BALANCED',
|
|
142
|
+
flagship: 'AI_MODEL_FLAGSHIP',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** budget_tokens sent to Anthropic's `thinking` param per effort level. 'none' disables thinking. */
|
|
146
|
+
const THINKING_BUDGETS: Record<ThinkingEffort, number> = {
|
|
147
|
+
none: 0,
|
|
148
|
+
low: 1024,
|
|
149
|
+
medium: 4096,
|
|
150
|
+
high: 8192,
|
|
151
|
+
max: 16384,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Task-priority → (model tier, thinking effort) matrix. This is the routing table requested by
|
|
156
|
+
* the architecture spec: callers pick a priority by *intent* ("this is a trivial file", "this
|
|
157
|
+
* previous attempt failed"), not by model name, and AIVendorManager decides what that costs.
|
|
158
|
+
*/
|
|
159
|
+
const PRIORITY_ROUTING: Record<TaskPriority, { tier: ModelTier; thinking: ThinkingEffort }> = {
|
|
160
|
+
'baja-prioridad': { tier: 'cheap', thinking: 'none' },
|
|
161
|
+
'baja-media-prioridad': { tier: 'cheap', thinking: 'max' },
|
|
162
|
+
'media-prioridad': { tier: 'balanced', thinking: 'none' },
|
|
163
|
+
'media-alta-prioridad': { tier: 'balanced', thinking: 'high' },
|
|
164
|
+
'alta-prioridad': { tier: 'balanced', thinking: 'max' },
|
|
165
|
+
'muy-alta-prioridad': { tier: 'flagship', thinking: 'max' },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Ordered priority ladder, used by bumpPriority() to escalate one level at a time. */
|
|
169
|
+
const PRIORITY_LADDER: TaskPriority[] = [
|
|
170
|
+
'baja-prioridad',
|
|
171
|
+
'baja-media-prioridad',
|
|
172
|
+
'media-prioridad',
|
|
173
|
+
'media-alta-prioridad',
|
|
174
|
+
'alta-prioridad',
|
|
175
|
+
'muy-alta-prioridad',
|
|
176
|
+
];
|
|
177
|
+
|
|
100
178
|
/**
|
|
101
179
|
* @class AIVendorManager
|
|
102
180
|
* @description Unified client for AI chat-completion APIs (Anthropic, OpenAI, Gemini).
|
|
@@ -154,9 +232,79 @@ export class AIVendorManager {
|
|
|
154
232
|
temperature: options?.temperature ?? getEnvDouble('AI_TEMPERATURE', DEFAULT_TEMPERATURE),
|
|
155
233
|
maxTokens: options?.maxTokens ?? getEnvInt('AI_MAX_TOKENS', DEFAULT_MAX_TOKENS),
|
|
156
234
|
maxRetries: options?.maxRetries ?? getEnvInt('AI_MAX_RETRIES', DEFAULT_MAX_RETRIES),
|
|
235
|
+
thinking: options?.thinking ?? 'none',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @method resolvePriority
|
|
241
|
+
* @description Translates a TaskPriority into concrete AICompletionOptions (model + thinking
|
|
242
|
+
* effort) for the currently configured vendor (or the vendor passed in `overrides`), via the
|
|
243
|
+
* routing matrix in PRIORITY_ROUTING. This is the single place that maps "how important/hard
|
|
244
|
+
* is this task" to "which model do we pay for" — Manager code should route through this
|
|
245
|
+
* instead of hardcoding model names.
|
|
246
|
+
* @param {TaskPriority} priority - One of the six priority levels (see TaskPriority).
|
|
247
|
+
* @param {AICompletionOptions} overrides - Optional overrides merged on top (e.g. tools, maxTokens).
|
|
248
|
+
* @returns {AICompletionOptions} Options ready to pass to complete()/stream().
|
|
249
|
+
* @example
|
|
250
|
+
* const options = aiVendor.resolvePriority('media-alta-prioridad', { tools: AGENT_TOOLS });
|
|
251
|
+
* const result = await aiVendor.complete(messages, options);
|
|
252
|
+
*/
|
|
253
|
+
public resolvePriority(priority: TaskPriority, overrides: AICompletionOptions = {}): AICompletionOptions {
|
|
254
|
+
const vendor = (overrides.vendor ?? (getEnvString('AI_VENDOR') as AIVendor | undefined) ?? 'anthropic')
|
|
255
|
+
.toString()
|
|
256
|
+
.toLowerCase() as AIVendor;
|
|
257
|
+
|
|
258
|
+
const route = PRIORITY_ROUTING[priority];
|
|
259
|
+
if (!route) {
|
|
260
|
+
throw new TyrError(`Unknown task priority: '${priority}'`, null, `Use one of: ${PRIORITY_LADDER.join(', ')}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const tierModel = getEnvString(MODEL_TIER_ENV[route.tier]) ?? MODEL_TIERS[vendor][route.tier];
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
vendor,
|
|
267
|
+
model: tierModel,
|
|
268
|
+
thinking: route.thinking,
|
|
269
|
+
...overrides,
|
|
157
270
|
};
|
|
158
271
|
}
|
|
159
272
|
|
|
273
|
+
/**
|
|
274
|
+
* @method bumpPriority
|
|
275
|
+
* @description Escalates a priority one level up the ladder (baja-prioridad → ... →
|
|
276
|
+
* muy-alta-prioridad), capping at the top. Used for self-healing retries: each consecutive
|
|
277
|
+
* failure (e.g. a broken compile after an applied patch) buys the model more capability.
|
|
278
|
+
* @param {TaskPriority} priority - The current priority.
|
|
279
|
+
* @returns {TaskPriority} The next priority level, or the same value if already at the top.
|
|
280
|
+
* @example
|
|
281
|
+
* priority = aiVendor.bumpPriority(priority); // one more level of thinking/model tier
|
|
282
|
+
*/
|
|
283
|
+
public bumpPriority(priority: TaskPriority): TaskPriority {
|
|
284
|
+
const index = PRIORITY_LADDER.indexOf(priority);
|
|
285
|
+
if (index === -1 || index === PRIORITY_LADDER.length - 1) return priority;
|
|
286
|
+
return PRIORITY_LADDER[index + 1];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @method completeWithPriority
|
|
291
|
+
* @description Convenience wrapper around complete() that resolves model + thinking effort
|
|
292
|
+
* from a TaskPriority first. Equivalent to `complete(messages, resolvePriority(priority, options))`.
|
|
293
|
+
* @param {AIMessage[]} messages - Conversation messages.
|
|
294
|
+
* @param {TaskPriority} priority - Routing priority (see TaskPriority).
|
|
295
|
+
* @param {AICompletionOptions} options - Optional overrides merged on top of the routed defaults.
|
|
296
|
+
* @returns {Promise<AICompletionResult>}
|
|
297
|
+
* @example
|
|
298
|
+
* const result = await aiVendor.completeWithPriority(messages, 'alta-prioridad', { tools });
|
|
299
|
+
*/
|
|
300
|
+
public async completeWithPriority(
|
|
301
|
+
messages: AIMessage[],
|
|
302
|
+
priority: TaskPriority,
|
|
303
|
+
options: AICompletionOptions = {}
|
|
304
|
+
): Promise<AICompletionResult> {
|
|
305
|
+
return this.complete(messages, this.resolvePriority(priority, options));
|
|
306
|
+
}
|
|
307
|
+
|
|
160
308
|
private splitSystem(messages: AIMessage[]): { system: string; turns: AIMessage[] } {
|
|
161
309
|
const system = messages
|
|
162
310
|
.filter(m => m.role === 'system')
|
|
@@ -190,6 +338,7 @@ export class AIVendorManager {
|
|
|
190
338
|
if (typeof content === 'string') return content;
|
|
191
339
|
return content.map(block => {
|
|
192
340
|
if (block.type === 'text') return { type: 'text', text: block.text };
|
|
341
|
+
if (block.type === 'image') return { type: 'image', source: { type: 'base64', media_type: block.mediaType, data: block.data } };
|
|
193
342
|
if (block.type === 'tool_use') return { type: 'tool_use', id: block.id, name: block.name, input: block.input };
|
|
194
343
|
return { type: 'tool_result', tool_use_id: block.tool_use_id, content: block.content, ...(block.is_error ? { is_error: true } : {}) };
|
|
195
344
|
});
|
|
@@ -231,11 +380,19 @@ export class AIVendorManager {
|
|
|
231
380
|
|
|
232
381
|
const toolResults = m.content.filter(b => b.type === 'tool_result') as Array<Extract<AIContentBlock, { type: 'tool_result' }>>;
|
|
233
382
|
const text = m.content.filter(b => b.type === 'text').map(b => (b as any).text).join('');
|
|
383
|
+
const images = m.content.filter(b => b.type === 'image') as Array<Extract<AIContentBlock, { type: 'image' }>>;
|
|
234
384
|
|
|
235
385
|
for (const tr of toolResults) {
|
|
236
386
|
result.push({ role: 'tool', tool_call_id: tr.tool_use_id, content: tr.content });
|
|
237
387
|
}
|
|
238
|
-
if (
|
|
388
|
+
if (images.length > 0) {
|
|
389
|
+
const parts: any[] = [];
|
|
390
|
+
if (text) parts.push({ type: 'text', text });
|
|
391
|
+
for (const img of images) {
|
|
392
|
+
parts.push({ type: 'image_url', image_url: { url: `data:${img.mediaType};base64,${img.data}` } });
|
|
393
|
+
}
|
|
394
|
+
result.push({ role: m.role, content: parts });
|
|
395
|
+
} else if (text) {
|
|
239
396
|
result.push({ role: m.role, content: text });
|
|
240
397
|
}
|
|
241
398
|
}
|
|
@@ -283,6 +440,7 @@ export class AIVendorManager {
|
|
|
283
440
|
|
|
284
441
|
const toolResults = m.content.filter(b => b.type === 'tool_result') as Array<Extract<AIContentBlock, { type: 'tool_result' }>>;
|
|
285
442
|
const textBlocks = m.content.filter(b => b.type === 'text') as Array<Extract<AIContentBlock, { type: 'text' }>>;
|
|
443
|
+
const imageBlocks = m.content.filter(b => b.type === 'image') as Array<Extract<AIContentBlock, { type: 'image' }>>;
|
|
286
444
|
|
|
287
445
|
if (toolResults.length > 0) {
|
|
288
446
|
result.push({
|
|
@@ -295,20 +453,51 @@ export class AIVendorManager {
|
|
|
295
453
|
})),
|
|
296
454
|
});
|
|
297
455
|
}
|
|
298
|
-
if (textBlocks.length > 0) {
|
|
299
|
-
result.push({
|
|
456
|
+
if (textBlocks.length > 0 || imageBlocks.length > 0) {
|
|
457
|
+
result.push({
|
|
458
|
+
role: 'user',
|
|
459
|
+
parts: [
|
|
460
|
+
...textBlocks.map(b => ({ text: b.text })),
|
|
461
|
+
...imageBlocks.map(b => ({ inlineData: { mimeType: b.mediaType, data: b.data } })),
|
|
462
|
+
],
|
|
463
|
+
});
|
|
300
464
|
}
|
|
301
465
|
}
|
|
302
466
|
|
|
303
467
|
return result;
|
|
304
468
|
}
|
|
305
469
|
|
|
470
|
+
/** Anthropic's extended thinking requires temperature === 1 and max_tokens strictly greater
|
|
471
|
+
* than budget_tokens; both are enforced here so callers never have to remember it. */
|
|
472
|
+
private anthropicThinkingFields(config: VendorConfig): { thinking?: any; temperature: number; max_tokens: number } {
|
|
473
|
+
const budget = THINKING_BUDGETS[config.thinking];
|
|
474
|
+
if (!budget) return { temperature: config.temperature, max_tokens: config.maxTokens };
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
thinking: { type: 'enabled', budget_tokens: budget },
|
|
478
|
+
temperature: 1,
|
|
479
|
+
max_tokens: Math.max(config.maxTokens, budget + 1024),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** OpenAI's `reasoning_effort` only exists on the 'o' reasoning model family and only accepts
|
|
484
|
+
* low/medium/high (no 'none'/'max') — mapped from our vendor-agnostic ThinkingEffort. Those
|
|
485
|
+
* models also reject a custom `temperature`, so it's omitted whenever reasoning_effort is set. */
|
|
486
|
+
private openaiReasoningFields(config: VendorConfig): { reasoning_effort?: string; temperature?: number } {
|
|
487
|
+
const isReasoningModel = /^o\d/.test(config.model);
|
|
488
|
+
if (!isReasoningModel || config.thinking === 'none') return { temperature: config.temperature };
|
|
489
|
+
|
|
490
|
+
const effortMap: Partial<Record<ThinkingEffort, string>> = { low: 'low', medium: 'medium', high: 'high', max: 'high' };
|
|
491
|
+
return { reasoning_effort: effortMap[config.thinking] };
|
|
492
|
+
}
|
|
493
|
+
|
|
306
494
|
private buildRequest(config: VendorConfig, messages: AIMessage[], stream: boolean, tools?: AITool[]): VendorRequest {
|
|
307
495
|
const { system, turns } = this.splitSystem(messages);
|
|
308
496
|
const vendorTools = this.buildVendorTools(config, tools);
|
|
309
497
|
|
|
310
498
|
switch (config.vendor) {
|
|
311
|
-
case 'anthropic':
|
|
499
|
+
case 'anthropic': {
|
|
500
|
+
const thinkingFields = this.anthropicThinkingFields(config);
|
|
312
501
|
return {
|
|
313
502
|
url: 'https://api.anthropic.com/v1/messages',
|
|
314
503
|
headers: {
|
|
@@ -320,14 +509,15 @@ export class AIVendorManager {
|
|
|
320
509
|
model: config.model,
|
|
321
510
|
system,
|
|
322
511
|
messages: this.buildAnthropicMessages(turns),
|
|
323
|
-
temperature: config.temperature,
|
|
324
|
-
max_tokens: config.maxTokens,
|
|
325
512
|
stream,
|
|
513
|
+
...thinkingFields,
|
|
326
514
|
...(vendorTools ? { tools: vendorTools } : {}),
|
|
327
515
|
},
|
|
328
516
|
};
|
|
517
|
+
}
|
|
329
518
|
|
|
330
|
-
case 'openai':
|
|
519
|
+
case 'openai': {
|
|
520
|
+
const reasoningFields = this.openaiReasoningFields(config);
|
|
331
521
|
return {
|
|
332
522
|
url: 'https://api.openai.com/v1/chat/completions',
|
|
333
523
|
headers: {
|
|
@@ -340,13 +530,14 @@ export class AIVendorManager {
|
|
|
340
530
|
...(system ? [{ role: 'system', content: system }] : []),
|
|
341
531
|
...this.buildOpenAIMessages(turns),
|
|
342
532
|
],
|
|
343
|
-
temperature: config.temperature,
|
|
344
533
|
max_tokens: config.maxTokens,
|
|
345
534
|
stream,
|
|
535
|
+
...reasoningFields,
|
|
346
536
|
...(stream ? { stream_options: { include_usage: true } } : {}),
|
|
347
537
|
...(vendorTools ? { tools: vendorTools } : {}),
|
|
348
538
|
},
|
|
349
539
|
};
|
|
540
|
+
}
|
|
350
541
|
|
|
351
542
|
case 'gemini': {
|
|
352
543
|
const action = stream ? 'streamGenerateContent?alt=sse&' : 'generateContent?';
|