feat: 라우팅 정책 모델 오케스트레이션 진행 및 provider config 반영

- seulgivibe OpenAI-compatible provider 완료 후 archive로 이동
- 모델 그룹 혼합 제공자 디스패치 milestone 진행
- agent-spec: OpenAI-compatible surface, provider pool config refresh 갱신
- Go: mapper.go/config.go provider config 매핑 개선
- agent-task: 새 subtask plan/code-review 문서 추가
This commit is contained in:
toki 2026-07-11 14:27:24 +09:00
parent a2523513f5
commit aeec784c7b
18 changed files with 1452 additions and 130 deletions

View file

@ -0,0 +1,102 @@
# Milestone: Seulgivibe OpenAI-compatible Provider 연동
## 위치
- Roadmap: [ROADMAP.md](../../../../ROADMAP.md)
- Phase: [PHASE.md](../../../../phase/routing-policy-model-orchestration/PHASE.md)
## 목표
Seulgivibe의 Claude/OpenAI 프록시 경로를 IOP의 OpenAI-compatible provider family로 관리한다.
Claude 모델 3종과 OpenAI/Codex 모델 축을 정적 catalog로 노출하고, 사용자별 raw token은 IOP 호출 시점에 provider tunnel header로만 전달한다.
Codex `wire_api=responses` 경로가 provider tunnel을 통해 동작하도록 `/v1/responses` raw passthrough parity를 확보한다.
## 상태
[완료]
## 승격 조건
- 없음
## 구현 잠금
- 상태: 해제
- SDD: 필요
- SDD 문서: [SDD.md](../../../sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md)
- SDD 사유: OpenAI-compatible API/config schema, provider auth header, 외부 provider passthrough 계약이 바뀌는 Milestone이다.
- 잠금 해제 조건:
- [x] SDD 잠금이 해제되어 있다
- [x] SDD 사용자 리뷰가 없거나 승인/해결되었다
- [x] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다
- [x] Evidence Map이 완료 시 `Roadmap Completion`과 최종 검증 evidence로 검증 가능하게 연결되어 있다
- 결정 필요: 없음
## 범위
- Seulgivibe Claude/OpenAI proxy endpoint를 별도 provider id/type 축으로 관리하는 config/catalog 계약
- `seulgivibe_claude`, `seulgivibe_openai` provider type alias와 OpenAI-compatible adapter 재사용
- Claude 모델 `claude-sonnet-4-5`, `claude-opus-4-8`, `claude-fable-5` 정적 model catalog
- OpenAI/Codex 모델 `gpt-5.1`, `gpt-5.5` 정적 model catalog
- 사용자별 raw token을 inbound request header에서 읽어 provider tunnel `Authorization` header로 전달하는 경계
- Chat Completions provider tunnel과 Responses provider tunnel의 Seulgivibe passthrough 지원
- Seulgivibe provider가 model group에 참여할 때 `seulgivibe_claude`, `seulgivibe_openai`를 OpenAI-compatible passthrough(+sideband) 계열 provider로 유지하는 기준
- Seulgivibe provider 자체가 별도 execution path selector를 추가하지 않는 기준. model group 전역 selector 제거/거부는 [Model Group Mixed Provider Dispatch](../../../../phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)가 소유한다.
## 기능
### Epic: [seulgivibe-provider] Seulgivibe Provider Surface
Seulgivibe를 IOP 내부에서는 provider-first OpenAI-compatible resource로 다루고, 외부 호출자는 OpenAI-compatible model id와 request-time provider token만 사용하게 하는 기능을 묶는다.
- [x] [config-auth-catalog] Seulgivibe Claude/OpenAI provider aliases, `openai.provider_auth` schema, 정적 model catalog/계약 예시가 추가되어 있다. 검증: `GOCACHE=/config/workspace/iop/.cache/go-build go test ./packages/go/config -count=1`, `GOCACHE=/config/workspace/iop/.cache/go-build go test ./apps/edge/internal/node -count=1`, tracked docs/config secret scan이 통과했다.
- [x] [provider-token-tunnel] Edge OpenAI Chat Completions provider tunnel이 configured request header의 raw user token을 provider `Authorization` header로 전달하고 missing-required를 dispatch 전에 차단한다. 검증: `GOCACHE=/config/workspace/iop/.cache/go-build go test ./apps/edge/internal/openai -count=1`이 auth forwarding/missing tests를 포함해 통과했다.
- [x] [responses-passthrough] OpenAI-compatible provider route에서 `/v1/responses` raw passthrough가 동작하고 Codex-style unknown fields, streaming, provider auth, model rewrite를 보존/검증한다. 검증: `GOCACHE=/config/workspace/iop/.cache/go-build go test ./apps/edge/internal/openai -count=1`이 Responses passthrough tests를 포함해 통과했다.
## 현 작업 현황
- [x] 현재 checkout에는 `config-auth-catalog`, `provider-token-tunnel`, `responses-passthrough` 구현과 관련 regression test가 반영되어 있다.
- [x] 로컬 검증: `GOCACHE=/config/workspace/iop/.cache/go-build go test ./packages/go/config -count=1`, `GOCACHE=/config/workspace/iop/.cache/go-build go test ./apps/edge/internal/node -count=1`, `GOCACHE=/config/workspace/iop/.cache/go-build go test ./apps/edge/internal/openai -count=1`이 통과했다.
- [x] tracked docs/config secret scan은 실제 provider token/key 후보 없이 통과했다. `task-123` 같은 문서 예시가 `sk-123` 부분 문자열로 잡히는 오탐은 길이 기준 재검사에서 제외됐다.
- [x] `agent-task/m-seulgivibe-openai-compatible-provider/**/complete.log`는 현재 worktree에 없고, 본 종료는 2026-07-11 현재 checkout 코드 감사와 사용자 종료 요청을 기준으로 처리했다.
- [x] model group 전역 execution path selector 제거/거부는 [Model Group Mixed Provider Dispatch](../../../../phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)의 `no-client-response-mode` 후속 범위로 분리했다.
## 완료 리뷰
- 상태: 통과
- 요청일: 2026-07-11
- 완료 근거:
- 현재 checkout에서 세 기능 Task의 코드 경로와 regression test가 확인됐다.
- 코드 감사 중 Seulgivibe provider type label 정규화 누락을 보완했고 회귀 테스트를 추가했다.
- `go test ./packages/go/config -count=1`, `go test ./packages/go/... -count=1`, `go test ./apps/edge/internal/node -count=1`, `go test ./apps/edge/internal/openai -count=1`, `go test ./apps/edge/... -count=1`, `go test ./apps/node/internal/adapters/openai_compat -count=1`을 workspace Go cache로 실행해 모두 통과했다.
- tracked docs/config 범위의 secret scan에서 실제 provider token/key 후보가 없음을 확인했다.
- Spec sync: [provider-pool-config-refresh.md](../../../../../agent-spec/runtime/provider-pool-config-refresh.md), [openai-compatible-surface.md](../../../../../agent-spec/input/openai-compatible-surface.md)에 Seulgivibe/provider auth 현 구현을 반영했다.
- Workspace 잠금: 관련 lock 없음.
- 검토 항목:
- [x] 세 기능 Task가 현재 checkout 기준으로 구현되어 있다.
- [x] 최종 검증 출력이 SDD Evidence Map과 일치한다.
- [x] 실제 token 값이 tracked 문서/config/test output에 남지 않았다.
- [x] 사용자 완료 결과 확인을 받는다.
- [x] archive 이동을 승인받는다.
- agent-ui 상태 반영: 해당 없음
- 리뷰 코멘트: 현재 worktree에는 `agent-task/m-seulgivibe-openai-compatible-provider` task artifact가 없으므로, 완료 판정은 현재 checkout 코드 감사, spec sync, 검증 결과를 기준으로 처리했다.
## 범위 제외
- host-local `~/.claude/anthropic_key.sh`, `~/.codex/config.toml`, Pi coding 설정 변경
- 실제 JWT/API key/token 값을 tracked config, docs, task artifact에 저장
- Seulgivibe `/v1/models` endpoint를 catalog source of truth로 사용하는 방식
- provider response payload의 model echo rewrite 또는 sideband injection을 Responses 기본 passthrough에 강제하는 작업
- billing/chargeback, 조직 IAM, 장기 retention 정책
## 작업 컨텍스트
- 관련 경로: `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/runtime`, `proto/iop/runtime.proto`, [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md)
- 표준선(선택): Seulgivibe는 새 wire adapter가 아니라 OpenAI-compatible provider family로 관리하고, provider별 특수 처리는 generation passthrough 밖의 auth/catalog/config 경계에만 둔다.
- 표준선(선택): Seulgivibe Claude/OpenAI provider aliases는 OpenAI-compatible 호출 방식을 지원하므로 [Model Group Mixed Provider Dispatch](../../../../phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)의 passthrough(+sideband) 계열 provider에 포함된다. mixed provider dispatch의 일반 selection/path 분기는 해당 Milestone이 소유하고, 본 Milestone은 Seulgivibe auth/catalog/Responses raw passthrough를 소유한다.
- 표준선(선택): 사용자별 provider token은 request-time raw value로만 받고, Edge가 provider tunnel request header로 변환한다. Node나 host-local helper script가 사용자 token source of truth가 되지 않는다.
- 표준선(선택): provider `/models` endpoint가 실패해도 IOP `/v1/models`는 top-level `models[]` catalog를 source of truth로 노출한다.
- 우선순위 순서: [OpenAI-compatible 출력 검증 필터](../../../../phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md) 완료 후 본 Milestone을 진행한다.
- 선행 작업: [OpenAI-compatible Raw Tunnel과 Sideband Passthrough](openai-compatible-raw-tunnel-sideband-passthrough.md), [Model Alias Provider Pool과 Provider Catalog](../../operational-observability-provider-management/milestones/provider-catalog-device-status.md)
- 후속 작업: [Model Group Mixed Provider Dispatch](../../../../phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)의 model group mixed dispatch와 execution path selector 제거, 자동 route scorer 구현, provider auth per-provider granularity, Seulgivibe live smoke profile 정리
- 확인 필요: 없음

View file

