chatgpt2api / api /ai.py
tx1538's picture
Upload 179 files
9d7ddb9 verified
Raw
History Blame
6.93 kB
from __future__ import annotations
from fastapi import APIRouter, File, Form, Header, HTTPException, Request, UploadFile
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel, ConfigDict, Field
from api.support import require_identity, resolve_image_base_url
from services.content_filter import check_request, request_text
from services.log_service import LoggedCall
from services.protocol import (
anthropic_v1_messages,
openai_v1_chat_complete,
openai_v1_image_edit,
openai_v1_image_generations,
openai_v1_models,
openai_v1_response,
)
class ImageGenerationRequest(BaseModel):
prompt: str = Field(..., min_length=1)
model: str = "gpt-image-2"
n: int = Field(default=1, ge=1, le=4)
size: str | None = None
response_format: str = "b64_json"
history_disabled: bool = True
stream: bool | None = None
class ChatCompletionRequest(BaseModel):
model_config = ConfigDict(extra="allow")
model: str | None = None
prompt: str | None = None
n: int | None = None
stream: bool | None = None
modalities: list[str] | None = None
messages: list[dict[str, object]] | None = None
class ResponseCreateRequest(BaseModel):
model_config = ConfigDict(extra="allow")
model: str | None = None
input: object | None = None
tools: list[dict[str, object]] | None = None
tool_choice: object | None = None
stream: bool | None = None
class AnthropicMessageRequest(BaseModel):
model_config = ConfigDict(extra="allow")
model: str | None = None
messages: list[dict[str, object]] | None = None
system: object | None = None
stream: bool | None = None
async def filter_or_log(call: LoggedCall, text: str) -> None:
try:
await run_in_threadpool(check_request, text)
except HTTPException as exc:
call.log("调用失败", status="failed", error=str(exc.detail))
raise
def create_router() -> APIRouter:
router = APIRouter()
@router.get("/v1/models")
async def list_models(authorization: str | None = Header(default=None)):
require_identity(authorization)
try:
return await run_in_threadpool(openai_v1_models.list_models)
except Exception as exc:
raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc
@router.post("/v1/images/generations")
async def generate_images(
body: ImageGenerationRequest,
request: Request,
authorization: str | None = Header(default=None),
):
identity = require_identity(authorization)
payload = body.model_dump(mode="python")
payload["base_url"] = resolve_image_base_url(request)
call = LoggedCall(identity, "/v1/images/generations", body.model, "文生图", request_text=body.prompt)
await filter_or_log(call, body.prompt)
return await call.run(openai_v1_image_generations.handle, payload)
@router.post("/v1/images/edits")
async def edit_images(
request: Request,
authorization: str | None = Header(default=None),
image: list[UploadFile] | None = File(default=None),
image_list: list[UploadFile] | None = File(default=None, alias="image[]"),
prompt: str = Form(...),
model: str = Form(default="gpt-image-2"),
n: int = Form(default=1),
size: str | None = Form(default=None),
response_format: str = Form(default="b64_json"),
stream: bool | None = Form(default=None),
):
identity = require_identity(authorization)
call = LoggedCall(identity, "/v1/images/edits", model, "图生图", request_text=prompt)
if n < 1 or n > 4:
raise HTTPException(status_code=400, detail={"error": "n must be between 1 and 4"})
await filter_or_log(call, prompt)
uploads = [*(image or []), *(image_list or [])]
if not uploads:
raise HTTPException(status_code=400, detail={"error": "image file is required"})
images: list[tuple[bytes, str, str]] = []
for upload in uploads:
image_data = await upload.read()
if not image_data:
raise HTTPException(status_code=400, detail={"error": "image file is empty"})
images.append((image_data, upload.filename or "image.png", upload.content_type or "image/png"))
payload = {
"prompt": prompt,
"images": images,
"model": model,
"n": n,
"size": size,
"response_format": response_format,
"stream": stream,
"base_url": resolve_image_base_url(request),
}
return await call.run(openai_v1_image_edit.handle, payload)
@router.post("/v1/chat/completions")
async def create_chat_completion(body: ChatCompletionRequest, authorization: str | None = Header(default=None)):
identity = require_identity(authorization)
payload = body.model_dump(mode="python")
model = str(payload.get("model") or "auto")
request_preview = request_text(payload.get("prompt"), payload.get("messages"))
call = LoggedCall(identity, "/v1/chat/completions", model, "文本生成", request_text=request_preview)
await filter_or_log(call, request_preview)
return await call.run(openai_v1_chat_complete.handle, payload)
@router.post("/v1/responses")
async def create_response(body: ResponseCreateRequest, authorization: str | None = Header(default=None)):
identity = require_identity(authorization)
payload = body.model_dump(mode="python")
model = str(payload.get("model") or "auto")
request_preview = request_text(payload.get("input"), payload.get("instructions"))
call = LoggedCall(identity, "/v1/responses", model, "Responses", request_text=request_preview)
await filter_or_log(call, request_preview)
return await call.run(openai_v1_response.handle, payload)
@router.post("/v1/messages")
async def create_message(
body: AnthropicMessageRequest,
authorization: str | None = Header(default=None),
x_api_key: str | None = Header(default=None, alias="x-api-key"),
anthropic_version: str | None = Header(default=None, alias="anthropic-version"),
):
identity = require_identity(authorization or (f"Bearer {x_api_key}" if x_api_key else None))
payload = body.model_dump(mode="python")
model = str(payload.get("model") or "auto")
request_preview = request_text(payload.get("system"), payload.get("messages"), payload.get("tools"))
call = LoggedCall(identity, "/v1/messages", model, "Messages", request_text=request_preview)
await filter_or_log(call, request_preview)
return await call.run(anthropic_v1_messages.handle, payload, sse="anthropic")
return router