---
spec_doc_type: spec
spec_id: runtime/provider-pool-config-refresh
status: 부분
source_evidence:
  - type: contract
    path: agent-contract/inner/edge-config-runtime-refresh.md
    notes: Edge config, provider pool, config refresh, Node payload 연결 계약
  - type: code
    path: packages/go/config/provider_types.go
    notes: provider/model catalog 설정 타입
  - type: code
    path: packages/go/config/edge_types.go
    notes: Edge root provider_pool canonical queue policy 타입과 기본값
  - type: code
    path: packages/go/config/load.go
    notes: Edge config load와 default 적용
  - type: code
    path: packages/go/config/validate.go
    notes: provider/model 참조와 numeric bound 검증
  - type: code
    path: configs/edge.yaml
    notes: provider-pool 권장 설정 예시와 live refresh 설정 예시
  - type: code
    path: apps/edge/internal/service/provider_resolution.go
    notes: provider 후보 해석과 capacity/priority policy 구성
  - type: code
    path: apps/edge/internal/service/model_queue_admission.go
    notes: provider 전역 lease, 공통 pending 상한, global enqueue 순서와 long-context admission
  - type: code
    path: apps/edge/internal/service/model_queue_release.go
    notes: lease 반환, disconnect/reconnect candidate 재구성과 global queue pump
  - type: code
    path: apps/edge/internal/service/status_provider.go
    notes: lease state와 candidate pressure 기반 online/offline provider snapshot
  - type: code
    path: apps/edge/internal/configrefresh/classify.go
    notes: dry-run/apply classification과 changed path report 생성
  - type: code
    path: apps/edge/internal/bootstrap/runtime.go
    notes: mutable config apply, runtime snapshot 교체, Node refresh push
  - type: code
    path: apps/edge/internal/node/mapper.go
    notes: provider-first NodeConfigPayload compile과 OpenAI-compatible provider label 보존
  - type: test
    path: packages/go/config/provider_catalog_config_test.go
    notes: provider/model catalog load와 queue 설정 검증
  - type: test
    path: packages/go/config/provider_catalog_validation_config_test.go
    notes: provider/model 참조와 validation 검증
  - type: test
    path: apps/edge/internal/node/mapper_test.go
    notes: provider-first adapter payload, Seulgivibe alias/provider label 검증
  - type: test
    path: apps/edge/internal/configrefresh/node_runtime_classify_test.go
    notes: node runtime과 model catalog refresh classification 검증
  - type: test
    path: apps/edge/internal/configrefresh/provider_classify_test.go
    notes: provider field refresh classification 검증
  - type: test
    path: apps/edge/internal/service/model_queue_admission_test.go
    notes: cross-model capacity, terminal unavailable, disconnect/event-drop 회귀 검증
  - type: test
    path: apps/edge/internal/service/status_provider_test.go
    notes: cross-model candidate pressure와 offline/reconnect snapshot 검증
  - type: test
    path: apps/edge/internal/bootstrap/reconnect_readiness_integration_test.go
    notes: dispatch-ready reconnect가 기존 queued waiter를 실제 Node terminal까지 수렴시키는 검증
  - type: test
    path: scripts/e2e-provider-capacity-smoke.sh
    notes: loopback OpenAI-compatible provider에서 two-alias capacity-1 queue와 final counter 회복 검증
---

# 스펙: Provider Pool과 Config Refresh

## 목적

Edge 설정에서 provider-pool이 어떻게 모델 실행 후보를 고르고, 어떤 설정 변경이 재시작 없이 반영되는지 설명한다.

## 기능 목록

