<!-- task=m-model-group-mixed-provider-dispatch/01-provider-path-classifier plan=0 tag=MIXED_CLASSIFIER -->

# PLAN-local-G06: Provider Path Classifier

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 마지막 필수 단계다. 검증을 실행하고, active plan/review 파일을 유지한 채 리뷰 준비 상태를 보고한다. 종결 처리, log rename, `complete.log`, archive 이동은 code-review 에이전트 전용이다.

선택된 Milestone의 `구현 잠금 > 결정 필요` 항목이 실제 구현을 막는 경우에만 대응 review stub의 `사용자 리뷰 요청` 섹션에 연결 근거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/외부 서비스 준비, 일반 scope 조정, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 follow-up plan으로 남긴다.

## 배경

현재 provider-pool 후보에는 provider별 실행 방식이 보존되지 않아 OpenAI handler가 catalog match만 보고 tunnel 여부를 먼저 결정한다. SDD는 OpenAI-compatible 계열은 passthrough, Ollama/CLI/native 계열은 normalized로 분류한 뒤 선택된 provider에 맞춰 실행하라고 요구한다. `vllm-mlx` alias와 provider label 정규화는 작은 직접 수정으로 이미 반영되었으므로, 이 계획은 service 후보까지 classifier metadata를 보존하고 검증하는 남은 큰 단위를 다룬다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 Milestone lock 결정이 구현을 차단할 때만 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/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- Task ids:
  - `provider-path-classifier`: Provider classifier가 OpenAI-compatible 호출 방식을 지원하는 모든 provider를 passthrough 방식으로, Ollama/CLI/native provider를 normalized로 분류한다. Seulgivibe aliases는 passthrough provider에 포함한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/analyze-roadmap-position/SKILL.md`
- `agent-ops/skills/common/_templates/roadmap-position-report-template.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md`
- `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/mapper_test.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/model_queue_test.go`
- `apps/edge/internal/service/run_dispatch_internal_test.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/config_set_test.go`
- `apps/node/internal/adapters/factory_internal_test.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md`
- 상태: `[승인됨]`, SDD lock 해제
- 대상 Acceptance Scenario: S03, S04
- Milestone Task id: `provider-path-classifier`
- Evidence Map:
  - S03: OpenAI-compatible alias classifier 테스트. `go test ./packages/go/config -count=1`, `go test ./apps/edge/internal/node -count=1`, `go test ./apps/node/internal/adapters -count=1`.
  - S04: Ollama/CLI/native provider가 normalized 후보로만 분류되는 service/provider classifier 테스트.
- 이 계획은 config/edge-node mapper alias 보강을 유지하고, service 후보 `candidateNode`에 실행 path metadata를 추가해 다음 selection-first plan이 선택 결과만 보고 path를 결정할 수 있게 한다.

### 테스트 환경 규칙

- 선택 test_env: `local`
- `agent-test/local/rules.md`와 `edge-smoke`, `node-smoke`, `platform-common-smoke` 프로필을 읽었다.
- 기본 Go build cache가 `/config/.cache/go-build` permission 오류를 냈으므로 현재 checkout에서는 `GOCACHE=/tmp/iop-go-cache`를 붙여 실행한다. 최종 검증은 cached output으로 대체하지 않는다.
- 적용 명령:
  - `GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1`
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1`
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1`
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`

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

- 이미 반영된 작은 수정은 `TestNormalizeProviderTypeOpenAICompatibleAliases`와 `TestBuildConfigPayload_ProviderFirstOpenAICompatDefaultsProviderFromType`가 alias/label을 검증한다.
- service 후보에는 아직 OpenAI-compatible vs normalized 실행 path assertion이 없다.
- node adapter registry는 provider type을 보존하지만, provider classifier 자체는 Edge service 후보에 있어야 하므로 adapter package는 회귀 smoke로만 둔다.

### 심볼 참조

- 기존 symbol rename/remove 없음.
- 새 symbol을 만들 경우 unexported classifier type/function으로 제한하고, call site는 `resolveProviderPoolCandidates`와 관련 service tests로 한정한다.

### 분할 판단

- split policy를 평가했고 multi-plan을 선택했다.
- 공유 task group: `agent-task/m-model-group-mixed-provider-dispatch/`
- sibling subtask:
  - `01-provider-path-classifier`: classifier metadata foundation. 선행 의존성 없음.
  - `02-selection-first-provider-dispatch`: 이 plan의 PASS 후 실행해야 한다. 현재 active plan/review가 있고 `complete.log`는 아직 없다.
  - `03-mixed-model-group-openai`: 02 PASS 후 실행해야 한다.

### 범위 결정 근거

- `model-group-surface`, `contract-spec` milestone tasks는 SDD S06-S10 범위라 제외한다.
- OpenAI handler의 selection-first 통합은 02/03 plan으로 넘긴다. 이 plan은 classifier metadata와 테스트 증거까지만 다룬다.
- `agent-task/archive/**`, `agent-roadmap/archive/**`, `agent-spec/archive/**`는 읽지 않는다.

### 빌드 등급

- `local-G06`: config, edge node mapper, service 후보 metadata, node adapter smoke를 함께 보지만 API shape 변경 없이 bounded Go unit test로 검증 가능하다.

## 구현 체크리스트

- [ ] provider 실행 path classifier를 service 후보 생성 경로에 추가하고 OpenAI-compatible aliases는 passthrough, Ollama/CLI/native는 normalized로 분류한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`.
- [ ] 기존 직접 수정인 `vllm-mlx`/Seulgivibe alias 정규화가 유지되는지 config, edge node mapper, node adapter smoke로 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1`, `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1`, `GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1`.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [MIXED_CLASSIFIER-1] Service Candidate Execution Path

