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

> Explore the Prompt Explorer feature in Open-SEO. Run one prompt across four LLM models, compare responses side-by-side with citations, and highlight brand mentions. Optimize your AI strategy today.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-08-17

---

**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](https://github.com/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/ai-search.ts) using TanStack's `createServerFn`, which automatically serializes the request payload.

```tsx
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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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:

```ts
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`](https://github.com/every-app/open-seo/blob/main/src/client/features/ai-search/components/PromptExplorerForm.tsx):

```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`](https://github.com/every-app/open-seo/blob/main/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:

```ts
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.