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:
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user