# SDD: Node Provider-First Config Surface

## 위치

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-provider-first-config-surface.md`
- Phase: `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
  - 없음

## 문제 / 비목표

- 문제: 현재 Edge config는 실행 driver 정보가 `nodes[].adapters`에, 운영 resource/catalog 정보가 `nodes[].providers[]`에, 외부 model alias mapping이 `models[]`에 나뉘어 있어 한 Node의 가용 resource를 이해하려면 여러 섹션을 왕복해야 한다. 이 SDD는 `nodes[].providers[]`를 사용자-facing source of truth로 고정하고, 내부 adapter registry는 provider config에서 컴파일되는 실행 IR로 낮추는 기준을 정한다.
- 비목표:
  - Node 내부 adapter abstraction을 즉시 제거하지 않는다.
  - Control Plane을 Edge-local config/provider registry의 canonical store로 만들지 않는다.
  - provider/device/model qualification report와 lifecycle policy는 이번 Milestone에서 구현하지 않는다.
  - billing, org IAM, 장기 audit retention, 품질 평가 기반 routing은 다루지 않는다.
  - 새 public endpoint를 추가하지 않는다.

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/operational-observability-provider-management/milestones/node-provider-first-config-surface.md` | 목표, 기능 Task, 구현 잠금, 완료 판단 기준 |
| Contract | `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-contract/inner/edge-node-runtime-wire.md` | config schema, provider/resource source of truth, Edge-to-Node payload 의미 기준 |
| Code | `packages/go/config`, `apps/edge/internal/edgevalidate`, `apps/edge/internal/node/mapper.go`, `apps/edge/internal/service`, `apps/node/internal/adapters`, `apps/node/internal/router`, `proto/iop/runtime.proto` | config load/validation, internal adapter compile, dispatch/status/runtime 실행 기준 |
| External Provider | Ollama, OpenAI-compatible providers, vLLM/MLX, Lemonade/SGLang-compatible endpoints, CLI tools | provider `type`별 실행 필드와 smoke 기준 |
| User Decision | 2026-06-29, 2026-06-30 대화 결정 | 사용자-facing Node config는 `providers[]` resource list 중심으로 정리하고, provider-pool dispatch priority는 같은 `in_flight` 후보의 tie-breaker로 둔다 |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| `provider-config-loaded` | `nodes[].providers[]`에 provider/resource가 선언된다 | `provider-config-validated` 또는 `config-error` | Edge config load/validate |
| `provider-config-validated` | provider id, type, model alias/served mapping, capacity/queue, type별 실행 필드가 유효하다 | `internal-adapter-compiled` | config normalization |
| `internal-adapter-compiled` | provider-first config가 `AdapterConfig`/CLI profile/internal registry item으로 변환된다 | `node-config-sent` 또는 `config-error` | Edge node mapper/config refresh |
| `node-config-sent` | Node가 provider-derived payload를 받는다 | `node-runtime-ready` 또는 `node-config-error` | Edge-Node register/refresh |
| `node-runtime-ready` | Node adapter registry가 provider-derived adapters를 등록한다 | `provider-routed` 또는 `direct-cli-routed` 또는 `status-reported` | Node router/adapter registry |
| `provider-routed` | OpenAI-compatible model alias가 provider resource 후보를 선택한다 | `run-complete` 또는 `run-error` | Edge service queue/dispatch |
| `direct-cli-routed` | CLI provider/resource가 직접 실행 route로 선택된다 | `run-complete` 또는 `run-error` | Edge service/Node CLI adapter |
| `status-reported` | Edge/Control Plane status가 Node provider/resource snapshot을 조회한다 | 없음 | Edge status provider |

## Interface Contract

- 계약 원문: `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-contract/inner/edge-node-runtime-wire.md`
- 입력:
  - `nodes[].providers[].id`: Node 안에서 고유한 provider/resource identity이며 기본 internal adapter instance key다.
  - `nodes[].providers[].type`: 실행 driver 선택자다. MVP 후보는 `openai_compat`, `ollama`, `cli`이며 provider/runtime label은 별도 `provider` 또는 type별 필드로 표현할 수 있다.
  - `nodes[].providers[].models[]`: 외부 alias와 provider served target mapping을 provider resource 안에서 표현한다.
  - `nodes[].providers[].capacity`, `max_queue`, `queue_timeout_ms`, `request_timeout_ms`: provider/resource의 scheduling과 실행 timeout 기준이다.
  - `nodes[].providers[].priority`: provider-pool dispatch tie-breaker다. 기본값은 0이고 음수는 validation error다. dispatch는 `in_flight < capacity`인 후보 중 `in_flight`가 가장 낮은 provider를 먼저 고르며, `in_flight`가 같은 후보에서만 낮은 숫자의 `priority`를 우선한다. `in_flight`와 `priority`가 모두 같으면 기존 순환을 유지한다. `priority` 변경은 routing policy 변경으로 live apply 대상이다.
  - type별 실행 필드: `openai_compat`는 endpoint/headers/provider label, `ollama`는 base_url/context_size, `cli`는 command/args/resume_args/output_format/mode/session 옵션을 가진다.
  - legacy `nodes[].adapters`: compat 입력이다. provider-first config와 충돌하면 validation error 또는 명확한 우선순위 규칙을 적용한다.
- 출력:
  - internal adapter registry: provider-first config에서 컴파일된 adapter instance와 CLI profile set이다.
  - `NodeConfigPayload`: Node가 기존 `adapter + target` runtime으로 실행할 수 있는 provider-derived payload다.
  - provider/resource status snapshot: `providers[]` resource identity, type/category, served models, health, capacity, in-flight, queued 상태다.
- 금지:
  - 사용자-facing 기본 config에서 같은 provider endpoint/capacity/model 정보를 `adapters`와 `providers`에 중복 작성하도록 요구하지 않는다.
  - provider id, adapter instance key, model alias, served model 의미를 섞지 않는다.
  - Node runtime을 global concurrency gate로 되돌리지 않는다.
  - 기존 config를 silent break하지 않는다. legacy/compat 또는 명확한 migration error를 제공한다.
  - `priority` 숫자가 더 낮다는 이유로 더 낮은 `in_flight` provider를 건너뛰지 않는다.
  - dispatch 가능한 provider가 남아 있는데 priority만을 이유로 요청을 queue에 넣지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `schema-source` | 한 Node에 `openai_compat`, `ollama`, `cli` provider resource가 `providers[]`에 선언된다 | config load/validation을 실행한다 | `providers[]`만으로 type별 필드, model alias/served target, capacity/queue가 검증된다 |
| S02 | `normalize-compile` | provider-first config가 유효하다 | Edge가 Node config payload를 만든다 | provider id가 internal adapter instance key 또는 CLI target으로 컴파일되고 Node가 기존 adapter registry로 실행 가능하다 |
| S03 | `legacy-compat` | 기존 `nodes[].adapters` config 또는 provider-first와 legacy adapter가 섞인 config가 있다 | config check를 실행한다 | legacy config는 회귀 없이 통과하거나, 충돌 config는 명확한 validation error를 낸다 |
| S04 | `routing-status-refresh` | OpenAI-compatible 요청이 provider model alias를 사용한다 | Edge dispatch를 실행한다 | provider-first `models[]` mapping에서 후보를 선택하고 provider type별 internal adapter/served target으로 dispatch한다 |
| S05 | `routing-status-refresh` | provider-first config로 연결된 Node status를 조회한다 | Edge/Control Plane status snapshot을 만든다 | status는 `providers[]` resource catalog를 우선하고 adapter duplicate snapshot을 만들지 않는다 |
| S06 | `dev-runtime-docs` | 운영자가 dev-runtime guide와 config 예시를 읽는다 | Mac CLI + MLX provider node를 확인한다 | `providers[]` 한 곳에 CLI resource와 MLX vLLM provider resource가 나열되고 `adapters` 중복 선언을 기본 경로로 요구하지 않는다 |
| S07 | `full-cycle-smoke` | dev-runtime provider-first config가 배포된다 | config check/refresh 또는 restart, `/v1/models`, `/v1/responses`, `/v1/chat/completions` capacity smoke를 실행한다 | 3 connected nodes, 3 provider candidates, total capacity 10 기준 smoke가 유지된다 |
| S08 | `priority-routing` | 같은 model alias를 serve하는 provider 후보들이 서로 다른 `in_flight`와 `priority`를 가진다 | Edge provider-pool dispatch를 실행한다 | dispatch 가능한 후보 중 `in_flight`가 가장 낮은 provider를 먼저 고르고, `in_flight`가 같은 경우에만 낮은 숫자의 `priority`를 우선하며, 둘 다 같으면 기존 순환을 유지한다 |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | config schema/validation unit tests and `configs/edge.yaml` provider-first example diff | `agent-task/m-node-provider-first-config-surface/01_schema_source` | `schema-source` Roadmap Completion과 config loader/validation verification output |
| S02 | mapper/config_set/router tests proving provider-derived adapter payload | `agent-task/m-node-provider-first-config-surface/02+01_normalize_compile` | `normalize-compile` Roadmap Completion과 Edge-to-Node payload tests |
| S03 | legacy/mixed config compatibility tests | `agent-task/m-node-provider-first-config-surface/03+01_legacy_compat` | `legacy-compat` Roadmap Completion과 backward compatibility verification |
| S04 | Edge service/OpenAI route tests | `agent-task/m-node-provider-first-config-surface/04+02,03_routing_status_refresh` | `routing-status-refresh` Roadmap Completion과 provider-first dispatch evidence |
| S05 | status/config refresh tests | `agent-task/m-node-provider-first-config-surface/04+02,03_routing_status_refresh` | `routing-status-refresh` Roadmap Completion과 provider-first snapshot/config refresh evidence |
| S06 | docs/inventory/stale-reference check | `agent-task/m-node-provider-first-config-surface/05+01_dev_runtime_docs` | `dev-runtime-docs` Roadmap Completion과 `rg` stale reference verification |
| S07 | dev-runtime config check, refresh/restart evidence, OpenAI-compatible capacity smoke | `agent-task/m-node-provider-first-config-surface/06+04,05_full_cycle_smoke` | `full-cycle-smoke` Roadmap Completion과 `/v1/responses`, `/v1/chat/completions`, capacity accounting evidence |
| S08 | config contract update, config validation, model queue dispatch order tests, config refresh classification tests | `agent-task/m-node-provider-first-config-surface/07_priority_routing` | `priority-routing` Roadmap Completion과 priority default/non-negative validation, `in_flight` 우선 selection, equal `in_flight` priority tie-break, equal priority rotation evidence |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.

## 사용자 리뷰 이력

- 2026-06-29: 사용자 대화에서 Node config 표면은 `providers[]` resource list 중심이어야 하며 `adapters`와 `providers` 중복 source of truth는 이해하기 어렵고 관리 비용을 높인다는 방향을 확인했다.
- 2026-06-30: 사용자 대화에서 provider `priority`는 기본값 0, 음수 불가, 숫자가 낮을수록 우선으로 정리했다. dispatch 선택은 `capacity`가 아니라 현재 활성 수인 `in_flight`를 먼저 보고, 같은 `in_flight` 후보에서만 `priority`를 적용하기로 확인했다.

## 작업 컨텍스트

- 표준선: 사용자-facing config는 provider/resource-first로 단순화하고, 내부 adapter registry는 provider config에서 컴파일되는 실행 IR로 유지한다.
- 표준선: 기존 adapter runtime은 안정화된 내부 실행 구조로 유지하되, 새 config 예시와 dev-runtime 운영 경로는 provider-first를 기본으로 한다.
- 표준선: provider priority는 routing preference가 아니라 같은 `in_flight` 후보의 tie-breaker다. `capacity`는 dispatch 가능 여부와 queue 진입 판단에만 쓰고, `priority`가 낮은 `in_flight` 후보를 뒤집지 않는다.
- 후속 SDD: 요청 실행 로그와 Usage Ledger 기반이 provider/resource/node identity를 ledger schema로 확장할 때 이 SDD의 provider id 기준을 참조한다.
