update: dev-corp 환경 추가

This commit is contained in:
leedongmyung 2026-06-25 15:15:49 +09:00
parent 91bbd2d798
commit 0327d11dcc
17 changed files with 1495 additions and 25 deletions

View file

@ -48,3 +48,4 @@
**테스트 실행/검증 작업이 포함된 경우, 작업 환경에 맞게 최초 1회 읽고 수행한다. 환경 미지정은 local로 본다.**
- local: `agent-test/local/rules.md` (없으면 `create-test`)
- dev-corp: `agent-test/dev-corp/rules.md`

View file

@ -94,6 +94,7 @@
## 스킬 라우팅
- dev-corp 배포, dev-corp runtime 배포, 회사망 mac-mini Edge/Node dev-corp 환경 배포, dev-corp provider pool 배포, dev-corp OpenAI-compatible capacity smoke 검증: `agent-ops/skills/project/dev-corp-runtime-deploy/SKILL.md`
- dev 배포, dev-runtime 배포, Edge/Node dev 환경 배포, provider pool 배포, OpenAI-compatible capacity smoke 검증: `agent-ops/skills/project/dev-runtime-deploy/SKILL.md`
- 사용자 실행 파이프라인 검증, repo 내부 edge-node 진단, 메시지 2회 왕복, edge command 응답, 보조 E2E smoke, full-cycle 실제 구동, `scripts/dev/edge.sh`/`scripts/dev/node.sh` 진단 테스트: `agent-ops/skills/project/e2e-smoke/SKILL.md`
- field 테스트 포트, artifact/bootstrap HTTP, 외부 테스트 환경: `agent-test/local/rules.md`를 따른다.

View file

@ -0,0 +1,164 @@
---
name: dev-corp-runtime-deploy
version: 1.0.0
description: dev-corp 배포, 회사망 mac-mini Edge와 내부 DGX/Mac Studio provider pool 배포 및 OpenAI-compatible capacity smoke 절차
---
# dev-corp-runtime-deploy
## 목적
dev-corp provider pool을 현재 dev-runtime과 같은 구조로 배포한다. 배포는 mac-mini checkout 준비, source clean sync, 테스트, dev-corp runtime rebuild, Edge/Node 재시작, provider snapshot 기반 capacity 검증까지 포함한다.
## 언제 호출할지
- 사용자가 `dev-corp 배포`, `dev-corp runtime 배포`, `회사 dev 환경 배포`처럼 dev-corp 환경 배포를 요청할 때
- mac-mini Edge와 DGX Spark 01/02 vLLM node, Mac Studio vLLM-MLX node로 이루어진 provider pool을 갱신할 때
- dev-corp model alias, provider endpoint, provider capacity, node 접속 정보를 배포 절차에 반영할 때
- dev-corp OpenAI-compatible `/v1/responses` 또는 `/v1/chat/completions` 경로가 provider capacity만큼 채워지는지 검증할 때
## 입력
- `env`: 배포 대상 환경. 기본값은 `dev-corp`이다.
- `model`: OpenAI-compatible model alias. 지정하지 않으면 `agent-test/dev-corp/inventory.yaml``model.alias`를 사용한다.
- `capacity_targets`: provider별 기대 capacity. 지정하지 않으면 `agent-test/dev-corp/inventory.yaml`의 target 후보 값을 사용한다.
- `source_ref`: 배포할 git ref. 지정하지 않으면 mac-mini checkout의 기본 배포 branch 기준을 따른다.
## 먼저 확인할 것
- [ ] `agent-ops/rules/project/domain/testing/rules.md`를 읽고 사용자 실행 파이프라인 검증 기준을 확인한다.
- [ ] `agent-test/dev-corp/inventory.yaml`을 먼저 읽는다. 파일이 없으면 `agent-test/dev-corp/rules.md`, `agent-test/dev-corp/edge-smoke.md`, `agent-test/dev-corp/node-smoke.md`로 fallback하고, 구조화 inventory 누락을 보고한다.
- [ ] dev-corp provider pool과 compose/local/dev-runtime profile을 섞지 않는다. dev-corp provider pool은 mac-mini Edge와 dev-corp config를 기준으로 한다.
- [ ] mac-mini에 `/Users/fe/agent-work/iop-dev-corp` checkout이 없으면 배포를 시작하지 말고 checkout 생성과 source sync를 setup blocker로 보고한다.
- [ ] mac-mini에서 `192.168.2.2:8002`, `192.168.2.4:8004`, `192.168.2.3:8004``/health``/v1/models`가 성공하는지 확인한다.
- [ ] Edge config의 OpenAI-compatible adapter endpoint는 각 Node process 기준으로 평가된다. direct preflight는 `192.168.2.x` 주소로 하되, Edge가 Node에 내려주는 provider endpoint는 node-local `127.0.0.1:<provider-port>`를 우선한다.
- [ ] DGX Spark 02가 mac-mini `172.23.78.70:18085/18086/18087`에 직접 접근하지 못하면 mac-mini에서 reverse SSH tunnel을 유지하고 node02 Edge addr을 tunnel local port로 설정한다.
- [ ] `gemma4:26b` alias와 총 capacity `13`은 target 후보이다. Edge config 반영과 capacity smoke가 통과하기 전에는 확정값으로 보고하지 않는다.
- [ ] DGX Spark vLLM provider는 capacity `4`, 호출당 최대 context window `262144`, requested KV `262144x2` 이상을 기준으로 한다. vLLM에는 vLLM-MLX식 `--max-kv-size`가 없으므로 실제 KV cache는 `gpu_memory_utilization` 적용 후 startup log의 `GPU KV cache size``Maximum concurrency for 262,144 tokens per request`로 검증한다. DGX01/02 검증 기준은 `--max-num-seqs 4`, `--max-model-len 262144`, `--gpu-memory-utilization 0.40`이고, FP4 MoE 커널 한계 보정을 위해 `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`도 함께 둔다.
- [ ] DGX Spark 02 endpoint는 Docker publish 때문에 `192.168.2.4:8004`이다. `8000`, `8001`, `8002`를 기본 endpoint로 쓰지 않는다.
- [ ] Mac Studio `192.168.2.3:8005` DiffusionGemma endpoint는 secondary provider 후보이며 기본 pool에 자동 포함하지 않는다.
- [ ] Mac Studio `192.168.2.3:8004` vLLM-MLX runtime은 capacity `5`, 호출당 최대 context window `262144`, requested KV `262144x3` 기준으로 `--max-num-seqs 5`, `--max-request-tokens 262144`, `--max-kv-size 786432`을 사용한다.
- [ ] Mac Studio `192.168.2.3:8004` vLLM-MLX runtime은 2026-06-25 기준 detached `screen` session `vllm_mlx_8004`로 관리한다. 이 host는 Python `pyexpat` 로딩에 `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib`가 필요하므로 재시작 명령에 반드시 포함한다.
- [ ] 현재 구현의 completion 검증 대상은 legacy `/v1/completions`가 아니라 `/v1/chat/completions`이다. `/v1/completions`는 route가 구현되어 있을 때만 별도 검증한다.
- [ ] dev-corp current runtime은 OpenAI-compatible bearer token이 켜져 있다. smoke는 token 원문을 출력하지 말고 `OPENAI_API_KEY=$(cat build/dev-corp-runtime/.secrets/openai_api_key)` 또는 `--api-key-file`로 실행한다.
- [ ] token, secret header, bootstrap token, private key 경로는 최종 보고에 원문으로 출력하지 않는다.
## 2026-06-25 known runtime update notes
- DGX Spark 01/02의 requested KV `262144x2`는 vLLM에서 직접 `--max-kv-size`로 표현하지 않는다. `--max_num_batched_tokens`는 scheduler iteration budget이므로 KV cache 총량으로 취급하지 않는다. dev-corp 기준은 `--max-model-len 262144``gpu_memory_utilization` 적용 후 startup log의 KV cache/concurrency 확인이다.
- DGX Spark에서 explicit `--max-num-batched-tokens 524288`를 사용하는 경우 first profile 중 FP4 MoE token-per-expert 기본 한계에 걸릴 수 있다. `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`를 반드시 유지한다.
- DGX Spark 01은 FlashInfer FP4 JIT가 first compile 중 `ninja` 실행 파일을 PATH에서 찾는다. `ninja`는 venv에 있으므로 `/home/digitalcommerce_dgx_spark_01/vllm_env/bin`을 PATH 앞에 둔다.
- DGX Spark 02 Docker image `vllm/vllm-openai:latest`의 entrypoint는 이미 `vllm serve`이다. container command에는 `serve`를 중복으로 넣지 말고 positional model `/models/gemma-4-26B-A4B-it-NVFP4`부터 둔다. 현재 Docker publish는 host `8004` -> container `8004` 기준이다.
- Mac Studio는 `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib` 없이 Python `pyexpat` import가 실패할 수 있다. `screen` session `vllm_mlx_8004` 안에서 이 환경변수를 export한 뒤 시작한다.
- 2026-06-25 재확인 기준 DGX Spark 01은 `--gpu-memory-utilization 0.40`로 재기동 후 mac-mini public forward `http://172.23.78.70:8002`에서 `/health` 200, `/v1/models`, `/v1/chat/completions` 단문 smoke를 통과했고 startup log에서 GPU KV cache `804,421` tokens, full-context concurrency `3.07x`를 확인했다.
- 2026-06-25 재확인 기준 DGX Spark 02는 Docker `vllm-gemma4` host/container `8004`, `--gpu-memory-utilization 0.40`, `--max-model-len 262144`, `--max-num-seqs 4`로 동작하며 mac-mini와 node-local `/health`, `/v1/models`, 직접 동시성 `1..4` chat completion benchmark를 통과했다. startup log에서 GPU KV cache `860,222` tokens, full-context concurrency `3.28x`를 확인했다.
- 2026-06-25 재확인 기준 Mac Studio provider는 새 설정으로 `/health`와 직접 동시성 `1..5` benchmark를 통과했다. provider-pool 전체 Edge OpenAI-compatible capacity smoke는 별도로 검증해야 한다.
## 실행 절차
1. **환경 인벤토리 확정**
- 배포 대상 runner, repo path, Edge id, Control Plane status URL, OpenAI base URL, Edge admin URL, Node SSH 정보를 `agent-test/dev-corp/inventory.yaml`에서 확정한다.
- provider pool 대상 model alias와 provider별 capacity target을 확정한다.
- 필수 정보가 없거나 서로 충돌하면 배포를 시작하지 말고 누락/충돌 항목을 보고한다.
2. **provider direct preflight**
- mac-mini에서 DGX Spark 01 `http://192.168.2.2:8002/v1/models`, DGX Spark 02 `http://192.168.2.4:8004/v1/models`, Mac Studio `http://192.168.2.3:8004/v1/models`를 확인한다.
- 각 endpoint가 기대 `served_model`을 노출하는지 확인한다.
- DGX Spark 02가 provider port down과 SSH banner exchange timeout을 동시에 보이면 runtime 추가 조작을 보류하고, SSH 회복 후 process/log부터 확인한다.
- 실패한 provider는 Edge config에 넣지 않거나 배포 blocker로 보고한다.
3. **mac-mini checkout clean sync**
- mac-mini checkout에서 `git fetch` 후 배포 기준 ref로 `git reset --hard`를 수행한다.
- dirty 파일은 보존 대상으로 보지 않는다. 배포 전 clean 상태를 만든다.
- 기본 cleanup은 `git clean -fd`이다. `git clean -fdx`는 config, token, secret, runtime artifact까지 삭제할 수 있으므로 사용하지 않는다.
- sync 후 `git status --short --branch``git log --oneline -1`을 기록한다.
4. **빌드 전 테스트**
- clean source 기준으로 `go test ./...`를 실행한다.
- client/Flutter, proto, Makefile, script, config 변경이 배포 범위에 포함되면 해당 도메인 규칙의 테스트도 추가한다.
- 테스트가 실패하면 build/deploy를 진행하지 않고 실패 패키지와 핵심 오류를 보고한다.
- mac-mini 또는 로컬 PATH에 Go가 없으면 시스템 전역 설치 대신 `build/.tools` 같은 ignored runtime 경로에 임시 Go toolchain을 두고 사용한다.
5. **dev-corp runtime rebuild**
- 같은 source ref에서 dev-corp Edge binary, mac node binary, Linux ARM64 node binary를 다시 빌드한다.
- mac-mini에서 직접 빌드할 수 없으면 같은 source ref 기준으로 로컬에서 빌드한 뒤 `rsync/scp`로 mac-mini runtime 경로에 배포한다.
- Windows AMD64 node binary는 dev-corp 기본 provider pool 대상이 아니다.
- stale binary가 의심되거나 `config refresh` subcommand, admin port, version 출력이 맞지 않으면 clean sync부터 다시 시작한다.
- 빌드 산출물 경로, timestamp, 크기, 실행 가능 여부를 기록한다.
6. **빌드 후 config check**
- 빌드된 Edge binary로 `config check`, `config refresh --help`, `config refresh --mode dry-run`을 실행한다.
- dry-run이 `rejected` 또는 예상 밖 `restart_required`를 반환하면 배포를 멈추고 config diff를 보고한다.
7. **배포와 재시작**
- Edge는 mac-mini에서 빌드 산출물 기준으로 재시작한다.
- DGX Spark 01/02는 Linux ARM64 node binary를 mac-mini 경유로 배포하고 기존 node process를 재시작한다.
- Mac Studio는 macOS node binary를 mac-mini 경유로 배포하고 기존 node process를 재시작한다.
- provider runtime 옵션을 갱신하는 배포라면 DGX Spark 01은 `/home/digitalcommerce_dgx_spark_01/start_vllm_8002.sh``PATH=/home/digitalcommerce_dgx_spark_01/vllm_env/bin:$PATH`, `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`, `--max-num-seqs 4`, `--max-model-len 262144`, `--gpu-memory-utilization 0.40`를 확인하고 tmux `vllm_server_8002`를 재기동한다. FlashInfer FP4 JIT가 first compile 중 `ninja`를 PATH에서 찾는다.
- provider runtime 옵션을 갱신하는 배포라면 DGX Spark 02는 Docker container `vllm-gemma4`를 image entrypoint `vllm serve` 기준으로 재생성한다. container env에는 `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`를 넣고, container args는 positional model `/models/gemma-4-26B-A4B-it-NVFP4` 뒤에 `--max-model-len 262144`, `--gpu-memory-utilization 0.40`, `--max-num-seqs 4`, `--port 8004`를 둔다.
- provider runtime 옵션을 갱신하는 배포라면 Mac Studio는 `screen -dmS vllm_mlx_8004` 안에서 `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib`를 export한 뒤 `vllm_mlx.cli serve``--max-num-seqs 5`, `--max-kv-size 786432`, `--max-request-tokens 262144`로 재기동한다.
- Node bootstrap은 Edge의 `node register`가 출력한 OS별 완성 명령을 우선한다. 다만 node-local provider endpoint, 별도 `IOP_HOME`, 또는 node02 reverse tunnel처럼 host별 Edge addr이 필요한 경우에는 `~/iop-dev-corp-field/node.yaml`을 수동 배치할 수 있고, token 원문은 로그/보고에 남기지 않는다.
8. **배포 후 연결 검증**
- Edge, OpenAI-compatible listener, Node TCP, admin port, Control Plane status port가 열려 있는지 확인한다.
- Control Plane status에서 Edge가 connected이고 dev-corp 기준 3개 provider node가 connected인지 확인한다.
- 각 node의 `provider_snapshots`에서 provider `id`, `capacity`, `in_flight`, `queued`, `health`, `served_models`를 확인한다.
- `/v1/models`가 대상 model alias를 노출하는지 확인한다.
9. **OpenAI-compatible capacity smoke**
- `/v1/responses``/v1/chat/completions`를 각각 검증한다. legacy `/v1/completions`는 구현되어 있지 않으면 실패로 보지 않는다.
- 표준 부하 프롬프트는 700~1200 token 수준의 구조화된 답변을 유도해 요청이 동시에 관측될 시간을 만든다.
- endpoint별로 총 provider capacity + 1개 요청을 동시에 보낸다. Edge config에서 기본 dev-corp 후보 capacity `4 + 4 + 5 = 13`을 확정한 경우 14개 동시 호출을 보낸다.
- 요청 실행 중 Control Plane status를 반복 polling하여 대상 provider들의 `in_flight` 합이 총 capacity에 도달하고 `queued` 합이 1 이상이 되는 순간을 증거로 남긴다.
- 각 provider의 `in_flight`가 자기 capacity를 넘지 않고, 적어도 한 번은 기대 capacity까지 차는지 확인한다.
- 모든 요청 완료 후 같은 status에서 대상 provider들의 `in_flight=0`, `queued=0`으로 돌아오는지 확인한다.
- Gemma 계열 reasoning/tool-parser 텍스트는 정상 응답으로 허용한다. exact-output match를 smoke 성공 기준으로 삼지 않는다.
10. **결과 보고**
- source ref, clean sync 결과, 테스트 결과, 빌드 산출물, process/port 상태, connected node 목록, provider capacity snapshot, capacity smoke 관측값을 보고한다.
- 실패한 단계가 있으면 다음 단계를 진행했는지 여부를 명확히 구분한다.
- capacity smoke가 타이밍 문제로 관측 실패했으면 요청 성공과 별도로 `capacity 관측 미충족`으로 보고하고, 프롬프트 길이 또는 status polling 간격 조정을 제안한다.
## 실행 결과 검증
- [ ] mac-mini checkout이 배포 기준 ref로 clean sync되었는가
- [ ] provider direct endpoint 3개가 mac-mini에서 `/health``/v1/models`에 성공했는가
- [ ] 빌드 전 `go test ./...`와 필요한 추가 테스트가 통과했는가
- [ ] dev-corp Edge/mac/Linux ARM64 binary가 같은 source ref에서 rebuild되었는가
- [ ] 빌드 후 config check, refresh help, refresh dry-run이 통과했는가
- [ ] Edge와 3개 provider node가 재시작되고 connected 상태인가
- [ ] `/v1/responses` capacity smoke에서 총 capacity만큼 `in_flight`가 차고 초과 요청이 queue에 잡혔는가
- [ ] `/v1/chat/completions` capacity smoke에서 총 capacity만큼 `in_flight`가 차고 초과 요청이 queue에 잡혔는가
- [ ] 완료 후 provider `in_flight=0`, `queued=0`으로 회복되었는가
- 검증 실패 시: 실패 단계, 실패한 host/provider/endpoint, 관측된 snapshot, 진행 중단 여부를 보고한다.
## 출력 형식
```text
dev-corp runtime 배포 결과
- Source: <branch/ref/commit>, clean=<yes|no>
- Provider preflight: dgx01=<pass|fail>, dgx02=<pass|fail>, mac-studio=<pass|fail>
- Pre-build tests: <command> - <pass|fail|not-run>
- Build: edge=<path>, mac-node=<path>, linux-arm64-node=<path>
- Post-build checks: config-check=<pass|fail>, refresh-help=<pass|fail>, refresh-dry-run=<status>
- Deployment: edge=<pid/status>, dgx01=<pid/status>, dgx02=<pid/status>, mac-studio=<pid/status>
- Ports: <port summary>
- Nodes: <node_id connected summary>
- Providers: <provider_id capacity/in_flight/queued/health summary>
- OpenAI-compatible: models=<pass|fail>, responses-capacity=<pass|fail>, chat-completions-capacity=<pass|fail>
- Capacity evidence: <endpoint별 max in_flight/queued snapshot>
- Blockers/Risk: <없음 또는 내용>
```
## 금지 사항
- mac-mini checkout이 없거나 dirty/divergent 상태인데 배포를 계속하지 않는다.
- `git clean -fdx`를 기본 cleanup으로 사용하지 않는다.
- 빌드 전 테스트 실패 후 배포를 계속하지 않는다.
- DGX Spark 02 provider endpoint를 `192.168.2.4:8000`, `8001`, `8002`로 잘못 사용하지 않는다.
- Mac Studio secondary `8005` endpoint를 별도 alias/capacity 결정 없이 기본 provider pool에 포함하지 않는다.
- Mac Studio `8004` vLLM-MLX runtime 재시작 방식을 확인하지 않고 provider process를 임의 종료하지 않는다.
- node02처럼 tunnel 예외가 필요한 경우를 제외하고 Node bootstrap 기본 경로에서 수동 token 치환, named env parameter, 수동 `node.yaml` 작성을 요구하지 않는다.
- Gemma reasoning/tool-parser 출력을 실패로 판정하거나 exact-output smoke를 기본 성공 기준으로 삼지 않는다.
- OpenAI-compatible 요청 성공만으로 capacity 검증 성공을 선언하지 않는다. provider snapshot의 `in_flight/queued` 관측을 함께 남긴다.
- 이 프로젝트 전용 배포 절차를 `agent-ops/rules/common/_templates` 또는 common skill에 넣지 않는다.

