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_GEMINI=1 - Enable Gemini CLI provider
* CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider * CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider
* CCS_WEBSEARCH_GROK=1 - Enable Grok CLI 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 * CCS_DEBUG=1 - Enable debug output
* *
* Exit codes: * Exit codes:
@@ -61,8 +61,8 @@ const PROVIDER_CONFIG = {
opencode: { opencode: {
// Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var) // Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var)
model: 'opencode/gpt-5-nano', model: 'opencode/grok-code',
// Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet // Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet, opencode/gpt-5-nano
// Provider-specific: OpenCode has built-in web search via Zen // Provider-specific: OpenCode has built-in web search via Zen
toolInstruction: 'Search the web using your built-in capabilities.', toolInstruction: 'Search the web using your built-in capabilities.',
+4 -4
View File
@@ -144,8 +144,8 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
}, },
opencode: { opencode: {
enabled: partial.websearch?.providers?.opencode?.enabled ?? false, enabled: partial.websearch?.providers?.opencode?.enabled ?? false,
model: partial.websearch?.providers?.opencode?.model ?? 'opencode/gpt-5-nano', model: partial.websearch?.providers?.opencode?.model ?? 'opencode/grok-code',
timeout: partial.websearch?.providers?.opencode?.timeout ?? 60, timeout: partial.websearch?.providers?.opencode?.timeout ?? 90,
}, },
grok: { grok: {
enabled: partial.websearch?.providers?.grok?.enabled ?? false, enabled: partial.websearch?.providers?.grok?.enabled ?? false,
@@ -392,8 +392,8 @@ export function getWebSearchConfig(): {
const opencodeConfig = { const opencodeConfig = {
enabled: config.websearch?.providers?.opencode?.enabled ?? false, enabled: config.websearch?.providers?.opencode?.enabled ?? false,
model: config.websearch?.providers?.opencode?.model ?? 'opencode/gpt-5-nano', model: config.websearch?.providers?.opencode?.model ?? 'opencode/grok-code',
timeout: config.websearch?.providers?.opencode?.timeout ?? 60, timeout: config.websearch?.providers?.opencode?.timeout ?? 90,
}; };
const grokConfig = { const grokConfig = {
+1 -1
View File
@@ -127,7 +127,7 @@ export interface GrokWebSearchConfig {
export interface OpenCodeWebSearchConfig { export interface OpenCodeWebSearchConfig {
/** Enable OpenCode CLI for WebSearch (default: false) */ /** Enable OpenCode CLI for WebSearch (default: false) */
enabled?: boolean; enabled?: boolean;
/** Model to use (default: opencode/gpt-5-nano) */ /** Model to use (default: opencode/grok-code) */
model?: string; model?: string;
/** Timeout in seconds (default: 60) */ /** Timeout in seconds (default: 60) */
timeout?: number; timeout?: number;
+43 -8
View File
@@ -26,8 +26,11 @@ const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks');
// Hook file name // Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs'; const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
// Default hook timeout in seconds (Gemini CLI needs time) // Buffer time added to max provider timeout for hook timeout (seconds)
const HOOK_TIMEOUT_SECONDS = 60; 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 // Path to Claude settings.json
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.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 * 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 * Install: curl -fsSL https://opencode.ai/install | bash
* *
* @returns OpenCode CLI status with path and version * @returns OpenCode CLI status with path and version
@@ -482,9 +485,27 @@ export function hasWebSearchHook(): boolean {
/** /**
* Get WebSearch hook configuration for settings.json * Get WebSearch hook configuration for settings.json
* Timeout is computed from max provider timeout in config.yaml + buffer
*/ */
export function getWebSearchHookConfig(): Record<string, unknown> { export function getWebSearchHookConfig(): Record<string, unknown> {
const hookPath = path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK); 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 { return {
PreToolUse: [ PreToolUse: [
@@ -494,7 +515,7 @@ export function getWebSearchHookConfig(): Record<string, unknown> {
{ {
type: 'command', type: 'command',
command: `node "${hookPath}"`, command: `node "${hookPath}"`,
timeout: HOOK_TIMEOUT_SECONDS, timeout: hookTimeout,
}, },
], ],
}, },
@@ -538,16 +559,30 @@ function ensureHookConfig(): boolean {
}); });
if (webSearchHookIndex !== -1) { 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 existingHook = hooks.PreToolUse[webSearchHookIndex] as Record<string, unknown>;
const existingHooks = existingHook.hooks as Array<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) { if (existingHooks?.[0]?.command !== expectedCommand) {
// Update the hook command to point to new file
existingHooks[0].command = expectedCommand; 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'); fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8');
if (process.env.CCS_DEBUG) { 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; return true;
@@ -629,7 +664,7 @@ export function getWebSearchHookEnv(): Record<string, string> {
} }
// Use opencode timeout if no gemini timeout set // Use opencode timeout if no gemini timeout set
if (!env.CCS_WEBSEARCH_TIMEOUT) { 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: model:
providers.opencode?.model ?? providers.opencode?.model ??
existingConfig.websearch?.providers?.opencode?.model ?? existingConfig.websearch?.providers?.opencode?.model ??
'opencode/gpt-5-nano', 'opencode/grok-code',
timeout: timeout:
providers.opencode?.timeout ?? providers.opencode?.timeout ??
existingConfig.websearch?.providers?.opencode?.timeout ?? existingConfig.websearch?.providers?.opencode?.timeout ??