diff --git a/README.de.md b/README.de.md index 38e8687..66f0b89 100644 --- a/README.de.md +++ b/README.de.md @@ -3,8 +3,8 @@ ![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) ![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green) -![Version](https://img.shields.io/badge/Version-1.2.2-orange) -![Tests](https://img.shields.io/badge/Tests-158%20passing-brightgreen) +![Version](https://img.shields.io/badge/Version-1.3.0-orange) +![Tests](https://img.shields.io/badge/Tests-163%20passing-brightgreen) 🇬🇧 [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: -- **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. +- **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. - **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 Telemetrie.** BreznGEO sendet keine Daten nach Hause. Kein Usage-Tracking, kein Remote-Logging, keine Analytics, die den eigenen Server verlassen. @@ -104,12 +104,13 @@ brezngeo/ │ │ ├── KeywordVariants.php # Locale-basierte Keyword-Varianten (EN/DE) │ │ └── TokenEstimator.php # Grobe Token-Schätzung für Kostenvorschau im Bulk │ └── Providers/ -│ ├── ProviderInterface.php # Interface: getId, getName, getModels, testConnection, generateText -│ ├── ProviderRegistry.php # Registry-Pattern: Provider registrieren und abrufen -│ ├── AnthropicProvider.php # Claude API (Messages API) -│ ├── GeminiProvider.php # Google Gemini (generateContent API) -│ ├── GrokProvider.php # xAI Grok (OpenAI-kompatibler Endpunkt) -│ └── OpenAIProvider.php # OpenAI GPT (Chat Completions API) +│ ├── ProviderInterface.php # Interface: getId, getName, getModels, testConnection, generateText +│ ├── ProviderRegistry.php # Registry-Pattern: Provider registrieren und abrufen +│ ├── AnthropicProvider.php # Claude API (Messages API) +│ ├── GeminiProvider.php # Google Gemini (generateContent API) +│ ├── GrokProvider.php # xAI Grok (OpenAI-kompatibler Endpunkt) +│ ├── OpenAIProvider.php # OpenAI GPT (Chat Completions API) +│ └── OpenRouterProvider.php # OpenRouter (600+ Modelle über eine OpenAI-kompatible API) └── vendor/ # Composer-Abhängigkeiten (nur Produktionsstand) ``` @@ -336,6 +337,9 @@ CrawlerLog speichert IPs ausschließlich als SHA-256-Hash. Originalwert wird nie | Anthropic | `AnthropicProvider` | `https://api.anthropic.com/v1/messages` | | Google Gemini | `GeminiProvider` | `https://generativelanguage.googleapis.com/...` | | 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. @@ -371,6 +375,7 @@ Alle Endpunkte erfordern `manage_options` (kein `nopriv`). | `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_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_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 | @@ -421,7 +426,7 @@ Kein JavaScript-Build-Step. Alle Assets unter `assets/` sind direkte JS/CSS-Date | Caching | WordPress Transients | | Frontend | Vanilla JS + jQuery (WordPress-integriert), kein Build-Step | | I18n | `.pot`-File, Text-Domain `brezngeo` | -| Tests | PHPUnit (158 Tests, 301 Assertions) | +| Tests | PHPUnit (163 Tests, 311 Assertions) | | Coding Standard | WordPress PHPCS | | Lizenz | GPL-2.0-or-later | diff --git a/README.md b/README.md index fefc1f3..7edf5a4 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ ![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) ![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green) -![Version](https://img.shields.io/badge/Version-1.2.2-orange) -![Tests](https://img.shields.io/badge/Tests-158%20passing-brightgreen) +![Version](https://img.shields.io/badge/Version-1.3.0-orange) +![Tests](https://img.shields.io/badge/Tests-163%20passing-brightgreen) 🇩🇪 [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: -- **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. +- **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. - **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 telemetry.** BreznGEO sends no data home. No usage tracking, no remote logging, no analytics leaving your server. @@ -104,12 +104,13 @@ brezngeo/ │ │ ├── KeywordVariants.php # Locale-aware keyword variant generation (EN/DE) │ │ └── TokenEstimator.php # Rough token estimate for cost preview in bulk │ └── Providers/ -│ ├── ProviderInterface.php # Interface: getId, getName, getModels, testConnection, generateText -│ ├── ProviderRegistry.php # Registry pattern: register and retrieve providers -│ ├── AnthropicProvider.php # Claude API (Messages API) -│ ├── GeminiProvider.php # Google Gemini (generateContent API) -│ ├── GrokProvider.php # xAI Grok (OpenAI-compatible endpoint) -│ └── OpenAIProvider.php # OpenAI GPT (Chat Completions API) +│ ├── ProviderInterface.php # Interface: getId, getName, getModels, testConnection, generateText +│ ├── ProviderRegistry.php # Registry pattern: register and retrieve providers +│ ├── AnthropicProvider.php # Claude API (Messages API) +│ ├── GeminiProvider.php # Google Gemini (generateContent API) +│ ├── GrokProvider.php # xAI Grok (OpenAI-compatible endpoint) +│ ├── OpenAIProvider.php # OpenAI GPT (Chat Completions API) +│ └── OpenRouterProvider.php # OpenRouter (600+ models via one OpenAI-compatible API) └── vendor/ # Composer dependencies (production only) ``` @@ -444,7 +445,7 @@ The crawler log stores IP addresses exclusively as SHA-256 hashes. The original ## AI Providers -BreznGEO supports four providers, all implementing the same `ProviderInterface`: +BreznGEO supports five providers, all implementing the same `ProviderInterface`: | Provider | Class | API Base URL | |---|---|---| @@ -452,6 +453,9 @@ BreznGEO supports four providers, all implementing the same `ProviderInterface`: | Anthropic | `AnthropicProvider` | `https://api.anthropic.com/v1/messages` | | Google Gemini | `GeminiProvider` | `https://generativelanguage.googleapis.com/...` | | 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 @@ -525,6 +529,7 @@ 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_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_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_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 | @@ -576,7 +581,7 @@ The plugin has no JavaScript build step. All assets under `assets/` are direct J | Caching | WordPress transients (llms.txt, link analysis, bulk lock) | | Frontend | Vanilla JS + jQuery (WordPress-bundled), no build step | | i18n | `.pot` file, text domain `brezngeo` | -| Tests | PHPUnit (158 tests, 301 assertions) | +| Tests | PHPUnit (163 tests, 311 assertions) | | Coding standard | WordPress PHPCS | | License | GPL-2.0-or-later | diff --git a/brezngeo/assets/admin.js b/brezngeo/assets/admin.js index 7d6787b..ec05fb1 100644 --- a/brezngeo/assets/admin.js +++ b/brezngeo/assets/admin.js @@ -33,6 +33,68 @@ 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 $' + Number( inVal ).toFixed( 4 ) + + ' / 1M \u00b7 Output $' + Number( outVal ).toFixed( 4 ) + + ' / 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 = $( '' ) + .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 () { if ( ! confirm( brezngeoAdmin.resetConfirm ) ) return; $.post( brezngeoAdmin.ajaxUrl, { diff --git a/brezngeo/brezngeo.php b/brezngeo/brezngeo.php index 938b947..4d28209 100644 --- a/brezngeo/brezngeo.php +++ b/brezngeo/brezngeo.php @@ -3,7 +3,7 @@ * Plugin Name: BreznGEO * Plugin URI: https://brezngeo.com/ * Description: AI-powered meta descriptions, GEO structured data, and llms.txt for WordPress. - * Version: 1.2.2 + * Version: 1.3.0 * Requires at least: 6.0 * Requires PHP: 8.0 * Author: NoSchmarrn.dev @@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'BREZNGEO_VERSION', '1.2.2' ); +define( 'BREZNGEO_VERSION', '1.3.0' ); define( 'BREZNGEO_FILE', __FILE__ ); define( 'BREZNGEO_DIR', plugin_dir_path( __FILE__ ) ); define( 'BREZNGEO_URL', plugin_dir_url( __FILE__ ) ); diff --git a/brezngeo/includes/Admin/AdminMenu.php b/brezngeo/includes/Admin/AdminMenu.php index 335a0bd..be4a0c2 100644 --- a/brezngeo/includes/Admin/AdminMenu.php +++ b/brezngeo/includes/Admin/AdminMenu.php @@ -51,7 +51,7 @@ class AdminMenu { wp_safe_redirect( add_query_arg( array( - 'page' => 'brezngeo', + 'page' => 'brezngeo', 'brezngeo-saved' => '1', ), admin_url( 'admin.php' ) @@ -207,8 +207,8 @@ class AdminMenu { $provider = $prov_obj ? $prov_obj->getName() : $provider_key; } - $post_types = $settings['meta_post_types'] ?? array( 'post', 'page' ); - $meta_stats = $this->get_meta_stats( $post_types ); + $post_types = $settings['meta_post_types'] ?? array( 'post', 'page' ); + $meta_stats = $this->get_meta_stats( $post_types ); $brezngeo_compat = $this->get_compat_info(); $brezngeo_show_welcome = $this->should_show_welcome(); diff --git a/brezngeo/includes/Admin/ProviderPage.php b/brezngeo/includes/Admin/ProviderPage.php index 543679d..37b46cb 100644 --- a/brezngeo/includes/Admin/ProviderPage.php +++ b/brezngeo/includes/Admin/ProviderPage.php @@ -7,13 +7,15 @@ if ( ! defined( 'ABSPATH' ) ) { use BreznGEO\ProviderRegistry; use BreznGEO\Helpers\KeyVault; +use BreznGEO\Providers\OpenRouterProvider; class ProviderPage { private const PRICING_URLS = array( - 'openai' => 'https://openai.com/de-DE/api/pricing', - 'anthropic' => 'https://platform.claude.com/docs/en/about-claude/pricing', - 'gemini' => 'https://ai.google.dev/gemini-api/docs/pricing?hl=de', - 'grok' => 'https://docs.x.ai/developers/models', + 'openai' => 'https://openai.com/de-DE/api/pricing', + 'anthropic' => 'https://platform.claude.com/docs/en/about-claude/pricing', + 'gemini' => 'https://ai.google.dev/gemini-api/docs/pricing?hl=de', + 'grok' => 'https://docs.x.ai/developers/models', + 'openrouter' => 'https://openrouter.ai/models', ); public function register(): void { @@ -21,6 +23,7 @@ class ProviderPage { 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_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 { @@ -74,7 +77,13 @@ class ProviderPage { $clean['models'] = array(); foreach ( ( $input['models'] ?? array() ) as $provider_id => $model ) { - $clean['models'][ sanitize_key( $provider_id ) ] = sanitize_text_field( $model ); + $pid = sanitize_key( $provider_id ); + $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(); @@ -92,6 +101,18 @@ 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; } @@ -126,6 +147,59 @@ class ProviderPage { 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 { if ( ! current_user_can( 'manage_options' ) ) { return; diff --git a/brezngeo/includes/Admin/views/partials/openrouter-model-field.php b/brezngeo/includes/Admin/views/partials/openrouter-model-field.php new file mode 100644 index 0000000..d6fe1a9 --- /dev/null +++ b/brezngeo/includes/Admin/views/partials/openrouter-model-field.php @@ -0,0 +1,81 @@ +getModels(); +$or_saved_model = $settings['models']['openrouter'] ?? ''; +$or_is_custom = $or_saved_model !== '' && ! array_key_exists( $or_saved_model, $or_models ); +$or_cached_pricing = get_transient( \BreznGEO\Providers\OpenRouterProvider::MODELS_CACHE ); +$or_cache_is_array = is_array( $or_cached_pricing ); +$or_selected_pricing = ( $or_cache_is_array && isset( $or_cached_pricing[ $or_saved_model ] ) ) + ? $or_cached_pricing[ $or_saved_model ] + : null; +?> +

+ + + + + +
+ + +

+ + + +

+
+ + +

+ + + +

+ + +

+
+ + Input $ + / 1M · Output $ + / 1M + + + + + +
diff --git a/brezngeo/includes/Admin/views/provider.php b/brezngeo/includes/Admin/views/provider.php index 00583e9..de65d19 100644 --- a/brezngeo/includes/Admin/views/provider.php +++ b/brezngeo/includes/Admin/views/provider.php @@ -54,6 +54,9 @@ + + +

- + $pricing_url = $pricing_urls[ $id ] ?? ''; + if ( $pricing_url ) : + ?>

- +

- +

- getModels() as $model_id => $model_label ) : // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound - $saved_costs = $settings['costs'][ $id ][ $model_id ] ?? array(); - ?> + getModels() as $model_id => $model_label ) : // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + $saved_costs = $settings['costs'][ $id ][ $model_id ] ?? array(); + ?>
Input $ / 1M
- + + diff --git a/brezngeo/includes/Core.php b/brezngeo/includes/Core.php index b21a85d..c3262a3 100644 --- a/brezngeo/includes/Core.php +++ b/brezngeo/includes/Core.php @@ -28,6 +28,7 @@ class Core { require_once BREZNGEO_DIR . 'includes/Providers/AnthropicProvider.php'; require_once BREZNGEO_DIR . 'includes/Providers/GeminiProvider.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/TokenEstimator.php'; require_once BREZNGEO_DIR . 'includes/Helpers/FallbackMeta.php'; @@ -65,6 +66,7 @@ class Core { $registry->register( new Providers\AnthropicProvider() ); $registry->register( new Providers\GeminiProvider() ); $registry->register( new Providers\GrokProvider() ); + $registry->register( new Providers\OpenRouterProvider() ); ( new Features\MetaGenerator() )->register(); ( new Features\SchemaEnhancer() )->register(); diff --git a/brezngeo/includes/Providers/OpenRouterProvider.php b/brezngeo/includes/Providers/OpenRouterProvider.php new file mode 100644 index 0000000..46c2e3a --- /dev/null +++ b/brezngeo/includes/Providers/OpenRouterProvider.php @@ -0,0 +1,106 @@ + $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'] ?? '' ); + } +} diff --git a/brezngeo/languages/brezngeo-de_DE.mo b/brezngeo/languages/brezngeo-de_DE.mo index a9f2ba2..d3d7664 100644 Binary files a/brezngeo/languages/brezngeo-de_DE.mo and b/brezngeo/languages/brezngeo-de_DE.mo differ diff --git a/brezngeo/languages/brezngeo-de_DE.po b/brezngeo/languages/brezngeo-de_DE.po index f5cc8a7..5e85ba1 100644 --- a/brezngeo/languages/brezngeo-de_DE.po +++ b/brezngeo/languages/brezngeo-de_DE.po @@ -1617,3 +1617,51 @@ msgstr "Hell \xe2\x80\x94 klare Karte mit blauem Akzent. Dunkel \xe2\x80\x94 das #, php-format msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). Configure a provider \xe2\x86\x92" msgstr "Kein KI-Anbieter verbunden \xe2\x80\x94 Beschreibungen werden ohne KI aus dem Inhalt generiert (Fallback-Modus). Anbieter konfigurieren \xe2\x86\x92" + +#: 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." diff --git a/brezngeo/languages/brezngeo-en_US.mo b/brezngeo/languages/brezngeo-en_US.mo index d46bef1..c8ee909 100644 Binary files a/brezngeo/languages/brezngeo-en_US.mo and b/brezngeo/languages/brezngeo-en_US.mo differ diff --git a/brezngeo/languages/brezngeo-en_US.po b/brezngeo/languages/brezngeo-en_US.po index f51707d..80e2a72 100644 --- a/brezngeo/languages/brezngeo-en_US.po +++ b/brezngeo/languages/brezngeo-en_US.po @@ -1617,3 +1617,51 @@ msgstr "Light \xe2\x80\x94 clean card with a blue accent. Dark \xe2\x80\x94 same #, php-format msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). Configure a provider \xe2\x86\x92" msgstr "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). Configure a provider \xe2\x86\x92" + +#: 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." diff --git a/brezngeo/languages/brezngeo.pot b/brezngeo/languages/brezngeo.pot index 0d7c4f5..8d9d9ef 100644 --- a/brezngeo/languages/brezngeo.pot +++ b/brezngeo/languages/brezngeo.pot @@ -1617,3 +1617,51 @@ msgstr "" #, php-format msgid "No AI provider connected \xe2\x80\x94 descriptions will be generated from content without AI (fallback mode). Configure a provider \xe2\x86\x92" 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 "" diff --git a/brezngeo/readme.txt b/brezngeo/readme.txt index 4323b03..f038a72 100644 --- a/brezngeo/readme.txt +++ b/brezngeo/readme.txt @@ -3,7 +3,7 @@ Contributors: mifupadev Tags: seo, ai, meta description, schema, llms.txt Requires at least: 6.0 Tested up to: 6.9 -Stable tag: 1.2.2 +Stable tag: 1.3.0 Requires PHP: 8.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -69,12 +69,13 @@ Finds all published posts without a meta description (including descriptions set = Multi-Provider AI Support = -Choose from four AI providers and switch at any time without losing your settings: +Choose from five AI providers — or access 600+ models through a single OpenRouter API key. Switch at any time without losing your settings: * OpenAI (GPT-4.1, GPT-4o, GPT-4o mini, 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) * 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) = @@ -140,7 +141,7 @@ An API key is required for AI-generated meta descriptions. Without one, the plug = 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. +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". = Are my API keys stored securely? = @@ -222,8 +223,21 @@ No data is transmitted during normal page loads or to visitors. * Privacy policy: https://x.ai/privacy-policy * 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 == += 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. + = 1.2.2 = * i18n: Added explicit load_plugin_textdomain() call for reliable translation loading on ClassicPress and other WordPress derivatives.