Skip to content

Commit dde47d9

Browse files
john-zhhclaude
andcommitted
docs: add feature-015 design specs (mark provider unavailable)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 73e3bca commit dde47d9

7 files changed

Lines changed: 729 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Specification Quality Checklist: Manual Provider Unavailability Marking
2+
3+
**Purpose**: Validate specification completeness and quality before proceeding to planning
4+
**Created**: 2026-03-07
5+
**Feature**: [spec.md](../spec.md)
6+
7+
## Content Quality
8+
9+
- [x] No implementation details (languages, frameworks, APIs)
10+
- [x] Focused on user value and business needs
11+
- [x] Written for non-technical stakeholders
12+
- [x] All mandatory sections completed
13+
14+
## Requirement Completeness
15+
16+
- [x] No [NEEDS CLARIFICATION] markers remain
17+
- [x] Requirements are testable and unambiguous
18+
- [x] Success criteria are measurable
19+
- [x] Success criteria are technology-agnostic (no implementation details)
20+
- [x] All acceptance scenarios are defined
21+
- [x] Edge cases are identified
22+
- [x] Scope is clearly bounded
23+
- [x] Dependencies and assumptions identified
24+
25+
## Feature Readiness
26+
27+
- [x] All functional requirements have clear acceptance criteria
28+
- [x] User scenarios cover primary flows
29+
- [x] Feature meets measurable outcomes defined in Success Criteria
30+
- [x] No implementation details leak into specification
31+
32+
## Notes
33+
34+
- All items pass validation. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
35+
- Feature description was detailed enough to avoid any [NEEDS CLARIFICATION] markers.
36+
- Assumptions documented in spec cover timezone handling, marking granularity, confirmation UX, and daemon communication.
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# API Contracts: Provider Unavailability
2+
3+
**Feature**: 015-mark-provider-unavailable
4+
**Date**: 2026-03-07
5+
6+
## Web API Endpoints
7+
8+
### POST /api/v1/providers/{name}/disable
9+
10+
Mark a provider as unavailable.
11+
12+
**Request Body**:
13+
```json
14+
{
15+
"type": "today" | "month" | "permanent"
16+
}
17+
```
18+
19+
**Success Response** (200 OK):
20+
```json
21+
{
22+
"provider": "openai-backup",
23+
"disabled": true,
24+
"type": "today",
25+
"created_at": "2026-03-07T14:30:00+08:00",
26+
"expires_at": "2026-03-07T23:59:59+08:00"
27+
}
28+
```
29+
30+
**Error Responses**:
31+
- 400: `{"error": "invalid type: must be 'today', 'month', or 'permanent'"}`
32+
- 404: `{"error": "provider 'xxx' not found"}`
33+
34+
---
35+
36+
### POST /api/v1/providers/{name}/enable
37+
38+
Clear the unavailability marking for a provider.
39+
40+
**Request Body**: None (empty body)
41+
42+
**Success Response** (200 OK):
43+
```json
44+
{
45+
"provider": "openai-backup",
46+
"disabled": false
47+
}
48+
```
49+
50+
**Error Responses**:
51+
- 404: `{"error": "provider 'xxx' not found"}`
52+
53+
---
54+
55+
### GET /api/v1/providers/disabled
56+
57+
List all currently disabled providers (active markings only, excludes expired).
58+
59+
**Response** (200 OK):
60+
```json
61+
{
62+
"disabled_providers": [
63+
{
64+
"provider": "openai-backup",
65+
"type": "today",
66+
"created_at": "2026-03-07T14:30:00+08:00",
67+
"expires_at": "2026-03-07T23:59:59+08:00"
68+
}
69+
]
70+
}
71+
```
72+
73+
---
74+
75+
### GET /api/v1/providers (existing, extended)
76+
77+
The existing provider list response is extended with an optional `disabled` field.
78+
79+
**Extended Response**:
80+
```json
81+
[
82+
{
83+
"name": "anthropic-main",
84+
"type": "anthropic",
85+
"base_url": "https://api.anthropic.com",
86+
"auth_token": "sk-an...abcd"
87+
},
88+
{
89+
"name": "openai-backup",
90+
"type": "openai",
91+
"base_url": "https://api.openai.com",
92+
"auth_token": "sk-op...efgh",
93+
"disabled": {
94+
"type": "today",
95+
"created_at": "2026-03-07T14:30:00+08:00",
96+
"expires_at": "2026-03-07T23:59:59+08:00"
97+
}
98+
}
99+
]
100+
```
101+
102+
---
103+
104+
### Proxy Error Response (new)
105+
106+
When all providers are disabled, the proxy returns:
107+
108+
**Response** (503 Service Unavailable):
109+
```json
110+
{
111+
"error": {
112+
"type": "all_providers_unavailable",
113+
"message": "All providers are manually marked as unavailable. Please re-enable a provider via Web UI or 'zen enable <provider>'.",
114+
"disabled_providers": ["anthropic-main", "openai-backup"]
115+
}
116+
}
117+
```
118+
119+
## CLI Commands
120+
121+
### zen disable
122+
123+
```
124+
zen disable <provider> [--today|--month|--permanent]
125+
126+
Flags:
127+
--today Mark unavailable for today only (default)
128+
--month Mark unavailable for this calendar month
129+
--permanent Mark unavailable permanently until manually cleared
130+
131+
Examples:
132+
zen disable openai-backup # unavailable today (default)
133+
zen disable openai-backup --month # unavailable this month
134+
zen disable openai-backup --permanent # unavailable until cleared
135+
```
136+
137+
**Output**:
138+
```
139+
Provider "openai-backup" marked as unavailable (today, expires 2026-03-07 23:59:59)
140+
```
141+
142+
**Error Output**:
143+
```
144+
Error: provider "xxx" not found
145+
Available providers: anthropic-main, openai-backup
146+
```
147+
148+
### zen enable
149+
150+
```
151+
zen enable <provider>
152+
153+
Examples:
154+
zen enable openai-backup
155+
```
156+
157+
**Output**:
158+
```
159+
Provider "openai-backup" is now available
160+
```
161+
162+
### zen disable --list
163+
164+
```
165+
zen disable --list
166+
167+
Examples:
168+
zen disable --list
169+
```
170+
171+
**Output**:
172+
```
173+
Disabled providers:
174+
openai-backup today expires 2026-03-07 23:59:59
175+
anthropic-alt permanent no expiration
176+
```
177+
178+
**Output (none disabled)**:
179+
```
180+
No providers are currently disabled.
181+
```
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Data Model: Manual Provider Unavailability Marking
2+
3+
**Feature**: 015-mark-provider-unavailable
4+
**Date**: 2026-03-07
5+
6+
## New Entity: UnavailableMarking
7+
8+
Represents a user's manual decision to mark a provider as temporarily or permanently unavailable.
9+
10+
### Fields
11+
12+
| Field | Type | Description |
13+
|-------------|--------|---------------------------------------------------------------------------------------------------------|
14+
| Type | string | Expiration type: `"today"`, `"month"`, or `"permanent"` |
15+
| CreatedAt | time | Timestamp when the marking was created (local time) |
16+
| ExpiresAt | time | Pre-computed expiration timestamp. Zero value for permanent markings. Calculated at creation time. |
17+
18+
### JSON Representation
19+
20+
```json
21+
{
22+
"type": "today",
23+
"created_at": "2026-03-07T14:30:00+08:00",
24+
"expires_at": "2026-03-07T23:59:59+08:00"
25+
}
26+
```
27+
28+
### Behaviors
29+
30+
- `IsExpired()`: Returns true if `ExpiresAt` is non-zero and `time.Now()` is after `ExpiresAt`. Permanent markings (zero `ExpiresAt`) never expire.
31+
- `IsActive()`: Returns true if the marking exists and is not expired.
32+
33+
### Constraints
34+
35+
- One marking per provider (keyed by provider name)
36+
- Setting a new marking replaces any existing one
37+
- Marking is removed when provider is deleted from config
38+
39+
## Modified Entity: OpenCCConfig
40+
41+
### New Field
42+
43+
| Field | Type | JSON Key | Description |
44+
|--------------------|-----------------------------------|-----------------------|----------------------------------------------|
45+
| DisabledProviders | `map[string]*UnavailableMarking` | `disabled_providers` | Map of provider name → unavailability marking |
46+
47+
### Version Change
48+
49+
- Config version: 13 → 14
50+
- Migration: No special migration needed. Old configs without `disabled_providers` parse as empty map.
51+
52+
### JSON Example (within zen.json)
53+
54+
```json
55+
{
56+
"version": 14,
57+
"providers": {
58+
"anthropic-main": { "base_url": "...", "auth_token": "..." },
59+
"openai-backup": { "base_url": "...", "auth_token": "..." }
60+
},
61+
"disabled_providers": {
62+
"openai-backup": {
63+
"type": "today",
64+
"created_at": "2026-03-07T14:30:00+08:00",
65+
"expires_at": "2026-03-07T23:59:59+08:00"
66+
}
67+
}
68+
}
69+
```
70+
71+
## State Transitions
72+
73+
```
74+
Available ──[user marks disable]──→ Unavailable (today/month/permanent)
75+
↑ │
76+
│ │
77+
├──[user clears marking]───────────────┘
78+
│ │
79+
└──[expiration time reached]───────────┘
80+
```
81+
82+
## Interaction with Existing Health System
83+
84+
The provider has two independent availability dimensions:
85+
86+
1. **Auto health** (existing): `Provider.IsHealthy()` — backoff-based, auto-recovers
87+
2. **Manual unavailability** (new): `config.IsProviderDisabled(name)` → calls `UnavailableMarking.IsActive()` — user-controlled, checked directly from config at request time (lazy evaluation, no sync needed)
88+
89+
During proxy routing (in `tryProviders()`):
90+
- Check `config.IsProviderDisabled(p.Name)` for each provider before attempting it
91+
- If manually marked unavailable AND active → skip (unless all providers unavailable → return 503 error)
92+
- If auto-unhealthy → existing backoff logic applies (skip unless last provider)
93+
- Both dimensions are independent: clearing a manual mark doesn't affect auto health, and vice versa
94+
- Expiration is evaluated lazily via `IsActive()` — no background timer or sync mechanism needed
95+
- `zen use <provider>` bypasses both manual unavailability (by design; auto health doesn't apply to direct use)
96+
97+
## Cleanup Rules
98+
99+
- When a provider is deleted from config (`DeleteProvider`), its entry in `disabled_providers` is also removed
100+
- Expired markings are lazily ignored (treated as available) and optionally cleaned up on next config save

0 commit comments

Comments
 (0)