exec('rm -rf ' . escapeshellarg($site))); foreach (['includes', 'admin', 'VERSION'] as $item) exec('cp -R ' . escapeshellarg($root . '/' . $item) . ' ' . escapeshellarg($site . '/')); foreach (['admin/config.php', 'admin/smtp-config.php', 'includes/nav-config.php'] as $local) @unlink($site . '/' . $local); define('SITE_LANG_DEFAULT', 'en'); $_SESSION = ['admin_role' => 'admin', 'admin_user_id' => 'u_admin', 'admin_username' => 'admin']; require_once $site . '/includes/ai/ai-helper.php'; $openai = ['provider' => 'openai-compatible', 'baseUrl' => 'https://api.openai.com/v1', 'pricing' => ['models' => []], 'reasoning' => ['chat' => 'recommended', 'text' => 'recommended'], 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna']; $anthropic = ['provider' => 'anthropic', 'baseUrl' => 'https://api.anthropic.com/v1', 'pricing' => ['models' => []], 'reasoning' => ['chat' => 'recommended', 'text' => 'recommended'], 'chatModel' => 'claude-sonnet-5-5', 'textModel' => 'claude-haiku-5-5']; $ollama = ['provider' => 'openai-compatible', 'baseUrl' => 'http://127.0.0.1:11434/v1', 'pricing' => ['models' => []], 'reasoning' => ['chat' => 'recommended', 'text' => 'recommended'], 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'llama3']; $openrouter = ['provider' => 'openrouter', 'baseUrl' => 'https://openrouter.ai/api/v1', 'pricing' => ['models' => []], 'reasoning' => ['chat' => 'recommended', 'text' => 'recommended'], 'chatModel' => 'openai/gpt-6.1-sol', 'textModel' => 'openai/gpt-6-luna']; // ---- Registry and effort ------------------------------------------------------------------------------------------ pricingAssert(raijuAiModelInfo($openai, 'gpt-6.1-sol') !== null && raijuAiModelInfo($anthropic, 'claude-haiku-5-5') !== null, 'known models are found'); pricingAssert(raijuAiModelInfo($ollama, 'gpt-6.1-sol') === null, 'the same model id on a custom endpoint is unknown: no OpenAI rules for compatible hosts'); pricingAssert(raijuAiModelInfo($openrouter, 'openai/gpt-6.1-sol') === null && raijuAiModelInfo($openai, 'gpt-4.1-mini') === null, 'OpenRouter models and unknown models have no registry entry'); pricingSame('medium', raijuAiResolveEffort($openai, 'gpt-6.1-sol', 'chat'), 'recommended chat level'); pricingSame('low', raijuAiResolveEffort($openai, 'gpt-6-luna', 'text'), 'recommended text level'); pricingSame('medium', raijuAiResolveEffort($anthropic, 'claude-sonnet-5-5', 'chat'), 'Anthropic chat level is set explicitly (the API default there is high)'); pricingSame('low', raijuAiResolveEffort($anthropic, 'claude-haiku-5-5', 'text'), 'Anthropic text level'); pricingSame(null, raijuAiResolveEffort($ollama, 'llama3', 'text'), 'unknown model: nothing is sent'); $same = $openai; $same['textModel'] = 'gpt-6.1-sol'; $same['reasoning'] = ['chat' => 'high', 'text' => 'low']; pricingSame('high', raijuAiResolveEffort($same, 'gpt-6.1-sol', 'chat'), 'the same model keeps its own chat level'); pricingSame('low', raijuAiResolveEffort($same, 'gpt-6.1-sol', 'text'), '... and its own text level'); pricingSame('medium', raijuAiResolveEffort($same, 'gpt-6-luna', 'chat'), 'a model other than the configured one gets the recommendation of that model'); $legacy = $openai; $legacy['reasoning'] = ['chat' => 'legacy', 'text' => 'legacy']; pricingSame(null, raijuAiResolveEffort($legacy, 'gpt-6.1-sol', 'chat'), 'legacy installs send nothing'); // ---- Request bodies ----------------------------------------------------------------------------------------------- $plan = raijuAiReasoningPlan($openai, 'gpt-6.1-sol', 'chat'); $responses = raijuAiApplyReasoning(raijuAgentResponsesBody([['role' => 'user', 'content' => 'hi']], [], 'gpt-6.1-sol', 1000, 0.2), $plan, 'responses'); pricingSame(['effort' => 'medium'], $responses['reasoning'] ?? null, 'Responses API: reasoning.effort'); pricingAssert(!isset($responses['temperature']) && !isset($responses['reasoning_effort']), 'Responses API: no temperature, no chat parameter'); $chat = raijuAiApplyReasoning(['model' => 'gpt-6-luna', 'messages' => [], 'max_tokens' => 10, 'temperature' => 0.3, 'stream' => false], raijuAiReasoningPlan($openai, 'gpt-6-luna', 'text'), 'chat'); pricingSame('low', $chat['reasoning_effort'] ?? null, 'Chat Completions: reasoning_effort'); pricingAssert(!isset($chat['temperature']) && !isset($chat['reasoning']), 'Chat Completions: no temperature, no Responses parameter'); $claude = raijuAiApplyReasoning(['model' => 'claude-sonnet-5-5', 'max_tokens' => 10, 'messages' => [], 'temperature' => 0.3], raijuAiReasoningPlan($anthropic, 'claude-sonnet-5-5', 'chat'), 'anthropic'); pricingSame(['effort' => 'medium'], $claude['output_config'] ?? null, 'Anthropic: output_config.effort'); pricingAssert(!isset($claude['temperature']) && !isset($claude['thinking']), 'Anthropic: no temperature (not accepted on Sonnet 5.5), no thinking block'); $untouched = ['model' => 'llama3', 'messages' => [], 'max_tokens' => 10, 'temperature' => 0.3, 'stream' => false]; pricingSame($untouched, raijuAiApplyReasoning($untouched, raijuAiReasoningPlan($ollama, 'llama3', 'text'), 'chat'), 'unknown model: the body stays byte for byte as before'); pricingSame($untouched, raijuAiApplyReasoning($untouched, raijuAiReasoningPlan($legacy, 'gpt-6.1-sol', 'chat'), 'chat'), 'legacy: the body stays as before'); pricingSame(4000, $plan['headroom'], 'thinking headroom of medium effort'); pricingSame(16000, raijuAiOutputBudget(['limits' => ['maxOutputTokens' => 16000]], ['maxOutputTokens' => 15000], $plan), 'the output limit stays the ceiling'); pricingSame(1204, raijuAiOutputBudget(['limits' => ['maxOutputTokens' => 16000]], ['maxOutputTokens' => 1200], ['headroom' => 4]), 'headroom is added below the ceiling'); // ---- Costs -------------------------------------------------------------------------------------------------------- $fx = 1.1208; $expectedCents = static fn(float $usd): float => round($usd / $fx * 100, 4); pricingSame($expectedCents(0.007), raijuAiCostForUsage($openai, 'gpt-6.1-sol', ['input' => 1000, 'output' => 500])['cents'], 'Sol: 1000 in, 500 out'); pricingSame($expectedCents(300000 * 4 / 1e6 + 1000 * 15 / 1e6), raijuAiCostForUsage($openai, 'gpt-6.1-sol', ['input' => 300000, 'output' => 1000])['cents'], 'Sol over 272,000 input tokens: 2x input, 1.5x output for the whole request'); pricingSame($expectedCents(272000 * 2 / 1e6), raijuAiCostForUsage($openai, 'gpt-6.1-sol', ['input' => 272000, 'output' => 0])['cents'], 'exactly 272,000 tokens is not over the threshold'); pricingSame($expectedCents(150000 * 0.5 / 1e6 + 1000 * 2.5 / 1e6), raijuAiCostForUsage($anthropic, 'claude-haiku-5-5', ['input' => 150000, 'output' => 1000])['cents'], 'Haiku over 100,000 tokens has its own rates'); pricingSame($expectedCents(60000 * 0.5 / 1e6 + 50000 * 0.05 / 1e6 + 1000 * 2.5 / 1e6), raijuAiCostForUsage($anthropic, 'claude-haiku-5-5', ['input' => 60000, 'cacheRead' => 50000, 'output' => 1000])['cents'], 'cache reads count toward the tier and are billed at the cache rate of that tier'); pricingSame($expectedCents(1000 * 2 / 1e6 + 2000 * 0.10 / 1e6 + 500 * 2.5 / 1e6 + 300 * 10 / 1e6), raijuAiCostForUsage($anthropic, 'claude-sonnet-5-5', ['input' => 1000, 'cacheRead' => 2000, 'cacheWrite' => 500, 'output' => 300])['cents'], 'Sonnet with cache read and write'); // Provider reports: OpenAI cached tokens are part of the prompt; reasoning tokens are part of the output and not added twice $openAiUsage = raijuAiNormalizeUsage(['prompt_tokens' => 1000, 'completion_tokens' => 300, 'prompt_tokens_details' => ['cached_tokens' => 400], 'completion_tokens_details' => ['reasoning_tokens' => 100]]); pricingSame(['input' => 600, 'cacheRead' => 400, 'cacheWrite' => 0, 'cacheWrite1h' => 0, 'output' => 300, 'reasoning' => 100, 'reported' => true], $openAiUsage, 'OpenAI usage'); $claudeUsage = raijuAiNormalizeUsage(['input_tokens' => 50, 'cache_read_input_tokens' => 900, 'cache_creation_input_tokens' => 80, 'cache_creation' => ['ephemeral_1h_input_tokens' => 30], 'output_tokens' => 20]); pricingSame(['input' => 50, 'cacheRead' => 900, 'cacheWrite' => 50, 'cacheWrite1h' => 30, 'output' => 20, 'reasoning' => 0, 'reported' => true], $claudeUsage, 'Anthropic usage'); pricingSame($expectedCents(300 * 10 / 1e6 + 600 * 2 / 1e6 + 400 * 0.10 / 1e6), raijuAiCostForUsage($openai, 'gpt-6.1-sol', $openAiUsage)['cents'], 'reasoning tokens are billed once, as part of the output'); pricingSame(raijuAiCostForUsage($openai, 'gpt-6.1-sol', ['input' => 1000, 'output' => 1000])['cents'], raijuAiCostForUsage(array_replace($openai, ['reasoning' => ['chat' => 'high', 'text' => 'high']]), 'gpt-6.1-sol', ['input' => 1000, 'output' => 1000])['cents'], 'no multiplier for the effort level'); // Unknown prices are never 0 and never another model's price $unknown = raijuAiCostForUsage($ollama, 'llama3', ['input' => 1000, 'output' => 1000]); pricingAssert($unknown['priced'] === false && $unknown['basis']['priced'] === false, 'unknown price is reported as unknown'); pricingAssert(raijuAiPriceFor($ollama, 'gpt-6.1-sol') === null && raijuAiPriceFor($openrouter, 'openai/gpt-6.1-sol') === null, 'a custom endpoint or OpenRouter does not borrow the OpenAI price'); // Own prices $own = $ollama; $own['pricing']['models'] = ['openai-compatible|llama3' => ['input' => 1.0, 'output' => 2.0, 'currency' => 'EUR']]; pricingSame(round((1000 * 1.0 + 1000 * 2.0) / 1e6 * 100, 4), raijuAiCostForUsage($own, 'llama3', ['input' => 1000, 'output' => 1000])['cents'], 'EUR override is not converted'); $overridden = $openai; $overridden['pricing']['models'] = ['openai-compatible|gpt-6.1-sol' => ['input' => 4.0, 'output' => 20.0, 'currency' => 'USD']]; pricingSame($expectedCents(4000 / 1e6 + 20000 / 1e6), raijuAiCostForUsage($overridden, 'gpt-6.1-sol', ['input' => 1000, 'output' => 1000])['cents'], 'an override wins over the catalog'); $ownRate = $openai; $ownRate['pricing']['usdPerEur'] = 2.0; pricingSame(round(0.007 / 2.0 * 100, 4), raijuAiCostForUsage($ownRate, 'gpt-6.1-sol', ['input' => 1000, 'output' => 500])['cents'], 'own exchange rate'); $basis = raijuAiCostForUsage($openai, 'gpt-6.1-sol', ['input' => 1000, 'output' => 500])['basis']; pricingAssert($basis['usdPerEur'] == 1.1208 && $basis['fxAsOf'] !== '' && $basis['input'] == 2.0 && $basis['output'] == 10.0, 'the price basis keeps rates, exchange rate and its date'); // ---- Storage: new installs, per-provider models, reasoning, key handling --------------------------------------------- $fresh = raijuAiLoadSettings(false); pricingSame(['gpt-6.1-sol', 'gpt-6-luna'], [$fresh['chatModel'], $fresh['textModel']], 'new installs: OpenAI defaults'); pricingSame(['recommended', 'recommended'], array_values($fresh['reasoning']), 'new installs: recommended levels'); $saved = raijuAiSaveSettings(['enabled' => true, 'provider' => 'anthropic', 'apiKey' => 'sk-ant-secret-key-0001', 'chatModel' => '', 'textModel' => '']); pricingSame(['claude-sonnet-5-5', 'claude-haiku-5-5'], [$saved['chatModel'], $saved['textModel']], 'empty models fall back to the provider presets'); $saved = raijuAiSaveSettings(['provider' => 'openai-compatible', 'apiKey' => 'sk-openai-secret-key-0002', 'chatModel' => 'gpt-6-luna', 'textModel' => 'gpt-6-luna', 'reasoning' => ['chat' => 'high', 'text' => 'low']]); pricingSame(['gpt-6-luna', 'high', 'low'], [$saved['chatModel'], $saved['reasoning']['chat'], $saved['reasoning']['text']], 'saved OpenAI choices'); $saved = raijuAiSaveSettings(['provider' => 'anthropic', 'chatModel' => 'claude-sonnet-5-5', 'textModel' => 'claude-haiku-5-5', 'reasoning' => ['chat' => 'recommended', 'text' => 'medium']]); pricingSame('medium', $saved['reasoning']['text'], 'saved Anthropic effort'); pricingSame('high', $saved['providerModels']['openai-compatible']['reasoning']['chat'], 'the other provider keeps its choices'); $back = raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-6-luna', 'textModel' => 'gpt-6-luna']); pricingSame(['high', 'low'], [$back['reasoning']['chat'], $back['reasoning']['text']], 'switching back restores the effort of that provider'); pricingSame('medium', $back['providerModels']['anthropic']['reasoning']['text'], 'and the other side stays'); $invalid = raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-4.1-mini', 'reasoning' => ['chat' => 'xhigh', 'text' => 'high']]); pricingSame(['recommended', 'recommended'], [$invalid['reasoning']['chat'], $invalid['reasoning']['text']], 'unsupported levels and levels for unknown models become "recommended"'); // Keys $stored = static fn(): array => raijuAiReadStoredSettings(); pricingSame('sk-openai-secret-key-0002', $stored()['providerCredentials']['openai-compatible']['apiKey'], 'the key is stored'); raijuAiSaveSettings(['provider' => 'openai-compatible', 'apiKey' => "\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}", 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna']); pricingSame('sk-openai-secret-key-0002', $stored()['providerCredentials']['openai-compatible']['apiKey'], 'a mask is never saved as a key'); raijuAiSaveSettings(['provider' => 'openai-compatible', 'apiKey' => '', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna']); pricingSame('sk-openai-secret-key-0002', $stored()['providerCredentials']['openai-compatible']['apiKey'], 'an empty field keeps the key'); raijuAiSaveSettings(['provider' => 'openai-compatible', 'apiKey' => 'sk-openai-secret-key-0003', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna']); pricingSame('sk-openai-secret-key-0003', $stored()['providerCredentials']['openai-compatible']['apiKey'], 'a real entry replaces the key'); $public = raijuAiLoadSettings(true); pricingAssert(!str_contains(json_encode($public), 'sk-openai') && !str_contains(json_encode($public), 'sk-ant'), 'no key is sent to the browser'); pricingAssert($public['providerCredentials']['openai-compatible']['hasApiKey'] === true && $public['providerCredentials']['anthropic']['hasApiKey'] === true, 'only the stored state is sent'); pricingSame(true, raijuAiClearSecret('provider:anthropic'), 'removing a stored key reports it existed'); $public = raijuAiLoadSettings(true); pricingAssert($public['providerCredentials']['anthropic']['hasApiKey'] === false && $public['providerCredentials']['openai-compatible']['hasApiKey'] === true, 'removal affects only that provider'); pricingSame(false, raijuAiClearSecret('provider:anthropic'), 'removing twice is harmless'); raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna', 'research' => ['braveApiKey' => 'BSA-brave-secret-key-1234'], 'stock' => ['pexelsApiKey' => 'pexels-secret-key-ABCDEFGH']]); pricingSame('BSA-brave-secret-key-1234', $stored()['research']['brave']['apiKey'], 'Brave key stored'); raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna', 'research' => ['braveApiKey' => "\u{2022}\u{2022}\u{2022}\u{2022}"]]); pricingSame('BSA-brave-secret-key-1234', $stored()['research']['brave']['apiKey'], 'a mask does not replace the Brave key'); pricingSame(true, raijuAiClearSecret('brave'), 'Brave key removed'); pricingAssert(!isset($stored()['research']['brave']['apiKey']) && ($stored()['stock']['pexels']['apiKey'] ?? '') === 'pexels-secret-key-ABCDEFGH', 'only the Brave key is gone'); pricingAssert((raijuAiLoadSettings(true)['research']['hasBraveApiKey'] ?? true) === false, 'the status says so'); try { raijuAiClearSecret('../etc'); pricingAssert(false, 'unknown targets are refused'); } catch (InvalidArgumentException $e) { /* expected */ } pricingAssert((fileperms(RAIJU_AI_SECRETS_PATH) & 0777) === 0600, 'the secrets file stays owner-only'); // ---- Price overrides through the form ----------------------------------------------------------------------------------- $withPrice = raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna', 'pricing' => ['models' => ['openai-compatible|gpt-6-luna' => ['input' => '0.2', 'output' => '1', 'currency' => 'EUR']], 'usdPerEur' => 1.2]]); pricingAssert($withPrice['pricing']['models']['openai-compatible|gpt-6-luna'] == ['input' => 0.2, 'output' => 1.0, 'currency' => 'EUR'], 'a price entered in the form is stored'); pricingAssert($withPrice['pricing']['usdPerEur'] == 1.2, 'own exchange rate is stored'); pricingAssert(preg_match('/^\d{4}-\d{2}-\d{2}$/', $withPrice['pricing']['usdPerEurAsOf']) === 1, 'with its date'); $kept = raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna', 'pricing' => ['models' => []]]); pricingAssert(isset($kept['pricing']['models']['openai-compatible|gpt-6-luna']), 'prices the form does not mention stay'); $removed = raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna', 'pricing' => ['models' => ['openai-compatible|gpt-6-luna' => null], 'usdPerEur' => 0]]); pricingAssert(!isset($removed['pricing']['models']['openai-compatible|gpt-6-luna']) && (float)$removed['pricing']['usdPerEur'] === 0.0, 'a price can be removed, and the exchange rate reset'); // ---- Migration of an older settings file ----------------------------------------------------------------------------- unlink(RAIJU_AI_SETTINGS_PATH); @unlink(RAIJU_AI_SECRETS_PATH); file_put_contents(RAIJU_AI_SETTINGS_PATH, json_encode([ 'enabled' => true, 'provider' => 'openai-compatible', 'baseUrl' => 'https://api.openai.com/v1', 'chatModel' => 'gpt-4.1-mini', 'textModel' => 'gpt-4.1-mini', 'pricing' => ['inputCentsPerMillion' => 20, 'outputCentsPerMillion' => 80, 'imageCentsPerRequest' => 7], 'providerCredentials' => ['openai-compatible' => ['baseUrl' => 'https://api.openai.com/v1', 'apiKey' => 'sk-old-secret-key-0000', 'organization' => 'org-1']] ])); $old = raijuAiLoadSettings(false); pricingSame(['gpt-4.1-mini', 'gpt-4.1-mini'], [$old['chatModel'], $old['textModel']], 'existing models are kept'); pricingSame(['legacy', 'legacy'], array_values($old['reasoning']), 'no stored effort: the former request behavior (nothing sent)'); pricingSame(null, raijuAiResolveEffort($old, 'gpt-4.1-mini', 'chat'), 'no parameter is sent for them'); $migrated = $old['pricing']['models']['openai-compatible|gpt-4.1-mini'] ?? null; pricingAssert($migrated == ['input' => 0.2, 'output' => 0.8, 'currency' => 'EUR', 'migrated' => true], 'the manual prices carry over as marked estimates (cents per million to euro per million)'); pricingSame(0.2 * 1000 / 1e6 * 100 + 0.8 * 500 / 1e6 * 100, raijuAiCostForUsage($old, 'gpt-4.1-mini', ['input' => 1000, 'output' => 500])['cents'], 'the estimate is the same as before for the models in use (without the whole-cent rounding)'); $resaved = raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-4.1-mini', 'textModel' => 'gpt-4.1-mini']); pricingSame(['legacy', 'legacy'], array_values($resaved['reasoning']), 'saving without touching the setting keeps legacy'); pricingAssert(!empty($resaved['pricing']['models']['openai-compatible|gpt-4.1-mini']['migrated']) && $resaved['pricing']['imageCentsPerRequest'] === 7, 'the carried-over price is persisted and still marked; the image price stays'); pricingAssert(!isset(json_decode((string)file_get_contents(RAIJU_AI_SETTINGS_PATH), true)['pricing']['inputCentsPerMillion']), 'the former global pair is gone from the file after a save'); $switched = raijuAiSaveSettings(['provider' => 'openai-compatible', 'chatModel' => 'gpt-6.1-sol', 'textModel' => 'gpt-6-luna', 'reasoning' => ['chat' => 'recommended', 'text' => 'recommended']]); pricingSame(['recommended', 'recommended'], array_values($switched['reasoning']), 'choosing "recommended" leaves legacy'); pricingAssert(!empty($switched['pricing']['models']['openai-compatible|gpt-4.1-mini']), 'prices of models no longer in use stay'); pricingAssert(file_exists(RAIJU_AI_SECRETS_PATH) && ($stored()['providerCredentials']['openai-compatible']['apiKey'] ?? '') === 'sk-old-secret-key-0000', 'existing credentials stay'); // ---- Sums of tiny amounts, ledger ------------------------------------------------------------------------------------------ @unlink(RAIJU_AI_USAGE_PATH); foreach (range(1, 1000) as $i) { unset($GLOBALS['raijuAiRequestId']); raijuAiRecordUsage('text', 10, 5, 0.0132, ['model' => 'gpt-6-luna', 'priced' => true]); } raijuAiRecordUsage('text', 10, 5, 0.0, ['model' => 'llama3', 'priced' => false]); $month = raijuAiLoadUsage()['months'][gmdate('Y-m')]; pricingAssert((float)$month['estimatedCostCents'] === 13.2, '1000 requests of 0.0132 cents add up exactly (no whole-cent rounding per request)'); pricingSame([1001, 1], [$month['requests'], $month['unpricedRequests']], 'unpriced requests are counted apart, not as 0 cents'); // A settled request keeps the price basis it was priced with $GLOBALS['raijuAiRequestId'] = 'req_basis'; raijuAiAssertWithinLimits($openai + ['limits' => ['monthlyBudgetCents' => 0, 'dailyRequests' => 0, 'dailyTextRequests' => 0, 'dailyImageRequests' => 0, 'maxInputTokens' => 1, 'maxOutputTokens' => 1, 'requestTimeoutSeconds' => 1], 'imageModel' => 'x'], 'text', 0.5, ['model' => 'gpt-6.1-sol', 'priced' => true]); $settled = raijuAiSettleText($openai, 'gpt-6.1-sol', ['prompt_tokens' => 100, 'completion_tokens' => 50], 100, 'ok'); unset($GLOBALS['raijuAiRequestId']); $entry = raijuAiLoadUsage()['reservations']['req_basis']; pricingAssert($entry['status'] === 'settled' && $entry['model'] === 'gpt-6.1-sol' && $entry['priceBasis']['input'] == 2.0 && $entry['priceBasis']['usdPerEur'] == 1.1208, 'the ledger entry stores the model and the price basis of its time'); pricingAssert($settled['estimatedCostCents'] == $entry['actualEstimatedCents'], 'and the cost it was settled with'); echo "PASS model registry, effort per purpose, request bodies, tiered prices, cache and reasoning tokens, unknown prices, migration, per-provider models, key handling, tiny sums\n";