| 기능 | 설명 |
|------|------|
| model catalog | `models[].id`는 외부 OpenAI-compatible `model` key이자 provider-pool `ModelGroupKey`다. |
| provider mapping | `models[].providers`는 provider id를 실제 served model name으로 매핑한다. |
| node provider catalog | `nodes[].providers[]`는 Node 아래 resource/provider catalog이며 provider id는 Edge config에서 전역 유일해야 한다. |
| config validation | config load가 provider id 참조, served model membership, numeric bounds, long-context budget을 검증한다. |
| provider 후보 필터링 | dispatch는 dispatch-ready connection을 가진 Node의 provider 후보 중 catalog match, enabled, healthy/available, capacity 조건을 만족하는 후보만 사용한다. |
| provider 전역 capacity/priority dispatch | `node_id + provider_id` lease가 여러 model group의 일반·long in-flight를 합산한다. available 후보 중 낮은 in-flight를 고르고 동률이면 낮은 `priority`와 round-robin을 적용한다. |
| provider-pool 공통 queue policy | Edge root `provider_pool.max_queue`가 모든 model group의 전체 pending 상한을, `queue_timeout_ms`가 각 pending request timeout을 소유한다. |
| global queue 재평가 | lease 반환, capacity/priority/enabled refresh, disconnect/reconnect 뒤 global enqueue 순서에서 현재 dispatch 가능한 가장 이른 waiter부터 candidate를 다시 구성한다. |
| provider snapshot | 일반·long in-flight는 provider lease state, queued 값은 Edge queue에서 해당 provider를 후보로 포함하는 고유 pending request pressure에서 계산한다. offline provider는 catalog identity를 유지하고 effective 수치를 0으로 보고한다. |
| mixed provider execution path | 같은 model group의 OpenAI-compatible provider와 Ollama/CLI/native provider를 같은 후보군으로 두며, 선택된 provider capability로 passthrough 또는 normalized 실행 경로를 결정한다. |
| long-context admission | estimated input token이 threshold 이상이면 `context_class=long`으로 분류하고, provider long slot이 있으면 일반 capacity slot과 함께 점유한다. |
| config refresh dry-run/apply | loopback admin HTTP `POST /refresh`가 candidate config를 dry-run 또는 apply한다. |
| refresh classification | listener, Edge identity, bootstrap path, adapter structural 변경 등은 restart-required로 분류한다. |
| mutable apply | 적용 가능한 변경은 Edge `Cfg`, `NodeStore`, service/input model catalog, OpenAI long-context threshold를 copy-on-write로 교체한다. |
| Node config refresh push | 변경이 있으면 Edge가 dispatch-ready Node에 node-specific `NodeConfigRefreshRequest`를 push한다. accepted지만 pending인 Node는 register response config를 적용한 뒤 ready가 될 때까지 push 대상이 아니다. |
| Node registry swap | Node는 refresh payload로 새 adapter registry를 만들고 router registry를 swap한다. old registry stop은 active run이 있으면 drain 이후로 지연한다. |
| principal token mapping config | `openai.principal_tokens[]`는 raw token 없이 `token_ref`, `token_hash_sha256`, `principal_ref`, optional alias를 관리하고 OpenAI usage metering의 principal/token label 후보를 제공한다. 같은 principal에 여러 token entry를 둘 수 있다. |
| provider auth forwarding config | `openai.provider_auth`는 caller가 요청 header로 제공한 raw provider token을 selected OpenAI-compatible provider tunnel header로 전달하는 규칙만 저장한다. |
| Seulgivibe provider aliases | `seulgivibe_claude`, `seulgivibe_openai` provider type은 runtime adapter type을 `openai_compat`로 정규화하고, 명시 provider label이 없으면 canonical Seulgivibe alias를 Node payload provider label로 보존한다. |

## 범위

- 포함: Edge config load/default/validation, provider-pool dispatch, queue admission, long-context admission, config refresh classification/apply, Node config refresh payload.
- 제외: 개별 adapter의 provider API 호출 세부, OpenAI HTTP request/response shape, Control Plane 원격 config 변경 UX, private credential 관리.

## 주요 흐름

```mermaid
sequenceDiagram
  participant Caller
  participant Service as Edge service
  participant Queue as Provider queue
  participant Node

  Caller->>Service: SubmitRun(model)
  Service->>Queue: dispatch-ready provider 후보 선택(capacity + priority)
  Queue-->>Service: selected provider + served target
  alt selected provider supports OpenAI-compatible call
    Service->>Node: ProviderTunnelRequest(adapter, served target)
  else selected provider is Ollama/CLI/native
    Service->>Node: RunRequest(adapter, served target)
  end

  participant Operator
  participant Refresh as Config refresh
  Operator->>Refresh: POST /refresh(apply)
  Refresh->>Refresh: load, validate, classify
  Refresh->>Node: NodeConfigRefreshRequest
```

## 계약

- `iop.edge-config-runtime-refresh`: `agent-contract/inner/edge-config-runtime-refresh.md`
- `iop.edge-node-runtime-wire`: `agent-contract/inner/edge-node-runtime-wire.md`
- proto 원문: `proto/iop/runtime.proto`

## 설정/데이터/이벤트