View file

@ -54,3 +54,4 @@
- `agent-test/local/rules.md`
- `agent-test/dev/rules.md`
- `agent-test/dev-corp/rules.md`

View file

@ -0,0 +1,98 @@
---
test_env: dev-corp
test_profile: client-smoke
domain: client
verification_type: smoke
last_rule_updated_at: 2026-06-25
---
# client-smoke dev-corp 테스트
## 읽기 조건
- `apps/client/**` 또는 `packages/flutter/iop_console/**` 변경의 dev-corp 테스트, 검증, 실행 조건 판단이 필요한 경우
## 적용 범위
- IOP Flutter client app shell
- IOP-owned embeddable console package
- Client-Control Plane proto-socket hello baseline
- Flutter widget/unit tests
- dev-corp web preview와 compose web build URL 주입
## 분류
- domain: client
- verification_type: smoke
- scope: Flutter client와 IOP console package 변경
## 환경
- host: Flutter client 또는 `packages/flutter/iop_console`가 포함된 runtime evidence는 mac-mini `ssh fe@172.23.78.70``/Users/fe/agent-work/iop-dev-corp` 기준으로 수행한다.
- port: web/dev preview `13002`, Control Plane HTTP `18002`, Client WS `19004`, Edge-Node TCP transport `19006`.
- runtime: Flutter/Dart
- package manager: Flutter pub
- docker: compose 검증은 mac-mini에서 `.env.dev-corp.example` 기준이다.
- external service: sibling path dependencies `../agent-shell`, `../nexo/packages/messaging_flutter`, `../proto-socket/dart`
- model endpoint:
- credential: token/secret/API key 원문은 문서에 기록하지 않는다.
명령은 mac-mini runner 사용 시 `/Users/fe/agent-work/iop-dev-corp` 기준으로 실행한다. 현재 작업 컨테이너의 변경분이 mac-mini checkout에 동기화되지 않았으면 원격 검증 완료로 보지 않는다.
## 명령
- setup: `cd apps/client && flutter pub get`
- package-setup: `cd packages/flutter/iop_console && flutter pub get`
- unit: `cd apps/client && flutter test`
- package-unit: `cd packages/flutter/iop_console && flutter test`
- lint: `cd apps/client && flutter analyze --no-fatal-infos`
- smoke: `cd apps/client && flutter test`
- e2e: `IOP_WEB_PORT=13002 IOP_CONTROL_PLANE_HTTP_URL=http://172.23.78.70:18002 IOP_CONTROL_PLANE_WIRE_URL=ws://172.23.78.70:19004/client ./scripts/dev/web.sh`
- compose-e2e: mac-mini 환경에서 `IOP_EDGE_NODE_TOKEN`을 안전하게 주입한 뒤 `docker compose --env-file .env.dev-corp.example up --build`
- web-build: `make client-build-web IOP_CONTROL_PLANE_HTTP_URL=http://172.23.78.70:18002 IOP_CONTROL_PLANE_WIRE_URL=ws://172.23.78.70:19004/client`
- model:
- full-cycle:
## 필수 검증
- client 또는 `packages/flutter/iop_console` 변경은 최소 `cd apps/client && flutter test`로 확인한다.
- `packages/flutter/iop_console` 자체 API나 widget 구조가 바뀌면 `cd packages/flutter/iop_console && flutter test` 실행 가능 여부를 확인한다.
- analyzer 영향이 있는 dependency, app shell, generated import 변경이면 `cd apps/client && flutter analyze --no-fatal-infos`를 함께 확인한다.
- Web build/deploy, `apps/client/Dockerfile`, `docker-compose.yml`, `scripts/dev/web.sh`, `Makefile client-build-web` 변경은 mac-mini에서 dev-server 기동 또는 compose build 중 변경 범위에 맞는 경로를 확인하고, 외부 브라우저 확인 URL이 `http://172.23.78.70:13002` 계열인지 보고한다.
- compose 경로를 확인할 때는 `IOP_EDGE_NODE_TOKEN`을 mac-mini 환경에서 주입하고, token 원문은 tracked 파일에 기록하지 않는다.
## 보조 검증
- `cd apps/client && flutter pub get`
- `cd packages/flutter/iop_console && flutter pub get`
## 판정 기준
- Flutter test가 실패 없이 종료한다.
- Analyzer는 기존 non-fatal info를 기능 실패로 확대하지 않는다.
- path dependency가 없어 실행하지 못하면 누락된 sibling workspace를 차단 사유로 보고한다.
- dev-corp web preview가 필요한 경우 browser-facing URL과 compiled Control Plane HTTP/WS URL이 모두 dev-corp 포트(`13002`, `18002`, `19004`)를 사용한다.
## 기준 출력 예시
```text
All tests passed!
```
## 차단 기준
- mac-mini runner 접근, Flutter SDK, Docker compose, 또는 필요한 dev-corp 포트 개방이 없어 client runtime 검증을 실행할 수 없다.
- 필요한 sibling path dependency가 없어 `flutter pub get` 또는 test가 진행되지 않는다.
## 보고 항목
- 실행한 명령:
- 성공한 검증:
- 실패/차단된 검증:
- 생략 사유:
- 남은 위험:
## 금지 사항
- secret, token, API key 원문은 tracked 파일에 기록하지 않는다.
- IOP client에 NomadCode product shell UX를 직접 복제하지 않는다.

