<!-- task=m-lemonade-provider-serving-validation/02+01_openai_compatible_adapter plan=0 tag=API -->

# Plan - API

## 이 파일을 읽는 구현 에이전트에게

구현 완료 전 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 반드시 채운다. 구현 중 사용자 결정, 사용자 소유 외부 환경, secret, 서비스 준비, 또는 범위 충돌 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 남기고 멈춘다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다. 최종 판정, archive, `complete.log` 작성은 code-review-skill 전용이다.

## 배경

Lemonade Server는 OpenAI-compatible `/v1/models`와 `/v1/chat/completions` 표면을 제공한다. 현재 Node에는 vLLM 전용 이름의 adapter가 같은 HTTP/SSE 흐름을 일부 구현하지만, Lemonade를 vLLM adapter로 노출하면 provider 경계와 설정 계약이 흐려진다. 이 plan은 선행 config/proto 계약을 사용해 `openai_compat` Node adapter를 구현하고 provider availability 기준을 검증한다.

## 사용자 리뷰 요청 흐름

구현 중 차단이 생기면 active review stub의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 구현 에이전트가 채팅으로 직접 사용자에게 묻는 것은 금지되며, code-review가 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- Task ids:
  - `availability`: Lemonade provider의 endpoint/model probe 결과가 Node 공통 상태 모델 `unknown`, `available`, `unavailable`로 노출되는 기준이 검증되어 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- `proto/iop/runtime.proto`
- `packages/go/config/config.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/factory_internal_test.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`
- `apps/node/internal/adapters/vllm/vllm.go`
- `apps/node/internal/adapters/vllm/vllm_test.go`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/server_test.go`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`, `node-smoke`, `edge-smoke`, `platform-common-smoke`를 읽었다.
- 적용 명령: Node adapter 변경은 변경 패키지 또는 `go test ./apps/node/...`; OpenAI-compatible 경계는 후속 smoke에서 `iop-edge smoke openai` 또는 동등한 HTTP 확인을 적용한다.
- 이 subtask는 httptest 기반 deterministic Node adapter 검증까지 담당한다. Edge HTTP field convergence는 `03+02_field_smoke`에서 담당한다.
- 신선한 실행이 필요하므로 Go test cache 출력은 허용하지 않고 `-count=1`을 사용한다.

### 테스트 커버리지 공백

- `apps/node/internal/adapters/vllm/vllm_test.go`는 `/v1/models`, SSE delta, target miss를 검증하지만 adapter name, provider label, headers, option passthrough가 없다.
- `apps/node/internal/adapters/factory_internal_test.go`는 vLLM typed/legacy config만 검증한다. `openai_compat` typed config와 legacy settings fallback 테스트가 필요하다.
- `apps/node/internal/adapters/adapters_blackbox_test.go`는 vLLM execution만 있다. `openai_compat` registry key, headers, options, complete metadata를 검증하는 blackbox test가 필요하다.
- real Lemonade endpoint 검증은 사용자 소유 환경에 의존하므로 이 subtask 범위가 아니다.

### 심볼 참조

- rename/remove 없음.
- 새 adapter type string `openai_compat` call site: `apps/node/internal/adapters/factory.go`, `apps/node/internal/adapters/adapters_blackbox_test.go`, 후속 `apps/edge/internal/node/mapper.go` payload 출력.

### 분할 판단

- split decision policy를 plan 작성 전에 평가했다.
- 공유 task group: `agent-task/m-lemonade-provider-serving-validation`.
- `01_config_contract`: 선행 config/proto 계약. 현재 active/archive `complete.log` 없음.
- `02+01_openai_compatible_adapter`: 이 plan. predecessor `01`은 아직 만족되지 않았다. 구현 시작 전 같은 task group의 `01_*` active/archive `complete.log`가 있어야 한다.
- `03+02_field_smoke`: 후속 field smoke. 이 plan의 PASS 후 시작한다.

### 범위 결정 근거

- 선행 `01_config_contract`의 type/field 이름을 사용한다. 선행 결과와 이름이 다르면 이 plan의 `계획 대비 변경 사항`에 맞춘다.
- `apps/node/internal/adapters/vllm`의 public behavior를 깨뜨리지 않는다. 공통 로직 추출은 허용하지만 기존 `vllm` adapter name, default capacity, tests는 유지한다.
- Edge OpenAI HTTP handler의 request parsing/response formatting은 수정하지 않는다. Edge route convergence는 후속 smoke에서 검증한다.
- real Lemonade installation, credentials, external endpoint 준비는 제외한다.

### 빌드 등급

- `local-G08`: streaming/probe/error handling을 다루지만 httptest로 deterministic하게 검증 가능하고 Node adapter 경계로 범위를 제한한다.

## 구현 체크리스트

