OpenClaw 자동화

내가 고른 모델이 아닌 모델이 답했을 때|OpenClaw 모델 선택 순서와 폴백 규칙

깊음위에 2026. 9. 29. 08:20

OpenClaw를 며칠 굴리다 보면 채팅창에 이런 줄이 한 번은 뜹니다.

↪️ Model Fallback: <fallback> (selected <primary>; <reason>)

분명히 내가 고른 모델이 있는데 다른 모델이 답했다는 뜻이거든요. 어떤 분은 "그럼 내 설정이 무시된 건가" 하시고, 어떤 분은 "그냥 알아서 잘 돌아가는 거겠지" 하고 넘기시더라고요. 둘 다 반만 맞습니다. OpenClaw는 모델을 하나 고르는 게 아니라 후보 줄을 세워두고, 앞에서부터 실패하면 뒤로 넘어가는 구조예요. 그 순서와 규칙이 공식 문서에 꽤 자세히 적혀 있습니다.

이 글은 OpenClaw 공식 문서 concepts/models.md, concepts/model-providers.md, concepts/model-failover.md의 내용을 제가 한국어로 풀어 쓴 것입니다. 원문은 docs.openclaw.ai에서 보실 수 있어요. 제가 직접 측정한 수치나 성능 비교는 없습니다.

모델을 고르는 순서는 세 단계입니다

문서가 말하는 선택 순서는 이렇습니다.

  1. agents.defaults.model.primary (또는 agents.defaults.model을 그냥 문자열로 쓴 값)
  2. agents.defaults.model.fallbacks를 적힌 순서대로
  3. 같은 provider 안에서의 auth 프로필 로테이션

여기서 3번이 2번보다 먼저 일어난다는 게 중요합니다. 문서에 "Auth-profile rotation happens inside a provider before OpenClaw moves to the next fallback model"이라고 적혀 있거든요. 계정이 두 개 붙어 있으면 모델을 바꾸기 전에 계정부터 바꿔 봅니다.

