ai-consultants 2.10.1

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.
Files changed (62) hide show
  1. package/.claude/commands/ai-consultants:consult.md +94 -0
  2. package/.claude/commands/ai-consultants:debate.md +103 -0
  3. package/.claude/commands/ai-consultants:help.md +30 -0
  4. package/CLAUDE.md +839 -0
  5. package/LICENSE +21 -0
  6. package/README.md +758 -0
  7. package/SKILL.md +156 -0
  8. package/bin/ai-consultants +96 -0
  9. package/docs/COST_RATES.md +172 -0
  10. package/docs/JSON_SCHEMA.md +305 -0
  11. package/docs/SETUP.md +879 -0
  12. package/docs/SMART_ROUTING.md +219 -0
  13. package/docs/cost_rates.json +143 -0
  14. package/package.json +47 -0
  15. package/references/configuration.md +141 -0
  16. package/references/details.md +60 -0
  17. package/scripts/build_context.sh +562 -0
  18. package/scripts/classify_question.sh +148 -0
  19. package/scripts/config.sh +753 -0
  20. package/scripts/configure.sh +1093 -0
  21. package/scripts/consult_all.sh +934 -0
  22. package/scripts/debate_round.sh +421 -0
  23. package/scripts/doctor.sh +642 -0
  24. package/scripts/followup.sh +197 -0
  25. package/scripts/install.sh +319 -0
  26. package/scripts/lib/api.sh +640 -0
  27. package/scripts/lib/api_query.sh +228 -0
  28. package/scripts/lib/cache.sh +338 -0
  29. package/scripts/lib/chunking.sh +880 -0
  30. package/scripts/lib/code_optimizer.sh +671 -0
  31. package/scripts/lib/common.sh +1088 -0
  32. package/scripts/lib/costs.sh +674 -0
  33. package/scripts/lib/personas.sh +372 -0
  34. package/scripts/lib/progress.sh +251 -0
  35. package/scripts/lib/reflection.sh +447 -0
  36. package/scripts/lib/routing.sh +566 -0
  37. package/scripts/lib/schema.json +165 -0
  38. package/scripts/lib/session.sh +327 -0
  39. package/scripts/lib/symbol_map.sh +445 -0
  40. package/scripts/lib/voting.sh +495 -0
  41. package/scripts/peer_review.sh +460 -0
  42. package/scripts/preflight_check.sh +452 -0
  43. package/scripts/query_aider.sh +83 -0
  44. package/scripts/query_amp.sh +67 -0
  45. package/scripts/query_claude.sh +99 -0
  46. package/scripts/query_codex.sh +92 -0
  47. package/scripts/query_cursor.sh +72 -0
  48. package/scripts/query_deepseek.sh +17 -0
  49. package/scripts/query_gemini.sh +85 -0
  50. package/scripts/query_glm.sh +17 -0
  51. package/scripts/query_grok.sh +17 -0
  52. package/scripts/query_kilo.sh +140 -0
  53. package/scripts/query_kimi.sh +67 -0
  54. package/scripts/query_minimax.sh +17 -0
  55. package/scripts/query_mistral.sh +97 -0
  56. package/scripts/query_ollama.sh +328 -0
  57. package/scripts/query_qwen3.sh +93 -0
  58. package/scripts/setup_wizard.sh +367 -0
  59. package/scripts/skillport-install.sh +196 -0
  60. package/scripts/synthesize.sh +332 -0
  61. package/templates/consultation_report.md +323 -0
  62. package/templates/synthesis_prompt.md +120 -0
