목록으로 돌아가기
Google Gemini CLI 오픈소스 기여: 토큰 에러 핸들링 시스템 구현

Google Gemini CLI 오픈소스 기여: 토큰 에러 핸들링 시스템 구현

2025년 9월 28일
3개 태그
gemini
google
test

🚀 시작하며

안녕하세요! 이번에는 Google에서 개발한 Gemini CLI 오픈소스 프로젝트에 기여한 경험을 공유하고자 합니다. 특히 토큰 제한 에러를 효과적으로 처리하는 시스템을 구현하는 과정에서 얻은 인사이트와 기술적 도전을 중심으로 이야기해보겠습니다.

📋 프로젝트 소개

Gemini CLI는 터미널에서 직접 Gemini AI 모델에 접근할 수 있게 해주는 오픈소스 도구입니다. 주요 특징은 다음과 같습니다:

  • 무료 티어: 개인 Google 계정으로 분당 60회, 일일 1,000회 요청 가능
  • 강력한 Gemini 2.5 Pro: 100만 토큰 컨텍스트 윈도우 지원
  • 내장 도구: Google 검색, 파일 작업, 셸 명령어, 웹 페칭 등
  • 확장 가능: MCP(Model Context Protocol) 지원으로 커스텀 통합 가능
  • 터미널 중심: 개발자를 위한 명령줄 인터페이스 설계
  • 오픈소스: Apache 2.0 라이선스

🎯 기여한 기능: 토큰 에러 핸들링 시스템

문제 상황

Gemini CLI를 사용하다 보면 다음과 같은 상황이 자주 발생했습니다:

⚠️  Input is too long to process.
📊 Tokens used: 5,911,388 / 1,048,576 (564%)

사용자가 긴 코드베이스나 대용량 파일을 분석하려고 할 때 토큰 제한에 걸려 작업이 중단되는 문제였습니다. 기존에는 단순히 에러 메시지만 표시하고 작업이 실패했지만, 이를 개선하여 자동 복구 및 재시도 메커니즘을 구현했습니다.

구현한 솔루션

1. 토큰 관리자 (TokenManager)

export class TokenManager {
  private tokenUsage: number = 0;
  private readonly config: TokenManagerConfig;

  constructor(config: TokenManagerConfig) {
    this.config = config;
  }

  checkTokenLimit(projectedTokens: number): TokenStatus {
    const totalProjected = this.tokenUsage + projectedTokens;

    if (totalProjected > this.config.maxTokens) {
      return TokenStatus.LIMIT_EXCEEDED;
    }

    if (totalProjected > this.config.maxTokens * this.config.warningThreshold) {
      return TokenStatus.WARNING;
    }

    return TokenStatus.OK;
  }

  updateTokenUsage(usage: TokenUsageInfo): void {
    this.tokenUsage += usage.totalTokens;
  }

  shouldCompress(): boolean {
    return (
      this.tokenUsage > this.config.maxTokens * this.config.compressionThreshold
    );
  }
}

핵심 기능:

  • 토큰 사용량 실시간 추적
  • 사전 토큰 제한 검사
  • 압축 필요성 판단
  • 사용량 초기화 및 관리

2. 토큰 에러 감지 및 파싱

export function isTokenLimitExceededError(
  error: unknown,
): error is TokenLimitError {
  if (typeof error === 'string') {
    return (
      error.includes('exceeds the maximum number of tokens allowed') ||
      (error.includes('INVALID_ARGUMENT') && error.includes('token count'))
    );
  }

  if (error && typeof error === 'object') {
    const errorObj = error as Record<string, unknown>;

    // 다양한 에러 형태 지원
    if (errorObj['message'] && typeof errorObj['message'] === 'string') {
      const message = errorObj['message'];
      return (
        message.includes('exceeds the maximum number of tokens allowed') ||
        (message.includes('INVALID_ARGUMENT') && message.includes('token count'))
      );
    }
  }

  return false;
}

export function extractTokenInfo(
  error: string,
): { current: number; max: number } | null {
  const tokenCountMatch = error.match(
    /token count \\((\\d+)\\) exceeds the maximum number of tokens allowed \\((\\d+)\\)/,
  );
  if (tokenCountMatch) {
    return {
      current: parseInt(tokenCountMatch[1], 10),
      max: parseInt(tokenCountMatch[2], 10),
    };
  }
  return null;
}

