diff --git a/README.de.md b/README.de.md
index 66f0b89..e4fe721 100644
--- a/README.de.md
+++ b/README.de.md
@@ -300,10 +300,11 @@ Kein `openssl_*` oder externe Extension nötig — läuft auf jeder PHP 8.0+ Ins
**Sicherheitsgrenzen:** XOR mit statischem Salt ist Verschleierung, keine kryptografische Verschlüsselung. Für maximale Sicherheit können Keys als `wp-config.php`-Konstanten definiert werden:
```php
-define( 'BREZNGEO_OPENAI_KEY', 'sk-...' );
-define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
-define( 'BREZNGEO_GEMINI_KEY', 'AI...' );
-define( 'BREZNGEO_GROK_KEY', 'xai-...' );
+define( 'BREZNGEO_OPENAI_KEY', 'sk-...' );
+define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
+define( 'BREZNGEO_GEMINI_KEY', 'AI...' );
+define( 'BREZNGEO_GROK_KEY', 'xai-...' );
+define( 'BREZNGEO_OPENROUTER_KEY', 'sk-or-...' );
```
### CSRF-Schutz und Capability Checks
diff --git a/README.md b/README.md
index 7edf5a4..156eea8 100644
--- a/README.md
+++ b/README.md
@@ -409,10 +409,11 @@ Plaintext key → XOR(key, sha256(AUTH_KEY . SECURE_AUTH_KEY)) → base64
**Security boundary:** XOR with a static salt is obfuscation, not cryptographic encryption. An attacker with access to **both** the database **and** `wp-config.php` can reconstruct the key. For maximum security, keys can be defined as `wp-config.php` constants — these take precedence over the database version:
```php
-define( 'BREZNGEO_OPENAI_KEY', 'sk-...' );
-define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
-define( 'BREZNGEO_GEMINI_KEY', 'AI...' );
-define( 'BREZNGEO_GROK_KEY', 'xai-...' );
+define( 'BREZNGEO_OPENAI_KEY', 'sk-...' );
+define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
+define( 'BREZNGEO_GEMINI_KEY', 'AI...' );
+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).
diff --git a/brezngeo/includes/Admin/ProviderPage.php b/brezngeo/includes/Admin/ProviderPage.php
index 37b46cb..568b516 100644
--- a/brezngeo/includes/Admin/ProviderPage.php
+++ b/brezngeo/includes/Admin/ProviderPage.php
@@ -74,6 +74,13 @@ class ProviderPage {
$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();
foreach ( ( $input['models'] ?? array() ) as $provider_id => $model ) {
diff --git a/brezngeo/includes/Admin/SettingsPage.php b/brezngeo/includes/Admin/SettingsPage.php
index 68eeed9..ea0da93 100644
--- a/brezngeo/includes/Admin/SettingsPage.php
+++ b/brezngeo/includes/Admin/SettingsPage.php
@@ -23,6 +23,12 @@ class SettingsPage {
*/
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.
* Called by MetaGenerator, SchemaEnhancer, BulkPage, and admin pages.
@@ -62,9 +68,37 @@ class SettingsPage {
$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;
}
+ /**
+ * 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 {
$locale = get_locale();
$is_german = str_starts_with( $locale, 'de_' );
diff --git a/brezngeo/includes/Admin/views/partials/openrouter-model-field.php b/brezngeo/includes/Admin/views/partials/openrouter-model-field.php
index d6fe1a9..5ef5e5e 100644
--- a/brezngeo/includes/Admin/views/partials/openrouter-model-field.php
+++ b/brezngeo/includes/Admin/views/partials/openrouter-model-field.php
@@ -9,31 +9,31 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
-$or_models = $provider->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 ]
+$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;
?>
@@ -42,14 +42,14 @@ $or_selected_pricing = ( $or_cache_is_array && isset( $or_cached_pricing[ $or_sa
-