Merge branch 'release/dev-1008' into dev

This commit is contained in:
toki 2026-08-14 02:46:51 +09:00
commit e86b47e326
5 changed files with 132 additions and 3 deletions

View file

@ -51,7 +51,7 @@ tracked config에는 public 예시와 기본 구조만 두고, 실제 endpoint/c
- `ConcreteProtocolProfile.ResolveOperationURL(op)`는 완성된 resolved upstream URL을 반환한다. absolute operation URL은 그대로 보존하며 relative operation path는 normalized base URL에 1회 join된다. 표기된 `/v1/...` 값은 return value가 아니라 operation-path input이다 (`models` → `GET /v1/models` 또는 `GET /anthropic/v1/models`, `chat_completions``POST /v1/chat/completions`, `messages``POST /v1/messages`, `count_tokens``POST /v1/messages/count_tokens`, `responses``POST /v1/responses`).
- `validOperationsByDriver`는 driver별 허용 operation의 closed set이다. `openai_chat``models`, `chat_completions`, `responses`, `count_tokens`를 허용한다. `anthropic_messages``models`, `messages`, `count_tokens`를 허용한다. `openai_responses``models`, `responses`, `count_tokens`를 허용한다.
- `openai.provider_auth` is a legacy-mode-only request-time raw provider token forwarding rule. `enabled=false` is the default; when enabled in legacy mode, omitted fields resolve to `from_header=X-IOP-Provider-Authorization`, `target_header=Authorization`, `scheme=Bearer`, and `required=true`. Managed mode rejects this configuration and rejects a caller-supplied legacy provider credential header.
- `openai.stream_evidence_gate` configures request-local Recovery Coordinator limits, the ingress snapshot bound, and optional semantic policy. Every supported Chat Completions, normalized Responses, provider tunnel, provider-pool, and tool-validation response already uses the `packages/go/streamgate` request runtime as its sole liveness owner. `enabled` defaults to false and controls only configured semantic filter registration/capability admission; false preserves endpoint-native compatibility inside the same runtime and does not restore a legacy response or retry owner. `max_request_fault_recovery`는 요청당 전체 fault recovery 상한(`0..3`, omitted 기본값 3, explicit 0은 모든 fault recovery 비활성화)이다. `max_strategy_fault_recovery`는 fault strategy(exact_replay/continuation_repair/schema_repair)별 상한(`0..max_request_fault_recovery`, omitted 기본값은 effective request total 상속, explicit 0은 해당 strategy 비활성화)이며 request-start 시점에 immutable runtime option snapshot으로 각 fault strategy에 동일하게 적용된다. `max_ingress_snapshot_bytes`는 ingress snapshot 바이트 상한(`1..16777216` [16 MiB], omitted/0 기본값 16 MiB)이다. `environment`는 request-start selector snapshot이며 `dev|dev-corp`만 허용하고 omitted 기본값은 `dev`다. `filters[]`는 unique `filter` (`repeat_guard|schema_gate|provider_error`) policy이다. `enabled` omitted=true, `enforcement` omitted=`blocking`, `capability` omitted=`output.<filter>`, `hold_evidence_runes` omitted=500, `timeout_ms` omitted=5000으로 정규화하며 selector는 `environment|model_group|model|provider`로만 filter enablement/enforcement를 보정한다. base-disabled filter도 registry snapshot에 남아 더 구체적인 selector가 활성화할 수 있고, 실제 target에서 활성화된 `blocking` filter만 provider capability admission에 참여한다. `observe_only`는 evidence를 만들지만 admission을 막지 않는다. `repeat_guard` uses the configured rune bound for active request-local history/current-stream inspection and stores only bounded fingerprints, counts, and offsets in its semantic snapshot and observations. `schema_gate` and `provider_error` remain lifecycle foundations until their matcher Tasks; an unmatched provider error never creates exact replay. Config accepts no caller/agent selector.
- `openai.stream_evidence_gate` configures request-local Recovery Coordinator limits, the ingress snapshot bound, and optional semantic policy. Every supported Chat Completions, normalized Responses, provider tunnel, provider-pool, and tool-validation response already uses the `packages/go/streamgate` request runtime as its sole liveness owner. `enabled` defaults to false and controls only configured semantic filter registration/capability admission; false preserves endpoint-native compatibility inside the same runtime and does not restore a legacy response or retry owner. `max_request_fault_recovery`는 요청당 전체 fault recovery 상한(`0..3`, omitted 기본값 3, explicit 0은 모든 fault recovery 비활성화)이다. `max_strategy_fault_recovery`는 fault strategy(exact_replay/continuation_repair/schema_repair)별 상한(`0..max_request_fault_recovery`, omitted 기본값은 effective request total 상속, explicit 0은 해당 strategy 비활성화)이며 request-start 시점에 immutable runtime option snapshot으로 각 fault strategy에 동일하게 적용된다. base 또는 selector에서 활성화될 수 있는 blocking `repeat_guard`는 continuation recovery를 생성하므로 두 recovery 상한이 모두 최소 1이어야 하며, 모순된 설정은 load 시 거부한다. `max_ingress_snapshot_bytes`는 ingress snapshot 바이트 상한(`1..16777216` [16 MiB], omitted/0 기본값 16 MiB)이다. `environment`는 request-start selector snapshot이며 `dev|dev-corp`만 허용하고 omitted 기본값은 `dev`다. `filters[]`는 unique `filter` (`repeat_guard|schema_gate|provider_error`) policy이다. `enabled` omitted=true, `enforcement` omitted=`blocking`, `capability` omitted=`output.<filter>`, `hold_evidence_runes` omitted=500, `timeout_ms` omitted=5000으로 정규화하며 selector는 `environment|model_group|model|provider`로만 filter enablement/enforcement를 보정한다. base-disabled filter도 registry snapshot에 남아 더 구체적인 selector가 활성화할 수 있고, 실제 target에서 활성화된 `blocking` filter만 provider capability admission에 참여한다. `observe_only`는 evidence를 만들지만 admission을 막지 않는다. `repeat_guard` uses the configured rune bound for active request-local history/current-stream inspection and stores only bounded fingerprints, counts, and offsets in its semantic snapshot and observations. `schema_gate` and `provider_error` remain lifecycle foundations until their matcher Tasks; an unmatched provider error never creates exact replay. Config accepts no caller/agent selector.
- `openai.stream_evidence_gate` 설정은 request-start 시점에 snapshot으로 고정되며 in-flight request의 실행 중 refresh 영향에서 격리된다 (generation isolation). 새 generation의 설정은 이후 시작되는 새 request에만 적용된다.
- The internal `response_stalled` recovery registration is always present for a supported OpenAI runtime request. It is not a member of `filters[]`, has no configurable capability, and does not participate in provider capability admission. It consumes only an Edge-confirmed typed handoff; configurable `provider_error` keeps its generic foundation behavior.
- The request-start `models[].context_window_tokens` snapshot is the resume builder's target context bound. Each Chat/Responses runtime shares one request-local content/reasoning recorder across its initial and recovery event sources. A continuation rebuild uses only that recorder and the fixed directive; unknown or exceeded context rejects the rebuild before re-admission. An omitted caller temperature selects `0.2`, `0.4`, then `0.6` by continuation strategy attempt, while an explicit value is preserved. Recorder state and its raw values remain request-local, are consumed once per attempt, and are never added to config refresh state or observations. Repeat history and counters are pinned to the same request-start config generation and are not refreshable TTL/session state.

