serpkite 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -8
- package/dist/index.cjs +311 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1363 -53
- package/dist/index.d.ts +1363 -53
- package/dist/index.js +311 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48,8 +48,9 @@ function errorFromResponse(status, headers, body) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// src/client.ts
|
|
51
|
-
var VERSION = "0.
|
|
51
|
+
var VERSION = "0.8.0";
|
|
52
52
|
var DEFAULT_BASE_URL = "https://api.serpkite.com";
|
|
53
|
+
var ASSISTANT_TIMEOUT_MS = 12e4;
|
|
53
54
|
var TASK_DONE = /* @__PURE__ */ new Set(["completed", "failed", "canceled"]);
|
|
54
55
|
var SerpKite = class {
|
|
55
56
|
baseUrl;
|
|
@@ -122,6 +123,24 @@ var SerpKite = class {
|
|
|
122
123
|
autocomplete = this.#vertical("/v1/autocomplete");
|
|
123
124
|
/** Fetch any public URL as clean Markdown plus metadata. */
|
|
124
125
|
webpage = this.#vertical("/v1/webpage");
|
|
126
|
+
// ── AI assistants ─────────────────────────────────────────────────────
|
|
127
|
+
// Billed even when the response is lost: retried only on 429 (failed, refused and empty
|
|
128
|
+
// answers are free), and each attempt waits at least 120 s (answers take 10-90 s).
|
|
129
|
+
/**
|
|
130
|
+
* Ask ChatGPT as a logged-out user: the answer (`text`, `markdown`), cited `sources`,
|
|
131
|
+
* `search_queries`, `products`, `places` and the guest `model`. `web_search` (default true)
|
|
132
|
+
* makes it search the web; `country` asks from an exit in that country. `track` returns
|
|
133
|
+
* `mentions` of your brands and domains. 5 credits.
|
|
134
|
+
*/
|
|
135
|
+
chatgpt = this.#assistant("/v1/chatgpt");
|
|
136
|
+
/** Ask Google Gemini as a logged-out user: the answer with the sources it grounded on. 5 credits. */
|
|
137
|
+
gemini = this.#assistant("/v1/gemini");
|
|
138
|
+
/** Microsoft Copilot Search on Bing: the answer, cited sources and products. 3 credits. */
|
|
139
|
+
copilot = this.#assistant("/v1/copilot");
|
|
140
|
+
/** Google AI Mode: the answer, cited sources and products. 3 credits. */
|
|
141
|
+
aiMode = this.#assistant("/v1/ai_mode");
|
|
142
|
+
/** Brave Search's AI answer: cited sources, the searches it ran and products. 2 credits. */
|
|
143
|
+
braveAi = this.#assistant("/v1/brave_ai");
|
|
125
144
|
/** Position of `domain` for keyword `q` in the top `num` results (default 100). */
|
|
126
145
|
rank(params, options) {
|
|
127
146
|
return this.request({ method: "POST", path: "/v1/rank", body: params, options });
|
|
@@ -130,9 +149,14 @@ var SerpKite = class {
|
|
|
130
149
|
* Read up to 20 URLs (HTML or PDF) as Markdown, text or HTML in one call, with optional
|
|
131
150
|
* query-ranked `highlights` (BM25), `include_links` and `include_images`. 1 credit per URL
|
|
132
151
|
* that comes back (0.5 from cache); URLs that fail are listed in `failed` and cost nothing.
|
|
152
|
+
* `summary: true` adds an AI summary of each page and `output_schema` (a JSON Schema, guided
|
|
153
|
+
* by an optional `prompt`) returns each page's data as JSON in `output`, each at an extra
|
|
154
|
+
* per-page price (https://serpkite.com/pricing); a page whose model step failed has
|
|
155
|
+
* `summary_error` / `output_error` instead and isn't charged for it.
|
|
133
156
|
*/
|
|
134
157
|
extract(params, options) {
|
|
135
|
-
const
|
|
158
|
+
const ai = params.summary === true || params.output_schema !== void 0;
|
|
159
|
+
const wait = ((params.timeout ?? (ai ? 75 : 50)) + 15) * 1e3;
|
|
136
160
|
return this.request({
|
|
137
161
|
method: "POST",
|
|
138
162
|
path: "/v1/extract",
|
|
@@ -157,6 +181,47 @@ var SerpKite = class {
|
|
|
157
181
|
account(options) {
|
|
158
182
|
return this.request({ method: "GET", path: "/v1/account", options });
|
|
159
183
|
}
|
|
184
|
+
// ── AI answers ────────────────────────────────────────────────────────
|
|
185
|
+
/**
|
|
186
|
+
* A cited answer grounded on live Google results: Markdown with `[n]` markers in `answer`,
|
|
187
|
+
* the cited sources in `citations`. `depth: "fast"` answers from the result page;
|
|
188
|
+
* `"standard"` (default) also reads the top `num_sources` pages and costs more
|
|
189
|
+
* (https://serpkite.com/pricing; `meta.credits` splits search and model). With
|
|
190
|
+
* `output_schema` the answer is a JSON value in `output`.
|
|
191
|
+
* When the model fails the call throws `ai_unavailable` (503) and the model part is refunded;
|
|
192
|
+
* a search with nothing to answer from returns `answer: null` and a `meta.warnings` entry.
|
|
193
|
+
*/
|
|
194
|
+
answer(params, options) {
|
|
195
|
+
return this.request({
|
|
196
|
+
method: "POST",
|
|
197
|
+
path: "/v1/answer",
|
|
198
|
+
body: params,
|
|
199
|
+
options: { ...options, timeoutMs: this.#answerTimeout(options) },
|
|
200
|
+
retry: "rate_limit"
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Streams an answer as Server-Sent Events: `results` first, then `delta` text pieces,
|
|
205
|
+
* then `done` (final answer, citations, meta). A server `error` event is thrown as a
|
|
206
|
+
* `SerpKiteError` (`ai_unavailable`). Breaking out of the loop closes the connection
|
|
207
|
+
* (the model part is then not charged). Can't be combined with `output_schema`.
|
|
208
|
+
*
|
|
209
|
+
* ```ts
|
|
210
|
+
* for await (const ev of sk.answerStream({ q: "what is retrieval-augmented generation" })) {
|
|
211
|
+
* if (ev.event === "delta") process.stdout.write(ev.data.text);
|
|
212
|
+
* }
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
answerStream(params, options) {
|
|
216
|
+
return this.#stream(
|
|
217
|
+
{ ...params, stream: true },
|
|
218
|
+
{ ...options, timeoutMs: this.#answerTimeout(options) }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
/** The search (pages read included) runs before the first byte: wait at least 100 s. */
|
|
222
|
+
#answerTimeout(options) {
|
|
223
|
+
return options?.timeoutMs ?? (this.timeoutMs > 0 ? Math.max(this.timeoutMs, 1e5) : 0);
|
|
224
|
+
}
|
|
160
225
|
// ── Async tasks: crawl ────────────────────────────────────────────────
|
|
161
226
|
/**
|
|
162
227
|
* Starts an async crawl of one site and returns the queued task (`202`). 1 credit per page
|
|
@@ -199,6 +264,58 @@ var SerpKite = class {
|
|
|
199
264
|
timeoutMs: options.timeoutMs ?? 21e5
|
|
200
265
|
});
|
|
201
266
|
}
|
|
267
|
+
// ── Async tasks: research ─────────────────────────────────────────────
|
|
268
|
+
/**
|
|
269
|
+
* Starts an async research task: the model plans searches, reads the sources and writes a
|
|
270
|
+
* cited Markdown report (`result.report`). `effort` (`lite`, `standard` (default), `deep`)
|
|
271
|
+
* sets how much is searched and read; its flat price (https://serpkite.com/pricing) is
|
|
272
|
+
* reserved up front and charged only when the report is written (a canceled task is charged
|
|
273
|
+
* for the searches already run). With
|
|
274
|
+
* `output_schema` the report is also returned as JSON in `result.output`. Poll with
|
|
275
|
+
* `getResearch` / `waitResearch` or receive the signed `research.completed` webhook.
|
|
276
|
+
* Only retried on 429, so a lost response never starts (and reserves) a second task.
|
|
277
|
+
*/
|
|
278
|
+
research(params, options) {
|
|
279
|
+
return this.request({
|
|
280
|
+
method: "POST",
|
|
281
|
+
path: "/v1/research",
|
|
282
|
+
body: params,
|
|
283
|
+
options,
|
|
284
|
+
retry: "rate_limit"
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/** Polls a research task (`progress` while it runs; results are kept for 24 hours). */
|
|
288
|
+
getResearch(id, options) {
|
|
289
|
+
return this.request({
|
|
290
|
+
method: "GET",
|
|
291
|
+
path: `/v1/research/${encodeURIComponent(id)}`,
|
|
292
|
+
options
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Cancels a research task: refunded if still queued, else stops at its next round and is
|
|
297
|
+
* charged for the searches run.
|
|
298
|
+
*/
|
|
299
|
+
cancelResearch(id, options) {
|
|
300
|
+
return this.request({
|
|
301
|
+
method: "DELETE",
|
|
302
|
+
path: `/v1/research/${encodeURIComponent(id)}`,
|
|
303
|
+
options
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Polls until the research task is `completed`, `failed` or `canceled` and returns it (a
|
|
308
|
+
* failed task is returned, not thrown; check `status` and `error`). The report is in
|
|
309
|
+
* `result.report`. Accepts an id or the task returned by `research`. Default timeout
|
|
310
|
+
* 15 minutes.
|
|
311
|
+
*/
|
|
312
|
+
async waitResearch(idOrTask, options = {}) {
|
|
313
|
+
return this.#waitTask(
|
|
314
|
+
taskId(idOrTask),
|
|
315
|
+
(id) => this.getResearch(id, { signal: options.signal }),
|
|
316
|
+
{ ...options, timeoutMs: options.timeoutMs ?? 9e5 }
|
|
317
|
+
);
|
|
318
|
+
}
|
|
202
319
|
// ── Monitors ──────────────────────────────────────────────────────────
|
|
203
320
|
monitors = {
|
|
204
321
|
/**
|
|
@@ -208,7 +325,9 @@ var SerpKite = class {
|
|
|
208
325
|
* and reports it (`MonitorPageChange`) when its content changes. `metadata` (your JSON
|
|
209
326
|
* object, at most 2 KB) is echoed on the monitor and in its webhooks. Without a
|
|
210
327
|
* `webhook_url`, read new results from `monitors.runs`. `active: false`
|
|
211
|
-
* creates it paused.
|
|
328
|
+
* creates it paused. With an AI assistant `endpoint` (`chatgpt`, `gemini`, `copilot`,
|
|
329
|
+
* `ai_mode`, `brave_ai`) and a `prompt` every run reports the answer (`MonitorAnswer`:
|
|
330
|
+
* sources, `new_sources`, `mentions` with `track`). A monitor pauses itself after 10 failed runs in a row
|
|
212
331
|
* (`last_status: "paused"`); `update(id, { active: true })` resumes it. Each run costs
|
|
213
332
|
* what its search costs. Only retried on 429.
|
|
214
333
|
*/
|
|
@@ -331,6 +450,18 @@ var SerpKite = class {
|
|
|
331
450
|
#vertical(path) {
|
|
332
451
|
return ((params, options) => this.request({ method: "POST", path, body: params, options }));
|
|
333
452
|
}
|
|
453
|
+
#assistant(path) {
|
|
454
|
+
return ((params, options) => this.request({
|
|
455
|
+
method: "POST",
|
|
456
|
+
path,
|
|
457
|
+
body: params,
|
|
458
|
+
options: {
|
|
459
|
+
...options,
|
|
460
|
+
timeoutMs: options?.timeoutMs ?? (this.timeoutMs > 0 ? Math.max(this.timeoutMs, ASSISTANT_TIMEOUT_MS) : 0)
|
|
461
|
+
},
|
|
462
|
+
retry: "rate_limit"
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
334
465
|
async #wait(idOrEntry, o) {
|
|
335
466
|
let id;
|
|
336
467
|
if (typeof idOrEntry === "string") {
|
|
@@ -368,6 +499,149 @@ var SerpKite = class {
|
|
|
368
499
|
interval = Math.min(interval * 1.5, maxInterval);
|
|
369
500
|
}
|
|
370
501
|
}
|
|
502
|
+
/** Opens `POST /v1/answer` as an event stream (retrying the connect like `request`) and parses it. */
|
|
503
|
+
async *#stream(body, options = {}) {
|
|
504
|
+
const maxRetries = Math.max(0, options.maxRetries ?? this.maxRetries);
|
|
505
|
+
const timeoutMs = options.timeoutMs ?? this.timeoutMs;
|
|
506
|
+
const headers = {
|
|
507
|
+
Accept: "text/event-stream, application/json;q=0.9",
|
|
508
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
509
|
+
...this.#headers,
|
|
510
|
+
...options.headers,
|
|
511
|
+
"Content-Type": "application/json"
|
|
512
|
+
};
|
|
513
|
+
if (!isBrowser()) headers["User-Agent"] = `serpkite-typescript/${VERSION}`;
|
|
514
|
+
let res;
|
|
515
|
+
let cleanup = () => {
|
|
516
|
+
};
|
|
517
|
+
for (let attempt = 0; !res; attempt++) {
|
|
518
|
+
const canRetry = attempt < maxRetries;
|
|
519
|
+
const t = withTimeout(options.signal, timeoutMs);
|
|
520
|
+
let r;
|
|
521
|
+
try {
|
|
522
|
+
r = await this.#fetch(`${this.baseUrl}/v1/answer`, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers,
|
|
525
|
+
body: JSON.stringify(body),
|
|
526
|
+
signal: t.signal
|
|
527
|
+
});
|
|
528
|
+
} catch (err) {
|
|
529
|
+
t.cleanup();
|
|
530
|
+
if (options.signal?.aborted) throw err;
|
|
531
|
+
if (canRetry) {
|
|
532
|
+
await sleep(this.#backoff(attempt), options.signal);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const code = t.timedOut() ? "timeout" : "connection_error";
|
|
536
|
+
const msg = t.timedOut() ? `request timed out after ${timeoutMs} ms` : `request failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
537
|
+
throw new SerpKiteError(0, code, msg, void 0, void 0, { cause: err });
|
|
538
|
+
}
|
|
539
|
+
if (!r.ok) {
|
|
540
|
+
const text = await r.text().catch(() => "");
|
|
541
|
+
t.cleanup();
|
|
542
|
+
if (canRetry && (r.status === 429 || r.status >= 500)) {
|
|
543
|
+
await sleep(this.#retryDelay(attempt, r.headers), options.signal);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
throw errorFromResponse(r.status, r.headers, text);
|
|
547
|
+
}
|
|
548
|
+
t.stopTimer();
|
|
549
|
+
cleanup = t.cleanup;
|
|
550
|
+
res = r;
|
|
551
|
+
}
|
|
552
|
+
options.onResponse?.(responseInfo(res));
|
|
553
|
+
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
554
|
+
if (!res.body) {
|
|
555
|
+
cleanup();
|
|
556
|
+
throw new SerpKiteError(
|
|
557
|
+
res.status,
|
|
558
|
+
"invalid_response",
|
|
559
|
+
"response has no body",
|
|
560
|
+
requestId,
|
|
561
|
+
res.headers
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
if ((res.headers.get("content-type") ?? "").includes("json")) {
|
|
565
|
+
let answer;
|
|
566
|
+
try {
|
|
567
|
+
answer = JSON.parse(await res.text());
|
|
568
|
+
} catch (err) {
|
|
569
|
+
throw new SerpKiteError(
|
|
570
|
+
res.status,
|
|
571
|
+
"invalid_response",
|
|
572
|
+
"response is not valid JSON",
|
|
573
|
+
requestId,
|
|
574
|
+
res.headers,
|
|
575
|
+
{ cause: err }
|
|
576
|
+
);
|
|
577
|
+
} finally {
|
|
578
|
+
cleanup();
|
|
579
|
+
}
|
|
580
|
+
yield { event: "results", data: { request: answer.request, results: answer.results } };
|
|
581
|
+
yield {
|
|
582
|
+
event: "done",
|
|
583
|
+
data: { answer: answer.answer ?? "", citations: answer.citations, meta: answer.meta }
|
|
584
|
+
};
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const reader = res.body.getReader();
|
|
588
|
+
const decoder = new TextDecoder();
|
|
589
|
+
let buf = "";
|
|
590
|
+
let done = false;
|
|
591
|
+
try {
|
|
592
|
+
while (!done) {
|
|
593
|
+
let chunk;
|
|
594
|
+
try {
|
|
595
|
+
chunk = await reader.read();
|
|
596
|
+
} catch (err) {
|
|
597
|
+
if (options.signal?.aborted) throw err;
|
|
598
|
+
throw new SerpKiteError(
|
|
599
|
+
0,
|
|
600
|
+
"connection_error",
|
|
601
|
+
`stream read failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
602
|
+
requestId,
|
|
603
|
+
res.headers,
|
|
604
|
+
{ cause: err }
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (chunk.done) {
|
|
608
|
+
buf += decoder.decode();
|
|
609
|
+
done = true;
|
|
610
|
+
} else {
|
|
611
|
+
buf += decoder.decode(chunk.value, { stream: true });
|
|
612
|
+
}
|
|
613
|
+
const held = !done && buf.endsWith("\r") ? "\r" : "";
|
|
614
|
+
if (held) buf = buf.slice(0, -1);
|
|
615
|
+
buf = buf.replace(/\r\n?/g, "\n");
|
|
616
|
+
let sep = buf.indexOf("\n\n");
|
|
617
|
+
while (sep >= 0 || done && buf.trim() !== "") {
|
|
618
|
+
const raw = sep >= 0 ? buf.slice(0, sep) : buf;
|
|
619
|
+
buf = sep >= 0 ? buf.slice(sep + 2) : "";
|
|
620
|
+
const ev = parseSSE(raw);
|
|
621
|
+
if (ev) {
|
|
622
|
+
if (ev.event === "error") throw sseError(ev.data, res.headers, requestId);
|
|
623
|
+
if (ev.event === "results" || ev.event === "delta" || ev.event === "done") {
|
|
624
|
+
yield { event: ev.event, data: ev.data };
|
|
625
|
+
if (ev.event === "done") return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
sep = buf.indexOf("\n\n");
|
|
629
|
+
}
|
|
630
|
+
buf += held;
|
|
631
|
+
}
|
|
632
|
+
throw new SerpKiteError(
|
|
633
|
+
res.status,
|
|
634
|
+
"invalid_response",
|
|
635
|
+
"the stream ended before the done event",
|
|
636
|
+
requestId,
|
|
637
|
+
res.headers
|
|
638
|
+
);
|
|
639
|
+
} finally {
|
|
640
|
+
cleanup();
|
|
641
|
+
reader.cancel().catch(() => {
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
371
645
|
/** Low-level request: JSON body in, parsed JSON (or text for non-JSON responses) out. */
|
|
372
646
|
async request(call) {
|
|
373
647
|
const { method, path, body, options = {} } = call;
|
|
@@ -475,6 +749,36 @@ function taskId(idOrTask) {
|
|
|
475
749
|
if (!id) throw new SerpKiteError(0, "invalid_request", "a task id is required");
|
|
476
750
|
return id;
|
|
477
751
|
}
|
|
752
|
+
function parseSSE(block) {
|
|
753
|
+
let event = "message";
|
|
754
|
+
const data = [];
|
|
755
|
+
for (const line of block.split("\n")) {
|
|
756
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
757
|
+
const i = line.indexOf(":");
|
|
758
|
+
const field = i < 0 ? line : line.slice(0, i);
|
|
759
|
+
let value = i < 0 ? "" : line.slice(i + 1);
|
|
760
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
761
|
+
if (field === "event") event = value;
|
|
762
|
+
else if (field === "data") data.push(value);
|
|
763
|
+
}
|
|
764
|
+
if (data.length === 0) return void 0;
|
|
765
|
+
const text = data.join("\n");
|
|
766
|
+
try {
|
|
767
|
+
return { event, data: JSON.parse(text) };
|
|
768
|
+
} catch {
|
|
769
|
+
return { event, data: text };
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function sseError(data, headers, requestId) {
|
|
773
|
+
const e = data?.error;
|
|
774
|
+
return new SerpKiteError(
|
|
775
|
+
503,
|
|
776
|
+
e?.code ?? "unavailable",
|
|
777
|
+
e?.message ?? "the answer stream failed",
|
|
778
|
+
e?.request_id ?? requestId,
|
|
779
|
+
headers
|
|
780
|
+
);
|
|
781
|
+
}
|
|
478
782
|
function parseRetryAfter(v) {
|
|
479
783
|
if (!v) return void 0;
|
|
480
784
|
const secs = Number(v);
|
|
@@ -516,6 +820,9 @@ function withTimeout(outer, ms) {
|
|
|
516
820
|
return {
|
|
517
821
|
signal: ctrl.signal,
|
|
518
822
|
timedOut: () => timedOut,
|
|
823
|
+
stopTimer: () => {
|
|
824
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
825
|
+
},
|
|
519
826
|
cleanup: () => {
|
|
520
827
|
if (timer !== void 0) clearTimeout(timer);
|
|
521
828
|
outer?.removeEventListener("abort", onAbort);
|
|
@@ -619,6 +926,6 @@ async function parseWebhook(secret, payload, headers, options = {}) {
|
|
|
619
926
|
return { type, deliveryId: header(headers, "x-serpkite-delivery"), data };
|
|
620
927
|
}
|
|
621
928
|
|
|
622
|
-
export { DEFAULT_BASE_URL, SerpKite, SerpKiteError, VERSION, isBatchError, parseRetryAfter, parseWebhook, verifyWebhook };
|
|
929
|
+
export { ASSISTANT_TIMEOUT_MS, DEFAULT_BASE_URL, SerpKite, SerpKiteError, VERSION, isBatchError, parseRetryAfter, parseWebhook, verifyWebhook };
|
|
623
930
|
//# sourceMappingURL=index.js.map
|
|
624
931
|
//# sourceMappingURL=index.js.map
|