@usagekit/providers 0.8.0 → 0.9.1
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/dist/catalog.d.ts +8 -14
- package/dist/catalog.js +102 -17
- package/dist/dataforseo/descriptor.d.ts +2 -2
- package/dist/dataforseo/descriptor.js +54 -13
- package/dist/dataforseo/extractors.js +278 -9
- package/dist/dataforseo/history-window.d.ts +5 -0
- package/dist/dataforseo/history-window.js +36 -0
- package/dist/dataforseo/request-pricing.d.ts +2 -0
- package/dist/dataforseo/request-pricing.js +401 -0
- package/dist/execute.d.ts +9 -7
- package/dist/execute.js +5 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/priced-request.d.ts +49 -0
- package/dist/priced-request.js +234 -0
- package/dist/prices.d.ts +3 -1
- package/dist/prices.js +59 -2
- package/dist/serpapi/descriptor.js +16 -9
- package/dist/testing/example.js +1 -0
- package/dist/types.d.ts +4 -0
- package/package.json +2 -2
package/dist/catalog.d.ts
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
import type { BalanceSnapshot, Budget, EstimateSource, OperationId, ProviderDescriptor, ProviderId, ProviderOperation, ProviderPlan, Quantity } from "@usagekit/core";
|
|
1
|
+
import type { BalanceSnapshot, Budget, EstimateSource, ConnectionPolicy, OperationId, ProviderDescriptor, ProviderId, ProviderOperation, ProviderPlan, Quantity } from "@usagekit/core";
|
|
2
2
|
import type { Extractors, ProviderModule, ProviderRequest, ProviderResponse, ReceiptDraft } from "./types.js";
|
|
3
3
|
import type { BudgetTarget } from "./budget.js";
|
|
4
4
|
export type { EstimateSource };
|
|
5
5
|
/** Connection data the catalog needs; the catalog itself stays storage-free. */
|
|
6
|
-
export type CatalogConnection = {
|
|
6
|
+
export type CatalogConnection = Omit<ConnectionPolicy, "tracking"> & {
|
|
7
7
|
id: string;
|
|
8
|
-
provider: ProviderId;
|
|
9
|
-
plan?: string;
|
|
10
|
-
/** Per-call manual price for (connection, operation). */
|
|
11
|
-
manualPrices?: Readonly<Record<OperationId, Quantity>>;
|
|
12
|
-
/** Median of this connection's settled receipts for the operation, with its sample size. */
|
|
13
|
-
measured?: (operation: OperationId) => {
|
|
14
|
-
quantity: Quantity;
|
|
15
|
-
samples: number;
|
|
16
|
-
} | undefined;
|
|
17
|
-
/** The plan allowance is exhausted: only overage rows apply. */
|
|
18
|
-
overage?: boolean;
|
|
19
8
|
};
|
|
20
9
|
export type CatalogEstimate = {
|
|
21
10
|
/** Always includes one request for a billable operation; empty for a free operation. */
|
|
@@ -23,6 +12,8 @@ export type CatalogEstimate = {
|
|
|
23
12
|
source: EstimateSource;
|
|
24
13
|
/** "<provider>:<validFrom>" of the list row, when the source is list. */
|
|
25
14
|
priceVersion?: string;
|
|
15
|
+
checkedAt?: string;
|
|
16
|
+
sampleSize?: number;
|
|
26
17
|
};
|
|
27
18
|
export type CatalogMatch = {
|
|
28
19
|
operation: ProviderOperation;
|
|
@@ -45,7 +36,10 @@ export declare function createCatalog({ providers, enabled, measuredMinSamples,
|
|
|
45
36
|
}): {
|
|
46
37
|
providers: () => ProviderDescriptor[];
|
|
47
38
|
match(providerId: ProviderId, request: ProviderRequest): CatalogMatch;
|
|
48
|
-
|
|
39
|
+
canEstimate(providerId: ProviderId, options: Readonly<Record<string, string>>): boolean;
|
|
40
|
+
/** BYOK caller estimates also retain any documented surcharge floor. */
|
|
41
|
+
boundEstimate(providerId: ProviderId, operationId: OperationId, options: Readonly<Record<string, string>>, quantities: readonly Quantity[]): Quantity[];
|
|
42
|
+
/** Resolution order: request safety, manual, measured with enough samples, list row, unknown. */
|
|
49
43
|
estimate(connection: CatalogConnection, operationId: OperationId, options: Readonly<Record<string, string>>): CatalogEstimate;
|
|
50
44
|
extract(providerId: ProviderId, operationId: OperationId | "unknown", request: ProviderRequest, response: ProviderResponse, body: unknown): ReceiptDraft;
|
|
51
45
|
/** Price list and billing export parsers of an enabled descriptor, when it has them. */
|
package/dist/catalog.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { validateDescriptor } from "@usagekit/core";
|
|
1
|
+
import { compareQuantity, validateDescriptor } from "@usagekit/core";
|
|
2
2
|
import { matchOperation } from "./match.js";
|
|
3
|
-
import { selectPriceRow } from "./prices.js";
|
|
3
|
+
import { calculatePrice, selectPriceRow } from "./prices.js";
|
|
4
4
|
import { pointer } from "./pointer.js";
|
|
5
5
|
import { decimalQuantity, requestsMeasured } from "./receipts.js";
|
|
6
6
|
import { proposePlanBudget } from "./budget.js";
|
|
7
7
|
const oneRequest = () => ({ value: 1n, scale: 0, unit: "requests" });
|
|
8
8
|
const withRequest = (q) => q.some((x) => x.unit === "requests") ? q : [...q, oneRequest()];
|
|
9
|
+
/** Compare exact quantities at their own scales; never round a reservation down. */
|
|
10
|
+
function withFloor(quantities, floor) {
|
|
11
|
+
const result = [...quantities];
|
|
12
|
+
for (const bound of floor) {
|
|
13
|
+
const index = result.findIndex((q) => q.unit === bound.unit);
|
|
14
|
+
if (index < 0)
|
|
15
|
+
result.push(bound);
|
|
16
|
+
else if (compareQuantity(result[index], bound) < 0)
|
|
17
|
+
result[index] = bound;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
9
21
|
function isoTimestamp(value) {
|
|
10
22
|
if (typeof value !== "string" || !value)
|
|
11
23
|
return undefined;
|
|
@@ -72,26 +84,60 @@ export function createCatalog({ providers, enabled, measuredMinSamples = 5, now
|
|
|
72
84
|
providers: () => [...enabled].map((id) => modules.get(id).descriptor),
|
|
73
85
|
match(providerId, request) {
|
|
74
86
|
const module = active(providerId);
|
|
75
|
-
|
|
76
|
-
if (!module || !operation)
|
|
87
|
+
if (!module)
|
|
77
88
|
return { operation: "unknown" };
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
const url = new URL(request.url, module.descriptor.upstream);
|
|
90
|
+
if (module.descriptor.pricingInputs.includes("query")) {
|
|
91
|
+
const keys = new Set();
|
|
92
|
+
for (const key of url.searchParams.keys()) {
|
|
93
|
+
if (keys.has(key))
|
|
94
|
+
throw new Error("InvalidInput: duplicate query key");
|
|
95
|
+
keys.add(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
url.search = "";
|
|
100
|
+
// Pricing sees only declared inputs. Receipts and transport retain the full request.
|
|
101
|
+
const { body, ...rest } = request;
|
|
102
|
+
const pricingRequest = {
|
|
103
|
+
...rest,
|
|
104
|
+
url: url.href,
|
|
105
|
+
...(module.descriptor.pricingInputs.includes("body") && body !== undefined ? { body } : {}),
|
|
106
|
+
};
|
|
107
|
+
const operation = matchOperation(module.descriptor, pricingRequest);
|
|
108
|
+
if (!operation)
|
|
109
|
+
return { operation: "unknown" };
|
|
110
|
+
return { operation, options: module.extractors.options(operation, pricingRequest) };
|
|
111
|
+
},
|
|
112
|
+
canEstimate(providerId, options) {
|
|
113
|
+
return require(providerId).extractors.canEstimate?.(options) ?? true;
|
|
81
114
|
},
|
|
82
|
-
/**
|
|
115
|
+
/** BYOK caller estimates also retain any documented surcharge floor. */
|
|
116
|
+
boundEstimate(providerId, operationId, options, quantities) {
|
|
117
|
+
const module = require(providerId);
|
|
118
|
+
const operation = module.descriptor.operations.find((o) => o.id === operationId);
|
|
119
|
+
if (!operation || !module.extractors.requiresListPriceFloor?.(options))
|
|
120
|
+
return [...quantities];
|
|
121
|
+
const listed = module.extractors.estimate({
|
|
122
|
+
operation,
|
|
123
|
+
options,
|
|
124
|
+
prices: validPrices(module.descriptor),
|
|
125
|
+
});
|
|
126
|
+
return withFloor(quantities, listed);
|
|
127
|
+
},
|
|
128
|
+
/** Resolution order: request safety, manual, measured with enough samples, list row, unknown. */
|
|
83
129
|
estimate(connection, operationId, options) {
|
|
84
130
|
const module = active(connection.provider), operation = module?.descriptor.operations.find((o) => o.id === operationId);
|
|
85
131
|
if (!module || !operation)
|
|
86
132
|
return { quantities: [oneRequest()], source: "unknown" };
|
|
87
133
|
if (!operation.billable)
|
|
88
134
|
return { quantities: [], source: "list" };
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
return { quantities:
|
|
135
|
+
// Provider prices estimate BYOK upstream cost only. A host must supply customer prices
|
|
136
|
+
// explicitly for platform funding; neither manual nor list prices price credits.
|
|
137
|
+
if (connection.fundingSource !== "byok")
|
|
138
|
+
return { quantities: [oneRequest()], source: "unknown" };
|
|
139
|
+
if (module.extractors.canEstimate?.(options) === false)
|
|
140
|
+
return { quantities: [oneRequest()], source: "unknown" };
|
|
95
141
|
const input = {
|
|
96
142
|
operation,
|
|
97
143
|
options,
|
|
@@ -99,14 +145,53 @@ export function createCatalog({ providers, enabled, measuredMinSamples = 5, now
|
|
|
99
145
|
...(connection.plan !== undefined ? { plan: connection.plan } : {}),
|
|
100
146
|
...(connection.overage ? { overage: true } : {}),
|
|
101
147
|
};
|
|
102
|
-
const listed = module.extractors.estimate(input);
|
|
148
|
+
const listed = module.extractors.estimate(input), row = selectPriceRow(input);
|
|
149
|
+
const manual = connection.manualPrices?.[operationId];
|
|
150
|
+
const measured = manual ? undefined : connection.measured?.(operationId);
|
|
151
|
+
const override = manual ??
|
|
152
|
+
(measured &&
|
|
153
|
+
Number.isSafeInteger(measured.samples) &&
|
|
154
|
+
measured.samples >= measuredMinSamples
|
|
155
|
+
? measured.quantity
|
|
156
|
+
: undefined);
|
|
157
|
+
if (override) {
|
|
158
|
+
const basis = manual ? connection.manualPriceBasis?.[operationId] : measured?.basis;
|
|
159
|
+
if (basis === "item" && (!row || row.calculation?.kind !== "base_plus_items"))
|
|
160
|
+
return {
|
|
161
|
+
quantities: withRequest(listed.filter((q) => q.unit !== override.unit)),
|
|
162
|
+
source: "unknown",
|
|
163
|
+
};
|
|
164
|
+
const quantity = basis === "item" ? calculatePrice(row, options, override) : override;
|
|
165
|
+
if (!quantity)
|
|
166
|
+
return {
|
|
167
|
+
quantities: withRequest(listed.filter((q) => q.unit !== override.unit)),
|
|
168
|
+
source: "unknown",
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
quantities: withRequest(withFloor([
|
|
172
|
+
quantity,
|
|
173
|
+
...listed.filter((q) => q.unit !== quantity.unit && q.unit !== "requests"),
|
|
174
|
+
], module.extractors.requiresListPriceFloor?.(options) ? listed : [])),
|
|
175
|
+
source: manual ? "manual" : "measured",
|
|
176
|
+
...(basis === "item" && row
|
|
177
|
+
? { priceVersion: `${module.descriptor.id}:${row.validFrom}`, checkedAt: row.checkedAt }
|
|
178
|
+
: {}),
|
|
179
|
+
...(!manual && measured
|
|
180
|
+
? {
|
|
181
|
+
sampleSize: measured.samples,
|
|
182
|
+
...(measured.checkedAt ? { checkedAt: measured.checkedAt } : {}),
|
|
183
|
+
}
|
|
184
|
+
: {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
103
187
|
if (!listed.length)
|
|
104
188
|
return { quantities: [oneRequest()], source: "unknown" };
|
|
105
|
-
const row = selectPriceRow(input);
|
|
106
189
|
return {
|
|
107
190
|
quantities: withRequest(listed),
|
|
108
191
|
source: "list",
|
|
109
|
-
...(row
|
|
192
|
+
...(row
|
|
193
|
+
? { priceVersion: `${module.descriptor.id}:${row.validFrom}`, checkedAt: row.checkedAt }
|
|
194
|
+
: {}),
|
|
110
195
|
};
|
|
111
196
|
},
|
|
112
197
|
extract(providerId, operationId, request, response, body) {
|
|
@@ -4,7 +4,7 @@ export declare const labsPricing = "https://dataforseo.com/pricing/dataforseo-la
|
|
|
4
4
|
export declare const backlinksPricing = "https://dataforseo.com/pricing/backlinks/backlinks";
|
|
5
5
|
/**
|
|
6
6
|
* Provider A: cost per call in every response, prepaid money balance, basic auth. List prices
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* include request formulas in cents. Actual settlement continues to use response evidence.
|
|
8
|
+
* Formulas preserve host characterization values; the existing check date is not refreshed.
|
|
9
9
|
*/
|
|
10
10
|
export declare const descriptor: ProviderDescriptor;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import { requestPricing } from "./request-pricing.js";
|
|
1
2
|
const docs = "https://docs.dataforseo.com/v3/";
|
|
2
3
|
export const serpPricing = "https://dataforseo.com/pricing/serp/google-organic-serp-api";
|
|
3
4
|
export const labsPricing = "https://dataforseo.com/pricing/dataforseo-labs/dataforseo-google-api";
|
|
4
5
|
export const backlinksPricing = "https://dataforseo.com/pricing/backlinks/backlinks";
|
|
5
6
|
const checked = "2026-09-25";
|
|
6
|
-
const live = (id, label, path) => ({
|
|
7
|
+
const live = (id, label, path, optionKeys) => ({
|
|
7
8
|
id,
|
|
8
9
|
label,
|
|
9
10
|
billable: true,
|
|
11
|
+
requestPricing: requestPricing[id],
|
|
10
12
|
match: [{ method: "POST", path }],
|
|
11
13
|
costEvidence: "response",
|
|
14
|
+
...(optionKeys ? { optionKeys } : {}),
|
|
12
15
|
});
|
|
13
16
|
const free = (id, label, method, path) => ({
|
|
14
17
|
id,
|
|
@@ -32,34 +35,64 @@ const backlinks = [
|
|
|
32
35
|
["history", "Backlinks history"],
|
|
33
36
|
["backlinks", "Backlinks live"],
|
|
34
37
|
];
|
|
35
|
-
const row = (operation, perUnit, source, option) => ({
|
|
38
|
+
const row = (operation, perUnit, source, option, calculation) => ({
|
|
36
39
|
validFrom: checked,
|
|
37
40
|
operation,
|
|
38
41
|
...(option ? { option } : {}),
|
|
39
42
|
unit: "cents",
|
|
40
43
|
perUnit,
|
|
44
|
+
...(calculation ? { calculation } : {}),
|
|
41
45
|
source,
|
|
42
46
|
checkedAt: checked,
|
|
43
47
|
});
|
|
48
|
+
// Bill each SERP of up to 10 results at the full base: base * ceil(depth / 10).
|
|
49
|
+
// https://docs.dataforseo.com/v3/serp/google/organic/live/advanced/
|
|
50
|
+
// https://docs.dataforseo.com/v3/serp/google/organic/task_post/
|
|
51
|
+
// The pricing page multiplies for each 10 results; the cost formula charges a full page 2.
|
|
52
|
+
// https://dataforseo.com/pricing/serp/google-organic-serp-api
|
|
53
|
+
// https://dataforseo.com/help-center/serp-api-cost-explained
|
|
54
|
+
// The conflicting 25% later-page discount can only lower the charge, so do not apply it.
|
|
55
|
+
// Unreturned results are refunded: full base per page is a conservative upper bound
|
|
56
|
+
// that never under-reserves; omitted depth still means one page.
|
|
57
|
+
// https://dataforseo.com/help-center/above-100-results-with-serp-api
|
|
58
|
+
const pages = {
|
|
59
|
+
kind: "additional_pages",
|
|
60
|
+
depthOption: "depth",
|
|
61
|
+
pageSize: 10,
|
|
62
|
+
additionalPageFactor: "1",
|
|
63
|
+
};
|
|
64
|
+
const items = (perItem, clickstream = true) => ({
|
|
65
|
+
kind: "base_plus_items",
|
|
66
|
+
itemCountOption: "itemCount",
|
|
67
|
+
perItem,
|
|
68
|
+
...(clickstream ? { multiplierOption: { key: "clickstream", value: "true", factor: "2" } } : {}),
|
|
69
|
+
});
|
|
44
70
|
/**
|
|
45
71
|
* Provider A: cost per call in every response, prepaid money balance, basic auth. List prices
|
|
46
|
-
*
|
|
47
|
-
*
|
|
72
|
+
* include request formulas in cents. Actual settlement continues to use response evidence.
|
|
73
|
+
* Formulas preserve host characterization values; the existing check date is not refreshed.
|
|
48
74
|
*/
|
|
49
75
|
export const descriptor = {
|
|
50
76
|
id: "dataforseo",
|
|
51
77
|
label: "DataForSEO",
|
|
52
78
|
upstream: "https://api.dataforseo.com",
|
|
79
|
+
pricingInputs: ["body"],
|
|
53
80
|
auth: { kind: "basic" },
|
|
54
81
|
stripRequestHeaders: ["authorization"],
|
|
55
82
|
billing: { unit: "cents", cycle: "prepaid" },
|
|
56
83
|
plans: [{ id: "prepaid", label: "Prepaid balance", cycle: "prepaid", allowanceMode: "hard" }],
|
|
57
84
|
prices: [
|
|
58
|
-
row("serp.google.organic.live.advanced", "0.2", serpPricing),
|
|
59
|
-
row("serp.google.organic.task_post", "0.06", serpPricing, { priority: "normal" }),
|
|
60
|
-
row("serp.google.organic.task_post", "0.12", serpPricing, { priority: "high" }),
|
|
61
|
-
...labs.map(([name]) => row(`dataforseo_labs.google.${name}.live`, name === "historical_rank_overview" ? "12" : "1.2", labsPricing
|
|
62
|
-
|
|
85
|
+
row("serp.google.organic.live.advanced", "0.2", serpPricing, undefined, pages),
|
|
86
|
+
row("serp.google.organic.task_post", "0.06", serpPricing, { priority: "normal" }, pages),
|
|
87
|
+
row("serp.google.organic.task_post", "0.12", serpPricing, { priority: "high" }, pages),
|
|
88
|
+
...labs.map(([name]) => row(`dataforseo_labs.google.${name}.live`, name === "historical_rank_overview" ? "12" : "1.2", labsPricing, undefined,
|
|
89
|
+
// Per-request plus per-item, including ranked_keywords/domain_rank_overview.
|
|
90
|
+
// https://dataforseo.com/pricing/dataforseo-labs/dataforseo-google-api
|
|
91
|
+
items(name === "historical_rank_overview" ? "0.12" : "0.012"))),
|
|
92
|
+
...backlinks.map(([name]) => row(`backlinks.${name}.live`, "2.4", backlinksPricing, undefined,
|
|
93
|
+
// Every returned row is billed, including summary and history.
|
|
94
|
+
// https://dataforseo.com/pricing/backlinks/backlinks
|
|
95
|
+
items("0.0036", false))),
|
|
63
96
|
],
|
|
64
97
|
priceList: {
|
|
65
98
|
kind: "endpoint",
|
|
@@ -68,19 +101,27 @@ export const descriptor = {
|
|
|
68
101
|
mapping: "provider-specific",
|
|
69
102
|
},
|
|
70
103
|
operations: [
|
|
71
|
-
live("serp.google.organic.live.advanced", "Google organic SERP, live advanced", "/v3/serp/google/organic/live/advanced"),
|
|
104
|
+
live("serp.google.organic.live.advanced", "Google organic SERP, live advanced", "/v3/serp/google/organic/live/advanced", ["depth"]),
|
|
72
105
|
{
|
|
73
106
|
id: "serp.google.organic.task_post",
|
|
74
107
|
label: "Google organic SERP, task post",
|
|
75
108
|
billable: true,
|
|
76
109
|
match: [{ method: "POST", path: "/v3/serp/google/organic/task_post" }],
|
|
77
|
-
optionKeys: ["priority"],
|
|
110
|
+
optionKeys: ["priority", "depth"],
|
|
78
111
|
costEvidence: "deferred",
|
|
112
|
+
requestPricing: requestPricing["serp.google.organic.task_post"],
|
|
79
113
|
},
|
|
80
114
|
free("serp.google.organic.tasks_ready", "Google organic SERP, tasks ready", "GET", "/v3/serp/google/organic/tasks_ready"),
|
|
81
115
|
free("serp.google.organic.task_get.advanced", "Google organic SERP, task get advanced", "GET", "/v3/serp/google/organic/task_get/advanced/:id"),
|
|
82
|
-
...labs.map(([name, label]) => live(`dataforseo_labs.google.${name}.live`, label, `/v3/dataforseo_labs/google/${name}/live
|
|
83
|
-
|
|
116
|
+
...labs.map(([name, label]) => live(`dataforseo_labs.google.${name}.live`, label, `/v3/dataforseo_labs/google/${name}/live`, [
|
|
117
|
+
"itemCount",
|
|
118
|
+
"clickstream",
|
|
119
|
+
...(name === "historical_rank_overview" ? ["dateFrom", "dateTo"] : []),
|
|
120
|
+
])),
|
|
121
|
+
...backlinks.map(([name, label]) => live(`backlinks.${name}.live`, label, `/v3/backlinks/${name}/live`, [
|
|
122
|
+
"itemCount",
|
|
123
|
+
...(name === "history" ? ["dateFrom", "dateTo"] : []),
|
|
124
|
+
])),
|
|
84
125
|
free("appendix.user_data", "User data (balance, limits, prices)", "GET", "/v3/appendix/user_data"),
|
|
85
126
|
free("dataforseo_labs.status", "Labs status", "GET", "/v3/dataforseo_labs/status"),
|
|
86
127
|
],
|