Skip to content

Commit debafc1

Browse files
authored
Merge pull request #99 from tarosky/feature/configurable-ai-model-temperature
AI Overview: モデル選択UIとtemperature設定を追加(複数コネクタ対応・temperature 400修正)
2 parents 6fdecbc + e90a4d7 commit debafc1

8 files changed

Lines changed: 399 additions & 7 deletions

File tree

.wp-env.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"phpVersion": "7.4",
33
"plugins": [
44
".",
5-
"https://downloads.wordpress.org/plugin/query-monitor.latest-stable.zip"
5+
"https://downloads.wordpress.org/plugin/query-monitor.latest-stable.zip",
6+
"https://downloads.wordpress.org/plugin/ai-provider-for-google.latest-stable.zip",
7+
"https://downloads.wordpress.org/plugin/ai-provider-for-anthropic.latest-stable.zip"
68
],
79
"themes": [
810
"https://downloads.wordpress.org/theme/twentytwentyone.latest-stable.zip"

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Contributors: tarosky, hametuha, Takahashi_Fumiki
44
Tags: faq,help
55
Tested up to: 7.0
6-
Stable Tag: 2.3.1
6+
Stable Tag: 2.4.0
77
License: GPL 3.0 or later
88
License URI: https://www.gnu.org/licenses/gpl-3.0.html
99

@@ -111,6 +111,14 @@ You can contribute to our github repo. Any [issues](https://github.com/tarosky/h
111111

112112
## Changelog
113113

114+
For full release notes of each version, see [GitHub Releases](https://github.com/tarosky/hamelp/releases).
115+
116+
### 2.4.0
117+
118+
- Add an **AI Model** setting to pin the provider/model used for AI Overview. Only configured connectors (WordPress 7.0 Settings → Connectors) are listed, and if the chosen model later becomes unavailable it falls back to auto-selection instead of failing.
119+
- Change the default **temperature** to omitted. Some models (e.g. Claude Opus/Sonnet) reject a temperature and returned a 400 error when auto-selected; omitting is safe for every model. Set a value on the settings page only if you need it.
120+
- Add `hamelp_ai_model` and `hamelp_ai_temperature` filters to override the model and temperature from code.
121+
114122
### 2.3.1
115123

116124
- User can change the reference prefix (Ref. 1).

app/Hametuha/Hamelp/Hooks/Settings.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace Hametuha\Hamelp\Hooks;
99

1010
use Hametuha\Hamelp\Pattern\Singleton;
11+
use Hametuha\Hamelp\Services\AiModelResolver;
1112
use Hametuha\Hamelp\Services\FaqCatalogBuilder;
1213

1314
/**
@@ -223,6 +224,52 @@ public function register_settings() {
223224
]
224225
);
225226

227+
// Preferred AI model (auto-select by default; validated against the live registry).
228+
register_setting(
229+
self::OPTION_GROUP,
230+
AiModelResolver::OPTION_MODEL,
231+
[
232+
'type' => 'string',
233+
'sanitize_callback' => [ $this, 'sanitize_model' ],
234+
'default' => '',
235+
]
236+
);
237+
238+
add_settings_field(
239+
AiModelResolver::OPTION_MODEL,
240+
__( 'AI Model', 'hamelp' ),
241+
[ $this, 'render_model_select' ],
242+
self::PAGE_SLUG,
243+
'hamelp_ai_section',
244+
[
245+
'option_name' => AiModelResolver::OPTION_MODEL,
246+
]
247+
);
248+
249+
// Sampling temperature (blank = omit, which is the default and safe for all models).
250+
register_setting(
251+
self::OPTION_GROUP,
252+
AiModelResolver::OPTION_TEMPERATURE,
253+
[
254+
'type' => 'string',
255+
'sanitize_callback' => [ $this, 'sanitize_temperature' ],
256+
'default' => '',
257+
]
258+
);
259+
260+
add_settings_field(
261+
AiModelResolver::OPTION_TEMPERATURE,
262+
__( 'Temperature', 'hamelp' ),
263+
[ $this, 'render_text' ],
264+
self::PAGE_SLUG,
265+
'hamelp_ai_section',
266+
[
267+
'option_name' => AiModelResolver::OPTION_TEMPERATURE,
268+
'default' => '',
269+
'description' => __( 'Sampling temperature between 0 and 2 (e.g. 0.3). Leave blank (the default) to omit it entirely — omitting is safe for every model, whereas some models (e.g. Claude Opus) return an error if a temperature is supplied.', 'hamelp' ),
270+
]
271+
);
272+
226273
// Citation reference label (customizable, translatable).
227274
register_setting(
228275
self::OPTION_GROUP,
@@ -555,6 +602,91 @@ public function sanitize_mode( $value ) {
555602
return in_array( $value, $allowed, true ) ? $value : 'conversation';
556603
}
557604

605+
/**
606+
* Render the AI model select field.
607+
*
608+
* Choices are built from the live registry so only currently configured
609+
* providers/models appear. Warns when a previously saved model is no longer
610+
* available (e.g. its connector was disabled outside Hamelp).
611+
*
612+
* @param array $args Field arguments (option_name).
613+
*/
614+
public function render_model_select( array $args ) {
615+
if ( ! AiModelResolver::is_ai_available() ) {
616+
printf(
617+
'<p class="description">%s</p>',
618+
esc_html__( 'No AI provider is available. Connect one in Settings → Connectors, then reload this page.', 'hamelp' )
619+
);
620+
return;
621+
}
622+
623+
$value = (string) get_option( $args['option_name'], '' );
624+
$choices = [ '' => __( 'Auto (recommended)', 'hamelp' ) ] + AiModelResolver::get_available_models();
625+
626+
printf( '<select name="%1$s" id="%1$s">', esc_attr( $args['option_name'] ) );
627+
foreach ( $choices as $key => $label ) {
628+
printf(
629+
'<option value="%s" %s>%s</option>',
630+
esc_attr( $key ),
631+
selected( $value, $key, false ),
632+
esc_html( $label )
633+
);
634+
}
635+
echo '</select>';
636+
637+
if ( AiModelResolver::is_stored_model_stale() ) {
638+
printf(
639+
'<p class="description" style="color:#b32d2e;">%s</p>',
640+
esc_html__( 'The previously selected model is no longer available (its connector may have been disabled). Auto-selection is being used until you choose an available model.', 'hamelp' )
641+
);
642+
}
643+
644+
printf(
645+
'<p class="description">%s</p>',
646+
esc_html__( 'Pin the provider/model used for AI Overview. Only configured connectors are listed. If the chosen model becomes unavailable, Hamelp falls back to auto-selection instead of failing.', 'hamelp' )
647+
);
648+
}
649+
650+
/**
651+
* Sanitize the AI model option.
652+
*
653+
* Accepts only the empty string (auto) or a currently available
654+
* `provider_id|model_id` value; anything else resets to auto.
655+
*
656+
* @param string $value Submitted value.
657+
* @return string A valid model key, or empty string for auto-select.
658+
*/
659+
public function sanitize_model( $value ) {
660+
$value = (string) $value;
661+
if ( '' === $value ) {
662+
return '';
663+
}
664+
$available = AiModelResolver::get_available_models();
665+
return isset( $available[ $value ] ) ? $value : '';
666+
}
667+
668+
/**
669+
* Sanitize the temperature option.
670+
*
671+
* Empty string is preserved (omit the parameter). A numeric value is clamped
672+
* to the 0–2 range. Any other input falls back to the default.
673+
*
674+
* @param string $value Submitted value.
675+
* @return string Sanitized temperature, or empty string to omit it.
676+
*/
677+
public function sanitize_temperature( $value ) {
678+
$value = trim( (string) $value );
679+
if ( '' === $value ) {
680+
return '';
681+
}
682+
if ( ! is_numeric( $value ) ) {
683+
return '0.3';
684+
}
685+
$float = (float) $value;
686+
$float = max( 0.0, min( 2.0, $float ) );
687+
return (string) $float;
688+
}
689+
558690
/**
559691
* Render a checkbox field.
560692
*
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
/**
3+
* AI model resolution and enumeration.
4+
*
5+
* @package hamelp
6+
*/
7+
8+
namespace Hametuha\Hamelp\Services;
9+
10+
use WordPress\AiClient\AiClient;
11+
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
12+
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
13+
14+
/**
15+
* Bridges the WordPress 7.0 AI client registry with Hamelp settings.
16+
*
17+
* Everything here reads the *live* registry so that connectors toggled on or
18+
* off outside Hamelp (Settings → Connectors) are always reflected. A stored
19+
* model selection is never trusted blindly: it is validated against the
20+
* currently configured providers on every request, and a selection that no
21+
* longer exists silently falls back to auto-selection rather than pinning a
22+
* dead model endpoint.
23+
*/
24+
class AiModelResolver {
25+
26+
/**
27+
* Option holding the preferred model, formatted `provider_id|model_id`.
28+
*
29+
* Empty string means "auto-select".
30+
*
31+
* @var string
32+
*/
33+
const OPTION_MODEL = 'hamelp_ai_model';
34+
35+
/**
36+
* Option holding the sampling temperature.
37+
*
38+
* Empty string means "omit the parameter" and is the default, since omitting
39+
* is safe for every model while some (e.g. Claude Opus) reject a temperature.
40+
*
41+
* @var string
42+
*/
43+
const OPTION_TEMPERATURE = 'hamelp_ai_temperature';
44+
45+
/**
46+
* Separator between provider id and model id in the stored option value.
47+
*
48+
* @var string
49+
*/
50+
const SEPARATOR = '|';
51+
52+
/**
53+
* Whether the WordPress AI client is available in this environment.
54+
*
55+
* @return bool
56+
*/
57+
public static function is_ai_available(): bool {
58+
return function_exists( 'wp_ai_client_prompt' )
59+
&& function_exists( 'wp_supports_ai' )
60+
&& wp_supports_ai()
61+
&& class_exists( '\WordPress\AiClient\AiClient' );
62+
}
63+
64+
/**
65+
* Enumerate configured providers/models capable of text generation.
66+
*
67+
* Only providers that are currently configured (connector enabled and
68+
* authenticated) are returned, so the list mirrors the live registry.
69+
*
70+
* @return array<string, string> Map of `provider_id|model_id` => `Provider / Model` label.
71+
*/
72+
public static function get_available_models(): array {
73+
if ( ! self::is_ai_available() ) {
74+
return [];
75+
}
76+
77+
$models = [];
78+
try {
79+
$registry = AiClient::defaultRegistry();
80+
$requirements = new ModelRequirements( [ CapabilityEnum::textGeneration() ], [] );
81+
foreach ( $registry->findModelsMetadataForSupport( $requirements ) as $provider_models ) {
82+
$provider = $provider_models->getProvider();
83+
$provider_id = $provider->getId();
84+
$provider_name = $provider->getName();
85+
foreach ( $provider_models->getModels() as $model ) {
86+
$key = $provider_id . self::SEPARATOR . $model->getId();
87+
$models[ $key ] = sprintf( '%s / %s', $provider_name, $model->getName() );
88+
}
89+
}
90+
} catch ( \Throwable $e ) {
91+
// Registry not ready or provider misbehaving: degrade to auto-select.
92+
return [];
93+
}
94+
95+
return $models;
96+
}
97+
98+
/**
99+
* Resolve the effective model preference for a generation request.
100+
*
101+
* Returns a `[ provider_id, model_id ]` pair suitable for the AI client's
102+
* model preference API, or null to let the client auto-select. A stored
103+
* selection that is no longer available resolves to null.
104+
*
105+
* @return array{0: string, 1: string}|null
106+
*/
107+
public static function get_effective_model_preference(): ?array {
108+
$stored = (string) get_option( self::OPTION_MODEL, '' );
109+
if ( '' === $stored ) {
110+
return null;
111+
}
112+
113+
// Validate against the live registry so a removed/disabled model is dropped.
114+
$available = self::get_available_models();
115+
if ( ! isset( $available[ $stored ] ) ) {
116+
return null;
117+
}
118+
119+
$parts = explode( self::SEPARATOR, $stored, 2 );
120+
if ( 2 !== count( $parts ) || '' === $parts[0] || '' === $parts[1] ) {
121+
return null;
122+
}
123+
124+
return [ $parts[0], $parts[1] ];
125+
}
126+
127+
/**
128+
* Resolve the effective temperature for a generation request.
129+
*
130+
* @return float|null Temperature, or null to omit the parameter entirely.
131+
*/
132+
public static function get_effective_temperature(): ?float {
133+
$stored = get_option( self::OPTION_TEMPERATURE, '' );
134+
if ( '' === $stored || null === $stored ) {
135+
return null;
136+
}
137+
return (float) $stored;
138+
}
139+
140+
/**
141+
* Whether a stored model selection has become unavailable.
142+
*
143+
* Used by the settings screen to warn that a previously chosen model is no
144+
* longer offered (e.g. its connector was disabled outside Hamelp).
145+
*
146+
* @return bool
147+
*/
148+
public static function is_stored_model_stale(): bool {
149+
$stored = (string) get_option( self::OPTION_MODEL, '' );
150+
if ( '' === $stored ) {
151+
return false;
152+
}
153+
$available = self::get_available_models();
154+
return ! isset( $available[ $stored ] );
155+
}
156+
}

app/Hametuha/Hamelp/Services/FaqSearchService.php

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,43 @@ public function generate_overview( string $query, array $history = [] ) {
7474
}
7575
$messages[] = new UserMessage( [ new MessagePart( $query ) ] );
7676

77-
$response = wp_ai_client_prompt( $messages )
78-
->using_system_instruction( $system_prompt )
79-
->using_temperature( 0.3 )
77+
$prompt = wp_ai_client_prompt( $messages )
78+
->using_system_instruction( $system_prompt );
79+
80+
/**
81+
* Filter the preferred AI model for FAQ overview generation.
82+
*
83+
* The default is resolved from the Hamelp settings screen and validated
84+
* against the live provider registry, so a selection whose connector was
85+
* disabled outside Hamelp falls back to auto-selection. Return a value to
86+
* override. Accepted forms mirror the AI client's model preference API:
87+
*
88+
* - a model ID string, e.g. `'gemini-2.5-flash'`
89+
* - a `[ provider_id, model_id ]` pair, e.g. `[ 'anthropic', 'claude-opus-4-1' ]`
90+
*
91+
* @param string|array|null $model Preferred model. Null auto-selects.
92+
*/
93+
$model = apply_filters( 'hamelp_ai_model', AiModelResolver::get_effective_model_preference() );
94+
if ( ! empty( $model ) ) {
95+
$prompt = $prompt->using_model_preference( $model );
96+
}
97+
98+
/**
99+
* Filter the sampling temperature for FAQ overview generation.
100+
*
101+
* The default is resolved from the Hamelp settings screen. Return `null`
102+
* to omit the temperature entirely. This is required for models that
103+
* reject the parameter (e.g. Claude Opus responds with a 400 error when a
104+
* temperature is supplied).
105+
*
106+
* @param float|null $temperature Sampling temperature. Null omits it.
107+
*/
108+
$temperature = apply_filters( 'hamelp_ai_temperature', AiModelResolver::get_effective_temperature() );
109+
if ( null !== $temperature ) {
110+
$prompt = $prompt->using_temperature( (float) $temperature );
111+
}
112+
113+
$response = $prompt
80114
->as_json_response()
81115
->generate_text();
82116

0 commit comments

Comments
 (0)