@ -3,7 +3,7 @@
## 위치
- Milestone: [Seulgivibe OpenAI-compatible Provider 연동](../../../phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)
- Phase: [PHASE.md](../../../phase/routing-policy-model-orchestration/PHASE.md)
- Phase: [PHASE.md](../../../../phase/routing-policy-model-orchestration/PHASE.md)
## 상태
@ -30,10 +30,10 @@
| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [Seulgivibe OpenAI-compatible Provider 연동](../../../phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md) | 목표, 기능 Task, 범위 제외 기준 |
| Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md) | 외부 OpenAI-compatible request/response, model list, provider auth header 계약 |
| Contract | [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md) | 외부 OpenAI-compatible request/response, model list, provider auth header 계약 |
| Code | `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/runtime`, `proto/iop/runtime.proto` | config schema, route dispatch, provider tunnel, Node adapter header relay 구현 기준 |
| External Provider | Seulgivibe Claude/OpenAI proxy | Claude path와 OpenAI path 모두 OpenAI-compatible provider로 취급한다. Catalog는 IOP static config가 source of truth다. |
| User Decision | 현재 사용자 요청 | 세부 구현은 기존 provider-first/openai_compat/passthrough 표준선으로 확정 가능하다. Seulgivibe Claude/OpenAI aliases는 OpenAI-compatible 호출 방식을 지원하므로 model group mixed dispatch에서 passthrough provider에 포함하며, model group request가 `metadata.iop_response_mode`로 실행 경로를 고르지 않는다. |
| User Decision | 현재 사용자 요청 | 세부 구현은 기존 provider-first/openai_compat/passthrough 표준선으로 확정 가능하다. Seulgivibe Claude/OpenAI aliases는 OpenAI-compatible 호출 방식을 지원하므로 model group mixed dispatch에서 passthrough(+sideband) 계열 provider에 포함한다. model group 전역 execution path selector 제거/거부는 Model Group Mixed Provider Dispatch가 소유한다. |
## State Machine
@ -49,7 +49,7 @@
## Interface Contract
- 계약 원문: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md)
- 계약 원문: [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md)
- 입력:
- `nodes[].providers[].type`: `seulgivibe_claude` 또는 `seulgivibe_openai`를 허용하고 runtime type은 `openai_compat`로 정규화한다.
- `nodes[].providers[].endpoint`: Seulgivibe Claude path는 `/anthropic/v1`, OpenAI path는 `/openai/v1` compatible base URL이다.
@ -63,14 +63,14 @@
- IOP `/v1/models`: static `models[]` catalog의 Seulgivibe model ids를 반환한다.
- Chat Completions provider tunnel: provider status/header/body bytes를 raw relay하고 provider auth header를 포함한다.
- Responses provider tunnel: `/v1/responses` raw body를 model rewrite 외에는 보존해 provider로 전달하고 raw response를 relay한다.
- Seulgivibe provider가 model group에 포함될 때 execution path는 provider-derived이며, Seulgivibe aliases는 OpenAI-compatible passthrough provider로 분류된다.
- Seulgivibe provider가 model group에 포함될 때 execution path는 provider-derived이며, Seulgivibe aliases는 OpenAI-compatible passthrough(+sideband) 계열 provider로 분류된다.
- 금지:
- IOP runtime이 `~/.claude/anthropic_key.sh`, `~/.codex/config.toml`, local env helper를 token source로 읽지 않는다.
- 실제 token 값을 tracked config/docs/task artifact/log에 쓰지 않는다.
- inbound IOP auth `Authorization` header를 provider token source로 재사용하지 않는다.
- Seulgivibe `/v1/models` 실패를 IOP catalog 노출 실패로 연결하지 않는다.
- Responses passthrough body를 Chat Completions shape인 `max_tokens`로 변환하지 않는다.
- model group request에서 `metadata.iop_response_mode` 또는 동등한 passthrough/normalized selector로 Seulgivibe 실행 경로를 고르게 하지 않는다.
- Seulgivibe provider가 별도 execution path selector를 정의하지 않는다. model group 전역 selector 제거/거부는 [Model Group Mixed Provider Dispatch](../../../../phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)가 소유한다.
## Acceptance Scenarios
@ -80,17 +80,15 @@
| S02 | `config-auth-catalog` | `/v1/models` provider endpoint가 catalog source로 안정적이지 않다 | IOP model list와 provider pool validation을 수행한다 | static `models[]` catalog가 Claude/OpenAI model ids를 노출하고 provider served model membership을 검증한다 |
| S03 | `provider-token-tunnel` | caller가 configured provider auth header에 raw user token을 넣는다 | Chat Completions provider tunnel을 연다 | Edge가 provider `Authorization` header를 만들고 missing-required는 dispatch 전에 400으로 차단한다 |
| S04 | `responses-passthrough` | Codex-style `/v1/responses` payload가 provider-pool model id, unknown fields, `stream`을 포함할 수 있다 | Responses provider route가 실행된다 | Edge가 strict normalized parser를 우회해 raw body를 model rewrite만 적용한 뒤 `/v1/responses` provider tunnel로 전달한다 |
| S05 | `responses-passthrough` | Seulgivibe model group request가 `metadata.iop_response_mode` 또는 동등한 execution path selector를 포함한다 | Edge handler가 route envelope를 parse한다 | request는 `400 invalid_request_error`로 거부되고 provider dispatch가 발생하지 않는다 |
## Evidence Map
| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | config and mapper unit tests | `agent-task/m-seulgivibe-openai-compatible-provider/01_config_auth_catalog` | `Roadmap Completion``config-auth-catalog``go test ./packages/go/config -count=1`, `go test ./apps/edge/internal/node -count=1` 결과 |
| S02 | static catalog validation and secret scan | `agent-task/m-seulgivibe-openai-compatible-provider/01_config_auth_catalog` | `Roadmap Completion``config-auth-catalog`와 secret pattern scan 결과 |
| S03 | Edge OpenAI handler auth forwarding/missing tests | `agent-task/m-seulgivibe-openai-compatible-provider/02+01_provider_tunnel_auth` | `Roadmap Completion``provider-token-tunnel``go test ./apps/edge/internal/openai -count=1` 결과 |
| S04 | Edge OpenAI Responses tunnel tests | `agent-task/m-seulgivibe-openai-compatible-provider/03+01,02_responses_passthrough` | `Roadmap Completion``responses-passthrough``go test ./apps/edge/internal/openai -count=1` 결과 |
| S05 | Edge OpenAI handler selector rejection tests | `agent-task/m-seulgivibe-openai-compatible-provider/03+01,02_responses_passthrough` | `Roadmap Completion``responses-passthrough``metadata.iop_response_mode` 거부 assertion |
| S01 | config and mapper unit tests | 현재 checkout 검증 기준 | `config-auth-catalog``GOCACHE=/config/workspace/iop/.cache/go-build go test ./packages/go/config -count=1`, `GOCACHE=/config/workspace/iop/.cache/go-build go test ./apps/edge/internal/node -count=1` 결과 |
| S02 | static catalog validation and secret scan | 현재 checkout 검증 기준 | `config-auth-catalog`와 tracked docs/config secret scan 결과 |
| S03 | Edge OpenAI handler auth forwarding/missing tests | 현재 checkout 검증 기준 | `provider-token-tunnel``GOCACHE=/config/workspace/iop/.cache/go-build go test ./apps/edge/internal/openai -count=1` 결과 |
| S04 | Edge OpenAI Responses tunnel tests | 현재 checkout 검증 기준 | `responses-passthrough``GOCACHE=/config/workspace/iop/.cache/go-build go test ./apps/edge/internal/openai -count=1` 결과 |
## Cross-repo Dependencies
@ -99,7 +97,7 @@
## Drift Check
- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] Evidence Map이 현재 checkout 검증 결과로 검증 가능하다. `agent-task` complete 로그는 현재 worktree에 없다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.
@ -111,6 +109,6 @@
- 표준선: Seulgivibe는 OpenAI-compatible provider family로 관리하고, generation request body는 provider tunnel passthrough를 우선한다.
- 표준선: 사용자별 provider token은 request-time header value이며 IOP config에는 token source와 forwarding rule만 둔다.
- 표준선: `/v1/responses` provider route는 provider-original `passthrough`를 기본값으로 둔다. Seulgivibe model group request가 `metadata.iop_response_mode`로 response path를 선택하지 않는다. response body model echo rewrite는 하지 않는다.
- 표준선: Seulgivibe Claude/OpenAI aliases는 OpenAI-compatible 호출 방식을 지원하므로 Model Group Mixed Provider Dispatch의 passthrough provider에 포함한다. mixed provider dispatch 일반화는 별도 Milestone이 소유한다.
- 표준선: `/v1/responses` provider route는 provider-original `passthrough`를 기본값으로 둔다. Seulgivibe 자체는 별도 execution path selector를 정의하지 않는다. response body model echo rewrite는 하지 않는다.
- 표준선: Seulgivibe Claude/OpenAI aliases는 OpenAI-compatible 호출 방식을 지원하므로 Model Group Mixed Provider Dispatch의 passthrough(+sideband) 계열 provider에 포함한다. mixed provider dispatch 일반화는 별도 Milestone이 소유한다.
- 후속 SDD: 없음

View file

@ -20,9 +20,9 @@ IOP의 OpenAI-compatible, A2A, IOP native 입력 표면에서 들어온 요청
- 경로: [openai-compatible-raw-tunnel-sideband-passthrough](../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- 요약: OpenAI-compatible provider 응답을 기존 Edge-Node proto-socket 위 lossless raw tunnel로 전달하고, 기본값은 provider-original `passthrough`로 두며, 요청이 명시한 경우에만 `passthrough+sideband` 또는 `transformed`를 사용한다.
- [계획] Seulgivibe OpenAI-compatible Provider 연동
- 경로: [seulgivibe-openai-compatible-provider](milestones/seulgivibe-openai-compatible-provider.md)
- 요약: Seulgivibe Claude/OpenAI 프록시를 OpenAI-compatible provider family로 관리하고, 정적 catalog, 요청 시점 provider token forwarding, Codex Responses passthrough를 구현한다.
- [완료] Seulgivibe OpenAI-compatible Provider 연동
- 경로: [seulgivibe-openai-compatible-provider](../../archive/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)
- 요약: Seulgivibe Claude/OpenAI 프록시를 OpenAI-compatible provider family로 관리하고, model group에서는 passthrough(+sideband) 계열 provider로 분류한다. 정적 catalog, 요청 시점 provider token forwarding, Codex Responses passthrough는 코드 감사와 spec sync까지 통과해 archive했다.
- [계획] Model Group Mixed Provider Dispatch
- 경로: [model-group-mixed-provider-dispatch](milestones/model-group-mixed-provider-dispatch.md)

View file

@ -7,7 +7,7 @@
## 목표
Model group provider pool이 OpenAI-compatible wire provider와 native/normalized provider를 같은 후보군으로 다룰 수 있게 한다.
Model group provider pool이 OpenAI-compatible provider와 native/normalized provider를 같은 후보군으로 다룰 수 있게 한다.
Edge는 기존 `capacity + priority` 기준으로 provider를 먼저 선택하고, 선택된 provider가 OpenAI-compatible 호출 방식을 지원하면 모두 passthrough 방식으로 dispatch한다.
OpenAI-compatible 호출 방식을 지원하지 않는 Ollama/CLI 같은 native provider만 normalized 실행 경로로 dispatch한다.
Client 요청은 provider 실행 경로를 고르는 필드를 넣지 않으며, provider-specific/custom request field는 provider-pool route에서 먼저 보존한 뒤 선택된 실행 경로의 계약에 맞게 처리한다.
@ -25,7 +25,7 @@ Client 요청은 provider 실행 경로를 고르는 필드를 넣지 않으며,
- 상태: 해제
- SDD: 필요
- SDD 문서: [SDD.md](../../../sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md)
- SDD 사유: Model group provider-pool dispatch, OpenAI-compatible request surface, Edge-Node runtime path, provider capability 계약이 함께 바뀌는 Milestone이다.
- SDD 사유: Model group provider-pool dispatch, OpenAI-compatible request surface, Edge-Node runtime path, provider 실행 경로 계약이 함께 바뀌는 Milestone이다.
- 잠금 해제 조건:
- [x] SDD 잠금이 해제되어 있다
- [x] SDD 사용자 리뷰가 없거나 승인/해결되었다
@ -39,8 +39,8 @@ Client 요청은 provider 실행 경로를 고르는 필드를 넣지 않으며,
- `capacity + priority` 기준의 기존 provider 선택 정책 유지
- OpenAI-compatible 호출 방식 지원 여부 기반 실행 경로 분기
- OpenAI-compatible 호출 방식을 지원하는 모든 provider: passthrough 방식
- provider type/label/capability가 `openai_compat`, `openai_api`, `vllm`, `vllm-mlx`, `lemonade`, `sglang`, `seulgivibe_*`, openweight cloud request model 계열인 provider: OpenAI-compatible provider로 보고 passthrough 방식
- provider type/capability가 `ollama`, `cli` 및 OpenAI-compatible wire를 지원하지 않는 provider: normalized
- provider type/label/선언된 지원 방식이 `openai_compat`, `openai_api`, `vllm`, `vllm-mlx`, `lemonade`, `sglang`, `seulgivibe_*`, openweight cloud request model 계열인 provider: OpenAI-compatible provider로 보고 passthrough 방식
- provider type/선언된 지원 방식이 `ollama`, `cli` 및 OpenAI-compatible 호출 방식을 지원하지 않는 provider: normalized
- Seulgivibe Claude/OpenAI provider는 현재 구현 중인 Seulgivibe Milestone의 auth/catalog/Responses passthrough 범위를 유지하되, 이 Milestone의 classifier에서는 `seulgivibe_claude`, `seulgivibe_openai` 모두 OpenAI-compatible passthrough provider에 포함한다.
- OpenAI-compatible provider의 tunnel 구현이 누락된 경우 normalized fallback으로 낮추지 않고 구현 결함 또는 unsupported 상태로 처리하는 기준
- Ollama provider를 model group에서 제외하지 않고, 작은 capacity/priority 설정으로 운영자가 후보 가중치를 조절하는 방식
@ -53,10 +53,10 @@ Client 요청은 provider 실행 경로를 고르는 필드를 넣지 않으며,
### Epic: [mixed-dispatch] Mixed Provider Dispatch
Model group이 provider 종류에 따라 normalized-only로 후퇴하지 않고, 선택된 provider 성격에 맞는 실행 경로로 dispatch되는 capability를 묶는다.
Model group이 provider 종류에 따라 normalized-only로 후퇴하지 않고, 선택된 provider의 OpenAI-compatible 지원 여부에 맞는 실행 경로로 dispatch되는 기능을 묶는다.
- [ ] [selection-first-path] Provider-pool route가 provider 후보를 먼저 선택하고 같은 queue slot/lease로 `ProviderTunnelRequest` 또는 normalized `RunRequest` 중 하나를 실행한다. 검증: `go test ./apps/edge/internal/service -count=1`에서 double scheduling 없이 selected provider path가 고정됨을 확인한다.
- [ ] [provider-path-classifier] Provider classifier가 OpenAI-compatible 호출 방식을 지원하는 모든 provider를 passthrough 방식으로, Ollama/CLI/native provider를 normalized로 분류한다. Seulgivibe aliases는 passthrough provider에 포함한다. 검증: `go test ./packages/go/config -count=1`, `go test ./apps/edge/internal/node -count=1`, `go test ./apps/node/internal/adapters -count=1`이 provider alias와 adapter capability case를 포함해 통과한다.
- [ ] [provider-path-classifier] Provider classifier가 OpenAI-compatible 호출 방식을 지원하는 모든 provider를 passthrough 방식으로, Ollama/CLI/native provider를 normalized로 분류한다. Seulgivibe aliases는 passthrough provider에 포함한다. 검증: `go test ./packages/go/config -count=1`, `go test ./apps/edge/internal/node -count=1`, `go test ./apps/node/internal/adapters -count=1`이 provider alias와 adapter implementation case를 포함해 통과한다.
- [ ] [mixed-model-group] 같은 model group 안의 vLLM/vLLM-MLX/Lemonade/openweight cloud provider와 Ollama provider가 모두 후보로 남고, 선택된 provider별로 tunnel 또는 normalized path가 실행된다. 검증: `go test ./apps/edge/internal/openai -count=1`, `go test ./apps/edge/internal/service -count=1`이 mixed group, Ollama-only group, tunnel-only group fixture를 포함해 통과한다.
### Epic: [model-group-surface] Model Group Client Surface
@ -64,7 +64,7 @@ Model group이 provider 종류에 따라 normalized-only로 후퇴하지 않고,
Client가 OpenAI-compatible 표면으로 호출하되 model group 내부 실행 방식을 직접 고르지 않도록 계약과 handler를 정리한다.
- [ ] [no-client-response-mode] Model group route에서 `metadata.iop_response_mode` 또는 동등한 passthrough/normalized selector를 지원하지 않도록 계약, 구현, 테스트를 정리한다. 검증: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md), [openai-compatible-surface.md](../../../../agent-spec/input/openai-compatible-surface.md), handler tests가 model group selector 거부/제거 기준과 일치한다.
- [ ] [custom-field-preservation] Provider-pool Chat route가 Responses route처럼 strict decode 전에 raw body와 routing envelope를 분리하고, passthrough-capable provider 선택 시 unknown/Codex/provider-specific fields를 model rewrite 외에는 보존한다. 검증: `go test ./apps/edge/internal/openai -count=1`이 Chat unknown field preservation과 normalized-provider 선택 시 supported field mapping/unsupported policy를 확인한다.
- [ ] [custom-field-preservation] Provider-pool Chat route가 Responses route처럼 strict decode 전에 raw body와 routing envelope를 분리하고, OpenAI-compatible provider 선택 시 unknown/Codex/provider-specific fields를 model rewrite 외에는 보존한다. 검증: `go test ./apps/edge/internal/openai -count=1`이 Chat unknown field preservation과 normalized-provider 선택 시 supported field mapping/unsupported policy를 확인한다.
- [ ] [sideband-observation] Provider-pool 실행 결과가 selected provider, adapter, served target, execution path, queue decision, usage 후보를 sideband/log/metric에 남기며 표준 client 응답에는 불필요한 custom field를 섞지 않는다. 검증: `go test ./apps/edge/internal/openai -count=1`, `go test ./apps/edge/internal/service -count=1`이 standard-client response와 IOP-aware observation fixture를 함께 확인한다.
### Epic: [contract-spec] Contract and Spec Sync
@ -99,10 +99,10 @@ Model group mixed dispatch의 공개/내부 계약을 문서와 구현 타입에
## 작업 컨텍스트
- 관련 경로: `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/adapters/ollama`, `apps/node/internal/runtime`, `proto/iop/runtime.proto`, [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md), [edge-node-runtime-wire.md](../../../../agent-contract/inner/edge-node-runtime-wire.md), [edge-config-runtime-refresh.md](../../../../agent-contract/inner/edge-config-runtime-refresh.md)
- 표준선(선택): Model group은 normalized provider만 묶는 추상화가 아니다. OpenAI-compatible wire provider와 native provider를 같은 candidate set에 두고, 선택된 provider가 실행 경로를 결정한다.
- 표준선(선택): Model group은 normalized provider만 묶는 추상화가 아니다. OpenAI-compatible provider와 native provider를 같은 candidate set에 두고, 선택된 provider가 실행 경로를 결정한다.
- 표준선(선택): OpenAI-compatible 호출 방식을 지원하는 모든 provider는 passthrough 방식에 속한다. 여기에는 openweight cloud provider, Seulgivibe Claude/OpenAI provider, 로컬 vLLM/vLLM-MLX/Lemonade/SGLang 계열이 포함된다. sideband는 내부 observation 또는 문서화된 extension-safe 지점으로만 다루며, client 요청 selector가 아니다.
- 표준선(선택): OpenAI-compatible provider에서 tunnel/passthrough 구현이 빠져 있으면 normalized fallback으로 처리하지 않는다. 이는 구현 결함 또는 unsupported 상태다.
- 표준선(선택): Ollama와 CLI처럼 OpenAI-compatible wire를 지원하지 않는 provider는 model group 안에서도 normalized로 실행한다. Ollama는 제외하지 않으며 운영자는 capacity/priority로 낮은 동시성을 표현한다.
- 표준선(선택): Ollama와 CLI처럼 OpenAI-compatible 호출 방식을 지원하지 않는 provider는 model group 안에서도 normalized로 실행한다. Ollama는 제외하지 않으며 운영자는 capacity/priority로 낮은 동시성을 표현한다.
- 표준선(선택): Model group client request에는 `passthrough`, `passthrough+sideband`, `normalized`, `transformed` 같은 실행 경로 selector를 넣지 않는다.
- 표준선(선택): 표준 OpenAI-compatible client 요청은 표준-compatible 응답을 받고, IOP-aware/custom 요청은 문서화된 extension-safe 지점에서만 sideband/custom observation을 볼 수 있다.
- 우선순위/정합성: 현재 active 흐름에서는 [OpenAI-compatible 출력 검증 필터](../../knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)와 [Seulgivibe OpenAI-compatible Provider 연동](seulgivibe-openai-compatible-provider.md)을 함께 고려한다. Seulgivibe 구현은 OpenAI-compatible provider는 passthrough 방식이라는 기준과 충돌하지 않아야 하며, mixed provider dispatch 자체는 본 Milestone이 소유한다.

