feat: Token-Tracking fuer VLM-Bildanalyse
Usage-Daten (input/output/cache tokens, theoretische USD-Kosten) werden jetzt wie beim Monitor aus der Claude CLI-Antwort extrahiert und in token_usage_monthly geschrieben. Credits werden auf der Lizenz abgezogen. Verwaltungsportal zeigt die Zahlen automatisch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -9,8 +9,11 @@ from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from data_geoint import extract_exif
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
||||
from pydantic import BaseModel
|
||||
from auth import get_current_user
|
||||
from database import get_db
|
||||
|
||||
logger = logging.getLogger("globe.vlm")
|
||||
router = APIRouter()
|
||||
@@ -378,15 +381,30 @@ async def _run_claude(image_path: str, viewport_info: str = "", filename: str =
|
||||
try:
|
||||
wrapper = json.loads(stdout.decode())
|
||||
|
||||
# Usage extrahieren (theoretische Token-Zahlen wie beim Monitor)
|
||||
u = wrapper.get("usage", {})
|
||||
usage = {
|
||||
"input_tokens": u.get("input_tokens", 0),
|
||||
"output_tokens": u.get("output_tokens", 0),
|
||||
"cache_creation_tokens": u.get("cache_creation_input_tokens", 0),
|
||||
"cache_read_tokens": u.get("cache_read_input_tokens", 0),
|
||||
"total_cost_usd": wrapper.get("total_cost_usd", 0.0),
|
||||
}
|
||||
logger.info(
|
||||
f"VLM-Usage: {usage['input_tokens']} in / {usage['output_tokens']} out / "
|
||||
f"cache {usage['cache_creation_tokens']}+{usage['cache_read_tokens']} / "
|
||||
f"${usage['total_cost_usd']:.4f}"
|
||||
)
|
||||
|
||||
# --json-schema liefert structured_output (bevorzugt)
|
||||
structured = wrapper.get("structured_output")
|
||||
if structured and isinstance(structured, dict):
|
||||
return structured
|
||||
return structured, usage
|
||||
|
||||
# Fallback: result als JSON-String parsen
|
||||
result_str = wrapper.get("result", "")
|
||||
if isinstance(result_str, str) and result_str.strip().startswith("{"):
|
||||
return json.loads(result_str)
|
||||
return json.loads(result_str), usage
|
||||
|
||||
# Fallback: Claude hat Klartext statt JSON geliefert
|
||||
logger.warning(f"VLM: Kein strukturiertes JSON, Klartext: {result_str[:200]}")
|
||||
@@ -395,7 +413,7 @@ async def _run_claude(image_path: str, viewport_info: str = "", filename: str =
|
||||
"objects": [],
|
||||
"terrain": "unknown",
|
||||
"estimated_location_type": "unknown",
|
||||
}
|
||||
}, usage
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.error(f"VLM JSON-Parse-Fehler: {e}, stdout={stdout.decode()[:500]}")
|
||||
raise HTTPException(502, "VLM-Analyse: Unerwartetes Antwortformat")
|
||||
@@ -405,6 +423,8 @@ async def _run_claude(image_path: str, viewport_info: str = "", filename: str =
|
||||
async def analyze_image(
|
||||
file: UploadFile = File(...),
|
||||
viewport_info: str = Form(""),
|
||||
user: dict = Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""Analysiert ein hochgeladenes Bild mittels Claude Code VLM."""
|
||||
# Validierung
|
||||
@@ -439,7 +459,7 @@ async def analyze_image(
|
||||
logger.info(f"EXIF GPS: {exif_data['latitude']}, {exif_data['longitude']}")
|
||||
|
||||
# Claude Code aufrufen (Dateiname + EXIF als Kontext, NICHT Viewport)
|
||||
result = await _run_claude(resized_path, filename=file.filename, exif_data=exif_data)
|
||||
result, usage = await _run_claude(resized_path, filename=file.filename, exif_data=exif_data)
|
||||
|
||||
# EXIF-Daten zum Ergebnis hinzufuegen
|
||||
result["exif"] = exif_data
|
||||
@@ -452,6 +472,46 @@ async def analyze_image(
|
||||
f"Koordinaten: {ec.get('latitude')}/{ec.get('longitude')} (r={ec.get('confidence_radius_km')}km), "
|
||||
f"Gewaesser: {idf.get('water_body')}, Region: {idf.get('specific_region')}"
|
||||
)
|
||||
|
||||
# Token-Tracking: Usage in DB schreiben (wie Monitor-Orchestrator)
|
||||
org_id = user.get("organization_id")
|
||||
if org_id and usage.get("total_cost_usd", 0) > 0:
|
||||
try:
|
||||
year_month = datetime.now(timezone.utc).strftime('%Y-%m')
|
||||
await db.execute("""
|
||||
INSERT INTO token_usage_monthly
|
||||
(organization_id, year_month, input_tokens, output_tokens,
|
||||
cache_creation_tokens, cache_read_tokens, total_cost_usd, api_calls, refresh_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, 1)
|
||||
ON CONFLICT(organization_id, year_month) DO UPDATE SET
|
||||
input_tokens = input_tokens + excluded.input_tokens,
|
||||
output_tokens = output_tokens + excluded.output_tokens,
|
||||
cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,
|
||||
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
||||
total_cost_usd = total_cost_usd + excluded.total_cost_usd,
|
||||
api_calls = api_calls + excluded.api_calls,
|
||||
refresh_count = refresh_count + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", (org_id, year_month,
|
||||
usage["input_tokens"], usage["output_tokens"],
|
||||
usage["cache_creation_tokens"], usage["cache_read_tokens"],
|
||||
round(usage["total_cost_usd"], 7)))
|
||||
|
||||
# Credits auf Lizenz abziehen
|
||||
lic_cursor = await db.execute(
|
||||
"SELECT cost_per_credit FROM licenses WHERE organization_id = ? AND status = 'active' ORDER BY id DESC LIMIT 1",
|
||||
(org_id,))
|
||||
lic = await lic_cursor.fetchone()
|
||||
if lic and lic["cost_per_credit"] and lic["cost_per_credit"] > 0:
|
||||
credits_consumed = usage["total_cost_usd"] / lic["cost_per_credit"]
|
||||
await db.execute(
|
||||
"UPDATE licenses SET credits_used = COALESCE(credits_used, 0) + ? WHERE organization_id = ? AND status = 'active'",
|
||||
(round(credits_consumed, 2), org_id))
|
||||
logger.info(f"VLM Credits: {round(credits_consumed, 2)} abgezogen fuer Org {org_id}")
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"VLM Token-Tracking Fehler: {e}")
|
||||
|
||||
return result
|
||||
|
||||
finally:
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren