seedproject-web/api/app/LLM/svcDeepSeek.php
Carlos Arias 1559ce017d chore: scaffold SeedProject base (Phase 1)
Clean-room copy of the reusable engines from comiida, with all
instance data, secrets, dependencies, and build output excluded:
- app/         Astro theme skeleton (no comiida blog posts; hero image -> placeholder)
- api/         SeedProject PHP framework (no vendor/.env/config.php)
- content-pipeline/  engine only (scripts/admin/prompts; empty runtime state)
- astroagent.config.json + app/.astroagent/skills

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
2026-07-04 22:53:10 +00:00

177 lines
5.8 KiB
PHP

<?php
namespace App\LLM;
/**
* DeepSeek provider implementation.
*
* DeepSeek is fully OpenAI API-compatible, so the structure mirrors
* svcOpenAI with DeepSeek-specific endpoints and models.
*
* Config keys (from sp_orgs_meta metval JSON):
* secretKey — required — DeepSeek API key
* model — optional — default: deepseek-chat
* max_tokens — optional — default: 4096
* temperature — optional — default: 0.7
*
* Usage:
* $llm = LLMManager::forOrg($orgId, 'deepseek');
* $res = $llm->sendPrompt([['role' => 'user', 'content' => 'Hello']]);
* echo $res['content'];
*/
class svcDeepSeek extends LLMProvider
{
private const API_BASE = 'https://api.deepseek.com/v1';
private const API_CHAT = self::API_BASE . '/chat/completions';
private const API_MODELS = self::API_BASE . '/models';
private static array $availableModels = [
['id' => 'deepseek-chat', 'label' => 'DeepSeek Chat (V3)'],
['id' => 'deepseek-reasoner', 'label' => 'DeepSeek Reasoner (R1)'],
];
public function __construct(array $config)
{
if (empty($config['secretKey'])) {
throw new \Exception('DeepSeek secretKey is required');
}
$this->apiKey = $config['secretKey'];
$this->model = $config['model'] ?? 'deepseek-chat';
$this->maxTokens = (int)($config['max_tokens'] ?? 4096);
$this->temperature = (float)($config['temperature'] ?? 0.7);
}
/**
* Send a chat prompt and return the full response.
*/
public function sendPrompt(array $messages, array $options = []): array
{
try {
$payload = $this->buildPayload($messages, $options);
$response = $this->curlPost(self::API_CHAT, $this->headers(), $payload);
$content = $response['choices'][0]['message']['content'] ?? '';
$usage = $response['usage'] ?? [];
// DeepSeek Reasoner includes reasoning_content separately
$reasoning = $response['choices'][0]['message']['reasoning_content'] ?? '';
return $this->successResponse($content, [
'prompt_tokens' => $usage['prompt_tokens'] ?? 0,
'completion_tokens' => $usage['completion_tokens'] ?? 0,
'total_tokens' => $usage['total_tokens'] ?? 0,
'reasoning_tokens' => $usage['completion_tokens_details']['reasoning_tokens'] ?? 0,
'reasoning_content' => $reasoning,
]);
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage());
}
}
/**
* Stream a chat prompt, calling $callback with each text chunk.
*
* @param callable $callback function(string $chunk)
*/
public function streamPrompt(array $messages, array $options, callable $callback): array
{
try {
$payload = $this->buildPayload($messages, $options);
$this->curlStream(self::API_CHAT, $this->headers(), $payload, function(array $chunk) use ($callback) {
$delta = $chunk['choices'][0]['delta']['content'] ?? '';
if ($delta !== '') {
$callback($delta);
}
});
return ['success' => true, 'error' => ''];
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* Return supported DeepSeek models.
*/
public function getModels(): array
{
return self::$availableModels;
}
/**
* Validate the API key via the models endpoint.
*/
public function testConnection(): array
{
try {
$start = microtime(true);
$ch = curl_init(self::API_MODELS);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $this->headers(),
CURLOPT_TIMEOUT => $this->timeout,
]);
$body = curl_exec($ch);
$error = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$latency = (int)((microtime(true) - $start) * 1000);
if ($error) throw new \Exception("cURL error: $error");
$decoded = json_decode($body, true);
if ($code !== 200) {
$msg = $decoded['error']['message'] ?? "HTTP $code";
throw new \Exception($msg);
}
return ['success' => true, 'latency_ms' => $latency, 'error' => ''];
} catch (\Exception $e) {
return ['success' => false, 'latency_ms' => 0, 'error' => $e->getMessage()];
}
}
// ─── Private Helpers ─────────────────────────────────────────────────────
private function headers(): array
{
return [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey,
];
}
private function buildPayload(array $messages, array $options): array
{
$model = $options['model'] ?? $this->model;
$maxTokens = (int)($options['max_tokens'] ?? $this->maxTokens);
$temperature = (float)($options['temperature'] ?? $this->temperature);
if ($this->systemPrompt !== '') {
array_unshift($messages, ['role' => 'system', 'content' => $this->systemPrompt]);
}
$payload = [
'model' => $model,
'messages' => $messages,
'max_tokens' => $maxTokens,
'temperature' => $temperature,
];
// deepseek-reasoner does not support temperature
if ($model === 'deepseek-reasoner') {
unset($payload['temperature']);
}
return $payload;
}
}