diff --git a/.env.dev-corp.example b/.env.dev-corp.example new file mode 100644 index 0000000..4595a00 --- /dev/null +++ b/.env.dev-corp.example @@ -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: diff --git a/.env.dev.example b/.env.dev.example index 7f67683..efeaf5d 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -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 diff --git a/.env.example b/.env.example index 5a9c950..b65050c 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/agent-contract/inner/edge-config-runtime-refresh.md b/agent-contract/inner/edge-config-runtime-refresh.md index 9542300..00bde9b 100644 --- a/agent-contract/inner/edge-config-runtime-refresh.md +++ b/agent-contract/inner/edge-config-runtime-refresh.md @@ -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 안에서 중복되면 안 된다. diff --git a/agent-contract/inner/edge-node-runtime-wire.md b/agent-contract/inner/edge-node-runtime-wire.md index 8af6e04..4a1b2a9 100644 --- a/agent-contract/inner/edge-node-runtime-wire.md +++ b/agent-contract/inner/edge-node-runtime-wire.md @@ -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`로 적용/재시작 필요/실패를 응답한다. diff --git a/agent-contract/outer/openai-compatible-api.md b/agent-contract/outer/openai-compatible-api.md index e62d98c..3b5c38c 100644 --- a/agent-contract/outer/openai-compatible-api.md +++ b/agent-contract/outer/openai-compatible-api.md @@ -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를 결정한다. diff --git a/agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md b/agent-roadmap/archive/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md similarity index 72% rename from agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md rename to agent-roadmap/archive/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md index 21175fd..63d4d2b 100644 --- a/agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md +++ b/agent-roadmap/archive/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md @@ -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 - 확인 필요: 없음 diff --git a/agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md b/agent-roadmap/archive/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md similarity index 94% rename from agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md rename to agent-roadmap/archive/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md index 626a8ea..9ae33f9 100644 --- a/agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md +++ b/agent-roadmap/archive/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md @@ -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를 따른다. diff --git a/agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md b/agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md index 7ef10bb..a71bd9a 100644 --- a/agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md +++ b/agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md @@ -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 하이브리드 라우팅과 컨텍스트 최적화 diff --git a/agent-spec/input/openai-compatible-surface.md b/agent-spec/input/openai-compatible-surface.md index e03dacb..9c415c7 100644 --- a/agent-spec/input/openai-compatible-surface.md +++ b/agent-spec/input/openai-compatible-surface.md @@ -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 실행 경로를 반영. diff --git a/agent-spec/runtime/edge-node-execution.md b/agent-spec/runtime/edge-node-execution.md index 727ea26..3123b10 100644 --- a/agent-spec/runtime/edge-node-execution.md +++ b/agent-spec/runtime/edge-node-execution.md @@ -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` 분기 경계를 반영. diff --git a/agent-spec/runtime/provider-pool-config-refresh.md b/agent-spec/runtime/provider-pool-config-refresh.md index 962d886..d1bedc1 100644 --- a/agent-spec/runtime/provider-pool-config-refresh.md +++ b/agent-spec/runtime/provider-pool-config-refresh.md @@ -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 가중 예시를 반영. diff --git a/agent-test/_templates/env-rules-template.md b/agent-test/_templates/env-rules-template.md index f68b358..e0380c6 100644 --- a/agent-test/_templates/env-rules-template.md +++ b/agent-test/_templates/env-rules-template.md @@ -46,6 +46,9 @@ last_rule_updated_at: | 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` | `` | diff --git a/agent-test/dev-corp/control-plane-smoke.md b/agent-test/dev-corp/control-plane-smoke.md index 446b7bb..704db9f 100644 --- a/agent-test/dev-corp/control-plane-smoke.md +++ b/agent-test/dev-corp/control-plane-smoke.md @@ -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 포트를 점유하지 않는다. diff --git a/agent-test/dev-corp/rules.md b/agent-test/dev-corp/rules.md index b13944d..ee598e6 100644 --- a/agent-test/dev-corp/rules.md +++ b/agent-test/dev-corp/rules.md @@ -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`다. diff --git a/agent-test/dev-corp/testing-smoke.md b/agent-test/dev-corp/testing-smoke.md index 333d84f..b7689bd 100644 --- a/agent-test/dev-corp/testing-smoke.md +++ b/agent-test/dev-corp/testing-smoke.md @@ -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 값 하나만 포함하는 한 줄 명령 형태를 유지한다. diff --git a/agent-test/dev/control-plane-smoke.md b/agent-test/dev/control-plane-smoke.md index cecd2a4..cfd4409 100644 --- a/agent-test/dev/control-plane-smoke.md +++ b/agent-test/dev/control-plane-smoke.md @@ -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= docker compose --env-file .env.dev.example up -d postgres redis control-plane edge web` +- setup: `IOP_EDGE_NODE_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`)를 점유하지 않는다. diff --git a/agent-test/dev/rules.md b/agent-test/dev/rules.md index 9426f52..1ca577a 100644 --- a/agent-test/dev/rules.md +++ b/agent-test/dev/rules.md @@ -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 경유를 필수 조건으로 보지 않는다. diff --git a/agent-test/dev/testing-smoke.md b/agent-test/dev/testing-smoke.md index e218eb0..f8950fa 100644 --- a/agent-test/dev/testing-smoke.md +++ b/agent-test/dev/testing-smoke.md @@ -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= docker compose --env-file .env.dev.example up -d postgres redis control-plane edge web` +- compose-smoke: `IOP_EDGE_NODE_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 ` 형태를 유지한다. diff --git a/apps/control-plane/cmd/control-plane/main.go b/apps/control-plane/cmd/control-plane/main.go index 32c5a78..66c09f6 100644 --- a/apps/control-plane/cmd/control-plane/main.go +++ b/apps/control-plane/cmd/control-plane/main.go @@ -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 { diff --git a/apps/control-plane/cmd/control-plane/main_test.go b/apps/control-plane/cmd/control-plane/main_test.go index f9614ea..39c5850 100644 --- a/apps/control-plane/cmd/control-plane/main_test.go +++ b/apps/control-plane/cmd/control-plane/main_test.go @@ -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) { diff --git a/apps/control-plane/cmd/control-plane/server.go b/apps/control-plane/cmd/control-plane/server.go index 12287f5..f390624 100644 --- a/apps/control-plane/cmd/control-plane/server.go +++ b/apps/control-plane/cmd/control-plane/server.go @@ -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) diff --git a/configs/edge-compose.yaml.tmpl b/configs/edge-compose.yaml.tmpl index 66db942..c3f728f 100644 --- a/configs/edge-compose.yaml.tmpl +++ b/configs/edge-compose.yaml.tmpl @@ -20,7 +20,7 @@ logging: path: "" metrics: - port: 0 + port: ${IOP_EDGE_METRICS_CONTAINER_PORT} control_plane: enabled: true diff --git a/configs/edge.yaml b/configs/edge.yaml index bcbe714..eaa328e 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -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" diff --git a/configs/grafana/provisioning/datasources/prometheus.yml b/configs/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 0000000..9febbfa --- /dev/null +++ b/configs/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: IOP Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true diff --git a/configs/prometheus/prometheus.dev.yml b/configs/prometheus/prometheus.dev.yml new file mode 100644 index 0000000..aaaf097 --- /dev/null +++ b/configs/prometheus/prometheus.dev.yml @@ -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 diff --git a/configs/prometheus/prometheus.yml b/configs/prometheus/prometheus.yml new file mode 100644 index 0000000..4b0df74 --- /dev/null +++ b/configs/prometheus/prometheus.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 3fdd41e..ca18818 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} diff --git a/docs/openai-usage-grafana.md b/docs/openai-usage-grafana.md index db32e66..2400f87 100644 --- a/docs/openai-usage-grafana.md +++ b/docs/openai-usage-grafana.md @@ -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 세팅은 별도 세션에서 수행한다. diff --git a/packages/go/config/config_test.go b/packages/go/config/config_test.go index bac1a63..a9552d8 100644 --- a/packages/go/config/config_test.go +++ b/packages/go/config/config_test.go @@ -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")