- [ ] 구현 시작 전 `01_config_contract`의 `complete.log`를 확인하고, 선행 계약의 type/field 이름을 이 plan에 맞춰 반영한다.
- [ ] `openai_compat` Node adapter를 추가하고 `/v1/models`, `/v1/chat/completions` streaming, headers, option passthrough를 구현한다.
- [ ] factory가 typed/legacy `openai_compat` payload를 registry에 등록하도록 연결한다.
- [ ] Node adapter/factory/blackbox 테스트를 작성하고 기존 vLLM 테스트가 그대로 PASS하는지 확인한다.
- [ ] 계획의 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 붙인다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 디렉터리명 `02+01_openai_compatible_adapter`의 predecessor는 `01`뿐이다.
- 구현 시작 전 `agent-task/m-lemonade-provider-serving-validation/01_config_contract/complete.log` 또는 archive의 matching `01_*` `complete.log`를 확인한다.
- 이 plan 본문은 추가 runtime dependency를 만들지 않는다.

## API-1 Common OpenAI-compatible adapter

### 문제

`apps/node/internal/adapters/vllm/vllm.go:1-3`은 이미 OpenAI-compatible inference engine이라고 설명하지만 package/type/name은 vLLM 전용이다. `apps/node/internal/adapters/vllm/vllm.go:85-123`은 request body가 `model/messages/stream`뿐이라 Lemonade의 option passthrough와 auth/header 계약을 사용할 수 없다.

```go
1	// Package vllm provides an Adapter for OpenAI-compatible inference engines
2	// such as vLLM and SGLang via the /v1/chat/completions SSE endpoint.
3	package vllm
```

```go
109		chatReq := vllmChatRequest{
110			Model:    model,
111			Messages: messages,
112			Stream:   true,
113		}
114		body, err := json.Marshal(chatReq)
115		if err != nil {
116			return fmt.Errorf("vllm adapter: marshal request: %w", err)
117		}
118		req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(v.endpoint, "/v1/chat/completions"), bytes.NewReader(body))
119		if err != nil {
120			return fmt.Errorf("vllm adapter: build request: %w", err)
121		}
122		req.Header.Set("Content-Type", "application/json")
```

### 해결 방법

- 새 package `apps/node/internal/adapters/openai_compat`를 만든다.
- `const Name = "openai_compat"`와 adapter type을 정의한다.
- config는 선행 task의 `config.OpenAICompatConf`를 사용한다.
- endpoint는 `strings.TrimRight(endpoint, "/")`; empty endpoint는 execution/probe에서 error. `joinOpenAIPath` helper는 endpoint path가 이미 `/v1`로 끝나도 `/v1/v1/...`을 만들지 않아야 한다.
- HTTP headers는 `/v1/models`와 `/v1/chat/completions` 양쪽에 적용한다. `Content-Type: application/json`은 POST에서 adapter가 항상 설정하고, user header가 같은 key를 제공하면 secret/auth header만 허용하되 content type override는 명시적으로 막거나 review에 근거를 기록한다.
- request body는 `map[string]any`로 만들고 adapter-owned `model`, `messages`, `stream=true`를 우선한다. `spec.Input["options"].(map[string]any)`는 top-level OpenAI-compatible request field로 복사하되 `model`, `messages`, `stream`은 덮어쓰지 못하게 한다. `tools`, `format`, `think`, `keep_alive`는 현재 Edge input 그대로 필요 시 복사한다.
- SSE parser는 `data: [DONE]`에서 complete를 emit한다. chunk `choices[0].delta.content`는 `EventTypeDelta`, `choices[0].delta.reasoning_content`가 있으면 `EventTypeReasoningDelta`로 emit한다.
- chunk `choices[0].finish_reason`은 complete event `Metadata["finish_reason"]`로 보존한다. usage가 있으면 `RuntimeEvent.Usage`에 반영하고, 없으면 현재 vLLM처럼 output token fallback을 사용한다.
- `Capabilities`와 `ProbeProvider`는 `/v1/models`의 `data[].id`를 사용하고 status는 `runtime.NormalizeProviderStatus`로 `available`/`unavailable`만 반환한다.

### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/adapters/openai_compat/openai_compat.go`: adapter 구현 추가.
- [ ] `apps/node/internal/adapters/openai_compat/openai_compat_test.go`: capabilities/probe/execute/header/options/stream tests 추가.
- [ ] 필요 시 `apps/node/internal/adapters/vllm/vllm.go`: 공통 helper reuse를 위한 최소 수정. vLLM behavior/name 회귀 금지.
- [ ] 필요 시 `apps/node/internal/adapters/vllm/vllm_test.go`: 기존 expectations 유지 또는 공통 helper 이동에 따른 import 조정.

### 테스트 작성

- `TestOpenAICompatCapabilitiesQueryModels`
- `TestOpenAICompatProbeProviderAvailability`
- `TestOpenAICompatExecuteStreamsDeltasAndFinishReason`
- `TestOpenAICompatExecuteSendsHeaders`
- `TestOpenAICompatExecutePassesOptionsAsTopLevelFields`
- `TestOpenAICompatExecuteRejectsEmptyEndpointOrModel`

### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm
```

기대 결과: 새 adapter tests PASS, 기존 vLLM tests PASS.

## API-2 Factory registration

### 문제

`apps/node/internal/adapters/factory.go:27-56`는 `mock`, `ollama`, `vllm`, `cli`만 등록한다. 선행 task가 `openai_compat` typed payload를 만들더라도 Node가 이를 모르면 등록 실패가 난다.

```go
27		switch typeName {
28		case "mock":
29			reg.RegisterKeyed(instanceKey, typeName, mock.New(logger))
30		case "ollama":
31			var cfg config.OllamaConf
32			if m := ac.GetOllama(); m != nil {
33				cfg = ollamaConfFromProto(m)
34			} else {
35				cfg = ollamaConfFromStruct(ac.GetSettings())
36			}
37			reg.RegisterKeyed(instanceKey, typeName, ollama.New(cfg, logger, instanceKey))
38		case "vllm":
```

### 해결 방법

- factory imports에 `iop/apps/node/internal/adapters/openai_compat`를 추가한다.
- switch에 `case "openai_compat"`를 추가한다.
- typed payload는 `ac.GetOpenaiCompat()` 또는 generated accessor 이름을 사용한다. accessor 이름은 `make proto` 결과를 확인해 정확히 맞춘다.
- `openAICompatConfFromProto`와 `openAICompatConfFromStruct` helper를 추가한다.
- legacy settings fallback은 `provider`, `endpoint`, `headers`, queue fields를 읽는다.
- `reg.RegisterKeyed(instanceKey, typeName, openai_compat.New(cfg, logger, instanceKey))` 형태로 등록한다.

### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/adapters/factory.go`: import, switch case, proto/struct config helpers 추가.
- [ ] `apps/node/internal/adapters/factory_internal_test.go`: typed config와 legacy settings fallback 테스트 추가.
- [ ] `apps/node/internal/adapters/adapters_blackbox_test.go`: payload에서 registry key와 execution까지 blackbox 테스트 추가.

### 테스트 작성

- `TestOpenAICompatConfFromProto`
- `TestOpenAICompatConfFromStruct_LegacyPayload`
- `TestBuildFromPayload_OpenAICompatTypedRegistered`
- `TestBuildFromPayload_OpenAICompatExecution`

### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters
```

기대 결과: factory/internal blackbox tests PASS.

## API-3 Node execution regression coverage

### 문제

`apps/node/internal/adapters/adapters_blackbox_test.go:520-585`는 vLLM execution만 검증한다. `openai_compat`의 registry key, headers, options passthrough, model fallback이 route-level로 깨져도 현재 blackbox coverage가 없다.

```go
520	func TestBuildFromPayload_VllmExecution(t *testing.T) {
521		var gotModel string
522		var gotStream bool
...
549		reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
550			Adapters: []*iop.AdapterConfig{
551				{
552					Type:    "vllm",
553					Name:    "vllm@gpu",
554					Enabled: true,
555					Config:  &iop.AdapterConfig_Vllm{Vllm: &iop.VllmAdapterConfig{Endpoint: server.URL}},
```

### 해결 방법

- blackbox test server가 `/v1/models`와 `/v1/chat/completions`를 모두 제공하게 한다.
- typed `OpenAICompatAdapterConfig` payload로 registry를 만들고 `reg.Get("lemonade-local")` lookup을 검증한다.
- `ExecutionSpec{Target:"lemonade-model", Input:{"messages":..., "options":{"temperature":0.2,"max_tokens":8}}}` 실행 후 server가 header와 top-level options를 받았는지 확인한다.
- stream은 `data: {"choices":[{"delta":{"content":"LEMONADE_"}}]}`와 `[DONE]`을 사용한다.

### 수정 파일 및 체크리스트

- [ ] `apps/node/internal/adapters/adapters_blackbox_test.go`: integration-style blackbox test 추가.
- [ ] 필요 시 shared test sink/helper를 중복 최소화한다. 과도한 test abstraction은 만들지 않는다.

### 테스트 작성

- `TestBuildFromPayload_OpenAICompatExecution`

### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters
```

기대 결과: registry/factory/execution blackbox tests PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/openai_compat/openai_compat.go` | API-1 |
| `apps/node/internal/adapters/openai_compat/openai_compat_test.go` | API-1 |
| `apps/node/internal/adapters/vllm/vllm.go` | API-1 |
| `apps/node/internal/adapters/vllm/vllm_test.go` | API-1 |
| `apps/node/internal/adapters/factory.go` | API-2 |
| `apps/node/internal/adapters/factory_internal_test.go` | API-2 |
| `apps/node/internal/adapters/adapters_blackbox_test.go` | API-2, API-3 |

## 최종 검증

```bash
go test -count=1 ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm ./apps/node/internal/adapters ./apps/node/...
git diff --check
```

기대 결과: 모든 명령이 exit 0. `git diff --check`는 whitespace error 없음.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