- `long_context_threshold_tokens` 기본 예시는 `100000`이고 0 이하 값은 config load에서 거부된다.
- `provider_pool.max_queue`는 0/생략 시 기본값 `16`, `queue_timeout_ms`는 생략 시 `30000`이고 명시적 0은 timeout 없음이다. canonical root key가 없을 때만 서로 같은 legacy provider queue pair를 승격하며 값이 다르면 load를 거부한다.
- `nodes[].providers[].capacity`와 `long_context_capacity`는 provider resource 속성이고 같은 provider를 공유하는 model alias가 합산 점유한다. `total_context_tokens`는 runtime ledger가 아니라 `context_window_tokens * long_context_capacity` 정적 validation 값이다.
- provider `enabled=false`는 dispatch pool에서 제외하지만 adapter process lifecycle 변경을 의미하지 않는다.
- accepted registration은 provider candidate를 바로 복구하지 않는다. Node가 config 적용과 handler 설치 뒤 ready ack를 받아야 해당 generation이 candidate, connected snapshot, refresh push 대상이 되며 이 transition이 stranded provider-pool waiter를 재평가한다.
- provider capacity, long-context capacity, priority, enabled toggle, root queue policy와 model generation policy는 live apply 대상으로 분류된다. apply는 기존 lease를 보존하고 이후 admission 및 모든 관련 waiter의 live candidate/deadline을 새 값으로 재평가한다.
- Edge listener, control plane, openai/a2a listener, bootstrap artifact path, node 추가/삭제, node token/alias, adapter 설정 변경은 restart-required 대상이다.
- `openai.principal_tokens[]`는 `token_ref`와 `token_hash_sha256` 중복을 거부하고, raw token 원문은 tracked config에 저장하지 않는다.
- 여러 `openai.principal_tokens[]` entry가 같은 `principal_ref`를 공유할 수 있으며, 이때 `token_ref`가 앱/통합/용도별 사용량 분해 기준이다.
- `openai.principal_tokens[]` 변경은 credential/hash 변경으로 보고 restart-required로 분류된다.
- `openai.provider_auth.enabled=true`이면 생략된 header fields는 `from_header=X-IOP-Provider-Authorization`, `target_header=Authorization`, `scheme=Bearer`, `required=true`로 해석된다. raw provider token 값은 config/spec/docs에 저장하지 않는다.
- Seulgivibe provider catalog는 top-level `models[]`의 정적 provider mapping을 source of truth로 사용한다. provider `/models` endpoint는 IOP catalog source가 아니다.
- `models[]`는 mixed provider group과 Ollama-only group을 모두 표현할 수 있다. Ollama는 후보에서 제외하지 않고 `capacity`/`priority`로 운영자가 낮은 동시성과 낮은 선호도를 표현한다.
- refresh result는 changed nodes/providers/models와 restart-required paths를 stable non-nil slice로 보고한다.

## 검증

- `go test ./packages/go/config`
- `go test ./apps/edge/internal/configrefresh`
- `go test ./apps/edge/internal/service`
- `go test ./apps/edge/internal/node`
- `go test ./apps/node/internal/adapters ./apps/node/internal/node`
- `go test ./apps/edge/internal/bootstrap -run '^TestActualNodeReconnectReadyPumpsQueuedWaiterExactlyOnce$'`
- `./scripts/e2e-provider-capacity-smoke.sh`

## 한계와 주의사항

- provider health는 현재 config/provider snapshot 기반이다. 모든 runtime에 대한 active health probe가 완성된 것은 아니다.
- refresh admin API는 operator-local 표면이다. 접근 제어 없이 public interface에 노출하지 않는다.
- adapter structural 변경은 contract상 restart-required로 분류된다. Node handler가 registry swap을 지원하더라도 Edge refresh classifier가 허용한 변경만 apply해야 한다.
- no-change apply는 runtime snapshot을 교체하지만 Node push는 생략한다.
- `NodeRuntimeConfig.concurrency`는 legacy metadata이며 provider-pool admission의 node-wide capacity로 쓰지 않는다.
- provider가 full인 상태는 일시적인 queue block이지만, disconnect/disable로 live candidate가 모두 사라지면 waiter는 기존 queue timeout까지 남지 않고 unavailable로 종료된다.
- credential, private endpoint, bearer token 원문은 tracked config/docs/spec에 남기지 않는다.
- principal token mapping은 완성된 사용자/테넌트 source of truth가 아니라 외부 `principal_ref` 또는 내부 alias에 대한 얇은 운영 매핑이다.
- Seulgivibe provider endpoint와 raw user token은 환경별 private config 또는 request-time header로 주입해야 하며 tracked 예시에 실제 값을 남기지 않는다.

## 변경 기록

- 2026-07-07: 현재 코드, 계약, config 예시 기준으로 bootstrap spec 작성.
- 2026-07-07: 기능 목록 중심으로 축소하고 주요 흐름을 Mermaid sequence diagram으로 정리.
- 2026-07-10: OpenAI usage metering용 principal token hash mapping config와 restart-required 기준을 반영.
- 2026-07-11: Seulgivibe OpenAI-compatible provider aliases, provider auth forwarding config, static catalog 기준을 현재 코드/계약/테스트 기준으로 반영.
- 2026-07-12: Model Group Mixed Provider Dispatch 종료 검토 기준으로 mixed provider group과 Ollama-only group, capacity/priority 가중 예시를 반영.
- 2026-07-18: 저장소 구조 분해 뒤 config, provider resolution/admission, split test의 `source_evidence`를 현재 경로로 동기화.
- 2026-07-22: pending accepted connection과 dispatch-ready connection을 구분하고, ready ack 뒤에만 provider candidate 복구·refresh push·queued waiter pump가 일어나는 현재 동작을 반영.
- 2026-07-22: root provider-pool queue policy, cross-model provider lease, global queue 재평가, refresh lease 보존과 connectivity 기반 snapshot 의미를 현재 구현·계약·테스트 기준으로 동기화.
