Block A: License-Check + Credits-Tracking fuer Enhance und Chat

- Neuer Helper charge_usage_to_tenant() in services/license_service.py:
  UPSERT in token_usage_monthly und Credits-Abzug aus licenses.credits_used.
  Wiederverwendbar fuer alle Claude-Call-Verursacher.
- Orchestrator: Inline-Buchungslogik (35 Zeilen) durch Helper-Aufruf ersetzt.
- routers/incidents.py POST /enhance-description: require_writable_license
  statt get_current_user, db_dependency hinzugefuegt, Credits-Buchung mit
  source="enhance" nach jedem Claude-Call.
- routers/chat.py POST /: analog require_writable_license + Credits-Buchung
  mit source="chat". _call_claude_chat() gibt jetzt zusaetzlich ClaudeUsage
  zurueck.

Abgelaufene/gesperrte Lizenzen koennen damit keine Haiku-Calls mehr ausloesen,
und alle Kosten werden konsistent auf Tenant-Ebene verbucht.
Dieser Commit ist enthalten in:
claude-dev
2026-04-23 17:49:32 +00:00
Ursprung c8a8e10020
Commit e8ac0d0c50
4 geänderte Dateien mit 119 neuen und 40 gelöschten Zeilen

Datei anzeigen

@@ -12,6 +12,11 @@ from pydantic import BaseModel, Field
from auth import get_current_user
from config import CLAUDE_PATH, CLAUDE_MODEL_FAST
from database import db_dependency
from middleware.license_check import require_writable_license
from services.license_service import charge_usage_to_tenant
from agents.claude_client import ClaudeUsage
import aiosqlite
logger = logging.getLogger("osint.chat")
@@ -21,8 +26,8 @@ router = APIRouter(tags=["chat"])
# Claude CLI Aufruf (Chat-spezifisch, kein JSON-Modus)
# ---------------------------------------------------------------------------
async def _call_claude_chat(prompt: str) -> tuple[str, int]:
"""Ruft Claude CLI fuer Chat auf. Gibt (text, duration_ms) zurueck.
async def _call_claude_chat(prompt: str) -> tuple[str, int, ClaudeUsage]:
"""Ruft Claude CLI fuer Chat auf. Gibt (text, duration_ms, usage) zurueck.
Anders als call_claude(): kein JSON-Output-Modus, kein append-system-prompt.
"""
@@ -62,21 +67,29 @@ async def _call_claude_chat(prompt: str) -> tuple[str, int]:
raw = stdout.decode("utf-8", errors="replace").strip()
duration_ms = 0
result_text = raw
usage = ClaudeUsage()
try:
data = _json.loads(raw)
result_text = data.get("result", raw)
duration_ms = data.get("duration_ms", 0)
cost = data.get("total_cost_usd", 0.0)
u = data.get("usage", {})
usage = ClaudeUsage(
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),
cost_usd=data.get("total_cost_usd", 0.0),
duration_ms=duration_ms,
)
logger.info(
f"Chat Claude: {u.get('input_tokens', 0)} in / {u.get('output_tokens', 0)} out / "
f"${cost:.4f} / {duration_ms}ms"
f"Chat Claude: {usage.input_tokens} in / {usage.output_tokens} out / "
f"${usage.cost_usd:.4f} / {duration_ms}ms"
)
except _json.JSONDecodeError:
logger.warning("Chat Claude CLI Antwort kein JSON, nutze raw output")
return result_text, duration_ms
return result_text, duration_ms, usage
# ---------------------------------------------------------------------------
# Models
@@ -395,7 +408,8 @@ def _build_prompt(user_message: str, history: list[dict]) -> str:
@router.post("", response_model=ChatResponse)
async def chat(
req: ChatRequest,
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_writable_license),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Chat-Nachricht verarbeiten und Antwort generieren."""
user_id = current_user["id"]
@@ -420,7 +434,7 @@ async def chat(
# Claude CLI aufrufen
try:
result, duration_ms = await _call_claude_chat(prompt)
result, duration_ms, usage = await _call_claude_chat(prompt)
except TimeoutError:
raise HTTPException(status_code=504, detail="Der Assistent antwortet gerade nicht. Bitte versuche es erneut.")
except RuntimeError as e:
@@ -430,6 +444,10 @@ async def chat(
logger.error(f"Chat Claude-Fehler: {e}")
raise HTTPException(status_code=502, detail="Der Assistent ist voruebergehend nicht erreichbar.")
# Credits buchen
await charge_usage_to_tenant(db, current_user.get("tenant_id"), usage, source="chat")
await db.commit()
# Output sanitieren
reply = _sanitize_output(result)
if not reply: