<!-- task=m-model-group-long-context-admission/01_input_estimator plan=0 tag=INPUT_ESTIMATOR -->

# Plan - INPUT_ESTIMATOR

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 구현의 필수 마지막 단계다. 코드를 바꾸고 검증을 실행한 뒤 실제 변경 내용, 계획 대비 변경 사항, 검증 stdout/stderr를 review stub에 채우고 active 파일을 그대로 둔 채 review 준비 상태로 보고한다. 종결 처리, log rename, `complete.log`, archive 이동은 code-review skill 전용이다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록하고 멈춘다. 구현 중 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`/`complete.log`를 만들지 않는다.

## 배경

Milestone은 long-context admission을 입력 토큰 추정치만 기준으로 분류하도록 고정했다. 현재 OpenAI-compatible chat/responses handler는 service 요청을 만들지만 입력 크기를 추정하거나 `normal`/`long` flag를 넘기지 않는다. 이 작업은 queue admission 전에 `estimated_input_tokens`와 `context_class`를 붙이는 foundation이다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에만 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 템플릿을 따른다. 직접 사용자 프롬프트는 금지되며, code-review가 요청의 타당성 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/model-group-long-context-admission.md`
- Task ids:
  - `input-estimator`: OpenAI-compatible chat/responses 요청의 입력 기준 토큰 근사 추정과 long flag 부여
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/model-group-long-context-admission.md`
- `agent-roadmap/sdd/operational-observability-provider-management/model-group-long-context-admission/SDD.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/domain/platform-common.md`
- `agent-ops/rules/domain/edge.md`
- `agent-ops/rules/domain/testing.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-contract/index.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/outer/openai-compatible-api.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `configs/edge.yaml`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/server_test.go` (metadata/SubmitRun 관련 구간 확인)
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/service_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/model-group-long-context-admission/SDD.md`, 상태 `[승인됨]`.
- 대상 Acceptance: S03 / Milestone Task `input-estimator`.
- Evidence Map: estimator unit test와 request classification log가 필요하다.
- 적용: 구현 체크리스트는 chat/responses 입력 추정, metadata/typed request propagation, unit/handler test, log/metadata evidence 순서로 구성한다.

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` 읽음. Edge 변경은 `agent-test/local/edge-smoke.md`의 Go unit/config check 흐름을 따른다.
- `agent-test/local/platform-common-smoke.md`도 config 패키지 변경 가능성 때문에 확인했다.
- 최종 검증은 fresh 실행이 필요하므로 `go test -count=1`을 사용한다. config check는 `go run ./apps/edge/cmd/edge config check --config configs/edge.yaml`을 사용한다.

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

- chat 입력 추정: 기존 handler tests는 metadata contract는 검증하지만 `estimated_input_tokens`/`context_class`는 검증하지 않는다. 새 테스트 필요.
- responses 입력 추정: 기존 responses metadata contract는 있으나 long 분류가 없다. 새 테스트 필요.
- threshold live config는 선행 직접 작업에서 config tests와 configrefresh tests가 추가되었다. 이 plan에서는 OpenAI server가 해당 값을 쓰는지만 검증한다.

### 심볼 참조

- renamed/removed symbol: none.
- 새 field 후보: `edgeservice.SubmitRunRequest.EstimatedInputTokens`, `edgeservice.SubmitRunRequest.ContextClass`. call site는 `apps/edge/internal/openai/chat_handler.go`, `apps/edge/internal/openai/responses_handler.go`, service tests의 direct literals다.

### 분할 판단

- split decision policy 적용 완료. 공유 task group은 `agent-task/m-model-group-long-context-admission`.
- sibling plans:
  - `01_input_estimator`: 선행 foundation, 의존성 없음.
  - `02+01_long_capacity`: 01의 context class가 필요하다.
  - `03+02_queue_skip`: long capacity admission 후 queue scan을 바꾼다.
  - `04+01,02_status_logs`: estimator와 long slot state가 필요하다.
  - `05+02_dev_runtime_policy`: provider config field가 필요하다.
  - `06+01,02,03,04,05_capacity_smoke`: 모든 기능과 dev policy가 필요하다.

### 범위 결정 근거

- tokenizer별 정확 계산기는 범위 제외다. SDD가 "보수적 근사 추정"을 허용한다.
- output reserve, hidden thinking, max_tokens는 long 사전 분류 기준에서 제외한다.
- queue admission, provider long slot, status proto는 후속 sibling plan 범위다.

### 빌드 등급

- `local-G06`: OpenAI handler와 service request 타입에 걸친 bounded foundation이며 local unit/config check로 검증 가능하다.

## 구현 체크리스트

- [ ] OpenAI server가 Edge root `long_context_threshold_tokens`를 보관하고 runtime config refresh 시 갱신되게 한다.
- [ ] chat/responses 입력에서 system/developer/user messages, tool schema, metadata payload를 입력 기준으로 근사 추정하고 `estimated_input_tokens`, `context_class`를 service request와 metadata에 붙인다. 검증: 작은 요청은 normal, 100k 이상 요청은 long이다.
- [ ] estimator와 handler propagation 테스트를 추가한다.
- [ ] `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./packages/go/config` 및 config check를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [INPUT_ESTIMATOR-1] OpenAI 입력 추정과 분류 propagation

### 문제

- [apps/edge/internal/openai/chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:77)는 `chatRunMetadata` 후 `chatSubmitRunRequest`를 호출하지만 입력 토큰 추정값이 없다.
- [apps/edge/internal/openai/responses_handler.go](/config/workspace/iop/apps/edge/internal/openai/responses_handler.go:96)는 `SubmitRunRequest`를 직접 만들며 threshold를 보지 않는다.
- [apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:18)의 `SubmitRunRequest`에는 admission에서 쓸 typed context 정보가 없다.

### 해결 방법

- `openai.Server`에 `longContextThresholdTokens int`를 추가하고 기본값 100000을 적용한다. bootstrap과 config refresh 경로에서 `EdgeConfig.LongContextThresholdTokens`를 setter로 갱신한다.
- package-local estimator helper를 추가한다. JSON 직렬화 가능한 입력 요소(messages/input/tools/tool_choice/metadata)를 문자열 길이 기준으로 근사 계산한다. 초기 공식은 보수적으로 `ceil(len(runes)/4) + 구조 오버헤드` 형태로 둔다.
- chat/responses handler가 `SubmitRunRequest.EstimatedInputTokens`, `SubmitRunRequest.ContextClass`를 채우고 metadata에도 문자열로 `estimated_input_tokens`, `context_class`를 넣는다.

예상 전후:

```go
// apps/edge/internal/openai/chat_handler.go:77
metadata := chatRunMetadata(runMeta, req, outputPolicy)
submitReq := chatSubmitRunRequest(dispatch, req, workspace, prompt, input, metadata)
```

```go
estimate := s.estimateChatInputTokens(req, runMeta)
contextClass := classifyContext(estimate, s.longContextThreshold())
metadata := chatRunMetadata(runMeta, req, outputPolicy)
metadata["estimated_input_tokens"] = strconv.Itoa(estimate)
metadata["context_class"] = contextClass
submitReq := chatSubmitRunRequest(dispatch, req, workspace, prompt, input, metadata)
submitReq.EstimatedInputTokens = estimate
submitReq.ContextClass = contextClass
```

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

- [ ] `apps/edge/internal/openai/server.go`: threshold field, default, setter/snapshot.
- [ ] `apps/edge/internal/openai/chat_handler.go`: chat estimate/class propagation.
- [ ] `apps/edge/internal/openai/responses_handler.go`: responses estimate/class propagation.
- [ ] `apps/edge/internal/openai/input_estimator.go` 또는 기존 적절한 helper 파일: estimator 구현.
- [ ] `apps/edge/internal/service/run_dispatch.go`: typed fields 추가, metadata copy 기존 동작 보존.
- [ ] `apps/edge/internal/bootstrap/runtime.go`, `apps/edge/internal/bootstrap/refresh_admin.go`: OpenAI server threshold refresh 연결.

### 테스트 작성

- [ ] `apps/edge/internal/openai/input_estimator_test.go`: 작은 chat/responses 요청은 threshold 미만, 100k 이상 text payload는 long.
- [ ] `apps/edge/internal/openai/server_test.go`: chat/responses handler가 fake service request와 metadata에 `estimated_input_tokens`, `context_class`를 전달한다.
- [ ] `apps/edge/internal/service/service_test.go`: `BuildRunRequest`가 새 metadata를 보존하고 typed fields 추가가 기존 dispatch metadata를 깨지 않는다.

### 중간 검증

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

기대 결과: PASS. 작은 요청은 `context_class=normal`, 큰 요청은 `context_class=long` assertion이 포함되어야 한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/server.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/openai/chat_handler.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/openai/responses_handler.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/openai/input_estimator.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/openai/input_estimator_test.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/openai/server_test.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/service/run_dispatch.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/service/service_test.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/bootstrap/runtime.go` | INPUT_ESTIMATOR-1 |
| `apps/edge/internal/bootstrap/refresh_admin.go` | INPUT_ESTIMATOR-1 |

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./packages/go/config
go run ./apps/edge/cmd/edge config check --config configs/edge.yaml
```

기대 결과: 모든 명령 PASS, config check는 `OK configs/edge.yaml`.

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