@orxataguy/tyr 1.0.34 → 1.0.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orxataguy/tyr",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tyr": "./bin/tyr.js"
@@ -43,9 +43,10 @@ export interface AITool {
43
43
 
44
44
  /**
45
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.
46
+ * vendor concreto. Se traduce a `thinking.budget_tokens` en Anthropic, a
47
+ * `generationConfig.thinkingConfig.thinkingBudget` en Gemini 2.5+ (mismo concepto de presupuesto
48
+ * de tokens que Anthropic) y a `reasoning_effort` en los modelos de OpenAI que lo soportan (serie
49
+ * 'o'), que solo acepta un vocabulario fijo (low/medium/high) en lugar de un número de tokens.
49
50
  */
50
51
  export type ThinkingEffort = 'none' | 'low' | 'medium' | 'high' | 'max';
51
52
 
@@ -144,8 +145,10 @@ const MODEL_TIER_DEFAULT: Record<string,string> = {
144
145
  gemini: 'gemini-2.5-flash-lite',
145
146
  };
146
147
 
147
- /** budget_tokens sent to Anthropic's `thinking` param per effort level. 'none' disables thinking. */
148
- const THINKING_BUDGETS: Record<ThinkingEffort, number> = {
148
+ /** budget_tokens sent to Anthropic's `thinking` param per effort level. 'none' disables thinking.
149
+ * Each is overridable via env var (see ANTHROPIC_THINKING_BUDGET_ENV); these are the fallback
150
+ * values used when the corresponding env var is not set. */
151
+ const ANTHROPIC_THINKING_BUDGETS: Record<ThinkingEffort, number> = {
149
152
  none: 0,
150
153
  low: 1024,
151
154
  medium: 4096,
@@ -153,20 +156,111 @@ const THINKING_BUDGETS: Record<ThinkingEffort, number> = {
153
156
  max: 16384,
154
157
  };
155
158
 
159
+ const ANTHROPIC_THINKING_BUDGET_ENV: Record<ThinkingEffort, string> = {
160
+ none: 'ANTHROPIC_THINKING_BUDGET_NONE',
161
+ low: 'ANTHROPIC_THINKING_BUDGET_LOW',
162
+ medium: 'ANTHROPIC_THINKING_BUDGET_MEDIUM',
163
+ high: 'ANTHROPIC_THINKING_BUDGET_HIGH',
164
+ max: 'ANTHROPIC_THINKING_BUDGET_MAX',
165
+ };
166
+
167
+ /** Gemini 2.5+'s `generationConfig.thinkingConfig.thinkingBudget` is the same token-budget concept
168
+ * as Anthropic's, so it reuses the same default magnitudes — overridable independently via its
169
+ * own env vars since Gemini's actual valid range differs by model (flash allows 0, pro doesn't). */
170
+ const GEMINI_THINKING_BUDGETS: Record<ThinkingEffort, number> = {
171
+ none: 0,
172
+ low: 1024,
173
+ medium: 4096,
174
+ high: 8192,
175
+ max: 16384,
176
+ };
177
+
178
+ const GEMINI_THINKING_BUDGET_ENV: Record<ThinkingEffort, string> = {
179
+ none: 'GEMINI_THINKING_BUDGET_NONE',
180
+ low: 'GEMINI_THINKING_BUDGET_LOW',
181
+ medium: 'GEMINI_THINKING_BUDGET_MEDIUM',
182
+ high: 'GEMINI_THINKING_BUDGET_HIGH',
183
+ max: 'GEMINI_THINKING_BUDGET_MAX',
184
+ };
185
+
186
+ function resolveThinkingBudget(
187
+ effort: ThinkingEffort,
188
+ defaults: Record<ThinkingEffort, number>,
189
+ env: Record<ThinkingEffort, string>
190
+ ): number {
191
+ return getEnvInt(env[effort], defaults[effort]);
192
+ }
193
+
194
+ /** OpenAI's reasoning models (serie 'o', gpt-5*) solo aceptan un vocabulario fijo para
195
+ * `reasoning_effort` — no un número de tokens — así que aquí se mapea ThinkingEffort a esas
196
+ * cadenas en lugar de a un budget. 'none' está ausente a propósito: esos modelos no permiten
197
+ * desactivar el razonamiento, así que ThinkingEffort 'none' simplemente omite el parámetro
198
+ * (ver openaiReasoningFields). Cada nivel es overridable vía env var por si OpenAI cambia el
199
+ * vocabulario aceptado (p.ej. añade 'minimal') sin tener que tocar código. */
200
+ const OPENAI_REASONING_EFFORTS: Partial<Record<ThinkingEffort, string>> = {
201
+ low: 'low',
202
+ medium: 'medium',
203
+ high: 'high',
204
+ max: 'high',
205
+ };
206
+
207
+ const OPENAI_REASONING_EFFORT_ENV: Partial<Record<ThinkingEffort, string>> = {
208
+ low: 'OPENAI_REASONING_EFFORT_LOW',
209
+ medium: 'OPENAI_REASONING_EFFORT_MEDIUM',
210
+ high: 'OPENAI_REASONING_EFFORT_HIGH',
211
+ max: 'OPENAI_REASONING_EFFORT_MAX',
212
+ };
213
+
214
+ function resolveOpenAIReasoningEffort(effort: ThinkingEffort): string | undefined {
215
+ const envVar = OPENAI_REASONING_EFFORT_ENV[effort];
216
+ const override = envVar ? getEnvString(envVar) : undefined;
217
+ return override ?? OPENAI_REASONING_EFFORTS[effort];
218
+ }
219
+
156
220
  /**
157
221
  * Task-priority → (model tier, thinking effort) matrix. This is the routing table requested by
158
222
  * the architecture spec: callers pick a priority by *intent* ("this is a trivial file", "this
159
223
  * previous attempt failed"), not by model name, and AIVendorManager decides what that costs.
224
+ * Each entry is overridable via env var (see PRIORITY_ROUTING_ENV); these are the fallback values
225
+ * used when the corresponding env var is unset or holds an invalid tier/thinking value.
160
226
  */
161
227
  const PRIORITY_ROUTING: Record<TaskPriority, { tier: ModelTier; thinking: ThinkingEffort }> = {
162
228
  'baja-prioridad': { tier: 'cheap', thinking: 'none' },
163
- 'baja-media-prioridad': { tier: 'cheap', thinking: 'max' },
229
+ 'baja-media-prioridad': { tier: 'cheap', thinking: 'high' },
164
230
  'media-prioridad': { tier: 'balanced', thinking: 'none' },
165
231
  'media-alta-prioridad': { tier: 'balanced', thinking: 'high' },
166
232
  'alta-prioridad': { tier: 'balanced', thinking: 'max' },
167
233
  'muy-alta-prioridad': { tier: 'flagship', thinking: 'max' },
168
234
  };
169
235
 
236
+ const PRIORITY_ROUTING_ENV: Record<TaskPriority, { tier: string; thinking: string }> = {
237
+ 'baja-prioridad': { tier: 'AI_PRIORITY_BAJA_TIER', thinking: 'AI_PRIORITY_BAJA_THINKING' },
238
+ 'baja-media-prioridad': { tier: 'AI_PRIORITY_BAJA_MEDIA_TIER', thinking: 'AI_PRIORITY_BAJA_MEDIA_THINKING' },
239
+ 'media-prioridad': { tier: 'AI_PRIORITY_MEDIA_TIER', thinking: 'AI_PRIORITY_MEDIA_THINKING' },
240
+ 'media-alta-prioridad': { tier: 'AI_PRIORITY_MEDIA_ALTA_TIER', thinking: 'AI_PRIORITY_MEDIA_ALTA_THINKING' },
241
+ 'alta-prioridad': { tier: 'AI_PRIORITY_ALTA_TIER', thinking: 'AI_PRIORITY_ALTA_THINKING' },
242
+ 'muy-alta-prioridad': { tier: 'AI_PRIORITY_MUY_ALTA_TIER', thinking: 'AI_PRIORITY_MUY_ALTA_THINKING' },
243
+ };
244
+
245
+ const VALID_MODEL_TIERS: ModelTier[] = ['cheap', 'balanced', 'flagship'];
246
+ const VALID_THINKING_EFFORTS: ThinkingEffort[] = ['none', 'low', 'medium', 'high', 'max'];
247
+
248
+ /** Resolves the (tier, thinking) route for a priority, letting env vars override either half of
249
+ * PRIORITY_ROUTING independently. An env var holding anything other than a valid tier/thinking
250
+ * value is ignored in favor of the hardcoded default, rather than passed through to the vendor. */
251
+ function resolvePriorityRoute(priority: TaskPriority): { tier: ModelTier; thinking: ThinkingEffort } {
252
+ const defaults = PRIORITY_ROUTING[priority];
253
+ const envVars = PRIORITY_ROUTING_ENV[priority];
254
+
255
+ const tierOverride = getEnvString(envVars.tier)?.toLowerCase() as ModelTier | undefined;
256
+ const thinkingOverride = getEnvString(envVars.thinking)?.toLowerCase() as ThinkingEffort | undefined;
257
+
258
+ return {
259
+ tier: tierOverride && VALID_MODEL_TIERS.includes(tierOverride) ? tierOverride : defaults.tier,
260
+ thinking: thinkingOverride && VALID_THINKING_EFFORTS.includes(thinkingOverride) ? thinkingOverride : defaults.thinking,
261
+ };
262
+ }
263
+
170
264
  /** Ordered priority ladder, used by bumpPriority() to escalate one level at a time. */
171
265
  const PRIORITY_LADDER: TaskPriority[] = [
172
266
  'baja-prioridad',
@@ -257,10 +351,10 @@ export class AIVendorManager {
257
351
  .toString()
258
352
  .toLowerCase() as AIVendor;
259
353
 
260
- const route = PRIORITY_ROUTING[priority];
261
- if (!route) {
354
+ if (!PRIORITY_ROUTING[priority]) {
262
355
  throw new TyrError(`Unknown task priority: '${priority}'`, null, `Use one of: ${PRIORITY_LADDER.join(', ')}`);
263
356
  }
357
+ const route = resolvePriorityRoute(priority);
264
358
 
265
359
  const tierModel = getEnvString(MODEL_TIER_ENV[vendor][route.tier], MODEL_TIER_DEFAULT[vendor]);
266
360
 
@@ -472,7 +566,7 @@ export class AIVendorManager {
472
566
  /** Anthropic's extended thinking requires temperature === 1 and max_tokens strictly greater
473
567
  * than budget_tokens; both are enforced here so callers never have to remember it. */
474
568
  private anthropicThinkingFields(config: VendorConfig): { thinking?: any; temperature: number; max_tokens: number } {
475
- const budget = THINKING_BUDGETS[config.thinking];
569
+ const budget = resolveThinkingBudget(config.thinking, ANTHROPIC_THINKING_BUDGETS, ANTHROPIC_THINKING_BUDGET_ENV);
476
570
  if (!budget) return { temperature: config.temperature, max_tokens: config.maxTokens };
477
571
 
478
572
  return {
@@ -483,14 +577,23 @@ export class AIVendorManager {
483
577
  }
484
578
 
485
579
  /** OpenAI's `reasoning_effort` only exists on the 'o' reasoning model family and only accepts
486
- * low/medium/high (no 'none'/'max') — mapped from our vendor-agnostic ThinkingEffort. Those
487
- * models also reject a custom `temperature`, so it's omitted whenever reasoning_effort is set. */
580
+ * a fixed vocabulary (no 'none') — mapped from our vendor-agnostic ThinkingEffort via
581
+ * OPENAI_REASONING_EFFORTS/_ENV. Those models also reject a custom `temperature`, so it's
582
+ * omitted whenever reasoning_effort is set. */
488
583
  private openaiReasoningFields(config: VendorConfig): { reasoning_effort?: string; temperature?: number } {
489
584
  const isReasoningModel = /^o\d/.test(config.model);
490
585
  if (!isReasoningModel || config.thinking === 'none') return { temperature: config.temperature };
491
586
 
492
- const effortMap: Partial<Record<ThinkingEffort, string>> = { low: 'low', medium: 'medium', high: 'high', max: 'high' };
493
- return { reasoning_effort: effortMap[config.thinking] };
587
+ return { reasoning_effort: resolveOpenAIReasoningEffort(config.thinking) };
588
+ }
589
+
590
+ /** Gemini 2.5+'s `thinkingConfig.thinkingBudget` is omitted entirely for 'none' rather than
591
+ * sent as 0, because 0 is invalid on Pro models (which can't fully disable thinking) — only
592
+ * Flash accepts it. Omitting the field is the safe "off" behavior across both. */
593
+ private geminiThinkingFields(config: VendorConfig): { thinkingConfig?: { thinkingBudget: number } } {
594
+ if (config.thinking === 'none') return {};
595
+ const budget = resolveThinkingBudget(config.thinking, GEMINI_THINKING_BUDGETS, GEMINI_THINKING_BUDGET_ENV);
596
+ return { thinkingConfig: { thinkingBudget: budget } };
494
597
  }
495
598
 
496
599
  private buildRequest(config: VendorConfig, messages: AIMessage[], stream: boolean, tools?: AITool[]): VendorRequest {
@@ -543,6 +646,7 @@ export class AIVendorManager {
543
646
 
544
647
  case 'gemini': {
545
648
  const action = stream ? 'streamGenerateContent?alt=sse&' : 'generateContent?';
649
+ const thinkingFields = this.geminiThinkingFields(config);
546
650
  return {
547
651
  url: `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:${action}key=${config.apiKey}`,
548
652
  headers: { 'content-type': 'application/json' },
@@ -552,6 +656,7 @@ export class AIVendorManager {
552
656
  generationConfig: {
553
657
  temperature: config.temperature,
554
658
  maxOutputTokens: config.maxTokens,
659
+ ...thinkingFields,
555
660
  },
556
661
  ...(vendorTools ? { tools: vendorTools } : {}),
557
662
  },