핵심 기능:

  • 다양한 에러 형태에서 토큰 제한 에러 감지
  • 에러 메시지에서 현재/최대 토큰 수 추출
  • 타입 안전성 보장

3. 사용자 친화적 에러 메시지 생성

export function getTokenLimitErrorMessage(
  error: TokenLimitError,
  authType?: AuthType,
  userTier?: UserTierId,
  currentModel?: string,
  fallbackModel?: string,
): string {
  const { currentTokens, maxTokens } = error;
  const usagePercent = Math.round((currentTokens / maxTokens) * 100);

  let baseMessage = `⚠️  Input is too long to process.\\n`;
  baseMessage += `📊 Tokens used: ${currentTokens.toLocaleString()} / ${maxTokens.toLocaleString()} (${usagePercent}%)\\n`;

  // 인증 타입별 복구 제안
  switch (authType) {
    case AuthType.LOGIN_WITH_GOOGLE: {
      const isPaidTier = userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
      if (isPaidTier) {
        baseMessage += `🔄 Automatically switching to ${fallbackModel || DEFAULT_GEMINI_FLASH_MODEL} model and retrying.\\n`;
        baseMessage += `💡 For higher token limits, try using an AI Studio API key: <https://aistudio.google.com/apikey`>;
      } else {
        baseMessage += `🔄 Automatically switching to ${fallbackModel || DEFAULT_GEMINI_FLASH_MODEL} model and retrying.\\n`;
        baseMessage += `💡 For higher limits, upgrade your plan or use an AI Studio API key: <https://aistudio.google.com/apikey`>;
      }
      break;
    }
    case AuthType.USE_GEMINI:
      baseMessage += `🔄 Compressing context and retrying.\\n`;
      baseMessage += `💡 For higher limits, request quota increase in AI Studio.`;
      break;
    case AuthType.USE_VERTEX_AI:
      baseMessage += `🔄 Compressing context and retrying.\\n`;
      baseMessage += `💡 For higher limits, request quota increase in Vertex AI.`;
      break;
    default:
      baseMessage += `🔄 Automatically switching to ${fallbackModel || DEFAULT_GEMINI_FLASH_MODEL} model and retrying.`;
  }

  return baseMessage;
}

핵심 기능:

  • 인증 타입별 맞춤형 복구 제안
  • 사용자 티어에 따른 차별화된 메시지
  • 이모지를 활용한 직관적인 UI

4. 컨텍스트 압축 및 자동 재시도

export class TokenErrorRetryHandler {
  private readonly compressor: ContextCompressor;
  private readonly tokenManager: TokenManager;

  async handleTokenLimitError<T>(
    content: Content[],
    apiCall: (compressedContent: Content[]) => Promise<T>,
    options?: Partial<TokenRetryOptions>,
  ): Promise<T> {
    const retryOptions = { ...DEFAULT_RETRY_OPTIONS, ...options };
    let lastError: Error | null = null;

    for (let attempt = 0; attempt < retryOptions.maxRetries; attempt++) {
      try {
        let contentToSend = content;

        // 첫 번째 시도가 아닌 경우 압축 적용
        if (attempt > 0) {
          const compressionOptions: CompressionOptions = {
            strategy: CompressionStrategy.CHUNK_AND_SUMMARIZE,
            maxTokens: Math.floor(this.tokenManager.getRemainingCapacity() * 0.8),
            preserveRecent: retryOptions.preserveRecentMessages,
            summaryRatio: retryOptions.compressionRatio,
          };

          const compressionResult = await this.compressor.compressContext(
            content,
            compressionOptions,
          );
          contentToSend = compressionResult.compressedContent;

          this.logger?.(
            `🔄 Compressed context: ${content.length} → ${contentToSend.length} messages (${Math.round(compressionResult.compressionRatio * 100)}% compression)`,
          );
        }

        const result = await apiCall(contentToSend);
        return result;
      } catch (error) {
        lastError = error as Error;

        // 토큰 제한 에러인지 확인
        if (isTokenLimitError(error)) {
          this.logger?.(
            `⚠️  Token limit error occurred (attempt ${attempt + 1}/${retryOptions.maxRetries})`,
          );

          // 지수 백오프 적용
          if (attempt < retryOptions.maxRetries - 1) {
            const delay = Math.pow(retryOptions.backoffMultiplier, attempt) * 1000;
            await new Promise((resolve) => setTimeout(resolve, delay));
          }
        } else {
          // 토큰 제한 에러가 아닌 경우 즉시 재발생
          throw error;
        }
      }
    }

    // 모든 재시도 실패
    throw new Error(
      `Unable to resolve token limit error after ${retryOptions.maxRetries} attempts: ${lastError?.message}`,
    );
  }
}

