cjovs commited on
Commit
bc91c2e
·
verified ·
1 Parent(s): 46e1036

Add /v1/responses compatibility and collapse placeholder account

Browse files
Files changed (2) hide show
  1. app/config.py +6 -2
  2. app/routes.py +196 -0
app/config.py CHANGED
@@ -44,8 +44,12 @@ def _merge_env_secrets(config: dict) -> dict:
44
  if not isinstance(account, dict):
45
  continue
46
  current_identity = account.get("email") or account.get("mobile")
47
- if current_identity == desired_identity:
48
- updated = dict(account)
 
 
 
 
49
  updated[identity_key] = desired_identity
50
  updated["password"] = env_account["password"]
51
  merged_accounts.append(updated)
 
44
  if not isinstance(account, dict):
45
  continue
46
  current_identity = account.get("email") or account.get("mobile")
47
+ if current_identity in {desired_identity, ACCOUNT_PLACEHOLDER}:
48
+ updated = {
49
+ k: v
50
+ for k, v in account.items()
51
+ if k not in {"email", "mobile", "password", "token", "hif_dliq", "hif_leim"}
52
+ }
53
  updated[identity_key] = desired_identity
54
  updated["password"] = env_account["password"]
55
  merged_accounts.append(updated)
app/routes.py CHANGED
@@ -1,12 +1,15 @@
1
  import json
2
  import logging
 
3
  import queue
4
  import re
5
  import threading
6
  import time
 
7
 
8
  from fastapi import APIRouter, HTTPException, Request
9
  from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
 
10
 
11
  from . import chat, config, constants, session as session_module
