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

# Plan - API Node Provider Mapping

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

`01_edge_request_contract`의 `complete.log`가 있어야 이 plan을 구현한다. 구현 완료는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지다. Milestone lock decision이 막으면 review stub의 `사용자 리뷰 요청`에만 기록하고, 직접 사용자에게 질문하거나 archive/complete 파일을 만들지 않는다.

## 배경

Edge가 공개 think-control 필드를 내부 input map으로 전달해도 Node adapter가 provider별 request body로 매핑하지 않으면 실제 generation 제어가 동작하지 않는다. 현재 `openai_compat` adapter는 `think`를 단순 top-level passthrough하고, provider label별 `chat_template_kwargs.enable_thinking` 매핑이 없다. 이 split은 vLLM, vLLM-MLX, Lemonade mock request body를 고정하고 omitted field 회귀를 같이 닫는다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 Milestone lock decision이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 환경/secret/provider 접근 실패는 사용자 리뷰가 아니라 검증 blocker 또는 follow-up이다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-think-control.md`
- Task ids:
  - `provider-mapping`: Edge run input과 Node `openai_compat` adapter가 요청별 think 설정을 provider별 top-level field 또는 내부 `chat_template_kwargs.enable_thinking`으로 매핑한다.
  - `default-compat`: think 관련 필드가 생략된 기존 요청은 현재 provider 기본 thinking 동작을 유지한다.
- 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/node/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `apps/node/internal/adapters/openai_compat/openai_compat_test.go`
- `apps/edge/internal/openai/types.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: S02 -> `provider-mapping`, S04 -> `default-compat`
- Evidence Map: S02 requires Node request body unit tests with provider mocks; S04 requires existing chat/tool/provider-pool regression evidence.
- 이 plan은 provider body mock tests를 completion evidence로 삼는다. External three-provider smoke is excluded to `05+01,02,03,04_dev_smoke`.

### 테스트 환경 규칙

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

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

- `think=false` provider mapping: current tests do not assert `chat_template_kwargs.enable_thinking=false` for vLLM/vLLM-MLX or top-level Lemonade mapping.
- `think=true` and `reasoning_effort=none`: no provider body test.
- Omitted field default compatibility: existing tests check generic options/tool passthrough but not absence of `think`/`chat_template_kwargs`.

### 심볼 참조

- renamed/removed symbol: none. If `buildRequestBody` signature changes, update its call at `apps/node/internal/adapters/openai_compat/openai_compat.go:131` and tests in `apps/node/internal/adapters/openai_compat/openai_compat_test.go`.

### 분할 판단

- shared task group: `agent-task/m-openai-compatible-think-control/`
- current subtask: `02+01_node_provider_mapping`
- dependency from directory name: predecessor `01_edge_request_contract`
- predecessor completion: missing in active and archive lookup. Implementation must wait for `agent-task/m-openai-compatible-think-control/01_edge_request_contract/complete.log` or matching archive complete log.
- split reason: provider mapping touches Node adapter and mock provider semantics; Edge response visibility and docs have separate ownership/risk.

### 범위 결정 근거

- Edge request decoder is excluded except for running its targeted tests after dependency completion.
- Edge response `reasoning_content` filtering is excluded to `03+01_edge_reasoning_visibility`.
- Real dev-runtime provider validation is excluded to `05+01,02,03,04_dev_smoke`.

### 빌드 등급

- `local-G05`: bounded Node adapter change, but provider-specific compatibility matrix and default-preservation tests raise risk.

## 구현 체크리스트

- [ ] `01_edge_request_contract` predecessor `complete.log`를 확인한 뒤 시작한다.
- [ ] `openai_compat` adapter request body builder가 provider label을 받아 think-control input을 provider별 body로 매핑하도록 바꾼다. 검증: vLLM Qwen3, vLLM-MLX, Lemonade mock이 같은 요청 플래그에 대해 기대 request body를 받는다.
- [ ] omitted think-control field가 `think`, `reasoning_effort`, `thinking_token_budget`, `chat_template_kwargs`를 만들지 않는 회귀 테스트를 추가한다. 검증: 기존 chat/tool/provider pool targeted tests가 통과한다.
- [ ] `go test -count=1 ./apps/node/internal/adapters/openai_compat ./apps/edge/internal/openai`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

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

