Skip to content

[회귀] OpenAI Chat 스트림의 tool_calls id/name=null 후속 청크를 invalid tool calls로 거부 #1731

Description

@idguyst

Client or integration

Codex App의 qwen_review 서브에이전트

Provider or upstream service

Modal OpenAI-compatible Chat Completions endpoint, SGLang backend, model family Qwen/Qwen3.8-2.4T-A95B

공통 Modal gateway에서 endpoint hostname을 model ID로 쓰는 경로와 전용 endpoint의 /v1에서 정식 모델명을 쓰는 경로 모두 같은 스트림 형태를 반환했습니다. hostname-as-model 자체는 원인이 아닙니다.

OpenCodex version

  • 2.13.0: 동일한 후속 청크의 null 값을 허용하여 정상 동작
  • 2.14.0 preview/stable: 회귀 시작
  • 2.17.0: 실제 서브에이전트 호출에서 재현
  • 2.19.0: 최신 소스에도 같은 엄격 검사 조건이 남아 있음

Endpoint or capability

OpenAI-compatible /v1/chat/completions, streaming tool calls

stream: false 응답에서는 idfunction.name이 유효한 문자열이며 정상 동작합니다.

Current behaviour

SGLang 스트림의 첫 tool-call delta에는 유효한 idfunction.name이 있습니다. 이후 arguments continuation delta에서는 이미 보낸 필드를 null로 보냅니다.

OpenCodex 2.14.0 이후 parser는 후속 청크의 null을 “문자열이 아닌 값”으로 판단하여 전체 도구 호출을 무효화합니다. Qwen 서브에이전트는 첫 도구 호출을 실행하기 전에 종료되어 “응답 없음”처럼 보입니다.

Expected behaviour

  • continuation delta의 id: null, function.name: null, function.arguments: null은 “이번 delta에는 새 값이 없음”으로 취급해야 합니다.
  • 첫 청크에서 받은 기존 값은 유지해야 합니다.
  • 스트림 종료 시점까지 function name을 한 번도 받지 못한 호출은 기존처럼 거부해야 합니다. fail-closed 최종 검증은 유지해야 합니다.

Minimal redacted request or reproduction

# 첫 tool-call delta
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_redacted","function":{"name":"exec","arguments":""}}]}}]}

# arguments continuation delta
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":null,"function":{"name":null,"arguments":"...chunk..."}}]}}]}

# 비교 절차
1. 위 스트림을 OpenCodex 2.13.0과 2.14.0 이상에 입력
2. 동일한 tool call이 실행되는지 확인
3. stream=false 응답과 비교

Actual response or error

stream disconnected before completion:
Provider stream error: upstream response contained invalid tool calls

OpenCodex 2.13.0: tool call 실행 및 최종 응답 성공
OpenCodex 2.17.0: invalid tool calls로 즉시 종료
reasoning effort=max: 동일 실패
reasoning effort=xhigh: 6/6 동일 실패
stream=false: 유효한 string 필드로 성공

reasoning effort나 Qwen 모델의 도구 선택 문제가 아니라 streaming parser 호환성 문제로 보입니다.

Upstream documentation

Suggested mapping or implementation notes

회귀를 도입한 것으로 보이는 커밋:

2.19.0에도 남아 있는 검사:

rawName !== undefined && typeof rawName !== "string"
rawArguments !== undefined && typeof rawArguments !== "string"
tc.id !== undefined && typeof tc.id !== "string"

중간 delta에서 null을 unset과 동일하게 처리하도록 다음처럼 바꾸고, 스트림 종료 후 name 존재 검증은 유지하는 방법을 제안합니다.

rawName != null && typeof rawName !== "string"
rawArguments != null && typeof rawArguments !== "string"
tc.id != null && typeof tc.id !== "string"

회귀 테스트:

  1. 첫 delta에 유효한 name/id, 후속 delta에 null → 성공
  2. 모든 delta에서 name이 null/누락 → 기존처럼 실패

Additional context and attachments

  • #1483은 표면 오류는 비슷하지만 유효한 첫 청크 뒤 id:null / name:null continuation으로 발생한 버전 회귀를 특정하지 않습니다.
  • 관련 PR: fix(openai-chat): tolerate null tool-call padding #1468, fix(openai-chat): refuse to emit an unnamed streamed tool call #1531. 현재 2.19.0 소스에는 위 nested field 검사가 남아 있어 이 재현은 여전히 실패합니다.
  • Windows에서도 같은 서브에이전트 실패가 관찰됐지만, 결정적 프로토콜 로그는 macOS에서 캡처했습니다. parser 코드는 플랫폼 공통입니다.
  • 원본 JSONL과 전체 터미널 스크린샷에는 endpoint 식별자, 개인 경로, 요청 ID가 포함될 수 있어 올리지 않고, 재현에 필요한 SSE 구조와 오류 문구만 비식별 텍스트로 첨부했습니다.

Checks

  • I searched existing provider and compatibility issues.
  • The request and response were redacted.
  • The expected behaviour is based on an upstream specification or a concrete client requirement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproviderProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reportstoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions