ubersearch 1.5.0 → 1.6.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/package.json +2 -1
- package/src/cli.ts +2 -2
- package/src/config/load.ts +42 -44
- package/src/core/orchestrator.ts +1 -1
- package/src/core/strategy/AllProvidersStrategy.ts +19 -14
- package/src/core/strategy/FirstSuccessStrategy.ts +10 -7
- package/src/core/types.ts +1 -0
- package/src/providers/retry.ts +10 -3
- package/src/providers/utils.ts +3 -1
- package/src/tool/uberSearchTool.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ubersearch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"zod": "^4.1.12"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
+
"prepare": "git config core.hooksPath scripts/hooks",
|
|
42
43
|
"lint": "bunx biome check .",
|
|
43
44
|
"lint:fix": "bunx biome check --write .",
|
|
44
45
|
"format": "bunx biome format --write .",
|
package/src/cli.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
import { bootstrapContainer, isLifecycleProvider } from "./bootstrap/container";
|
|
9
9
|
import type { ProviderRegistry } from "./core/provider";
|
|
10
10
|
import { ServiceKeys } from "./core/serviceKeys";
|
|
11
|
+
import { serve as serveMcp } from "./mcp-server";
|
|
11
12
|
import type { UberSearchOutput } from "./tool/interface";
|
|
12
13
|
import { getCreditStatus, uberSearch } from "./tool/uberSearchTool";
|
|
13
|
-
import { serve as serveMcp } from "./mcp-server";
|
|
14
14
|
|
|
15
15
|
// Parse command line arguments
|
|
16
16
|
const args = process.argv.slice(2);
|
|
@@ -47,7 +47,7 @@ OPTIONS:
|
|
|
47
47
|
--json Output results as JSON
|
|
48
48
|
--engines <engine,list> Use specific engines (comma-separated)
|
|
49
49
|
--categories <cat,list> SearXNG categories (comma-separated)
|
|
50
|
-
--strategy <strategy> Search strategy: 'all' or 'first-success' (default:
|
|
50
|
+
--strategy <strategy> Search strategy: 'all' or 'first-success' (default: first-success)
|
|
51
51
|
--limit <number> Maximum results per engine
|
|
52
52
|
--include-raw Include raw provider responses
|
|
53
53
|
--config <path> Path to configuration file
|
package/src/config/load.ts
CHANGED
|
@@ -203,29 +203,37 @@ export interface LoadConfigOptions {
|
|
|
203
203
|
*/
|
|
204
204
|
/**
|
|
205
205
|
* Get default configuration when no config file is found
|
|
206
|
-
*
|
|
206
|
+
* SearXNG is always first (free, unlimited), then cloud providers by free tier generosity
|
|
207
207
|
*/
|
|
208
208
|
function getDefaultConfig(): ExtendedSearchConfig {
|
|
209
209
|
const engines: ExtendedSearchConfig["engines"] = [];
|
|
210
210
|
const defaultEngineOrder: string[] = [];
|
|
211
211
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
212
|
+
// Always add SearXNG first - it's free and unlimited (requires Docker)
|
|
213
|
+
const packageRoot = getPackageRoot();
|
|
214
|
+
const composeFile = join(packageRoot, "providers", "searxng", "docker-compose.yml");
|
|
215
|
+
|
|
216
|
+
defaultEngineOrder.push("searchxng");
|
|
217
|
+
engines.push({
|
|
218
|
+
id: "searchxng",
|
|
219
|
+
type: "searchxng",
|
|
220
|
+
enabled: true,
|
|
221
|
+
displayName: "SearXNG (Local)",
|
|
222
|
+
apiKeyEnv: "SEARXNG_API_KEY",
|
|
223
|
+
endpoint: "http://localhost:8888/search",
|
|
224
|
+
composeFile,
|
|
225
|
+
containerName: "searxng",
|
|
226
|
+
healthEndpoint: "http://localhost:8888/healthz",
|
|
227
|
+
defaultLimit: 15,
|
|
228
|
+
monthlyQuota: 10000,
|
|
229
|
+
creditCostPerSearch: 0,
|
|
230
|
+
lowCreditThresholdPercent: 80,
|
|
231
|
+
autoStart: true,
|
|
232
|
+
autoStop: true,
|
|
233
|
+
initTimeoutMs: 60000,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Add cloud providers in order of free tier generosity: Brave (2000/mo) > Tavily (1000/mo) > Linkup
|
|
229
237
|
if (process.env.BRAVE_API_KEY) {
|
|
230
238
|
defaultEngineOrder.push("brave");
|
|
231
239
|
engines.push({
|
|
@@ -236,6 +244,22 @@ function getDefaultConfig(): ExtendedSearchConfig {
|
|
|
236
244
|
apiKeyEnv: "BRAVE_API_KEY",
|
|
237
245
|
endpoint: "https://api.search.brave.com/res/v1/web/search",
|
|
238
246
|
defaultLimit: 15,
|
|
247
|
+
monthlyQuota: 2000,
|
|
248
|
+
creditCostPerSearch: 1,
|
|
249
|
+
lowCreditThresholdPercent: 80,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (process.env.TAVILY_API_KEY) {
|
|
254
|
+
defaultEngineOrder.push("tavily");
|
|
255
|
+
engines.push({
|
|
256
|
+
id: "tavily",
|
|
257
|
+
type: "tavily",
|
|
258
|
+
enabled: true,
|
|
259
|
+
displayName: "Tavily Search",
|
|
260
|
+
apiKeyEnv: "TAVILY_API_KEY",
|
|
261
|
+
endpoint: "https://api.tavily.com/search",
|
|
262
|
+
searchDepth: "basic",
|
|
239
263
|
monthlyQuota: 1000,
|
|
240
264
|
creditCostPerSearch: 1,
|
|
241
265
|
lowCreditThresholdPercent: 80,
|
|
@@ -257,32 +281,6 @@ function getDefaultConfig(): ExtendedSearchConfig {
|
|
|
257
281
|
});
|
|
258
282
|
}
|
|
259
283
|
|
|
260
|
-
// If no cloud providers configured, fall back to SearXNG (requires Docker)
|
|
261
|
-
if (engines.length === 0) {
|
|
262
|
-
const packageRoot = getPackageRoot();
|
|
263
|
-
const composeFile = join(packageRoot, "providers", "searxng", "docker-compose.yml");
|
|
264
|
-
|
|
265
|
-
defaultEngineOrder.push("searchxng");
|
|
266
|
-
engines.push({
|
|
267
|
-
id: "searchxng",
|
|
268
|
-
type: "searchxng",
|
|
269
|
-
enabled: true,
|
|
270
|
-
displayName: "SearXNG (Local)",
|
|
271
|
-
apiKeyEnv: "SEARXNG_API_KEY",
|
|
272
|
-
endpoint: "http://localhost:8888/search",
|
|
273
|
-
composeFile,
|
|
274
|
-
containerName: "searxng",
|
|
275
|
-
healthEndpoint: "http://localhost:8888/healthz",
|
|
276
|
-
defaultLimit: 15,
|
|
277
|
-
monthlyQuota: 10000,
|
|
278
|
-
creditCostPerSearch: 0,
|
|
279
|
-
lowCreditThresholdPercent: 80,
|
|
280
|
-
autoStart: true,
|
|
281
|
-
autoStop: true,
|
|
282
|
-
initTimeoutMs: 60000,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
284
|
return { defaultEngineOrder, engines };
|
|
287
285
|
}
|
|
288
286
|
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -71,7 +71,7 @@ export class UberSearchOrchestrator {
|
|
|
71
71
|
*/
|
|
72
72
|
async run(query: string, options: UberSearchOptions = {}): Promise<OrchestratorResult> {
|
|
73
73
|
const order = this.getEngineOrder(options.engineOrderOverride);
|
|
74
|
-
const strategyName = options.strategy ?? "
|
|
74
|
+
const strategyName = options.strategy ?? "first-success";
|
|
75
75
|
|
|
76
76
|
if (order.length === 0) {
|
|
77
77
|
throw new Error("No engines configured or selected");
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* the overall search.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { withRetry } from "../../providers/retry";
|
|
10
11
|
import { createLogger } from "../logger";
|
|
11
12
|
import type { SearchProvider } from "../provider";
|
|
12
13
|
import type { EngineId, SearchResponse, SearchResultItem } from "../types";
|
|
@@ -87,13 +88,15 @@ export class AllProvidersStrategy implements ISearchStrategy {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
try {
|
|
90
|
-
// Execute search
|
|
91
|
-
const response = await
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
// Execute search with retry logic for transient failures
|
|
92
|
+
const response = await withRetry(engineId, () =>
|
|
93
|
+
provider.search({
|
|
94
|
+
query,
|
|
95
|
+
limit: options.limit,
|
|
96
|
+
includeRaw: options.includeRaw,
|
|
97
|
+
categories: options.categories,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
97
100
|
|
|
98
101
|
// Deduct credits
|
|
99
102
|
if (!context.creditManager.charge(engineId)) {
|
|
@@ -168,16 +171,18 @@ export class AllProvidersStrategy implements ISearchStrategy {
|
|
|
168
171
|
eligibleEngines.push({ engineId, provider });
|
|
169
172
|
}
|
|
170
173
|
|
|
171
|
-
// Execute all eligible searches in parallel
|
|
174
|
+
// Execute all eligible searches in parallel with retry logic
|
|
172
175
|
const searchPromises = eligibleEngines.map(
|
|
173
176
|
async ({ engineId, provider }): Promise<EngineSearchResult> => {
|
|
174
177
|
try {
|
|
175
|
-
const response = await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
const response = await withRetry(engineId, () =>
|
|
179
|
+
provider.search({
|
|
180
|
+
query,
|
|
181
|
+
limit: options.limit,
|
|
182
|
+
includeRaw: options.includeRaw,
|
|
183
|
+
categories: options.categories,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
181
186
|
return { engineId, response };
|
|
182
187
|
} catch (error) {
|
|
183
188
|
return { engineId, error: error instanceof Error ? error : new Error(String(error)) };
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* with insufficient credits and stops execution after the first success.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { withRetry } from "../../providers/retry";
|
|
9
10
|
import { createLogger } from "../logger";
|
|
10
11
|
import type { EngineId } from "../types";
|
|
11
12
|
import { SearchError } from "../types";
|
|
@@ -59,13 +60,15 @@ export class FirstSuccessStrategy implements ISearchStrategy {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
try {
|
|
62
|
-
// Execute search
|
|
63
|
-
const response = await
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
// Execute search with retry logic for transient failures
|
|
64
|
+
const response = await withRetry(engineId, () =>
|
|
65
|
+
provider.search({
|
|
66
|
+
query,
|
|
67
|
+
limit: options.limit,
|
|
68
|
+
includeRaw: options.includeRaw,
|
|
69
|
+
categories: options.categories,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
69
72
|
|
|
70
73
|
// Deduct credits
|
|
71
74
|
if (!context.creditManager.charge(engineId)) {
|
package/src/core/types.ts
CHANGED
package/src/providers/retry.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface RetryConfig {
|
|
|
25
25
|
maxDelayMs?: number;
|
|
26
26
|
|
|
27
27
|
/** Whether to retry on specific error types */
|
|
28
|
-
retryableErrors?: Array<"network_error" | "api_error" | "timeout">;
|
|
28
|
+
retryableErrors?: Array<"network_error" | "api_error" | "rate_limit" | "no_results" | "timeout">;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -36,7 +36,7 @@ export const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
|
|
|
36
36
|
initialDelayMs: 1000,
|
|
37
37
|
backoffMultiplier: 2,
|
|
38
38
|
maxDelayMs: 10000,
|
|
39
|
-
retryableErrors: ["network_error", "api_error"],
|
|
39
|
+
retryableErrors: ["network_error", "api_error", "rate_limit", "no_results"],
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -53,6 +53,11 @@ export async function withRetry<T>(
|
|
|
53
53
|
fn: () => Promise<T>,
|
|
54
54
|
config: RetryConfig = {},
|
|
55
55
|
): Promise<T> {
|
|
56
|
+
// Skip retries in test environment to avoid timeouts
|
|
57
|
+
if (process.env.DISABLE_RETRY === "true") {
|
|
58
|
+
return fn();
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
const {
|
|
57
62
|
maxAttempts = DEFAULT_RETRY_CONFIG.maxAttempts,
|
|
58
63
|
initialDelayMs = DEFAULT_RETRY_CONFIG.initialDelayMs,
|
|
@@ -74,7 +79,9 @@ export async function withRetry<T>(
|
|
|
74
79
|
throw error;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
const shouldRetry = retryableErrors.includes(
|
|
82
|
+
const shouldRetry = retryableErrors.includes(
|
|
83
|
+
error.reason as "network_error" | "api_error" | "rate_limit" | "no_results",
|
|
84
|
+
);
|
|
78
85
|
|
|
79
86
|
if (!shouldRetry || attempt >= maxAttempts) {
|
|
80
87
|
throw error;
|
package/src/providers/utils.ts
CHANGED
|
@@ -116,10 +116,12 @@ export async function fetchWithErrorHandling<T>(
|
|
|
116
116
|
// Ignore error body parsing failures
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Detect rate limiting (HTTP 429)
|
|
120
|
+
const reason = response.status === 429 ? "rate_limit" : "api_error";
|
|
119
121
|
const errorPrefix = providerDisplayName ? `${providerDisplayName} API error` : "API error";
|
|
120
122
|
throw new SearchError(
|
|
121
123
|
engineId,
|
|
122
|
-
|
|
124
|
+
reason,
|
|
123
125
|
`${errorPrefix}: HTTP ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`,
|
|
124
126
|
response.status,
|
|
125
127
|
);
|