Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .distignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ tailwind.config.js
/.wordpress-org
/.idea
/src
/patches
/bin
/tests
/phpunit.xml
Expand Down
11 changes: 10 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,20 @@
},
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"php-http/discovery": true
"php-http/discovery": true,
"cweagans/composer-patches": true
}
},
"extra": {
"patches": {
"your1/qdrant": {
"Add #[\\ReturnTypeWillChange] to Response::offsetGet() for PHP 8.1+ compatibility": "patches/qdrant-response-returntypewillchange.patch"
}
}
},
"require": {
"codeinwp/themeisle-sdk": "^3.3",
"cweagans/composer-patches": "^1.7",
"guttedgarden/tiktoken": "^1.2",
"your1/qdrant": "^0.5.3"
}
Expand Down
50 changes: 49 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 100 additions & 10 deletions inc/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ public function update_settings( $request ) {
}

if ( empty( $updated ) ) {
return rest_ensure_response( [ 'error' => __( 'No settings to update.', 'hyve-lite' ) ] );
return $this->settings_response( [ 'success' => __( 'Settings are already up to date.', 'hyve-lite' ) ] );
}

$validation = apply_filters(
Expand Down Expand Up @@ -418,7 +418,7 @@ function ( $carry, $item ) {

foreach ( $updated as $key => $value ) {
if ( ! $validation[ $key ]['validate']( $value ) ) {
return rest_ensure_response(
return $this->settings_response(
[
// translators: %s: option key.
'error' => sprintf( __( 'Invalid value: %s', 'hyve-lite' ), $key ),
Expand All @@ -429,15 +429,34 @@ function ( $carry, $item ) {
$updated[ $key ] = $validation[ $key ]['sanitize']( $value );
}

$api_warning = '';
$api_key_error = null;
$key_validated = false;

foreach ( $updated as $key => $value ) {
$settings[ $key ] = $value;

if ( 'api_key' === $key && ! empty( $value ) ) {
$openai = new OpenAI( $value );
$valid_api = $openai->moderate( 'This is a test message.' );
$openai = new OpenAI( $value );

// Validate against the embeddings endpoint. Suppress automatic
// persistence: the dashboard notice is reconciled after the save
// actually lands, so it can never reflect a key that was not
// stored. See Codeinwp/hyve#149.
$validation = $openai->set_error_persistence( false )->create_embeddings( 'Test connection.' );
$key_validated = true;

if ( is_wp_error( $validation ) && $this->is_auth_error( $validation->get_error_code() ) ) {
// The key itself is invalid — block the save.
return $this->settings_response( [ 'error' => $this->get_error_message( $validation ) ] );
}

if ( is_wp_error( $valid_api ) ) {
return rest_ensure_response( [ 'error' => $this->get_error_message( $valid_api ) ] );
if ( is_wp_error( $validation ) ) {
// The key is well-formed but the account is rate-limited or
// has no credits (new, unfunded accounts return a 429). Save
// the key but warn; the notice is recorded after the save.
$api_warning = $this->get_error_message( $validation );
$api_key_error = $validation;
}
}

Expand All @@ -451,13 +470,63 @@ function ( $carry, $item ) {
$init = $qdrant->init();

if ( is_wp_error( $init ) ) {
return rest_ensure_response( [ 'error' => $this->get_error_message( $init ) ] );
return $this->settings_response( [ 'error' => $this->get_error_message( $init ) ] );
}
}

update_option( 'hyve_settings', $settings );

return rest_ensure_response( __( 'Settings updated.', 'hyve-lite' ) );
// Reconcile the dashboard service-error notice with the key that was just
// saved — only now that the save has actually landed (no earlier exit can
// leave a notice for an unsaved key). Clear any stale notice first, then
// re-record one for the new key when its error is actionable (a no-op for
// transient codes such as rate limits). See Codeinwp/hyve#149.
if ( $key_validated ) {
delete_option( OpenAI::ERROR_OPTION_KEY );

if ( null !== $api_key_error ) {
OpenAI::instance()->save_service_error( $api_key_error );
}
}

if ( ! empty( $api_warning ) ) {
return $this->settings_response( [ 'warning' => $api_warning ] );
}

return $this->settings_response( [ 'success' => __( 'Settings updated.', 'hyve-lite' ) ] );
}

/**
* Build a settings REST response carrying the current service errors.
*
* Returning the freshly-computed service errors lets the dashboard notice
* update immediately after a save, without a page reload. See Codeinwp/hyve#200.
*
* @param array<string, mixed> $payload The response payload.
*
* @return \WP_REST_Response
*/
private function settings_response( $payload ) {
$options = apply_filters( 'hyve_options_data', [] );

$payload['serviceErrors'] = ( is_array( $options ) && isset( $options['serviceErrors'] ) ) ? $options['serviceErrors'] : [];

return rest_ensure_response( $payload );
}

/**
* Whether an error code means the API key itself is invalid.
*
* These block a key from being saved. Account-level problems (no credits,
* billing, rate limits) do not: the key is valid, the account just needs
* attention, so we save it and warn instead of blocking. See Codeinwp/hyve#149.
*
* @param int|string $code The error code.
*
* @return bool
*/
private function is_auth_error( $code ) {
return in_array( $code, OpenAI::AUTH_ERROR_CODES, true );
}

/**
Expand Down Expand Up @@ -584,6 +653,12 @@ public function get_data( $request ) {
$post_data['review'] = $review;
}

$processing_error = get_post_meta( $post_id, '_hyve_processing_error', true );

if ( ! empty( $processing_error ) && get_post_meta( $post_id, '_hyve_added', true ) ) {
$post_data['error'] = $processing_error;
}

$posts_data[] = $post_data;
}
}
Expand Down Expand Up @@ -628,6 +703,20 @@ public function add_data( $request ) {
return rest_ensure_response( [ 'error' => $this->get_error_message( $process ) ] );
}

// The content was stored, but the synchronous indexing attempt may have
// failed (e.g. rate limit, no credits). Surface that as a non-blocking
// warning so the admin sees it immediately, not only as a list badge.
$processing_error = get_post_meta( $post_id, '_hyve_processing_error', true );

if ( ! empty( $processing_error ) ) {
return rest_ensure_response(
[
'success' => true,
'warning' => $processing_error,
]
);
}

return rest_ensure_response( true );
}

Expand All @@ -646,8 +735,8 @@ public function delete_data( $request ) {
try {
$delete_result = Qdrant_API::instance()->delete_point( $id );

if ( ! $delete_result ) {
throw new \Exception( __( 'Failed to delete point in Qdrant.', 'hyve-lite' ) );
if ( is_wp_error( $delete_result ) || ! $delete_result ) {
throw new \Exception( is_wp_error( $delete_result ) ? $delete_result->get_error_message() : __( 'Failed to delete point in Qdrant.', 'hyve-lite' ) );
}
} catch ( \Exception $e ) {
return rest_ensure_response( [ 'error' => $e->getMessage() ] );
Expand All @@ -660,6 +749,7 @@ public function delete_data( $request ) {
delete_post_meta( $id, '_hyve_needs_update' );
delete_post_meta( $id, '_hyve_moderation_failed' );
delete_post_meta( $id, '_hyve_moderation_review' );
delete_post_meta( $id, '_hyve_processing_error' );
return rest_ensure_response( true );
}

Expand Down
10 changes: 4 additions & 6 deletions inc/BaseAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,10 @@ public function __construct() {
* @return string
*/
public function get_error_message( $error ) {
$errors = [
'invalid_api_key' => __( 'Incorrect API key provided.', 'hyve-lite' ),
'missing_scope' => __( 'You have insufficient permissions for this operation.', 'hyve-lite' ),
];
if ( isset( $errors[ $error->get_error_code() ] ) ) {
return $errors[ $error->get_error_code() ];
$message = OpenAI::get_error_message_for_code( $error->get_error_code() );

if ( null !== $message ) {
return $message;
}

return $error->get_error_message();
Expand Down
Loading
Loading