122 lines
6.2 KiB
PHP
122 lines
6.2 KiB
PHP
<?php
|
|
/**
|
|
* Central, extendable registry of what is known about models: whether a reasoning-effort setting
|
|
* exists, which levels are valid, the recommended level per purpose and sampling restrictions.
|
|
* UI and backend both read model-capabilities.json, so they cannot disagree.
|
|
*
|
|
* Reasoning parameters are only ever sent for a model listed there on its official host. Unknown
|
|
* models, hosts and the "legacy" mode of migrated installs send nothing and keep the provider default.
|
|
*/
|
|
|
|
/** Purposes a model call serves. chat = assistant, agent, full content drafts; text = field suggestions, rewrites, SEO. */
|
|
const RAIJU_AI_PURPOSES = ['chat', 'text'];
|
|
/** Levels offered in the UI. none, minimal, xhigh and max exist at some providers but are not offered. */
|
|
const RAIJU_AI_EFFORT_LEVELS = ['low', 'medium', 'high'];
|
|
|
|
function raijuAiCapabilityCatalog(): array {
|
|
static $catalog;
|
|
return $catalog ??= json_decode((string)file_get_contents(__DIR__ . '/model-capabilities.json'), true, 512, JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
function raijuAiBaseUrlHost(string $baseUrl): string {
|
|
return strtolower((string)parse_url($baseUrl, PHP_URL_HOST));
|
|
}
|
|
|
|
/** The registry entry of a model on the configured provider and host, or null when it is not known. */
|
|
function raijuAiModelInfo(array $settings, string $model): ?array {
|
|
$model = strtolower(trim($model));
|
|
if ($model === '') return null;
|
|
$provider = raijuAiProviderKey($settings);
|
|
$host = raijuAiBaseUrlHost((string)($settings['baseUrl'] ?? ''));
|
|
foreach (raijuAiCapabilityCatalog()['models'] as $entry) {
|
|
if ($entry['provider'] === $provider && strtolower($entry['host']) === $host && strtolower($entry['model']) === $model) {
|
|
return $entry;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Stored mode for a purpose: recommended, low, medium, high, or legacy (nothing sent; installs from before the setting existed). */
|
|
function raijuAiReasoningMode(array $settings, string $purpose): string {
|
|
$mode = (string)($settings['reasoning'][$purpose] ?? 'legacy');
|
|
return in_array($mode, array_merge(['recommended', 'legacy'], RAIJU_AI_EFFORT_LEVELS), true) ? $mode : 'recommended';
|
|
}
|
|
|
|
/** The model configured for a purpose. */
|
|
function raijuAiPurposeModel(array $settings, string $purpose): string {
|
|
$chat = (string)($settings['chatModel'] ?? '');
|
|
return $purpose === 'text' ? ((string)($settings['textModel'] ?? '') ?: $chat) : $chat;
|
|
}
|
|
|
|
/**
|
|
* The effort level to send for a call, or null to send nothing. A model other than the one configured
|
|
* for the purpose (an explicit override by the caller) gets the recommended level of that model.
|
|
*/
|
|
function raijuAiResolveEffort(array $settings, string $model, string $purpose): ?string {
|
|
if (!in_array($purpose, RAIJU_AI_PURPOSES, true)) return null;
|
|
$info = raijuAiModelInfo($settings, $model);
|
|
if (!$info || empty($info['effort'])) return null;
|
|
$mode = raijuAiReasoningMode($settings, $purpose);
|
|
if ($mode === 'legacy') return null;
|
|
if (strtolower(trim($model)) !== strtolower(trim(raijuAiPurposeModel($settings, $purpose)))) $mode = 'recommended';
|
|
if ($mode === 'recommended') return (string)($info['effort']['recommended'][$purpose] ?? $info['effort']['recommended']['chat'] ?? null) ?: null;
|
|
return in_array($mode, $info['effort']['levels'], true) ? $mode : ((string)($info['effort']['recommended'][$purpose] ?? '') ?: null);
|
|
}
|
|
|
|
/** Extra output tokens a thinking model needs on top of the visible answer, because thinking counts toward the output limit. */
|
|
function raijuAiEffortHeadroom(?string $effort): int {
|
|
return $effort === null ? 0 : (int)(raijuAiCapabilityCatalog()['headroomTokens'][$effort] ?? 0);
|
|
}
|
|
|
|
/**
|
|
* Settle the reasoning part of a request for one call.
|
|
*
|
|
* @return array{effort:?string,omitTemperature:bool,headroom:int,param:?string}
|
|
*/
|
|
function raijuAiReasoningPlan(array $settings, string $model, string $purpose): array {
|
|
$effort = raijuAiResolveEffort($settings, $model, $purpose);
|
|
$info = $effort !== null ? raijuAiModelInfo($settings, $model) : null;
|
|
return [
|
|
'effort' => $effort,
|
|
'omitTemperature' => $info !== null && !empty($info['omitTemperature']),
|
|
'headroom' => raijuAiEffortHeadroom($effort),
|
|
'param' => $info['effort']['param'] ?? null
|
|
];
|
|
}
|
|
|
|
/** Add the effort parameter of the right API to an already built request body. $api: chat (Chat Completions), responses, anthropic. */
|
|
function raijuAiApplyReasoning(array $body, array $plan, string $api): array {
|
|
if ($plan['effort'] === null) return $body;
|
|
if ($plan['omitTemperature']) unset($body['temperature']);
|
|
if ($api === 'anthropic' && $plan['param'] === 'anthropic') {
|
|
$body['output_config'] = ['effort' => $plan['effort']];
|
|
} elseif ($api === 'responses' && $plan['param'] === 'openai') {
|
|
$body['reasoning'] = ['effort' => $plan['effort']];
|
|
} elseif ($api === 'chat' && $plan['param'] === 'openai') {
|
|
$body['reasoning_effort'] = $plan['effort'];
|
|
}
|
|
return $body;
|
|
}
|
|
|
|
/** Recommended and default models for a provider, with the default effort setting. Custom OpenAI-compatible hosts get no preset. */
|
|
function raijuAiProviderModelDefaults(string $provider, string $baseUrl = ''): array {
|
|
$catalog = raijuAiModelCatalog()[$provider] ?? raijuAiModelCatalog()['openai-compatible'];
|
|
$preset = ['chatModel' => (string)$catalog['text']['chat'], 'textModel' => (string)$catalog['text']['text']];
|
|
if ($provider === 'openai-compatible' && $baseUrl !== '' && raijuAiBaseUrlHost($baseUrl) !== 'api.openai.com') {
|
|
$preset = ['chatModel' => '', 'textModel' => ''];
|
|
}
|
|
return $preset + ['reasoning' => ['chat' => 'recommended', 'text' => 'recommended']];
|
|
}
|
|
|
|
/** What the browser needs: the registry reduced to the fields the UI uses. */
|
|
function raijuAiCapabilitiesForUi(): array {
|
|
$models = [];
|
|
foreach (raijuAiCapabilityCatalog()['models'] as $entry) {
|
|
$models[] = [
|
|
'provider' => $entry['provider'], 'host' => $entry['host'], 'model' => $entry['model'], 'label' => $entry['label'] ?? $entry['model'],
|
|
'tier' => $entry['tier'] ?? 'light', 'levels' => $entry['effort']['levels'] ?? [], 'recommended' => $entry['effort']['recommended'] ?? []
|
|
];
|
|
}
|
|
return ['models' => $models, 'levels' => RAIJU_AI_EFFORT_LEVELS];
|
|
}
|