View file

@ -1,87 +0,0 @@
# Milestone: Seulgivibe OpenAI-compatible Provider 연동
## 위치
- Roadmap: [ROADMAP.md](../../../ROADMAP.md)
- Phase: [PHASE.md](../PHASE.md)
## 목표
Seulgivibe의 Claude/OpenAI 프록시 경로를 IOP의 OpenAI-compatible provider family로 관리한다.
Claude 모델 3종과 OpenAI/Codex 모델 축을 정적 catalog로 노출하고, 사용자별 raw token은 IOP 호출 시점에 provider tunnel header로만 전달한다.
Codex `wire_api=responses` 경로가 provider tunnel을 통해 동작하도록 `/v1/responses` raw passthrough parity를 확보한다.
## 상태
[계획]
## 승격 조건
- 없음
## 구현 잠금
- 상태: 해제
- SDD: 필요
- SDD 문서: [SDD.md](../../../sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md)
- SDD 사유: OpenAI-compatible API/config schema, provider auth header, 외부 provider passthrough 계약이 바뀌는 Milestone이다.
- 잠금 해제 조건:
- [x] SDD 잠금이 해제되어 있다
- [x] SDD 사용자 리뷰가 없거나 승인/해결되었다
- [x] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다
- [x] Evidence Map이 완료 시 `Roadmap Completion`과 최종 검증 evidence로 검증 가능하게 연결되어 있다
- 결정 필요: 없음
## 범위
- Seulgivibe Claude/OpenAI proxy endpoint를 별도 provider id/type 축으로 관리하는 config/catalog 계약
- `seulgivibe_claude`, `seulgivibe_openai` provider type alias와 OpenAI-compatible adapter 재사용
- Claude 모델 `claude-sonnet-4-5`, `claude-opus-4-8`, `claude-fable-5` 정적 model catalog
- OpenAI/Codex 모델 `gpt-5.1`, `gpt-5.5` 정적 model catalog
- 사용자별 raw token을 inbound request header에서 읽어 provider tunnel `Authorization` header로 전달하는 경계
- Chat Completions provider tunnel과 Responses provider tunnel의 Seulgivibe passthrough 지원
- Seulgivibe provider가 model group에 참여할 때 `seulgivibe_claude`, `seulgivibe_openai`를 OpenAI-compatible passthrough provider로 유지하는 기준
- model group request에서 client-controlled `metadata.iop_response_mode` 또는 동등한 passthrough/normalized selector를 받지 않는 기준
## 기능
### Epic: [seulgivibe-provider] Seulgivibe Provider Surface
Seulgivibe를 IOP 내부에서는 provider-first OpenAI-compatible resource로 다루고, 외부 호출자는 OpenAI-compatible model id와 request-time provider token만 사용하게 하는 capability를 묶는다.
- [ ] [config-auth-catalog] Seulgivibe Claude/OpenAI provider aliases, `openai.provider_auth` schema, 정적 model catalog/계약 예시가 추가되어 있다. 검증: `go test ./packages/go/config -count=1`, `go test ./apps/edge/internal/node -count=1`, secret pattern scan이 통과한다.
- [ ] [provider-token-tunnel] Edge OpenAI Chat Completions provider tunnel이 configured request header의 raw user token을 provider `Authorization` header로 전달하고 missing-required를 dispatch 전에 차단한다. 검증: `go test ./apps/edge/internal/openai -count=1`이 auth forwarding/missing tests를 포함해 통과한다.
- [ ] [responses-passthrough] OpenAI-compatible provider route에서 `/v1/responses` raw passthrough가 동작하고 Codex-style unknown fields, streaming, provider auth, model rewrite를 보존/검증한다. model group request는 client-controlled response mode selector를 받지 않는다. 검증: `go test ./apps/edge/internal/openai -count=1`이 Responses passthrough와 selector rejection tests를 포함해 통과한다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 기능 Task가 아직 충족되지 않았다.
- 검토 항목:
- [ ] 세 subtask의 `complete.log`가 각 Roadmap Completion task id를 기록한다.
- [ ] 최종 검증 출력이 SDD Evidence Map과 일치한다.
- [ ] 실제 token 값이 tracked 문서/config/test output에 남지 않았다.
- [ ] Seulgivibe model group 요청에서 client-controlled response path selector가 남아 있지 않다.
- agent-ui 상태 반영: 해당 없음
- 리뷰 코멘트: 없음
## 범위 제외
- host-local `~/.claude/anthropic_key.sh`, `~/.codex/config.toml`, Pi coding 설정 변경
- 실제 JWT/API key/token 값을 tracked config, docs, task artifact에 저장
- Seulgivibe `/v1/models` endpoint를 catalog source of truth로 사용하는 방식
- provider response payload의 model echo rewrite 또는 sideband injection을 Responses 기본 passthrough에 강제하는 작업
- billing/chargeback, 조직 IAM, 장기 retention 정책
## 작업 컨텍스트
- 관련 경로: `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/runtime`, `proto/iop/runtime.proto`, [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md)
- 표준선(선택): Seulgivibe는 새 wire adapter가 아니라 OpenAI-compatible provider family로 관리하고, provider별 특수 처리는 generation passthrough 밖의 auth/catalog/config 경계에만 둔다.
- 표준선(선택): Seulgivibe Claude/OpenAI provider aliases는 OpenAI-compatible 호출 방식을 지원하므로 [Model Group Mixed Provider Dispatch](model-group-mixed-provider-dispatch.md)의 passthrough provider에 포함된다. mixed provider dispatch의 일반 selection/path 분기는 해당 Milestone이 소유하고, 본 Milestone은 Seulgivibe auth/catalog/Responses passthrough를 소유한다.
- 표준선(선택): 사용자별 provider token은 request-time raw value로만 받고, Edge가 provider tunnel request header로 변환한다. Node나 host-local helper script가 사용자 token source of truth가 되지 않는다.
- 표준선(선택): provider `/models` endpoint가 실패해도 IOP `/v1/models`는 top-level `models[]` catalog를 source of truth로 노출한다.
- 우선순위 순서: [OpenAI-compatible 출력 검증 필터](../../knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md) 완료 후 본 Milestone을 진행한다.
- 선행 작업: [OpenAI-compatible Raw Tunnel과 Sideband Passthrough](../../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md), [Model Alias Provider Pool과 Provider Catalog](../../operational-observability-provider-management/milestones/provider-catalog-device-status.md)
- 후속 작업: [Model Group Mixed Provider Dispatch](model-group-mixed-provider-dispatch.md), 자동 route scorer 구현, provider auth per-provider granularity, Seulgivibe live smoke profile 정리
- 확인 필요: 없음

View file

