feat: lemonade provider serving validation milestone completion

- Update PHASE.md and milestone roadmap
- Archive field smoke test artifacts (plan, code review, complete.log)
- Add e2e-openai-lemonade.sh script
- Update Makefile
This commit is contained in:
toki 2026-06-15 15:39:03 +09:00
parent 898da6e3af
commit a27994f4c5
10 changed files with 912 additions and 24 deletions

View file

@ -1,4 +1,4 @@
.PHONY: all build build-local build-edge build-edge-host build-node build-node-target build-node-targets pack-node-target pack-edge archive-edge tidy test test-e2e test-control-plane-edge-wire test-openai-ollama proto proto-dart client-test client-build-web clean
.PHONY: all build build-local build-edge build-edge-host build-node build-node-target build-node-targets pack-node-target pack-edge archive-edge tidy test test-e2e test-control-plane-edge-wire test-openai-ollama test-openai-lemonade proto proto-dart client-test client-build-web clean
GOFLAGS ?= -trimpath
BUILD_DIR ?= build
@ -88,6 +88,9 @@ test-control-plane-edge-wire:
test-openai-ollama:
./scripts/e2e-openai-ollama.sh
test-openai-lemonade:
./scripts/e2e-openai-lemonade.sh
# Requires: protoc + protoc-gen-go (go install google.golang.org/protobuf/cmd/protoc-gen-go@latest)
proto:
protoc \

View file

