Compare commits

..

No commits in common. "main" and "v1.2.2" have entirely different histories.
main ... v1.2.2

17 changed files with 54 additions and 613 deletions

View file

@ -3,8 +3,8 @@
![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue) ![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue)
![WordPress 6.0+](https://img.shields.io/badge/WordPress-6.0%2B-21759b) ![WordPress 6.0+](https://img.shields.io/badge/WordPress-6.0%2B-21759b)
![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green) ![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green)
![Version](https://img.shields.io/badge/Version-1.3.0-orange) ![Version](https://img.shields.io/badge/Version-1.2.2-orange)
![Tests](https://img.shields.io/badge/Tests-163%20passing-brightgreen) ![Tests](https://img.shields.io/badge/Tests-158%20passing-brightgreen)
🇬🇧 [English version → README.md](README.md) 🇬🇧 [English version → README.md](README.md)
@ -26,7 +26,7 @@ Die KI-Welle hat es schlimmer gemacht. Plugins fingen an, „KI-gestützte" Feat
BreznGEO verfolgt einen anderen Ansatz: BreznGEO verfolgt einen anderen Ansatz:
- **Direkter API-Zugriff.** Du hinterlegst deinen eigenen Key von OpenAI, Anthropic, Google, xAI oder OpenRouter (600+ Modelle über einen Key). BreznGEO ruft die API direkt auf. Kein Mittelsmann, keine Marge, keine Daten über Server Dritter. - **Direkter API-Zugriff.** Du hinterlegst deinen eigenen Key von OpenAI, Anthropic, Google oder xAI. BreznGEO ruft die API direkt auf. Kein Mittelsmann, keine Marge, keine Daten über Server Dritter.
- **Klarer Output, kein Lärm.** Metabeschreibungen, Strukturdaten, KI-Inhaltsblöcke für GEO, Bot-Steuerung. Keine Lesbarkeits-Scores, keine Keyword-Dichte-Meter, keine Upsell-Banner. - **Klarer Output, kein Lärm.** Metabeschreibungen, Strukturdaten, KI-Inhaltsblöcke für GEO, Bot-Steuerung. Keine Lesbarkeits-Scores, keine Keyword-Dichte-Meter, keine Upsell-Banner.
- **Keine Subscription.** GPL-2.0. Kostenlos auf beliebig vielen Sites nutzbar. Die einzigen Kosten sind die API-Nutzung — typischerweise Bruchteile eines Cents pro Beitrag. - **Keine Subscription.** GPL-2.0. Kostenlos auf beliebig vielen Sites nutzbar. Die einzigen Kosten sind die API-Nutzung — typischerweise Bruchteile eines Cents pro Beitrag.
- **Keine Telemetrie.** BreznGEO sendet keine Daten nach Hause. Kein Usage-Tracking, kein Remote-Logging, keine Analytics, die den eigenen Server verlassen. - **Keine Telemetrie.** BreznGEO sendet keine Daten nach Hause. Kein Usage-Tracking, kein Remote-Logging, keine Analytics, die den eigenen Server verlassen.
@ -109,8 +109,7 @@ brezngeo/
│ ├── AnthropicProvider.php # Claude API (Messages API) │ ├── AnthropicProvider.php # Claude API (Messages API)
│ ├── GeminiProvider.php # Google Gemini (generateContent API) │ ├── GeminiProvider.php # Google Gemini (generateContent API)
│ ├── GrokProvider.php # xAI Grok (OpenAI-kompatibler Endpunkt) │ ├── GrokProvider.php # xAI Grok (OpenAI-kompatibler Endpunkt)
│ ├── OpenAIProvider.php # OpenAI GPT (Chat Completions API) │ └── OpenAIProvider.php # OpenAI GPT (Chat Completions API)
│ └── OpenRouterProvider.php # OpenRouter (600+ Modelle über eine OpenAI-kompatible API)
└── vendor/ # Composer-Abhängigkeiten (nur Produktionsstand) └── vendor/ # Composer-Abhängigkeiten (nur Produktionsstand)
``` ```
@ -304,7 +303,6 @@ define( 'BREZNGEO_OPENAI_KEY', 'sk-...' );
define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' ); define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
define( 'BREZNGEO_GEMINI_KEY', 'AI...' ); define( 'BREZNGEO_GEMINI_KEY', 'AI...' );
define( 'BREZNGEO_GROK_KEY', 'xai-...' ); define( 'BREZNGEO_GROK_KEY', 'xai-...' );
define( 'BREZNGEO_OPENROUTER_KEY', 'sk-or-...' );
``` ```
### CSRF-Schutz und Capability Checks ### CSRF-Schutz und Capability Checks
@ -338,9 +336,6 @@ CrawlerLog speichert IPs ausschließlich als SHA-256-Hash. Originalwert wird nie
| Anthropic | `AnthropicProvider` | `https://api.anthropic.com/v1/messages` | | Anthropic | `AnthropicProvider` | `https://api.anthropic.com/v1/messages` |
| Google Gemini | `GeminiProvider` | `https://generativelanguage.googleapis.com/...` | | Google Gemini | `GeminiProvider` | `https://generativelanguage.googleapis.com/...` |
| xAI Grok | `GrokProvider` | `https://api.x.ai/v1/chat/completions` | | xAI Grok | `GrokProvider` | `https://api.x.ai/v1/chat/completions` |
| OpenRouter | `OpenRouterProvider` | `https://openrouter.ai/api/v1/chat/completions` |
**Zu OpenRouter:** Ein einziger API-Key öffnet den Zugang zu 600+ Modellen von OpenAI, Anthropic, Google, Meta, Mistral, xAI, DeepSeek u.v.m. Die kuratierte Marketing/SEO-Auswahl wird on demand geladen ("Modelle laden"-Button) und 12 Stunden im Transient gecached. Preise werden automatisch aus OpenRouter übernommen. Eigene Modell-IDs (z. B. `anthropic/claude-opus-4.7`) werden unterstützt.
Neuen Provider hinzufügen: `ProviderInterface` implementieren, in `Core.php` via `$registry->register()` eintragen — erscheint automatisch in allen Dropdowns. Neuen Provider hinzufügen: `ProviderInterface` implementieren, in `Core.php` via `$registry->register()` eintragen — erscheint automatisch in allen Dropdowns.
@ -376,7 +371,6 @@ Alle Endpunkte erfordern `manage_options` (kein `nopriv`).
| `brezngeo_regen_meta` | `MetaEditorBox::ajax_regen` | Meta-Beschreibung für einzelnen Post neu generieren | | `brezngeo_regen_meta` | `MetaEditorBox::ajax_regen` | Meta-Beschreibung für einzelnen Post neu generieren |
| `brezngeo_test_connection` | `ProviderPage::ajax_test_connection` | API-Key und Verbindung testen | | `brezngeo_test_connection` | `ProviderPage::ajax_test_connection` | API-Key und Verbindung testen |
| `brezngeo_get_default_prompt` | `ProviderPage::ajax_get_default_prompt` | Standard-Prompt zurücksetzen | | `brezngeo_get_default_prompt` | `ProviderPage::ajax_get_default_prompt` | Standard-Prompt zurücksetzen |
| `brezngeo_openrouter_load_models` | `ProviderPage::ajax_openrouter_load_models` | Kuratierte OpenRouter-Marketing/SEO-Modell-Liste laden (12 h Transient-Cache) |
| `brezngeo_link_analysis` | `LinkAnalysis::ajax_analyse` | Link-Analyse ausführen | | `brezngeo_link_analysis` | `LinkAnalysis::ajax_analyse` | Link-Analyse ausführen |
| `brezngeo_link_suggestions` | `LinkSuggest::ajax_suggest` | Top-10 interne Link-Vorschläge für aktuellen Beitrag zurückgeben | | `brezngeo_link_suggestions` | `LinkSuggest::ajax_suggest` | Top-10 interne Link-Vorschläge für aktuellen Beitrag zurückgeben |
| `brezngeo_geo_generate` | `GeoEditorBox::ajax_generate` | GEO Block generieren | | `brezngeo_geo_generate` | `GeoEditorBox::ajax_generate` | GEO Block generieren |
@ -427,7 +421,7 @@ Kein JavaScript-Build-Step. Alle Assets unter `assets/` sind direkte JS/CSS-Date
| Caching | WordPress Transients | | Caching | WordPress Transients |
| Frontend | Vanilla JS + jQuery (WordPress-integriert), kein Build-Step | | Frontend | Vanilla JS + jQuery (WordPress-integriert), kein Build-Step |
| I18n | `.pot`-File, Text-Domain `brezngeo` | | I18n | `.pot`-File, Text-Domain `brezngeo` |
| Tests | PHPUnit (163 Tests, 311 Assertions) | | Tests | PHPUnit (158 Tests, 301 Assertions) |
| Coding Standard | WordPress PHPCS | | Coding Standard | WordPress PHPCS |
| Lizenz | GPL-2.0-or-later | | Lizenz | GPL-2.0-or-later |

View file

@ -3,8 +3,8 @@
![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue) ![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue)
![WordPress 6.0+](https://img.shields.io/badge/WordPress-6.0%2B-21759b) ![WordPress 6.0+](https://img.shields.io/badge/WordPress-6.0%2B-21759b)
![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green) ![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green)
![Version](https://img.shields.io/badge/Version-1.3.0-orange) ![Version](https://img.shields.io/badge/Version-1.2.2-orange)
![Tests](https://img.shields.io/badge/Tests-163%20passing-brightgreen) ![Tests](https://img.shields.io/badge/Tests-158%20passing-brightgreen)
🇩🇪 [Deutsche Version → README.de.md](README.de.md) 🇩🇪 [Deutsche Version → README.de.md](README.de.md)
@ -26,7 +26,7 @@ The AI wave made it worse. Plugins started offering "AI-powered" features — bu
BreznGEO takes a different approach: BreznGEO takes a different approach:
- **Direct API access.** You store your own key from OpenAI, Anthropic, Google, xAI, or OpenRouter (600+ models through one key). BreznGEO calls the API directly. No middleman, no margin, no data passing through third-party servers. - **Direct API access.** You store your own key from OpenAI, Anthropic, Google, or xAI. BreznGEO calls the API directly. No middleman, no margin, no data passing through third-party servers.
- **Clear output, not noise.** Meta descriptions, structured data, GEO content blocks, bot management. No readability scores, no keyword density meters, no upsell banners. - **Clear output, not noise.** Meta descriptions, structured data, GEO content blocks, bot management. No readability scores, no keyword density meters, no upsell banners.
- **No subscription.** GPL-2.0. Free to use on any number of sites. The only costs are API usage — typically fractions of a cent per post. - **No subscription.** GPL-2.0. Free to use on any number of sites. The only costs are API usage — typically fractions of a cent per post.
- **No telemetry.** BreznGEO sends no data home. No usage tracking, no remote logging, no analytics leaving your server. - **No telemetry.** BreznGEO sends no data home. No usage tracking, no remote logging, no analytics leaving your server.
@ -109,8 +109,7 @@ brezngeo/
│ ├── AnthropicProvider.php # Claude API (Messages API) │ ├── AnthropicProvider.php # Claude API (Messages API)
│ ├── GeminiProvider.php # Google Gemini (generateContent API) │ ├── GeminiProvider.php # Google Gemini (generateContent API)
│ ├── GrokProvider.php # xAI Grok (OpenAI-compatible endpoint) │ ├── GrokProvider.php # xAI Grok (OpenAI-compatible endpoint)
│ ├── OpenAIProvider.php # OpenAI GPT (Chat Completions API) │ └── OpenAIProvider.php # OpenAI GPT (Chat Completions API)
│ └── OpenRouterProvider.php # OpenRouter (600+ models via one OpenAI-compatible API)
└── vendor/ # Composer dependencies (production only) └── vendor/ # Composer dependencies (production only)
``` ```
@ -413,7 +412,6 @@ define( 'BREZNGEO_OPENAI_KEY', 'sk-...' );
define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' ); define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
define( 'BREZNGEO_GEMINI_KEY', 'AI...' ); define( 'BREZNGEO_GEMINI_KEY', 'AI...' );
define( 'BREZNGEO_GROK_KEY', 'xai-...' ); define( 'BREZNGEO_GROK_KEY', 'xai-...' );
define( 'BREZNGEO_OPENROUTER_KEY', 'sk-or-...' );
``` ```
In the admin UI, keys are always displayed masked: `••••••Ab3c9` (only the last 5 characters visible). In the admin UI, keys are always displayed masked: `••••••Ab3c9` (only the last 5 characters visible).
@ -446,7 +444,7 @@ The crawler log stores IP addresses exclusively as SHA-256 hashes. The original
## AI Providers ## AI Providers
BreznGEO supports five providers, all implementing the same `ProviderInterface`: BreznGEO supports four providers, all implementing the same `ProviderInterface`:
| Provider | Class | API Base URL | | Provider | Class | API Base URL |
|---|---|---| |---|---|---|
@ -454,9 +452,6 @@ BreznGEO supports five providers, all implementing the same `ProviderInterface`:
| Anthropic | `AnthropicProvider` | `https://api.anthropic.com/v1/messages` | | Anthropic | `AnthropicProvider` | `https://api.anthropic.com/v1/messages` |
| Google Gemini | `GeminiProvider` | `https://generativelanguage.googleapis.com/...` | | Google Gemini | `GeminiProvider` | `https://generativelanguage.googleapis.com/...` |
| xAI Grok | `GrokProvider` | `https://api.x.ai/v1/chat/completions` | | xAI Grok | `GrokProvider` | `https://api.x.ai/v1/chat/completions` |
| OpenRouter | `OpenRouterProvider` | `https://openrouter.ai/api/v1/chat/completions` |
**About OpenRouter:** A single API key opens access to 600+ models from OpenAI, Anthropic, Google, Meta, Mistral, xAI, DeepSeek and others. The curated Marketing/SEO selection is fetched on demand ("Load models" button) and cached for 12 hours. Pricing is read from OpenRouter automatically. Custom model IDs (e.g. `anthropic/claude-opus-4.7`) are supported.
### Adding a New Provider ### Adding a New Provider
@ -530,7 +525,6 @@ All endpoints are exclusively accessible to logged-in users with `manage_options
| `brezngeo_regen_meta` | `MetaEditorBox::ajax_regen` | Regenerate meta description for a single post | | `brezngeo_regen_meta` | `MetaEditorBox::ajax_regen` | Regenerate meta description for a single post |
| `brezngeo_test_connection` | `ProviderPage::ajax_test_connection` | Test API key and connection | | `brezngeo_test_connection` | `ProviderPage::ajax_test_connection` | Test API key and connection |
| `brezngeo_get_default_prompt` | `ProviderPage::ajax_get_default_prompt` | Reset to default prompt | | `brezngeo_get_default_prompt` | `ProviderPage::ajax_get_default_prompt` | Reset to default prompt |
| `brezngeo_openrouter_load_models` | `ProviderPage::ajax_openrouter_load_models` | Fetch curated OpenRouter marketing/SEO model list (12 h transient cache) |
| `brezngeo_link_analysis` | `LinkAnalysis::ajax_analyse` | Run link analysis for the dashboard | | `brezngeo_link_analysis` | `LinkAnalysis::ajax_analyse` | Run link analysis for the dashboard |
| `brezngeo_link_suggestions` | `LinkSuggest::ajax_suggest` | Return top-10 internal link suggestions for current post | | `brezngeo_link_suggestions` | `LinkSuggest::ajax_suggest` | Return top-10 internal link suggestions for current post |
| `brezngeo_geo_generate` | `GeoEditorBox::ajax_generate` | Generate GEO block for a single post | | `brezngeo_geo_generate` | `GeoEditorBox::ajax_generate` | Generate GEO block for a single post |
@ -582,7 +576,7 @@ The plugin has no JavaScript build step. All assets under `assets/` are direct J
| Caching | WordPress transients (llms.txt, link analysis, bulk lock) | | Caching | WordPress transients (llms.txt, link analysis, bulk lock) |
| Frontend | Vanilla JS + jQuery (WordPress-bundled), no build step | | Frontend | Vanilla JS + jQuery (WordPress-bundled), no build step |
| i18n | `.pot` file, text domain `brezngeo` | | i18n | `.pot` file, text domain `brezngeo` |
| Tests | PHPUnit (163 tests, 311 assertions) | | Tests | PHPUnit (158 tests, 301 assertions) |
| Coding standard | WordPress PHPCS | | Coding standard | WordPress PHPCS |
| License | GPL-2.0-or-later | | License | GPL-2.0-or-later |

View file

@ -33,68 +33,6 @@ jQuery( function ( $ ) {
} ); } );
} ); } );
// OpenRouter: toggle custom-model field when dropdown value is __custom__
function updateOpenRouterCustom() {
var val = $( '#brezngeo-openrouter-model' ).val();
$( '.brezngeo-openrouter-custom-wrap' ).toggle( val === '__custom__' );
}
$( document ).on( 'change', '#brezngeo-openrouter-model', function () {
updateOpenRouterCustom();
var $opt = $( this ).find( 'option:selected' );
var inVal = $opt.data( 'input' );
var outVal = $opt.data( 'output' );
if ( inVal !== undefined && inVal !== '' ) {
$( '#brezngeo-openrouter-pricing' ).html(
'Input $<span class="or-price-input">' + Number( inVal ).toFixed( 4 ) +
'</span> / 1M \u00b7 Output $<span class="or-price-output">' + Number( outVal ).toFixed( 4 ) +
'</span> / 1M'
);
}
} );
if ( $( '#brezngeo-openrouter-model' ).length ) updateOpenRouterCustom();
// OpenRouter: "Load models" button — fetches curated Marketing/SEO list from OpenRouter
$( document ).on( 'click', '.brezngeo-openrouter-load-btn', function () {
var btn = $( this );
var status = $( '.brezngeo-openrouter-load-status' );
var select = $( '#brezngeo-openrouter-model' );
btn.prop( 'disabled', true );
status.removeClass( 'success error' ).text( brezngeoAdmin.testing );
$.post( brezngeoAdmin.ajaxUrl, {
action: 'brezngeo_openrouter_load_models',
nonce: brezngeoAdmin.nonce,
} ).done( function ( res ) {
if ( ! res.success ) {
status.addClass( 'error' ).text( '\u2717 ' + res.data );
return;
}
var models = res.data;
var previous = select.val();
// Preserve the __custom__ option at the bottom
select.find( 'option' ).not( '[value="__custom__"]' ).remove();
select.find( 'option[value=""]' ).remove();
$.each( models, function ( id, meta ) {
var opt = $( '<option></option>' )
.val( id )
.text( meta.label || id )
.attr( 'data-input', meta.input_cost )
.attr( 'data-output', meta.output_cost );
select.find( 'option[value="__custom__"]' ).before( opt );
} );
if ( previous && select.find( 'option[value="' + previous.replace( /"/g, '\\"' ) + '"]' ).length ) {
select.val( previous );
}
select.trigger( 'change' );
status.addClass( 'success' ).text( '\u2713 ' + Object.keys( models ).length );
} ).fail( function () {
status.addClass( 'error' ).text( '\u2717 ' + brezngeoAdmin.networkError );
} ).always( function () {
btn.prop( 'disabled', false );
} );
} );
$( '#brezngeo-reset-prompt' ).on( 'click', function () { $( '#brezngeo-reset-prompt' ).on( 'click', function () {
if ( ! confirm( brezngeoAdmin.resetConfirm ) ) return; if ( ! confirm( brezngeoAdmin.resetConfirm ) ) return;
$.post( brezngeoAdmin.ajaxUrl, { $.post( brezngeoAdmin.ajaxUrl, {

View file

@ -3,7 +3,7 @@
* Plugin Name: BreznGEO * Plugin Name: BreznGEO
* Plugin URI: https://brezngeo.com/ * Plugin URI: https://brezngeo.com/
* Description: AI-powered meta descriptions, GEO structured data, and llms.txt for WordPress. * Description: AI-powered meta descriptions, GEO structured data, and llms.txt for WordPress.
* Version: 1.3.0 * Version: 1.2.2
* Requires at least: 6.0 * Requires at least: 6.0
* Requires PHP: 8.0 * Requires PHP: 8.0
* Author: NoSchmarrn.dev * Author: NoSchmarrn.dev
@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
define( 'BREZNGEO_VERSION', '1.3.0' ); define( 'BREZNGEO_VERSION', '1.2.2' );
define( 'BREZNGEO_FILE', __FILE__ ); define( 'BREZNGEO_FILE', __FILE__ );
define( 'BREZNGEO_DIR', plugin_dir_path( __FILE__ ) ); define( 'BREZNGEO_DIR', plugin_dir_path( __FILE__ ) );
define( 'BREZNGEO_URL', plugin_dir_url( __FILE__ ) ); define( 'BREZNGEO_URL', plugin_dir_url( __FILE__ ) );

View file

@ -7,7 +7,6 @@ if ( ! defined( 'ABSPATH' ) ) {
use BreznGEO\ProviderRegistry; use BreznGEO\ProviderRegistry;
use BreznGEO\Helpers\KeyVault; use BreznGEO\Helpers\KeyVault;
use BreznGEO\Providers\OpenRouterProvider;
class ProviderPage { class ProviderPage {
private const PRICING_URLS = array( private const PRICING_URLS = array(
@ -15,7 +14,6 @@ class ProviderPage {
'anthropic' => 'https://platform.claude.com/docs/en/about-claude/pricing', 'anthropic' => 'https://platform.claude.com/docs/en/about-claude/pricing',
'gemini' => 'https://ai.google.dev/gemini-api/docs/pricing?hl=de', 'gemini' => 'https://ai.google.dev/gemini-api/docs/pricing?hl=de',
'grok' => 'https://docs.x.ai/developers/models', 'grok' => 'https://docs.x.ai/developers/models',
'openrouter' => 'https://openrouter.ai/models',
); );
public function register(): void { public function register(): void {
@ -23,7 +21,6 @@ class ProviderPage {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'wp_ajax_brezngeo_test_connection', array( $this, 'ajax_test_connection' ) ); add_action( 'wp_ajax_brezngeo_test_connection', array( $this, 'ajax_test_connection' ) );
add_action( 'wp_ajax_brezngeo_get_default_prompt', array( $this, 'ajax_get_default_prompt' ) ); add_action( 'wp_ajax_brezngeo_get_default_prompt', array( $this, 'ajax_get_default_prompt' ) );
add_action( 'wp_ajax_brezngeo_openrouter_load_models', array( $this, 'ajax_openrouter_load_models' ) );
} }
public function register_settings(): void { public function register_settings(): void {
@ -74,23 +71,10 @@ class ProviderPage {
$clean['api_keys'][ $provider_id ] = $existing['api_keys'][ $provider_id ]; $clean['api_keys'][ $provider_id ] = $existing['api_keys'][ $provider_id ];
} }
} }
// Preserve DB-stored keys for providers whose UI field was disabled
// (wp-config.php constant override) and therefore never submitted.
foreach ( ( $existing['api_keys'] ?? array() ) as $provider_id => $stored ) {
if ( ! isset( $clean['api_keys'][ $provider_id ] ) ) {
$clean['api_keys'][ $provider_id ] = $stored;
}
}
$clean['models'] = array(); $clean['models'] = array();
foreach ( ( $input['models'] ?? array() ) as $provider_id => $model ) { foreach ( ( $input['models'] ?? array() ) as $provider_id => $model ) {
$pid = sanitize_key( $provider_id ); $clean['models'][ sanitize_key( $provider_id ) ] = sanitize_text_field( $model );
$value = sanitize_text_field( $model );
if ( $pid === 'openrouter' && $value === '__custom__' ) {
$custom_raw = (string) ( $input['openrouter_custom_model'] ?? '' );
$value = sanitize_text_field( $custom_raw );
}
$clean['models'][ $pid ] = $value;
} }
$clean['costs'] = array(); $clean['costs'] = array();
@ -108,18 +92,6 @@ class ProviderPage {
} }
} }
$selected_openrouter = $clean['models']['openrouter'] ?? '';
if ( $selected_openrouter !== '' ) {
$cached = get_transient( \BreznGEO\Providers\OpenRouterProvider::MODELS_CACHE );
if ( is_array( $cached ) && isset( $cached[ $selected_openrouter ] ) ) {
$meta = $cached[ $selected_openrouter ];
$clean['costs']['openrouter'][ $selected_openrouter ] = array(
'input' => (float) ( $meta['input_cost'] ?? 0 ),
'output' => (float) ( $meta['output_cost'] ?? 0 ),
);
}
}
return $clean; return $clean;
} }
@ -154,59 +126,6 @@ class ProviderPage {
wp_send_json_success( SettingsPage::getDefaultPrompt() ); wp_send_json_success( SettingsPage::getDefaultPrompt() );
} }
public function ajax_openrouter_load_models(): void {
check_ajax_referer( 'brezngeo_admin', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'Insufficient permissions.', 'brezngeo' ) );
}
$response = wp_remote_get(
OpenRouterProvider::MODELS_URL . '?category=marketing',
array(
'timeout' => 15,
'headers' => array(
'Accept' => 'application/json',
'HTTP-Referer' => home_url( '/' ),
'X-Title' => 'BreznGEO',
),
)
);
if ( is_wp_error( $response ) ) {
wp_send_json_error( $response->get_error_message() );
}
$code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( $code !== 200 || ! isset( $body['data'] ) || ! is_array( $body['data'] ) ) {
$msg = $body['error']['message'] ?? "HTTP $code";
wp_send_json_error( $msg );
}
$normalized = array();
foreach ( $body['data'] as $model ) {
if ( ! is_array( $model ) || empty( $model['id'] ) ) {
continue;
}
$id = (string) $model['id'];
$label = isset( $model['name'] ) && is_string( $model['name'] ) && $model['name'] !== '' ? (string) $model['name'] : $id;
$input_per_token = isset( $model['pricing']['prompt'] ) ? (float) $model['pricing']['prompt'] : 0.0;
$output_per_token = isset( $model['pricing']['completion'] ) ? (float) $model['pricing']['completion'] : 0.0;
$normalized[ $id ] = array(
'label' => $label,
'input_cost' => round( $input_per_token * 1_000_000, 4 ),
'output_cost' => round( $output_per_token * 1_000_000, 4 ),
);
}
if ( empty( $normalized ) ) {
wp_send_json_error( __( 'No models returned by OpenRouter.', 'brezngeo' ) );
}
set_transient( OpenRouterProvider::MODELS_CACHE, $normalized, 12 * HOUR_IN_SECONDS );
wp_send_json_success( $normalized );
}
public function render(): void { public function render(): void {
if ( ! current_user_can( 'manage_options' ) ) { if ( ! current_user_can( 'manage_options' ) ) {
return; return;

View file

@ -23,12 +23,6 @@ class SettingsPage {
*/ */
public const OPTION_KEY_SCHEMA = 'brezngeo_schema_settings'; public const OPTION_KEY_SCHEMA = 'brezngeo_schema_settings';
/**
* Provider IDs that support a wp-config.php constant override.
* A defined constant wins over the DB value and locks the UI field.
*/
private const CONSTANT_KEY_PROVIDERS = array( 'openai', 'anthropic', 'gemini', 'grok', 'openrouter' );
/** /**
* Returns merged settings from both option keys with defaults applied. * Returns merged settings from both option keys with defaults applied.
* Called by MetaGenerator, SchemaEnhancer, BulkPage, and admin pages. * Called by MetaGenerator, SchemaEnhancer, BulkPage, and admin pages.
@ -68,37 +62,9 @@ class SettingsPage {
$settings['api_keys'][ $id ] = $decrypted !== '' ? $decrypted : $stored; $settings['api_keys'][ $id ] = $decrypted !== '' ? $decrypted : $stored;
} }
// wp-config.php constants override the DB value and lock the admin field.
$settings['api_keys_locked'] = array();
foreach ( self::CONSTANT_KEY_PROVIDERS as $provider_id ) {
$constant = self::constantNameForProvider( $provider_id );
if ( $constant !== '' && defined( $constant ) ) {
$settings['api_keys'][ $provider_id ] = (string) constant( $constant );
$settings['api_keys_locked'][ $provider_id ] = true;
}
}
return $settings; return $settings;
} }
/**
* Returns the wp-config.php constant name for a given provider ID,
* or empty string if the provider has no constant override.
*
* Must be pure no WP hooks, no option reads. Called once per entry
* in CONSTANT_KEY_PROVIDERS during getSettings().
*
* The website (de/index.html, howto.html) already promises the names
* BREZNGEO_OPENAI_KEY, BREZNGEO_ANTHROPIC_KEY, BREZNGEO_GEMINI_KEY,
* BREZNGEO_GROK_KEY and BREZNGEO_OPENROUTER_KEY those are the contract.
*/
private static function constantNameForProvider( string $provider_id ): string {
if ( ! in_array( $provider_id, self::CONSTANT_KEY_PROVIDERS, true ) ) {
return '';
}
return 'BREZNGEO_' . strtoupper( $provider_id ) . '_KEY';
}
public static function getDefaultPrompt(): string { public static function getDefaultPrompt(): string {
$locale = get_locale(); $locale = get_locale();
$is_german = str_starts_with( $locale, 'de_' ); $is_german = str_starts_with( $locale, 'de_' );

View file

@ -1,81 +0,0 @@
<?php
/**
* OpenRouter-specific model field. Rendered from provider.php.
*
* Expected in scope:
* $provider (OpenRouterProvider), $settings (array), $pricing_urls (array)
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$brezngeo_or_models = $provider->getModels();
$brezngeo_or_saved_model = $settings['models']['openrouter'] ?? '';
$brezngeo_or_is_custom = $brezngeo_or_saved_model !== '' && ! array_key_exists( $brezngeo_or_saved_model, $brezngeo_or_models );
$brezngeo_or_cached_pricing = get_transient( \BreznGEO\Providers\OpenRouterProvider::MODELS_CACHE );
$brezngeo_or_cache_is_array = is_array( $brezngeo_or_cached_pricing );
$brezngeo_or_selected_pricing = ( $brezngeo_or_cache_is_array && isset( $brezngeo_or_cached_pricing[ $brezngeo_or_saved_model ] ) )
? $brezngeo_or_cached_pricing[ $brezngeo_or_saved_model ]
: null;
?>
<br><br>
<label><?php esc_html_e( 'Model:', 'brezngeo' ); ?></label>
<select name="brezngeo_settings[models][openrouter]" class="brezngeo-openrouter-model-select" id="brezngeo-openrouter-model">
<?php if ( empty( $brezngeo_or_models ) ) : ?>
<option value=""><?php esc_html_e( 'No models loaded yet — click "Load models"', 'brezngeo' ); ?></option>
<?php else : ?>
<?php foreach ( $brezngeo_or_models as $brezngeo_or_mid => $brezngeo_or_label ) : ?>
<option value="<?php echo esc_attr( $brezngeo_or_mid ); ?>"
<?php selected( $brezngeo_or_saved_model, $brezngeo_or_mid ); ?>
data-input="<?php echo esc_attr( isset( $brezngeo_or_cached_pricing[ $brezngeo_or_mid ]['input_cost'] ) ? $brezngeo_or_cached_pricing[ $brezngeo_or_mid ]['input_cost'] : '' ); ?>"
data-output="<?php echo esc_attr( isset( $brezngeo_or_cached_pricing[ $brezngeo_or_mid ]['output_cost'] ) ? $brezngeo_or_cached_pricing[ $brezngeo_or_mid ]['output_cost'] : '' ); ?>">
<?php echo esc_html( $brezngeo_or_label ); ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
<option value="__custom__" <?php selected( $brezngeo_or_is_custom ); ?>>
<?php esc_html_e( 'Custom model ID…', 'brezngeo' ); ?>
</option>
</select>
<button type="button" class="button brezngeo-openrouter-load-btn">
<?php esc_html_e( 'Load models', 'brezngeo' ); ?>
</button>
<span class="brezngeo-openrouter-load-status" aria-live="polite"></span>
<div class="brezngeo-openrouter-custom-wrap" style="<?php echo $brezngeo_or_is_custom ? '' : 'display:none;'; ?>margin-top:10px;">
<label for="brezngeo-openrouter-custom">
<?php esc_html_e( 'Custom model ID:', 'brezngeo' ); ?>
</label>
<input type="text"
id="brezngeo-openrouter-custom"
name="brezngeo_settings[openrouter_custom_model]"
value="<?php echo esc_attr( $brezngeo_or_is_custom ? $brezngeo_or_saved_model : '' ); ?>"
placeholder="<?php esc_attr_e( 'e.g. anthropic/claude-opus-4.7', 'brezngeo' ); ?>"
class="regular-text">
<p class="description">
<a href="https://brezngeo.com/faq.html#openrouter" target="_blank" rel="noopener">
<?php esc_html_e( 'Learn how to find OpenRouter model IDs →', 'brezngeo' ); ?>
</a>
</p>
</div>
<?php if ( ! empty( $pricing_urls['openrouter'] ) ) : ?>
<p style="margin-top:8px;">
<a href="<?php echo esc_url( $pricing_urls['openrouter'] ); ?>" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'Browse all OpenRouter models →', 'brezngeo' ); ?>
</a>
</p>
<?php endif; ?>
<p style="margin-top:12px;"><strong><?php esc_html_e( 'Pricing (automatically from OpenRouter, per 1M tokens):', 'brezngeo' ); ?></strong></p>
<div class="brezngeo-openrouter-pricing-display" id="brezngeo-openrouter-pricing" style="font-size:12px;color:#555;">
<?php if ( $brezngeo_or_selected_pricing ) : ?>
Input $<span class="or-price-input"><?php echo esc_html( number_format( (float) $brezngeo_or_selected_pricing['input_cost'], 4 ) ); ?></span>
/ 1M · Output $<span class="or-price-output"><?php echo esc_html( number_format( (float) $brezngeo_or_selected_pricing['output_cost'], 4 ) ); ?></span>
/ 1M
<?php elseif ( $brezngeo_or_is_custom ) : ?>
<em><?php esc_html_e( 'Pricing unknown for custom models — will be populated after you click "Load models".', 'brezngeo' ); ?></em>
<?php else : ?>
<em><?php esc_html_e( 'Click "Load models" to fetch pricing from OpenRouter.', 'brezngeo' ); ?></em>
<?php endif; ?>
</div>

View file

@ -36,26 +36,9 @@
</td> </td>
</tr> </tr>
<?php foreach ( $providers as $id => $provider ) : // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <?php foreach ( $providers as $id => $provider ) : // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?>
<?php $brezngeo_locked = ! empty( $settings['api_keys_locked'][ $id ] ); ?>
<tr class="brezngeo-provider-row" data-provider="<?php echo esc_attr( $id ); ?>"> <tr class="brezngeo-provider-row" data-provider="<?php echo esc_attr( $id ); ?>">
<th scope="row"><?php echo esc_html( $provider->getName() ); ?> <?php esc_html_e( 'API Key', 'brezngeo' ); ?></th> <th scope="row"><?php echo esc_html( $provider->getName() ); ?> <?php esc_html_e( 'API Key', 'brezngeo' ); ?></th>
<td> <td>
<?php if ( $brezngeo_locked ) : ?>
<span class="brezngeo-key-saved">
<?php
printf(
/* translators: %s: wp-config.php constant name */
esc_html__( 'Loaded from wp-config.php: %s', 'brezngeo' ),
'<code>BREZNGEO_' . esc_html( strtoupper( $id ) ) . '_KEY</code>'
);
?>
</span><br>
<input type="password" value="" placeholder="&#8212;" class="regular-text" disabled>
<button type="button" class="button brezngeo-test-btn" data-provider="<?php echo esc_attr( $id ); ?>">
<?php esc_html_e( 'Test connection', 'brezngeo' ); ?>
</button>
<span class="brezngeo-test-result" id="test-result-<?php echo esc_attr( $id ); ?>"></span>
<?php else : ?>
<?php if ( ! empty( $masked_keys[ $id ] ) ) : ?> <?php if ( ! empty( $masked_keys[ $id ] ) ) : ?>
<span class="brezngeo-key-saved"> <span class="brezngeo-key-saved">
<?php esc_html_e( 'Saved:', 'brezngeo' ); ?> <code><?php echo esc_html( $masked_keys[ $id ] ); ?></code> <?php esc_html_e( 'Saved:', 'brezngeo' ); ?> <code><?php echo esc_html( $masked_keys[ $id ] ); ?></code>
@ -71,10 +54,6 @@
<?php esc_html_e( 'Test connection', 'brezngeo' ); ?> <?php esc_html_e( 'Test connection', 'brezngeo' ); ?>
</button> </button>
<span class="brezngeo-test-result" id="test-result-<?php echo esc_attr( $id ); ?>"></span> <span class="brezngeo-test-result" id="test-result-<?php echo esc_attr( $id ); ?>"></span>
<?php endif; ?>
<?php if ( $id === 'openrouter' ) : ?>
<?php include BREZNGEO_DIR . 'includes/Admin/views/partials/openrouter-model-field.php'; ?>
<?php else : ?>
<br><br> <br><br>
<label><?php esc_html_e( 'Model:', 'brezngeo' ); ?></label> <label><?php esc_html_e( 'Model:', 'brezngeo' ); ?></label>
<select name="brezngeo_settings[models][<?php echo esc_attr( $id ); ?>]"> <select name="brezngeo_settings[models][<?php echo esc_attr( $id ); ?>]">
@ -118,7 +97,6 @@
placeholder="z.B. 0.60" style="width:75px;"> / 1M</span> placeholder="z.B. 0.60" style="width:75px;"> / 1M</span>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>

View file

@ -28,7 +28,6 @@ class Core {
require_once BREZNGEO_DIR . 'includes/Providers/AnthropicProvider.php'; require_once BREZNGEO_DIR . 'includes/Providers/AnthropicProvider.php';
require_once BREZNGEO_DIR . 'includes/Providers/GeminiProvider.php'; require_once BREZNGEO_DIR . 'includes/Providers/GeminiProvider.php';
require_once BREZNGEO_DIR . 'includes/Providers/GrokProvider.php'; require_once BREZNGEO_DIR . 'includes/Providers/GrokProvider.php';
require_once BREZNGEO_DIR . 'includes/Providers/OpenRouterProvider.php';
require_once BREZNGEO_DIR . 'includes/Helpers/KeyVault.php'; require_once BREZNGEO_DIR . 'includes/Helpers/KeyVault.php';
require_once BREZNGEO_DIR . 'includes/Helpers/TokenEstimator.php'; require_once BREZNGEO_DIR . 'includes/Helpers/TokenEstimator.php';
require_once BREZNGEO_DIR . 'includes/Helpers/FallbackMeta.php'; require_once BREZNGEO_DIR . 'includes/Helpers/FallbackMeta.php';
@ -66,7 +65,6 @@ class Core {
$registry->register( new Providers\AnthropicProvider() ); $registry->register( new Providers\AnthropicProvider() );
$registry->register( new Providers\GeminiProvider() ); $registry->register( new Providers\GeminiProvider() );
$registry->register( new Providers\GrokProvider() ); $registry->register( new Providers\GrokProvider() );
$registry->register( new Providers\OpenRouterProvider() );
( new Features\MetaGenerator() )->register(); ( new Features\MetaGenerator() )->register();
( new Features\SchemaEnhancer() )->register(); ( new Features\SchemaEnhancer() )->register();

View file

@ -1,106 +0,0 @@
<?php
namespace BreznGEO\Providers;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class OpenRouterProvider implements ProviderInterface {
public const API_URL = 'https://openrouter.ai/api/v1/chat/completions';
public const MODELS_URL = 'https://openrouter.ai/api/v1/models';
public const MODELS_CACHE = 'brezngeo_openrouter_models';
public const FALLBACK_TEST = 'openai/gpt-4o-mini';
public function getId(): string {
return 'openrouter'; }
public function getName(): string {
return 'OpenRouter'; }
/**
* Returns cached curated Marketing/SEO models plus any saved custom model.
* When the cache is empty, returns an empty array the admin view shows a "Load models" hint.
*/
public function getModels(): array {
$cached = get_transient( self::MODELS_CACHE );
$models = array();
if ( is_array( $cached ) ) {
foreach ( $cached as $id => $meta ) {
if ( is_array( $meta ) && isset( $meta['label'] ) ) {
$models[ (string) $id ] = (string) $meta['label'];
}
}
}
if ( class_exists( '\BreznGEO\Admin\SettingsPage' ) ) {
$settings = \BreznGEO\Admin\SettingsPage::getSettings();
$custom = $settings['models'][ $this->getId() ] ?? '';
if ( is_string( $custom ) && $custom !== '' && ! isset( $models[ $custom ] ) ) {
$models[ $custom ] = $custom . ' (' . __( 'custom', 'brezngeo' ) . ')';
}
}
return $models;
}
public function testConnection( string $api_key ): array {
try {
$this->generateText( 'Say "ok"', $api_key, self::FALLBACK_TEST, 5 );
return array(
'success' => true,
'message' => __( 'Connection successful', 'brezngeo' ),
);
} catch ( \RuntimeException $e ) {
return array(
'success' => false,
'message' => $e->getMessage(),
);
}
}
public function generateText( string $prompt, string $api_key, string $model, int $max_tokens = 300 ): string {
$response = wp_remote_post(
self::API_URL,
array(
'timeout' => 30,
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
'HTTP-Referer' => home_url( '/' ),
'X-Title' => 'BreznGEO',
),
'body' => wp_json_encode(
array(
'model' => $model,
'messages' => array(
array(
'role' => 'user',
'content' => $prompt,
),
),
'max_tokens' => $max_tokens,
)
),
)
);
return $this->parseResponse( $response );
}
private function parseResponse( $response ): string {
if ( is_wp_error( $response ) ) {
throw new \RuntimeException( esc_html( $response->get_error_message() ) );
}
$code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( $code !== 200 ) {
$msg = $body['error']['message'] ?? "HTTP $code";
throw new \RuntimeException( esc_html( $msg ) );
}
return trim( $body['choices'][0]['message']['content'] ?? '' );
}
}

Binary file not shown.

View file

@ -1617,51 +1617,3 @@ msgstr "Hell \xe2\x80\x94 klare Karte mit blauem Akzent. Dunkel \xe2\x80\x94 das
#, php-format #, php-format
msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). <a href=\"%s\">Configure a provider \xe2\x86\x92</a>" msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). <a href=\"%s\">Configure a provider \xe2\x86\x92</a>"
msgstr "Kein KI-Anbieter verbunden \xe2\x80\x94 Beschreibungen werden ohne KI aus dem Inhalt generiert (Fallback-Modus). <a href=\"%s\">Anbieter konfigurieren \xe2\x86\x92</a>" msgstr "Kein KI-Anbieter verbunden \xe2\x80\x94 Beschreibungen werden ohne KI aus dem Inhalt generiert (Fallback-Modus). <a href=\"%s\">Anbieter konfigurieren \xe2\x86\x92</a>"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Load models"
msgstr "Modelle laden"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Custom model ID\xe2\x80\xa6"
msgstr "Eigene Modell-ID\xe2\x80\xa6"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Custom model ID:"
msgstr "Eigene Modell-ID:"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "e.g. anthropic/claude-opus-4.7"
msgstr "z. B. anthropic/claude-opus-4.7"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Learn how to find OpenRouter model IDs \xe2\x86\x92"
msgstr "So findest du OpenRouter-Modell-IDs \xe2\x86\x92"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Browse all OpenRouter models \xe2\x86\x92"
msgstr "Alle OpenRouter-Modelle ansehen \xe2\x86\x92"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "No models loaded yet \xe2\x80\x94 click \"Load models\""
msgstr "Noch keine Modelle geladen \xe2\x80\x94 auf \"Modelle laden\" klicken"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Pricing (automatically from OpenRouter, per 1M tokens):"
msgstr "Preise (automatisch von OpenRouter, pro 1M Tokens):"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Pricing unknown for custom models \xe2\x80\x94 will be populated after you click \"Load models\"."
msgstr "Preise f\xc3\xbcr eigene Modelle unbekannt \xe2\x80\x94 werden nach Klick auf \"Modelle laden\" bef\xc3\xbcllt."
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Click \"Load models\" to fetch pricing from OpenRouter."
msgstr "Klick auf \"Modelle laden\", um Preise von OpenRouter zu holen."
#: includes/Providers/OpenRouterProvider.php
msgid "custom"
msgstr "benutzerdefiniert"
#: includes/Admin/ProviderPage.php
msgid "No models returned by OpenRouter."
msgstr "OpenRouter hat keine Modelle geliefert."

Binary file not shown.

View file

@ -1617,51 +1617,3 @@ msgstr "Light \xe2\x80\x94 clean card with a blue accent. Dark \xe2\x80\x94 same
#, php-format #, php-format
msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). <a href=\"%s\">Configure a provider \xe2\x86\x92</a>" msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). <a href=\"%s\">Configure a provider \xe2\x86\x92</a>"
msgstr "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). <a href=\"%s\">Configure a provider \xe2\x86\x92</a>" msgstr "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). <a href=\"%s\">Configure a provider \xe2\x86\x92</a>"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Load models"
msgstr "Load models"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Custom model ID\xe2\x80\xa6"
msgstr "Custom model ID\xe2\x80\xa6"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Custom model ID:"
msgstr "Custom model ID:"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "e.g. anthropic/claude-opus-4.7"
msgstr "e.g. anthropic/claude-opus-4.7"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Learn how to find OpenRouter model IDs \xe2\x86\x92"
msgstr "Learn how to find OpenRouter model IDs \xe2\x86\x92"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Browse all OpenRouter models \xe2\x86\x92"
msgstr "Browse all OpenRouter models \xe2\x86\x92"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "No models loaded yet \xe2\x80\x94 click \"Load models\""
msgstr "No models loaded yet \xe2\x80\x94 click \"Load models\""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Pricing (automatically from OpenRouter, per 1M tokens):"
msgstr "Pricing (automatically from OpenRouter, per 1M tokens):"
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Pricing unknown for custom models \xe2\x80\x94 will be populated after you click \"Load models\"."
msgstr "Pricing unknown for custom models \xe2\x80\x94 will be populated after you click \"Load models\"."
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Click \"Load models\" to fetch pricing from OpenRouter."
msgstr "Click \"Load models\" to fetch pricing from OpenRouter."
#: includes/Providers/OpenRouterProvider.php
msgid "custom"
msgstr "custom"
#: includes/Admin/ProviderPage.php
msgid "No models returned by OpenRouter."
msgstr "No models returned by OpenRouter."

View file

@ -1617,51 +1617,3 @@ msgstr ""
#, php-format #, php-format
msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). <a href=\"%s\">Configure a provider \xe2\x86\x92</a>" msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). <a href=\"%s\">Configure a provider \xe2\x86\x92</a>"
msgstr "" msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Load models"
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Custom model ID\xe2\x80\xa6"
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Custom model ID:"
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "e.g. anthropic/claude-opus-4.7"
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Learn how to find OpenRouter model IDs \xe2\x86\x92"
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Browse all OpenRouter models \xe2\x86\x92"
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "No models loaded yet \xe2\x80\x94 click \"Load models\""
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Pricing (automatically from OpenRouter, per 1M tokens):"
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Pricing unknown for custom models \xe2\x80\x94 will be populated after you click \"Load models\"."
msgstr ""
#: includes/Admin/views/partials/openrouter-model-field.php
msgid "Click \"Load models\" to fetch pricing from OpenRouter."
msgstr ""
#: includes/Providers/OpenRouterProvider.php
msgid "custom"
msgstr ""
#: includes/Admin/ProviderPage.php
msgid "No models returned by OpenRouter."
msgstr ""

View file

@ -3,7 +3,7 @@ Contributors: mifupadev
Tags: seo, ai, meta description, schema, llms.txt Tags: seo, ai, meta description, schema, llms.txt
Requires at least: 6.0 Requires at least: 6.0
Tested up to: 6.9 Tested up to: 6.9
Stable tag: 1.3.0 Stable tag: 1.2.2
Requires PHP: 8.0 Requires PHP: 8.0
License: GPL-2.0-or-later License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html License URI: https://www.gnu.org/licenses/gpl-2.0.html
@ -69,13 +69,12 @@ Finds all published posts without a meta description (including descriptions set
= Multi-Provider AI Support = = Multi-Provider AI Support =
Choose from five AI providers — or access 600+ models through a single OpenRouter API key. Switch at any time without losing your settings: Choose from four AI providers and switch at any time without losing your settings:
* OpenAI (GPT-4.1, GPT-4o, GPT-4o mini, and more) * OpenAI (GPT-4.1, GPT-4o, GPT-4o mini, and more)
* Anthropic Claude (Claude 3.5 Sonnet, Claude 3 Haiku, and more) * Anthropic Claude (Claude 3.5 Sonnet, Claude 3 Haiku, and more)
* Google Gemini (Gemini 2.0 Flash, Gemini 1.5 Pro, and more) * Google Gemini (Gemini 2.0 Flash, Gemini 1.5 Pro, and more)
* xAI Grok (Grok 3, Grok 3 mini, and more) * xAI Grok (Grok 3, Grok 3 mini, and more)
* OpenRouter (access to 600+ models including Claude, GPT, Gemini, Llama, Mistral, DeepSeek, and more through a single API key)
= Schema.org Enhancer (GEO) = = Schema.org Enhancer (GEO) =
@ -141,7 +140,7 @@ An API key is required for AI-generated meta descriptions. Without one, the plug
= How much does it cost to generate meta descriptions? = = How much does it cost to generate meta descriptions? =
Cost depends on the AI provider and model you choose. A single meta description typically uses fewer than 1,500 tokens (input + output combined). As a rough reference, 1,000 descriptions with GPT-4o mini has cost around $0.50$1.00 at recent rates — but AI provider pricing changes over time. The AI Provider settings page links directly to the current pricing page for each supported provider. For OpenRouter, per-model pricing is fetched directly from the API and displayed in-plugin after you click "Load models". Cost depends on the AI provider and model you choose. A single meta description typically uses fewer than 1,500 tokens (input + output combined). As a rough reference, 1,000 descriptions with GPT-4o mini has cost around $0.50$1.00 at recent rates — but AI provider pricing changes over time. The AI Provider settings page links directly to the current pricing page for each supported provider.
= Are my API keys stored securely? = = Are my API keys stored securely? =
@ -223,22 +222,8 @@ No data is transmitted during normal page loads or to visitors.
* Privacy policy: https://x.ai/privacy-policy * Privacy policy: https://x.ai/privacy-policy
* Terms of use: https://x.ai/legal/terms-of-service * Terms of use: https://x.ai/legal/terms-of-service
= OpenRouter =
* Data sent (only when selected as active provider): Post title and content excerpt (meta descriptions, GEO Block); candidate post titles and URLs (link suggestions); post content and keyword (keyword analysis).
* Additional request (only when you click "Load models" in the provider settings): a list of available models is fetched from the OpenRouter models API. No user data is sent with this request.
* API endpoints: `https://openrouter.ai/api/v1/chat/completions` (text generation), `https://openrouter.ai/api/v1/models` (model list, on demand).
* Note: OpenRouter is a routing aggregator. The actual AI model selected by the user may be served by OpenAI, Anthropic, Google, Meta, xAI, Mistral, DeepSeek or another upstream provider. See OpenRouter's privacy policy for details on upstream routing.
* Privacy policy: https://openrouter.ai/privacy
* Terms of use: https://openrouter.ai/terms
== Changelog == == Changelog ==
= 1.3.0 =
* New: OpenRouter as a fifth AI provider — access to 600+ models (Claude, GPT, Gemini, Llama, Mistral, DeepSeek, and more) through a single API key.
* New: On-demand model loader fetches OpenRouter's curated Marketing/SEO model list with live per-model pricing.
* New: Custom model ID field lets you route to any OpenRouter model (e.g. anthropic/claude-opus-4.7) without waiting for a plugin update.
* New: OpenRouter joins the wp-config.php key-mapping (BREZNGEO_OPENROUTER_KEY) — define any provider key as a constant to keep it out of the database entirely; the admin field becomes read-only.
= 1.2.2 = = 1.2.2 =
* i18n: Added explicit load_plugin_textdomain() call for reliable translation loading on ClassicPress and other WordPress derivatives. * i18n: Added explicit load_plugin_textdomain() call for reliable translation loading on ClassicPress and other WordPress derivatives.