How the Prompt Explorer Feature Works in Open-SEO: A Technical Deep Dive

The Prompt Explorer feature lets users run a single prompt simultaneously against up to four LLM models (ChatGPT, Claude, Gemini, Perplexity) and compare responses side-by-side with source citations and optional brand highlighting.

The prompt explorer feature in the every-app/open-seo repository enables comprehensive AI model comparison by orchestrating parallel LLM calls, intelligent caching via Cloudflare R2, and real-time brand mention detection. This article examines the complete architecture from React client to serverless functions, revealing how the system minimizes latency and cost while maximizing reliability through graceful degradation.

Architecture Overview

The implementation follows a modern full-stack pattern using TanStack Start deployed on Cloudflare Workers. The client layer uses React Query for state management, while the server layer leverages createServerFn for type-safe API routes. At the core, the system interfaces with DataForSEO's LLM API and caches responses in Cloudflare R2 to reduce redundant compute costs.

Execution Flow: From Browser to LLM and Back

Client-Side Initialization and Form Submission

The feature mounts at the route /p/:projectId/prompt-explorer, which renders PromptExplorerPage from src/client/features/ai-search/PromptExplorerPage.tsx. This page composes PromptExplorerForm for user input and manages URL state for the prompt, selected models, and highlight brand.

When a user clicks Run, the form triggers a React Query mutation that calls explorePrompt. This function is defined as a server function in src/serverFunctions/ai-search.ts using TanStack's createServerFn, which automatically serializes the request payload.

import { PromptExplorerPage } from "@/client/features/ai-search/PromptExplorerPage";

export function ProjectPromptExplorer({ projectId }: { projectId: string }) {
  const [urlState, setUrlState] = useState({
    prompt: "",
    highlightBrand: "",
    models: [] as PromptExplorerModel[],
    webSearch: false,
    webSearchCountryCode: "US" as WebSearchCountryCode,
  });

  return (
    <PromptExplorerPage
      projectId={projectId}
      urlState={urlState}
      onSubmit={setUrlState}
    />
  );
}

Server-Side Validation and Access Control

Upon receiving the request, explorePrompt validates the payload against promptExplorerInputSchema and immediately invokes assertPaidPlan. In hosted deployments, this checks the organization's subscription status via customerHasPaidPlan in src/server/billing/subscription.ts. Self-hosted installations bypass this gate entirely.

Core Service Orchestration

After passing the guard clause, execution flows to runExplorePrompt in src/server/features/ai-search/services/promptExplorer.ts. This service performs three critical operations:

  1. Model deduplication: The service sanitizes the input models array using Array.from(new Set(input.models)) (lines 53-56) to prevent redundant API calls when duplicate models are specified.

  2. Parallel execution: Each unique model triggers a runModel call wrapped in Promise.allSettled (lines 57-66). This pattern ensures that a timeout or error from Claude does not block the Gemini response.

  3. Result aggregation: Settled promises are mapped to either successful PromptExplorerModelResult objects or error states via mapErrorToResult (lines 69-75).

Caching Layer and Cache Key Strategy

Before invoking external APIs, runModel generates a deterministic cache key using buildCacheKey (lines 96-108). The key incorporates:

  • Organization ID and project ID
  • Model identifier
  • Normalized prompt text
  • Web search flags and country code
  • systemPromptV version number

Crucially, the highlight brand is excluded from the cache key. This design decision allows the system to cache the raw LLM response once and reuse it for any brand-highlight query, applying the highlighting logic post-retrieval.

If R2 contains a valid cached entry with status "success", the service returns the stored payload immediately. Fresh responses are cached for 7 days (PROMPT_RESPONSE_TTL_SECONDS) using waitUntil to ensure the cache write does not delay the HTTP response (lines 22-28).

LLM Integration and Response Processing

For cache misses, fetchModelResponse contacts DataForSEO's LLM endpoint (dataforseo.aiSearch.llmResponse) with the model slug, user prompt, web-search configuration, and PROMPT_RESPONSE_MAX_TOKENS limit (lines 43-51).

The raw response undergoes shaping via shapeSuccess, which:

  • Extracts plain text using extractText
  • Parses citations via extractCitations
  • Caps fan-out queries
  • Records token usage metrics (lines 60-82)

Subsequently, reapplyHighlightBrand processes the content (lines 84-102). It marks citations containing the target brand using matchesBrand and scans answer text with a word-boundary-aware regex (mentionRegex). This post-processing step enables brand highlighting without invalidating the underlying cache.