12
  from .account import (
@@ -89,6 +92,199 @@ def list_models():
89
  return JSONResponse(content=data, status_code=200)
90
 
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  # ----------------------------------------------------------------------
93
  # 路由:/v1/chat/completions
94
  # ----------------------------------------------------------------------
 
1
  import json
2
  import logging
3
+ import os
4
  import queue
5
  import re
6
  import threading
7
  import time
8
+ from uuid import uuid4
9
 
10
  from fastapi import APIRouter, HTTPException, Request
11
  from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
12
+ from curl_cffi import requests as curl_requests
13
 
14
  from . import chat, config, constants, session as session_module
15
  from .account import (
 
92
  return JSONResponse(content=data, status_code=200)
93
 
94
 
95
+ def _responses_input_to_messages(req_data: dict) -> list[dict]:
96
+ messages = []
97
+
98
+ instructions = req_data.get("instructions")
99
+ if instructions:
100
+ messages.append({"role": "system", "content": str(instructions)})
101
+
102
+ def append_message(role: str, content):
103
+ if content is None:
104
+ return
105
+ if isinstance(content, list):
106
+ text_parts = []
107
+ for item in content:
108
+ if isinstance(item, str):
109
+ text_parts.append(item)
110
+ elif isinstance(item, dict):
111
+ if item.get("type") in {"input_text", "output_text", "text"}:
112
+ text = item.get("text") or item.get("content")
113
+ if text:
114
+ text_parts.append(str(text))
115
+ content = "\n".join(text_parts)
116
+ elif isinstance(content, dict):
117
+ content = json.dumps(content, ensure_ascii=False)
118
+ else:
119
+ content = str(content)
120
+ if content:
121
+ messages.append({"role": role or "user", "content": content})
122
+
123
+ input_data = req_data.get("input")
124
+ if isinstance(input_data, str):
125
+ append_message("user", input_data)
126
+ elif isinstance(input_data, dict):
127
+ if "role" in input_data:
128
+ append_message(input_data.get("role", "user"), input_data.get("content") or input_data.get("text"))
129
+ else:
130
+ append_message("user", input_data)
131
+ elif isinstance(input_data, list):
132
+ for item in input_data:
133
+ if isinstance(item, str):
134
+ append_message("user", item)
135
+ elif isinstance(item, dict):
136
+ if item.get("type") == "message":
137
+ append_message(item.get("role", "user"), item.get("content"))
138
+ elif item.get("type") in {"input_text", "text"}:
139
+ append_message("user", item.get("text") or item.get("content"))
140
+ elif "role" in item:
141
+ append_message(item.get("role", "user"), item.get("content") or item.get("text"))
142
+ else:
143
+ append_message("user", item)
144
+
145
+ return messages
146
+
147
+
148
+ def _responses_tools_to_chat_tools(tools) -> list[dict]:
149
+ chat_tools = []
150
+ for tool in tools or []:
151
+ if not isinstance(tool, dict):
152
+ continue
153
+ if tool.get("type") != "function":
154
+ continue
155
+ if isinstance(tool.get("function"), dict):
156
+ chat_tools.append(tool)
157
+ continue
158
+ chat_tools.append(
159
+ {
160
+ "type": "function",
161
+ "function": {
162
+ "name": tool.get("name"),
163
+ "description": tool.get("description", ""),
164
+ "parameters": tool.get("parameters") or tool.get("input_schema") or {},
165
+ },
166
+ }
167
+ )
168
+ return chat_tools
169
+
170
+
171
+ def _call_local_chat_completions(chat_body: dict, auth_header: str, x_api_key: str):
172
+ local_port = os.getenv("PORT", "7860")
173
+ headers = {"Content-Type": "application/json"}
174
+ if auth_header:
175
+ headers["Authorization"] = auth_header
176
+ if x_api_key:
177
+ headers["x-api-key"] = x_api_key
178
+ resp = curl_requests.post(
179
+ f"http://127.0.0.1:{local_port}/v1/chat/completions",
180
+ headers=headers,
181
+ json=chat_body,
182
+ impersonate="safari15_3",
183
+ timeout=180,
184
+ )
185
+ try:
186
+ payload = resp.json()
187
+ except Exception:
188
+ payload = {"error": resp.text}
189
+ return resp.status_code, payload
190
+
191
+
192
+ def _chat_completion_to_response_payload(chat_payload: dict) -> dict:
193
+ choice = ((chat_payload.get("choices") or [{}])[0])
194
+ message = choice.get("message") or {}
195
+ text = message.get("content", "") or ""
196
+ tool_calls = message.get("tool_calls") or []
197
+ output = []
198
+
199
+ if text or not tool_calls:
200
+ output.append(
201
+ {
202
+ "id": f"msg_{uuid4().hex}",
203
+ "type": "message",
204
+ "status": "completed",
205
+ "role": "assistant",
206
+ "content": [{"type": "output_text", "text": text, "annotations": []}],
207
+ }
208
+ )
209
+
210
+ for tool_call in tool_calls:
211
+ function = tool_call.get("function") or {}
212
+ output.append(
213
+ {
214
+ "id": tool_call.get("id") or f"fc_{uuid4().hex}",
215
+ "type": "function_call",
216
+ "call_id": tool_call.get("id") or f"call_{uuid4().hex}",
217
+ "name": function.get("name", ""),
218
+ "arguments": function.get("arguments", "{}"),
219
+ "status": "completed",
220
+ }
221
+ )
222
+
223
+ usage = chat_payload.get("usage") or {}
224
+ return {
225
+ "id": f"resp_{chat_payload.get('id', uuid4().hex)}",
226
+ "object": "response",
227
+ "created": chat_payload.get("created", int(time.time())),
228
+ "status": "completed",
229
+ "model": chat_payload.get("model"),
230
+ "output": output,
231
+ "output_text": text,
232
+ "parallel_tool_calls": bool(tool_calls),
233
+ "usage": {
234
+ "input_tokens": usage.get("prompt_tokens", 0),
235
+ "output_tokens": usage.get("completion_tokens", 0),
236
+ "total_tokens": usage.get("total_tokens", 0),
237
+ },
238
+ }
239
+
240
+
241
+ @router.post("/v1/responses")
242
+ async def responses_api(request: Request):
243
+ try:
244
+ req_data = await request.json()
245
+ if bool(req_data.get("stream", False)):
246
+ return JSONResponse(
247
+ status_code=400,
248
+ content={"error": "/v1/responses streaming is not supported yet. Use /v1/chat/completions with stream=true."},
249
+ )
250
+
251
+ model = req_data.get("model")
252
+ messages = _responses_input_to_messages(req_data)
253
+ if not model or not messages:
254
+ return JSONResponse(
255
+ status_code=400,
256
+ content={"error": "Request must include 'model' and a non-empty 'input'."},
257
+ )
258
+
259
+ chat_body = {
260
+ "model": model,
261
+ "messages": messages,
262
+ "stream": False,
263
+ }
264
+ if "tools" in req_data:
265
+ chat_body["tools"] = _responses_tools_to_chat_tools(req_data.get("tools"))
266
+ if "tool_choice" in req_data:
267
+ chat_body["tool_choice"] = req_data.get("tool_choice")
268
+ if "temperature" in req_data:
269
+ chat_body["temperature"] = req_data.get("temperature")
270
+ if "top_p" in req_data:
271
+ chat_body["top_p"] = req_data.get("top_p")
272
+
273
+ status_code, chat_payload = await __import__("asyncio").to_thread(
274
+ _call_local_chat_completions,
275
+ chat_body,
276
+ request.headers.get("Authorization", ""),
277
+ request.headers.get("x-api-key", ""),
278
+ )
279
+ if status_code != 200:
280
+ return JSONResponse(status_code=status_code, content=chat_payload if isinstance(chat_payload, dict) else {"error": str(chat_payload)})
281
+
282
+ return JSONResponse(content=_chat_completion_to_response_payload(chat_payload), status_code=200)
283
+ except Exception as exc:
284
+ logger.error(f"[responses_api] 未知异常: {exc}")
285
+ return JSONResponse(status_code=500, content={"error": "Internal Server Error"})
286
+
287
+
288
  # ----------------------------------------------------------------------
289
  # 路由:/v1/chat/completions
290
  # ----------------------------------------------------------------------