View file

@ -0,0 +1,96 @@
---
test_env: dev-corp
test_profile: control-plane-smoke
domain: control-plane
verification_type: smoke
last_rule_updated_at: 2026-06-25
---
# control-plane-smoke dev-corp 테스트
## 읽기 조건
- `apps/control-plane/**` 변경 또는 dev-corp Control Plane health/readiness, config loading, wire endpoint 검증 판단이 필요한 경우
## 적용 범위
- `apps/control-plane/cmd/control-plane/**`
- `apps/control-plane/internal/wire/**`
- `apps/control-plane/Dockerfile`
- Control Plane HTTP health/readiness와 proto-socket WebSocket hello baseline
- Control Plane-Edge TCP wire endpoint, Edge outbound hello, disconnect baseline
- dev-corp compose host publish와 DB/Redis 분리
## 분류
- domain: control-plane
- verification_type: smoke
- scope: dev-corp control-plane HTTP와 wire baseline
## 환경
- host: Docker compose 또는 Flutter client가 포함된 runtime evidence는 mac-mini `ssh fe@172.23.78.70``/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`.
- runtime: Go `1.24`, Docker compose
- package manager: Go modules / Makefile
- docker: compose 검증은 mac-mini에서 `.env.dev-corp.example` 기준으로 수행한다. 파일 생성 전에는 compose 검증을 차단으로 보고한다.
- external service: postgres host publish `15402`, redis host publish `16302`
- model endpoint:
- 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`
- lint:
- unit: `go test ./apps/control-plane/...`
- smoke: `curl -fsS http://127.0.0.1:18002/healthz`, `curl -fsS http://127.0.0.1:18002/readyz`
- e2e: `make test-control-plane-edge-wire`는 보조 smoke이며 기본 포트를 쓰는 스크립트이므로 dev-corp 포트 override 가능 여부를 먼저 확인한다.
- model:
- full-cycle: Control Plane command, health/readiness, wire hello 수동 검증
## 필수 검증
- 변경한 control-plane 패키지 또는 `go test ./apps/control-plane/...`를 실행한다.
- HTTP lifecycle 변경 시 dev-corp port `/healthz``/readyz`를 확인한다.
- Client-Control Plane wire protocol 변경 시 proto-socket WebSocket hello baseline을 dev-corp wire port `19004` 기준으로 확인한다.
- Control Plane-Edge TCP wire, Edge outbound connector, registry lifecycle 변경 시 dev-corp `19005` 기준으로 hello accepted, Edge connected, Edge disconnected marker를 확인한다.
## 보조 검증
- docker compose control-plane service healthcheck를 확인한다.
- Edge 포함 compose 검증에서는 `IOP_EDGE_NODE_TOKEN`을 mac-mini 환경에서 주입하고, token 원문은 tracked 파일에 기록하지 않는다.
## 판정 기준
- control-plane service가 dev-corp stack으로 기동되고 health endpoint가 성공 응답을 반환한다.
- wire endpoint는 Control Plane 브라우저/앱 경계로 유지되고 Node 직접 연결/스케줄링 경로로 확장되지 않는다.
- dev-corp stack은 local/test/dev stack의 host publish 포트를 점유하지 않는다.
## 기준 출력 예시
```text
curl -fsS http://127.0.0.1:18002/healthz
curl -fsS http://127.0.0.1:18002/readyz
```
## 차단 기준
- mac-mini runner checkout 또는 docker compose 실행 권한이 없다.
- `.env.dev-corp.example` 또는 compose override가 없어 dev-corp compose stack을 렌더링할 수 없다.
- 현재 작업 컨테이너에서 Docker-in-Docker가 필요해지는 검증은 수행하지 않고 mac-mini runner 차단으로 보고한다.
- 실제 DB/Redis credential 확인이 필요한데 안전하게 제공되지 않았다.
- dev-corp 포트가 이미 점유되어 local/test/dev stack과 공존할 수 없다.
## 보고 항목
- 실행한 명령:
- 성공한 검증:
- 실패/차단된 검증:
- 생략 사유:
- 남은 위험:
## 금지 사항
- Control Plane에서 Node 직접 연결이나 직접 스케줄링을 제품 기본 계약으로 만들지 않는다.
- 실제 DB/Redis credential이나 private endpoint를 tracked docs/rules에 기록하지 않는다.
- secret, token, API key 원문은 tracked 파일에 기록하지 않는다.

View file

@ -0,0 +1,157 @@
---
test_env: dev-corp
test_profile: edge-smoke
domain: edge
verification_type: smoke
last_rule_updated_at: 2026-06-25
---
# edge-smoke dev-corp 테스트
## 읽기 조건
- `apps/edge/**` 변경 또는 dev-corp Edge registry, transport, service, OpenAI-compatible/A2A 입력 표면 검증 판단이 필요한 경우
## 적용 범위
- `apps/edge/cmd/edge/**`
- `apps/edge/internal/bootstrap/**`
- `apps/edge/internal/transport/**`
- `apps/edge/internal/service/**`
- `apps/edge/internal/events/**`
- `apps/edge/internal/input/**`
- `apps/edge/internal/openai/**`
- `apps/edge/internal/opsconsole/**`
- `apps/edge/internal/node/**`
- dev-corp Edge host, provider pool config, OpenAI-compatible model alias routing
## 분류
- domain: edge
- verification_type: smoke
- scope: dev-corp Edge 실행 그룹, provider pool, input surface baseline
## 환경
- host: local checkout. dev-corp runtime, provider pool, shared port evidence가 필요하면 mac-mini `ssh fe@172.23.78.70``/Users/fe/agent-work/iop-dev-corp` checkout을 사용한다.
- port: compose Edge-Node TCP transport `19006`; native provider-pool Edge-Node TCP 후보 `18087`; artifact/bootstrap HTTP 후보 `18085`; Edge OpenAI-compatible HTTP 후보 `18086`; Edge metrics 후보 `19102`; admin 후보 `19094`.
- runtime: Go `1.24`
- package manager: Go modules / Makefile
- docker: unit/smoke quick check는 Docker를 요구하지 않는다. compose dev-corp 검증은 mac-mini에서 `.env.dev-corp.example` 기준으로 수행한다.
- external service: dev-corp artifact/base URL 후보 `http://172.23.78.70:18085`, dev-corp Edge runtime 후보 `172.23.78.70:18087`
- model endpoint: dev-corp OpenAI-compatible base URL 후보 `http://172.23.78.70:18086/v1`
- credential: token/secret/API key 원문은 문서에 기록하지 않는다.
## dev-corp provider pool 인벤토리
dev-corp provider pool과 3-node 연결 상태를 점검할 때는 `agent-test/dev-corp/inventory.yaml`의 machine-readable 값을 우선하고, mac-mini `ssh fe@172.23.78.70``/Users/fe/agent-work/iop-dev-corp` checkout을 기준으로 한다.
- Edge host: `Mac-mini.local`, `fe@172.23.78.70`, Apple M2 Pro / 16GB
- Edge config: `build/dev-corp-runtime/edge.yaml`
- Edge id: `edge-dev-corp`
- Control Plane HTTP 후보: `http://127.0.0.1:18002`
- bootstrap HTTP 후보: `http://172.23.78.70:18085`
- Edge OpenAI-compatible base URL 후보: `http://172.23.78.70:18086/v1`
- Edge-Node TCP transport 후보: `172.23.78.70:18087`
- model alias 후보: `gemma4:26b`
노드 후보:
- DGX Spark 01 vLLM node: `corp-dgx-spark-01-vllm-node` / `corp-dgx-spark-01-vllm`
- SSH/user: mac-mini에서 `ssh digitalcommerce_dgx_spark_01@192.168.2.2`
- provider endpoint: `http://192.168.2.2:8002/v1`
- Edge adapter endpoint: node-local `http://127.0.0.1:8002/v1`
- served model: `gemma-4-26B-A4B-it-NVFP4`
- capacity baseline 후보: `4`
- runtime: tmux `vllm_server_8002`, start script `/home/digitalcommerce_dgx_spark_01/start_vllm_8002.sh`, `vllm=0.23.0`, `PATH=/home/digitalcommerce_dgx_spark_01/vllm_env/bin:$PATH`, `--max-num-seqs 4`, `--max-model-len 262144`, `--gpu-memory-utilization 0.40`, startup log GPU KV cache `804,421` tokens / full-context concurrency `3.07x`, `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`
- DGX Spark 02 vLLM node: `corp-dgx-spark-02-vllm-node` / `corp-dgx-spark-02-vllm`
- SSH/user: mac-mini에서 `ssh dplab@192.168.2.4`
- provider endpoint: `http://192.168.2.4:8004/v1`
- Edge adapter endpoint: node-local `http://127.0.0.1:8004/v1`
- Edge connectivity: node02는 mac-mini `172.23.78.70:18085/18086` 직접 접근이 timeout이므로 mac-mini의 reverse SSH tunnel `127.0.0.1:28085/28087`을 사용한다.
- served model: `gemma-4-26B-A4B-it-NVFP4`
- capacity baseline 후보: `4`
- runtime: Docker container `vllm-gemma4`, start script `/home/dplab/start_vllm_gemma4_8004.sh`, host `8004` -> container `8004`, `vllm=0.23.0`, `--max-num-seqs 4`, `--max-model-len 262144`, `--gpu-memory-utilization 0.40`, startup log GPU KV cache `860,222` tokens / full-context concurrency `3.28x`, `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`
- Mac Studio vLLM-MLX node: `corp-mac-studio-mlx-vllm-node` / `corp-mac-studio-mlx-vllm`
- SSH/user: mac-mini에서 `ssh dc_dev@192.168.2.3`
- provider endpoint: `http://192.168.2.3:8004/v1`
- Edge adapter endpoint: node-local `http://127.0.0.1:8004/v1`
- served model: `mlx-community/gemma-4-26b-a4b-it-nvfp4`
- capacity baseline 후보: `5` (`--max-num-seqs 5` 기준)
- runtime: `vllm-mlx=0.3.0`, start script `/Users/dc_dev/iop-dev-corp-field/start_vllm_mlx_8004.sh`, `--max-num-seqs 5`, `--max-kv-size 786432`, `--max-request-tokens 262144`
- process manager: 2026-06-25 기준 detached `screen` session `vllm_mlx_8004`; `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib` 필요
Mac Studio의 `http://192.168.2.3:8005/v1` mlx-vlm DiffusionGemma endpoint는 확인된 secondary provider 후보지만, 기본 provider pool에는 넣지 않는다. 별도 alias와 capacity policy가 결정된 뒤 추가한다.
## 2026-06-25 provider runtime update 상태
- DGX Spark 01 provider runtime은 capacity `4`, context window `262144`, requested KV `262144x2` 이상의 기준으로 재설정했다. dev-corp DGX01 vLLM 기준으로는 `--max-num-seqs 4`, `--max-model-len 262144`, `--gpu-memory-utilization 0.40`, `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304` 조합이며, startup log에서 GPU KV cache `804,421` tokens와 `262144` tokens/request concurrency `3.07x`를 확인했다.
- DGX Spark 02 provider runtime은 capacity `4`, context window `262144`, requested KV `262144x2` 이상의 기준으로 설정됐다. Docker `vllm-gemma4`는 host/container `8004` 기준으로 동작하며 startup log에서 GPU KV cache `860,222` tokens와 `262144` tokens/request concurrency `3.28x`를 확인했다.
- DGX Spark 01은 FlashInfer FP4 JIT가 `ninja`를 PATH에서 호출하므로 start script에 `PATH=/home/digitalcommerce_dgx_spark_01/vllm_env/bin:$PATH`를 둔다.
- Mac Studio provider runtime은 capacity `5`, context window `262144`, requested KV `262144x3` 기준으로 `--continuous-batching`, `--max-num-seqs 5`, `--prefill-batch-size 5`, `--completion-batch-size 5`, `--chunked-prefill-tokens 1024`, `--max-request-tokens 262144`, `--max-kv-size 786432`, `--reasoning-parser gemma4`을 적용했고 `/health`, `/v1/models`, 직접 동시성 `1..5` chat completion benchmark 통과를 확인했다.
- 2026-06-25 재확인 기준 DGX Spark 01은 mac-mini public forward `http://172.23.78.70:8002`에서 `/health` 200, `/v1/models`, `/v1/chat/completions` 단문 smoke를 통과했다. DGX Spark 02는 mac-mini에서 SSH, provider `/health`, `/v1/models`가 회복됐고 직접 provider benchmark를 통과했다. Edge OpenAI-compatible capacity smoke는 별도로 재검증해야 한다.
## 명령
- setup:
- lint:
- unit: `go test ./apps/edge/...`
- smoke: `./scripts/e2e-smoke.sh`는 기본 포트 임시 설정을 쓰는 보조 smoke이므로 dev-corp 포트 override 필요 여부를 먼저 확인한다.
- e2e: `make test-e2e`는 보조 smoke이며 full-cycle 실제 구동을 대체하지 않는다.
- model: dev-corp OpenAI-compatible profile을 띄운 경우 `OPENAI_API_KEY=$(cat build/dev-corp-runtime/.secrets/openai_api_key) iop-edge smoke openai --model gemma4:26b --base-url http://127.0.0.1:18086`
- direct-provider: mac-mini에서 `curl -fsS http://192.168.2.2:8002/v1/models`, `curl -fsS http://192.168.2.4:8004/v1/models`, `curl -fsS http://192.168.2.3:8004/v1/models`
- full-cycle: repo 내부 edge-node 진단, `iop-edge smoke openai`, OpenAI-compatible 입력 표면 수동 검증
## 필수 검증
- 변경한 edge 패키지 또는 `go test ./apps/edge/...`를 실행한다.
- registry, service, transport, console, HTTP/A2A 입력 표면을 바꾼 경우 edge-node 메시지 2회 왕복과 command 응답을 확인한다.
- OpenAI-compatible 경계를 바꾼 경우 dev-corp `18086` 기준 `iop-edge smoke openai` 또는 동등한 `/healthz`, `/v1/models`, `/v1/responses` 확인으로 edge service와 node adapter 경로 수렴을 확인한다.
- provider pool config 변경 전 mac-mini에서 세 기본 provider endpoint의 `/health``/v1/models`가 성공하는지 확인한다.
- DGX Spark 02가 provider port down과 SSH banner exchange timeout을 동시에 보이면 provider runtime 추가 조작을 보류하고, SSH 회복 후 process/log, `/health`, `/v1/models`, Edge OpenAI-compatible smoke를 순서대로 재검증한다.
- bootstrap/artifact 경계를 바꾼 경우 dev-corp artifact/base URL 후보 `18085`가 local/test/dev field baseline을 덮어쓰지 않는지 확인한다.
## 보조 검증
- `./scripts/e2e-smoke.sh`는 edge-node 최소 생존 확인에 사용한다.
- `./scripts/e2e-openai-ollama.sh`는 OpenAI-compatible 입력 표면 보조 확인에 사용할 수 있으나 provider pool capacity 검증을 대체하지 않는다.
## 판정 기준
- node 등록, `/nodes`, console 메시지 전송, 기대 payload를 포함한 `[node-*-message]` 출력, 같은 run의 complete event가 확인된다.
- node 로컬 `[node-message]` payload 라인 목록과 edge `[node-*-message]` payload 라인 목록이 run별로 내용/순서까지 동일해야 한다.
- edge complete는 같은 run의 마지막 `[node-*-message]` 이후에만 정상이다.
- OpenAI-compatible smoke에서 `/healthz`, `/v1/models`, `/v1/responses`가 기대 상태로 응답한다.
- dev-corp capacity smoke는 `/v1/responses``/v1/chat/completions` 각각에 provider capacity 총합 + 1개 동시 요청을 보낸다. Edge config에서 기본 후보 capacity `4 + 4 + 5 = 13`을 확정한 경우 Control Plane status의 `provider_snapshots`에서 총 `in_flight=13`, `queued>=1` 관측을 기준으로 한다.
- capacity smoke 완료 후 대상 provider의 `in_flight=0`, `queued=0` 회복을 확인한다.
- Gemma 계열 provider-pool smoke는 reasoning/tool-parser 관련 텍스트가 포함될 수 있다. exact-output match를 기본 판정으로 쓰지 않는다.
## 기준 출력 예시
```text
curl -fsS http://192.168.2.2:8002/v1/models
curl -fsS http://192.168.2.4:8004/v1/models
curl -fsS http://192.168.2.3:8004/v1/models
```
## 차단 기준
- mac-mini host 또는 Edge-Node TCP 후보 port 접근이 불가능하다.
- mac-mini에 `/Users/fe/agent-work/iop-dev-corp` checkout이 없어 dev-corp runtime artifact를 만들 수 없다.
- provider endpoint `192.168.2.2:8002`, `192.168.2.4:8004`, `192.168.2.3:8004` 중 필수 endpoint가 닫혀 있다.
- 외부 CLI profile 검증에 필요한 CLI 설치, 계정, provider 상태가 없다.
- dev-corp 포트가 local/test/dev baseline과 충돌한다.
## 보고 항목
- 실행한 명령:
- 성공한 검증:
- 실패/차단된 검증:
- 생략 사유:
- 남은 위험:
## 금지 사항
- secret, token, API key 원문은 tracked 파일에 기록하지 않는다.
- mac-mini에서 다시 도달 가능한 provider endpoint를 확인하지 않고 Edge provider config 성공을 선언하지 않는다.
- field baseline 포트(`18080`, `18081`, `19090`, `19092`)나 dev 포트(`18082`, `18083`, `19003`, `19101`)를 dev-corp 전용 포트로 재사용하지 않는다.