@@ -0,0 +1,674 @@
1
+ #!/bin/bash
2
+ # costs.sh - Cost tracking and budget management for AI Consultants
3
+ #
4
+ # Tracks consultation costs based on estimated token usage
5
+ # and manages budget limits.
6
+
7
+ # =============================================================================
8
+ # EXTERNAL RATES FILE (v2.4)
9
+ # =============================================================================
10
+ # Path to external rates file for easy updates
11
+
12
+ # Get the script directory for relative path resolution
13
+ _COSTS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ COST_RATES_FILE="${COST_RATES_FILE:-$_COSTS_SCRIPT_DIR/../../docs/cost_rates.json}"
15
+
16
+ # Load rate from JSON file
17
+ # Usage: get_rate_from_file <model> <type: input|output>
18
+ # Returns: rate as string, or exits with 1 if not found
19
+ get_rate_from_file() {
20
+ local model="$1"
21
+ local type="$2"
22
+
23
+ if [[ -f "$COST_RATES_FILE" ]]; then
24
+ local rate
25
+ rate=$(jq -r ".models[\"$model\"].$type // null" "$COST_RATES_FILE" 2>/dev/null)
26
+ if [[ "$rate" != "null" && -n "$rate" ]]; then
27
+ echo "$rate"
28
+ return 0
29
+ fi
30
+ fi
31
+ return 1
32
+ }
33
+
34
+ # Get fallback model for a consultant
35
+ # Usage: get_consultant_fallback_model <consultant>
36
+ # Returns: fallback model name (empty string if not found)
37
+ get_consultant_fallback_model() {
38
+ local consultant="$1"
39
+ consultant=$(echo "$consultant" | tr '[:upper:]' '[:lower:]')
40
+
41
+ if [[ -f "$COST_RATES_FILE" ]]; then
42
+ local fallback
43
+ fallback=$(jq -r ".consultant_fallbacks[\"$consultant\"] // null" "$COST_RATES_FILE" 2>/dev/null)
44
+ if [[ "$fallback" != "null" && -n "$fallback" ]]; then
45
+ echo "$fallback"
46
+ fi
47
+ fi
48
+ }
49
+
50
+ # Resolve model name: use reported model if known, else fallback
51
+ # Usage: resolve_model_for_cost <reported_model> <consultant>
52
+ # Returns: resolved model name for cost calculation
53
+ resolve_model_for_cost() {
54
+ local model="$1"
55
+ local consultant="$2"
56
+
57
+ # Fetch fallback once for reuse
58
+ local fallback
59
+ fallback=$(get_consultant_fallback_model "$consultant")
60
+
61
+ # If model is "default", empty, or unknown, use fallback
62
+ if [[ -z "$model" || "$model" == "default" ]]; then
63
+ if [[ -n "$fallback" ]]; then
64
+ type log_debug &>/dev/null && log_debug "Using fallback model '$fallback' for $consultant (reported: '$model')"
65
+ echo "$fallback"
66
+ return 0
67
+ fi
68
+ fi
69
+
70
+ # Check if model exists in rates file
71
+ if get_rate_from_file "$model" "input" >/dev/null 2>&1; then
72
+ echo "$model"
73
+ return 0
74
+ fi
75
+
76
+ # Model not in rates file, try fallback
77
+ if [[ -n "$fallback" ]]; then
78
+ type log_debug &>/dev/null && log_debug "Model '$model' not in rates, using fallback '$fallback' for $consultant"
79
+ echo "$fallback"
80
+ return 0
81
+ fi
82
+
83
+ # Return original (will use default rate)
84
+ echo "$model"
85
+ }
86
+
87
+ # Get default rate from JSON file
88
+ # Usage: get_default_rate <type: input|output>
89
+ get_default_rate() {
90
+ local type="$1"
91
+ if [[ -f "$COST_RATES_FILE" ]]; then
92
+ local rate
93
+ rate=$(jq -r ".default_rate.$type // null" "$COST_RATES_FILE" 2>/dev/null)
94
+ if [[ "$rate" != "null" && -n "$rate" ]]; then
95
+ echo "$rate"
96
+ return 0
97
+ fi
98
+ fi
99
+ # Hardcoded fallback
100
+ case "$type" in
101
+ input) echo "0.005" ;;
102
+ output) echo "0.015" ;;
103
+ esac
104
+ }
105
+
106
+ # =============================================================================
107
+ # COST RATES (USD per 1K tokens)
108
+ # =============================================================================
109
+ # Using case statements for bash 3.2 compatibility (no associative arrays)
110
+ # External JSON file is tried first, then fallback to hardcoded rates
111
+
112
+ # Get input token cost per 1K tokens
113
+ # Usage: get_input_cost_per_1k <model>
114
+ get_input_cost_per_1k() {
115
+ local model="$1"
116
+ model=$(echo "$model" | tr '[:upper:]' '[:lower:]')
117
+
118
+ # Try external file first
119
+ local rate
120
+ if rate=$(get_rate_from_file "$model" "input" 2>/dev/null); then
121
+ echo "$rate"
122
+ return
123
+ fi
124
+
125
+ # Fallback to hardcoded rates for backwards compatibility
126
+ case "$model" in
127
+ gemini-2.5-pro) echo "0.00125" ;;
128
+ gemini-2.5-flash) echo "0.000075" ;;
129
+ gemini-2.0-flash) echo "0.0001" ;;
130
+ gpt-4) echo "0.03" ;;
131
+ gpt-4-turbo) echo "0.01" ;;
132
+ gpt-4o) echo "0.005" ;;
133
+ gpt-4o-mini) echo "0.00015" ;;
134
+ o1) echo "0.015" ;;
135
+ o3) echo "0.015" ;;
136
+ claude-3-opus) echo "0.015" ;;
137
+ claude-3-sonnet) echo "0.003" ;;
138
+ claude-3-haiku) echo "0.00025" ;;
139
+ mistral-large) echo "0.004" ;;
140
+ mistral-medium) echo "0.0027" ;;
141
+ mistral-small) echo "0.001" ;;
142
+ kilo) echo "0.002" ;;
143
+ cursor) echo "0.005" ;;
144
+ # Qwen3 models (Alibaba DashScope)
145
+ qwen-max) echo "0.004" ;;
146
+ qwen-plus) echo "0.002" ;;
147
+ qwen-turbo) echo "0.0008" ;;
148
+ # GLM models (Zhipu AI)
149
+ glm-4) echo "0.003" ;;
150
+ glm-3-turbo) echo "0.001" ;;
151
+ # Grok models (xAI)
152
+ grok-4-1-fast-reasoning) echo "0.005" ;;
153
+ grok-2) echo "0.01" ;;
154
+ # Default
155
+ *) echo "0.005" ;;
156
+ esac
157
+ }
158
+
159
+ # Get output token cost per 1K tokens
160
+ # Usage: get_output_cost_per_1k <model>
161
+ get_output_cost_per_1k() {
162
+ local model="$1"
163
+ model=$(echo "$model" | tr '[:upper:]' '[:lower:]')
164
+
165
+ # Try external file first
166
+ local rate
167
+ if rate=$(get_rate_from_file "$model" "output" 2>/dev/null); then
168
+ echo "$rate"
169
+ return
170
+ fi
171
+
172
+ # Fallback to hardcoded rates for backwards compatibility
173
+ case "$model" in
174
+ gemini-2.5-pro) echo "0.005" ;;
175
+ gemini-2.5-flash) echo "0.0003" ;;
176
+ gemini-2.0-flash) echo "0.0004" ;;
177
+ gpt-4) echo "0.06" ;;
178
+ gpt-4-turbo) echo "0.03" ;;
179
+ gpt-4o) echo "0.015" ;;
180
+ gpt-4o-mini) echo "0.0006" ;;
181
+ o1) echo "0.06" ;;
182
+ o3) echo "0.06" ;;
183
+ claude-3-opus) echo "0.075" ;;
184
+ claude-3-sonnet) echo "0.015" ;;
185
+ claude-3-haiku) echo "0.00125" ;;
186
+ mistral-large) echo "0.012" ;;
187
+ mistral-medium) echo "0.0081" ;;
188
+ mistral-small) echo "0.003" ;;
189
+ kilo) echo "0.006" ;;
190
+ cursor) echo "0.015" ;;
191
+ # Qwen3 models (Alibaba DashScope)
192
+ qwen-max) echo "0.012" ;;
193
+ qwen-plus) echo "0.006" ;;
194
+ qwen-turbo) echo "0.002" ;;
195
+ # GLM models (Zhipu AI)
196
+ glm-4) echo "0.009" ;;
197
+ glm-3-turbo) echo "0.003" ;;
198
+ # Grok models (xAI)
199
+ grok-4-1-fast-reasoning) echo "0.015" ;;
200
+ grok-2) echo "0.03" ;;
201
+ # Default
202
+ *) echo "0.015" ;;
203
+ esac
204
+ }
205
+
206
+ # =============================================================================
207
+ # COST CALCULATION
208
+ # =============================================================================
209
+
210
+ # Estimate cost for a query
211
+ # Usage: estimate_query_cost <model> <input_tokens> <output_tokens>
212
+ estimate_query_cost() {
213
+ local model="$1"
214
+ local input_tokens="${2:-1000}"
215
+ local output_tokens="${3:-500}"
216
+
217
+ # Normalize model name
218
+ model=$(echo "$model" | tr '[:upper:]' '[:lower:]')
219
+
220
+ # Get rates using lookup functions
221
+ local input_rate output_rate
222
+ input_rate=$(get_input_cost_per_1k "$model")
223
+ output_rate=$(get_output_cost_per_1k "$model")
224
+
225
+ # Calculate cost
226
+ local input_cost output_cost total_cost
227
+ input_cost=$(echo "scale=6; $input_tokens / 1000 * $input_rate" | bc)
228
+ output_cost=$(echo "scale=6; $output_tokens / 1000 * $output_rate" | bc)
229
+ total_cost=$(echo "scale=6; $input_cost + $output_cost" | bc)
230
+
231
+ echo "$total_cost"
232
+ }
233
+
234
+ # Calculate total session cost from responses
235
+ # Usage: calculate_session_cost <responses_dir>
236
+ calculate_session_cost() {
237
+ local responses_dir="$1"
238
+ local total_cost=0
239
+
240
+ for f in "$responses_dir"/*.json; do
241
+ if [[ -f "$f" && -s "$f" ]]; then
242
+ local model=$(jq -r '.model // "default"' "$f" 2>/dev/null)
243
+ local tokens=$(jq -r '.metadata.tokens_used // 1000' "$f" 2>/dev/null)
244
+
245
+ # Assume 60% input, 40% output
246
+ local input_tokens=$((tokens * 60 / 100))
247
+ local output_tokens=$((tokens * 40 / 100))
248
+
249
+ local cost=$(estimate_query_cost "$model" "$input_tokens" "$output_tokens")
250
+ total_cost=$(echo "scale=6; $total_cost + $cost" | bc)
251
+ fi
252
+ done
253
+
254
+ echo "$total_cost"
255
+ }
256
+
257
+ # Format cost for display
258
+ # Usage: format_cost <cost_usd>
259
+ format_cost() {
260
+ local cost="$1"
261
+
262
+ # Convert to cents if very small
263
+ if (( $(echo "$cost < 0.01" | bc -l) )); then
264
+ local cents=$(echo "scale=2; $cost * 100" | bc)
265
+ echo "${cents}¢"
266
+ else
267
+ printf "\$%.4f" "$cost"
268
+ fi
269
+ }
270
+
271
+ # =============================================================================
272
+ # BUDGET MANAGEMENT
273
+ # =============================================================================
274
+
275
+ # Check if cost exceeds budget
276
+ # Usage: check_budget <cost> <budget>
277
+ check_budget() {
278
+ local cost="$1"
279
+ local budget="${2:-${MAX_SESSION_COST:-1.00}}"
280
+
281
+ if (( $(echo "$cost > $budget" | bc -l) )); then
282
+ return 1 # Over budget
283
+ fi
284
+ return 0 # Within budget
285
+ }
286
+
287
+ # Check if we are close to warning threshold
288
+ # Usage: check_warning_threshold <cost>
289
+ check_warning_threshold() {
290
+ local cost="$1"
291
+ local threshold="${WARN_AT_COST:-0.50}"
292
+
293
+ if (( $(echo "$cost > $threshold" | bc -l) )); then
294
+ return 0 # Should warn
295
+ fi
296
+ return 1 # No warning needed
297
+ }
298
+
299
+ # Estimate pre-consultation cost (before executing)
300
+ # Usage: estimate_consultation_cost <num_consultants> <context_size_chars> [consultants_csv]
301
+ # The consultants_csv parameter is a comma-separated list of consultant names (e.g., "Gemini,Codex,Mistral")
302
+ estimate_consultation_cost() {
303
+ local num_consultants="${1:-5}"
304
+ local context_size="${2:-5000}"
305
+ local consultants="${3:-}"
306
+
307
+ # Estimate tokens from context (approximately 4 chars per token)
308
+ local estimated_input_tokens=$((context_size / 4))
309
+
310
+ # Estimate output tokens (approximately 500-1000 per response)
311
+ local estimated_output_tokens=750
312
+
313
+ local total=0
314
+
315
+ if [[ -n "$consultants" ]]; then
316
+ # Use provided consultant list with fallback models
317
+ local IFS=','
318
+ read -ra consultant_list <<< "$consultants"
319
+ for consultant in "${consultant_list[@]}"; do
320
+ local fallback_model
321
+ fallback_model=$(get_consultant_fallback_model "$consultant")
322
+ local model_to_use="${fallback_model:-default}"
323
+ local cost=$(estimate_query_cost "$model_to_use" "$estimated_input_tokens" "$estimated_output_tokens")
324
+ total=$(echo "scale=6; $total + $cost" | bc)
325
+ done
326
+ else
327
+ # Fallback to generic estimate using default rate
328
+ for ((i=0; i<num_consultants; i++)); do
329
+ local default_input default_output
330
+ default_input=$(get_default_rate "input")
331
+ default_output=$(get_default_rate "output")
332
+ local input_cost output_cost cost
333
+ input_cost=$(echo "scale=6; $estimated_input_tokens / 1000 * $default_input" | bc)
334
+ output_cost=$(echo "scale=6; $estimated_output_tokens / 1000 * $default_output" | bc)
335
+ cost=$(echo "scale=6; $input_cost + $output_cost" | bc)
336
+ total=$(echo "scale=6; $total + $cost" | bc)
337
+ done
338
+ fi
339
+
340
+ echo "$total"
341
+ }
342
+
343
+ # =============================================================================
344
+ # COST TRACKING
345
+ # =============================================================================
346
+
347
+ # File for cumulative tracking
348
+ COST_TRACKING_FILE="${COST_TRACKING_FILE:-/tmp/ai_consultants_costs.json}"
349
+
350
+ # Record session cost
351
+ # Usage: track_session_cost <session_id> <cost>
352
+ track_session_cost() {
353
+ local session_id="$1"
354
+ local cost="$2"
355
+ local timestamp=$(date -Iseconds)
356
+
357
+ # Create file if it doesn't exist
358
+ if [[ ! -f "$COST_TRACKING_FILE" ]]; then
359
+ echo '{"sessions": [], "total_cost": 0}' > "$COST_TRACKING_FILE"
360
+ fi
361
+
362
+ # Add session and update total
363
+ jq --arg id "$session_id" \
364
+ --arg cost "$cost" \
365
+ --arg ts "$timestamp" \
366
+ '.sessions += [{id: $id, cost: ($cost | tonumber), timestamp: $ts}] | .total_cost += ($cost | tonumber)' \
367
+ "$COST_TRACKING_FILE" > "${COST_TRACKING_FILE}.tmp" && \
368
+ mv "${COST_TRACKING_FILE}.tmp" "$COST_TRACKING_FILE"
369
+ }
370
+
371
+ # Get total tracked cost
372
+ # Usage: get_total_tracked_cost
373
+ get_total_tracked_cost() {
374
+ if [[ -f "$COST_TRACKING_FILE" ]]; then
375
+ jq -r '.total_cost // 0' "$COST_TRACKING_FILE"
376
+ else
377
+ echo 0
378
+ fi
379
+ }
380
+
381
+ # Generate cost report
382
+ # Usage: generate_cost_report
383
+ generate_cost_report() {
384
+ if [[ ! -f "$COST_TRACKING_FILE" ]]; then
385
+ echo "No cost data available"
386
+ return
387
+ fi
388
+
389
+ local total=$(jq -r '.total_cost // 0' "$COST_TRACKING_FILE")
390
+ local sessions=$(jq -r '.sessions | length' "$COST_TRACKING_FILE")
391
+ local avg=0
392
+ if [[ $sessions -gt 0 ]]; then
393
+ avg=$(echo "scale=6; $total / $sessions" | bc)
394
+ fi
395
+
396
+ echo "╔══════════════════════════════════════════════════════════════╗"
397
+ echo "║ Cost Report ║"
398
+ echo "╚══════════════════════════════════════════════════════════════╝"
399
+ echo ""
400
+ echo " Total sessions: $sessions"
401
+ echo " Total cost: $(format_cost $total)"
402
+ echo " Average per session: $(format_cost $avg)"
403
+ echo ""
404
+
405
+ # Last 5 sessions
406
+ echo " Recent sessions:"
407
+ jq -r '.sessions | .[-5:] | .[] | " \(.timestamp): \(.id) - $\(.cost)"' "$COST_TRACKING_FILE" 2>/dev/null || echo " No sessions"
408
+ }
409
+
410
+ # =============================================================================
411
+ # UTILITY
412
+ # =============================================================================
413
+
414
+ # Convert tokens to estimated cost (quick helper)
415
+ # Usage: tokens_to_cost <tokens> [model]
416
+ tokens_to_cost() {
417
+ local tokens="$1"
418
+ local model="${2:-default}"
419
+
420
+ # Assume 60/40 split
421
+ local input=$((tokens * 60 / 100))
422
+ local output=$((tokens * 40 / 100))
423
+
424
+ estimate_query_cost "$model" "$input" "$output"
425
+ }
426
+
427
+ # =============================================================================
428
+ # RESPONSE LENGTH LIMITS (v2.3)
429
+ # =============================================================================
430
+
431
+ # Get max response tokens for a category
432
+ # Usage: get_max_response_tokens <category>
433
+ get_max_response_tokens() {
434
+ local category="$1"
435
+ local limits="${MAX_RESPONSE_TOKENS_BY_CATEGORY:-QUICK_SYNTAX:200,CODE_REVIEW:800,BUG_DEBUG:800,ARCHITECTURE:1000,SECURITY:1000,DATABASE:600,GENERAL:500}"
436
+
437
+ # Search for category in the limits string
438
+ local limit
439
+ limit=$(echo "$limits" | tr ',' '\n' | grep -i "^${category}:" | cut -d: -f2 | head -1)
440
+
441
+ # Return limit or default
442
+ if [[ -n "$limit" && "$limit" =~ ^[0-9]+$ ]]; then
443
+ echo "$limit"
444
+ else
445
+ echo "500" # Default
446
+ fi
447
+ }
448
+
449
+ # Check if response limits are enabled
450
+ # Usage: is_response_limits_enabled
451
+ # NOTE: Default is false (opt-in) per quality review - aligns with config.sh
452
+ is_response_limits_enabled() {
453
+ [[ "${ENABLE_RESPONSE_LIMITS:-false}" == "true" ]]
454
+ }
455
+
456
+ # Get model tier (economy, standard, premium)
457
+ # Usage: get_model_tier <model>
458
+ get_model_tier() {
459
+ local model="$1"
460
+ model=$(echo "$model" | tr '[:upper:]' '[:lower:]')
461
+
462
+ case "$model" in
463
+ # Economy tier - cheapest models
464
+ gemini-2.5-flash|gemini-2.0-flash|gpt-4o-mini|claude-3-haiku|mistral-small|qwen-turbo|glm-3-turbo)
465
+ echo "economy"
466
+ ;;
467
+ # Premium tier - most expensive models
468
+ gpt-4|gpt-4-turbo|o1|o3|claude-3-opus|mistral-large|grok-2|qwen-max)
469
+ echo "premium"
470
+ ;;
471
+ # Standard tier - default/mid-range
472
+ *)
473
+ echo "standard"
474
+ ;;
475
+ esac
476
+ }
477
+
478
+ # Get economic model for a consultant
479
+ # Usage: get_economic_model <consultant>
480
+ get_economic_model() {
481
+ local consultant="$1"
482
+ consultant=$(echo "$consultant" | tr '[:upper:]' '[:lower:]')
483
+
484
+ case "$consultant" in
485
+ gemini) echo "gemini-2.5-flash" ;;
486
+ codex) echo "gpt-4o-mini" ;;
487
+ mistral) echo "mistral-small" ;;
488
+ kilo) echo "kilo" ;; # No economy variant
489
+ cursor) echo "cursor" ;; # No economy variant
490
+ claude) echo "claude-3-haiku" ;;
491
+ qwen3) echo "qwen-turbo" ;;
492
+ glm) echo "glm-3-turbo" ;;
493
+ grok) echo "grok-3-mini" ;;
494
+ deepseek) echo "deepseek-chat" ;;
495
+
496
+ minimax) echo "MiniMax-M2.5-highspeed" ;;
497
+ *) echo "" ;;
498
+ esac
499
+ }
500
+
501
+ # Calculate query complexity score (1-10)
502
+ # Usage: calculate_query_complexity <query> <num_files> <category>
503
+ calculate_query_complexity() {
504
+ local query="$1"
505
+ local num_files="${2:-0}"
506
+ local category="${3:-GENERAL}"
507
+
508
+ local score=5 # Base score
509
+
510
+ # Length factor
511
+ local query_len=${#query}
512
+ if [[ $query_len -gt 500 ]]; then
513
+ score=$((score + 2))
514
+ elif [[ $query_len -gt 200 ]]; then
515
+ score=$((score + 1))
516
+ elif [[ $query_len -lt 50 ]]; then
517
+ score=$((score - 1))
518
+ fi
519
+
520
+ # File count factor
521
+ if [[ $num_files -gt 5 ]]; then
522
+ score=$((score + 2))
523
+ elif [[ $num_files -gt 2 ]]; then
524
+ score=$((score + 1))
525
+ elif [[ $num_files -eq 0 ]]; then
526
+ score=$((score - 1))
527
+ fi
528
+
529
+ # Category factor
530
+ case "$category" in
531
+ ARCHITECTURE|SECURITY)
532
+ score=$((score + 2))
533
+ ;;
534
+ CODE_REVIEW|BUG_DEBUG)
535
+ score=$((score + 1))
536
+ ;;
537
+ QUICK_SYNTAX)
538
+ score=$((score - 2))
539
+ ;;
540
+ esac
541
+
542
+ # Keyword complexity indicators
543
+ if echo "$query" | grep -qiE "(architecture|design|scalab|security|performance|refactor|migrate)"; then
544
+ score=$((score + 1))
545
+ fi
546
+ if echo "$query" | grep -qiE "(fix|bug|error|typo|rename)"; then
547
+ score=$((score - 1))
548
+ fi
549
+
550
+ # Cap at 1-10
551
+ [[ $score -gt 10 ]] && score=10
552
+ [[ $score -lt 1 ]] && score=1
553
+
554
+ echo "$score"
555
+ }
556
+
557
+ # Check if query is simple (should use economic models)
558
+ # Usage: is_simple_query <complexity_score>
559
+ is_simple_query() {
560
+ local complexity="${1:-5}"
561
+ local threshold="${COMPLEXITY_THRESHOLD_SIMPLE:-3}"
562
+
563
+ [[ $complexity -le $threshold ]]
564
+ }
565
+
566
+ # Check if query is complex (should use premium models)
567
+ # Usage: is_complex_query <complexity_score>
568
+ is_complex_query() {
569
+ local complexity="${1:-5}"
570
+ local threshold="${COMPLEXITY_THRESHOLD_MEDIUM:-6}"
571
+
572
+ [[ $complexity -gt $threshold ]]
573
+ }
574
+
575
+ # =============================================================================
576
+ # BUDGET ENFORCEMENT (v2.4)
577
+ # =============================================================================
578
+
579
+ # Check if budget enforcement is enabled
580
+ # Usage: is_budget_enabled
581
+ is_budget_enabled() {
582
+ [[ "${ENABLE_BUDGET_LIMIT:-false}" == "true" ]]
583
+ }
584
+
585
+ # Get remaining budget
586
+ # Usage: get_remaining_budget <current_cost>
587
+ get_remaining_budget() {
588
+ local current_cost="${1:-0}"
589
+ local budget="${MAX_SESSION_COST:-1.00}"
590
+
591
+ echo "scale=6; $budget - $current_cost" | bc
592
+ }
593
+
594
+ # Format budget status for display
595
+ # Usage: format_budget_status <current_cost> [budget]
596
+ format_budget_status() {
597
+ local current_cost="${1:-0}"
598
+ local budget="${2:-${MAX_SESSION_COST:-1.00}}"
599
+
600
+ local remaining
601
+ remaining=$(get_remaining_budget "$current_cost")
602
+ local percent_used
603
+ percent_used=$(echo "scale=1; $current_cost / $budget * 100" | bc 2>/dev/null || echo "0")
604
+
605
+ echo "$(format_cost "$current_cost") / $(format_cost "$budget") (${percent_used}% used)"
606
+ }
607
+
608
+ # Enforce budget and take action based on BUDGET_ACTION
609
+ # Returns: 0 = proceed, 1 = stop
610
+ # Usage: enforce_budget <current_cost> <additional_estimate> <context_msg>
611
+ enforce_budget() {
612
+ local current_cost="${1:-0}"
613
+ local additional_estimate="${2:-0}"
614
+ local context_msg="${3:-operation}"
615
+ local budget="${MAX_SESSION_COST:-1.00}"
616
+ local action="${BUDGET_ACTION:-warn}"
617
+
618
+ # Skip if budget enforcement is disabled
619
+ if ! is_budget_enabled; then
620
+ return 0
621
+ fi
622
+
623
+ # Calculate projected cost
624
+ local projected_cost
625
+ projected_cost=$(echo "scale=6; $current_cost + $additional_estimate" | bc)
626
+
627
+ # Check if projected cost exceeds budget
628
+ if ! check_budget "$projected_cost" "$budget"; then
629
+ local msg="Budget limit exceeded for $context_msg: projected $(format_cost "$projected_cost") > $(format_cost "$budget")"
630
+
631
+ case "$action" in
632
+ stop)
633
+ log_error "$msg"
634
+ log_error "Stopping consultation (BUDGET_ACTION=stop)"
635
+ return 1
636
+ ;;
637
+ warn|*)
638
+ log_warn "$msg"
639
+ log_warn "Continuing despite budget limit (BUDGET_ACTION=warn)"
640
+ return 0
641
+ ;;
642
+ esac
643
+ fi
644
+
645
+ return 0
646
+ }
647
+
648
+ # Estimate cost for a specific phase
649
+ # Usage: estimate_phase_cost <phase> <num_consultants> <context_size>
650
+ estimate_phase_cost() {
651
+ local phase="$1"
652
+ local num_consultants="${2:-4}"
653
+ local context_size="${3:-5000}"
654
+
655
+ case "$phase" in
656
+ round1|consultation)
657
+ # Initial consultation: all consultants
658
+ estimate_consultation_cost "$num_consultants" "$context_size"
659
+ ;;
660
+ debate)
661
+ # Debate round: ~50% of consultation cost per round
662
+ local base
663
+ base=$(estimate_consultation_cost "$num_consultants" "$context_size")
664
+ echo "scale=6; $base * 0.5" | bc
665
+ ;;
666
+ synthesis)
667
+ # Synthesis: single model, larger output
668
+ estimate_query_cost "claude-3-sonnet" 2000 1500
669
+ ;;
670
+ *)
671
+ echo "0"
672
+ ;;
673
+ esac
674
+ }