Repository navigation
[회귀] OpenAI Chat 스트림의 tool_calls id/name=null 후속 청크를 invalid tool calls로 거부 #1731
Description
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.0preview/stable: 회귀 시작2.17.0: 실제 서브에이전트 호출에서 재현2.19.0: 최신 소스에도 같은 엄격 검사 조건이 남아 있음
Endpoint or capability
OpenAI-compatible /v1/chat/completions, streaming tool calls
stream: false 응답에서는 id와 function.name이 유효한 문자열이며 정상 동작합니다.
Current behaviour
SGLang 스트림의 첫 tool-call delta에는 유효한 id와 function.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
- OpenAI Function calling — Streaming:
https://developers.openai.com/api/docs/guides/function-calling#streaming - Modal/SGLang의 이 null continuation 형태를 명시한 provider-specific 공개 사양은 찾지 못했습니다. 해당 endpoint는 OpenAI-compatible Chat Completions를 표방하며 non-stream 응답은 정상입니다.
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"회귀 테스트:
- 첫 delta에 유효한 name/id, 후속 delta에 null → 성공
- 모든 delta에서 name이 null/누락 → 기존처럼 실패
Additional context and attachments
- #1483은 표면 오류는 비슷하지만 유효한 첫 청크 뒤
id:null/name:nullcontinuation으로 발생한 버전 회귀를 특정하지 않습니다. - 관련 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.
Activity
coderabbitai commentedon Aug 15, 2026
🔗 Related PRs
#302 - feat(kiro): harden completion and transport integration [merged]
#363 - fix(chat-completions): emit tool call arguments once [merged]
#1361 - fix(responses): restore DeepSeek streaming and routed code-mode tools [merged]
#1468 - fix(openai-chat): tolerate null tool-call padding [merged]
#1531 - fix(openai-chat): refuse to emit an unnamed streamed tool call [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
github-actions commentedon Aug 15, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
Ingwannu commentedon Aug 15, 2026
Confirmed on current
dev. The streamed tool-call parser still treats continuation padding such asid: null/function.name: nullas an invalid non-string value even when an earlier delta already supplied the valid field.I will make the compatibility change narrowly:
nullwill be treated the same as an omitted field only while accumulating deltas. Non-null non-string values will still fail closed, and the existing final validation will continue rejecting any completed tool call that never acquired a valid function name.I will add regressions for both boundaries:
id/namefollowed bynullcontinuation padding succeeds;Ingwannu commentedon Aug 15, 2026
Implemented the narrow fix in #1754.
The patch treats only
null/omitted continuation fields as absent while retaining the existing fail-closed behavior for non-null non-string values and for a completed tool call that never receives a valid function name.Local validation on current
dev:I will keep the issue linked to the PR so it closes only when the fix actually lands.
fix(openai-chat): tolerate null tool delta padding
fix(openai-chat): tolerate null tool delta padding (#1754)
lidge-jun commentedon Aug 15, 2026
Fixed on
devas the squash of #1754.The stream parser now treats null and undefined alike as absent for a streamed
id/name/arguments, so a continuation delta that repeats already-sent fields as null no longer aborts the turn. Any other non-string value still fails closed.One addition beyond the original patch: the diagnostics path used its own null predicate, so accepted padding was still being classified as the invalid delta — a payload with real corruption later in the array would have reported the harmless padding instead and aimed compatibility work at the wrong chunk. The accumulator and the diagnostic now share one predicate, with a regression proving the diagnostic points at the genuinely malformed entry.
Verified on the remote validation host: 97 pass / 0 fail across
tests/openai-chat-parallel-stream.test.ts,tests/openai-chat-hardening.test.ts,tests/openai-chat-eof.test.ts.Thanks @Ingwannu.
fix(openai-chat): tolerate null tool delta padding (lidge-jun#1754)