| @@ -69,9 +69,9 @@ | ||
| 69 | 69 | * @param string $api_key OpenRouter API key |
| 70 | 70 | * @param string $model Default model to use |
| 71 | 71 | * @param int $timeout Request timeout |
| 72 | 72 | */ |
| 73 | - public function __construct(string $api_key, string $model = 'openai/gpt-4o-mini', int $timeout = 30) { | |
| 73 | + public function __construct(string $api_key, string $model = \ThinkRank\Core\Settings::DEFAULT_OPENROUTER_MODEL, int $timeout = 30) { | |
| 74 | 74 | $this->api_key = $api_key; |
| 75 | 75 | $this->model = $model; |
| 76 | 76 | $this->timeout = $timeout; |
| 77 | 77 | } |
| @@ -158,9 +158,10 @@ | ||
| 158 | 158 | $content_type = $options['content_type'] ?? 'blog_post'; |
| 159 | 159 | $tone = $options['tone'] ?? 'professional'; |
| 160 | 160 | |
| 161 | 161 | $prompt_builder = $this->get_prompt_builder(); |
| 162 | - $prompt = $prompt_builder->build_seo_prompt($content, $target_keyword, $content_type, $tone, 'openrouter'); | |
| 162 | + $language = is_string($options['language'] ?? null) ? $options['language'] : ''; | |
| 163 | + $prompt = $prompt_builder->build_seo_prompt($content, $target_keyword, $content_type, $tone, 'openrouter', $language); | |
| 163 | 164 | |
| 164 | 165 | $response = $this->generate_completion($prompt, [ |
| 165 | 166 | 'max_tokens' => $this->get_recommended_tokens('seo_metadata'), |
| 166 | 167 | 'temperature' => 0.3, // Lower temperature for more consistent SEO output |
| @@ -195,8 +196,19 @@ | ||
| 195 | 196 | * OpenRouter normalises `max_tokens` across very different underlying |
| 196 | 197 | * models, so we apply a single conservative ceiling rather than per-model |
| 197 | 198 | * limits. |
| 198 | 199 | * |
| 200 | + * Deliberately NOT given the per-model table Claude_Client gained in #665, | |
| 201 | + * and still subject to the same truncation symptom as a result. | |
| 202 | + * | |
| 203 | + * That table is only safe for Claude because its model list is short, known | |
| 204 | + * and verifiable. OpenRouter routes to arbitrary models from many providers | |
| 205 | + * with no curated list here, and a ceiling guessed too high returns a | |
| 206 | + * provider 400 rather than a smaller answer — a worse failure than the one | |
| 207 | + * it would be fixing. Raising this needs either a per-model table sourced | |
| 208 | + * from OpenRouter's own model metadata endpoint, or streaming so a large | |
| 209 | + * budget cannot time out. Tracked separately; see #665. | |
| 210 | + * | |
| 199 | 211 | * @param string $model Model name |
| 200 | 212 | * @return int Maximum completion tokens |
| 201 | 213 | */ |
| 202 | 214 | private function get_max_completion_tokens(string $model): int { |
| @@ -298,8 +310,14 @@ | ||
| 298 | 310 | * @return array Response data |
| 299 | 311 | * @throws \Exception If request fails |
| 300 | 312 | */ |
| 301 | 313 | private function make_request(string $endpoint, array $body = []): array { |
| 314 | + // The user's daily ceiling and kill switch are enforced here, at the | |
| 315 | + // one place every outbound OpenRouter call passes through, so no feature | |
| 316 | + // path can bypass them by forgetting to ask first (#448). | |
| 317 | + Spend_Guard::guard(); | |
| 318 | + Spend_Guard::record(); | |
| 319 | + | |
| 302 | 320 | $url = self::API_BASE_URL . '/' . ltrim($endpoint, '/'); |
| 303 | 321 | |
| 304 | 322 | $args = [ |
| 305 | 323 | 'timeout' => $this->timeout, |
| @@ -667,9 +685,9 @@ | ||
| 667 | 685 | return [ |
| 668 | 686 | 'optimized_data' => [ |
| 669 | 687 | 'site_name' => sanitize_text_field($optimization['optimized_data']['site_name'] ?? ''), |
| 670 | 688 | 'project_overview' => sanitize_textarea_field($optimization['optimized_data']['project_overview'] ?? ''), |
| 671 | - 'key_features' => sanitize_textarea_field($optimization['optimized_data']['key_features'] ?? ''), | |
| 689 | + 'key_features' => \ThinkRank\SEO\LLMs_Txt_Manager::normalize_ai_key_features($optimization['optimized_data']['key_features'] ?? ''), | |
| 672 | 690 | 'architecture' => sanitize_textarea_field($optimization['optimized_data']['architecture'] ?? ''), |
| 673 | 691 | 'development_guidelines' => sanitize_textarea_field($optimization['optimized_data']['development_guidelines'] ?? ''), |
| 674 | 692 | 'ai_context' => sanitize_textarea_field($optimization['optimized_data']['ai_context'] ?? ''), |
| 675 | 693 | ], |