Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 84 additions & 9 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def __init__(self, message: str, status_code: Optional[int] = None):

class DeepSeekAPI:
BASE_URL = "https://chat.deepseek.com/api/v0"
MODEL_TYPE = 'default' # 'default' — supports files (only text data, images, and documents)
# 'expert' — does not support files (only prompts)
# 'vision' — supports files (any images and documents)

def __init__(self, auth_token: str):
if not auth_token or not isinstance(auth_token, str):
Expand Down Expand Up @@ -88,7 +91,7 @@ def _get_headers(self, pow_response: Optional[str] = None) -> Dict[str, str]:
'x-app-version': '20241129.1',
'x-client-locale': 'en_US',
'x-client-platform': 'web',
'x-client-version': '1.0.0-always',
'x-client-version': '2.0.0',
}

if pow_response:
Expand Down Expand Up @@ -211,7 +214,7 @@ async def create_chat_session(self) -> str:
'/chat_session/create',
{'character_id': None}
)
return response['data']['biz_data']['id']
return response['data']['biz_data']['chat_session']['id']
except KeyError:
raise APIError("Invalid session creation response format from server")

Expand All @@ -230,7 +233,6 @@ async def delete_chat_session(self, chat_session_id: str) -> str:
async def _upload_single_file(self, file_path: str) -> str:
"""Upload a single file and return its ID"""
url = f"{self.BASE_URL}/file/upload_file"

# Get challenge and solve it
challenge = await self._get_pow_challenge_for_upload()
pow_response = await self.pow_solver.solve_challenge(challenge)
Expand All @@ -243,11 +245,12 @@ async def _upload_single_file(self, file_path: str) -> str:
'origin': 'https://chat.deepseek.com',
'referer': 'https://chat.deepseek.com/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
'x-app-version': '20241129.1',
'x-app-version': '2.0.0',
'x-client-locale': 'en_US',
'x-client-platform': 'web',
'x-client-version': '1.0.0-always',
'x-client-version': '2.0.0',
'x-ds-pow-response': pow_response,
'x-model-type': self.MODEL_TYPE
}

retry_count = 0
Expand Down Expand Up @@ -288,15 +291,82 @@ async def _upload_single_file(self, file_path: str) -> str:
raise APIError(text, response.status_code)

result = json.loads(text)
return result['data']['biz_data']['id']


if result['data'] is not None:
file_id = result['data']['biz_data']['id']
# We wait for the file to be ready with a timeout of 60 seconds, polling every 2 seconds.
ready_file_id = await self._wait_for_file_ready(file_id, timeout=60.0, poll_interval=2.0)
return ready_file_id

except Exception as e:
if retry_count >= max_retries - 1:
raise NetworkError(f"Failed to upload {file_path}: {str(e)}")
retry_count += 1

raise APIError(f"Failed to upload {file_path} after retries")

async def _wait_for_file_ready(self, file_id: str, timeout: float = 60.0, poll_interval: float = 2.0) -> str:
"""
Wait until uploaded file is successfully processed.
Returns file_id on success, empty string on failure/timeout.
"""
start = asyncio.get_event_loop().time()
last_error = None

while True:
elapsed = asyncio.get_event_loop().time() - start
if elapsed > timeout:
print(f"\033[93mTimeout waiting for file {file_id} to be ready\033[0m", file=sys.stderr)
return ''

try:
url = f"{self.BASE_URL}/file/fetch_files?file_ids={file_id}"
challenge = await self._get_pow_challenge()
pow_response = await self.pow_solver.solve_challenge(challenge)
headers = self._get_headers(pow_response)

res = await self.session.get(
url,
headers=headers,
cookies=self.cookies,
impersonate='chrome120'
)
text = res.text
if "<!DOCTYPE html>" in text and "Just a moment" in text:
print("Cloudflare while polling file status", file=sys.stderr)
await self._refresh_cookies()
await asyncio.sleep(poll_interval)
continue

result = json.loads(text)

# Check file ready status
if result.get('data') and result['data'].get('biz_data'):
files = result['data']['biz_data'].get('files', [])
if files:
status = files[0].get('status')
if status == "SUCCESS":
return file_id
elif status == "PARSING":
await asyncio.sleep(poll_interval)
continue
else:
print(f"File {file_id} ended with unexpected status: {status}", file=sys.stderr)
return ''
else:
print(f"Unexpected API response while polling file {file_id}", file=sys.stderr)
return ''

except json.JSONDecodeError as e:
last_error = f"JSON decode error: {e}"
except Exception as e:
last_error = str(e)

# Pause before retrying after an error
print(f"\033[93mError polling file {file_id} (will retry): {last_error}\033[0m", file=sys.stderr)
await asyncio.sleep(poll_interval)


async def upload_files(self, file_paths: List[str]) -> List[str]:
"""
Upload multiple files concurrently and return their IDs
Expand All @@ -309,9 +379,11 @@ async def upload_files(self, file_paths: List[str]) -> List[str]:
"""
# Create tasks for concurrent uploads
tasks = [self._upload_single_file(file_path) for file_path in file_paths]

# Run all uploads concurrently
file_ids = await asyncio.gather(*tasks)

file_ids = [res for res in file_ids if res != '']

return file_ids

Expand Down Expand Up @@ -353,6 +425,9 @@ async def chat_completion(
'ref_file_ids': ref_file_ids if ref_file_ids else [],
'thinking_enabled': thinking_enabled,
'search_enabled': search_enabled,
'model_type': self.MODEL_TYPE,
'preempt': False,
'action': None
}

# Get challenge and solve it
Expand Down Expand Up @@ -499,4 +574,4 @@ def _parse_chunk_sync(self, chunk: str) -> Optional[Dict[str, Any]]:
return None
async def close(self):
"""Close the async session"""
await self.session.close()
await self.session.close()