fix(websearch): support Gemini positional prompt mode

This commit is contained in:
Tam Nhu Tran
2026-02-26 22:12:16 +07:00
parent bf198e4af0
commit 0d09604bb9
2 changed files with 86 additions and 60 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
# WebSearch Configuration Guide # WebSearch Configuration Guide
Last Updated: 2026-02-04 Last Updated: 2026-02-26
CCS provides automatic web search capability for all profiles, including third-party providers that cannot access Anthropic's native WebSearch API. CCS provides automatic web search capability for all profiles, including third-party providers that cannot access Anthropic's native WebSearch API.
@@ -19,7 +19,7 @@ Third-party profiles (OAuth and API-based) cannot use Anthropic's WebSearch beca
CCS solves this with a hybrid fallback approach: CCS solves this with a hybrid fallback approach:
1. **Gemini CLI Transformer** (Primary) - Uses `gemini -p` with `google_web_search` tool 1. **Gemini CLI Transformer** (Primary) - Uses positional Gemini prompt mode (with legacy `-p` fallback) and `google_web_search` tool
2. **MCP Fallback Chain** (Secondary) - MCP-based web search servers 2. **MCP Fallback Chain** (Secondary) - MCP-based web search servers
## Architecture ## Architecture
@@ -53,7 +53,7 @@ The **ultimate solution** for third-party WebSearch. Uses `gemini` CLI with OAut
### How It Works ### How It Works
1. A PreToolUse hook intercepts WebSearch tool calls 1. A PreToolUse hook intercepts WebSearch tool calls
2. Executes `gemini -p` with explicit google_web_search instruction 2. Executes `gemini "<prompt>"` (positional mode) with explicit google_web_search instruction
3. Returns search results directly to Claude via the hook's deny reason 3. Returns search results directly to Claude via the hook's deny reason
4. Claude receives full search results and continues the conversation 4. Claude receives full search results and continues the conversation
+83 -57
View File
@@ -282,6 +282,61 @@ async function processHook() {
/** /**
* Execute search via Gemini CLI * Execute search via Gemini CLI
*/ */
function shouldRetryGeminiWithLegacyPrompt(errorMessage) {
const lowerError = (errorMessage || '').toLowerCase();
return (
lowerError.includes('unknown option') ||
lowerError.includes('unknown argument') ||
lowerError.includes('unrecognized option') ||
lowerError.includes('usage: gemini') ||
lowerError.includes('use --prompt') ||
lowerError.includes('using the --prompt option')
);
}
function runGeminiCommand(args, timeoutMs) {
const spawnResult = spawnSync('gemini', args, {
encoding: 'utf8',
timeout: timeoutMs,
maxBuffer: 1024 * 1024 * 2,
stdio: ['pipe', 'pipe', 'pipe'],
shell: isWindows,
});
if (spawnResult.error) {
if (spawnResult.error.code === 'ENOENT') {
return { success: false, error: 'Gemini CLI not installed' };
}
throw spawnResult.error;
}
if (spawnResult.status !== 0) {
const stderr = (spawnResult.stderr || '').trim();
return {
success: false,
error: stderr || `Gemini CLI exited with code ${spawnResult.status}`,
};
}
const result = (spawnResult.stdout || '').trim();
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
return { success: false, error: 'Empty or too short response from Gemini' };
}
const lowerResult = result.toLowerCase();
if (
lowerResult.includes('error:') ||
lowerResult.includes('failed to') ||
lowerResult.includes('authentication required')
) {
return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` };
}
return { success: true, content: result };
}
function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) { function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
try { try {
const timeoutMs = timeoutSec * 1000; const timeoutMs = timeoutSec * 1000;
@@ -290,54 +345,31 @@ function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
// Allow model override via env var // Allow model override via env var
const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model; const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model;
const baseArgs = ['--model', model, '--yolo'];
const positionalArgs = [...baseArgs, prompt];
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo "..."`);
}
// Current Gemini CLI prefers positional prompts and deprecates -p/--prompt.
// Retry once with -p for legacy CLIs that still require it.
const positionalResult = runGeminiCommand(positionalArgs, timeoutMs);
if (positionalResult.success) {
return positionalResult;
}
if (!shouldRetryGeminiWithLegacyPrompt(positionalResult.error)) {
return positionalResult;
}
if (process.env.CCS_DEBUG) {
console.error('[CCS Hook] Positional Gemini prompt failed; retrying with -p for legacy CLI');
console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`); console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`);
} }
const spawnResult = spawnSync( const legacyPromptArgs = [...baseArgs, '-p', prompt];
'gemini', return runGeminiCommand(legacyPromptArgs, timeoutMs);
['--model', model, '--yolo', '-p', prompt],
{
encoding: 'utf8',
timeout: timeoutMs,
maxBuffer: 1024 * 1024 * 2,
stdio: ['pipe', 'pipe', 'pipe'],
shell: isWindows,
}
);
if (spawnResult.error) {
if (spawnResult.error.code === 'ENOENT') {
return { success: false, error: 'Gemini CLI not installed' };
}
throw spawnResult.error;
}
if (spawnResult.status !== 0) {
const stderr = (spawnResult.stderr || '').trim();
return {
success: false,
error: stderr || `Gemini CLI exited with code ${spawnResult.status}`,
};
}
const result = (spawnResult.stdout || '').trim();
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
return { success: false, error: 'Empty or too short response from Gemini' };
}
const lowerResult = result.toLowerCase();
if (
lowerResult.includes('error:') ||
lowerResult.includes('failed to') ||
lowerResult.includes('authentication required')
) {
return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` };
}
return { success: true, content: result };
} catch (err) { } catch (err) {
if (err.killed) { if (err.killed) {
return { success: false, error: 'Gemini CLI timed out' }; return { success: false, error: 'Gemini CLI timed out' };
@@ -362,17 +394,13 @@ function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`); console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`);
} }
const spawnResult = spawnSync( const spawnResult = spawnSync('opencode', ['run', prompt, '--model', model], {
'opencode', encoding: 'utf8',
['run', prompt, '--model', model], timeout: timeoutMs,
{ maxBuffer: 1024 * 1024 * 2,
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
timeout: timeoutMs, shell: isWindows,
maxBuffer: 1024 * 1024 * 2, });
stdio: ['pipe', 'pipe', 'pipe'],
shell: isWindows,
}
);
if (spawnResult.error) { if (spawnResult.error) {
if (spawnResult.error.code === 'ENOENT') { if (spawnResult.error.code === 'ENOENT') {
@@ -599,9 +627,7 @@ function outputNoToolsMessage(query) {
* Output all providers failed message * Output all providers failed message
*/ */
function outputAllFailedMessage(query, errors) { function outputAllFailedMessage(query, errors) {
const errorDetails = errors const errorDetails = errors.map((e) => ` - ${e.provider}: ${e.error}`).join('\n');
.map((e) => ` - ${e.provider}: ${e.error}`)
.join('\n');
const message = [ const message = [
'[WebSearch - All Providers Failed]', '[WebSearch - All Providers Failed]',