Published on

OpenAI Responses API 400 invalid_request_error 해결 가이드

Authors

서버가 5xx를 뱉는 것도 아니고, 네트워크 타임아웃도 아닌데 **Responses API가 400 invalid_request_error**로 떨어지면 대부분 “요청 바디가 스펙과 다르다”는 뜻입니다. 문제는 이 에러가 한 문장으로 끝나는 경우가 많아(예: Invalid request) 어디를 고쳐야 할지 감이 안 온다는 점입니다.

이 글은 실제 운영에서 자주 만나는 400 케이스를 체크리스트처럼 빠르게 진단할 수 있게 정리합니다. 특히 input/messages 혼용, response_format/JSON 스키마, 툴 호출 파라미터, 모델-기능 불일치 같은 “겉으로는 정상처럼 보이지만 스펙상 틀린” 요청을 중심으로 다룹니다.

> 참고: 요청 바디 자체가 JSON 파싱 실패로 400이 나는 경우는 성격이 다릅니다. 그 케이스는 Python OpenAI SDK 400 invalid_json 원인과 해결에서 별도로 정리했습니다.


1) 400 invalid_request_error의 본질: “스키마/제약 위반”

Responses API의 400은 크게 아래로 나뉩니다.

  • 필수 필드 누락: model, input
  • 필드 타입 오류: 문자열이어야 하는데 배열, 객체여야 하는데 문자열
  • 허용되지 않는 필드 조합: 같은 의미의 필드를 중복 지정하거나, 특정 옵션과 같이 쓸 수 없는 옵션을 같이 씀
  • 모델/기능 불일치: 해당 모델이 tools, json_schema, 특정 모달리티 등을 지원하지 않음
  • 제한값 위반: 길이, 크기, enum 값(예: role) 등

진단의 핵심은 “내가 보낸 JSON이 API가 기대하는 스키마와 정확히 일치하는가”입니다. 따라서 가장 먼저 할 일은 실제 전송된 요청 바디를 그대로 로그로 남기고, 에러 응답의 message, param, code를 구조적으로 기록하는 것입니다.


2) 우선 적용: 에러 응답을 구조적으로 로깅하기

Python(OpenAI SDK) 예시

import json
from openai import OpenAI

client = OpenAI()

def call_responses(payload: dict):
    try:
        return client.responses.create(**payload)
    except Exception as e:
        # SDK 예외 객체는 버전에 따라 구조가 다를 수 있어 안전하게 출력
        print("=== request payload ===")
        print(json.dumps(payload, ensure_ascii=False, indent=2))
        print("=== exception ===")
        print(repr(e))
        raise

payload = {
    "model": "gpt-4.1-mini",
    "input": "ping"
}

call_responses(payload)

Node(fetch) 예시

const payload = {
  model: "gpt-4.1-mini",
  input: "ping",
};

const res = await fetch("https://api.openai.com/v1/responses", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
  },
  body: JSON.stringify(payload),
});

const body = await res.json().catch(() => null);
if (!res.ok) {
  console.error("status=", res.status);
  console.error("payload=", payload);
  console.error("error=", body);
  throw new Error("OpenAI request failed");
}

이렇게 “요청/응답”을 남기면, 아래 섹션의 체크리스트로 빠르게 매칭할 수 있습니다.


3) 가장 흔한 실수 1: inputmessages를 섞어 쓰기

Responses API는 보통 input을 사용합니다. 그런데 Chat Completions 시절 습관으로 messages를 그대로 넣거나, inputmessages를 동시에 넣어 스키마 충돌을 내는 경우가 많습니다.

잘못된 예(혼용)

{
  "model": "gpt-4.1-mini",
  "input": "요약해줘",
  "messages": [{"role": "user", "content": "Hello"}]
}

올바른 예(Responses 스타일로 통일)

{
  "model": "gpt-4.1-mini",
  "input": "Hello를 한 문장으로 요약해줘"
}

대화형 입력이 필요하면 input을 배열로 구성

{
  "model": "gpt-4.1-mini",
  "input": [
    {"role": "system", "content": "너는 간결한 한국어 비서다."},
    {"role": "user", "content": "아래 글을 3줄로 요약해줘..."}
  ]
}

