mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
feat(search): add Xquik X search provider (#11370)
Merged into release/v3.8.51 via batch validation: xquik provider suites green on the combined tree (193 node:test assertions incl. your 7 new cases), check:provider-consistency OK (353 canonical providers), static gates green. Well-scoped fallbackOnly X-provider with clean citation building — thanks @kriptoburak!
This commit is contained in:
@@ -167,6 +167,13 @@ export const SEARCH_VALIDATOR_CONFIGS: Record<
|
||||
}),
|
||||
},
|
||||
}),
|
||||
"xquik-search": (apiKey) => ({
|
||||
url: "https://xquik.com/api/v1/x/tweets/search?q=test&limit=1",
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json", "x-api-key": apiKey },
|
||||
},
|
||||
}),
|
||||
"zai-search": (apiKey, providerSpecificData = {}) => {
|
||||
const baseUrl =
|
||||
typeof providerSpecificData?.baseUrl === "string" && providerSpecificData.baseUrl.trim()
|
||||
|
||||
@@ -127,7 +127,10 @@ export async function executeWebSearch(
|
||||
|
||||
const log = input.log || defaultLog;
|
||||
if (input.provider === "x_search") input.provider = "x-search";
|
||||
if (input.provider === "x-search") input.search_type = "x";
|
||||
if (input.provider === "xquik" || input.provider === "xquik_search") {
|
||||
input.provider = "xquik-search";
|
||||
}
|
||||
if (input.provider === "x-search" || input.provider === "xquik-search") input.search_type = "x";
|
||||
const searchType = input.search_type || "web";
|
||||
|
||||
if (input.provider) {
|
||||
|
||||
@@ -141,6 +141,18 @@ export const SEARCH_PROVIDERS = {
|
||||
"SuperGrok OAuth (xai-oauth) or xAI API key. This is Grok X Search, not the X Developer MCP.",
|
||||
serviceKinds: ["webSearch"],
|
||||
},
|
||||
"xquik-search": {
|
||||
id: "xquik-search",
|
||||
alias: "xquik",
|
||||
name: "Xquik X Search",
|
||||
icon: "tag",
|
||||
color: "#111827",
|
||||
textIcon: "XQ",
|
||||
website: "https://docs.xquik.com",
|
||||
authHint:
|
||||
"Xquik API key (xq_...). Search is metered per returned post; the catalog estimate uses 5 results.",
|
||||
serviceKinds: ["webSearch"],
|
||||
},
|
||||
"ollama-search": {
|
||||
id: "ollama-search",
|
||||
alias: "ollama-search",
|
||||
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
} from "@/shared/reasoning/effortStandardization";
|
||||
|
||||
import { modelIdSchema, nonEmptyStringSchema } from "./misc.ts";
|
||||
import {
|
||||
isCanonicalEmbeddingItem,
|
||||
JINA_NATIVE_MEDIA_KEYS,
|
||||
} from "../jinaNativeEmbeddingInput.ts";
|
||||
import { isCanonicalEmbeddingItem, JINA_NATIVE_MEDIA_KEYS } from "../jinaNativeEmbeddingInput.ts";
|
||||
import { isGeminiNativeEmbeddingItem } from "../geminiNativeEmbeddingInput.ts";
|
||||
|
||||
export const embeddingTokenArraySchema = z
|
||||
@@ -215,7 +212,9 @@ const jinaNativeMediaStringSchema = z.string().trim().min(1).superRefine(refineJ
|
||||
|
||||
function exactlyOneJinaMediaKey(value: Record<string, unknown>, key: string): boolean {
|
||||
if (isCanonicalEmbeddingItem(value)) return false;
|
||||
return JINA_NATIVE_MEDIA_KEYS.filter((mediaKey) => mediaKey in value).length === 1 && key in value;
|
||||
return (
|
||||
JINA_NATIVE_MEDIA_KEYS.filter((mediaKey) => mediaKey in value).length === 1 && key in value
|
||||
);
|
||||
}
|
||||
|
||||
const jinaTextDocSchema = z
|
||||
@@ -264,7 +263,9 @@ export const jinaNativeDocSchema = z.union([
|
||||
export const jinaMergedContentGroupSchema = z
|
||||
.object({
|
||||
content: z
|
||||
.array(z.union([jinaTextDocSchema, jinaImageDocSchema, jinaAudioDocSchema, jinaVideoDocSchema]))
|
||||
.array(
|
||||
z.union([jinaTextDocSchema, jinaImageDocSchema, jinaAudioDocSchema, jinaVideoDocSchema])
|
||||
)
|
||||
.min(1, "content must contain at least one chunk"),
|
||||
})
|
||||
.passthrough();
|
||||
@@ -330,9 +331,12 @@ export const geminiNativePartSchema = z
|
||||
fileData: geminiFileDataSchema.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.refine((value) => isGeminiNativeEmbeddingItem(value) && !("parts" in value) && !("content" in value), {
|
||||
message: "Gemini part must be { text }, { inline_data }, or { file_data }",
|
||||
});
|
||||
.refine(
|
||||
(value) => isGeminiNativeEmbeddingItem(value) && !("parts" in value) && !("content" in value),
|
||||
{
|
||||
message: "Gemini part must be { text }, { inline_data }, or { file_data }",
|
||||
}
|
||||
);
|
||||
|
||||
export const geminiNativeContentSchema = z
|
||||
.object({
|
||||
@@ -445,7 +449,6 @@ export const v1ImageUpscaleSchema = z
|
||||
})
|
||||
.catchall(z.unknown());
|
||||
|
||||
|
||||
export const v1AudioSpeechSchema = z
|
||||
.object({
|
||||
model: modelIdSchema,
|
||||
@@ -565,71 +568,72 @@ export const v1SearchSchema = z.preprocess(
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
||||
const o = { ...(raw as Record<string, unknown>) };
|
||||
if (o.provider === "x_search") o.provider = "x-search";
|
||||
if (o.provider === "x-search") o.search_type = "x";
|
||||
if (o.provider === "xquik" || o.provider === "xquik_search") o.provider = "xquik-search";
|
||||
if (o.provider === "x-search" || o.provider === "xquik-search") o.search_type = "x";
|
||||
return o;
|
||||
},
|
||||
z
|
||||
.object({
|
||||
// Core
|
||||
query: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Query is required")
|
||||
.max(500, "Query must be 500 characters or fewer"),
|
||||
// Not a z.enum: the runtime catalog (SEARCH_PROVIDERS + SEARCH_PROVIDER_ALIASES in
|
||||
// open-sse/config/searchRegistry.ts) is the source of truth via resolveSearchProvider(),
|
||||
// which already returns a named "Unknown search provider: <id>" error for bad ids (see
|
||||
// src/app/api/v1/search/route.ts). A hard-coded enum here would 400 before that check
|
||||
// ever runs, hiding the informative message behind a generic Zod failure (#10849).
|
||||
// Known catalog ids as of this writing: serper-search, brave-search, perplexity-search,
|
||||
// exa-search, tavily-search, firecrawl, google-pse-search, linkup-search, ollama-search,
|
||||
// searchapi-search, youcom-search, searxng-search, zai-search, jina-search, jina-ai,
|
||||
// jina, duckduckgo-free, x-search, x_search (plus short aliases resolved by
|
||||
// SEARCH_PROVIDER_ALIASES).
|
||||
provider: z.string().min(1).optional(),
|
||||
max_results: z.coerce.number().int().min(1).max(100).default(5),
|
||||
search_type: z.enum(["web", "news", "x"]).default("web"),
|
||||
offset: z.coerce.number().int().min(0).default(0),
|
||||
// Core
|
||||
query: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Query is required")
|
||||
.max(500, "Query must be 500 characters or fewer"),
|
||||
// Not a z.enum: the runtime catalog (SEARCH_PROVIDERS + SEARCH_PROVIDER_ALIASES in
|
||||
// open-sse/config/searchRegistry.ts) is the source of truth via resolveSearchProvider(),
|
||||
// which already returns a named "Unknown search provider: <id>" error for bad ids (see
|
||||
// src/app/api/v1/search/route.ts). A hard-coded enum here would 400 before that check
|
||||
// ever runs, hiding the informative message behind a generic Zod failure (#10849).
|
||||
// Known catalog ids as of this writing: serper-search, brave-search, perplexity-search,
|
||||
// exa-search, tavily-search, firecrawl, google-pse-search, linkup-search, ollama-search,
|
||||
// searchapi-search, youcom-search, searxng-search, zai-search, jina-search, jina-ai,
|
||||
// jina, duckduckgo-free, x-search, x_search, xquik-search, xquik (plus short aliases resolved by
|
||||
// SEARCH_PROVIDER_ALIASES).
|
||||
provider: z.string().min(1).optional(),
|
||||
max_results: z.coerce.number().int().min(1).max(100).default(5),
|
||||
search_type: z.enum(["web", "news", "x"]).default("web"),
|
||||
offset: z.coerce.number().int().min(0).default(0),
|
||||
|
||||
// Locale
|
||||
country: z.string().max(2).toUpperCase().optional(),
|
||||
language: z.string().min(2).max(5).optional(),
|
||||
time_range: z.enum(["any", "hour", "day", "week", "month", "year"]).optional(),
|
||||
// Locale
|
||||
country: z.string().max(2).toUpperCase().optional(),
|
||||
language: z.string().min(2).max(5).optional(),
|
||||
time_range: z.enum(["any", "hour", "day", "week", "month", "year"]).optional(),
|
||||
|
||||
// Content control
|
||||
content: z
|
||||
.object({
|
||||
snippet: z.boolean().default(true),
|
||||
full_page: z.boolean().default(false),
|
||||
format: z.enum(["text", "markdown"]).default("text"),
|
||||
max_characters: z.coerce.number().int().min(100).max(100000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
// Content control
|
||||
content: z
|
||||
.object({
|
||||
snippet: z.boolean().default(true),
|
||||
full_page: z.boolean().default(false),
|
||||
format: z.enum(["text", "markdown"]).default("text"),
|
||||
max_characters: z.coerce.number().int().min(100).max(100000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Filters
|
||||
filters: z
|
||||
.object({
|
||||
include_domains: z.array(z.string().max(253)).max(20).optional(),
|
||||
exclude_domains: z.array(z.string().max(253)).max(20).optional(),
|
||||
safe_search: z.enum(["off", "moderate", "strict"]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
// Filters
|
||||
filters: z
|
||||
.object({
|
||||
include_domains: z.array(z.string().max(253)).max(20).optional(),
|
||||
exclude_domains: z.array(z.string().max(253)).max(20).optional(),
|
||||
safe_search: z.enum(["off", "moderate", "strict"]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Answer synthesis (Phase 2 — returns null until implemented)
|
||||
synthesis: z
|
||||
.object({
|
||||
strategy: z.enum(["none", "auto", "provider", "internal"]).default("none"),
|
||||
model: z.string().optional(),
|
||||
max_tokens: z.coerce.number().int().min(1).max(4000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
// Answer synthesis (Phase 2 — returns null until implemented)
|
||||
synthesis: z
|
||||
.object({
|
||||
strategy: z.enum(["none", "auto", "provider", "internal"]).default("none"),
|
||||
model: z.string().optional(),
|
||||
max_tokens: z.coerce.number().int().min(1).max(4000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Provider-specific passthrough
|
||||
provider_options: z.record(z.string(), z.unknown()).optional(),
|
||||
// Provider-specific passthrough
|
||||
provider_options: z.record(z.string(), z.unknown()).optional(),
|
||||
|
||||
// Strict mode — reject if provider doesn't support a requested filter
|
||||
strict_filters: z.boolean().default(false),
|
||||
})
|
||||
// Strict mode — reject if provider doesn't support a requested filter
|
||||
strict_filters: z.boolean().default(false),
|
||||
})
|
||||
.catchall(z.unknown())
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user