PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/ai-copilot/client.php +96 -7 1.0.11.1.10 View file →
@@ -20,8 +20,10 @@
20 20
21 21 use WordPress\AiClient\Messages\DTO\Message;
22 22 use WordPress\AiClient\Messages\DTO\MessagePart;
23 23 use WordPress\AiClient\Messages\DTO\UserMessage;
24 +use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
25 +use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
24 26 use WordPress\AiClient\Tools\DTO\FunctionCall;
25 27 use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
26 28 use WordPress\AiClient\Tools\DTO\FunctionResponse;
27 29
@@ -143,8 +145,84 @@
143 145 );
144 146 }
145 147
146 148 /**
149 + * Applies the site's model config to a prompt builder.
150 + *
151 + * @param mixed $builder WP_AI_Client_Prompt_Builder.
152 + * @param array $context Partial filter context; missing keys are defaulted.
153 + * @return mixed
154 + */
155 +function openstation_ai_apply_model_config( $builder, array $context ) {
156 + $context = array_merge(
157 + array(
158 + 'user_id' => 0,
159 + 'request_id' => '',
160 + 'source' => '',
161 + 'has_tools' => false,
162 + 'has_schema' => false,
163 + ),
164 + $context
165 + );
166 +
167 + /**
168 + * Filters the model config for one AI turn.
169 + *
170 + * Defaults to empty. Recipe: `docs/examples/ai-model-config.md`.
171 + *
172 + * @param array $config { model?: string|ModelInterface, max_tokens?: int, temperature?: float, custom_options?: array<string, mixed> }.
173 + * @param array $context { user_id, request_id, source, has_tools, has_schema }.
174 + */
175 + $config = apply_filters( 'openstation_ai_model_config', array(), $context );
176 + if ( ! is_array( $config ) ) {
177 + return $builder;
178 + }
179 +
180 + $model_config = new ModelConfig();
181 +
182 + if ( isset( $config['max_tokens'] ) && is_numeric( $config['max_tokens'] ) && (int) $config['max_tokens'] > 0 ) {
183 + $model_config->setMaxTokens( (int) $config['max_tokens'] );
184 + }
185 +
186 + // Unlike max_tokens, 0.0 is a legitimate temperature (deterministic). The
187 + // 2.0 ceiling is the range the SDK's own schema declares.
188 + if ( isset( $config['temperature'] ) && is_numeric( $config['temperature'] )
189 + && (float) $config['temperature'] >= 0.0 && (float) $config['temperature'] <= 2.0 ) {
190 + $model_config->setTemperature( (float) $config['temperature'] );
191 + }
192 +
193 + $custom_options = array();
194 + if ( isset( $config['custom_options'] ) && is_array( $config['custom_options'] ) ) {
195 + foreach ( $config['custom_options'] as $key => $value ) {
196 + // A list would reach the provider as parameters named `0`, `1`, ….
197 + if ( is_string( $key ) && '' !== $key ) {
198 + $custom_options[ $key ] = $value;
199 + }
200 + }
201 + }
202 +
203 + if ( ! empty( $custom_options ) ) {
204 + $model_config->setCustomOptions( $custom_options );
205 + }
206 +
207 + $builder = $builder->using_model_config( $model_config );
208 +
209 + // After the config: `using_model()` merges the model's own defaults under
210 + // whatever the builder already carries, so ours has to land first.
211 + $model = isset( $config['model'] ) ? $config['model'] : null;
212 + if ( $model instanceof ModelInterface ) {
213 + $builder = $builder->using_model( $model );
214 + } elseif ( is_string( $model ) && '' !== trim( $model ) ) {
215 + // `using_model()` needs a ModelInterface, so a bare model id goes
216 + // through `using_model_preference()`, which throws on anything that
217 + // isn't a non-empty string.
218 + $builder = $builder->using_model_preference( trim( $model ) );
219 + }
220 +
221 + return $builder;
222 +}
223 +
224 +/**
147 225 * Runs one generation turn through the AI Client.
148 226 *
149 227 * Rebuilds the prompt from the full ordered message list each turn (the
150 228 * builder's `with_history()` prepends, so it can't append turns in a loop),
@@ -152,19 +230,18 @@
152 230 * answer to `$answer_schema` when given. Returns the assistant turn normalized
153 231 * to the shape the loop consumes; `message` has thought-channel parts stripped
154 232 * ({@see openstation_ai_strip_thought_parts()}) so it is safe to replay.
155 233 *
156 - * @param int $user_id Requesting user id. Currently unused — the
157 - * provider comes from Connectors and no
158 - * per-user preference is applied; retained for
159 - * signature stability and future attribution.
234 + * @param int $user_id Requesting user id.
160 235 * @param array $messages Ordered conversation as SDK Message objects.
161 236 * @param array $tool_defs Tool definitions to advertise.
162 237 * @param array|null $answer_schema JSON Schema for the final answer, or null.
163 238 * @param string $instructions System instruction.
239 + * @param array $context Optional. `{ source?: string, request_id?: string }`
240 + * for the model-config filter.
164 241 * @return array{ text: ?string, function_calls: array, message: mixed, usage: ?array, model: ?array }|WP_Error
165 242 */
166 -function openstation_ai_client_generate( $user_id, array $messages, array $tool_defs, $answer_schema, $instructions ) {
243 +function openstation_ai_client_generate( $user_id, array $messages, array $tool_defs, $answer_schema, $instructions, array $context = array() ) {
167 244 $builder = wp_ai_client_prompt( $messages );
168 245
169 246 if ( is_string( $instructions ) && '' !== $instructions ) {
170 247 $builder = $builder->using_system_instruction( $instructions );
@@ -169,10 +246,10 @@
169 246 if ( is_string( $instructions ) && '' !== $instructions ) {
170 247 $builder = $builder->using_system_instruction( $instructions );
171 248 }
172 249
173 - // Provider + model selection is delegated entirely to the Core AI Client
174 - // (Connector-backed); OpenStation pins neither.
250 + // Provider + model selection is delegated to the Core AI Client
251 + // (Connector-backed) unless the model-config filter says otherwise.
175 252
176 253 $declarations = openstation_ai_build_function_declarations( $tool_defs );
177 254 if ( ! empty( $declarations ) ) {
178 255 $builder = $builder->using_function_declarations( ...$declarations );
@@ -183,8 +260,20 @@
183 260 // doesn't set `additionalProperties: false`, and one such node 400s the
184 261 // whole turn. Normalize here so no schema author has to know that.
185 262 $builder = $builder->as_json_response( openstation_ai_normalize_response_schema( $answer_schema ) );
186 263 }
264 +
265 + $builder = openstation_ai_apply_model_config(
266 + $builder,
267 + array_merge(
268 + $context,
269 + array(
270 + 'user_id' => (int) $user_id,
271 + 'has_tools' => ! empty( $declarations ),
272 + 'has_schema' => is_array( $answer_schema ),
273 + )
274 + )
275 + );
187 276
188 277 $result = $builder->generate_result();
189 278 if ( is_wp_error( $result ) ) {
190 279 return $result;