핵심 기능:

  • 자동 컨텍스트 압축
  • 지수 백오프를 통한 재시도
  • 다양한 압축 전략 지원
  • 상세한 로깅

5. 사전 토큰 모니터링

export class ProactiveTokenMonitor {
  private readonly tokenManager: TokenManager;
  private readonly config: TokenMonitoringConfig;
  private readonly alertCallbacks: TokenAlertCallback[] = [];

  checkTokenUsage(): void {
    const currentUsage = this.tokenManager.getCurrentUsage();
    const maxTokens = this.tokenManager.getRemainingCapacity() + currentUsage;
    const usagePercent = currentUsage / maxTokens;

    let alertLevel: TokenUsageAlert['level'] | null = null;
    let message = '';
    let recommendedAction = '';

    if (usagePercent >= this.config.criticalThreshold) {
      alertLevel = 'critical';
      message = `🚨 Token usage has reached dangerous levels!`;
      recommendedAction = 'Immediately compress context or start a new session.';
    } else if (usagePercent >= this.config.warningThreshold) {
      alertLevel = 'warning';
      message = `⚠️ Token usage is high.`;
      recommendedAction = 'Context compression may be needed soon.';
    } else if (usagePercent >= 0.5) {
      alertLevel = 'info';
      message = `ℹ️ Token usage is at moderate levels.`;
      recommendedAction = 'You can continue using normally.';
    }

    // 알림 레벨이 변경되었거나 중요한 알림인 경우에만 전송
    if (alertLevel && (alertLevel !== this.lastAlertLevel || alertLevel === 'critical')) {
      const alert: TokenUsageAlert = {
        level: alertLevel,
        message,
        currentUsage,
        maxTokens,
        usagePercent: Math.round(usagePercent * 100),
        recommendedAction,
      };

      this.triggerAlert(alert);
      this.lastAlertLevel = alertLevel;
    }
  }
}

핵심 기능:

  • 실시간 토큰 사용량 모니터링
  • 임계값 기반 알림 시스템
  • 압축 권장사항 제공
  • 진행률 표시바 생성

🧪 테스트 코드 작성

포괄적인 테스트 코드를 작성하여 시스템의 안정성을 보장했습니다:

describe('Token Error Handling', () => {
  let tokenManager: TokenManager;

  beforeEach(() => {
    tokenManager = new TokenManager(DEFAULT_TOKEN_CONFIG);
  });

  describe('TokenManager', () => {
    it('should check token limits correctly', () => {
      expect(tokenManager.checkTokenLimit(100)).toBe(TokenStatus.OK);

      // 높은 사용량으로 경고 트리거
      tokenManager.updateTokenUsage({
        promptTokens: 800000,
        completionTokens: 0,
        totalTokens: 800000,
      });
      expect(tokenManager.checkTokenLimit(100000)).toBe(TokenStatus.WARNING);

      // 제한 초과 테스트
      expect(tokenManager.checkTokenLimit(300000)).toBe(TokenStatus.LIMIT_EXCEEDED);
    });
  });

  describe('Token Error Detection', () => {
    it('should detect token limit exceeded errors', () => {
      const errorString = 'The input token count (5911388) exceeds the maximum number of tokens allowed (1048576).';
      expect(isTokenLimitExceededError(errorString)).toBe(true);
    });

    it('should extract token information from error', () => {
      const errorString = 'The input token count (5911388) exceeds the maximum number of tokens allowed (1048576).';
      const tokenInfo = extractTokenInfo(errorString);

      expect(tokenInfo).toEqual({
        current: 5911388,
        max: 1048576,
      });
    });
  });
});

🎨 사용자 경험 개선

Before (기존)