Aggregation and Client Delivery

Once all Promise.allSettled calls complete, the service assembles the final PromptExplorerResult containing the original prompt, highlight brand, fetch timestamp, and per-model results array. The client's exploreQuery hook receives this payload, and PromptExplorerResults.tsx renders the side-by-side comparison view.

Key Architectural Patterns

Server Function Pattern with TanStack Start

The explorePrompt function exemplifies the ServerFn pattern, providing end-to-end type safety between the React client and Cloudflare Workers. This eliminates the need for manual API route definitions and ensures input validation at the network boundary.

Intelligent Caching for Cost Optimization

The R2 caching strategy reduces DataForSEO API costs by storing responses for one week. By excluding the highlight brand from the cache key, the system supports unlimited brand-highlight variations per cached LLM response, significantly reducing compute for repeated queries.

Fault Isolation via Promise.allSettled

The architecture treats each LLM provider as an independent failure domain. Using Promise.allSettled rather than Promise.all ensures that transient outages or rate limits from one provider do not cascade to the entire comparison view. Failed models display error states while successful models render normally.

Implementation Examples

Directly Invoking the Server Function

For programmatic access or custom components, import explorePrompt directly:

import { explorePrompt } from "@/serverFunctions/ai-search";

async function runPrompt(projectId: string) {
  const result = await explorePrompt({
    data: {
      projectId,
      prompt: "What is the difference between GPT‑4 and Gemini‑2.5‑pro?",
      models: ["chat_gpt", "gemini"],
      highlightBrand: "OpenAI",
      webSearch: false,
      webSearchCountryCode: "US",
    },
  });

  console.log(result); // => PromptExplorerResult
}

Adding a New Model to the UI Selector

Extend the available models by modifying the options array in src/client/features/ai-search/components/PromptExplorerForm.tsx:

const MODEL_OPTIONS = [
  { value: "chat_gpt", label: "ChatGPT" },
  { value: "claude", label: "Claude" },
  { value: "gemini", label: "Gemini" },
  { value: "perplexity", label: "Perplexity" },
  // add a new option here
];

The server-side MODEL_NAMES mapping in promptExplorer.ts automatically translates the UI value to the appropriate DataForSEO model slug.

Inspecting the Cache Key for Debugging

To verify cache behavior or debug hits/misses:

import { buildCacheKey } from "@/server/lib/r2-cache";

async function debugKey() {
  const key = await buildCacheKey("ai-search-prompt", {
    organizationId: "org_123",
    projectId: "proj_456",
    model: "chat_gpt",
    prompt: "Explain SEO basics",
    webSearch: false,
    webSearchCountryCode: null,
    systemPromptV: 5,
  });
  console.log(key); // e.g. "ai-search-prompt|org_123|proj_456|chat_gpt|Explain SEO basics|false|null|5"
}

Summary

  • The prompt explorer feature executes prompts against up to four LLM models simultaneously using Promise.allSettled for fault isolation.
  • Cloudflare R2 caches responses for 7 days using a deterministic key that excludes the highlight brand, enabling efficient reuse across different brand queries.
  • DataForSEO serves as the upstream LLM provider, with responses shaped and processed to extract citations and text.
  • The TanStack Start createServerFn pattern provides type-safe client-server communication with built-in validation via Zod schemas.
  • Graceful degradation ensures that individual model failures do not compromise the entire comparison view.

Frequently Asked Questions

How does the prompt explorer feature handle different LLM models simultaneously?

The system deduplicates the requested models array, then invokes runModel for each unique provider via Promise.allSettled. This parallel execution strategy ensures that slow responses or errors from one model do not block others, with results aggregated into a unified response object.

Why is brand highlighting not included in the R2 cache key?

Excluding the highlight brand from the cache key allows the system to store one canonical LLM response and reuse it for any brand-highlight permutation. When retrieving from cache, the service reapplies highlighting via reapplyHighlightBrand, significantly reducing API costs for users running the same prompt with different brand targets.

What happens if one LLM model fails while others succeed?

The Promise.allSettled pattern captures both fulfilled and rejected promises. Failed models are transformed into error results via mapErrorToResult, displaying a failure message in the UI, while successful model responses render normally. This isolation prevents total feature failure during provider outages.

How long are LLM responses cached in the prompt explorer feature?

Responses are cached in Cloudflare R2 for 7 days (defined by PROMPT_RESPONSE_TTL_SECONDS). The cache write operation uses waitUntil to run asynchronously, ensuring that cache persistence does not add latency to the HTTP response.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →