# SDD: Model Alias Provider Pool과 Provider Catalog

## 위치

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Phase: `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
  - [x] [D01] Edge config는 `models[]`를 model catalog이자 외부 model key 목록으로 두고, 각 model의 `providers`에 provider id와 실제 served model name을 매핑한다. provider 정의는 `nodes[].providers[]` 아래에 두며, 각 provider는 제공 가능한 `models[]` list를 가진다.
  - [x] [D02] MVP provider 선택은 available provider 중 `in_flight / capacity` load ratio가 가장 낮은 후보를 우선한다. priority/weight/fallback 정책은 후속 확장으로 둔다.
  - [x] [D03] provider별 실제 served target rewrite와 routing/control 책임은 Edge가 가진다. Node/provider adapter는 Edge가 전달한 concrete target을 실행한다.
  - [x] [D04] MVP provider category는 `api`, `cli`, `local_inference`로 시작하고, 상태 추적은 `health`, `capacity`, `in_flight`, derived `load_ratio`, `models`, `queued`까지 포함한다. runtime process와 resource telemetry는 후속으로 둔다.
  - [x] [D05] MVP catalog에는 provider별 lifecycle capability를 coarse flags로만 표시하고, 상세 lifecycle 동작, compatibility, benchmark, quality, model별 qualification 결과는 후속 Qualification Report Milestone으로 둔다.
  - [x] [D06] MVP는 읽기 전용 관찰과 Edge config 기반 routing 계약까지만 포함한다. enable/disable, drain, fallback 우선순위 변경, capacity override 같은 runtime 제어는 후속 Milestone으로 둔다.

## 문제 / 비목표

- 문제: 현재 OpenAI-compatible `model` route는 단일 `adapter + target`으로 해석되고, Edge queue도 같은 resolved target 기준으로 후보를 필터링한다. `qwen3.6:35b` 같은 논리 model alias 아래에 vLLM, Lemonade, Ollama, SGLang 같은 여러 provider candidate를 묶으려면 alias를 canonical routing key로 유지하는 계약, candidate schema, selection/queue 정책, served target rewrite 책임, catalog 표시 기준을 먼저 고정해야 한다.
- 비목표:
  - vLLM, vLLM-MLX, Lemonade, SGLang provider adapter 구현 자체
  - provider pool scheduler 코드 구현과 config migration 실행
  - provider/device/model qualification report 저장/조회/비교
  - 자동 benchmark, 품질 평가, provider marketplace, cross-Edge provider balancing
  - cloud fallback과 품질 평가 feedback 구현

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md` | Milestone 목표, 범위, 기능 Task, 잠금 해제 기준 |
| Code | `packages/go/config/config.go` | 현재 `openai.model_routes[]`, adapter instance config, capacity/queue config 구조 |
| Code | `apps/edge/internal/openai/routes.go` | `/v1/models`가 `model_routes`를 catalog처럼 노출하는 현재 외부 model 표면 |
| Code | `apps/edge/internal/service/run_dispatch.go` | `ModelGroupKey` 기반 queue, candidate filtering, capacity policy의 현재 dispatch 기준 |
| Code | `proto/iop/runtime.proto` | `ProviderSnapshot`의 provider 상태, capacity, in-flight, queued 기준 |
| External Provider | 없음 | 이번 Milestone은 외부 provider 상태를 변경하지 않고 IOP 내부 계약과 catalog 기준을 고정한다. |
| User Decision | `user_review_0.log` | D01-D06 resolved |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| Model Accepted | OpenAI-compatible `model` 또는 내부 실행 요청이 `models[].id`에 정의된 model id를 포함한다. | Candidate Resolving | `agent-contract/provided/openai-compatible-api.md`, `SubmitRunRequest.ModelGroupKey` |
| Candidate Resolving | Edge가 `models[].providers`에서 provider id와 실제 served model name 매핑을 읽는다. | Candidate Ready 또는 No Candidate | `packages/go/config/config.go`, `apps/edge/internal/service/run_dispatch.go` |
| Provider Matched | `models[].providers`의 provider id가 `nodes[].providers[].id`와 매칭되고, served model name이 해당 provider의 `models[]`에 존재한다. | Candidate Ready 또는 No Candidate | Edge config validation |
| Candidate Ready | candidate가 enabled이고 health/capacity/in-flight 기준을 만족한다. | Load Ranked | provider snapshot, adapter instance capacity, queue policy |
| Load Ranked | Edge가 available candidate의 `in_flight / capacity` load ratio를 계산한다. | Target Rewritten 또는 Queued | 낮은 load ratio 우선, 동률 시 deterministic tie-break |
| Target Rewritten | Edge가 선택된 provider id의 served model name을 `models[].providers[provider_id]`에서 읽는다. | Dispatched | Edge-owned routing/control 책임 |
| Queued | 즉시 사용 가능한 slot이 없고 queue policy가 대기를 허용한다. | Dispatched 또는 Timed Out | `ModelGroupKey` queue, `max_queue`, `queue_timeout_ms` |
| Dispatched | Edge가 선택한 node/provider와 concrete served target을 Node에 전달한다. | Completed 또는 Failed | Edge-Node run request, run event |
| Catalog Observed | Node/Edge가 provider snapshot 또는 catalog 상태를 갱신한다. | Available, Degraded, Unavailable | `health`, `capacity`, `in_flight`, `load_ratio`, `models`, `queued` |

