
오픈라우터에서 Five controls to set before production traffic 이라는 제목의 메일이 왔다. 프로덕션 트래픽을 전제로 한 내용인데, 읽어보니 개인용으로도 그대로 적용할 만한 것들이었다.
메일의 문제의식은 이 문장으로 요약된다.
Default routing is built to keep requests succeeding, which is the right default and the wrong one for a workload with opinions about cost, providers, or data handling. These five are where you state the opinion.
기본값은 “성공”에 최적화되어 있고, 비용·제공자·데이터 처리에 의견이 있는 워크로드에는 그게 틀린 기본값이라는 것. 그래서 아래 다섯 항목마다 메일 원문을 먼저 인용하고, 내가 그걸 어떻게 해석해서 무엇을 했는지를 적었다. 원문을 같이 두는 이유는, 나중에 내 판단이 틀렸을 때 무엇을 근거로 그렇게 했는지 되짚기 위해서다.
1. 자동 충전은 15달러 / 15달러
Auto top-up replenishes the balance when it drops below a threshold you set. Beyond avoiding a hard stop, it avoids a latency penalty: OpenRouter runs extra billing checks and expires caches faster once a balance reaches single-digit dollars. The documented working floor is 10 to 20 dollars.
왜 이렇게 했나
메일에서 눈에 들어온 건 하드 스톱 얘기가 아니라 잔액이 한 자릿수로 떨어지면 과금 검사가 추가되고 캐시가 더 빨리 만료된다는 문장이었다. 캐싱 정책이 잔액에 따라 달라진다는 발상이 재미있어서 ㅎㅎ;; (다만 이건 메일 내용이고, 공식 문서에서 같은 표현을 찾지는 못했다. 그대로 인용해둔다.)
그래서 자동 충전을 임계값 15달러, 충전액 15달러로 걸었다. 이러면 잔액이 15–30달러 사이를 오간다.
왜 10달러가 아니라 15달러인가
충전에는 수수료가 붙는다. 5.5%, 최소 $0.80이다.
| 충전액 | 수수료 | 실효 요율 |
|---|---|---|
| $10 | $0.80 (최소액) | 8.0% |
| $15 | $0.825 | 5.5% |
| $30 | $1.65 | 5.5% |
최소 수수료가 실효 요율을 눌러앉히는 손익분기점은 $14.55(= 0.80 ÷ 0.055)다. 그 아래로 내려가면 매번 손해다. 그래서 15달러. 30달러도 요율은 같지만, 키에 하루 $10 상한을 걸어둔 상태라 잔액을 그 이상 쌓아둘 이유가 없다. 15/15면 하한만 봐도 하루 반나절치는 남는다.
과청구 방지
혹시 모르니 결제 카드는 트래블월렛 체크카드로 넣었다. 자동 충전은 잔액이 임계값 밑으로 떨어질 때마다 별도 결제가 발생하니, 신용카드 대신 잔액 한도가 명확한 체크카드가 안전하다.
2. 키 상한은 하루 10달러
A per-key credit limit is the blast-radius control. One key per workload means a runaway job exhausts its own budget and returns 402 while everything else keeps serving. GET /api/v1/key exposes limit_remaining and limit_reset so a job can watch its own headroom.
왜 이렇게 했나
이건 이미 걸려 있었다. daily $10.
메일이 “blast-radius control”이라고 부른 게 정확하다. 이건 비용 절감 장치가 아니라 피해 범위를 자르는 장치다. 에이전트가 루프에 빠지거나 잘못된 배치 작업이 돌면, 그 키는 하루 $10에서 402를 뱉고 멈춘다. 잔액 전체를 태우는 것과 하루 $10을 태우는 것의 차이다.
한 가지 아쉬운 점은, 키를 하나만 쓰면 이 $10이 공유 예산이 된다는 것이다. 백그라운드 작업이 한도를 다 쓰면 대화형 요청도 같이 죽는다. 메일은 “워크로드마다 키 하나”를 권하지만, 혼자 쓰는 환경에서는 거기까지 갈 이유가 없어서 그대로 뒀다.
3. 엔드포인트 자격 — 일부만
The
providerobject decides which endpoints can serve a request.orderlists providers to try in sequence,onlyandignorenarrow the pool, andrequire_parameterskeeps the request away from any provider that would quietly ignore the parameters you sent.
왜 이렇게 했나
여기서 유용한 건 두 가지였다.
require_parameters: true
이건 속도나 가격이 아니라 정확성 문제다. 메일 표현대로 “파라미터를 조용히 무시하는 제공자”를 걸러낸다. 어떤 제공자는 response_format이나 tool_choice를 지원하지 않는데도 요청을 그대로 받아들이고 해당 파라미터를 버린다. 결과는 에러가 아니라 “그럴듯하게 생겼는데 형태가 틀린” 응답이다. 구조화 출력이나 툴 호출을 쓴다면 선택 사항이 아니다.
data_collection: "deny"
학습에 데이터를 쓰는 제공자를 후보에서 제외한다. 프라이버시 쪽에서 실질적으로 체감되는 유일한 스위치라고 생각한다.
order는 강제가 아니다 — 이건 메일에 없던 얘기인데, 문서를 읽다가 알았다. order는 자격 있는 후보 사이의 선호 순위일 뿐이라, “A 아니면 안 됨”을 원하면 allow_fallbacks: false가 함께 필요하다. 그게 없으면 다음 제공자로 조용히 넘어간다.
4. 폴백은 생략
A
modelsarray is a priority list of models; any error moves the request to the next one, including context-length validation, moderation refusals, rate limiting, and downtime. Billing follows the model that actually ran.
왜 이렇게 했나
이건 서비스 가용성을 위한 장치다. 첫 선택이 실패하면 다음 모델로 넘겨서 사용자 요청이 실패하지 않게 만든다. 개인이 쓰는 환경에서는 실패하면 다시 하면 되므로 넣지 않았다.
대신 이 문장에서 챙긴 건 “Billing follows the model that actually ran” 쪽이다. 폴백을 쓰면 실제로 실행된 모델로 과금되므로, 비싼 모델이 섞여 있으면 조용히 비용이 올라간다. 폴백을 안 쓰는 이유이기도 하다.
5. ZDR은 켜지 않았다
Sometimes failing is correct.
allow_fallbacks: falsemakes a request fail rather than route somewhere you did not choose.data_collection: "deny"andzdr: trueare the data-handling versions of the same decision, restricting routing to compliant and zero-retention endpoints; a 503 then means no provider met your requirements, which is a policy working rather than an outage.
왜 이렇게 했나
이게 제일 고민되는 부분이었다.
ZDR(Zero Data Retention)을 켜면 프롬프트와 응답을 저장하지 않는 엔드포인트만 쓸 수 있게 된다. 문제는 후보가 줄어든다는 것이다. 같은 모델이라도 제공자마다 가격이 다르고, 그 편차가 작지 않다.
내가 쓰는 DeepSeek 모델 기준으로 실제 엔드포인트 가격을 뽑아봤다. 입력 $/M 기준이다.
| 제공자 | 입력 | 출력 |
|---|---|---|
| Morph | 0.022 | 0.400 |
| Particle AI | 0.030 | 0.250 |
| Wafer | 0.038 | 0.500 |
| DeepSeek (본가) | 0.150 | 0.600 |
| Together 등 | 0.300 | 1.200 |
| BaseTen | 0.600 | 2.400 |
최저와 최고가 입력 기준 27배 차이다. ZDR을 강제하면 저가 구간이 빠지고 남은 곳으로 라우팅되면서, 저렴한 모델을 쓰는 이유가 상당 부분 사라진다. 실제로 ZDR 준수 여부는 API로 확인할 수 없어서, 어느 제공자가 남는지는 켜보기 전엔 모른다.
그래서 ZDR은 켜지 않았고, data_collection: "deny"만 유지했다. 학습 이용은 막고, 보관 정책은 감수하는 쪽.
참고로 ZDR은 계정 전역이 아니라 요청 단위로도 켤 수 있다. 평소엔 끄고, 민감한 문서를 다루는 요청에만 zdr: true를 붙이는 게 합리적이다.
덧붙여 — 시간대 할증
DeepSeek 본가 엔드포인트에는 시간대별 요율이 있다. UTC 01:40–06:40, 10:00–16:40에 요율이 2배가 된다. 한국 시간으로 10:40–15:40, 19:00–01:40이다. 본가를 쓴다면 이 시간대를 피하는 것만으로 체감 비용이 달라진다.
6. 추가한 것 — 내 사용 패턴으로 가격을 다시 계산하는 자동화
여기까지 하고 나서 이상한 점이 하나 남았다. sort: "price"를 켜면 오픈라우터가 “가장 싼” 제공자를 골라주는데, 그 “싼”이 내 사용 패턴 기준인지 확신이 없었다.
가격표는 한 줄이지만, 비용은 여러 변수로 결정된다
내 세션 로그를 집계해보니 패턴이 이랬다.
프롬프트 토큰 중 캐시 읽기 94.0%
출력 토큰 비중 0.12%
프롬프트의 94%가 캐시에서 읽힌다. 즉 입력 단가보다 캐시 단가가 비용을 지배한다. 그런데 sort: "price"는 그런 걸 고려하지 않는 고정 정렬이다. 실제로 그 정렬이 고른 제공자는, 내 믹스로 재계산하면 최적이 아니었다.
| 제공자 | 입력 $/M | 캐시 $/M | 내 믹스 기준 |
|---|---|---|---|
| Morph | 0.022 | 0.0040 | 기준 |
| Particle AI | 0.030 | 0.0030 | 30% 저렴 |
| DeepInfra | 0.078 | 0.0024 | 거의 동일, uptime 100% |
입력가만 보면 Morph가 압도적이지만, 캐시가 94%를 차지하면 캐시 단가가 낮은 쪽이 따라붙는다. 입력가 3.5배 차이가 거의 상쇄되는 것이다.
판단 기준을 순서로 정리하면
가격부터 보면 안 된다. 영향력 순서로 봐야 한다.
| 순위 | 요소 | 이유 |
|---|---|---|
| 1 | 캐시 동작 | 프롬프트의 92%가 캐시. 안 되면 비용 5배 |
| 2 | 제공자 신뢰성 | 죽으면 요청 실패 |
| 3 | 데이터 정책 | 학습 차단은 필수, 보관은 선택 |
| 4 | 품질 프록시 | 양자화(fp8/fp4), 툴 지원 |
| 5 | 가격 | 캐시가 지배하면 입력가 차이는 상쇄 |
그래서 세 개를 자동화했다
① 30분 — 잔액·키 한도
/credits와 /key를 직접 호출해 잔액, 키 잔여, 키 만료를 본다. 402로 요청이 죽기 전에 알아야 하므로 주기가 짧다.
② 매일 09:10 — 상태·품질
가격보다 먼저 봐야 하는 건 캐시가 실제로 동작하는가다. 단가표의 캐시 가격은 그 제공자가 캐시를 할 때만 의미가 있다. 같은 프리픽스를 두 번 보내 cached_tokens를 확인하고, 함께 status·uptime·응답 지연을 잰다.
여기에 품질 프로브를 하나 더 붙였다. response_format: json_object로 정해진 키·타입의 JSON을 요구하고, 파싱이 되는지 검사한다. 양자화가 거친 제공자는 형식이 깨지는 경우가 있어서, 이게 값싼 품질 신호가 된다.
DeepInfra 응답 0.83s · 캐시 적중 · 품질 통과 · 1일 uptime 100%
그리고 풀 단위 경보를 넣었다. only로 3곳만 허용한 상태에서 2곳이 비정상이면 폴백이 1곳만 남는다. 그 순간 다음 실패가 곧 503이므로, 개별 장애와 별개로 알린다.
임계값은 처음에 99%로 잡았다가 95%로 내렸다. 99%는 하루 14분만 죽어도 울린다는 뜻이고, 개인 사용에는 과하다. 대신 30분 창은 80% 로 따로 뒀다 — 급성 장애는 빨리 잡아야 하니까. 역할을 나눈 셈이다. 참고로 uptime_last_1d는 트래픽이 적으면 노이즈가 크다. 요청 20건 중 1건 실패가 95%이므로, 이 지표는 “심하게 나쁜 곳을 거르는 용도”로만 쓰고 정밀 판단은 실측 프로브에 맡긴다.
반대로 어떤 제공자는 단가표에 캐시 가격이 싸게 적혀 있어도 두 번 연속 cached=0이 나왔다. 그런 곳은 표값이 무의미하다.
③ 매일 09:20 — 비용 재계산과 캐시 히트율
내 토큰 믹스를 다시 재고, 엔드포인트 가격표에 곱해서 제공자별 1M당 비용을 계산한다. 허용 풀(only) 안에서만 비교한다 — 쓸 수 없는 제공자가 “더 싸다”고 알리는 건 소음이다.
캐시 히트율도 여기서 추적한다. 세션 로그에서 cacheRead / (input + cacheRead)를 계산해서, 85% 미만이거나 직전보다 10%p 이상 떨어지면 알린다.
주기는 주 1회에서 매일로 줄였다. 가격이나 믹스는 하루면 충분히 변할 수 있는데, 주 1회면 탐지가 최대 7일 늦어진다. 알림 빈도는 6일 중복 억제가 눌러주므로 그대로다.
시간대를 고정한 이유
DeepSeek 본가에 시간대 할증(10:40–15:40, 19:00–01:40)이 있다. 그 시간에 점검을 돌리면 본가 가격이 2배로 보여서 “다른 곳이 싸다”는 거짓 경보가 난다. 그래서 09:20 — 정상 요율 구간에 고정했다.
④ 매일 09:30 — 변경 효과 검증
설정을 바꾼 뒤 그게 실제로 도움이 됐는지 확인하는 장치도 붙였다. 라우팅 설정의 해시를 감시하다가, 바뀌면 직전 7일 기준선($/1M 토큰)을 저장한다. 3일 뒤부터 변경 후 실제 비용과 비교해서 15% 이상 움직이면 개선인지 악화인지 알린다.
이게 없으면 “바꿨으니 나아졌겠지”로 끝난다. 실제로 이 글을 쓰는 시점에 첫 측정을 해봤더니, 예상과 반대로 비용이 9% 올라 있었다.
변경 전 $0.0185/1M (1,336건 · 289M 토큰)
변경 후 $0.0202/1M (26건 · 9.9M 토큰)
이유는 캐시 비중이었다. 캐시가 92%일 때, 나머지 8%의 신규 입력가가 승부를 가른다. DeepInfra는 캐시 단가가 40% 싸지만 신규 입력가는 3.5배 비싸다. 내가 세운 지표가 틀렸던 것이고, 그래서 이런 검증 장치가 필요하다.
(표본 26건이라 확정은 아니다. 3일 뒤 자동 비교가 다시 판정한다.)
비용은 0이다
세 자동화 모두 모델을 호출하지 않는다. 스크립트가 API를 직접 때리고 판정해서, 조건에 걸릴 때만 메시지를 보낸다. 토큰 과금이 없으니 감시를 위해 감시 비용을 내는 모순이 생기지 않는다.
알림은 채팅과 폰 두 곳으로 간다. 정상이면 아무것도 오지 않는다 — 조용한 게 정상 상태다.
한 가지 한계
로컬 집계는 청구서와 완전히 일치하지 않는다. 30일 기준 로컬 합계가 실제 청구액의 83% 수준이었다. 유틸리티 호출이나 중단된 시도가 로컬에 덜 기록되기 때문이다. 그래서 이 자동화는 상대 비교에는 쓸 수 있지만, 절대 금액은 오픈라우터 쪽 숫자를 봐야 한다.
현재 설정 전체
말로만 정리하면 실제와 다를 수 있으니, 지금 돌아가는 설정을 그대로 적어둔다. 나중에 “이때 나는 이렇게 해뒀구나”를 확인할 수 있게.
모델
agents: {
defaults: {
model: "openrouter/deepseek/deepseek-v4.1-flash-1017",
utilityModel: "openrouter/deepseek/deepseek-v4.1-flash-1017",
},
},
제공자 라우팅
models: {
providers: {
openrouter: {
params: {
provider: {
order: ["PrimeIntellect", "Morph", "DeepInfra", "Cloudflare", "Together"],
only: ["PrimeIntellect", "Morph", "DeepInfra", "Cloudflare", "Together"],
allow_fallbacks: true,
require_parameters: true,
data_collection: "deny",
},
},
},
},
},
sort: "price"를 뺀 이유는 6번에 적었다. 대신 order로 검증된 제공자를 앞세우고, 전부 불가할 때만 폴백한다.
only를 추가한 이유는 따로 있다. order는 “시도할 순서”일 뿐이라 목록을 다 소진하면 자격을 만족하는 다른 제공자로 넘어간다. only를 걸어야 후보 풀이 그 목록으로 제한된다. 대신 넷 다 죽으면 503이다 — 모르는 제공자로 새는 것보다 낫다고 판단했다.
순서는 실측으로 정했다. 같은 조건에서 세 번씩 재보니 PrimeIntellect가 Morph보다 빨랐다.
| TTFT | 생성 속도 | 캐시 적중 | |
|---|---|---|---|
| PrimeIntellect | 0.7–0.9s | 164–200 tok/s | 3/3 |
| Morph | 1.1–3.2s | 44–64 tok/s | 2/3 |
가격도 PrimeIntellect가 40% 쌈다. 그래서 1순위를 바꿨고, Morph는 2순위 폴백으로 남겼다.
계정 설정
| 항목 | 값 |
|---|---|
| 자동 충전 | 임계값 $15 / 충전액 $15 |
| 결제 수단 | 체크카드 (한도 명확) |
| 키 한도 | daily $10 |
| ZDR | 사용 안 함 |
data_collection | deny |
자동화 3개 (모두 모델 호출 없음)
| 주기 | 하는 일 | 알림 조건 |
|---|---|---|
| 30분 | 잔액·키 잔여·키 만료 | 잔액 < $10, 잔여 < $2, 만료 14일 전 |
| 매일 09:10 | status·uptime·지연·캐시 적중·품질 프로브·풀 위험 | status 이상, uptime 1일 < 95%, 30분 < 80%, 지연 > 15s, 캐시 미적중, 품질 실패, 풀 2/3 비정상 |
| 매일 09:20 | 토큰 믹스 재계산, 허용 풀 내 비용 비교, 히트율 추적 | 대안 20% 이상 저렴, 히트율 < 85%, 10%p 하락 |
정상일 때는 셋 다 침묵한다. 알림은 채팅과 폰으로 간다.
정리 — 개인 사용자에게 필요한 건 세 개
Then give the configuration a home. A preset holds the model, provider preferences, system prompt, and parameters under a slug your code calls as
@preset/your-slug, with versions kept so a change is reviewable without a deploy.
메일은 마지막에 프리셋과 워크스페이스로 설정을 “집”에 넣으라고 한다. 모델·제공자 선호·시스템 프롬프트·파라미터를 슬러그로 묶어 @preset/...로 호출하고, 변경 이력을 남기라는 것.
개인 사용자에게 이건 과하다. 프리셋이 값어치를 하려면 호출하는 코드가 여러 개여야 하는데, 나는 설정 파일 한 곳에서 관리하면 끝이다. 워크스페이스도 마찬가지 — 스테이징과 프로덕션이 없으니 나눌 대상이 없다.
대신 메일의 다섯 항목 중 혼자 쓰는 환경에서 실제로 값어치가 있는 건 이 정도로 추렸다.
- 자동 충전을 걸어라. 하한은 $14.55 이상, 충전액은 하루 상한의 1.5배 정도.
- 키에 하루 상한을 걸어라. 이건 절약이 아니라 사고 대비다.
require_parameters와data_collection: deny는 켜라. 비용 없이 정확성과 프라이버시를 얻는다.
ZDR과 다중 프로바이더 폴백은 서비스 운영자용이다. 개인이라면 민감한 요청에만 선택적으로.
한 가지 더 — 잔액과 키 잔여 한도는 API로 조회할 수 있으니(/api/v1/key의 limit_remaining), 자동화로 감시해두면 402로 죽기 전에 알 수 있다. 나는 30분마다 확인해서 조건에 걸릴 때만 알림이 오게 해뒀다.