View file

@ -0,0 +1,267 @@
test_env: dev-corp
profile: dev-corp-provider-pool
last_updated_at: "2026-06-25"
source:
remote_runner:
ssh: fe@172.23.78.70
repo_root: /Users/fe/agent-work/iop-dev-corp
setup_required: true
setup_status: deployed
current_observation: /Users/fe/agent-work/iop-dev-corp checkout and build/dev-corp-runtime runtime were created on 2026-06-25; /Users/fe/iop-field remains unrelated legacy field state
clean_sync:
- git fetch origin main
- git reset --hard origin/main
- git clean -fd
dirty_policy: discard
edge:
id: edge-dev-corp
host: mac-mini
ssh: fe@172.23.78.70
os: macOS 26.2
hardware: Apple M2 Pro / 16GB
config_path: build/dev-corp-runtime/edge.yaml
control_plane_enabled_current_runtime: false
control_plane_http: http://127.0.0.1:18002
control_plane_status_url: http://127.0.0.1:18002/edges/edge-dev-corp/status
bootstrap_http_public: http://172.23.78.70:18085
openai_base_url_public: http://172.23.78.70:18086/v1
openai_base_url_runner: http://127.0.0.1:18086/v1
openai_api_key_required_current_runtime: true
openai_api_key_secret_path_remote: build/dev-corp-runtime/.secrets/openai_api_key
openai_api_key_value_tracked: false
edge_node_tcp_public: 172.23.78.70:18087
admin_addr_runner: 127.0.0.1:19094
build:
binaries:
edge: build/dev-corp-runtime/bin/iop-edge
node_macos: build/dev-corp-runtime/bin/iop-node-darwin-arm64
node_linux_arm64: build/dev-corp-runtime/bin/iop-node-linux-arm64
node_windows_amd64: null
node_windows_amd64_note: not part of the default dev-corp provider pool
model:
alias: gemma4:26b
alias_status: candidate
alias_policy: expose one IOP model alias and map provider-specific served_model values per provider
provider_capacity_total: 13
provider_capacity_status: all_three_direct_provider_benchmarks_passed_edge_capacity_queue_not_run
context_window_max: 262144
runtime_capacity_targets:
dgx_spark:
capacity: 4
context_window_max: 262144
kv_size_tokens_effective: 524288
kv_size_basis: requested minimum 262144x2; verify actual vLLM KV cache from startup log because gpu_memory_utilization controls available KV cache
fp4_moe_env: VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304
mac_studio:
capacity: 5
context_window_max: 262144
kv_size_tokens_effective: 786432
kv_size_basis: requested 262144x3 mapped to vLLM-MLX max_kv_size
capacity_smoke:
endpoints:
- /v1/responses
- /v1/chat/completions
concurrent_requests: capacity_plus_one
expected_total_in_flight: 13
expected_total_in_flight_status: target_unverified_until_edge_config_and_capacity_smoke
expected_min_queued: 1
extended_concurrent_requests: 16
extended_expected_min_queued: 3
prompt_policy: long_reasoning_allowed
exact_output_match: false
latest_runtime_update:
date: "2026-06-25"
reason: user requested dev-corp node capacity/context/KV alignment
requested_targets:
dgx_spark:
capacity: 4
context_window_max: 262144
kv_size: 262144x2
mac_studio:
capacity: 5
context_window_max: 262144
kv_size: 262144x3
applied_mapping:
dgx_spark: vLLM has no vLLM-MLX style max_kv_size flag; requested KV 262144x2 is validated from startup log KV cache size and max_model_len 262144
mac_studio: vLLM-MLX requested KV 262144x3 is represented as max_kv_size 786432 with max_request_tokens 262144
remediation_applied:
- DGX Spark vLLM FP4 MoE first profile required VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304 for max_num_batched_tokens 524288
- DGX Spark 01 FlashInfer FP4 JIT invokes ninja from PATH; start script now prefixes /home/digitalcommerce_dgx_spark_01/vllm_env/bin
- Mac Studio vLLM-MLX pyexpat required DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib
verification:
last_rechecked_at: "2026-06-25"
mac_studio_health: passed
dgx01_health: passed
dgx02_health: passed
direct_health_observation:
dgx01: public forward http://172.23.78.70:8002 returned /health 200, /v1/models exposed gemma-4-26B-A4B-it-NVFP4, and /v1/chat/completions returned OK after gpu_memory_utilization 0.40 restart
dgx02: node-local http://127.0.0.1:8004 and mac-mini http://192.168.2.4:8004 returned /health 200, /v1/models exposed gemma-4-26B-A4B-it-NVFP4, and direct concurrency 1..4 chat completion benchmark passed
mac_studio: node-local http://127.0.0.1:8004 and mac-mini http://192.168.2.3:8004 returned /health 200, /v1/models exposed mlx-community/gemma-4-26b-a4b-it-nvfp4, and direct concurrency 1..5 chat completion benchmark passed
edge_openai_smoke: target_unverified_until_edge_provider_pool_capacity_smoke
nodes:
- id: corp-dgx-spark-01-vllm-node
alias: corp-dgx-spark-01-vllm
role: vllm-provider
ssh: digitalcommerce_dgx_spark_01@192.168.2.2
ssh_origin: mac-mini
workspace: /home/digitalcommerce_dgx_spark_01
provider_pool_candidate: true
provider:
id: corp-dgx-spark-01-vllm
type: vllm
endpoint: http://192.168.2.2:8002/v1
edge_adapter_endpoint: http://127.0.0.1:8002/v1
health: http://192.168.2.2:8002/health
served_model: gemma-4-26B-A4B-it-NVFP4
capacity: 4
capacity_status: configured_health_passed_direct_smoke
last_runtime_observation: restarted with gpu_memory_utilization 0.40; public forward http://172.23.78.70:8002 returned /health 200, /v1/models, and a chat completion; startup log reported GPU KV cache size 804,421 tokens and 3.07x concurrency for 262,144-token requests
capacity_basis: dev-corp target baseline; vLLM --max-num-seqs 4, --max-model-len 262144, --gpu-memory-utilization 0.40; observed KV cache satisfies requested 262144x2 minimum
runtime:
host: spark-0b30
os: Ubuntu 24.04.3 LTS / aarch64
hardware: NVIDIA DGX Spark / NVIDIA GB10 / 119GiB RAM
manager: tmux
tmux_session: vllm_server_8002
start_script: /home/digitalcommerce_dgx_spark_01/start_vllm_8002.sh
python: /home/digitalcommerce_dgx_spark_01/vllm_env/bin/python3
path_prefix_required: /home/digitalcommerce_dgx_spark_01/vllm_env/bin
path_prefix_reason: FlashInfer FP4 JIT invokes ninja from PATH during first compile
package_baseline:
vllm: 0.23.0
torch: 2.11.0
transformers: 5.12.1
flashinfer_python: 0.6.12
huggingface_hub: 1.20.1
args:
env:
PATH_prefix: /home/digitalcommerce_dgx_spark_01/vllm_env/bin
VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE: "4194304"
host: 0.0.0.0
port: 8002
dtype: bfloat16
gpu_memory_utilization: 0.40
max_model_len: 262144
max_num_seqs: 4
max_num_batched_tokens: auto_2496
max_num_batched_tokens_note: vLLM raised the default from 2048 to 2496 for the Gemma4 prefix-LM/video path; it is a scheduler iteration budget, not the total KV cache size
context_window_max: 262144
kv_size_tokens_effective: 804421
full_context_concurrency_observed: 3.07
- id: corp-dgx-spark-02-vllm-node
alias: corp-dgx-spark-02-vllm
role: vllm-provider
ssh: dplab@192.168.2.4
ssh_origin: mac-mini
workspace: /home/dplab
provider_pool_candidate: true
provider:
id: corp-dgx-spark-02-vllm
type: vllm
endpoint: http://192.168.2.4:8004/v1
edge_adapter_endpoint: http://127.0.0.1:8004/v1
health: http://192.168.2.4:8004/health
served_model: gemma-4-26B-A4B-it-NVFP4
capacity: 4
capacity_status: configured_health_passed_direct_smoke
last_runtime_observation: Docker vllm-gemma4 running with gpu_memory_utilization 0.40; node-local and mac-mini /health passed, /v1/models exposed gemma-4-26B-A4B-it-NVFP4, and direct concurrency 1..4 chat completion benchmark passed; startup log reported GPU KV cache size 860,222 tokens and 3.28x concurrency for 262,144-token requests
capacity_basis: dev-corp target baseline; Docker publishes host 8004 to container 8004; vLLM --max-num-seqs 4, --max-model-len 262144, --gpu-memory-utilization 0.40; observed KV cache satisfies requested 262144x2 minimum
edge_connectivity:
mode: reverse_ssh_tunnel
reason: node02 cannot reach mac-mini 172.23.78.70:18085/18086 directly from 192.168.2.4
tunnel_pid_file: /Users/fe/agent-work/iop-dev-corp/build/dev-corp-runtime/node02-tunnel.pid
remote_forwards:
artifact: 127.0.0.1:28085 -> mac-mini 127.0.0.1:18085
edge_node_tcp: 127.0.0.1:28087 -> mac-mini 127.0.0.1:18087
runtime:
host: spark-fb94
os: Ubuntu 24.04.3 LTS / aarch64
hardware: NVIDIA DGX Spark / NVIDIA GB10 / 119GiB RAM
manager: docker
container_name: vllm-gemma4
start_script: /home/dplab/start_vllm_gemma4_8004.sh
env_file: /home/dplab/.config/iop-dev-corp/vllm-gemma4.env
env_file_contains_secret: true
image: vllm/vllm-openai:latest
container_port: 8004
host_port: 8004
package_baseline:
vllm: 0.23.0
torch: 2.11.0+cu130
transformers: 5.12.0
flashinfer_python: 0.6.12
huggingface_hub: 1.19.0
args:
env:
VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE: "4194304"
gpu_memory_utilization: 0.40
max_model_len: 262144
max_num_seqs: 4
max_num_batched_tokens: auto_2496
max_num_batched_tokens_note: vLLM raised the default from 2048 to 2496 for the Gemma4 prefix-LM/video path; it is a scheduler iteration budget, not the total KV cache size
context_window_max: 262144
kv_size_tokens_effective: 860222
full_context_concurrency_observed: 3.28
- id: corp-mac-studio-mlx-vllm-node
alias: corp-mac-studio-mlx-vllm
role: vllm-mlx-provider
ssh: dc_dev@192.168.2.3
ssh_origin: mac-mini
workspace: /Users/dc_dev
provider_pool_candidate: true
provider:
id: corp-mac-studio-mlx-vllm
type: vllm-mlx
endpoint: http://192.168.2.3:8004/v1
edge_adapter_endpoint: http://127.0.0.1:8004/v1
health: http://192.168.2.3:8004/health
served_model: mlx-community/gemma-4-26b-a4b-it-nvfp4
capacity: 5
capacity_status: configured_health_passed_direct_smoke
last_runtime_observation: screen vllm_mlx_8004 running with max_num_seqs 5, max_request_tokens 262144, max_kv_size 786432; node-local and mac-mini /health passed, /v1/models exposed mlx-community/gemma-4-26b-a4b-it-nvfp4, and direct concurrency 1..5 chat completion benchmark passed
capacity_basis: vllm-mlx --max-num-seqs 5 with requested KV 262144x3 mapped to --max-kv-size 786432
runtime:
host: dc-devui-MacStudio.local
os: macOS 26.2
hardware: Apple M3 Ultra / 512GB RAM / 80-core GPU
manager: screen
manager_status: detached screen session vllm_mlx_8004; DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib is required for pyexpat on this host
screen_session: vllm_mlx_8004
start_script: /Users/dc_dev/iop-dev-corp-field/start_vllm_mlx_8004.sh
python: /Users/dc_dev/vllm-env/bin/python
package_baseline:
vllm_mlx: 0.3.0
mlx: 0.31.2
mlx_lm: 0.31.3
mlx_vlm: 0.6.3
torch: 2.12.1
transformers: 5.12.1
huggingface_hub: 1.20.1
args:
host: 0.0.0.0
port: 8004
continuous_batching: true
max_num_seqs: 5
prefill_batch_size: 5
completion_batch_size: 5
chunked_prefill_tokens: 1024
max_kv_size: 786432
max_request_tokens: 262144
context_window_max: 262144
kv_size_tokens_effective: 786432
reasoning_parser: gemma4
secondary_provider_candidate:
id: corp-mac-studio-mlx-vlm-diffusiongemma
type: mlx-vlm
endpoint: http://192.168.2.3:8005/v1
health: http://192.168.2.3:8005/health
served_model: mlx-community/diffusiongemma-26B-A4B-it-OptiQ-4bit
include_in_default_pool: false
note: exposes multiple models and should be routed only after alias/capacity policy is decided

