# Complete - openai_text_tool_call_boundary

## 완료 일시

2026-07-06

## 요약

OpenAI-compatible 경계의 text tool-call 처리 하드닝: regex 기반 파서를 scanner 공유 구조로 재작성하고 tools stream을 buffered 검증 경로로 라우팅한 1차 루프(WARN) 후, native tool call 혼합 content의 선행 prose 보존 회귀 수정과 Nit 정리(2차 루프)로 종결. 루프 2회, 최종 판정 PASS.

## 루프 이력

| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | WARN | scanner 재작성/buffered 라우팅은 양호. Suggested 1건: native tool call+content 공존 시 `FlushNonCandidate()` 단독 사용으로 선행 prose까지 유실. Nit 2건: dead text tool-call match 체인, `close` shadowing/passthrough wrapper. |
| `plan_local_G04_1.log` | `code_review_local_G04_1.log` | PASS | `collectChatCompletionOutput`의 content 정리를 `Append` 기반으로 교체해 live stream 경로와 동일 의미로 선행 prose 보존, raw 후보 무누출. 회귀 테스트 2개 추가. Nit 정리(dead 체인 삭제, `closeIdx` 개명, `toolValidationContractForChatRequest` wrapper 제거) 완료. |

## 구현/정리 내용

- `apps/edge/internal/openai/chat_handler.go`: native tool call과 content 혼합 시 `streamToolTextFilter{}.Append()`로 안전 prefix를 먼저 방출한 뒤 잔여만 `FlushNonCandidate()`로 제거해 선행 prose를 보존. `toolValidationContractFromRequest(req)` 직접 호출로 전환.
- `apps/edge/internal/openai/tool_validation.go`: stream 분기 없는 passthrough가 된 `toolValidationContractForChatRequest` wrapper 삭제(tools stream은 `stream.go:37`에서 전부 buffered 경로로 라우팅되므로 동작 동일).
- `apps/edge/internal/openai/types.go`: 호출자 없는 dead 체인(`collectTextToolCallMatches`, `collectXMLTextToolCallMatches`, `collectMustacheTextToolCallMatches`, `parseTextToolCallBlock`, `textToolCallMatch`, `mustacheTextToolCallBlock`, 미사용 regex 4종, `regexp` import) 삭제. `findXMLOpenTag`의 builtin shadowing 변수 `close`를 `closeIdx`로 개명.
- `apps/edge/internal/openai/server_test.go`: `TestChatCompletionsNativeToolCallPreservesLeadingProse`, `TestChatCompletionsToolsStreamNativeToolCallPreservesLeadingProse` 회귀 테스트 추가(prose 보존, raw/escaped `<tool_call` 무누출, native call 유지, `finish_reason=tool_calls`).

## 최종 검증

- `go test ./apps/edge/internal/openai -run 'TestChatCompletions(NativeToolCallPreservesLeadingProse|ToolsStreamNativeToolCallPreservesLeadingProse)$'` - PASS; ok 0.005s (리뷰 재실행 동일)
- `go build ./apps/edge/...` - PASS; 출력 없음
- `go test -count=1 -timeout 60s ./apps/edge/internal/openai` - PASS; ok iop/apps/edge/internal/openai 1.576s (구현 보고 1.579s와 동일 범위)
- `gofmt -l apps/edge/internal/openai` - PASS; 출력 없음

## 잔여 Nit

- 없음

## 후속 작업

- dev-runtime smoke는 로컬 변경 미배포로 미실행(1차 plan의 조건부 항목). 배포 시 `agent-ops/skills/project/dev-runtime-deploy/SKILL.md` 흐름의 OpenAI-compatible capacity smoke로 확인.