@ -24,9 +24,9 @@ Ollama 경로가 안정화된 뒤, 그 결과를 기준선으로 삼아 Lemonade
- 경로: `agent-roadmap/archive/phase/inference-provider-extension/milestones/provider-availability-capacity-queue-foundation.md`
- 요약: NomadCode workspace 실행 계약이 닫힌 뒤 provider별 health/model probe 차이를 Node adapter 내부로 숨기고, Edge가 공통으로 볼 수 있는 최소 상태와 capacity/in-flight/queued snapshot, FIFO admission queue 기준선을 만든다.
- [계획] Lemonade provider 서빙 경로 추가
- [검토중] Lemonade provider 서빙 경로 추가
- 경로: `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- 요약: Node provider 상태와 capacity queue 기반 위에서 Lemonade provider의 adapter/config/target/model 매핑과 모델 조회, non-streaming/streaming chat 경로를 검증다.
- 요약: Node provider 상태와 capacity queue 기반 위에서 Lemonade provider의 adapter/config/target/model 매핑과 모델 조회, non-streaming/streaming chat 경로를 검증했고, 사용자 최종 확인과 archive 승인을 기다린다.
- [계획] vLLM provider 서빙 경로 추가
- 경로: `agent-roadmap/phase/inference-provider-extension/milestones/vllm-provider-serving-validation.md`

View file

@ -13,7 +13,7 @@ Edge OpenAI-compatible 입력 표면에서 Lemonade target/model의 모델 조
## 상태
[계획]
[검토중]
## 승격 조건
@ -21,10 +21,11 @@ Edge OpenAI-compatible 입력 표면에서 Lemonade target/model의 모델 조
## 구현 잠금
- 상태: 잠금
- 결정 필요: 아래 체크리스트
- 상태: 해제
- 결정 필요: 없음
- 해소된 결정:
- [x] Lemonade provider를 독립 `lemonade` adapter로 둘지, OpenAI-compatible inference server 공통 adapter 계열로 둘지 결정한다.
- [ ] 실테스트에 사용할 Lemonade endpoint, served model name 또는 기준 model, 인증/헤더 필요 여부, streaming 지원 기대 동작을 확인한다.
- [x] 실테스트에 사용할 Lemonade endpoint, served model name 또는 기준 model, 인증/헤더 필요 여부, streaming 지원 기대 동작을 확인한다.
## 범위
@ -40,22 +41,25 @@ Edge OpenAI-compatible 입력 표면에서 Lemonade target/model의 모델 조
### Epic: [lemonade-provider] Lemonade Provider Serving Path
- [x] [provider-boundary] Lemonade provider를 독립 adapter로 둘지 OpenAI-compatible inference server 공통 adapter로 둘지 결정 근거가 정리되어 있다.
- [ ] [config-contract] Lemonade endpoint, served model name 또는 model, auth/header, timeout, option passthrough 설정 계약이 정리되어 있다.
- [ ] [availability] Lemonade provider의 endpoint/model probe 결과가 Node 공통 상태 모델 `unknown`, `available`, `unavailable`로 노출되는 기준이 검증되어 있다.
- [ ] [models-chat] Edge OpenAI-compatible `/v1/models`와 non-streaming `/v1/chat/completions`가 Lemonade provider로 수렴하는 기준이 검증되어 있다.
- [ ] [streaming] streaming `/v1/chat/completions`에서 SSE chunk, finish reason, 종료 신호가 Lemonade provider 경로로 안정적으로 전달되는지 검증되어 있다.
- [ ] [field-smoke] split-host field smoke에서 Lemonade target/model을 선택해 모델 조회와 chat 왕복이 검증되어 있다.
- [ ] [follow-up-scope] 코드 수정이 필요한 항목은 이 Milestone의 추가 Task 또는 같은 Milestone task group의 후속 plan으로 정리되어 있다.
- [x] [config-contract] Lemonade endpoint, served model name 또는 model, auth/header, timeout, option passthrough 설정 계약이 정리되어 있다.
- [x] [availability] Lemonade provider의 endpoint/model probe 결과가 Node 공통 상태 모델 `unknown`, `available`, `unavailable`로 노출되는 기준이 검증되어 있다.
- [x] [models-chat] Edge OpenAI-compatible `/v1/models`와 non-streaming `/v1/chat/completions`가 Lemonade provider로 수렴하는 기준이 검증되어 있다.
- [x] [streaming] streaming `/v1/chat/completions`에서 SSE chunk, finish reason, 종료 신호가 Lemonade provider 경로로 안정적으로 전달되는지 검증되어 있다.
- [x] [field-smoke] split-host field smoke에서 Lemonade target/model을 선택해 모델 조회와 chat 왕복이 검증되어 있다.
- [x] [follow-up-scope] 코드 수정이 필요한 항목은 이 Milestone의 추가 Task 또는 같은 Milestone task group의 후속 plan으로 정리되어 있다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 모든 기능 Task가 아직 충족되지 않았다.
- 상태: 요청됨
- 요청일: 2026-06-15
- 완료 근거:
- `01_config_contract`, `02+01_openai_compatible_adapter`, `03+02_field_smoke` 완료 로그의 `Roadmap Completion`이 모든 기능 Task id를 PASS로 보고했다.
- Lemonade real smoke는 local LAN Lemonade server 일반 포트 `13305``Gemma-4-26B-A4B-it-GGUF` 기준으로 PASS했다.
- fake smoke, Makefile target, 관련 Go package tests, `git diff --check`가 PASS했다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
- 리뷰 코멘트: 코드레벨 완료 검토에서 smoke assertion 보강만 직접 반영했고, 큰 후속 plan이 필요한 이슈는 발견되지 않았다. 모든 기능 Task가 충족되어 완료 후보 상태다. 사용자 최종 확인과 archive 승인 전까지 활성 Milestone 경로에 유지한다.
## 범위 제외
@ -75,4 +79,4 @@ Edge OpenAI-compatible 입력 표면에서 Lemonade target/model의 모델 조
- 표준선(확정): Lemonade는 독립 `lemonade` adapter를 새로 만들기보다 OpenAI-compatible inference server 공통 adapter 계열로 둔다. 공식 Lemonade Server 문서가 `/v1/models`, `/v1/chat/completions`와 streaming SSE를 OpenAI-compatible 표면으로 제공하고, 현재 코드에도 `vllm` adapter가 같은 `/v1/models`/`/v1/chat/completions` 기준선을 이미 구현하고 있기 때문이다.
- 선행 작업: OpenAI Workspace Agent Execution Contract, Node provider 상태와 Capacity Queue 기반
- 후속 작업: Lemonade provider 실테스트에서 확인된 serving 경로 안정화 보완
- 확인 필요: 실테스트 endpoint/served model name 또는 model, 인증/헤더 필요 여부, streaming 기대 동작
- 확인 완료: 실테스트 기준은 현재 host에서 접근 가능한 local LAN Lemonade server의 일반 포트 `13305`, served model `Gemma-4-26B-A4B-it-GGUF`, 인증/헤더 없음, streaming SSE 종료 신호 `data: [DONE]` 지원 PASS로 정리한다. endpoint 원문은 tracked roadmap에 기록하지 않는다.

View file

@ -0,0 +1,68 @@
# User Review Resolved - m-lemonade-provider-serving-validation/03+02_field_smoke
## 요청 일시
2026-06-15
## 상태
RESOLVED
## 사유
- 유형: environment-blocked
- 현재 리뷰 회차: 2
- 최종 판정: PASS
- 요약: 사용자가 현재 호스트 기준 real Lemonade endpoint를 지정했고, `Gemma-4-26B-A4B-it-GGUF` 모델로 real mode field smoke를 실행해 통과했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_0.log` | `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_0.log` | FAIL | 구현 산출물과 검증 출력이 없어 후속 구현 계획을 생성했다. |
| `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_1.log` | `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_1.log` | FAIL | 사용자 소유 real Lemonade endpoint/model/auth 값이 없어 real mode field smoke가 차단됐다. |
| `USER_REVIEW.md` | user decision | PASS/RESOLVED | 사용자가 현재 호스트 기준 real Lemonade endpoint를 지정했고 real smoke가 PASS했다. |
## 차단 근거
- 문제: `IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh`는 real Lemonade endpoint/model/auth를 필요로 하지만 현재 환경에 해당 값이 없다.
- 현재 archive plan: `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_1.log`
- 현재 archive review: `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_1.log`
- 검증 명령: `IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh`
- 실제 출력: `./scripts/e2e-openai-lemonade.sh: line 69: IOP_LEMONADE_ENDPOINT: real mode requires IOP_LEMONADE_ENDPOINT`
- 차단 판단 근거: real endpoint/model/auth는 사용자 소유 외부 서비스와 secret에 해당한다. 후속 에이전트가 repo 내부 코드 변경이나 명령 재실행만으로 만들어낼 수 없다.
## 해소 근거
- 사용자 결정: 현재 호스트 기준 real Lemonade endpoint를 사용해 접속한다.
- 모델 조회: default Lemonade port `13305``/v1/models`가 응답했고 `Gemma-4-26B-A4B-it-GGUF` 모델을 확인했다.
- 검증 명령: `IOP_LEMONADE_MODE=real IOP_LEMONADE_ENDPOINT=<redacted-local-lan-endpoint> IOP_LEMONADE_MODEL=Gemma-4-26B-A4B-it-GGUF ./scripts/e2e-openai-lemonade.sh`
- 실제 출력:
```text
[openai-lemonade] real mode against external Lemonade endpoint (model=Gemma-4-26B-A4B-it-GGUF)
[openai-lemonade] OpenAI-compatible Lemonade serving test PASSED (mode=real).
```
- 해소 판정: real mode field smoke가 PASS했으므로 environment-blocked stop state를 완료로 해소한다.
## 사용자 결정 필요
- [ ] 자동 follow-up plan/review를 계속 진행한다.
- [ ] 계획을 재작성한다.
- [x] 테스트 환경, secret, 외부 서비스, SDK, 장비 조건을 준비한 뒤 재시도한다.
- [ ] 작업 범위를 줄이거나 보류/폐기한다.
## 재개 조건
- 충족됨: real Lemonade server endpoint와 served model name이 준비되어 있고 real smoke가 PASS했다.
- 인증값은 필요하지 않았다.
- secret/개인 endpoint 원문은 tracked 문서에 남기지 않았다.
## 다음 실행 힌트
- 추가 자동 follow-up은 필요하지 않다.
- code-review가 `complete.log`를 작성하고 task directory를 archive로 이동한다.
## 종료 규칙
- 사용자가 이 stop state를 완료/PASS로 해소하면 `USER_REVIEW.md`를 해소 상태로 갱신하고, `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
- 새 구현이 필요하면 `plan` 스킬이 `USER_REVIEW.md``user_review_N.log`로 아카이브한 뒤 새 `PLAN-*-G??.md` / `CODE_REVIEW-*-G??.md`를 작성한다.

View file

@ -63,16 +63,16 @@ task=m-lemonade-provider-serving-validation/03+02_field_smoke, plan=0, tag=TEST
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/`를 `agent-task/archive/YYYY/MM/m-lemonade-provider-serving-validation/03+02_field_smoke/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-lemonade-provider-serving-validation/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
@ -155,3 +155,20 @@ $ git diff --check
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- correctness: Fail
- completeness: Fail
- test coverage: Fail
- API contract: Fail
- code quality: Pass
- plan deviation: Fail
- verification trust: Fail
- 발견된 문제:
- Required: `CODE_REVIEW-cloud-G07.md:48`의 구현 항목별 완료 여부가 모두 `[ ]`이고, `scripts/e2e-openai-lemonade.sh`도 존재하지 않습니다. 계획의 핵심 산출물인 Lemonade OpenAI-compatible smoke script를 추가하고 fake/real mode 검증 경로를 구현해야 합니다.
- Required: `CODE_REVIEW-cloud-G07.md:56` 및 `Makefile:1`, `Makefile:88` 기준으로 `test-openai-lemonade` target이 `.PHONY`와 Makefile target에 추가되지 않았습니다. 명시 실행 target을 추가하되 `test-e2e` 기본 dependency에는 넣지 않아야 합니다.
- Required: `CODE_REVIEW-cloud-G07.md:117`부터 `CODE_REVIEW-cloud-G07.md:150`까지 검증 출력이 `(output)` placeholder로 남아 있고, `CODE_REVIEW-cloud-G07.md:79`와 `CODE_REVIEW-cloud-G07.md:83`의 구현 소유 섹션도 placeholder 상태입니다. 계획된 명령을 실제 실행하고 stdout/stderr, 설계 결정, 계획 대비 변경 사항을 채워야 합니다.
- 다음 단계: WARN/FAIL follow-up으로 새 `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성한다.

