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:
-
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. -
Parallel execution: Each unique model triggers a
runModelcall wrapped inPromise.allSettled(lines 57-66). This pattern ensures that a timeout or error from Claude does not block the Gemini response. -
Result aggregation: Settled promises are mapped to either successful
PromptExplorerModelResultobjects or error states viamapErrorToResult(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
systemPromptVversion 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.allSettledfor 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
createServerFnpattern 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →