Skip to content

fix(intelligence): resolve stale AI model ids instead of fatalling on /ai/generate - #3328

Open
MdAsifHossainNadim wants to merge 3 commits into
developfrom
fix/4910-ai-generate-null-model
Open

fix(intelligence): resolve stale AI model ids instead of fatalling on /ai/generate#3328
MdAsifHossainNadim wants to merge 3 commits into
developfrom
fix/4910-ai-generate-null-model

Conversation

@MdAsifHossainNadim

@MdAsifHossainNadim MdAsifHossainNadim commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

All Submissions:

  • My code follow the WordPress' coding standards
  • My code satisfies feature requirements
  • My code is tested
  • My code passes the PHPCS tests
  • My code has proper inline documentation

Changes proposed in this Pull Request:

Gemini::get_default_model_id() returned gemini-2.0-flash, an id that is no longer among the registered Gemini models (gemini-2.5-flash, gemini-2.5-pro, gemini-2.5-flash-lite-preview-06-17). Provider::get_model() returns null for an unknown id, and handle_request() passed that null straight into method_exists() — the exact TypeError in the report.

The reporter's second observation — "some dropdowns, which were previously selected, appear empty after a few days" — is the same root cause, not a separate symptom. That same unresolvable id is the default of the admin Model <select>, so it is not among the option keys and the select renders blank; saving the tab then persists gemini-2.0-flash into the dokan_ai option, which is what later reaches /ai/generate.

What changed:

  1. Providers/Gemini.php — default now points at a registered model.
  2. Services/Provider.php — new resolve_model( $type, $model_id ) degrades a retired saved id to a working model of the right type instead of returning null, plus get_default_model_id_by_type(). Both are additive; the get_default_model_id(): string abstract signature is untouched so Pro's BriaAi keeps compiling.
  3. REST/AIRequestController.php — returns WP_Error for every misconfiguration instead of dereferencing a null provider/model. Also fixes a second, independent fatal one line earlier: $manager->get_provider() returns null for an unregistered id, so a site still holding the 4.1 default dokan_ai_engine = 'chatgpt' hit Call to a member function get_model() on null.
  4. Intelligence/Admin/Settings.php — type-aware default, plus a read-time remap of retired ids so the select self-heals.
  5. src/admin/pages/Settings.vue — the hydration loop assigned the whole saved section inside the per-field loop, discarding defaults already applied to earlier fields. Hoisted the copy out of the loop.

Two deliberate calls worth reviewer attention:

  • Gated on $manager->is_configured( $type ) rather than empty( $provider_id ). active_engine() falls back to openai, so an emptiness check would have been dead code and an unconfigured site would have attempted a keyless upstream call.
  • AIProviderInterface was NOT widened. resolve_model() / get_default_model_id_by_type() are not on the public interface, so every call site guards with instanceof Provider (or method_exists in Pro). Adding them to the interface would be a BC break for third-party providers.

Also enforces dokan_ai_image_gen_availability at the API — image generation is billed to the marketplace owner and the admin toggle previously held only in the UI.

Related Pull Request(s)

  • dokan-pro companion (image-model default): filed alongside — must ship in the same release train, though it is method_exists-guarded so merge order does not matter.

Closes

How to test the changes in this Pull Request:

Put the site into the reported configuration and call the endpoint:

wp eval '
$o = get_option( "dokan_ai", [] );
$o["dokan_ai_engine"]         = "gemini";
$o["dokan_ai_gemini_model"]   = "gemini-2.0-flash";  // retired id
$o["dokan_ai_gemini_api_key"] = "dummy";
update_option( "dokan_ai", $o );
wp_set_current_user( 1 );
$r = new WP_REST_Request( "POST", "/dokan/v1/ai/generate" );
$r->set_param( "prompt", "a short product title" );
$r->set_param( "payload", [ "field" => "title", "type" => "text" ] );
$res = rest_do_request( $r );
echo $res->get_status() . " " . wp_json_encode( $res->get_data() ) . "\n";'

Before: PHP Fatal error: Uncaught TypeError: method_exists(): Argument #1 ... null given ... AIRequestController.php:97 with method_exists(NULL, 'process_text') — byte-identical to the trace in the issue.
After: no fatal; a clean JSON error response.

Verified matrix (all previously fatal or misleading, now clean — Lite 5.0.9 / Pro 5.0.7 / WC 10.9.4 / PHP 8.4):

scenario after
stale gemini-2.0-flash 403 upstream error (model resolved, request attempted)
legacy engine chatgpt 400 dokan_ai_not_configured (was a fatal on line 93)
unknown engine 400 dokan_ai_not_configured
no API key 400 dokan_ai_not_configured
type=image, toggle off 400 "AI image generation is not enabled for this marketplace."
payload.field/type as arrays 403, no TypeError

Blank-dropdown half:

