Anthropic, Gemini, and OpenAi providers each repeated: API-key gate, chronological price-list building, response validation (direction/confidence/reasoning), TrendDirection::tryFrom, confidence cap at 85, and the top-level try/catch + Log::error. Now in AbstractLlmPredictionProvider: - LLM_MAX_CONFIDENCE constant - buildPriceList(Collection) helper - buildPrediction(input, ?source) — handles direction validation, confidence cap, model construction - defaultPrompt(priceList) — shared by Gemini and OpenAi - Default predict() flow (apiKey + callProvider + buildPrediction + try/catch). Gemini and OpenAi only implement apiKey() and callProvider(). Anthropic overrides predict() because of its multi-phase web-search + forced-tool flow but reuses the helpers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
2.1 KiB
PHP
63 lines
2.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\LlmPrediction;
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class OpenAiPredictionProvider extends AbstractLlmPredictionProvider
|
|
{
|
|
protected function apiKey(): ?string
|
|
{
|
|
return config('services.openai.api_key');
|
|
}
|
|
|
|
protected function callProvider(string $apiKey, string $priceList): ?array
|
|
{
|
|
$url = 'https://api.openai.com/v1/chat/completions';
|
|
|
|
$response = $this->apiLogger->send('openai', 'POST', $url, fn () => Http::timeout(15)
|
|
->withToken($apiKey)
|
|
->post($url, [
|
|
'model' => config('services.openai.model', 'gpt-4o-mini'),
|
|
'response_format' => [
|
|
'type' => 'json_schema',
|
|
'json_schema' => [
|
|
'name' => 'oil_prediction',
|
|
'strict' => true,
|
|
'schema' => [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'direction' => ['type' => 'string', 'enum' => ['rising', 'falling', 'flat']],
|
|
'confidence' => ['type' => 'integer'],
|
|
'reasoning' => ['type' => 'string'],
|
|
],
|
|
'required' => ['direction', 'confidence', 'reasoning'],
|
|
'additionalProperties' => false,
|
|
],
|
|
],
|
|
],
|
|
'messages' => [[
|
|
'role' => 'user',
|
|
'content' => $this->defaultPrompt($priceList),
|
|
]],
|
|
]));
|
|
|
|
if (! $response->successful()) {
|
|
Log::error(self::class.': request failed', ['status' => $response->status()]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($response->json('choices.0.message.content') ?? '{}', true);
|
|
|
|
if (! isset($data['direction'], $data['confidence'], $data['reasoning'])) {
|
|
Log::error(self::class.': unexpected response format', ['data' => $data]);
|
|
|
|
return null;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|