Merge PR #2611: Add web search provider exclusions
This commit is contained in:
@@ -277,6 +277,15 @@
|
||||
- External editor (Ctrl-G) now shows full pasted content instead of `[paste #N ...]` placeholders ([#444](https://github.com/badlogic/pi-mono/pull/444) by [@aliou](https://github.com/aliou))
|
||||
- Subagent example README referenced incorrect filename `subagent.ts` instead of `index.ts` ([#427](https://github.com/badlogic/pi-mono/pull/427) by [@Whamp](https://github.com/Whamp))
|
||||
|
||||
|
||||
### Added
|
||||
|
||||
- Added web search provider exclusions so `web_search` can skip configured providers without disabling them for model use ([#2608](https://github.com/can1357/oh-my-pi/issues/2608)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `web_search` using stale or missing provider exclusions after `/move` or resuming a session from another project. Provider preferences (`providers.webSearchExclude`, `providers.webSearch`, `providers.image`) are now reapplied when project settings reload on cwd change ([#2611](https://github.com/can1357/oh-my-pi/pull/2611)).
|
||||
|
||||
## [15.13.1] - 2026-06-15
|
||||
|
||||
### Added
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
TTS_LOCAL_VOICE_VALUES,
|
||||
} from "../tts/models";
|
||||
import { EDIT_MODES } from "../utils/edit-mode";
|
||||
import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES } from "../web/search/types";
|
||||
import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES, type SearchProviderId } from "../web/search/types";
|
||||
|
||||
/** Unified settings schema - single source of truth for all settings.
|
||||
*
|
||||
@@ -3846,6 +3846,16 @@ export const SETTINGS_SCHEMA = {
|
||||
options: SEARCH_PROVIDER_OPTIONS,
|
||||
},
|
||||
},
|
||||
"providers.webSearchExclude": {
|
||||
type: "array",
|
||||
default: [] as SearchProviderId[],
|
||||
ui: {
|
||||
tab: "providers",
|
||||
group: "Services",
|
||||
label: "Excluded Web Search Providers",
|
||||
description: "Providers that web_search should never use, even as fallbacks",
|
||||
},
|
||||
},
|
||||
"providers.image": {
|
||||
type: "enum",
|
||||
values: ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"] as const,
|
||||
|
||||
@@ -40,7 +40,9 @@ import {
|
||||
import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
|
||||
import {
|
||||
isImageProviderPreference,
|
||||
isSearchProviderId,
|
||||
isSearchProviderPreference,
|
||||
setExcludedSearchProviders,
|
||||
setPreferredImageProvider,
|
||||
setPreferredSearchProvider,
|
||||
} from "../../tools";
|
||||
@@ -419,6 +421,11 @@ export class SelectorController {
|
||||
setPreferredSearchProvider(value);
|
||||
}
|
||||
break;
|
||||
case "providers.webSearchExclude":
|
||||
if (Array.isArray(value)) {
|
||||
setExcludedSearchProviders(value.filter(isSearchProviderId));
|
||||
}
|
||||
break;
|
||||
case "providers.image":
|
||||
if (isImageProviderPreference(value)) {
|
||||
setPreferredImageProvider(value);
|
||||
|
||||
@@ -92,6 +92,7 @@ import { STTController, type SttState } from "../stt";
|
||||
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
||||
import { formatTaskId } from "../task/render";
|
||||
import type { LspStartupServerInfo } from "../tools";
|
||||
import { isImageProviderPreference, setPreferredImageProvider } from "../tools/image-gen";
|
||||
import { normalizeLocalScheme } from "../tools/path-utils";
|
||||
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
|
||||
import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
|
||||
@@ -103,6 +104,12 @@ import type { EventBus } from "../utils/event-bus";
|
||||
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
||||
import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
|
||||
import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
|
||||
import {
|
||||
isSearchProviderId,
|
||||
isSearchProviderPreference,
|
||||
setExcludedSearchProviders,
|
||||
setPreferredSearchProvider,
|
||||
} from "../web/search";
|
||||
import type { AssistantMessageComponent } from "./components/assistant-message";
|
||||
import type { BashExecutionComponent } from "./components/bash-execution";
|
||||
import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
|
||||
@@ -904,6 +911,22 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
// up the destination project's configuration.
|
||||
if (isSettingsInitialized()) {
|
||||
await settings.reloadForCwd(newCwd);
|
||||
// Reapply provider preferences from the newly-loaded settings so the
|
||||
// module-level search/image provider state reflects the destination
|
||||
// project's configuration. Without this, the previous project's
|
||||
// exclusions leak and newly-excluded providers are still used.
|
||||
const excludedWebSearchProviders = settings.get("providers.webSearchExclude");
|
||||
if (Array.isArray(excludedWebSearchProviders)) {
|
||||
setExcludedSearchProviders(excludedWebSearchProviders.filter(isSearchProviderId));
|
||||
}
|
||||
const webSearchProvider = settings.get("providers.webSearch");
|
||||
if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
|
||||
setPreferredSearchProvider(webSearchProvider);
|
||||
}
|
||||
const imageProvider = settings.get("providers.image");
|
||||
if (isImageProviderPreference(imageProvider)) {
|
||||
setPreferredImageProvider(imageProvider);
|
||||
}
|
||||
}
|
||||
// Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
|
||||
// the next prompt sees everything scoped to the new project directory.
|
||||
|
||||
@@ -177,6 +177,7 @@ import {
|
||||
getSearchTools,
|
||||
HIDDEN_TOOLS,
|
||||
isImageProviderPreference,
|
||||
isSearchProviderId,
|
||||
isSearchProviderPreference,
|
||||
type LspStartupServerInfo,
|
||||
loadSshTool,
|
||||
@@ -185,6 +186,7 @@ import {
|
||||
renderSearchToolBm25Description,
|
||||
SearchTool,
|
||||
SearchToolBm25Tool,
|
||||
setExcludedSearchProviders,
|
||||
setPreferredImageProvider,
|
||||
setPreferredSearchProvider,
|
||||
type Tool,
|
||||
@@ -1164,6 +1166,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
discoveredSkillsPromise?.catch(() => {});
|
||||
|
||||
// Initialize provider preferences from settings
|
||||
const excludedWebSearchProviders = settings.get("providers.webSearchExclude");
|
||||
if (Array.isArray(excludedWebSearchProviders)) {
|
||||
setExcludedSearchProviders(excludedWebSearchProviders.filter(isSearchProviderId));
|
||||
}
|
||||
|
||||
const webSearchProvider = settings.get("providers.webSearch");
|
||||
if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
|
||||
setPreferredSearchProvider(webSearchProvider);
|
||||
|
||||
@@ -300,6 +300,6 @@ export function getSearchTools(): CustomTool<any, any>[] {
|
||||
return [webSearchCustomTool];
|
||||
}
|
||||
|
||||
export { getSearchProvider, setPreferredSearchProvider } from "./provider";
|
||||
export { getSearchProvider, setExcludedSearchProviders, setPreferredSearchProvider } from "./provider";
|
||||
export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
|
||||
export { isSearchProviderPreference } from "./types";
|
||||
export { isSearchProviderId, isSearchProviderPreference } from "./types";
|
||||
|
||||
@@ -127,6 +127,18 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"):
|
||||
preferredProvId = provider;
|
||||
}
|
||||
|
||||
/** Providers excluded from web search resolution via settings. */
|
||||
let excludedProvIds = new Set<SearchProviderId>();
|
||||
|
||||
/** Set providers that web search should never use, including fallbacks. */
|
||||
export function setExcludedSearchProviders(providers: readonly SearchProviderId[]): void {
|
||||
excludedProvIds = new Set(providers);
|
||||
}
|
||||
|
||||
function isSearchProviderExcluded(id: SearchProviderId): boolean {
|
||||
return excludedProvIds.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which providers are configured and currently available.
|
||||
* Each candidate is loaded (and its `isAvailable()` called) only as the chain
|
||||
@@ -138,7 +150,7 @@ export async function resolveProviderChain(
|
||||
): Promise<SearchProvider[]> {
|
||||
const providers: SearchProvider[] = [];
|
||||
|
||||
if (preferredProvider !== "auto") {
|
||||
if (preferredProvider !== "auto" && !isSearchProviderExcluded(preferredProvider)) {
|
||||
const provider = await getSearchProvider(preferredProvider);
|
||||
if (await provider.isExplicitlyAvailable(authStorage)) {
|
||||
providers.push(provider);
|
||||
@@ -146,7 +158,7 @@ export async function resolveProviderChain(
|
||||
}
|
||||
|
||||
for (const id of SEARCH_PROVIDER_ORDER) {
|
||||
if (id === preferredProvider) continue;
|
||||
if (id === preferredProvider || isSearchProviderExcluded(id)) continue;
|
||||
const provider = await getSearchProvider(id);
|
||||
if (await provider.isAvailable(authStorage)) {
|
||||
providers.push(provider);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
||||
import { SelectorController } from "@oh-my-pi/pi-coding-agent/modes/controllers/selector-controller";
|
||||
import {
|
||||
resolveProviderChain,
|
||||
setExcludedSearchProviders,
|
||||
setPreferredSearchProvider,
|
||||
} from "@oh-my-pi/pi-coding-agent/web/search/provider";
|
||||
import { SEARCH_PROVIDER_ORDER } from "@oh-my-pi/pi-coding-agent/web/search/types";
|
||||
|
||||
const authStorage = {} as AuthStorage;
|
||||
const originalBraveApiKey = process.env.BRAVE_API_KEY;
|
||||
const originalJinaApiKey = process.env.JINA_API_KEY;
|
||||
|
||||
function enableKeyBackedProviders(): void {
|
||||
process.env.BRAVE_API_KEY = "test-brave-key";
|
||||
process.env.JINA_API_KEY = "test-jina-key";
|
||||
}
|
||||
|
||||
function restoreEnv(): void {
|
||||
if (originalBraveApiKey === undefined) {
|
||||
delete process.env.BRAVE_API_KEY;
|
||||
} else {
|
||||
process.env.BRAVE_API_KEY = originalBraveApiKey;
|
||||
}
|
||||
|
||||
if (originalJinaApiKey === undefined) {
|
||||
delete process.env.JINA_API_KEY;
|
||||
} else {
|
||||
process.env.JINA_API_KEY = originalJinaApiKey;
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
setPreferredSearchProvider("auto");
|
||||
setExcludedSearchProviders([]);
|
||||
restoreEnv();
|
||||
});
|
||||
|
||||
describe("resolveProviderChain", () => {
|
||||
it("omits excluded providers from the fallback chain", async () => {
|
||||
enableKeyBackedProviders();
|
||||
setExcludedSearchProviders(SEARCH_PROVIDER_ORDER.filter(id => id !== "jina"));
|
||||
|
||||
const providers = await resolveProviderChain(authStorage, "auto");
|
||||
|
||||
expect(providers.map(provider => provider.id)).toEqual(["jina"]);
|
||||
});
|
||||
|
||||
it("ignores the preferred provider when it is excluded", async () => {
|
||||
enableKeyBackedProviders();
|
||||
setExcludedSearchProviders(SEARCH_PROVIDER_ORDER.filter(id => id !== "jina"));
|
||||
|
||||
const providers = await resolveProviderChain(authStorage, "brave");
|
||||
|
||||
expect(providers.map(provider => provider.id)).toEqual(["jina"]);
|
||||
});
|
||||
|
||||
it("applies live settings edits to the exclusion chain", async () => {
|
||||
enableKeyBackedProviders();
|
||||
const controller = new SelectorController({} as unknown as ConstructorParameters<typeof SelectorController>[0]);
|
||||
|
||||
controller.handleSettingChange(
|
||||
"providers.webSearchExclude",
|
||||
SEARCH_PROVIDER_ORDER.filter(id => id !== "jina"),
|
||||
);
|
||||
|
||||
const providers = await resolveProviderChain(authStorage, "auto");
|
||||
|
||||
expect(providers.map(provider => provider.id)).toEqual(["jina"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user