View file

@ -0,0 +1,214 @@
<!-- task=m-lemonade-provider-serving-validation/03+02_field_smoke plan=1 tag=REVIEW_TEST -->
# Code Review Reference - REVIEW_TEST
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-15
task=m-lemonade-provider-serving-validation/03+02_field_smoke, plan=1, tag=REVIEW_TEST
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- Task ids:
- `models-chat`: Edge OpenAI-compatible `/v1/models`와 non-streaming `/v1/chat/completions`가 Lemonade provider로 수렴하는 기준이 검증되어 있다.
- `streaming`: streaming `/v1/chat/completions`에서 SSE chunk, finish reason, 종료 신호가 Lemonade provider 경로로 안정적으로 전달되는지 검증되어 있다.
- `field-smoke`: split-host field smoke에서 Lemonade target/model을 선택해 모델 조회와 chat 왕복이 검증되어 있다.
- `follow-up-scope`: 코드 수정이 필요한 항목은 이 Milestone의 추가 Task 또는 같은 Milestone task group의 후속 plan으로 정리되어 있다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-lemonade-provider-serving-validation/03+02_field_smoke/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_TEST-1] Lemonade smoke script | [x] |
| [REVIEW_TEST-2] Makefile target | [x] |
| [REVIEW_TEST-3] Verification and review artifact completion | [x] |
## 구현 체크리스트
- [x] `scripts/e2e-openai-lemonade.sh`를 추가해 fake mode와 real mode에서 Edge/Node/OpenAI-compatible route를 검증한다.
- [x] `Makefile`에 명시 실행 target `test-openai-lemonade`를 추가하되 `test-e2e` 기본 dependency에는 넣지 않는다.
- [x] fake mode smoke, real mode smoke, 관련 Go package tests, `git diff --check`를 실행한다. real mode 환경값이 없으면 review stub의 `사용자 리뷰 요청`에 기록하고 PASS를 주장하지 않는다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/`를 `agent-task/archive/YYYY/MM/m-lemonade-provider-serving-validation/03+02_field_smoke/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-lemonade-provider-serving-validation/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [x] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [x] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
- 검증 명령은 계획/stub 고정 계약 그대로 실행했다(대체 없음). `IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh`만 사용자 소유 외부 환경값 미준비로 끝까지 실행하지 못했고, 사유와 실제 출력을 `사용자 리뷰 요청`에 기록했다.
- 계획의 "Edge `/healthz`" 검증은 Edge OpenAI-compatible 서버(`apps/edge/internal/openai/routes.go:11`이 `/healthz` 핸들러를 등록)의 OpenAI listen 포트에서 `curl /healthz`로 구현했다. 별도 health 포트를 새로 만들지 않았다.
- model route lookup은 계획대로 `adapter: "openai_compat"`(type-name)을 썼다. node에는 `openai_compat_instances` 단일 인스턴스 `lemonade-local`만 등록되므로 registry legacy type lookup이 유일하게 해소된다.
- fake server는 `/v1/chat/completions`에서 `stream=true`만 허용하고 non-stream 요청은 400을 반환한다. Node `openai_compat` adapter가 provider로 항상 `stream=true`를 보내기 때문이며, Edge가 client에 대해 non-streaming/streaming 변환을 담당한다.
## 주요 설계 결정
- **fake/real mode 구성**: `IOP_LEMONADE_MODE`(기본 `fake`). fake는 임시 Go OpenAI-compatible server가 `/v1/models`와 SSE `/v1/chat/completions`(reasoning_content → content 청크 2개 → finish_reason=stop → `[DONE]`, usage 포함)를 제공한다. real은 `IOP_LEMONADE_ENDPOINT`/`IOP_LEMONADE_MODEL`을 node adapter endpoint/target에 주입하고 fake server를 띄우지 않는다.
- **sentinel/검증 분리**: fake mode는 non-streaming 응답에서 `IOP_OPENAI_LEMONADE_OK`와 `reasoning_content`, streaming에서 `"content":"IOP_OPENAI_"`와 `data: [DONE]`을 고정 검증한다. real mode는 고정 sentinel 대신 non-empty `"content"`와 streaming `data: [DONE]`만 요구해 외부 모델 출력 차이를 허용한다.
- **endpoint normalization**: real mode endpoint는 root 또는 trailing `/v1` 둘 다 허용한다. 경로 중복(`/v1/v1`) 방지는 Node `openai_compat` adapter의 `joinOpenAIPath`가 담당하므로 script는 endpoint 원문을 그대로 config에 넣는다.
- **auth redaction**: `IOP_LEMONADE_AUTH_HEADER_NAME`/`IOP_LEMONADE_AUTH_HEADER_VALUE`가 모두 있을 때만 generated temp node config의 `headers:`에 기록한다. header value는 stdout/stderr나 tracked 파일에 출력하지 않는다.
- **cleanup**: trap EXIT에서 edge console에 `/exit`, node/fake server kill, temp dir 삭제를 처리한다. 기존 `scripts/e2e-openai-ollama.sh` 패턴을 따랐다.
- **Makefile**: `test-openai-lemonade`를 `.PHONY`와 명시 target에만 추가하고 `test-e2e` 기본 의존성에는 넣지 않았다(외부 Lemonade 의존성을 기본 smoke 흐름에 추가하지 않기 위함).
## 사용자 리뷰 요청
- 상태: 있음
- 사유 유형: 사용자 소유 외부 환경/서비스 준비 (real Lemonade endpoint/model/auth)
- 결정 필요: real mode field smoke를 검증하려면 사용자 소유 Lemonade 서버의 `IOP_LEMONADE_ENDPOINT`, `IOP_LEMONADE_MODEL`(필요 시 `IOP_LEMONADE_AUTH_HEADER_NAME`/`IOP_LEMONADE_AUTH_HEADER_VALUE`)을 제공받아야 한다. 이 값들은 secret/개인 endpoint이므로 사용자만 제공할 수 있다.
- 차단 근거: real mode는 외부 Lemonade 서버에 의존한다. 현재 환경에 endpoint/model/auth 값이 없어 `IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh`가 required env 가드(`IOP_LEMONADE_ENDPOINT`)에서 즉시 종료한다.
- 실행한 검증/명령:
- `IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh` → `./scripts/e2e-openai-lemonade.sh: line 69: IOP_LEMONADE_ENDPOINT: real mode requires IOP_LEMONADE_ENDPOINT` (exit 1). real Lemonade 왕복 미검증.
- fake mode smoke, `make test-openai-lemonade`, Go package tests, `git diff --check`는 모두 PASS(아래 검증 결과 참조).
- 자동 후속 불가 이유: real endpoint/model/auth는 사용자 소유 외부 자원이자 secret이다. 후속 에이전트가 명령 재실행이나 산출물 수집만으로 만들어낼 수 없다.
- 재개 조건: 사용자가 real Lemonade endpoint/model(및 필요 시 auth header)을 제공하면 `IOP_LEMONADE_MODE=real ... ./scripts/e2e-openai-lemonade.sh`를 실행해 real mode 증거를 채울 수 있다. 또는 code-review가 real field 검증을 별도 후속 task로 분리하기로 결정할 수 있다.
## 리뷰어를 위한 체크포인트
- `scripts/e2e-openai-lemonade.sh`가 fake mode와 real mode 모두를 구현하고, fake mode가 Edge/Node/OpenAI-compatible route를 실제로 통과하는지 확인한다.
- `Makefile` target이 명시 실행용으로만 추가되고 `test-e2e` 기본 dependency에는 들어가지 않았는지 확인한다.
- script가 auth header value나 개인 endpoint를 tracked 파일 또는 review 출력에 노출하지 않았는지 확인한다.
- real mode evidence가 없으면 PASS가 아니라 사용자 리뷰 요청 또는 후속 plan으로 남았는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### REVIEW_TEST-1 중간 검증
```bash
$ bash -n scripts/e2e-openai-lemonade.sh
(출력 없음, exit 0 — syntax OK)
$ ./scripts/e2e-openai-lemonade.sh
[openai-lemonade] OpenAI-compatible Lemonade serving test PASSED (mode=fake).
```
### REVIEW_TEST-2 중간 검증
```bash
$ make test-openai-lemonade
./scripts/e2e-openai-lemonade.sh
[openai-lemonade] OpenAI-compatible Lemonade serving test PASSED (mode=fake).
```
### REVIEW_TEST-3 중간 검증
```bash
$ IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh
./scripts/e2e-openai-lemonade.sh: line 69: IOP_LEMONADE_ENDPOINT: real mode requires IOP_LEMONADE_ENDPOINT
# exit 1 — real Lemonade endpoint/model/auth는 사용자 소유 외부 환경이라 미준비. 사용자 리뷰 요청 참조. real mode PASS 미주장.
$ go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/edgecmd ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters
ok iop/apps/edge/internal/openai 1.509s
ok iop/apps/edge/internal/edgecmd 0.007s
ok iop/apps/node/internal/adapters/openai_compat 0.007s
ok iop/apps/node/internal/adapters 0.008s
$ git diff --check
(출력 없음, exit 0 — whitespace error 없음)
```
### 최종 검증
```bash
$ bash -n scripts/e2e-openai-lemonade.sh
(출력 없음, exit 0 — syntax OK)
$ ./scripts/e2e-openai-lemonade.sh
[openai-lemonade] OpenAI-compatible Lemonade serving test PASSED (mode=fake).
$ make test-openai-lemonade
./scripts/e2e-openai-lemonade.sh
[openai-lemonade] OpenAI-compatible Lemonade serving test PASSED (mode=fake).
$ IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh
./scripts/e2e-openai-lemonade.sh: line 69: IOP_LEMONADE_ENDPOINT: real mode requires IOP_LEMONADE_ENDPOINT
# exit 1 — 사용자 소유 외부 환경 미준비. 사용자 리뷰 요청 참조.
$ go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/edgecmd ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters
ok iop/apps/edge/internal/openai 1.509s
ok iop/apps/edge/internal/edgecmd 0.007s
ok iop/apps/node/internal/adapters/openai_compat 0.007s
ok iop/apps/node/internal/adapters 0.008s
$ git diff --check
(출력 없음, exit 0 — whitespace error 없음)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
Sections and their ownership:
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` -> `[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` -> `[x]` only; final checkbox is mandatory before saving |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed; do not ask the user directly during implementation; when filled, include exact decision, evidence, commands/output, why automatic follow-up cannot resolve it, and resume condition |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
| 코드리뷰 결과 | Review agent appends | Not included in stub |
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- correctness: Pass
- completeness: Fail
- test coverage: Fail
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Fail
- 발견된 문제:
- Required: `CODE_REVIEW-cloud-G07.md:95`의 사용자 리뷰 요청대로 real Lemonade field smoke가 미검증 상태입니다. `CODE_REVIEW-cloud-G07.md:140`의 `IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh`는 `IOP_LEMONADE_ENDPOINT` 미설정으로 exit 1이며, Roadmap Targets의 `field-smoke`/real provider evidence를 충족하려면 사용자 소유 Lemonade endpoint/model/auth 값을 준비한 뒤 real mode smoke를 다시 실행해야 합니다.
- 다음 단계: USER_REVIEW로 전환한다.

