feat: AI model selector + i18n nav labels (RU/EN) on new modules
AI model selector:
- AiAssistantService::MODEL_DEFAULTS and MODEL_OPTIONS const tables (3 picks per
provider: Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5, OpenAI 4o / 4o-mini,
Gemini 1.5 Pro / Flash). Default upgraded from Sonnet 4.5 → Sonnet 4.6.
- modelFor(provider, company?) resolves tenant override > global default.
- All 8 hardcoded model strings replaced with modelFor() across callClaude
(chat with tool-use), callOpenAI, callGemini (chat), postClaude/postOpenAI/
postGemini (single-shot), and OcrInvoiceService.
- Settings page adds 3 model selectors per provider with persistence at
settings.ai.models.{claude,gpt,gemini}.
i18n nav labels:
- TireSet / Bodyshop / Subcontractor / SubcontractJob / PricingCoefficient /
ShopCustomer resources: getNavigationLabel / getNavigationGroup /
getModelLabel / getPluralModelLabel return __()-wrapped strings.
- 20 keys added to lang/ru.json and lang/en.json.
Tests (4 new): default model, tenant override wins, unknown provider falls
back to claude default, options dictionary contains each default key.
Full suite: 134 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,9 @@ class Settings extends Page
|
|||||||
'ai_claude_key' => $settings['ai']['claude_key'] ?? null,
|
'ai_claude_key' => $settings['ai']['claude_key'] ?? null,
|
||||||
'ai_gpt_key' => $settings['ai']['gpt_key'] ?? null,
|
'ai_gpt_key' => $settings['ai']['gpt_key'] ?? null,
|
||||||
'ai_gemini_key' => $settings['ai']['gemini_key'] ?? null,
|
'ai_gemini_key' => $settings['ai']['gemini_key'] ?? null,
|
||||||
|
'ai_model_claude' => data_get($settings, 'ai.models.claude', \App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['claude']),
|
||||||
|
'ai_model_gpt' => data_get($settings, 'ai.models.gpt', \App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['gpt']),
|
||||||
|
'ai_model_gemini' => data_get($settings, 'ai.models.gemini', \App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['gemini']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +196,20 @@ class Settings extends Page
|
|||||||
->options(['claude' => 'Claude (Anthropic)', 'gpt' => 'ChatGPT (OpenAI)', 'gemini' => 'Gemini (Google)'])
|
->options(['claude' => 'Claude (Anthropic)', 'gpt' => 'ChatGPT (OpenAI)', 'gemini' => 'Gemini (Google)'])
|
||||||
->default('claude'),
|
->default('claude'),
|
||||||
Forms\Components\TextInput::make('ai_claude_key')->label('Claude API Key')->password()->revealable()->placeholder('sk-ant-...'),
|
Forms\Components\TextInput::make('ai_claude_key')->label('Claude API Key')->password()->revealable()->placeholder('sk-ant-...'),
|
||||||
|
Forms\Components\Select::make('ai_model_claude')
|
||||||
|
->label('Model Claude')
|
||||||
|
->options(\App\Services\Ai\AiAssistantService::MODEL_OPTIONS['claude'])
|
||||||
|
->default(\App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['claude']),
|
||||||
Forms\Components\TextInput::make('ai_gpt_key')->label('OpenAI API Key')->password()->revealable()->placeholder('sk-proj-...'),
|
Forms\Components\TextInput::make('ai_gpt_key')->label('OpenAI API Key')->password()->revealable()->placeholder('sk-proj-...'),
|
||||||
|
Forms\Components\Select::make('ai_model_gpt')
|
||||||
|
->label('Model OpenAI')
|
||||||
|
->options(\App\Services\Ai\AiAssistantService::MODEL_OPTIONS['gpt'])
|
||||||
|
->default(\App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['gpt']),
|
||||||
Forms\Components\TextInput::make('ai_gemini_key')->label('Gemini API Key')->password()->revealable(),
|
Forms\Components\TextInput::make('ai_gemini_key')->label('Gemini API Key')->password()->revealable(),
|
||||||
|
Forms\Components\Select::make('ai_model_gemini')
|
||||||
|
->label('Model Gemini')
|
||||||
|
->options(\App\Services\Ai\AiAssistantService::MODEL_OPTIONS['gemini'])
|
||||||
|
->default(\App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['gemini']),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->statePath('data');
|
->statePath('data');
|
||||||
@@ -246,6 +261,11 @@ class Settings extends Page
|
|||||||
'claude_key' => $data['ai_claude_key'] ?? null,
|
'claude_key' => $data['ai_claude_key'] ?? null,
|
||||||
'gpt_key' => $data['ai_gpt_key'] ?? null,
|
'gpt_key' => $data['ai_gpt_key'] ?? null,
|
||||||
'gemini_key' => $data['ai_gemini_key'] ?? null,
|
'gemini_key' => $data['ai_gemini_key'] ?? null,
|
||||||
|
'models' => [
|
||||||
|
'claude' => $data['ai_model_claude'] ?? \App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['claude'],
|
||||||
|
'gpt' => $data['ai_model_gpt'] ?? \App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['gpt'],
|
||||||
|
'gemini' => $data['ai_model_gemini'] ?? \App\Services\Ai\AiAssistantService::MODEL_DEFAULTS['gemini'],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -134,6 +134,27 @@ class BodyshopJobResource extends Resource
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('Tinichigerie / Detailing');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('Tinichigerie');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('lucrare caroserie');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('lucrări caroserie');
|
||||||
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -97,6 +97,27 @@ class PricingCoefficientResource extends Resource
|
|||||||
->defaultSort('priority');
|
->defaultSort('priority');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('Coeficienți preț');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('Depozit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('coeficient');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('coeficienți preț');
|
||||||
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -93,6 +93,27 @@ class ShopCustomerResource extends Resource
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('Clienți magazin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('Magazin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('client magazin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('clienți magazin');
|
||||||
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -122,6 +122,27 @@ class SubcontractJobResource extends Resource
|
|||||||
->defaultSort('created_at', 'desc');
|
->defaultSort('created_at', 'desc');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('Lucrări terți');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('Subcontractare');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('lucrare terți');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('lucrări terți');
|
||||||
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -73,6 +73,27 @@ class SubcontractorResource extends Resource
|
|||||||
->defaultSort('name');
|
->defaultSort('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('Subcontractori');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('Subcontractare');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('subcontractor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('subcontractori');
|
||||||
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -185,6 +185,27 @@ class TireSetResource extends Resource
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('Seturi anvelope');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('Anvelope');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('set anvelope');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('seturi anvelope');
|
||||||
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -21,6 +21,37 @@ use Illuminate\Support\Facades\Http;
|
|||||||
*/
|
*/
|
||||||
class AiAssistantService
|
class AiAssistantService
|
||||||
{
|
{
|
||||||
|
/** Per-provider default model + the dropdown options exposed in Settings. */
|
||||||
|
public const MODEL_DEFAULTS = [
|
||||||
|
'claude' => 'claude-sonnet-4-6',
|
||||||
|
'gpt' => 'gpt-4o-mini',
|
||||||
|
'gemini' => 'gemini-1.5-flash',
|
||||||
|
];
|
||||||
|
|
||||||
|
public const MODEL_OPTIONS = [
|
||||||
|
'claude' => [
|
||||||
|
'claude-opus-4-7' => 'Opus 4.7 — cel mai capabil',
|
||||||
|
'claude-sonnet-4-6' => 'Sonnet 4.6 — echilibrat (recomandat)',
|
||||||
|
'claude-haiku-4-5-20251001' => 'Haiku 4.5 — rapid și ieftin',
|
||||||
|
],
|
||||||
|
'gpt' => [
|
||||||
|
'gpt-4o' => 'GPT-4o',
|
||||||
|
'gpt-4o-mini' => 'GPT-4o mini (recomandat)',
|
||||||
|
],
|
||||||
|
'gemini' => [
|
||||||
|
'gemini-1.5-pro' => 'Gemini 1.5 Pro',
|
||||||
|
'gemini-1.5-flash' => 'Gemini 1.5 Flash (recomandat)',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Resolve the model id for a provider — tenant override > global default. */
|
||||||
|
public function modelFor(string $provider, ?Company $company = null): string
|
||||||
|
{
|
||||||
|
$company ??= $this->currentCompany();
|
||||||
|
$override = $company ? data_get($company->settings, "ai.models.{$provider}") : null;
|
||||||
|
return $override ?: (self::MODEL_DEFAULTS[$provider] ?? 'claude-sonnet-4-6');
|
||||||
|
}
|
||||||
|
|
||||||
public function ask(AiChat $chat, string $userMessage): AiMessage
|
public function ask(AiChat $chat, string $userMessage): AiMessage
|
||||||
{
|
{
|
||||||
// Persist user message.
|
// Persist user message.
|
||||||
@@ -148,7 +179,7 @@ TXT;
|
|||||||
$r = Http::withHeaders($headers)->timeout(60)->post(
|
$r = Http::withHeaders($headers)->timeout(60)->post(
|
||||||
'https://api.anthropic.com/v1/messages',
|
'https://api.anthropic.com/v1/messages',
|
||||||
[
|
[
|
||||||
'model' => 'claude-sonnet-4-5',
|
'model' => $this->modelFor('claude', $company),
|
||||||
'max_tokens' => 1024,
|
'max_tokens' => 1024,
|
||||||
'system' => $system,
|
'system' => $system,
|
||||||
'tools' => $tools,
|
'tools' => $tools,
|
||||||
@@ -214,7 +245,7 @@ TXT;
|
|||||||
$r = Http::withHeaders(['Authorization' => 'Bearer ' . $key, 'content-type' => 'application/json'])
|
$r = Http::withHeaders(['Authorization' => 'Bearer ' . $key, 'content-type' => 'application/json'])
|
||||||
->timeout(60)
|
->timeout(60)
|
||||||
->post('https://api.openai.com/v1/chat/completions', [
|
->post('https://api.openai.com/v1/chat/completions', [
|
||||||
'model' => 'gpt-4o-mini',
|
'model' => $this->modelFor('gpt', $company),
|
||||||
'messages' => $messages,
|
'messages' => $messages,
|
||||||
'max_tokens' => 1024,
|
'max_tokens' => 1024,
|
||||||
]);
|
]);
|
||||||
@@ -390,7 +421,7 @@ TXT;
|
|||||||
])
|
])
|
||||||
->timeout(60)
|
->timeout(60)
|
||||||
->post('https://api.anthropic.com/v1/messages', [
|
->post('https://api.anthropic.com/v1/messages', [
|
||||||
'model' => 'claude-sonnet-4-5',
|
'model' => $this->modelFor('claude'),
|
||||||
'max_tokens' => 1024,
|
'max_tokens' => 1024,
|
||||||
'system' => $system,
|
'system' => $system,
|
||||||
'messages' => $messages,
|
'messages' => $messages,
|
||||||
@@ -413,7 +444,7 @@ TXT;
|
|||||||
$r = Http::withHeaders(['Authorization' => 'Bearer ' . $key, 'content-type' => 'application/json'])
|
$r = Http::withHeaders(['Authorization' => 'Bearer ' . $key, 'content-type' => 'application/json'])
|
||||||
->timeout(60)
|
->timeout(60)
|
||||||
->post('https://api.openai.com/v1/chat/completions', [
|
->post('https://api.openai.com/v1/chat/completions', [
|
||||||
'model' => 'gpt-4o-mini',
|
'model' => $this->modelFor('gpt'),
|
||||||
'messages' => array_merge([['role' => 'system', 'content' => $system]], $messages),
|
'messages' => array_merge([['role' => 'system', 'content' => $system]], $messages),
|
||||||
'max_tokens' => 1024,
|
'max_tokens' => 1024,
|
||||||
]);
|
]);
|
||||||
@@ -442,7 +473,7 @@ TXT;
|
|||||||
}
|
}
|
||||||
$r = Http::withHeaders(['content-type' => 'application/json'])
|
$r = Http::withHeaders(['content-type' => 'application/json'])
|
||||||
->timeout(60)
|
->timeout(60)
|
||||||
->post('https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=' . $key, [
|
->post('https://generativelanguage.googleapis.com/v1beta/models/' . $this->modelFor('gemini') . ':generateContent?key=' . $key, [
|
||||||
'systemInstruction' => ['parts' => [['text' => $system]]],
|
'systemInstruction' => ['parts' => [['text' => $system]]],
|
||||||
'contents' => $contents,
|
'contents' => $contents,
|
||||||
'generationConfig' => ['maxOutputTokens' => 1024],
|
'generationConfig' => ['maxOutputTokens' => 1024],
|
||||||
@@ -452,7 +483,7 @@ TXT;
|
|||||||
}
|
}
|
||||||
$body = $r->json();
|
$body = $r->json();
|
||||||
$text = $body['candidates'][0]['content']['parts'][0]['text'] ?? '(răspuns gol)';
|
$text = $body['candidates'][0]['content']['parts'][0]['text'] ?? '(răspuns gol)';
|
||||||
return [$text, ['model' => 'gemini-1.5-flash', 'tokens' => $body['usageMetadata'] ?? null]];
|
return [$text, ['model' => $this->modelFor('gemini'), 'tokens' => $body['usageMetadata'] ?? null]];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function currentCompany(): ?Company
|
protected function currentCompany(): ?Company
|
||||||
@@ -474,7 +505,7 @@ TXT;
|
|||||||
|
|
||||||
$r = Http::withHeaders(['content-type' => 'application/json'])
|
$r = Http::withHeaders(['content-type' => 'application/json'])
|
||||||
->timeout(60)
|
->timeout(60)
|
||||||
->post('https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=' . $key, [
|
->post('https://generativelanguage.googleapis.com/v1beta/models/' . $this->modelFor('gemini', $company) . ':generateContent?key=' . $key, [
|
||||||
'systemInstruction' => ['parts' => [['text' => $this->buildSystemPrompt($company)]]],
|
'systemInstruction' => ['parts' => [['text' => $this->buildSystemPrompt($company)]]],
|
||||||
'contents' => $contents,
|
'contents' => $contents,
|
||||||
'generationConfig' => ['maxOutputTokens' => 1024],
|
'generationConfig' => ['maxOutputTokens' => 1024],
|
||||||
@@ -486,7 +517,7 @@ TXT;
|
|||||||
$body = $r->json();
|
$body = $r->json();
|
||||||
$text = $body['candidates'][0]['content']['parts'][0]['text'] ?? '(răspuns gol)';
|
$text = $body['candidates'][0]['content']['parts'][0]['text'] ?? '(răspuns gol)';
|
||||||
return [$text, [
|
return [$text, [
|
||||||
'model' => 'gemini-1.5-flash',
|
'model' => $this->modelFor('gemini', $company),
|
||||||
'tokens' => $body['usageMetadata'] ?? null,
|
'tokens' => $body['usageMetadata'] ?? null,
|
||||||
]];
|
]];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class OcrInvoiceService
|
|||||||
])
|
])
|
||||||
->timeout(60)
|
->timeout(60)
|
||||||
->post('https://api.anthropic.com/v1/messages', [
|
->post('https://api.anthropic.com/v1/messages', [
|
||||||
'model' => 'claude-sonnet-4-5',
|
'model' => app(AiAssistantService::class)->modelFor('claude', $company),
|
||||||
'max_tokens' => 2048,
|
'max_tokens' => 2048,
|
||||||
'system' => $this->systemPrompt(),
|
'system' => $this->systemPrompt(),
|
||||||
'messages' => [[
|
'messages' => [[
|
||||||
|
|||||||
+21
-2
@@ -21,7 +21,6 @@
|
|||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
"Notifications": "Notifications",
|
"Notifications": "Notifications",
|
||||||
|
|
||||||
"Clienți": "Clients",
|
"Clienți": "Clients",
|
||||||
"Mașini": "Vehicles",
|
"Mașini": "Vehicles",
|
||||||
"Cereri": "Leads",
|
"Cereri": "Leads",
|
||||||
@@ -53,5 +52,25 @@
|
|||||||
"Jurnal": "Audit log",
|
"Jurnal": "Audit log",
|
||||||
"Telefonie": "Calls",
|
"Telefonie": "Calls",
|
||||||
"Finanțe": "Finance",
|
"Finanțe": "Finance",
|
||||||
"Site PSauto": "Public site"
|
"Site PSauto": "Public site",
|
||||||
|
"Seturi anvelope": "Tire sets",
|
||||||
|
"Anvelope": "Tires",
|
||||||
|
"set anvelope": "tire set",
|
||||||
|
"seturi anvelope": "tire sets",
|
||||||
|
"Tinichigerie / Detailing": "Body / Detailing",
|
||||||
|
"Tinichigerie": "Body shop",
|
||||||
|
"lucrare caroserie": "body job",
|
||||||
|
"lucrări caroserie": "body jobs",
|
||||||
|
"Subcontractori": "Subcontractors",
|
||||||
|
"Subcontractare": "Subcontracting",
|
||||||
|
"subcontractor": "subcontractor",
|
||||||
|
"Lucrări terți": "Outsourced jobs",
|
||||||
|
"lucrare terți": "outsourced job",
|
||||||
|
"lucrări terți": "outsourced jobs",
|
||||||
|
"Coeficienți preț": "Pricing coefficients",
|
||||||
|
"coeficient": "coefficient",
|
||||||
|
"Magazin": "Shop",
|
||||||
|
"Clienți magazin": "Shop customers",
|
||||||
|
"client magazin": "shop customer",
|
||||||
|
"clienți magazin": "shop customers"
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-2
@@ -21,7 +21,6 @@
|
|||||||
"Status": "Статус",
|
"Status": "Статус",
|
||||||
"Actions": "Действия",
|
"Actions": "Действия",
|
||||||
"Notifications": "Уведомления",
|
"Notifications": "Уведомления",
|
||||||
|
|
||||||
"Clienți": "Клиенты",
|
"Clienți": "Клиенты",
|
||||||
"Mașini": "Машины",
|
"Mașini": "Машины",
|
||||||
"Cereri": "Заявки",
|
"Cereri": "Заявки",
|
||||||
@@ -53,5 +52,25 @@
|
|||||||
"Jurnal": "Журнал",
|
"Jurnal": "Журнал",
|
||||||
"Telefonie": "Телефония",
|
"Telefonie": "Телефония",
|
||||||
"Finanțe": "Финансы",
|
"Finanțe": "Финансы",
|
||||||
"Site PSauto": "Сайт"
|
"Site PSauto": "Сайт",
|
||||||
|
"Seturi anvelope": "Шины (комплекты)",
|
||||||
|
"Anvelope": "Шины",
|
||||||
|
"set anvelope": "комплект шин",
|
||||||
|
"seturi anvelope": "комплекты шин",
|
||||||
|
"Tinichigerie / Detailing": "Кузов / Детейлинг",
|
||||||
|
"Tinichigerie": "Кузовной цех",
|
||||||
|
"lucrare caroserie": "кузовная работа",
|
||||||
|
"lucrări caroserie": "кузовные работы",
|
||||||
|
"Subcontractori": "Субподрядчики",
|
||||||
|
"Subcontractare": "Субподряд",
|
||||||
|
"subcontractor": "субподрядчик",
|
||||||
|
"Lucrări terți": "Работы у третьих лиц",
|
||||||
|
"lucrare terți": "работа у третьих лиц",
|
||||||
|
"lucrări terți": "работы у третьих лиц",
|
||||||
|
"Coeficienți preț": "Ценовые коэффициенты",
|
||||||
|
"coeficient": "коэффициент",
|
||||||
|
"Magazin": "Магазин",
|
||||||
|
"Clienți magazin": "Клиенты магазина",
|
||||||
|
"client magazin": "клиент магазина",
|
||||||
|
"clienți magazin": "клиенты магазина"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Central\Company;
|
||||||
|
use App\Models\Central\Plan;
|
||||||
|
use App\Services\Ai\AiAssistantService;
|
||||||
|
use App\Tenancy\TenantManager;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AiModelSelectorTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_default_model_when_no_override(): void
|
||||||
|
{
|
||||||
|
$svc = app(AiAssistantService::class);
|
||||||
|
$company = $this->makeCompany(['ai' => ['claude_key' => 'sk']]);
|
||||||
|
|
||||||
|
$this->assertEquals('claude-sonnet-4-6', $svc->modelFor('claude', $company));
|
||||||
|
$this->assertEquals('gpt-4o-mini', $svc->modelFor('gpt', $company));
|
||||||
|
$this->assertEquals('gemini-1.5-flash', $svc->modelFor('gemini', $company));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_tenant_override_wins(): void
|
||||||
|
{
|
||||||
|
$svc = app(AiAssistantService::class);
|
||||||
|
$company = $this->makeCompany([
|
||||||
|
'ai' => [
|
||||||
|
'claude_key' => 'sk',
|
||||||
|
'models' => [
|
||||||
|
'claude' => 'claude-opus-4-7',
|
||||||
|
'gpt' => 'gpt-4o',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals('claude-opus-4-7', $svc->modelFor('claude', $company));
|
||||||
|
$this->assertEquals('gpt-4o', $svc->modelFor('gpt', $company));
|
||||||
|
// Provider without override → default.
|
||||||
|
$this->assertEquals('gemini-1.5-flash', $svc->modelFor('gemini', $company));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unknown_provider_falls_back(): void
|
||||||
|
{
|
||||||
|
$svc = app(AiAssistantService::class);
|
||||||
|
$company = $this->makeCompany([]);
|
||||||
|
$this->assertEquals('claude-sonnet-4-6', $svc->modelFor('xyz', $company));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_options_dictionary_keys_match_defaults(): void
|
||||||
|
{
|
||||||
|
foreach (AiAssistantService::MODEL_DEFAULTS as $provider => $default) {
|
||||||
|
$this->assertArrayHasKey(
|
||||||
|
$default,
|
||||||
|
AiAssistantService::MODEL_OPTIONS[$provider],
|
||||||
|
"default '$default' for $provider is not in MODEL_OPTIONS"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeCompany(array $settings): Company
|
||||||
|
{
|
||||||
|
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||||
|
$company = Company::create([
|
||||||
|
'plan_id' => $plan->id, 'slug' => 'mdl-' . uniqid(),
|
||||||
|
'name' => 'Model Co', 'status' => 'active', 'settings' => $settings,
|
||||||
|
]);
|
||||||
|
app(TenantManager::class)->setCurrent($company);
|
||||||
|
return $company;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user