feat(providers): search connections by name and baseUrl (#12495)

Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check:provider-consistency` OK (273 entradas REGISTRY, **356** providers canônicos), `check-docs-counts-sync` exit 0 e **300/300** nos testes que a leva toca.

O crescimento de arquivo que os PRs empilham uns sobre os outros foi rebaselinado num único registro datado (`_rebaseline_2026_09_03_houminxi_batch`), com a decomposição por arquivo: `providers/page.tsx` +18 (import CSV do #12504 + busca do #12495 no mesmo painel), `accountFallback.ts` +6 (o #12566 sobre o rebaseline que o #12590 já registrou — os dois tocam `checkFallbackError`) e `chatCore.ts` +3 (invalidez de cache de quota no 429 do #12325). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva.
This commit is contained in:
Bob.Hou
2026-09-03 11:38:16 -04:00
committed by GitHub
parent c9fb06e26c
commit 0f5fc78d8a
6 changed files with 197 additions and 21 deletions

View File

@@ -4,7 +4,8 @@
*
* Case-insensitive, plain SUBSTRING match (mirrors the semantics of
* `src/shared/utils/modelCatalogSearch.ts` — do not reimplement a fuzzy
* matcher here). Matches against id, tag, name, and email.
* matcher here). Matches against id, tag, name, email, and
* providerSpecificData.baseUrl (#12108).
*/
import type { ConnectionRowConnection } from "./components/ConnectionRow";
@@ -17,6 +18,11 @@ function getConnectionTag(conn: ConnectionRowConnection): string {
return typeof tag === "string" ? tag : "";
}
function getConnectionBaseUrl(conn: ConnectionRowConnection): string {
const baseUrl = conn.providerSpecificData?.baseUrl;
return typeof baseUrl === "string" ? baseUrl : "";
}
/** True when `conn` matches `query` (empty/whitespace query always matches). */
export function matchesAccountQuery(query: string, conn: ConnectionRowConnection): boolean {
const normalizedQuery = normalize(query);
@@ -27,6 +33,7 @@ export function matchesAccountQuery(query: string, conn: ConnectionRowConnection
normalize(getConnectionTag(conn)),
normalize(conn.name),
normalize(conn.email),
normalize(getConnectionBaseUrl(conn)),
];
return haystacks.some((haystack) => haystack.includes(normalizedQuery));
}

View File

@@ -558,7 +558,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const rawNoAuthEntriesAll = buildStaticProviderEntries("no-auth", getProviderStats);
@@ -576,7 +577,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const apiKeyProviderEntriesAll = buildStaticProviderEntries("apikey", getProviderStats);
@@ -595,7 +597,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const aggregatorProviderEntriesAll = apiKeyProviderEntriesAll.filter((entry) =>
AGGREGATOR_PROVIDER_IDS.has(entry.providerId)
@@ -607,7 +610,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const imageProviderEntriesAll = apiKeyProviderEntriesAll.filter((entry) =>
IMAGE_ONLY_PROVIDER_IDS.has(entry.providerId)
@@ -619,7 +623,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const enterpriseProviderEntriesAll = apiKeyProviderEntriesAll.filter((entry) =>
ENTERPRISE_CLOUD_PROVIDER_IDS.has(entry.providerId)
@@ -631,7 +636,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const videoProviderEntriesAll = apiKeyProviderEntriesAll.filter((entry) =>
VIDEO_PROVIDER_IDS.has(entry.providerId)
@@ -643,7 +649,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const embeddingRerankProviderEntriesAll = apiKeyProviderEntriesAll.filter((entry) =>
EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId)
@@ -655,7 +662,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const webCookieProviderEntriesAll = buildStaticProviderEntries("web-cookie", getProviderStats);
@@ -666,7 +674,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const localProviderEntriesAll = buildStaticProviderEntries("local", getProviderStats);
@@ -677,7 +686,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const searchProviderEntriesAll = buildStaticProviderEntries("search", getProviderStats);
@@ -688,7 +698,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const audioProviderEntriesAll = buildStaticProviderEntries("audio", getProviderStats);
@@ -699,7 +710,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const cloudAgentProviderEntriesAll = buildStaticProviderEntries("cloud-agent", getProviderStats);
@@ -710,7 +722,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const upstreamProxyEntriesAll = buildStaticProviderEntries("upstream-proxy", getProviderStats);
@@ -721,7 +734,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const compatibleProviderEntriesAll = [
@@ -754,7 +768,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const staticProviderEntriesAll = dedupeProviderEntries([
@@ -780,7 +795,8 @@ function ProvidersPageContent() {
undefined,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
// IDE providers: subset of oauth/apikey providers that are editors/IDEs with
@@ -796,7 +812,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const oauthOnlyEntriesAll = oauthProviderEntriesAll
@@ -817,7 +834,8 @@ function ProvidersPageContent() {
showFreeOnly,
modelSearchQuery,
activeServiceKind,
liveModelsByProviderId
liveModelsByProviderId,
connections
);
const compactProviderEntries = buildCompactProviderEntriesForPage({

View File

@@ -421,6 +421,27 @@ function getFilterableModelsForEntry(
return [...staticModels, ...liveModels];
}
/**
* Dashboard-card search identity for an imported connection (#12108).
* Only `name` and `providerSpecificData.baseUrl` — those are the two
* fields the issue asked for. id/tag/email stay on the detail-page
* haystack (`matchesAccountQuery`); surfacing a provider card from an
* account email would mix account-picker UX into the catalog filter.
*/
export type ProviderSearchConnection = {
provider?: string | null;
name?: string | null;
providerSpecificData?: Record<string, unknown> | null;
};
function connectionSearchHaystacks(conn: ProviderSearchConnection): string[] {
const baseUrl = conn.providerSpecificData?.baseUrl;
return [
typeof conn.name === "string" ? conn.name : "",
typeof baseUrl === "string" ? baseUrl : "",
];
}
export function filterConfiguredProviderEntries<TProvider>(
entries: ProviderEntry<TProvider>[],
showConfiguredOnly: boolean,
@@ -428,7 +449,8 @@ export function filterConfiguredProviderEntries<TProvider>(
showFreeOnly?: boolean,
modelSearchQuery?: string,
serviceKindFilter?: string | null,
liveModelsByProviderId?: LiveModelsByProviderId
liveModelsByProviderId?: LiveModelsByProviderId,
connections?: ProviderSearchConnection[]
): ProviderEntry<TProvider>[] {
let filtered = entries;
@@ -461,9 +483,26 @@ export function filterConfiguredProviderEntries<TProvider>(
if (searchQuery && searchQuery.trim()) {
filtered = filtered.filter((entry) => {
const provider = entry.provider as Record<string, unknown>;
return (
if (
matchesAnyToken(String(provider.name || ""), searchQuery) ||
matchesAnyToken(entry.providerId, searchQuery)
) {
return true;
}
// #12108: imported connections live under the canonical provider card.
// Match their operator-visible name / baseUrl so "Grade-S-Node" or an
// IP in the search box surfaces the OpenAI card instead of vanishing.
// Same matcher as provider.name / providerId above (matchesAnyToken:
// full-string first, then whitespace-token OR). The detail page uses
// a single-substring haystack — that is a different surface, not a
// bug in this filter.
if (!connections || connections.length === 0) return false;
return connections.some(
(conn) =>
connectionBelongsToProviderPage(conn.provider, entry.providerId) &&
connectionSearchHaystacks(conn).some((haystack) =>
matchesAnyToken(haystack, searchQuery)
)
);
});
}