Files
wp-agentic-writer/recommender-impl-brief.md
2026-01-28 00:26:00 +07:00

29 KiB
Raw Blame History

WP Agentic Writer: Model Recommender Implementation Brief

Executive Summary

The Model Recommender is an interactive guided experience in the plugin settings that asks users 45 questions, then automatically generates and applies a personalized OpenRouter model configuration matching their budget and use case.

Why it exists: Users arrive with widely varying budgets (totally free → premium agencies). The 3 presets (Budget/Balanced/Premium) cover 80% of use cases, but the Recommender handles the remaining 20%: ultra-low-budget users, niche workflows, or custom combinations.

Where it lives: Settings page → "AI Model Configuration" card → Button: "Open Model Recommender"

Output: A filled-in model configuration that users can save or customize.


Table of Contents

  1. User Flow
  2. System Prompt (Agent Logic)
  3. Question Schema
  4. Response JSON Format
  5. Backend Implementation
  6. Frontend UI Component
  7. Conversation Examples
  8. Edge Cases & Fallbacks
  9. Development Checklist

User Flow

User clicks: [Open Model Recommender] button on settings page
    ↓
Modal opens with welcoming intro text
    ↓
Question 1: "What's your main goal?"
    ├─ Dev blog, agency client content, hobby writing, etc.
    ↓
Question 2: "What's your budget per article?"
    ├─ <$0.01, $0.010.05, $0.050.20, >$0.20
    ↓
Question 3: "How many articles per month?"
    ├─ 13, 48, 915, 15+
    ↓
Question 4: "Do you need AI-generated images?"
    ├─ Yes, No, Optional (depends on image quality)
    ↓
(Optional) Question 5: "Any specific provider preference?"
    ├─ No preference, Google Gemini, OpenAI, Anthropic
    ↓
Agent processes answers → generates recommended config JSON
    ↓
Modal displays: "Recommended config: [Name]"
    ├─ Chat: [Model]
    ├─ Writing: [Model]
    ├─ etc.
    ├─ Estimated cost: $X/article
    ↓
User: [Apply Config] or [Customize Manually] or [Cancel]
    ↓
Config saved to plugin options / UI refreshed

System Prompt (Agent Logic)

This prompt runs server-side to convert user answers into a model configuration.

System Prompt for Model Recommender Agent
═══════════════════════════════════════════════════════════════

You are a Model Configuration Recommender for WP Agentic Writer.

Your job: Convert user answers (goal, budget, volume, preferences) 
into an optimal OpenRouter model preset for 6 tasks:
- chat, clarity, planning, writing, refinement, image

You MUST:
1. Respect budget constraints strictly
2. Optimize cost:quality tradeoff for use case
3. Prefer Gemini Flash for planning/chat (best value)
4. Prefer Claude Sonnet for writing (industry standard)
5. Disable images if budget <$0.01/article
6. Return ONLY valid JSON (no extra text)

Budget tiers (for reference):
- Ultra (<$0.01/article): Gemini Flash only, no images
- Budget ($0.010.05): DeepSeek + Flash, FLUX.2 klein images
- Balanced ($0.050.20): Gemini Flash + Claude Sonnet, Riverflow images
- Premium (>$0.20): GPT-5.2/Opus + Flash, FLUX.2 max images

User answers will come as:
{
  "goal": "string",
  "budget_per_article": "string ($0.010.05, etc.)",
  "articles_per_month": "number",
  "images_needed": "yes/no/optional",
  "provider_preference": "string or null"
}

Return a JSON object with this schema:
{
  "preset_name": "string (e.g., 'Budget: Gemini Flash + DeepSeek')",
  "rationale": "string (brief explanation of why this config)",
  "estimated_cost_per_article": number (USD, including 5.5% fee),
  "models": {
    "chat": "model_slug_string",
    "clarity": "model_slug_string",
    "planning": "model_slug_string",
    "writing": "model_slug_string",
    "refinement": "model_slug_string",
    "image": "model_slug_string or null"
  },
  "recommendations": {
    "when_to_use": "string",
    "workflow_expectation": "string"
  }
}

