Download app.py from bahi-bh/Codefree: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/spaces/bahi-bh/Codefree/resolve/main/app.py
- Command line
-
hf download hf://spaces/bahi-bh/Codefree/app.py
-
curl -L -o app.py https://huggingface.co/spaces/bahi-bh/Codefree/resolve/main/app.py
24.4 kB
| """ | |
| OpenCode ↔ Anthropic Gateway — latency-optimized edition | |
| """ | |
| import os | |
| import hashlib | |
| import base64 | |
| import uuid | |
| import asyncio | |
| import logging | |
| from contextlib import asynccontextmanager | |
| try: | |
| import orjson as _json_lib | |
| def _dumps(obj: object) -> str: | |
| return _json_lib.dumps(obj).decode() | |
| def _loads(s): | |
| return _json_lib.loads(s) | |
| except ImportError: | |
| import json as _json_lib # type: ignore[assignment] | |
| def _dumps(obj: object) -> str: | |
| return _json_lib.dumps(obj, separators=(",", ":")) | |
| def _loads(s): | |
| return _json_lib.loads(s) | |
| import json | |
| import httpx | |
| from fastapi import FastAPI, Request, Header | |
| from fastapi.responses import JSONResponse, StreamingResponse | |
| from starlette.background import BackgroundTask | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(message)s", | |
| ) | |
| logger = logging.getLogger("OpenCodeGateway") | |
| OPENCODE_API_KEY = os.getenv("OPENCODE_API_KEY", "").strip() | |
| UPSTREAM_URL = "https://opencode.ai/zen/v1/chat/completions" | |
| MAX_RETRIES = 2 | |
| THINKING_MODE = os.getenv("THINKING_MODE", "enabled").strip().lower() | |
| _OPENCODE_VERSION = os.getenv("OPENCODE_VERSION", "1.15.4") | |
| _USER_ID = os.getenv("OPENCODE_USER_ID", uuid.uuid4().hex) | |
| _PROJECT_ID = os.getenv("OPENCODE_PROJECT_ID", uuid.uuid4().hex) | |
| _STATIC_UPSTREAM_HEADERS = { | |
| "Content-Type": "application/json", | |
| "Accept-Encoding": "identity", | |
| "Accept": "application/json", | |
| "Origin": "https://opencode.ai", | |
| "Referer": "https://opencode.ai/", | |
| "User-Agent": f"opencode/{_OPENCODE_VERSION}", | |
| "x-opencode-client": "cli", | |
| } | |
| def _make_upstream_headers(api_key: str) -> dict: | |
| return { | |
| **_STATIC_UPSTREAM_HEADERS, | |
| "x-api-key": api_key, | |
| "x-opencode-request": _USER_ID, | |
| "x-opencode-project": _PROJECT_ID, | |
| "x-opencode-session": uuid.uuid4().hex, | |
| } | |
| STREAM_RESPONSE_HEADERS = { | |
| "Cache-Control": "no-cache", | |
| "Connection": "keep-alive", | |
| "X-Accel-Buffering": "no", | |
| } | |
| # ========================================================= | |
| # MODELS | |
| # ========================================================= | |
| # النماذج المجانية Ø§Ù„ØØ§Ù„ية ÙÙŠ OpenCode Zen (Ù…ÙØØ¯ÙŽÙ‘Ø«Ø© 2026-06): | |
| # deepseek-v4-flash → DeepSeek V4 Flash Free (الأقوى والأسرع) | |
| # mimo-v2.5 → MiMo-V2.5 Free (سريع جداً) | |
| # north-mini-code → North Mini Code Free (مخصص للكود) | |
| # big-pickle → Big Pickle Free (مجاني دائم) | |
| # | |
| # الأسماء القديمة (nemotron-3-super-free, deepseek-v4-flash-free …) | |
| # لم تعد موجودة — الأسماء الصØÙŠØØ© الآن بدون لاØÙ‚Ø© "-free". | |
| MODEL_MAP = { | |
| # Opus → أقوى نموذج مجاني Ù…ØªØ§Ø | |
| "claude-opus-4-6": "deepseek-v4-flash", | |
| "claude-opus-4-5": "deepseek-v4-flash", | |
| # Sonnet → DeepSeek V4 Flash (Ø£ÙØ¶Ù„ توازن جودة/سرعة) | |
| "claude-sonnet-4-6": "deepseek-v4-flash", | |
| "claude-sonnet-4-5": "deepseek-v4-flash", | |
| # Haiku → MiMo-V2.5 (أسرع للمهام الخÙÙŠÙØ©) | |
| "claude-haiku-4-6": "mimo-v2.5", | |
| "claude-haiku-4-5": "mimo-v2.5", | |
| "claude-haiku-4-5-20251001": "mimo-v2.5", | |
| } | |
| DEFAULT_MODEL = "deepseek-v4-flash" | |
| _MODELS_CACHE = { | |
| "object": "list", | |
| "data": [ | |
| {"id": m, "object": "model", "created": 1710000000, "owned_by": "gateway"} | |
| for m in MODEL_MAP | |
| ], | |
| } | |
| async def lifespan(app: FastAPI): | |
| app.state.client = httpx.AsyncClient( | |
| http2=True, | |
| timeout=httpx.Timeout(connect=15, read=300, write=60, pool=30), | |
| limits=httpx.Limits(max_connections=100, max_keepalive_connections=30), | |
| follow_redirects=True, | |
| ) | |
| logger.info("Gateway started — THINKING_MODE=%s json_backend=%s", | |
| THINKING_MODE, _json_lib.__name__) | |
| yield | |
| await app.state.client.aclose() | |
| app = FastAPI(lifespan=lifespan) | |
| async def root(): | |
| return {"status": "ok", "service": "OpenCode Gateway"} | |
| async def health(): | |
| return {"status": "healthy"} | |
| async def get_models(): | |
| return _MODELS_CACHE | |
| def _fix_schema(schema): | |
| if not isinstance(schema, dict): | |
| return {"type": "object", "properties": {}} | |
| if schema.get("type") not in ( | |
| "object", "array", "string", "number", "boolean", "integer" | |
| ): | |
| schema = dict(schema) | |
| schema["type"] = "object" | |
| schema.setdefault("properties", {}) | |
| return schema | |
| def _synthetic_signature(text: str) -> str: | |
| digest = hashlib.sha256(text.encode()).digest() | |
| return "synthetic_" + base64.urlsafe_b64encode(digest).decode().rstrip("=") | |
| def normalize_messages(messages: list) -> list: | |
| output = [] | |
| for msg in messages: | |
| role = msg.get("role", "user") | |
| content = msg.get("content", "") | |
| reasoning_content = msg.get("reasoning_content") | |
| if isinstance(content, str): | |
| result = {"role": role, "content": content} | |
| if reasoning_content is not None: | |
| result["reasoning_content"] = reasoning_content | |
| output.append(result) | |
| continue | |
| if isinstance(content, list): | |
| text_parts = [] | |
| tool_calls = [] | |
| tool_results = [] | |
| thinking_parts = [] | |
| for item in content: | |
| if not isinstance(item, dict): | |
| text_parts.append(str(item)) | |
| continue | |
| t = item.get("type", "") | |
| if t == "text": | |
| txt = item.get("text", "") | |
| if txt: text_parts.append(txt) | |
| elif t == "thinking": | |
| think_txt = item.get("thinking", "") | |
| if think_txt: thinking_parts.append(think_txt) | |
| elif t == "tool_use": | |
| tool_calls.append({ | |
| "id": item.get("id", f"call_{uuid.uuid4().hex[:8]}"), | |
| "type": "function", | |
| "function": { | |
| "name": item.get("name", ""), | |
| "arguments": json.dumps(item.get("input", {})), | |
| }, | |
| }) | |
| elif t == "tool_result": | |
| rc = item.get("content", "") | |
| if isinstance(rc, list): | |
| rc = "\n".join( | |
| c.get("text", "") if isinstance(c, dict) else str(c) | |
| for c in rc | |
| ) | |
| tool_results.append({ | |
| "role": "tool", | |
| "tool_call_id": item.get("tool_use_id", ""), | |
| "content": str(rc), | |
| }) | |
| final_reasoning = ( | |
| reasoning_content if reasoning_content is not None | |
| else ("\n".join(thinking_parts) if thinking_parts else None) | |
| ) | |
| if tool_calls: | |
| assistant_msg = { | |
| "role": "assistant", | |
| "tool_calls": tool_calls, | |
| "content": "\n".join(text_parts) if text_parts else None, | |
| } | |
| if final_reasoning is not None: | |
| assistant_msg["reasoning_content"] = final_reasoning | |
| output.append(assistant_msg) | |
| elif tool_results: | |
| output.extend(tool_results) | |
| else: | |
| result = {"role": role, "content": "\n".join(text_parts)} | |
| if final_reasoning is not None: | |
| result["reasoning_content"] = final_reasoning | |
| output.append(result) | |
| continue | |
| result = {"role": role, "content": str(content)} | |
| if reasoning_content is not None: | |
| result["reasoning_content"] = reasoning_content | |
| output.append(result) | |
| return output | |
| def convert_tools_to_openai(tools: list) -> list: | |
| if not tools: | |
| return [] | |
| return [ | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": t.get("name", ""), | |
| "description": t.get("description", ""), | |
| "parameters": _fix_schema(t.get("input_schema", {})), | |
| }, | |
| } | |
| for t in tools | |
| ] | |
| def map_tool_choice(raw, has_tools: bool, thinking_on: bool): | |
| if not raw or not has_tools: return None | |
| if isinstance(raw, dict): | |
| kind = raw.get("type", "auto") | |
| if thinking_on and kind in ("any", "tool"): | |
| logger.warning("tool_choice '%s' incompatible with extended thinking → auto", kind) | |
| return "auto" | |
| if kind == "any": return "required" | |
| if kind == "tool": return {"type": "function", "function": {"name": raw.get("name", "")}} | |
| return kind | |
| if thinking_on and raw in ("any", "required"): | |
| return "auto" | |
| return raw | |
| def build_payload(body, messages, tools, tool_choice, model, stream): | |
| thinking_on = THINKING_MODE == "enabled" | |
| payload = { | |
| "model": model, | |
| "messages": messages, | |
| "max_tokens": min(body.get("max_tokens", 32000), 64000), | |
| "stream": stream, | |
| "thinking": {"type": "enabled"} if thinking_on else {"type": "disabled"}, | |
| } | |
| if not thinking_on: | |
| payload["temperature"] = body.get("temperature", 0.7) | |
| elif "temperature" in body: | |
| payload["temperature"] = body["temperature"] | |
| if tools: | |
| payload["tools"] = tools | |
| if tool_choice is not None: | |
| payload["tool_choice"] = tool_choice | |
| return payload | |
| _FINISH_MAP = {"tool_calls": "tool_use", "length": "max_tokens"} | |
| def convert_to_anthropic(parsed: dict, requested_model: str) -> dict: | |
| content_blocks = [] | |
| stop_reason = "end_turn" | |
| try: | |
| choice = (parsed.get("choices") or [{}])[0] | |
| message = choice.get("message", {}) | |
| stop_reason = _FINISH_MAP.get(choice.get("finish_reason"), "end_turn") | |
| reasoning = message.get("reasoning_content") | |
| if reasoning: | |
| content_blocks.append({ | |
| "type": "thinking", "thinking": reasoning, | |
| "signature": _synthetic_signature(reasoning), | |
| }) | |
| text = message.get("content") or "" | |
| if text: | |
| content_blocks.append({"type": "text", "text": text}) | |
| for tc in (message.get("tool_calls") or []): | |
| fn = tc.get("function", {}) | |
| try: parsed_args = _loads(fn.get("arguments", "{}")) | |
| except: parsed_args = {"raw": fn.get("arguments", "")} | |
| content_blocks.append({ | |
| "type": "tool_use", | |
| "id": tc.get("id", f"toolu_{uuid.uuid4().hex[:8]}"), | |
| "name": fn.get("name", ""), | |
| "input": parsed_args, | |
| }) | |
| except Exception as exc: | |
| logger.warning("convert_to_anthropic error: %s", exc) | |
| if not content_blocks: | |
| content_blocks.append({"type": "text", "text": ""}) | |
| usage = parsed.get("usage", {}) | |
| return { | |
| "id": f"msg_{uuid.uuid4().hex}", "type": "message", | |
| "role": "assistant", "model": requested_model, | |
| "content": content_blocks, "stop_reason": stop_reason, | |
| "stop_sequence": None, | |
| "usage": { | |
| "input_tokens": usage.get("prompt_tokens", 0), | |
| "output_tokens": usage.get("completion_tokens", 0), | |
| }, | |
| } | |
| async def _iter_sse_lines(response): | |
| buf = b"" | |
| async for chunk in response.aiter_bytes(): | |
| if not chunk: continue | |
| buf += chunk | |
| while True: | |
| nl = buf.find(b"\n") | |
| if nl == -1: break | |
| line = buf[:nl].rstrip(b"\r").decode("utf-8", errors="replace") | |
| buf = buf[nl + 1:] | |
| yield line | |
| _MSG_START = "event: message_start\ndata: " | |
| _BLK_START = "event: content_block_start\ndata: " | |
| _BLK_DELTA = "event: content_block_delta\ndata: " | |
| _BLK_STOP = "event: content_block_stop\ndata: " | |
| _MSG_DELTA = "event: message_delta\ndata: " | |
| _MSG_STOP = "event: message_stop\ndata: " | |
| async def convert_stream_to_anthropic(response, requested_model: str): | |
| msg_id = f"msg_{uuid.uuid4().hex}" | |
| input_tokens = 0 | |
| output_tokens = 0 | |
| yield (f"{_MSG_START}" + _dumps({ | |
| "type": "message_start", | |
| "message": { | |
| "id": msg_id, "type": "message", "role": "assistant", | |
| "model": requested_model, "content": [], | |
| "stop_reason": None, "stop_sequence": None, | |
| "usage": {"input_tokens": 0, "output_tokens": 0}, | |
| }, | |
| }) + "\n\n") | |
| yield 'event: ping\ndata: {"type":"ping"}\n\n' | |
| tool_state = {} | |
| tool_block_idx = {} | |
| next_idx = 0 | |
| reasoning_block_idx = None | |
| reasoning_text_acc = "" | |
| reasoning_closed = False | |
| text_block_idx = None | |
| stop_reason = "end_turn" | |
| try: | |
| async for line in _iter_sse_lines(response): | |
| if not line or not line.startswith("data:"): continue | |
| data = line[5:].strip() | |
| if data == "[DONE]": break | |
| try: chunk = _loads(data) | |
| except: continue | |
| if chunk.get("usage"): | |
| u = chunk["usage"] | |
| input_tokens = u.get("prompt_tokens", input_tokens) | |
| output_tokens = u.get("completion_tokens", output_tokens) | |
| choices = chunk.get("choices") | |
| if not choices: continue | |
| choice = choices[0] | |
| delta = choice.get("delta", {}) | |
| finish_reason = choice.get("finish_reason") | |
| if finish_reason == "tool_calls": stop_reason = "tool_use" | |
| elif finish_reason == "length": stop_reason = "max_tokens" | |
| reasoning_delta = delta.get("reasoning_content") | |
| if reasoning_delta: | |
| reasoning_text_acc += reasoning_delta | |
| if reasoning_block_idx is None: | |
| reasoning_block_idx = next_idx; next_idx += 1 | |
| yield (_BLK_START + _dumps({"type": "content_block_start", | |
| "index": reasoning_block_idx, | |
| "content_block": {"type": "thinking", "thinking": ""}}) + "\n\n") | |
| yield (_BLK_DELTA + _dumps({"type": "content_block_delta", | |
| "index": reasoning_block_idx, | |
| "delta": {"type": "thinking_delta", "thinking": reasoning_delta}}) + "\n\n") | |
| text_delta = delta.get("content") | |
| if text_delta: | |
| if reasoning_block_idx is not None and not reasoning_closed: | |
| sig = _synthetic_signature(reasoning_text_acc) | |
| yield (_BLK_DELTA + _dumps({"type": "content_block_delta", | |
| "index": reasoning_block_idx, | |
| "delta": {"type": "signature_delta", "signature": sig}}) + "\n\n") | |
| yield (_BLK_STOP + _dumps({"type": "content_block_stop", | |
| "index": reasoning_block_idx}) + "\n\n") | |
| reasoning_closed = True | |
| if text_block_idx is None: | |
| text_block_idx = next_idx; next_idx += 1 | |
| yield (_BLK_START + _dumps({"type": "content_block_start", | |
| "index": text_block_idx, | |
| "content_block": {"type": "text", "text": ""}}) + "\n\n") | |
| yield (_BLK_DELTA + _dumps({"type": "content_block_delta", | |
| "index": text_block_idx, | |
| "delta": {"type": "text_delta", "text": text_delta}}) + "\n\n") | |
| tool_call_deltas = delta.get("tool_calls") | |
| if tool_call_deltas: | |
| if reasoning_block_idx is not None and not reasoning_closed: | |
| sig = _synthetic_signature(reasoning_text_acc) | |
| yield (_BLK_DELTA + _dumps({"type": "content_block_delta", | |
| "index": reasoning_block_idx, | |
| "delta": {"type": "signature_delta", "signature": sig}}) + "\n\n") | |
| yield (_BLK_STOP + _dumps({"type": "content_block_stop", | |
| "index": reasoning_block_idx}) + "\n\n") | |
| reasoning_closed = True | |
| for tc_delta in tool_call_deltas: | |
| idx = tc_delta.get("index", 0) | |
| if idx not in tool_state: | |
| fn_init = tc_delta.get("function", {}) | |
| tool_state[idx] = { | |
| "id": tc_delta.get("id", f"toolu_{uuid.uuid4().hex[:8]}"), | |
| "name": fn_init.get("name", ""), "args": "", | |
| } | |
| blk = next_idx; next_idx += 1; tool_block_idx[idx] = blk | |
| yield (_BLK_START + _dumps({"type": "content_block_start", "index": blk, | |
| "content_block": {"type": "tool_use", "id": tool_state[idx]["id"], | |
| "name": tool_state[idx]["name"], "input": {}}}) + "\n\n") | |
| fn = tc_delta.get("function", {}) | |
| args = fn.get("arguments", "") | |
| if args: | |
| tool_state[idx]["args"] += args | |
| yield (_BLK_DELTA + _dumps({"type": "content_block_delta", | |
| "index": tool_block_idx[idx], | |
| "delta": {"type": "input_json_delta", "partial_json": args}}) + "\n\n") | |
| except Exception as exc: | |
| logger.warning("stream error: %s", exc) | |
| if reasoning_block_idx is not None and not reasoning_closed: | |
| sig = _synthetic_signature(reasoning_text_acc) | |
| yield (_BLK_DELTA + _dumps({"type": "content_block_delta", "index": reasoning_block_idx, | |
| "delta": {"type": "signature_delta", "signature": sig}}) + "\n\n") | |
| yield (_BLK_STOP + _dumps({"type": "content_block_stop", | |
| "index": reasoning_block_idx}) + "\n\n") | |
| if text_block_idx is not None: | |
| yield (_BLK_STOP + _dumps({"type": "content_block_stop", | |
| "index": text_block_idx}) + "\n\n") | |
| for blk in tool_block_idx.values(): | |
| yield (_BLK_STOP + _dumps({"type": "content_block_stop", "index": blk}) + "\n\n") | |
| yield (_MSG_DELTA + _dumps({"type": "message_delta", | |
| "delta": {"stop_reason": stop_reason, "stop_sequence": None}, | |
| "usage": {"output_tokens": output_tokens}}) + "\n\n") | |
| yield f"{_MSG_STOP}" + '{"type":"message_stop"}' + "\n\n" | |
| async def messages( | |
| request: Request, | |
| authorization: str = Header(default=None), | |
| ): | |
| request_id = uuid.uuid4().hex[:8] | |
| if not OPENCODE_API_KEY: | |
| return JSONResponse(status_code=401, content={"error": "Missing OPENCODE_API_KEY"}) | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| return JSONResponse(status_code=400, content={"error": "Invalid JSON"}) | |
| requested_model = body.get("model", "claude-sonnet-4-6") | |
| mapped_model = MODEL_MAP.get(requested_model, DEFAULT_MODEL) | |
| stream = bool(body.get("stream", False)) | |
| thinking_on = THINKING_MODE == "enabled" | |
| anthropic_tools = body.get("tools", []) | |
| openai_tools = convert_tools_to_openai(anthropic_tools) | |
| openai_tool_choice = map_tool_choice(body.get("tool_choice"), bool(openai_tools), thinking_on) | |
| openai_messages = normalize_messages(body.get("messages", [])) | |
| system_prompt = body.get("system", "") | |
| if system_prompt: | |
| if isinstance(system_prompt, list): | |
| system_prompt = "\n".join( | |
| b.get("text", "") if isinstance(b, dict) else str(b) | |
| for b in system_prompt | |
| ) | |
| openai_messages.insert(0, {"role": "system", "content": system_prompt}) | |
| payload = build_payload(body, openai_messages, openai_tools, | |
| openai_tool_choice, mapped_model, stream) | |
| headers = _make_upstream_headers(OPENCODE_API_KEY) | |
| logger.info("[%s] %s→%s stream=%s tools=%d thinking=%s msgs=%d", | |
| request_id, requested_model, mapped_model, | |
| stream, len(openai_tools), THINKING_MODE, len(openai_messages)) | |
| client = request.app.state.client | |
| last_error = None | |
| for attempt in range(1, MAX_RETRIES + 1): | |
| if attempt > 1: | |
| await asyncio.sleep(0.5 * (attempt - 1)) | |
| try: | |
| if stream: | |
| upstream_req = client.build_request( | |
| "POST", UPSTREAM_URL, json=payload, headers=headers | |
| ) | |
| response = await client.send(upstream_req, stream=True) | |
| if response.status_code == 429: | |
| retry_after = float(response.headers.get("retry-after", "0")) | |
| raw_err = (await response.aread()).decode(errors="ignore") | |
| logger.warning("[%s] 429 — retry-after=%.0fs — body: %s", | |
| request_id, retry_after, raw_err[:400]) | |
| await response.aclose() | |
| return JSONResponse(status_code=429, content={ | |
| "type": "error", "error": { | |
| "type": "rate_limit_error", | |
| "message": f"Upstream rate-limited. retry-after={retry_after:.0f}s. Detail: {raw_err[:200]}", | |
| }}, headers={"retry-after": str(int(retry_after))} if retry_after else {}) | |
| if response.status_code != 200: | |
| raw_err = (await response.aread()).decode(errors="ignore") | |
| logger.warning("[%s] upstream %d: %s", | |
| request_id, response.status_code, raw_err[:300]) | |
| last_error = raw_err; await response.aclose(); continue | |
| return StreamingResponse( | |
| convert_stream_to_anthropic(response, requested_model), | |
| media_type="text/event-stream", | |
| headers=STREAM_RESPONSE_HEADERS, | |
| background=BackgroundTask(response.aclose), | |
| ) | |
| response = await client.post(UPSTREAM_URL, json=payload, headers=headers) | |
| if response.status_code == 429: | |
| retry_after = float(response.headers.get("retry-after", "0")) | |
| logger.warning("[%s] 429 — retry-after=%.0fs", request_id, retry_after) | |
| return JSONResponse(status_code=429, content={ | |
| "type": "error", "error": { | |
| "type": "rate_limit_error", | |
| "message": f"Upstream rate-limited. retry-after={retry_after:.0f}s. Detail: {response.text[:200]}", | |
| }}, headers={"retry-after": str(int(retry_after))} if retry_after else {}) | |
| if response.status_code != 200: | |
| logger.warning("[%s] upstream %d", request_id, response.status_code) | |
| last_error = response.text; continue | |
| raw = response.content | |
| if not raw or not raw.strip(): | |
| return JSONResponse(status_code=502, content={"error": "Empty upstream response"}) | |
| try: parsed = _loads(raw) | |
| except: return JSONResponse(status_code=502, content={"error": "Invalid upstream JSON"}) | |
| return JSONResponse(content=convert_to_anthropic(parsed, requested_model)) | |
| except Exception as exc: | |
| last_error = str(exc) | |
| logger.exception("[%s] exception attempt %d", request_id, attempt) | |
| return JSONResponse(status_code=502, | |
| content={"error": "Upstream failed", "details": last_error}) |