<!-- task=m-openai-compatible-think-control/03+01_edge_reasoning_visibility plan=0 tag=API -->

# Plan - API Edge Reasoning Visibility

## 이 파일을 읽는 구현 에이전트에게

`01_edge_request_contract`의 `complete.log`가 있어야 이 plan을 구현한다. 구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 내용과 검증 출력으로 채운 상태다. review finalization, archive, `complete.log` 작성은 code-review 전용이다.

## 배경

SDD는 `include_reasoning=false`를 generation control이 아니라 OpenAI-compatible response exposure control로 정의했다. 현재 Edge는 non-streaming `message.reasoning_content`와 live SSE `delta.reasoning_content`를 기본으로 노출한다. 요청자가 reasoning 생성을 유지하면서 노출만 숨길 수 있도록 response writer 단에서 필터를 추가해야 한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock decision이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 외부 환경이나 검증 evidence 공백은 사용자 리뷰가 아니라 blocker/follow-up으로 기록한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-think-control.md`
- Task ids:
  - `reasoning-visibility`: `include_reasoning=false`가 provider reasoning 생성 여부와 별개로 OpenAI-compatible 응답의 `reasoning_content` 노출을 억제한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-think-control.md`
- `agent-roadmap/sdd/knowledge-tool-optimization-extension/openai-compatible-think-control/SDD.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/server_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/openai-compatible-think-control/SDD.md`
- 상태: `[승인됨]`, SDD 잠금: `해제`, 사용자 리뷰: 없음
- Acceptance Scenario: S03 -> `reasoning-visibility`
- Evidence Map: S03 requires stream/non-stream response filtering tests and `Roadmap Completion` evidence for `reasoning-visibility`.

### 테스트 환경 규칙

- test_env: `local`
- 읽은 env/profile: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`
- 적용 명령: `go test -count=1 ./apps/edge/internal/openai`
- baseline 확인: `go test -count=1 ./apps/edge/internal/openai ./apps/node/internal/adapters/openai_compat` PASS
- 외부 runner preflight: 이 split은 current checkout unit test만 필요하므로 없음.

### 테스트 커버리지 공백

- Current tests assert default reasoning exposure:
  - non-streaming `TestChatCompletionsReturnsReasoningContentSeparately`
  - streaming `TestChatCompletionsStreamsReasoningSSE`
- No tests assert `include_reasoning=false` suppression while preserving content/tool output.
- Strict-output tests suppress reasoning for a different policy and must not be reused as this feature's evidence.

### 심볼 참조

- renamed/removed symbol: none.

### 분할 판단

- current subtask: `03+01_edge_reasoning_visibility`
- dependency from directory name: predecessor `01_edge_request_contract`
- predecessor completion: missing in active and archive lookup. Implementation must wait for `01_edge_request_contract/complete.log`.
- split reason: response filtering is Edge-only but depends on the request field introduced by 01; Node provider body mapping remains separate.

### 범위 결정 근거

- Request decoder validation is excluded to predecessor 01.
- Provider-side generation control is excluded to sibling 02.
- Contract docs and dev smoke are excluded to later dependent subtasks.

### 빌드 등급

- `local-G04`: bounded Edge stream/non-stream filtering with deterministic tests, but SSE behavior and tool output preservation add moderate risk.

## 구현 체크리스트

- [ ] `01_edge_request_contract` predecessor `complete.log`를 확인한 뒤 시작한다.
- [ ] non-streaming completion에서 `include_reasoning=false`이면 `message.reasoning_content`를 비우되 `content`, `tool_calls`, `finish_reason`, usage는 유지한다. 검증: non-stream 응답에서 reasoning field가 빠지고 content/tool output은 유지된다.
- [ ] live SSE stream에서 `include_reasoning=false`이면 `delta.reasoning_content` chunk를 쓰지 않되 content/tool chunks와 `[DONE]`은 유지한다. 검증: stream 응답에서 reasoning delta가 빠지고 content/tool output은 유지된다.
- [ ] default/omitted `include_reasoning`은 기존 reasoning exposure tests를 유지한다.
- [ ] `go test -count=1 ./apps/edge/internal/openai`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `03+01_edge_reasoning_visibility` decodes to `depends_on=[01]`.
- Do not implement until predecessor `01_edge_request_contract` has a `complete.log`.

### [API-1] Non-Streaming Reasoning Suppression

#### 문제

[apps/edge/internal/openai/chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:249)의 collection path always builds `chatMessage{ReasoningContent: reasoning}` after normalization. No request-level visibility switch is applied.

Before:

```go
// apps/edge/internal/openai/chat_handler.go:249
text, reasoning, finishReason, nativeToolCalls, usage, textToolFallback, err := collectRunResult(ctx, handle.Stream(), handle.WaitTimeout())
...
message := chatMessage{Role: "assistant", Content: text, ReasoningContent: reasoning}
```

After:

```go
// apply after normalization, before response serialization
if !req.includeReasoning() {
	message.ReasoningContent = ""
}
```

#### 해결 방법

- Add a small helper such as `req.includeReasoning()` that defaults to true.
- Filter only response exposure. Do not alter collected reasoning logs or provider generation input.
- Ensure tool call synthesis still sees content and does not depend on reasoning.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/types.go`: include helper if not already added by 01.
- [ ] `apps/edge/internal/openai/chat_handler.go`: non-streaming response filter.
- [ ] `apps/edge/internal/openai/server_test.go`: non-stream suppression tests.

#### 테스트 작성

- Add `TestChatCompletionsSuppressesReasoningContentWhenExcluded`.
- Add a tool-call preserving case if existing native/text tool tests make this cheap; otherwise include content preservation in the suppression test.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/openai
```

Expected: PASS.

### [API-2] Streaming Reasoning Suppression

#### 문제

- [apps/edge/internal/openai/stream.go](/config/workspace/iop/apps/edge/internal/openai/stream.go:65) writes every reasoning delta to SSE.
- [apps/edge/internal/openai/stream.go](/config/workspace/iop/apps/edge/internal/openai/stream.go:167) only suppresses reasoning for strict-output, not request visibility.

Before:

```go
// apps/edge/internal/openai/stream.go:167
case "reasoning_delta":
	if event.GetDelta() == "" {
		continue
	}
	reasoningBuilder.WriteString(event.GetDelta())
	if outputPolicy.Strict {
		continue
	}
	writeTracedReasoningDelta(event.GetDelta())
```

After:

```go
if outputPolicy.Strict || !req.includeReasoning() {
	continue
}
writeTracedReasoningDelta(event.GetDelta())
```

#### 해결 방법

- Suppress live SSE reasoning chunks when `include_reasoning=false`.
- Keep `reasoningBuilder` for logs and diagnostics.
- Buffered strict stream currently emits only content/tool output; verify it still does not leak reasoning and add a non-strict live SSE test.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/stream.go`: live SSE reasoning filter.
- [ ] `apps/edge/internal/openai/server_test.go`: streaming suppression tests.

#### 테스트 작성

- Add `TestChatCompletionsStreamSuppressesReasoningWhenExcluded` asserting no `reasoning_content`, content chunk present, `[DONE]` present.
- Keep `TestChatCompletionsStreamsReasoningSSE` passing for omitted `include_reasoning`.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/openai
```

Expected: PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/types.go` | API-1 |
| `apps/edge/internal/openai/chat_handler.go` | API-1 |
| `apps/edge/internal/openai/stream.go` | API-2 |
| `apps/edge/internal/openai/server_test.go` | API-1, API-2 |

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/openai
```

Expected: PASS. Fresh execution is required; do not rely on Go test cache.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