## Interface Contract

- 계약 원문: `agent-contract/provided/openai-compatible-api.md`
- 입력:
  - `models[].id`: OpenAI-compatible API의 `model`로 호출 가능한 model id이며, `qwen3.6:35b` 같은 형식을 유지한다.
  - `models[].display_name`: 운영 화면에서 보여줄 model 이름이다.
  - `models[].providers`: provider id를 key로, 해당 provider에서 실제 호출할 served model name을 value로 둔 map이다.
  - `models[].providers.<provider_id>`: `nodes[].providers[].id`와 매칭되는 provider id다.
  - `models[].providers[<provider_id>]`: provider가 실제로 제공하는 model id 또는 target profile이며, 해당 provider의 `models[]` list 안에 있어야 한다.
  - `nodes[].id`: provider를 소유하는 Node identity다.
  - `nodes[].providers[]`: provider 정의는 Node 아래에 둔다. 같은 Node 아래에 여러 provider가 있을 수 있다.
  - `nodes[].providers[].id`: `models[].providers`의 key로 참조되는 provider id다.
  - `nodes[].providers[].type`: provider runtime type이다. 예: `ollama`, `vllm`, `lemonade`, `sglang`, `openai_api`, `cli`.
  - `nodes[].providers[].category`: MVP provider category다. 값은 `api`, `cli`, `local_inference`로 시작한다.
  - `nodes[].providers[].models`: 해당 provider가 실제로 제공 가능한 served model name list다.
  - `provider.health`: `available`, `unavailable`, `unknown` 중 하나의 최소 provider health 상태다.
  - `provider.capacity`: provider가 동시에 처리할 수 있는 실행 slot 수다.
  - `provider.in_flight`: provider에서 현재 실행 중인 요청 수다.
  - `provider.load_ratio`: `in_flight / capacity`로 계산하는 선택 기준이다. capacity가 0이거나 알 수 없으면 선택 대상에서 제외하거나 unavailable로 본다.
  - `provider.queued`: Edge queue에서 관측 가능한 provider 또는 model group 대기 요청 수다.
  - `provider.lifecycle_capabilities`: provider별 model lifecycle capability를 coarse flags로 표시한다. 예: `list_models`, `load_model`, `unload_model`, `pull_model`, `delete_model`.
  - `catalog.status`: MVP에서는 health, capacity, in-flight, derived load ratio, model list, queued, coarse lifecycle capability를 표시한다. runtime process/container detail과 CPU/GPU/RAM/VRAM resource telemetry는 후속으로 둔다.
  - `catalog.control`: MVP에서는 읽기 전용 관찰과 Edge config 기반 routing 계약만 포함한다. runtime control은 후속 Milestone 책임이다.
- 출력:
  - `resolution.adapter`: Node에 전달할 adapter type 또는 adapter instance key다.
  - `resolution.target`: Node adapter가 실행할 concrete served target이다.
  - `resolution.node_id`: 선택된 Node identity다.
  - `resolution.provider_id`: Edge가 선택한 provider identity다.
  - `resolution.model_group_key`: queue와 관측에 남길 `models[].id`다.
  - `catalog.providers[]`: 운영자가 provider pool과 provider 상태를 비교할 수 있는 표시 record다.
- 금지:
  - OpenAI-compatible 외부 경계에서 `model`을 제거하거나 caller에게 `adapter + target`을 직접 요구하지 않는다.
  - `models[]`에 정의되지 않은 model id를 새 provider-pool 경로에서 암묵적으로 실행하지 않는다.
  - 단일 provider model이라도 별도 축약 구조를 만들지 않고 `models[].providers`에 provider 한 개만 둔다.
  - `models[].providers`에서 참조한 provider id가 `nodes[].providers[].id`에 없거나, served model name이 해당 provider의 `models[]`에 없으면 유효한 route로 보지 않는다.
  - capacity가 남았다는 이유만으로 더 높은 load ratio의 provider를 우선 선택하지 않는다.
  - MVP selection policy에 priority, weight, fallback policy를 필수 필드로 넣지 않는다.
  - Node/provider adapter가 catalog model id나 alias를 자체적으로 provider별 served model name으로 rewrite하지 않는다.
  - MVP catalog/status에 runtime process/container detail이나 CPU/GPU/RAM/VRAM resource telemetry를 필수 필드로 넣지 않는다.
  - MVP catalog에 lifecycle API 상세 동작, compatibility, benchmark, quality score, model별 qualification 결과를 직접 넣지 않는다.
  - MVP catalog/control 표면에 provider enable/disable, drain, fallback 우선순위 변경, capacity override 같은 runtime 제어를 넣지 않는다.
  - Control Plane을 Edge-owned provider 상태의 canonical store로 만들지 않는다.
  - SDD 본문에 agent-contract 원문을 복제하지 않는다.
  - qualification report, benchmark, 품질 비교를 이번 Milestone의 필수 구현으로 끌어오지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `alias-contract` | 운영자가 `models[]`에 `qwen3.6:35b` 같은 model id를 정의한다. | OpenAI-compatible 요청 또는 내부 실행 요청이 해당 model id를 사용한다. | model id는 queue와 catalog의 canonical key로 유지되고, dispatch 직전에만 provider별 served model name으로 해석된다. |
