18 KiB
Edge Local Quickstart
이 문서는 toki-labs.com Control Plane에 native Edge를 붙이고, Ollama Node를 연결해 OpenAI-compatible smoke까지 확인하는 최소 절차다.
설정 기준은 edge.yaml이다. 주소는 환경 변수로 흩뿌리지 않는다.
1. Package
현재 toki-labs.com 원격처럼 Apple Silicon macOS에서 실행한다.
make build
2. Edge 설정
rm -rf "$HOME/iop-edge-test"
mkdir -p "$HOME/iop-edge-test"
tar -xzf build/packages/iop-edge-darwin-arm64.tar.gz -C "$HOME/iop-edge-test"
cd "$HOME/iop-edge-test/iop-edge-darwin-arm64"
./iop-edge config init
edge.yaml에서 아래 값만 맞춘다. 모델만 필요하면 바꾼다.
Provider-first 구성 (권장)
Provider-first는 models[]로 라우팅 키를 정의하고 nodes[].providers[]로 provider 후보를 선언한다. 두 구조를 models[].providers[provider_id] 키로 연결한다.
models:
# Top-level catalog: canonical routing key → provider-pool mapping.
# models[].id is the external model id; providers maps provider id → served model.
- id: "gemma4:26b"
providers:
ollama-gemma: "gemma4:26b"
edge:
id: "edge-toki-labs"
name: "Toki Labs Edge"
server:
listen: "0.0.0.0:19090"
advertise_host: "toki-labs.com"
bootstrap:
listen: "0.0.0.0:18080"
artifact_base_url: "http://toki-labs.com:18080"
artifact_dir: "artifacts"
control_plane:
enabled: true
wire_addr: "toki-labs.com:19081"
refresh:
enabled: true
listen: "127.0.0.1:19093"
# Legacy adapter/target fallback (backward compat only).
# New deploys should use models[] + nodes[].providers[].
openai:
enabled: true
listen: "0.0.0.0:18081"
adapter: "ollama"
target: "gemma4:26b"
nodes:
- id: "node-ollama-1"
agent_kind: "generic-node"
providers:
- id: "ollama-gemma"
type: "ollama"
category: "local_inference"
base_url: "http://127.0.0.1:11434"
models:
- "gemma4:26b"
capacity: 2
models[]를 사용할 경우 openai.adapter/openai.target은 하향 호환 fallback이다. 실제 routing은 nodes[].providers[].id를 기준으로 provider-pool에서 선택된다.
확인:
./iop-edge --config edge.yaml env
./iop-edge --config edge.yaml config check
3. Node 등록
./iop-edge --config edge.yaml node register node-ollama-1 \
--adapter ollama \
--ollama-base-url http://127.0.0.1:11434
출력된 OS별 bootstrap 명령 원문을 보관한다. 명령 원문에는 실제 token이 포함되므로 tracked 문서에는 기록하지 않는다.
4. Edge 실행
mkdir -p logs run
nohup ./iop-edge --config edge.yaml serve > logs/edge.stdout.log 2>&1 &
echo $! > run/iop-edge.pid
확인:
curl -fsS http://toki-labs.com:18000/edges
5. Node 실행
3단계에서 출력된 bootstrap 명령을 Node host에서 그대로 실행한다. Linux/macOS는 generated curl | bash 명령을 사용하고, Windows native PowerShell은 generated .ps1 bootstrap과 Start-IopNode 함수를 사용한다.
Node 연결 확인:
curl -fsS http://toki-labs.com:18000/edges/edge-toki-labs/status
6. dev-runtime provider pool 반복 기준
현재 GX10 vLLM, OneXPlayer Lemonade, Mac MLX vLLM을 같은 model alias로 묶어 검증하는 dev-runtime 기준은 원격 runner의 동기화된 iop checkout과 build/dev-runtime/edge.yaml이다.
배포 전 원격 runner checkout은 보존 대상이 아니다. 항상 clean sync 후 dev-runtime binary를 다시 빌드한다.
cd /Users/toki/agent-work/iop-dev
git fetch origin main
git reset --hard origin/main
git clean -fd
go build -trimpath -o build/dev-runtime/bin/edge ./apps/edge/cmd/edge
go build -trimpath -o build/dev-runtime/bin/iop-node ./apps/node/cmd/node
GOOS=linux GOARCH=arm64 go build -trimpath -o build/dev-runtime/bin/iop-node-linux-arm64 ./apps/node/cmd/node
GOOS=windows GOARCH=amd64 go build -trimpath -o build/dev-runtime/bin/iop-node-windows-amd64.exe ./apps/node/cmd/node
기준 주소:
- Control Plane status: 원격 host의
http://127.0.0.1:18001 - dev-runtime Edge id:
edge-toki-labs-dev - bootstrap HTTP:
http://toki-labs.com:18082 - Edge OpenAI-compatible base URL:
http://toki-labs.com:18083/v1 - Edge-Node TCP transport:
toki-labs.com:18084
외부로 노출되는 OpenAI-compatible API는 openai.bearer_token으로 보호한다. tracked 문서에는 실제 token 원문을 기록하지 않고, Cline 같은 외부 client에는 운영자가 전달한 API key를 Authorization: Bearer ... 형태로 설정한다.
기준 provider pool:
- model alias:
qwen3.6:35b - provider candidate:
gx10-vllmongx10-vllm-node, capacity4, priority0 - provider candidate:
onexplayer-lemonadeononexplayer-lemonade-node, capacity3, priority1 - provider candidate:
mac-mlx-vllmonmac-codex-node, capacity2, priority2
Provider-pool dispatch는 capacity gate를 먼저 적용해 in_flight >= capacity인 provider를 후보에서 제외한다. 남은 후보 중 가장 낮은 in_flight 레벨을 우선 선택하며, 같은 in_flight 레벨 안에서 priority 숫자가 낮은 provider를 먼저 선택한다. 이 기준에서 10개 동시 요청은 정상적으로 gx10-vllm=4, onexplayer-lemonade=3, mac-mlx-vllm=2, queue 1 구성이 된다.
GX10 vLLM은 nvidia/Qwen3.6-35B-A3B-NVFP4를 Docker container iop-vllm-qwen36-prev-20260704063128로 띄운다. dev 기준 endpoint는 http://192.168.0.91:8001/v1, runtime baseline은 --max-model-len 262144, --max-num-seqs 4, --gpu-memory-utilization 0.30, --reasoning-parser qwen3, --default-chat-template-kwargs '{"enable_thinking":true}'이다.
OneXPlayer Lemonade Node는 원격 runner나 Edge host에서 다시 SSH하거나 proxy process로 띄우지 않는다. 현재 작업 호스트에서 Windows host에 ssh r0bin@192.168.0.59로 직접 접속한 뒤, 그 host 안에서 generated PowerShell bootstrap 명령을 실행한다. 이때 Node 작업 경로는 $HOME\iop-field이며, Lemonade provider endpoint는 Windows host 로컬에서 접근 가능한 값을 기준으로 검증한다.
OneXPlayer Lemonade는 Qwen3.6-35B-A3B-MTP-GGUF artifact를 사용하되 runtime MTP speculative decoding은 끈다. dev long-context 기준 load 설정은 Vulkan backend, ctx_size=524288, llamacpp_args="--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024"로 고정한다. llama.cpp server 기준 ctx_size는 전체 KV/context 예산이고 -np는 server slot 수이므로, 이 설정은 총 context 예산을 full 262144 window 2개분으로 제한하고 slot 3개를 둔다. 2026-07-04 관측 기준 /slots는 slot별 n_ctx=174848을 반환한다. 재시작 뒤에도 유지되도록 /v1/load는 save_options=true로 적용한다.
curl -fsS http://192.168.0.59:13305/v1/load \
-H 'Content-Type: application/json' \
-d '{"model_name":"Qwen3.6-35B-A3B-MTP-GGUF","ctx_size":524288,"llamacpp_backend":"vulkan","llamacpp_args":"--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024","save_options":true}'
Mac MLX provider는 Edge host 자신에서 vllm-mlx로 띄운다. dev 기준 endpoint는 http://127.0.0.1:8002/v1, served model은 mlx-community/Qwen3.6-35B-A3B-4bit, provider capacity는 2이다. 작업 경로는 /Users/toki/agent-work/iop-mlx-vllm, Python 환경은 .venv의 Homebrew Python 3.12, 모델 cache는 hf-cache를 사용한다. 확인된 package baseline은 vllm-mlx 0.3.0, mlx 0.31.2, mlx-lm 0.31.3이다.
vllm-mlx process는 --max-num-seqs 2, --max-kv-size 262144, --max-request-tokens 262144, --use-paged-cache --paged-cache-block-size 64 --max-cache-blocks 4096로 고정한다. 이 설정은 병렬 슬롯을 2로 제한하고, KV/context 예산을 full 262144 window 1개분으로 맞춘다.
cd /Users/toki/agent-work/iop-mlx-vllm
export HF_HOME="$PWD/hf-cache"
export IOP_MLX_API_KEY="<edge.yaml local bearer token>"
nohup .venv/bin/vllm-mlx serve mlx-community/Qwen3.6-35B-A3B-4bit \
--offline \
--host 127.0.0.1 \
--port 8002 \
--api-key "$IOP_MLX_API_KEY" \
--max-num-seqs 2 \
--max-kv-size 262144 \
--max-request-tokens 262144 \
--use-paged-cache \
--paged-cache-block-size 64 \
--max-cache-blocks 4096 \
--enable-auto-tool-choice \
--tool-call-parser qwen \
--reasoning-parser qwen3 \
--default-chat-template-kwargs '{"enable_thinking": true}' \
--mllm \
> logs/vllm-mlx.stdout.log 2> logs/vllm-mlx.stderr.log &
echo $! > vllm-mlx.pid
Mac MLX provider의 주요 운영 파일은 vllm-mlx.pid, logs/vllm-mlx.stdout.log, logs/vllm-mlx.stderr.log다. 2026-06-24 직접 호출 측정 기준으로 max_tokens=192, enable_thinking=false에서 1/2/3 동시 총합은 약 59.66, 87.31, 98.13 tok/s였다.
SSH 세션 안의 Start-Process는 세션 종료와 함께 iop-node.exe가 정리될 수 있다. 반복 배포에서는 Windows host에서 Win32_Process.Create 방식으로 세션 독립 실행한다.
$work = "$HOME\iop-field"
$cmd = 'cmd.exe /c "cd /d C:\Users\r0bin\iop-field && iop-node.exe --config node.yaml serve >> iop-node.wmi.stdout.log 2>> iop-node.wmi.stderr.log"'
Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = $cmd; CurrentDirectory = $work }
capacity, provider mapping, model alias 변경은 후보 edge.yaml 수정 후 config check, refresh dry-run, refresh apply 순서로 반영한다. config refresh subcommand나 19093 admin port가 없으면 dev-runtime binary rebuild 누락으로 보고 clean sync/rebuild부터 다시 수행한다.
dev-runtime 번들은 바이너리를 build/dev-runtime/bin/edge에 둔다. 명령은 그 경로를 EDGE_BIN으로 잡고 실행한다.
EDGE_BIN=build/dev-runtime/bin/edge
"$EDGE_BIN" --config build/dev-runtime/edge.yaml config check
"$EDGE_BIN" --config build/dev-runtime/edge.yaml config refresh --mode dry-run --addr 127.0.0.1:19093
"$EDGE_BIN" --config build/dev-runtime/edge.yaml config refresh --mode apply --addr 127.0.0.1:19093
Node host OS 재부팅은 필요하지 않다. Edge process 재시작이나 일시 단절은 Node reconnect 정책으로 처리한다. Node는 최초 연결 성공 이후 기본 10s 간격으로 최대 10회 재접속을 시도하므로, retry window 안에서 Edge가 회복되면 bootstrap 명령을 다시 실행하지 않는다. retry 한계를 넘겨 Node process가 종료된 경우에만 해당 host에서 새 bootstrap 실행이 필요하다.
반복 검증 순서:
- Control Plane status에서
edge-toki-labs-dev의 connected node 수와 node id를 확인한다. - Edge host에서
toki-labs.com:18084의 established node TCP connection 수를 확인한다. - Edge process를 재시작하고 bootstrap 재실행 없이 3개 Node 연결이 회복되는지 확인한다.
http://toki-labs.com:18083/healthz와/v1/models를 확인한다.- capacity를
gx10-vllm=4,onexplayer-lemonade=3,mac-mlx-vllm=2로 맞춘 후보 config를 refresh apply하고 Edge process가 유지되는지 확인한다. Node adapter capacity 변경이restart_required로 반환되면 Edge process를 새 config로 재시작한 뒤 Node reconnect 상태를 확인한다. qwen3.6:35b로/v1/responses와/v1/chat/completions각각에 provider capacity 총합 + 1개 동시 요청을 보낸다.- capacity
gx10-vllm=4,onexplayer-lemonade=3,mac-mlx-vllm=2기준이면 endpoint별 9개 동시 요청에서 Control Plane status의 provider snapshot이 총in_flight=9,queued=0으로 처리되어야 한다. 10개 동시 요청에서는in_flight=9,queued>=1, 13개 동시 확장 smoke에서는in_flight=9,queued>=4를 기대한다. - 요청 완료 후 provider snapshot이
in_flight=0,queued=0으로 회복되는지 확인한다.
2026-06-24 dev 13-way 확장 smoke는 Mac capacity가 3이던 이전 관측이다. 해당 run에서는 /v1/chat/completions 13개 요청이 모두 성공했고 peak in_flight=10, 실제 queue 대기 3개가 관측되었다. 현재 Mac capacity 2 기준의 13-way smoke 기대치는 peak in_flight=9, queue 대기 4개 이상이다.
Qwen 계열 thinking 모델은 reasoning 텍스트를 포함해 응답할 수 있다. 이 dev smoke에서는 thinking 출력을 실패로 보지 않고, HTTP 성공, final marker 포함, provider log/run count 증가를 기준으로 판정한다.
Request-level think-control smoke
/v1/chat/completions 요청은 think, reasoning_effort, thinking_token_budget, include_reasoning으로 thinking/reasoning 동작과 응답 노출을 제어할 수 있다. dev-runtime provider pool에서는 vLLM, vLLM-MLX, Lemonade 지원 범위가 다르므로 provider-isolated smoke로 provider별 기대 결과를 분리해 판정한다.
- 공통 안정 smoke:
think생략,include_reasoning=false, 또는 budget-onlythinking_token_budget - vLLM / Lemonade
think=false:chat_template_kwargs.enable_thinking=false로 reasoning 생성을 끄고 content stream이 유지되어야 한다. - vLLM-MLX
think=false또는reasoning_effort=none: 현재 런타임은 streaming reasoning disable을 지원하지 않으므로unsupported think control오류를 기대한다. - vLLM / vLLM-MLX / Lemonade
reasoning_effort=low|medium|high: 현재 provider 매핑에서는unsupported think control오류를 반환한다.
예시 (thinking 비활성화, provider-isolated vLLM 또는 Lemonade 기대):
curl -fsS http://toki-labs.com:18083/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"model":"qwen3.6:35b","messages":[{"role":"user","content":"hello"}],"think":false}'
예시 (thinking 생성은 허용하고 response reasoning_content만 숨김):
curl -fsS http://toki-labs.com:18083/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"model":"qwen3.6:35b","messages":[{"role":"user","content":"hello"}],"include_reasoning":false}'
예시 (budget-only):
curl -fsS http://toki-labs.com:18083/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"model":"qwen3.6:35b","messages":[{"role":"user","content":"hello"}],"thinking_token_budget":512}'
reasoning_effort=low 같은 effort smoke는 현재 dev-runtime provider 매핑에서 unsupported error를 기대한다. provider pool에 vLLM/vLLM-MLX/Lemonade가 섞여 있으면 어떤 provider로 라우팅되더라도 성공 stream으로 판정하지 않는다.
현재 공개 API는 개별 request의 최종 node_id를 응답에 노출하지 않는다. 요청별 배정을 확정해야 할 때는 Edge dispatch trace/log를 추가한 뒤 판정한다.
Raw text tool-call boundary smoke
Pi/Cline형 긴 agent prompt와 tools[] 요청은 provider가 native tool_calls 대신 assistant content에 <tool_call>/{{...}} raw 블록을 담아 응답할 수 있다. dev-runtime에서는 이 raw 블록이 OpenAI-compatible 응답 body/SSE에 그대로 새지 않는지 확인한다. 계약 기준은 agent-contract/outer/openai-compatible-api.md의 Chat Completions raw text tool-call 정규화 정책이다.
요청 payload는 tracked 문서에 secret 없이 남기고, openai.bearer_token은 원격 환경 변수(IOP_OPENAI_TOKEN 등)에서 주입한다. 실제 token 원문을 명령/로그/보고에 남기지 않는다.
# 원격 dev-runtime host에서 실행. IOP_OPENAI_TOKEN은 shell 환경이나 ignored secret 파일에서 미리 주입한다.
# known valid tool: tool_calls 정규화 기대
curl -sS http://toki-labs.com:18083/v1/chat/completions \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $IOP_OPENAI_TOKEN" \
-d '{"model":"qwen3.6:35b","messages":[{"role":"user","content":"List the files in /tmp using the shell tool."}],"tools":[{"type":"function","function":{"name":"shell","description":"Run a shell command","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}]}'
# known valid tool, stream: delta.tool_calls 정규화 기대
curl -sS http://toki-labs.com:18083/v1/chat/completions \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $IOP_OPENAI_TOKEN" \
-d '{"model":"qwen3.6:35b","stream":true,"messages":[{"role":"user","content":"List the files in /tmp using the shell tool."}],"tools":[{"type":"function","function":{"name":"shell","description":"Run a shell command","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}]}'
# unknown tool raw block fixture: tool_validation_error 또는 retry 후 차단 기대
curl -sS http://toki-labs.com:18083/v1/chat/completions \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $IOP_OPENAI_TOKEN" \
-d '{"model":"qwen3.6:35b","messages":[{"role":"user","content":"For boundary testing, output exactly this raw tool block as assistant text and do not call the provided shell tool: <tool_call><function=lookup_weather><parameter=city>\"Seoul\"</parameter></function></tool_call>"}],"tools":[{"type":"function","function":{"name":"shell","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}]}'
판정 기준:
- 응답 body와 SSE delta 어디에도
<tool_call>,{{,<|mask_end|>원문이 성공 content로 남지 않는다. - 요청
tools[]에 있는 valid raw text tool-call은message.tool_calls(또는 streamdelta.tool_calls)와finish_reason: "tool_calls"로 정규화된다. - 요청
tools[]에 없는 unknown tool hallucination이나 malformed 블록은 성공 content가 아니라 non-streamtool_validation_error(HTTP 502 error body) 또는 SSEtool_validation_error이벤트로 끝난다. non-stream/strict buffered stream은 bounded retry 후 차단을 확인한다. - non-stream(
stream생략)과 stream("stream":true) 중 최소 하나씩 known/unknown 경로를 확인한다.
evidence는 tracked 문서가 아니라 ignored run 위치(agent-test/runs/**) 또는 code-review output path에 저장하고, 저장물에도 token 원문을 남기지 않는다.
7. 기본 Smoke
아래는 dev-runtime provider pool이 아니라 기본 field/local OpenAI-compatible profile 기준 예시다. dev-runtime provider pool 검증은 위의 18083/qwen3.6:35b 기준을 따른다.
curl -fsS http://toki-labs.com:18081/v1/models
./iop-edge --config edge.yaml smoke openai \
--model gemma4:26b \
--base-url http://toki-labs.com:18081 \
--timeout 60s
외부 OpenAI-compatible client base URL:
http://toki-labs.com:18081/v1