- Published on
OpenAI Responses API 400 invalid_request_error 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 OpenAI Responses API를 붙이다 보면, 가장 시간을 잡아먹는 오류가 400 invalid_request_error입니다. 이유는 간단합니다. 401처럼 인증이 틀린 것도 아니고, 429처럼 트래픽 문제도 아니며, 대개 요청 바디의 구조(스키마)나 파라미터 조합이 미묘하게 틀린 경우가 많기 때문입니다.
이 글은 “왜 400이 났는지”를 감으로 때려맞추는 대신, 로그에서 원인을 분류하고 재현 가능한 형태로 축소한 뒤, 수정안을 검증하는 흐름으로 정리합니다. Node.js 예제를 기준으로 하지만, 원리는 언어와 무관합니다.
또한 대량 처리/재시도 관점은 OpenAI Batch API 429·큐 지연·부분실패 재시도 전략 글이 도움이 됩니다. 400은 재시도로 해결되지 않는 경우가 대부분이라, 재시도 전에 반드시 “요청 자체가 유효한가”를 먼저 확인해야 합니다.
1) invalid_request_error의 의미: “요청 스키마/조합이 유효하지 않다”
Responses API에서 400 invalid_request_error는 대체로 아래 범주 중 하나입니다.
- 필수 필드 누락:
model,input등 - 타입 불일치: 문자열이어야 하는데 배열/객체를 넣음, 또는 반대
input포맷 오류: 멀티모달/메시지 구조가 잘못됨- 모델-기능 조합 오류: 해당 모델이 지원하지 않는
response_format,tools,audio등을 요청 tools정의 오류: JSON Schema가 불완전하거나required/properties/type이 어긋남- 토큰/길이/콘텐츠 정책 관련: 너무 긴 입력, 혹은 특정 필드 제한 위반
핵심은 에러 메시지에 힌트가 거의 항상 있다는 점입니다. 다만 운영 로그에서 메시지가 잘리지 않게 “에러 객체 전체”를 남겨야 합니다.
2) 가장 먼저 할 일: 에러 객체를 “원문 그대로” 수집하기
Node.js에서 흔히 하는 실수는 err.message만 찍고 끝내는 것입니다. Responses API의 400은 error 객체의 message, type, param, code 조합이 중요합니다.
Node.js(공식 SDK) 에러 로깅 예시
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
try {
const res = await client.responses.create({
model: "gpt-4.1-mini",
input: "hello"
});
console.log(res.output_text);
} catch (err) {
// SDK 에러는 형태가 다양한 편이라 안전하게 분기
const status = err?.status;
const data = err?.error || err?.response?.data || err;
console.error("OpenAI error status:", status);
console.error("OpenAI error payload:", JSON.stringify(data, null, 2));
throw err;
}
이렇게 남기면 보통 message에 다음과 같은 단서가 들어옵니다.
Unknown parameter: ...Missing required parameter: ...Invalid type for ... expected ...The model ... does not support ...Invalid schema for tool ...
이 문구를 기반으로 아래 체크리스트에서 빠르게 갈라타면 됩니다.
3) 체크리스트 A: 필수 필드/파라미터 이름 오타
흔한 원인
model누락input대신messages를 최상위에 둠(Responses API는input이 기본)response_format을 잘못된 구조로 넣음- 과거 API 파라미터(
max_tokens등)를 무심코 섞어 씀
올바른 최소 요청(텍스트)
const res = await client.responses.create({
model: "gpt-4.1-mini",
input: "한국어로 한 문장만 요약해줘"
});
console.log(res.output_text);
진단 포인트
- 에러가
Unknown parameter면 키 이름이 틀렸을 확률이 큽니다. - 에러가
Missing required parameter면 최상위 필수 필드부터 확인합니다.
4) 체크리스트 B: input 포맷(메시지/멀티모달) 구조 오류
Responses API는 input에 문자열을 넣을 수도 있고, 구조화된 입력을 넣을 수도 있습니다. 여기서 400이 자주 납니다.
올바른 메시지 스타일 입력 예시
const res = await client.responses.create({
model: "gpt-4.1-mini",
input: [
{
role: "system",
content: [
{ type: "text", text: "너는 꼼꼼한 코드 리뷰어야." }
]
},
{
role: "user",
content: [
{ type: "text", text: "이 함수의 잠재 버그를 찾아줘" }
]
}
]
});
console.log(res.output_text);
자주 하는 실수
content를 문자열로 넣어야 하는데 배열을 넣거나(또는 그 반대)content배열 요소에서type누락role오타(예:assistantt)
진단 포인트
Invalid type for input또는Invalid type for input[0].content같은 메시지면 중첩 타입을 의심하세요.
5) 체크리스트 C: 모델-기능 조합 불일치
400의 상당수는 “이 모델은 그 기능을 지원하지 않는다”입니다. 예를 들어 특정 모델에서 response_format(JSON 강제), tools, audio 등을 지원하지 않는데 요청하면 400이 납니다.
예: JSON 출력 강제(response_format) 사용 시
const res = await client.responses.create({
model: "gpt-4.1-mini",
input: "키와 몸무게로 BMI를 계산해줘. JSON으로만 답해.",
response_format: {
type: "json_schema",
json_schema: {
name: "bmi_result",
schema: {
type: "object",
properties: {
bmi: { type: "number" },
category: { type: "string" }
},
required: ["bmi", "category"],
additionalProperties: false
}
}
}
});
console.log(res.output_text);
진단 포인트
- 에러 메시지에
does not support가 있으면 모델을 바꾸거나 기능을 빼야 합니다. - 운영에서는 “모델별 지원 기능 매트릭스”를 코드로 관리하는 편이 안전합니다.
6) 체크리스트 D: tools(함수 호출) 스키마가 잘못됨
tools는 400을 가장 많이 만드는 구간 중 하나입니다. 특히 JSON Schema에서 type, properties, required, additionalProperties 조합이 어긋나면 바로 invalid_request_error가 납니다.
올바른 tools 정의 예시
const res = await client.responses.create({
model: "gpt-4.1-mini",
input: "서울의 내일 날씨를 조회해서 요약해줘.",
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "도시의 날씨를 조회한다",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "도시명" },
unit: { type: "string", enum: ["c", "f"] }
},
required: ["city"],
additionalProperties: false
}
}
}
]
});
흔한 스키마 오류 패턴
parameters에type: "object"가 없음properties가 없는데required만 있음required에properties에 없는 키를 넣음additionalProperties를 기대와 다르게 설정(엄격 모드에서 특히)
스키마 관련 400은 Claude 쪽이지만 원리가 유사합니다. JSON Schema 디버깅 관점은 Claude Tool Use 400 에러 - JSON 스키마 해결법도 참고할 만합니다.
7) 체크리스트 E: JSON 모드/스키마 모드인데 출력이 JSON이 되지 않게 유도함
response_format을 JSON으로 강제했는데, 프롬프트에서 “설명도 덧붙여줘” 같은 문장을 넣으면 모델이 규칙을 어기려다 실패하거나(혹은 서버가 거부) 400이 날 수 있습니다.
권장 패턴
- 시스템 메시지에 “JSON만 출력”을 명확히
- 사용자의 요청에도 “JSON만”을 반복
- 스키마는
additionalProperties: false로 불필요한 키를 차단
const res = await client.responses.create({
model: "gpt-4.1-mini",
input: [
{
role: "system",
content: [{ type: "text", text: "반드시 JSON만 출력한다. 다른 텍스트 금지." }]
},
{
role: "user",
content: [{ type: "text", text: "다음 문장을 감정 분석해 JSON으로만 답해: '오늘은 정말 최고야'" }]
}
],
response_format: {
type: "json_schema",
json_schema: {
name: "sentiment",
schema: {
type: "object",
properties: {
label: { type: "string", enum: ["positive", "neutral", "negative"] },
confidence: { type: "number" }
},
required: ["label", "confidence"],
additionalProperties: false
}
}
}
});
8) 체크리스트 F: 입력 크기/배열 크기/로그 잘림로 인한 “숨은” 400
입력이 너무 길거나(특히 대용량 문서), 배열이 너무 크거나, 특정 필드 제한을 넘으면 400이 날 수 있습니다. 그런데 운영 환경에서는 다음 문제로 원인 파악이 더 어려워집니다.
- 프록시/게이트웨이에서 바디를 잘라 전송
- 로깅에서 요청 바디를 잘라 저장
- gzip/압축 설정 문제로 일부가 유실
권장 디버깅 방법
- 실패한 요청의 바디를 파일로 덤프해서 재현
- 최소 재현 케이스로 줄이기(문장 단위로 절반씩 줄여보기)
- 서버/프록시의 최대 바디 크기 제한 확인
리눅스/인프라에서 “원인이 다른 곳에 있는” 케이스는 의외로 많습니다. 예를 들어 프로세스/서비스 재시작 루프가 끼어 있으면 요청이 중간에 깨질 수 있는데, 이런 류의 운영 디버깅 패턴은 systemd 재시작 루프 - ExecStart 디버깅 가이드 같은 접근이 도움이 됩니다.
9) 실무용: 400을 빠르게 줄이는 방어 코드(요청 전 검증)
운영에서 400은 “재시도”로 해결되지 않으므로, 요청 생성 단계에서 스키마를 검증하는 편이 비용을 크게 줄입니다.
Zod로 요청 바디 최소 검증(예시)
import { z } from "zod";
const ResponseRequestSchema = z.object({
model: z.string().min(1),
input: z.union([
z.string().min(1),
z.array(
z.object({
role: z.enum(["system", "user", "assistant"]),
content: z.array(
z.object({
type: z.literal("text"),
text: z.string()
})
)
})
)
]),
tools: z
.array(
z.object({
type: z.literal("function"),
function: z.object({
name: z.string().min(1),
description: z.string().optional(),
parameters: z.any()
})
})
)
.optional()
});
export function assertValidRequest(body) {
return ResponseRequestSchema.parse(body);
}
이 정도만 해도 “키 오타/타입 불일치”로 발생하는 400의 상당수를 사전에 제거할 수 있습니다.
10) 재현 가능한 디버깅 플로우(추천)
- 실패 요청의
requestId(있다면)와 에러 payload 전체를 저장 - 같은 요청을 curl 또는 작은 스크립트로 단독 재현
- 요청 바디를 최소화:
tools제거,response_format제거,input을 문자열로 단순화 - 하나씩 다시 추가하며 어느 옵션에서 400이 시작되는지 확인
- 모델 교체로 기능 지원 여부 확인
curl로 최소 재현(예시)
아래 예시는 “형태”를 보여주기 위한 것으로, 실제 엔드포인트/헤더는 사용 중인 환경에 맞춰 조정하세요.
curl -sS https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4.1-mini",
"input": "ping"
}'
여기서도 400이면, 애플리케이션 코드가 아니라 키/권한/계정/모델명 문제일 수 있습니다. 반대로 curl은 성공하는데 앱만 실패하면, 앱에서 직렬화/미들웨어/프록시가 바디를 변형하고 있을 가능성이 큽니다.
11) 결론: 400은 “요청 검증” 문제, 해결은 체크리스트화
400 invalid_request_error는 대부분 아래 3가지만 제대로 하면 해결 속도가 급격히 빨라집니다.
- 에러 payload를 절대 축약하지 말고 JSON 원문으로 저장
- 요청 바디를 최소 재현으로 줄여 “어느 필드에서 깨지는지” 찾기
input/tools/response_format은 스키마 검증을 붙여 사전에 차단
특히 tools와 JSON 스키마는 한번 안정화해두면 이후 운영 비용이 크게 줄어듭니다. 400을 “재시도”로 덮지 말고, 요청 생성 단계에서 유효성 검사를 통과한 것만 보내는 구조로 바꾸는 것이 정답입니다.