Examples of model slugs (valid on OpenRouter):
- deepseek-v3
- google/gemini-3-flash-preview
- anthropic/claude-3.5-sonnet
- anthropic/claude-sonnet-4
- mistral/mistral-small
- openai/gpt-5.2
- black-forest-labs/flux.2-klein
- black-forest-labs/flux.2-max
- sourceful/riverflow-v2-max

If budget <$0.01/article:
- Recommend: Gemini Flash for ALL tasks
- Image: null (disabled)
- Estimated cost: ~$0.0080.012/article

If budget $0.010.05/article:
- Chat/Planning: Gemini Flash
- Writing/Refinement: Mistral Small or DeepSeek
- Images: FLUX.2 klein or null
- Estimated cost: $0.030.05/article

If budget $0.050.20/article:
- Chat/Clarity/Planning: Gemini Flash
- Writing/Refinement: Claude Sonnet
- Images: Riverflow V2 Max or FLUX.2 Pro
- Estimated cost: $0.100.18/article

If budget >$0.20/article:
- Chat/Planning: Gemini Flash (cost doesn't improve)
- Clarity: Claude Sonnet 4 (nuanced feedback)
- Writing/Refinement: GPT-5.2 or Claude Opus
- Images: FLUX.2 max
- Estimated cost: $0.250.50+/article

Return valid JSON only. No markdown, no explanations outside the JSON.

Question Schema

Frontend: Questions Array (React/JS)

const recommenderQuestions = [
  {
    id: "goal",
    question: "What's your main goal for this plugin?",
    type: "radio",
    options: [
      { value: "dev_blog", label: "Dev blog / technical tutorials" },
      { value: "agency_content", label: "Agency client content" },
      { value: "hobby_writing", label: "Hobby writing / personal blog" },
      { value: "marketing", label: "Marketing / sales content" },
      { value: "research", label: "Research papers / long-form" }
    ],
    required: true
  },
  {
    id: "budget_per_article",
    question: "What's your budget limit per article?",
    type: "radio",
    options: [
      { value: "<0.01", label: "Ultra-budget (< $0.01 / article)" },
      { value: "0.01-0.05", label: "Budget ($0.01$0.05 / article)" },
      { value: "0.05-0.20", label: "Balanced ($0.05$0.20 / article)" },
      { value: ">0.20", label: "Premium (> $0.20 / article)" },
      { value: "no_limit", label: "No budget limit" }
    ],
    required: true
  },
  {
    id: "articles_per_month",
    question: "How many articles per month do you plan to generate?",
    type: "radio",
    options: [
      { value: "1-3", label: "13 articles/month" },
      { value: "4-8", label: "48 articles/month" },
      { value: "9-15", label: "915 articles/month" },
      { value: "15+", label: "15+ articles/month" }
    ],
    required: true
  },
  {
    id: "images_needed",
    question: "Do you need AI-generated images?",
    type: "radio",
    options: [
      { value: "yes", label: "Yes, high-quality hero images" },
      { value: "optional", label: "Optional / nice-to-have" },
      { value: "no", label: "No, I'll upload my own" }
    ],
    required: true
  },
  {
    id: "provider_preference",
    question: "(Optional) Any provider preference?",
    type: "radio",
    options: [
      { value: null, label: "No preference (use what's best)" },
      { value: "google", label: "Google (Gemini)" },
      { value: "openai", label: "OpenAI (GPT)" },
      { value: "anthropic", label: "Anthropic (Claude)" }
    ],
    required: false
  }
];

Response JSON Format

Backend: Agent Response

{
  "preset_name": "Balanced: Gemini Flash + Claude Sonnet + Riverflow",
  "rationale": "Your budget ($0.100.20/article) and moderate volume (8 articles/month) fit the Balanced preset perfectly. Gemini Flash handles chat/planning efficiently; Claude Sonnet is the industry standard for long-form writing. Riverflow images provide high quality at flat $0.03/image.",
  "estimated_cost_per_article": 0.1359,
  "models": {
    "chat": "google/gemini-3-flash-preview",
    "clarity": "google/gemini-3-flash-preview",
    "planning": "google/gemini-3-flash-preview",
    "writing": "anthropic/claude-3.5-sonnet",
    "refinement": "anthropic/claude-3.5-sonnet",
    "image": "sourceful/riverflow-v2-max"
  },
  "recommendations": {
    "when_to_use": "Regular blogging (410 articles/month), mixed content types, publishing to professional blog or portfolio.",
    "workflow_expectation": "User does one refinement cycle; publishes with high confidence."
  }
}

Backend Implementation

1. PHP Endpoint (WordPress REST API)

<?php
// File: includes/class-model-recommender.php

class Agentic_Writer_Model_Recommender {
    
    /**
     * Register REST endpoint
     */
    public static function register_endpoints() {
        register_rest_route(
            'agentic-writer/v1',
            '/recommend-models',
            [
                'methods' => 'POST',
                'callback' => [ self::class, 'get_recommendation' ],
                'permission_callback' => [ self::class, 'check_permissions' ],
                'args' => [
                    'goal' => ['required' => true],
                    'budget_per_article' => ['required' => true],
                    'articles_per_month' => ['required' => true],
                    'images_needed' => ['required' => true],
                    'provider_preference' => ['required' => false],
                ]
            ]
        );
    }
    
    /**
     * Get model recommendation from Claude/LLM
     */
    public static function get_recommendation( $request ) {
        $params = $request->get_json_params();
        
        // Build prompt with user answers
        $user_input = self::format_user_input( $params );
        
        // Call OpenRouter API (using Claude 3.5 Sonnet for reasoning)
        $recommendation = self::call_recommender_agent( $user_input );
        
        if ( is_wp_error( $recommendation ) ) {
            return new WP_REST_Response(
                ['error' => $recommendation->get_error_message()],
                500
            );
        }
        
        return new WP_REST_Response( $recommendation, 200 );
    }
    
    /**
     * Format user answers into structured prompt
     */
    private static function format_user_input( $params ) {
        return json_encode([
            'goal' => $params['goal'] ?? null,
            'budget_per_article' => $params['budget_per_article'] ?? null,
            'articles_per_month' => (int) $params['articles_per_month'] ?? null,
            'images_needed' => $params['images_needed'] ?? 'no',
            'provider_preference' => $params['provider_preference'] ?? null,
        ]);
    }
    
    /**
     * Call OpenRouter API with system prompt + user input
     */
    private static function call_recommender_agent( $user_input ) {
        $api_key = get_option( 'agentic_writer_openrouter_api_key' );
        
        if ( !$api_key ) {
            return new WP_Error(
                'no_api_key',
                'OpenRouter API key not configured.'
            );
        }
        
        $system_prompt = self::get_system_prompt();
        
        $response = wp_remote_post(
            'https://openrouter.ai/api/v1/chat/completions',
            [
                'headers' => [
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type' => 'application/json',
                ],
                'body' => json_encode([
                    'model' => 'anthropic/claude-3.5-sonnet',
                    'messages' => [
                        [
                            'role' => 'system',
                            'content' => $system_prompt,
                        ],
                        [
                            'role' => 'user',
                            'content' => "User answers: {$user_input}\n\nBased on these answers, generate a recommended model configuration.",
                        ]
                    ],
                    'temperature' => 0.2, // Deterministic
                    'max_tokens' => 500,
                ]),
            ]
        );
        
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        
        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        
        if ( !isset( $body['choices'][0]['message']['content'] ) ) {
            return new WP_Error(
                'invalid_response',
                'Invalid response from OpenRouter API.'
            );
        }
        
        $content = $body['choices'][0]['message']['content'];
        
        // Parse JSON from response
        $recommendation = json_decode( $content, true );
        
        if ( !$recommendation ) {
            return new WP_Error(
                'invalid_json',
                'Could not parse agent recommendation.'
            );
        }
        
        return $recommendation;
    }
    
    /**
     * Get system prompt (embedded in class)
     */
    private static function get_system_prompt() {
        return <<<'PROMPT'
You are a Model Configuration Recommender for WP Agentic Writer.

Your job: Convert user answers (goal, budget, volume, preferences) 
into an optimal OpenRouter model preset for 6 tasks:
- chat, clarity, planning, writing, refinement, image

You MUST:
1. Respect budget constraints strictly
2. Optimize cost:quality tradeoff for use case
3. Prefer Gemini Flash for planning/chat (best value)
4. Prefer Claude Sonnet for writing (industry standard)
5. Disable images if budget <$0.01/article
6. Return ONLY valid JSON (no extra text)

Budget tiers (for reference):
- Ultra (<$0.01/article): Gemini Flash only, no images
- Budget ($0.010.05): DeepSeek + Flash, FLUX.2 klein images
- Balanced ($0.050.20): Gemini Flash + Claude Sonnet, Riverflow images
- Premium (>$0.20): GPT-5.2/Opus + Flash, FLUX.2 max images

Valid model slugs (OpenRouter):
- deepseek-v3
- google/gemini-3-flash-preview
- anthropic/claude-3.5-sonnet
- anthropic/claude-sonnet-4
- mistral/mistral-small
- openai/gpt-5.2
- black-forest-labs/flux.2-klein
- black-forest-labs/flux.2-max
- sourceful/riverflow-v2-max

Return ONLY this JSON structure (no markdown, no extra text):
{
  "preset_name": "string",
  "rationale": "string",
  "estimated_cost_per_article": number,
  "models": {
    "chat": "string",
    "clarity": "string",
    "planning": "string",
    "writing": "string",
    "refinement": "string",
    "image": "string or null"
  },
  "recommendations": {
    "when_to_use": "string",
    "workflow_expectation": "string"
  }
}
PROMPT;
    }
    
    /**
     * Check user permissions
     */
    public static function check_permissions() {
        return current_user_can( 'manage_options' );
    }
}

// Hook to register on init
add_action( 'rest_api_init', [ 'Agentic_Writer_Model_Recommender', 'register_endpoints' ] );

Frontend UI Component

React Component (Gutenberg/Admin UI)

// File: components/ModelRecommender.jsx

import React, { useState } from 'react';
import { Button, Modal, RadioControl, Spinner } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';

export const ModelRecommender = ({ onApplyConfig }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [currentStep, setCurrentStep] = useState(0);
  const [answers, setAnswers] = useState({
    goal: null,
    budget_per_article: null,
    articles_per_month: null,
    images_needed: 'no',
    provider_preference: null,
  });
  const [loading, setLoading] = useState(false);
  const [recommendation, setRecommendation] = useState(null);

  const questions = [
    {
      id: 'goal',
      question: 'What\'s your main goal for this plugin?',
      options: [
        { value: 'dev_blog', label: 'Dev blog / technical tutorials' },
        { value: 'agency_content', label: 'Agency client content' },
        { value: 'hobby_writing', label: 'Hobby writing / personal blog' },
        { value: 'marketing', label: 'Marketing / sales content' },
        { value: 'research', label: 'Research papers / long-form' },
      ],
    },
    {
      id: 'budget_per_article',
      question: 'What\'s your budget limit per article?',
      options: [
        { value: '<0.01', label: 'Ultra-budget (< $0.01)' },
        { value: '0.01-0.05', label: 'Budget ($0.01$0.05)' },
        { value: '0.05-0.20', label: 'Balanced ($0.05$0.20)' },
        { value: '>0.20', label: 'Premium (> $0.20)' },
        { value: 'no_limit', label: 'No budget limit' },
      ],
    },
    {
      id: 'articles_per_month',
      question: 'How many articles per month?',
      options: [
        { value: '1-3', label: '13 articles/month' },
        { value: '4-8', label: '48 articles/month' },
        { value: '9-15', label: '915 articles/month' },
        { value: '15+', label: '15+ articles/month' },
      ],
    },
    {
      id: 'images_needed',
      question: 'Do you need AI-generated images?',
      options: [
        { value: 'yes', label: 'Yes, high-quality hero images' },
        { value: 'optional', label: 'Optional / nice-to-have' },
        { value: 'no', label: 'No, I\'ll upload my own' },
      ],
    },
    {
      id: 'provider_preference',
      question: '(Optional) Any provider preference?',
      options: [
        { value: null, label: 'No preference' },
        { value: 'google', label: 'Google Gemini' },
        { value: 'openai', label: 'OpenAI GPT' },
        { value: 'anthropic', label: 'Anthropic Claude' },
      ],
    },
  ];

  const handleAnswerChange = (answerId, value) => {
    setAnswers(prev => ({ ...prev, [answerId]: value }));
  };

  const handleNext = async () => {
    if (currentStep < questions.length - 1) {
      setCurrentStep(currentStep + 1);
    } else {
      // Submit to backend
      await submitRecommendation();
    }
  };

  const submitRecommendation = async () => {
    setLoading(true);
    try {
      const rec = await apiFetch({
        path: '/agentic-writer/v1/recommend-models',
        method: 'POST',
        data: answers,
      });
      setRecommendation(rec);
    } catch (error) {
      alert('Error generating recommendation: ' + error.message);
    } finally {
      setLoading(false);
    }
  };

  const handleApply = () => {
    onApplyConfig(recommendation);
    setIsOpen(false);
    setCurrentStep(0);
    setRecommendation(null);
  };

  const currentQuestion = questions[currentStep];
  const isAnswered = answers[currentQuestion.id] !== null;

  return (
    <>
      <Button 
        variant="primary" 
        onClick={() => setIsOpen(true)}
        style={{ marginTop: '10px' }}
      >
        Open Model Recommender
      </Button>

      {isOpen && (
        <Modal
          title="AI Model Recommender"
          onRequestClose={() => setIsOpen(false)}
          style={{ width: '500px' }}
        >
          {recommendation ? (
            // Results view
            <div style={{ padding: '20px' }}>
              <h3>Recommended Configuration</h3>
              <p><strong>{recommendation.preset_name}</strong></p>
              <p><em>{recommendation.rationale}</em></p>
              
              <table style={{ width: '100%', marginTop: '20px', borderCollapse: 'collapse' }}>
                <tbody>
                  <tr>
                    <td><strong>Chat</strong></td>
                    <td>{recommendation.models.chat}</td>
                  </tr>
                  <tr>
                    <td><strong>Clarity</strong></td>
                    <td>{recommendation.models.clarity}</td>
                  </tr>
                  <tr>
                    <td><strong>Planning</strong></td>
                    <td>{recommendation.models.planning}</td>
                  </tr>
                  <tr>
                    <td><strong>Writing</strong></td>
                    <td>{recommendation.models.writing}</td>
                  </tr>
                  <tr>
                    <td><strong>Refinement</strong></td>
                    <td>{recommendation.models.refinement}</td>
                  </tr>
                  <tr>
                    <td><strong>Image</strong></td>
                    <td>{recommendation.models.image || 'Disabled'}</td>
                  </tr>
                </tbody>
              </table>

              <p style={{ marginTop: '20px' }}>
                <strong>Estimated cost:</strong> ${recommendation.estimated_cost_per_article.toFixed(4)}/article
              </p>

              <div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
                <Button 
                  variant="primary" 
                  onClick={handleApply}
                >
                  Apply This Configuration
                </Button>
                <Button 
                  variant="secondary" 
                  onClick={() => {
                    setRecommendation(null);
                    setCurrentStep(0);
                  }}
                >
                  Go Back
                </Button>
              </div>
            </div>
          ) : (
            // Questions view
            <div style={{ padding: '20px' }}>
              <p><strong>Question {currentStep + 1} of {questions.length}</strong></p>
              <h3>{currentQuestion.question}</h3>

              <RadioControl
                selected={answers[currentQuestion.id]}
                options={currentQuestion.options}
                onChange={(value) => handleAnswerChange(currentQuestion.id, value)}
              />

              <div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
                <Button 
                  variant="primary" 
                  onClick={handleNext}
                  disabled={!isAnswered || loading}
                >
                  {loading ? <Spinner /> : (currentStep === questions.length - 1 ? 'Get Recommendation' : 'Next')}
                </Button>
                {currentStep > 0 && (
                  <Button 
                    variant="secondary" 
                    onClick={() => setCurrentStep(currentStep - 1)}
                  >
                    Back
                  </Button>
                )}
              </div>
            </div>
          )}
        </Modal>
      )}
    </>
  );
};

Conversation Examples

Example 1: Ultra-Budget Dev Blogger

User answers:

  • Goal: Dev blog
  • Budget: <$0.01/article
  • Volume: 2 articles/month
  • Images: No
  • Provider: No preference

Agent response:

{
  "preset_name": "Ultra-Budget: Gemini Flash Only",
  "rationale": "Your ultra-low budget and dev blog focus make Gemini 3 Flash the perfect fit. It's optimized for coding + reasoning at nearly free cost. No images keeps cost minimal.",
  "estimated_cost_per_article": 0.0082,
  "models": {
    "chat": "google/gemini-3-flash-preview",
    "clarity": "google/gemini-3-flash-preview",
    "planning": "google/gemini-3-flash-preview",
    "writing": "google/gemini-3-flash-preview",
    "refinement": "google/gemini-3-flash-preview",
    "image": null
  },
  "recommendations": {
    "when_to_use": "Solo dev bloggers with minimal budget; testing the plugin before investing.",
    "workflow_expectation": "Output may need 12 editing rounds before publishing."
  }
}

Example 2: Agency with Quality Focus

User answers:

  • Goal: Agency client content
  • Budget: >$0.20/article
  • Volume: 20 articles/month
  • Images: Yes, high-quality
  • Provider: No preference

Agent response:

{
  "preset_name": "Premium: GPT-5.2 + Gemini Flash + FLUX.2 max",
  "rationale": "Your agency volume and quality-first mindset justify premium models. GPT-5.2 delivers publication-ready prose; FLUX.2 max generates hero images. Your clients will notice the difference.",
  "estimated_cost_per_article": 0.3142,
  "models": {
    "chat": "google/gemini-3-flash-preview",
    "clarity": "anthropic/claude-sonnet-4",
    "planning": "google/gemini-3-flash-preview",
    "writing": "openai/gpt-5.2",
    "refinement": "openai/gpt-5.2",
    "image": "black-forest-labs/flux.2-max"
  },
  "recommendations": {
    "when_to_use": "Agencies publishing 15+ articles/month; thought leadership, flagship posts, client deliverables.",
    "workflow_expectation": "Minimal editing required; publish with high confidence."
  }
}

Example 3: Balanced Hobbyist

User answers:

  • Goal: Hobby writing / personal blog
  • Budget: $0.05$0.20/article
  • Volume: 4 articles/month
  • Images: Optional
  • Provider: Google Gemini

Agent response:

{
  "preset_name": "Balanced: Gemini Flash + Claude Sonnet + Riverflow",
  "rationale": "Your modest volume and provider preference align perfectly with the Balanced preset. All-Gemini planning + Claude writing = great quality at fair cost. Riverflow images optional but affordable.",
  "estimated_cost_per_article": 0.1359,
  "models": {
    "chat": "google/gemini-3-flash-preview",
    "clarity": "google/gemini-3-flash-preview",
    "planning": "google/gemini-3-flash-preview",
    "writing": "anthropic/claude-3.5-sonnet",
    "refinement": "anthropic/claude-3.5-sonnet",
    "image": "sourceful/riverflow-v2-max"
  },
  "recommendations": {
    "when_to_use": "Regular hobby blogging or personal portfolio; professional output without premium cost.",
    "workflow_expectation": "One refinement cycle typical; polished final product."
  }
}

Edge Cases & Fallbacks

1. API failure (OpenRouter down)

Fallback behavior:

// If recommender API fails, load from static presets
if ( is_wp_error( $recommendation ) ) {
    $recommendation = self::get_fallback_preset_by_budget( 
        $params['budget_per_article']
    );
}

2. Budget = "no_limit"

Logic:

  • Treat as ">$0.20" (Premium tier).
  • Recommend frontier models + best images.

3. Invalid JSON from agent

Fallback:

  • Log error to error_log.
  • Show message: "Recommender had trouble generating a config. Try adjusting your answers."
  • Offer manual preset picker instead.

4. User cancels mid-flow

Behavior:

  • Modal closes cleanly.
  • No settings changed.
  • State resets for next usage.

Development Checklist

  • Backend setup

    • Create includes/class-model-recommender.php
    • Register REST endpoint /agentic-writer/v1/recommend-models
    • Implement call_recommender_agent() with OpenRouter API
    • Add error handling + fallbacks
    • Test with real OpenRouter key
  • System prompt

    • Write and refine system prompt (see above)
    • Test with 35 example user inputs
    • Verify JSON output is parseable
    • Ensure cost estimates match model-presets.md
  • Frontend component

    • Create React component: ModelRecommender.jsx
    • Implement question flow (5 questions)
    • Add results display with config summary
    • Wire up "Apply Configuration" button
    • Test modal UX (open/close/back/next)
  • Integration

    • Add button to settings page → "AI Model Configuration" card
    • Wire up onApplyConfig callback to save settings
    • Refresh model selectors after apply
    • Show success toast message
  • Testing

    • Test all 5 questions with valid OpenRouter API key
    • Test budget boundary cases ($0.01, $0.05, $0.20)
    • Test edge cases (ultra-budget, no-limit, specific providers)
    • Test API failure + fallback behavior
    • Test invalid JSON response handling
    • Test user cancellation mid-flow
  • Documentation

    • Add to settings help: "Click 'Open Model Recommender' for personalized setup"
    • Document system prompt in code comments
    • Add troubleshooting guide: "Recommender not working? Check API key."

Security Considerations

  1. API Key Protection:

    • Only server-side calls to OpenRouter (key never exposed to client)
    • REST endpoint requires manage_options capability
  2. Rate Limiting:

    • Add WordPress nonce to prevent CSRF
    • Limit calls to 1 per minute per user
  3. Input Validation:

    • Validate all user answers against whitelist
    • Reject invalid budget ranges
// Example: Add nonce validation
register_rest_route(
    'agentic-writer/v1',
    '/recommend-models',
    [
        'callback' => [ self::class, 'get_recommendation' ],
        'permission_callback' => function () {
            return current_user_can( 'manage_options' ) 
                && isset( $_REQUEST['_wpnonce'] )
                && wp_verify_nonce( $_REQUEST['_wpnonce'], 'agentic_recommender' );
        }
    ]
);

Cost Estimation for Development

Task Time Notes
Backend endpoint + system prompt 34 hours Includes testing with OpenRouter
React component + UI 34 hours Includes modal, form flow, validation
Integration with settings page 12 hours Wire up button, callbacks, save logic
Testing + refinement 23 hours Edge cases, error handling
Total 913 hours Can be split across team

Next Steps (Priority Order)

  1. Refine system prompt Test with Claude to ensure it generates correct JSON
  2. Build backend endpoint Implement /recommend-models route with error handling
  3. Build React component Create modal UI with question flow
  4. Integration Wire up to settings page
  5. Testing Full QA with real OpenRouter API

Document version: 1.0
Date: January 22, 2026
Author: WP Agentic Writer Product Team
Status: Ready for Development