#### 문제

`candidateNode`는 provider-pool 후보의 `providerID`, `adapter`, `servedTarget`만 들고 있으며 실행 path를 보존하지 않는다 ([apps/edge/internal/service/model_queue.go](/config/workspace/iop/apps/edge/internal/service/model_queue.go:28)). `resolveProviderPoolCandidates`도 provider type을 보고 후보를 만들지만 path 분류를 candidate에 싣지 않는다 ([apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:562)).

#### 해결 방법

service package에 unexported execution path type과 classifier helper를 둔다. classifier는 `config.NormalizeProviderType`를 사용해 OpenAI-compatible aliases를 tunnel/passthrough로, `ollama`와 `cli` 및 unknown/native를 normalized로 분류한다.

Before:

```go
type candidateNode struct {
    providerID   string
    adapter      string
    servedTarget string
}
```

After:

```go
type providerExecutionPath string

const (
    providerExecutionPathTunnel     providerExecutionPath = "provider_tunnel"
    providerExecutionPathNormalized providerExecutionPath = "normalized"
)

type candidateNode struct {
    providerID     string
    adapter        string
    servedTarget   string
    executionPath  providerExecutionPath
}
```

`resolveProviderPoolCandidates`에서 `executionPath: classifyProviderExecutionPath(prov)`를 설정한다.

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

- [ ] `apps/edge/internal/service/model_queue.go`: `candidateNode`에 execution path 필드와 주석 추가.
- [ ] `apps/edge/internal/service/run_dispatch.go`: classifier helper 추가 및 후보 생성 시 설정.
- [ ] `apps/edge/internal/service/model_queue_test.go` 또는 `run_dispatch_internal_test.go`: OpenAI-compatible aliases와 Ollama/CLI/native 분류 assertion 추가.

#### 테스트 작성

- 작성: `TestResolveProviderPoolCandidatesClassifiesExecutionPath`
- 목표: `openai_compat`, `openai_api`, `vllm`, `vllm-mlx`, `lemonade`, `sglang`, `seulgivibe_claude`, `seulgivibe_openai`은 tunnel path, `ollama`, `cli`, unknown/native는 normalized path로 후보에 기록된다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
```

기대 결과: PASS.

### [MIXED_CLASSIFIER-2] Alias Regression Verification

#### 문제

SDD는 `vllm-mlx`와 Seulgivibe aliases를 OpenAI-compatible provider에 포함한다. 직접 수정으로 `NormalizeProviderType`는 `vllm-mlx`를 `openai_compat`로 매핑하고 ([packages/go/config/config.go](/config/workspace/iop/packages/go/config/config.go:27)), mapper label도 lowercase/trim 후 aliases를 반환한다 ([apps/edge/internal/node/mapper.go](/config/workspace/iop/apps/edge/internal/node/mapper.go:239)). 이 상태가 후속 구현 중 깨지지 않도록 명시 검증을 유지해야 한다.

#### 해결 방법

이미 추가된 테스트를 유지하고 필요 시 classifier helper의 alias 목록과 동일한 표본을 맞춘다.

Before:

```go
case "openai_compat", "openai_api", "vllm", "lemonade", "sglang":
    return "openai_compat"
```

After:

```go
case "openai_compat", "openai_api", "vllm", "vllm-mlx", "lemonade", "sglang", "seulgivibe_claude", "seulgivibe_openai":
    return "openai_compat"
```

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

- [ ] `packages/go/config/config.go`: alias 목록 유지.
- [ ] `packages/go/config/config_test.go`: `TestNormalizeProviderTypeOpenAICompatibleAliases` 유지 ([packages/go/config/config_test.go](/config/workspace/iop/packages/go/config/config_test.go:358)).
- [ ] `apps/edge/internal/node/mapper.go`: `openAICompatProviderLabel` 정규화 유지.
- [ ] `apps/edge/internal/node/mapper_test.go`: provider-first label cases 유지 ([apps/edge/internal/node/mapper_test.go](/config/workspace/iop/apps/edge/internal/node/mapper_test.go:586)).
- [ ] `apps/node/internal/adapters/*`: 직접 변경하지 않되 package smoke를 실행한다.

#### 테스트 작성

- 추가 테스트가 필요한 경우 service classifier test와 중복되지 않는 config/mapper alias case만 추가한다.
- node adapters는 BuildFromPayload/registry behavior 회귀 검증만 실행한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1
```

기대 결과: 모두 PASS.

## 수정 파일 요약

| 파일 | 항목 |
| --- | --- |
| `apps/edge/internal/service/model_queue.go` | MIXED_CLASSIFIER-1 |
| `apps/edge/internal/service/run_dispatch.go` | MIXED_CLASSIFIER-1 |
| `apps/edge/internal/service/model_queue_test.go` | MIXED_CLASSIFIER-1 |
| `apps/edge/internal/service/run_dispatch_internal_test.go` | MIXED_CLASSIFIER-1 |
| `packages/go/config/config.go` | MIXED_CLASSIFIER-2 |
| `packages/go/config/config_test.go` | MIXED_CLASSIFIER-2 |
| `apps/edge/internal/node/mapper.go` | MIXED_CLASSIFIER-2 |
| `apps/edge/internal/node/mapper_test.go` | MIXED_CLASSIFIER-2 |
| `apps/node/internal/adapters/config_set.go` | MIXED_CLASSIFIER-2 검증 |
| `apps/node/internal/adapters/factory.go` | MIXED_CLASSIFIER-2 검증 |
| `apps/node/internal/adapters/registry.go` | MIXED_CLASSIFIER-2 검증 |

## 최종 검증

```bash
GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
```

기대 결과: 모든 명령 PASS.

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