|
|
|
|
|
|
|
|
|
|
|
|
|
|
import json |
|
|
import os |
|
|
import time |
|
|
import uuid |
|
|
import threading |
|
|
from typing import Any, Dict, List, Optional, TypedDict, Union |
|
|
|
|
|
import requests |
|
|
from fastapi import FastAPI, HTTPException, Depends, Query |
|
|
from fastapi.responses import StreamingResponse |
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials |
|
|
from pydantic import BaseModel, Field |
|
|
|
|
|
|
|
|
class CodeGeeXToken(TypedDict): |
|
|
token: str |
|
|
is_valid: bool |
|
|
last_used: float |
|
|
error_count: int |
|
|
|
|
|
VALID_CLIENT_KEYS: set = set() |
|
|
CODEGEEX_TOKENS: List[CodeGeeXToken] = [] |
|
|
CODEGEEX_MODELS: List[str] = ["claude-3-7-sonnet", "claude-sonnet-4"] |
|
|
token_rotation_lock = threading.Lock() |
|
|
MAX_ERROR_COUNT = 3 |
|
|
ERROR_COOLDOWN = 300 |
|
|
DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true" |
|
|
|
|
|
|
|
|
class ChatMessage(BaseModel): |
|
|
role: str |
|
|
content: Union[str, List[Dict[str, Any]]] |
|
|
reasoning_content: Optional[str] = None |
|
|
class ChatCompletionRequest(BaseModel): |
|
|
model: str |
|
|
messages: List[ChatMessage] |
|
|
stream: bool = True |
|
|
temperature: Optional[float] = None |
|
|
max_tokens: Optional[int] = None |
|
|
top_p: Optional[float] = None |
|
|
class ModelInfo(BaseModel): |
|
|
id: str |
|
|
object: str = "model" |
|
|
created: int |
|
|
owned_by: str |
|
|
class ModelList(BaseModel): |
|
|
object: str = "list" |
|
|
data: List[ModelInfo] |
|
|
class ChatCompletionChoice(BaseModel): |
|
|
message: ChatMessage |
|
|
index: int = 0 |
|
|
finish_reason: str = "stop" |
|
|
class ChatCompletionResponse(BaseModel): |
|
|
id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}") |
|
|
object: str = "chat.completion" |
|
|
created: int = Field(default_factory=lambda: int(time.time())) |
|
|
model: str |
|
|
choices: List[ChatCompletionChoice] |
|
|
usage: Dict[str, int] = Field(default_factory=lambda: {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}) |
|
|
class StreamChoice(BaseModel): |
|
|
delta: Dict[str, Any] = Field(default_factory=dict) |
|
|
index: int = 0 |
|
|
finish_reason: Optional[str] = None |
|
|
class StreamResponse(BaseModel): |
|
|
id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}") |
|
|
object: str = "chat.completion.chunk" |
|
|
created: int = Field(default_factory=lambda: int(time.time())) |
|
|
model: str |
|
|
choices: List[StreamChoice] |
|
|
|
|
|
|
|
|
app = FastAPI(title="CodeGeeX OpenAI API Adapter") |
|
|
security = HTTPBearer(auto_error=False) |
|
|
|
|
|
def log_debug(message: str): |
|
|
if DEBUG_MODE: |
|
|
print(f"[DEBUG] {message}") |
|
|
|
|
|
|
|
|
def load_client_api_keys_from_secrets(): |
|
|
global VALID_CLIENT_KEYS |
|
|
try: |
|
|
keys_str = os.environ.get("CLIENT_API_KEYS") |
|
|
if not keys_str: raise ValueError("Secret 'CLIENT_API_KEYS' not found.") |
|
|
keys = json.loads(keys_str) |
|
|
VALID_CLIENT_KEYS = set(keys) if isinstance(keys, list) else set() |
|
|
print(f"Successfully loaded {len(VALID_CLIENT_KEYS)} client API keys.") |
|
|
except Exception as e: |
|
|
print(f"FATAL: Error loading client API keys: {e}") |
|
|
VALID_CLIENT_KEYS = set() |
|
|
|
|
|
def load_codegeex_tokens_from_secrets(): |
|
|
global CODEGEEX_TOKENS |
|
|
CODEGEEX_TOKENS = [] |
|
|
try: |
|
|
tokens_str = os.environ.get("CODEGEEX_TOKENS") |
|
|
if not tokens_str: raise ValueError("Secret 'CODEGEEX_TOKENS' not found.") |
|
|
tokens = json.loads(tokens_str) |
|
|
if not isinstance(tokens, list): raise TypeError("Secret 'CODEGEEX_TOKENS' must be a JSON list.") |
|
|
for token in tokens: |
|
|
if isinstance(token, str) and token: |
|
|
CODEGEEX_TOKENS.append({"token": token, "is_valid": True, "last_used": 0, "error_count": 0}) |
|
|
print(f"Successfully loaded {len(CODEGEEX_TOKENS)} CodeGeeX tokens.") |
|
|
except Exception as e: |
|
|
print(f"FATAL: Error loading CodeGeeX tokens: {e}") |
|
|
|
|
|
|
|
|
def get_best_codegeex_token() -> Optional[CodeGeeXToken]: |
|
|
with token_rotation_lock: |
|
|
now = time.time() |
|
|
valid_tokens = [t for t in CODEGEEX_TOKENS if t["is_valid"] and (t["error_count"] < MAX_ERROR_COUNT or now - t["last_used"] > ERROR_COOLDOWN)] |
|
|
if not valid_tokens: return None |
|
|
for token in valid_tokens: |
|
|
if token["error_count"] >= MAX_ERROR_COUNT and now - token["last_used"] > ERROR_COOLDOWN: token["error_count"] = 0 |
|
|
valid_tokens.sort(key=lambda x: (x["last_used"], x["error_count"])) |
|
|
token = valid_tokens[0] |
|
|
token["last_used"] = now |
|
|
return token |
|
|
|
|
|
def _convert_messages_to_codegeex_format(messages: List[ChatMessage]): |
|
|
if not messages: return "", [] |
|
|
last_user_msg = next((msg for msg in reversed(messages) if msg.role == "user"), None) |
|
|
if not last_user_msg: raise HTTPException(status_code=400, detail="No user message found.") |
|
|
prompt = last_user_msg.content if isinstance(last_user_msg.content, str) else "" |
|
|
history, user_content, assistant_content = [], "", "" |
|
|
for msg in messages: |
|
|
if msg == last_user_msg: break |
|
|
if msg.role == "user": |
|
|
if user_content and assistant_content: history.append({"query": user_content, "answer": assistant_content, "id": f"{uuid.uuid4()}"}); user_content, assistant_content = "", "" |
|
|
user_content = msg.content if isinstance(msg.content, str) else "" |
|
|
elif msg.role == "assistant": |
|
|
assistant_content = msg.content if isinstance(msg.content, str) else "" |
|
|
if user_content: history.append({"query": user_content, "answer": assistant_content, "id": f"{uuid.uuid4()}"}); user_content, assistant_content = "", "" |
|
|
if user_content and not assistant_content: prompt = user_content + "\n" + prompt |
|
|
return prompt, history |
|
|
|
|
|
async def authenticate_client(auth: Optional[HTTPAuthorizationCredentials] = Depends(security)): |
|
|
if not VALID_CLIENT_KEYS: raise HTTPException(status_code=503, detail="Service unavailable: Client API keys not configured.") |
|
|
if not auth or not auth.credentials: raise HTTPException(status_code=401, detail="API key required.", headers={"WWW-Authenticate": "Bearer"}) |
|
|
if auth.credentials not in VALID_CLIENT_KEYS: raise HTTPException(status_code=403, detail="Invalid client API key.") |
|
|
|
|
|
|
|
|
@app.on_event("startup") |
|
|
async def startup(): |
|
|
print("Starting CodeGeeX OpenAI API Adapter server...") |
|
|
load_client_api_keys_from_secrets() |
|
|
load_codegeex_tokens_from_secrets() |
|
|
print("Server initialization completed.") |
|
|
|
|
|
@app.get("/") |
|
|
def health_check(): |
|
|
return {"status": "ok", "message": "CodeGeeX API Adapter is running."} |
|
|
|
|
|
def get_models_list_response() -> ModelList: |
|
|
return ModelList(data=[ModelInfo(id=model, created=int(time.time()), owned_by="anthropic") for model in CODEGEEX_MODELS]) |
|
|
|
|
|
@app.get("/v1/models", response_model=ModelList) |
|
|
async def list_v1_models(_: None = Depends(authenticate_client)): |
|
|
return get_models_list_response() |
|
|
|
|
|
@app.get("/models", response_model=ModelList) |
|
|
async def list_models_no_auth(): |
|
|
return get_models_list_response() |
|
|
|
|
|
def _codegeex_stream_generator(response, model: str): |
|
|
stream_id = f"chatcmpl-{uuid.uuid4().hex}" |
|
|
created_time = int(time.time()) |
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'role': 'assistant'})]).json()}\n\n" |
|
|
buffer = "" |
|
|
try: |
|
|
for chunk in response.iter_content(chunk_size=1024): |
|
|
if not chunk: continue |
|
|
buffer += chunk.decode("utf-8", errors='ignore') |
|
|
while "\n\n" in buffer: |
|
|
event_data, buffer = buffer.split("\n\n", 1) |
|
|
event_data = event_data.strip() |
|
|
if not event_data: continue |
|
|
event_type, data_json = None, None |
|
|
for line in event_data.split("\n"): |
|
|
if line.startswith("event:"): event_type = line[6:].strip() |
|
|
elif line.startswith("data:"): |
|
|
try: data_json = json.loads(line[5:].strip()) |
|
|
except: continue |
|
|
if not event_type or not data_json: continue |
|
|
if event_type == "add": |
|
|
delta = data_json.get("text", "") |
|
|
if delta: yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta})]).json()}\n\n" |
|
|
elif event_type == "finish": |
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={}, finish_reason='stop')]).json()}\n\n" |
|
|
yield "data: [DONE]\n\n" |
|
|
return |
|
|
except Exception as e: |
|
|
log_debug(f"Stream processing error: {e}") |
|
|
yield f"data: {json.dumps({'error': str(e)})}\n\n" |
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={}, finish_reason='stop')]).json()}\n\n" |
|
|
yield "data: [DONE]\n\n" |
|
|
|
|
|
def _build_codegeex_non_stream_response(response, model: str) -> ChatCompletionResponse: |
|
|
full_content = "" |
|
|
buffer = "" |
|
|
for chunk in response.iter_content(chunk_size=1024): |
|
|
if not chunk: continue |
|
|
buffer += chunk.decode("utf-8", errors='ignore') |
|
|
while "\n\n" in buffer: |
|
|
event_data, buffer = buffer.split("\n\n", 1) |
|
|
event_data = event_data.strip() |
|
|
if not event_data: continue |
|
|
event_type, data_json = None, None |
|
|
for line in event_data.split("\n"): |
|
|
if line.startswith("event:"): event_type = line[6:].strip() |
|
|
elif line.startswith("data:"): |
|
|
try: data_json = json.loads(line[5:].strip()) |
|
|
except: continue |
|
|
if not event_type or not data_json: continue |
|
|
if event_type == "add": full_content += data_json.get("text", "") |
|
|
elif event_type == "finish": |
|
|
finish_text = data_json.get("text", "") |
|
|
if finish_text: full_content = finish_text |
|
|
return ChatCompletionResponse(model=model, choices=[ChatCompletionChoice(message=ChatMessage(role="assistant", content=full_content))]) |
|
|
return ChatCompletionResponse(model=model, choices=[ChatCompletionChoice(message=ChatMessage(role="assistant", content=full_content))]) |
|
|
|
|
|
@app.post("/v1/chat/completions") |
|
|
async def chat_completions(request: ChatCompletionRequest, _: None = Depends(authenticate_client)): |
|
|
if request.model not in CODEGEEX_MODELS: raise HTTPException(status_code=404, detail=f"Model '{request.model}' not found.") |
|
|
if not request.messages: raise HTTPException(status_code=400, detail="No messages provided.") |
|
|
try: prompt, history = _convert_messages_to_codegeex_format(request.messages) |
|
|
except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to process messages: {e}") |
|
|
for attempt in range(len(CODEGEEX_TOKENS) + 1): |
|
|
if attempt == len(CODEGEEX_TOKENS): raise HTTPException(status_code=503, detail="All attempts to contact CodeGeeX API failed.") |
|
|
token = get_best_codegeex_token() |
|
|
if not token: raise HTTPException(status_code=503, detail="No valid CodeGeeX tokens available.") |
|
|
try: |
|
|
payload = {"user_role": 0, "ide": "VSCode", "prompt": prompt, "model": request.model, "history": history, "talkId": f"{uuid.uuid4()}", "plugin_version": "", "locale": "", "agent": None, "candidates": {"candidate_msg_id": "", "candidate_type": "", "selected_candidate": ""}, "ide_version": "", "machineId": ""} |
|
|
headers = {"User-Agent": "Mozilla/5.0", "Accept": "text/event-stream", "Content-Type": "application/json", "code-token": token["token"]} |
|
|
response = requests.post("https://codegeex.cn/prod/code/chatCodeSseV3/chat", data=json.dumps(payload), headers=headers, stream=True, timeout=300.0) |
|
|
response.raise_for_status() |
|
|
if request.stream: return StreamingResponse(_codegeex_stream_generator(response, request.model), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"}) |
|
|
else: return _build_codegeex_non_stream_response(response, request.model) |
|
|
except requests.HTTPError as e: |
|
|
status_code = getattr(e.response, "status_code", 500) |
|
|
with token_rotation_lock: |
|
|
if status_code in [401, 403]: token["is_valid"] = False |
|
|
elif status_code in [429, 500, 502, 503, 504]: token["error_count"] += 1 |
|
|
except Exception as e: |
|
|
with token_rotation_lock: token["error_count"] += 1 |
|
|
|
|
|
|