View file

@ -0,0 +1,52 @@
# Complete - m-lemonade-provider-serving-validation/03+02_field_smoke
## 완료 일시
2026-06-15
## 요약
Lemonade provider field smoke review completed after USER_REVIEW environment resolution with final verdict PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_0.log` | `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_0.log` | FAIL | 구현 산출물과 검증 출력이 없어 후속 구현 계획을 생성했다. |
| `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_1.log` | `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_1.log` | FAIL | fake smoke와 테스트는 통과했지만 real Lemonade endpoint/model 준비가 필요해 USER_REVIEW로 전환했다. |
| `USER_REVIEW.md` | user decision | PASS/RESOLVED | 사용자가 현재 호스트 기준 real Lemonade endpoint를 지정했고 real smoke가 PASS했다. |
## 구현/정리 내용
- Added `scripts/e2e-openai-lemonade.sh` with fake and real Lemonade/OpenAI-compatible smoke paths.
- Added explicit `make test-openai-lemonade` target without adding real Lemonade dependency to `test-e2e`.
- Verified fake mode, Makefile target, related Go packages, whitespace, and real mode against the user-provided local Lemonade server.
- Completion audit tightened the smoke script to verify client-facing model exposure, alias-based CLI routing, bounded real chat prompts, and real reasoning-model payloads through either `content` or `reasoning_content`.
## 최종 검증
- `bash -n scripts/e2e-openai-lemonade.sh` - PASS; no syntax output.
- `./scripts/e2e-openai-lemonade.sh` - PASS; output included `OpenAI-compatible Lemonade serving test PASSED (mode=fake).`
- `make test-openai-lemonade` - PASS; output included `OpenAI-compatible Lemonade serving test PASSED (mode=fake).`
- `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/edgecmd ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters` - PASS; all listed packages returned `ok`.
- `git diff --check` - PASS; no whitespace errors.
- `curl -fsS --connect-timeout 5 --max-time 20 <redacted-local-lan-endpoint>/v1/models` - PASS; returned available Lemonade models including `Gemma-4-26B-A4B-it-GGUF`.
- `IOP_LEMONADE_MODE=real IOP_LEMONADE_ENDPOINT=<redacted-local-lan-endpoint> IOP_LEMONADE_MODEL=Gemma-4-26B-A4B-it-GGUF ./scripts/e2e-openai-lemonade.sh` - PASS; output included `OpenAI-compatible Lemonade serving test PASSED (mode=real).`
## Roadmap Completion
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- Completed task ids:
- `models-chat`: PASS; evidence=`agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_1.log`, `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_1.log`; verification=`./scripts/e2e-openai-lemonade.sh`, `IOP_LEMONADE_MODE=real IOP_LEMONADE_ENDPOINT=<redacted-local-lan-endpoint> IOP_LEMONADE_MODEL=Gemma-4-26B-A4B-it-GGUF ./scripts/e2e-openai-lemonade.sh`
- `streaming`: PASS; evidence=`agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_1.log`, `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_1.log`; verification=`./scripts/e2e-openai-lemonade.sh`, `IOP_LEMONADE_MODE=real IOP_LEMONADE_ENDPOINT=<redacted-local-lan-endpoint> IOP_LEMONADE_MODEL=Gemma-4-26B-A4B-it-GGUF ./scripts/e2e-openai-lemonade.sh`
- `field-smoke`: PASS; evidence=`agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_1.log`, `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_1.log`; verification=`IOP_LEMONADE_MODE=real IOP_LEMONADE_ENDPOINT=<redacted-local-lan-endpoint> IOP_LEMONADE_MODEL=Gemma-4-26B-A4B-it-GGUF ./scripts/e2e-openai-lemonade.sh`
- `follow-up-scope`: PASS; evidence=`agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/USER_REVIEW.md`, `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/03+02_field_smoke/complete.log`; verification=`USER_REVIEW.md resolved with real smoke PASS`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,232 @@
<!-- task=m-lemonade-provider-serving-validation/03+02_field_smoke plan=1 tag=REVIEW_TEST -->
# Plan - REVIEW_TEST
## 이 파일을 읽는 구현 에이전트에게
구현 완료 전 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 반드시 채운다. 구현 중 사용자 결정, 사용자 소유 외부 환경, secret, 서비스 준비, 또는 범위 충돌 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 남기고 멈춘다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다. 최종 판정, archive, `complete.log` 작성은 code-review-skill 전용이다.
## 배경
이전 리뷰는 구현 파일 변경이 없고 review stub의 구현 소유 섹션과 검증 출력이 비어 있어 `FAIL`로 종료됐다. 선행 OpenAI-compatible adapter subtask는 archive의 `complete.log`로 완료됐으므로, 이번 후속 작업은 Lemonade field smoke script, Makefile target, 실제 검증 증거를 완성하는 데 집중한다.
## 사용자 리뷰 요청 흐름
구현 중 차단이 생기면 active review stub의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 구현 에이전트가 채팅으로 직접 사용자에게 묻는 것은 금지되며, code-review가 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. 실제 Lemonade endpoint/model/auth 값이 준비되지 않아 real mode smoke를 실행할 수 없으면 이 섹션에 실행한 명령과 출력, 미실행 사유, 재개 조건을 기록하고 PASS를 주장하지 않는다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- Task ids:
- `models-chat`: Edge OpenAI-compatible `/v1/models`와 non-streaming `/v1/chat/completions`가 Lemonade provider로 수렴하는 기준이 검증되어 있다.
- `streaming`: streaming `/v1/chat/completions`에서 SSE chunk, finish reason, 종료 신호가 Lemonade provider 경로로 안정적으로 전달되는지 검증되어 있다.
- `field-smoke`: split-host field smoke에서 Lemonade target/model을 선택해 모델 조회와 chat 왕복이 검증되어 있다.
- `follow-up-scope`: 코드 수정이 필요한 항목은 이 Milestone의 추가 Task 또는 같은 Milestone task group의 후속 plan으로 정리되어 있다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/testing-smoke.md`
- `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/02+01_openai_compatible_adapter/complete.log`
- `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/plan_cloud_G07_0.log`
- `agent-task/m-lemonade-provider-serving-validation/03+02_field_smoke/code_review_cloud_G07_0.log`
- `Makefile`
- `scripts/e2e-openai-lemonade.sh` 존재 확인: missing
### 테스트 환경 규칙
- `test_env=local`.
- `agent-test/local/rules.md`와 matched profile `agent-test/local/testing-smoke.md`를 읽었다.
- 테스트 도구 자체를 바꾸는 작업이므로 해당 도구를 직접 실행해야 한다.
- 적용 명령: `bash -n scripts/e2e-openai-lemonade.sh`, `./scripts/e2e-openai-lemonade.sh`, `make test-openai-lemonade`, real mode smoke, 관련 Go package tests, `git diff --check`.
- field/runtime 외부 endpoint evidence가 필요하면 사용자 소유 host 또는 원격 runner가 필요할 수 있다. real mode 환경값이 없으면 review stub의 사용자 리뷰 요청으로 멈춘다.
- secret, token, 개인 endpoint 원문은 tracked 파일과 최종 보고에 기록하지 않는다.
### 테스트 커버리지 공백
- `scripts/e2e-openai-lemonade.sh`가 없어서 Lemonade/OpenAI-compatible adapter route fake/real smoke가 없다.
- `Makefile`에 `test-openai-lemonade` target이 없어 명시 실행 계약이 없다.
- 이전 review stub은 실제 stdout/stderr를 붙이지 않아 fake mode, real mode, Go package tests, whitespace 검증 신뢰성이 없다.
### 심볼 참조
- rename/remove 없음.
- 새 script target `test-openai-lemonade` call site는 `Makefile`에 한정한다.
### 분할 판단
- 이 후속 plan은 기존 split subtask `03+02_field_smoke` 안에서 이전 `FAIL` findings를 해소한다.
- predecessor `02`는 `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/02+01_openai_compatible_adapter/complete.log`로 충족됐다.
- 새 split은 만들지 않는다. 남은 문제는 script, Makefile target, 검증 증거가 같은 field smoke 계약 안에서 함께 판정되어야 한다.
### 범위 결정 근거
- Lemonade 서버 설치, 모델 다운로드, 운영 제품화는 제외한다.
- `make test-e2e` 기본 흐름에는 real Lemonade dependency를 추가하지 않는다.
- secret/header 값은 tracked script, config, docs, roadmap에 쓰지 않는다.
- smoke 중 발견된 provider 또는 OpenAI-compatible runtime 이슈가 script/config 소규모 보정으로 해결되지 않으면, 범위를 확장해 고치지 말고 review stub에 증거를 남겨 후속 plan으로 넘긴다.
### 빌드 등급
- `cloud-G07`: shell smoke orchestration, process control, stdout/stderr evidence, external real mode gate가 핵심이다.
## 구현 체크리스트
- [ ] `scripts/e2e-openai-lemonade.sh`를 추가해 fake mode와 real mode에서 Edge/Node/OpenAI-compatible route를 검증한다.
- [ ] `Makefile`에 명시 실행 target `test-openai-lemonade`를 추가하되 `test-e2e` 기본 dependency에는 넣지 않는다.
- [ ] fake mode smoke, real mode smoke, 관련 Go package tests, `git diff --check`를 실행한다. real mode 환경값이 없으면 review stub의 `사용자 리뷰 요청`에 기록하고 PASS를 주장하지 않는다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- `03+02_field_smoke`의 predecessor `02`는 `agent-task/archive/2026/06/m-lemonade-provider-serving-validation/02+01_openai_compatible_adapter/complete.log`로 충족됐다.
- 먼저 `scripts/e2e-openai-lemonade.sh`를 구현하고 fake mode를 직접 통과시킨다.
- 그 다음 `Makefile` target을 추가하고 `make test-openai-lemonade`가 같은 script를 실행하는지 확인한다.
- 마지막으로 real mode와 Go package tests, `git diff --check` 결과를 review stub에 실제 출력으로 붙인다.
## REVIEW_TEST-1 Lemonade smoke script
### 문제
`scripts/e2e-openai-lemonade.sh`가 없고, 이전 review의 `code_review_cloud_G07_0.log`는 `CODE_REVIEW-cloud-G07.md:48`에서 모든 구현 항목이 미완료였다. Lemonade/OpenAI-compatible route를 fake/real mode로 검증하는 산출물이 없다.
### 해결 방법
- 새 `scripts/e2e-openai-lemonade.sh`를 추가한다.
- 기본 `IOP_LEMONADE_MODE`는 `fake`다. fake mode는 임시 Go server가 `/v1/models`와 `/v1/chat/completions` non-streaming/streaming SSE를 제공하게 한다.
- real mode는 `IOP_LEMONADE_MODE=real`일 때 외부 Lemonade server를 사용한다. required env는 `IOP_LEMONADE_ENDPOINT`, `IOP_LEMONADE_MODEL`이다. endpoint는 root 또는 trailing `/v1` 둘 다 허용하되 Node adapter config에는 root 형태로 넣는다.
- optional auth는 `IOP_LEMONADE_AUTH_HEADER_NAME`과 `IOP_LEMONADE_AUTH_HEADER_VALUE`가 모두 있을 때 generated temp YAML `headers`에만 쓴다. script stdout/stderr에는 header value를 출력하지 않는다.
- generated Edge config는 `openai.model_routes`에 client-facing model `client-request-model`, `adapter: "openai_compat"`, `target: "$MODEL"`, `node: "test-node"`를 둔다.
- generated node adapter는 `openai_compat_instances` name `lemonade-local`, provider `lemonade`, endpoint, capacity/queue timeout을 사용한다.
- 검증 단계는 Edge `/healthz`, Edge `/v1/models`, Edge `/v1/chat/completions` non-streaming sentinel, streaming SSE content chunk와 `data: [DONE]`, `go run ./apps/edge/cmd/edge smoke openai --model client-request-model ...` 순서로 둔다.
- fake mode sentinel은 `IOP_OPENAI_LEMONADE_OK`로 고정한다. real mode는 empty response가 아니고 model route가 성공하면 통과하되, streaming `[DONE]`은 반드시 확인한다.
### 수정 파일 및 체크리스트
- [ ] `scripts/e2e-openai-lemonade.sh`: 새 smoke script 추가.
- [ ] script 내부 temp config에는 secret value를 출력하지 않는다.
- [ ] script cleanup은 Edge/Node/fake server process와 temp dir를 정리한다.
### 테스트 작성
- shell script 자체는 Go unit test 대신 `bash -n`, fake mode 실행, Makefile target 실행으로 검증한다.
### 중간 검증
```bash
bash -n scripts/e2e-openai-lemonade.sh
./scripts/e2e-openai-lemonade.sh
```
기대 결과: syntax check PASS, fake mode smoke PASS.
## REVIEW_TEST-2 Makefile target
### 문제
`Makefile:1`의 `.PHONY`와 `Makefile:88` 주변 target 목록에 `test-openai-lemonade`가 없다.
```makefile
1 .PHONY: all build build-local build-edge build-edge-host build-node build-node-target build-node-targets pack-node-target pack-edge archive-edge tidy test test-e2e test-control-plane-edge-wire test-openai-ollama proto proto-dart client-test client-build-web clean
...
88 test-openai-ollama:
89 ./scripts/e2e-openai-ollama.sh
```
### 해결 방법
- `.PHONY`에 `test-openai-lemonade`를 추가한다.
- `test-openai-lemonade` target은 `./scripts/e2e-openai-lemonade.sh`를 실행한다.
- `test-e2e`에는 추가하지 않는다.
```makefile
.PHONY: ... test-openai-ollama test-openai-lemonade ...
test-openai-ollama:
./scripts/e2e-openai-ollama.sh
test-openai-lemonade:
./scripts/e2e-openai-lemonade.sh
```
### 수정 파일 및 체크리스트
- [ ] `Makefile`: `.PHONY`와 `test-openai-lemonade` target 추가.
### 테스트 작성
- Makefile target은 별도 unit test 없이 `make test-openai-lemonade` 실행으로 검증한다.
### 중간 검증
```bash
make test-openai-lemonade
```
기대 결과: fake mode smoke PASS.
## REVIEW_TEST-3 Verification and review artifact completion
### 문제
이전 review의 `code_review_cloud_G07_0.log`는 `CODE_REVIEW-cloud-G07.md:117`부터 `CODE_REVIEW-cloud-G07.md:150`까지 검증 출력이 `(output)` placeholder였고, `계획 대비 변경 사항`과 `주요 설계 결정`도 placeholder였다. 구현 완료 여부를 판단할 실제 stdout/stderr와 구현 설명이 없다.
### 해결 방법
- 계획된 검증 명령을 실제 실행하고 `CODE_REVIEW-cloud-G07.md`의 `검증 결과`에 실제 stdout/stderr를 붙인다.
- `계획 대비 변경 사항`은 변경이 없으면 `없음`으로, 변경이 있으면 이유와 대체 명령까지 적는다.
- `주요 설계 결정`에는 fake/real mode 구성, endpoint normalization, auth redaction, cleanup 처리 결정을 짧게 기록한다.
- real mode 환경값이 없거나 endpoint/model/auth 준비가 안 되어 있으면 `사용자 리뷰 요청`에 실제 명령과 출력, 자동 후속 불가 이유, 재개 조건을 기록하고 구현 완료를 주장하지 않는다.
### 수정 파일 및 체크리스트
- [ ] `CODE_REVIEW-cloud-G07.md`: 구현 항목별 완료 여부, 구현 체크리스트, 계획 대비 변경 사항, 주요 설계 결정, 사용자 리뷰 요청, 검증 결과를 실제 내용으로 채운다.
### 테스트 작성
- 테스트 작성 없음. 이 항목은 검증 evidence와 review artifact 완성 항목이다.
### 중간 검증
```bash
IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh
go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/edgecmd ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters
git diff --check
```
기대 결과: real mode 환경값이 있으면 real Lemonade smoke PASS. 없으면 사용자 리뷰 요청에 blocker evidence를 남기고 PASS 금지. Go package tests와 whitespace check는 PASS.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `scripts/e2e-openai-lemonade.sh` | REVIEW_TEST-1, REVIEW_TEST-3 |
| `Makefile` | REVIEW_TEST-2 |
| `CODE_REVIEW-cloud-G07.md` | REVIEW_TEST-3 |
## 최종 검증
```bash
bash -n scripts/e2e-openai-lemonade.sh
./scripts/e2e-openai-lemonade.sh
make test-openai-lemonade
IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh
go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/edgecmd ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters
git diff --check
```
기대 결과: 모든 명령이 exit 0. real mode 환경값이 없거나 endpoint/model/auth 준비가 안 되어 있으면 PASS 금지, `사용자 리뷰 요청` 작성.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