provider/model이라는 표기는 provider와 모델을 고르는 것이지 하위 런타임을 고르는 게 아닙니다. openai/* 접두사만으로 Codex가 선택되지는 않아요.

같은 모델이라도 어디서 골랐느냐에 따라 다르게 굴러갑니다

이게 저는 제일 안 알려진 부분이라고 생각합니다. 똑같은 provider/model인데 그 값이 어디서 왔느냐에 따라 실패했을 때 행동이 달라져요.

출처 실패했을 때
설정 기본값 (model.primary) 보통의 시작점. 설정된 fallbacks를 씁니다
자동 폴백 임시 복구 상태. modelOverrideSource: "auto"로 저장되고, 원래 primary를 주기적으로 다시 찔러본 뒤 회복되면 해제합니다
사용자 세션 선택 (/model, 모델 선택기, sessions.patch) 엄격합니다. modelOverrideSource: "user". 그 provider/model이 안 되면 다른 모델로 몰래 넘어가지 않고 그냥 실패로 보입니다
Cron --model 그 잡의 primary. 잡이 자체 fallbacks를 안 주면 설정된 fallbacks를 그대로 씁니다. fallbacks: []를 주면 엄격 실행이 돼요

그러니까 "폴백이 안 먹는다"고 느끼셨다면 /model로 직접 고른 상태일 가능성이 큽니다. 그건 버그가 아니라 의도된 동작이에요. 내가 명시적으로 고른 모델이 안 되는데 다른 모델이 대신 답해 버리면 그게 더 곤란하니까요.

비슷한 함정이 하나 더 있습니다. agents.defaults.model.primary를 바꿔도 이미 핀이 박힌 세션은 안 바뀝니다. 상태에 This session is pinned to X; config primary Y will apply to new/unpinned sessions.가 보이면 /model default로 핀을 풀어야 해요.

Model is not allowed가 뜨는 이유

이 메시지를 보면 보통 "카탈로그에 없는 모델을 골라서 그렇구나" 하고 생각하게 되는데, 문서는 반대로 말합니다. 카탈로그는 둘러보기용 목록이지 허용 목록이 아니에요. modelPolicy.allow가 비어 있으면 선택기에 안 보이는 provider/model도 직접 적어서 고를 수 있습니다.

대신 agents.defaults.modelPolicy.allow에 값이 하나라도 있으면 그때부터 그게 허용 목록이 됩니다. 밖에 있는 모델을 고르면 답변을 만들기 전에 이렇게 끊깁니다.

Model override "provider/model" is not allowed by agents.defaults.modelPolicy.allow.

고치는 방법은 세 가지예요. 그 모델을 목록에 넣거나, 목록을 비우거나, /model list에 있는 것 중에 고르는 겁니다. provider 단위로 열고 싶으면 뒤에 와일드카드를 씁니다.

{
  agents: {
    defaults: {
      modelPolicy: {
        allow: ["openai/*", "vllm/*"],
      },
    },
  },
}

로컬 모델을 쓰시는 분들은 여기서 한 번씩 걸리십니다. 허용 목록이 켜져 있으면 ollama/gemma4:26b처럼 provider 접두사가 붙은 전체 ref가 필요해요. 파일 이름이나 화면에 보이는 이름만으로는 안 통합니다.

그리고 허용 목록에 넣었다고 그 모델이 동작한다는 뜻은 아닙니다. provider 가용성, 런타임 호환성, 인증은 따로 확인하거든요.

실패하면 모델부터 바꾸지 않습니다

문서는 실패 처리를 두 단계로 정리합니다. 같은 provider 안에서의 auth 프로필 로테이션이 먼저고, 그 다음이 모델 폴백이에요. 그런데 그 앞에 한 단계가 더 있습니다.

일시적인 rate limit이나 provider 장애는 같은 모델로 먼저 다시 시도합니다. 이때 대화 기록을 이어서 쓰기 때문에 이미 만들어진 부분과 끝난 작업이 날아가지 않아요. 에이전트에게는 "중간에 끊긴 동작을 먼저 확인하고 다시 할지 정하라"고 지시가 들어갑니다. 화면에는 대기 시간과 시도 횟수가 표시되고요.

재시도 예산은 종류별로 다릅니다. rate limit은 최대 10회까지, 그 외 일시적 실패는 8회에 90초 창입니다. 대기는 지수적으로 늘되 30초에서 멈추는데, provider가 retry-after로 더 긴 시간을 주면 그건 최소 대기로 지켜집니다.

이 예산을 다 쓰고 나서야 프로필 로테이션이나 모델 폴백으로 갑니다.

계정이 여러 개일 때의 순서

provider 하나에 프로필이 여러 개 붙어 있으면 이 순서로 정해집니다.

  1. openclaw models auth order set으로 저장한 순서
  2. 설정의 auth.order[provider]
  3. auth.profiles 중 그 provider 것
  4. 에이전트별 SQLite에 저장된 프로필

아무것도 지정 안 했으면 라운드로빈으로 돕니다. 1순위 기준은 프로필 종류인데 OAuth가 먼저, 그 다음 static token, 마지막이 API key예요. OAuth끼리는 지금 쓸 수 있는 토큰을 가진 쪽이 앞입니다. 만료된 OAuth도 후보에서 빼지는 않아요. 쓸 만한 다른 프로필이 없으면 갱신해서 쓸 수 있으니까요. 그 다음이 마지막 사용 시각 오래된 순, 쿨다운이나 비활성 상태는 맨 뒤로 밀립니다.

다만 매 요청마다 돌리지는 않습니다. provider 캐시를 따뜻하게 유지하려고 자동으로 고른 프로필을 세션에 핀으로 박아 둬요. 이 자동 핀이 풀리는 경우는 세 가지입니다. 세션을 리셋할 때(/new, /reset), 컴팩션이 한 번 끝났을 때, 그리고 그 프로필이 쿨다운이나 비활성일 때.

/model …@<profileId> -s로 직접 고르면 사용자 핀이 됩니다. 이건 /new, /reset, 세션 롤오버, 컴팩션, 쿨다운 창을 다 넘기고 살아남아요. 그 프로필이 쿨다운인 동안에는 같은 provider의 다른 프로필로 잠깐 돌아가지만, 저장된 핀 자체는 안 지웁니다.

쿨다운은 30초, 1분, 5분

인증 오류나 rate limit으로 프로필이 실패하면 쿨다운이 걸리고 다음 프로필로 넘어갑니다. 대기 시간은 최근 실패 횟수에 따라 늘어나요.

  • 1차 실패: 30초
  • 2차 실패: 1분
  • 3차 이상: 5분 (여기가 상한)

rate limit으로 분류되는 범위가 생각보다 넓습니다. HTTP 429만이 아니라 Too many concurrent requests, ThrottlingException, concurrency limit reached, throttled, resource exhausted 같은 문구도 들어가고, weekly limit reached나 monthly limit exhausted처럼 주기적인 사용량 한도도 여기 들어갑니다. rate limit처럼 보이는 타임아웃도 같은 취급이에요.

반대로 형식 오류나 잘못된 요청은 보통 여기서 끝납니다. 같은 페이로드를 다시 보내 봐야 똑같이 실패할 테니 프로필을 돌리지 않고 그냥 보여 줍니다.

쿨다운이 모델 단위로 잡히는 경우도 있어요. 실패한 모델 id를 알 수 있으면 cooldownModel을 기록하는데, 그러면 같은 provider의 형제 모델은 여전히 시도할 수 있습니다. 다만 결제나 크레딧 문제로 인한 비활성 창은 모델과 상관없이 프로필 전체를 막습니다. 이건 처음에 10분이고, 또 걸리면 새 10분이 시작돼요.

그 다음이 모델 폴백입니다

한 provider의 프로필을 다 쓰고도 안 되면 fallbacks의 다음 모델로 넘어갑니다. 후보 줄을 만드는 규칙이 몇 가지 있어요.

  • 요청한 모델이 항상 첫 번째입니다.
  • 설정에 적힌 폴백은 중복만 제거하고 허용 목록으로 거르지 않습니다. 운영자가 일부러 적은 거라고 보는 거예요.
  • 폴백 override를 따로 안 주면, 요청한 모델이 다른 provider라도 설정된 폴백들을 configured primary보다 먼저 시도합니다.
  • 그리고 맨 뒤에 configured primary가 한 번 더 붙습니다. 앞 후보들이 다 떨어지면 원래 기본값으로 돌아와 보는 거죠.
  • 호출하는 쪽이 fallbacksOverride를 주면 요청 모델 + 그 목록만 씁니다. 빈 목록을 주면 폴백이 꺼지고, 숨은 재시도 대상으로 primary가 붙는 것도 막힙니다.

폴백으로 넘어가는 오류와 넘어가지 않는 오류

모든 실패가 다음 모델로 넘어가지는 않습니다. 문서가 양쪽을 다 적어 뒀어요.

넘어가는 쪽: 인증 실패, rate limit과 쿨다운 소진, provider 과부하, 타임아웃 모양의 실패, 결제로 인한 비활성, model_not_found(조건에 맞는 HTTP 404 포함), 그리고 아직 후보가 남아 있을 때의 분류되지 않은 오류.

안 넘어가는 쪽: 타임아웃이 아닌 명시적 중단, 컨텍스트 초과 오류(request_too_large, input too long for the model, ollama error: context length exceeded 등), 후보가 하나도 안 남은 상태의 unknown 오류, provider의 최종 거부.

컨텍스트 초과가 폴백 대상이 아닌 게 저는 납득이 갑니다. 그건 모델을 바꿔서 풀 문제가 아니라 컴팩션과 프루닝이 맡는 일이거든요. 여기서 모델을 갈아 끼우면 원인은 그대로 둔 채 증상만 옮기는 게 됩니다. 이건 제 해석이고 문서가 이유까지 적어 둔 건 아닙니다.

provider가 최종 거부를 하면 그 턴은 거기서 끝납니다. 자동 복구 턴도, 컴팩션 재시도도, 엉뚱한 모델로 갈아타는 것도 없어요. 다음 사용자 메시지는 새 턴으로 다시 시작합니다.

폴백은 그 턴에서만 삽니다

이 성질을 모르면 오해하기 쉽습니다. 폴백으로 답이 나왔다고 해서 세션의 선택 모델이 그 모델로 바뀌지는 않아요. 문서 표현으로는 turn-local입니다. 다음 턴은 다시 원래 고른 모델에서 시작합니다.

대신 알림을 띄울 수 있게 폴백 상태만 저장해 둡니다. 그래서 /status에서 "고른 모델"과 "실제로 답한 모델"을 구분해서 볼 수 있어요.

자동으로 잡힌 폴백 오버라이드는 원래 자리를 주기적으로 다시 찔러보고, 회복되면 스스로 풀립니다. /new, /reset, sessions.reset은 그걸 즉시 지워요.

그리고 폴백 실행은 모델 선택 필드를 쓰지 않습니다. 재시도 도중에 내가 새로 고른 모델을 폴백이 덮어쓸 수 없다는 뜻이에요.

채팅에 뜨는 알림 두 줄

맨 앞에서 본 그 줄입니다.

↪️ Model Fallback: <fallback> (selected <primary>; <reason>)
↪️ Model Fallback cleared: <primary> (was <fallback>)

이건 어시스턴트가 한 말이 아니라 운영 메시지예요. 상태가 바뀔 때 한 번만 갑니다. 같은 조합으로 턴이 반복돼도 또 보내지 않아요.

단서가 하나 있습니다. 그룹과 채널 대화에서는 이 알림이 안 뜹니다. 폴백 상태와 내부 이벤트는 똑같이 돌아가는데 화면에 안 보이는 거예요. 그러니 단체방에서 "알림이 없었으니 폴백도 없었겠지"라고 읽으시면 안 됩니다. /status로 확인하셔야 해요. 여기서 단체방 설정을 한 번 정리해 두면 덜 헷갈립니다.

폴백이 답을 만들어 냈을 때 Control UI는 성공한 답 하나만 보여 주고, 같은 실행에서 비어 있던 실패 자리는 치웁니다. 원본 기록에는 실패한 시도가 그대로 남아 있어서 나중에 따져 볼 수 있어요.

정리

세 가지만 기억하시면 충분합니다.

  • 실패 순서는 같은 모델 재시도 → 같은 provider의 다른 계정 → 다음 모델입니다. 모델이 제일 나중이에요.
  • /model로 직접 고른 모델은 엄격합니다. 폴백이 안 도는 게 정상이에요.
  • 폴백은 그 턴에서만 유효하고, 그룹·채널에서는 알림이 안 보입니다.

같은 시리즈의 다른 글을 같이 보시면 전체 구조가 더 잘 보입니다. OpenClaw가 무엇인지부터, 도구 정책과 권한, 서브에이전트, 큐와 스티어링 순서로 읽으시면 됩니다.

출처는 OpenClaw 공식 문서 concepts/models.md, concepts/model-providers.md, concepts/model-failover.md입니다. 설정 키 이름과 기본값은 버전에 따라 달라질 수 있으니 실제 적용 전에는 docs.openclaw.ai의 해당 페이지를 확인하세요.