이력서와 공고의 적합도를 판정하는 개인 프로젝트를 만들면서 OpenAI 크레딧 없이 실제 모델 경로를 검증하려고 OpenRouter의 무료 모델을 썼다. 나흘 동안 사용 확인 한 번과 평가 실행 네 번, 호출 백여 회다. 이 글은 그 과정에서 사용하며 확인한 점을 정리한 목록이다. 무료 모델의 제약인 것과, 무료와 무관하게 내 코드와 도구의 문제였던 것을 나눠 적는다. 제품 이야기는 시리즈의 다른 글에 있다.
1. 구조화 출력은 모델과 엔드포인트를 함께 봐야 한다
OpenRouter는 모델 하나를 여러 상위 제공자 엔드포인트로 라우팅한다. 우리 앱은 모델에게 엄격한 JSON 스키마를 요구하는데, 구조화 출력 지원은 모델과 제공자 엔드포인트를 함께 확인해야 했다. 요청에 provider.require_parameters를 켜서 요청의 파라미터를 전부 지원하는 엔드포인트로만 가게 하고, 응답은 우리 쪽에서 다시 검증해 안 맞으면 닫았다. 공개 모델 메타데이터로 구조화 출력을 지원한다고 표시된 무료 모델 여섯 중 넷을 같은 입력으로 돌린 첫 사용 확인의 결과는 이랬다. 하나는 125초 뒤 상위 엔드포인트가 오류로 종료했고(추론 토큰 116개, 본문 없음), 하나는 5분이 지나도 응답이 없었고, 하나는 응답은 왔지만 출력 상한 8천 토큰에 추출과 판정이 잘렸고, 하나만 세 공고 중 둘을 판정했다. 서로 다른 네 모델을 비교한 것이라 모델의 차이인지 엔드포인트의 차이인지는 가르지 못한다. 이번에 시도한 네 모델·엔드포인트 조합의 결과가 달랐다는 것까지다. “무료 모델을 쓸 수 있다”와 “무료 모델로 이 앱을 쓸 수 있다”는 다른 문장이었다.
2. 응답이 오지 않아도 토큰은 소비된다
5분 무응답이었던 모델은 우리가 포기한 뒤에도 상위에서 계속 생성해, 대시보드에는 출력 31,045토큰으로 완료된 기록이 남았다. 다른 공고 하나는 요청 시작 약 515초 뒤에 잘린 응답이 도착했고 출력 토큰은 그 엔드포인트의 상한인 65,536이었다. 무료라 비용은 0이었지만, 이번 비스트리밍 실행에서는 로컬 대기가 끝난 뒤에도 생성 완료 기록이 남았다. 다른 제공자나 스트리밍 호출의 취소 동작까지 말하는 것은 아니다.
3. 추론 토큰이 출력의 대부분이다
판정을 낸 모델도 출력 토큰의 대부분을 추론에 썼다. 13건 평가 실행에서 사용량이 보고된 23회의 출력 토큰 54,731 중 49,040이 추론이었고, 판정 호출 8회의 지연 중앙값은 약 54초였으며 그중 한 건은 300초 제한으로 중단됐다. OpenRouter의 reasoning.effort를 옵션으로 붙였지만 이것은 강도 힌트이지 토큰 상한이 아니고, 무료 엔드포인트에서 실제로 추론 토큰을 줄이는지는 확인하지 못했다. 시간 예산에는 최종 답변 길이뿐 아니라 추론 토큰과 실제 호출 지연도 고려해야 했다.
4. SDK의 timeout은 응답 헤더까지였다 (무료의 제약이 아니라 내 코드의 문제)
OpenAI Node SDK 7.4.0으로 OpenRouter를 부르면서 timeout을 걸어 두었는데, 이번에 사용한 비스트리밍 경로에서는 이 값만으로 느린 본문 수신을 제한하지 못했다. 헤더가 먼저 오고 본문이 천천히 흐르면 timeout 뒤에도 호출은 살아 있다. 루프백 서버로 재현하니 timeout 100밀리초에 본문은 400밀리초 뒤 정상 도착했다. 본문까지 끊으려면 호출마다 AbortSignal을 넘겨야 했고, 이 교훈은 러너를 새로 만들 때마다 한 번씩 다시 놓쳤다. 실제 제공자 상대로 300초 deadline이 판정 호출을 끊고 다음 사례로 넘어간 것은 첫 라이브 실행에서 봤다.
5. 제공자를 골랐다고 무료가 되지는 않는다
무료는 OpenRouter라는 제공자의 속성이 아니라 모델 id의 :free 접미사가 붙은 변형의 속성이다. 환경 설정에 다른 모델이 들어 있으면 같은 코드가 과금 경로로 나간다. 리뷰에서 이걸 지적받고 러너가 :free로 끝나는 id만 받도록 고쳤다. openrouter/free라는 id도 있는데, 무료 모델 가운데 하나를 무작위로 고르는 라우터다. 응답에 실제 모델 이름이 오니 기록은 할 수 있지만 호출 전에 모델을 고정할 수 없어, 같은 모델 조건의 평가를 위해 이번 러너에서는 제외했다. 응답의 모델과 상위 엔드포인트 이름은 호출마다 기록한다.
6. 하루 50회, 키가 아니라 계정에 걸린다
9월 15일 오전, 골든셋 33건을 처음 끝까지 돌린 실행에서 52번째 호출부터 남은 사례가 전부 실패했다. 같은 날 오후에 보낸 확인 호출은 HTTP 429였고, 새 키를 만들어 보낸 첫 요청도 429였다. 한도 문서의 표는 변수라 비어 있었지만 페이지에 박힌 상수에 값이 있었다. 무료 변형은 분당 20회, 크레딧 구매 누계가 10달러 미만인 계정은 하루 50회, 이상이면 1,000회. 문서는 429가 상위 제공자의 제한에서도 올 수 있다고 하고, 우리 어댑터는 오류 본문을 감추기 때문에 이번 429가 일일 한도라는 것을 본문으로 확정하지는 못했다. 다만 프로필 추출 첫 호출부터 연속으로 실패하기 시작한 지점이 52번째 호출이었고, 새 키의 첫 요청부터 막혔고, 분당 한도에 닿을 속도는 아니었다는 세 관찰이 하루 50회·계정 단위 한도와 맞는다. 성공 응답에는 X-RateLimit 헤더가 없어 남은 양을 미리 알 수 없고, 이번에 확인한 429 요청은 활동 로그에서 찾지 못해 대시보드만 보면 아무 일도 없었던 것처럼 보였다. 하루 50회 제한이 적용된다면, 99회 호출 계획은 재시도 없이도 최소 이틀로 나눠야 한다.
7. 키는 실행 중에 끊길 수 있다 (재개는 내 코드의 몫)
단기 만료로 만든 키 하나가 실행 도중 더 이상 받아들여지지 않았다. 남은 사례 다섯이 프로필 추출 첫 호출에서 75~402밀리초 만에 인증 오류로 끝났고, 실행 뒤 키 정보 조회도 401이었다. 만료 시각 전이었고 원인은 확정하지 못했다. 러너 쪽에서는 실패를 실패로 분류하고 다음 사례로 넘어간 것이 전부지만, 그 뒤 잘린 실행을 이어 붙이는 재개 기능이 필요해졌다. 그 이야기는 따로 썼다. 키의 상태는 모델을 부르지 않고도 GET /api/v1/key로 확인할 수 있어서, 실행 전에 만료와 tier를 이걸로 본다.
8. 무료는 비공개가 아니고, 대시보드는 대조할 수 있다
이력서와 공고 텍스트는 OpenRouter를 거쳐 상위 엔드포인트로 간다. 우리 보고서의 store: "n/a"는 보관하지 않는다는 뜻이 아니라 우리가 통제하지 못한다는 뜻이고, 실제 취급은 OpenRouter의 데이터 정책과 계정 설정, 상위 제공자의 정책을 따른다. 그래서 합성 텍스트만 보냈다. 대신 대시보드는 대조가 됐다. 호출별 입력·출력 토큰이 우리 텔레메트리와 건마다 일치했고 비용은 전부 0이었다. 앱 이름 열이 Unknown으로 나온 것은 HTTP-Referer 헤더를 안 보냈기 때문이다.
정리
비용을 줄이려는 선택이 검증 범위와 실행 계획까지 바꿨다. 무료로 얻은 숫자는 그 엔드포인트의 숫자이고, 하루 50회라는 상한은 골든셋 크기와 실행 계획을 정하는 조건이 됐다. 제공자의 제한과 앱이 책임져야 할 실패 처리를 구분해야 실행 계획을 세울 수 있었다.