From 54201947f2598c0d7ab3066018acb28017011162 Mon Sep 17 00:00:00 2001 From: George Pickett Date: Thu, 10 Sep 2026 15:53:51 -0700 Subject: [PATCH 1/2] Fix Vercel starter Search and Extract v1 requests --- .../parallel-vercel-template/README.md | 14 +-- .../app/api/extract/route.ts | 6 +- .../app/api/search/route.ts | 38 ++++++-- .../app/api/tasks/[runId]/events/route.ts | 4 +- .../app/components/ExtractDemo.tsx | 2 +- .../app/components/SearchDemo.tsx | 2 +- .../package-lock.json | 8 +- .../parallel-vercel-template/package.json | 2 +- .../parallel-vercel-template/pnpm-lock.yaml | 10 +-- .../tests/api-routes.test.mjs | 90 +++++++++++++++++++ 10 files changed, 146 insertions(+), 30 deletions(-) create mode 100644 typescript-recipes/parallel-vercel-template/tests/api-routes.test.mjs diff --git a/typescript-recipes/parallel-vercel-template/README.md b/typescript-recipes/parallel-vercel-template/README.md index 88712e5..31c8deb 100644 --- a/typescript-recipes/parallel-vercel-template/README.md +++ b/typescript-recipes/parallel-vercel-template/README.md @@ -29,18 +29,18 @@ View the demo at: │ localStorage │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ -│ SearchDemo │ │ /api/search │ → client.beta.search() -│ ExtractDemo │ │ /api/extract │ → client.beta.extract() +│ SearchDemo │ │ /api/search │ → client.search() +│ ExtractDemo │ │ /api/extract │ → client.extract() │ TasksDemo │ │ /api/tasks │ → client.taskRun.create() └─────────────────┘ │ /api/tasks/[id]/status │ → client.taskRun.retrieve() - │ /api/tasks/[id]/events │ → client.beta.taskRun.events() + │ /api/tasks/[id]/events │ → client.taskRun.events() └─────────────────┘ ``` ### How It Works -1. **Search**: User enters a search objective and optional queries → API calls `client.beta.search()` → Returns ranked results with excerpts -2. **Extract**: User enters URLs and optional objective → API calls `client.beta.extract()` → Returns extracted content +1. **Search**: User enters a search objective and optional queries → API calls `client.search()` → Returns ranked results with excerpts. The route uses the objective as a query when queries are omitted, and maps one-shot to `basic` and agentic to `advanced` +2. **Extract**: User enters URLs and optional objective → API calls `client.extract()` → Returns extracted content 3. **Tasks**: User enters a research task → API calls `client.taskRun.create()` → SSE stream delivers real-time progress → Final output displayed on completion ## Quick Start @@ -126,8 +126,8 @@ Go to your [Vercel Integration page](https://vercel.com/marketplace/parallel), s ## Resources - [Parallel Documentation](https://docs.parallel.ai) -- [Search API Reference](https://docs.parallel.ai/api-reference/search-beta/search) -- [Extract API Reference](https://docs.parallel.ai/api-reference/extract-beta/extract) +- [Search API Reference](https://docs.parallel.ai/api-reference/search/search) +- [Extract API Reference](https://docs.parallel.ai/api-reference/extract/extract) - [Tasks API Reference](https://docs.parallel.ai/api-reference/tasks-v1/create-task-run) - [SSE Streaming Guide](https://docs.parallel.ai/task-api/task-sse) - [Pricing](https://docs.parallel.ai/resources/pricing) diff --git a/typescript-recipes/parallel-vercel-template/app/api/extract/route.ts b/typescript-recipes/parallel-vercel-template/app/api/extract/route.ts index b13aabf..72ab6a8 100644 --- a/typescript-recipes/parallel-vercel-template/app/api/extract/route.ts +++ b/typescript-recipes/parallel-vercel-template/app/api/extract/route.ts @@ -16,11 +16,11 @@ export async function POST(request: NextRequest) { const client = getParallelClient(); - const extractResult = await client.beta.extract({ + const extractResult = await client.extract({ urls, objective: objective?.trim() || undefined, - excerpts: true, - full_content: false, + // Excerpts are enabled by default in v1. + advanced_settings: { full_content: false }, }); return NextResponse.json(extractResult); diff --git a/typescript-recipes/parallel-vercel-template/app/api/search/route.ts b/typescript-recipes/parallel-vercel-template/app/api/search/route.ts index d4dafa5..f74af7b 100644 --- a/typescript-recipes/parallel-vercel-template/app/api/search/route.ts +++ b/typescript-recipes/parallel-vercel-template/app/api/search/route.ts @@ -11,18 +11,44 @@ export async function POST(request: NextRequest) { const body = await request.json(); const { objective, searchQueries, mode, maxResults } = body; - if (!objective) { + if (typeof objective !== "string" || !objective.trim()) { return errorResponse("objective is required", 400); } + if ( + searchQueries !== undefined && + (!Array.isArray(searchQueries) || + searchQueries.some((query: unknown) => typeof query !== "string")) + ) { + return errorResponse("searchQueries must be an array of strings", 400); + } + + // Preserve the demo's legacy mode selections using their current equivalents. + const searchMode = + mode === "agentic" + ? "advanced" + : mode === "one-shot" || !mode + ? "basic" + : mode; + if (!["turbo", "fast", "basic", "advanced"].includes(searchMode)) { + return errorResponse("Unsupported search mode", 400); + } + const queries = (searchQueries ?? []) + .map((query: string) => query.trim()) + .filter(Boolean); const client = getParallelClient(); - const searchResult = await client.beta.search({ + const searchResult = await client.search({ objective, - search_queries: searchQueries || undefined, - mode: mode || "one-shot", - max_results: maxResults || SEARCH_DEFAULTS.MAX_RESULTS, - max_chars_per_result: SEARCH_DEFAULTS.MAX_CHARS_PER_RESULT, + // Keep objective-only submissions working with v1's required queries. + search_queries: queries.length ? queries : [objective.trim()], + mode: searchMode, + advanced_settings: { + max_results: maxResults || SEARCH_DEFAULTS.MAX_RESULTS, + excerpt_settings: { + max_chars_per_result: SEARCH_DEFAULTS.MAX_CHARS_PER_RESULT, + }, + }, }); return NextResponse.json(searchResult); diff --git a/typescript-recipes/parallel-vercel-template/app/api/tasks/[runId]/events/route.ts b/typescript-recipes/parallel-vercel-template/app/api/tasks/[runId]/events/route.ts index 21211ef..a8f5464 100644 --- a/typescript-recipes/parallel-vercel-template/app/api/tasks/[runId]/events/route.ts +++ b/typescript-recipes/parallel-vercel-template/app/api/tasks/[runId]/events/route.ts @@ -14,8 +14,8 @@ export async function GET( try { const client = getParallelClient(); - // Use the SDK's beta.taskRun.events() method to get the event stream - const eventStream = await client.beta.taskRun.events(runId); + // Use the SDK's taskRun.events() method to get the event stream + const eventStream = await client.taskRun.events(runId); // Create a ReadableStream that converts SDK events to SSE format const stream = new ReadableStream({ diff --git a/typescript-recipes/parallel-vercel-template/app/components/ExtractDemo.tsx b/typescript-recipes/parallel-vercel-template/app/components/ExtractDemo.tsx index a2d64a8..22fe928 100644 --- a/typescript-recipes/parallel-vercel-template/app/components/ExtractDemo.tsx +++ b/typescript-recipes/parallel-vercel-template/app/components/ExtractDemo.tsx @@ -103,7 +103,7 @@ export default function ExtractDemo() { URLs to Extract (one per line) =10'} - parallel-web@0.2.4: - resolution: {integrity: sha512-684TtVFf0wvyZ83FMrhxX1Qg0+PDjsB7bPu0SXBCRVywxSeOloFIKHPKiAsAKWpDgAz3ok/bZmd2CAXImqH3Iw==} + parallel-web@1.3.3: + resolution: {integrity: sha512-TX8zRvWQ+OSF9U4uuc5kyGx83t1c9AlgT/dRhpCgngYLMN2e0rMiH1Mhmnlmv7WTVn/jAdhtT7cAuJYyEwgXRQ==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -3556,7 +3556,7 @@ snapshots: dependencies: p-limit: 3.1.0 - parallel-web@0.2.4: {} + parallel-web@1.3.3: {} parent-module@1.0.1: dependencies: diff --git a/typescript-recipes/parallel-vercel-template/tests/api-routes.test.mjs b/typescript-recipes/parallel-vercel-template/tests/api-routes.test.mjs new file mode 100644 index 0000000..501e786 --- /dev/null +++ b/typescript-recipes/parallel-vercel-template/tests/api-routes.test.mjs @@ -0,0 +1,90 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import Module from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load route handlers without a Next server, keeping the real SDK and serializer. +function load(relativePath) { + const filename = path.resolve(dirname, "..", relativePath); + const compiled = ts.transpileModule(fs.readFileSync(filename, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + }).outputText; + const mod = new Module(filename); + mod.filename = filename; + mod.paths = Module._nodeModulePaths(path.dirname(filename)); + const originalRequire = mod.require.bind(mod); + mod.require = (name) => name === "@/lib/parallel" ? load("lib/parallel.ts") : originalRequire(name); + mod._compile(compiled, filename); + return mod.exports; +} + +const post = (body) => new Request("http://localhost/api", { + method: "POST", body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, +}); + +test("routes serialize v1 requests and preserve response and Task contracts", async () => { + const previousFetch = global.fetch; + const previousKey = process.env.PARALLEL_API_KEY; + process.env.PARALLEL_API_KEY = "test-key"; + const calls = []; + let responseBody = { results: [] }; + global.fetch = async (url, options) => { + calls.push({ path: new URL(url).pathname, body: options.body && JSON.parse(options.body) }); + if (String(url).endsWith("/events")) { + return new Response('event: task_run.status\ndata: {"type":"task_run.status","status":"completed"}\n\n', { + headers: { "Content-Type": "text/event-stream" }, + }); + } + return Response.json(responseBody); + }; + try { + const search = load("app/api/search/route.ts"); + assert.deepEqual(await (await search.POST(post({ objective: "AI safety" }))).json(), responseBody); + assert.deepEqual(calls.pop(), { path: "/v1/search", body: { + objective: "AI safety", search_queries: ["AI safety"], mode: "basic", + advanced_settings: { max_results: 10, excerpt_settings: { max_chars_per_result: 2500 } }, + } }); + await search.POST(post({ objective: "AI safety", searchQueries: [" alignment ", " "], mode: "agentic", maxResults: 3 })); + assert.deepEqual(calls.at(-1).body.search_queries, ["alignment"]); + assert.equal(calls.at(-1).body.mode, "advanced"); + assert.equal(calls.at(-1).body.advanced_settings.max_results, 3); + await search.POST(post({ objective: "AI safety", searchQueries: [" "] })); + assert.deepEqual(calls.at(-1).body.search_queries, ["AI safety"]); + const count = calls.length; + for (const body of [{ objective: " " }, { objective: "x", searchQueries: "x" }, { objective: "x", mode: "invalid" }]) { + assert.equal((await search.POST(post(body))).status, 400); + } + assert.equal(calls.length, count); + const extract = load("app/api/extract/route.ts"); + assert.deepEqual(await (await extract.POST(post({ urls: ["https://example.com"], objective: " facts " }))).json(), responseBody); + assert.deepEqual(calls.pop(), { path: "/v1/extract", body: { + urls: ["https://example.com"], objective: "facts", advanced_settings: { full_content: false }, + } }); + const tasks = load("app/api/tasks/route.ts"); + responseBody = { run_id: "run_test", status: "queued" }; + for (const [processor, type] of [["lite", "text"], ["pro", "auto"]]) { + assert.deepEqual(await (await tasks.POST(post({ input: "Research", processor }))).json(), responseBody); + assert.deepEqual(calls.pop(), { path: "/v1/tasks/runs", body: { input: "Research", processor, task_spec: { output_schema: { type } } } }); + } + const params = { params: Promise.resolve({ runId: "run_test" }) }; + const status = load("app/api/tasks/[runId]/status/route.ts"); + responseBody = { status: "completed", output: { content: "Done" } }; + assert.deepEqual(await (await status.GET(null, params)).json(), responseBody); + assert.deepEqual(calls.slice(-2).map((call) => call.path), ["/v1/tasks/runs/run_test", "/v1/tasks/runs/run_test/result"]); + const events = load("app/api/tasks/[runId]/events/route.ts"); + const stream = await events.GET(null, params); + assert.equal(stream.headers.get("Content-Type"), "text/event-stream"); + assert.equal(await stream.text(), 'data: {"type":"task_run.status","status":"completed"}\n\n'); + assert.equal(calls.at(-1).path, "/v1/tasks/runs/run_test/events"); + } finally { + global.fetch = previousFetch; + if (previousKey === undefined) delete process.env.PARALLEL_API_KEY; + else process.env.PARALLEL_API_KEY = previousKey; + } +}); From 04042c8eab59e3dfe6be2c06f2fca4f80131ec69 Mon Sep 17 00:00:00 2001 From: George Pickett Date: Thu, 10 Sep 2026 16:18:07 -0700 Subject: [PATCH 2/2] Use current Search modes directly in Vercel demo --- .../parallel-vercel-template/README.md | 2 +- .../app/api/search/route.ts | 8 +------ .../app/api/tasks/[runId]/events/route.ts | 1 - .../app/components/SearchDemo.tsx | 24 +++++++++---------- .../tests/api-routes.test.mjs | 4 ++-- 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/typescript-recipes/parallel-vercel-template/README.md b/typescript-recipes/parallel-vercel-template/README.md index 31c8deb..ff84e9a 100644 --- a/typescript-recipes/parallel-vercel-template/README.md +++ b/typescript-recipes/parallel-vercel-template/README.md @@ -39,7 +39,7 @@ View the demo at: ### How It Works -1. **Search**: User enters a search objective and optional queries → API calls `client.search()` → Returns ranked results with excerpts. The route uses the objective as a query when queries are omitted, and maps one-shot to `basic` and agentic to `advanced` +1. **Search**: User enters a search objective and optional queries → API calls `client.search()` → Returns ranked results with excerpts. The route uses the objective as a query when queries are omitted, with a choice of `basic` or `advanced` search 2. **Extract**: User enters URLs and optional objective → API calls `client.extract()` → Returns extracted content 3. **Tasks**: User enters a research task → API calls `client.taskRun.create()` → SSE stream delivers real-time progress → Final output displayed on completion diff --git a/typescript-recipes/parallel-vercel-template/app/api/search/route.ts b/typescript-recipes/parallel-vercel-template/app/api/search/route.ts index f74af7b..08814f4 100644 --- a/typescript-recipes/parallel-vercel-template/app/api/search/route.ts +++ b/typescript-recipes/parallel-vercel-template/app/api/search/route.ts @@ -23,13 +23,7 @@ export async function POST(request: NextRequest) { return errorResponse("searchQueries must be an array of strings", 400); } - // Preserve the demo's legacy mode selections using their current equivalents. - const searchMode = - mode === "agentic" - ? "advanced" - : mode === "one-shot" || !mode - ? "basic" - : mode; + const searchMode = mode || "basic"; if (!["turbo", "fast", "basic", "advanced"].includes(searchMode)) { return errorResponse("Unsupported search mode", 400); } diff --git a/typescript-recipes/parallel-vercel-template/app/api/tasks/[runId]/events/route.ts b/typescript-recipes/parallel-vercel-template/app/api/tasks/[runId]/events/route.ts index a8f5464..5890df1 100644 --- a/typescript-recipes/parallel-vercel-template/app/api/tasks/[runId]/events/route.ts +++ b/typescript-recipes/parallel-vercel-template/app/api/tasks/[runId]/events/route.ts @@ -14,7 +14,6 @@ export async function GET( try { const client = getParallelClient(); - // Use the SDK's taskRun.events() method to get the event stream const eventStream = await client.taskRun.events(runId); // Create a ReadableStream that converts SDK events to SSE format diff --git a/typescript-recipes/parallel-vercel-template/app/components/SearchDemo.tsx b/typescript-recipes/parallel-vercel-template/app/components/SearchDemo.tsx index 24b9723..ecc75d6 100644 --- a/typescript-recipes/parallel-vercel-template/app/components/SearchDemo.tsx +++ b/typescript-recipes/parallel-vercel-template/app/components/SearchDemo.tsx @@ -14,7 +14,7 @@ interface SearchResponse { error?: string; } -type SearchMode = "one-shot" | "agentic"; +type SearchMode = "basic" | "advanced"; interface StoredSearchState { objective: string; @@ -27,14 +27,14 @@ interface StoredSearchState { const INITIAL_STATE: StoredSearchState = { objective: "", searchQueries: "", - mode: "one-shot", + mode: "basic", results: [], error: null, }; export default function SearchDemo() { const [storedState, setStoredState, clearStoredState, isHydrated] = - useSessionStorage("parallel-search-demo", INITIAL_STATE); + useSessionStorage("parallel-search-demo-v1", INITIAL_STATE); const [loading, setLoading] = useState(false); @@ -150,31 +150,31 @@ export default function SearchDemo() {

- {mode === "one-shot" - ? "Comprehensive results with longer excerpts for single-query answers" - : "Concise, token-efficient results for use in agentic loops"} + {mode === "basic" + ? "Low-latency search, best with 2-3 focused queries" + : "Higher-quality search with more advanced retrieval and compression"}

diff --git a/typescript-recipes/parallel-vercel-template/tests/api-routes.test.mjs b/typescript-recipes/parallel-vercel-template/tests/api-routes.test.mjs index 501e786..2bf5b87 100644 --- a/typescript-recipes/parallel-vercel-template/tests/api-routes.test.mjs +++ b/typescript-recipes/parallel-vercel-template/tests/api-routes.test.mjs @@ -50,14 +50,14 @@ test("routes serialize v1 requests and preserve response and Task contracts", as objective: "AI safety", search_queries: ["AI safety"], mode: "basic", advanced_settings: { max_results: 10, excerpt_settings: { max_chars_per_result: 2500 } }, } }); - await search.POST(post({ objective: "AI safety", searchQueries: [" alignment ", " "], mode: "agentic", maxResults: 3 })); + await search.POST(post({ objective: "AI safety", searchQueries: [" alignment ", " "], mode: "advanced", maxResults: 3 })); assert.deepEqual(calls.at(-1).body.search_queries, ["alignment"]); assert.equal(calls.at(-1).body.mode, "advanced"); assert.equal(calls.at(-1).body.advanced_settings.max_results, 3); await search.POST(post({ objective: "AI safety", searchQueries: [" "] })); assert.deepEqual(calls.at(-1).body.search_queries, ["AI safety"]); const count = calls.length; - for (const body of [{ objective: " " }, { objective: "x", searchQueries: "x" }, { objective: "x", mode: "invalid" }]) { + for (const body of [{ objective: " " }, { objective: "x", searchQueries: "x" }, { objective: "x", mode: "invalid" }, { objective: "x", mode: "agentic" }]) { assert.equal((await search.POST(post(body))).status, 400); } assert.equal(calls.length, count);