feat(fallbackpolicy): three-state threshold policy for upstream fallback - #4
Merged
damonleelcx merged 1 commit intoJul 31, 2026
Merged
Conversation
…thresholds
[pkg] Foundation for P1b of openspec change `aliyun-aigw-p0-upstream-fallback`
(tasks 0.14 / 1b.2 / 1b.3 / 1b.6 / 1b.7).
Lives in pkg/ for the same reason routingwire does: the control plane emits these
values and the proxy consumes them, so a private copy on each side is a wire that can
drift silently. One definition, two importers.
🔴 The three-state rule (I22) is the whole point. `unset` / `0` / `a concrete value`
are THREE states, so every field is a POINTER and there is no plain int64 getter that
could paper over nil. The incident this prevents: treating unset as 0 means that on
upgrade every organization instantly gets cooldown=0 (every request retries a
known-dead upstream first) and budget=0 (every request fails immediately) — and
nothing looks broken in the console, because the numbers really are zero.
Zero is not uniformly legal, and the split is behavioral rather than stylistic:
cooldown=0 / idle_gap=0 / stickiness=0 ✅ real preferences (never cool down,
always probe, never stick)
attempt_timeout=0 🚫 an upstream given zero ms can never answer
chain_budget=0 🚫 every request fails instantly
Cross-field: budget ≥ attempt timeout, compared against EFFECTIVE values rather than
the configured pair — otherwise the rule is bypassed by leaving one field unset, and
the first hop would already exceed the budget so no fallback is ever reached.
Precedence org → local yaml → builtin, first hit wins, and every value reports its
Source. That is not a debugging aid: an admin's configured 5 minutes and the default
5 minutes are indistinguishable without it, and that ambiguity is what turns a
private-deployment call into two hours of mutual disbelief. Only the per-attempt
timeout has a local-yaml layer (it already existed as providers.<name>.timeout);
widening it would recreate the four-source base_url drift this project has paid for.
Defaults are ENGINEERING PLACEHOLDERS, flagged as such in the code: F-9b requires
calibration against the real upstream-latency and session-gap distributions before
release. A too-low attempt timeout turns a healthy-but-slow long-context request into
"the upstream is down", which then cools that upstream off for minutes. A fence keeps
the default budget visibly below attempt × chain length, or the cap is decorative.
AssertNoSecretShaped backs 1b.5. Five int64 pointers cannot hold a key today; the
check exists so that stays true when somebody later adds "just one string field",
because the door it guards is one-way — once a secret has gone out over a side
channel, rotation is the only remedy.
10 tests. Injecting the unwrap_or(0) collapse turns three of them red, naming the
incident; green on revert.
Edition impact matrix:
Personal N — no control plane, so org is always nil and every threshold resolves
to builtin through the SAME code path (no edition branch).
Trial Y — org-configured thresholds take effect.
Production Y — same.
Cluster Y — same; each node resolves independently.
Refs:
- Tasks: roadmap20260320/技术实现/阶段8-平台化/阿里云AI网关对标/openspec/changes/aliyun-aigw-p0-upstream-fallback/tasks.md
- Freeze: .../aliyun-aigw-p0-upstream-fallback/contract-freeze.md §11
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The shared threshold contract:
未配置/0/具体值are three distinct semantics, and collapsing the first two is the upgrade-day accident this package exists to prevent (cooldown → 0 means every request hits the wall first; budget → 0 means everything fails instantly).8/8 tests green, including
TestJSONRoundTripPreservesUnsetVersusExplicitZeroandTestPolicyPayloadCarriesNoSecrets.What this is
Part of openspec change
aliyun-aigw-p0-upstream-fallback(P0a 上游 Fallback).Base is
develop-v1.0.5; the branch was cut fromdevelop-v1.0.4and has haddevelop-v1.0.5merged in.Two sign-off gates closed (2026-07-30)
F-19 / D-5 — App pipeline gets the candidate loop. The blocking concern
("app-scoped pinning isn't identical to a VK chain") dissolved on inspection:
that pin is a row in the same
user_profile_provider_bindingstableaikey usewrites, atprofile_id='app:<slug>'. Task 1.2b already addedroute_group_idto it, so 0b.8c's three-state derivation applies verbatim.One rule, two call sites.
0b.3c — the mandatory-route-group
CHECKis WITHDRAWN, not deferred. Itcontradicts tasks 2.0 and 0b.9, which require 「没有组」 and 「组里只有一个成员」
to stay two distinguishable terminal states. 0b.3c had already banned the
auto-single-member-group workaround for that exact reason — the workaround and
the CHECK reach the same forbidden end state from opposite directions. Its
stated precondition ("after 4.32") was also never actually satisfied: 4.32 gave
bulk issuance the ability to stamp, not universal stamping, and three of four
VK-level write paths still create groupless bindings by design.
🔴 What this does NOT cover
Verification, not implementation, is what remains. P6 is not done — 6.6
(stage Aliyun cluster / offline package · macOS · Windows), 6.7, 6.8 and
6.12 need environments; 6.1's L2/L3/L4 need live event runs.
Consequently the capability matrix stays 🟡 and must not be flipped to ✅
until P6 passes (task 7.1). The prepared diff is in
p7-sales-enablement-closure.md§1.Docs for this change live in the
roadmap20260320repo and are not in thisPR.
🤖 Generated with Claude Code