diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e94d16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Leopold + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/agent_platform/api.py b/agent_platform/api.py index 555ccaf..5b2ec1e 100644 --- a/agent_platform/api.py +++ b/agent_platform/api.py @@ -8,7 +8,7 @@ from typing import Optional, Dict, Any, List from contextlib import asynccontextmanager from pathlib import Path -from fastapi import FastAPI, Depends, HTTPException, Request +from fastapi import FastAPI, Depends, HTTPException, Request, Response from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -198,10 +198,10 @@ async def api_delete_agent( # Code-Agents: nur deaktivieren, damit sie beim nächsten Sync wieder da sind await set_agent_enabled(db, agent_id, False) await log_audit(db, agent_id=agent_id, user_id=user_id, action="agent_disabled_via_delete", target=agent_id) - return {"status": "disabled", "note": "Code-Agents werden nicht gelöscht, nur deaktiviert"} + return Response(status_code=204) await delete_agent(db, agent_id) await log_audit(db, agent_id=agent_id, user_id=user_id, action="agent_deleted", target=agent_id) - return {"status": "deleted"} + return Response(status_code=204) @app.post("/api/agents/{agent_id}/enable") diff --git a/agent_platform/db.py b/agent_platform/db.py index a3a48d0..1ff109d 100644 --- a/agent_platform/db.py +++ b/agent_platform/db.py @@ -8,7 +8,7 @@ import aiosqlite import json import uuid import secrets -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from config import settings @@ -267,7 +267,7 @@ async def list_audit(db, agent_id=None, limit=100): async def create_session(db, user_id="default", ttl_hours=24): token = secrets.token_urlsafe(32) - expires = (datetime.utcnow() + timedelta(hours=ttl_hours)).isoformat() + expires = (datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(hours=ttl_hours)).isoformat() await db.execute( "INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)", (token, user_id, expires) @@ -284,6 +284,6 @@ async def validate_session(db, token): row = await cur.fetchone() if not row: return None - if row[1] and datetime.fromisoformat(row[1]) < datetime.utcnow(): + if row[1] and datetime.fromisoformat(row[1]) < datetime.now(timezone.utc).replace(tzinfo=None): return None return row[0] diff --git a/agent_platform/llm.py b/agent_platform/llm.py index e8eaf90..90bc5c7 100644 --- a/agent_platform/llm.py +++ b/agent_platform/llm.py @@ -72,10 +72,20 @@ async def chat( usage = {} if hasattr(response, "usage") and response.usage: + # LiteLLM exposes cached prompt tokens via prompt_tokens_details.cached_tokens + # (see litellm.types.utils.Usage). Providers like Anthropic/OpenAI surface this + # when prompt caching is active; surfacing it here flows through LLMResponse.usage + # to any audit log that serialises the usage dict. + cached_tokens = 0 + details = getattr(response.usage, "prompt_tokens_details", None) + if details is not None: + cached_tokens = getattr(details, "cached_tokens", None) or 0 + usage = { "prompt_tokens": response.usage.prompt_tokens or 0, "completion_tokens": response.usage.completion_tokens or 0, "total_tokens": response.usage.total_tokens or 0, + "cached_tokens": cached_tokens, "finish_reason": response.choices[0].finish_reason or "stop", }