From 5139e5ad294cace159d26f03523eb7bae1d230d8 Mon Sep 17 00:00:00 2001 From: noschmarrn Date: Fri, 17 Apr 2026 17:19:51 +0000 Subject: [PATCH] release: v1.3.0 --- README.de.md | 25 +++-- README.md | 27 +++-- brezngeo/assets/admin.js | 62 ++++++++++ brezngeo/brezngeo.php | 4 +- brezngeo/includes/Admin/AdminMenu.php | 6 +- brezngeo/includes/Admin/ProviderPage.php | 84 +++++++++++++- .../views/partials/openrouter-model-field.php | 81 +++++++++++++ brezngeo/includes/Admin/views/provider.php | 28 +++-- brezngeo/includes/Core.php | 2 + .../includes/Providers/OpenRouterProvider.php | 106 ++++++++++++++++++ brezngeo/languages/brezngeo-de_DE.mo | Bin 31913 -> 33047 bytes brezngeo/languages/brezngeo-de_DE.po | 48 ++++++++ brezngeo/languages/brezngeo-en_US.mo | Bin 30656 -> 31748 bytes brezngeo/languages/brezngeo-en_US.po | 48 ++++++++ brezngeo/languages/brezngeo.pot | 48 ++++++++ brezngeo/readme.txt | 20 +++- 16 files changed, 543 insertions(+), 46 deletions(-) create mode 100644 brezngeo/includes/Admin/views/partials/openrouter-model-field.php create mode 100644 brezngeo/includes/Providers/OpenRouterProvider.php 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 a9f2ba279f68e3b9c84cb3cfa2b72a1c6da48e97..d3d766427fd2f1ef21ba8e3ec45040f7d94e11da 100644 GIT binary patch delta 9808 zcmaLc349bq+Q;z@goJP;A>0s16OI76xX*wfM<65-!VMb9F@ca_CQdSj(*Z;TMXm@4 zsO*c1AhNJVK?OklkX|2GSn9vOt$EFa=ip5h6EUSGj%Uab^t(Mgy-m+R_OH9Vz7>NZKg+6SDOOb!9 z$N8atj$wT~hfVQYjKHw=PJ1*qv8;dF-M<>^;Z_X89jJls!DxI5HLw#H$Mdbz6q0DTg0(TBqh+PCV+URpy*1si6=tFaFw@k%sEOPcpr8kA zLv6Z!r~$oYE;x&gss9TbVqIPhb=VA*x$daz`=K&73e`_G*26ibeJ*MMccC({9F>8< zLliXAKcd$70BU9jF%@4!J@9+Xz-Ss9;B<_{Sr~;Cs0`hUx_^tQKaSeO2QU-gL){hCc2 z$B$9{G-czo()-_*f=1LIZ5)Z(G&4~HSYqlAq2AwTPy_k^qp{j}3AN^-J)HsAs1$cY z^*0jrrJR8_F2&Az{~t3A$5HR)H>eRu^G-I$WK;(uP`mtgBpFr!mD+vA6WEdZ1=K)e zdOP2RzNmraqV~*u)Y2`%fYxd)g|4^--S`@2VUIq}rdo_oQeTcrT?*@@nfJxurb7)h z8#RF<bjRvOY{yl#ZOTgywsQcYpt%*&kHJgoW2YqEP+WsQc3TlYgZmn}+6i8)~=vaV4(BG;B4%vTnl3 zI2bqKKztutVGI=wC>iydWuU$jIjEFR#z!$1HPByC1B(gJyOzL4b{sJGxAerQRKVkbO~n&~Cf3_}^M9@q?%FcFo>DW*LzpF&F-)}UUWU8psB z2Q`E9sE#h69&i=&u_NhFeFbXC_9Dr$4w?F8)Wm8Jaqe%3+MIFN0!JX330S!l)PW0A z(TAGhc2p+zp=NLlWAPJH{|Pno$f3@Inxi&VJJcRYLjG03N<-Z@1LJTJ>b^%XO7H&; z3R<)M$Ud|V<0QO{8fn(e<^zL_*z%#;=b>h}-kg6JHSnFNe)c2#&-#aHPZ;Lh*BjMO zHpcOMYa#``9)9Bz976p8)QnD`*6t%zCc=h0pW4=_j#5w=?2VfF&6tQesLgp7Dg&EQ zn|wb8`^7*g4Ifd^$j_isegQSZ+9R9?#G(e+9(7|XYR?QnF10409=sW2a2ION95S9T zeuhf@Mbv%j}kyTL6+V65(_XX!|8afaVvJA zeFui)+gKMrH0M9Z1nL)1OWI_NleuIJqaGMdVG)I~s7#!|Zg>VGFe;Ocge_5f!;QM} zZnW_})Pr}M_9|>Z{YBIRkD-?4Gt|JYVQs9N6|4uW1{BnBQ{KM6g8mtP}iM9UH2^};Z^K|i4&Y}d_Fd&z5-+LVN}NU zqEi1CF8rJjrJ@gOR7XYnw$o#gyQ^C{j$y~kwdJ|Cu1UxXUq z9@KzdK<$xlP5Td+PrddOXO9(4A^)1;Y#KDfg{TzYkJ_#4P;bQ+WI3&;P&4@%HNcpu z&QkP2-Jg!y6O&B)3{*dN;9Q)K8qg)wQd|j8Xi6b`n)Ca-4W?6{jy67qTB}#FCBB8) zj9;N<8a~}gabwiLI-v$U2nS*oCgMuez@9@*_y}r=0`E}Jh`ulvTtIbj8M82q_0UKs zU@OeS7Pt^KunngD52yhi#3uM2DnnnR9u#?-^UEh5RUhEg16DQ#ZK5L7`(9}-*oxX@ zd$9|?gxXB!QD4j}r~zC@1&rPYv1n7 zycz1jov=CfLUoXdTB=ECqZ`NL1E|;WbJTqoQJJ%5I7^w0%FIU8%=e)N_^J$0c$dOx ztVX@RNqNpdvQhP1)D4RFrYPB zOF?V=EGiSPp!UEi)Pv5U25=2^V`PEzK}o=P>gmSW*p2!F*d7mKC#*&-VML+RUr%gH zJ-d+nYlL%X=!PL#mzf$Fz>8#-$Scm#jjKvkG2R?xV@FeQ>X*kPS>o`X&0{+=`v?K!Ac${uwIOmrxx>7CSe@8~dU*Vb~vf{C?B^ z8tQ@7xCO7`VB9>L!D8S;3PUKgDsetQ(@`VejJmN3!|^RtWn93&t$KKz6pz9N)bGYbT#Nd$9YUq>T~vo( zqL#$+I%_=;wK)q=duSnQV2`3E@;qt+r%?Tz#uUB(|D-U3hPI{7-+BRzq<#no;+v=& z!r4ht7>i1M5_Z7B7<@1)QwvcOScXZs&eRX12J|MXzb~+z-v28UG{ab*^UIYHlVKiPsZJN+>$6=^7UW$5;SE2gZjrtxugZebThq~@F9FAY3`c3sa{S5Y#f2Cv* z4cY^nP&e$sCU^j2@HlqEv#3oKeWx??UZ~9FpdMI^y1yKivAfX5ji`YgKrPK_)b;1@ zB>yU0q(KAu6?3q8g|o)9Q6pZ6jq!eLirY}H-E*i1oO5^HM9NL3{Rsv_ztyJQS+RPv_{ptVIxd4^=xcTeVS?ap&qmh({Kaw zk9CqCqcC&6^EL!FP{^iXD~`vn(T#%_I2Y_j4e(E>2S1M**sG|FRHJtNWz>wWVkkx| zbe1j>m9ciHCGCq@n2F3SU~QqG8yhcjY>Qo}cR_VL9<`gNquz!>)L!smYkUlq+9Rmz z&m!5k&Z9DL`(4g!T#D-FX`F!1VGF(g^$4A4k9xiI--OlCI8=(KqV_-uw#4}uhYw=# z?Lp1>psBx%TEb&E7SEXTsf(R{d!aIthV?mTqVCKt1rM>n*t1l;?0_Br%QXP3ZWUXhQj1 zu=d-Js}!IAt!m9S<%7nH#zJgIOreh+rcYmp^3TzeSWM&n#L{33Go$=8Wv#KYQ{UXA zJ5~|vh{*&Wx8PsK>A2in(-7NIztz-paRha3@Sc=2iH9hk(DR?8kU$(JbhO}PcVu7Q zIM}Jy8rr`wEz2nDSb&+xXCrvb;^$W40`XtO?ZkMl>4%>ZJqQ=ki?*r6!4c8kFz_;k zw~0ljF#|hL&%v%lJ>o;^Ls7?{iSLMaRN?T`J`U5&`8O%&5HAq(h<^~H3B8hgh-HM1 zPF#PF*8iAOw`}}_DbF#kG@af;{WYQw_0~jpBA>XKwkmV&b^21cmvck$LA;9?OZkww zU?Fw=l1sp+wEi{)KMn5^cT(2zJ`qa!E1W_MqdbOqkMbm90_Ac-$4;W9DgQOt;{5na zjq*OqADemuuKkI!zNUS&{`^)99*g;@FVh<0GSPr&Ow=N7;ik_~hnw;fPBHkiBlS(D z9)U5`yWlFkgD9iC!<@g)A1UZ_9Pn^~j?+X_%JYeh#98XYh!2Q1L@U~U!B>duhl@5H z_Y%JbOU^gn6s=RHkZ7*c=X)LHH*g7YtG@r&kAa-XCWe@X+LQ+qI_|^i#Dh*X_;ao~ zr^;l?1Bhp9oL6y+se6qRxn_2a`#gGnDkqa^)F*Wm77=xco2b7^tfj2~JAVDR!xYxj zwwCh!L?q?&csEf@c?J$5I#3=>G$v{uW2vXAP4oY!IkOx)6OBxLfH9g2vxv^baoY5| zU>xN(L^DE1hJ$qpZ#Hei@n6&nOg(^=)PE!v5Q~VwQgbqzN?*#?@cPk+`aM*Zm}}Ht zhw?0<)LisBHYGkI+(bXl?ZP4W2+l+uM;wB`Ionh2Mda!IPvL?Tb87n+;n zX=_3BC$5A{=zY%AMY(htK&V7wxn2*DW7m0`v^52qzjv1V+=M;ng z^Vy<4n0f`)BFczn)Hj-IqNwXwNgNhjPuMJ?#(Bu!83Yk4ZE{ zQVPdpj3s^`atR$-#5Y8VDjXXef`297Mmf>cU&2cI@DkmKSmJxyMiEENH4}~hgHy-w zquMld!kf5ZHx`*TG1HVk!D*%}{+rlOJW3=HeDivU+(hRbG(HvcbQ$ie0`<2%c<1(OmAMHX^gkay>^kSykM4H>MJfN_RO@4eBL=V z?oO#n>zW$Uewe?koc3T>_UPdQtlu?MFWyl#JMnDY&UrqMJ4BE=}nj3AOtK9GNaC!c` z8jq^lmNvR}AUK`Qd42|-S6*B|R_6VFc1d04Er(WF89p>DUU#-x(7w2n08dCFX~T%O>9 z-aYA?es-R}$d12$)=l<2@kzFSjy=}rDrP&}>Gjm;#-8tIf!r=nO4aPiu8@u+if6h! zF59_1Sxfu-7Om}xDHkIW#k z#8rIbK`wiyD>%|%I{%Le*dEc%<2_lwQNGLLXSCc(VvCtaW$c2kar0B` OoBzMvF=9bs$iD$Y-9bwL delta 8672 zcmZA630xOt9>?*AL*+&hQRMU>Q1Dg+OhQHR0P!Fd%_H%~0~HTY8Y|7Sa2ar*O)_1RBKF5Gh27c95*8WnJ4(s91o$|`4NLLgvv(P z1Ou@xsy+iFjd4t_ttdvGZ6;z{oP%RL*4&1@}IfLk1iOU#7yygQ%Is4r=xDH!YI5G<8TXR-~mj=E2yb#!L;!4qXyHtVbXoP7boiPLJ;tW*J%W*I+M$OD&)Y>1job@1sHuJ%&^9DO-+e?JhwqvROi$$paxKbTAFw5`L9tOyoy~hG?n?+Ztb1wj$l0cQ!d3oEJO9Y67z5+ z2ID)Zx8f6QikDFjjASFJ9E<93SJVLWQG04MssoGd`85tnLn=07C_am7;1$#qeTur_ z3~GjcK{a#*gRm*@r0SDU9Y{ruxD#qddZU(R6lw`4pawPt+o3auL=Suf`{P~=!2sSt zjl2=611(XnUuRSUdA2+lwMi#nKHi49{|JWSNgRM@QB$7IQ#Io=k-g!V)g&6pBdDG} zjoooKYNQu27#p^CcYgwgQ_e%p+;CKbrI?8;Q0*K*4eV`HhrYr@ynxyxksZ_l*1s)@ zDte>d&#|ZuEyenHxAkGvn(sh$pawO?pP=r)fPC~!C_6L}+aWK48Dz^9$hXXFM0I#C zHrD(95s4c38MUiJIvGPJO&V%yM_U(R66I=CNB1MIxH*mLSTOIf_DnK{VO!Kvb;s$L zhXuF`wP$?!3OQRz0!TDv&!9&90_uXdurVG*_52%5$6$Wwx}MkwhoWX|Dr#oRF$$NW zrv3qp#BHdK?nTYihw03}Zn#2)rX-jh(g0IXH)LT$EVP!OHq`=D2bQ54T#veMCu-!c zVq-jx+5_L=O03Ig@OoT}H(*T$^Pf!;nrTco%tzg@0@a~MQLoiYsPDo-)D+j?COm=a z=-e!~V=GWoz8-b|!x)8oaRQ#eyD*c7X$cQHBpT5<)ULmRT8d1%skQBi8tGKj2rE$! zTw~pg(Uf;%G9E=O$t6^W!)R3ZHAJ6I92}puQKsqdFLr8Up?zV zqBSnEPO>gUP4z0&60Aer_zdcSFCpI^^C8AzD6Q-DOGQ063)P|C7=!t!=S;zPtjuNp zwRY81XqRtBm3N{>xCgan2T?O~4Ap^is0Um{KAI+=H@{G@8|L8?SO5?~8=__;9yNs>Q0Keb^SS6xxe(RCBGib+qpmMUCc`Yj5_}ca zZc1O*PS}caHrB%_4oLt>8S26cOva_CDSiqyb+4g1atf>P8`Mlp>gWEhn2(yNtvCdC zp!SA;fA_u^Or+c#_26EpP3R0D(St@{0M14~thD7@Pz^1|0l3MYKZ|PU2h>dbit2zr z7int4P#um$Jtzh>Qz@wSvXHmVF@s6?y<%44oj$yFs1X+wxc}goirO0+Pz`KDy@q=* z5kEq0zAG4t(F5HFB%?Z%jltL#^%fLi7|z5Hz5i7te4NZGEXOx76|)Q7y)X@1Q?5e# zX0~A%UPLwQH^?;{HS!o#hnt}~+!6IlEEk*M2;?hoD$!r>|85eE=tb02eT+f)4XS|) zs0Y;KmVcI^PA=p*+-e!%#CZ9@DT4)A2z}!V{)=2{*bOO1+W!*CrWH zg-%Sx@i-5)2|vIFcoH?jA5c?#6}4IYhPiJ+2r@|~7B!F=SRa?7?z<1Q7q-~??Wp_r z4`copk{qN$9qBRLU4p*Yi1Lk?hozW^9@L0Vpf=NIsLgj7HGsw=+&$9*qbR3iI2NGR zd^DzFHL63eIV5`U5nJ&d>W0se4Q7;9JxKFJdAFj&i5G zHEIbnQ6ue+9WW0yfC}Um$J|4rhR>l!t{tWa*GKg*9@RiPYDsc15r-lB%`8Bz_1mcX zPNHV+ENba8M!Pe!1T_;IQF~&$SJrXgPQ02a;8>XSR=h&3;J=hJO zM$O!}_WVz%y%03kwugFf25P4AQSID}EgX_c60Pwj)J!~ynu&d=2OUOr;9JyB$8cwg+w%g{ zjgwJRcstg?)wcXM+(x+?qp{~yw_`V9Amvijn$JUZyc&6i*@5dYdYb#k?bE2Y=quFB zI{zWj6m~6jzf8HPFIO3AMwX)*{5xv%?ZReQgX+kSsE!9tcRQSfx-S*mVh)bOS$I7j zMeUi;8Q!_S=m6A! zW?%p=#bCS>y?_5#lW2rnQ4idYdf;h{!>bsEF|*wI)~JShpSnVbpv2GOB|oQJd&D)YNw^ zcc*kXssmF{GdCACuu9Z`Hlgl+7PVBzF%my3XZ}@jkqY%t|2j}P0yW}zRDC+u$G)fz z6k`Rh$G%v9zPtO2aUkWfI25-d8{YUXaL>;|b#NZ)Ig1?<^=t(;#K%y(d=F|w2hbmD zFanQZBRqo|!4)jP`U~AZWX7QGd*Av6wxWCv)ox>Yt<9T&q3EQLXfvdv_XC8Q+S^e# zJc>NSJc*it7M1RAz0RnH%CHD4un8Wv^%%q%&U2rb?@cjQf zVtm|U2~7CYnUUmsiQ~lggr?l#T3%uA?0`9`9~@U*uKjFFc9_VuHEG=U19?yUkDgyl zqW8D9%~T`bX@m`~_fMZ8M9Pb}fO+le4z zhP`Ji^<&95>G>Z~NTINd*hZdfFS?a{3{iV@A&IXoSTneG2YCzP4qJCKb(4sjDeJ>D zl>7_)i5N)U33W6l*S|l##~ywbP|=&1L_Q7mc?`pPSb?*N>&ZVM{vbLKZK-QS%q0GL z{EdoG>i>;viD<%~G9N@UjR+^7=>3S0R8z5rm_fWqY$P5eYL6`>w-DpFb_`KUevueR zG^CtL^d}!dB#^hk+9Qi{1%;Nj;vMU2=)~EA$g+7kULZ0Ee_JP#h|x5rqa$9yM2sRP zl7B@6k}oFoYdjKtiO0!xBofzA)-lz^@%~-j&n=i$oD3rWnJ6M3K{O!d5{Iapil;D# zs69GS*N&J#{Q3Bl!UIGb>SORI(Svx5h`V+z>mNx)?eRRx8$>hWVd4%hoNq6Fo$@}S zC!wR?wMN{se%9D@G7kFF`P<05QWuMji7I>Uy!PL(_NJ9oerR(sfM{>ayYOD374aMq z!nJqeXT-;Zj%4C3;$K7v(S>Us!tI2P!IYnLQ`3!Hhn{bi5}8zVwv``HK1~D=I(pdp z_BezXMBOvqGynV1-CnZ@7gL>M%e~2c$ODM+woZ4=A@8Q=zfGZ>!UkMOq!AI6&)|*t zE$Zk+9`2?lp1hFwk@7O)pTr8vI-*>>|7WDqgT!<~BYlcU;<|KVKY4^|o06O(h7i9K zWyE_#W8yACM;+VvZ*I~1zu`Pf{;tju$@aX5yg&J^LiO+ulI|{u^ 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 d46bef1323ddfa06fcf9f9396758a356381430fc..c8ee909c315a056a9c4091f355ba199ac73f9396 100644 GIT binary patch delta 9735 zcmeI%iGNR5{>Sl~W+8+`NJ4~sZCQvc_9anji3A}*)Dp5sD)}a|h^@Yq*rr9PwH@Y% zYH3Z4v~)_#jH;SJnJ#DzEm}IG+h{xORGHWNdyh_j^C$cszx(JX&vVYb_uO+n=iHm7 zJ^Gc$k{>)=mjk`lTO1obEGrD(^RuiD4_MaYt<-8+E!$gGYixyGu_rdgJoLv3Y=@5_ z|5!Wup?*$aW4w+*_%-^XPY1g_09#m=%L+3M3COdpB#gyWoQiWW0xw`7-ot1N>}Xjj zn1~HVzqQu!06qBo5ctufdQYj8X6!Erb$%D#U)dQ<-hSqw(K?PzF|^I^2fE z_^he>cd?fu2-!4NJ5&mLp=Mly;kXUmsYY$OQ>cA>6*ZwdsHO5^y>&h~hWu;9F*NkX zMAYuBLe1z&)WEi&Hs1^Ag>RrQ9z)IOG$!LEY>XXwRrJ=xV%TN

7t+q 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 delta 8674 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

D7pHNf{!=j}(WkrUVn zub^(+^igME9Z-+B@1xXT$w=EV6;MO{A|YvVN3qg#cQv2YyqS4R;Yn&G>sXLbNpnG>i1Tt(gBPt?B$D&;zVaNLh6 zv|q>acpa1RHul1{dCngk%TfC)j&}y=i@IKbi$u>V3{}FW_JAaNKr-qPq@xDd7pq_< z>iCJ+5g$XoIc6W~!qF2P<1w0cThv6eQ5DX$`(61Yk#x*Om3R}XbRVHg`3=s%?@^T) zInnuDF&R~*UmV{g!zTx&;Z`l_}Uk@#!-6Vd*hr|KGotRAj z9AYr}BI0eLGwl@ODe?!23giQbQRL%rK5Bc2Tw6=)H1fxZeA>~r?>Oy=1oL;cBV~zI zU$22SA7xL{>$Hgce!|`MJx8BD)pdzUgtmpmUg8sC7RNnKcoAdmIUDF7OwK=OT;>do zC>nE#SI9frgB~LvM3imKN%RfBcWcG5Tj;1q%(FX&(l>$_N;`w-L;el^O!OpghT7_q z_br?MJ9MPdkxYyrAC3CR_+dqyin+x7F}lhH(W;4AUtU2 z<7mR4e0b?cgydN|787HMcZpTR^F-P9BFRi*2*(a0#*p72dJ@%W#}M7f`w?N}4Y6!X zq&<~JecN%&x({8UwjmO2J^`;2tq2d>CnAV}TufV2yo2EwL<}dtKvW{1PUzDdfaQo+ z$+d+O4QOl2c5s#c#nRPjn1$^0BL9WRAn!+1A;uGj>C48iu^myiHKQ+%$Rh6D&e2#+ zG^D>49wpinFB74EAItg=prdU2FUiM59bz3Zj{_&!gFm9Zi|9aT>-zUaoVI=z+jJzR z-?M)fsF7(^4v2Aa|Cy8j{Ey9Omm*AJg=Y+OM;vn%J zq2GYbIc6=sL1^nm`*kNZt;x0N{$>u5Ku5go{FL^0geRe`z3p#|y@_=CHka=C=SLfR z%ru-%cRSloCU+zEB!<~Oos~!4TK7LhV*-t*aS9PjRHc0$`{MVgts}X=lbYJ(X~Z?! zbBQ;J1+=vVIh6jt0j2wi96~eQNJMa43*vq9s_Lyta)szk{7H->P7}ezQbJpKd-30$ zX6Zklx09dJJ|fcYFCyConfigure 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.