Skip to content

feat(fallbackpolicy): three-state threshold policy for upstream fallback - #4

Merged
damonleelcx merged 1 commit into
develop-v1.0.5from
feat/aliyun-aigw-p0-upstream-fallback
Jul 31, 2026
Merged

feat(fallbackpolicy): three-state threshold policy for upstream fallback#4
damonleelcx merged 1 commit into
develop-v1.0.5from
feat/aliyun-aigw-p0-upstream-fallback

Conversation

@damonleelcx

Copy link
Copy Markdown

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 TestJSONRoundTripPreservesUnsetVersusExplicitZero and TestPolicyPayloadCarriesNoSecrets.

What this is

Part of openspec change aliyun-aigw-p0-upstream-fallback (P0a 上游 Fallback).
Base is develop-v1.0.5; the branch was cut from develop-v1.0.4 and has had
develop-v1.0.5 merged in.

ℹ️ develop-v1.0.4develop-v1.0.5 was verified to be a no-op in all 28
repos
— v1.0.5 is identical to v1.0.4 or already ahead. The sync that had
real content was the reverse direction, which is what this branch carries.

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_bindings table
aikey use writes, at profile_id='app:<slug>'. Task 1.2b already added
route_group_id to it, so 0b.8c's three-state derivation applies verbatim.
One rule, two call sites.

0b.3c — the mandatory-route-group CHECK is WITHDRAWN, not deferred. It
contradicts 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 roadmap20260320 repo and are not in this
PR.

🤖 Generated with Claude Code

…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>
@damonleelcx
damonleelcx merged commit ef82547 into develop-v1.0.5 Jul 31, 2026
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant