diff --git a/typescript-recipes/parallel-vercel-template/README.md b/typescript-recipes/parallel-vercel-template/README.md index 88712e5..ff84e9a 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, 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 ## 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..08814f4 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,38 @@ 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); + } + + const searchMode = mode || "basic"; + 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..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,8 +14,7 @@ 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); + 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) ("parallel-search-demo", INITIAL_STATE); + useSessionStorage("parallel-search-demo-v1", INITIAL_STATE); const [loading, setLoading] = useState(false); @@ -108,7 +108,7 @@ export default function SearchDemo() { Search Objective

- {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/package-lock.json b/typescript-recipes/parallel-vercel-template/package-lock.json index 7385e5e..b4092ae 100644 --- a/typescript-recipes/parallel-vercel-template/package-lock.json +++ b/typescript-recipes/parallel-vercel-template/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "next": "16.2.3", - "parallel-web": "^0.2.4", + "parallel-web": "^1.3.3", "react": "19.2.3", "react-dom": "19.2.3" }, @@ -5292,9 +5292,9 @@ } }, "node_modules/parallel-web": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/parallel-web/-/parallel-web-0.2.4.tgz", - "integrity": "sha512-684TtVFf0wvyZ83FMrhxX1Qg0+PDjsB7bPu0SXBCRVywxSeOloFIKHPKiAsAKWpDgAz3ok/bZmd2CAXImqH3Iw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parallel-web/-/parallel-web-1.3.3.tgz", + "integrity": "sha512-TX8zRvWQ+OSF9U4uuc5kyGx83t1c9AlgT/dRhpCgngYLMN2e0rMiH1Mhmnlmv7WTVn/jAdhtT7cAuJYyEwgXRQ==", "license": "MIT" }, "node_modules/parent-module": { diff --git a/typescript-recipes/parallel-vercel-template/package.json b/typescript-recipes/parallel-vercel-template/package.json index 8846a4f..2218a3a 100644 --- a/typescript-recipes/parallel-vercel-template/package.json +++ b/typescript-recipes/parallel-vercel-template/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "next": "16.2.3", - "parallel-web": "^0.2.4", + "parallel-web": "^1.3.3", "react": "19.2.3", "react-dom": "19.2.3" }, diff --git a/typescript-recipes/parallel-vercel-template/pnpm-lock.yaml b/typescript-recipes/parallel-vercel-template/pnpm-lock.yaml index 79674a4..dca53c9 100644 --- a/typescript-recipes/parallel-vercel-template/pnpm-lock.yaml +++ b/typescript-recipes/parallel-vercel-template/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: 16.2.3 version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) parallel-web: - specifier: ^0.2.4 - version: 0.2.4 + specifier: ^1.3.3 + version: 1.3.3 react: specifier: 19.2.3 version: 19.2.3 @@ -1567,8 +1567,8 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=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..2bf5b87 --- /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: "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" }, { objective: "x", mode: "agentic" }]) { + 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; + } +});