Cpptai / src /cpptai /deepseek_client.py
Fra150
CPPTAI v2 + Gradio demo offline (rispetta .gitignore)
c0e3412
Raw History Blame Contribute Delete
4.68 kB
"""Minimal DeepSeek API client using only the Python standard library.
This client targets the Chat Completions endpoint and defaults to model
"DeepSeek-V3.2-Exp" per user request. It reads the API key from the
environment variable `DEEPSEEK_API_KEY` and avoids external dependencies.
"""
from __future__ import annotations
import json
import os
import ssl
from typing import Dict, List, Optional
from urllib.request import Request, urlopen
from .env import load_env
from pathlib import Path
import hashlib
import urllib.error
DEEPSEEK_BASE_URL = "https://api.deepseek.com"
CHAT_COMPLETIONS_PATH = "/chat/completions"
_MODEL_ALIASES = {
"DeepSeek-V3.2-Exp": "deepseek-chat",
"DeepSeek-V3.2": "deepseek-chat",
"DeepSeek-V3": "deepseek-chat",
"DeepSeek-R1": "deepseek-reasoner",
}
def _normalize_model_name(model: str) -> str:
raw = (model or "").strip()
if raw in _MODEL_ALIASES:
return _MODEL_ALIASES[raw]
return raw or "deepseek-chat"
def deepseek_chat(
messages: List[Dict[str, str]],
model: str = "DeepSeek-V3.2-Exp",
stream: bool = False,
base_url: str = DEEPSEEK_BASE_URL,
temperature: float = 0,
max_tokens: int = 2048,
) -> Optional[Dict]:
"""Call DeepSeek Chat Completions API and return the parsed JSON response.
Args:
messages: Conversation messages in OpenAI-compatible format.
model: Model name. Defaults to "DeepSeek-V3.2-Exp" per the request.
stream: When True, asks the API to stream. This client does not handle
streaming responses; the flag is forwarded as-is.
base_url: API base URL. Default points to the official DeepSeek API.
temperature: Sampling temperature (0 = deterministic). Defaults to 0.
max_tokens: Maximum tokens in the response. Defaults to 2048.
Returns:
Parsed JSON dictionary on success, or None if a recoverable error occurs.
"""
# Load .env once before reading variables.
load_env()
api_key = (os.getenv("DEEPSEEK_API_KEY") or "").strip()
if not api_key:
# Fail gracefully if no key is present.
return None
url = f"{base_url}{CHAT_COMPLETIONS_PATH}"
normalized_model = _normalize_model_name(model)
body = {
"model": normalized_model,
"messages": messages,
"stream": stream,
"temperature": temperature,
"max_tokens": max_tokens,
"seed": 0,
}
data = json.dumps(body, sort_keys=True).encode("utf-8")
req = Request(url, data=data, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", f"Bearer {api_key}")
# Create a default SSL context; can be customized if needed.
context = ssl.create_default_context()
use_cache = os.getenv("DEEPSEEK_CACHE", "0") == "1"
cache_dir = Path(".cache")
cache_dir.mkdir(exist_ok=True)
cache_key = hashlib.sha256((url + data.decode("utf-8")).encode("utf-8")).hexdigest()
cache_path = cache_dir / f"deepseek_{cache_key}.json"
import time as _time
max_retries = 3
for attempt in range(max_retries):
try:
if use_cache and cache_path.exists():
return json.loads(cache_path.read_text(encoding="utf-8"))
with urlopen(req, context=context, timeout=30) as resp:
payload = resp.read().decode("utf-8")
parsed = json.loads(payload)
if use_cache:
try:
cache_path.write_text(json.dumps(parsed, ensure_ascii=False), encoding="utf-8")
except (IOError, OSError):
pass
return parsed
except urllib.error.HTTPError as e:
if e.code == 429 and attempt < max_retries - 1:
wait = 2 ** attempt
_time.sleep(wait)
continue
return None
except (urllib.error.URLError, ssl.SSLError, json.JSONDecodeError, TimeoutError):
if attempt < max_retries - 1:
_time.sleep(1)
continue
return None
return None
def extract_text_answer(response: Dict) -> Optional[str]:
"""Extract the assistant text from a chat completions response.
The function safely navigates the typical OpenAI-compatible structure and
returns None if the expected fields are absent.
"""
try:
choices = response.get("choices") or []
if not choices:
return None
message = choices[0].get("message") or {}
return message.get("content")
except (KeyError, IndexError, TypeError) as e:
return None