View file

@ -0,0 +1,156 @@
---
test_env: dev-corp
test_profile: node-smoke
domain: node
verification_type: smoke
last_rule_updated_at: 2026-06-25
---
# node-smoke dev-corp 테스트
## 읽기 조건
- `apps/node/**` 변경 또는 dev-corp node 실행, adapter, transport, router, store 검증 판단이 필요한 경우
## 적용 범위
- `apps/node/cmd/node/**`
- `apps/node/internal/bootstrap/**`
- `apps/node/internal/node/**`
- `apps/node/internal/runtime/**`
- `apps/node/internal/router/**`
- `apps/node/internal/transport/**`
- `apps/node/internal/adapters/**`
- `apps/node/internal/store/**`
- dev-corp Linux ARM64/macOS Node bootstrap과 provider endpoint 연결
## 분류
- domain: node
- verification_type: smoke
- scope: dev-corp node 실행 파이프라인과 Edge 연결 baseline
## 환경
- host: local checkout. dev-corp host, provider, shared Edge runtime evidence가 필요하면 mac-mini `ssh fe@172.23.78.70`을 사용한다.
- port: compose Edge-Node TCP transport `19006`, native provider-pool Edge-Node TCP 후보 `18087`
- runtime: Go `1.24`
- package manager: Go modules / Makefile
- docker: unit/smoke quick check는 Docker를 요구하지 않는다. compose dev-corp 검증은 mac-mini에서 수행한다.
- external service: dev-corp Edge runtime 후보 `172.23.78.70:18087`
- model endpoint: dev-corp OpenAI-compatible base URL 후보 `http://172.23.78.70:18086/v1`
- credential: token/secret/API key 원문은 문서에 기록하지 않는다.
## dev-corp Node 접속 기준
dev-corp의 실제 3-node 연결을 점검할 때는 mac-mini `ssh fe@172.23.78.70``/Users/fe/agent-work/iop-dev-corp` checkout과 `build/dev-corp-runtime/edge.yaml`을 기준으로 한다. Node는 provider pool 기준에서 Edge-Node TCP `172.23.78.70:18087`로 붙는다.
단, DGX Spark 02는 2026-06-25 기준 mac-mini Edge port 직접 접근이 timeout이므로 `127.0.0.1:28087 -> mac-mini 127.0.0.1:18087` reverse SSH tunnel로 붙는다.
- DGX Spark 01 vLLM node: `corp-dgx-spark-01-vllm-node` / `corp-dgx-spark-01-vllm`
- SSH/user: mac-mini에서 `ssh digitalcommerce_dgx_spark_01@192.168.2.2`
- provider endpoint: `http://192.168.2.2:8002/v1`
- Edge adapter endpoint: node-local `http://127.0.0.1:8002/v1`
- served model: `gemma-4-26B-A4B-it-NVFP4`
- capacity baseline 후보: `4`
- runtime capacity/context/KV: `--max-num-seqs 4`, `--max-model-len 262144`, `--gpu-memory-utilization 0.40`; 2026-06-25 startup log 기준 GPU KV cache `804,421` tokens, `262144` full-context concurrency `3.07x`; FP4 MoE env `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`
- start script: `/home/digitalcommerce_dgx_spark_01/start_vllm_8002.sh`
- 주의: FlashInfer FP4 JIT가 first compile 중 `ninja`를 호출하므로 start script에서 `/home/digitalcommerce_dgx_spark_01/vllm_env/bin``PATH` 앞에 둔다.
- workspace: `/home/digitalcommerce_dgx_spark_01`
- DGX Spark 02 vLLM node: `corp-dgx-spark-02-vllm-node` / `corp-dgx-spark-02-vllm`
- SSH/user: mac-mini에서 `ssh dplab@192.168.2.4`
- provider endpoint: `http://192.168.2.4:8004/v1`
- Edge adapter endpoint: node-local `http://127.0.0.1:8004/v1`
- Edge addr: `127.0.0.1:28087` via mac-mini reverse SSH tunnel
- served model: `gemma-4-26B-A4B-it-NVFP4`
- capacity baseline 후보: `4`
- runtime capacity/context/KV: `--max-num-seqs 4`, `--max-model-len 262144`, `--gpu-memory-utilization 0.40`; 2026-06-25 startup log 기준 GPU KV cache `860,222` tokens, `262144` full-context concurrency `3.28x`; FP4 MoE env `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304`
- start script: `/home/dplab/start_vllm_gemma4_8004.sh`
- workspace: `/home/dplab`
- 주의: Docker publish 때문에 host endpoint와 container endpoint 모두 `8004`이다. `8000`, `8001`, `8002`를 기본 endpoint로 쓰지 않는다.
- Mac Studio vLLM-MLX node: `corp-mac-studio-mlx-vllm-node` / `corp-mac-studio-mlx-vllm`
- SSH/user: mac-mini에서 `ssh dc_dev@192.168.2.3`
- provider endpoint: `http://192.168.2.3:8004/v1`
- Edge adapter endpoint: node-local `http://127.0.0.1:8004/v1`
- served model: `mlx-community/gemma-4-26b-a4b-it-nvfp4`
- capacity baseline 후보: `5` (`--max-num-seqs 5` 기준)
- runtime capacity/context/KV: `--max-num-seqs 5`, `--max-request-tokens 262144`, requested KV `262144x3` mapped to `--max-kv-size 786432`
- start script: `/Users/dc_dev/iop-dev-corp-field/start_vllm_mlx_8004.sh`
- workspace: `/Users/dc_dev`
- process manager: 2026-06-25 기준 detached `screen` session `vllm_mlx_8004`; 이 host는 Python `pyexpat` 로딩에 `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib`가 필요하다.
DGX Spark nodes는 Linux/ARM64 bootstrap을 기본으로 한다. Mac Studio node는 macOS bootstrap을 기본으로 한다. Windows native bootstrap은 dev-corp provider pool 기본 대상이 아니다.
Mac Studio의 secondary `http://192.168.2.3:8005/v1` endpoint는 기본 Node/provider pool 검증 대상이 아니다. 별도 alias/capacity 정책이 확정된 후 추가한다.
## 2026-06-25 런타임 설정 반영 상태
- DGX Spark 01에는 capacity `4`, context window `262144`, requested KV `262144x2` 이상의 기준을 반영했다. vLLM에는 vLLM-MLX식 `--max-kv-size`가 없으므로 DGX01은 `--max-num-seqs 4`, `--max-model-len 262144`, `--gpu-memory-utilization 0.40`로 운용하고, 실제 KV cache는 startup log의 `GPU KV cache size`로 검증한다.
- DGX Spark 01은 2026-06-25 재기동 후 mac-mini public forward `http://172.23.78.70:8002` 기준 `/health` 200, `/v1/models`, `/v1/chat/completions` 단문 smoke를 통과했다. 같은 로그에서 GPU KV cache `804,421` tokens, `262144` tokens/request concurrency `3.07x`가 확인됐다.
- DGX Spark 02에는 capacity `4`, context window `262144`, requested KV `262144x2` 이상의 기준을 반영했다. Docker `vllm-gemma4`는 host/container `8004` 기준으로 동작하며, node-local과 mac-mini 경유 `/health`, `/v1/models`, 직접 동시성 `1..4` chat completion benchmark를 통과했다.
- DGX Spark에서 explicit `--max-num-batched-tokens 524288`를 사용하는 경우 first profile 중 FP4 MoE 커널 한계에 걸릴 수 있어 `VLLM_MAX_TOKENS_PER_EXPERT_FP4_MOE=4194304` 보정이 필요하다.
- DGX Spark 01은 FlashInfer FP4 JIT가 first compile 중 `ninja`를 PATH에서 찾으므로 `/home/digitalcommerce_dgx_spark_01/vllm_env/bin` PATH prefix가 필요하다.
- Mac Studio에는 capacity `5`, context window `262144`, requested KV `262144x3` 기준을 `--max-num-seqs 5`, `--max-request-tokens 262144`, `--max-kv-size 786432`로 반영했고 `/health`, `/v1/models`, 직접 동시성 `1..5` chat completion benchmark 통과를 확인했다.
- 2026-06-25 재확인 기준 DGX Spark 02는 mac-mini에서 SSH, provider `/health`, `/v1/models`가 회복됐고 직접 provider benchmark를 통과했다. Edge OpenAI-compatible capacity smoke는 별도로 재검증해야 한다.
## 명령
- setup:
- lint:
- unit: `go test ./apps/node/...`
- smoke: `./scripts/e2e-smoke.sh`는 기본 포트 임시 설정을 쓰는 보조 smoke이므로 dev-corp 포트 override 필요 여부를 먼저 확인한다.
- e2e: `make test-e2e`
- 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`
- full-cycle: repo 내부 edge-node 진단과 사용자 실행 cycle 수동 검증
## 필수 검증
- 변경한 node 패키지 또는 `go test ./apps/node/...`를 실행한다.
- 실행 요청, stream, cancel, status, session, adapter registry 경로를 바꾼 경우 repo 내부 edge-node 진단과 full-cycle 실제 구동 기준을 함께 적용한다.
- vLLM/OpenAI-compatible adapter 설정을 바꾼 경우 mac-mini에서 provider `/health``/v1/models`를 먼저 확인하고, 이후 Edge OpenAI-compatible 경로에서 model alias 후보 `gemma4:26b` 또는 작업에서 확정한 alias가 노출되는지 확인한다.
- dev-corp Edge runtime으로 연결하는 경우 Node가 `18087` native provider-pool TCP를 사용하고 local/test/dev baseline으로 붙지 않는지 확인한다.
- Node bootstrap UX는 Edge가 출력한 완성 명령을 사용하고, 사용자에게 수동 `node.yaml` 편집이나 named env parameter를 기본 경로로 요구하지 않는다.
## 보조 검증
- `./scripts/e2e-smoke.sh`는 mock adapter 기반 보조 smoke로 사용한다.
- `make test-e2e`는 보조 smoke이며 full-cycle 실제 구동을 대체하지 않는다.
## 판정 기준
- node 등록 후 edge console에서 node가 조회된다.
- 같은 session에서 메시지 2회가 start, 기대 payload를 포함한 `[node-*-message]`, 같은 run의 complete 순서로 edge 화면에 도착한다.
- node 로컬 `[node-message]` payload 라인 목록과 edge `[node-*-message]` payload 라인 목록이 run별로 내용/순서까지 동일해야 한다.
- edge complete는 같은 run의 마지막 `[node-*-message]` 이후에만 정상이다.
- command 결과가 `[node-*-<command>]` 또는 명확한 성공/unsupported/error 출력으로 edge 화면에 표시된다.
- provider nodes는 mac-mini에서 내부 endpoint로 `/health``/v1/models`가 성공해야 한다.
## 기준 출력 예시
```text
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
```
## 차단 기준
- mac-mini 또는 내부 provider node SSH 접근이 불가능하다.
- dev-corp Edge-Node TCP 후보 port 접근이 불가능하다.
- 필수 provider endpoint가 닫혀 있거나 `/v1/models`가 기대 모델을 노출하지 않는다.
- DGX Spark 02가 provider port down과 SSH banner exchange timeout을 동시에 보인다.
- Mac Studio vLLM-MLX node를 재시작해야 하는데 `8004` runtime의 `screen` session 또는 `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib` 적용 여부를 확인할 수 없다.
- 외부 CLI profile 검증에 필요한 CLI 설치, 계정, provider 상태가 없다.
## 보고 항목
- 실행한 명령:
- 성공한 검증:
- 실패/차단된 검증:
- 생략 사유:
- 남은 위험:
## 금지 사항
- 사용자 실행 파이프라인 변경을 unit test만으로 완료 처리하지 않는다.
- `proto/gen/iop/*.pb.go` 생성 파일을 직접 수정하지 않는다.
- secret, token, API key 원문은 tracked 파일에 기록하지 않는다.

View file

@ -0,0 +1,96 @@
---
test_env: dev-corp
test_profile: platform-common-smoke
domain: platform-common
verification_type: smoke
last_rule_updated_at: 2026-06-25
---
# platform-common-smoke dev-corp 테스트
## 읽기 조건
- `packages/go/**`, `proto/**`, `configs/**` 변경 또는 dev-corp 공통 설정, protobuf 계약, host setup 검증 판단이 필요한 경우
## 적용 범위
- `packages/go/**`
- `proto/iop/**`
- `proto/gen/iop/**`
- `configs/**`
- dev-corp env/compose/config 계약
- provider model alias와 provider-specific served_model mapping
## 분류
- domain: platform-common
- verification_type: smoke
- scope: 공통 패키지, 설정, protobuf 계약 baseline
## 환경
- host: local checkout. dev-corp field/runtime evidence가 필요한 경우에만 mac-mini runner를 사용한다.
- port: dev-corp Edge-Node TCP transport `19006`, native provider-pool Edge-Node TCP 후보 `18087`, Edge OpenAI-compatible HTTP 후보 `18086`, Client WS `19004`, artifact/bootstrap 후보 `18085`.
- runtime: Go `1.24`
- package manager: Go modules / Makefile
- docker: unit/codegen quick check는 Docker를 요구하지 않는다. compose dev-corp 검증은 mac-mini에서 수행한다.
- external service: dev-corp artifact/base URL 후보 `http://172.23.78.70:18085`
- model endpoint: dev-corp OpenAI-compatible base URL 후보 `http://172.23.78.70:18086/v1`
- credential: token/secret/API key 원문은 문서에 기록하지 않는다.
## 명령
- setup:
- lint:
- unit: `go test ./packages/go/... ./proto/gen/...`
- smoke: `go test ./...`
- e2e: `make test-e2e`
- model:
- full-cycle: 설정/proto 변경이 사용자 실행 파이프라인에 닿으면 edge-node 실제 구동 검증
## 필수 검증
- 공통 패키지 변경 시 변경 패키지 테스트 또는 `go test ./packages/go/... ./proto/gen/...`를 실행한다.
- protobuf 원본 변경 시 `make proto`로 Go 생성물을 갱신하고 생성물 diff를 확인한다.
- config 계약 변경 시 `packages/go/config` struct/default와 `configs/*.yaml` 예시가 일치하는지 확인한다.
- dev-corp env/compose 계약 변경 시 `.env.dev-corp.example`, `docker-compose.yml` 또는 compose override, `agent-test/dev-corp/rules.md`의 포트가 서로 일치하는지 확인한다.
- provider config 변경 시 IOP model alias 후보 `gemma4:26b` 또는 작업에서 확정한 alias와 각 provider `served_model` 값이 `agent-test/dev-corp/inventory.yaml`과 맞는지 확인한다.
## 보조 검증
- `go test ./...`는 저장소 전체 회귀 확인으로 사용한다.
- 실행 경로에 닿는 config/proto 변경은 `make test-e2e`를 보조 확인으로 사용할 수 있다.
## 판정 기준
- 공통 패키지는 앱 내부 패키지를 import하지 않는다.
- proto 생성물은 원본 proto와 `make proto` 결과로만 갱신된다.
- 설정 예시는 loader/default와 어긋나지 않는다.
- dev-corp 포트는 local/test/dev 포트를 덮어쓰지 않고 dev-corp profile에서만 사용된다.
- provider-specific served model id 차이는 Edge/OpenAI-compatible 경계에서 내부 `adapter + target` 매핑으로 흡수된다.
## 기준 출력 예시
```text
go test ./packages/go/... ./proto/gen/...
```
## 차단 기준
- `protoc` 또는 `protoc-gen-go`가 없어서 proto 생성 검증을 실행할 수 없다.
- config 변경이 실제 환경값이나 credential 없이는 판정 불가능하다.
- dev-corp provider endpoint나 model alias 결정이 불명확해 config baseline을 확정할 수 없다.
## 보고 항목
- 실행한 명령:
- 성공한 검증:
- 실패/차단된 검증:
- 생략 사유:
- 남은 위험:
## 금지 사항
- `proto/gen/iop/*.pb.go` 생성 파일을 직접 수정하지 않는다.
- 앱 하나만을 위한 임시 타입을 충분한 근거 없이 공통 패키지로 승격하지 않는다.
- secret, token, API key 원문은 tracked 파일에 기록하지 않는다.

View file

@ -0,0 +1,107 @@
---
test_env: dev-corp
last_rule_updated_at: 2026-06-25
---
# dev-corp 테스트 규칙
**현재 문서를 반드시 끝까지 정독하고 작업한다. 다 읽지 않고 즉각 작업은 금지한다.**
**dev-corp 테스트 환경은 회사망 내부 mac-mini Edge host와 내부 provider nodes를 기준으로 하며, local/dev stack과 공존하도록 host publish 포트와 runtime profile을 분리한다.**
## 공통 규칙
- dev-corp 테스트/검증은 이 파일을 기준으로 판단한다.
- 작업 완료 검증은 변경 범위 기준으로 선택한다.
- 필수 검증을 실행하지 못하면 차단 사유로 보고한다.
- 최종 보고에는 실행 명령, 결과, 생략 사유, 남은 위험을 남긴다.
- 환경값, secret, 개인 endpoint는 tracked docs/roadmap에 쓰지 않는다.
- token/secret/API key 원문은 원격 환경에서 주입하고, shell stdout/stderr와 최종 보고에 출력하지 않는다.
- private IP와 SSH user는 테스트 접속에 필요한 환경값으로 기록할 수 있으나, token/secret 원문은 기록하지 않는다.
- 테스트용 Docker 환경을 작성/수정할 때는 Docker Compose `networks`, IPAM subnet, static IPv4, `extra_hosts`, host publish 문서 어디에도 회사 LAN/VPN과 충돌하는 대역을 사용하지 않는다.
## 기본 환경
- host: Edge runner 후보는 `ssh fe@172.23.78.70` mac-mini이다.
- repo root: 목표 checkout은 `/Users/fe/agent-work/iop-dev-corp`이다. 2026-06-25 확인 기준 해당 checkout은 없고 `/Users/fe/iop-field`만 있으므로 최초 배포 전 repo root 생성이 필요하다.
- 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`.
- 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
- docker: 현재 작업 컨테이너에서는 Docker-in-Docker를 사용하지 않는다. Docker compose 검증은 mac-mini checkout에서 수행한다.
- external service: dev-corp Control Plane 후보 `http://172.23.78.70:18002`, Client wire 후보 `ws://172.23.78.70:19004/client`.
- model endpoint: Edge 배포 후 후보 `http://172.23.78.70:18086/v1`; 직접 provider 확인은 `agent-test/dev-corp/inventory.yaml`의 provider endpoint를 따른다.
- credential: secret/token/API key 원문은 문서에 기록하지 않는다.
## 포트 매핑
| 용도 | local/test | dev | dev-corp |
|---|---:|---:|---:|
| Web preview | `13000` | `13001` | `13002` |
| Control Plane HTTP | `18000` | `18001` | `18002` |
| CP Client WS | `19080` | `19001` | `19004` |
| CP-Edge wire | `19081` | `19002` | `19005` |
| Edge-Node TCP | `19090` | `19003` | `19006` |
| Edge artifact/bootstrap | `18080` | `18082` | `18085` |
| Edge OpenAI-compatible | `18081` | `18083` | `18086` |
| Edge metrics | `19092` | `19101` | `19102` |
| PostgreSQL host publish | `15400` | `15401` | `15402` |
| Redis host publish | `16300` | `16301` | `16302` |
dev-corp native/provider-pool profile은 compose Edge-Node TCP `19006`이 아니라 native Edge listen 후보 `18087`을 사용한다. 세부 provider endpoint와 Node 접속 기준은 `agent-test/dev-corp/inventory.yaml`, `agent-test/dev-corp/edge-smoke.md`, `agent-test/dev-corp/node-smoke.md`를 따른다.
## 런타임 프로필
- local quick check: 현재 checkout에서 Go quick check를 우선 실행한다.
- remote runner: dev-corp runtime evidence, Flutter client, Docker compose, field/bootstrap, provider-pool evidence는 `ssh fe@172.23.78.70``/Users/fe/agent-work/iop-dev-corp` 기준으로 수행한다.
- 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`을 사용한다.
- Edge direct dev-corp profile: artifact/bootstrap `18085`, OpenAI-compatible `18086`, metrics `19102`, admin `19094`를 사용한다.
- dev-corp provider pool: `/Users/fe/agent-work/iop-dev-corp/build/dev-corp-runtime/edge.yaml`과 Edge-Node TCP `172.23.78.70:18087`을 사용한다. 3-node/provider 세부는 `agent-test/dev-corp/edge-smoke.md``agent-test/dev-corp/node-smoke.md`를 따른다.
- external provider field: DGX Spark 01/02와 Mac Studio는 mac-mini에서 내부 SSH로 접속한다. 직접 접속 경로와 provider endpoint는 `agent-test/dev-corp/inventory.yaml`을 따른다.
## 프리플라이트
- plan 작성 시 필수 검증이 현재 checkout을 벗어나 mac-mini runner, field/bootstrap, 외부 provider, Docker/code-server, emulator/device, 공유 장기 runtime을 사용하면 먼저 테스트 환경 프리플라이트를 계획에 기록한다.
- 테스트 환경 프리플라이트에는 runner, repo root/workdir, branch/HEAD/dirty 상태, local 변경과 원격 source 동기화 여부, binary/artifact 경로와 필요한 help/version 출력, config path, runtime identity, port/process 상태, 외부 host, OS/arch 가정을 포함한다.
- mac-mini에 `/Users/fe/agent-work/iop-dev-corp` checkout이 없으면 배포/검증을 시작하지 말고 checkout 생성과 source sync를 setup blocker로 보고한다.
- provider pool 검증 전 mac-mini에서 `192.168.2.2:8002`, `192.168.2.4:8004`, `192.168.2.3:8004``/health``/v1/models`를 먼저 확인한다.
- `gemma4:26b` alias와 총 capacity `13`은 dev-corp target 후보이며, Edge config 반영과 capacity smoke가 통과하기 전에는 확정값으로 보고하지 않는다.
- 프리플라이트에서 dirty/divergent checkout 또는 stale artifact가 확인되면 dev-corp 배포에서는 먼저 원격 checkout을 clean sync하고 dev-corp runtime 바이너리를 rebuild한다. 잘못된 identity, missing command, closed port, host OS 불일치, source 미동기화가 확인되면 plan은 setup/sync/rebuild 단계를 만들거나 blocker로 보고한다.
## 노드/Provider 인벤토리 위치
- dev-corp provider pool machine-readable 기준: `agent-test/dev-corp/inventory.yaml`
- dev-corp provider pool과 Edge/OpenAI-compatible 입력 표면 상세 기준: `agent-test/dev-corp/edge-smoke.md`
- dev-corp Node 접속과 bootstrap 상세 기준: `agent-test/dev-corp/node-smoke.md`
- 공통 provider config 계약 기준: `agent-test/dev-corp/platform-common-smoke.md`
- compose stack 기준: `agent-test/dev-corp/control-plane-smoke.md`, `agent-test/dev-corp/client-smoke.md`, `agent-test/dev-corp/testing-smoke.md`
## Field/bootstrap 반복 테스트 기준
- field/bootstrap 검증은 mac-mini runner의 `/Users/fe/agent-work/iop-dev-corp` checkout과 dev-corp runtime artifact를 기준으로 수행한다.
- compose dev-corp stack은 Edge-Node TCP `19006`을 사용하고, dev-corp provider pool은 native Edge listen `18087`을 사용한다. 두 프로필을 섞어서 판정하지 않는다.
- Node bootstrap은 Edge의 `node register`가 출력한 OS별 완성 명령을 그대로 사용한다. 사용자에게 안내하는 명령에는 임의 placeholder, 수동 token 치환, `IOP_*=` named environment parameter를 넣지 않는다.
- token, API key, private credential 원문은 tracked docs, roadmap, 테스트 규칙에 기록하지 않는다. 실행 증거에는 token을 마스킹하거나 명령 생성 사실만 남긴다.
- Linux/macOS Node는 생성된 `curl | bash` 계열 명령을 사용한다. Windows Node는 dev-corp provider pool의 기본 대상이 아니다.
- dev-corp runtime 배포는 clean sync 후 `build/dev-corp-runtime/bin/iop-edge`, mac node, Linux ARM64 node binary를 같은 source 기준으로 rebuild한다.
- provider capacity, model/provider mapping은 `config check``config refresh --mode dry-run`을 통과한 뒤 `config refresh --mode apply`로 반영한다. refresh subcommand 또는 `19094` admin port가 없으면 바이너리 rebuild 누락으로 보고 먼저 rebuild한다.
- Edge process restart 또는 일시 단절 후에는 Node reconnect 정책을 검증한다. retry 한계를 넘겨 Node process가 종료된 경우에만 해당 Node host에서 새 bootstrap 실행이 필요하다.
## 라우팅
- node / smoke / node 실행 파이프라인 baseline: `agent-test/dev-corp/node-smoke.md`
- edge / smoke / edge 실행 그룹과 입력 표면 baseline: `agent-test/dev-corp/edge-smoke.md`
- dev-corp provider pool, 3-node 연결, DGX Spark vLLM, Mac Studio vLLM-MLX 점검: `agent-test/dev-corp/edge-smoke.md`, `agent-test/dev-corp/node-smoke.md`
- control-plane / smoke / control-plane health와 wire baseline: `agent-test/dev-corp/control-plane-smoke.md`
- client / smoke / Flutter client와 IOP console package baseline: `agent-test/dev-corp/client-smoke.md`
- platform-common / smoke / 공통 설정과 protobuf 계약 baseline: `agent-test/dev-corp/platform-common-smoke.md`
- testing / smoke / 테스트 도구와 full-cycle 검증 baseline: `agent-test/dev-corp/testing-smoke.md`
## 라우팅 규칙
- 여러 항목이 맞으면 모두 읽는다.
- 도메인 매핑이나 domain rule이 있으면 각 도메인의 `<domain>-smoke` 문서를 기본 baseline으로 둔다.
- 도메인이 아직 없을 때만 project-smoke를 fallback baseline으로 둔다.
- 도메인/검증 시나리오별 문서는 다른 테스트 문서로 라우팅하지 않는다.

View file

@ -0,0 +1,110 @@
---
test_env: dev-corp
test_profile: testing-smoke
domain: testing
verification_type: smoke
last_rule_updated_at: 2026-06-25
---
# testing-smoke dev-corp 테스트
## 읽기 조건
- `Makefile`, `scripts/dev/**`, `scripts/e2e-*.sh`, bootstrap pack, 테스트/검증 절차, dev-corp compose/profile 변경 판단이 필요한 경우
## 적용 범위
- `Makefile`
- `scripts/dev/edge.sh`
- `scripts/dev/node.sh`
- `scripts/e2e-smoke.sh`
- `scripts/e2e-openai-ollama.sh`
- `iop-edge bootstrap pack`
- `docker-compose.yml`
- `.env.dev-corp.example` 또는 dev-corp compose override
- `agent-test/dev-corp/**`
## 분류
- domain: testing
- verification_type: smoke
- scope: dev-corp 테스트 도구, 보조 smoke, full-cycle 실제 구동 기준
## 환경
- 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`.
- 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 후보 `http://172.23.78.70:18002`, dev-corp Client wire 후보 `ws://172.23.78.70:19004/client`, dev-corp Edge runtime 후보 `172.23.78.70:18087`
- model endpoint: dev-corp OpenAI-compatible base URL 후보 `http://172.23.78.70:18086/v1`
- credential: token/secret/API key 원문은 문서에 기록하지 않는다.
## 명령
- setup:
- 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-health: `curl -fsS http://127.0.0.1:18002/healthz`
- 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`
- e2e: `make test-e2e`
- model: `./scripts/e2e-openai-ollama.sh` 또는 dev-corp OpenAI-compatible smoke
- full-cycle: `make pack-node-target`, repo 내부 edge-node 진단, field bootstrap UX, 실제 provider pool 검증
## 필수 검증
- 테스트 도구 자체를 바꾼 경우 해당 도구를 직접 실행해 성공/실패 판정을 확인한다.
- 사용자 실행 파이프라인에 닿는 변경은 일반 Go 테스트와 변경 범위에 맞는 full-cycle 실제 구동을 함께 검증한다.
- dev-corp compose/profile 변경 시 `docker compose --env-file .env.dev-corp.example config`로 렌더링된 host publish 포트와 network name을 확인한다.
- `iop-edge bootstrap pack`, `make pack-edge`, 내장 artifact server 변경 시 현재 host target build와 artifact/checksum/bootstrap script를 확인한다.
- field/bootstrap/deploy 작업은 one-line bootstrap UX 기준을 적용한다.
- 사용자에게 전달하는 Node bootstrap 명령은 완성된 URL과 token positional value 하나만 포함한다.
- dev-corp provider-pool 작업은 mac-mini에서 provider direct endpoint health를 먼저 확인한다.
## 보조 검증
- `make test-e2e`, `./scripts/e2e-smoke.sh`, `./scripts/e2e-openai-ollama.sh`는 보조 smoke다.
- 보조 smoke는 full-cycle 실제 구동이나 field bootstrap 검증을 대체하지 않는다.
## 판정 기준
- dev-corp compose는 `COMPOSE_PROJECT_NAME=iop-dev-corp-agent``IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`을 사용한다.
- dev-corp compose가 local/test/dev 포트를 점유하지 않는다.
- 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 값 하나만 포함하는 한 줄 명령 형태를 유지한다.
- dev-corp provider pool의 기본 endpoint 3개가 mac-mini에서 `/health``/v1/models`에 성공 응답한다.
## 기준 출력 예시
```text
docker compose --env-file .env.dev-corp.example config
curl -fsS http://127.0.0.1:18002/healthz
curl -fsS http://192.168.2.2:8002/v1/models
```
## 차단 기준
- mac-mini 접근, 지정 port, provider 상태가 없어 필수 검증을 수행할 수 없다.
- mac-mini에 `/Users/fe/agent-work/iop-dev-corp` checkout이 없다.
- 사용자가 명시하지 않았는데 code-server 컨테이너 재시작이 필요한 상황이다.
- dev-corp 포트가 이미 사용 중이라 local/test/dev stack과 공존할 수 없다.
## 보고 항목
- 실행한 명령:
- 성공한 검증:
- 실패/차단된 검증:
- 생략 사유:
- 남은 위험:
## 금지 사항
- 보조 E2E smoke 통과만으로 완료 처리하지 않는다.
- 사용자 기본 경로에서 `IOP_ARTIFACT_BASE_URL=...`, `IOP_EDGE_ADDR=...`, `IOP_NODE_TOKEN=...` 같은 named environment parameter를 요구하지 않는다.
- 사용자가 명시하지 않으면 code-server 컨테이너를 재시작하지 않는다.
- 환경값을 사람용 docs나 roadmap에 복사하지 않는다.
- secret, token, API key 원문은 tracked 파일에 기록하지 않는다.

View file

@ -1141,6 +1141,70 @@ func TestSmokeOpenAICommandSuccess(t *testing.T) {
}
}
func TestSmokeOpenAICommandAuthenticatedEndpoint(t *testing.T) {
oldAPIKey, hadAPIKey := os.LookupEnv("OPENAI_API_KEY")
os.Unsetenv("OPENAI_API_KEY")
defer func() {
if hadAPIKey {
os.Setenv("OPENAI_API_KEY", oldAPIKey)
} else {
os.Unsetenv("OPENAI_API_KEY")
}
}()
const token = "test-token"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/healthz":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
case "/v1/models", "/v1/responses":
if r.Header.Get("Authorization") != "Bearer "+token {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":{"type":"unauthorized","message":"missing bearer token"}}`))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if r.URL.Path == "/v1/models" {
w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model","created":123456,"owned_by":"iop"}]}`))
return
}
w.Write([]byte(`{"id":"resp-123","object":"response","created_at":123456,"model":"test-model","output_text":"responses pong","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"responses pong"}]}]}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
root := rootCmd()
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"smoke", "openai", "--model", "test-model", "--base-url", server.URL, "--timeout", "5s"})
err := root.Execute()
if err == nil {
t.Fatalf("expected unauthenticated smoke to fail\n%s", out.String())
}
if !strings.Contains(err.Error(), "401") {
t.Fatalf("expected 401 error, got %v\n%s", err, out.String())
}
os.Setenv("OPENAI_API_KEY", token)
root = rootCmd()
out.Reset()
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"smoke", "openai", "--model", "test-model", "--base-url", server.URL, "--timeout", "5s"})
if err := root.Execute(); err != nil {
t.Fatalf("authenticated smoke failed: %v\n%s", err, out.String())
}
if !strings.Contains(out.String(), "IOP Edge OpenAI Smoke Test SUCCESS!") {
t.Fatalf("success output missing:\n%s", out.String())
}
}
func TestSmokeOpenAICommandWorkspace(t *testing.T) {
// Create a temporary directory for workspace
tmpDir, err := os.MkdirTemp("", "iop-smoke-workspace-*")

View file

@ -41,6 +41,8 @@ var (
smokeWorkspace string
smokeExpectFile string
smokeExpectContains string
smokeAPIKey string
smokeAPIKeyFile string
)
type openAIModel struct {
@ -111,6 +113,20 @@ func resolveSmokeBaseURL(cmd *cobra.Command) (string, error) {
return "http://" + net.JoinHostPort(host, port), nil
}
func resolveSmokeAPIKey() (string, error) {
if strings.TrimSpace(smokeAPIKey) != "" {
return strings.TrimSpace(smokeAPIKey), nil
}
if strings.TrimSpace(smokeAPIKeyFile) != "" {
data, err := os.ReadFile(smokeAPIKeyFile)
if err != nil {
return "", fmt.Errorf("read --api-key-file: %w", err)
}
return strings.TrimSpace(string(data)), nil
}
return strings.TrimSpace(os.Getenv("OPENAI_API_KEY")), nil
}
func smokeOpenAICmd() *cobra.Command {
c := &cobra.Command{
Use: "openai",
@ -142,6 +158,16 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti
client := &http.Client{
Timeout: timeoutDur,
}
apiKey, err := resolveSmokeAPIKey()
if err != nil {
return err
}
doRequest := func(req *http.Request) (*http.Response, error) {
if apiKey != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
return client.Do(req)
}
fmt.Fprintf(cmd.OutOrStdout(), "IOP Edge OpenAI Smoke Test\n")
fmt.Fprintf(cmd.OutOrStdout(), "========================================\n")
@ -151,7 +177,11 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti
fmt.Fprintf(cmd.OutOrStdout(), "Step 1: Checking /healthz ... ")
healthURL := baseURL + "/healthz"
resp, err := client.Get(healthURL)
req, err := http.NewRequest(http.MethodGet, healthURL, nil)
if err != nil {
return fmt.Errorf("build GET %s: %w", healthURL, err)
}
resp, err := doRequest(req)
if err != nil {
fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]")
return fmt.Errorf("GET %s failed: %w (make sure the edge server is running)", healthURL, err)
@ -171,7 +201,11 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti
fmt.Fprintf(cmd.OutOrStdout(), "Step 2: Checking /v1/models ... ")
modelsURL := baseURL + "/v1/models"
resp, err = client.Get(modelsURL)
req, err = http.NewRequest(http.MethodGet, modelsURL, nil)
if err != nil {
return fmt.Errorf("build GET %s: %w", modelsURL, err)
}
resp, err = doRequest(req)
if err != nil {
fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]")
return fmt.Errorf("GET %s failed: %w", modelsURL, err)
@ -219,10 +253,12 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti
}
responsesPayload := map[string]interface{}{
"model": smokeModel,
"input": prompt,
"stream": false,
"metadata": metadata,
"model": smokeModel,
"input": prompt,
"stream": false,
"metadata": metadata,
"max_output_tokens": 32,
"temperature": 0,
}
payloadBytes, err := json.Marshal(responsesPayload)
if err != nil {
@ -230,7 +266,12 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti
}
responsesURL := baseURL + "/v1/responses"
resp, err = client.Post(responsesURL, "application/json", bytes.NewReader(payloadBytes))
req, err = http.NewRequest(http.MethodPost, responsesURL, bytes.NewReader(payloadBytes))
if err != nil {
return fmt.Errorf("build POST %s: %w", responsesURL, err)
}
req.Header.Set("Content-Type", "application/json")
resp, err = doRequest(req)
if err != nil {
fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]")
return fmt.Errorf("POST %s failed: %w (make sure active nodes are online)", responsesURL, err)
@ -287,6 +328,8 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti
c.Flags().StringVar(&smokeWorkspace, "workspace", "", "path to the temporary workspace directory")
c.Flags().StringVar(&smokeExpectFile, "expect-file", "", "relative path under workspace to check for existence after smoke")
c.Flags().StringVar(&smokeExpectContains, "expect-contains", "", "substring expected to be found in the expected file")
c.Flags().StringVar(&smokeAPIKey, "api-key", "", "bearer token for authenticated OpenAI-compatible endpoints (defaults to OPENAI_API_KEY)")
c.Flags().StringVar(&smokeAPIKeyFile, "api-key-file", "", "file containing bearer token for authenticated OpenAI-compatible endpoints")
c.MarkFlagRequired("model")
return c

View file

@ -66,6 +66,9 @@ func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) {
if outputPolicy.Strict {
input["think"] = false
}
if options := req.providerOptions(); len(options) > 0 {
input["options"] = options
}
s.logger.Info("openai responses input",
zap.String("model", req.Model),

View file

@ -194,6 +194,8 @@ func TestChatCompletionsPassesOllamaOptions(t *testing.T) {
"model":"from-request",
"messages":[{"role":"user","content":"hi"}],
"options":{"temperature":0.2,"top_p":0.9,"num_predict":32,"stop":["END"]},
"max_tokens":12,
"temperature":0.1,
"keep_alive":"10m",
"think":false,
"format":"json",
@ -210,9 +212,12 @@ func TestChatCompletionsPassesOllamaOptions(t *testing.T) {
if !ok {
t.Fatalf("options not passed: %+v", fake.req.Input)
}
if options["temperature"].(float64) != 0.2 || options["top_p"].(float64) != 0.9 || options["num_predict"].(float64) != 32 {
if options["temperature"].(float64) != 0.1 || options["top_p"].(float64) != 0.9 || options["num_predict"].(float64) != 32 {
t.Fatalf("unexpected options: %+v", options)
}
if options["max_tokens"].(int) != 12 {
t.Fatalf("max_tokens not passed: %+v", options)
}
if fake.req.Input["keep_alive"] != "10m" || fake.req.Input["think"] != false || fake.req.Input["format"] != "json" {
t.Fatalf("top-level ollama fields not passed: %+v", fake.req.Input)
}
@ -221,6 +226,33 @@ func TestChatCompletionsPassesOllamaOptions(t *testing.T) {
}
}
func TestChatCompletionsMapsMaxCompletionTokens(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "openai_compat"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"from-request",
"messages":[{"role":"user","content":"hi"}],
"max_completion_tokens":7
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
options, ok := fake.req.Input["options"].(map[string]any)
if !ok {
t.Fatalf("options not passed: %+v", fake.req.Input)
}
if options["max_tokens"].(int) != 7 {
t.Fatalf("max_completion_tokens not mapped to provider max_tokens: %+v", options)
}
}
func TestChatCompletionsStreamsSSE(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
@ -581,7 +613,9 @@ func TestResponsesDispatchesNonStreamingRequest(t *testing.T) {
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"say hello"
"input":"say hello",
"max_output_tokens":16,
"temperature":0.2
}`))
w := httptest.NewRecorder()
@ -596,6 +630,13 @@ func TestResponsesDispatchesNonStreamingRequest(t *testing.T) {
if fake.req.Prompt != "say hello" {
t.Fatalf("prompt: got %q", fake.req.Prompt)
}
options, ok := fake.req.Input["options"].(map[string]any)
if !ok {
t.Fatalf("options not passed: %+v", fake.req.Input)
}
if options["max_tokens"].(int) != 16 || options["temperature"].(float64) != 0.2 {
t.Fatalf("unexpected response options: %+v", options)
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {

View file

@ -6,15 +6,24 @@ import (
)
type chatCompletionRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
Stream bool `json:"stream"`
Metadata json.RawMessage `json:"metadata,omitempty"`
Options map[string]any `json:"options,omitempty"`
Format any `json:"format,omitempty"`
KeepAlive any `json:"keep_alive,omitempty"`
Think any `json:"think,omitempty"`
Tools []any `json:"tools,omitempty"`
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
Stream bool `json:"stream"`
Metadata json.RawMessage `json:"metadata,omitempty"`
Options map[string]any `json:"options,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
Seed *int `json:"seed,omitempty"`
Stop any `json:"stop,omitempty"`
ResponseFormat any `json:"response_format,omitempty"`
Format any `json:"format,omitempty"`
KeepAlive any `json:"keep_alive,omitempty"`
Think any `json:"think,omitempty"`
Tools []any `json:"tools,omitempty"`
}
type chatMessage struct {
@ -52,8 +61,25 @@ func (req chatCompletionRequest) runInput(prompt string, messages []chatMessage,
"prompt": prompt,
"messages": chatMessagesInput(messages),
}
if len(req.Options) > 0 {
input["options"] = req.Options
options := cloneOptions(req.Options)
if req.MaxTokens != nil {
options["max_tokens"] = *req.MaxTokens
} else {
setOptionInt(options, "max_tokens", req.MaxCompletionTokens)
}
setOptionFloat(options, "temperature", req.Temperature)
setOptionFloat(options, "top_p", req.TopP)
setOptionFloat(options, "presence_penalty", req.PresencePenalty)
setOptionFloat(options, "frequency_penalty", req.FrequencyPenalty)
setOptionInt(options, "seed", req.Seed)
if req.Stop != nil {
options["stop"] = req.Stop
}
if req.ResponseFormat != nil {
options["response_format"] = req.ResponseFormat
}
if len(options) > 0 {
input["options"] = options
}
if req.Format != nil {
input["format"] = req.Format
@ -72,6 +98,29 @@ func (req chatCompletionRequest) runInput(prompt string, messages []chatMessage,
return input
}
func cloneOptions(options map[string]any) map[string]any {
if len(options) == 0 {
return map[string]any{}
}
out := make(map[string]any, len(options))
for k, v := range options {
out[k] = v
}
return out
}
func setOptionInt(options map[string]any, key string, val *int) {
if val != nil {
options[key] = *val
}
}
func setOptionFloat(options map[string]any, key string, val *float64) {
if val != nil {
options[key] = *val
}
}
type strictOutputPolicy struct {
Strict bool
StreamBuffer bool
@ -204,11 +253,27 @@ type errorBody struct {
// Responses API types
type responsesRequest struct {
Model string `json:"model"`
Input json.RawMessage `json:"input"`
Instructions string `json:"instructions,omitempty"`
Stream bool `json:"stream"`
Metadata json.RawMessage `json:"metadata,omitempty"`
Model string `json:"model"`
Input json.RawMessage `json:"input"`
Instructions string `json:"instructions,omitempty"`
Stream bool `json:"stream"`
Metadata json.RawMessage `json:"metadata,omitempty"`
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
}
func (req responsesRequest) providerOptions() map[string]any {
options := map[string]any{}
if req.MaxOutputTokens != nil {
options["max_tokens"] = *req.MaxOutputTokens
} else if req.MaxTokens != nil {
options["max_tokens"] = *req.MaxTokens
}
setOptionFloat(options, "temperature", req.Temperature)
setOptionFloat(options, "top_p", req.TopP)
return options
}
type responsesMetadata struct {