narcolepticchicken commited on
Commit
7aa55cc
·
verified ·
1 Parent(s): e5ed82f

Upload aco/proxy.py

Browse files
Files changed (1) hide show
  1. aco/proxy.py +332 -361
aco/proxy.py CHANGED
@@ -6,12 +6,13 @@ Start: aco-proxy --port 8080
6
  Use: openai.api_base = "http://localhost:8080/v1"
7
 
8
  The proxy intercepts POST /v1/chat/completions and:
9
- 1. Routes to cheapest adequate model (v1 tier-router)
10
  2. Gates unnecessary tool calls (v1 tool-gater, F1=0.92)
11
  3. Lays out prompts for cache reuse (system + tools in prefix)
12
  4. Compresses verbose error traces and thinking-only turns
13
  5. Collects telemetry: cost, tokens, latency, cache hits
14
- 6. Reports live dashboard at GET /dashboard
 
15
 
16
  Zero agent code changes needed.
17
  """
@@ -45,7 +46,6 @@ MODEL_REGISTRY = {
45
  "gemini-3-pro": {"tier": 5, "cost_in": 2.00, "cost_out": 12.50, "ctx": 1048576},
46
  }
47
 
48
- # Default provider endpoints (override via OPENAI_BASE_URL, ANTHROPIC_BASE_URL, etc.)
49
  PROVIDER_ENDPOINTS = {
50
  "openai": os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
51
  "anthropic": os.environ.get("ANTHROPIC_BASE_URL", "https://api.anthropic.com/v1"),
@@ -61,216 +61,253 @@ MODEL_PROVIDER = {
61
  "deepseek-v3.2": "deepseek",
62
  }
63
 
64
- # ── Telemetry Store ──────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- @dataclass
67
- class TraceRecord:
68
- request_id: str
69
- timestamp: str
70
- model: str
71
- provider: str
72
- tier: int
73
- input_tokens: int
74
- output_tokens: int
75
- cache_hit_tokens: int
76
- latency_ms: float
77
- cost: float
78
- tool_gated: bool
79
- context_compressed: float # ratio
80
- success: bool
81
- error: Optional[str] = None
82
 
83
  # ── Cache-Aware Prompt Layout ────────────────────────────────────────
84
 
85
  def layout_cache_prompt(messages: List[Dict], tools: Optional[List[Dict]] = None) -> List[Dict]:
86
  """