포인트는 하나입니다. 한 요청에서 한 가지 스타일로만 구성하세요.


4) 가장 흔한 실수 2: input 타입/구조가 스펙과 다름

input은 보통 문자열 또는(대화형) role/content 객체 배열을 씁니다. 여기서 자주 틀리는 패턴:

  • content에 문자열이 아닌 객체를 넣음
  • role에 허용되지 않는 값(예: "assistant"만 계속 쓰거나 오타)
  • 배열 구조를 잘못 중첩

잘못된 예(content가 객체)

{
  "model": "gpt-4.1-mini",
  "input": [{"role": "user", "content": {"text": "hi"}}]
}

올바른 예

{
  "model": "gpt-4.1-mini",
  "input": [{"role": "user", "content": "hi"}]
}

운영 코드에서는 요청 직전에 간단한 validation을 넣는 것만으로도 400의 70%를 막습니다.


5) 가장 흔한 실수 3: JSON 출력 강제(response_format)의 오용

구조화된 JSON을 받으려고 response_format을 쓰다가 400이 나는 경우가 많습니다. 특히 다음 케이스를 조심하세요.

  • json_schema를 쓰는데 schema가 JSON Schema 규격에 맞지 않음
  • strict 옵션과 함께 스키마가 너무 느슨/애매하거나, required 정의가 꼬임
  • 모델이 해당 출력 형식을 지원하지 않음(모델-기능 불일치)

예: JSON Schema로 응답 강제(정상 예시)

from openai import OpenAI
client = OpenAI()

resp = client.responses.create(
    model="gpt-4.1-mini",
    input="사용자 이름과 나이를 추출해 JSON으로 반환: 홍길동 31세",
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "user_profile",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"}
                },
                "required": ["name", "age"],
                "additionalProperties": False
            },
            "strict": True
        }
    }
)

print(resp.output_text)

실패를 줄이는 팁

  • 스키마는 최소 필드부터 시작해 점진적으로 확장
  • additionalProperties: false를 켰다면 모델이 생성할 수 있는 필드가 스키마에 모두 있는지 확인
  • 숫자 타입(integer/number)을 엄격히 나눴다면 입력에서 파싱 가능한지 확인

6) 가장 흔한 실수 4: tools/함수 호출 스펙 불일치

툴 호출은 400을 가장 많이 유발합니다. 이유는 간단합니다. 함수 스키마가 조금이라도 틀리면 바로 invalid_request가 납니다.

정상적인 tools 정의 예시

from openai import OpenAI
client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "도시의 현재 날씨를 조회",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"},
                    "unit": {"type": "string", "enum": ["c", "f"]}
                },
                "required": ["city"],
                "additionalProperties": False
            }
        }
    }
]

resp = client.responses.create(
    model="gpt-4.1-mini",
    input="서울 날씨 알려줘. 섭씨로.",
    tools=tools,
)

print(resp.output[0].type)

자주 틀리는 포인트

  • tools 최상위 원소에 type: function 누락
  • function.name에 공백/특수문자 사용(허용 범위 밖)
  • parameters가 JSON Schema가 아닌 임의 포맷
  • requiredproperties에 없는 필드를 참조

운영에서는 tools 스키마를 코드로 동적 생성하기보다, 정적 상수 + 테스트로 고정하는 편이 훨씬 안전합니다.


7) 가장 흔한 실수 5: 모델/기능 불일치(지원하지 않는 옵션 사용)

400이지만 실제 원인은 “이 모델에서 그 옵션을 못 쓴다”인 경우가 있습니다.

예시 패턴:

  • 특정 모델에서 response_format: json_schema가 제한됨
  • 멀티모달 입력(이미지/오디오)을 지원하지 않는 모델에 해당 input을 넣음
  • tools를 지원하지 않는 모델 조합

해결 전략

  • 문제가 되는 옵션을 최소화한 요청으로 줄여서(모델 + input만) 정상 동작 확인
  • 정상 동작이 확인되면 옵션을 하나씩 추가하며 어떤 옵션에서 깨지는지 pinpoint
  • 모델을 교체해야 한다면, 운영 환경에서 모델명을 설정값으로 분리(핫픽스 가능)

