Wilker commited on
Commit ·
ec1946d
1
Parent(s): 8b22735
feat: /v1/messages suporta stream=true SSE compatível com Claude Code SDK
Browse files
app.py
CHANGED
|
@@ -1084,28 +1084,57 @@ class AnthropicRequest(BaseModel):
|
|
| 1084 |
stream: Optional[bool] = False
|
| 1085 |
|
| 1086 |
@app.post("/v1/messages")
|
| 1087 |
-
def anthropic_messages(req: AnthropicRequest, request: Request):
|
| 1088 |
-
"""Rota compatível com SDK Anthropic —
|
|
|
|
| 1089 |
# Monta as mensagens no formato interno
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1109 |
reply = result["choices"][0]["message"]["content"]
|
| 1110 |
usage = result.get("usage", {})
|
| 1111 |
return {
|
|
@@ -1121,3 +1150,66 @@ def anthropic_messages(req: AnthropicRequest, request: Request):
|
|
| 1121 |
"output_tokens": usage.get("completion_tokens", 0),
|
| 1122 |
},
|
| 1123 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1084 |
stream: Optional[bool] = False
|
| 1085 |
|
| 1086 |
@app.post("/v1/messages")
|
| 1087 |
+
async def anthropic_messages(req: AnthropicRequest, request: Request):
|
| 1088 |
+
"""Rota compatível com SDK Anthropic — suporta stream=true (SSE) e stream=false (JSON)."""
|
| 1089 |
+
|
| 1090 |
# Monta as mensagens no formato interno
|
| 1091 |
+
def build_chat_req():
|
| 1092 |
+
msgs = []
|
| 1093 |
+
if req.system:
|
| 1094 |
+
msgs.append(Message(role="system", content=req.system))
|
| 1095 |
+
for m in req.messages:
|
| 1096 |
+
msgs.append(Message(role=m.role, content=m.content))
|
| 1097 |
+
return ChatRequest(
|
| 1098 |
+
model=req.model,
|
| 1099 |
+
messages=msgs,
|
| 1100 |
+
max_tokens=req.max_tokens,
|
| 1101 |
+
temperature=req.temperature,
|
| 1102 |
+
mode="medium",
|
| 1103 |
+
web=False,
|
| 1104 |
+
free_mode=False,
|
| 1105 |
+
)
|
| 1106 |
+
|
| 1107 |
+
# ── Modo streaming (Claude Code usa isso por padrão) ──────────────────
|
| 1108 |
+
if req.stream:
|
| 1109 |
+
async def event_stream():
|
| 1110 |
+
msg_id = f"msg_thc_{uuid.uuid4().hex[:8]}"
|
| 1111 |
+
|
| 1112 |
+
yield f"event: message_start\ndata: {json.dumps({'type':'message_start','message':{'id':msg_id,'type':'message','role':'assistant','content':[],'model':req.model,'stop_reason':None,'stop_sequence':None,'usage':{'input_tokens':0,'output_tokens':0}}})}\n\n"
|
| 1113 |
+
yield f"event: content_block_start\ndata: {json.dumps({'type':'content_block_start','index':0,'content_block':{'type':'text','text':''}})}\n\n"
|
| 1114 |
+
yield f"event: ping\ndata: {json.dumps({'type':'ping'})}\n\n"
|
| 1115 |
+
|
| 1116 |
+
try:
|
| 1117 |
+
result = chat_completions(build_chat_req())
|
| 1118 |
+
reply = result["choices"][0]["message"]["content"]
|
| 1119 |
+
usage = result.get("usage", {})
|
| 1120 |
+
except Exception as e:
|
| 1121 |
+
reply = f"Erro: {str(e)}"
|
| 1122 |
+
usage = {}
|
| 1123 |
+
|
| 1124 |
+
chunk_size = 20
|
| 1125 |
+
for i in range(0, len(reply), chunk_size):
|
| 1126 |
+
chunk = reply[i:i+chunk_size]
|
| 1127 |
+
yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':chunk}})}\n\n"
|
| 1128 |
+
await asyncio.sleep(0.01)
|
| 1129 |
+
|
| 1130 |
+
yield f"event: content_block_stop\ndata: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
|
| 1131 |
+
yield f"event: message_delta\ndata: {json.dumps({'type':'message_delta','delta':{'stop_reason':'end_turn','stop_sequence':None},'usage':{'output_tokens':usage.get('completion_tokens',0)}})}\n\n"
|
| 1132 |
+
yield f"event: message_stop\ndata: {json.dumps({'type':'message_stop'})}\n\n"
|
| 1133 |
+
|
| 1134 |
+
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
| 1135 |
+
|
| 1136 |
+
# ── Modo JSON normal ───────────────────────────────────────────────────
|
| 1137 |
+
result = chat_completions(build_chat_req())
|
| 1138 |
reply = result["choices"][0]["message"]["content"]
|
| 1139 |
usage = result.get("usage", {})
|
| 1140 |
return {
|
|
|
|
| 1150 |
"output_tokens": usage.get("completion_tokens", 0),
|
| 1151 |
},
|
| 1152 |
}
|
| 1153 |
+
|
| 1154 |
+
# ── Streaming SSE para /v1/messages (Claude Code compat) ─────────────────────
|
| 1155 |
+
from fastapi.responses import StreamingResponse
|
| 1156 |
+
import asyncio
|
| 1157 |
+
|
| 1158 |
+
@app.post("/v1/messages/stream")
|
| 1159 |
+
async def anthropic_messages_stream(req: AnthropicRequest, request: Request):
|
| 1160 |
+
"""Endpoint de streaming SSE compatível com Claude Code SDK."""
|
| 1161 |
+
|
| 1162 |
+
async def event_stream():
|
| 1163 |
+
msg_id = f"msg_thc_{uuid.uuid4().hex[:8]}"
|
| 1164 |
+
|
| 1165 |
+
# message_start
|
| 1166 |
+
yield f"event: message_start\ndata: {json.dumps({'type':'message_start','message':{'id':msg_id,'type':'message','role':'assistant','content':[],'model':req.model,'stop_reason':None,'stop_sequence':None,'usage':{'input_tokens':0,'output_tokens':0}}})}\n\n"
|
| 1167 |
+
|
| 1168 |
+
# content_block_start
|
| 1169 |
+
yield f"event: content_block_start\ndata: {json.dumps({'type':'content_block_start','index':0,'content_block':{'type':'text','text':''}})}\n\n"
|
| 1170 |
+
|
| 1171 |
+
# ping
|
| 1172 |
+
yield f"event: ping\ndata: {json.dumps({'type':'ping'})}\n\n"
|
| 1173 |
+
|
| 1174 |
+
# Chama o endpoint síncrono existente
|
| 1175 |
+
msgs = []
|
| 1176 |
+
if req.system:
|
| 1177 |
+
msgs.append(Message(role="system", content=req.system))
|
| 1178 |
+
for m in req.messages:
|
| 1179 |
+
msgs.append(Message(role=m.role, content=m.content))
|
| 1180 |
+
|
| 1181 |
+
chat_req = ChatRequest(
|
| 1182 |
+
model=req.model,
|
| 1183 |
+
messages=msgs,
|
| 1184 |
+
max_tokens=req.max_tokens,
|
| 1185 |
+
temperature=req.temperature,
|
| 1186 |
+
mode="medium",
|
| 1187 |
+
web=False,
|
| 1188 |
+
free_mode=False,
|
| 1189 |
+
)
|
| 1190 |
+
|
| 1191 |
+
try:
|
| 1192 |
+
result = chat_completions(chat_req)
|
| 1193 |
+
reply = result["choices"][0]["message"]["content"]
|
| 1194 |
+
usage = result.get("usage", {})
|
| 1195 |
+
except Exception as e:
|
| 1196 |
+
reply = f"Erro: {str(e)}"
|
| 1197 |
+
usage = {}
|
| 1198 |
+
|
| 1199 |
+
# Emite o texto em chunks de ~20 chars para simular streaming
|
| 1200 |
+
chunk_size = 20
|
| 1201 |
+
for i in range(0, len(reply), chunk_size):
|
| 1202 |
+
chunk = reply[i:i+chunk_size]
|
| 1203 |
+
yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':chunk}})}\n\n"
|
| 1204 |
+
await asyncio.sleep(0.01)
|
| 1205 |
+
|
| 1206 |
+
# content_block_stop
|
| 1207 |
+
yield f"event: content_block_stop\ndata: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
|
| 1208 |
+
|
| 1209 |
+
# message_delta (stop_reason)
|
| 1210 |
+
yield f"event: message_delta\ndata: {json.dumps({'type':'message_delta','delta':{'stop_reason':'end_turn','stop_sequence':None},'usage':{'output_tokens':usage.get('completion_tokens',0)}})}\n\n"
|
| 1211 |
+
|
| 1212 |
+
# message_stop
|
| 1213 |
+
yield f"event: message_stop\ndata: {json.dumps({'type':'message_stop'})}\n\n"
|
| 1214 |
+
|
| 1215 |
+
return StreamingResponse(event_stream(), media_type="text/event-stream")
|