@ -18,7 +18,7 @@
## 문제 / 비목표
- 문제: 현재 provider-pool/model group route는 provider pool이면 raw tunnel을 먼저 가정하는 경향이 있어, 같은 model group 안에 Ollama 같은 normalized-only provider가 들어오면 선택된 provider capability와 실행 경로가 어긋날 수 있다. 반대로 model group 전체를 normalized로 낮추면 vLLM/vLLM-MLX/Lemonade/openweight cloud provider의 provider-original passthrough 장점이 사라진다. Model group은 provider 후보를 동일하게 평가하되, 선택된 provider 성격에 따라 실행 경로를 자동 결정해야 한다.
- 문제: 현재 provider-pool/model group route는 provider pool이면 raw tunnel을 먼저 가정하는 경향이 있어, 같은 model group 안에 Ollama 같은 normalized-only provider가 들어오면 선택된 provider의 OpenAI-compatible 지원 여부와 실행 경로가 어긋날 수 있다. 반대로 model group 전체를 normalized로 낮추면 vLLM/vLLM-MLX/Lemonade/openweight cloud provider의 provider-original passthrough 장점이 사라진다. Model group은 provider 후보를 동일하게 평가하되, 선택된 provider의 OpenAI-compatible 지원 여부에 따라 실행 경로를 자동 결정해야 한다.
- 비목표:
- provider 선택 기준을 `capacity + priority` 밖의 score policy로 바꾼다.
- Ollama를 model group에서 제외한다.
@ -33,7 +33,7 @@
| Roadmap | [Model Group Mixed Provider Dispatch](../../../phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md) | 목표, 기능 Task, 범위 제외 기준 |
| Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md), [edge-node-runtime-wire.md](../../../../agent-contract/inner/edge-node-runtime-wire.md), [edge-config-runtime-refresh.md](../../../../agent-contract/inner/edge-config-runtime-refresh.md) | 외부 OpenAI-compatible model group surface, provider tunnel, normalized run dispatch, provider config 계약 |
| Spec | [openai-compatible-surface.md](../../../../agent-spec/input/openai-compatible-surface.md), [provider-pool-config-refresh.md](../../../../agent-spec/runtime/provider-pool-config-refresh.md), [edge-node-execution.md](../../../../agent-spec/runtime/edge-node-execution.md) | 현재 구현 surface와 runtime behavior 문서 |
| Code | `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/adapters/ollama`, `apps/node/internal/runtime`, `proto/iop/runtime.proto` | config validation, provider selection, OpenAI handler, Edge-Node wire, adapter capability 구현 기준 |
| Code | `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/node`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/adapters/ollama`, `apps/node/internal/runtime`, `proto/iop/runtime.proto` | config validation, provider selection, OpenAI handler, Edge-Node wire, adapter implementation 기준 |
| User Decision | 현재 사용자 요청 | Ollama는 model group에서 제외하지 않는다. 후보는 동일하게 두고 capacity/priority로 가중한다. model group request에는 response path selector를 두지 않는다. OpenAI-compatible 호출 방식을 지원하는 provider는 모두 passthrough 방식으로 처리하고, Ollama/CLI/native provider는 normalized로 처리한다. Seulgivibe Claude/OpenAI aliases는 passthrough provider에 포함한다. custom request fields는 provider-pool ingress에서 보존한다. |
## State Machine
@ -65,20 +65,20 @@
- `nodes[].providers[].capacity`, `priority`, `enabled`, health/status, model membership은 기존 provider selection 기준이다.
- 실행 경로:
- OpenAI-compatible 호출 방식을 지원하는 provider는 모두 passthrough 실행 경로를 사용한다.
- OpenAI-compatible provider는 Node adapter가 `ProviderTunnelAdapter` capability를 제공해야 한다. 이 capability가 없으면 normalized fallback이 아니라 구현 결함 또는 unsupported 상태로 처리한다.
- `ollama`, `cli` 및 OpenAI-compatible wire를 지원하지 않는 native provider는 normalized `RunRequest` 실행 경로를 사용한다.
- provider type/label/capability가 `openai_compat`, `openai_api`, `vllm`, `vllm-mlx`, `lemonade`, `sglang`, `seulgivibe_claude`, `seulgivibe_openai` 계열이면 tunnel-capable provider로 분류한다.
- provider type/capability가 `ollama`, `cli`이면 normalized-only provider로 분류한다.
- OpenAI-compatible provider는 Node adapter가 `ProviderTunnelAdapter` interface를 구현해야 한다. 이 interface가 없으면 normalized fallback이 아니라 구현 결함 또는 unsupported 상태로 처리한다.
- `ollama`, `cli` 및 OpenAI-compatible 호출 방식을 지원하지 않는 native provider는 normalized `RunRequest` 실행 경로를 사용한다.
- provider type/label/선언된 지원 방식이 `openai_compat`, `openai_api`, `vllm`, `vllm-mlx`, `lemonade`, `sglang`, `seulgivibe_claude`, `seulgivibe_openai` 계열이면 OpenAI-compatible provider로 분류한다.
- provider type/선언된 지원 방식이 `ollama`, `cli`이면 normalized-only provider로 분류한다.
- Seulgivibe Claude/OpenAI provider는 현재 Seulgivibe Milestone에서 다루는 auth/catalog/Responses passthrough 구현을 유지하면서도, 이 classifier에서는 OpenAI-compatible passthrough provider에 포함된다.
- selection에서 provider type만을 이유로 Ollama를 제외하지 않는다. 운영자는 작은 capacity/priority로 Ollama 동시성 한계를 표현한다.
- 출력:
- 표준 OpenAI-compatible client 요청에는 표준-compatible response를 반환하고 불필요한 IOP custom field/event를 섞지 않는다.
- passthrough-capable provider 선택 시 provider HTTP status/header/body 또는 provider SSE를 최대한 보존하며, model rewrite와 문서화된 IOP extension-safe 지점만 예외다.
- OpenAI-compatible provider 선택 시 provider HTTP status/header/body 또는 provider SSE를 최대한 보존하며, model rewrite와 문서화된 IOP extension-safe 지점만 예외다.
- normalized provider 선택 시 runtime event를 OpenAI-compatible Chat/Responses response shape로 변환한다.
- 모든 실행 경로는 내부 observation으로 selected provider id/type, adapter, served target, execution path, queue decision, usage 후보를 남긴다.
- IOP-aware/custom request surface가 문서화된 extension-safe 지점을 사용하면 sideband/custom observation을 받을 수 있다. 표준-only request에는 응답 custom field를 만들지 않는다.
- custom field 처리:
- passthrough-capable provider가 선택되면 raw body의 unknown/Codex/provider-specific field는 model rewrite와 명시 변환 외에는 provider로 보존 전달한다.
- OpenAI-compatible provider가 선택되면 raw body의 unknown/Codex/provider-specific field는 model rewrite와 명시 변환 외에는 provider로 보존 전달한다.
- normalized-only provider가 선택되면 standard OpenAI-compatible subset과 IOP가 아는 extension만 native adapter request로 변환한다. 변환할 수 없는 provider-specific required field는 dispatch 전 명시적 unsupported error로 끝내고, 단순 unknown field는 observation에 남기되 native provider request에 임의로 invent하지 않는다.
- 금지:
- model group 전체를 normalized로 낮춰 vLLM/vLLM-MLX/Lemonade/openweight cloud provider의 raw passthrough 경로를 잃지 않는다.
@ -92,11 +92,11 @@
|----|----------------|-------|------|------|
| S01 | `selection-first-path` | mixed model group에 vLLM provider와 Ollama provider가 있고 둘 다 healthy/capacity가 있다 | provider-pool route를 실행한다 | 기존 capacity/priority 기준으로 provider를 한 번 선택하고, 같은 selected provider로 tunnel 또는 normalized 실행을 수행한다 |
| S02 | `selection-first-path` | selected provider가 Ollama다 | Chat Completions request를 처리한다 | Edge가 `ProviderTunnelRequest`를 보내지 않고 normalized `RunRequest`로 Ollama adapter를 실행한다 |
| S03 | `provider-path-classifier` | provider type/label/capability가 `vllm`, `vllm-mlx`, `lemonade`, `openai_compat`, `seulgivibe_claude`, `seulgivibe_openai` 중 하나이거나 OpenAI-compatible 호출 방식을 지원한다 | execution path를 resolve한다 | passthrough path를 선택한다 |
| S04 | `provider-path-classifier` | provider type/capability가 `ollama` 또는 `cli`다 | execution path를 resolve한다 | normalized-only로 분류하고 model group 후보에서는 제외하지 않는다 |
| S03 | `provider-path-classifier` | provider type/label/선언된 지원 방식이 `vllm`, `vllm-mlx`, `lemonade`, `openai_compat`, `seulgivibe_claude`, `seulgivibe_openai` 중 하나이거나 OpenAI-compatible 호출 방식을 지원한다 | execution path를 resolve한다 | passthrough path를 선택한다 |
| S04 | `provider-path-classifier` | provider type/선언된 지원 방식이 `ollama` 또는 `cli`다 | execution path를 resolve한다 | normalized-only로 분류하고 model group 후보에서는 제외하지 않는다 |
| S05 | `mixed-model-group` | Ollama-only model group config가 있다 | OpenAI-compatible Chat request를 보낸다 | provider-pool route가 정상 후보를 찾고 normalized OpenAI-compatible response를 반환한다 |
| S06 | `no-client-response-mode` | model group request가 `metadata.iop_response_mode`를 포함한다 | Edge handler가 route envelope를 parse한다 | request는 `400 invalid_request_error`로 거부되고 provider dispatch가 발생하지 않는다 |
| S07 | `custom-field-preservation` | Chat request에 Codex/provider-specific unknown field가 있고 selected provider가 tunnel-capable이다 | provider tunnel request body를 만든다 | unknown field가 model rewrite 외에는 그대로 provider로 전달된다 |
| S07 | `custom-field-preservation` | Chat request에 Codex/provider-specific unknown field가 있고 selected provider가 OpenAI-compatible provider다 | provider tunnel request body를 만든다 | unknown field가 model rewrite 외에는 그대로 provider로 전달된다 |
| S08 | `custom-field-preservation` | 같은 unknown field request에서 selected provider가 Ollama다 | normalized request를 만든다 | standard field는 Ollama request로 변환되고, 변환 불가능한 required field는 explicit unsupported error 또는 observation으로 처리된다 |
| S09 | `sideband-observation` | 표준 OpenAI-compatible client가 model group을 호출한다 | provider가 성공 응답을 반환한다 | caller response에는 불필요한 IOP custom field가 없고, internal log/metric에는 selected provider와 execution path가 남는다 |
| S10 | `contract-sync` | contract/spec 문서를 검토한다 | Milestone 구현 diff가 준비된다 | model group의 provider-derived execution path, selector 금지, custom field 보존 기준이 문서와 구현에 동일하게 반영되어 있다 |

View file

@ -52,6 +52,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행
| bearer auth | `openai.bearer_token`이 있으면 matching bearer authorization header를 요구한다. |
| principal token auth | `openai.principal_tokens[]`가 설정된 경우 raw token의 SHA-256 hash를 `token_hash_sha256`과 매칭하고, 매칭 시 `iop_principal_ref`, `iop_principal_alias`, `iop_token_ref`, `iop_principal_source` metadata를 채운다. |
| multi-token principal | 같은 `principal_ref`에 여러 `token_ref`를 연결할 수 있으며, 사용량 metric은 사용자 합산과 token/app별 breakdown을 모두 가능하게 한다. |
| provider auth forwarding | `openai.provider_auth`가 활성화된 provider tunnel route는 caller의 configured request header에서 raw provider token을 읽어 provider request header로 전달하고, required header가 없으면 dispatch 전에 거부한다. |
| model catalog | `/v1/models`는 provider-pool `models[]`, legacy `openai.model_routes[]`, `openai.models` 또는 `openai.target` 순서로 노출 모델을 만든다. |
| model dispatch | request `model`은 provider-pool catalog, legacy model route, single target fallback 순서로 해석된다. |
| provider-pool handoff | provider-pool catalog에 model이 있으면 service 요청은 `ProviderPool=true`로 전달되고 adapter/target은 provider selection 이후 확정된다. |
@ -111,6 +112,7 @@ sequenceDiagram
- `configs/edge.yaml``openai` 섹션이 listener, bearer token, legacy adapter/target, model routes, strict output을 제공한다.
- top-level `models[]`가 있으면 OpenAI model list와 provider-pool dispatch에서 legacy route보다 우선한다.
- `openai.provider_auth`는 provider tunnel forwarding rule만 저장하고 raw provider token 값은 request-time header에서만 읽는다. inbound IOP `Authorization` header를 provider token source로 재사용하지 않는다.
- OpenAI request의 `metadata.workspace`는 absolute path가 필요한 route에서만 필수 검증된다.
- OpenAI Chat Completions와 provider route Responses request의 `metadata.iop_response_mode``passthrough`, `passthrough+sideband`, `transformed`만 허용한다. provider route에서 생략하면 `passthrough`다. Responses provider route의 `transformed`는 지원하지 않는다.
- run metadata에는 `openai_model`, `openai_stream`, `strict_output`, `estimated_input_tokens`, `context_class`가 들어갈 수 있다.
@ -148,6 +150,7 @@ sequenceDiagram
- principal token auth가 실패하면 legacy `openai.bearer_token`이 unmapped fallback으로 동작한다.
- provider가 별도 reasoning token을 보고하지 않으면 reasoning text를 token으로 추정하지 않는다.
- Grafana guide는 metric 조회와 operator-managed price baseline 예시이며 live cloud pricing, billing, chargeback, long-term ledger, 사용자별 제한 enforcement의 source of truth가 아니다.
- Seulgivibe Claude/OpenAI proxy는 별도 OpenAI-compatible provider family label로 보존될 수 있지만, HTTP body shape는 provider tunnel passthrough 경계를 따른다.
## 변경 기록
@ -158,3 +161,4 @@ sequenceDiagram
- 2026-07-10: 일별 Usage 비용/ROI 리포트 MVP 종료 검토에서 daily/monthly rollup, usage origin breakdown, cloud-equivalent cost, avoided-cost ROI 문서 표면을 반영.
- 2026-07-11: provider model group `/v1/responses` raw passthrough 동작(모델 rewrite, unknown/Codex field 보존, streaming relay, provider auth forwarding)과 endpoint=`responses` usage metric label을 현재 코드 기준으로 반영.
- 2026-07-11: Responses provider route의 명시적 `passthrough+sideband` 동작을 반영. non-stream은 응답 `metadata`, stream은 `event: iop.sideband`를 확장 지점으로 사용한다.
- 2026-07-11: provider auth forwarding과 Seulgivibe OpenAI-compatible provider family surface를 종료 검토 기준으로 보강.

View file

@ -21,9 +21,15 @@ source_evidence:
- type: code
path: apps/edge/internal/bootstrap/runtime.go
notes: mutable config apply, runtime snapshot 교체, Node refresh push
- type: code
path: apps/edge/internal/node/mapper.go
notes: provider-first NodeConfigPayload compile과 OpenAI-compatible provider label 보존
- type: test
path: packages/go/config/config_test.go
notes: config load/validation 검증
- type: test
path: apps/edge/internal/node/mapper_test.go
notes: provider-first adapter payload, Seulgivibe alias/provider label 검증
- type: test
path: apps/edge/internal/configrefresh/classify_test.go
notes: refresh classification 검증
@ -52,6 +58,8 @@ Edge 설정에서 provider-pool이 어떻게 모델 실행 후보를 고르고,
| Node config refresh push | 변경이 있으면 Edge가 연결된 Node에 node-specific `NodeConfigRefreshRequest`를 push한다. |
| Node registry swap | Node는 refresh payload로 새 adapter registry를 만들고 router registry를 swap한다. old registry stop은 active run이 있으면 drain 이후로 지연한다. |
| principal token mapping config | `openai.principal_tokens[]`는 raw token 없이 `token_ref`, `token_hash_sha256`, `principal_ref`, optional alias를 관리하고 OpenAI usage metering의 principal/token label 후보를 제공한다. 같은 principal에 여러 token entry를 둘 수 있다. |
| provider auth forwarding config | `openai.provider_auth`는 caller가 요청 header로 제공한 raw provider token을 selected OpenAI-compatible provider tunnel header로 전달하는 규칙만 저장한다. |
| Seulgivibe provider aliases | `seulgivibe_claude`, `seulgivibe_openai` provider type은 runtime adapter type을 `openai_compat`로 정규화하고, 명시 provider label이 없으면 canonical Seulgivibe alias를 Node payload provider label로 보존한다. |
## 범위
@ -94,6 +102,8 @@ sequenceDiagram
- `openai.principal_tokens[]``token_ref``token_hash_sha256` 중복을 거부하고, raw token 원문은 tracked config에 저장하지 않는다.
- 여러 `openai.principal_tokens[]` entry가 같은 `principal_ref`를 공유할 수 있으며, 이때 `token_ref`가 앱/통합/용도별 사용량 분해 기준이다.
- `openai.principal_tokens[]` 변경은 credential/hash 변경으로 보고 restart-required로 분류된다.
- `openai.provider_auth.enabled=true`이면 생략된 header fields는 `from_header=X-IOP-Provider-Authorization`, `target_header=Authorization`, `scheme=Bearer`, `required=true`로 해석된다. raw provider token 값은 config/spec/docs에 저장하지 않는다.
- Seulgivibe provider catalog는 top-level `models[]`의 정적 provider mapping을 source of truth로 사용한다. provider `/models` endpoint는 IOP catalog source가 아니다.
- refresh result는 changed nodes/providers/models와 restart-required paths를 stable non-nil slice로 보고한다.
## 검증
@ -113,9 +123,11 @@ sequenceDiagram
- `NodeRuntimeConfig.concurrency`는 legacy metadata이며 provider-pool admission의 node-wide capacity로 쓰지 않는다.
- credential, private endpoint, bearer token 원문은 tracked config/docs/spec에 남기지 않는다.
- principal token mapping은 완성된 사용자/테넌트 source of truth가 아니라 외부 `principal_ref` 또는 내부 alias에 대한 얇은 운영 매핑이다.
- Seulgivibe provider endpoint와 raw user token은 환경별 private config 또는 request-time header로 주입해야 하며 tracked 예시에 실제 값을 남기지 않는다.
## 변경 기록
- 2026-07-07: 현재 코드, 계약, config 예시 기준으로 bootstrap spec 작성.
- 2026-07-07: 기능 목록 중심으로 축소하고 주요 흐름을 Mermaid sequence diagram으로 정리.
- 2026-07-10: OpenAI usage metering용 principal token hash mapping config와 restart-required 기준을 반영.
- 2026-07-11: Seulgivibe OpenAI-compatible provider aliases, provider auth forwarding config, static catalog 기준을 현재 코드/계약/테스트 기준으로 반영.

View file

@ -0,0 +1,143 @@
<!-- task=m-model-group-mixed-provider-dispatch/01-provider-path-classifier plan=0 tag=MIXED_CLASSIFIER -->
# Code Review Reference - MIXED_CLASSIFIER
> **[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 selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close 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 only Milestone lock decisions 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-07-11
task=m-model-group-mixed-provider-dispatch/01-provider-path-classifier, plan=0, tag=MIXED_CLASSIFIER
## Roadmap Targets
- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- Task ids:
- `provider-path-classifier`: Provider classifier가 OpenAI-compatible 호출 방식을 지원하는 모든 provider를 passthrough 방식으로, Ollama/CLI/native provider를 normalized로 분류한다. Seulgivibe aliases는 passthrough provider에 포함한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G06.md``code_review_local_G06_N.log`, `PLAN-local-G06.md``plan_local_G06_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-model-group-mixed-provider-dispatch`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [MIXED_CLASSIFIER-1] Service Candidate Execution Path | [ ] |
| [MIXED_CLASSIFIER-2] Alias Regression Verification | [ ] |
## 구현 체크리스트
- [ ] provider 실행 path classifier를 service 후보 생성 경로에 추가하고 OpenAI-compatible aliases는 passthrough, Ollama/CLI/native는 normalized로 분류한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`.
- [ ] 기존 직접 수정인 `vllm-mlx`/Seulgivibe alias 정규화가 유지되는지 config, edge node mapper, node adapter smoke로 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1`, `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1`, `GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1`.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_local_G06_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_local_G06_M.log`로 아카이브한다.
- [ ] `.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-model-group-mixed-provider-dispatch/01-provider-path-classifier/``agent-task/archive/YYYY/MM/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-model-group-mixed-provider-dispatch`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-model-group-mixed-provider-dispatch/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md``CODE_REVIEW-local-G06.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가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `candidateNode`에 실행 path가 기록되고 provider-pool 후보 생성에서 빠짐없이 설정되는지 확인한다.
- OpenAI-compatible alias 목록이 config, mapper, service classifier에서 어긋나지 않는지 확인한다.
- Ollama/CLI/native 후보가 tunnel path로 분류되지 않는지 확인한다.
- `apps/node/internal/adapters`는 불필요한 구현 변경 없이 smoke 검증만 했는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
### MIXED_CLASSIFIER-1 중간 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
(output)
```
### MIXED_CLASSIFIER-2 중간 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1
(output)
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1
(output)
$ GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1
(output)
```
### 최종 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1
(output)
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1
(output)
$ GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1
(output)
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
(output)
```
---
> **[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.

View file

@ -0,0 +1,268 @@
<!-- task=m-model-group-mixed-provider-dispatch/01-provider-path-classifier plan=0 tag=MIXED_CLASSIFIER -->
# PLAN-local-G06: Provider Path Classifier
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 마지막 필수 단계다. 검증을 실행하고, active plan/review 파일을 유지한 채 리뷰 준비 상태를 보고한다. 종결 처리, log rename, `complete.log`, archive 이동은 code-review 에이전트 전용이다.
선택된 Milestone의 `구현 잠금 > 결정 필요` 항목이 실제 구현을 막는 경우에만 대응 review stub의 `사용자 리뷰 요청` 섹션에 연결 근거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/외부 서비스 준비, 일반 scope 조정, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 follow-up plan으로 남긴다.
## 배경
현재 provider-pool 후보에는 provider별 실행 방식이 보존되지 않아 OpenAI handler가 catalog match만 보고 tunnel 여부를 먼저 결정한다. SDD는 OpenAI-compatible 계열은 passthrough, Ollama/CLI/native 계열은 normalized로 분류한 뒤 선택된 provider에 맞춰 실행하라고 요구한다. `vllm-mlx` alias와 provider label 정규화는 작은 직접 수정으로 이미 반영되었으므로, 이 계획은 service 후보까지 classifier metadata를 보존하고 검증하는 남은 큰 단위를 다룬다.
## 사용자 리뷰 요청 흐름
사용자 리뷰 요청은 선택된 Milestone lock 결정이 구현을 차단할 때만 active `CODE_REVIEW-*-G??.md``사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 에이전트는 사용자에게 직접 묻지 않고, code-review 에이전트가 요청 타당성을 검증한 뒤 필요할 때만 실제 `USER_REVIEW.md`를 작성한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- Task ids:
- `provider-path-classifier`: Provider classifier가 OpenAI-compatible 호출 방식을 지원하는 모든 provider를 passthrough 방식으로, Ollama/CLI/native provider를 normalized로 분류한다. Seulgivibe aliases는 passthrough provider에 포함한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/analyze-roadmap-position/SKILL.md`
- `agent-ops/skills/common/_templates/roadmap-position-report-template.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md`
- `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/mapper_test.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/model_queue_test.go`
- `apps/edge/internal/service/run_dispatch_internal_test.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/config_set_test.go`
- `apps/node/internal/adapters/factory_internal_test.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`
### SDD 기준
- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md`
- 상태: `[승인됨]`, SDD lock 해제
- 대상 Acceptance Scenario: S03, S04
- Milestone Task id: `provider-path-classifier`
- Evidence Map:
- S03: OpenAI-compatible alias classifier 테스트. `go test ./packages/go/config -count=1`, `go test ./apps/edge/internal/node -count=1`, `go test ./apps/node/internal/adapters -count=1`.
- S04: Ollama/CLI/native provider가 normalized 후보로만 분류되는 service/provider classifier 테스트.
- 이 계획은 config/edge-node mapper alias 보강을 유지하고, service 후보 `candidateNode`에 실행 path metadata를 추가해 다음 selection-first plan이 선택 결과만 보고 path를 결정할 수 있게 한다.
### 테스트 환경 규칙
- 선택 test_env: `local`
- `agent-test/local/rules.md``edge-smoke`, `node-smoke`, `platform-common-smoke` 프로필을 읽었다.
- 기본 Go build cache가 `/config/.cache/go-build` permission 오류를 냈으므로 현재 checkout에서는 `GOCACHE=/tmp/iop-go-cache`를 붙여 실행한다. 최종 검증은 cached output으로 대체하지 않는다.
- 적용 명령:
- `GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1`
- `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1`
- `GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1`
- `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`
### 테스트 커버리지 공백
- 이미 반영된 작은 수정은 `TestNormalizeProviderTypeOpenAICompatibleAliases``TestBuildConfigPayload_ProviderFirstOpenAICompatDefaultsProviderFromType`가 alias/label을 검증한다.
- service 후보에는 아직 OpenAI-compatible vs normalized 실행 path assertion이 없다.
- node adapter registry는 provider type을 보존하지만, provider classifier 자체는 Edge service 후보에 있어야 하므로 adapter package는 회귀 smoke로만 둔다.
### 심볼 참조
- 기존 symbol rename/remove 없음.
- 새 symbol을 만들 경우 unexported classifier type/function으로 제한하고, call site는 `resolveProviderPoolCandidates`와 관련 service tests로 한정한다.
### 분할 판단
- split policy를 평가했고 multi-plan을 선택했다.
- 공유 task group: `agent-task/m-model-group-mixed-provider-dispatch/`
- sibling subtask:
- `01-provider-path-classifier`: classifier metadata foundation. 선행 의존성 없음.
- `02-selection-first-provider-dispatch`: 이 plan의 PASS 후 실행해야 한다. 현재 active plan/review가 있고 `complete.log`는 아직 없다.
- `03-mixed-model-group-openai`: 02 PASS 후 실행해야 한다.
### 범위 결정 근거
- `model-group-surface`, `contract-spec` milestone tasks는 SDD S06-S10 범위라 제외한다.
- OpenAI handler의 selection-first 통합은 02/03 plan으로 넘긴다. 이 plan은 classifier metadata와 테스트 증거까지만 다룬다.
- `agent-task/archive/**`, `agent-roadmap/archive/**`, `agent-spec/archive/**`는 읽지 않는다.
### 빌드 등급
- `local-G06`: config, edge node mapper, service 후보 metadata, node adapter smoke를 함께 보지만 API shape 변경 없이 bounded Go unit test로 검증 가능하다.
## 구현 체크리스트
- [ ] provider 실행 path classifier를 service 후보 생성 경로에 추가하고 OpenAI-compatible aliases는 passthrough, Ollama/CLI/native는 normalized로 분류한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`.
- [ ] 기존 직접 수정인 `vllm-mlx`/Seulgivibe alias 정규화가 유지되는지 config, edge node mapper, node adapter smoke로 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1`, `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1`, `GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1`.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [MIXED_CLASSIFIER-1] Service Candidate Execution Path
#### 문제
`candidateNode`는 provider-pool 후보의 `providerID`, `adapter`, `servedTarget`만 들고 있으며 실행 path를 보존하지 않는다 ([apps/edge/internal/service/model_queue.go](/config/workspace/iop/apps/edge/internal/service/model_queue.go:28)). `resolveProviderPoolCandidates`도 provider type을 보고 후보를 만들지만 path 분류를 candidate에 싣지 않는다 ([apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:562)).
#### 해결 방법
service package에 unexported execution path type과 classifier helper를 둔다. classifier는 `config.NormalizeProviderType`를 사용해 OpenAI-compatible aliases를 tunnel/passthrough로, `ollama``cli` 및 unknown/native를 normalized로 분류한다.
Before:
```go
type candidateNode struct {
providerID string
adapter string
servedTarget string
}
```
After:
```go
type providerExecutionPath string
const (
providerExecutionPathTunnel providerExecutionPath = "provider_tunnel"
providerExecutionPathNormalized providerExecutionPath = "normalized"
)
type candidateNode struct {
providerID string
adapter string
servedTarget string
executionPath providerExecutionPath
}
```
`resolveProviderPoolCandidates`에서 `executionPath: classifyProviderExecutionPath(prov)`를 설정한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/service/model_queue.go`: `candidateNode`에 execution path 필드와 주석 추가.
- [ ] `apps/edge/internal/service/run_dispatch.go`: classifier helper 추가 및 후보 생성 시 설정.
- [ ] `apps/edge/internal/service/model_queue_test.go` 또는 `run_dispatch_internal_test.go`: OpenAI-compatible aliases와 Ollama/CLI/native 분류 assertion 추가.
#### 테스트 작성
- 작성: `TestResolveProviderPoolCandidatesClassifiesExecutionPath`
- 목표: `openai_compat`, `openai_api`, `vllm`, `vllm-mlx`, `lemonade`, `sglang`, `seulgivibe_claude`, `seulgivibe_openai`은 tunnel path, `ollama`, `cli`, unknown/native는 normalized path로 후보에 기록된다.
#### 중간 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
```
기대 결과: PASS.
### [MIXED_CLASSIFIER-2] Alias Regression Verification
#### 문제
SDD는 `vllm-mlx`와 Seulgivibe aliases를 OpenAI-compatible provider에 포함한다. 직접 수정으로 `NormalizeProviderType``vllm-mlx``openai_compat`로 매핑하고 ([packages/go/config/config.go](/config/workspace/iop/packages/go/config/config.go:27)), mapper label도 lowercase/trim 후 aliases를 반환한다 ([apps/edge/internal/node/mapper.go](/config/workspace/iop/apps/edge/internal/node/mapper.go:239)). 이 상태가 후속 구현 중 깨지지 않도록 명시 검증을 유지해야 한다.
#### 해결 방법
이미 추가된 테스트를 유지하고 필요 시 classifier helper의 alias 목록과 동일한 표본을 맞춘다.
Before:
```go
case "openai_compat", "openai_api", "vllm", "lemonade", "sglang":
return "openai_compat"
```
After:
```go
case "openai_compat", "openai_api", "vllm", "vllm-mlx", "lemonade", "sglang", "seulgivibe_claude", "seulgivibe_openai":
return "openai_compat"
```
#### 수정 파일 및 체크리스트
- [ ] `packages/go/config/config.go`: alias 목록 유지.
- [ ] `packages/go/config/config_test.go`: `TestNormalizeProviderTypeOpenAICompatibleAliases` 유지 ([packages/go/config/config_test.go](/config/workspace/iop/packages/go/config/config_test.go:358)).
- [ ] `apps/edge/internal/node/mapper.go`: `openAICompatProviderLabel` 정규화 유지.
- [ ] `apps/edge/internal/node/mapper_test.go`: provider-first label cases 유지 ([apps/edge/internal/node/mapper_test.go](/config/workspace/iop/apps/edge/internal/node/mapper_test.go:586)).
- [ ] `apps/node/internal/adapters/*`: 직접 변경하지 않되 package smoke를 실행한다.
#### 테스트 작성
- 추가 테스트가 필요한 경우 service classifier test와 중복되지 않는 config/mapper alias case만 추가한다.
- node adapters는 BuildFromPayload/registry behavior 회귀 검증만 실행한다.
#### 중간 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1
```
기대 결과: 모두 PASS.
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `apps/edge/internal/service/model_queue.go` | MIXED_CLASSIFIER-1 |
| `apps/edge/internal/service/run_dispatch.go` | MIXED_CLASSIFIER-1 |
| `apps/edge/internal/service/model_queue_test.go` | MIXED_CLASSIFIER-1 |
| `apps/edge/internal/service/run_dispatch_internal_test.go` | MIXED_CLASSIFIER-1 |
| `packages/go/config/config.go` | MIXED_CLASSIFIER-2 |
| `packages/go/config/config_test.go` | MIXED_CLASSIFIER-2 |
| `apps/edge/internal/node/mapper.go` | MIXED_CLASSIFIER-2 |
| `apps/edge/internal/node/mapper_test.go` | MIXED_CLASSIFIER-2 |
| `apps/node/internal/adapters/config_set.go` | MIXED_CLASSIFIER-2 검증 |
| `apps/node/internal/adapters/factory.go` | MIXED_CLASSIFIER-2 검증 |
| `apps/node/internal/adapters/registry.go` | MIXED_CLASSIFIER-2 검증 |
## 최종 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./packages/go/config -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/node -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/node/internal/adapters -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
```
기대 결과: 모든 명령 PASS.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,137 @@
<!-- task=m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch plan=0 tag=MIXED_SELECT -->
# Code Review Reference - MIXED_SELECT
> **[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 selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close 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 only Milestone lock decisions 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-07-11
task=m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch, plan=0, tag=MIXED_SELECT
## Roadmap Targets
- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- Task ids:
- `selection-first-path`: Provider-pool route가 provider 후보를 먼저 선택하고 같은 queue slot/lease로 ProviderTunnelRequest 또는 normalized RunRequest 중 하나를 실행한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G07.md``code_review_local_G07_N.log`, `PLAN-local-G07.md``plan_local_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-model-group-mixed-provider-dispatch`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [MIXED_SELECT-1] One-Shot Provider Pool Dispatch In Service | [ ] |
| [MIXED_SELECT-2] OpenAI Chat Uses Selection Result | [ ] |
## 구현 체크리스트
- [ ] 01-provider-path-classifier PASS/complete 여부를 확인하고, 미완료면 이 plan 구현을 시작하지 않는다.
- [ ] service에 provider-pool one-shot selection dispatch를 추가해 동일 admission slot으로 selected execution path에 따라 `RunRequest` 또는 `ProviderTunnelRequest` 중 하나만 전송한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`.
- [ ] OpenAI Chat provider-pool default path가 catalog match만으로 tunnel을 고르지 않고 service selection 결과를 따르도록 통합한다. Ollama-selected provider-pool 요청은 tunnel 없이 normalized `SubmitRun`만 호출된다는 테스트를 추가한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1`.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_local_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_local_G07_M.log`로 아카이브한다.
- [ ] `.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-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/``agent-task/archive/YYYY/MM/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-model-group-mixed-provider-dispatch`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-model-group-mixed-provider-dispatch/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G07.md``CODE_REVIEW-local-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가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- provider-pool selection이 정확히 한 번만 queue admission을 수행하는지 확인한다.
- selected path가 normalized일 때 ProviderTunnelRequest가 생성/전송되지 않는지 확인한다.
- selected path가 tunnel일 때 RunRequest가 생성/전송되지 않는지 확인한다.
- slot release가 RunEvent terminal과 ProviderTunnel END/ERROR/Close에서 기존과 같이 정확히 한 번 일어나는지 확인한다.
- legacy direct `openai_compat`/`vllm` route behavior가 불필요하게 깨지지 않았는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
### MIXED_SELECT-1 중간 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
(output)
```
### MIXED_SELECT-2 중간 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
(output)
```
### 최종 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
(output)
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
(output)
```
---
> **[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.

View file

@ -0,0 +1,276 @@
<!-- task=m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch plan=0 tag=MIXED_SELECT -->
# PLAN-local-G07: Selection-First Provider Dispatch
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 마지막 필수 단계다. 검증을 실행하고, active plan/review 파일을 유지한 채 리뷰 준비 상태를 보고한다. 종결 처리, log rename, `complete.log`, archive 이동은 code-review 에이전트 전용이다.
선택된 Milestone의 `구현 잠금 > 결정 필요` 항목이 실제 구현을 막는 경우에만 대응 review stub의 `사용자 리뷰 요청` 섹션에 연결 근거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/외부 서비스 준비, 일반 scope 조정, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 follow-up plan으로 남긴다.
## 배경
현재 Chat/Responses 핸들러는 catalog match인 provider-pool route를 `routeUsesProviderTunnel`에서 즉시 tunnel로 판단한다. service는 이미 `SubmitRun``SubmitProviderTunnel` 각각에서 queue admission과 slot release를 지원하지만, mixed group에서는 provider를 먼저 하나 선택한 뒤 그 provider의 path로만 실행해야 한다. 이 계획은 01 classifier plan의 execution path metadata를 사용해 같은 queue slot/lease에서 normalized RunRequest 또는 ProviderTunnelRequest 중 하나만 보내는 service-level dispatch를 만든다.
## 사용자 리뷰 요청 흐름
사용자 리뷰 요청은 선택된 Milestone lock 결정이 구현을 차단할 때만 active `CODE_REVIEW-*-G??.md``사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 에이전트는 사용자에게 직접 묻지 않고, code-review 에이전트가 요청 타당성을 검증한 뒤 필요할 때만 실제 `USER_REVIEW.md`를 작성한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- Task ids:
- `selection-first-path`: Provider-pool route가 provider 후보를 먼저 선택하고 같은 queue slot/lease로 ProviderTunnelRequest 또는 normalized RunRequest 중 하나를 실행한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/analyze-roadmap-position/SKILL.md`
- `agent-ops/skills/common/_templates/roadmap-position-report-template.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md`
- `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/mapper_test.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/model_queue_test.go`
- `apps/edge/internal/service/run_dispatch_internal_test.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/config_set_test.go`
- `apps/node/internal/adapters/factory_internal_test.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`
### SDD 기준
- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md`
- 상태: `[승인됨]`, SDD lock 해제
- 대상 Acceptance Scenario: S01, S02
- Milestone Task id: `selection-first-path`
- Evidence Map:
- S01: service queue/selection unit tests, `go test ./apps/edge/internal/service -count=1`.
- S02: edge service/OpenAI handler tests proving Ollama-selected provider-pool dispatch does not use tunnel.
- 이 계획은 service에서 one-shot provider selection을 만들고 OpenAI handler가 catalog match만으로 tunnel을 고르지 않게 해 S01/S02 증거를 만든다.
### 테스트 환경 규칙
- 선택 test_env: `local`
- `agent-test/local/rules.md`, `edge-smoke`, `node-smoke`, `platform-common-smoke`를 읽었다.
- 기본 Go build cache permission 오류가 있었으므로 `GOCACHE=/tmp/iop-go-cache`를 사용한다. cached output으로 최종 검증을 대체하지 않는다.
- 적용 명령:
- `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`
- `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1`
### 테스트 커버리지 공백
- service tests는 현재 `SubmitRun` provider-pool과 `SubmitProviderTunnel` provider-pool을 별도로 검증한다 ([apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:121), [apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:872)).
- 같은 admission 결과를 가지고 path에 따라 둘 중 하나만 실행하는 통합 surface가 없다.
- OpenAI tests는 provider-pool default가 tunnel이라고 기대하는 기존 테스트가 있어 selection-first 기대값으로 바꿔야 한다 ([apps/edge/internal/openai/server_test.go](/config/workspace/iop/apps/edge/internal/openai/server_test.go:4116)).
### 심볼 참조
- 기존 exported symbol 제거는 피한다.
- 새 service method를 추가할 경우 `openai.runService` interface와 `fakeRunService` call site를 함께 갱신한다 ([apps/edge/internal/openai/server.go](/config/workspace/iop/apps/edge/internal/openai/server.go:18)).
- `routeUsesProviderTunnel`는 legacy direct provider route 판정으로 좁히거나 provider-pool을 제외하도록 변경할 수 있다 ([apps/edge/internal/openai/chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:353)).
### 분할 판단
- split policy를 평가했고 multi-plan을 선택했다.
- 공유 task group: `agent-task/m-model-group-mixed-provider-dispatch/`
- 선행 의존성:
- predecessor 01: `agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/`; active plan/review 존재, `complete.log` 없음. 이 plan은 01 PASS 후 실행한다.
- 후속 의존성:
- `03-mixed-model-group-openai`는 이 plan PASS 후 실행한다.
### 범위 결정 근거
- 이 plan은 selection-first mechanics와 S01/S02 evidence까지만 다룬다.
- S05 mixed fixture matrix와 broader Chat/Responses passthrough byte-identity 회귀 조정은 03 plan으로 넘긴다.
- provider registry/config alias 보강은 01 plan 범위로 본다.
### 빌드 등급
- `local-G07`: service admission/lease, provider tunnel, normalized run, OpenAI handler interface를 함께 건드리는 cross-module 변경이지만 local Go tests로 닫을 수 있다.
## 구현 체크리스트
- [ ] 01-provider-path-classifier PASS/complete 여부를 확인하고, 미완료면 이 plan 구현을 시작하지 않는다.
- [ ] service에 provider-pool one-shot selection dispatch를 추가해 동일 admission slot으로 selected execution path에 따라 `RunRequest` 또는 `ProviderTunnelRequest` 중 하나만 전송한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`.
- [ ] OpenAI Chat provider-pool default path가 catalog match만으로 tunnel을 고르지 않고 service selection 결과를 따르도록 통합한다. Ollama-selected provider-pool 요청은 tunnel 없이 normalized `SubmitRun`만 호출된다는 테스트를 추가한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1`.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
1. `01-provider-path-classifier`가 PASS되어 candidate execution path가 존재하는지 확인한다.
2. service one-shot selection helper/result type을 만든다.
3. 기존 `SubmitRun`/`SubmitProviderTunnel` queued path를 helper 재사용으로 정리해 slot release semantics가 유지되는지 검증한다.
4. OpenAI Chat handler와 fake service를 새 selection-first surface에 맞춘다.
### [MIXED_SELECT-1] One-Shot Provider Pool Dispatch In Service
#### 문제
`SubmitRun`은 provider-pool일 때 `submitRunQueued`에서 admission을 하고 selected provider로 RunRequest를 보낸다 ([apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:136)). `SubmitProviderTunnel`도 별도 admission을 하고 selected provider로 ProviderTunnelRequest를 보낸다 ([apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:879)). 호출자가 path를 먼저 골라 두 경로 중 하나를 호출해야 하므로, mixed provider group에서는 provider 선택 전에 path가 결정되는 문제가 남는다.
#### 해결 방법
provider-pool 전용 one-shot surface를 추가한다. 구현명은 codebase 스타일에 맞춰 조정할 수 있지만, 핵심은 `resolveProviderPoolCandidates``queue.admitWithReason`을 정확히 한 번 호출하고 selected candidate의 `executionPath`로만 분기하는 것이다.
Before:
```go
if routeUsesProviderTunnel(dispatch) {
return service.SubmitProviderTunnel(ctx, tunnelReq)
}
return service.SubmitRun(ctx, runReq)
```
After:
```go
result, err := service.SubmitProviderPool(ctx, ProviderPoolDispatchRequest{
Run: runReq,
Tunnel: tunnelReq,
})
switch result.Path {
case ProviderPoolPathTunnel:
return result.Tunnel
case ProviderPoolPathNormalized:
return result.Run
}
```
Factor shared helpers so existing `SubmitRun` and `SubmitProviderTunnel` keep behavior but use the same selected-candidate dispatch internals where practical.
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/service/run_dispatch.go`: provider-pool dispatch request/result type, one-shot method, helper extraction.
- [ ] `apps/edge/internal/service/model_queue.go`: no release semantic regression; only touch if helper requires small type additions.
- [ ] `apps/edge/internal/service/run_dispatch_internal_test.go`: tunnel and normalized one-shot tests.
- [ ] `apps/edge/internal/service/model_queue_test.go`: candidate/path selection edge tests if not covered in internal test.
#### 테스트 작성
- 작성: `TestSubmitProviderPoolSelectsTunnelProviderAndReleasesSlot`
- 작성: `TestSubmitProviderPoolSelectsNormalizedProviderAndDoesNotOpenTunnel`
- 작성: `TestSubmitProviderPoolUsesSingleAdmissionForMixedCandidates`
- assertion: selected provider의 adapter/target rewrite, sent protobuf type, queue inflight release, non-selected path not sent.
#### 중간 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
```
기대 결과: PASS.
### [MIXED_SELECT-2] OpenAI Chat Uses Selection Result
#### 문제
`handleChatCompletions``providerTunnelRoute := routeUsesProviderTunnel(dispatch)`를 먼저 계산하고 provider-pool이면 바로 passthrough/tunnel branch로 들어간다 ([apps/edge/internal/openai/chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:67)). `routeUsesProviderTunnel``d.ProviderPool`이면 무조건 true를 반환한다 ([apps/edge/internal/openai/chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:357)). 그래서 catalog에 Ollama provider만 있어도 tunnel path로 고정된다.
#### 해결 방법
provider-pool route는 legacy direct provider route와 분리한다. catalog match는 provider-pool selection request를 만들고, service 결과가 tunnel이면 기존 passthrough writer를 사용하고 normalized면 기존 RunResult completion/stream path를 사용한다. legacy `openai_compat`/`vllm` direct route는 기존 tunnel default를 유지한다.
Before:
```go
func routeUsesProviderTunnel(d routeDispatch) bool {
if d.ProviderPool {
return true
}
return d.Adapter == "openai_compat" || d.Adapter == "vllm"
}
```
After:
```go
func routeUsesProviderTunnel(d routeDispatch) bool {
if d.ProviderPool {
return false
}
return d.Adapter == "openai_compat" || d.Adapter == "vllm"
}
```
Provider-pool handling should explicitly call the new service selection method before choosing response writer.
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/openai/server.go`: `runService` interface에 selection-first method 추가.
- [ ] `apps/edge/internal/openai/chat_handler.go`: provider-pool branch를 selection-first로 전환.
- [ ] `apps/edge/internal/openai/server_test.go`: `fakeRunService`에 selection-first method 및 fixtures 추가.
- [ ] 기존 provider-pool default tunnel tests를 selected tunnel provider 기대값으로 조정.
#### 테스트 작성
- 작성: `TestChatCompletionsProviderPoolOllamaSelectionUsesNormalizedRun`
- 작성: `TestChatCompletionsProviderPoolTunnelSelectionUsesPassthrough`
- assertion: Ollama-selected fixture는 `len(tunnelReqs)==0`, `len(reqs)==1`, `ProviderPool=true`; tunnel-selected fixture는 반대.
#### 중간 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
```
기대 결과: PASS.
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `apps/edge/internal/service/run_dispatch.go` | MIXED_SELECT-1 |
| `apps/edge/internal/service/model_queue.go` | MIXED_SELECT-1 |
| `apps/edge/internal/service/run_dispatch_internal_test.go` | MIXED_SELECT-1 |
| `apps/edge/internal/service/model_queue_test.go` | MIXED_SELECT-1 |
| `apps/edge/internal/openai/server.go` | MIXED_SELECT-2 |
| `apps/edge/internal/openai/chat_handler.go` | MIXED_SELECT-2 |
| `apps/edge/internal/openai/server_test.go` | MIXED_SELECT-2 |
## 최종 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
```
기대 결과: 모든 명령 PASS.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,138 @@
<!-- task=m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai plan=0 tag=MIXED_GROUP -->
# Code Review Reference - MIXED_GROUP
> **[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 selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close 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 only Milestone lock decisions 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-07-11
task=m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai, plan=0, tag=MIXED_GROUP
## Roadmap Targets
- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- Task ids:
- `mixed-model-group`: 같은 model group 안의 vLLM/vLLM-MLX/Lemonade/openweight cloud provider와 Ollama provider가 모두 후보로 남고, 선택된 provider별로 tunnel 또는 normalized path가 실행된다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G06.md``code_review_local_G06_N.log`, `PLAN-local-G06.md``plan_local_G06_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-model-group-mixed-provider-dispatch`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [MIXED_GROUP-1] Service Mixed Candidate Retention | [ ] |
| [MIXED_GROUP-2] OpenAI Mixed/Ollama-Only Fixtures | [ ] |
| [MIXED_GROUP-3] Existing Passthrough Fixture Alignment | [ ] |
## 구현 체크리스트
- [ ] 01-provider-path-classifier와 02-selection-first-provider-dispatch PASS/complete 여부를 확인하고, 미완료면 이 plan 구현을 시작하지 않는다.
- [ ] service test에 mixed provider group 후보 retention fixture를 추가해 OpenAI-compatible provider와 Ollama provider가 같은 model group 후보로 모두 남고 각 execution path가 유지됨을 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`.
- [ ] OpenAI server test에 mixed group과 Ollama-only group fixtures를 추가해 selected OpenAI-compatible provider는 tunnel, selected Ollama provider는 normalized path를 실행함을 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1`.
- [ ] 기존 provider-pool passthrough tests가 selection-first 이후에도 tunnel-selected provider fixture임을 명확히 하도록 helper/expectation을 정리한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_local_G06_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_local_G06_M.log`로 아카이브한다.
- [ ] `.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-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/``agent-task/archive/YYYY/MM/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-model-group-mixed-provider-dispatch`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-model-group-mixed-provider-dispatch/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md``CODE_REVIEW-local-G06.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가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- mixed service fixture가 OpenAI-compatible provider와 Ollama provider를 모두 후보로 유지하는지 확인한다.
- selected OpenAI-compatible provider는 tunnel path, selected Ollama provider는 normalized path를 실행하는지 확인한다.
- Ollama-only provider-pool group이 tunnel을 열지 않는지 확인한다.
- 기존 passthrough byte-identity, sideband, provider auth, cancel/write failure tests가 tunnel-selected provider fixture로 의미를 유지하는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
### MIXED_GROUP-1 중간 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
(output)
```
### MIXED_GROUP-2/3 중간 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
(output)
```
### 최종 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
(output)
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
(output)
```
---
> **[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.

View file

@ -0,0 +1,300 @@
<!-- task=m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai plan=0 tag=MIXED_GROUP -->
# PLAN-local-G06: Mixed Model Group OpenAI Fixtures
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 마지막 필수 단계다. 검증을 실행하고, active plan/review 파일을 유지한 채 리뷰 준비 상태를 보고한다. 종결 처리, log rename, `complete.log`, archive 이동은 code-review 에이전트 전용이다.
선택된 Milestone의 `구현 잠금 > 결정 필요` 항목이 실제 구현을 막는 경우에만 대응 review stub의 `사용자 리뷰 요청` 섹션에 연결 근거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/외부 서비스 준비, 일반 scope 조정, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 follow-up plan으로 남긴다.
## 배경
01/02 plan이 provider path classifier와 selection-first dispatch를 제공하면, 마지막으로 mixed model group이 실제 OpenAI surface에서 두 provider 종류를 모두 후보로 유지하고 선택 결과에 따라 다른 path를 실행한다는 fixture 증거가 필요하다. 현재 OpenAI tests는 provider-pool catalog match를 tunnel default로 보는 기존 기대값이 많아, mixed/Ollama-only group이 normalized path를 타는 회귀 증거가 부족하다. 이 계획은 S05 acceptance를 닫기 위한 OpenAI/service 테스트 matrix를 추가한다.
## 사용자 리뷰 요청 흐름
사용자 리뷰 요청은 선택된 Milestone lock 결정이 구현을 차단할 때만 active `CODE_REVIEW-*-G??.md``사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 에이전트는 사용자에게 직접 묻지 않고, code-review 에이전트가 요청 타당성을 검증한 뒤 필요할 때만 실제 `USER_REVIEW.md`를 작성한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- Task ids:
- `mixed-model-group`: 같은 model group 안의 vLLM/vLLM-MLX/Lemonade/openweight cloud provider와 Ollama provider가 모두 후보로 남고, 선택된 provider별로 tunnel 또는 normalized path가 실행된다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/analyze-roadmap-position/SKILL.md`
- `agent-ops/skills/common/_templates/roadmap-position-report-template.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md`
- `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/mapper_test.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/model_queue_test.go`
- `apps/edge/internal/service/run_dispatch_internal_test.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/config_set_test.go`
- `apps/node/internal/adapters/factory_internal_test.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`
### SDD 기준
- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md`
- 상태: `[승인됨]`, SDD lock 해제
- 대상 Acceptance Scenario: S05
- Milestone Task id: `mixed-model-group`
- Evidence Map:
- S05: mixed and Ollama-only model group fixtures.
- 검증: `go test ./apps/edge/internal/openai -count=1`, `go test ./apps/edge/internal/service -count=1`.
- 이 계획은 service candidate retention과 OpenAI surface path selection fixture를 모두 추가해 S05를 닫는다.
### 테스트 환경 규칙
- 선택 test_env: `local`
- `agent-test/local/rules.md`, `edge-smoke`, `node-smoke`, `platform-common-smoke`를 읽었다.
- 기본 Go build cache permission 오류가 있었으므로 `GOCACHE=/tmp/iop-go-cache`를 사용한다. cached output으로 최종 검증을 대체하지 않는다.
- 적용 명령:
- `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`
- `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1`
### 테스트 커버리지 공백
- service candidate tests는 disabled/provider-first 일부를 다루지만, 같은 catalog model group 안에 OpenAI-compatible provider와 Ollama provider가 함께 남는 S05 fixture가 없다.
- OpenAI tests는 `TestChatCompletionsProviderPoolDispatch` 등에서 tunnel default를 전제로 한다 ([apps/edge/internal/openai/server_test.go](/config/workspace/iop/apps/edge/internal/openai/server_test.go:4116)).
- `fakeRunService`는 tunnel/normalized 호출 기록은 있지만, selection-first 결과를 mixed group case별로 명시하는 fixture field가 필요하다 ([apps/edge/internal/openai/server_test.go](/config/workspace/iop/apps/edge/internal/openai/server_test.go:25)).
### 심볼 참조
- 기존 exported symbol 제거는 피한다.
- 02 plan에서 추가된 selection-first service method와 fake service extension을 재사용한다.
- 새 test helper가 필요하면 `server_test.go` 내부 helper로 한정한다.
### 분할 판단
- split policy를 평가했고 multi-plan을 선택했다.
- 공유 task group: `agent-task/m-model-group-mixed-provider-dispatch/`
- 선행 의존성:
- predecessor 01: active plan/review 존재, `complete.log` 없음.
- predecessor 02: active plan/review 존재, `complete.log` 없음.
- 이 plan은 01과 02가 PASS된 뒤 실행해야 한다. 선행 complete.log가 없으면 구현을 시작하지 않는다.
### 범위 결정 근거
- `/v1/responses` provider-pool behavior는 02에서 shared helper 변화에 따른 회귀 검증 대상일 수 있지만, S05 completion fixture는 Chat/OpenAI server tests와 service tests에 집중한다.
- provider auth, sideband byte-identity, write failure/cancel timeout 등 기존 passthrough semantics는 기존 tests를 통과시키는 회귀 범위로만 다룬다.
- contract/spec 문서 갱신은 이 milestone의 `contract-spec` task 범위라 제외한다.
### 빌드 등급
- `local-G06`: 선행 service/interface 변경 위에 test matrix와 bounded handler adjustments를 추가하는 작업이며 local Go package tests로 검증 가능하다.
## 구현 체크리스트
- [ ] 01-provider-path-classifier와 02-selection-first-provider-dispatch PASS/complete 여부를 확인하고, 미완료면 이 plan 구현을 시작하지 않는다.
- [ ] service test에 mixed provider group 후보 retention fixture를 추가해 OpenAI-compatible provider와 Ollama provider가 같은 model group 후보로 모두 남고 각 execution path가 유지됨을 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1`.
- [ ] OpenAI server test에 mixed group과 Ollama-only group fixtures를 추가해 selected OpenAI-compatible provider는 tunnel, selected Ollama provider는 normalized path를 실행함을 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1`.
- [ ] 기존 provider-pool passthrough tests가 selection-first 이후에도 tunnel-selected provider fixture임을 명확히 하도록 helper/expectation을 정리한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
1. 01/02 plan의 PASS `complete.log` 존재 여부를 확인한다.
2. service mixed candidate retention test를 먼저 추가해 S05 후보 조건을 고정한다.
3. OpenAI fake service selection fixture를 이용해 tunnel-selected와 normalized-selected cases를 추가한다.
4. 기존 passthrough tests가 실패하면 provider selection fixture를 tunnel로 명시해 기존 byte-identity assertions가 계속 같은 의미를 갖게 한다.
### [MIXED_GROUP-1] Service Mixed Candidate Retention
#### 문제
`resolveProviderPoolCandidates`는 catalog providers를 순회하며 후보를 만든다 ([apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:596)). 하지만 S05처럼 vLLM/vLLM-MLX/Lemonade/openweight cloud provider와 Ollama provider가 같은 model group에 있을 때 둘 다 후보로 남는지, path metadata가 다른지 직접 증명하는 test가 없다.
#### 해결 방법
service test fixture를 추가한다. catalog model `mixed-model``prov-vllm`, `prov-vllm-mlx`, `prov-lemonade`, `prov-ollama`를 넣고, node store의 provider configs에는 각 provider의 served model, health, capacity를 모두 valid로 둔다. resolver 결과에서 provider ids가 모두 존재하고 OpenAI-compatible 계열은 tunnel path, Ollama는 normalized path인지 assertion한다.
Before:
```go
candidates, _, err := svc.resolveProviderPoolCandidates(req, store, catalog)
// existing tests assert disabled/provider-first subsets only
```
After:
```go
candidates, _, err := svc.resolveProviderPoolCandidates(req, store, catalog)
assertCandidate("prov-vllm", providerExecutionPathTunnel)
assertCandidate("prov-ollama", providerExecutionPathNormalized)
```
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/service/model_queue_test.go` 또는 `run_dispatch_internal_test.go`: mixed candidate retention test 추가.
- [ ] 필요 시 helper 함수로 candidates by providerID map 생성.
#### 테스트 작성
- 작성: `TestResolveProviderPoolCandidatesKeepsMixedProviderTypes`
- assertion: all valid providers remain, served target and adapter key are preserved, execution path differs by provider type.
#### 중간 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
```
기대 결과: PASS.
### [MIXED_GROUP-2] OpenAI Mixed/Ollama-Only Fixtures
#### 문제
OpenAI server tests currently record tunnel requests and run requests separately but provider-pool tests assume catalog match implies tunnel ([apps/edge/internal/openai/server_test.go](/config/workspace/iop/apps/edge/internal/openai/server_test.go:77), [apps/edge/internal/openai/server_test.go](/config/workspace/iop/apps/edge/internal/openai/server_test.go:4116)). S05 needs selected-provider-specific behavior.
#### 해결 방법
02 plan의 selection-first fake service hook을 사용해 three cases를 추가한다.
Before:
```go
catalog := []config.ModelCatalogEntry{{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}}}
// request always expects tunnel
```
After:
```go
catalog := []config.ModelCatalogEntry{{
ID: "mixed-model",
Providers: map[string]string{"prov-vllm": "served-vllm", "prov-ollama": "served-ollama"},
}}
fake.selectProviderPath = normalized // or tunnel
```
Cases:
- mixed group, selected tunnel provider: one tunnel request, no normalized run.
- mixed group, selected Ollama provider: one normalized run, no tunnel request.
- Ollama-only model group: normalized run, no tunnel request.
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/openai/server_test.go`: fake service fixture fields/helpers 추가 또는 02 helper 재사용.
- [ ] `apps/edge/internal/openai/server_test.go`: mixed/tunnel-selected test.
- [ ] `apps/edge/internal/openai/server_test.go`: mixed/Ollama-selected test.
- [ ] `apps/edge/internal/openai/server_test.go`: Ollama-only group normalized test.
#### 테스트 작성
- 작성: `TestChatCompletionsMixedProviderPoolTunnelSelection`
- 작성: `TestChatCompletionsMixedProviderPoolOllamaSelection`
- 작성: `TestChatCompletionsOllamaOnlyProviderPoolUsesNormalizedPath`
#### 중간 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
```
기대 결과: PASS.
### [MIXED_GROUP-3] Existing Passthrough Fixture Alignment
#### 문제
Provider-pool passthrough, sideband, provider auth, write failure, generation policy tests use a helper catalog with a generic provider id and expect tunnel behavior. After selection-first, those tests should remain valid only when the selected provider is OpenAI-compatible/tunnel-capable.
#### 해결 방법
Existing helpers such as `chatSidebandServer`, `chatProviderAuthServer`, `chatPassthroughServer`, `responsesProviderTunnelServer` should explicitly configure fake selected path/provider as tunnel where needed. Do not weaken byte-identity or cancel assertions; make the provider selection fixture explicit.
Before:
```go
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}}})
```
After:
```go
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "pool-model", Providers: map[string]string{"prov-vllm": "served-model"}}})
fake.selectedProviderPath = providerPathTunnel
```
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/openai/server_test.go`: helper catalog provider ids/types/selection fixture 정리.
- [ ] 필요한 경우 comments에서 “catalog match == tunnel” 표현을 “selected tunnel provider”로 바꾼다.
- [ ] 기존 byte identity/provider auth/cancel tests assertion은 유지한다.
#### 테스트 작성
- 새 behavior test는 MIXED_GROUP-2에서 작성한다.
- 여기서는 기존 tests가 selection fixture를 명확히 하는 refactor 성격이며 별도 test name 추가는 선택 사항이다.
#### 중간 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
```
기대 결과: PASS.
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `apps/edge/internal/service/model_queue_test.go` | MIXED_GROUP-1 |
| `apps/edge/internal/service/run_dispatch_internal_test.go` | MIXED_GROUP-1 |
| `apps/edge/internal/openai/server_test.go` | MIXED_GROUP-2, MIXED_GROUP-3 |
| `apps/edge/internal/openai/chat_handler.go` | MIXED_GROUP-2 회귀 대응 필요 시 |
| `apps/edge/internal/openai/server.go` | MIXED_GROUP-2 fake/interface 대응 필요 시 |
## 최종 검증
```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -count=1
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
```
기대 결과: 모든 명령 PASS.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -240,9 +240,10 @@ func openAICompatProviderLabel(p config.NodeProviderConf) string {
if provider := strings.TrimSpace(p.Provider); provider != "" {
return provider
}
switch strings.TrimSpace(p.Type) {
case "vllm", "lemonade", "sglang", "openai_api", "seulgivibe_claude", "seulgivibe_openai":
return strings.TrimSpace(p.Type)
providerType := strings.ToLower(strings.TrimSpace(p.Type))
switch providerType {
case "vllm", "vllm-mlx", "lemonade", "sglang", "openai_api", "seulgivibe_claude", "seulgivibe_openai":
return providerType
default:
return ""
}

View file

@ -591,11 +591,14 @@ func TestBuildConfigPayload_ProviderFirstOpenAICompatDefaultsProviderFromType(t
want string
}{
{name: "vllm", typ: "vllm", want: "vllm"},
{name: "vllm mlx", typ: "vllm-mlx", want: "vllm-mlx"},
{name: "lemonade", typ: "lemonade", want: "lemonade"},
{name: "sglang", typ: "sglang", want: "sglang"},
{name: "openai api", typ: "openai_api", want: "openai_api"},
{name: "seulgivibe claude", typ: "seulgivibe_claude", want: "seulgivibe_claude"},
{name: "seulgivibe openai", typ: "seulgivibe_openai", want: "seulgivibe_openai"},
{name: "seulgivibe claude normalized label", typ: " Seulgivibe_Claude ", want: "seulgivibe_claude"},
{name: "seulgivibe openai normalized label", typ: " SEULGIVIBE_OPENAI ", want: "seulgivibe_openai"},
{name: "explicit override", typ: "openai_compat", provider: "vllm-mlx", want: "vllm-mlx"},
}
for _, tc := range cases {

View file

@ -27,7 +27,7 @@ func NormalizeAgentKind(kind string) (string, error) {
// NormalizeProviderType returns the canonical driver name for provider type.
func NormalizeProviderType(t string) string {
switch strings.ToLower(strings.TrimSpace(t)) {
case "openai_compat", "openai_api", "vllm", "lemonade", "sglang", "seulgivibe_claude", "seulgivibe_openai":
case "openai_compat", "openai_api", "vllm", "vllm-mlx", "lemonade", "sglang", "seulgivibe_claude", "seulgivibe_openai":
return "openai_compat"
case "ollama":
return "ollama"
@ -134,7 +134,7 @@ type NodeProviderConf struct {
// ID uniquely identifies this provider within the node and is referenced
// by models[].providers keys.
ID string `mapstructure:"id" yaml:"id"`
// Type is the runtime type (e.g. "ollama", "vllm", "lemonade", "sglang", "openai_api", "cli").
// Type is the runtime type (e.g. "ollama", "vllm", "vllm-mlx", "lemonade", "sglang", "openai_api", "cli").
Type string `mapstructure:"type" yaml:"type"`
// Category classifies the resource; MVP values: api, cli, local_inference.
Category Category `mapstructure:"category" yaml:"category"`

View file

@ -355,6 +355,33 @@ openai:
}
}
func TestNormalizeProviderTypeOpenAICompatibleAliases(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{name: "openai compat", in: "openai_compat", want: "openai_compat"},
{name: "openai api", in: "openai_api", want: "openai_compat"},
{name: "vllm", in: "vllm", want: "openai_compat"},
{name: "vllm mlx", in: "vllm-mlx", want: "openai_compat"},
{name: "lemonade", in: "lemonade", want: "openai_compat"},
{name: "sglang", in: "sglang", want: "openai_compat"},
{name: "seulgivibe claude", in: "seulgivibe_claude", want: "openai_compat"},
{name: "seulgivibe openai", in: "seulgivibe_openai", want: "openai_compat"},
{name: "normalized case and space", in: " VLLM-MLX ", want: "openai_compat"},
{name: "ollama", in: "ollama", want: "ollama"},
{name: "cli", in: "cli", want: "cli"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := config.NormalizeProviderType(tc.in); got != tc.want {
t.Fatalf("NormalizeProviderType(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
func TestLoadEdge_OpenAIPrincipalTokens(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")