From 0f662914b47328bec0384312ce9eb2bcd4712401 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Wed, 27 May 2026 17:59:33 -0500 Subject: [PATCH 1/3] feat: surface model mismatch on query responses Co-Authored-By: Claude Opus 4.7 (1M context) --- includes/class-wpvdb-rest.php | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/includes/class-wpvdb-rest.php b/includes/class-wpvdb-rest.php index 80a75e8..538cec3 100644 --- a/includes/class-wpvdb-rest.php +++ b/includes/class-wpvdb-rest.php @@ -533,9 +533,9 @@ public static function handle_query( \WP_REST_Request $request ) { $timing['server_elapsed_ms'] = (int) round( ( microtime( true ) - $server_start ) * 1000 ); $response = $cached_result; $response['debug'] = $timing; - return rest_ensure_response( $response ); + return self::add_model_mismatch_header( $response, $model ); } - return rest_ensure_response( $cached_result ); + return self::add_model_mismatch_header( $cached_result, $model ); } // Try to generate an embedding for the query. @@ -840,13 +840,39 @@ function ( $row ) { $timing['server_elapsed_ms'] = (int) round( ( microtime( true ) - $server_start ) * 1000 ); $response_data['debug'] = $timing; } - return rest_ensure_response( $response_data ); + return self::add_model_mismatch_header( $response_data, $model ); } catch ( \Exception $e ) { Logger::log_exception( $e, 'Unhandled query exception' ); return new \WP_Error( 'error', $e->getMessage(), array( 'status' => 500 ) ); } } + /** + * Wrap a query response, flagging a requested model that differs from the active one. + * + * The /query path embeds with and filters by the requested `model`, so a stale + * client that pins an old model after a migration silently gets zero results. This + * adds an `X-WPVDB-Model-Mismatch` response header (visibility only; the request + * still succeeds) when the requested model is not the active default model. + * + * @param mixed $response Response payload to return. + * @param string $requested_model Model the request resolved to. + * @return \WP_REST_Response + */ + private static function add_model_mismatch_header( $response, $requested_model ) { + $response = rest_ensure_response( $response ); + $active_model = Settings::get_default_model(); + + if ( '' !== (string) $requested_model && $requested_model !== $active_model ) { + $response->header( + 'X-WPVDB-Model-Mismatch', + sprintf( 'requested=%s; active=%s', $requested_model, $active_model ) + ); + } + + return $response; + } + /** * Return metadata about the vector database. * This provides information that might be useful to clients. From 55b9fecc91f132617bfe194fc05b0186aa729b88 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Wed, 27 May 2026 18:37:28 -0500 Subject: [PATCH 2/3] fix: skip model-mismatch header for vector queries Co-Authored-By: Claude Opus 4.7 (1M context) --- includes/class-wpvdb-rest.php | 41 +++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/includes/class-wpvdb-rest.php b/includes/class-wpvdb-rest.php index 538cec3..7a9fa64 100644 --- a/includes/class-wpvdb-rest.php +++ b/includes/class-wpvdb-rest.php @@ -533,9 +533,9 @@ public static function handle_query( \WP_REST_Request $request ) { $timing['server_elapsed_ms'] = (int) round( ( microtime( true ) - $server_start ) * 1000 ); $response = $cached_result; $response['debug'] = $timing; - return self::add_model_mismatch_header( $response, $model ); + return self::add_model_mismatch_header( $response, $model, ! $has_provided_vector ); } - return self::add_model_mismatch_header( $cached_result, $model ); + return self::add_model_mismatch_header( $cached_result, $model, ! $has_provided_vector ); } // Try to generate an embedding for the query. @@ -840,7 +840,7 @@ function ( $row ) { $timing['server_elapsed_ms'] = (int) round( ( microtime( true ) - $server_start ) * 1000 ); $response_data['debug'] = $timing; } - return self::add_model_mismatch_header( $response_data, $model ); + return self::add_model_mismatch_header( $response_data, $model, ! $has_provided_vector ); } catch ( \Exception $e ) { Logger::log_exception( $e, 'Unhandled query exception' ); return new \WP_Error( 'error', $e->getMessage(), array( 'status' => 500 ) ); @@ -850,24 +850,33 @@ function ( $row ) { /** * Wrap a query response, flagging a requested model that differs from the active one. * - * The /query path embeds with and filters by the requested `model`, so a stale - * client that pins an old model after a migration silently gets zero results. This - * adds an `X-WPVDB-Model-Mismatch` response header (visibility only; the request - * still succeeds) when the requested model is not the active default model. + * On the text-query path the request embeds with and filters by the requested + * `model`, so a stale client that pins an old model after a migration silently + * gets zero results. This adds an `X-WPVDB-Model-Mismatch` response header + * (visibility only; the request still succeeds) when the requested model is not + * the active default model. + * + * The header is skipped for client-supplied-vector queries: those intentionally + * target rows of a specific (often non-active) model with a matching vector, so a + * mismatch there is expected and the header would be noise. * * @param mixed $response Response payload to return. * @param string $requested_model Model the request resolved to. + * @param bool $is_text_query Whether this is a text query (not a provided-vector query). * @return \WP_REST_Response */ - private static function add_model_mismatch_header( $response, $requested_model ) { - $response = rest_ensure_response( $response ); - $active_model = Settings::get_default_model(); - - if ( '' !== (string) $requested_model && $requested_model !== $active_model ) { - $response->header( - 'X-WPVDB-Model-Mismatch', - sprintf( 'requested=%s; active=%s', $requested_model, $active_model ) - ); + private static function add_model_mismatch_header( $response, $requested_model, $is_text_query = true ) { + $response = rest_ensure_response( $response ); + + if ( $is_text_query ) { + $active_model = Settings::get_default_model(); + + if ( '' !== (string) $requested_model && $requested_model !== $active_model ) { + $response->header( + 'X-WPVDB-Model-Mismatch', + sprintf( 'requested=%s; active=%s', $requested_model, $active_model ) + ); + } } return $response; From 3764f4ca72e7c6a6b683f85d719e132e1e82433d Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Wed, 27 May 2026 19:14:51 -0500 Subject: [PATCH 3/3] fix: normalize empty query model to default before use An explicit "model": "" resolved to an empty model, bypassing the default fallback so text queries embedded with an empty model and vector queries filtered WHERE model = ''. Normalize like the /vectors path does. Co-Authored-By: Claude Opus 4.7 (1M context) --- includes/class-wpvdb-rest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/class-wpvdb-rest.php b/includes/class-wpvdb-rest.php index 7a9fa64..6cfe34c 100644 --- a/includes/class-wpvdb-rest.php +++ b/includes/class-wpvdb-rest.php @@ -492,7 +492,8 @@ public static function handle_query( \WP_REST_Request $request ) { ); $text = isset( $data['query'] ) ? sanitize_textarea_field( $data['query'] ) : ''; - $model = isset( $data['model'] ) ? sanitize_text_field( $data['model'] ) : Settings::get_default_model(); + $model = isset( $data['model'] ) ? sanitize_text_field( $data['model'] ) : ''; + $model = $model ? $model : Settings::get_default_model(); $provider = isset( $data['provider'] ) ? sanitize_text_field( $data['provider'] ) : Settings::get_active_provider(); $api_base = $has_provided_vector ? '' : Settings::get_api_base_for_provider( $provider ); $cache_key_override = null;