| S02 | `candidate-schema` | 하나의 `models[]` 항목에 여러 provider id와 served model name 매핑이 있다. | provider pool schema를 검토한다. | `models[].providers`가 provider id -> 실제 served model name map으로 정리되어 있고, provider 정의는 Node 아래 provider list로 분리되며, provider별 `models[]` list가 제공 가능 모델의 기준이 된다. |
| S03 | `target-rewrite` | provider마다 실제 served model id가 다르다. | Edge가 candidate를 선택한다. | Edge가 `models[].providers[provider_id]` 값을 concrete target으로 rewrite해 Node에 전달하고, Node/provider adapter는 전달받은 target만 실행한다. |
| S04 | `selection-policy` | 여러 candidate가 서로 다른 health, capacity, in-flight 값을 가진다. | Edge가 실행 후보를 고른다. | unavailable 후보를 제외하고 `in_flight / capacity` load ratio가 가장 낮은 provider를 우선 선택한다. 예: 1/3 provider가 2/3 provider보다 우선한다. |
| S05 | `pool-compat` | 기존 `openai.model_routes`, `adapter + target`, Node provider instance 설정이 존재한다. | 새 pool 계약을 도입하거나 compat layer를 설계한다. | 기존 route와 provider instance 설정의 호환/마이그레이션 방향이 정리되어 있다. |
| S06 | `category-map` | API, CLI, local inference provider가 같은 catalog에 노출된다. | catalog 표시 필드를 검토한다. | MVP category는 `api`, `cli`, `local_inference`로 정의되어 있다. |
| S07 | `device-status` | local inference provider가 health, capacity, in-flight, model list, queue 상태 일부를 제공한다. | 운영자가 상태를 조회한다. | MVP 상태 필드는 `health`, `capacity`, `in_flight`, derived `load_ratio`, `models`, `queued`이며 runtime/resource telemetry는 후속으로 분리되어 있다. |
| S08 | `lifecycle-cap` | Ollama/Lemonade와 vLLM/SGLang의 model lifecycle 동작이 다르다. | catalog 또는 후속 report 입력을 설계한다. | MVP catalog에는 coarse lifecycle capability flags만 표시하고 상세 lifecycle/compatibility/benchmark/quality/qualification 결과는 후속 Milestone으로 분리되어 있다. |
| S09 | `boundary` | adapter 구현, pool routing, 운영 catalog가 서로 다른 책임을 가진다. | 구현 계획을 작성한다. | provider adapter 구현, provider pool routing, read-only operations catalog, 후속 runtime control의 책임 경계가 분리되어 있다. |
| S10 | `ops-review` | SDD와 사용자 결정 항목이 준비되어 있다. | 사용자가 MVP 범위와 2차 후보를 검토한다. | D01-D06 결정이 `user_review_0.log`와 SDD에 반영되고 SDD 잠금이 해제되어 있다. |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | `Spec Completion` 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | pool 계약 문서 또는 config/proto/API diff, OpenAI-compatible model id test | `agent-task/m-provider-catalog-device-status/...` | `alias-contract`가 `models[].id` canonical key와 dispatch resolution 기준을 충족했다는 근거 |
| S02 | candidate schema diff 또는 SDD/plan 확정 표, config validation test | `agent-task/m-provider-catalog-device-status/...` | `candidate-schema`가 `models[].providers` map, Node 하위 provider 정의, provider별 `models[]` list 검증 기준을 충족했다는 근거 |
| S03 | target rewrite 책임 문서, Edge/Node request mapping test | `agent-task/m-provider-catalog-device-status/...` | `target-rewrite`가 Edge-owned routing/control 원칙을 따르고 concrete target 전달을 검증했다는 근거 |
| S04 | queue/selection policy test 또는 smoke, timeout/overflow case evidence | `agent-task/m-provider-catalog-device-status/...` | `selection-policy`가 available provider 중 낮은 `in_flight / capacity` load ratio를 우선하고, 동률 tie-break와 queue timeout을 검증했다는 근거 |
| S05 | migration/compat note, 기존 `model_routes` regression test | `agent-task/m-provider-catalog-device-status/...` | `pool-compat`가 기존 OpenAI route와 Node provider instance 설정을 깨지 않는다는 근거 |
| S06 | catalog DTO/API/UI/CLI 표시 evidence | `agent-task/m-provider-catalog-device-status/...` | `category-map`이 `api`, `cli`, `local_inference` category를 확인했다는 근거 |
| S07 | provider snapshot/status test, unknown/unavailable case evidence | `agent-task/m-provider-catalog-device-status/...` | `device-status`가 health/capacity/in-flight/load-ratio/models/queued 범위를 검증하고 runtime/resource telemetry를 후속으로 분리했다는 근거 |
| S08 | lifecycle capability 표 또는 follow-up handoff evidence | `agent-task/m-provider-catalog-device-status/...` | `lifecycle-cap`이 coarse flags로 catalog에 표시되고 상세 qualification 항목은 후속 Milestone으로 분리되었다는 근거 |
| S09 | architecture boundary note, package/API ownership evidence | `agent-task/m-provider-catalog-device-status/...` | `boundary`가 adapter, routing, read-only catalog, 후속 runtime control 책임을 분리했다는 근거 |
| S10 | `user_review_0.log` 해결 log 또는 사용자 승인 기록 | `agent-task/m-provider-catalog-device-status/...` | `ops-review`가 완료되어 SDD 잠금 해제 상태를 확인했다는 근거 |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 plan/code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 열린 사용자 리뷰가 없고 해결된 리뷰는 `user_review_0.log`에 남겼다.

## 사용자 리뷰 이력

- `user_review_0.log`: D01-D06 결정 반영, SDD 잠금 해제

## 작업 컨텍스트

- 표준선: 외부 OpenAI-compatible 경계는 `model`을 유지하고, 내부 실행은 `adapter + target`으로 resolve한다. Edge는 queue와 dispatch를 담당하며, Edge-owned 상태의 canonical store는 Control Plane이 아니라 Edge/Node 쪽에 둔다.
- 후속 SDD: 없음. Provider-Device-Model Qualification 리포트 Milestone에서 lifecycle capability, benchmark, 품질 비교 저장/조회가 구현 범위가 되면 별도 SDD 필요 여부를 다시 판정한다.
