From aeec784c7b5345e39a930bfa1560ab2ffa7e82de Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 11 Jul 2026 14:27:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EB=AA=A8=EB=8D=B8=20=EC=98=A4=EC=BC=80=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?=EB=B0=8F=20provider=20config=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 문서 추가 --- .../seulgivibe-openai-compatible-provider.md | 102 ++++++ .../SDD.md | 28 +- .../PHASE.md | 6 +- .../model-group-mixed-provider-dispatch.md | 18 +- .../seulgivibe-openai-compatible-provider.md | 87 ----- .../SDD.md | 22 +- agent-spec/input/openai-compatible-surface.md | 4 + .../runtime/provider-pool-config-refresh.md | 12 + .../CODE_REVIEW-local-G06.md | 143 +++++++++ .../PLAN-local-G06.md | 268 ++++++++++++++++ .../CODE_REVIEW-local-G07.md | 137 ++++++++ .../PLAN-local-G07.md | 276 ++++++++++++++++ .../CODE_REVIEW-local-G06.md | 138 ++++++++ .../PLAN-local-G06.md | 300 ++++++++++++++++++ apps/edge/internal/node/mapper.go | 7 +- apps/edge/internal/node/mapper_test.go | 3 + packages/go/config/config.go | 4 +- packages/go/config/config_test.go | 27 ++ 18 files changed, 1452 insertions(+), 130 deletions(-) create mode 100644 agent-roadmap/archive/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md rename agent-roadmap/{ => archive}/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md (74%) delete mode 100644 agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md create mode 100644 agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/CODE_REVIEW-local-G06.md create mode 100644 agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/PLAN-local-G06.md create mode 100644 agent-task/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/CODE_REVIEW-local-G07.md create mode 100644 agent-task/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/PLAN-local-G07.md create mode 100644 agent-task/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/CODE_REVIEW-local-G06.md create mode 100644 agent-task/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/PLAN-local-G06.md diff --git a/agent-roadmap/archive/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md b/agent-roadmap/archive/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md new file mode 100644 index 0000000..eefd48b --- /dev/null +++ b/agent-roadmap/archive/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md @@ -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 정리 +- 확인 필요: 없음 diff --git a/agent-roadmap/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md b/agent-roadmap/archive/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md similarity index 74% rename from agent-roadmap/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md rename to agent-roadmap/archive/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md index fa07032..e45246f 100644 --- a/agent-roadmap/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md +++ b/agent-roadmap/archive/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md @@ -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: 없음 diff --git a/agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md b/agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md index f3ad2f5..e862982 100644 --- a/agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md +++ b/agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md @@ -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) diff --git a/agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md b/agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md index 2cdd490..896066f 100644 --- a/agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md +++ b/agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md @@ -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이 소유한다. diff --git a/agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md b/agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md deleted file mode 100644 index 340522c..0000000 --- a/agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md +++ /dev/null @@ -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 정리 -- 확인 필요: 없음 diff --git a/agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md b/agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md index 9155390..626a8ea 100644 --- a/agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md +++ b/agent-roadmap/sdd/routing-policy-model-orchestration/model-group-mixed-provider-dispatch/SDD.md @@ -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 보존 기준이 문서와 구현에 동일하게 반영되어 있다 | diff --git a/agent-spec/input/openai-compatible-surface.md b/agent-spec/input/openai-compatible-surface.md index 68eb502..f480128 100644 --- a/agent-spec/input/openai-compatible-surface.md +++ b/agent-spec/input/openai-compatible-surface.md @@ -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를 종료 검토 기준으로 보강. diff --git a/agent-spec/runtime/provider-pool-config-refresh.md b/agent-spec/runtime/provider-pool-config-refresh.md index ac8af1b..962d886 100644 --- a/agent-spec/runtime/provider-pool-config-refresh.md +++ b/agent-spec/runtime/provider-pool-config-refresh.md @@ -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 기준을 현재 코드/계약/테스트 기준으로 반영. diff --git a/agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/CODE_REVIEW-local-G06.md b/agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/CODE_REVIEW-local-G06.md new file mode 100644 index 0000000..0e452a0 --- /dev/null +++ b/agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/CODE_REVIEW-local-G06.md @@ -0,0 +1,143 @@ + + +# 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. diff --git a/agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/PLAN-local-G06.md b/agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/PLAN-local-G06.md new file mode 100644 index 0000000..61d36a1 --- /dev/null +++ b/agent-task/m-model-group-mixed-provider-dispatch/01-provider-path-classifier/PLAN-local-G06.md @@ -0,0 +1,268 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/CODE_REVIEW-local-G07.md b/agent-task/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/CODE_REVIEW-local-G07.md new file mode 100644 index 0000000..a28f735 --- /dev/null +++ b/agent-task/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/CODE_REVIEW-local-G07.md @@ -0,0 +1,137 @@ + + +# 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. diff --git a/agent-task/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/PLAN-local-G07.md b/agent-task/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/PLAN-local-G07.md new file mode 100644 index 0000000..854c19d --- /dev/null +++ b/agent-task/m-model-group-mixed-provider-dispatch/02-selection-first-provider-dispatch/PLAN-local-G07.md @@ -0,0 +1,276 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/CODE_REVIEW-local-G06.md b/agent-task/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/CODE_REVIEW-local-G06.md new file mode 100644 index 0000000..dca65b1 --- /dev/null +++ b/agent-task/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/CODE_REVIEW-local-G06.md @@ -0,0 +1,138 @@ + + +# 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. diff --git a/agent-task/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/PLAN-local-G06.md b/agent-task/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/PLAN-local-G06.md new file mode 100644 index 0000000..15f23ae --- /dev/null +++ b/agent-task/m-model-group-mixed-provider-dispatch/03-mixed-model-group-openai/PLAN-local-G06.md @@ -0,0 +1,300 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/edge/internal/node/mapper.go b/apps/edge/internal/node/mapper.go index 491627f..9cf6761 100644 --- a/apps/edge/internal/node/mapper.go +++ b/apps/edge/internal/node/mapper.go @@ -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 "" } diff --git a/apps/edge/internal/node/mapper_test.go b/apps/edge/internal/node/mapper_test.go index 6736c3a..1fc8d69 100644 --- a/apps/edge/internal/node/mapper_test.go +++ b/apps/edge/internal/node/mapper_test.go @@ -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 { diff --git a/packages/go/config/config.go b/packages/go/config/config.go index e23c6db..77c9f16 100644 --- a/packages/go/config/config.go +++ b/packages/go/config/config.go @@ -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"` diff --git a/packages/go/config/config_test.go b/packages/go/config/config_test.go index 6a5a37f..bac1a63 100644 --- a/packages/go/config/config_test.go +++ b/packages/go/config/config_test.go @@ -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")