Ornith provider pool의 현재 구성과 동시성 검증 근거를 일치시키고, stream gate 운영 모드와 관련 작업 규칙을 함께 추적하기 위해 반영한다.
311 lines
20 KiB
Markdown
311 lines
20 KiB
Markdown
# Edge Local Quickstart
|
|
|
|
이 문서는 Edge를 로컬에서 빌드하고 실행해 Node를 연결하고, OpenAI-compatible smoke까지 확인하는 최소 절차다.
|
|
|
|
설정 기준은 `edge.yaml`이다. 주소는 환경 변수로 흩뿌리지 않는다.
|
|
|
|
## 1. Package
|
|
|
|
```bash
|
|
make build
|
|
```
|
|
|
|
## 2. Edge 설정
|
|
|
|
```bash
|
|
rm -rf "$HOME/iop-edge-test"
|
|
mkdir -p "$HOME/iop-edge-test"
|
|
tar -xzf build/packages/iop-edge-<platform>.tar.gz -C "$HOME/iop-edge-test"
|
|
cd "$HOME/iop-edge-test/iop-edge-<platform>"
|
|
./iop-edge config init
|
|
```
|
|
|
|
`edge.yaml`에서 아래 값만 맞춘다. 모델만 필요하면 바꾼다.
|
|
|
|
### Provider-first 구성 (권장)
|
|
|
|
Provider-first는 `models[]`로 라우팅 키를 정의하고 `nodes[].providers[]`로 provider 후보를 선언한다. 두 구조를 `models[].providers[provider_id]` 키로 연결한다.
|
|
|
|
```yaml
|
|
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-1"
|
|
name: "Edge 1"
|
|
|
|
server:
|
|
listen: "0.0.0.0:19090"
|
|
|
|
bootstrap:
|
|
listen: "0.0.0.0:18080"
|
|
artifact_dir: "artifacts"
|
|
|
|
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"
|
|
provider_id: "ollama-gemma"
|
|
adapter: "ollama"
|
|
target: "gemma4:26b"
|
|
|
|
nodes:
|
|
- id: "node-ollama-1"
|
|
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에서 선택된다.
|
|
|
|
### Provider response-stall timeout ownership
|
|
|
|
dev-runtime에서 긴 응답의 정지 판정은 아래 순서를 유지한다.
|
|
|
|
```text
|
|
Node provider response_stall_timeout_ms = 60000
|
|
Node close/probe join ceiling = 5000
|
|
Agent dispatcher silence safety = 70000
|
|
Edge request hard timeout > 70000
|
|
```
|
|
|
|
dev-runtime은 blocking `repeat_guard`와 함께 `max_request_fault_recovery: 1`, `max_strategy_fault_recovery: 1`을 사용하고 Pi agent retry는 비활성화한다. 따라서 반복 출력에는 Stream Evidence Gate가 요청 내부 continuation recovery를 최대 한 번 수행하고, 외부 Pi 재시도나 중첩 요청은 만들지 않는다.
|
|
|
|
`response_stall_timeout_ms` 변경은 restart-required다. `config check`와 `config refresh --mode dry-run`에서 이를 확인한 뒤 Edge와 Node를 같은 source ref로 rebuild/restart하고, 각 binary의 build identity와 실행 중인 process identity를 다시 대조한다. tracked 검증 근거에는 source/build/config 식별자, 단조 시간, terminal 개수와 결과 분류만 남기며 prompt, output, token, credential 원문은 기록하지 않는다.
|
|
|
|
확인:
|
|
|
|
```bash
|
|
./iop-edge --config edge.yaml env
|
|
./iop-edge --config edge.yaml config check
|
|
```
|
|
|
|
## 3. Node 등록
|
|
|
|
```bash
|
|
./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 실행
|
|
|
|
```bash
|
|
mkdir -p logs run
|
|
nohup ./iop-edge --config edge.yaml serve > logs/edge.stdout.log 2>&1 &
|
|
echo $! > run/iop-edge.pid
|
|
```
|
|
|
|
확인:
|
|
|
|
```bash
|
|
# Control Plane에 연결된 Edge 상태 조회
|
|
curl -fsS http://<control-plane-host>:18000/edges
|
|
```
|
|
|
|
## 5. Node 실행
|
|
|
|
3단계에서 출력된 bootstrap 명령을 Node host에서 그대로 실행한다. Linux/macOS는 generated `curl | bash` 명령을 사용하고, Windows native PowerShell은 generated `.ps1` bootstrap과 `Start-IopNode` 함수를 사용한다.
|
|
|
|
Node 연결 확인:
|
|
|
|
```bash
|
|
curl -fsS http://<control-plane-host>:18000/edges/<edge-id>/status
|
|
```
|
|
|
|
## 6. 기본 Smoke
|
|
|
|
```bash
|
|
curl -fsS http://<edge-host>:18081/v1/models
|
|
|
|
./iop-edge --config edge.yaml smoke openai \
|
|
--model gemma4:26b \
|
|
--base-url http://<edge-host>:18081 \
|
|
--timeout 60s
|
|
```
|
|
|
|
외부 OpenAI-compatible client base URL:
|
|
|
|
```text
|
|
http://<edge-host>:18081/v1
|
|
```
|
|
|
|
## 7. Thinking/Reasoning 제어 smoke
|
|
|
|
`/v1/chat/completions` 요청은 OpenAI-compatible field를 기본으로 사용한다. Provider-pool pure `passthrough`는 selected provider가 지원하는 OpenAI-compatible 표준 field와 provider extension field를 보존해야 하며, `chat_template_kwargs` 같은 provider-native option을 IOP allowlist로 막지 않는다. `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning`은 normalized backend 또는 provider별 차이를 보완하기 위한 IOP 확장 field다.
|
|
|
|
- dev GX10/OneXPlayer/RTX5090 Ornith sampling baseline은 `temperature=0.6`, `top_p=0.95`, `top_k=20`을 유지한다.
|
|
- dev OneXPlayer `ornith:35b`와 RTX5090 `ornith-fast` sampling baseline은 `temperature=0.6`, `top_p=0.95`, `top_k=20`을 유지한다. 공식 예제에 없는 non-neutral `repeat_penalty`는 임의로 추가하지 않는다. RTX5090 saved recipe의 `--repeat-penalty 1.0`과 `--min-p 0.00`은 출력을 바꾸지 않는 neutral runtime serialization로만 유지한다.
|
|
- Ornith 공식 근거: https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B
|
|
- 이 값은 caller가 sampling field를 생략했을 때 쓰는 provider 기본값이다. caller가 명시한 sampling field가 있으면 요청값이 우선한다.
|
|
- dev GX10 vLLM은 `deepreinforce-ai/Ornith-1.0-35B-FP8`을 `ornith:35b`, context `262144`, slots `4`, `--gpu-memory-utilization 0.50`, `--tool-call-parser qwen3_xml`, `--reasoning-parser qwen3`, `--language-model-only`로 제공한다. `total_context_tokens=1048576`, `long_context_capacity=4`를 보수적 admission 기준으로 사용한다.
|
|
- GX10의 `iop-vllm-laguna-s21` 컨테이너와 Laguna/DFlash 다운로드는 stopped rollback 자산으로만 보존하며 active dev provider나 capacity로 계산하지 않는다.
|
|
- dev OneXPlayer/RTX5090 Lemonade는 해당 Ornith recipe의 `llamacpp_args`에 `--temp 0.6 --top-p 0.95 --top-k 20`을 저장한다. RTX5090 수동 toggle은 이 sampling 값과 함께 `preserve_thinking=true`, unified KV, Q8 KV, neutral min-p/repeat-penalty를 exact profile로 검증한다.
|
|
- RTX5090에는 `ggml-org/Qwen3.8-27B-GGUF:Q4_K_M` 체크포인트와 CUDA/262144 context/Q8 KV/단일 슬롯 recipe가 standby 자원으로 저장되어 있다. 현재 로드 모델과 dev route/capacity의 활성 대상은 Ornith이며, Qwen3.8은 명시적으로 로드하고 별도 route 정합성 검증을 마치기 전까지 dev provider 자원으로 계산하지 않는다.
|
|
- GX10 Ornith reasoning은 `chat_template_kwargs.enable_thinking`으로 제어하고 vLLM `qwen3` reasoning parser 결과를 사용한다. tool-call은 `qwen3_xml` parser로 정규화한다.
|
|
- 출력 smoke는 같은 요청을 Pi `high`와 `off`로 대조한다. `high`에서는 `thinking_start`/`thinking_delta`/`thinking_end`와 최종 text가, `off`에서는 thinking event 0개와 최종 text가 나와야 한다. agentic multi-turn에서는 tool-call 전후 reasoning, tool result, 최종 text까지 확인한다.
|
|
- 현재 dev-corp provider-pool device mapping은 `gemma4:26b` -> Mac Studio provider capacity `5`, `ornith:35b` -> DGX Spark 01/02 provider 합산 capacity `8`이다. 세부 endpoint와 runtime args는 `agent-test/inventory-dev-corp.yaml`을 기준으로 한다.
|
|
- dev-corp provider-pool 안정 smoke: 기본 smoke에서는 `think`, `reasoning_effort`, `thinking_token_budget`을 생략하고 현재 provider 기본값을 유지한다. Provider-native passthrough를 검증할 때는 selected provider가 직접 지원하는 field를 그대로 보낸다.
|
|
- 현재 dev-corp public capacity smoke 표준은 public OpenAI-compatible base `https://digitalplatform.iop.ai.kr/v1`의 `/chat/completions`에서 `ornith:35b` 9/6 동시 요청과 `gemma4:26b` 9/6 동시 요청을 각각 확인하는 방식이다.
|
|
- 일반 표준 caller의 `stream=false` 측정은 `/v1/chat/completions`에서 요청 파라미터만으로 확인한다.
|
|
- `include_reasoning=false`는 non-provider normalized route의 hide 동작 기준이다. dev-corp provider-pool pure `passthrough`에서는 client가 reasoning field를 선택적으로 무시/제거한다.
|
|
- `think=false` 또는 `reasoning_effort=none`은 hide-only 옵션이 아니라 thinking disable 요청이다. Provider-native field가 있는 경우 해당 field를 우선 사용해 passthrough 보존을 검증한다.
|
|
- Provider-pool passthrough 파라미터의 세부 계약과 금지/허용 범위는 `agent-contract/outer/openai-compatible-api.md`를 기준으로 한다.
|
|
|
|
### Managed route-qualified capacity smoke
|
|
|
|
Managed mode의 capacity는 전역 model catalog나 같은 upstream model을 제공하는 모든 provider의 무조건적인 합이 아닙니다. 먼저 OpenAI ingress와 같은 principal token으로 active route alias를 조회합니다. explicit `resource_selector`는 그 provider 하나만 선택하고, `default` selector는 route의 profile/upstream model과 model group이 모두 일치하는 healthy provider를 풀링합니다. 현재 `ornith:35b`는 `default` selector로 GX10 `4`와 OneXPlayer `3`, 합계 `7`을 사용하며 RTX5090은 후보가 아니라 `ornith-fast` 전용입니다.
|
|
|
|
정상 capacity 검증은 Chat과 Responses를 한 endpoint·route씩 실행합니다. 실제 emitted JSON의 Unicode rune 수와 `runes/4 + runes/16` estimate를 계산해 `normal`임을 확인합니다. explicit selector는 selected provider의 `capacity + 1`, `default` pool은 eligible provider capacity 합계에 1을 더한 요청을 전송하고 provider별 peak가 각 capacity에 도달했는지 확인합니다. 별도 long-context/repeat smoke는 `long_context_capacity`를 사용하며 normal-capacity 완료 근거를 대체하지 않습니다.
|
|
|
|
성공 조건은 모든 요청 HTTP 200, Chat의 finish terminal과 Responses의 `response.completed` 각각 정확히 1개, stream별 `[DONE]` 정확히 1개, selected provider peak가 eligible capacity와 같고 queue가 1 이상인 상태, 최종 `in_flight=0`/`queued=0`입니다. route mismatch, context class mismatch, non-selected capacity 포함, terminal 누락/중복, status 관측 누락은 fail-closed입니다.
|
|
|
|
2026-08-15 `ornith:35b` default pool Chat capacity smoke는 8/8 HTTP 200과 정상 terminal, GX10 peak `4`, OneXPlayer peak `3`, 합산 peak `7`, queue peak `2`, RTX peak `0`, final `0/0`으로 통과했습니다. 별도 Responses smoke는 5/5 HTTP 200과 각 `response.completed`까지 수신했지만 stream별 `[DONE]`이 없어 미통과이므로 Responses stream terminal은 후속 확인 대상으로 유지합니다.
|
|
|
|
각 invocation은 ignored `agent-test/runs/**` 아래 mode `0700` unique directory를 생성하고 current-run manifest가 소유한 request/result/status만 판정합니다. Raw route DTO, token/header, route/slot id, prompt, request/response body와 모델 출력은 해당 디렉터리 밖으로 복사하지 않습니다. tracked evidence와 code review에는 allowlist된 sanitized summary의 run id, script hash, route alias, selected provider, endpoint, computed request shape/class, terminal count, peak/queue/final counter와 outcome만 남깁니다.
|
|
|
|
예시 (dev-corp `gemma4:26b` provider-pool non-stream 측정, think 생략):
|
|
|
|
아래 `18081` 포트는 local Edge 예시다. dev-corp public smoke에서는 base URL을 `https://digitalplatform.iop.ai.kr/v1`로 바꾼다. Direct Edge listener `http://digitalplatform.iop.ai.kr:18086/v1`도 동작하지만 사용자-facing 기본값은 포트 없는 public URL이다.
|
|
|
|
```bash
|
|
curl -fsS http://<edge-host>:18081/v1/chat/completions \
|
|
-H 'Content-Type: application/json' \
|
|
-H 'Authorization: Bearer <token>' \
|
|
-d '{"model":"gemma4:26b","messages":[{"role":"user","content":"hello"}],"stream":false}'
|
|
```
|
|
|
|
예시 (non-provider normalized route에서 response reasoning_content 숨김):
|
|
|
|
```bash
|
|
curl -fsS http://<edge-host>:18081/v1/chat/completions \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"model":"<model-alias>","messages":[{"role":"user","content":"hello"}],"include_reasoning":false}'
|
|
```
|
|
|
|
## 8. Raw text tool-call boundary smoke
|
|
|
|
긴 agent prompt와 `tools[]` 요청은 provider가 native `tool_calls` 대신 assistant content에 raw 블록을 담아 응답할 수 있다. 계약 기준은 `agent-contract/outer/openai-compatible-api.md`의 Chat Completions raw text tool-call 정규화 정책이다.
|
|
|
|
요청 payload는 tracked 문서에 secret 없이 남기고, token은 원격 환경 변수에서 주입한다. 실제 token 원문을 명령/로그/보고에 남기지 않는다.
|
|
|
|
판정 기준:
|
|
|
|
- 응답 body와 SSE delta 어디에도 raw tool block 원문이 성공 content로 남지 않는다.
|
|
- 요청 `tools[]`에 있는 valid tool-call은 `message.tool_calls` 또는 stream `delta.tool_calls`와 `finish_reason: "tool_calls"`로 정규화된다.
|
|
- 요청 `tools[]`에 없는 unknown tool hallucination이나 malformed 블록은 success content가 아니라 `tool_validation_error`로 끝난다.
|
|
|
|
evidence는 tracked 문서가 아니라 ignored run 위치(`agent-test/runs/**`) 또는 code-review output path에 저장한다.
|
|
|
|
## 9. Managed credential plane and TLS startup
|
|
|
|
Managed mode is a separate startup profile, not a live toggle. Keep every certificate, private key, at-rest keyring, issuer key, recipient key, IOP token, and provider credential outside the checkout in an operator-owned directory with restrictive permissions. Tracked YAML contains file paths only.
|
|
|
|
The complete chain must be configured before any process starts:
|
|
|
|
- Control Plane credential HTTPS: server certificate/key for the credential API; the caller validates its CA and server name.
|
|
- Control Plane to Edge: mutual TLS with CA-signed workload identities. The Control Plane expects `role=edge` and the hello `edge_id`; Edge expects the configured Control Plane role/name and server name.
|
|
- Edge to Node: mutual TLS with exact Edge/Node role and name expectations.
|
|
- Lease crypto: Control Plane mounts the at-rest keyring and issuer private key; Node mounts its recipient private key and the issuer public key. Edge receives no decryption key.
|
|
- OpenAI/Anthropic ingress: Edge serves TLS whenever managed mode is enabled.
|
|
|
|
Representative Edge settings:
|
|
|
|
```yaml
|
|
credential_plane:
|
|
enabled: true
|
|
lease_ttl_seconds: 30
|
|
lease_cache_size: 256
|
|
|
|
tls:
|
|
enabled: true
|
|
cert: /etc/iop/secrets/edge.crt
|
|
key: /etc/iop/secrets/edge.key
|
|
ca: /etc/iop/secrets/credential-ca.crt
|
|
peer_role: node
|
|
|
|
control_plane:
|
|
enabled: true
|
|
wire_addr: cp.internal:18002
|
|
tls:
|
|
enabled: true
|
|
cert: /etc/iop/secrets/edge.crt
|
|
key: /etc/iop/secrets/edge.key
|
|
ca: /etc/iop/secrets/credential-ca.crt
|
|
server_name: cp.internal
|
|
peer_role: control-plane
|
|
|
|
openai:
|
|
enabled: true
|
|
tls:
|
|
enabled: true
|
|
cert: /etc/iop/secrets/edge-http.crt
|
|
key: /etc/iop/secrets/edge-http.key
|
|
```
|
|
|
|
Managed validation rejects legacy principal mappings, `openai.bearer_token`, `openai.provider_auth`, provider credential headers/environment/arguments, endpoint user-info, or any plaintext hop. Use `config check` before `serve`; do not weaken validation to mix legacy and managed sources.
|
|
|
|
### Safe slot lifecycle
|
|
|
|
1. Run principal bootstrap locally on the Control Plane host. Capture the one-time token only in a protected secret channel; never paste it into YAML, shell history, logs, review artifacts, or chat.
|
|
2. Call the dedicated credential HTTPS listener with that bearer token. Create accepts provider material only as `application/octet-stream` plus the vendor/kind/alias headers. Create the route separately with slot id, profile id, upstream model, and resource selector.
|
|
3. Confirm the authenticated principal sees only its projected route id/alias from Edge model discovery. A managed request must bind to that route and must never fall back to a legacy model, provider, or another same-model slot.
|
|
4. Rotate with the current `IOP-Expected-Revision`. Confirm the next successful provider attempt reports the new safe `credential_revision`; the former plaintext must not appear in SQLite, logs, metrics, or captured output.
|
|
5. Disable for reversible suspension or revoke for permanent invalidation. Confirm a later full request fails, no Node lease consumption or upstream counter advances, and a same-model slot is not selected as fallback.
|
|
6. For lease-expiry confirmation, wait past the configured TTL and verify an old envelope cannot be replayed. A later authorized request must acquire a fresh lease against the current projection and revisions.
|
|
|
|
The repository qualification exercises CA-signed identities, no-cert/wrong-peer failures, two same-model slots, ciphertext persistence, rotation attribution, and post-revoke no-fallback:
|
|
|
|
```bash
|
|
credential_smoke_parent="$(mktemp -d /config/workspace/iop-credential-slot-guide.XXXXXX)"
|
|
TMPDIR="$credential_smoke_parent" make test-credential-slot-smoke
|
|
rmdir "$credential_smoke_parent"
|
|
```
|
|
|
|
The deterministic Messages qualification succeeds alongside Chat: the Control Plane canonicalizes built-in lowercase API-key header names (for example `x-api-key` to `X-Api-Key`) before signing the lease scope, so both managed profiles reach Node/upstream exactly once with their exact header semantics. A lease failure fails closed before dispatch and never falls back to caller auth or another slot; treat a Chat-only result or any fallback as a qualification failure.
|
|
|
|
### Official agy route smoke
|
|
|
|
공식 `agy` 1.1.12 API-key provider는 upstream Gemini key가 아니라 관리형 IOP principal token을 사용한다. dev operator가 이미 발급한 token과 CA 파일을 보호된 `token/` 아래에 둔 경우 값을 명령행에 직접 쓰지 않고 다음처럼 읽는다.
|
|
|
|
```bash
|
|
read -r IOP_AGY_SMOKE_TOKEN < token/.iop-principal
|
|
|
|
GEMINI_API_KEY="$IOP_AGY_SMOKE_TOKEN" \
|
|
SSL_CERT_FILE="$PWD/token/iop-dev-ca.pem" \
|
|
NODE_EXTRA_CA_CERTS="$PWD/token/iop-dev-ca.pem" \
|
|
GOOGLE_GEMINI_BASE_URL="https://<edge-host>:<https-port>/gemini/<direct-route-id>" \
|
|
agy --sandbox --output-format stream-json --model 'Gemini 3.6 Flash' \
|
|
--print 'Reply only with OK. Do not use tools or modify files.'
|
|
|
|
GEMINI_API_KEY="$IOP_AGY_SMOKE_TOKEN" \
|
|
SSL_CERT_FILE="$PWD/token/iop-dev-ca.pem" \
|
|
NODE_EXTRA_CA_CERTS="$PWD/token/iop-dev-ca.pem" \
|
|
GOOGLE_GEMINI_BASE_URL="https://<edge-host>:<https-port>/gemini/<hybrid-preset-id>" \
|
|
agy --sandbox --output-format stream-json --model 'Gemini 3.6 Flash' \
|
|
--print 'Inspect README.md and report only its first Markdown heading. Do not modify files.'
|
|
|
|
unset IOP_AGY_SMOKE_TOKEN
|
|
```
|
|
|
|
`SSL_CERT_FILE`과 `NODE_EXTRA_CA_CERTS`는 위 caller process에만 적용한다. Codex/IDE 시작 환경이나 ambient shell에 export하면 ChatGPT WebSocket 같은 공개 TLS 연결까지 사설 CA override를 사용하므로 금지한다. `--effort`는 API-key provider 호출에 넣지 않는다. direct와 hybrid 모두 JSONL의 마지막 record가 `event=result`, 중첩 `result.status=SUCCESS` 한 건이어야 한다. hybrid는 plan/work/review가 포함되므로 direct보다 오래 걸릴 수 있다. 한 경로가 실패하면 다른 경로를 묶어 재실행하지 않고 해당 IOP ingress, route binding, preset stage 또는 caller terminal을 분리해 확인한다. Gemini ingress와 공식 event 구조의 상세 계약은 `agent-contract/outer/gemini-compatible-api.md`를 기준으로 한다.
|
|
|
|
### Incident redaction check
|
|
|
|
Before retaining logs or evidence, reject any artifact containing an IOP bearer token, provider credential, slot alias, lease id, certificate private key, keyring material, recipient/issuer private key, target URL with credentials, prompt, or response body. Public metrics may contain only stable safe references such as `credential_slot_ref` and `credential_revision`; request/run/session/attempt/node ids and raw payloads are not credential-attribution labels.
|