fix(openai): Cline 도구 호출 변환을 지원한다
Cline이 텍스트 XML 블록으로 도구 호출을 내보내는 경우 OpenAI-compatible tool_calls 응답으로 변환해야 클라이언트 실행 루프가 멈추지 않는다. Node resource model 로드맵과 agent-task 문서는 요청된 전체 변경 범위에 포함한다.
This commit is contained in:
parent
912e900244
commit
135259619e
18 changed files with 1936 additions and 12 deletions
|
|
@ -165,7 +165,9 @@ Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compa
|
|||
- `parallel_tool_calls`
|
||||
- `stream_options`
|
||||
|
||||
`tools`가 있는 Chat Completions 요청에서 현재 Edge는 OpenAI tool call 생성을 보장하지 않는다.
|
||||
`tools`가 있는 Chat Completions 요청에서 Edge는 backend-native tool call stream을 일반 구조화 이벤트로 중계하지 않는다.
|
||||
단, backend가 assistant content에 Cline-style 텍스트 tool call 블록을 출력하고 그 function name이 요청의 `tools[].function.name`에 포함되어 있으면 Edge는 해당 블록을 OpenAI-compatible `message.tool_calls` 또는 stream `delta.tool_calls`로 변환하고 `finish_reason: "tool_calls"`를 반환한다.
|
||||
지원하는 텍스트 블록의 최소 형태는 `<tool_call><function=<name>><parameter=<key>>JSON-or-text</parameter></function></tool_call>`이다.
|
||||
호환성을 위해 `tool_choice`가 생략되었거나 `auto`/강제 tool 선택 형태로 들어오면 내부 실행 입력에서는 `tool_choice: "none"`으로 낮춰 백엔드의 auto tool-calling 요구 조건에 의해 요청 전체가 실패하지 않게 한다.
|
||||
`parallel_tool_calls`와 `stream_options`는 클라이언트 호환성을 위해 수신하지만 현재 Edge 실행 의미에는 반영하지 않는다.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ provider 확장 Phase에서 검증한 Ollama, vLLM, SGLang, Lemonade 같은 추
|
|||
- 경로: `agent-roadmap/archive/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
|
||||
- 요약: `models[]`를 외부 model key이자 운영 catalog model로 두고, Node 하위 provider와 served model list를 기준으로 Edge가 provider 선택, load-ratio routing, target rewrite, read-only catalog 상태를 소유하는 MVP 계약을 완료했다.
|
||||
|
||||
- [계획] Node Resource Model Unification
|
||||
- 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- 요약: Node를 Edge 연결 identity로 두고 CLI, OpenAI-compatible provider, 기타 resource를 같은 Node 아래 나열하며 provider/resource capacity만 concurrency를 소유하도록 runtime 계약과 dev-runtime 구성을 정렬한다.
|
||||
|
||||
- [스케치] 사용량, 토큰, 로그 운영 추적 MVP
|
||||
- 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md`
|
||||
- 요약: 사용자 관리, token 관리, API/CLI/local inference 사용량 집계, 요청별 device/provider/time/token ledger, 로그 관리의 1차 운영 경계를 스케치한다.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
# Milestone: Node Resource Model Unification
|
||||
|
||||
## 위치
|
||||
|
||||
- Roadmap: `agent-roadmap/ROADMAP.md`
|
||||
- Phase: `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
|
||||
|
||||
## 목표
|
||||
|
||||
Node를 provider/CLI를 함께 담는 Edge 연결 identity로 정리한다.
|
||||
Node 자체는 global gate가 되지 않고, CLI, OpenAI-compatible provider, 기타 resource는 Node 아래 catalog로 나열되며 concurrency와 queue는 provider/resource capacity에서만 관리한다.
|
||||
Mac dev-runtime에서는 Codex CLI와 MLX vLLM provider를 하나의 `mac-codex-node` 아래 resource로 합치고, Edge routing/status와 smoke 기준을 이 모델에 맞춘다.
|
||||
|
||||
## 상태
|
||||
|
||||
[계획]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
- 상태: 해제
|
||||
- SDD: 필요
|
||||
- SDD 문서: `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`
|
||||
- SDD 사유: Node identity, config schema, runtime wire 의미, Edge dispatch/status, Node admission, dev field smoke가 함께 바뀌는 runtime 책임 경계 변경이다.
|
||||
- 잠금 해제 조건: 아래 체크리스트
|
||||
- [x] SDD 잠금이 해제되어 있다.
|
||||
- [x] SDD 사용자 리뷰가 없거나 승인/해결되었다.
|
||||
- [x] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다.
|
||||
- [x] Evidence Map이 완료 시 `Roadmap Completion`과 최종 검증 evidence로 검증 가능하게 연결되어 있다.
|
||||
- 결정 필요: 없음
|
||||
|
||||
## 범위
|
||||
|
||||
- `NodeRuntimeConfig.concurrency`와 `runtime.concurrency`를 legacy/compat metadata로 낮추고 node-wide admission source에서 제외한다.
|
||||
- `nodes[].providers[]`를 Node 아래 resource/provider catalog로 해석하고, provider가 참조하는 adapter instance validation을 강화한다.
|
||||
- Node runtime admission에서 global gate를 제거하고 provider adapter capacity gate만 유지한다.
|
||||
- CLI adapter는 IOP 레벨 concurrency 제한을 갖지 않는 unlimited resource로 표현한다.
|
||||
- Edge dispatch는 provider pool 요청만 queue/concurrency path를 사용하고, CLI/legacy direct route는 provider queue를 타지 않는다.
|
||||
- Edge status는 Node 아래 정의된 resource/provider catalog를 우선해 중복 snapshot을 피한다.
|
||||
- dev-runtime 구성과 문서, smoke 기준을 `mac-codex-node` 하나에 Codex CLI와 `mac-mlx-vllm` provider resource가 함께 있는 형태로 정렬한다.
|
||||
|
||||
## 기능
|
||||
|
||||
### Epic: [node-resource-model] Node Identity and Resource Catalog
|
||||
|
||||
Node를 Edge 연결 owner로 두고, 실행 제어는 Node 하위 resource/provider가 소유하도록 계약, runtime, dev 검증을 하나로 묶는다.
|
||||
|
||||
- [ ] [contract-schema] Node runtime contract와 config validation이 Node identity, resource catalog, provider adapter reference 의미를 고정한다. 현재 작업: `agent-task/m-node-resource-model-unification/01_contract_schema/PLAN-cloud-G07.md`. 검증: contract/config/proto 관련 unit test와 `Roadmap Completion` evidence가 남아 있다.
|
||||
- [ ] [node-admission] Node runtime admission에서 node-wide global gate가 제거되고 CLI는 unlimited, provider adapter는 capacity gate를 유지한다. 현재 작업: `agent-task/m-node-resource-model-unification/02+01_node_admission/PLAN-local-G06.md`. 검증: Node concurrency regression test와 CLI capability test가 통과한다.
|
||||
- [ ] [edge-routing-status] Edge dispatch와 status가 provider pool/resource catalog 기준으로 동작하고 CLI/direct route는 provider queue에 묶이지 않는다. 현재 작업: `agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/PLAN-cloud-G07.md`. 검증: Edge queue, OpenAI-compatible route, status provider test가 통과한다.
|
||||
- [ ] [dev-runtime-profile] dev-runtime에서 Mac Codex CLI와 MLX vLLM provider가 하나의 Node identity 아래 resource로 배포되고 3 connected nodes, 3 provider candidates, total capacity 10 smoke가 확인된다. 현재 작업: `agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/PLAN-cloud-G07.md`. 검증: clean sync/rebuild, config check, refresh dry-run 또는 apply, `/v1/responses`와 `/v1/chat/completions` capacity smoke evidence가 남아 있다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 새 계획 Milestone이며 기능 Task가 아직 충족되지 않았다.
|
||||
- 검토 항목: 모든 기능 Task의 `Roadmap Completion`, SDD Evidence Map, 최종 dev-runtime smoke evidence
|
||||
- 리뷰 코멘트: 없음
|
||||
|
||||
## 범위 제외
|
||||
|
||||
- Node를 여러 resource의 global gate 또는 global scheduler로 확장하는 설계
|
||||
- Control Plane을 Edge-local runtime registry의 canonical store로 만드는 변경
|
||||
- 새 외부 HTTP/gRPC endpoint 추가
|
||||
- provider/device/model qualification report, lifecycle policy, 품질 기반 routing
|
||||
- billing, chargeback, 조직 IAM, 장기 audit retention
|
||||
|
||||
## 작업 컨텍스트
|
||||
|
||||
- 관련 경로: `agent-contract/inner/edge-node-runtime-wire.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `proto/iop/runtime.proto`, `packages/go/config`, `apps/edge`, `apps/node`, `configs`, `agent-test/dev`, `agent-test/local`, `docs/edge-local-dev-guide.md`
|
||||
- 표준선(선택): Node는 Edge와 연결되는 identity와 resource registry 책임을 가진다. Node 자체는 global admission gate가 아니며, resource/provider는 정의된 대로만 실행에 사용된다.
|
||||
- 표준선(선택): provider concurrency와 queue는 provider pool/resource capacity에서만 관리하고, CLI 호출 수는 IOP 레벨에서 제한하지 않는다.
|
||||
- 선행 작업: Model Alias Provider Pool과 Provider Catalog
|
||||
- 후속 작업: 요청 실행 로그와 Usage Ledger 기반, Provider-Device-Model Qualification 리포트
|
||||
- 현재 작업 연결:
|
||||
- `agent-task/m-node-resource-model-unification/01_contract_schema/PLAN-cloud-G07.md`
|
||||
- `agent-task/m-node-resource-model-unification/02+01_node_admission/PLAN-local-G06.md`
|
||||
- `agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/PLAN-cloud-G07.md`
|
||||
- `agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/PLAN-cloud-G07.md`
|
||||
- 확인 필요: 없음
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
# SDD: Node Resource Model Unification
|
||||
|
||||
## 위치
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Phase: `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
|
||||
|
||||
## 상태
|
||||
|
||||
[승인됨]
|
||||
|
||||
## SDD 잠금
|
||||
|
||||
- 상태: 해제
|
||||
- 사용자 리뷰: 없음
|
||||
- 잠금 항목:
|
||||
- 없음
|
||||
|
||||
## 문제 / 비목표
|
||||
|
||||
- 문제: 같은 Mac host에 Codex CLI와 MLX vLLM provider가 함께 있어도 현재 runtime과 dev profile은 이를 별도 Node처럼 취급할 수 있고, `runtime.concurrency`/global gate 의미가 Node를 shared capacity gate로 오해하게 만든다. 이 SDD는 Node를 Edge 연결 identity와 resource registry로 고정하고, provider/resource만 concurrency를 소유하도록 계약과 검증 기준을 고정한다.
|
||||
- 비목표:
|
||||
- Node를 global scheduler, global resource gate, 또는 Control Plane canonical registry로 만들지 않는다.
|
||||
- provider/device/model qualification report나 lifecycle policy는 이번 Milestone에서 구현하지 않는다.
|
||||
- 새 public endpoint나 새 provider engine을 추가하지 않는다.
|
||||
- billing, org IAM, 장기 audit retention, 품질 평가 기반 routing은 다루지 않는다.
|
||||
|
||||
## Source of Truth
|
||||
|
||||
| 영역 | 기준 | 메모 |
|
||||
|------|------|------|
|
||||
| Roadmap | `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` | 목표, 기능 Task, 구현 잠금, 완료 판단 기준 |
|
||||
| Contract | `agent-contract/inner/edge-node-runtime-wire.md`, `agent-contract/inner/edge-config-runtime-refresh.md` | Node runtime config, Edge-Node wire, config refresh 의미 기준 |
|
||||
| Code | `proto/iop/runtime.proto`, `packages/go/config`, `apps/edge`, `apps/node`, `configs` | config validation, runtime admission, Edge dispatch/status 구현 기준 |
|
||||
| External Provider | dev-runtime vLLM/MLX, Lemonade, vLLM provider endpoints | provider id, adapter instance, capacity smoke 기준 |
|
||||
| User Decision | 없음 | 사용자가 Node/resource/provider 책임 경계와 provider-only concurrency 방향을 확정했다 |
|
||||
|
||||
## State Machine
|
||||
|
||||
| 상태 | 진입 조건 | 다음 상태 | 근거 |
|
||||
|------|-----------|-----------|------|
|
||||
| `node-configured` | `nodes[].id`와 adapter/resource catalog가 config에 존재한다 | `resource-catalog-validated` 또는 `config-error` | Edge config load/refresh |
|
||||
| `resource-catalog-validated` | provider/resource가 같은 Node 안의 enabled adapter instance와 capacity/category를 참조한다 | `node-connected` | config validation result |
|
||||
| `node-connected` | Node가 Edge에 연결되고 runtime config를 받는다 | `run-routed` 또는 `status-reported` | Edge-Node transport |
|
||||
| `run-routed` | Edge가 CLI/direct request 또는 provider pool request를 받는다 | `provider-admitted` 또는 `direct-dispatched` | RunRequest routing fields |
|
||||
| `provider-admitted` | request가 provider pool이고 provider capacity slot을 얻었다 | `provider-dispatched` 또는 `queued` | provider pool queue/admission |
|
||||
| `direct-dispatched` | request가 CLI 또는 legacy direct route다 | `run-complete` 또는 `run-error` | Node adapter execution |
|
||||
| `provider-dispatched` | Edge가 선택된 provider adapter target으로 dispatch했다 | `run-complete` 또는 `run-error` | provider adapter execution |
|
||||
| `status-reported` | Edge가 Node/resource/provider snapshot을 집계한다 | 없음 | status provider snapshot |
|
||||
|
||||
## Interface Contract
|
||||
|
||||
- 계약 원문: `agent-contract/inner/edge-node-runtime-wire.md`, `agent-contract/inner/edge-config-runtime-refresh.md`
|
||||
- 입력:
|
||||
- `nodes[].id`: Edge와 연결되는 Node identity다. 같은 host의 CLI와 provider resource가 이 id 아래에 함께 있을 수 있다.
|
||||
- `nodes[].alias`: 운영자가 인식하는 Node 별칭이다. routing source of truth는 id와 resource/provider id다.
|
||||
- `nodes[].adapters`: Node 안에서 실행 가능한 adapter instance 목록이다.
|
||||
- `nodes[].providers[]`: Node 아래 resource/provider catalog다. provider pool candidate와 status snapshot의 resource source로 사용한다.
|
||||
- `nodes[].providers[].adapter`: 같은 Node 안의 enabled adapter instance를 참조해야 한다.
|
||||
- `nodes[].providers[].capacity`: provider/resource admission capacity다. provider pool concurrency와 queue는 이 값을 기준으로 한다.
|
||||
- `NodeRuntimeConfig.concurrency` / `runtime.concurrency`: legacy/compat metadata다. Node-wide admission source로 사용하지 않는다.
|
||||
- `RunRequest.ProviderPool`: provider pool queue/admission을 사용할지 결정하는 flag다. `ModelGroupKey`만으로 queue에 진입하지 않는다.
|
||||
- 출력:
|
||||
- provider/resource status snapshot: Node 아래 정의된 resource/provider catalog를 우선해 provider id, node id, model/capacity/in-flight/queued 상태를 표현한다.
|
||||
- direct execution result: CLI/direct route는 provider queue accounting을 증가시키지 않는다.
|
||||
- provider execution result: provider pool route는 provider capacity와 queue accounting을 반영한다.
|
||||
- 금지:
|
||||
- Node id를 여러 resource의 global gate나 global concurrency bucket으로 쓰지 않는다.
|
||||
- CLI adapter를 IOP level concurrency로 제한하지 않는다.
|
||||
- `ModelGroupKey`가 있다는 이유만으로 CLI/direct route를 provider queue에 넣지 않는다.
|
||||
- `mac-mlx-vllm` provider를 dev-runtime에서 별도 IOP Node identity로 분리하지 않는다.
|
||||
- Control Plane을 Edge-local runtime registry의 canonical source로 만들지 않는다.
|
||||
|
||||
## Acceptance Scenarios
|
||||
|
||||
| ID | Milestone Task | Given | When | Then |
|
||||
|----|----------------|-------|------|------|
|
||||
| S01 | `contract-schema` | runtime contract와 config schema가 `NodeRuntimeConfig.concurrency`와 `nodes[].providers[]`를 포함한다 | 계약과 comments를 갱신한다 | node-wide concurrency가 legacy/compat metadata로 문서화되고 resource/provider capacity가 admission source로 문서화된다 |
|
||||
| S02 | `contract-schema` | provider resource가 같은 Node의 disabled/missing adapter를 참조한다 | config validation을 실행한다 | validation error가 발생하고 enabled adapter reference만 통과한다 |
|
||||
| S03 | `node-admission` | Node runtime concurrency가 1이고 서로 다른 CLI/provider resource가 있다 | 동시에 실행 요청을 보낸다 | Node global gate rejection 없이 adapter/resource capacity만 적용된다 |
|
||||
| S04 | `node-admission` | CLI adapter capabilities를 조회한다 | status/capability snapshot을 만든다 | CLI `MaxConcurrency`는 unlimited 의미인 `0`으로 표현되고 provider adapters는 capacity gate를 유지한다 |
|
||||
| S05 | `edge-routing-status` | CLI/direct request와 provider pool request가 모두 `ModelGroupKey`를 가진다 | Edge dispatch를 실행한다 | provider pool request만 queue를 사용하고 CLI/direct request는 direct dispatch된다 |
|
||||
| S06 | `edge-routing-status` | Node가 resource catalog와 legacy adapter snapshot을 모두 가질 수 있다 | Edge status snapshot을 조회한다 | 정의된 resource/provider catalog가 우선되고 중복 adapter-level provider snapshot은 생성되지 않는다 |
|
||||
| S07 | `dev-runtime-profile` | dev-runtime Mac host가 Codex CLI와 MLX vLLM provider를 실행한다 | clean deploy와 OpenAI-compatible capacity smoke를 실행한다 | connected Node는 `mac-codex-node`, `gx10-vllm-node`, `onexplayer-lemonade-node`이고 provider capacity total 10, queued >= 1, final in_flight/queued 0/0이 관측된다 |
|
||||
|
||||
## Evidence Map
|
||||
|
||||
| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|
||||
|----------|-------------------|------------------|---------------------------|
|
||||
| S01 | contract/proto/comment diff plus targeted tests | `agent-task/m-node-resource-model-unification/01_contract_schema` | `contract-schema` Roadmap Completion과 contract/config/proto verification output |
|
||||
| S02 | config validation unit test | `agent-task/m-node-resource-model-unification/01_contract_schema` | `contract-schema` Roadmap Completion과 missing/disabled adapter validation output |
|
||||
| S03 | Node concurrency regression test | `agent-task/m-node-resource-model-unification/02+01_node_admission` | `node-admission` Roadmap Completion과 global gate no-op evidence |
|
||||
| S04 | CLI capability and provider capacity tests | `agent-task/m-node-resource-model-unification/02+01_node_admission` | `node-admission` Roadmap Completion과 CLI unlimited/provider gate output |
|
||||
| S05 | Edge dispatch/provider queue tests | `agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status` | `edge-routing-status` Roadmap Completion과 provider-only queue evidence |
|
||||
| S06 | Edge status provider tests | `agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status` | `edge-routing-status` Roadmap Completion과 resource catalog snapshot evidence |
|
||||
| S07 | dev-runtime clean deploy and capacity smoke | `agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile` | `dev-runtime-profile` Roadmap Completion과 `/v1/responses`, `/v1/chat/completions`, capacity accounting evidence |
|
||||
|
||||
## Cross-repo Dependencies
|
||||
|
||||
- 없음
|
||||
|
||||
## Drift Check
|
||||
|
||||
- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
|
||||
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
|
||||
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
|
||||
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.
|
||||
|
||||
## 사용자 리뷰 이력
|
||||
|
||||
- 없음
|
||||
|
||||
## 작업 컨텍스트
|
||||
|
||||
- 표준선: Node는 Edge 연결 identity와 resource registry 책임을 가진다. CLI와 provider는 같은 Node 아래 resource로 공존할 수 있으며, concurrency는 provider/resource capacity가 소유한다.
|
||||
- 표준선: provider pool queue는 `ProviderPool`이 true인 요청에서만 사용한다. CLI/direct route는 `ModelGroupKey`가 있어도 provider queue accounting을 사용하지 않는다.
|
||||
- 후속 SDD: 요청 실행 로그와 Usage Ledger 기반이 provider/resource/node identity를 ledger schema로 확장할 때 이 SDD의 identity 기준을 참조한다.
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<!-- task=m-node-resource-model-unification/01_contract_schema plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[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`.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
|
||||
> Finalization is review-agent-only.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-27
|
||||
task=m-node-resource-model-unification/01_contract_schema, plan=0, tag=API
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Task ids:
|
||||
- `contract-schema`: Node runtime contract and resource catalog validation
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Runtime Concurrency Contract | [ ] |
|
||||
| [API-2] Resource Catalog Validation | [ ] |
|
||||
| [API-3] Resource Snapshot Compatibility | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `NodeRuntimeConfig.concurrency`를 node-wide admission source가 아닌 legacy/compat runtime payload로 계약 문서와 comments에서 정리한다.
|
||||
- [ ] `nodes[].providers[]`를 현재 wire의 resource catalog로 해석한다는 compat 기준을 문서화하고, `category=cli|api|local_inference`가 CLI/provider/resource 구분자임을 명시한다.
|
||||
- [ ] `nodes[].providers[].adapter`가 같은 node 안의 enabled adapter instance로 해석되어야 한다는 config validation을 추가한다.
|
||||
- [ ] provider adapter reference, duplicate/resource category, legacy config compatibility regression test를 추가한다.
|
||||
- [ ] proto 변경이 발생하면 `make proto`를 실행하고 generated diff가 원본 proto와 일치하는지 확인한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_0.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_0.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 task artifact를 unignore하는지 확인한다.
|
||||
- [ ] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다.
|
||||
- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `runtime.concurrency`가 더 이상 node-wide admission source로 문서화되지 않았는지 확인한다.
|
||||
- provider/resource adapter validation이 같은 node의 enabled adapter 기준인지 확인한다.
|
||||
- proto 변경이 있으면 generated 파일이 `make proto` 결과인지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### API-1 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/internal/configrefresh
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./packages/go/config ./apps/edge/internal/edgevalidate
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./proto/gen/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/configrefresh ./apps/edge/internal/node
|
||||
(output)
|
||||
```
|
||||
|
||||
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
<!-- task=m-node-resource-model-unification/01_contract_schema plan=0 tag=API -->
|
||||
|
||||
# Node Resource Model Contract Plan
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 계획을 구현한 뒤에는 반드시 같은 디렉터리의 `CODE_REVIEW-cloud-G07.md`에서 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 채운다. 구현 중 선택된 Milestone의 `구현 잠금 > 결정 필요`가 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 외부 환경, secret, 서비스 준비, 증거 공백은 사용자 리뷰 요청이 아니라 검증/후속 계획으로 기록한다.
|
||||
|
||||
## 배경
|
||||
|
||||
현재 config와 wire는 `nodes[].adapters`와 `nodes[].providers`를 이미 갖고 있지만, 계약 문서는 `NodeRuntimeConfig.concurrency`를 node-wide 실행 제한으로 정의한다. 사용자가 정리한 방향은 node가 global gate가 아니라 하나의 identity/connection owner이고, provider/CLI 같은 resource가 node 아래에 명시되는 구조다. 첫 단계는 구현 전 계약과 validation 기준을 맞춰 이후 Node/Edge 변경이 같은 의미를 공유하게 하는 것이다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 사용자 리뷰 요청은 선택된 Milestone lock 결정이 직접 막을 때만 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 선택된 Milestone의 구현 잠금은 해제되어 있으므로 기본값은 `없음`이다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Task ids:
|
||||
- `contract-schema`: Node runtime contract and resource catalog validation
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/skills/common/router.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
|
||||
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`
|
||||
- `agent-roadmap/phase/update-plane-self-update-foundation/PHASE.md`
|
||||
- `agent-roadmap/phase/update-plane-self-update-foundation/milestones/update-plane-stable-protocol.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/node-smoke.md`
|
||||
- `agent-test/local/edge-smoke.md`
|
||||
- `agent-test/local/platform-common-smoke.md`
|
||||
- `agent-test/dev/inventory.yaml`
|
||||
- `agent-test/dev/edge-smoke.md`
|
||||
- `agent-test/dev/node-smoke.md`
|
||||
- `agent-test/dev/platform-common-smoke.md`
|
||||
- `agent-contract/index.md`
|
||||
- `agent-contract/inner/edge-node-runtime-wire.md`
|
||||
- `agent-contract/inner/control-plane-edge-wire.md`
|
||||
- `agent-contract/inner/edge-config-runtime-refresh.md`
|
||||
- `agent-contract/outer/openai-compatible-api.md`
|
||||
- `proto/iop/runtime.proto`
|
||||
- `proto/iop/control.proto`
|
||||
- `packages/go/config/config.go`
|
||||
- `apps/edge/internal/edgevalidate/validate.go`
|
||||
- `apps/edge/internal/configrefresh/classify.go`
|
||||
- `apps/edge/internal/node/mapper.go`
|
||||
- `apps/node/internal/adapters/config_set.go`
|
||||
- `packages/go/config/config_test.go`
|
||||
- `apps/edge/internal/configrefresh/classify_test.go`
|
||||
- `apps/edge/internal/node/mapper_test.go`
|
||||
- `apps/node/internal/adapters/config_set_test.go`
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S01`, `S02`와 Milestone Task `contract-schema`를 대상으로 한다.
|
||||
|
||||
Evidence Map은 contract/proto/comment diff, config validation unit test, missing/disabled adapter validation output을 요구한다. 따라서 구현 체크리스트는 `NodeRuntimeConfig.concurrency` legacy/no-op 계약 정리, `nodes[].providers[]` resource catalog 문서화, provider adapter reference validation, 관련 regression test를 포함한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env는 기본값 `local`이다. `agent-test/local/rules.md`와 matching profile `node-smoke`, `edge-smoke`, `platform-common-smoke`를 읽었다. 이 subtask는 `packages/go/**`, `proto/**`, `agent-contract/**`를 건드리므로 `go test -count=1 ./packages/go/... ./proto/gen/...`와, proto를 바꾸는 경우 `make proto`를 적용한다. dev-runtime 문서와 원격 smoke는 이 subtask의 필수 검증이 아니며 04번 subtask가 맡는다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- `NodeRuntimeConfig.concurrency` 의미를 legacy/no-op으로 바꾸는 계약 문구는 문서성 변경이라 unit coverage가 없다.
|
||||
- provider adapter reference validation은 config/edgevalidate regression test가 필요하다.
|
||||
- `ProviderSnapshot`을 resource snapshot으로 취급하는 compat 의미는 proto field 추가 없이 문서와 status tests로 검증한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
삭제/rename 예정 symbol은 없다. 의미 변경 대상은 `NodeRuntimeConfig.concurrency`, `ProviderSnapshot`, `nodes[].providers[].adapter`, `runtime.concurrency` 문자열이다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
split policy를 먼저 평가했다. 공유 task group은 `m-node-resource-model-unification`이다.
|
||||
|
||||
- `01_contract_schema`: 계약/config validation 기준.
|
||||
- `02+01_node_admission`: 01 완료 후 Node admission 의미 변경.
|
||||
- `03+01_edge_dispatch_status`: 01 완료 후 Edge dispatch/status 의미 변경.
|
||||
- `04+01,02,03_dev_runtime_profile`: 앞선 세 구현 완료 후 dev-runtime 문서/config/smoke 정리.
|
||||
|
||||
이 subtask는 선행 의존성이 없다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
새 외부 API endpoint나 gRPC 도입은 제외한다. `proto`는 가능하면 field 추가 없이 legacy `ProviderSnapshot`을 resource snapshot compat surface로 문서화하고, 실제 새 field가 꼭 필요하다고 구현 중 판단될 때만 `make proto`와 생성물 diff를 포함한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
`cloud-G07`: config/proto/contract 의미가 여러 런타임 경계에 영향을 주며, wrong-local implementation이 dev/runtime status를 깨뜨릴 수 있다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `NodeRuntimeConfig.concurrency`를 node-wide admission source가 아닌 legacy/compat runtime payload로 계약 문서와 comments에서 정리한다.
|
||||
- [ ] `nodes[].providers[]`를 현재 wire의 resource catalog로 해석한다는 compat 기준을 문서화하고, `category=cli|api|local_inference`가 CLI/provider/resource 구분자임을 명시한다.
|
||||
- [ ] `nodes[].providers[].adapter`가 같은 node 안의 enabled adapter instance로 해석되어야 한다는 config validation을 추가한다.
|
||||
- [ ] provider adapter reference, duplicate/resource category, legacy config compatibility regression test를 추가한다.
|
||||
- [ ] proto 변경이 발생하면 `make proto`를 실행하고 generated diff가 원본 proto와 일치하는지 확인한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [API-1] Runtime Concurrency Contract
|
||||
|
||||
**문제:** `agent-contract/inner/edge-node-runtime-wire.md`는 `NodeRuntimeConfig.concurrency`를 node-wide 실행 제한으로 정의하고, `apps/edge/internal/configrefresh/classify.go:220`도 live-applyable node-wide concurrency로 주석화한다. 사용자의 방향과 충돌한다.
|
||||
|
||||
**해결 방법:** contract, proto comment, Go comments를 `legacy compatibility payload; provider/resource capacity owns admission` 의미로 바꾼다. Wire field를 즉시 제거하지 않는다.
|
||||
|
||||
Before:
|
||||
|
||||
```proto
|
||||
// proto/iop/runtime.proto:251
|
||||
message NodeRuntimeConfig {
|
||||
int32 concurrency = 1;
|
||||
reserved 2;
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```proto
|
||||
// NodeRuntimeConfig carries legacy node runtime metadata. Execution admission
|
||||
// must not use this as a node-wide global gate; provider/resource capacity owns concurrency.
|
||||
message NodeRuntimeConfig {
|
||||
int32 concurrency = 1; // legacy compatibility, 0/unset means no node-wide limit
|
||||
reserved 2;
|
||||
}
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `proto/iop/runtime.proto`
|
||||
- [ ] `agent-contract/inner/edge-node-runtime-wire.md`
|
||||
- [ ] `agent-contract/inner/edge-config-runtime-refresh.md`
|
||||
- [ ] `apps/edge/internal/configrefresh/classify.go`
|
||||
- [ ] `apps/edge/internal/configrefresh/classify_test.go`
|
||||
|
||||
**테스트 작성:** `TestClassifyRuntimeConcurrencyLegacyAppliedOrIgnored` 형태로 기존 `runtime.concurrency` only diff의 기대 의미를 갱신한다. 즉시 제거가 아니면 refresh result는 compat상 `applied`여도 admission 변경을 뜻하지 않는다고 확인한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/internal/configrefresh
|
||||
```
|
||||
|
||||
### [API-2] Resource Catalog Validation
|
||||
|
||||
**문제:** `packages/go/config/config.go:112`의 `NodeProviderConf`는 `Adapter`를 문자열로 받지만, `LoadEdge`의 provider loop(`packages/go/config/config.go:512`)와 `edgevalidate.ValidateEdgeConfig`는 adapter가 같은 node의 enabled adapter instance인지 확인하지 않는다. Edge dispatch의 `providerDispatchable`도 `apps/edge/internal/service/run_dispatch.go:476`에서 non-empty만 본다.
|
||||
|
||||
**해결 방법:** `edgevalidate` 또는 config helper에 provider adapter reference resolver를 추가한다. Exact instance key를 우선하고, type-name route는 enabled instance가 정확히 1개일 때만 허용한다. `category=cli` resource는 CLI enabled를 요구한다.
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `apps/edge/internal/edgevalidate/validate.go`
|
||||
- [ ] `packages/go/config/config.go`
|
||||
- [ ] `packages/go/config/config_test.go`
|
||||
- [ ] `apps/edge/internal/edgevalidate/*_test.go` 또는 기존 test package
|
||||
|
||||
**테스트 작성:** bad adapter, disabled adapter, ambiguous type route, valid CLI resource, valid OpenAI-compatible provider instance를 table test로 추가한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./packages/go/config ./apps/edge/internal/edgevalidate
|
||||
```
|
||||
|
||||
### [API-3] Resource Snapshot Compatibility
|
||||
|
||||
**문제:** wire는 `proto/iop/runtime.proto:118`의 `ProviderSnapshot`과 `proto/iop/control.proto:83`의 `provider_snapshots`만 갖는다. 이름은 provider지만 사용자는 CLI/provider 등 resource 목록을 node 아래에 보고 싶어 한다.
|
||||
|
||||
**해결 방법:** field rename 없이 `ProviderSnapshot`을 legacy wire name으로 유지하고, 계약 문서에서 `category`가 resource kind를 나타내며 provider-pool 대상은 `models[].providers`에 참조된 catalog resource뿐이라고 명시한다. 실제 status 정리는 03번 subtask에서 수행한다.
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `proto/iop/runtime.proto`
|
||||
- [ ] `proto/iop/control.proto`
|
||||
- [ ] `agent-contract/inner/control-plane-edge-wire.md`
|
||||
- [ ] `agent-contract/inner/edge-config-runtime-refresh.md`
|
||||
|
||||
**테스트 작성:** proto field 추가가 없으면 새 generated test는 생략한다. 문서/comment-only면 `go test` regression으로 충분하다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./proto/gen/...
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| `proto/iop/runtime.proto` | API-1, API-3 |
|
||||
| `proto/iop/control.proto` | API-3 |
|
||||
| `agent-contract/inner/edge-node-runtime-wire.md` | API-1 |
|
||||
| `agent-contract/inner/control-plane-edge-wire.md` | API-3 |
|
||||
| `agent-contract/inner/edge-config-runtime-refresh.md` | API-1, API-3 |
|
||||
| `packages/go/config/config.go` | API-2 |
|
||||
| `packages/go/config/config_test.go` | API-2 |
|
||||
| `apps/edge/internal/edgevalidate/validate.go` | API-2 |
|
||||
| `apps/edge/internal/configrefresh/classify.go` | API-1 |
|
||||
| `apps/edge/internal/configrefresh/classify_test.go` | API-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/configrefresh ./apps/edge/internal/node
|
||||
```
|
||||
|
||||
Proto 원본을 변경했다면 추가로 실행한다.
|
||||
|
||||
```bash
|
||||
make proto
|
||||
git diff -- proto/gen/iop
|
||||
```
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<!-- task=m-node-resource-model-unification/02+01_node_admission plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> Complete the checklist, fill implementation-owned sections, then stop with active files in place and report ready for review. Do not ask the user directly during implementation.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-27
|
||||
task=m-node-resource-model-unification/02+01_node_admission, plan=0, tag=REFACTOR
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Task ids:
|
||||
- `node-admission`: Node admission global gate removal and resource capacity preservation
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] Remove Node Global Admission | [ ] |
|
||||
| [REFACTOR-2] Make Runtime Concurrency No-op For Admission | [ ] |
|
||||
| [REFACTOR-3] CLI Capability Unlimited | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `globalGate`를 admission path에서 제거하거나 no-op으로 축소해 node-wide 실행 제한이 생기지 않게 한다.
|
||||
- [ ] adapter gate는 유지하고, provider/openai_compat/vllm/ollama capacity가 `Capabilities().MaxConcurrency`로 제한되는지 보존한다.
|
||||
- [ ] `NodeRuntimeConfig.concurrency` refresh는 config metadata로만 보존하고 admission capacity를 바꾸지 않게 한다.
|
||||
- [ ] CLI adapter `Capabilities()`의 `MaxConcurrency`를 `0`으로 바꿔 unlimited 의미를 노출한다.
|
||||
- [ ] Node concurrency tests를 새 의미에 맞게 갱신하고, provider adapter capacity 제한 regression을 유지한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] active files를 `.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `complete.log`를 작성하고 archive로 이동한다.
|
||||
- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Node admission이 adapter gate만 사용하는지 확인한다.
|
||||
- runtime concurrency refresh가 admission capacity를 변경하지 않는지 확인한다.
|
||||
- CLI capabilities가 `MaxConcurrency=0`이고 provider adapters는 capacity gate를 유지하는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/node/internal/node
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-2 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-3 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/node
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/node/...
|
||||
(output)
|
||||
```
|
||||
|
||||
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<!-- task=m-node-resource-model-unification/02+01_node_admission plan=0 tag=REFACTOR -->
|
||||
|
||||
# Node Admission Resource Plan
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 계획은 `01_contract_schema` 완료 후 구현한다. 구현 뒤에는 반드시 `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 채운다. 구현 중 직접 사용자에게 질문하지 말고, 선택된 Milestone lock 결정만 review stub의 `사용자 리뷰 요청`에 기록한다.
|
||||
|
||||
## 배경
|
||||
|
||||
Node는 현재 하나의 identity/connection owner가 아니라 `globalGate`를 가진 shared capacity gate처럼 동작한다. 사용자가 정리한 방향에서는 node-wide concurrency가 없어야 하며, CLI는 IOP 레벨에서 제한하지 않고 provider/resource capacity만 admission을 가져야 한다. 이 subtask는 Node 내부 admission을 resource/adapter 단위로 낮춘다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
선택된 Milestone의 구현 잠금은 해제되어 있으므로 사용자 리뷰 요청 기본값은 `없음`이다. 환경/secret/provider 상태 문제는 검증 결과나 후속 계획으로 기록한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Task ids:
|
||||
- `node-admission`: Node admission global gate removal and resource capacity preservation
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `apps/node/internal/node/node.go`
|
||||
- `apps/node/internal/node/run_manager.go`
|
||||
- `apps/node/internal/adapters/cli/cli.go`
|
||||
- `apps/node/internal/adapters/config_set.go`
|
||||
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
|
||||
- `proto/iop/runtime.proto`
|
||||
- `apps/node/internal/node/node_test.go`
|
||||
- `apps/node/internal/node/node_concurrency_integration_test.go`
|
||||
- `apps/node/internal/adapters/cli/cli_internal_test.go`
|
||||
- `apps/node/internal/adapters/config_set_test.go`
|
||||
- `apps/node/internal/adapters/openai_compat/openai_compat_test.go`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/node-smoke.md`
|
||||
- `agent-test/local/platform-common-smoke.md`
|
||||
- `agent-contract/inner/edge-node-runtime-wire.md`
|
||||
- `agent-contract/inner/edge-config-runtime-refresh.md`
|
||||
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S03`, `S04`와 Milestone Task `node-admission`을 대상으로 한다.
|
||||
|
||||
Evidence Map은 Node concurrency regression, global gate no-op evidence, CLI unlimited/provider gate output을 요구한다. 따라서 구현 체크리스트는 `globalGate` 제거 또는 no-op 축소, adapter gate 보존, runtime concurrency refresh no-op, CLI `MaxConcurrency=0`, 관련 Node/CLI regression test를 포함한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env는 `local`이다. Node 변경이므로 `agent-test/local/node-smoke.md` 기준을 적용한다. 필수 unit은 `go test -count=1 ./apps/node/...`이며, 실행 요청/admission을 바꾸므로 가능하면 repo 내부 edge-node smoke도 04번 통합 task에서 확인한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- 기존 `apps/node/internal/node/node_test.go`는 global gate rejection을 기대하는 테스트를 포함한다. 이 테스트들은 새 의미로 갱신해야 한다.
|
||||
- CLI `MaxConcurrency=4` 기대 테스트는 명시적으로 없지만 capabilities 결과를 쓰는 status/capability 테스트가 영향받을 수 있다.
|
||||
- provider adapter gate는 기존 concurrency tests가 일부 커버하지만, runtime concurrency가 no-op임을 별도 regression으로 확인해야 한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
제거 또는 의미 축소 대상:
|
||||
|
||||
- `globalGate`: `apps/node/internal/node/node.go:37`, `:68`, `:212`, `:345`, `:357`, `:402`, `:405`
|
||||
- `admissionManager.global`: `apps/node/internal/node/run_manager.go:195`
|
||||
- `MaxConcurrency: 4` for CLI: `apps/node/internal/adapters/cli/cli.go:213`
|
||||
- runtime concurrency tests: `apps/node/internal/node/node_test.go` around NCDRAIN runtime concurrency tests and concurrency integration tests.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
이 subtask는 `02+01_node_admission`이므로 predecessor `01_contract_schema`의 `complete.log`가 필요하다. 현재 predecessor는 active plan 상태이며 `complete.log`가 없다. 구현자는 runtime이 dependency를 만족시킨 뒤 시작해야 한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
Edge queue/status 정책은 03번 subtask 범위다. 이 subtask는 Node-local admission과 CLI capability만 바꾼다. provider adapter capacity는 adapter `Capabilities().MaxConcurrency`로 계속 Node safety gate를 유지한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
`local-G06`: Node-local refactor이며 test coverage가 가까이에 있지만 concurrency behavior 변경이라 ordering regression 위험이 있다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
`02+01_node_admission`은 sibling `01_contract_schema` 완료 후 구현한다. directory dependency 외 추가 의존성은 없다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `globalGate`를 admission path에서 제거하거나 no-op으로 축소해 node-wide 실행 제한이 생기지 않게 한다.
|
||||
- [ ] adapter gate는 유지하고, provider/openai_compat/vllm/ollama capacity가 `Capabilities().MaxConcurrency`로 제한되는지 보존한다.
|
||||
- [ ] `NodeRuntimeConfig.concurrency` refresh는 config metadata로만 보존하고 admission capacity를 바꾸지 않게 한다.
|
||||
- [ ] CLI adapter `Capabilities()`의 `MaxConcurrency`를 `0`으로 바꿔 unlimited 의미를 노출한다.
|
||||
- [ ] Node concurrency tests를 새 의미에 맞게 갱신하고, provider adapter capacity 제한 regression을 유지한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [REFACTOR-1] Remove Node Global Admission
|
||||
|
||||
**문제:** `apps/node/internal/node/node.go:208`의 `admissionFor`가 global gate와 adapter gate를 같이 잡고, `apps/node/internal/node/run_manager.go:207`의 `acquire`가 adapter 성공 뒤 global 실패 시 실행을 거부한다. 이 구조는 하나의 node에 CLI와 MLX provider가 동시에 있을 때 서로를 막을 수 있다.
|
||||
|
||||
**해결 방법:** admission manager를 adapter gate only로 단순화한다. `Node.New`의 `globalConcurrency` 인자는 compatibility를 위해 남기더라도 admission에 쓰지 않는다. 관련 comments를 node identity/resource model에 맞게 고친다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// apps/node/internal/node/node.go:210
|
||||
return &admissionManager{
|
||||
global: n.globalGate,
|
||||
adapter: n.adapterGateFor(adapterKey, caps),
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
return &admissionManager{
|
||||
adapter: n.adapterGateFor(adapterKey, caps),
|
||||
}
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `apps/node/internal/node/node.go`
|
||||
- [ ] `apps/node/internal/node/run_manager.go`
|
||||
- [ ] `apps/node/internal/node/node_test.go`
|
||||
- [ ] `apps/node/internal/node/node_concurrency_integration_test.go`
|
||||
|
||||
**테스트 작성:** runtime concurrency가 1이어도 서로 다른 unlimited adapters가 global rejection을 받지 않는 test를 추가하고, adapter `MaxConcurrency=1`은 두 번째 run을 reject하는 기존 test를 유지/갱신한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/node/internal/node
|
||||
```
|
||||
|
||||
### [REFACTOR-2] Make Runtime Concurrency No-op For Admission
|
||||
|
||||
**문제:** `apps/node/internal/node/node.go:337`부터 refresh가 `runtime.concurrency` change를 계산하고 `n.globalGate.updateCapacity`를 호출한다. 새 방향에서는 provider/resource capacity가 concurrency source다.
|
||||
|
||||
**해결 방법:** `ConfigSet.Runtime`에는 값을 보존하되 live admission mutation을 제거한다. no-adapter-change refresh와 adapter swap refresh 모두 runtime concurrency 때문에 gate가 바뀌지 않아야 한다.
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `apps/node/internal/node/node.go`
|
||||
- [ ] `apps/node/internal/adapters/config_set.go`
|
||||
- [ ] `apps/node/internal/node/node_test.go`
|
||||
- [ ] `apps/node/internal/adapters/config_set_test.go`
|
||||
|
||||
**테스트 작성:** 기존 NCDRAIN runtime concurrency tests를 "config refresh stores runtime metadata but does not throttle admission"으로 갱신한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters
|
||||
```
|
||||
|
||||
### [REFACTOR-3] CLI Capability Unlimited
|
||||
|
||||
**문제:** CLI adapter는 `apps/node/internal/adapters/cli/cli.go:213`에서 `MaxConcurrency: 4`를 반환한다. 현재 admission path에서는 CLI 호출도 4개로 제한된다.
|
||||
|
||||
**해결 방법:** CLI `MaxConcurrency`를 `0`으로 반환한다. `fifoGate`의 기존 의미(`capacity <= 0` unlimited)를 그대로 사용한다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// apps/node/internal/adapters/cli/cli.go:210
|
||||
return runtime.Capabilities{
|
||||
AdapterName: Name,
|
||||
Targets: profiles,
|
||||
MaxConcurrency: 4,
|
||||
}, nil
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
return runtime.Capabilities{
|
||||
AdapterName: Name,
|
||||
Targets: profiles,
|
||||
MaxConcurrency: 0,
|
||||
}, nil
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `apps/node/internal/adapters/cli/cli.go`
|
||||
- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go`
|
||||
- [ ] `apps/node/internal/node/node_test.go`
|
||||
|
||||
**테스트 작성:** CLI capabilities regression을 추가하거나 Node capability command test 기대값을 `0`으로 갱신한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/node
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| `apps/node/internal/node/node.go` | REFACTOR-1, REFACTOR-2 |
|
||||
| `apps/node/internal/node/run_manager.go` | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/cli.go` | REFACTOR-3 |
|
||||
| `apps/node/internal/adapters/config_set.go` | REFACTOR-2 |
|
||||
| `apps/node/internal/node/node_test.go` | REFACTOR-1, REFACTOR-2, REFACTOR-3 |
|
||||
| `apps/node/internal/node/node_concurrency_integration_test.go` | REFACTOR-1 |
|
||||
| `apps/node/internal/adapters/cli/cli_internal_test.go` | REFACTOR-3 |
|
||||
| `apps/node/internal/adapters/config_set_test.go` | REFACTOR-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/node/...
|
||||
```
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<!-- task=m-node-resource-model-unification/03+01_edge_dispatch_status plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> Complete the checklist, fill implementation-owned sections, then stop with active files in place and report ready for review. Do not ask the user directly during implementation.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-27
|
||||
task=m-node-resource-model-unification/03+01_edge_dispatch_status, plan=0, tag=REFACTOR
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Task ids:
|
||||
- `edge-routing-status`: Edge provider-only queue routing and resource-first status
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] Provider-only Queue Admission | [ ] |
|
||||
| [REFACTOR-2] Provider Adapter Dispatch Validation | [ ] |
|
||||
| [REFACTOR-3] Resource-first Status Snapshot | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] service queue 진입 조건을 `ProviderPool && ModelGroupKey != "" && queue != nil`로 제한한다.
|
||||
- [ ] CLI/openai legacy route는 `ModelGroupKey`가 있어도 direct dispatch로 흐르고 provider pool queue/inflight를 사용하지 않게 한다.
|
||||
- [ ] provider-pool candidate resolution에서 provider adapter가 같은 node의 enabled adapter instance인지 확인한다.
|
||||
- [ ] status snapshot은 정의된 resource/provider catalog를 우선하고, legacy adapter snapshot은 catalog가 없을 때만 compat로 낸다.
|
||||
- [ ] provider별 in_flight/queued accounting이 같은 node의 여러 provider에서 독립적으로 유지되는지 test를 보강한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] active files를 `.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `complete.log`를 작성하고 archive로 이동한다.
|
||||
- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- provider-pool만 queue를 타는지 확인한다.
|
||||
- bad provider adapter가 config/load 또는 candidate resolution에서 막히는지 확인한다.
|
||||
- status provider snapshot이 catalog resource 중심으로 중복 없이 나오는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-2 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/edgevalidate
|
||||
(output)
|
||||
```
|
||||
|
||||
### REFACTOR-3 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/internal/service
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/...
|
||||
(output)
|
||||
```
|
||||
|
||||
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
<!-- task=m-node-resource-model-unification/03+01_edge_dispatch_status plan=0 tag=REFACTOR -->
|
||||
|
||||
# Edge Dispatch And Status Resource Plan
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 계획은 `01_contract_schema` 완료 후 구현한다. 구현 뒤에는 반드시 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채운다. Milestone lock 결정 외 이유로 사용자에게 직접 질문하지 않는다.
|
||||
|
||||
## 배경
|
||||
|
||||
Edge는 OpenAI-compatible 요청에 `ModelGroupKey`를 항상 담고, service는 `ModelGroupKey != ""`이면 queue를 탄다. 사용자의 방향에서는 provider pool만 concurrency/queue를 관리해야 하며 CLI route는 queue/global gate에 묶이지 않아야 한다. 또한 Control Plane status는 node 아래 실제 정의된 resource/provider를 중복 없이 보여야 한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
선택된 Milestone의 구현 잠금은 해제되어 있으므로 기본값은 `없음`이다. 외부 provider 접근 실패나 dev smoke 증거 공백은 04번 검증 또는 후속 계획으로 기록한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Task ids:
|
||||
- `edge-routing-status`: Edge provider-only queue routing and resource-first status
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `apps/edge/internal/service/run_dispatch.go`
|
||||
- `apps/edge/internal/service/model_queue.go`
|
||||
- `apps/edge/internal/service/status_provider.go`
|
||||
- `apps/edge/internal/openai/chat_handler.go`
|
||||
- `apps/edge/internal/openai/responses_handler.go`
|
||||
- `apps/edge/internal/openai/routes.go`
|
||||
- `apps/edge/internal/edgevalidate/validate.go`
|
||||
- `apps/edge/internal/node/mapper.go`
|
||||
- `apps/edge/internal/node/store.go`
|
||||
- `apps/edge/internal/bootstrap/runtime.go`
|
||||
- `apps/edge/internal/transport/server.go`
|
||||
- `apps/edge/internal/service/model_queue_test.go`
|
||||
- `apps/edge/internal/service/service_test.go`
|
||||
- `apps/edge/internal/service/service_internal_test.go`
|
||||
- `apps/edge/internal/service/status_provider_test.go`
|
||||
- `apps/edge/internal/openai/server_test.go`
|
||||
- `apps/edge/internal/node/mapper_test.go`
|
||||
- `apps/edge/internal/node/store_test.go`
|
||||
- `apps/edge/internal/bootstrap/runtime_test.go`
|
||||
- `apps/edge/internal/transport/server_test.go`
|
||||
- `agent-test/local/edge-smoke.md`
|
||||
- `agent-test/local/platform-common-smoke.md`
|
||||
- `agent-contract/outer/openai-compatible-api.md`
|
||||
- `agent-contract/inner/control-plane-edge-wire.md`
|
||||
- `agent-contract/inner/edge-config-runtime-refresh.md`
|
||||
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S05`, `S06`과 Milestone Task `edge-routing-status`를 대상으로 한다.
|
||||
|
||||
Evidence Map은 provider-only queue test, CLI/direct route direct dispatch evidence, resource catalog status snapshot evidence를 요구한다. 따라서 구현 체크리스트는 `ProviderPool && ModelGroupKey != "" && queue != nil` queue 조건, provider adapter validation, resource-first status snapshot, provider별 accounting regression을 포함한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env는 `local`이다. Edge service/OpenAI/status 변경이므로 `agent-test/local/edge-smoke.md`와 `platform-common-smoke.md`를 적용한다. 필수 unit은 `go test -count=1 ./apps/edge/...`이며, OpenAI-compatible boundary는 `/v1/models`, `/v1/responses`, `/v1/chat/completions` unit/e2e coverage를 확인한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- CLI route가 `ModelGroupKey`를 가져도 provider queue를 타지 않는 regression이 필요하다.
|
||||
- provider adapter가 missing/disabled/ambiguous일 때 config validation 또는 candidate resolution에서 제외되는 test가 필요하다.
|
||||
- `getSnapshotForNode`가 adapter-level snapshot과 provider catalog snapshot을 중복으로 내는 현상은 status provider test 갱신이 필요하다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
의미 변경 대상:
|
||||
|
||||
- `SubmitRun`: `apps/edge/internal/service/run_dispatch.go:107`
|
||||
- `resolveAdapterForNode`: `apps/edge/internal/service/run_dispatch.go:297`
|
||||
- `providerDispatchable`: `apps/edge/internal/service/run_dispatch.go:476`
|
||||
- `getSnapshotForNode`: `apps/edge/internal/service/model_queue.go:529`
|
||||
- `ModelGroupKey` writes: `apps/edge/internal/openai/chat_handler.go:75`, `apps/edge/internal/openai/responses_handler.go:89`
|
||||
|
||||
### 분할 판단
|
||||
|
||||
이 subtask는 `03+01_edge_dispatch_status`이므로 predecessor `01_contract_schema` 완료가 필요하다. 현재 predecessor는 active plan 상태이며 `complete.log`가 없다. `02+01_node_admission`과 병렬 가능하나 04번은 둘 다 완료 후 진행한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
Node-local gate 제거는 02번 범위다. dev-runtime remote deploy와 actual `build/dev-runtime/edge.yaml` 반영은 04번 범위다. 이 subtask는 Edge runtime code와 tests만 변경한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
`cloud-G07`: OpenAI outer API, service dispatch, provider queue, Control Plane status가 같이 걸린 cross-domain 변경이다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
`03+01_edge_dispatch_status`는 sibling `01_contract_schema` 완료 후 구현한다. `02`와 직접 dependency는 없지만 04가 두 결과를 통합 검증한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] service queue 진입 조건을 `ProviderPool && ModelGroupKey != "" && queue != nil`로 제한한다.
|
||||
- [ ] CLI/openai legacy route는 `ModelGroupKey`가 있어도 direct dispatch로 흐르고 provider pool queue/inflight를 사용하지 않게 한다.
|
||||
- [ ] provider-pool candidate resolution에서 provider adapter가 같은 node의 enabled adapter instance인지 확인한다.
|
||||
- [ ] status snapshot은 정의된 resource/provider catalog를 우선하고, legacy adapter snapshot은 catalog가 없을 때만 compat로 낸다.
|
||||
- [ ] provider별 in_flight/queued accounting이 같은 node의 여러 provider에서 독립적으로 유지되는지 test를 보강한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [REFACTOR-1] Provider-only Queue Admission
|
||||
|
||||
**문제:** `apps/edge/internal/service/run_dispatch.go:107`은 `ModelGroupKey != ""`이면 모든 route를 queue로 보낸다. OpenAI handlers는 legacy CLI route에도 `ModelGroupKey`를 채운다(`chat_handler.go:75`, `responses_handler.go:89`).
|
||||
|
||||
**해결 방법:** queue path는 `req.ProviderPool` true일 때만 사용한다. OpenAI handler는 그대로 `ProviderPool`을 전달하므로 handler 변경은 최소화한다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// apps/edge/internal/service/run_dispatch.go:107
|
||||
if req.ModelGroupKey != "" && s.queue != nil {
|
||||
return s.submitRunQueued(ctx, req)
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
if req.ProviderPool && req.ModelGroupKey != "" && s.queue != nil {
|
||||
return s.submitRunQueued(ctx, req)
|
||||
}
|
||||
return s.submitRunDirect(req)
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `apps/edge/internal/service/run_dispatch.go`
|
||||
- [ ] `apps/edge/internal/openai/server_test.go`
|
||||
- [ ] `apps/edge/internal/service/service_test.go`
|
||||
|
||||
**테스트 작성:** legacy CLI model route request with non-empty `ModelGroupKey`가 queue groups/inflight를 만들지 않고 direct dispatch되는 test를 추가한다. provider-pool route는 기존처럼 queue를 탄다는 test를 유지한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai
|
||||
```
|
||||
|
||||
### [REFACTOR-2] Provider Adapter Dispatch Validation
|
||||
|
||||
**문제:** `providerDispatchable`은 `apps/edge/internal/service/run_dispatch.go:476`에서 non-empty adapter만 확인한다. `nodes[].providers[].adapter`가 실제 node adapter와 어긋나도 provider-pool candidate가 될 수 있다.
|
||||
|
||||
**해결 방법:** 01번 validation helper를 재사용하거나 service-local resolver를 추가해 provider adapter가 enabled exact instance 또는 unambiguous type route인지 확인한다. Config validation에서 막되, service candidate resolution도 defensive check를 둔다.
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `apps/edge/internal/service/run_dispatch.go`
|
||||
- [ ] `apps/edge/internal/service/model_queue_test.go`
|
||||
- [ ] `apps/edge/internal/edgevalidate/validate.go`
|
||||
- [ ] `apps/edge/internal/edgevalidate/*_test.go`
|
||||
|
||||
**테스트 작성:** missing adapter, disabled adapter, ambiguous `openai_compat` type route, valid named adapter 각각 provider-pool candidate 결과를 검증한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/edgevalidate
|
||||
```
|
||||
|
||||
### [REFACTOR-3] Resource-first Status Snapshot
|
||||
|
||||
**문제:** `getSnapshotForNode`는 `apps/edge/internal/service/model_queue.go:540`부터 CLI/Ollama/vLLM/OpenAICompat adapter snapshots를 만들고, 다시 `:626`부터 provider catalog snapshots를 추가한다. 같은 OpenAICompat provider가 adapter snapshot과 provider snapshot으로 중복 노출된다.
|
||||
|
||||
**해결 방법:** `rec.Providers`가 있으면 그 catalog를 source of truth로 삼아 snapshots를 만들고 providerID stats만 붙인다. `rec.Providers`가 비어 있는 old config에서만 adapter-level compat snapshots를 낸다. CLI resource를 status에 보이게 하려면 config에 category `cli` provider/resource entry가 있어야 한다.
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `apps/edge/internal/service/model_queue.go`
|
||||
- [ ] `apps/edge/internal/service/status_provider.go`
|
||||
- [ ] `apps/edge/internal/service/status_provider_test.go`
|
||||
- [ ] `apps/edge/internal/service/model_queue_test.go`
|
||||
- [ ] `proto/iop/control.proto` comments only if needed
|
||||
|
||||
**테스트 작성:** node with CLI + OpenAICompat + providers catalog가 catalog snapshots만 반환하고, provider pool stats가 provider id별로 붙는지 확인한다. providers empty legacy node는 old adapter snapshot fallback을 확인한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/internal/service
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| `apps/edge/internal/service/run_dispatch.go` | REFACTOR-1, REFACTOR-2 |
|
||||
| `apps/edge/internal/service/model_queue.go` | REFACTOR-2, REFACTOR-3 |
|
||||
| `apps/edge/internal/service/status_provider.go` | REFACTOR-3 |
|
||||
| `apps/edge/internal/openai/server_test.go` | REFACTOR-1 |
|
||||
| `apps/edge/internal/service/model_queue_test.go` | REFACTOR-2, REFACTOR-3 |
|
||||
| `apps/edge/internal/service/service_test.go` | REFACTOR-1 |
|
||||
| `apps/edge/internal/service/status_provider_test.go` | REFACTOR-3 |
|
||||
| `apps/edge/internal/edgevalidate/validate.go` | REFACTOR-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/...
|
||||
```
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
<!-- task=m-node-resource-model-unification/04+01,02,03_dev_runtime_profile plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> Complete the checklist, fill implementation-owned sections, then stop with active files in place and report ready for review. Do not ask the user directly during implementation.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-27
|
||||
task=m-node-resource-model-unification/04+01,02,03_dev_runtime_profile, plan=0, tag=TEST
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Task ids:
|
||||
- `dev-runtime-profile`: Dev runtime single Mac node profile and capacity smoke
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] Dev Profile Merge For Mac Resources | [ ] |
|
||||
| [TEST-2] Dev Docs And Deploy Skill Alignment | [ ] |
|
||||
| [TEST-3] Remote Dev Runtime Smoke | [ ] |
|
||||
| [TEST-4] Capacity Smoke With Provider-only Concurrency | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] dev-runtime config/profile에서 `mac-codex-node` 하나에 CLI adapter와 `mac-mlx-vllm` OpenAI-compatible provider resource를 함께 둔다.
|
||||
- [ ] `mac-mlx-vllm-node` 별도 node/process/log 기준을 제거하거나 legacy note로 낮추고, provider id `mac-mlx-vllm`은 유지한다.
|
||||
- [ ] `agent-test/dev/*`, `agent-test/local/*`, `docs/edge-local-dev-guide.md`, dev-runtime deploy skill의 node/provider 기준을 3 connected nodes + 3 provider candidates로 일치시킨다.
|
||||
- [ ] dev-runtime clean sync, rebuild, config check, refresh dry-run/apply 또는 restart-required handling을 수행한다.
|
||||
- [ ] `/v1/responses`와 `/v1/chat/completions` capacity smoke에서 total capacity 10, queued >= 1을 관측하고 완료 후 0/0 회복을 기록한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] active files를 `.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `complete.log`를 작성하고 archive로 이동한다.
|
||||
- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- dev profile이 3 connected nodes + 3 provider candidates로 일관되는지 확인한다.
|
||||
- `mac-mlx-vllm` provider id가 유지되고 `mac-codex-node` 아래 resource로 이동했는지 확인한다.
|
||||
- remote smoke가 HTTP success뿐 아니라 provider snapshot capacity evidence를 남겼는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```text
|
||||
$ rg --sort path -n "4-node|4개 Node|mac-mlx-vllm-node|connected non-candidate" agent-test/dev agent-test/local docs/edge-local-dev-guide.md agent-ops/skills/project/dev-runtime-deploy/SKILL.md
|
||||
(output)
|
||||
```
|
||||
|
||||
### TEST-2 중간 검증
|
||||
```text
|
||||
$ rg --sort path -n "4-node|4개 Node|4개 node|mac-mlx-vllm-node|node-mac-mlx-vllm" agent-test/dev agent-test/local docs/edge-local-dev-guide.md agent-ops/skills/project/dev-runtime-deploy/SKILL.md
|
||||
(output)
|
||||
```
|
||||
|
||||
### TEST-3 중간 검증
|
||||
```text
|
||||
$ ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git status --short --branch && git log --oneline -1 && go test ./...'
|
||||
(output)
|
||||
```
|
||||
|
||||
### TEST-4 중간 검증
|
||||
```text
|
||||
$ ssh toki@toki-labs.com 'curl -fsS http://127.0.0.1:18001/edges/edge-toki-labs-dev/status'
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ go test -count=1 ./...
|
||||
(output)
|
||||
|
||||
$ ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git status --short --branch && git log --oneline -1 && test -x build/dev-runtime/bin/edge && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config check && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config refresh --mode dry-run --addr 127.0.0.1:19093'
|
||||
(output)
|
||||
|
||||
$ ssh toki@toki-labs.com 'curl -fsS http://127.0.0.1:18001/edges/edge-toki-labs-dev/status && curl -fsS http://127.0.0.1:18083/v1/models'
|
||||
(output)
|
||||
```
|
||||
|
||||
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
<!-- task=m-node-resource-model-unification/04+01,02,03_dev_runtime_profile plan=0 tag=TEST -->
|
||||
|
||||
# Dev Runtime Single Mac Node Profile Plan
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 계획은 `01_contract_schema`, `02+01_node_admission`, `03+01_edge_dispatch_status` 완료 후 구현한다. 구현 뒤에는 반드시 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채운다. 원격 runner, secret, provider 접근 문제가 있으면 사용자에게 묻지 말고 검증 결과와 남은 위험으로 기록한다.
|
||||
|
||||
## 배경
|
||||
|
||||
사용자는 mac에 연결된 Codex CLI와 MLX vLLM provider가 node 두 개가 아니라 하나의 node id/alias 아래 resource로 나열되기를 원한다. 현재 dev 문서와 inventory는 `mac-codex-node`와 `mac-mlx-vllm-node`를 분리하고 4-node 기준을 섞어 둔다. 이 subtask는 구현 결과를 dev-runtime 기준에 반영하고, 3 connected nodes + 3 provider candidates로 배포 검증한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
선택된 Milestone의 구현 잠금은 해제되어 있으므로 기본값은 `없음`이다. 원격 host, token, provider 상태, port 충돌, smoke 불안정은 사용자 리뷰 요청이 아니라 검증 blocker/후속 evidence로 기록한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- Task ids:
|
||||
- `dev-runtime-profile`: Dev runtime single Mac node profile and capacity smoke
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-test/dev/inventory.yaml`
|
||||
- `agent-test/dev/rules.md`
|
||||
- `agent-test/dev/edge-smoke.md`
|
||||
- `agent-test/dev/node-smoke.md`
|
||||
- `agent-test/dev/platform-common-smoke.md`
|
||||
- `agent-test/local/edge-smoke.md`
|
||||
- `agent-test/local/node-smoke.md`
|
||||
- `agent-test/local/platform-common-smoke.md`
|
||||
- `docs/edge-local-dev-guide.md`
|
||||
- `agent-ops/skills/project/dev-runtime-deploy/SKILL.md`
|
||||
- `scripts/dev/edge.sh`
|
||||
- `scripts/dev/node.sh`
|
||||
- `scripts/dev/web.sh`
|
||||
- `configs/edge.yaml`
|
||||
- `apps/edge/internal/edgecmd/node_register.go`
|
||||
- `apps/edge/internal/edgecmd/edgecmd_test.go`
|
||||
- `apps/edge/internal/bootstrap/runtime.go`
|
||||
- `apps/edge/internal/bootstrap/runtime_test.go`
|
||||
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S07`과 Milestone Task `dev-runtime-profile`을 대상으로 한다.
|
||||
|
||||
Evidence Map은 dev-runtime clean deploy, connected nodes/provider candidates 확인, `/v1/responses`, `/v1/chat/completions`, capacity accounting smoke evidence를 요구한다. 따라서 구현 체크리스트는 Mac resource merge, dev docs alignment, config check/refresh handling, total capacity 10과 queued >= 1 smoke 기록을 포함한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env는 plan 기본값 `local`이지만 이 subtask는 dev-runtime 원격 배포 검증을 포함한다. `agent-test/dev/inventory.yaml`, `agent-test/dev/edge-smoke.md`, `agent-test/dev/node-smoke.md`를 추가로 읽고 dev preflight를 계획에 포함한다. 필수 local unit은 `go test -count=1 ./...`이고, 원격 검증은 clean sync/rebuild 후 `config check`, `config refresh --mode dry-run`, `/v1/models`, `/v1/responses`, `/v1/chat/completions` capacity smoke다.
|
||||
|
||||
### 테스트 환경 프리플라이트
|
||||
|
||||
- runner: `ssh toki@toki-labs.com`
|
||||
- repo root: `/Users/toki/agent-work/iop-dev`
|
||||
- Edge config: `build/dev-runtime/edge.yaml`
|
||||
- Edge id: `edge-toki-labs-dev`
|
||||
- binaries: `build/dev-runtime/bin/edge`, `build/dev-runtime/bin/iop-node`, `build/dev-runtime/bin/iop-node-linux-arm64`, `build/dev-runtime/bin/iop-node-windows-amd64.exe`
|
||||
- ports: Control Plane `18001`, OpenAI `18083`, Edge-Node TCP `18084`, refresh admin `19093`
|
||||
- source sync: clean `origin/main` sync and `go test ./...` before rebuild.
|
||||
- expected nodes after change: `mac-codex-node`, `gx10-vllm-node`, `onexplayer-lemonade-node`.
|
||||
- expected provider candidates: `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=3`.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- `build/dev-runtime/edge.yaml` is runtime artifact, not tracked; unit tests cannot prove remote config was actually merged.
|
||||
- Capacity smoke is timing-sensitive; plan must record max observed `in_flight` and `queued`, not just HTTP success.
|
||||
- Dev docs currently disagree: deploy skill mentions 3 connected nodes while `agent-test/dev/*` and docs still say 4 nodes.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
삭제/rename symbol 없음. config/resource id changes:
|
||||
|
||||
- remove separate `mac-mlx-vllm-node` runtime node process from dev profile.
|
||||
- keep provider id `mac-mlx-vllm` under `mac-codex-node` so `models[].providers` can remain stable.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
이 subtask는 `04+01,02,03_dev_runtime_profile`이다. predecessors `01`, `02`, `03` 모두 `complete.log`가 필요하다. 현재 모두 active plan 상태이며 `complete.log`가 없다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
This task does not rework Edge/Node source behavior beyond prior subtasks. It updates dev profile/docs and performs deployment validation. Production/corp environment is excluded.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
`cloud-G07`: remote runner, multi-host runtime, Windows/macOS/Linux node bootstrap, and external provider capacity smoke are central.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
`04+01,02,03_dev_runtime_profile` must wait for sibling `01_contract_schema`, `02+01_node_admission`, and `03+01_edge_dispatch_status` completion. No additional hidden dependency is added.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] dev-runtime config/profile에서 `mac-codex-node` 하나에 CLI adapter와 `mac-mlx-vllm` OpenAI-compatible provider resource를 함께 둔다.
|
||||
- [ ] `mac-mlx-vllm-node` 별도 node/process/log 기준을 제거하거나 legacy note로 낮추고, provider id `mac-mlx-vllm`은 유지한다.
|
||||
- [ ] `agent-test/dev/*`, `agent-test/local/*`, `docs/edge-local-dev-guide.md`, dev-runtime deploy skill의 node/provider 기준을 3 connected nodes + 3 provider candidates로 일치시킨다.
|
||||
- [ ] dev-runtime clean sync, rebuild, config check, refresh dry-run/apply 또는 restart-required handling을 수행한다.
|
||||
- [ ] `/v1/responses`와 `/v1/chat/completions` capacity smoke에서 total capacity 10, queued >= 1을 관측하고 완료 후 0/0 회복을 기록한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] Dev Profile Merge For Mac Resources
|
||||
|
||||
**문제:** `agent-test/dev/inventory.yaml:49`는 `mac-codex-node`를 CLI node로, `:69`는 `mac-mlx-vllm-node`를 별도 provider node로 둔다. 사용자의 원하는 구조는 같은 mac node identity 아래 CLI와 MLX provider resource가 함께 나열되는 것이다.
|
||||
|
||||
**해결 방법:** dev-runtime `edge.yaml` candidate에서 `mac-codex-node` adapters에 CLI와 `openai_compat_instances[name=mac-mlx-vllm]`를 같이 두고, `providers[]`에 `id=mac-mlx-vllm`, `adapter=mac-mlx-vllm`, `capacity=3`을 둔다. `models[].providers.mac-mlx-vllm`는 유지한다.
|
||||
|
||||
Example target shape:
|
||||
|
||||
```yaml
|
||||
nodes:
|
||||
- id: mac-codex-node
|
||||
alias: mac-codex
|
||||
adapters:
|
||||
cli:
|
||||
enabled: true
|
||||
openai_compat_instances:
|
||||
- name: mac-mlx-vllm
|
||||
enabled: true
|
||||
provider: vllm-mlx
|
||||
endpoint: http://127.0.0.1:8002/v1
|
||||
capacity: 3
|
||||
providers:
|
||||
- id: mac-mlx-vllm
|
||||
type: vllm-mlx
|
||||
category: local_inference
|
||||
adapter: mac-mlx-vllm
|
||||
capacity: 3
|
||||
```
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] remote `build/dev-runtime/edge.yaml` candidate
|
||||
- [ ] `agent-test/dev/inventory.yaml`
|
||||
- [ ] `agent-test/dev/edge-smoke.md`
|
||||
- [ ] `agent-test/dev/node-smoke.md`
|
||||
- [ ] `agent-test/local/edge-smoke.md`
|
||||
- [ ] `agent-test/local/node-smoke.md`
|
||||
|
||||
**테스트 작성:** tracked test rule/docs 변경은 unit test 대신 deterministic grep 검증을 둔다. Remote `config check`가 schema test 역할을 한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
rg --sort path -n "4-node|4개 Node|mac-mlx-vllm-node|connected non-candidate" agent-test/dev agent-test/local docs/edge-local-dev-guide.md agent-ops/skills/project/dev-runtime-deploy/SKILL.md
|
||||
```
|
||||
|
||||
### [TEST-2] Dev Docs And Deploy Skill Alignment
|
||||
|
||||
**문제:** `docs/edge-local-dev-guide.md:133`부터 provider candidates와 `:136` connected non-candidate가 4-node 기준으로 남아 있고, `agent-test/dev/edge-smoke.md:46`도 4-node 연결을 말한다. deploy skill은 이미 3개 node connected 기준을 일부 사용한다.
|
||||
|
||||
**해결 방법:** 모든 dev-runtime 문서의 표현을 "3 connected nodes, 3 provider candidates, mac node has CLI + MLX provider resource"로 통일한다. Mac MLX process 운영 파일은 vllm-mlx provider process 로그로 유지하되 별도 iop-node 로그 기준은 제거한다.
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] `docs/edge-local-dev-guide.md`
|
||||
- [ ] `agent-ops/skills/project/dev-runtime-deploy/SKILL.md`
|
||||
- [ ] `agent-test/dev/rules.md`
|
||||
- [ ] `agent-test/dev/edge-smoke.md`
|
||||
- [ ] `agent-test/dev/node-smoke.md`
|
||||
- [ ] `agent-test/local/edge-smoke.md`
|
||||
- [ ] `agent-test/local/node-smoke.md`
|
||||
|
||||
**테스트 작성:** docs-only consistency grep을 사용한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
rg --sort path -n "4-node|4개 Node|4개 node|mac-mlx-vllm-node|node-mac-mlx-vllm" agent-test/dev agent-test/local docs/edge-local-dev-guide.md agent-ops/skills/project/dev-runtime-deploy/SKILL.md
|
||||
```
|
||||
|
||||
### [TEST-3] Remote Dev Runtime Smoke
|
||||
|
||||
**문제:** source 변경만으로는 사용자가 원하는 runtime 구조가 실제 dev environment에서 동작하는지 확인할 수 없다. 별도 mac MLX node process가 남으면 SQLite lock이나 duplicate node confusion이 재발할 수 있다.
|
||||
|
||||
**해결 방법:** remote runner를 clean sync/rebuild하고 Edge/mac/gx10/onexplayer runtime을 새 config로 재시작한다. Control Plane status에서 connected node가 3개인지, `mac-codex-node` snapshot에 CLI resource와 `mac-mlx-vllm` provider resource가 같이 보이는지 확인한다.
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] remote `/Users/toki/agent-work/iop-dev/build/dev-runtime/edge.yaml`
|
||||
- [ ] remote process scripts/commands used for Edge and Node restart
|
||||
- [ ] `CODE_REVIEW-cloud-G07.md` verification output
|
||||
|
||||
**테스트 작성:** no unit test. 실제 remote smoke가 test다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git status --short --branch && git log --oneline -1 && go test ./...'
|
||||
```
|
||||
|
||||
### [TEST-4] Capacity Smoke With Provider-only Concurrency
|
||||
|
||||
**문제:** 새 구조에서 mac CLI와 mac MLX provider가 같은 node id를 공유하므로, provider capacity accounting이 node-wide gate 없이 provider id별로 독립 유지되어야 한다.
|
||||
|
||||
**해결 방법:** `/v1/responses`와 `/v1/chat/completions` 각각 capacity+1 동시 요청을 보내고, Control Plane `provider_snapshots`에서 `gx10-vllm + onexplayer-lemonade + mac-mlx-vllm` total `in_flight=10`, `queued>=1`, 완료 후 `0/0`을 기록한다. CLI 호출은 별도 provider queue를 차지하지 않는지 status로 확인한다.
|
||||
|
||||
**수정 파일 및 체크리스트:**
|
||||
|
||||
- [ ] remote smoke command/output 기록
|
||||
- [ ] `CODE_REVIEW-cloud-G07.md` capacity evidence
|
||||
|
||||
**테스트 작성:** remote smoke evidence. 요청별 node assignment 공개 API가 없으므로 aggregate snapshot으로 판정한다.
|
||||
|
||||
**중간 검증:**
|
||||
|
||||
```bash
|
||||
ssh toki@toki-labs.com 'curl -fsS http://127.0.0.1:18001/edges/edge-toki-labs-dev/status'
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| `agent-test/dev/inventory.yaml` | TEST-1 |
|
||||
| `agent-test/dev/rules.md` | TEST-2 |
|
||||
| `agent-test/dev/edge-smoke.md` | TEST-1, TEST-2 |
|
||||
| `agent-test/dev/node-smoke.md` | TEST-1, TEST-2 |
|
||||
| `agent-test/local/edge-smoke.md` | TEST-1, TEST-2 |
|
||||
| `agent-test/local/node-smoke.md` | TEST-1, TEST-2 |
|
||||
| `docs/edge-local-dev-guide.md` | TEST-2 |
|
||||
| `agent-ops/skills/project/dev-runtime-deploy/SKILL.md` | TEST-2 |
|
||||
| remote `build/dev-runtime/edge.yaml` | TEST-1, TEST-3, TEST-4 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./...
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git status --short --branch && git log --oneline -1 && test -x build/dev-runtime/bin/edge && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config check && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config refresh --mode dry-run --addr 127.0.0.1:19093'
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh toki@toki-labs.com 'curl -fsS http://127.0.0.1:18001/edges/edge-toki-labs-dev/status && curl -fsS http://127.0.0.1:18083/v1/models'
|
||||
```
|
||||
|
||||
Capacity smoke command may use a temporary script under `/tmp` on the runner. Record the exact script path and command in the review file; do not add it to the repo. Expected result: responses and chat completions each observe total provider `in_flight=10`, `queued>=1`, and final provider snapshots recover to `in_flight=0`, `queued=0`.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -149,6 +149,13 @@ func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request,
|
|||
return
|
||||
}
|
||||
text, reasoning, normalized := normalizeCompletionOutput(outputPolicy, text, reasoning)
|
||||
message := chatMessage{Role: "assistant", Content: text, ReasoningContent: reasoning}
|
||||
if cleaned, toolCalls := synthesizeToolCallsFromText(text, req.Tools, handle.Dispatch().RunID); len(toolCalls) > 0 {
|
||||
message.Content = cleaned
|
||||
message.ReasoningContent = ""
|
||||
message.ToolCalls = toolCalls
|
||||
finishReason = "tool_calls"
|
||||
}
|
||||
s.logger.Info("openai chat completion output",
|
||||
zap.String("run_id", handle.Dispatch().RunID),
|
||||
zap.Bool("strict_output", outputPolicy.Strict),
|
||||
|
|
@ -167,7 +174,7 @@ func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request,
|
|||
Model: responseModel(req.Model, handle.Dispatch().Target),
|
||||
Choices: []chatCompletionChoice{{
|
||||
Index: 0,
|
||||
Message: chatMessage{Role: "assistant", Content: text, ReasoningContent: reasoning},
|
||||
Message: message,
|
||||
FinishReason: finishReason,
|
||||
}},
|
||||
Usage: usage,
|
||||
|
|
|
|||
|
|
@ -263,6 +263,95 @@ func TestChatCompletionsDowngradesToolChoiceAutoToNone(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsSynthesizesToolCallsFromClineTextBlock(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n<tool_call>\n<function=run_commands>\n<parameter=commands>\n[\"cd /config/workspace/iop && git status\"]\n</parameter>\n</function>\n</tool_call>"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "openai_compat"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"qwen3.6:35b",
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
|
||||
"tool_choice":"auto"
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
choice := resp.Choices[0]
|
||||
if choice.FinishReason != "tool_calls" {
|
||||
t.Fatalf("finish_reason: got %q, want tool_calls", choice.FinishReason)
|
||||
}
|
||||
if strings.Contains(choice.Message.Content, "<tool_call>") {
|
||||
t.Fatalf("content still contains raw tool_call block: %q", choice.Message.Content)
|
||||
}
|
||||
if choice.Message.Content != "먼저 현재 git 상태를 확인하겠습니다." {
|
||||
t.Fatalf("content: got %q", choice.Message.Content)
|
||||
}
|
||||
if len(choice.Message.ToolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(choice.Message.ToolCalls))
|
||||
}
|
||||
call, ok := choice.Message.ToolCalls[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("tool call shape: %+v", choice.Message.ToolCalls[0])
|
||||
}
|
||||
if call["id"] != "call_iop_run-test_1" {
|
||||
t.Fatalf("tool call id: got %+v", call["id"])
|
||||
}
|
||||
fn, ok := call["function"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("function shape: %+v", call["function"])
|
||||
}
|
||||
if fn["name"] != "run_commands" {
|
||||
t.Fatalf("function name: got %+v", fn["name"])
|
||||
}
|
||||
var args map[string][]string
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
if len(args["commands"]) != 1 || args["commands"][0] != "cd /config/workspace/iop && git status" {
|
||||
t.Fatalf("commands args: %+v", args["commands"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsLeavesUnknownTextToolCallAsContent(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=delete_everything><parameter=confirm>true</parameter></function></tool_call>"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "openai_compat"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"qwen3.6:35b",
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if resp.Choices[0].FinishReason == "tool_calls" || len(resp.Choices[0].Message.ToolCalls) != 0 {
|
||||
t.Fatalf("unexpected tool call conversion: %+v", resp.Choices[0])
|
||||
}
|
||||
if !strings.Contains(resp.Choices[0].Message.Content, "<tool_call>") {
|
||||
t.Fatalf("raw unknown tool call should remain content: %q", resp.Choices[0].Message.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsMapsMaxCompletionTokens(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
|
||||
|
|
@ -317,6 +406,41 @@ func TestChatCompletionsStreamsSSE(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamSynthesizesToolCallsFromClineTextBlock(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call>\n<function=run_commands>\n<parameter=commands>\n[\"cd /config/workspace/iop && git status\"]\n</parameter>\n</function>\n</tool_call>"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "openai_compat"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"finish_reason":"tool_calls"`) {
|
||||
t.Fatalf("stream did not synthesize tool_calls:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, "<tool_call>") || strings.Contains(body, `\u003ctool_call`) {
|
||||
t.Fatalf("stream leaked raw tool_call block:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"index":0`) {
|
||||
t.Fatalf("stream tool_call delta should include index:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"content":"먼저 현재 git 상태를 확인하겠습니다."`) {
|
||||
t.Fatalf("stream should preserve text before tool call:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamingReportsClosedRunStream(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
|
||||
close(fake.events)
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, re
|
|||
}},
|
||||
})
|
||||
|
||||
if outputPolicy.Strict && outputPolicy.StreamBuffer {
|
||||
s.streamBufferedChatCompletion(w, r, handle, id, created, model, flusher, outputPolicy)
|
||||
if len(req.Tools) > 0 || (outputPolicy.Strict && outputPolicy.StreamBuffer) {
|
||||
s.streamBufferedChatCompletion(w, r, handle, id, created, model, flusher, outputPolicy, req.Tools)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, re
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Server) streamBufferedChatCompletion(w http.ResponseWriter, r *http.Request, handle edgeservice.RunResult, id string, created int64, model string, flusher http.Flusher, outputPolicy strictOutputPolicy) {
|
||||
func (s *Server) streamBufferedChatCompletion(w http.ResponseWriter, r *http.Request, handle edgeservice.RunResult, id string, created int64, model string, flusher http.Flusher, outputPolicy strictOutputPolicy, tools []any) {
|
||||
text, reasoning, finishReason, _, err := collectRunResult(r.Context(), handle.Stream(), handle.WaitTimeout())
|
||||
if err != nil {
|
||||
writeSSEError(w, flusher, err.Error())
|
||||
|
|
@ -170,7 +170,31 @@ func (s *Server) streamBufferedChatCompletion(w http.ResponseWriter, r *http.Req
|
|||
zap.String("content_preview", previewString(text, 1000)),
|
||||
zap.String("reasoning_preview", previewString(reasoning, 1000)),
|
||||
)
|
||||
if text != "" {
|
||||
if cleaned, toolCalls := synthesizeToolCallsFromText(text, tools, handle.Dispatch().RunID); len(toolCalls) > 0 {
|
||||
if cleaned != "" {
|
||||
writeSSE(w, flusher, chatCompletionChunk{
|
||||
ID: id,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: created,
|
||||
Model: model,
|
||||
Choices: []chatCompletionChunkChoice{{
|
||||
Index: 0,
|
||||
Delta: chatDelta{Content: cleaned},
|
||||
}},
|
||||
})
|
||||
}
|
||||
writeSSE(w, flusher, chatCompletionChunk{
|
||||
ID: id,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: created,
|
||||
Model: model,
|
||||
Choices: []chatCompletionChunkChoice{{
|
||||
Index: 0,
|
||||
Delta: chatDelta{ToolCalls: toolCallsForStreamDelta(toolCalls)},
|
||||
}},
|
||||
})
|
||||
finishReason = "tool_calls"
|
||||
} else if text != "" {
|
||||
writeSSE(w, flusher, chatCompletionChunk{
|
||||
ID: id,
|
||||
Object: "chat.completion.chunk",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package openai
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -31,6 +33,7 @@ type chatMessage struct {
|
|||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
Images []any `json:"images,omitempty"`
|
||||
ToolCalls []any `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +44,7 @@ func (m *chatMessage) UnmarshalJSON(b []byte) error {
|
|||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
Images []any `json:"images,omitempty"`
|
||||
ToolCalls []any `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
|
|
@ -51,6 +55,7 @@ func (m *chatMessage) UnmarshalJSON(b []byte) error {
|
|||
m.ReasoningContent = raw.ReasoningContent
|
||||
m.Images = raw.Images
|
||||
m.ToolCalls = raw.ToolCalls
|
||||
m.ToolCallID = raw.ToolCallID
|
||||
m.ToolName = raw.ToolName
|
||||
return nil
|
||||
}
|
||||
|
|
@ -91,8 +96,10 @@ func (req chatCompletionRequest) runInput(prompt string, messages []chatMessage,
|
|||
}
|
||||
|
||||
func normalizeToolChoice(any) string {
|
||||
// IOP does not yet emit OpenAI tool_calls, so prevent downstream auto
|
||||
// tool selection from turning an otherwise valid chat request into a 400.
|
||||
// IOP can synthesize OpenAI tool_calls from recognized text tool-call
|
||||
// blocks, but it still cannot carry backend-native tool calls through the
|
||||
// runtime event stream. Keep downstream auto tool selection disabled so a
|
||||
// provider does not return structured calls that would be dropped.
|
||||
return "none"
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +139,9 @@ func chatMessagesInput(messages []chatMessage) []any {
|
|||
if len(msg.ToolCalls) > 0 {
|
||||
m["tool_calls"] = msg.ToolCalls
|
||||
}
|
||||
if msg.ToolCallID != "" {
|
||||
m["tool_call_id"] = msg.ToolCallID
|
||||
}
|
||||
if msg.ToolName != "" {
|
||||
m["tool_name"] = msg.ToolName
|
||||
}
|
||||
|
|
@ -142,6 +152,8 @@ func chatMessagesInput(messages []chatMessage) []any {
|
|||
|
||||
func contentToString(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return t
|
||||
case []any:
|
||||
|
|
@ -160,6 +172,153 @@ func contentToString(v any) string {
|
|||
}
|
||||
}
|
||||
|
||||
var (
|
||||
textToolCallBlockRE = regexp.MustCompile(`(?is)<\s*tool_call\s*>(.*?)<\s*/\s*tool_call\s*>`)
|
||||
textToolFunctionRE = regexp.MustCompile(`(?is)<\s*function\s*=\s*([A-Za-z_][A-Za-z0-9_.:-]*)\s*>(.*?)<\s*/\s*function(?:\s*=\s*[A-Za-z_][A-Za-z0-9_.:-]*)?\s*>`)
|
||||
textToolParameterRE = regexp.MustCompile(`(?is)<\s*parameter\s*=\s*([A-Za-z_][A-Za-z0-9_.:-]*)\s*>(.*?)<\s*/\s*parameter(?:\s*=\s*[A-Za-z_][A-Za-z0-9_.:-]*)?\s*>`)
|
||||
textToolCallOpeningHint = "<tool_call"
|
||||
)
|
||||
|
||||
func synthesizeToolCallsFromText(content string, tools []any, runID string) (string, []any) {
|
||||
allowed := requestedToolNames(tools)
|
||||
if len(allowed) == 0 || !strings.Contains(strings.ToLower(content), textToolCallOpeningHint) {
|
||||
return content, nil
|
||||
}
|
||||
matches := textToolCallBlockRE.FindAllStringSubmatchIndex(content, -1)
|
||||
if len(matches) == 0 {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
var cleaned strings.Builder
|
||||
cursor := 0
|
||||
toolCalls := make([]any, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
block := content[match[0]:match[1]]
|
||||
name, arguments, ok := parseTextToolCallBlock(block, allowed)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cleaned.WriteString(content[cursor:match[0]])
|
||||
cursor = match[1]
|
||||
toolCalls = append(toolCalls, map[string]any{
|
||||
"id": textToolCallID(runID, len(toolCalls)+1),
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": name,
|
||||
"arguments": arguments,
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(toolCalls) == 0 {
|
||||
return content, nil
|
||||
}
|
||||
cleaned.WriteString(content[cursor:])
|
||||
return strings.TrimSpace(cleaned.String()), toolCalls
|
||||
}
|
||||
|
||||
func textToolCallID(runID string, ordinal int) string {
|
||||
normalized := normalizeToolCallIDPart(runID)
|
||||
if normalized == "" {
|
||||
return fmt.Sprintf("call_iop_%d", ordinal)
|
||||
}
|
||||
return fmt.Sprintf("call_iop_%s_%d", normalized, ordinal)
|
||||
}
|
||||
|
||||
func normalizeToolCallIDPart(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
b.WriteRune(r)
|
||||
case r >= 'A' && r <= 'Z':
|
||||
b.WriteRune(r)
|
||||
case r >= '0' && r <= '9':
|
||||
b.WriteRune(r)
|
||||
case r == '_' || r == '-':
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return strings.Trim(b.String(), "_-")
|
||||
}
|
||||
|
||||
func toolCallsForStreamDelta(toolCalls []any) []any {
|
||||
out := make([]any, 0, len(toolCalls))
|
||||
for i, item := range toolCalls {
|
||||
call, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
out = append(out, item)
|
||||
continue
|
||||
}
|
||||
delta := make(map[string]any, len(call)+1)
|
||||
for k, v := range call {
|
||||
delta[k] = v
|
||||
}
|
||||
delta["index"] = i
|
||||
out = append(out, delta)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func requestedToolNames(tools []any) map[string]struct{} {
|
||||
names := make(map[string]struct{}, len(tools))
|
||||
for _, tool := range tools {
|
||||
m, ok := tool.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name, ok := m["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||
names[strings.TrimSpace(name)] = struct{}{}
|
||||
}
|
||||
fn, ok := m["function"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name, ok := fn["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||
names[strings.TrimSpace(name)] = struct{}{}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func parseTextToolCallBlock(block string, allowed map[string]struct{}) (string, string, bool) {
|
||||
match := textToolFunctionRE.FindStringSubmatchIndex(block)
|
||||
if match == nil {
|
||||
return "", "", false
|
||||
}
|
||||
name := strings.TrimSpace(block[match[2]:match[3]])
|
||||
if _, ok := allowed[name]; !ok {
|
||||
return "", "", false
|
||||
}
|
||||
body := block[match[4]:match[5]]
|
||||
args := map[string]any{}
|
||||
for _, param := range textToolParameterRE.FindAllStringSubmatchIndex(body, -1) {
|
||||
key := strings.TrimSpace(body[param[2]:param[3]])
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
args[key] = parseTextToolParameterValue(body[param[4]:param[5]])
|
||||
}
|
||||
encoded, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
return name, string(encoded), true
|
||||
}
|
||||
|
||||
func parseTextToolParameterValue(raw string) any {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
var decoded any
|
||||
if err := json.Unmarshal([]byte(trimmed), &decoded); err == nil {
|
||||
return decoded
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func previewString(s string, max int) string {
|
||||
if max <= 0 || len(s) <= max {
|
||||
return s
|
||||
|
|
@ -208,6 +367,7 @@ type chatDelta struct {
|
|||
Role string `json:"role,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
ToolCalls []any `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
type openAIUsage struct {
|
||||
|
|
|
|||
|
|
@ -355,12 +355,20 @@ func messagesFromInput(input map[string]any) []chatMessage {
|
|||
}
|
||||
role, _ := m["role"].(string)
|
||||
content, _ := m["content"].(string)
|
||||
toolCallID, _ := m["tool_call_id"].(string)
|
||||
toolCalls := anySlice(m["tool_calls"])
|
||||
role = strings.TrimSpace(role)
|
||||
content = strings.TrimSpace(content)
|
||||
if role == "" || content == "" {
|
||||
toolCallID = strings.TrimSpace(toolCallID)
|
||||
if role == "" || (content == "" && len(toolCalls) == 0) {
|
||||
continue
|
||||
}
|
||||
out = append(out, chatMessage{Role: role, Content: content})
|
||||
out = append(out, chatMessage{
|
||||
Role: role,
|
||||
Content: content,
|
||||
ToolCalls: toolCalls,
|
||||
ToolCallID: toolCallID,
|
||||
})
|
||||
}
|
||||
if len(out) > 0 {
|
||||
return out
|
||||
|
|
@ -391,6 +399,13 @@ func stringInput(input map[string]any, key string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func anySlice(v any) []any {
|
||||
if items, ok := v.([]any); ok {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// joinOpenAIPath appends an OpenAI-compatible path to the endpoint. When the
|
||||
// endpoint already targets the /v1 root it strips the leading /v1 from the path
|
||||
// so a configured ".../v1" endpoint does not produce ".../v1/v1/...".
|
||||
|
|
@ -413,8 +428,10 @@ func readLimited(r io.Reader, limit int64) string {
|
|||
}
|
||||
|
||||
type chatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ToolCalls []any `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
}
|
||||
|
||||
type chatChunk struct {
|
||||
|
|
|
|||
|
|
@ -321,6 +321,61 @@ func TestOpenAICompatExecutePassesToolsAndToolChoice(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatExecutePreservesToolCallMessages(t *testing.T) {
|
||||
var body map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
||||
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
adapter := New(config.OpenAICompatConf{Endpoint: server.URL}, zap.NewNop())
|
||||
sink := &fakeSink{}
|
||||
if err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
||||
RunID: "run-tool-turn",
|
||||
Target: "qwen3.6:35b",
|
||||
Input: map[string]any{
|
||||
"messages": []any{
|
||||
map[string]any{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": []any{map[string]any{
|
||||
"id": "call_iop_1",
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
"arguments": `{"commands":["git status"]}`,
|
||||
},
|
||||
}},
|
||||
},
|
||||
map[string]any{
|
||||
"role": "tool",
|
||||
"content": "clean",
|
||||
"tool_call_id": "call_iop_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, sink); err != nil {
|
||||
t.Fatalf("Execute failed: %v", err)
|
||||
}
|
||||
messages, ok := body["messages"].([]any)
|
||||
if !ok || len(messages) != 2 {
|
||||
t.Fatalf("messages not preserved: %+v", body["messages"])
|
||||
}
|
||||
assistantMsg := messages[0].(map[string]any)
|
||||
if calls, ok := assistantMsg["tool_calls"].([]any); !ok || len(calls) != 1 {
|
||||
t.Fatalf("assistant tool_calls not preserved: %+v", assistantMsg)
|
||||
}
|
||||
toolMsg := messages[1].(map[string]any)
|
||||
if toolMsg["tool_call_id"] != "call_iop_1" {
|
||||
t.Fatalf("tool_call_id not preserved: %+v", toolMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatExecuteRejectsEmptyEndpointOrModel(t *testing.T) {
|
||||
t.Run("empty_endpoint", func(t *testing.T) {
|
||||
adapter := New(config.OpenAICompatConf{}, zap.NewNop())
|
||||
|
|
|
|||
Loading…
Reference in a new issue