raw saved value      : gemini-2.0-flash
value seen by the UI : gemini-2.5-flash
select options       : gemini-2.5-flash, gemini-2.5-pro, gemini-2.5-flash-lite-preview-06-17
field default        : gemini-2.5-flash
default IS an option : YES   (before: NO -> blank dropdown)
other section intact : {"foo":"bar"}

Browser check still needed from a reviewer (my local session could not reach /wp-admin): Dokan → Settings → AI Assist, set Engine = Gemini, confirm the Model select pre-selects "Gemini 2.5 Flash" instead of rendering blank. src/ changed, so run npm run build first — assets/js/ is gitignored and no build artefacts are committed.

PHPCS clean on all four PHP files. src/admin/pages/Settings.vue has a pre-existing eslint parse error at 4:52, identical on develop — unrelated to this change; the script block passes node --check.

Changelog entry

Fix — AI Assist: a retired model id no longer crashes generation or blanks the settings dropdown

Previously, when a provider retired a model that was already saved in settings (Gemini's default gemini-2.0-flash), the Model dropdown rendered blank and any vendor AI generation request crashed with a PHP fatal error. Dokan now falls back to a working model of the correct type, remaps retired ids when settings load, and returns a clear error message for every misconfiguration instead of failing fatally.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • AI model selection now automatically falls back to a supported default when a saved/retired model isn’t available.
    • AI requests now resolve text vs. image compatible models more reliably.
  • Bug Fixes
    • Updated the default Gemini model to Gemini 2.5 Flash.
    • Fixed AI settings loading to copy saved section values once and only apply field defaults when a value is missing.

… /ai/generate

Gemini's default model id was gemini-2.0-flash, which is no longer among
the registered models, so Provider::get_model() returned null and
handle_request() passed that null to method_exists() -- a TypeError, and
a 500 instead of an error response. The same unresolvable id was fed to
the admin Model select as its `default`, which is why the dropdown
rendered blank and why saving the AI Assist tab persisted the broken id.

- Gemini now defaults to a model that is actually registered.
- Provider::resolve_model() degrades a retired saved id to a working
  model of the right type rather than returning null.
- The controller returns WP_Error for every misconfiguration instead of
  dereferencing a null provider or model, gates on is_configured()
  (active_engine() falls back to openai, so emptiness proved nothing),
  enforces the image-generation toggle at the API, and coerces the
  unconstrained payload members.
- Saved model ids that a provider has retired are remapped at read time,
  so the settings select shows a real value instead of blank.
- The settings Vue app copied a whole saved section inside the per-field
  loop, discarding defaults already applied to earlier fields.

Closes #4910

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 646cfd02-efe7-487f-b44b-67b883faec89

📥 Commits

Reviewing files that changed from the base of the PR and between 536d4b5 and 4bc33cd.

📒 Files selected for processing (2)
  • includes/Intelligence/REST/AIRequestController.php
  • src/admin/pages/Settings.vue
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/admin/pages/Settings.vue
  • includes/Intelligence/REST/AIRequestController.php

📝 Walkthrough

Walkthrough

AI provider model resolution now handles saved, default, and fallback models by type. AI settings remap retired model IDs, REST generation validates configuration and capabilities, and the settings page applies per-field defaults more consistently.

Changes

AI model flow

Layer / File(s) Summary
Provider model resolution
includes/Intelligence/Services/Provider.php, includes/Intelligence/Services/Providers/Gemini.php
Providers resolve supported saved or default models, fall back to the first available model, and use Gemini’s updated default model ID.
AI settings model migration
includes/Intelligence/Admin/Settings.php
Retired saved model IDs are remapped to provider defaults by model type, and text model fields use type-specific defaults.
Generation request validation
includes/Intelligence/REST/AIRequestController.php
Generation requests sanitize inputs, validate availability and configuration, resolve models defensively, verify processing support, and return standardized errors for configuration failures.
Settings value loading
src/admin/pages/Settings.vue
Saved section values are copied once, with defaults applied only to fields whose saved values are undefined.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AIRequestController
  participant Provider
  participant AIModelInterface
  Client->>AIRequestController: Submit generation payload
  AIRequestController->>Provider: Resolve provider and model by type
  Provider->>AIModelInterface: Verify process_type support
  AIModelInterface-->>Provider: Return supported model
  Provider-->>AIRequestController: Return generated result or WP_Error
  AIRequestController-->>Client: Return response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: stale AI model IDs causing /ai/generate failures.
Description check ✅ Passed The description is mostly complete and includes the fix summary, testing, changelog, and related PR context.
Linked Issues check ✅ Passed The changes address #4910 by preventing null provider/model crashes and returning graceful WP_Error responses.
Out of Scope Changes check ✅ Passed The added settings and Vue hydration changes are tied to the stale-ID/dropdown issue and are not out of scope.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/4910-ai-generate-null-model

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
includes/Intelligence/REST/AIRequestController.php (2)

90-98: 🧹 Nitpick | 🔵 Trivial

Operational note: no visible throttling on paid AI generation calls.

The image-generation toggle gates a marketplace-level feature flag, but there's no visible per-vendor rate limiting/quota on this endpoint despite the inline comment noting costs are billed to the marketplace owner. Consider whether abuse protection (rate limiting, per-vendor caps) exists upstream or should be added, since REST endpoints triggering paid third-party API calls are a common cost/DoS vector.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/Intelligence/REST/AIRequestController.php` around lines 90 - 98, Add
or reuse abuse protection in the REST handler around the paid image-generation
path identified by Model::SUPPORTS_IMAGE and the AI request controller,
enforcing a per-vendor rate limit or quota before invoking the third-party API.
Reject over-limit requests with the controller’s existing configuration/error
response mechanism, while preserving the marketplace toggle and configuration
checks.

71-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding regression tests for the resolved bug's guard paths.

This diff fixes a production fatal error (#4910) via several new guard clauses (instanceof AIProviderInterface, instanceof AIModelInterface, retired-model fallback, Throwable catch). Given the criticality, unit/integration tests covering: null/invalid provider, retired saved model id, unsupported type payload, and a thrown Error/Exception during generation would help lock in this fix.
Want me to draft PHPUnit tests for these paths?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/Intelligence/REST/AIRequestController.php` around lines 71 - 151,
Add regression tests for handle_request covering invalid or null providers,
retired saved model IDs falling back to a valid model, unsupported or malformed
type payloads, and both Error and Exception thrown during generation. Assert
each guard returns the expected configuration or service WP_Error response and
that valid fallback generation remains successful.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@includes/Intelligence/REST/AIRequestController.php`:
- Around line 90-98: Add or reuse abuse protection in the REST handler around
the paid image-generation path identified by Model::SUPPORTS_IMAGE and the AI
request controller, enforcing a per-vendor rate limit or quota before invoking
the third-party API. Reject over-limit requests with the controller’s existing
configuration/error response mechanism, while preserving the marketplace toggle
and configuration checks.
- Around line 71-151: Add regression tests for handle_request covering invalid
or null providers, retired saved model IDs falling back to a valid model,
unsupported or malformed type payloads, and both Error and Exception thrown
during generation. Assert each guard returns the expected configuration or
service WP_Error response and that valid fallback generation remains successful.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a76b8e7-3afb-4830-8df8-0d7f8732c03a

📥 Commits

Reviewing files that changed from the base of the PR and between db92d4b and 8266db4.

📒 Files selected for processing (5)
  • includes/Intelligence/Admin/Settings.php
  • includes/Intelligence/REST/AIRequestController.php
  • includes/Intelligence/Services/Provider.php
  • includes/Intelligence/Services/Providers/Gemini.php
  • src/admin/pages/Settings.vue

@MdAsifHossainNadim

Copy link
Copy Markdown
Contributor Author

CI note — the 2 PHPUnit failures are pre-existing on develop

The "Run PHPCS inspection" workflow is red, but the PHPCS step itself passes — the failing step inside it is Running the phpunit tests:

1) WeDevs\Dokan\Test\Orders\OrderStatusChangeTest::test_allowed_status_transitions
   with data set #4 ('pending', 'cancelled')
2) WeDevs\Dokan\Test\Orders\OrderStatusChangeTest::test_pending_to_any_status
   with data set #3 ('pending', 'cancelled')
   Failed asserting that two strings are equal.  -'cancelled'  +'pending'
