feat(observability): compose 관측 구성을 추가한다
This commit is contained in:
parent
469d2f32b4
commit
5368926822
30 changed files with 348 additions and 50 deletions
28
.env.dev-corp.example
Normal file
28
.env.dev-corp.example
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
COMPOSE_PROJECT_NAME=iop-dev-corp-agent
|
||||
IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net
|
||||
IOP_COMPOSE_SUBNET=10.89.2.0/24
|
||||
|
||||
IOP_CONTROL_PLANE_HTTP_PORT=18002
|
||||
IOP_CONTROL_PLANE_WIRE_PORT=19004
|
||||
IOP_CONTROL_PLANE_EDGE_WIRE_PORT=19005
|
||||
IOP_CONTROL_PLANE_METRICS_PORT=19104
|
||||
IOP_EDGE_NODE_PORT=19006
|
||||
IOP_EDGE_METRICS_PORT=19102
|
||||
IOP_WEB_PORT=13002
|
||||
IOP_PROMETHEUS_PORT=19112
|
||||
IOP_GRAFANA_BIND=127.0.0.1
|
||||
IOP_GRAFANA_PORT=19122
|
||||
IOP_GRAFANA_ADMIN_USER=admin
|
||||
IOP_GRAFANA_ADMIN_PASSWORD=iop_dev_admin
|
||||
|
||||
IOP_CONTROL_PLANE_HTTP_URL=http://iop.ai.kr:18002
|
||||
IOP_CONTROL_PLANE_WIRE_URL=ws://iop.ai.kr:19004/client
|
||||
|
||||
IOP_POSTGRES_USER=iop
|
||||
IOP_POSTGRES_PASSWORD=iop_dev_password
|
||||
IOP_POSTGRES_DB=iop-control-plane-dev-corp
|
||||
IOP_POSTGRES_PORT=15402
|
||||
|
||||
IOP_REDIS_PORT=16302
|
||||
IOP_REDIS_DB=4
|
||||
IOP_REDIS_KEY_PREFIX=iop:control-plane:dev-corp:
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
COMPOSE_PROJECT_NAME=iop-dev-agent
|
||||
IOP_COMPOSE_NETWORK=iop-dev-agent-net
|
||||
IOP_COMPOSE_SUBNET=10.89.1.0/24
|
||||
|
||||
IOP_CONTROL_PLANE_HTTP_PORT=18001
|
||||
IOP_CONTROL_PLANE_WIRE_PORT=19001
|
||||
IOP_CONTROL_PLANE_EDGE_WIRE_PORT=19002
|
||||
IOP_CONTROL_PLANE_METRICS_PORT=19103
|
||||
IOP_EDGE_NODE_PORT=19003
|
||||
IOP_EDGE_METRICS_PORT=19101
|
||||
IOP_WEB_PORT=13001
|
||||
IOP_PROMETHEUS_PORT=19111
|
||||
IOP_PROMETHEUS_CONFIG=./configs/prometheus/prometheus.dev.yml
|
||||
IOP_GRAFANA_BIND=127.0.0.1
|
||||
IOP_GRAFANA_PORT=19121
|
||||
IOP_GRAFANA_ADMIN_USER=admin
|
||||
IOP_GRAFANA_ADMIN_PASSWORD=iop_dev_admin
|
||||
|
||||
IOP_CONTROL_PLANE_HTTP_URL=http://toki-labs.com:18001
|
||||
IOP_CONTROL_PLANE_WIRE_URL=ws://toki-labs.com:19001/client
|
||||
|
|
|
|||
|
|
@ -3,8 +3,15 @@ COMPOSE_PROJECT_NAME=iop-dev
|
|||
IOP_CONTROL_PLANE_HTTP_PORT=18000
|
||||
IOP_CONTROL_PLANE_WIRE_PORT=19080
|
||||
IOP_CONTROL_PLANE_EDGE_WIRE_PORT=19081
|
||||
IOP_CONTROL_PLANE_METRICS_PORT=19100
|
||||
IOP_EDGE_NODE_PORT=19090
|
||||
IOP_EDGE_METRICS_PORT=19092
|
||||
IOP_WEB_PORT=13000
|
||||
IOP_PROMETHEUS_PORT=19110
|
||||
IOP_GRAFANA_BIND=127.0.0.1
|
||||
IOP_GRAFANA_PORT=19120
|
||||
IOP_GRAFANA_ADMIN_USER=admin
|
||||
IOP_GRAFANA_ADMIN_PASSWORD=iop_dev_admin
|
||||
|
||||
IOP_POSTGRES_USER=iop
|
||||
IOP_POSTGRES_PASSWORD=iop_dev_password
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ tracked config에는 public 예시와 기본 구조만 두고, 실제 endpoint/c
|
|||
- `openai.model_routes[]`는 외부 OpenAI-compatible `model` id를 내부 `adapter + target` route로 매핑하는 compatibility catalog다.
|
||||
- `long_context_threshold_tokens`는 Edge root의 입력 토큰 추정 기준 long-context 분류 threshold다. 기본값은 `100000`이며 0 이하 값은 config load에서 거부한다.
|
||||
- `models[]`는 provider pool 방향의 canonical routing key이며 `nodes[].providers[].id`를 참조한다. `context_window_tokens`는 해당 model group의 provider 공통 단일 요청 최대 context 계약이다. `default_max_tokens`, `min_max_tokens`, `default_thinking_token_budget`은 OpenAI-compatible 요청을 내부 실행으로 넘기기 전에 적용하는 모델 단위 generation policy다.
|
||||
- 하나의 `models[]` entry는 OpenAI-compatible provider와 normalized-only provider를 함께 참조할 수 있다. 선택된 provider가 OpenAI-compatible 호출 방식을 지원하면 passthrough 실행 경로를 사용하고, `ollama`/`cli` 같은 normalized-only provider면 normalized 실행 경로를 사용한다. Ollama 후보는 model group에서 제거하지 않고 `capacity`와 `priority`로 낮은 동시성/선호도를 표현한다.
|
||||
- `nodes[].providers[]`는 Node 아래 resource/provider catalog다. `category`는 `api`, `cli`, `local_inference` resource kind를 나타낸다.
|
||||
- `nodes[].providers[].type`의 `seulgivibe_claude`와 `seulgivibe_openai`는 runtime type을 `openai_compat`로 정규화한다. Edge가 Node adapter payload를 만들 때 명시 provider label이 없으면 원래 Seulgivibe type alias를 `OpenAICompatAdapterConfig.provider`로 보존한다.
|
||||
- `nodes[].providers[].id`는 전체 Edge config 안에서 중복되면 안 된다.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ Edge는 Node 연결을 수락하고, Node는 연결 직후 등록 요청을 보
|
|||
- register: Node가 `RegisterRequest`를 보내고 Edge가 `RegisterResponse`로 수락 여부와 `NodeConfigPayload`를 돌려준다.
|
||||
- execution: Edge가 `RunRequest`를 보내고 Node가 `RunEvent` stream으로 실행 상태를 보낸다.
|
||||
- provider raw tunnel: Edge가 기존 Edge-Node socket으로 `ProviderTunnelRequest`를 보내고 Node가 provider HTTP/SSE 요청을 연 뒤 `ProviderTunnelFrame` stream으로 provider status/header/body/end/error/usage 후보를 sequence와 함께 돌려준다. 이 경로는 OpenAI-compatible provider `passthrough`와 `passthrough+sideband`용이며 `RunEvent` 실행 stream과 분리된다.
|
||||
- provider-pool mixed dispatch: Edge service는 model group provider candidate를 선택한 뒤, 같은 selected provider/queue lease로 OpenAI-compatible provider에는 `ProviderTunnelRequest`, Ollama/CLI/native provider에는 normalized `RunRequest`를 보낸다. Edge-Node wire는 client-provided response path selector를 받지 않고, provider type만으로 후보를 제외하지 않는다.
|
||||
- cancel: Edge가 `CancelRequest`를 보내며 `CANCEL_RUN`과 `TERMINATE_SESSION`을 구분한다.
|
||||
- command: Edge가 `NodeCommandRequest`를 보내고 Node가 `NodeCommandResponse`로 usage/capabilities/session/transport/provider 상태를 응답한다.
|
||||
- refresh: Edge가 `NodeConfigRefreshRequest`로 새 config payload를 보내고 Node가 `NodeConfigRefreshResponse`로 적용/재시작 필요/실패를 응답한다.
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ CLI agent 실행으로 라우팅되는 요청의 최소 형태:
|
|||
|
||||
- normalized(non-provider) `/v1/responses` route는 strict field validation을 유지하며 non-streaming string input만 지원한다.
|
||||
- provider-pool model group route(`models[]`)의 `/v1/responses` 호출은 `metadata.iop_response_mode`를 명시하면 값과 무관하게 `400 invalid_request_error`로 거부한다. 생략 시 raw passthrough로 provider `POST /v1/responses`에 전달한다. caller body는 `model` field만 served target으로 rewrite하고, unknown/Codex field(`max_output_tokens`, `tools`, `store`, ...)는 보존하며, `stream:true`는 provider raw SSE로 relay한다. provider auth forwarding이 적용되고, response model echo rewrite는 적용하지 않는다. 이 경로는 normalized `SubmitRun`으로 fallback하지 않는다.
|
||||
- provider-pool model group route는 provider candidate를 먼저 선택한다. 선택된 provider가 OpenAI-compatible 호출 방식을 지원하면 `ProviderTunnelRequest` passthrough를 사용하고, Ollama/CLI/native provider이면 normalized `RunRequest`를 사용한다. provider type만으로 Ollama를 candidate set에서 제거하지 않으며, OpenAI-compatible provider의 tunnel 구현이 없으면 normalized fallback이 아니라 unsupported/implementation error다.
|
||||
- direct legacy provider route(`openai.model_routes[]`의 `openai_compat`/`vllm` adapter)에서 명시적 `metadata.iop_response_mode="passthrough+sideband"`는 opt-in extension surface다. non-streaming provider JSON object 응답은 top-level `metadata` object를 만들거나 병합해 IOP sideband metadata를 삽입한다. streaming 응답은 provider SSE event stream 사이에 `event: iop.sideband`를 삽입한다. sideband 내용은 `metadata` object 아래 확장 가능하며, 현재 최소 marker는 `iop_response_mode="passthrough+sideband"`다. `"transformed"`는 provider tunnel route에서 `400 invalid_request_error`로 거부한다.
|
||||
- Responses provider passthrough success usage metric label은 endpoint=`responses`, response_mode=`passthrough` 또는 direct sideband route의 `passthrough+sideband`, model_group=request alias를 사용한다.
|
||||
- `metadata`는 최대 16개 string key/value를 허용한다. key는 64자 이하, value는 512자 이하를 기준으로 한다.
|
||||
|
|
@ -315,3 +316,4 @@ Edge 설정이 `openai.model_routes[]`를 제공하면 `model`은 먼저 route c
|
|||
CLI agent를 OpenAI-compatible API로 노출할 때는 route catalog에서 해당 `model`을 명시적으로 `adapter: "cli"`와 target profile로 매핑하는 방식을 우선한다.
|
||||
|
||||
Top-level `models[]`가 있으면 IOP `/v1/models`와 provider-pool dispatch의 static catalog source of truth다. Seulgivibe provider는 runtime adapter type을 `openai_compat`로 정규화하되 provider family label로 `seulgivibe_claude` 또는 `seulgivibe_openai`를 보존할 수 있다. Tracked catalog 예시는 model/provider mapping만 담고 실제 endpoint credential이나 raw user token은 담지 않는다.
|
||||
`models[]` provider mapping은 OpenAI-compatible provider와 normalized-only provider를 같은 model group 안에 둘 수 있다. dispatch는 기존 capacity + priority + availability 기준으로 provider를 한 번 선택하고, client request field가 아니라 selected provider capability로 passthrough 또는 normalized execution path를 결정한다.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
## 위치
|
||||
|
||||
- Roadmap: [ROADMAP.md](../../../ROADMAP.md)
|
||||
- Phase: [PHASE.md](../PHASE.md)
|
||||
- Roadmap: [ROADMAP.md](../../../../ROADMAP.md)
|
||||
- Phase: [PHASE.md](../../../../phase/routing-policy-model-orchestration/PHASE.md)
|
||||
|
||||
## 목표
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ Client 요청은 provider 실행 경로를 고르는 필드를 넣지 않으며,
|
|||
|
||||
## 상태
|
||||
|
||||
[진행중]
|
||||
[완료]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ Model group이 provider 종류에 따라 normalized-only로 후퇴하지 않고,
|
|||
|
||||
Client가 OpenAI-compatible 표면으로 호출하되 model group 내부 실행 방식을 직접 고르지 않도록 계약과 handler를 정리한다.
|
||||
|
||||
- [x] [no-client-response-mode] Model group route에서 `metadata.iop_response_mode` 또는 동등한 passthrough/normalized selector를 지원하지 않도록 계약, 구현, 테스트를 정리한다. 검증: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md), [openai-compatible-surface.md](../../../../agent-spec/input/openai-compatible-surface.md), handler tests가 model group selector 거부/제거 기준과 일치한다.
|
||||
- [x] [no-client-response-mode] Model group route에서 `metadata.iop_response_mode` 또는 동등한 passthrough/normalized selector를 지원하지 않도록 계약, 구현, 테스트를 정리한다. 검증: [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md), [openai-compatible-surface.md](../../../../../agent-spec/input/openai-compatible-surface.md), handler tests가 model group selector 거부/제거 기준과 일치한다.
|
||||
- [x] [custom-field-preservation] Provider-pool Chat route가 Responses route처럼 strict decode 전에 raw body와 routing envelope를 분리하고, OpenAI-compatible provider 선택 시 unknown/Codex/provider-specific fields를 model rewrite 외에는 보존한다. 검증: `go test ./apps/edge/internal/openai -count=1`이 Chat unknown field preservation과 normalized-provider 선택 시 supported field mapping/unsupported policy를 확인한다.
|
||||
- [x] [sideband-observation] Provider-pool 실행 결과가 selected provider, adapter, served target, execution path, queue decision, usage 후보를 sideband/log/metric에 남기며 표준 client 응답에는 불필요한 custom field를 섞지 않는다. 검증: `go test ./apps/edge/internal/openai -count=1`, `go test ./apps/edge/internal/service -count=1`이 standard-client response와 IOP-aware observation fixture를 함께 확인한다.
|
||||
|
||||
|
|
@ -71,21 +71,26 @@ Client가 OpenAI-compatible 표면으로 호출하되 model group 내부 실행
|
|||
|
||||
Model group mixed dispatch의 공개/내부 계약을 문서와 구현 타입에 맞춘다.
|
||||
|
||||
- [ ] [contract-sync] `agent-contract`와 `agent-spec`가 model group의 provider-derived execution path, custom field 보존, client selector 금지, normalized/passthrough 경계를 같은 용어로 설명한다. 검증: contract/spec diff와 관련 Go tests가 SDD Evidence Map에 연결된다.
|
||||
- [ ] [config-examples] provider-first config 예시가 mixed model group과 Ollama-only model group을 보여주되, Ollama는 capacity/priority로만 가중치를 조절한다. 검증: `go test ./packages/go/config -count=1`과 config fixture validation이 통과한다.
|
||||
- [x] [contract-sync] `agent-contract`와 `agent-spec`가 model group의 provider-derived execution path, custom field 보존, client selector 금지, normalized/passthrough 경계를 같은 용어로 설명한다. 검증: contract/spec diff와 관련 Go tests가 SDD Evidence Map에 연결된다.
|
||||
- [x] [config-examples] provider-first config 예시가 mixed model group과 Ollama-only model group을 보여주되, Ollama는 capacity/priority로만 가중치를 조절한다. 검증: `go test ./packages/go/config -count=1`과 config fixture validation이 통과한다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 기능 Task가 아직 충족되지 않았다.
|
||||
- 상태: 통과
|
||||
- 요청일: 2026-07-12
|
||||
- 완료 근거:
|
||||
- `contract-sync`: [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md), [edge-node-runtime-wire.md](../../../../../agent-contract/inner/edge-node-runtime-wire.md), [edge-config-runtime-refresh.md](../../../../../agent-contract/inner/edge-config-runtime-refresh.md), [openai-compatible-surface.md](../../../../../agent-spec/input/openai-compatible-surface.md), [provider-pool-config-refresh.md](../../../../../agent-spec/runtime/provider-pool-config-refresh.md), [edge-node-execution.md](../../../../../agent-spec/runtime/edge-node-execution.md)에 provider-first mixed dispatch 용어를 동기화했다.
|
||||
- `config-examples`: [edge.yaml](../../../../../configs/edge.yaml)에 mixed model group과 Ollama-only group을 추가하고 [config_test.go](../../../../../packages/go/config/config_test.go) fixture validation으로 보호했다.
|
||||
- 검증: `GOCACHE=/tmp/iop-go-build-cache go test ./packages/go/config -count=1`, `GOCACHE=/tmp/iop-go-build-cache go test ./packages/go/... -count=1`, `GOCACHE=/tmp/iop-go-build-cache go test ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/node ./apps/node/internal/adapters -count=1`, `git diff --check` 통과.
|
||||
- Spec sync: 완료. [openai-compatible-surface.md](../../../../../agent-spec/input/openai-compatible-surface.md), [provider-pool-config-refresh.md](../../../../../agent-spec/runtime/provider-pool-config-refresh.md), [edge-node-execution.md](../../../../../agent-spec/runtime/edge-node-execution.md)의 변경 기록까지 반영했다.
|
||||
- 남은 차단 항목: 없음
|
||||
- 검토 항목:
|
||||
- [ ] `complete.log`의 `Roadmap Completion`이 각 기능 Task id를 기록한다.
|
||||
- [ ] 최종 검증 출력이 SDD Evidence Map과 일치한다.
|
||||
- [ ] model group 요청에서 client-controlled response path selector가 남아 있지 않다.
|
||||
- [ ] mixed group에서 Ollama가 후보에서 제외되지 않고 normalized path로만 실행된다.
|
||||
- [x] 직접 완료 evidence가 각 기능 Task id와 연결된다.
|
||||
- [x] 최종 검증 출력이 SDD Evidence Map과 일치한다.
|
||||
- [x] model group 요청에서 client-controlled response path selector가 남아 있지 않다.
|
||||
- [x] mixed group에서 Ollama가 후보에서 제외되지 않고 normalized path로만 실행된다.
|
||||
- agent-ui 상태 반영: 해당 없음
|
||||
- 리뷰 코멘트: 없음
|
||||
- 리뷰 코멘트: 종료 감사에서 SDD gate, 기능 Task, complete.log evidence, 관련 Go test, spec sync가 모두 충족됨을 확인했다.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
|
|
@ -98,14 +103,14 @@ Model group mixed dispatch의 공개/내부 계약을 문서와 구현 타입에
|
|||
|
||||
## 작업 컨텍스트
|
||||
|
||||
- 관련 경로: `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/adapters/ollama`, `apps/node/internal/runtime`, `proto/iop/runtime.proto`, [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md), [edge-node-runtime-wire.md](../../../../agent-contract/inner/edge-node-runtime-wire.md), [edge-config-runtime-refresh.md](../../../../agent-contract/inner/edge-config-runtime-refresh.md)
|
||||
- 관련 경로: `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/adapters/ollama`, `apps/node/internal/runtime`, `proto/iop/runtime.proto`, [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md), [edge-node-runtime-wire.md](../../../../../agent-contract/inner/edge-node-runtime-wire.md), [edge-config-runtime-refresh.md](../../../../../agent-contract/inner/edge-config-runtime-refresh.md)
|
||||
- 표준선(선택): Model group은 normalized provider만 묶는 추상화가 아니다. OpenAI-compatible provider와 native provider를 같은 candidate set에 두고, 선택된 provider가 실행 경로를 결정한다.
|
||||
- 표준선(선택): OpenAI-compatible 호출 방식을 지원하는 모든 provider는 passthrough 방식에 속한다. 여기에는 openweight cloud provider, Seulgivibe Claude/OpenAI provider, 로컬 vLLM/vLLM-MLX/Lemonade/SGLang 계열이 포함된다. sideband는 내부 observation 또는 문서화된 extension-safe 지점으로만 다루며, client 요청 selector가 아니다.
|
||||
- 표준선(선택): OpenAI-compatible provider에서 tunnel/passthrough 구현이 빠져 있으면 normalized fallback으로 처리하지 않는다. 이는 구현 결함 또는 unsupported 상태다.
|
||||
- 표준선(선택): Ollama와 CLI처럼 OpenAI-compatible 호출 방식을 지원하지 않는 provider는 model group 안에서도 normalized로 실행한다. Ollama는 제외하지 않으며 운영자는 capacity/priority로 낮은 동시성을 표현한다.
|
||||
- 표준선(선택): Model group client request에는 `passthrough`, `passthrough+sideband`, `normalized`, `transformed` 같은 실행 경로 selector를 넣지 않는다.
|
||||
- 표준선(선택): 표준 OpenAI-compatible client 요청은 표준-compatible 응답을 받고, IOP-aware/custom 요청은 문서화된 extension-safe 지점에서만 sideband/custom observation을 볼 수 있다.
|
||||
- 우선순위/정합성: 현재 active 흐름에서는 [OpenAI-compatible 출력 검증 필터](../../knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)와 [Seulgivibe OpenAI-compatible Provider 연동](seulgivibe-openai-compatible-provider.md)을 함께 고려한다. Seulgivibe 구현은 OpenAI-compatible provider는 passthrough 방식이라는 기준과 충돌하지 않아야 하며, mixed provider dispatch 자체는 본 Milestone이 소유한다.
|
||||
- 선행 작업: [OpenAI-compatible Raw Tunnel과 Sideband Passthrough](../../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md), [Model Alias Provider Pool과 Provider Catalog](../../operational-observability-provider-management/milestones/provider-catalog-device-status.md)
|
||||
- 우선순위/정합성: 현재 active 흐름에서는 [OpenAI-compatible 출력 검증 필터](../../../../phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)와 [Seulgivibe OpenAI-compatible Provider 연동](seulgivibe-openai-compatible-provider.md)을 함께 고려한다. Seulgivibe 구현은 OpenAI-compatible provider는 passthrough 방식이라는 기준과 충돌하지 않아야 하며, mixed provider dispatch 자체는 본 Milestone이 소유한다.
|
||||
- 선행 작업: [OpenAI-compatible Raw Tunnel과 Sideband Passthrough](openai-compatible-raw-tunnel-sideband-passthrough.md), [Model Alias Provider Pool과 Provider Catalog](../../../../phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md)
|
||||
- 후속 작업: OpenAI-compatible 하이브리드 라우팅과 컨텍스트 최적화, route scorer, policy learning loop
|
||||
- 확인 필요: 없음
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
## 위치
|
||||
|
||||
- Milestone: [Model Group Mixed Provider Dispatch](../../../phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
|
||||
- Phase: [PHASE.md](../../../phase/routing-policy-model-orchestration/PHASE.md)
|
||||
- Phase: [PHASE.md](../../../../phase/routing-policy-model-orchestration/PHASE.md)
|
||||
|
||||
## 상태
|
||||
|
||||
|
|
@ -31,8 +31,8 @@
|
|||
| 영역 | 기준 | 메모 |
|
||||
|------|------|------|
|
||||
| Roadmap | [Model Group Mixed Provider Dispatch](../../../phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md) | 목표, 기능 Task, 범위 제외 기준 |
|
||||
| Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md), [edge-node-runtime-wire.md](../../../../agent-contract/inner/edge-node-runtime-wire.md), [edge-config-runtime-refresh.md](../../../../agent-contract/inner/edge-config-runtime-refresh.md) | 외부 OpenAI-compatible model group surface, provider tunnel, normalized run dispatch, provider config 계약 |
|
||||
| Spec | [openai-compatible-surface.md](../../../../agent-spec/input/openai-compatible-surface.md), [provider-pool-config-refresh.md](../../../../agent-spec/runtime/provider-pool-config-refresh.md), [edge-node-execution.md](../../../../agent-spec/runtime/edge-node-execution.md) | 현재 구현 surface와 runtime behavior 문서 |
|
||||
| Contract | [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md), [edge-node-runtime-wire.md](../../../../../agent-contract/inner/edge-node-runtime-wire.md), [edge-config-runtime-refresh.md](../../../../../agent-contract/inner/edge-config-runtime-refresh.md) | 외부 OpenAI-compatible model group surface, provider tunnel, normalized run dispatch, provider config 계약 |
|
||||
| Spec | [openai-compatible-surface.md](../../../../../agent-spec/input/openai-compatible-surface.md), [provider-pool-config-refresh.md](../../../../../agent-spec/runtime/provider-pool-config-refresh.md), [edge-node-execution.md](../../../../../agent-spec/runtime/edge-node-execution.md) | 현재 구현 surface와 runtime behavior 문서 |
|
||||
| Code | `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/adapters/ollama`, `apps/node/internal/runtime`, `proto/iop/runtime.proto` | config validation, provider selection, OpenAI handler, Edge-Node wire, adapter implementation 기준 |
|
||||
| User Decision | 현재 사용자 요청 | Ollama는 model group에서 제외하지 않는다. 후보는 동일하게 두고 capacity/priority로 가중한다. model group request에는 response path selector를 두지 않는다. OpenAI-compatible 호출 방식을 지원하는 provider는 모두 passthrough 방식으로 처리하고, Ollama/CLI/native provider는 normalized로 처리한다. Seulgivibe Claude/OpenAI aliases는 passthrough provider에 포함한다. custom request fields는 provider-pool ingress에서 보존한다. |
|
||||
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
|
||||
## Interface Contract
|
||||
|
||||
- 계약 원문: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md)
|
||||
- 계약 원문: [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md)
|
||||
- 입력:
|
||||
- `model`: caller-facing model group id다. `models[]` catalog와 provider mapping이 source of truth다.
|
||||
- `metadata.workspace`, `metadata.task_id` 등 routing/operation metadata는 기존 OpenAI-compatible surface를 따른다.
|
||||
|
|
@ -24,8 +24,8 @@ IOP의 OpenAI-compatible, A2A, IOP native 입력 표면에서 들어온 요청
|
|||
- 경로: [seulgivibe-openai-compatible-provider](../../archive/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)
|
||||
- 요약: Seulgivibe Claude/OpenAI 프록시를 OpenAI-compatible provider family로 관리하고, model group에서는 passthrough(+sideband) 계열 provider로 분류한다. 정적 catalog, 요청 시점 provider token forwarding, Codex Responses passthrough는 코드 감사와 spec sync까지 통과해 archive했다.
|
||||
|
||||
- [진행중] Model Group Mixed Provider Dispatch
|
||||
- 경로: [model-group-mixed-provider-dispatch](milestones/model-group-mixed-provider-dispatch.md)
|
||||
- [완료] Model Group Mixed Provider Dispatch
|
||||
- 경로: [model-group-mixed-provider-dispatch](../../archive/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
|
||||
- 요약: model group provider pool에서 OpenAI-compatible provider와 Ollama/CLI 같은 normalized provider를 같은 후보군으로 두고, 기존 capacity+priority 선택 뒤 OpenAI-compatible 지원 provider는 모두 passthrough, native provider는 normalized 실행 경로로 자동 결정한다.
|
||||
|
||||
- [스케치] OpenAI-compatible 하이브리드 라우팅과 컨텍스트 최적화
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행
|
|||
| model catalog | `/v1/models`는 provider-pool `models[]`, legacy `openai.model_routes[]`, `openai.models` 또는 `openai.target` 순서로 노출 모델을 만든다. |
|
||||
| model dispatch | request `model`은 provider-pool catalog, legacy model route, single target fallback 순서로 해석된다. |
|
||||
| provider-pool handoff | provider-pool catalog에 model이 있으면 service 요청은 `ProviderPool=true`로 전달되고 adapter/target은 provider selection 이후 확정된다. |
|
||||
| mixed provider dispatch | model group 안에 OpenAI-compatible provider와 Ollama/CLI/native provider가 함께 있어도 request field가 아니라 selected provider capability가 passthrough 또는 normalized 실행 경로를 결정한다. |
|
||||
| legacy route 변환 | legacy route는 외부 `model`을 route entry의 `adapter`, `target`, `node`, `session_id`, queue policy로 변환한다. |
|
||||
| metadata/workspace 처리 | `metadata.workspace`는 `RunRequest.workspace`로 분리하고, 일반 metadata는 최대 16개 string key/value만 허용한다. |
|
||||
| Chat Completions | `/v1/chat/completions`는 non-streaming과 streaming SSE를 지원한다. |
|
||||
|
|
@ -112,6 +113,7 @@ sequenceDiagram
|
|||
|
||||
- `configs/edge.yaml`의 `openai` 섹션이 listener, bearer token, legacy adapter/target, model routes, strict output을 제공한다.
|
||||
- top-level `models[]`가 있으면 OpenAI model list와 provider-pool dispatch에서 legacy route보다 우선한다.
|
||||
- provider-pool model group은 capacity + priority + availability 기준으로 provider candidate를 먼저 선택하고, 선택된 provider가 OpenAI-compatible 호출 방식을 지원하면 raw tunnel passthrough로 dispatch한다. Ollama/CLI/native provider가 선택되면 normalized `RunRequest` path로 dispatch한다.
|
||||
- `openai.provider_auth`는 provider tunnel forwarding rule만 저장하고 raw provider token 값은 request-time header에서만 읽는다. inbound IOP `Authorization` header를 provider token source로 재사용하지 않는다.
|
||||
- OpenAI request의 `metadata.workspace`는 absolute path가 필요한 route에서만 필수 검증된다.
|
||||
- provider-pool model group Chat Completions와 Responses request는 `metadata.iop_response_mode`를 명시하면 `passthrough`, `passthrough+sideband`, `transformed`, unknown 모두 거부한다. 생략하면 `passthrough`다.
|
||||
|
|
@ -165,3 +167,4 @@ sequenceDiagram
|
|||
- 2026-07-11: Responses provider route의 명시적 `passthrough+sideband` 동작을 반영. non-stream은 응답 `metadata`, stream은 `event: iop.sideband`를 확장 지점으로 사용한다.
|
||||
- 2026-07-11: provider auth forwarding과 Seulgivibe OpenAI-compatible provider family surface를 종료 검토 기준으로 보강.
|
||||
- 2026-07-12: provider-pool model group route에서 명시적 `metadata.iop_response_mode`를 거부하고, direct legacy provider route에서만 sideband extension selector를 유지하는 현재 surface를 반영.
|
||||
- 2026-07-12: Model Group Mixed Provider Dispatch 종료 검토 기준으로 selected provider capability 기반 passthrough/normalized 실행 경로를 반영.
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ Edge와 Node 사이에 현재 구현된 실행 기능을 기능 단위로 정리
|
|||
| adapter 실행 | Node가 `RunRequest.adapter`로 adapter instance를 찾고 adapter `Execute`를 호출한다. admission은 adapter `Capabilities().MaxConcurrency` 기준이다. |
|
||||
| 실행 이벤트 스트림 | Node adapter가 낸 start, delta, reasoning_delta, complete, error, cancelled 이벤트를 `RunEvent`로 Edge에 relay한다. |
|
||||
| provider raw tunnel | Edge가 `ProviderTunnelRequest`를 보내면 Node가 provider HTTP/SSE response를 열고 ordered `ProviderTunnelFrame`으로 status/header/body/end/error/usage 후보를 relay한다. |
|
||||
| mixed provider dispatch wire | provider-pool model group은 Edge service에서 provider를 먼저 선택한 뒤 OpenAI-compatible provider에는 `ProviderTunnelRequest`, Ollama/CLI/native provider에는 normalized `RunRequest`를 보낸다. |
|
||||
| usage breakdown relay | `RunEvent.usage`와 `ProviderTunnelFrame.usage`는 provider가 보고한 input/output/reasoning/cached input token count를 Edge 관측 계층으로 전달한다. |
|
||||
| terminal event 합성 | adapter가 terminal event 없이 종료하면 Node가 terminal event를 합성한다. |
|
||||
| run cancel | Edge가 `CancelRequest`를 보내면 Node run manager가 active run context를 cancel한다. |
|
||||
|
|
@ -156,6 +157,7 @@ sequenceDiagram
|
|||
- `RunEvent`는 adapter execution stream이고, `EdgeNodeEvent`는 node lifecycle/control event다.
|
||||
- `ProviderTunnelFrame.body`는 OpenAI-compatible provider passthrough의 source of truth이며 `RunEvent.delta`나 Edge event bus payload로 보내지 않는다.
|
||||
- `ProviderTunnelFrame.usage`와 `metadata`는 sideband observation 후보이며 pure passthrough body에 합쳐지지 않는다.
|
||||
- provider-pool mixed dispatch에서 `ProviderTunnelRequest`와 `RunRequest` 중 어느 wire를 사용할지는 selected provider capability에서 파생되며, client request metadata selector로 결정하지 않는다.
|
||||
- `Usage.reasoning_tokens`와 `Usage.cached_input_tokens`는 provider가 별도 보고한 경우에만 채워지는 optional breakdown이다.
|
||||
- Node local DB는 기본 `file:iop.db?cache=shared&mode=rwc`로 열린다.
|
||||
- heartbeat는 Edge와 Node transport 양쪽에서 30초 interval, 45초 wait 기준을 사용한다.
|
||||
|
|
@ -181,3 +183,4 @@ sequenceDiagram
|
|||
- 2026-07-07: 기능 목록 중심으로 축소하고 주요 흐름을 Mermaid sequence diagram으로 정리.
|
||||
- 2026-07-08: Provider raw tunnel 실행 흐름과 passthrough event/data 경계를 현재 계약 기준으로 반영.
|
||||
- 2026-07-10: `RunEvent.usage`/`ProviderTunnelFrame.usage`의 input/output/reasoning/cached input breakdown 전달 기준을 반영.
|
||||
- 2026-07-12: Model Group Mixed Provider Dispatch 종료 검토 기준으로 selected provider capability에서 파생되는 `ProviderTunnelRequest`/`RunRequest` 분기 경계를 반영.
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ Edge 설정에서 provider-pool이 어떻게 모델 실행 후보를 고르고,
|
|||
| config validation | config load가 provider id 참조, served model membership, numeric bounds, long-context budget을 검증한다. |
|
||||
| provider 후보 필터링 | dispatch는 connected node의 provider 후보 중 catalog match, enabled, healthy/available, capacity 조건을 만족하는 후보만 사용한다. |
|
||||
| capacity/priority dispatch | in-flight가 capacity 미만인 후보를 고르고, 동률이면 낮은 `priority`와 round-robin을 적용한다. |
|
||||
| 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로 분류한다. |
|
||||
|
|
@ -76,9 +77,13 @@ sequenceDiagram
|
|||
participant Node
|
||||
|
||||
Caller->>Service: SubmitRun(model)
|
||||
Service->>Queue: provider 후보 선택
|
||||
Queue-->>Service: adapter + served target
|
||||
Service->>Node: RunRequest(adapter, target)
|
||||
Service->>Queue: 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
|
||||
|
|
@ -104,6 +109,7 @@ sequenceDiagram
|
|||
- `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로 보고한다.
|
||||
|
||||
## 검증
|
||||
|
|
@ -131,3 +137,4 @@ sequenceDiagram
|
|||
- 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 가중 예시를 반영.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ last_rule_updated_at: <YYYY-MM-DD>
|
|||
| Edge artifact/bootstrap | `18080` | `18082` | `<port 또는 해당 없음>` |
|
||||
| Edge OpenAI-compatible | `18081` | `18083` | `<port 또는 해당 없음>` |
|
||||
| Edge metrics | `19092` | `19101` | `<port 또는 해당 없음>` |
|
||||
| Control Plane metrics | `19100` | `19103` | `<port 또는 해당 없음>` |
|
||||
| Prometheus UI/API | `19110` | `19111` | `<port 또는 해당 없음>` |
|
||||
| Grafana UI | `19120` | `19121` | `<port 또는 해당 없음>` |
|
||||
| PostgreSQL host publish | `15400` | `15401` | `<port 또는 해당 없음>` |
|
||||
| Redis host publish | `16300` | `16301` | `<port 또는 해당 없음>` |
|
||||
|
||||
|
|
|
|||
|
|
@ -30,17 +30,18 @@ last_rule_updated_at: 2026-07-02
|
|||
## 환경
|
||||
|
||||
- host: Docker compose 또는 Flutter client가 포함된 runtime evidence는 mac-mini `ssh fe@172.24.63.178`의 `/Users/fe/agent-work/iop-dev-corp` 기준으로 수행한다.
|
||||
- port: Control Plane HTTP `18002`, Client WS `19004`, CP-Edge wire `19005`, Edge-Node TCP transport `19006`, web/dev preview `13002`.
|
||||
- port: Control Plane HTTP `18002`, Client WS `19004`, CP-Edge wire `19005`, Edge-Node TCP transport `19006`, web/dev preview `13002`, Control Plane metrics `19104`, Prometheus `19112`, Grafana `19122`.
|
||||
- runtime: Go `1.24`, Docker compose
|
||||
- package manager: Go modules / Makefile
|
||||
- docker: compose 검증은 mac-mini에서 `.env.dev-corp.example` 기준으로 수행한다. 파일 생성 전에는 compose 검증을 차단으로 보고한다. provider-pool native runtime 검증은 `build/dev-corp-runtime/bin/control-plane`과 env override를 사용할 수 있다.
|
||||
- docker: compose 검증은 mac-mini에서 `.env.dev-corp.example` 기준으로 수행한다. provider-pool native runtime 검증은 `build/dev-corp-runtime/bin/control-plane`과 env override를 사용할 수 있다.
|
||||
- external service: postgres host publish `15402`, redis host publish `16302`
|
||||
- model endpoint:
|
||||
- operator notice: 기존 dev-corp Control Plane/UI/Edge 접속 포트는 유지하고 Control Plane metrics `19104`, Prometheus `19112`, Grafana `19122`는 관측용 정의로만 추가한다. 현재 세션에서는 dev-corp 실제 세팅을 수행하지 않는다.
|
||||
- credential: DB/Redis credential 원문은 문서에 기록하지 않는다.
|
||||
|
||||
## 명령
|
||||
|
||||
- setup: mac-mini 환경에서 `IOP_EDGE_NODE_TOKEN`을 안전하게 주입한 뒤 `docker compose --env-file .env.dev-corp.example up -d postgres redis control-plane edge web`
|
||||
- setup: mac-mini 환경에서 `IOP_EDGE_NODE_TOKEN`을 안전하게 주입한 뒤 `docker compose --env-file .env.dev-corp.example up -d postgres redis control-plane web prometheus grafana`
|
||||
- setup-native: mac-mini `/Users/fe/agent-work/iop-dev-corp`에서 `IOP_LISTEN=0.0.0.0:18002 IOP_WIRE_LISTEN=0.0.0.0:19004 IOP_EDGE_WIRE_LISTEN=0.0.0.0:19005 build/dev-corp-runtime/bin/control-plane serve --config configs/control-plane.yaml`
|
||||
- lint:
|
||||
- unit: `go test ./apps/control-plane/...`
|
||||
|
|
@ -72,6 +73,7 @@ last_rule_updated_at: 2026-07-02
|
|||
## 판정 기준
|
||||
|
||||
- control-plane service가 dev-corp stack으로 기동되고 health endpoint가 성공 응답을 반환한다.
|
||||
- Prometheus와 Grafana가 같은 dev-corp compose project에서 올라오며, Prometheus는 compose 내부 DNS로 `control-plane:9093`과 필요 시 `edge:9092`를 scrape한다.
|
||||
- wire endpoint는 Control Plane 브라우저/앱 경계로 유지되고 Node 직접 연결/스케줄링 경로로 확장되지 않는다.
|
||||
- dev-corp stack은 local/test/dev stack의 host publish 포트를 점유하지 않는다.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ last_rule_updated_at: 2026-07-11
|
|||
- repo root: 목표 checkout은 `/Users/fe/agent-work/iop-dev-corp`이다. 2026-07-02 기준 해당 checkout과 `build/dev-corp-runtime` runtime이 배포되어 있으며, `/Users/fe/iop-field`는 unrelated legacy field state로 본다.
|
||||
- sync 기준: dev-corp 배포 전 mac-mini checkout은 배포 기준 ref로 clean sync하고 dirty 변경은 보존 대상으로 보지 않는다.
|
||||
- env file: compose stack을 만들면 `.env.dev-corp.example`로 분리한다. 파일 생성 전에는 native/provider-pool profile만 기준으로 삼는다.
|
||||
- compose identity: `COMPOSE_PROJECT_NAME=iop-dev-corp-agent`, `IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`.
|
||||
- port: web/dev preview `13002`, Control Plane HTTP `18002`, Client WS `19004`, CP-Edge wire `19005`, compose Edge-Node TCP `19006`, native Edge-Node TCP 후보 `18087`, Edge admin 후보 `19094`.
|
||||
- compose identity: `COMPOSE_PROJECT_NAME=iop-dev-corp-agent`, `IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`, `IOP_COMPOSE_SUBNET=10.89.2.0/24`.
|
||||
- port: web/dev preview `13002`, Control Plane HTTP `18002`, Client WS `19004`, CP-Edge wire `19005`, compose Edge-Node TCP `19006`, native Edge-Node TCP 후보 `18087`, Edge admin 후보 `19094`, Control Plane metrics `19104`, Prometheus `19112`, Grafana `19122`.
|
||||
- operator notice: 기존 dev-corp 사용자-facing 접속점(`13002`, `18002`, Edge OpenAI-compatible 후보 `18086`, native Edge 후보 `18087`)은 변경하지 않는다. 현재 세션에서는 dev-corp 원격 접근이 불가하므로 compose/port 문서 정의만 갱신하고 실제 세팅은 수행하지 않는다.
|
||||
- optional field ports: artifact/bootstrap HTTP `18085`, Edge OpenAI-compatible HTTP `18086`, Edge metrics `19102`.
|
||||
- runtime: Go quick check는 local toolchain을 우선한다. Docker/Flutter/client/field/bootstrap/provider-pool evidence는 mac-mini와 내부 provider nodes를 사용한다.
|
||||
- package manager: Go modules / Makefile / Flutter pub
|
||||
|
|
@ -55,6 +56,9 @@ last_rule_updated_at: 2026-07-11
|
|||
| Edge artifact/bootstrap | `18080` | `18082` | `18085` |
|
||||
| Edge OpenAI-compatible | `18081` | `18083` | `18086` |
|
||||
| Edge metrics | `19092` | `19101` | `19102` |
|
||||
| Control Plane metrics | `19100` | `19103` | `19104` |
|
||||
| Prometheus UI/API | `19110` | `19111` | `19112` |
|
||||
| Grafana UI | `19120` | `19121` | `19122` |
|
||||
| PostgreSQL host publish | `15400` | `15401` | `15402` |
|
||||
| Redis host publish | `16300` | `16301` | `16302` |
|
||||
|
||||
|
|
@ -65,7 +69,8 @@ dev-corp native/provider-pool profile은 compose Edge-Node TCP `19006`이 아니
|
|||
- local quick check: 현재 checkout에서 Go quick check를 우선 실행한다.
|
||||
- remote runner: source sync/build/provider SSH 조작은 `ssh fe@172.24.63.178`의 `/Users/fe/agent-work/iop-dev-corp` 기준으로 수행할 수 있다.
|
||||
- public Edge evidence: Edge runtime evidence와 OpenAI-compatible smoke는 public `iop.ai.kr` 경로로 판정한다.
|
||||
- compose dev-corp stack: `.env.dev-corp.example`, `COMPOSE_PROJECT_NAME=iop-dev-corp-agent`, `IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`, Edge-Node TCP `19006`을 사용한다.
|
||||
- compose dev-corp stack: `.env.dev-corp.example`, `COMPOSE_PROJECT_NAME=iop-dev-corp-agent`, `IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`, `IOP_COMPOSE_SUBNET=10.89.2.0/24`, Edge-Node TCP `19006`, Control Plane metrics `19104`, Prometheus `19112`, Grafana `19122`를 사용한다.
|
||||
- compose observability: dev-corp Control Plane 관측 그룹 정의는 문서 기준으로만 유지한다. 접근 가능한 mac-mini runner에서 별도 요청이 있을 때 실제 compose 세팅/검증을 수행한다.
|
||||
- Edge direct dev-corp profile: artifact/bootstrap `18085`, OpenAI-compatible `18086`, metrics `19102`, admin `19094`를 사용한다.
|
||||
- dev-corp provider pool: 기본 public Edge-Node TCP `iop.ai.kr:18087`을 기준으로 한다. `/Users/fe/agent-work/iop-dev-corp/build/dev-corp-runtime/edge.yaml`은 mac-mini runner의 보조 산출물일 뿐 dev-corp Edge source of truth로 판정하지 않는다. retired reverse tunnel route는 Edge addr로 사용하지 않는다. 3-node/provider 세부는 `agent-test/dev-corp/edge-smoke.md`와 `agent-test/dev-corp/node-smoke.md`를 따른다.
|
||||
- current native Control Plane: 2026-07-02 기준 provider-pool runtime에서 `build/dev-corp-runtime/bin/control-plane`이 `18002/19004/19005`를 listen하고, Edge id `dev-corp-edge`가 `127.0.0.1:19005`로 연결된다. provider snapshot status 기준은 `http://127.0.0.1:18002/edges/dev-corp-edge/status`다.
|
||||
|
|
|
|||
|
|
@ -33,12 +33,13 @@ last_rule_updated_at: 2026-07-11
|
|||
## 환경
|
||||
|
||||
- host: local checkout. Flutter client 포함 검증, Docker compose, field/bootstrap, artifact server, external provider evidence가 필요하면 mac-mini runner를 사용한다.
|
||||
- port: web/dev preview `13002`, artifact/bootstrap HTTP 후보 `18085`, Edge OpenAI-compatible HTTP 후보 `18086`, Client WS `19004`, CP-Edge wire `19005`, compose Edge-Node TCP transport `19006`, native Edge-Node TCP 후보 `18087`, Edge metrics 후보 `19102`.
|
||||
- port: web/dev preview `13002`, artifact/bootstrap HTTP 후보 `18085`, Edge OpenAI-compatible HTTP 후보 `18086`, Client WS `19004`, CP-Edge wire `19005`, compose Edge-Node TCP transport `19006`, native Edge-Node TCP 후보 `18087`, Edge metrics 후보 `19102`, Control Plane metrics `19104`, Prometheus `19112`, Grafana `19122`.
|
||||
- runtime: Go `1.24`
|
||||
- package manager: Go modules / Makefile
|
||||
- docker: quick check는 Docker를 요구하지 않는다. 현재 작업 컨테이너에서는 Docker-in-Docker를 사용하지 않으며, Docker compose 검증은 mac-mini 환경에서 수행한다.
|
||||
- external service: dev-corp Control Plane 후보는 mac-mini local 기준 `http://127.0.0.1:18002`, 기본 외부 후보 `http://iop.ai.kr:18002`이다. Client wire 후보는 mac-mini local 기준 `ws://127.0.0.1:19004/client`, 기본 외부 후보 `ws://iop.ai.kr:19004/client`이다. dev-corp Edge runtime 기본 후보는 `iop.ai.kr:18087`이며, mac-mini runner/local legacy 경로를 Edge runtime 또는 Node `edge_addr`로 쓰지 않는다.
|
||||
- model endpoint: dev-corp OpenAI-compatible base URL 기본 후보 `http://digitalplatform.iop.ai.kr:18086/v1`
|
||||
- operator notice: 기존 dev-corp 사용자-facing URL은 변경하지 않는다. 현재 세션에서는 dev-corp runner 접근이 불가하므로 이 문서는 port/profile 정의와 향후 검증 절차만 기록한다.
|
||||
- credential: token/secret/API key 원문은 문서에 기록하지 않는다.
|
||||
|
||||
## 명령
|
||||
|
|
@ -47,7 +48,7 @@ last_rule_updated_at: 2026-07-11
|
|||
- lint:
|
||||
- unit: `make test`
|
||||
- smoke: `./scripts/e2e-smoke.sh`
|
||||
- compose-smoke: mac-mini 환경에서 `IOP_EDGE_NODE_TOKEN`을 안전하게 주입한 뒤 `docker compose --env-file .env.dev-corp.example up -d postgres redis control-plane edge web`
|
||||
- compose-smoke: mac-mini 환경에서 `IOP_EDGE_NODE_TOKEN`을 안전하게 주입한 뒤 `docker compose --env-file .env.dev-corp.example up -d postgres redis control-plane web prometheus grafana`
|
||||
- compose-health: `curl -fsS http://127.0.0.1:18002/healthz`
|
||||
- control-plane-status: mac-mini에서 `curl -fsS http://127.0.0.1:18002/edges/dev-corp-edge/status`
|
||||
- provider-health: mac-mini에서 `curl -fsS http://192.168.2.2:8002/health`, `curl -fsS http://192.168.2.4:8004/health`, `curl -fsS http://192.168.2.3:8004/health`
|
||||
|
|
@ -73,8 +74,9 @@ last_rule_updated_at: 2026-07-11
|
|||
|
||||
## 판정 기준
|
||||
|
||||
- dev-corp compose는 `COMPOSE_PROJECT_NAME=iop-dev-corp-agent`와 `IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`을 사용한다.
|
||||
- dev-corp compose는 `COMPOSE_PROJECT_NAME=iop-dev-corp-agent`, `IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`, `IOP_COMPOSE_SUBNET=10.89.2.0/24`를 사용한다.
|
||||
- dev-corp compose가 local/test/dev 포트를 점유하지 않는다.
|
||||
- dev-corp Control Plane 관측 그룹은 `control-plane`, `web`, `postgres`, `redis`, `prometheus`, `grafana`를 한 compose project로 묶고, Edge는 연결 검증이 필요할 때 같은 compose의 인접 service로 붙인다.
|
||||
- edge console에서 메시지 2회가 각각 기대 payload를 포함한 `[node-*-message]`로 표시되고, command 결과가 edge 화면에 도착한다.
|
||||
- node 로컬 `[node-message]` payload 라인 목록과 edge `[node-*-message]` payload 라인 목록이 run별로 내용/순서까지 동일해야 한다.
|
||||
- Node bootstrap UX는 Edge가 출력한 완성된 bootstrap URL과 실제 positional token 값 하나만 포함하는 한 줄 명령 형태를 유지한다.
|
||||
|
|
|
|||
|
|
@ -30,17 +30,18 @@ last_rule_updated_at: 2026-06-12
|
|||
## 환경
|
||||
|
||||
- host: Docker compose 또는 Flutter client가 포함된 runtime evidence는 원격 runner `ssh toki@toki-labs.com`의 `/Users/toki/agent-work/iop-dev` 기준으로 수행한다.
|
||||
- port: Control Plane HTTP `18001` (host-facing), Portal/Control Plane wire test endpoint `19001`, CP-Edge wire `19002`, Edge-Node TCP transport `19003`, web/dev preview `13001`.
|
||||
- port: Control Plane HTTP `18001` (host-facing), Portal/Control Plane wire test endpoint `19001`, CP-Edge wire `19002`, Edge-Node TCP transport `19003`, web/dev preview `13001`, Control Plane metrics `19103`, Prometheus `19111`, Grafana `19121`.
|
||||
- runtime: Go `1.24`, Docker compose
|
||||
- package manager: Go modules / Makefile
|
||||
- docker: `docker-compose.yml`에 postgres, redis, control-plane, edge, web service가 정의되어 있다. dev 검증은 `docker compose --env-file .env.dev.example ...`로 수행한다.
|
||||
- docker: `docker-compose.yml`에 postgres, redis, control-plane, web, prometheus, grafana service가 Control Plane 관측 그룹으로 정의되어 있다. edge service는 Control Plane-Edge 연결 검증을 위한 인접 runtime service다. dev 검증은 `docker compose --env-file .env.dev.example ...`로 수행한다.
|
||||
- external service: postgres host publish `15401`, redis host publish `16301`
|
||||
- model endpoint:
|
||||
- operator notice: 기존 dev Control Plane/UI/Edge 접속 포트는 유지하고 Control Plane metrics `19103`, Prometheus `19111`, Grafana `19121`만 관측용으로 추가한다. Grafana/Prometheus는 원격 runner 내부 또는 SSH 터널 기준이다.
|
||||
- credential: DB/Redis credential 원문은 문서에 기록하지 않는다.
|
||||
|
||||
## 명령
|
||||
|
||||
- setup: `IOP_EDGE_NODE_TOKEN=<token> docker compose --env-file .env.dev.example up -d postgres redis control-plane edge web`
|
||||
- setup: `IOP_EDGE_NODE_TOKEN=<token> docker compose --env-file .env.dev.example up -d postgres redis control-plane web prometheus grafana`
|
||||
- lint:
|
||||
- unit: `go test ./apps/control-plane/...`
|
||||
- smoke: `curl -fsS http://127.0.0.1:18001/healthz`, `curl -fsS http://127.0.0.1:18001/readyz`
|
||||
|
|
@ -63,6 +64,7 @@ last_rule_updated_at: 2026-06-12
|
|||
## 판정 기준
|
||||
|
||||
- control-plane service가 dev stack으로 기동되고 health endpoint가 성공 응답을 반환한다.
|
||||
- Prometheus와 Grafana가 같은 dev compose project에서 올라오며, Prometheus는 compose 내부 DNS로 `control-plane:9093`과 dev native Edge metrics `host.docker.internal:19101`을 scrape한다.
|
||||
- wire endpoint는 Control Plane 브라우저/앱 경계로 유지되고 Node 직접 연결/스케줄링 경로로 확장되지 않는다.
|
||||
- dev stack은 local/test stack의 host publish 포트(`13000`, `18000`, `19080`, `19081`, `15400`, `16300`)를 점유하지 않는다.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ last_rule_updated_at: 2026-06-24
|
|||
- repo root: 명령은 원격 checkout `/Users/toki/agent-work/iop-dev` 기준으로 실행한다.
|
||||
- sync 기준: dev 배포 전 원격 runner checkout은 `git fetch origin main`, `git reset --hard origin/main`, `git clean -fd`로 clean 상태를 만든 뒤 빌드한다. dev runner의 dirty 변경은 보존 대상으로 보지 않는다.
|
||||
- env file: dev stack은 `docker compose --env-file .env.dev.example ...`로 명시한다.
|
||||
- compose identity: `COMPOSE_PROJECT_NAME=iop-dev-agent`, `IOP_COMPOSE_NETWORK=iop-dev-agent-net`.
|
||||
- port: web/dev preview `13001`, Control Plane HTTP `18001`, Portal/Control Plane wire test endpoint `19001`, CP-Edge wire `19002`, Edge-Node TCP transport `19003`, Postgres host publish `15401`, Redis host publish `16301`.
|
||||
- compose identity: `COMPOSE_PROJECT_NAME=iop-dev-agent`, `IOP_COMPOSE_NETWORK=iop-dev-agent-net`, `IOP_COMPOSE_SUBNET=10.89.1.0/24`.
|
||||
- port: web/dev preview `13001`, Control Plane HTTP `18001`, Portal/Control Plane wire test endpoint `19001`, CP-Edge wire `19002`, Edge-Node TCP transport `19003`, Postgres host publish `15401`, Redis host publish `16301`, Control Plane metrics `19103`, Prometheus `19111`, Grafana `19121`.
|
||||
- operator notice: 기존 테스트 사용자-facing 접속점(`13001`, `18001`, Edge OpenAI-compatible 후보 `18083`)은 변경하지 않는다. 이번 profile 변경은 Control Plane/Edge 관측용 metrics/Prometheus/Grafana 포트 추가로만 공지한다. Grafana/Prometheus는 원격 runner 내부 또는 SSH 터널로 접근한다.
|
||||
- optional dev field ports: artifact/bootstrap HTTP `18082`, Edge OpenAI-compatible HTTP `18083`, Edge metrics `19101`. 이 값은 compose 기본 stack에 publish되지 않으며 field/bootstrap 또는 Edge direct dev profile이 필요할 때만 사용한다.
|
||||
- runtime: Go quick check는 local toolchain을 우선한다. Flutter client 포함 검증과 Docker/code-server/full-cycle runtime은 원격 runner를 사용한다.
|
||||
- package manager: Go modules / Makefile / Flutter pub
|
||||
|
|
@ -48,6 +49,9 @@ last_rule_updated_at: 2026-06-24
|
|||
| Edge artifact/bootstrap | `18080` | `18082` |
|
||||
| Edge OpenAI-compatible | `18081` | `18083` |
|
||||
| Edge metrics | `19092` | `19101` |
|
||||
| Control Plane metrics | `19100` | `19103` |
|
||||
| Prometheus UI/API | `19110` | `19111` |
|
||||
| Grafana UI | `19120` | `19121` |
|
||||
| PostgreSQL host publish | `15400` | `15401` |
|
||||
| Redis host publish | `16300` | `16301` |
|
||||
|
||||
|
|
@ -57,7 +61,8 @@ dev-runtime provider pool은 compose Edge-Node TCP `19003`이 아니라 native E
|
|||
|
||||
- local quick check: 현재 checkout에서 Go quick check를 우선 실행한다.
|
||||
- remote runner: dev runtime evidence, Flutter client, Docker compose, field/bootstrap, 외부 runtime evidence는 `ssh toki@toki-labs.com`의 `/Users/toki/agent-work/iop-dev` 기준으로 수행한다.
|
||||
- compose dev stack: `.env.dev.example`, `COMPOSE_PROJECT_NAME=iop-dev-agent`, `IOP_COMPOSE_NETWORK=iop-dev-agent-net`, Edge-Node TCP `19003`을 사용한다.
|
||||
- compose dev stack: `.env.dev.example`, `COMPOSE_PROJECT_NAME=iop-dev-agent`, `IOP_COMPOSE_NETWORK=iop-dev-agent-net`, `IOP_COMPOSE_SUBNET=10.89.1.0/24`, Edge-Node TCP `19003`을 사용한다.
|
||||
- compose observability: Control Plane 관측 그룹은 `postgres`, `redis`, `control-plane`, `web`, `prometheus`, `grafana`만 기본 세팅한다. Edge service는 endpoint 변경 없이 별도 연결 검증이 필요할 때만 붙인다. dev Prometheus는 기존 native Edge metrics `host.docker.internal:19101`을 scrape한다.
|
||||
- Edge direct dev profile: artifact/bootstrap `18082`, OpenAI-compatible `18083`, metrics `19101`을 필요할 때만 사용한다.
|
||||
- dev-runtime provider pool: `/Users/toki/agent-work/iop-dev/build/dev-runtime/edge.yaml`과 Edge-Node TCP `toki-labs.com:18084`를 사용한다. 3-node/provider 세부는 `agent-test/dev/edge-smoke.md`와 `agent-test/dev/node-smoke.md`를 따른다.
|
||||
- external provider field: GX10 vLLM은 `ssh toki@192.168.0.91`, OneXPlayer Lemonade는 현재 작업 호스트에서 `ssh r0bin@192.168.0.59`로 직접 접속해 확인한다. OneXPlayer 접속은 원격 runner 경유를 필수 조건으로 보지 않는다.
|
||||
|
|
|
|||
|
|
@ -33,12 +33,13 @@ last_rule_updated_at: 2026-06-12
|
|||
## 환경
|
||||
|
||||
- host: local checkout. Flutter client 포함 검증, Docker compose, field/bootstrap, artifact server, external CLI profile evidence가 필요하면 원격 runner를 사용한다.
|
||||
- port: web/dev preview `13001`, artifact/bootstrap HTTP 후보 `18082`, Edge OpenAI-compatible HTTP 후보 `18083`, Portal/Control Plane wire test endpoint `19001`, CP-Edge wire `19002`, Edge-Node TCP transport `19003`, Edge metrics 후보 `19101`.
|
||||
- port: web/dev preview `13001`, artifact/bootstrap HTTP 후보 `18082`, Edge OpenAI-compatible HTTP 후보 `18083`, Portal/Control Plane wire test endpoint `19001`, CP-Edge wire `19002`, Edge-Node TCP transport `19003`, Edge metrics 후보 `19101`, Control Plane metrics `19103`, Prometheus `19111`, Grafana `19121`.
|
||||
- runtime: Go `1.24`
|
||||
- package manager: Go modules / Makefile
|
||||
- docker: quick check는 Docker를 요구하지 않는다. 현재 작업 컨테이너에서는 Docker-in-Docker를 사용하지 않으며, Docker compose 검증은 원격 runner 또는 code-server 환경에서 수행한다.
|
||||
- external service: dev Control Plane HTTP `http://toki-labs.com:18001`, dev Client wire `ws://toki-labs.com:19001/client`, dev Edge runtime 후보 `toki-labs.com:19003`
|
||||
- model endpoint: dev OpenAI-compatible base URL 후보 `http://toki-labs.com:18083/v1`
|
||||
- operator notice: 기존 테스트 사용자 URL은 변경하지 않는다. Grafana `127.0.0.1:19121`, Prometheus `127.0.0.1:19111`은 원격 runner 내부 또는 SSH 터널로 접근하는 운영/검증자용 신규 관측 endpoint다.
|
||||
- credential: token/secret 원문은 문서에 기록하지 않는다.
|
||||
|
||||
## 명령
|
||||
|
|
@ -47,7 +48,7 @@ last_rule_updated_at: 2026-06-12
|
|||
- lint:
|
||||
- unit: `make test`
|
||||
- smoke: `./scripts/e2e-smoke.sh`
|
||||
- compose-smoke: `IOP_EDGE_NODE_TOKEN=<token> docker compose --env-file .env.dev.example up -d postgres redis control-plane edge web`
|
||||
- compose-smoke: `IOP_EDGE_NODE_TOKEN=<token> docker compose --env-file .env.dev.example up -d postgres redis control-plane web prometheus grafana`
|
||||
- compose-health: `curl -fsS http://127.0.0.1:18001/healthz`
|
||||
- e2e: `make test-e2e`
|
||||
- model: `./scripts/e2e-openai-ollama.sh`
|
||||
|
|
@ -69,8 +70,11 @@ last_rule_updated_at: 2026-06-12
|
|||
|
||||
## 판정 기준
|
||||
|
||||
- dev compose는 `COMPOSE_PROJECT_NAME=iop-dev-agent`와 `IOP_COMPOSE_NETWORK=iop-dev-agent-net`을 사용한다.
|
||||
- dev compose는 `COMPOSE_PROJECT_NAME=iop-dev-agent`, `IOP_COMPOSE_NETWORK=iop-dev-agent-net`, `IOP_COMPOSE_SUBNET=10.89.1.0/24`를 사용한다.
|
||||
- dev compose가 local/test 포트 `13000`, `18000`, `19080`, `19081`, `15400`, `16300`을 점유하지 않는다.
|
||||
- dev Control Plane 관측 그룹은 `control-plane`, `web`, `postgres`, `redis`, `prometheus`, `grafana`를 한 compose project로 묶고, Edge는 연결 검증이 필요할 때 같은 compose의 인접 service로 붙인다.
|
||||
- dev 관측 세팅은 기존 Edge/OpenAI-compatible 후보 `18083` 또는 기존 사용자-facing URL을 바꾸지 않는다.
|
||||
- Prometheus는 dev native Edge metrics `host.docker.internal:19101`을 scrape해 사용자별 OpenAI-compatible token usage metric을 수집한다.
|
||||
- edge console에서 메시지 2회가 각각 기대 payload를 포함한 `[node-*-message]`로 표시되고, command 결과가 edge 화면에 도착한다.
|
||||
- node 로컬 `[node-message]` payload 라인 목록과 edge `[node-*-message]` payload 라인 목록이 run별로 내용/순서까지 동일해야 한다.
|
||||
- Node bootstrap UX는 `curl -fsSL <완성된-bootstrap-url> | bash -s <token>` 형태를 유지한다.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ type controlPlaneConfig struct {
|
|||
Level string `yaml:"level"`
|
||||
Pretty bool `yaml:"pretty"`
|
||||
} `yaml:"logging"`
|
||||
Metrics struct {
|
||||
Port int `yaml:"port"`
|
||||
} `yaml:"metrics"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
@ -90,6 +93,7 @@ func defaultConfig() controlPlaneConfig {
|
|||
cfg.Redis.URL = ""
|
||||
cfg.Redis.KeyPrefix = "iop:control-plane:"
|
||||
cfg.Logging.Level = "info"
|
||||
cfg.Metrics.Port = 0
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
|
@ -127,6 +131,12 @@ func applyEnvOverrides(cfg *controlPlaneConfig) {
|
|||
if redisKeyPrefix := os.Getenv("IOP_REDIS_KEY_PREFIX"); redisKeyPrefix != "" {
|
||||
cfg.Redis.KeyPrefix = redisKeyPrefix
|
||||
}
|
||||
if metricsPort := os.Getenv("IOP_METRICS_PORT"); metricsPort != "" {
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(metricsPort, "%d", &port); err == nil {
|
||||
cfg.Metrics.Port = port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func databaseLogFields(rawURL string) []zap.Field {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ func TestLoadConfigRepositoryLocalConfig(t *testing.T) {
|
|||
t.Setenv("IOP_REDIS_KEY_PREFIX", "")
|
||||
t.Setenv("IOP_WIRE_LISTEN", "")
|
||||
t.Setenv("IOP_EDGE_WIRE_LISTEN", "")
|
||||
t.Setenv("IOP_METRICS_PORT", "")
|
||||
cfg, err := loadConfig("../../../../configs/control-plane.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("load config: %v", err)
|
||||
|
|
@ -97,6 +98,9 @@ func TestLoadConfigRepositoryLocalConfig(t *testing.T) {
|
|||
if cfg.Server.Listen != "0.0.0.0:18000" {
|
||||
t.Fatalf("listen: got %q want %q", cfg.Server.Listen, "0.0.0.0:18000")
|
||||
}
|
||||
if cfg.Metrics.Port != 9093 {
|
||||
t.Fatalf("metrics port: got %d want 9093", cfg.Metrics.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigEnvOverrides(t *testing.T) {
|
||||
|
|
@ -110,9 +114,11 @@ redis:
|
|||
const wantDatabase = "postgres://user:pass@db:5432/iop-control-plane-dev?sslmode=disable"
|
||||
const wantRedis = "redis://redis:6379/2"
|
||||
const wantRedisKeyPrefix = "iop:control-plane:dev:"
|
||||
const wantMetricsPort = 19103
|
||||
t.Setenv("IOP_DATABASE_URL", wantDatabase)
|
||||
t.Setenv("IOP_REDIS_URL", wantRedis)
|
||||
t.Setenv("IOP_REDIS_KEY_PREFIX", wantRedisKeyPrefix)
|
||||
t.Setenv("IOP_METRICS_PORT", fmt.Sprint(wantMetricsPort))
|
||||
|
||||
cfg, err := loadConfig(path)
|
||||
if err != nil {
|
||||
|
|
@ -127,6 +133,9 @@ redis:
|
|||
if cfg.Redis.KeyPrefix != wantRedisKeyPrefix {
|
||||
t.Fatalf("redis key prefix: got %q want %q", cfg.Redis.KeyPrefix, wantRedisKeyPrefix)
|
||||
}
|
||||
if cfg.Metrics.Port != wantMetricsPort {
|
||||
t.Fatalf("metrics port: got %d want %d", cfg.Metrics.Port, wantMetricsPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseLogFieldsDoNotExposeCredentials(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"go.uber.org/zap"
|
||||
|
||||
"iop/apps/control-plane/internal/wire"
|
||||
"iop/packages/go/observability"
|
||||
)
|
||||
|
||||
func run(ctx context.Context, cfg controlPlaneConfig, logger *zap.Logger) error {
|
||||
|
|
@ -31,6 +32,13 @@ func run(ctx context.Context, cfg controlPlaneConfig, logger *zap.Logger) error
|
|||
if cfg.Redis.URL != "" {
|
||||
logger.Info("control-plane redis configured", redisLogFields(cfg.Redis.URL, cfg.Redis.KeyPrefix)...)
|
||||
}
|
||||
if cfg.Metrics.Port > 0 {
|
||||
go func() {
|
||||
if err := observability.ServeMetrics(cfg.Metrics.Port); err != nil {
|
||||
logger.Warn("control-plane metrics server exited", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
clientServer, err := wire.NewClientServer(cfg.Server.WireListen, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wire server: %w", err)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ logging:
|
|||
path: ""
|
||||
|
||||
metrics:
|
||||
port: 0
|
||||
port: ${IOP_EDGE_METRICS_CONTAINER_PORT}
|
||||
|
||||
control_plane:
|
||||
enabled: true
|
||||
|
|
|
|||
|
|
@ -82,9 +82,11 @@ openai:
|
|||
# target_header: "Authorization"
|
||||
# scheme: "Bearer"
|
||||
# required: true
|
||||
# Chat Completions provider routes default to metadata.iop_response_mode=passthrough.
|
||||
# Callers can opt into passthrough+sideband or transformed per request metadata;
|
||||
# this config has no global response-mode switch.
|
||||
# Provider-pool model group routes do not accept metadata.iop_response_mode
|
||||
# as a caller selector. The selected provider determines the execution path:
|
||||
# OpenAI-compatible providers use passthrough, Ollama/CLI/native providers use
|
||||
# normalized execution. Direct legacy provider routes may still opt into the
|
||||
# documented passthrough+sideband extension per request metadata.
|
||||
node: ""
|
||||
adapter: "ollama"
|
||||
target: ""
|
||||
|
|
@ -156,6 +158,16 @@ models:
|
|||
default_thinking_token_budget: 1024
|
||||
providers:
|
||||
mac-mlx-vllm: "mlx-community/Qwen3.6-35B-A3B-4bit"
|
||||
# Same model group can keep a normalized-only Ollama provider as a low
|
||||
# capacity/high priority fallback candidate. Clients do not select this
|
||||
# execution path; capacity + priority + availability choose the provider.
|
||||
ollama-local: "qwen3.6:35b"
|
||||
- id: "local-llama3"
|
||||
display_name: "Local Llama 3"
|
||||
context_window_tokens: 8192
|
||||
providers:
|
||||
# Ollama-only model groups are valid and run through normalized execution.
|
||||
ollama-local: "llama3.1:8b"
|
||||
# Example: CLI providers (codex, codex-exec) do not serve models.
|
||||
# They are declared in nodes[].providers[] for dispatch, not in models[].
|
||||
# - id: "codex-task"
|
||||
|
|
@ -233,12 +245,26 @@ nodes:
|
|||
- "mlx-community/Qwen3.6-35B-A3B-4bit"
|
||||
health: "healthy"
|
||||
capacity: 2
|
||||
priority: 0
|
||||
# Long-context admission budget: total_context_tokens >= 262144 * 1.
|
||||
# mac-mlx-vllm runtime KV budget is 262144 (max_kv_size / max_request_tokens),
|
||||
# so only one full 262144-window long request fits at a time. long_context_capacity
|
||||
# is the long-slot count, not the normal capacity (2); do not conflate them.
|
||||
total_context_tokens: 262144
|
||||
long_context_capacity: 1
|
||||
# Ollama provider kept in the provider pool with explicit low concurrency.
|
||||
# It is not removed from mixed groups; operators weight it with capacity
|
||||
# and priority instead.
|
||||
- id: "ollama-local"
|
||||
type: "ollama"
|
||||
category: "local_inference"
|
||||
base_url: "http://127.0.0.1:11434"
|
||||
models:
|
||||
- "qwen3.6:35b"
|
||||
- "llama3.1:8b"
|
||||
health: "healthy"
|
||||
capacity: 1
|
||||
priority: 50
|
||||
# Seulgivibe OpenAI-compatible provider examples. Keep endpoint values
|
||||
# illustrative and provide user tokens per request via openai.provider_auth.
|
||||
# - id: "seulgivibe-claude"
|
||||
|
|
|
|||
9
configs/grafana/provisioning/datasources/prometheus.yml
Normal file
9
configs/grafana/provisioning/datasources/prometheus.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: IOP Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
26
configs/prometheus/prometheus.dev.yml
Normal file
26
configs/prometheus/prometheus.dev.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: prometheus
|
||||
static_configs:
|
||||
- targets:
|
||||
- prometheus:9090
|
||||
|
||||
- job_name: iop-control-plane
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- control-plane:9093
|
||||
labels:
|
||||
component: control-plane
|
||||
|
||||
- job_name: iop-edge-native
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- host.docker.internal:19101
|
||||
labels:
|
||||
component: edge
|
||||
runtime: native-dev
|
||||
25
configs/prometheus/prometheus.yml
Normal file
25
configs/prometheus/prometheus.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: prometheus
|
||||
static_configs:
|
||||
- targets:
|
||||
- prometheus:9090
|
||||
|
||||
- job_name: iop-control-plane
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- control-plane:9093
|
||||
labels:
|
||||
component: control-plane
|
||||
|
||||
- job_name: iop-edge
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- edge:9092
|
||||
labels:
|
||||
component: edge
|
||||
|
|
@ -41,10 +41,12 @@ services:
|
|||
IOP_DATABASE_URL: "postgres://${IOP_POSTGRES_USER:-iop}:${IOP_POSTGRES_PASSWORD:-iop_dev_password}@postgres:5432/${IOP_POSTGRES_DB:-iop-control-plane-dev}?sslmode=disable"
|
||||
IOP_REDIS_URL: "redis://redis:6379/${IOP_REDIS_DB:-2}"
|
||||
IOP_REDIS_KEY_PREFIX: "${IOP_REDIS_KEY_PREFIX:-iop:control-plane:dev:}"
|
||||
IOP_METRICS_PORT: "${IOP_CONTROL_PLANE_METRICS_CONTAINER_PORT:-9093}"
|
||||
ports:
|
||||
- "${IOP_CONTROL_PLANE_HTTP_BIND:-0.0.0.0}:${IOP_CONTROL_PLANE_HTTP_PORT:-18000}:18000"
|
||||
- "${IOP_CONTROL_PLANE_WIRE_BIND:-0.0.0.0}:${IOP_CONTROL_PLANE_WIRE_PORT:-19080}:19080"
|
||||
- "${IOP_CONTROL_PLANE_EDGE_WIRE_BIND:-0.0.0.0}:${IOP_CONTROL_PLANE_EDGE_WIRE_PORT:-19081}:19081"
|
||||
- "${IOP_CONTROL_PLANE_METRICS_BIND:-127.0.0.1}:${IOP_CONTROL_PLANE_METRICS_PORT:-19100}:${IOP_CONTROL_PLANE_METRICS_CONTAINER_PORT:-9093}"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
@ -65,12 +67,14 @@ services:
|
|||
IOP_EDGE_ID: "${IOP_EDGE_ID:-edge-compose}"
|
||||
IOP_EDGE_NAME: "${IOP_EDGE_NAME:-Compose Edge}"
|
||||
IOP_EDGE_LISTEN: "0.0.0.0:9090"
|
||||
IOP_EDGE_METRICS_CONTAINER_PORT: "${IOP_EDGE_METRICS_CONTAINER_PORT:-9092}"
|
||||
IOP_CONTROL_PLANE_EDGE_WIRE_ADDR: "control-plane:19081"
|
||||
IOP_EDGE_NODE_ID: "${IOP_EDGE_NODE_ID:-external-node}"
|
||||
IOP_EDGE_NODE_ALIAS: "${IOP_EDGE_NODE_ALIAS:-external-node}"
|
||||
IOP_EDGE_NODE_TOKEN: "${IOP_EDGE_NODE_TOKEN:-dev-node-token-change-me}"
|
||||
ports:
|
||||
- "${IOP_EDGE_NODE_BIND:-0.0.0.0}:${IOP_EDGE_NODE_PORT:-19090}:9090"
|
||||
- "${IOP_EDGE_METRICS_BIND:-127.0.0.1}:${IOP_EDGE_METRICS_PORT:-19092}:${IOP_EDGE_METRICS_CONTAINER_PORT:-9092}"
|
||||
depends_on:
|
||||
control-plane:
|
||||
condition: service_healthy
|
||||
|
|
@ -89,13 +93,46 @@ services:
|
|||
control-plane:
|
||||
condition: service_healthy
|
||||
|
||||
prometheus:
|
||||
image: ${IOP_PROMETHEUS_IMAGE:-prom/prometheus:latest}
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--web.enable-lifecycle"
|
||||
ports:
|
||||
- "${IOP_PROMETHEUS_BIND:-127.0.0.1}:${IOP_PROMETHEUS_PORT:-19110}:9090"
|
||||
volumes:
|
||||
- ${IOP_PROMETHEUS_CONFIG:-./configs/prometheus/prometheus.yml}:/etc/prometheus/prometheus.yml:ro
|
||||
- control-plane-prometheus:/prometheus
|
||||
depends_on:
|
||||
control-plane:
|
||||
condition: service_healthy
|
||||
|
||||
grafana:
|
||||
image: ${IOP_GRAFANA_IMAGE:-grafana/grafana-oss:latest}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_USER: "${IOP_GRAFANA_ADMIN_USER:-admin}"
|
||||
GF_SECURITY_ADMIN_PASSWORD: "${IOP_GRAFANA_ADMIN_PASSWORD:-iop_dev_admin}"
|
||||
ports:
|
||||
- "${IOP_GRAFANA_BIND:-0.0.0.0}:${IOP_GRAFANA_PORT:-19120}:3000"
|
||||
volumes:
|
||||
- ./configs/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- control-plane-grafana:/var/lib/grafana
|
||||
depends_on:
|
||||
prometheus:
|
||||
condition: service_started
|
||||
|
||||
volumes:
|
||||
control-plane-postgres:
|
||||
control-plane-redis:
|
||||
control-plane-prometheus:
|
||||
control-plane-grafana:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${IOP_COMPOSE_NETWORK:-iop-dev-net}
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 10.89.0.0/24
|
||||
- subnet: ${IOP_COMPOSE_SUBNET:-10.89.0.0/24}
|
||||
|
|
|
|||
|
|
@ -385,4 +385,12 @@ Grafana table 구성 예:
|
|||
|
||||
## Appendix: Grafana Scrape Target
|
||||
|
||||
Edge metrics는 보통 `localhost:19092` (local) 또는 해당 Edge metrics port에서 Prometheus scrape된다. Grafana data source에 Prometheus를 추가하고 위 PromQL을 테스트 패널에서 검증한다.
|
||||
Compose 관측 스택에서는 Prometheus가 내부 Docker DNS로 `control-plane:9093`을 scrape하고, Grafana datasource는 `http://prometheus:9090`을 사용한다. dev profile은 기존 native Edge endpoint를 바꾸지 않기 위해 `host.docker.internal:19101`도 scrape한다.
|
||||
|
||||
Host publish 기본 포트:
|
||||
|
||||
- local/test: Prometheus `19110`, Grafana `19120`, Control Plane metrics `19100`, Edge metrics `19092`
|
||||
- dev: Prometheus `19111`, Grafana `19121`, Control Plane metrics `19103`, Edge metrics `19101`
|
||||
- dev-corp compose: Prometheus `19112`, Grafana `19122`, Control Plane metrics `19104`, Edge metrics `19102`
|
||||
|
||||
이 관측 스택은 기존 테스트 사용자-facing endpoint를 변경하지 않는다. dev 기준 기존 UI/Control Plane/Edge OpenAI-compatible 접속점은 유지하며, Grafana/Prometheus/metrics 포트만 운영자용으로 추가한다. dev Grafana/Prometheus는 기본적으로 원격 runner 내부 또는 SSH 터널 접근 기준이다. dev-corp는 현재 접근 가능한 runner가 없을 때 문서 정의만 갱신하고 실제 compose 세팅은 별도 세션에서 수행한다.
|
||||
|
|
|
|||
|
|
@ -2264,6 +2264,57 @@ nodes:
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadEdge_ConfigExampleMixedAndOllamaOnlyProviderGroups(t *testing.T) {
|
||||
f := filepath.Join("..", "..", "..", "configs", "edge.yaml")
|
||||
cfg, err := config.LoadEdge(f)
|
||||
if err != nil {
|
||||
t.Fatalf("load example config: %v", err)
|
||||
}
|
||||
|
||||
models := map[string]config.ModelCatalogEntry{}
|
||||
for _, m := range cfg.Models {
|
||||
models[m.ID] = m
|
||||
}
|
||||
mixed, ok := models["qwen3.6:35b"]
|
||||
if !ok {
|
||||
t.Fatal("expected qwen3.6:35b mixed model group in example config")
|
||||
}
|
||||
if mixed.Providers["mac-mlx-vllm"] != "mlx-community/Qwen3.6-35B-A3B-4bit" {
|
||||
t.Fatalf("expected mac-mlx-vllm served model in mixed group, got %+v", mixed.Providers)
|
||||
}
|
||||
if mixed.Providers["ollama-local"] != "qwen3.6:35b" {
|
||||
t.Fatalf("expected ollama-local fallback in mixed group, got %+v", mixed.Providers)
|
||||
}
|
||||
ollamaOnly, ok := models["local-llama3"]
|
||||
if !ok {
|
||||
t.Fatal("expected local-llama3 Ollama-only model group in example config")
|
||||
}
|
||||
if len(ollamaOnly.Providers) != 1 || ollamaOnly.Providers["ollama-local"] != "llama3.1:8b" {
|
||||
t.Fatalf("expected local-llama3 to route only to ollama-local, got %+v", ollamaOnly.Providers)
|
||||
}
|
||||
|
||||
providers := map[string]config.NodeProviderConf{}
|
||||
for _, n := range cfg.Nodes {
|
||||
for _, p := range n.Providers {
|
||||
providers[p.ID] = p
|
||||
}
|
||||
}
|
||||
vllm := providers["mac-mlx-vllm"]
|
||||
ollama := providers["ollama-local"]
|
||||
if vllm.Type == "" || ollama.Type == "" {
|
||||
t.Fatalf("expected both mac-mlx-vllm and ollama-local providers, got %+v", providers)
|
||||
}
|
||||
if config.NormalizeProviderType(vllm.Type) != "openai_compat" {
|
||||
t.Fatalf("mac-mlx-vllm should classify as OpenAI-compatible, got %q", vllm.Type)
|
||||
}
|
||||
if config.NormalizeProviderType(ollama.Type) != "ollama" {
|
||||
t.Fatalf("ollama-local should classify as normalized Ollama, got %q", ollama.Type)
|
||||
}
|
||||
if ollama.Capacity != 1 || ollama.Priority <= vllm.Priority {
|
||||
t.Fatalf("expected Ollama example to use lower capacity and higher priority number than vLLM, got vllm=%+v ollama=%+v", vllm, ollama)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEdge_SeulgivibeProviderFirstAliases(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
f := filepath.Join(dir, "edge.yaml")
|
||||
|
|
|
|||
Loading…
Reference in a new issue