Spaces:
Paused
Paused
| import logging | |
| import os | |
| import time | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, Request, HTTPException | |
| from fastapi.responses import StreamingResponse, JSONResponse | |
| from config import ( | |
| UPSTREAM_URL, SUPPORTED_MODELS, DEFAULT_MODEL, | |
| UPSTREAM_TIMEOUT_SECS, UPSTREAM_CONNECT_TIMEOUT_SECS, | |
| UPSTREAM_MAX_CONNECTIONS, UPSTREAM_MAX_KEEPALIVE_CONNECTIONS, | |
| UPSTREAM_KEEPALIVE_EXPIRY_SECS, UPSTREAM_PROXY_URL, | |
| ) | |
| from response_cache import get_cache_service | |
| from translate import ( | |
| pick_model, build_payload, | |
| build_upstream_messages_anthropic, build_upstream_messages_openai, | |
| ) | |
| from upstream import create_upstream_client, close_upstream_client, fetch_completion_artifact | |
| from render import ( | |
| render_anthropic_json_from_artifact, render_openai_json_from_artifact, | |
| anthropic_stream_from_artifact, openai_stream_from_artifact, | |
| anthropic_aggregate, openai_aggregate, | |
| ) | |
| from stream import ( | |
| anthropic_stream_plain, anthropic_stream_with_tools, | |
| openai_stream_plain, openai_stream_with_tools, | |
| build_cache_headers, build_stream_headers, | |
| wait_for_inflight_artifact, finalize_stream_cache, | |
| ) | |
| from upstream import LiveArtifactCapture | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s %(levelname)s [%(name)s] %(message)s", | |
| datefmt="%Y-%m-%d %H:%M:%S", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| async def lifespan(_: FastAPI): | |
| client = create_upstream_client() | |
| logger.info("upstream client created (proxy=%s)", bool(UPSTREAM_PROXY_URL)) | |
| try: | |
| yield | |
| finally: | |
| await close_upstream_client() | |
| logger.info("upstream client closed") | |
| app = FastAPI(title="p5js.ai 2 API (Anthropic + OpenAI)", lifespan=lifespan) | |
| async def health(): | |
| cache_service = get_cache_service() | |
| return { | |
| "status": "ok", | |
| "service": "p5js.ai-2api", | |
| "endpoints": ["/v1/messages", "/v1/chat/completions", "/v1/models"], | |
| "models": sorted(SUPPORTED_MODELS), | |
| "tool_use": "pseudo (XML-based, upstream does not support native tool_use)", | |
| "cache": cache_service.describe(), | |
| "upstream": { | |
| "url": UPSTREAM_URL, | |
| "proxy_configured": bool(UPSTREAM_PROXY_URL or os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY")), | |
| "timeout_secs": UPSTREAM_TIMEOUT_SECS, | |
| "connect_timeout_secs": UPSTREAM_CONNECT_TIMEOUT_SECS, | |
| "max_connections": UPSTREAM_MAX_CONNECTIONS, | |
| "max_keepalive_connections": UPSTREAM_MAX_KEEPALIVE_CONNECTIONS, | |
| "keepalive_expiry_secs": UPSTREAM_KEEPALIVE_EXPIRY_SECS, | |
| }, | |
| } | |
| async def list_models(): | |
| now = int(time.time()) | |
| return { | |
| "data": [ | |
| {"id": m, "object": "model", "created": now, "owned_by": "anthropic"} | |
| for m in sorted(SUPPORTED_MODELS) | |
| ], | |
| "object": "list", | |
| } | |
| async def anthropic_messages(request: Request): | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| raise HTTPException(status_code=400, detail="invalid json body") | |
| model = pick_model(body.get("model")) | |
| stream = bool(body.get("stream", False)) | |
| tools = body.get("tools") or None | |
| has_tools = bool(tools) | |
| upstream_messages = build_upstream_messages_anthropic(body.get("system"), body.get("messages", []), tools) | |
| payload = build_payload(model, upstream_messages) | |
| cache_service = get_cache_service() | |
| bypass_cache = cache_service.should_bypass(request.headers) | |
| if bypass_cache: | |
| await cache_service.record_bypass() | |
| live_status = "MISS" if cache_service.config.enabled and not bypass_cache else ("BYPASS" if bypass_cache else "DISABLED") | |
| cache_key = cache_service.build_key( | |
| protocol_family="anthropic", | |
| resolved_model=model, | |
| upstream_messages=upstream_messages, | |
| auth_scope=cache_service.auth_scope_from_headers(request.headers), | |
| has_tools=has_tools, | |
| ) | |
| ttl_secs = cache_service.config.ttl_for(has_tools) | |
| if cache_service.config.enabled and not bypass_cache: | |
| lookup = await cache_service.get(cache_key) | |
| if lookup.artifact is not None: | |
| logger.debug("anthropic cache HIT key=%s source=%s", cache_key[:24], lookup.source) | |
| if stream: | |
| return StreamingResponse( | |
| anthropic_stream_from_artifact(lookup.artifact, has_tools), | |
| media_type="text/event-stream", | |
| headers=build_stream_headers("HIT", lookup.source), | |
| ) | |
| return JSONResponse( | |
| render_anthropic_json_from_artifact(lookup.artifact, has_tools), | |
| headers=build_cache_headers("HIT", lookup.source), | |
| ) | |
| is_leader, future = await cache_service.inflight.start(cache_key) | |
| if not is_leader: | |
| await cache_service.record_inflight_wait() | |
| artifact = await wait_for_inflight_artifact(future) | |
| if artifact is not None: | |
| if stream: | |
| return StreamingResponse( | |
| anthropic_stream_from_artifact(artifact, has_tools), | |
| media_type="text/event-stream", | |
| headers=build_stream_headers("HIT", "inflight"), | |
| ) | |
| return JSONResponse( | |
| render_anthropic_json_from_artifact(artifact, has_tools), | |
| headers=build_cache_headers("HIT", "inflight"), | |
| ) | |
| else: | |
| if stream: | |
| capture = LiveArtifactCapture(model) | |
| live_stream = anthropic_stream_with_tools(payload, capture) if has_tools else anthropic_stream_plain(payload, capture) | |
| return StreamingResponse( | |
| finalize_stream_cache(live_stream, capture, cache_key, ttl_secs), | |
| media_type="text/event-stream", | |
| headers=build_stream_headers("MISS", "live"), | |
| ) | |
| try: | |
| artifact = await fetch_completion_artifact(payload) | |
| await cache_service.set(cache_key, artifact, ttl_secs) | |
| await cache_service.inflight.resolve(cache_key, artifact) | |
| except Exception as exc: | |
| await cache_service.inflight.reject(cache_key, exc) | |
| raise | |
| return JSONResponse( | |
| render_anthropic_json_from_artifact(artifact, has_tools), | |
| headers=build_cache_headers("MISS", "live"), | |
| ) | |
| if stream: | |
| gen = anthropic_stream_with_tools(payload) if has_tools else anthropic_stream_plain(payload) | |
| return StreamingResponse( | |
| gen, | |
| media_type="text/event-stream", | |
| headers=build_stream_headers(live_status, "live"), | |
| ) | |
| return JSONResponse( | |
| await anthropic_aggregate(payload, has_tools), | |
| headers=build_cache_headers(live_status, "live"), | |
| ) | |
| async def openai_chat_completions(request: Request): | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| raise HTTPException(status_code=400, detail="invalid json body") | |
| requested_model = body.get("model") or DEFAULT_MODEL | |
| model = pick_model(requested_model) | |
| stream = bool(body.get("stream", False)) | |
| tools = body.get("tools") or None | |
| has_tools = bool(tools) | |
| upstream_messages = build_upstream_messages_openai(body.get("messages", []), tools) | |
| payload = build_payload(model, upstream_messages) | |
| cache_service = get_cache_service() | |
| bypass_cache = cache_service.should_bypass(request.headers) | |
| if bypass_cache: | |
| await cache_service.record_bypass() | |
| live_status = "MISS" if cache_service.config.enabled and not bypass_cache else ("BYPASS" if bypass_cache else "DISABLED") | |
| cache_key = cache_service.build_key( | |
| protocol_family="openai", | |
| resolved_model=model, | |
| upstream_messages=upstream_messages, | |
| auth_scope=cache_service.auth_scope_from_headers(request.headers), | |
| has_tools=has_tools, | |
| ) | |
| ttl_secs = cache_service.config.ttl_for(has_tools) | |
| if cache_service.config.enabled and not bypass_cache: | |
| lookup = await cache_service.get(cache_key) | |
| if lookup.artifact is not None: | |
| logger.debug("openai cache HIT key=%s source=%s", cache_key[:24], lookup.source) | |
| if stream: | |
| return StreamingResponse( | |
| openai_stream_from_artifact(lookup.artifact, has_tools), | |
| media_type="text/event-stream", | |
| headers=build_stream_headers("HIT", lookup.source), | |
| ) | |
| return JSONResponse( | |
| render_openai_json_from_artifact(lookup.artifact, has_tools), | |
| headers=build_cache_headers("HIT", lookup.source), | |
| ) | |
| is_leader, future = await cache_service.inflight.start(cache_key) | |
| if not is_leader: | |
| await cache_service.record_inflight_wait() | |
| artifact = await wait_for_inflight_artifact(future) | |
| if artifact is not None: | |
| if stream: | |
| return StreamingResponse( | |
| openai_stream_from_artifact(artifact, has_tools), | |
| media_type="text/event-stream", | |
| headers=build_stream_headers("HIT", "inflight"), | |
| ) | |
| return JSONResponse( | |
| render_openai_json_from_artifact(artifact, has_tools), | |
| headers=build_cache_headers("HIT", "inflight"), | |
| ) | |
| else: | |
| if stream: | |
| capture = LiveArtifactCapture(model) | |
| live_stream = openai_stream_with_tools(payload, requested_model, capture) if has_tools else openai_stream_plain(payload, requested_model, capture) | |
| return StreamingResponse( | |
| finalize_stream_cache(live_stream, capture, cache_key, ttl_secs), | |
| media_type="text/event-stream", | |
| headers=build_stream_headers("MISS", "live"), | |
| ) | |
| try: | |
| artifact = await fetch_completion_artifact(payload) | |
| await cache_service.set(cache_key, artifact, ttl_secs) | |
| await cache_service.inflight.resolve(cache_key, artifact) | |
| except Exception as exc: | |
| await cache_service.inflight.reject(cache_key, exc) | |
| raise | |
| return JSONResponse( | |
| render_openai_json_from_artifact(artifact, has_tools), | |
| headers=build_cache_headers("MISS", "live"), | |
| ) | |
| if stream: | |
| gen = openai_stream_with_tools(payload, requested_model) if has_tools else openai_stream_plain(payload, requested_model) | |
| return StreamingResponse( | |
| gen, | |
| media_type="text/event-stream", | |
| headers=build_stream_headers(live_status, "live"), | |
| ) | |
| return JSONResponse( | |
| await openai_aggregate(payload, requested_model, has_tools), | |
| headers=build_cache_headers(live_status, "live"), | |
| ) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=18185) | |