View file

@ -110,7 +110,7 @@ sequenceDiagram
## 설정/데이터/이벤트
- `openai.stream_evidence_gate.enabled` defaults to `false` and controls only configured semantic filters and their capability admission. The Core owns the supported response/liveness lifecycle in both states, while disabled mode preserves endpoint-native compatibility through runtime adapters.
- `max_request_fault_recovery`는 0..3, `max_strategy_fault_recovery`는 0..request-total이고 생략 시 request-total을 상속한다.
- `max_request_fault_recovery`는 0..3, `max_strategy_fault_recovery`는 0..request-total이고 생략 시 request-total을 상속한다. base 또는 selector에서 활성화될 수 있는 blocking `repeat_guard`는 두 상한이 모두 최소 1이어야 한다.
- `max_ingress_snapshot_bytes`는 1..16777216이며 생략 시 16 MiB다. raw body limit은 첫 read 전에 적용되고 canonical body, typed view와 rebuild peak가 같은 request-local ledger에 포함된다.
- Stream Evidence Gate 설정 변경은 현재 restart-required다. request가 시작된 뒤 config/registry snapshot은 바뀌지 않는다.
- The production Core registry includes the common Noop filter, configured active `repeat_guard`, schema/provider-error lifecycle foundations, and applicable request-local tool validation. Repeat detection uses the configured 500-rune default, never time-based release, and returns a continuation only before a tool/side-effect boundary. Provider-error still records unmatched errors as pass until its matcher Task.

