feat(websearch): dynamic hook timeout from config + grok-code default

- Hook timeout now computed from max provider timeout + 30s buffer
- Removes hardcoded HOOK_TIMEOUT_SECONDS constant
- ensureHookConfig() now updates timeout when it changes
- Default OpenCode model changed from gpt-5-nano to grok-code
- Default OpenCode timeout changed from 60s to 90s
- Single source of truth: config.yaml controls all timeouts
This commit is contained in:
kaitranntt
2025-12-16 23:07:11 -05:00
parent cbc2022ed1
commit d33fefd133
5 changed files with 52 additions and 17 deletions
+3 -3
View File
@@ -13,7 +13,7 @@
* CCS_WEBSEARCH_GEMINI=1 - Enable Gemini CLI provider
* CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider
* CCS_WEBSEARCH_GROK=1 - Enable Grok CLI provider
* CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/gpt-5-nano)
* CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/grok-code)
* CCS_DEBUG=1 - Enable debug output
*
* Exit codes:
@@ -61,8 +61,8 @@ const PROVIDER_CONFIG = {
opencode: {
// Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var)
model: 'opencode/gpt-5-nano',
// Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet
model: 'opencode/grok-code',
// Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet, opencode/gpt-5-nano
// Provider-specific: OpenCode has built-in web search via Zen
toolInstruction: 'Search the web using your built-in capabilities.',
+4 -4
View File
@@ -144,8 +144,8 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
},
opencode: {
enabled: partial.websearch?.providers?.opencode?.enabled ?? false,
model: partial.websearch?.providers?.opencode?.model ?? 'opencode/gpt-5-nano',
timeout: partial.websearch?.providers?.opencode?.timeout ?? 60,
model: partial.websearch?.providers?.opencode?.model ?? 'opencode/grok-code',
timeout: partial.websearch?.providers?.opencode?.timeout ?? 90,
},
grok: {
enabled: partial.websearch?.providers?.grok?.enabled ?? false,
@@ -392,8 +392,8 @@ export function getWebSearchConfig(): {
const opencodeConfig = {
enabled: config.websearch?.providers?.opencode?.enabled ?? false,
model: config.websearch?.providers?.opencode?.model ?? 'opencode/gpt-5-nano',
timeout: config.websearch?.providers?.opencode?.timeout ?? 60,
model: config.websearch?.providers?.opencode?.model ?? 'opencode/grok-code',
timeout: config.websearch?.providers?.opencode?.timeout ?? 90,
};
const grokConfig = {
+1 -1
View File
@@ -127,7 +127,7 @@ export interface GrokWebSearchConfig {
export interface OpenCodeWebSearchConfig {
/** Enable OpenCode CLI for WebSearch (default: false) */
enabled?: boolean;
/** Model to use (default: opencode/gpt-5-nano) */
/** Model to use (default: opencode/grok-code) */
model?: string;
/** Timeout in seconds (default: 60) */
timeout?: number;
+43 -8
View File
@@ -26,8 +26,11 @@ const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks');
// Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
// Default hook timeout in seconds (Gemini CLI needs time)
const HOOK_TIMEOUT_SECONDS = 60;
// Buffer time added to max provider timeout for hook timeout (seconds)
const HOOK_TIMEOUT_BUFFER = 30;
// Minimum hook timeout in seconds (fallback if no providers configured)
const MIN_HOOK_TIMEOUT = 60;
// Path to Claude settings.json
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
@@ -221,7 +224,7 @@ let opencodeCliCache: OpenCodeCliStatus | null = null;
/**
* Check if OpenCode CLI is installed globally
*
* OpenCode provides built-in web search via opencode/gpt-5-nano model.
* OpenCode provides built-in web search via opencode/grok-code model.
* Install: curl -fsSL https://opencode.ai/install | bash
*
* @returns OpenCode CLI status with path and version
@@ -482,9 +485,27 @@ export function hasWebSearchHook(): boolean {
/**
* Get WebSearch hook configuration for settings.json
* Timeout is computed from max provider timeout in config.yaml + buffer
*/
export function getWebSearchHookConfig(): Record<string, unknown> {
const hookPath = path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK);
const wsConfig = getWebSearchConfig();
// Compute max timeout from enabled providers
const timeouts: number[] = [];
if (wsConfig.providers?.gemini?.enabled && wsConfig.providers.gemini.timeout) {
timeouts.push(wsConfig.providers.gemini.timeout);
}
if (wsConfig.providers?.opencode?.enabled && wsConfig.providers.opencode.timeout) {
timeouts.push(wsConfig.providers.opencode.timeout);
}
if (wsConfig.providers?.grok?.enabled && wsConfig.providers.grok.timeout) {
timeouts.push(wsConfig.providers.grok.timeout);
}
// Hook timeout = max provider timeout + buffer (or minimum if none configured)
const maxProviderTimeout = timeouts.length > 0 ? Math.max(...timeouts) : MIN_HOOK_TIMEOUT;
const hookTimeout = maxProviderTimeout + HOOK_TIMEOUT_BUFFER;
return {
PreToolUse: [
@@ -494,7 +515,7 @@ export function getWebSearchHookConfig(): Record<string, unknown> {
{
type: 'command',
command: `node "${hookPath}"`,
timeout: HOOK_TIMEOUT_SECONDS,
timeout: hookTimeout,
},
],
},
@@ -538,16 +559,30 @@ function ensureHookConfig(): boolean {
});
if (webSearchHookIndex !== -1) {
// Hook exists - check if it needs updating (different command path)
// Hook exists - check if it needs updating (different command path or timeout)
const existingHook = hooks.PreToolUse[webSearchHookIndex] as Record<string, unknown>;
const existingHooks = existingHook.hooks as Array<Record<string, unknown>>;
const currentHookConfig = getWebSearchHookConfig();
const expectedHooks = (currentHookConfig.PreToolUse as Array<Record<string, unknown>>)[0]
.hooks as Array<Record<string, unknown>>;
const expectedTimeout = expectedHooks[0].timeout as number;
let needsUpdate = false;
if (existingHooks?.[0]?.command !== expectedCommand) {
// Update the hook command to point to new file
existingHooks[0].command = expectedCommand;
needsUpdate = true;
}
if (existingHooks?.[0]?.timeout !== expectedTimeout) {
existingHooks[0].timeout = expectedTimeout;
needsUpdate = true;
}
if (needsUpdate) {
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8');
if (process.env.CCS_DEBUG) {
console.error(info('Updated WebSearch hook command in settings.json'));
console.error(info('Updated WebSearch hook config in settings.json'));
}
}
return true;
@@ -629,7 +664,7 @@ export function getWebSearchHookEnv(): Record<string, string> {
}
// Use opencode timeout if no gemini timeout set
if (!env.CCS_WEBSEARCH_TIMEOUT) {
env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.opencode.timeout || 60);
env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.opencode.timeout || 90);
}
}
+1 -1
View File
@@ -1498,7 +1498,7 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => {
model:
providers.opencode?.model ??
existingConfig.websearch?.providers?.opencode?.model ??
'opencode/gpt-5-nano',
'opencode/grok-code',
timeout:
providers.opencode?.timeout ??
existingConfig.websearch?.providers?.opencode?.timeout ??