Error: The input token count (5911388) exceeds the maximum number of tokens allowed (1048576).

After (개선 후)

⚠️  Input is too long to process.
📊 Tokens used: 5,911,388 / 1,048,576 (564%)
🔄 Automatically switching to gemini-2.5-flash model and retrying.
💡 For higher token limits, try using an AI Studio API key: <https://aistudio.google.com/apikey>

🔧 기술적 도전과 해결책

1. 다양한 에러 형태 처리

도전: Gemini API에서 반환되는 에러 형태가 일관되지 않았습니다.

해결책:

  • 문자열, Error 객체, 중첩된 에러 객체 등 다양한 형태 지원
  • 타입 가드 함수를 통한 안전한 타입 검사
  • 정규식을 활용한 토큰 정보 추출

2. 컨텍스트 압축 전략

도전: 긴 대화 기록을 효과적으로 압축하면서도 중요한 정보를 보존해야 했습니다.

해결책:

  • 여러 압축 전략 구현 (청크 압축, 최근 메시지 우선, 요약 기반 등)
  • 사용자 설정 가능한 압축 비율
  • 최근 메시지 보존 옵션

3. 성능 최적화

도전: 토큰 계산과 모니터링이 메인 스레드를 블록하지 않아야 했습니다.

해결책:

  • 비동기 처리와 백그라운드 모니터링
  • 지수 백오프를 통한 재시도 최적화
  • 메모리 효율적인 토큰 추적

📊 기여 통계

  • 수정된 파일: 8개
  • 추가된 코드 라인: 약 1,200줄
  • 테스트 커버리지: 95%+
  • 지원하는 압축 전략: 4가지
  • 에러 형태 지원: 5가지

🚀 배운 점과 인사이트

1. 사용자 중심 설계의 중요성

단순히 에러를 표시하는 것이 아니라, 사용자가 다음에 무엇을 해야 하는지 명확하게 안내하는 것이 중요했습니다. 인증 타입과 사용자 티어에 따른 맞춤형 제안을 제공함으로써 사용자 경험을 크게 개선할 수 있었습니다.

2. 점진적 개선의 가치

한 번에 모든 것을 완벽하게 구현하려 하지 않고, 핵심 기능부터 단계적으로 구현했습니다. 이를 통해 빠른 피드백을 받고 지속적으로 개선할 수 있었습니다.

3. 테스트 주도 개발의 효과

포괄적인 테스트 코드를 작성함으로써 리팩토링과 기능 추가 시 안정성을 보장할 수 있었습니다. 특히 에러 처리 로직의 복잡성을 고려할 때 테스트의 중요성을 더욱 실감했습니다.

4. 오픈소스 커뮤니티의 가치

Google의 Gemini CLI 팀과의 소통을 통해 엔터프라이즈급 소프트웨어 개발의 모범 사례를 배울 수 있었습니다. 코드 리뷰 과정에서 받은 피드백은 개인적인 성장에 큰 도움이 되었습니다.

🔮 향후 계획

현재 구현한 토큰 에러 핸들링 시스템을 기반으로 다음과 같은 추가 기능을 고려하고 있습니다:

  1. 스마트 컨텍스트 압축: AI를 활용한 더 지능적인 컨텍스트 압축
  2. 사용 패턴 학습: 사용자별 토큰 사용 패턴 분석 및 최적화 제안
  3. 실시간 대시보드: 토큰 사용량을 시각적으로 모니터링할 수 있는 UI
  4. 자동 모델 전환: 토큰 제한에 따라 자동으로 적절한 모델로 전환

🎉 마무리

Gemini CLI 오픈소스 프로젝트에 기여하면서 단순한 버그 수정을 넘어서 사용자 경험을 근본적으로 개선할 수 있는 기회를 얻었습니다. 특히 토큰 제한이라는 기술적 제약을 사용자 친화적인 솔루션으로 변환하는 과정에서 많은 것을 배웠습니다.

오픈소스 기여는 단순히 코드를 작성하는 것이 아니라, 전 세계 개발자들과 함께 더 나은 도구를 만들어가는 과정입니다. 여러분도 관심 있는 오픈소스 프로젝트에 기여해보시길 추천드립니다!

댓글 (0)

댓글 수정 시 필요합니다. 최소 4자 이상 입력해주세요.

아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!