### [API-1] Provider-Specific Think Body Mapping

#### 문제

- [apps/node/internal/adapters/openai_compat/openai_compat.go](/config/workspace/iop/apps/node/internal/adapters/openai_compat/openai_compat.go:36)의 adapter는 `provider` label을 갖고 있지만 body builder가 이를 사용하지 않는다.
- [apps/node/internal/adapters/openai_compat/openai_compat.go](/config/workspace/iop/apps/node/internal/adapters/openai_compat/openai_compat.go:440)의 `buildRequestBody`는 `think`만 top-level passthrough한다.

Before:

```go
// apps/node/internal/adapters/openai_compat/openai_compat.go:440
func buildRequestBody(model string, messages []chatMessage, input map[string]any) map[string]any {
	body := make(map[string]any)
	...
	for _, key := range []string{"tools", "tool_choice", "format", "think", "keep_alive"} {
		if v, ok := input[key]; ok {
			body[key] = v
		}
	}
	body["model"] = model
	body["messages"] = messages
	body["stream"] = true
	return body
}
```

After:

```go
// apps/node/internal/adapters/openai_compat/openai_compat.go
reqBody, err := a.buildRequestBody(model, messages, spec.Input)
if err != nil {
	return err
}
```

Provider mapping baseline:

| Provider label | `think=false` / `reasoning_effort=none` | `think=true` | Budget/effort |
|---|---|---|---|
| `vllm`, `vllm-mlx` | `chat_template_kwargs.enable_thinking=false` | `chat_template_kwargs.enable_thinking=true` | place supported budget/effort under `chat_template_kwargs`; unsupported values return adapter error with `unsupported think control` |
| `lemonade` | top-level `think=false` | top-level `think=true` | top-level `reasoning_effort` / `thinking_token_budget` if present |
| empty/unknown | keep current top-level `think` passthrough for compatibility | same | top-level passthrough for explicit fields |

#### 해결 방법

- Convert `buildRequestBody` into an adapter method or pass provider label explicitly.
- Merge `chat_template_kwargs` only inside the adapter; do not expose raw wrapper at Edge.
- Preserve `tools`, `tool_choice`, `format`, `keep_alive` existing behavior unless tests show a regression.
- Return a clear adapter error before HTTP for explicitly unsupported provider/value combinations.

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

- [ ] `apps/node/internal/adapters/openai_compat/openai_compat.go`: provider-aware body builder and helper functions.
- [ ] `apps/node/internal/adapters/openai_compat/openai_compat_test.go`: provider body assertions for vLLM, vLLM-MLX, Lemonade, omitted default.

#### 테스트 작성

- Add `TestOpenAICompatExecuteMapsThinkFalseForVLLMChatTemplateKwargs`.
- Add `TestOpenAICompatExecuteMapsThinkFalseForVLLMMLXChatTemplateKwargs`.
- Add `TestOpenAICompatExecuteMapsThinkFalseForLemonadeTopLevel`.
- Add `TestOpenAICompatExecuteOmitsThinkControlWhenRequestOmitsIt`.
- Add/extend tests for `reasoning_effort=none`, `think=true`, and explicit unsupported budget/effort behavior.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters/openai_compat
```

Expected: PASS.

### [API-2] Default Compatibility Regression

#### 문제

SDD S04 requires omitted think fields to preserve existing provider default thinking behavior. Current body tests do not assert that omitted requests avoid `think` and `chat_template_kwargs`.

#### 해결 방법

- In the omitted-field test, decode the provider request body and assert absence of:
  - `think`
  - `reasoning_effort`
  - `thinking_token_budget`
  - `chat_template_kwargs`
- Keep existing options/tool tests passing.

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

- [ ] `apps/node/internal/adapters/openai_compat/openai_compat_test.go`: absence assertions.

#### 테스트 작성

- Covered by `TestOpenAICompatExecuteOmitsThinkControlWhenRequestOmitsIt`.

#### 중간 검증

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

Expected: PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/openai_compat/openai_compat.go` | API-1 |
| `apps/node/internal/adapters/openai_compat/openai_compat_test.go` | API-1, API-2 |

## 최종 검증

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

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

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