「maximum context length」 토큰 수가 안 맞을 때
tiktoken 으로 센 메시지는 7만 토큰인데 API 는 24만 토큰이라며 거절합니다. system·tools·스키마·이력까지 더한 예산을 호출 없이 계산하는 스크립트입니다.
This model's maximum context length is 128000 tokens. However, your messages resulted in 249114 tokens — OpenAI 포럼에 올라온 실제 사례에서 개발자가 tiktoken 으로 센 값은 73,878 토큰이었습니다(포럼 글). 차이는 대개 센 대상이 다르기 때문입니다.
[1] API 가 세는 범위부터 확인합니다. 포럼 답변은 system 메시지, tool·function 정의, 응답 형식 스키마, 대화 이력을 모두 합친 값이라고 설명합니다. 공식 함수 호출 가이드에도 같은 취지의 문장이 있습니다.
그래서 사용자 메시지 한 개만 tiktoken 으로 세면 항상 적게 나옵니다. 이미지를 base64 문자열로 본문에 붙였다면 그것도 일반 글자로 세어집니다.
[2] 모델에 맞는 인코딩을 고릅니다. OpenAI 쿡북의 표 기준으로 gpt-4o·gpt-4o-mini 는 o200k_base, gpt-4-turbo·gpt-4·gpt-3.5-turbo 는 cl100k_base 입니다. 직접 외우지 말고 tiktoken 에 물어봅니다.
$ python3 -m venv .venv && .venv/bin/pip install -q tiktoken$ .venv/bin/python -I -c "import tiktoken; print(tiktoken.encoding_for_model('gpt-4o').name)"o200k_base[3] 실제 요청과 같은 모양의 JSON 을 파일로 둡니다. 호출하지 않고 계산만 하므로 키는 필요 없습니다. 아래는 줄인 예시이고, 운영 요청에서 messages·tools·response_format 을 덤프해 쓰되, 이 스크립트는 값이 문자열인 메시지만 셉니다. 여러 부분으로 된 content 나 tool_calls 가 있으면 그 부분을 문자열로 바꿔 넣어야 오류가 나지 않습니다.
{ "model": "gpt-4o", "max_tokens": 4000, "messages": [ {"role": "system", "content": "You are a support assistant for Example Corp. Answer in the customer's language. Cite the order ID in every reply."}, {"role": "user", "content": "Where is my order 10234? It was supposed to arrive on Monday."}, {"role": "assistant", "content": "Let me check the shipping status for order 10234."}, {"role": "user", "content": "Please also tell me if I can change the delivery address."} ], "tools": [ {"type": "function", "function": { "name": "get_order_status", "description": "Look up the shipping status of an order by its numeric ID.", "parameters": {"type": "object", "properties": {"order_id": {"type": "integer", "description": "Numeric order ID"}}, "required": ["order_id"]}}} ], "response_format": {"type": "json_schema", "json_schema": {"name": "reply", "strict": true, "schema": {"type": "object", "properties": {"answer": {"type": "string"}, "order_id": {"type": "integer"}}, "required": ["answer", "order_id"], "additionalProperties": false}}}}[4] 계산 스크립트를 만듭니다. 메시지마다 3토큰, 답변 시작에 3토큰을 더하는 값은 쿡북의 num_tokens_from_messages 를 따랐습니다. tools 와 스키마는 서버가 내부에서 어떤 문법으로 바꾸는지 공개되어 있지 않아서, JSON 문자열을 센 근사값입니다.
#!/usr/bin/env python3"""usage: count_budget.py request.json CONTEXT_WINDOW — estimate input + reserved output without calling the API"""import json, sysimport tiktoken req = json.load(open(sys.argv[1], encoding="utf-8"))window = int(sys.argv[2])enc = tiktoken.encoding_for_model(req["model"]) def count_messages(messages): # per-message overhead from the OpenAI cookbook (gpt-4o-2024-08-06 / gpt-4-0613 values) n = 0 for m in messages: n += 3 for k, v in m.items(): n += len(enc.encode(v)) if k == "name": n += 1 return n + 3 # reply priming def count_json(obj): # rough: the server's own serialisation of tools/schema is not published return len(enc.encode(json.dumps(obj, separators=(",", ":")))) body = sum(len(enc.encode(m["content"])) for m in req["messages"])msgs = count_messages(req["messages"])tools = count_json(req["tools"]) if "tools" in req else 0fmt = count_json(req["response_format"]) if "response_format" in req else 0reserve = req.get("max_tokens", 0)inp = msgs + tools + fmt print(f"encoding {enc.name}")print(f"message text {body}")print(f"messages (+fmt) {msgs}")print(f"tools (approx) {tools}")print(f"schema (approx) {fmt}")print(f"input estimate {inp}")print(f"max_tokens {reserve}")print(f"input + reserve {inp + reserve} / window {window}")print("input alone:", "OVER" if inp > window else "fits")print("with reserve:", "OVER" if inp + reserve > window else "fits", f"(headroom {window - inp - reserve})")[5] 모델의 컨텍스트 창 크기를 두 번째 인자로 넣어 실행합니다. 창 크기는 모델별 문서에서 확인한 값을 씁니다. 아래는 gpt-4o 의 128,000 입니다.
$ .venv/bin/python -I count_budget.py request.json 128000encoding o200k_basemessage text 63messages (+fmt) 82tools (approx) 58schema (approx) 54input estimate 194max_tokens 4000input + reserve 4194 / window 128000input alone: fitswith reserve: fits (headroom 123806)message text 63 과 input estimate 194 를 비교합니다. 이 작은 요청에서도 본문만 센 값의 3배 가까이 됩니다.
[6] max_tokens 를 크게 잡았다면 예약분을 더한 값도 봅니다. 같은 요청을 창이 4,096 인 모델이라고 가정해 돌린 결과입니다.
$ .venv/bin/python -I count_budget.py request.json 4096 | tail -n 2input alone: fitswith reserve: OVER (headroom -98)입력만 보면 들어가지만 출력 한도까지 합치면 넘칩니다. 입력과 출력이 같은 창을 나눠 쓴다는 설명은 OpenAI 포럼 답변에서 확인했고 공식 문서에서는 문구를 찾지 못했습니다. 그러니 이 줄은 여유를 보는 용도로 쓰고, 넘으면 max_tokens 를 줄이거나 이력을 자릅니다.
[7] 스크립트 값과 API 값이 어긋나면 API 값이 맞습니다. 쿡북이 직접 이렇게 적고 있습니다: "Consider the counts from the function below an estimate, not a timeless guarantee." 오류 문구의 숫자와 input estimate 의 차이가 크면 tools·schema·이력 중 센 적 없는 항목이 있는지부터 봅니다.