@moguiyu/dsh-tavily 0.3.3 → 0.3.5
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 +10 -3
- package/lib/client.js +120 -2
- package/package.json +16 -11
- package/src/backend.js +110 -11
- package/src/client.js +120 -2
- package/src/lib.js +123 -3
- package/src/tools.js +538 -31
- package/src/invariant.js +0 -18
package/src/lib.js
CHANGED
|
@@ -3,12 +3,130 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readFileSync } from 'node:fs'
|
|
5
5
|
|
|
6
|
-
export const STRATEGIES = ['rotate', 'low-usage-first', 'high-usage-first']
|
|
6
|
+
export const STRATEGIES = ['rotate', 'low-usage-first', 'high-usage-first', 'load-balance']
|
|
7
7
|
|
|
8
8
|
export function isValidStrategy(value) {
|
|
9
9
|
return STRATEGIES.includes(value)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Concurrent requests allowed per Tavily account in `load-balance` mode. One is
|
|
14
|
+
* the safe default: Tavily's published limits are per environment/key
|
|
15
|
+
* (100 requests/minute on a free Development key) and every key of one account
|
|
16
|
+
* shares that budget and its credit pool.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_ACCOUNT_CAP = 1
|
|
19
|
+
export const MAX_ACCOUNT_CAP = 8
|
|
20
|
+
|
|
21
|
+
export function clampAccountCap(value) {
|
|
22
|
+
const n = Number(value)
|
|
23
|
+
if (!Number.isFinite(n)) return DEFAULT_ACCOUNT_CAP
|
|
24
|
+
return Math.min(MAX_ACCOUNT_CAP, Math.max(DEFAULT_ACCOUNT_CAP, Math.round(n)))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The `/usage` `account` fields that identify an account; order is canonical. */
|
|
28
|
+
const ACCOUNT_FIELDS = [
|
|
29
|
+
'current_plan', 'plan', 'plan_limit', 'plan_usage', 'paygo_limit', 'paygo_usage',
|
|
30
|
+
'search_usage', 'extract_usage', 'map_usage', 'crawl_usage', 'research_usage',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fingerprint a Tavily `/usage` `account` block. Tavily publishes no account id,
|
|
35
|
+
* but every key of one account reports the identical account block at the same
|
|
36
|
+
* moment (observed live: two keys both at search_usage 846 / extract 129 / map
|
|
37
|
+
* 23), so an equal fingerprint is the only account-level signal available.
|
|
38
|
+
* Returns `null` when the block carries no recognizable field, which means
|
|
39
|
+
* "makes no claim about the account" rather than "its own account".
|
|
40
|
+
*/
|
|
41
|
+
export function accountFingerprint(account) {
|
|
42
|
+
if (account === null || account === undefined || typeof account !== 'object' || Array.isArray(account)) return null
|
|
43
|
+
const parts = []
|
|
44
|
+
for (const field of ACCOUNT_FIELDS) {
|
|
45
|
+
const value = account[field]
|
|
46
|
+
if (value === undefined || value === null) continue
|
|
47
|
+
parts.push(field + '=' + String(value))
|
|
48
|
+
}
|
|
49
|
+
return parts.length === 0 ? null : parts.join('|')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Assign stable account group ids to the stored keys.
|
|
54
|
+
*
|
|
55
|
+
* `entries` is `[{ masked, fingerprint }]` in credential-list order; `previous`
|
|
56
|
+
* is the persisted `accounts` map. A `manual: true` entry is kept verbatim —
|
|
57
|
+
* that is the user's override — and offers its group id to keys that turn out
|
|
58
|
+
* to share its account. Inferred ids survive a re-run: a cluster carries over
|
|
59
|
+
* the id its own members already had instead of being renumbered, so usage
|
|
60
|
+
* drifting between fetches cannot reshuffle the groups.
|
|
61
|
+
*/
|
|
62
|
+
export function groupAccounts(entries, previous) {
|
|
63
|
+
const prior = previous !== null && previous !== undefined && typeof previous === 'object' && !Array.isArray(previous)
|
|
64
|
+
? previous
|
|
65
|
+
: {}
|
|
66
|
+
const assigned = {}
|
|
67
|
+
const used = new Set()
|
|
68
|
+
const manualGroupByFingerprint = new Map()
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const before = prior[entry.masked]
|
|
72
|
+
if (before === null || before === undefined || typeof before !== 'object') continue
|
|
73
|
+
if (before.manual !== true) continue
|
|
74
|
+
if (typeof before.group !== 'string' || before.group.length === 0) continue
|
|
75
|
+
assigned[entry.masked] = { group: before.group, manual: true }
|
|
76
|
+
used.add(before.group)
|
|
77
|
+
if (typeof entry.fingerprint === 'string' && entry.fingerprint.length > 0 && !manualGroupByFingerprint.has(entry.fingerprint)) {
|
|
78
|
+
manualGroupByFingerprint.set(entry.fingerprint, before.group)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const clusters = new Map()
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
if (assigned[entry.masked] !== undefined) continue
|
|
85
|
+
const fingerprint = typeof entry.fingerprint === 'string' && entry.fingerprint.length > 0 ? entry.fingerprint : null
|
|
86
|
+
if (fingerprint === null) continue
|
|
87
|
+
const members = clusters.get(fingerprint)
|
|
88
|
+
if (members === undefined) clusters.set(fingerprint, [entry.masked])
|
|
89
|
+
else members.push(entry.masked)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let next = 1
|
|
93
|
+
for (const [fingerprint, members] of clusters) {
|
|
94
|
+
const carried = new Map()
|
|
95
|
+
for (const masked of members) {
|
|
96
|
+
const before = prior[masked]
|
|
97
|
+
if (before !== null && before !== undefined && typeof before === 'object' &&
|
|
98
|
+
typeof before.group === 'string' && before.group.length > 0) {
|
|
99
|
+
carried.set(before.group, (carried.get(before.group) ?? 0) + 1)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let group = manualGroupByFingerprint.get(fingerprint)
|
|
103
|
+
if (group === undefined) {
|
|
104
|
+
let best = null
|
|
105
|
+
let bestCount = 0
|
|
106
|
+
for (const [id, count] of carried) {
|
|
107
|
+
if (count > bestCount) {
|
|
108
|
+
best = id
|
|
109
|
+
bestCount = count
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (best !== null) group = best
|
|
113
|
+
}
|
|
114
|
+
if (group === undefined) {
|
|
115
|
+
while (used.has('acct-' + next)) next += 1
|
|
116
|
+
group = 'acct-' + next
|
|
117
|
+
next += 1
|
|
118
|
+
}
|
|
119
|
+
used.add(group)
|
|
120
|
+
for (const masked of members) assigned[masked] = { group, manual: false }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const out = {}
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (assigned[entry.masked] !== undefined) out[entry.masked] = assigned[entry.masked]
|
|
126
|
+
}
|
|
127
|
+
return out
|
|
128
|
+
}
|
|
129
|
+
|
|
12
130
|
/**
|
|
13
131
|
* Mask a key for display: `first12…last4`; comma-separated lists are masked
|
|
14
132
|
* per part. Never reveals more than a fingerprint.
|
|
@@ -39,10 +157,12 @@ export function parseKeyList(value) {
|
|
|
39
157
|
|
|
40
158
|
/**
|
|
41
159
|
* Order keys according to a strategy. `usageOf(key)` returns a number or null
|
|
42
|
-
* (unknown usage is treated as 0). `rotate` keeps the given order
|
|
160
|
+
* (unknown usage is treated as 0). `rotate` keeps the given order, and so does
|
|
161
|
+
* `load-balance`: that mode is about how many requests may be in flight per
|
|
162
|
+
* account, not about which key comes first.
|
|
43
163
|
*/
|
|
44
164
|
export function orderKeys(values, strategy, usageOf) {
|
|
45
|
-
if (strategy === 'rotate') return [...values]
|
|
165
|
+
if (strategy === 'rotate' || strategy === 'load-balance') return [...values]
|
|
46
166
|
const usageOfKey = (value) => {
|
|
47
167
|
const usage = usageOf(value)
|
|
48
168
|
return usage !== null && typeof usage === 'number' ? usage : 0
|