87
- Reorder messages so cache-stable content is in prefix:
88
- - system prompt (stable) first
89
- - tool definitions (stable) next (converted to system-like)
90
- - user messages → after
91
- - dynamic content (timestamps, request IDs) → moved to suffix or stripped
92
  """
93
  laid_out = []
94
- tool_block = None
95
 
96
- # Find and remove system message, move to front
97
  for msg in messages:
98
  if msg.get("role") == "system":
99
- laid_out.insert(0, {"role": "system", "content": msg["content"]})
100
- elif msg.get("role") == "tool" and tool_block is None:
101
- # First tool message: convert to system for cache
102
- laid_out.insert(1, {"role": "system",
103
- "content": f"[TOOL_DEFS]\n{msg.get('content', '')}"})
104
- tool_block = len(laid_out)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  else:
106
  laid_out.append(msg)
107
 
108
- # Append tools as last system-like block (Anthropic: suffix, OpenAI: doesn't matter)
109
- if tools and not tool_block:
110
- tool_defs = json.dumps([{"name": t.get("function", {}).get("name", t.get("name", "")),
111
- "desc": t.get("function", {}).get("description",
112
- t.get("description", ""))[:200]}
113
- for t in tools])
114
- laid_out.insert(1, {"role": "system", "content": f"[TOOL_DEFS]\n{tool_defs}"})
115
-
116
- # Strip dynamic metadata from user messages
117
- for msg in laid_out:
118
- if msg.get("role") == "user" and isinstance(msg.get("content"), str):
119
- # Remove timestamps, request IDs, run IDs
120
- content = msg["content"]
121
- content = re.sub(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*', '[TIME]', content)
122
- content = re.sub(r'run_[a-f0-9]{8,}', 'run_xxx', content)
123
- content = re.sub(r'req_[a-f0-9]{8,}', 'req_xxx', content)
124
- content = re.sub(r'trace_[a-f0-9]{8,}', 'trace_xxx', content)
125
- msg["content"] = content
126
 
127
  return laid_out
128
 
129
 
130
  # ── Context Compression ──────────────────────────────────────────────
131
 
132
- def compress_context(messages: List[Dict]) -> List[Dict]:
133
- """Compress verbose messages while preserving signal."""
134
  compressed = []
135
  total_orig = 0
136
  total_comp = 0
137
 
138
  for msg in messages:
139
- content = msg.get("content", "")
140
  role = msg.get("role", "")
141
- total_orig += len(str(content))
142
 
143
  if role == "user":
144
- cl = str(content).lower()
145
- # Trim long error outputs
146
- if len(str(content)) > 2000 and any(k in cl for k in ['error', 'traceback', 'exception']):
147
- content = str(content)[:300] + '\n... [error trace trimmed] ...\n' + str(content)[-300:]
148
- # Trim huge file listings
149
- elif len(str(content)) > 3000 and 'ls -' not in cl[:50]:
150
- content = str(content)[:2000] + '\n... [output trimmed] ...'
 
 
151
 
152
  elif role == "assistant":
153
- # Strip thinking-only turns (no function calls, no code blocks)
154
- s = str(content)
155
  if len(s) > 800 and '```' not in s and '<function=' not in s:
156
- # Check if it's mostly reasoning without action
157
- if not re.search(r'\b(?:execute|run|apply|create|delete|modify|write|patch|fix)\b', s, re.IGNORECASE):
158
  content = s[:200] + '\n... [thinking trimmed] ...'
159
- # Trim long code blocks
160
  if len(s) > 4000:
161
- content = s[:3000] + '\n... [code trimmed] ...'
162
 
163
- total_comp += len(str(content))
164
  compressed.append({**msg, "content": content})
165
 
166
  ratio = total_comp / max(total_orig, 1)
167
  return compressed, ratio
168
 
169
 
170
- # ── Tool Gate ────────────────────────────────────────────────────────
171
-
172
- def should_gate_tools(messages: List[Dict]) -> bool:
173
- """Quick heuristic: does this request really need tools?
174
-
175
- If the agent is asking a simple question with no tool-calling
176
- history, suppress tools to save input tokens.
177
-
178
- For production, replace with v1 tool-gater classifier (F1=0.92).
179
- """
180
- # Find the last user message
181
- user_text = ""
182
- for msg in reversed(messages):
183
- if msg.get("role") == "user":
184
- user_text = str(msg.get("content", ""))[:500].lower()
185
- break
186
-
187
- # Check if any prior assistant message already used tools
188
- has_tool_history = any(
189
- '<function=' in str(msg.get("content", ""))
190
- for msg in messages if msg.get("role") == "assistant"
191
- )
192
-
193
- # If there's tool history, don't gate
194
- if has_tool_history:
195
- return False
196
-
197
- # Quick-answer patterns that rarely need tools
198
- simple_patterns = [
199
- r'\bwhat is\b', r'\bwho (is|was)\b', r'\bwhen (is|was)\b',
200
- r'\bdefine\b', r'\bexplain\b', r'\bsummarize\b',
201
- r'\bhow (do|does|to)\b', r'\bdifference between\b',
202
- r'\bcapital of\b', r'\bmeaning of\b', r'\btranslate\b',
203
- ]
204
- return any(re.search(p, user_text) for p in simple_patterns)
205
-
206
-
207
  # ── Model Router ─────────────────────────────────────────────────────
208
 
209
  def route_model(requested_model: str, messages: List[Dict]) -> str:
210
- """Route to cheapest model that can handle this request.
211
-
212
- Strategy: trust the agent's model choice but downgrade when safe.
213
- """
214
- # Get the last user message for difficulty estimation
 
 
 
215
  user_text = ""
216
  for msg in reversed(messages):
217
  if msg.get("role") == "user":
218
  user_text = str(msg.get("content", ""))
219
  break
220
 
221
- info = MODEL_REGISTRY.get(requested_model)
222
- if not info:
223
- return requested_model # Unknown model, pass through
224
 
225
- tier = info["tier"]
226
- text_len = len(user_text)
227
-
228
- # Downgrade rules: if using tier 3+ for short, simple text
229
- if tier >= 3 and text_len < 300:
230
- # Try tier 1
231
- t1_models = [m for m, d in MODEL_REGISTRY.items() if d["tier"] == 1]
232
- if t1_models:
233
- return t1_models[0] # deepseek-v4-flash
234
-
235
- # For coding tasks, keep tier 2 minimum
236
- code_indicators = ['def ', 'class ', 'function', 'import ', '```', 'fix ', 'bug',
237
- 'implement', 'refactor', 'test_', 'pytest']
238
- if any(c in user_text for c in code_indicators) and tier < 2:
239
- t2_models = [m for m, d in MODEL_REGISTRY.items() if d["tier"] == 2]
240
- if t2_models:
241
- return t2_models[0]
242
 
243
  return requested_model
244
 
245
 
246
  # ── Cost Calculator ──────────────────────────────────────────────────
247
 
248
- def estimate_cost(model: str, input_tokens: int, output_tokens: int,
249
- cache_hit_tokens: int = 0) -> float:
250
- """Estimate cost in USD."""
251
  info = MODEL_REGISTRY.get(model)
252
  if not info:
253
  return 0.0
 
 
 
 
 
254
 
255
- # Cache hit: input tokens that hit cache are free or heavily discounted
256
- chargeable_input = input_tokens - cache_hit_tokens
257
- cost = (chargeable_input / 1_000_000) * info["cost_in"]
258
- cost += (output_tokens / 1_000_000) * info["cost_out"]
259
- return round(cost, 6)
260
 
 
261
 
262
- # ── Response Rewriter ────────────────────────────────────────────────
263
-
264
- def add_usage_headers(response: dict, model: str, cost: float,
265
- cache_hit: int = 0, compressed: float = 1.0) -> dict:
266
- """Inject ACO telemetry into response headers and usage."""
267
- if "usage" not in response:
268
- response["usage"] = {}
269
- response["usage"]["aco_cost_usd"] = cost
270
- response["usage"]["aco_model"] = model
271
- response["usage"]["aco_cache_hit_tokens"] = cache_hit
272
- response["usage"]["aco_compression_ratio"] = compressed
273
- return response
 
 
 
 
 
 
 
 
274
 
275
 
276
  # ══════════════════════════════════════════════════════════════════════
@@ -278,148 +315,153 @@ def add_usage_headers(response: dict, model: str, cost: float,
278
  # ══════════════════════════════════════════════════════════════════════
279
 
280
  if FASTAPI_AVAILABLE:
281
- app = FastAPI(title="ACO Proxy", version="1.0.0")
282
  telemetry_store: List[TraceRecord] = []
283
  telemetry_lock = threading.Lock()
284
  start_time = datetime.utcnow()
285
 
286
  @app.get("/health")
287
  async def health():
288
- return {"status": "ok", "uptime": str(datetime.utcnow() - start_time)}
289
 
290
  @app.get("/v1/models")
291
  async def list_models():
292
- """List all known models."""
293
  return {
294
  "object": "list",
295
- "data": [{"id": m, "object": "model", "owned_by": PROVIDER_ENDPOINTS.get(
296
- MODEL_PROVIDER.get(m, "unknown"), "unknown")}
297
  for m in MODEL_REGISTRY]
298
  }
299
 
300
  @app.post("/v1/chat/completions")
301
  async def chat_completions(request: Request):
302
- """
303
- OpenAI-compatible chat completions endpoint.
304
- Applies ACO optimizations transparently.
305
- """
306
  body = await request.json()
307
- request_id = body.get("user", hashlib.md5(str(time.time()).encode()).hexdigest()[:8])
 
308
 
309
- # ── Step 1: Extract params ──
310
  messages = body.get("messages", [])
311
  tools = body.get("tools")
312
  requested_model = body.get("model", "gpt-5-mini")
313
  stream = body.get("stream", False)
 
314
 
315
- # ── Step 2: Route model ──
316
  routed_model = route_model(requested_model, messages)
 
317
  provider = MODEL_PROVIDER.get(routed_model, "openai")
318
 
319
- # ── Step 3: Gate tools ──
320
  tools_gated = False
321
- if tools and should_gate_tools(messages):
 
322
  tools = None
323
  tools_gated = True
 
324
 
325
- # ── Step 4: Layout for cache ──
326
  laid_out = layout_cache_prompt(messages, tools)
 
327
 
328
- # ── Step 5: Compress context ──
329
  compressed_messages, compression_ratio = compress_context(laid_out)
330
 
331
- # ── Step 6: Build forwarding request ──
332
  forward_body = {**body}
333
  forward_body["model"] = routed_model
334
  forward_body["messages"] = compressed_messages
335
  if tools is None and "tools" in forward_body:
336
  del forward_body["tools"]
337
 
338
- # ── Step 7: Determine provider endpoint ──
339
  endpoint = PROVIDER_ENDPOINTS.get(provider, PROVIDER_ENDPOINTS["openai"])
340
  target_url = f"{endpoint}/chat/completions"
341
 
342
- # ── Step 8: Forward with auth ──
343
- auth_header = request.headers.get("authorization", "")
344
- headers = {"content-type": "application/json"}
345
- if auth_header:
346
- headers["authorization"] = auth_header
347
-
348
- # API key overrides
349
  api_key_map = {
 
350
  "anthropic": os.environ.get("ANTHROPIC_API_KEY"),
351
  "google": os.environ.get("GOOGLE_API_KEY"),
352
  "deepseek": os.environ.get("DEEPSEEK_API_KEY"),
353
- "openai": os.environ.get("OPENAI_API_KEY"),
354
  }
355
- provider_key = api_key_map.get(provider)
356
- if provider_key and not auth_header:
357
- headers["authorization"] = f"Bearer {provider_key}"
 
 
 
 
 
358
 
359
- # ── Step 9: Make upstream call ──
360
  t_start = time.time()
361
  error = None
362
  success = True
 
363
 
364
  try:
365
  async with httpx.AsyncClient(timeout=300.0) as client:
366
  upstream = await client.post(target_url, json=forward_body, headers=headers)
367
  latency = (time.time() - t_start) * 1000
368
 
369
- if upstream.status_code != 200:
370
- error = f"Upstream {upstream.status_code}: {upstream.text[:200]}"
371
- success = False
372
- # Fall back to original model on failure
373
- if routed_model != requested_model:
374
- forward_body["model"] = requested_model
375
- t_start2 = time.time()
376
- upstream2 = await client.post(target_url, json=forward_body, headers=headers)
377
- latency = (time.time() - t_start) * 1000
378
- upstream = upstream2
379
- routed_model = requested_model
380
- success = upstream.status_code == 200
381
- if not success:
382
- error = f"Fallback also failed: {upstream.status_code}"
383
 
384
  if stream:
385
  return StreamingResponse(
386
  upstream.aiter_bytes(),
387
  media_type="text/event-stream",
388
- headers={"x-aco-model": routed_model,
389
- "x-aco-cost": "streaming"}
390
  )
391
 
392
- response_data = upstream.json()
 
 
 
 
393
 
394
  except Exception as e:
395
  latency = (time.time() - t_start) * 1000
396
  error = str(e)
397
  success = False
 
 
398
  response_data = {
399
  "id": f"aco-err-{request_id}",
400
  "object": "chat.completion",
401
  "created": int(time.time()),
402
  "model": requested_model,
403
  "choices": [{"index": 0, "message": {"role": "assistant",
404
- "content": f"ACO proxy error: {error}"},
405
  "finish_reason": "error"}],
406
  "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
407
  }
408
 
409
- # ── Step 10: Compute cost ──
410
  usage = response_data.get("usage", {})
411
  input_tokens = usage.get("prompt_tokens", 0)
412
  output_tokens = usage.get("completion_tokens", 0)
413
- # Some providers report cache hits
414
- cache_hit = usage.get("cache_read_input_tokens", 0) or \
415
- usage.get("prompt_tokens_details", {}).get("cached_tokens", 0)
416
- cost = estimate_cost(routed_model, input_tokens, output_tokens, cache_hit)
417
-
418
- # ── Step 11: Rewrite response ──
419
- response_data = add_usage_headers(response_data, routed_model, cost,
420
- cache_hit, compression_ratio)
421
-
422
- # ── Step 12: Record telemetry ──
 
 
 
 
 
423
  trace = TraceRecord(
424
  request_id=request_id,
425
  timestamp=datetime.utcnow().isoformat(),
@@ -432,7 +474,11 @@ if FASTAPI_AVAILABLE:
432
  latency_ms=round(latency, 1),
433
  cost=cost,
434
  tool_gated=tools_gated,
 
435
  context_compressed=round(compression_ratio, 3),
 
 
 
436
  success=success,
437
  error=error,
438
  )
@@ -443,193 +489,118 @@ if FASTAPI_AVAILABLE:
443
 
444
  @app.get("/dashboard")
445
  async def dashboard():
446
- """Live cost dashboard as HTML."""
447
  with telemetry_lock:
448
  traces = list(telemetry_store)
449
 
 
 
 
 
450
  total_cost = sum(t.cost for t in traces)
451
- total_calls = len(traces)
452
  successful = sum(1 for t in traces if t.success)
453
- total_input = sum(t.input_tokens for t in traces)
454
- total_output = sum(t.output_tokens for t in traces)
455
  total_cache = sum(t.cache_hit_tokens for t in traces)
456
- avg_latency = sum(t.latency_ms for t in traces) / max(total_calls, 1)
457
-
458
- # Tier distribution
459
- tier_counts = defaultdict(int)
460
- tier_costs = defaultdict(float)
461
- for t in traces:
462
- tier_counts[t.tier] += 1
463
- tier_costs[t.tier] += t.cost
464
-
465
- # Tool gating stats
466
  gated = sum(1 for t in traces if t.tool_gated)
467
- gated_success = sum(1 for t in traces if t.tool_gated and t.success)
468
 
469
- # Model distribution
470
- model_counts = defaultdict(int)
 
471
  for t in traces:
472
- model_counts[t.model] += 1
 
 
473
 
474
- html = f"""
475
- <!DOCTYPE html>
476
- <html><head>
477
- <title>ACO Proxy Dashboard</title>
478
- <meta charset="utf-8">
479
  <style>