View file

@ -83,7 +83,7 @@ Agent dispatcher silence safety = 70000
Edge request hard timeout > 70000
```
dev-runtime은 `max_request_fault_recovery: 0``max_strategy_fault_recovery: 0`을 명시하고 Pi agent retry도 비활성화한다. 따라서 provider 무진행은 Node가 60초에 한 번만 종료하며 Pi·Edge·Dispatcher가 같은 요청을 다시 중첩 실행하지 않는다.
dev-runtime은 blocking `repeat_guard`와 함께 `max_request_fault_recovery: 1`, `max_strategy_fault_recovery: 1`을 사용하고 Pi agent retry는 비활성화한다. 따라서 반복 출력에는 Stream Evidence Gate가 요청 내부 continuation recovery를 최대 한 번 수행하고, 외부 Pi 재시도나 중첩 요청은 만들지 않는다.
`response_stall_timeout_ms` 변경은 restart-required다. `config check``config refresh --mode dry-run`에서 이를 확인한 뒤 Edge와 Node를 같은 source ref로 rebuild/restart하고, 각 binary의 build identity와 실행 중인 process identity를 다시 대조한다. tracked 검증 근거에는 source/build/config 식별자, 단조 시간, terminal 개수와 결과 분류만 남기며 prompt, output, token, credential 원문은 기록하지 않는다.

View file

@ -540,6 +540,11 @@ func (s StreamEvidenceGateConf) Validate() error {
if effStrategy > effTotal {
return fmt.Errorf("max_strategy_fault_recovery (%d) must not exceed max_request_fault_recovery (%d)", effStrategy, effTotal)
}
for i, filter := range s.Filters {
if s.Enabled && filter.Filter == StreamGateFilterRepeatGuard && filterCanResolveBlocking(filter) && (effTotal == 0 || effStrategy == 0) {
return fmt.Errorf("stream_evidence_gate filters[%d]: blocking repeat_guard requires max_request_fault_recovery and max_strategy_fault_recovery to be at least 1", i)
}
}
effIngress := s.EffectiveMaxIngressSnapshotBytes()
maxAllowedIngress := 16 * 1024 * 1024
@ -561,6 +566,29 @@ func (s StreamEvidenceGateConf) Validate() error {
return nil
}
func filterCanResolveBlocking(filter StreamGateFilterPolicyConf) bool {
if filter.EffectiveEnabled() && filter.EffectiveEnforcement() == StreamGateFilterEnforcementBlocking {
return true
}
for _, selector := range filter.Selectors {
enabled := filter.EffectiveEnabled()
if selector.Enabled != nil {
enabled = *selector.Enabled
}
if !enabled {
continue
}
enforcement := filter.EffectiveEnforcement()
if selector.Enforcement != "" {
enforcement = selector.Enforcement
}
if enforcement == StreamGateFilterEnforcementBlocking {
return true
}
}
return false
}
// EdgeOpenAIProviderAuthConf configures request-time provider token forwarding.
// It only describes which caller header to forward and never stores raw token
// values in config.

View file

@ -406,6 +406,107 @@ openai:
}
}
func TestStreamEvidenceGate_BlockingRepeatGuardRequiresRecoveryBudget(t *testing.T) {
boolPtr := func(value bool) *bool { return &value }
zero, one := 0, 1
tests := []struct {
name string
gate config.StreamEvidenceGateConf
expectErr bool
}{
{
name: "blocking base with zero budget",
gate: config.StreamEvidenceGateConf{
Enabled: true,
MaxRequestFaultRecovery: &zero, MaxStrategyFaultRecovery: &zero,
Filters: []config.StreamGateFilterPolicyConf{{Filter: config.StreamGateFilterRepeatGuard}},
},
expectErr: true,
},
{
name: "blocking selector with zero budget",
gate: config.StreamEvidenceGateConf{
Enabled: true,
MaxRequestFaultRecovery: &zero, MaxStrategyFaultRecovery: &zero,
Filters: []config.StreamGateFilterPolicyConf{{
Filter: config.StreamGateFilterRepeatGuard, Enabled: boolPtr(false),
Selectors: []config.StreamGateFilterSelectorConf{{
Type: config.StreamGateFilterSelectorModelGroup, Key: "ornith:35b", Enabled: boolPtr(true),
}},
}},
},
expectErr: true,
},
{
name: "blocking selector with one recovery",
gate: config.StreamEvidenceGateConf{
Enabled: true,
MaxRequestFaultRecovery: &one, MaxStrategyFaultRecovery: &one,
Filters: []config.StreamGateFilterPolicyConf{{
Filter: config.StreamGateFilterRepeatGuard, Enabled: boolPtr(false),
Selectors: []config.StreamGateFilterSelectorConf{{
Type: config.StreamGateFilterSelectorModelGroup, Key: "ornith:35b", Enabled: boolPtr(true),
}},
}},
},
},
{
name: "observe only with zero budget",
gate: config.StreamEvidenceGateConf{
Enabled: true,
MaxRequestFaultRecovery: &zero, MaxStrategyFaultRecovery: &zero,
Filters: []config.StreamGateFilterPolicyConf{{
Filter: config.StreamGateFilterRepeatGuard, Enforcement: config.StreamGateFilterEnforcementObserveOnly,
}},
},
},
{
name: "blocking selector enforcement with zero budget",
gate: config.StreamEvidenceGateConf{
Enabled: true,
MaxRequestFaultRecovery: &zero, MaxStrategyFaultRecovery: &zero,
Filters: []config.StreamGateFilterPolicyConf{{
Filter: config.StreamGateFilterRepeatGuard, Enforcement: config.StreamGateFilterEnforcementObserveOnly,
Selectors: []config.StreamGateFilterSelectorConf{{
Type: config.StreamGateFilterSelectorModelGroup, Key: "ornith:35b", Enforcement: config.StreamGateFilterEnforcementBlocking,
}},
}},
},
expectErr: true,
},
{
name: "disabled with zero budget",
gate: config.StreamEvidenceGateConf{
Enabled: true,
MaxRequestFaultRecovery: &zero, MaxStrategyFaultRecovery: &zero,
Filters: []config.StreamGateFilterPolicyConf{{
Filter: config.StreamGateFilterRepeatGuard, Enabled: boolPtr(false),
}},
},
},
{
name: "disabled gate with blocking filter and zero budget",
gate: config.StreamEvidenceGateConf{
Enabled: false,
MaxRequestFaultRecovery: &zero, MaxStrategyFaultRecovery: &zero,
Filters: []config.StreamGateFilterPolicyConf{{Filter: config.StreamGateFilterRepeatGuard}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.gate.Validate()
if tt.expectErr && err == nil {
t.Fatal("Validate() succeeded, want recovery-budget error")
}
if !tt.expectErr && err != nil {
t.Fatalf("Validate() failed: %v", err)
}
})
}
}
func TestStreamEvidenceGate_IngressSnapshotBytes_TableFixture(t *testing.T) {
maxAllowed := 16 * 1024 * 1024 // 16777216