Wilker commited on
Commit
ec1946d
·
1 Parent(s): 8b22735

feat: /v1/messages suporta stream=true SSE compatível com Claude Code SDK

Browse files
Files changed (1) hide show
  1. app.py +113 -21
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 — converte para o formato interno e reutiliza chat_completions."""
 
1089
  # Monta as mensagens no formato interno
1090
- msgs = []
1091
- if req.system:
1092
- msgs.append(Message(role="system", content=req.system))
1093
- for m in req.messages:
1094
- msgs.append(Message(role=m.role, content=m.content))
1095
-
1096
- # Reutiliza exatamente a lógica já existente
1097
- chat_req = 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
- result = chat_completions(chat_req)
1107
-
1108
- # Converte resposta para formato Anthropic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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")