Tests: 245, Assertions: 951, Failures: 2.

These are order-status transition tests and are unrelated to this PR. Confirmed pre-existing: the identical two failures, same data sets, same assertion appear on #3329, which changes only a CSS shim in includes/ThemeSupport/Astra.php and cannot possibly affect order status logic — and on #3315 as well.

Worth a separate ticket: pending → cancelled is not applying in the test environment.

🤖 Generated with Claude Code

…ctions

- resolve_model() called get_model() twice and get_models_by_type() once,
  each of which re-runs the provider's models filter. Resolve the
  type-filtered list once and index into it; three dispatches become one
  and the method gets shorter.
- remap_retired_model_values() declared ': array' and cast on the decline
  path, so for every section that is not dokan_ai it rewrote whatever an
  earlier callback on the same filter had returned. It now returns the
  value untouched, matching its three siblings on dokan_get_settings_values.
- Collapse the $args['type'] write/read/write dance in handle_request().

No behaviour change: verified the resolve matrix and the full endpoint
failure matrix are identical before and after.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MdAsifHossainNadim MdAsifHossainNadim self-assigned this Jul 20, 2026
…d; tidy the settings loader

- move the scalar cast next to the option read it normalizes
- drop a dead callback param and collapse the default-pick branch in fetchSettingValues

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant