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.
- package/.claude/commands/ai-consultants:consult.md +94 -0
- package/.claude/commands/ai-consultants:debate.md +103 -0
- package/.claude/commands/ai-consultants:help.md +30 -0
- package/CLAUDE.md +839 -0
- package/LICENSE +21 -0
- package/README.md +758 -0
- package/SKILL.md +156 -0
- package/bin/ai-consultants +96 -0
- package/docs/COST_RATES.md +172 -0
- package/docs/JSON_SCHEMA.md +305 -0
- package/docs/SETUP.md +879 -0
- package/docs/SMART_ROUTING.md +219 -0
- package/docs/cost_rates.json +143 -0
- package/package.json +47 -0
- package/references/configuration.md +141 -0
- package/references/details.md +60 -0
- package/scripts/build_context.sh +562 -0
- package/scripts/classify_question.sh +148 -0
- package/scripts/config.sh +753 -0
- package/scripts/configure.sh +1093 -0
- package/scripts/consult_all.sh +934 -0
- package/scripts/debate_round.sh +421 -0
- package/scripts/doctor.sh +642 -0
- package/scripts/followup.sh +197 -0
- package/scripts/install.sh +319 -0
- package/scripts/lib/api.sh +640 -0
- package/scripts/lib/api_query.sh +228 -0
- package/scripts/lib/cache.sh +338 -0
- package/scripts/lib/chunking.sh +880 -0
- package/scripts/lib/code_optimizer.sh +671 -0
- package/scripts/lib/common.sh +1088 -0
- package/scripts/lib/costs.sh +674 -0
- package/scripts/lib/personas.sh +372 -0
- package/scripts/lib/progress.sh +251 -0
- package/scripts/lib/reflection.sh +447 -0
- package/scripts/lib/routing.sh +566 -0
- package/scripts/lib/schema.json +165 -0
- package/scripts/lib/session.sh +327 -0
- package/scripts/lib/symbol_map.sh +445 -0
- package/scripts/lib/voting.sh +495 -0
- package/scripts/peer_review.sh +460 -0
- package/scripts/preflight_check.sh +452 -0
- package/scripts/query_aider.sh +83 -0
- package/scripts/query_amp.sh +67 -0
- package/scripts/query_claude.sh +99 -0
- package/scripts/query_codex.sh +92 -0
- package/scripts/query_cursor.sh +72 -0
- package/scripts/query_deepseek.sh +17 -0
- package/scripts/query_gemini.sh +85 -0
- package/scripts/query_glm.sh +17 -0
- package/scripts/query_grok.sh +17 -0
- package/scripts/query_kilo.sh +140 -0
- package/scripts/query_kimi.sh +67 -0
- package/scripts/query_minimax.sh +17 -0
- package/scripts/query_mistral.sh +97 -0
- package/scripts/query_ollama.sh +328 -0
- package/scripts/query_qwen3.sh +93 -0
- package/scripts/setup_wizard.sh +367 -0
- package/scripts/skillport-install.sh +196 -0
- package/scripts/synthesize.sh +332 -0
- package/templates/consultation_report.md +323 -0
- 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
|
+
}
|