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

# Plan - API Edge Request Contract

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지가 구현 완료다. 검증을 실행하고 실제 출력/변경 내용을 채운 뒤 active 파일을 그대로 두고 review ready로 보고한다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청`만 채우고 멈춘다. 직접 사용자에게 묻거나 `USER_REVIEW.md`, `complete.log`, archive 파일을 만들지 않는다.

## 배경

현재 Milestone은 승인된 SDD를 통해 Chat Completions의 요청별 thinking 제어 필드를 공개 API로 열기로 했다. 현재 Edge decoder는 `think`를 unsupported field로 거부하고, strict-output 내부 경로만 `input["think"]=false`를 만든다. 먼저 공개 request contract와 내부 run input key를 고정해야 Node provider mapping과 response filtering split이 같은 기준을 쓸 수 있다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 Milestone lock decision이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 질문, 선택지 제시, `request_user_input` 호출은 금지이며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-think-control.md`
- Task ids:
  - `request-contract`: `/v1/chat/completions`가 `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning`을 허용하고 타입, 범위, 충돌 조합을 검증한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/knowledge-tool-optimization-extension/PHASE.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/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/chat_handler.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: S01 -> `request-contract`
- Evidence Map: S01 requires Edge OpenAI request decoder/unit test and `Roadmap Completion` evidence for `request-contract`.
- 이 plan은 S01만 완료 대상으로 삼는다. S02/S03/S05/S06은 sibling split에서 처리한다.

### 테스트 환경 규칙

- 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은 현재 checkout의 Edge unit test만 필요하므로 없음.

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

- 새 request field 허용: 현재 `TestChatCompletionsRejectsUnsupportedFields`가 `think`를 400으로 기대한다. 허용/거부 경계 테스트가 필요하다.
- 타입/범위/충돌: 현재 `thinking_token_budget`, `reasoning_effort`, `include_reasoning` 검증 테스트가 없다.
- 기존 unknown provider wrapper 거부: `options`, `format`, `keep_alive` 400 회귀를 유지해야 한다.

### 심볼 참조

- renamed/removed symbol: none.

### 분할 판단

- split decision policy를 먼저 적용했다.
- shared task group: `agent-task/m-openai-compatible-think-control/`
- siblings:
  - `01_edge_request_contract`: independent foundation, this plan.
  - `02+01_node_provider_mapping`: depends on 01 run input keys.
  - `03+01_edge_reasoning_visibility`: depends on 01 `include_reasoning` request field.
  - `04+01,02,03_contract_docs`: waits for implemented behavior before docs.
  - `05+01,02,03,04_dev_smoke`: waits for code/docs and external runtime.
- predecessor completion: none required for `01_edge_request_contract`; active/archive complete logs for this task group were absent.

### 범위 결정 근거

- Node provider request body mapping is excluded; `02+01_node_provider_mapping` owns it.
- `reasoning_content` response filtering is excluded; `03+01_edge_reasoning_visibility` owns it.
- Contract/dev docs and external smoke are excluded to avoid documenting or smoking behavior before implementation.

### 빌드 등급

- `local-G04`: public API decode/validation change with bounded Edge files and deterministic unit tests.

## 구현 체크리스트

- [ ] `chatCompletionRequest`에 `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning` 필드를 추가하고 `/v1/chat/completions` decoder allowlist와 validation을 갱신한다. 검증: unknown provider wrapper는 계속 400, 허용 필드는 Edge request decoder/unit test 통과.
- [ ] `runInput`이 validated think-control 값을 내부 input map에 보존하되, omitted field는 기존 provider default 동작을 유지하도록 어떤 key도 추가하지 않는다.
- [ ] `apps/edge/internal/openai/server_test.go`에 허용 필드, 범위/충돌 실패, unknown wrapper 회귀 테스트를 추가/갱신한다.
- [ ] `go test -count=1 ./apps/edge/internal/openai`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Edge Request Decode And Validation

#### 문제

- [apps/edge/internal/openai/types.go](/config/workspace/iop/apps/edge/internal/openai/types.go:13)의 `chatCompletionRequest`에 think-control 필드가 없다.
- [apps/edge/internal/openai/chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:92)의 decoder allowlist가 새 필드를 모두 거부한다.
- [apps/edge/internal/openai/server_test.go](/config/workspace/iop/apps/edge/internal/openai/server_test.go:2152)는 `think`를 unsupported로 기대한다.

Before:

```go
// apps/edge/internal/openai/chat_handler.go:97
for key := range raw {
	switch key {
	case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools", "tool_choice", "parallel_tool_calls", "stream_options", "store":
	default:
		return fmt.Errorf("%s is not supported for /v1/chat/completions", key)
	}
}
```

After:

```go
// apps/edge/internal/openai/chat_handler.go:97
case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools", "tool_choice", "parallel_tool_calls", "stream_options", "store", "think", "reasoning_effort", "thinking_token_budget", "include_reasoning":
```

Validation baseline:

- `thinking_token_budget` must be `>= 0`.
- `reasoning_effort` allowed values: `none`, `low`, `medium`, `high`; empty string is invalid when present.
- `think=false` conflicts with `reasoning_effort` values other than `none`.
- `thinking_token_budget` conflicts with `think=false` or `reasoning_effort=none`.
- `include_reasoning` is exposure-only and does not conflict with generation controls.

#### 해결 방법

- Add pointer fields to distinguish omitted from zero values.
- Add `validateThinkControl(req)` from `decodeChatCompletionRequest` after existing sampling validation.
- Add helper methods on `chatCompletionRequest` to copy present fields into `input`: `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning`.
- Keep strict-output `input["think"]=false` behavior unless request explicitly set `think=true`; explicit `think=true` should win only when validation allows it.

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

- [ ] `apps/edge/internal/openai/types.go`: request fields, run input copy, helper functions.
- [ ] `apps/edge/internal/openai/chat_handler.go`: decoder allowlist and validation call.
- [ ] `apps/edge/internal/openai/server_test.go`: request contract tests and unsupported wrapper regression.

#### 테스트 작성

- Add `TestChatCompletionsAcceptsThinkControlFields` asserting `fake.req.Input`.
- Update `TestChatCompletionsRejectsUnsupportedFields` to remove `think` from unsupported cases while keeping `options`, `format`, `keep_alive`.
- Add `TestChatCompletionsRejectsInvalidThinkControlFields` table for negative budget, bad effort, `think=false` + `high`, and `reasoning_effort=none` + budget.

#### 중간 검증

```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/server_test.go` | API-1 |

## 최종 검증

```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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
