Download api/ai.py from zlcy/abc: direct link, hf CLI and curl.
- Browser
- Download file 49.8 kB
-
https://huggingface.co/spaces/zlcy/abc/resolve/main/api/ai.py
- Command line
-
hf download hf://spaces/zlcy/abc/api/ai.py
-
curl -L -o ai.py https://huggingface.co/spaces/zlcy/abc/resolve/main/api/ai.py
49.8 kB
| from __future__ import annotations | |
| import mimetypes | |
| from decimal import Decimal, ROUND_HALF_UP | |
| from typing import Any | |
| from fastapi import APIRouter, Header, HTTPException, Request | |
| from fastapi.concurrency import run_in_threadpool | |
| from fastapi.responses import FileResponse, StreamingResponse | |
| from pydantic import BaseModel, ConfigDict, Field | |
| from curl_cffi import requests as curl_requests | |
| from api.image_inputs import parse_image_edit_request, read_image_sources | |
| from api.support import require_identity, resolve_image_base_url | |
| from services.auth_service import DAILY_GIFT_ACCOUNT_POOL_MODEL_ID, auth_service | |
| from services.chat_model_service import resolve_chat_models | |
| from services.image_model_service import resolve_image_models | |
| from services.system_model_service import resolve_system_models | |
| from services.content_filter import check_request, request_text | |
| from services.config import config | |
| from services.editable_file_task_service import editable_file_task_service | |
| from services.image_annotation_service import identify_image_region | |
| from services.log_service import LoggedCall | |
| from services.pool_pricing_service import pool_pricing_service | |
| from services.skill_service import skill_service | |
| 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, | |
| openai_search, | |
| ) | |
| from services.third_party_image_service import third_party_image_service | |
| from utils.helper import has_response_image_generation_tool, is_image_chat_request, parse_image_count | |
| 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 | |
| quality: str | None = None | |
| resolution: str | None = None | |
| image_resolution: str | None = None | |
| third_party_image_group_id: str | None = None | |
| response_format: str = "url" | |
| account_pool_strategy: str = "own_first" | |
| 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 | |
| class PromptOptimizationRequest(BaseModel): | |
| prompt: str = Field(..., min_length=1, max_length=30000) | |
| class ReversePromptRequest(BaseModel): | |
| image: str = Field(..., min_length=1, max_length=40_000_000) | |
| class ImageRegionBounds(BaseModel): | |
| x: float = Field(..., ge=0, le=100) | |
| y: float = Field(..., ge=0, le=100) | |
| width: float = Field(..., gt=0, le=100) | |
| height: float = Field(..., gt=0, le=100) | |
| class ImageRegionRecognitionRequest(BaseModel): | |
| image: str = Field(..., min_length=1, max_length=40_000_000) | |
| bounds: ImageRegionBounds | |
| class SearchRequest(BaseModel): | |
| prompt: str = Field(..., min_length=1) | |
| class EditableFileTaskRequest(BaseModel): | |
| prompt: str = "" | |
| base64_images: list[str] = Field(default_factory=list) | |
| client_task_id: str | 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 _to_positive_image_count(value: object, default: int = 1) -> int: | |
| try: | |
| return max(1, int(value or default)) | |
| except (TypeError, ValueError): | |
| return default | |
| def _credit_amount(value: object) -> int | float: | |
| try: | |
| amount = max(Decimal("0"), Decimal(str(value or 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) | |
| except Exception: | |
| amount = Decimal("0") | |
| return int(amount) if amount == amount.to_integral_value() else float(amount) | |
| def _third_party_activity_quote(identity: dict[str, object], quantity: int, *, group_id: str, base_unit_price: object) -> dict[str, object]: | |
| pricing_config = config.normal_user_third_party_image_pricing | |
| if str(identity.get("role") or "").strip().lower() != "normal" or not bool(pricing_config.get("enabled")): | |
| base = _credit_amount(base_unit_price) | |
| return { | |
| "applies": False, | |
| "target_type": "third_party_group", | |
| "third_party_group_id": group_id, | |
| "base_unit_price": base, | |
| "pay_percentage": 100, | |
| "unit_price": base, | |
| "quantity": quantity, | |
| "estimated_total": _credit_amount(float(base) * quantity), | |
| "rule_id": "", | |
| "rule_name": "", | |
| } | |
| return pool_pricing_service.quote( | |
| identity, | |
| quantity=quantity, | |
| pricing_config=pricing_config, | |
| target_type="third_party_group", | |
| third_party_group_id=group_id, | |
| base_unit_price=base_unit_price, | |
| ) | |
| def _third_party_wallet_remaining(identity: dict[str, object], group_id: str, model_id: str) -> int | float: | |
| try: | |
| summary = auth_service.credit_summary(identity) | |
| except Exception: | |
| return 0 | |
| wallets = summary.get("third_party_image_wallets") if isinstance(summary, dict) else [] | |
| wallet_key = f"{group_id}:{model_id}" | |
| wallet_items = wallets if isinstance(wallets, list) else [] | |
| balances: list[int | float] = [] | |
| for wallet in wallet_items: | |
| if not isinstance(wallet, dict): | |
| continue | |
| if str(wallet.get("id") or "") == wallet_key or ( | |
| str(wallet.get("group_id") or "") == group_id | |
| and str(wallet.get("model_id") or "").strip() == model_id | |
| ): | |
| try: | |
| balances.append(_credit_amount(wallet.get("remaining"))) | |
| except (TypeError, ValueError): | |
| balances.append(0) | |
| return max(balances) if balances else 0 | |
| def _count_data_images(value: object) -> int: | |
| if not isinstance(value, list): | |
| return 0 | |
| return sum( | |
| 1 | |
| for item in value | |
| if isinstance(item, dict) and (str(item.get("b64_json") or "").strip() or str(item.get("url") or "").strip()) | |
| ) | |
| def _count_image_generation_items(value: object) -> int: | |
| if not isinstance(value, list): | |
| return 0 | |
| count = 0 | |
| for item in value: | |
| if not isinstance(item, dict): | |
| continue | |
| if str(item.get("type") or "") == "image_generation_call" and str(item.get("result") or "").strip(): | |
| count += 1 | |
| return count | |
| def _count_data_url_images(text: object) -> int: | |
| return str(text or "").count("data:image/") | |
| def _count_successful_images(value: object) -> int: | |
| if not isinstance(value, dict): | |
| return 0 | |
| count = _count_data_images(value.get("data")) | |
| if count: | |
| return count | |
| output = value.get("output") | |
| count = _count_image_generation_items(output) | |
| if count: | |
| return count | |
| item = value.get("item") | |
| if isinstance(item, dict): | |
| count = _count_image_generation_items([item]) | |
| if count: | |
| return count | |
| choices = value.get("choices") | |
| if isinstance(choices, list): | |
| return sum( | |
| _count_data_url_images( | |
| ((choice.get("message") if isinstance(choice, dict) else {}) or {}).get("content") | |
| if isinstance(choice, dict) | |
| else "" | |
| ) | |
| + _count_data_url_images( | |
| ((choice.get("delta") if isinstance(choice, dict) else {}) or {}).get("content") | |
| if isinstance(choice, dict) | |
| else "" | |
| ) | |
| for choice in choices | |
| ) | |
| response = value.get("response") | |
| if isinstance(response, dict): | |
| return _count_successful_images(response) | |
| return 0 | |
| def _attach_account_pool(payload: dict[str, object], identity: dict[str, object]) -> None: | |
| has_personal_pool = identity.get("role") == "admin" or bool(identity.get("account_pool_enabled")) | |
| payload["account_owner_id"] = str(identity.get("id") or "").strip() | |
| if not has_personal_pool: | |
| payload["account_pool_strategy"] = "admin_only" | |
| payload["account_allow_shared"] = False | |
| return | |
| payload["account_pool_strategy"] = str(payload.get("account_pool_strategy") or "own_first") | |
| payload["account_allow_shared"] = bool(payload.get("account_allow_shared", True)) | |
| def _chat_completion_text(value: object) -> str: | |
| if not isinstance(value, dict): | |
| return "" | |
| choices = value.get("choices") | |
| if not isinstance(choices, list) or not choices or not isinstance(choices[0], dict): | |
| return "" | |
| message = choices[0].get("message") | |
| if not isinstance(message, dict): | |
| return "" | |
| content = message.get("content") | |
| if isinstance(content, str): | |
| return content.strip() | |
| if not isinstance(content, list): | |
| return "" | |
| return "".join( | |
| str(item.get("text") or "") | |
| for item in content | |
| if isinstance(item, dict) and str(item.get("type") or "") in {"text", "output_text"} | |
| ).strip() | |
| def _response_text(value: object) -> str: | |
| if not isinstance(value, dict): | |
| return "" | |
| direct = value.get("output_text") | |
| if isinstance(direct, str) and direct.strip(): | |
| return direct.strip() | |
| output = value.get("output") | |
| if not isinstance(output, list): | |
| return "" | |
| parts: list[str] = [] | |
| for item in output: | |
| if not isinstance(item, dict): | |
| continue | |
| content = item.get("content") | |
| if not isinstance(content, list): | |
| continue | |
| parts.extend( | |
| str(part.get("text") or "") | |
| for part in content | |
| if isinstance(part, dict) and str(part.get("type") or "") in {"text", "output_text"} | |
| ) | |
| return "".join(parts).strip() | |
| def _split_grants(grants: list[dict[str, object]], amount: int | float) -> tuple[list[dict[str, object]], list[dict[str, object]]]: | |
| remaining = Decimal(str(_credit_amount(amount))) | |
| used: list[dict[str, object]] = [] | |
| refunded: list[dict[str, object]] = [] | |
| for grant in grants: | |
| if not isinstance(grant, dict): | |
| continue | |
| grant_amount = Decimal(str(_credit_amount(grant.get("amount")))) | |
| grant_id = str(grant.get("id") or "").strip() | |
| if not grant_id or grant_amount <= 0: | |
| continue | |
| take = min(grant_amount, remaining) | |
| if take > 0: | |
| used.append({"id": grant_id, "amount": _credit_amount(take)}) | |
| if grant_amount - take > 0: | |
| refunded.append({"id": grant_id, "amount": _credit_amount(grant_amount - take)}) | |
| remaining -= take | |
| return used, refunded | |
| class CreditReservation: | |
| def __init__(self, user_id: str = "", amount: int | float = 0): | |
| self.user_id = user_id | |
| self.amount = _credit_amount(amount) | |
| self.daily_amount: int | float = 0 | |
| self.paid_amount: int | float = 0 | |
| self.package_reservations: list[dict[str, object]] = [] | |
| self.grant_reservations: list[dict[str, object]] = [] | |
| self.settled = False | |
| def reserve( | |
| cls, | |
| identity: dict[str, object], | |
| amount: int | float, | |
| *, | |
| model_id: str = DAILY_GIFT_ACCOUNT_POOL_MODEL_ID, | |
| ) -> "CreditReservation": | |
| if str(identity.get("role") or "").strip().lower() != "normal": | |
| return cls() | |
| reservation = cls(str(identity.get("id") or "").strip(), amount) | |
| if not reservation.user_id or reservation.amount <= 0: | |
| return reservation | |
| details = auth_service.reserve_credits( | |
| reservation.user_id, | |
| reservation.amount, | |
| model_id=model_id, | |
| ).get("_credit_reservation", {}) | |
| if not isinstance(details, dict): | |
| details = {} | |
| reservation.daily_amount = _credit_amount(details.get("daily")) | |
| reservation.paid_amount = _credit_amount(details.get("permanent_paid") if "permanent_paid" in details else details.get("paid")) | |
| reservation.package_reservations = details.get("packages") if isinstance(details.get("packages"), list) else [] | |
| reservation.grant_reservations = details.get("grants") if isinstance(details.get("grants"), list) else [] | |
| return reservation | |
| def settle(self, consume_amount: int | float) -> None: | |
| if self.settled: | |
| return | |
| if not self.user_id or self.amount <= 0: | |
| self.settled = True | |
| return | |
| consume_count = _credit_amount(min(float(self.amount), float(_credit_amount(consume_amount)))) | |
| daily_to_consume = _credit_amount(min(float(self.daily_amount), float(consume_count))) | |
| remaining = _credit_amount(float(consume_count) - float(daily_to_consume)) | |
| packages_to_consume, packages_to_refund = _split_grants(self.package_reservations, remaining) | |
| remaining = _credit_amount(float(remaining) - sum(float(_credit_amount(item.get("amount"))) for item in packages_to_consume)) | |
| grants_to_consume, grants_to_refund = _split_grants(self.grant_reservations, remaining) | |
| remaining = _credit_amount(float(remaining) - sum(float(_credit_amount(item.get("amount"))) for item in grants_to_consume)) | |
| paid_to_consume = _credit_amount(min(float(self.paid_amount), float(remaining))) | |
| daily_to_refund = _credit_amount(float(self.daily_amount) - float(daily_to_consume)) | |
| paid_to_refund = _credit_amount(float(self.paid_amount) - float(paid_to_consume)) | |
| if consume_count > 0: | |
| auth_service.consume_reserved_credits( | |
| self.user_id, | |
| consume_count, | |
| daily_amount=daily_to_consume, | |
| paid_amount=paid_to_consume, | |
| package_reservations=packages_to_consume, | |
| grant_reservations=grants_to_consume, | |
| ) | |
| refund_count = _credit_amount(float(self.amount) - float(consume_count)) | |
| if refund_count > 0: | |
| auth_service.refund_reserved_credits( | |
| self.user_id, | |
| refund_count, | |
| daily_amount=daily_to_refund, | |
| paid_amount=paid_to_refund, | |
| package_reservations=packages_to_refund, | |
| grant_reservations=grants_to_refund, | |
| ) | |
| self.settled = True | |
| class InternalImageModelDailyLimitReservation: | |
| def __init__(self, *, user_id: str = "", model_id: str = "", amount: int = 0, scope: str = ""): | |
| self.user_id = user_id | |
| self.model_id = model_id | |
| self.amount = max(0, int(amount or 0)) | |
| self.scope = scope | |
| self.settled = False | |
| def reserve( | |
| cls, | |
| identity: dict[str, object], | |
| model_id: object, | |
| amount: int, | |
| ) -> "InternalImageModelDailyLimitReservation": | |
| if str(identity.get("role") or "").strip().lower() != "user": | |
| return cls() | |
| user_id = str(identity.get("id") or "").strip() | |
| normalized_model_id = str(model_id or "gpt-image-2").strip() or "gpt-image-2" | |
| if not user_id: | |
| return cls() | |
| result = auth_service.reserve_internal_image_model_daily_limit( | |
| user_id, | |
| model_id=normalized_model_id, | |
| amount=amount, | |
| ) | |
| reservation = result.get("_internal_image_model_daily_limit_reservation") if isinstance(result, dict) else {} | |
| reserved_amount = max(0, int(reservation.get("amount") or 0)) if isinstance(reservation, dict) else 0 | |
| reserved_scope = str(reservation.get("scope") or "").strip() if isinstance(reservation, dict) else "" | |
| return cls(user_id=user_id, model_id=normalized_model_id, amount=reserved_amount, scope=reserved_scope) | |
| def settle(self, success_count: int) -> None: | |
| if self.settled: | |
| return | |
| success_amount = min(self.amount, max(0, int(success_count or 0))) | |
| refund_amount = self.amount - success_amount | |
| if self.user_id and self.model_id and success_amount > 0: | |
| consume_kwargs: dict[str, object] = { | |
| "model_id": self.model_id, | |
| "amount": success_amount, | |
| } | |
| if self.scope: | |
| consume_kwargs["scope"] = self.scope | |
| auth_service.consume_internal_image_model_daily_limit( | |
| self.user_id, | |
| **consume_kwargs, | |
| ) | |
| if self.user_id and self.model_id and refund_amount > 0: | |
| refund_kwargs: dict[str, object] = { | |
| "model_id": self.model_id, | |
| "amount": refund_amount, | |
| } | |
| if self.scope: | |
| refund_kwargs["scope"] = self.scope | |
| auth_service.refund_internal_image_model_daily_limit( | |
| self.user_id, | |
| **refund_kwargs, | |
| ) | |
| self.settled = True | |
| class ImageBillingReservation: | |
| def __init__( | |
| self, | |
| *, | |
| mode: str = "", | |
| user_id: str = "", | |
| amount: int | float = 0, | |
| per_image_cost: int | float = 1, | |
| group_id: str = "", | |
| model_id: str = "", | |
| credit_reservation: CreditReservation | None = None, | |
| internal_limit_reservation: InternalImageModelDailyLimitReservation | None = None, | |
| pricing: dict[str, object] | None = None, | |
| ): | |
| self.mode = mode | |
| self.user_id = user_id | |
| self.amount = _credit_amount(amount) | |
| self.per_image_cost = _credit_amount(per_image_cost) | |
| self.group_id = group_id | |
| self.model_id = model_id | |
| self.credit_reservation = credit_reservation | |
| self.internal_limit_reservation = internal_limit_reservation | |
| self.pricing = dict(pricing or {}) | |
| self.actual = 0 | |
| self.refund = 0 | |
| self.log_detail: dict[str, object] = {} | |
| if self.pricing: | |
| self.log_detail.update({ | |
| "billing_base_unit_price": self.pricing.get("base_unit_price", 1), | |
| "billing_effective_unit_price": self.pricing.get("unit_price", 1), | |
| "billing_pay_percentage": self.pricing.get("pay_percentage", 100), | |
| "billing_rule_id": self.pricing.get("rule_id", ""), | |
| "billing_rule_name": self.pricing.get("rule_name", ""), | |
| "billing_estimated": self.pricing.get("estimated_total", self.amount), | |
| "billing_actual": 0, | |
| "billing_refund": 0, | |
| "billing_priced_at": self.pricing.get("priced_at") or self.pricing.get("evaluated_at", ""), | |
| }) | |
| self.settled = False | |
| def reserve(cls, identity: dict[str, object], payload: dict[str, object], *, requested_amount: int | None = None) -> "ImageBillingReservation": | |
| amount = _to_positive_image_count(requested_amount if requested_amount is not None else payload.get("n"), 1) | |
| resolution = str(payload.get("resolution") or payload.get("image_resolution") or "").strip().lower() | |
| group_id = str(payload.get("third_party_image_group_id") or "").strip() | |
| use_third_party = resolution in {"1k", "2k", "4k"} or bool(group_id) | |
| route = None | |
| if not use_third_party: | |
| find_named_route = getattr(third_party_image_service, "find_named_route", None) | |
| route = find_named_route(payload.get("model"), advance=False) if callable(find_named_route) else None | |
| use_third_party = route is not None | |
| if not use_third_party: | |
| model_id = str(payload.get("model") or "gpt-image-2").strip() or "gpt-image-2" | |
| is_normal = str(identity.get("role") or "").strip().lower() == "normal" | |
| pricing = pool_pricing_service.quote(identity, quantity=amount) if is_normal else {} | |
| estimated_total = _credit_amount(pricing.get("estimated_total", amount)) | |
| return cls( | |
| mode="general", | |
| credit_reservation=CreditReservation.reserve( | |
| identity, | |
| estimated_total, | |
| model_id=DAILY_GIFT_ACCOUNT_POOL_MODEL_ID, | |
| ), | |
| internal_limit_reservation=InternalImageModelDailyLimitReservation.reserve(identity, model_id, amount), | |
| amount=estimated_total, | |
| per_image_cost=_credit_amount(pricing.get("unit_price", 1)), | |
| pricing=pricing, | |
| ) | |
| require_4k = resolution == "4k" | |
| if route is None and str(identity.get("role") or "").strip().lower() == "normal": | |
| find_wallet_route = getattr(third_party_image_service, "find_wallet_route_for_identity", None) | |
| if callable(find_wallet_route): | |
| route = find_wallet_route( | |
| identity, | |
| payload.get("model"), | |
| group_id=group_id, | |
| require_4k=require_4k, | |
| resolution=resolution, | |
| advance=False, | |
| ) | |
| if route is None: | |
| route = third_party_image_service.find_route( | |
| payload.get("model"), | |
| group_id=group_id, | |
| require_4k=require_4k, | |
| resolution=resolution or "1k", | |
| advance=False, | |
| ) | |
| if route is None: | |
| raise ValueError("No enabled image API model route") | |
| try: | |
| third_party_image_service.assert_access(identity, route) | |
| except PermissionError as exc: | |
| raise PermissionError(str(exc)) from exc | |
| payload["_third_party_image"] = True | |
| payload["_third_party_image_group_id"] = str(route.group.get("id") or "") | |
| payload["third_party_image_group_id"] = str(route.group.get("id") or "") | |
| payload["resolution"] = str(route.model.get("resolution") or resolution or "1k") | |
| per_image_cost = max(0, third_party_image_service.cost_for_route(route, payload.get("quality") or payload.get("image_quality"), 1)) | |
| total_cost = per_image_cost * amount | |
| group_id = str(route.group.get("id") or "") | |
| model_id = str(route.model.get("id") or "") | |
| billing_mode = str(route.group.get("billing_mode") or "") | |
| if billing_mode == "general_multiplier" and _third_party_wallet_remaining(identity, group_id, model_id) > 0: | |
| # A stale group setting must not strand an existing model-specific | |
| # dedicated balance. Treat it as "both" until that balance is used. | |
| billing_mode = "both" | |
| internal_limit_reservation = InternalImageModelDailyLimitReservation.reserve(identity, model_id, amount) | |
| if str(identity.get("role") or "").strip().lower() != "normal": | |
| return cls( | |
| mode=billing_mode, | |
| amount=0, | |
| per_image_cost=per_image_cost, | |
| group_id=group_id, | |
| model_id=model_id, | |
| internal_limit_reservation=internal_limit_reservation, | |
| ) | |
| user_id = str(identity.get("id") or "").strip() | |
| wallet_pricing = _third_party_activity_quote(identity, amount, group_id=group_id, base_unit_price=1) | |
| wallet_total_cost = _credit_amount(wallet_pricing.get("estimated_total", amount)) | |
| wallet_unit_cost = _credit_amount(wallet_pricing.get("unit_price", 1)) | |
| if billing_mode == "dedicated_wallet" or ( | |
| billing_mode == "both" and per_image_cost > 0 and _third_party_wallet_remaining(identity, group_id, model_id) >= wallet_total_cost | |
| ): | |
| if wallet_total_cost > 0: | |
| try: | |
| auth_service.reserve_third_party_image_wallet( | |
| user_id, | |
| group_id=group_id, | |
| model_id=model_id, | |
| amount=wallet_total_cost, | |
| ) | |
| except ValueError: | |
| if billing_mode != "both": | |
| raise | |
| else: | |
| return cls( | |
| mode="dedicated_wallet", | |
| user_id=user_id, | |
| amount=wallet_total_cost, | |
| per_image_cost=wallet_unit_cost, | |
| group_id=group_id, | |
| model_id=model_id, | |
| internal_limit_reservation=internal_limit_reservation, | |
| pricing=wallet_pricing, | |
| ) | |
| else: | |
| return cls( | |
| mode="dedicated_wallet", | |
| user_id=user_id, | |
| amount=0, | |
| per_image_cost=0, | |
| group_id=group_id, | |
| model_id=model_id, | |
| internal_limit_reservation=internal_limit_reservation, | |
| pricing=wallet_pricing, | |
| ) | |
| credit_pricing = _third_party_activity_quote(identity, amount, group_id=group_id, base_unit_price=per_image_cost) | |
| total_cost = _credit_amount(credit_pricing.get("estimated_total", total_cost)) | |
| effective_unit_cost = _credit_amount(credit_pricing.get("unit_price", per_image_cost)) | |
| return cls( | |
| mode=billing_mode if billing_mode == "both" else "general_multiplier", | |
| amount=total_cost, | |
| per_image_cost=effective_unit_cost, | |
| credit_reservation=CreditReservation.reserve( | |
| identity, | |
| total_cost, | |
| model_id=f"{group_id}:{model_id}", | |
| ), | |
| internal_limit_reservation=internal_limit_reservation, | |
| group_id=group_id, | |
| model_id=model_id, | |
| pricing=credit_pricing, | |
| ) | |
| def settle(self, success_count: int) -> None: | |
| if self.settled: | |
| return | |
| if self.internal_limit_reservation is not None: | |
| self.internal_limit_reservation.settle(success_count) | |
| success_cost = _credit_amount(min(float(self.amount), max(0, int(success_count or 0)) * float(self.per_image_cost))) | |
| if self.mode == "dedicated_wallet" and self.user_id and self.amount > 0: | |
| if success_cost > 0: | |
| auth_service.consume_third_party_image_wallet( | |
| self.user_id, | |
| group_id=self.group_id, | |
| model_id=self.model_id, | |
| amount=success_cost, | |
| ) | |
| refund = self.amount - success_cost | |
| if refund > 0: | |
| auth_service.refund_third_party_image_wallet( | |
| self.user_id, | |
| group_id=self.group_id, | |
| model_id=self.model_id, | |
| amount=refund, | |
| ) | |
| elif self.credit_reservation is not None: | |
| self.credit_reservation.settle(success_cost) | |
| self.actual = success_cost | |
| self.refund = _credit_amount(float(self.amount) - float(success_cost)) | |
| if self.pricing: | |
| self.log_detail["billing_actual"] = self.actual | |
| self.log_detail["billing_refund"] = self.refund | |
| self.settled = True | |
| async def run_metered_image_call( | |
| call: LoggedCall, | |
| handler, | |
| payload: dict[str, object], | |
| identity: dict[str, object], | |
| *, | |
| requested_amount: int | None = None, | |
| ): | |
| try: | |
| reservation = ImageBillingReservation.reserve(identity, payload, requested_amount=requested_amount) | |
| except PermissionError as exc: | |
| raise HTTPException(status_code=403, detail={"error": str(exc)}) from exc | |
| except ValueError as exc: | |
| raise HTTPException(status_code=402, detail={"error": str(exc)}) from exc | |
| call.billing_detail = reservation.log_detail | |
| stream_success_count = 0 | |
| def on_stream_item(item: object) -> None: | |
| nonlocal stream_success_count | |
| stream_success_count += _count_successful_images(item) | |
| def on_stream_complete() -> None: | |
| reservation.settle(stream_success_count) | |
| def on_stream_error(_exc: BaseException) -> None: | |
| reservation.settle(stream_success_count) | |
| try: | |
| result = await call.run( | |
| handler, | |
| payload, | |
| on_stream_item=on_stream_item, | |
| on_stream_complete=on_stream_complete, | |
| on_stream_error=on_stream_error, | |
| on_result=lambda value: reservation.settle(_count_successful_images(value)), | |
| ) | |
| except Exception: | |
| reservation.settle(0) | |
| raise | |
| if isinstance(result, StreamingResponse): | |
| return result | |
| reservation.settle(_count_successful_images(result)) | |
| return result | |
| def _external_prompt_optimization(prompt: str) -> tuple[str, str]: | |
| base_url = config.prompt_optimizer_base_url.rstrip("/") | |
| model = config.prompt_optimizer_model | |
| if not base_url: | |
| raise ValueError("第三方提示词优化 Base URL 不能为空") | |
| endpoint = base_url if base_url.endswith("/chat/completions") else f"{base_url}/chat/completions" | |
| headers = {"Content-Type": "application/json"} | |
| if config.prompt_optimizer_api_key: | |
| headers["Authorization"] = f"Bearer {config.prompt_optimizer_api_key}" | |
| response = curl_requests.post( | |
| endpoint, | |
| headers=headers, | |
| json={ | |
| "model": model, | |
| "stream": False, | |
| "messages": [ | |
| {"role": "system", "content": config.prompt_optimizer_system_prompt}, | |
| { | |
| "role": "user", | |
| "content": "只优化下面的原始提示词,保持原语言并仅返回改写后的提示词:\n" + prompt, | |
| }, | |
| ], | |
| }, | |
| timeout=60, | |
| ) | |
| if response.status_code >= 400: | |
| raise RuntimeError(f"第三方提示词优化接口返回 HTTP {response.status_code}: {response.text[:500]}") | |
| data = response.json() | |
| text = _chat_completion_text(data) if isinstance(data, dict) else "" | |
| if not text: | |
| raise RuntimeError("第三方提示词优化接口未返回有效内容") | |
| return text, model | |
| def _external_reverse_prompt(image: str) -> tuple[str, str]: | |
| base_url = config.reverse_prompt_base_url.rstrip("/") | |
| model = config.reverse_prompt_model | |
| if not base_url: | |
| raise ValueError("第三方反推提示词 Base URL 不能为空") | |
| endpoint = base_url if base_url.endswith("/chat/completions") else f"{base_url}/chat/completions" | |
| headers = {"Content-Type": "application/json"} | |
| if config.reverse_prompt_api_key: | |
| headers["Authorization"] = f"Bearer {config.reverse_prompt_api_key}" | |
| instruction = config.reverse_prompt_system_prompt | |
| response = curl_requests.post( | |
| endpoint, | |
| headers=headers, | |
| json={ | |
| "model": model, | |
| "stream": False, | |
| "messages": [ | |
| {"role": "system", "content": instruction}, | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "text", "text": instruction}, | |
| {"type": "image_url", "image_url": {"url": image}}, | |
| ], | |
| }, | |
| ], | |
| }, | |
| timeout=120, | |
| ) | |
| if response.status_code >= 400: | |
| raise RuntimeError(f"第三方反推提示词接口返回 HTTP {response.status_code}: {response.text[:500]}") | |
| data = response.json() | |
| text = _chat_completion_text(data) if isinstance(data, dict) else "" | |
| if not text: | |
| raise RuntimeError("第三方反推提示词接口未返回有效内容") | |
| return text, model | |
| def collect_models_result(identity: dict[str, object]) -> dict[str, Any]: | |
| """上游账号池模型 + 第三方接入模型,合并且按 id 去重后的完整目录。 | |
| `/v1/models`(对外 OpenAI 兼容)与 `/api/chat/models`(对话页、 | |
| 管理页的「对话可用模型」)必须用同一份目录,否则两边口径会不一致—— | |
| 之前 `/api/chat/models` 只取 `openai_v1_models.list_models()`, | |
| 漏掉了在这里额外拼接的第三方模型,导致第三方模型在对话候选里根本不出现。 | |
| """ | |
| result = openai_v1_models.list_models() | |
| data = result.get("data") if isinstance(result, dict) else None | |
| if not isinstance(data, list): | |
| return result | |
| seen = {str(item.get("id") or "").strip() for item in data if isinstance(item, dict)} | |
| for item in third_party_image_service.public_models_for_identity(identity): | |
| model_id = str(item.get("id") or "").strip() | |
| if not model_id or model_id in seen: | |
| continue | |
| data.append(item) | |
| seen.add(model_id) | |
| return result | |
| def create_router() -> APIRouter: | |
| router = APIRouter() | |
| async def list_models(authorization: str | None = Header(default=None)): | |
| identity = require_identity(authorization) | |
| try: | |
| return await run_in_threadpool(collect_models_result, identity) | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| async def list_chat_models(authorization: str | None = Header(default=None)): | |
| """对话页模型下拉 / 管理页「对话可用模型」的数据源。 | |
| `/v1/models` 是给外部 API 客户端用的 OpenAI 兼容接口,必须保持全量 | |
| (含第三方生图模型),所以这里另开一个接口:全量候选 + 每个模型的 | |
| `chat_enabled` 标记(自动规则叠加人工覆盖)。 | |
| 候选包含第三方接入模型——它们默认隐藏(生图模型),但可以被手动开启; | |
| 将来第三方接入文本 / 多模态模型时,只要不带生图能力就会默认出现在对话下拉里。 | |
| """ | |
| identity = require_identity(authorization) | |
| try: | |
| result = await run_in_threadpool(collect_models_result, identity) | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| items = result.get("data") if isinstance(result, dict) else None | |
| overrides = config.chat_models | |
| data = resolve_chat_models(items if isinstance(items, list) else [], overrides) | |
| return { | |
| "object": "list", | |
| "data": data, | |
| "include": overrides["include"], | |
| "exclude": overrides["exclude"], | |
| } | |
| async def list_image_models(authorization: str | None = Header(default=None)): | |
| """生图页模型下拉 / 管理页「生图可用模型」的数据源。 | |
| 只列出能生图的模型(账号池上游内置生图模型 + 第三方接入生图模型), | |
| 每个带 `image_enabled` 标记。默认全部启用 —— 这里的开关只是展示层过滤, | |
| 不改动第三方生图配置(计费 / 路由 / 额度 / 对外 API 均不受影响)。 | |
| """ | |
| identity = require_identity(authorization) | |
| try: | |
| result = await run_in_threadpool(collect_models_result, identity) | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| items = result.get("data") if isinstance(result, dict) else None | |
| overrides = config.image_models | |
| data = resolve_image_models(items if isinstance(items, list) else [], overrides) | |
| return { | |
| "object": "list", | |
| "data": data, | |
| "include": overrides["include"], | |
| "exclude": overrides["exclude"], | |
| } | |
| async def list_system_models(authorization: str | None = Header(default=None)): | |
| """管理页「系统可用模型」的数据源。 | |
| 与 `/v1/models` 同一份全量目录(上游账号池 + 第三方接入), | |
| 额外带上 `system_enabled` 标记:管理员可以把不关心的模型从这张列表里收起来。 | |
| 注意这**只是展示层过滤**:`/v1/models` 本身仍保持全量, | |
| 上游账号池、第三方配置、计费与路由都不受影响。 | |
| """ | |
| identity = require_identity(authorization) | |
| try: | |
| result = await run_in_threadpool(collect_models_result, identity) | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| items = result.get("data") if isinstance(result, dict) else None | |
| overrides = config.system_models | |
| data = resolve_system_models(items if isinstance(items, list) else [], overrides) | |
| return { | |
| "object": "list", | |
| "data": data, | |
| "include": overrides["include"], | |
| "exclude": overrides["exclude"], | |
| } | |
| 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) | |
| _attach_account_pool(payload, identity) | |
| call = LoggedCall(identity, "/v1/images/generations", body.model, "文生图", request_text=body.prompt) | |
| await filter_or_log(call, body.prompt) | |
| return await run_metered_image_call(call, openai_v1_image_generations.handle, payload, identity) | |
| async def edit_images( | |
| request: Request, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| payload, image_sources, mask_sources = await parse_image_edit_request(request) | |
| prompt = str(payload["prompt"]) | |
| model = str(payload["model"]) | |
| call = LoggedCall(identity, "/v1/images/edits", model, "图生图", request_text=prompt) | |
| await filter_or_log(call, prompt) | |
| payload["images"] = await read_image_sources(image_sources) | |
| if mask_sources: | |
| payload["mask"] = await read_image_sources(mask_sources) | |
| payload["base_url"] = resolve_image_base_url(request) | |
| _attach_account_pool(payload, identity) | |
| return await run_metered_image_call(call, openai_v1_image_edit.handle, payload, identity) | |
| async def create_chat_completion( | |
| body: ChatCompletionRequest, | |
| request: Request, | |
| 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")) | |
| is_image_request = is_image_chat_request(payload) | |
| payload["base_url"] = resolve_image_base_url(request) | |
| if is_image_request: | |
| _attach_account_pool(payload, identity) | |
| call = LoggedCall(identity, "/v1/chat/completions", model, "图片生成" if is_image_request else "文本生成", request_text=request_preview) | |
| if is_image_request: | |
| # AI review protects image accounts. Plain chat is also used by | |
| # internal Skill controllers and image planners; reviewing their | |
| # system protocols creates false positives and never protects an | |
| # image-account call because no image account is used here. | |
| await filter_or_log(call, request_preview) | |
| return await run_metered_image_call( | |
| call, | |
| openai_v1_chat_complete.handle, | |
| payload, | |
| identity, | |
| requested_amount=parse_image_count(payload.get("n")), | |
| ) | |
| return await call.run(openai_v1_chat_complete.handle, payload, identity) | |
| async def optimize_prompt( | |
| body: PromptOptimizationRequest, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| prompt = body.prompt.strip() | |
| if not prompt: | |
| raise HTTPException(status_code=400, detail={"error": "提示词不能为空"}) | |
| if config.prompt_optimizer_provider == "external": | |
| try: | |
| optimized_prompt, model = await run_in_threadpool(_external_prompt_optimization, prompt) | |
| except (ValueError, RuntimeError) as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| return {"prompt": optimized_prompt, "model": model} | |
| payload = { | |
| "model": config.prompt_optimizer_model, | |
| "stream": False, | |
| "messages": [ | |
| {"role": "system", "content": config.prompt_optimizer_system_prompt}, | |
| { | |
| "role": "user", | |
| "content": ( | |
| f"{config.prompt_optimizer_system_prompt}\n\n" | |
| "Improve only the original prompt below. Preserve its language exactly and return only the rewritten prompt.\n" | |
| f"<original_prompt>\n{prompt}\n</original_prompt>" | |
| ), | |
| }, | |
| ], | |
| } | |
| call = LoggedCall(identity, "/api/prompt/optimize", config.prompt_optimizer_model, "提示词优化", request_text=prompt) | |
| result = await call.run(openai_v1_chat_complete.handle, payload, identity) | |
| if not isinstance(result, dict): | |
| return result | |
| optimized_prompt = _chat_completion_text(result) | |
| if not optimized_prompt: | |
| raise HTTPException(status_code=502, detail={"error": "模型未返回优化后的提示词"}) | |
| return {"prompt": optimized_prompt, "model": config.prompt_optimizer_model} | |
| async def reverse_prompt( | |
| body: ReversePromptRequest, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| image = body.image.strip() | |
| header, separator, encoded = image.partition(",") | |
| if not separator or not header.startswith("data:image/") or ";base64" not in header or not encoded.strip(): | |
| raise HTTPException(status_code=400, detail={"error": "图片数据无效"}) | |
| if config.reverse_prompt_provider == "external": | |
| try: | |
| prompt, model = await run_in_threadpool(_external_reverse_prompt, image) | |
| except (ValueError, RuntimeError) as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| return {"prompt": prompt, "model": model} | |
| instruction = config.reverse_prompt_system_prompt | |
| model = config.reverse_prompt_model | |
| payload = { | |
| "model": model, | |
| "stream": False, | |
| "instructions": instruction, | |
| "input": [ | |
| {"type": "input_text", "text": instruction}, | |
| {"type": "input_image", "image_url": image}, | |
| ], | |
| } | |
| call = LoggedCall(identity, "/api/prompt/reverse", model, "反推提示词", request_text="参考图片") | |
| result = await call.run(openai_v1_response.handle, payload) | |
| if not isinstance(result, dict): | |
| return result | |
| prompt = _response_text(result) | |
| if not prompt: | |
| raise HTTPException(status_code=502, detail={"error": "模型未返回反推提示词"}) | |
| return {"prompt": prompt, "model": model} | |
| async def identify_annotation_region( | |
| body: ImageRegionRecognitionRequest, | |
| authorization: str | None = Header(default=None), | |
| ): | |
| identity = require_identity(authorization) | |
| try: | |
| return await run_in_threadpool( | |
| identify_image_region, | |
| identity, | |
| body.image, | |
| body.bounds.model_dump(mode="python"), | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc | |
| except RuntimeError as exc: | |
| raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc | |
| async def create_response( | |
| body: ResponseCreateRequest, | |
| request: Request, | |
| 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")) | |
| is_image_request = has_response_image_generation_tool(payload) | |
| if is_image_request: | |
| payload["base_url"] = resolve_image_base_url(request) | |
| _attach_account_pool(payload, identity) | |
| call = LoggedCall(identity, "/v1/responses", model, "Responses 图片生成" if is_image_request else "Responses", request_text=request_preview) | |
| if is_image_request: | |
| await filter_or_log(call, request_preview) | |
| return await run_metered_image_call( | |
| call, | |
| openai_v1_response.handle, | |
| payload, | |
| identity, | |
| requested_amount=1, | |
| ) | |
| return await call.run(openai_v1_response.handle, payload) | |
| 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) | |
| return await call.run(anthropic_v1_messages.handle, payload, sse="anthropic") | |
| async def search(body: SearchRequest, authorization: str | None = Header(default=None)): | |
| identity = require_identity(authorization) | |
| call = LoggedCall(identity, "/v1/search", openai_search.MODEL, "搜索", request_text=body.prompt) | |
| return await call.run(openai_search.handle, body.model_dump(mode="python")) | |
| async def list_editable_file_tasks(ids: str = "", authorization: str | None = Header(default=None)): | |
| identity = require_identity(authorization) | |
| task_ids = [item.strip() for item in ids.split(",") if item.strip()] | |
| return await run_in_threadpool(editable_file_task_service.list_tasks, identity, task_ids) | |
| async def download_editable_file(file_path: str, download: bool = False): | |
| if file_path.startswith("skills/"): | |
| await run_in_threadpool(skill_service.cleanup_expired_runs) | |
| try: | |
| path = await run_in_threadpool(editable_file_task_service.public_file_path, file_path) | |
| except Exception as exc: | |
| raise HTTPException(status_code=404, detail={"error": "file not found"}) from exc | |
| media_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream" | |
| if path.suffix.lower() == ".svg": | |
| media_type = "image/svg+xml" | |
| if download: | |
| return FileResponse(path, media_type=media_type, filename=path.name, content_disposition_type="attachment") | |
| return FileResponse(path, media_type=media_type) | |
| async def create_ppt_task(body: EditableFileTaskRequest, request: Request, authorization: str | None = Header(default=None)): | |
| identity = require_identity(authorization) | |
| await filter_or_log(LoggedCall(identity, "/v1/ppt/generations", "gpt-5-5-thinking", "PPT生成任务", request_text=body.prompt), body.prompt) | |
| return await run_in_threadpool( | |
| editable_file_task_service.submit_ppt, | |
| identity, | |
| client_task_id=body.client_task_id or "", | |
| prompt=body.prompt, | |
| base64_images=body.base64_images, | |
| base_url=resolve_image_base_url(request), | |
| ) | |
| async def create_psd_task(body: EditableFileTaskRequest, request: Request, authorization: str | None = Header(default=None)): | |
| identity = require_identity(authorization) | |
| await filter_or_log(LoggedCall(identity, "/v1/psd/generations", "gpt-5-5-thinking", "PSD生成任务", request_text=body.prompt), body.prompt) | |
| return await run_in_threadpool( | |
| editable_file_task_service.submit_psd, | |
| identity, | |
| client_task_id=body.client_task_id or "", | |
| prompt=body.prompt, | |
| base64_images=body.base64_images, | |
| base_url=resolve_image_base_url(request), | |
| ) | |
| return router | |