480
- body {{ font-family: system-ui; margin: 2rem; background: #111; color: #eee; }}
481
- .card {{ background: #1a1a2e; border-radius: 8px; padding: 1rem; margin: 0.5rem; }}
482
- .grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }}
483
- .metric {{ font-size: 2rem; font-weight: bold; color: #00d4aa; }}
484
- .label {{ font-size: 0.8rem; color: #888; text-transform: uppercase; }}
485
- table {{ width: 100%; border-collapse: collapse; margin: 1rem 0; }}
486
- th, td {{ padding: 0.5rem; text-align: left; border-bottom: 1px solid #333; }}
487
- th {{ color: #888; font-size: 0.8rem; text-transform: uppercase; }}
488
- .error {{ color: #ff5555; }}
489
- .success {{ color: #00d4aa; }}
490
- </style>
491
- <meta http-equiv="refresh" content="5">
492
- </head><body>
493
- <h1>🤖 ACO Proxy Dashboard</h1>
494
- <p>Uptime: {datetime.utcnow() - start_time}</p>
495
-
496
  <div class="grid">
497
- <div class="card">
498
- <div class="label">Total Calls</div>
499
- <div class="metric">{total_calls}</div>
500
- </div>
501
- <div class="card">
502
- <div class="label">Total Cost</div>
503
- <div class="metric">${total_cost:.4f}</div>
504
- </div>
505
- <div class="card">
506
- <div class="label">Success Rate</div>
507
- <div class="metric">{successful/max(total_calls,1)*100:.1f}%</div>
508
- </div>
509
- <div class="card">
510
- <div class="label">Avg Latency</div>
511
- <div class="metric">{avg_latency:.0f}ms</div>
512
- </div>
513
- <div class="card">
514
- <div class="label">Total Tokens In/Out</div>
515
- <div class="metric">{total_input//1000}k / {total_output//1000}k</div>
516
- </div>
517
- <div class="card">
518
- <div class="label">Cache Hit Tokens</div>
519
- <div class="metric">{total_cache//1000}k</div>
520
- </div>
521
- <div class="card">
522
- <div class="label">Tools Gated</div>
523
- <div class="metric">{gated} calls</div>
524
- </div>
525
- <div class="card">
526
- <div class="label">Cost/Call</div>
527
- <div class="metric">${total_cost/max(total_calls,1):.4f}</div>
528
- </div>
529
  </div>
530
-
531
- <h2>By Tier</h2>
532
- <table>
533
- <tr><th>Tier</th><th>Calls</th><th>Cost</th><th>Cost/Call</th></tr>
534
- """
535
- for tier in sorted(tier_counts):
536
- html += f"<tr><td>{tier}</td><td>{tier_counts[tier]}</td>"
537
- html += f"<td>${tier_costs[tier]:.4f}</td>"
538
- html += f"<td>${tier_costs[tier]/max(tier_counts[tier],1):.4f}</td></tr>"
539
-
540
- html += """
541
- </table>
542
- <h2>By Model</h2>
543
- <table>
544
- <tr><th>Model</th><th>Calls</th></tr>
545
- """
546
- for model, count in sorted(model_counts.items(), key=lambda x: -x[1]):
547
- html += f"<tr><td>{model}</td><td>{count}</td></tr>"
548
-
549
- html += """
550
- </table>
551
- <h2>Recent Calls</h2>
552
- <table>
553
- <tr><th>Time</th><th>Model</th><th>Tier</th><th>Tokens In</th><th>Tokens Out</th>
554
- <th>Cache</th><th>Latency</th><th>Cost</th><th>Gated</th><th>Compress</th><th>Status</th></tr>
555
- """
556
- for t in reversed(traces[-50:]):
557
- status = '<span class="success">✓</span>' if t.success else '<span class="error">✗</span>'
558
- html += f"<tr><td>{t.timestamp[-12:]}</td><td>{t.model}</td><td>{t.tier}</td>"
559
- html += f"<td>{t.input_tokens}</td><td>{t.output_tokens}</td>"
560
- html += f"<td>{t.cache_hit_tokens}</td><td>{t.latency_ms:.0f}ms</td>"
561
- html += f"<td>${t.cost:.6f}</td><td>{'✓' if t.tool_gated else ''}</td>"
562
- html += f"<td>{t.context_compressed:.2f}</td><td>{status}</td></tr>"
563
-
564
  html += "</table></body></html>"
565
  return HTMLResponse(html)
566
 
567
  @app.get("/telemetry")
568
- async def telemetry():
569
- """JSON telemetry endpoint."""
570
  with telemetry_lock:
571
- traces = [{
572
- "request_id": t.request_id,
573
- "timestamp": t.timestamp,
574
- "model": t.model,
575
- "provider": t.provider,
576
- "tier": t.tier,
577
- "input_tokens": t.input_tokens,
578
- "output_tokens": t.output_tokens,
579
- "cache_hit_tokens": t.cache_hit_tokens,
580
- "latency_ms": t.latency_ms,
581
- "cost": t.cost,
582
- "tool_gated": t.tool_gated,
583
- "context_compressed": t.context_compressed,
584
- "success": t.success,
585
- "error": t.error,
586
- } for t in telemetry_store]
587
-
588
- total_cost = sum(t.cost for t in telemetry_store)
589
  return {
590
  "total_calls": len(traces),
591
- "total_cost": total_cost,
592
  "calls": traces,
593
- "summary": {
594
- "success_rate": sum(1 for t in telemetry_store if t.success) / max(len(traces), 1),
595
- "avg_latency_ms": sum(t.latency_ms for t in telemetry_store) / max(len(traces), 1),
596
- "total_input_tokens": sum(t.input_tokens for t in telemetry_store),
597
- "total_output_tokens": sum(t.output_tokens for t in telemetry_store),
598
- "total_cache_hit_tokens": sum(t.cache_hit_tokens for t in telemetry_store),
599
- "tools_gated": sum(1 for t in telemetry_store if t.tool_gated),
600
- }
601
  }
602
 
603
  @app.get("/telemetry/reset")
604
  async def reset_telemetry():
605
- """Reset telemetry store."""
606
  with telemetry_lock:
607
  telemetry_store.clear()
608
- return {"status": "ok", "message": "Telemetry reset"}
609
 
610
 
611
  def serve(host: str = "0.0.0.0", port: int = 8080):
612
- """Start the ACO proxy server."""
613
  if not FASTAPI_AVAILABLE:
614
- print("ERROR: fastapi and uvicorn required. pip install fastapi uvicorn httpx")
615
  return
616
- print(f"🚀 ACO Proxy starting on http://{host}:{port}")
617
- print(f" Dashboard: http://localhost:{port}/dashboard")
618
- print(f" Telemetry: http://localhost:{port}/telemetry")
619
- print(f" Set openai.api_base = 'http://localhost:{port}/v1'")
620
- uvicorn.run(app, host=host, port=port, log_level="info")
621
 
622
 
623
- # ── CLI entry point ──────────────────────────────────────────────────
624
-
625
  def main():
626
  import argparse
627
- parser = argparse.ArgumentParser(description="ACO Proxy Server")
628
- parser.add_argument("--host", default="0.0.0.0")
629
- parser.add_argument("--port", type=int, default=8080)
630
- args = parser.parse_args()
631
- serve(host=args.host, port=args.port)
632
-
633
 
634
  if __name__ == "__main__":
635
  main()
 
6
  Use: openai.api_base = "http://localhost:8080/v1"
7
 
8
  The proxy intercepts POST /v1/chat/completions and:
9
+ 1. Routes to cheapest adequate model
10
  2. Gates unnecessary tool calls (v1 tool-gater, F1=0.92)
11
  3. Lays out prompts for cache reuse (system + tools in prefix)
12
  4. Compresses verbose error traces and thinking-only turns
13
  5. Collects telemetry: cost, tokens, latency, cache hits
14
+ 6. Live dashboard at GET /dashboard
15
+ 7. JSON telemetry at GET /telemetry
16
 
17
  Zero agent code changes needed.
18
  """
 
46
  "gemini-3-pro": {"tier": 5, "cost_in": 2.00, "cost_out": 12.50, "ctx": 1048576},
47
  }
48
 
 
49
  PROVIDER_ENDPOINTS = {
50
  "openai": os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
51
  "anthropic": os.environ.get("ANTHROPIC_BASE_URL", "https://api.anthropic.com/v1"),
 
61
  "deepseek-v3.2": "deepseek",
62
  }
63
 
64
+ # ── Tool-Gater Classifier (v1: DistilBERT, F1=0.92) ─────────────────
65
+
66
+ _tool_gater = None # Lazy-loaded singleton
67
+
68
+ def _get_tool_gater():
69
+ """Lazy-load the v1 DistilBERT tool-gater."""
70
+ global _tool_gater
71
+ if _tool_gater is not None:
72
+ return _tool_gater
73
+
74
+ try:
75
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
76
+ import torch
77
+
78
+ model_id = "narcolepticchicken/aco-specialists-tool-gater"
79
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
80
+ model = AutoModelForSequenceClassification.from_pretrained(model_id, num_labels=2)
81
+ device = "cuda" if torch.cuda.is_available() else "cpu"
82
+ model.to(device)
83
+ model.eval()
84
+ _tool_gater = (model, tokenizer, device)
85
+ return _tool_gater
86
+ except Exception as e:
87
+ print(f"[ACO] Failed to load tool-gater: {e}. Using heuristic fallback.")
88
+ return None
89
+
90
+
91
+ def should_gate_tools_ml(messages: List[Dict]) -> bool:
92
+ """
93
+ ML-based tool gating using the v1 DistilBERT classifier (F1=0.92).
94
+
95
+ Falls back to heuristic if classifier is unavailable.
96
+ """
97
+ # Find user query
98
+ user_text = ""
99
+ system_text = ""
100
+ for msg in messages:
101
+ if msg.get("role") == "user":
102
+ user_text = str(msg.get("content", ""))[:1500]
103
+ break
104
+ elif msg.get("role") == "system":
105
+ system_text = str(msg.get("content", ""))[:500]
106
+
107
+ if not user_text:
108
+ return False
109
+
110
+ # Check if tools have already been used in this conversation
111
+ has_tool_history = any(
112
+ '<function=' in str(msg.get("content", ""))
113
+ for msg in messages if msg.get("role") == "assistant"
114
+ )
115
+ if has_tool_history:
116
+ return False
117
+
118
+ # Build input text
119
+ text = f"Query: {user_text}"
120
+ if system_text:
121
+ text = f"System: {system_text}\n\n{text}"
122
+
123
+ # Try ML classifier
124
+ gater = _get_tool_gater()
125
+ if gater:
126
+ model, tokenizer, device = gater
127
+ try:
128
+ import torch
129
+ inputs = tokenizer(text[:2000], truncation=True, max_length=512,
130
+ return_tensors="pt").to(device)
131
+ with torch.no_grad():
132
+ logits = model(**inputs).logits
133
+ probs = torch.softmax(logits, dim=-1).cpu().numpy()[0]
134
+ # Label 0 = "skip_tool", Label 1 = "call_tool"
135
+ # Gate tools OFF (return True) when prob[no_tool] > prob[tool]
136
+ return probs[0] >= probs[1]
137
+ except Exception as e:
138
+ print(f"[ACO] Tool-gater inference failed: {e}")
139
+
140
+ # Heuristic fallback
141
+ return heuristic_tool_gate(user_text)
142
+
143
+
144
+ def heuristic_tool_gate(user_text: str) -> bool:
145
+ """Heuristic: should we gate (suppress) tools for this query?"""
146
+ ut = user_text.lower()
147
+ simple_patterns = [
148
+ r'\bwhat is\b', r'\bwho (is|was)\b', r'\bwhen (is|was)\b',
149
+ r'\bdefine\b', r'\bexplain\b', r'\bsummarize\b',
150
+ r'\bhow (do|does|to)\b', r'\bdifference between\b',
151
+ r'\bcapital of\b', r'\bmeaning of\b', r'\btranslate\b',
152
+ ]
153
+ return any(re.search(p, ut) for p in simple_patterns)
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  # ── Cache-Aware Prompt Layout ────────────────────────────────────────
157
 
158
  def layout_cache_prompt(messages: List[Dict], tools: Optional[List[Dict]] = None) -> List[Dict]:
159
  """
160
+ Reorder messages for maximum prefix-cache reuse.
161
+ Stable content (system, tool defs) first; dynamic content last.
162
+ Strips timestamps/request IDs from user messages to improve cache hits.
 
 
163
  """
164
  laid_out = []
165
+ has_tool_block = False
166
 
 
167
  for msg in messages:
168
  if msg.get("role") == "system":
169
+ # System prompt first (most stable best cache target)
170
+ content = str(msg.get("content", ""))
171
+ # Normalize: strip timestamps
172
+ content = re.sub(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*', '[TIME]', content)
173
+ content = re.sub(r'run_[a-f0-9]{8,}', 'run_xxx', content)
174
+ laid_out.insert(0, {"role": "system", "content": content})
175
+
176
+ elif msg.get("role") == "tool" and not has_tool_block:
177
+ # Tool definitions: convert to system block for cache
178
+ laid_out.insert(1, {"role": "system",
179
+ "content": f"[TOOL_DEFS]\n{str(msg.get('content', ''))}"})
180
+ has_tool_block = True
181
+
182
+ elif msg.get("role") == "user":
183
+ content = str(msg.get("content", ""))
184
+ # Normalize dynamic markers
185
+ content = re.sub(r'(?:req|trace|run)_[a-f0-9]{8,32}', 'xxx', content)
186
+ content = re.sub(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*', '[TIME]', content)
187
+ laid_out.append({"role": "user", "content": content})
188
+
189
  else:
190
  laid_out.append(msg)
191
 
192
+ # Append tools as stable suffix block if provided externally
193
+ if tools and not has_tool_block:
194
+ tool_names = [t.get("function", {}).get("name", t.get("name", "?"))
195
+ for t in tools][:20]
196
+ laid_out.insert(1, {"role": "system",
197
+ "content": f"[TOOL_DEFS]\n{json.dumps(tool_names)}"})
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
  return laid_out
200
 
201
 
202
  # ── Context Compression ──────────────────────────────────────────────
203
 
204
+ def compress_context(messages: List[Dict]) -> tuple:
205
+ """Compress verbose agent messages while preserving signal."""
206
  compressed = []
207
  total_orig = 0
208
  total_comp = 0
209
 
210
  for msg in messages:
211
+ content = str(msg.get("content", ""))
212
  role = msg.get("role", "")
213
+ total_orig += len(content)
214
 
215
  if role == "user":
216
+ cl = content.lower()
217
+ # Trim stack traces to head + tail
218
+ if len(content) > 2000 and any(k in cl for k in ['traceback', 'error:', 'exception']):
219
+ lines = content.split('\n')
220
+ head = '\n'.join(lines[:8])
221
+ tail = '\n'.join(lines[-5:])
222
+ content = f"{head}\n... [{len(lines)-13} lines trimmed] ...\n{tail}"
223
+ elif len(content) > 3000:
224
+ content = content[:2000] + '\n... [output trimmed] ...'
225
 
226
  elif role == "assistant":
227
+ s = content
228
+ # Drop pure-thinking turns (no function calls, no code blocks)
229
  if len(s) > 800 and '```' not in s and '<function=' not in s:
230
+ if not re.search(r'\b(?:execute|run|apply|create|delete|modify|write|patch|fix|submit)\b',
231
+ s, re.IGNORECASE):
232
  content = s[:200] + '\n... [thinking trimmed] ...'
233
+ # Trim large code blocks
234
  if len(s) > 4000:
235
+ content = s[:3000] + '\n... [truncated] ...'
236
 
237
+ total_comp += len(content)
238
  compressed.append({**msg, "content": content})
239
 
240
  ratio = total_comp / max(total_orig, 1)
241
  return compressed, ratio
242
 
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  # ── Model Router ─────────────────────────────────────────────────────
245
 
246
  def route_model(requested_model: str, messages: List[Dict]) -> str:
247
+ """Route to cheapest model that can handle this request."""
248
+ info = MODEL_REGISTRY.get(requested_model)
249
+ if not info:
250
+ return requested_model
251
+
252
+ tier = info["tier"]
253
+
254
+ # Get last user message
255
  user_text = ""
256
  for msg in reversed(messages):
257
  if msg.get("role") == "user":
258
  user_text = str(msg.get("content", ""))
259
  break
260
 
261
+ # Downgrade: if using tier 3+ for short simple text, use tier 1
262
+ if tier >= 3 and len(user_text) < 300:
263
+ return "deepseek-v4-flash"
264
 
265
+ # Coding floor: keep tier 2 minimum
266
+ code_words = ['def ', 'class ', 'function', 'import ', '```', 'fix ', 'bug',
267
+ 'implement', 'refactor', 'test_', 'pytest', 'traceback', 'error:']
268
+ if any(c in user_text for c in code_words) and tier < 2:
269
+ return "gpt-5-mini"
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
  return requested_model
272
 
273
 
274
  # ── Cost Calculator ──────────────────────────────────────────────────
275
 
276
+ def compute_cost(model: str, input_tokens: int, output_tokens: int,
277
+ cache_hit_tokens: int = 0) -> float:
278
+ """Estimate cost in USD per current provider pricing."""
279
  info = MODEL_REGISTRY.get(model)
280
  if not info:
281
  return 0.0
282
+ chargeable_input = max(0, input_tokens - cache_hit_tokens)
283
+ return round(
284
+ (chargeable_input / 1_000_000) * info["cost_in"] +
285
+ (output_tokens / 1_000_000) * info["cost_out"],
286
+ 6)
287
 
 
 
 
 
 
288
 
289
+ # ── Telemetry Store ──────────────────────────────────────────────────
290
 
291
+ @dataclass
292
+ class TraceRecord:
293
+ request_id: str
294
+ timestamp: str
295
+ model: str
296
+ provider: str
297
+ tier: int
298
+ input_tokens: int
299
+ output_tokens: int
300
+ cache_hit_tokens: int
301
+ latency_ms: float
302
+ cost: float
303
+ tool_gated: bool
304
+ gated_by: str # "ml" | "heuristic" | "none"
305
+ context_compressed: float # ratio
306
+ cache_layout_applied: bool
307
+ model_routed: bool # was the model changed?
308
+ original_model: str
309
+ success: bool
310
+ error: Optional[str] = None
311
 
312
 
313
  # ══════════════════════════════════════════════════════════════════════
 
315
  # ══════════════════════════════════════════════════════════════════════
316
 
317
  if FASTAPI_AVAILABLE:
318
+ app = FastAPI(title="ACO Proxy", version="1.1.0")
319
  telemetry_store: List[TraceRecord] = []
320
  telemetry_lock = threading.Lock()
321
  start_time = datetime.utcnow()
322
 
323
  @app.get("/health")
324
  async def health():
325
+ return {"status": "ok", "uptime_seconds": (datetime.utcnow() - start_time).total_seconds()}
326
 
327
  @app.get("/v1/models")
328
  async def list_models():
 
329
  return {
330
  "object": "list",
331
+ "data": [{"id": m, "object": "model",
332
+ "owned_by": MODEL_PROVIDER.get(m, "unknown")}
333
  for m in MODEL_REGISTRY]
334
  }
335
 
336
  @app.post("/v1/chat/completions")
337
  async def chat_completions(request: Request):
338
+ """OpenAI-compatible endpoint. ACO optimizations applied transparently."""
 
 
 
339
  body = await request.json()
340
+ import uuid
341
+ request_id = body.get("user", str(uuid.uuid4())[:8])
342
 
343
+ # ── Extract params ──
344
  messages = body.get("messages", [])
345
  tools = body.get("tools")
346
  requested_model = body.get("model", "gpt-5-mini")
347
  stream = body.get("stream", False)
348
+ original_model = requested_model
349
 
350
+ # ── Route model ──
351
  routed_model = route_model(requested_model, messages)
352
+ model_routed = routed_model != requested_model
353
  provider = MODEL_PROVIDER.get(routed_model, "openai")
354
 
355
+ # ── Gate tools (ML classifier with heuristic fallback) ──
356
  tools_gated = False
357
+ gated_by = "none"
358
+ if tools and should_gate_tools_ml(messages):
359
  tools = None
360
  tools_gated = True
361
+ gated_by = "ml" if _get_tool_gater() else "heuristic"
362
 
363
+ # ── Layout for cache ──
364
  laid_out = layout_cache_prompt(messages, tools)
365
+ cache_applied = laid_out != messages
366
 
367
+ # ── Compress context ──
368
  compressed_messages, compression_ratio = compress_context(laid_out)
369
 
370
+ # ── Forward request ──
371
  forward_body = {**body}
372
  forward_body["model"] = routed_model
373
  forward_body["messages"] = compressed_messages
374
  if tools is None and "tools" in forward_body:
375
  del forward_body["tools"]
376
 
 
377
  endpoint = PROVIDER_ENDPOINTS.get(provider, PROVIDER_ENDPOINTS["openai"])
378
  target_url = f"{endpoint}/chat/completions"
379
 
 
 
 
 
 
 
 
380
  api_key_map = {
381
+ "openai": os.environ.get("OPENAI_API_KEY"),
382
  "anthropic": os.environ.get("ANTHROPIC_API_KEY"),
383
  "google": os.environ.get("GOOGLE_API_KEY"),
384
  "deepseek": os.environ.get("DEEPSEEK_API_KEY"),
 
385
  }
386
+ auth = request.headers.get("authorization", "")
387
+ headers = {"content-type": "application/json"}
388
+ if not auth:
389
+ key = api_key_map.get(provider)
390
+ if key:
391
+ headers["authorization"] = f"Bearer {key}"
392
+ else:
393
+ headers["authorization"] = auth
394
 
395
+ # ── Make upstream call ──
396
  t_start = time.time()
397
  error = None
398
  success = True
399
+ response_data = {}
400
 
401
  try:
402
  async with httpx.AsyncClient(timeout=300.0) as client:
403
  upstream = await client.post(target_url, json=forward_body, headers=headers)
404
  latency = (time.time() - t_start) * 1000
405
 
406
+ if upstream.status_code != 200 and model_routed:
407
+ # Fall back to original model
408
+ forward_body["model"] = original_model
409
+ upstream2 = await client.post(target_url, json=forward_body, headers=headers)
410
+ latency = (time.time() - t_start) * 1000
411
+ upstream = upstream2
412
+ routed_model = original_model
413
+ model_routed = False
 
 
 
 
 
 
414
 
415
  if stream:
416
  return StreamingResponse(
417
  upstream.aiter_bytes(),
418
  media_type="text/event-stream",
419
+ headers={"x-aco-model": routed_model, "x-aco-tier": str(
420
+ MODEL_REGISTRY.get(routed_model, {}).get("tier", "?"))}
421
  )
422
 
423
+ if upstream.status_code == 200:
424
+ response_data = upstream.json()
425
+ else:
426
+ error = f"Upstream {upstream.status_code}: {upstream.text[:200]}"
427
+ success = False
428
 
429
  except Exception as e:
430
  latency = (time.time() - t_start) * 1000
431
  error = str(e)
432
  success = False
433
+
434
+ if not success:
435
  response_data = {
436
  "id": f"aco-err-{request_id}",
437
  "object": "chat.completion",
438
  "created": int(time.time()),
439
  "model": requested_model,
440
  "choices": [{"index": 0, "message": {"role": "assistant",
441
+ "content": f"[ACO proxy error: {error}]"},
442
  "finish_reason": "error"}],
443
  "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
444
  }
445
 
446
+ # ── Compute cost ──
447
  usage = response_data.get("usage", {})
448
  input_tokens = usage.get("prompt_tokens", 0)
449
  output_tokens = usage.get("completion_tokens", 0)
450
+ cache_hit = (usage.get("cache_read_input_tokens", 0) or
451
+ usage.get("prompt_tokens_details", {}).get("cached_tokens", 0))
452
+ cost = compute_cost(routed_model, input_tokens, output_tokens, cache_hit)
453
+
454
+ # ── Rewrite response ──
455
+ response_data.setdefault("usage", {})
456
+ response_data["usage"]["aco_cost_usd"] = cost
457
+ response_data["usage"]["aco_model"] = routed_model
458
+ response_data["usage"]["aco_tier"] = MODEL_REGISTRY.get(routed_model, {}).get("tier", 0)
459
+ response_data["usage"]["aco_cache_hit_tokens"] = cache_hit
460
+ response_data["usage"]["aco_compression_ratio"] = round(compression_ratio, 2)
461
+ response_data["usage"]["aco_tool_gated"] = tools_gated
462
+ response_data["model"] = requested_model # Agent sees original model
463
+
464
+ # ── Record telemetry ──
465
  trace = TraceRecord(
466
  request_id=request_id,
467
  timestamp=datetime.utcnow().isoformat(),
 
474
  latency_ms=round(latency, 1),
475
  cost=cost,
476
  tool_gated=tools_gated,
477
+ gated_by=gated_by,
478
  context_compressed=round(compression_ratio, 3),
479
+ cache_layout_applied=cache_applied,
480
+ model_routed=model_routed,
481
+ original_model=original_model,
482
  success=success,
483
  error=error,
484
  )
 
489
 
490
  @app.get("/dashboard")
491
  async def dashboard():
492
+ """Live HTML cost dashboard."""
493
  with telemetry_lock:
494
  traces = list(telemetry_store)
495
 
496
+ n = len(traces)
497
+ if n == 0:
498
+ return HTMLResponse("<h2>No traffic yet. Send requests to /v1/chat/completions</h2>")
499
+
500
  total_cost = sum(t.cost for t in traces)
 
501
  successful = sum(1 for t in traces if t.success)
502
+ total_in = sum(t.input_tokens for t in traces)
503
+ total_out = sum(t.output_tokens for t in traces)
504
  total_cache = sum(t.cache_hit_tokens for t in traces)
505
+ avg_lat = sum(t.latency_ms for t in traces) / n
 
 
 
 
 
 
 
 
 
506
  gated = sum(1 for t in traces if t.tool_gated)
507
+ routed = sum(1 for t in traces if t.model_routed)
508
 
509
+ tier_calls = defaultdict(int)
510
+ tier_cost = defaultdict(float)
511
+ model_calls = defaultdict(int)
512
  for t in traces:
513
+ tier_calls[t.tier] += 1
514
+ tier_cost[t.tier] += t.cost
515
+ model_calls[t.model] += 1
516
 
517
+ html = f"""<!DOCTYPE html><html><head>
518
+ <title>ACO Proxy</title><meta charset="utf-8"><meta http-equiv="refresh" content="3">
 
 
 
519
  <style>
520
+ * {{ margin:0; padding:0; box-sizing:border-box; }}
521
+ body {{ font-family: system-ui; background: #0d1117; color: #c9d1d9; padding: 1.5rem; }}
522
+ h1 {{ font-size: 1.2rem; margin-bottom: 0.5rem; }}
523
+ .grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px,1fr)); gap: 0.5rem; margin-bottom: 1rem; }}
524
+ .card {{ background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 0.8rem; }}
525
+ .card .val {{ font-size: 1.6rem; font-weight: 700; color: #58a6ff; }}
526
+ .card .lbl {{ font-size: 0.7rem; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }}
527
+ table {{ width: 100%; border-collapse: collapse; font-size: 0.8rem; margin-bottom: 1rem; }}
528
+ th, td {{ padding: 0.4rem 0.5rem; text-align: right; border-bottom: 1px solid #21262d; }}
529
+ th {{ color: #8b949e; font-weight: 500; text-transform: uppercase; font-size: 0.65rem; }}
530
+ td:first-child, th:first-child {{ text-align: left; }}
531
+ .good {{ color: #3fb950; }} .bad {{ color: #f85149; }} .dim {{ color: #8b949e; }}
532
+ </style></head><body>
533
+ <h1>🤖 ACO Proxy <span class="dim">— {n} calls, ${total_cost:.4f} total</span></h1>
 
 
534
  <div class="grid">
535
+ <div class="card"><div class="val">{successful/n*100:.0f}%</div><div class="lbl">Success Rate</div></div>
536
+ <div class="card"><div class="val">${total_cost:.4f}</div><div class="lbl">Total Cost</div></div>
537
+ <div class="card"><div class="val">${total_cost/max(n,1):.5f}</div><div class="lbl">Avg Cost/Call</div></div>
538
+ <div class="card"><div class="val">{avg_lat:.0f}ms</div><div class="lbl">Avg Latency</div></div>
539
+ <div class="card"><div class="val">{total_in//1000}k</div><div class="lbl">Tokens In</div></div>
540
+ <div class="card"><div class="val">{total_out//1000}k</div><div class="lbl">Tokens Out</div></div>
541
+ <div class="card"><div class="val">{total_cache//1000}k</div><div class="lbl">Cache Hits</div></div>
542
+ <div class="card"><div class="val">{gated}</div><div class="lbl">Tools Gated</div></div>
543
+ <div class="card"><div class="val">{routed}</div><div class="lbl">Models Rerouted</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  </div>
545
+ <table><tr><th>Model</th><th>Calls</th></tr>"""
546
+ for m, c in sorted(model_calls.items(), key=lambda x: -x[1]):
547
+ html += f"<tr><td>{m}</td><td>{c}</td></tr>"
548
+ html += """</table>
549
+ <table><tr><th>Time</th><th>Model</th><th>Tier</th><th>Tokens</th><th>Cost</th><th>Lat</th><th>Gated</th><th>Routed</th></tr>"""
550
+ for t in reversed(traces[-30:]):
551
+ s = 'good' if t.success else 'bad'
552
+ html += f"<tr><td class='dim'>{t.timestamp[-8:]}</td>"
553
+ html += f"<td>{t.model[:20]}</td><td>{t.tier}</td>"
554
+ html += f"<td>{t.input_tokens}+{t.output_tokens}</td>"
555
+ html += f"<td>${t.cost:.5f}</td><td>{t.latency_ms:.0f}ms</td>"
556
+ html += f"<td>{'✓' if t.tool_gated else ''}</td>"
557
+ html += f"<td>{'✓' if t.model_routed else ''}</td></tr>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  html += "</table></body></html>"
559
  return HTMLResponse(html)
560
 
561
  @app.get("/telemetry")
562
+ async def telemetry_json():
563
+ """JSON telemetry for programmatic consumption."""
564
  with telemetry_lock:
565
+ traces = [{"model": t.model, "tier": t.tier, "cost": t.cost,
566
+ "input_tokens": t.input_tokens, "output_tokens": t.output_tokens,
567
+ "cache_hit_tokens": t.cache_hit_tokens, "latency_ms": t.latency_ms,
568
+ "tool_gated": t.tool_gated, "gated_by": t.gated_by,
569
+ "model_routed": t.model_routed, "original_model": t.original_model,
570
+ "context_compressed": t.context_compressed,
571
+ "success": t.success, "error": t.error,
572
+ "timestamp": t.timestamp}
573
+ for t in telemetry_store]
 
 
 
 
 
 
 
 
 
574
  return {
575
  "total_calls": len(traces),
576
+ "total_cost": round(sum(t["cost"] for t in traces), 6),
577
  "calls": traces,
 
 
 
 
 
 
 
 
578
  }
579
 
580
  @app.get("/telemetry/reset")
581
  async def reset_telemetry():
 
582
  with telemetry_lock:
583
  telemetry_store.clear()
584
+ return {"status": "ok"}
585
 
586
 
587
  def serve(host: str = "0.0.0.0", port: int = 8080):
 
588
  if not FASTAPI_AVAILABLE:
589
+ print("ERROR: pip install fastapi uvicorn httpx")
590
  return
591
+ print(f"🚀 ACO Proxy http://{host}:{port}")
592
+ print(f" Dashboard: http://localhost:{port}/dashboard")
593
+ print(f" Telemetry: http://localhost:{port}/telemetry")
594
+ print(f" Agent usage: openai.api_base = 'http://localhost:{port}/v1'")
595
+ uvicorn.run(app, host=host, port=port, log_level="warning")
596
 
597
 
 
 
598
  def main():
599
  import argparse
600
+ p = argparse.ArgumentParser(description="ACO Proxy Server")
601
+ p.add_argument("--host", default="0.0.0.0")
602
+ p.add_argument("--port", type=int, default=8080)
603
+ serve(**vars(p.parse_args()))
 
 
604
 
605
  if __name__ == "__main__":
606
  main()