8) 가장 흔한 실수 6: max_output_tokens/토큰 관련 파라미터 오타

Chat Completions에서 쓰던 max_tokens를 그대로 넣거나, Responses에서 허용되지 않는 이름을 넣으면 400이 날 수 있습니다.

점검 포인트

  • SDK/REST 기준으로 정확한 파라미터 명을 사용했는지 확인
  • 사내 공통 모듈에서 오래된 파라미터를 자동으로 주입하고 있지 않은지 확인

이 케이스는 특히 “공통 래퍼”를 쓰는 조직에서 자주 발생합니다. 팀 A는 최신 SDK인데 팀 B의 래퍼가 구식 파라미터를 계속 붙이는 식입니다.


9) 재현 가능한 최소 요청(Minimal Repro) 만들기

400을 빠르게 잡는 가장 강력한 방법은 요청을 최소화하는 것입니다.

1단계: model + input만 남기기

curl 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이면: 인증/조직/프로젝트 키 문제라기보다 요청 포맷 자체 또는 계정 정책 이슈 가능
  • 여기서 200이면: 옵션(tools, response_format, metadata, 커스텀 필드 등) 중 하나가 원인

2단계: 옵션을 하나씩 추가

  • response_format 추가 → 실패하면 스키마/모델지원 확인
  • tools 추가 → 실패하면 함수 스키마/이름 규칙 확인
  • input을 배열(대화형)로 변경 → 실패하면 role/content 구조 확인

이 방식은 디버깅 시간을 체감적으로 1/3 이하로 줄입니다.


10) 운영 팁: 400은 재시도보다 “사전 검증”이 비용 절감

5xx/408처럼 재시도가 의미 있는 케이스와 달리, 400은 재시도해도 같은 요청이면 100% 재현됩니다. 따라서 운영 안정성 관점에서는 아래가 중요합니다.

  • 요청 직전 스키마 검증(Validation): Pydantic/JSONSchema 등을 사용
  • 공통 래퍼에서 허용 파라미터 화이트리스트 적용(모르는 키가 들어오면 로컬에서 실패)
  • 에러 응답의 param을 로그에 남겨, 어떤 필드가 문제였는지 바로 알 수 있게 함

반대로, 408/5xx는 재시도 전략이 핵심입니다. 타임아웃 케이스는 OpenAI Responses API 408 타임아웃 재현과 해결 실전 가이드, 500/503은 OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커에서 운영 패턴을 참고하면 좋습니다.


11) 실전 체크리스트(여기부터 순서대로 보면 됨)

A. 요청 바디/스키마

  • model 존재, 문자열이며 실제 사용 가능한 모델명
  • input 존재
  • input이 문자열이면 OK / 배열이면 각 원소가 {role, content} 형태
  • role 오타 없음(system, user, assistant 등 허용값)
  • content는 문자열(또는 API가 허용하는 정확한 구조)인지

B. 옵션 충돌/오타

  • messages 같은 구식 필드를 같이 보내지 않았는지
  • max_tokens 등 구식 파라미터가 섞이지 않았는지
  • 알 수 없는 커스텀 키가 래퍼에서 주입되지 않았는지

C. JSON/툴 기능

  • response_format의 타입/스키마가 유효한 JSON Schema인지
  • tools[].function.parameters가 JSON Schema인지
  • required/properties 불일치 없는지

D. 모델-기능 매칭

  • 해당 모델이 tools/json_schema/모달리티를 지원하는지

12) 결론: 400은 “최소 요청 → 옵션 추가”로 끝난다

invalid_request_error는 무섭게 보이지만, 대부분은 요청을 최소화해서 200을 만든 뒤 옵션을 하나씩 추가하면 정확히 원인이 드러납니다. 운영에서는 재시도 로직보다 사전 검증과 로깅이 훨씬 큰 효과를 냅니다.

만약 지금도 원인을 못 찾고 있다면, 아래 3가지만 준비해 보세요.

  1. 실제 전송된 요청 JSON(민감정보 제거)
  2. 400 응답의 message/param/code
  3. 최소 요청(model+input)은 성공하는지 여부

이 3개만 있으면 대부분의 400 invalid_request_error는 짧은 시간 안에 해결됩니다.