File size: 8,027 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/**
 * Background Task Detector β€” Feature 3
 *
 * Detects when CLI tools send "background" requests (title generation,
 * summarization, short descriptions) and provides model degradation
 * recommendations to save premium model quota.
 *
 * Detection heuristics:
 * - System prompt patterns indicating background/utility tasks
 * - Very short conversations with summary-like system prompts
 * - X-Request-Priority header
 */

// ── Configuration ───────────────────────────────────────────────────────────

interface DegradationConfig {
  enabled: boolean;
  degradationMap: Record<string, string>; // original β†’ cheaper model
  detectionPatterns: string[]; // regex patterns for system prompt matching
  stats: {
    detected: number;
    tokensSaved: number;
  };
}

const DEFAULT_DETECTION_PATTERNS = [
  "generate a title",
  "generate title",
  "create a title",
  "create a short",
  "summarize this",
  "summarize the",
  "write a brief",
  "write a summary",
  "one-line summary",
  "one line summary",
  "short description",
  "brief description",
  "conversation title",
  "chat title",
  "name this conversation",
  "name this chat",
  "title for this",
  "suggest a title",
  "label this",
];

const DEFAULT_DEGRADATION_MAP: Record<string, string> = {
  // Premium β†’ Cheap alternatives
  "claude-opus-4-6": "gemini-3-flash",
  "claude-opus-4-6-thinking": "gemini-3-flash",
  "claude-opus-4-5-20251101": "gemini-3-flash",
  "claude-sonnet-4-5-20250929": "gemini-3-flash",
  "claude-sonnet-4-20250514": "gemini-3-flash",
  "claude-sonnet-4": "gemini-3-flash",
  "gemini-3.1-pro": "gemini-3-flash",
  "gemini-3.1-pro-high": "gemini-3-flash",
  "gemini-3-pro-preview": "gemini-3-flash-preview",
  "gemini-2.5-pro": "gemini-3-flash",
  "gpt-4o": "gpt-4o-mini",
  "gpt-5": "gpt-5-mini",
  "gpt-5.1": "gpt-5-mini",
  "gpt-5.1-codex": "gpt-5.1-codex-mini",
};

// ── State ───────────────────────────────────────────────────────────────────

let _config: DegradationConfig = {
  enabled: false, // Disabled by default β€” user must opt in
  degradationMap: { ...DEFAULT_DEGRADATION_MAP },
  detectionPatterns: [...DEFAULT_DETECTION_PATTERNS],
  stats: { detected: 0, tokensSaved: 0 },
};

// ── Config Management ───────────────────────────────────────────────────────

/**
 * Set the background degradation config (called from settings API or startup).
 */
export function setBackgroundDegradationConfig(config: Partial<DegradationConfig>): void {
  _config = {
    ..._config,
    ...config,
    stats: _config.stats, // preserve stats across config changes
  };
}

/**
 * Get current background degradation config.
 */
export function getBackgroundDegradationConfig(): DegradationConfig {
  return {
    ..._config,
    degradationMap: { ..._config.degradationMap },
    detectionPatterns: [..._config.detectionPatterns],
    stats: { ..._config.stats },
  };
}

/**
 * Reset stats counters.
 */
export function resetStats(): void {
  _config.stats = { detected: 0, tokensSaved: 0 };
}

// ── Detection ───────────────────────────────────────────────────────────────

interface BackgroundMessage {
  role?: string;
  content?: unknown;
}

interface BackgroundTaskBody {
  messages?: BackgroundMessage[];
  input?: BackgroundMessage[];
  max_tokens?: unknown;
  max_completion_tokens?: unknown;
  max_output_tokens?: unknown;
}

function toMessageArray(value: unknown): BackgroundMessage[] {
  return Array.isArray(value) ? (value as BackgroundMessage[]) : [];
}

function toFiniteNumber(value: unknown): number | null {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string" && value.trim().length > 0) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}

function headerValue(headers: Record<string, string> | null, key: string): string {
  if (!headers) return "";
  const value = headers[key] ?? headers[key.toLowerCase()] ?? headers[key.toUpperCase()];
  return typeof value === "string" ? value.trim() : "";
}

/**
 * Get reason label when request is a background/utility task.
 *
 * @param {object} body - Request body
 * @param {object} [headers] - Request headers (optional)
 * @returns {string | null} Reason label or null when not detected
 */
export function getBackgroundTaskReason(
  body: BackgroundTaskBody | unknown,
  headers: Record<string, string> | null = null
): string | null {
  if (!body || typeof body !== "object") return null;
  const typedBody = body as BackgroundTaskBody;

  // 1. Check explicit header
  if (headers) {
    const taskType = headerValue(headers, "x-task-type");
    const priority = headerValue(headers, "x-request-priority");
    const initiator = headerValue(headers, "x-initiator");
    const explicitValue = [taskType, priority, initiator].find(Boolean);
    if (explicitValue && explicitValue.toLowerCase() === "background") {
      return "header_background";
    }
  }

  // 2. Very low max tokens usually indicates utility/background tasks
  const maxTokens = toFiniteNumber(
    typedBody.max_tokens ?? typedBody.max_completion_tokens ?? typedBody.max_output_tokens
  );
  if (maxTokens !== null && maxTokens > 0 && maxTokens < 50) {
    return "low_max_tokens";
  }

  // 3. Check system prompt for background task patterns
  const messages = toMessageArray(typedBody.messages ?? typedBody.input ?? []);
  if (!Array.isArray(messages) || messages.length === 0) return null;

  // Find system message
  const systemMsg = messages.find(
    (message: BackgroundMessage) => message.role === "system" || message.role === "developer"
  );
  if (!systemMsg) return null;

  const systemContent =
    typeof systemMsg.content === "string" ? systemMsg.content.toLowerCase() : "";

  if (!systemContent) return null;

  // Check against detection patterns
  const matched = _config.detectionPatterns.some((pattern) =>
    systemContent.includes(pattern.toLowerCase())
  );

  if (!matched) return null;

  // 4. Additional heuristic: background tasks typically have very few messages
  // (system + 1-2 user messages)
  const userMessages = messages.filter((message: BackgroundMessage) => message.role === "user");
  if (userMessages.length > 3) return null; // Too many turns for a background task

  return "system_prompt_pattern";
}

/**
 * Check if a request is a background/utility task.
 *
 * @param {object} body - Request body
 * @param {object} [headers] - Request headers (optional)
 * @returns {boolean} True if the request looks like a background task
 */
export function isBackgroundTask(
  body: BackgroundTaskBody | unknown,
  headers: Record<string, string> | null = null
): boolean {
  return getBackgroundTaskReason(body, headers) !== null;
}

/**
 * Get the degraded (cheaper) model for a given model.
 *
 * @param {string} originalModel - The original model ID
 * @returns {string} The cheaper model or original if no mapping exists
 */
export function getDegradedModel(originalModel: string): string {
  if (!originalModel) return originalModel;

  const degraded = _config.degradationMap[originalModel];
  if (degraded) {
    _config.stats.detected++;
    return degraded;
  }

  return originalModel;
}

/**
 * Get default degradation map (for UI reset).
 */
export function getDefaultDegradationMap(): Record<string, string> {
  return { ...DEFAULT_DEGRADATION_MAP };
}

/**
 * Get default detection patterns (for UI reset).
 */
export function getDefaultDetectionPatterns(): string[] {
  return [...DEFAULT_DETECTION_PATTERNS];
}