sync: routing policy model orchestration updates and node implementation

This commit is contained in:
toki 2026-07-08 10:21:10 +09:00
parent 3348b64284
commit bc966f8cb3
5 changed files with 99 additions and 16 deletions

View file

@ -13,7 +13,7 @@ OpenAI-compatible 추론 엔진(vLLM, SGLang, Lemonade, OpenAI-compatible API pr
## 상태
[진행중]
[완료]
## 승격 조건
@ -68,22 +68,24 @@ OpenAI-compatible provider의 원본 응답을 정규화/재조립하지 않고
raw bytes는 전송 source of truth로 쓰되, 운영 관측과 저장은 사람이 읽을 수 있는 조립 결과와 구조화된 sideband를 기준으로 한다.
- [ ] [assembled-log] 실행 로그에는 raw chunk payload 대신 assembled `content`, `reasoning`, `tool_calls`, sideband summary, transformed output을 남긴다. 검증: log fixture가 readable assembled string과 구조화 관측값을 포함하고 raw body chunk를 기본 저장하지 않는다.
- [ ] [backpressure-cancel] tunnel path는 기존 socket 위에서 ordered, lossless, backpressure-aware stream을 유지하고 cancel/control frame을 지연시키지 않는다. 검증: slow client, cancel, provider error test에서 chunk drop/reorder 없이 upstream request가 중단된다.
- [x] [assembled-log] 실행 로그에는 raw chunk payload 대신 assembled `content`, `reasoning`, `tool_calls`, sideband summary, transformed output을 남긴다. 검증: log fixture가 readable assembled string과 구조화 관측값을 포함하고 raw body chunk를 기본 저장하지 않는다.
- [x] [backpressure-cancel] tunnel path는 기존 socket 위에서 ordered, lossless, backpressure-aware stream을 유지하고 cancel/control frame을 지연시키지 않는다. 검증: slow client, cancel, provider error test에서 chunk drop/reorder 없이 upstream request가 중단된다.
- [x] [contract-docs] agent-contract, agent-spec, config/docs 예시에 raw tunnel, response mode, sideband/transformed semantics, legacy normalized path migration을 반영한다. 검증: contract/spec 링크와 코드 test fixture가 같은 mode 이름과 default를 사용한다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 기능 Task가 아직 충족되지 않았다.
- 상태: 통과
- 요청일: 2026-07-08
- 완료 근거: 기능 Task 9개와 SDD S01-S11 evidence를 같은 milestone task group의 archive `complete.log` 5개, 코드 감사, 최종 검증으로 확인했다.
- 검토 항목:
- [ ] 구현 결과가 pure `passthrough` 기본값과 provider-original byte stream을 보장한다.
- [ ] sideband/transformed mode가 opt-in으로만 동작한다.
- [ ] raw tunnel이 기존 Edge-Node socket 위에서 lossless/backpressure-aware하게 동작한다.
- [ ] archive 이동을 승인했다.
- [x] 구현 결과가 pure `passthrough` 기본값과 provider-original byte stream을 보장한다.
- [x] sideband/transformed mode가 opt-in으로만 동작한다.
- [x] raw tunnel이 기존 Edge-Node socket 위에서 lossless/backpressure-aware하게 동작한다.
- [x] archive 이동을 승인했다.
- agent-ui 상태 반영: 해당 없음
- 리뷰 코멘트: 없음
- Spec sync: not-needed - [openai-compatible-surface.md](../../../../../agent-spec/input/openai-compatible-surface.md)와 [edge-node-execution.md](../../../../../agent-spec/runtime/edge-node-execution.md)는 raw tunnel, response mode, cancel 경계를 이미 반영하며 이번 보완은 spec/contract와 구현을 맞춘 코드 수정이다.
- Workspace 잠금: 관련 lock 없음
- 리뷰 코멘트: 코드 감사 중 Node provider tunnel이 `CancelRequest(CANCEL_RUN)`으로 취소되지 않는 S09 회귀를 발견해 `apps/node/internal/node/node.go``apps/node/internal/node/node_test.go`에서 즉시 보완했다. `go test ./...`, targeted Node/Edge/race 테스트, `make test-e2e`, `git diff --check`로 종료 가능 상태를 확인했다.
## 범위 제외

View file

@ -16,8 +16,8 @@ IOP의 OpenAI-compatible, A2A, IOP native 입력 표면에서 들어온 요청
완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다.
스케치 Milestone은 아직 구현 가능한 계획이 아니므로 계획 Milestone보다 아래에 둔다.
- [진행중] OpenAI-compatible Raw Tunnel과 Sideband Passthrough
- 경로: [openai-compatible-raw-tunnel-sideband-passthrough](milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- [완료] OpenAI-compatible Raw Tunnel과 Sideband Passthrough
- 경로: [openai-compatible-raw-tunnel-sideband-passthrough](../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- 요약: OpenAI-compatible provider 응답을 기존 Edge-Node proto-socket 위 lossless raw tunnel로 전달하고, 기본값은 provider-original `passthrough`로 두며, 요청이 명시한 경우에만 `passthrough+sideband` 또는 `transformed`를 사용한다.
- [스케치] OpenAI-compatible 하이브리드 라우팅과 컨텍스트 최적화

View file

@ -269,12 +269,23 @@ func (n *Node) OnProviderTunnelRequest(ctx context.Context, sess *transport.Sess
nodeAlias: nodeAlias,
}
execCtx := ctx
execCtx, cancel := context.WithCancel(ctx)
if tr.TimeoutSec > 0 {
var cancel context.CancelFunc
execCtx, cancel = context.WithTimeout(ctx, time.Duration(tr.TimeoutSec)*time.Second)
defer cancel()
}
defer cancel()
h := &runHandle{
runID: tr.RunID,
adapter: tr.Adapter,
target: tr.Target,
sessionID: normalizeSessionID(tr.SessionID),
cancel: cancel,
done: make(chan struct{}),
}
n.runs.register(h)
defer n.runs.deregister(tr.RunID)
defer close(h.done)
if err := tunnelAdapter.TunnelProvider(execCtx, tr, sink); err != nil {
n.logger.Warn("provider tunnel error",

View file

@ -3077,6 +3077,23 @@ func (a *mockTunnelAdapter) TunnelProvider(ctx context.Context, req runtime.Prov
return err
}
type cancelAwareTunnelAdapter struct {
countingAdapter
started chan struct{}
observedCancel chan struct{}
}
func (a *cancelAwareTunnelAdapter) Name() string { return "openai_compat" }
func (a *cancelAwareTunnelAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
return runtime.Capabilities{AdapterName: "openai_compat", Targets: []string{"qwen"}}, nil
}
func (a *cancelAwareTunnelAdapter) TunnelProvider(ctx context.Context, _ runtime.ProviderTunnelRequest, _ runtime.ProviderTunnelSink) error {
close(a.started)
<-ctx.Done()
close(a.observedCancel)
return ctx.Err()
}
func TestNodeOnProviderTunnelRequest_Success(t *testing.T) {
mta := &mockTunnelAdapter{
t: t,
@ -3104,6 +3121,59 @@ func TestNodeOnProviderTunnelRequest_Success(t *testing.T) {
}
}
func TestNodeOnProviderTunnelRequest_CancelRequestCancelsProviderContext(t *testing.T) {
adapter := &cancelAwareTunnelAdapter{
started: make(chan struct{}),
observedCancel: make(chan struct{}),
}
router := &fixedRouter{adapterName: "openai_compat", adapters: map[string]runtime.Adapter{"openai_compat": adapter}}
n, _ := makeNode(t, router)
req := &iop.ProviderTunnelRequest{
RunId: "run-tunnel-cancel",
TunnelId: "tunnel-cancel",
Adapter: "openai_compat",
Target: "qwen",
Method: "POST",
Path: "/v1/chat/completions",
TimeoutSec: 30,
}
errCh := make(chan error, 1)
go func() {
errCh <- n.OnProviderTunnelRequest(context.Background(), nil, req)
}()
select {
case <-adapter.started:
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for tunnel adapter to start")
}
err := n.OnCancel(context.Background(), nil, &iop.CancelRequest{
RunId: "run-tunnel-cancel",
Action: iop.CancelAction_CANCEL_ACTION_CANCEL_RUN,
})
if err != nil {
t.Fatalf("OnCancel failed: %v", err)
}
select {
case <-adapter.observedCancel:
case <-time.After(2 * time.Second):
t.Fatal("tunnel adapter did not observe cancel request")
}
select {
case err := <-errCh:
if !errors.Is(err, context.Canceled) {
t.Fatalf("OnProviderTunnelRequest error = %v, want context.Canceled", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for tunnel request to finish after cancel")
}
}
func buildSessionTestPipeForNode(t *testing.T) (edgeSide *toki.TcpClient, sess *transport.Session) {
t.Helper()
edgeConn, nodeConn := net.Pipe()