feat: add log-level system to suppress noisy fallback debug output

Replace console.debug/log/warn calls in model router, retry, and daemon
startup with a structured logger that respects a configurable log_level.
Default level is 'info', suppressing verbose fallback debug messages in
the TUI while keeping them available via config when needed.

- Add src/logger.ts with debug/info/warn/error/silent levels
- Wire log_level into config schema (default: 'info')
- Initialize log level in both daemon and TUI startup paths
- Convert all console.debug in router.ts and retry.ts to logger.debug
- Convert console.log/warn in daemon/models.ts to logger.info/warn
This commit is contained in:
William Valentin
2026-02-09 21:23:07 -08:00
parent 94946eb7a8
commit 35f4cab0dc
8 changed files with 79 additions and 16 deletions
+4
View File
@@ -1,6 +1,10 @@
# Flynn Configuration
# Copy to ~/.config/flynn/config.yaml and customize
# Log verbosity: debug | info | warn | error | silent (default: info)
# Set to 'debug' to see model fallback details.
# log_level: info
telegram:
bot_token: ${FLYNN_TELEGRAM_TOKEN}
allowed_chat_ids: [] # Add your Telegram chat ID
+3
View File
@@ -4,6 +4,7 @@ import { loadConfigSafe, getConfigPath } from './shared.js';
import { existsSync, mkdirSync, readFileSync } from 'fs';
import { resolve } from 'path';
import { homedir } from 'os';
import { setLogLevel } from '../logger.js';
// ANSI color codes for tool status display
const toolColors = {
@@ -43,6 +44,8 @@ export function registerTuiCommand(program: Command): void {
// Dynamic imports to keep CLI startup fast
const { SessionStore, SessionManager } = await import('../session/index.js');
setLogLevel(config.log_level);
const { MinimalTui, startFullscreenTui } = await import('../frontends/tui/index.js');
const { NativeAgent } = await import('../backends/index.js');
const { ToolRegistry, ToolExecutor, allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager } = await import('../tools/index.js');
+4
View File
@@ -345,7 +345,10 @@ const sessionsSchema = z.object({
ttl: z.string().default('30d'),
}).default({});
const logLevelSchema = z.enum(['debug', 'info', 'warn', 'error', 'silent']).default('info');
export const configSchema = z.object({
log_level: logLevelSchema,
telegram: telegramSchema,
discord: discordSchema,
slack: slackSchema,
@@ -406,3 +409,4 @@ export type HeartbeatCheck = z.infer<typeof heartbeatCheckSchema>;
export type EmbeddingConfig = z.infer<typeof embeddingSchema>;
export type EmbeddingProvider = z.infer<typeof embeddingProviderSchema>;
export type PairingCodeConfig = z.infer<typeof pairingSchema>;
export type LogLevel = z.infer<typeof logLevelSchema>;
+4
View File
@@ -9,6 +9,7 @@ import type { AudioTranscriptionConfig } from '../models/media.js';
import type { ToolRegistry, ToolExecutor, BrowserManager } from '../tools/index.js';
import type { AgentConfigRegistry, AgentRouter } from '../agents/index.js';
import type { SandboxManager } from '../sandbox/index.js';
import { setLogLevel } from '../logger.js';
// ── Daemon Modules ──
import { Lifecycle } from './lifecycle.js';
@@ -51,6 +52,9 @@ export interface DaemonContext {
}
export async function startDaemon(config: Config): Promise<DaemonContext> {
// ── Log level ──
setLogLevel(config.log_level);
const lifecycle = new Lifecycle();
// ── Data & Sessions ──
+6 -5
View File
@@ -1,6 +1,7 @@
import type { Config, ModelConfig } from '../config/index.js';
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, GeminiClient, BedrockClient, GitHubModelsClient, ModelRouter, DEFAULT_RETRY_CONFIG } from '../models/index.js';
import type { ModelClient, RetryConfig, ModelTier } from '../models/index.js';
import { logger } from '../logger.js';
/**
* Create a ModelClient from a provider config entry.
@@ -166,7 +167,7 @@ export function createModelRouter(config: Config): ModelRouter {
// Named provider from local_providers map
fallbackChain.push(createClientFromConfig(models.local_providers[providerName]));
} else {
console.warn(`Fallback chain entry "${providerName}" not found — skipping`);
logger.warn(`Fallback chain entry "${providerName}" not found — skipping`);
}
}
@@ -211,13 +212,13 @@ export function createModelRouter(config: Config): ModelRouter {
if (tierFallbacks.size > 0) {
const tierNames = Array.from(tierFallbacks.keys()).join(', ');
console.log(`Per-tier fallbacks configured for: ${tierNames}`);
logger.info(`Per-tier fallbacks configured for: ${tierNames}`);
}
if (autoFallbackTiers.length > 0) {
console.log(`Auto same-model fallback (via GitHub Models) for: ${autoFallbackTiers.join(', ')}`);
logger.info(`Auto same-model fallback (via GitHub Models) for: ${autoFallbackTiers.join(', ')}`);
}
console.log(`Model router: default=${models.default.provider}/${models.default.model}, ` +
logger.info(`Model router: default=${models.default.provider}/${models.default.model}, ` +
`fallback=[${models.fallback_chain.join(', ')}]`);
// Build retry config if enabled
@@ -230,7 +231,7 @@ export function createModelRouter(config: Config): ModelRouter {
} : undefined;
if (retryConfig) {
console.log(`Retry policy: max_retries=${retryConfig.maxRetries}, initial_delay=${retryConfig.initialDelayMs}ms`);
logger.info(`Retry policy: max_retries=${retryConfig.maxRetries}, initial_delay=${retryConfig.initialDelayMs}ms`);
}
return new ModelRouter({
+44
View File
@@ -0,0 +1,44 @@
/** Simple log-level utility.
*
* Default level is `info`, which suppresses `debug` output.
* Set to `debug` (via config or `setLevel()`) to see all messages.
*/
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
const LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
silent: 4,
};
let currentLevel: LogLevel = 'info';
export function setLogLevel(level: LogLevel): void {
currentLevel = level;
}
export function getLogLevel(): LogLevel {
return currentLevel;
}
function shouldLog(level: LogLevel): boolean {
return LEVELS[level] >= LEVELS[currentLevel];
}
export const logger = {
debug(...args: unknown[]): void {
if (shouldLog('debug')) console.debug(...args);
},
info(...args: unknown[]): void {
if (shouldLog('info')) console.log(...args);
},
warn(...args: unknown[]): void {
if (shouldLog('warn')) console.warn(...args);
},
error(...args: unknown[]): void {
if (shouldLog('error')) console.error(...args);
},
};
+3 -1
View File
@@ -1,3 +1,5 @@
import { logger } from '../logger.js';
export interface RetryConfig {
/** Maximum number of retry attempts (default: 3). Does not count the initial attempt. */
maxRetries: number;
@@ -60,7 +62,7 @@ export async function withRetry<T>(
const delay = Math.min(baseDelay, config.maxDelayMs);
const jitter = delay * (0.5 + Math.random() * 0.5); // 50-100% of delay
console.debug(
logger.debug(
`[retry] ${label ?? 'operation'} attempt ${attempt + 1}/${config.maxRetries} failed: ${lastError.message}. Retrying in ${Math.round(jitter)}ms...`,
);
+11 -10
View File
@@ -1,6 +1,7 @@
import type { ChatRequest, ChatResponse, ChatStreamEvent, ModelClient } from './types.js';
import { withRetry } from './retry.js';
import type { RetryConfig } from './retry.js';
import { logger } from '../logger.js';
export type ModelTier = 'fast' | 'default' | 'complex' | 'local';
@@ -76,7 +77,7 @@ export class ModelRouter implements ModelClient {
return await primaryClient.chat(request);
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
console.debug(`Primary model failed: ${errors[0].message}`);
logger.debug(`Primary model failed: ${errors[0].message}`);
}
// Try tier-specific fallbacks first
@@ -84,12 +85,12 @@ export class ModelRouter implements ModelClient {
for (let i = 0; i < tierFallbackList.length; i++) {
try {
const reason = `Primary model failed (${errors[0].message}), using tier fallback #${i + 1}`;
console.debug(reason);
logger.debug(reason);
const response = await tierFallbackList[i].chat(request);
return { ...response, fallback: true, fallbackReason: reason };
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
console.debug(`Tier fallback #${i + 1} failed: ${errors[errors.length - 1].message}`);
logger.debug(`Tier fallback #${i + 1} failed: ${errors[errors.length - 1].message}`);
}
}
@@ -98,12 +99,12 @@ export class ModelRouter implements ModelClient {
const fallbackClient = this.fallbackChain[i];
try {
const reason = `Primary model failed (${errors[0].message}), using global fallback #${i + 1}`;
console.debug(reason);
logger.debug(reason);
const response = await fallbackClient.chat(request);
return { ...response, fallback: true, fallbackReason: reason };
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
console.debug(`Global fallback #${i + 1} failed: ${errors[errors.length - 1].message}`);
logger.debug(`Global fallback #${i + 1} failed: ${errors[errors.length - 1].message}`);
}
}
@@ -121,7 +122,7 @@ export class ModelRouter implements ModelClient {
if (event.type === 'error') {
hasError = true;
primaryError = event.error?.message ?? 'Unknown error';
console.debug(`Primary stream failed: ${primaryError}`);
logger.debug(`Primary stream failed: ${primaryError}`);
break;
}
yield event;
@@ -139,14 +140,14 @@ export class ModelRouter implements ModelClient {
if (!fallbackClient.chatStream) continue;
const reason = `Primary model failed (${primaryError}), using tier fallback #${i + 1}`;
console.debug(reason);
logger.debug(reason);
yield { type: 'fallback_warning', fallbackReason: reason };
let hasError = false;
for await (const event of fallbackClient.chatStream(request)) {
if (event.type === 'error') {
hasError = true;
console.debug(`Tier fallback stream #${i + 1} failed: ${event.error?.message}`);
logger.debug(`Tier fallback stream #${i + 1} failed: ${event.error?.message}`);
break;
}
yield event;
@@ -161,14 +162,14 @@ export class ModelRouter implements ModelClient {
if (!fallbackClient.chatStream) continue;
const reason = `Primary model failed (${primaryError}), using global fallback #${i + 1}`;
console.debug(reason);
logger.debug(reason);
yield { type: 'fallback_warning', fallbackReason: reason };
let hasError = false;
for await (const event of fallbackClient.chatStream(request)) {
if (event.type === 'error') {
hasError = true;
console.debug(`Global fallback stream #${i + 1} failed: ${event.error?.message}`);
logger.debug(`Global fallback stream #${i + 1} failed: ${event.error?.message}`);
break;
}
yield event;