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:
Burak Bayır
2026-08-25 07:39:39 +03:00
committed by GitHub
parent 14ca809924
commit a0ceccc6f0
70 changed files with 893 additions and 248 deletions

View File

@@ -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())
);