298
scripts/e2e-openai-lemonade.sh Executable file
View file

@ -0,0 +1,298 @@
#!/usr/bin/env bash
set -euo pipefail
# OpenAI-compatible Lemonade serving smoke.
#
# Verifies that the Edge OpenAI-compatible input surface converges onto the Node
# `openai_compat` adapter for a Lemonade-style provider: /v1/models lookup,
# non-streaming and streaming /v1/chat/completions, and the `edge smoke openai`
# command path.
#
# Modes (IOP_LEMONADE_MODE):
# fake (default) - a temporary Go OpenAI-compatible server stands in for
# Lemonade so the route can be checked with no external deps.
# real - an external Lemonade server is used. Required env:
# IOP_LEMONADE_ENDPOINT (root or trailing /v1 both allowed)
# IOP_LEMONADE_MODEL
# Optional auth (both must be set):
# IOP_LEMONADE_AUTH_HEADER_NAME
# IOP_LEMONADE_AUTH_HEADER_VALUE
# Auth header values are written only to the generated temp
# config and never printed to stdout/stderr.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
TMP_DIR=$(mktemp -d)
MODE="${IOP_LEMONADE_MODE:-fake}"
EDGE_PID=""
NODE_PID=""
LEMONADE_PID=""
EDGE_FD_OPEN=0
cleanup() {
if [ "$EDGE_FD_OPEN" -eq 1 ]; then
{ echo "/exit" >&3; } 2>/dev/null || true
exec 3>&- 2>/dev/null || true
fi
if [ -n "$EDGE_PID" ]; then wait "$EDGE_PID" 2>/dev/null || true; fi
if [ -n "$NODE_PID" ]; then kill "$NODE_PID" 2>/dev/null || true; fi
if [ -n "$LEMONADE_PID" ]; then kill "$LEMONADE_PID" 2>/dev/null || true; fi
if [ "${IOP_LEMONADE_KEEP_TMP:-0}" = "1" ]; then
echo "[openai-lemonade] keeping temp dir: $TMP_DIR"
return
fi
rm -rf "$TMP_DIR" 2>/dev/null || true
}
trap cleanup EXIT
PORT=$((31000 + RANDOM % 5000))
BOOTSTRAP_PORT=$((21000 + RANDOM % 5000))
OPENAI_PORT=$((36000 + RANDOM % 5000))
LEMONADE_PORT=$((41000 + RANDOM % 5000))
EDGE_METRICS_PORT=$((46000 + RANDOM % 5000))
NODE_METRICS_PORT=$((51000 + RANDOM % 5000))
wait_port() {
local host="$1"
local port="$2"
local label="$3"
local deadline=$((SECONDS + 20))
while ! timeout 1 bash -c 'cat < /dev/null > /dev/tcp/"$1"/"$2"' _ "$host" "$port" 2>/dev/null; do
if (( SECONDS >= deadline )); then
echo "[openai-lemonade] $label did not open on $host:$port"
return 1
fi
sleep 0.2
done
}
# Resolve provider endpoint and model based on mode.
if [ "$MODE" = "real" ]; then
: "${IOP_LEMONADE_ENDPOINT:?real mode requires IOP_LEMONADE_ENDPOINT}"
: "${IOP_LEMONADE_MODEL:?real mode requires IOP_LEMONADE_MODEL}"
NODE_ENDPOINT="$IOP_LEMONADE_ENDPOINT"
MODEL="$IOP_LEMONADE_MODEL"
echo "[openai-lemonade] real mode against external Lemonade endpoint (model=$MODEL)"
else
MODEL="fake-lemonade-model"
NODE_ENDPOINT="http://127.0.0.1:$LEMONADE_PORT"
FAKE_LEMONADE="$TMP_DIR/fake_lemonade.go"
cat > "$FAKE_LEMONADE" <<'EOF'
package main
import (
"encoding/json"
"log"
"net/http"
"os"
)
type chatRequest struct {
Model string `json:"model"`
Stream bool `json:"stream"`
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
}
func main() {
if len(os.Args) < 3 {
log.Fatal("usage: fake_lemonade <listen-addr> <model>")
}
model := os.Args[2]
mux := http.NewServeMux()
mux.HandleFunc("/v1/models", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"object":"list","data":[{"id":"` + model + `"}]}`))
})
mux.HandleFunc("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) {
var req chatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Model != model {
http.Error(w, "unexpected model: "+req.Model, http.StatusBadRequest)
return
}
if !req.Stream {
http.Error(w, "expected stream=true from openai_compat adapter", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/event-stream")
flush := func() {
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
_, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"reasoning_content":"thinking..."}}]}` + "\n\n"))
flush()
_, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"IOP_OPENAI_"}}]}` + "\n\n"))
flush()
_, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"LEMONADE_OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2}}` + "\n\n"))
flush()
_, _ = w.Write([]byte("data: [DONE]\n\n"))
flush()
})
log.Fatal(http.ListenAndServe(os.Args[1], mux))
}
EOF
go run "$FAKE_LEMONADE" "127.0.0.1:$LEMONADE_PORT" "$MODEL" > "$TMP_DIR/fake_lemonade.out" 2>&1 &
LEMONADE_PID=$!
wait_port 127.0.0.1 "$LEMONADE_PORT" "fake lemonade"
fi
# Build the node openai_compat adapter block. Auth headers, when provided, are
# written only into the generated temp config (never echoed).
ADAPTER_BLOCK="$TMP_DIR/adapter_block.yaml"
{
echo " openai_compat_instances:"
echo " - name: lemonade-local"
echo " enabled: true"
echo " provider: lemonade"
echo " endpoint: \"$NODE_ENDPOINT\""
echo " capacity: 4"
echo " queue_timeout_ms: 5000"
if [ -n "${IOP_LEMONADE_AUTH_HEADER_NAME:-}" ] && [ -n "${IOP_LEMONADE_AUTH_HEADER_VALUE:-}" ]; then
echo " headers:"
echo " \"$IOP_LEMONADE_AUTH_HEADER_NAME\": \"$IOP_LEMONADE_AUTH_HEADER_VALUE\""
fi
} > "$ADAPTER_BLOCK"
EDGE_CONFIG="$TMP_DIR/edge.yaml"
NODE_CONFIG="$TMP_DIR/node.yaml"
cat > "$EDGE_CONFIG" <<EOF
server:
listen: "127.0.0.1:$PORT"
metrics:
port: $EDGE_METRICS_PORT
bootstrap:
listen: "127.0.0.1:$BOOTSTRAP_PORT"
artifact_dir: "$TMP_DIR/artifacts"
openai:
enabled: true
listen: "127.0.0.1:$OPENAI_PORT"
adapter: "openai_compat"
session_id: "cline"
timeout_sec: 30
strict_output: false
model_routes:
- model: "client-request-model"
adapter: "openai_compat"
target: "$MODEL"
node: "test-node"
- model: "$MODEL"
adapter: "openai_compat"
target: "$MODEL"
node: "test-node"
console:
adapter: mock
target: mock-echo
session_id: default
nodes:
- id: test-node
alias: test-node
token: test-token
runtime:
workspace_root: "$TMP_DIR/workspace"
adapters:
$(cat "$ADAPTER_BLOCK")
EOF
cat > "$NODE_CONFIG" <<EOF
transport:
edge_addr: "127.0.0.1:$PORT"
token: test-token
reconnect_interval: 1s
metrics:
port: $NODE_METRICS_PORT
node:
id: test-node
alias: test-node
EOF
EDGE_OUT="$TMP_DIR/edge.out"
NODE_OUT="$TMP_DIR/node.out"
mkfifo "$TMP_DIR/edge_fifo"
IOP_EDGE_CONFIG="$EDGE_CONFIG" "$REPO_ROOT/scripts/dev/edge.sh" < "$TMP_DIR/edge_fifo" > "$EDGE_OUT" 2>&1 &
EDGE_PID=$!
exec 3> "$TMP_DIR/edge_fifo"
EDGE_FD_OPEN=1
wait_port 127.0.0.1 "$PORT" "edge node transport"
wait_port 127.0.0.1 "$OPENAI_PORT" "edge openai api"
IOP_NODE_CONFIG="$NODE_CONFIG" "$REPO_ROOT/scripts/dev/node.sh" > "$NODE_OUT" 2>&1 &
NODE_PID=$!
deadline=$((SECONDS + 30))
while ! grep -q '\[node0-evt\] connected reason="registered"' "$EDGE_OUT"; do
if (( SECONDS >= deadline )); then
echo "[openai-lemonade] node registration timed out"
echo "=== EDGE OUTPUT ==="; cat "$EDGE_OUT"
echo "=== NODE OUTPUT ==="; cat "$NODE_OUT"
exit 1
fi
sleep 0.2
done
# Edge health.
curl -fsS "http://127.0.0.1:$OPENAI_PORT/healthz" > /dev/null
# /v1/models must surface both the client-facing model and provider target.
MODELS_OUT="$TMP_DIR/models.json"
curl -fsS "http://127.0.0.1:$OPENAI_PORT/v1/models" > "$MODELS_OUT"
grep -q "client-request-model" "$MODELS_OUT"
grep -q "$MODEL" "$MODELS_OUT"
# Non-streaming chat completion.
CHAT_OUT="$TMP_DIR/chat.json"
curl -fsS \
-H "Content-Type: application/json" \
-d '{"model":"client-request-model","think":false,"options":{"max_tokens":32},"messages":[{"role":"user","content":"Reply with exactly: smoke token"}]}' \
"http://127.0.0.1:$OPENAI_PORT/v1/chat/completions" > "$CHAT_OUT"
if [ "$MODE" = "fake" ]; then
grep -q "IOP_OPENAI_LEMONADE_OK" "$CHAT_OUT"
grep -q "reasoning_content" "$CHAT_OUT"
else
# real mode: reasoning models can emit a reasoning-only assistant payload
# when max_tokens is intentionally small for smoke speed.
grep -Eq '"(content|reasoning_content)":"[^"]' "$CHAT_OUT"
fi
# Streaming chat completion: SSE content chunk(s) and a terminating [DONE].
STREAM_OUT="$TMP_DIR/stream.txt"
curl -fsS -N \
-H "Content-Type: application/json" \
-d '{"model":"client-request-model","stream":true,"think":false,"options":{"max_tokens":32},"messages":[{"role":"user","content":"Reply with exactly: stream token"}]}' \
"http://127.0.0.1:$OPENAI_PORT/v1/chat/completions" > "$STREAM_OUT"
if [ "$MODE" = "fake" ]; then
grep -q '"reasoning_content":"thinking..."' "$STREAM_OUT"
grep -q '"content":"IOP_OPENAI_"' "$STREAM_OUT"
else
grep -Eq '"(content|reasoning_content)":"[^"]' "$STREAM_OUT"
fi
grep -q 'data: \[DONE\]' "$STREAM_OUT"
# Command-path smoke via the edge CLI.
SMOKE_OUT="$TMP_DIR/iop-edge-smoke-openai.txt"
(cd "$REPO_ROOT" && go run ./apps/edge/cmd/edge smoke openai \
--model "client-request-model" \
--base-url "http://127.0.0.1:$OPENAI_PORT" \
--prompt "Reply with exactly: command smoke token" \
--timeout 60s) > "$SMOKE_OUT"
grep -q "IOP Edge OpenAI Smoke Test SUCCESS!" "$SMOKE_OUT"
if grep -i -E "node reported error|error run_id=|\[[^]]+-evt\] error|panic:" "$EDGE_OUT" "$NODE_OUT" >/dev/null; then
echo "[openai-lemonade] detected failure marker"
echo "=== EDGE OUTPUT ==="; cat "$EDGE_OUT"
echo "=== NODE OUTPUT ==="; cat "$NODE_OUT"
exit 1
fi
echo "[openai-lemonade] OpenAI-compatible Lemonade serving test PASSED (mode=$MODE)."