필드 테스트를 위해 edge OpenAI-compatible 경로와 node adapter 설정을 확장하고, Jenkins 바이너리 빌드 및 control-plane/web compose 배포 구성을 함께 정리한다.
174 lines
11 KiB
Markdown
174 lines
11 KiB
Markdown
# edge — Edge Execution Group Controller
|
|
|
|
여러 Node를 하나의 로컬 실행 그룹으로 묶고, IOP 내부 TCP/protobuf 프로토콜을 통해 adapter execution 요청과 이벤트 스트림을 중계한다.
|
|
|
|
Edge는 Node registry, node configuration, runtime routing, stream relay를 담당하는 백엔드 실행 그룹 컨트롤러다. 현재는 진단용 ops console과 함께 최소 OpenAI-compatible HTTP API 표면(`/v1/models`, `/v1/chat/completions`)을 제공하며, 이 HTTP 표면은 edge service를 통해 기존 edge-node transport를 사용한다.
|
|
|
|
**현재 상태: 초기 구현**
|
|
node 등록/레지스트리/transport와 edge 콘솔 기반 수동 통신 테스트가 구현되어 있다.
|
|
|
|
## 내부 경계
|
|
|
|
edge-local ops console은 최종 API가 아니라 diagnostic surface다. ops console의 `/` 명령은 직접 transport를 다루지 않고 `apps/edge/internal/service`를 호출한다. 이후 HTTP/API handler가 추가되면 같은 service를 호출하고, ops console은 필요하면 수동 테스트 도구로만 남긴다.
|
|
|
|
ops console은 edge-local diagnostic surface이고, HTTP/API는 central/remote management surface다.
|
|
|
|
현재 edge 내부 흐름은 다음처럼 나뉜다.
|
|
|
|
- `internal/transport`: node TCP/protobuf 연결, register handshake, protobuf message 수신
|
|
- `internal/node`: node registry와 사전 등록 node store
|
|
- `internal/service`: `SubmitRun`, `TerminateSession`, `UsageStatus`, `ListNodes`, `ResolveNode`
|
|
- `internal/openai`: OpenAI-compatible HTTP serving 표면. 외부 `model`은 이 경계에서 내부 `adapter + target`으로 변환된다.
|
|
- `internal/events`: `RunEvent`, `EdgeNodeEvent` in-process publish/subscribe fanout
|
|
- `cmd/edge`: serve/console CLI 진입점과 console 출력 포맷
|
|
|
|
실행 요청과 이벤트는 같은 TCP 연결을 공유하지만, 메시지 타입과 내부 파이프라인은 분리한다. `RunRequest`, `CancelRequest`, `NodeCommandRequest`는 edge가 node에 보내는 command/request 계열이고, `RunEvent`, `EdgeNodeEvent`는 node와 edge 내부에서 발생한 event 계열이다.
|
|
|
|
## 원격 CLI adapter bin shell 사용자 흐름 검증
|
|
|
|
사용자 실행 파이프라인 검증의 기준은 `make test-e2e`나 smoke 통과가 아니라, 사람이 하듯이 `bin/edge.sh`와 `bin/node.sh`를 각각 실행하고 edge console에서 메시지와 command를 직접 보내 결과가 edge 화면에 표시되는지 확인하는 것이다.
|
|
`make test-e2e` 명령은 `scripts/e2e-smoke.sh`를 실행하는 보조 smoke이며, 최소 생존 확인에는 쓸 수 있지만 완료 기준을 대체하지 않는다.
|
|
실제 외부 프로필(예: `gemini`)을 보조 smoke로 테스트하려면 `IOP_E2E_PROFILE=gemini make test-e2e`와 같이 환경 변수를 지정한다.
|
|
|
|
수동으로 실행하려면 아래 과정을 따른다.
|
|
|
|
`bin/edge.sh`는 edge 서버를 열고 입력 프롬프트를 제공한다.
|
|
`bin/node.sh`는 원격 edge 주소로 접속해 등록한 뒤, edge에서 보낸 `RunRequest`를 `adapter + target` 실행으로 처리한다.
|
|
테스트 파라미터는 `configs/edge.yaml`, `configs/node.yaml`에 하드코딩한다.
|
|
|
|
**전제 조건**: `configs/edge.yaml`의 `console.target`이 가리키는 CLI profile command가 node 호스트에서 실행 가능해야 한다. 기본 예시는 `opencode`이며, `claude`, `claude-tui`, `gemini`, `codex`, `cline-dgx` 등으로 바꿀 수 있다.
|
|
|
|
같은 `cli` 어댑터 안에 `claude`, `claude-tui`, `gemini`, `codex`, `opencode`, `cline` profile을 포함할 수 있다. 예를 들어 `claude`는 `claude -p` 기반 one-shot profile로 두고, `claude-tui`는 `persistent: true`, `terminal: true`, `mode: "persistent-lazy"`로 첫 요청 시 일반 Claude Code TUI 세션을 띄운다. `gemini --approval-mode yolo -p <prompt>`, `codex exec --dangerously-bypass-approvals-and-sandbox`, `cline -y --json --config /config/.cline/profiles/ollama-dgx <prompt>`처럼 headless 조합도 설정할 수 있다. `opencode`는 `mode: "opencode-sse"` profile로 `opencode serve`의 HTTP/SSE 인터페이스를 사용하며, args의 `--model`, `--dangerously-skip-permissions`, `--title` 등은 그대로 유지하면 된다.
|
|
|
|
실행 순서:
|
|
|
|
1. edge 호스트에서 `configs/edge.yaml`의 `server.listen`, `nodes[].token`을 확인한다. 예시 설정은 `console.adapter=cli`, `console.target=opencode`, `console.session_id=default`를 사용한다.
|
|
2. node 호스트에서 `configs/node.yaml`의 `transport.edge_addr`를 edge 호스트 주소로, `transport.token`을 edge token과 같게 맞춘다.
|
|
3. edge 호스트의 `9090/tcp` 포트를 node 호스트에서 접근 가능하게 연다.
|
|
4. edge 호스트에서 `./bin/edge.sh`
|
|
5. node 호스트에서 `./bin/node.sh` — edge 포트가 열릴 때까지 최대 30초 기다린 뒤, edge에서 받은 입력을 선택된 `adapter + target`으로 실행한다.
|
|
6. edge 콘솔에서 `/nodes`로 node 등록을 확인한다.
|
|
7. (선택 사항) node가 여러 대일 경우 `/node <id|alias>`로 대상을 선택한다. 1대일 경우 자동 선택된다.
|
|
8. edge 콘솔의 `edge>` 프롬프트에 메시지를 입력한다.
|
|
9. 각 메시지마다 `[edge] sent`, `[node{index}-evt] start`, 비어 있지 않은 `[node{index}-msg]`, `[node{index}-evt] complete`가 edge 화면에 표시되는지 확인한다.
|
|
10. node 로컬 출력에 생성된 `[node-message]` payload가 edge console의 `[node{index}-msg]` 출력에 모두 표시되었는지 확인한다. complete event만으로 정상 판정하지 않는다.
|
|
11. 같은 session에서 두 번째 메시지를 보내 같은 흐름이 다시 표시되는지 확인한다.
|
|
12. edge 콘솔에서 `/capabilities`, `/transport`, `/sessions` 등 command를 입력하고 `[node-{alias}-<command>]` 결과가 edge 화면에 도착하는지 확인한다. persistent profile이면 `/terminate-session`도 확인한다.
|
|
13. edge 콘솔에서 `/exit` 또는 `quit`로 종료한다.
|
|
|
|
기준 출력 예시:
|
|
|
|
```text
|
|
edge> /nodes
|
|
test-node (test-node)
|
|
|
|
edge> Convert token iop_manual_one and reply only with converted token
|
|
[edge] sent run_id=manual-... node=test-node adapter=cli target=fake-cli session=default background=false
|
|
[node0-evt] start run_id=manual-...
|
|
[node0-msg] IOP_MANUAL_ONE_OK
|
|
[node0-msg] IOP_MANUAL_ONE_TAIL
|
|
[node0-evt] complete run_id=manual-... detail="idle-timeout"
|
|
|
|
edge> Convert token iop_manual_two and reply only with converted token
|
|
[edge] sent run_id=manual-... node=test-node adapter=cli target=fake-cli session=default background=false
|
|
[node0-evt] start run_id=manual-...
|
|
[node0-msg] IOP_MANUAL_TWO_OK
|
|
[node0-msg] IOP_MANUAL_TWO_TAIL
|
|
[node0-evt] complete run_id=manual-... detail="idle-timeout"
|
|
|
|
edge> /capabilities
|
|
[node0-capabilities] target=fake-cli session=default
|
|
adapter = cli
|
|
max_concurrency = 4
|
|
targets = fake-cli
|
|
|
|
edge> /transport
|
|
[node0-transport] target=fake-cli session=default
|
|
adapter = cli
|
|
connected = true
|
|
node_id = test-node
|
|
session_id = default
|
|
target = fake-cli
|
|
|
|
edge> /sessions
|
|
[node0-sessions] target=fake-cli session=default
|
|
count = 1
|
|
sessions = persistent:fake-cli/default
|
|
|
|
edge> /terminate-session
|
|
terminated session default node=test-node
|
|
```
|
|
|
|
## Console 명령
|
|
|
|
| 명령 | 설명 |
|
|
|---|---|
|
|
| `/nodes` | 연결된 node 목록 확인. 선택된 node는 `*`로 표시됨. |
|
|
| `/node <id\|alias>` | 요청을 보낼 명시적 node 선택 |
|
|
| `/session <id>` | 현재 console이 사용할 logical session 변경 |
|
|
| `/background on\|off` | background 실행 모드 토글 (on: 응답 기다리지 않음) |
|
|
| `/terminate-session` | 현재 `adapter/target/session_id`의 worker process 종료 |
|
|
| `/status` | 현재 선택된 target/profile의 사용량(Usage Status) 조회 |
|
|
| `/capabilities` | 현재 선택된 adapter의 지원 target 등 capability 조회 |
|
|
| `/sessions` | 현재 선택된 adapter의 logical session 목록 조회 |
|
|
| `/transport` | 현재 선택된 node의 transport/runtime 상태 조회 |
|
|
| `/exit` | 콘솔 종료 |
|
|
|
|
`/capabilities`, `/sessions`, `/transport`는 `NodeCommandResponse.result` 맵을 그대로 키 정렬 순서로 출력한다. adapter가 명령을 지원하지 않거나 응답을 비워둔 경우 명시적인 unsupported / empty-payload 메시지가 출력된다.
|
|
|
|
## 멀티포인트 라우팅 (Multi-Point Routing)
|
|
|
|
edge는 여러 대의 node가 동시에 연결된 환경을 지원한다.
|
|
|
|
- **명시적 선택**: `/node <alias>` 또는 `/node <node_id>` 명령으로 특정 node를 고정할 수 있다.
|
|
- **Single-node Fallback**: 연결된 node가 정확히 1개일 때는 선택 없이도 해당 node가 자동 지정된다.
|
|
- **Ambiguous Error**: node가 2개 이상일 때 node 선택 없이 메시지를 보내면 에러가 발생하며 선택을 요구한다.
|
|
- **Node-aware Events**: 모든 이벤트와 메시지 출력에 `[node-{alias}-...]` 접두어가 붙어 출처를 식별할 수 있다.
|
|
- **Lifecycle Events**: node 등록/연결 해제는 실행 스트림과 분리된 `EdgeNodeEvent`로 인식하며, 콘솔에서는 `connected`/`disconnected` 이벤트와 reason을 출력한다.
|
|
|
|
## Transport 1개 · Logical Session 여러 개
|
|
|
|
edge-node transport 연결은 **node id당 1개** TCP 연결만 유지한다. 그 연결 위에서 edge는 `session_id`를 지정해 node의 cli adapter가 관리하는 개별 worker process에 접근한다. 같은 `codex` profile이라도 `session_id`가 다르면 독립적인 장수 process다.
|
|
|
|
- **cancel run**: 현재 run 중단, session process 유지
|
|
- **terminate session**: session process 명시 종료 (`/terminate-session` 또는 `CancelAction_TERMINATE_SESSION`)
|
|
|
|
프롬프트 없이 TCP/protobuf edge 서버만 실행하려면 기존처럼 `go run ./apps/edge/cmd/edge serve -c configs/edge.yaml`를 사용한다.
|
|
|
|
`bin/node.sh`의 edge 대기 시간은 `IOP_NODE_WAIT_TIMEOUT`, 확인 간격은 `IOP_NODE_WAIT_INTERVAL`, 접속 주소는 `IOP_EDGE_ADDR` 환경 변수로 임시 override할 수 있다.
|
|
|
|
## OpenAI-Compatible Serving
|
|
|
|
`configs/edge.yaml`의 `openai` 섹션을 켜면 edge가 별도 HTTP listener를 열고 외부 agent가 OpenAI API 형태로 접속할 수 있다. 1차 구현은 Ollama를 주 테스트 경로로 삼는다.
|
|
|
|
```yaml
|
|
openai:
|
|
enabled: true
|
|
listen: "0.0.0.0:8080"
|
|
adapter: "ollama"
|
|
target: "qwen3.6:35b-a3b-bf16"
|
|
models:
|
|
- "qwen3.6:35b-a3b-bf16"
|
|
session_id: "cline"
|
|
timeout_sec: 300
|
|
|
|
nodes:
|
|
- id: "node-dgx-01"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: "http://192.168.0.91:11434"
|
|
context_size: 262144
|
|
```
|
|
|
|
`openai.target`이 비어 있으면 HTTP 요청의 `model` 값을 내부 target으로 사용한다. 값이 있으면 Cline 같은 외부 agent가 보낸 `model`과 무관하게 YAML의 target으로 고정 라우팅한다.
|
|
|
|
기본 non-streaming과 streaming SSE 응답을 모두 지원한다.
|
|
|
|
```bash
|
|
curl -s http://127.0.0.1:8080/v1/chat/completions \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"model":"qwen3.6:35b-a3b-bf16","messages":[{"role":"user","content":"hello"}]}'
|
|
```
|
|
|
|
vLLM은 같은 `adapter + target` 경계를 쓰도록 설정과 `/v1/models` capability 조회 skeleton만 먼저 열어두었다. 실제 completion 실행은 다음 단계에서 붙인다.
|