Files
fuel-price/docs/llm-prediction-providers.md
Ovidiu U 6a80c11f38
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
feat: add LLM prediction providers with structured output support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:42:44 +01:00

146 lines
4.6 KiB
Markdown

# LLM Prediction Providers
The oil price direction prediction supports multiple LLM backends behind a shared interface. The active provider is selected via environment variable. All providers return the same response shape and fall back to EWMA if not configured or if the API call fails.
## Selecting a Provider
Set `LLM_PREDICTION_PROVIDER` in `.env`:
```
LLM_PREDICTION_PROVIDER=anthropic # default
LLM_PREDICTION_PROVIDER=openai
LLM_PREDICTION_PROVIDER=gemini
```
Each provider needs its own API key. If the key is missing or empty the provider returns `null` and EWMA is used instead.
---
## Providers
### Anthropic (default)
**Key:** `ANTHROPIC_API_KEY`
**Model:** `ANTHROPIC_MODEL` (default: `claude-sonnet-4-6`)
Uses **tool use** with a forced `submit_prediction` tool call — no JSON parsing, guaranteed schema. Structured output is enforced at the API level via `tool_choice: { type: "tool", name: "submit_prediction" }`.
Two-phase prediction flow:
1. **Context phase** — multi-turn web search (`web_search_20250305` tool) for recent oil/geopolitical news (up to 5 iterations, `pause_turn` loop)
2. **Submission phase** — once searches are complete, forces a `submit_prediction` tool call with the full conversation context
If the context phase fails, falls back to a single-turn basic prediction (tool use only, no web search).
```php
// Structured output schema (enforced by Anthropic)
'input_schema' => [
'type' => 'object',
'properties' => [
'direction' => ['type' => 'string', 'enum' => ['rising', 'falling', 'flat']],
'confidence' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 85],
'reasoning' => ['type' => 'string'],
],
'required' => ['direction', 'confidence', 'reasoning'],
],
```
`PredictionSource`: `llm_with_context` (web search succeeded) or `llm` (basic fallback).
---
### OpenAI
**Key:** `OPENAI_API_KEY`
**Model:** `OPENAI_MODEL` (default: `gpt-4o-mini`)
Uses `response_format: json_schema` with `strict: true`. The schema is sent to the API and the response is guaranteed to match it.
```php
'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,
],
],
],
```
Response is extracted from `choices.0.message.content` (a JSON string) and decoded.
`PredictionSource`: `llm`
---
### Gemini
**Key:** `GEMINI_API_KEY`
**Model:** `GEMINI_MODEL` (default: `gemini-2.0-flash`)
Uses `responseMimeType: application/json` and `responseSchema` in `generationConfig`. The API key is passed as a query parameter.
```php
'generationConfig' => [
'responseMimeType' => 'application/json',
'responseSchema' => [
'type' => 'OBJECT',
'properties' => [
'direction' => ['type' => 'STRING', 'enum' => ['rising', 'falling', 'flat']],
'confidence' => ['type' => 'INTEGER'],
'reasoning' => ['type' => 'STRING'],
],
'required' => ['direction', 'confidence', 'reasoning'],
],
],
```
Response is extracted from `candidates.0.content.parts.0.text` (a JSON string) and decoded.
`PredictionSource`: `llm`
---
## Confidence Caps
All providers cap confidence at **85** regardless of what the model returns. EWMA is capped at **65**.
---
## EWMA Fallback
`OilPriceService::generatePrediction()` always runs EWMA first and stores its result. The LLM provider runs after; its result is stored and returned if non-null. If the provider returns null (key missing, API error, malformed response), EWMA is returned instead.
```
generatePrediction()
├── generateEwmaPrediction() → always stored
└── provider->predict()
├── on success → stored and returned (LLM wins)
└── on null → EWMA returned
```
---
## Adding a New Provider
1. Create `app/Services/LlmPrediction/YourProvider.php` implementing `OilPredictionProvider`
2. Add a case to the `match` in `AppServiceProvider::register()`
3. Add key/model config to `config/services.php` and document the `.env` vars
The interface requires one method:
```php
public function predict(Collection $prices): ?PricePrediction;
```
Return `null` on any failure — the orchestrator handles the fallback.