mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Comments, doc comments, UI labels, log and error messages, LLM prompt strings, markdown docs, test data and assertions. Identifiers, enum keys, routes, placeholders and code structure are unchanged. Notes: - docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md - tests that exercised CJK specific handling (GBK zip names, Chinese full text search) now use ASCII equivalents; go build passes and the test suite matches the pre translation baseline (remaining failures need a PostgreSQL DSN and fail identically on the original) - frontend mention syntax now uses English kinds (finding, asset, ...) matching the backend, chat mentions tests pass
130 lines
4.8 KiB
Go
130 lines
4.8 KiB
Go
package db
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
// LLM retry policy: the global "attempts + interval" configuration for the five retry
|
|
// layers; see the LLM retry design doc.
|
|
// Stored as one JSON value in the settings table — it is one machine-wide runtime knob,
|
|
// not worth its own table; reads fall back to built-in defaults, so a missing key
|
|
// (fresh database / never configured) behaves exactly like the hard-coded-constant era.
|
|
|
|
const settingLLMRetryPolicy = "llm_retry_policy"
|
|
|
|
// RetryRule is one layer's knob pair. The zero value means "unset":
|
|
//
|
|
// Attempts 0 = use the built-in default attempts; -1 = disable this layer's retry; >0 = use this value
|
|
// IntervalMS 0 = keep this layer's original interval policy (usually exponential backoff); >0 = use this fixed millisecond interval
|
|
//
|
|
// -1 means "explicitly off" rather than "0 attempts", because 0 is already taken by "unset".
|
|
type RetryRule struct {
|
|
Attempts int `json:"attempts"`
|
|
IntervalMS int `json:"interval_ms"`
|
|
}
|
|
|
|
// Interval returns the configured fixed interval, or 0 when unset (caller keeps
|
|
// its own default ladder).
|
|
func (r RetryRule) Interval() time.Duration {
|
|
if r.IntervalMS <= 0 {
|
|
return 0
|
|
}
|
|
return time.Duration(r.IntervalMS) * time.Millisecond
|
|
}
|
|
|
|
// Or returns the rule with each unset field filled in from fallback. Used to
|
|
// layer a profile override on top of the global policy field by field, so a
|
|
// profile that only pins the interval still inherits the global count.
|
|
func (r RetryRule) Or(fallback RetryRule) RetryRule {
|
|
if r.Attempts == 0 {
|
|
r.Attempts = fallback.Attempts
|
|
}
|
|
if r.IntervalMS == 0 {
|
|
r.IntervalMS = fallback.IntervalMS
|
|
}
|
|
return r
|
|
}
|
|
|
|
// retry knob bounds. A count above the cap turns a blip into a token bonfire;
|
|
// an interval above an hour outlives any transient failure worth waiting out.
|
|
const (
|
|
maxRetryAttempts = 20
|
|
maxRetryIntervalMS = 3600_000 // 1h
|
|
)
|
|
|
|
// Clamped returns the rule with out-of-range values pulled back into the sane
|
|
// band (attempts within [-1, 20], interval within [0, 1h]).
|
|
func (r RetryRule) Clamped() RetryRule {
|
|
if r.Attempts < -1 {
|
|
r.Attempts = -1
|
|
}
|
|
if r.Attempts > maxRetryAttempts {
|
|
r.Attempts = maxRetryAttempts
|
|
}
|
|
if r.IntervalMS < 0 {
|
|
r.IntervalMS = 0
|
|
}
|
|
if r.IntervalMS > maxRetryIntervalMS {
|
|
r.IntervalMS = maxRetryIntervalMS
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Clamped bounds a profile's override the same way the global policy is bounded,
|
|
// so a hand-crafted API payload can't land a value the CHECK constraint rejects.
|
|
func (o RetryOverride) Clamped() RetryOverride {
|
|
o.Connect, o.Empty, o.Stream = o.Connect.Clamped(), o.Empty.Clamped(), o.Stream.Clamped()
|
|
return o
|
|
}
|
|
|
|
// LLMRetryPolicy holds the five-layer retry configuration. Connect/Empty/Stream are the
|
|
// per-request layers (a profile may override them, see LLMProfile.Retry);
|
|
// Breaker and Intent are process-wide by nature and live only here.
|
|
type LLMRetryPolicy struct {
|
|
// Connect: SDK connect retry (connection reset/timeout/429/5xx, before the stream starts). Default 3 attempts, exponential backoff.
|
|
Connect RetryRule `json:"connect"`
|
|
// Empty: SDK empty-response retry (completed but no content block, openai format only). Default 2 attempts, exponential backoff.
|
|
Empty RetryRule `json:"empty"`
|
|
// Stream: same-provider safe-window retry (replay of a broken stream before output was delivered). Default 2 attempts, exponential from 0.5s (capped at 4s).
|
|
Stream RetryRule `json:"stream"`
|
|
// Breaker: rotation breaker. Attempts=consecutive transient failures before tripping (default 3; -1=never trip on
|
|
// transient failures; hard failures like insufficient balance/invalid key still trip immediately); IntervalMS=fixed cooldown (0=default 1/5/30min ladder).
|
|
Breaker RetryRule `json:"breaker"`
|
|
// Intent: full-intent rerun after a worker ends with model_error. Default 2 attempts, fixed 3s.
|
|
Intent RetryRule `json:"intent"`
|
|
}
|
|
|
|
// Clamped returns the policy with every rule clamped.
|
|
func (p LLMRetryPolicy) Clamped() LLMRetryPolicy {
|
|
p.Connect, p.Empty, p.Stream = p.Connect.Clamped(), p.Empty.Clamped(), p.Stream.Clamped()
|
|
p.Breaker, p.Intent = p.Breaker.Clamped(), p.Intent.Clamped()
|
|
return p
|
|
}
|
|
|
|
// LLMRetryPolicy reads the global retry policy. A missing or unparseable value
|
|
// yields the zero policy — i.e. every layer on its built-in default.
|
|
func (d *DB) LLMRetryPolicy() LLMRetryPolicy {
|
|
var p LLMRetryPolicy
|
|
if d == nil {
|
|
return p
|
|
}
|
|
raw, ok, err := d.GetSetting(settingLLMRetryPolicy)
|
|
if err != nil || !ok || raw == "" {
|
|
return p
|
|
}
|
|
if err := json.Unmarshal([]byte(raw), &p); err != nil {
|
|
return LLMRetryPolicy{}
|
|
}
|
|
return p.Clamped()
|
|
}
|
|
|
|
// SetLLMRetryPolicy persists the global retry policy (values are clamped first).
|
|
func (d *DB) SetLLMRetryPolicy(p LLMRetryPolicy) error {
|
|
raw, err := json.Marshal(p.Clamped())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return d.SetSetting(settingLLMRetryPolicy, string(raw))
|
|
}
|