GLM-5 API를 호출했는데 429 에러만 돌아왔다. 크레딧 문제인 줄 알고 4일을 허비했다 — 원인은 단순히 엔드포인트 경로였다.
TL;DR
- 잘못된 경로:
/api/paas/v4/→ 429code:1113반환 - 올바른 경로:
/api/coding/paas/v4/→ 별도 리소스 풀로 정상 작동합니다 - max_tokens 필수: 설정하지 않으면 응답이 중간에 잘립니다
- JSON 파싱: 4단계 복구 로직이 필수입니다 (줄바꿈 escape를 하지 않는 모델 특성)
어떤 문제가 있었나요?
GLM-5 API를 연동하면서 4일간 겪은 문제를 타임라인으로 정리합니다.
- Day 1 — API 호출 시
429 code:1113 "余额不足"반환. “크레딧을 다 썼구나”라고 판단했지만, 실제로는 엔드포인트 오류였습니다. - Day 2 — 다른 LLM으로 폴백 로직을 추가했습니다. 응답은 오지만
JSON.parse()가 실패합니다. 원인은 literal newline이 JSON 안에 들어있는 것. - Day 4 오전 —
content.split('\n').join('\\n')으로 급하게 수정했습니다. 결과: 구조적 줄바꿈까지 망가져서 파싱이 더 안 됩니다. - Day 4 오후 — 문자열 내부만 escape하는
fixControl()을 구현하고 4단계 복구 파이프라인을 구축했습니다. JSON 파싱 성공. - Day 4 저녁 — 응답이 중간에 잘리는 현상 발견.
max_tokens: 4096추가 후 전체 응답 수신 성공. - Day 4 야간 —
/api/paas/v4→/api/coding/paas/v4로 경로 변경. 429 에러가 사라지고 정상 응답을 확인했습니다.
혼란스러웠던 이유는 에러 메시지가 “잔액 부족”이었기 때문입니다. 실제 원인은 리소스 풀이 소진된 엔드포인트를 사용한 것이었습니다.
해결 1: 올바른 엔드포인트 찾기
두 경로의 차이는 /coding/ 하나입니다.
❌ https://open.bigmodel.cn/api/paas/v4/chat/completions
→ 리소스 팩 소진 시 429 code:1113 "余额不足" 반환
✅ https://open.bigmodel.cn/api/coding/paas/v4/chat/completions
→ 별도 리소스 풀 사용, 정상 응답
/api/coding/paas/v4는 코딩용 엔드포인트지만, 분석·요약·창작 등 범용 프롬프트도 정상 처리됩니다. 공식 문서에서 이 구분이 명확히 표기되어 있지 않아 혼란이 발생했습니다.
TypeScript fetch 예시입니다.
const response = await fetch(
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "glm-5",
messages: [
{ role: "system", content: "JSON으로만 응답해." },
{ role: "user", content: prompt },
],
temperature: 0.3,
max_tokens: 4096,
}),
}
);
해결 2: JSON이 깨지는 문제
GLM-5는 thinking 모델이며 JSON 출력 시 다음 문제가 있습니다.
- 줄바꿈 escape 안 함 — 문자열 값 안에 literal newline을 넣습니다
- `json 래핑 — JSON을 마크다운 코드블록으로 감싸서 반환할 수 있습니다
- response_format 미지원 — 프롬프트로만 JSON을 강제해야 합니다
4단계 복구 로직
1단계: 직접 파싱
try {
return JSON.parse(content);
} catch {}
2단계: 코드블록 추출
const match = content.match(/```(?:json)?\s*([\s\S]*?)```/);
if (match) {
try { return JSON.parse(match[1].trim()); } catch {}
try { return JSON.parse(fixControl(match[1].trim())); } catch {}
}
3단계: 중괄호 추출 + 제어문자 수정
const first = content.indexOf("{");
const last = content.lastIndexOf("}");
if (first !== -1 && last > first) {
const extracted = content.slice(first, last + 1);
try { return JSON.parse(fixControl(extracted)); } catch {}
}
4단계: trailing comma 제거 + 재시도
const repaired = extracted.replace(/,\s*([}\]])/g, "$1");
return JSON.parse(fixControl(repaired));
절대 하면 안 되는 것:
// ❌ JSON 구조 자체가 망가짐! json.split("\n").join("\\n");이 방식은 문자열 외부의 구조적 줄바꿈까지 escape해버립니다.
{"key": "value"}→{\n "key": "value"\n}이\\n으로 변환되어 파싱 불가 상태가 됩니다.
fixControl 핵심 로직
문자열 내부의 제어문자만 escape하고, 문자열 외부의 구조적 줄바꿈은 유지합니다.
function fixControl(json: string): string {
let result = "", inStr = false, esc = false;
for (const ch of json) {
if (esc) { result += ch; esc = false; continue; }
if (ch === "\\" && inStr) { result += ch; esc = true; continue; }
if (ch === '"') { inStr = !inStr; result += ch; continue; }
if (inStr) {
if (ch === "\n") result += "\\n";
else if (ch === "\r") result += "\\r";
else if (ch === "\t") result += "\\t";
else result += ch;
} else {
result += ch;
}
}
return result;
}
따옴표 안팎을 inStr 플래그로 추적하여, 문자열 값 안의 \n, \r, \t만 escape합니다.
해결 3: max_tokens 설정
GLM-5는 thinking 모델이므로 max_tokens를 설정하지 않으면 기본값이 매우 작아 응답이 중간에 잘립니다.
max_tokens 미설정 → 응답이 반쯤 오다가 끊김
max_tokens: 4096 → 전체 응답 정상 수신
JSON 형식으로 긴 분석 결과를 받을 때 특히 치명적이므로 반드시 설정합니다.
파라미터 요약표
| 항목 | 값 | 비고 |
|---|---|---|
| 엔드포인트 | /api/coding/paas/v4/chat/completions | /api/paas/v4 사용 금지 (429 발생) |
| 모델 | glm-5 | thinking 모델 |
| temperature | 0.3 | 분석용. 창작은 0.7~0.8 |
| max_tokens | 4096 | 반드시 설정. 안 하면 응답 잘림 |
| 인증 | Authorization: Bearer {key} | OpenAI 호환 형식 |
| response_format | 미지원 | 프롬프트로 JSON 강제 |
전체 코드
📖 복사-붙여넣기용 완전 코드
```typescript const GLM_API_URL = "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions"; interface ChatMessage { role: "system" | "user" | "assistant"; content: string; } function fixControl(json: string): string { let result = ""; let inString = false; let escape = false; for (let i = 0; i < json.length; i++) { const ch = json[i]; if (escape) { result += ch; escape = false; continue; } if (ch === "\\") { result += ch; escape = true; continue; } if (ch === '"') { inString = !inString; result += ch; continue; } if (inString) { if (ch === "\n") result += "\\n"; else if (ch === "\r") result += "\\r"; else if (ch === "\t") result += "\\t"; else result += ch; } else { result += ch; } } return result; } function extractJSON(content: string): any { try { return JSON.parse(content); } catch {} const codeBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/); if (codeBlockMatch) { try { return JSON.parse(codeBlockMatch[1].trim()); } catch { try { return JSON.parse(fixControl(codeBlockMatch[1].trim())); } catch {} } } const braceStart = content.indexOf("{"); const braceEnd = content.lastIndexOf("}"); if (braceStart !== -1 && braceEnd !== -1) { const extracted = content.slice(braceStart, braceEnd + 1); try { return JSON.parse(fixControl(extracted)); } catch {} const cleaned = extracted.replace(/,\s*([}\]])/g, "$1"); try { return JSON.parse(fixControl(cleaned)); } catch {} } throw new Error("JSON parsing failed: " + content.slice(0, 200)); } async function callGLM( messages: ChatMessage[], apiKey: string = "YOUR_API_KEY" ): Promise게이트웨이 On-promise 제품 팀에서 시스템 모니터링 및 관리를 쉽게 다가갈 수 있도록 하기 위한 업무를 하고 있습니다.
Contact: lhjnano@gmail.com