feat(api): vertical slice POST /v1/agent/chat and the Celery ping path
One request exercises every layer: JWT auth, quota reservation, a versioned prompt through the LLM gateway, a cost row, quota commit or refund, and the unified envelope. The trivial ping task proves the queue routing and the tk_task transitions before any real long-running task is written. Routes are mounted at the /api/v1 base path of 架构文档 §9.1; /v1/* is rewritten to it at the ASGI layer so both spellings work from one route table.
This commit is contained in:
Executable
+98
@@ -0,0 +1,98 @@
|
||||
"""Request/response schemas.
|
||||
|
||||
NFR-MAINT-01 契约先行: schema 先定义,客户端类型由 OpenAPI 生成。
|
||||
Wire responses are wrapped by the unified envelope (架构文档 §9.2); these models
|
||||
describe the ``data`` payload only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class DevTokenRequest(BaseModel):
|
||||
device_key: str = Field(
|
||||
default="local-dev",
|
||||
min_length=1,
|
||||
max_length=64,
|
||||
description="任意稳定字符串;同一 device_key 复用同一测试用户",
|
||||
)
|
||||
|
||||
|
||||
class DevTokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "Bearer" # noqa: S105 — a scheme name, not a credential
|
||||
expires_in: int
|
||||
user_id: str = Field(description="雪花 ID 以字符串返回,避免 JS 精度丢失")
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str = Field(min_length=1, max_length=2000)
|
||||
request_key: str | None = Field(
|
||||
default=None,
|
||||
max_length=128,
|
||||
description="幂等键;相同键不会重复扣配额 (架构文档 §9.1)",
|
||||
)
|
||||
|
||||
|
||||
class ChatUsage(BaseModel):
|
||||
tokens_in: int
|
||||
tokens_out: int
|
||||
tokens_cached: int
|
||||
latency_ms: int
|
||||
cost_cents: str
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
reply: str
|
||||
provider: str
|
||||
model: str
|
||||
prompt_id: str | None
|
||||
prompt_version: str | None
|
||||
call_id: str
|
||||
usage: ChatUsage
|
||||
quota_remaining: int
|
||||
|
||||
|
||||
class QuotaResponse(BaseModel):
|
||||
quota_key: str
|
||||
period_key: str
|
||||
used: int
|
||||
limit_value: int
|
||||
bonus_value: int
|
||||
remaining: int
|
||||
reset_at: datetime | None
|
||||
|
||||
|
||||
class LedgerEntry(BaseModel):
|
||||
tx_id: str
|
||||
delta: int
|
||||
tx_type: str
|
||||
status: str
|
||||
reason: str | None
|
||||
quota_key: str | None
|
||||
period_key: str | None
|
||||
balance_after: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class TaskSubmitRequest(BaseModel):
|
||||
echo: str = Field(default="ping", max_length=200)
|
||||
idempotency_key: str | None = Field(default=None, max_length=128)
|
||||
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
task_id: str
|
||||
task_type: str
|
||||
status: str
|
||||
progress: int
|
||||
stage_text: str | None
|
||||
result: dict[str, object] | None
|
||||
error_code: int | None
|
||||
error_message: str | None
|
||||
retry_count: int
|
||||
submitted_at: datetime
|
||||
started_at: datetime | None
|
||||
finished_at: datetime | None
|
||||
Reference in New Issue
Block a user