pi-ccswitch-auto-switch 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/README.zh-CN.md +6 -2
- package/classify.ts +3 -1
- package/index.ts +15 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,17 +68,17 @@ Examples:
|
|
|
68
68
|
## Status bar
|
|
69
69
|
|
|
70
70
|
```text
|
|
71
|
-
CCS ✓70/131 · ⏳61 · ⛔0 ·
|
|
71
|
+
CCS ✓70/131 · ⏳61 · ⛔0 · 🔄3 · provider/model-id
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
- `✓70/131`: 70 healthy models out of 131 unique `provider/model` combinations in Pi's effective scope. Duplicate scoped entries are counted once.
|
|
75
75
|
- `⏳61`: 61 models are currently affected by automatic model, provider, or endpoint cooldowns. One provider breaker can account for many affected models.
|
|
76
76
|
- `⛔0`: no manually disabled models.
|
|
77
|
-
-
|
|
77
|
+
- `🔄3`: **本次 pi session 成功切换的模型次数**(衡量扩展有效程度)。`/new`、`/fork`、`/resume` 等新 session 开始时归零;但**失败记录与冷却状态不重置**(它们是物理事实,跨 session 保留)。累计切换数(`state.switches`)与最近 20 条切换日志会持久化到状态文件,可在 `/ccswitch` 面板和 `/ccswitch-test` 中查看。
|
|
78
78
|
- `provider/model-id`: 当前实际生效的模型(`provider/id`,切换后立即更新,长名自动截断)。
|
|
79
79
|
- During a switch, `CCS ↻2/5 provider/model` means the second of at most five attempts is being made.
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
四个状态(健康/冷却/禁用/切换)即使为 0 也始终显示,便于确认扩展处于监控中。All-healthy still shows zero counters, e.g. `CCS ✓131/131 · ⏳0 · ⛔0 · 🔄0 · provider/model-id`. Use `/ccswitch` or `/ccswitch-test` to see Pi's raw scope entry count, the deduplicated model count, affected model counts, and the underlying breaker-record count.
|
|
82
82
|
|
|
83
83
|
切换成功后扩展还会通过 `appendEntry` 向会话注入一条 `ccswitch-switch` custom entry(不参与 LLM 上下文),触发 TUI 底栏重绘——这样 Pi 右下角的模型名显示也会同步为切换后的模型。
|
|
84
84
|
|
package/README.zh-CN.md
CHANGED
|
@@ -58,15 +58,19 @@ pi install npm:pi-ccswitch-auto-switch
|
|
|
58
58
|
## 状态栏
|
|
59
59
|
|
|
60
60
|
```text
|
|
61
|
-
CCS ✓70/131 · ⏳61 · ⛔0
|
|
61
|
+
CCS ✓70/131 · ⏳61 · ⛔0 · 🔄3 · provider/model-id
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
- `✓70/131`:Pi 有效范围内共有 131 个去重后的 `provider/model` 组合,其中 70 个健康;重复的 scope 条目只计一次。
|
|
65
65
|
- `⏳61`:有 61 个模型正受模型、Provider 或端点自动冷却影响;一条 Provider 熔断记录可能同时影响许多模型。
|
|
66
66
|
- `⛔0`:没有被手动禁用的模型。
|
|
67
|
+
- `🔄3`:**本次 pi session 成功切换的模型次数**(衡量扩展有效程度,0 时也显示图标)。`/new`、`/fork`、`/resume` 等新 session 开始时归零;但**失败记录与冷却状态不重置**(它们是物理事实,跨 session 保留)。累计切换数(`state.switches`)与最近 20 条切换日志会持久化到状态文件,可在 `/ccswitch` 面板和 `/ccswitch-test` 中查看。
|
|
68
|
+
- `provider/model-id`:当前实际生效的模型(`provider/id`,切换后立即更新,长名自动截断)。
|
|
67
69
|
- 切换中出现 `CCS ↻2/5 provider/model`,表示正在进行最多 5 次尝试中的第 2 次。
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
四个状态(健康/冷却/禁用/切换)即使为 0 也始终显示,便于确认扩展处于监控中;全部健康时仍显示零计数,例如 `CCS ✓131/131 · ⏳0 · ⛔0 · 🔄0 · provider/model-id`。通过 `/ccswitch` 或 `/ccswitch-test` 可同时查看 Pi 原始 scope 条目数、去重模型数、受影响模型数、底层熔断记录数、本 session 切换数与累计切换数。
|
|
72
|
+
|
|
73
|
+
切换成功后扩展还会通过 `appendEntry` 向会话注入一条 `ccswitch-switch` custom entry(不参与 LLM 上下文),触发 TUI 底栏重绘——这样 Pi 右下角的模型名显示也会同步为切换后的模型。
|
|
70
74
|
|
|
71
75
|
## 故障转移逻辑
|
|
72
76
|
|
package/classify.ts
CHANGED
|
@@ -17,7 +17,9 @@ export function classifyFailure(observation: FailureObservation): Classification
|
|
|
17
17
|
const status = observation.status
|
|
18
18
|
if (observation.aborted && !observation.watchdog) return { kind: 'unknown', roundOnly: true }
|
|
19
19
|
if (observation.watchdog) return { kind: 'timeout', scope: 'endpoint', roundOnly: false }
|
|
20
|
-
|
|
20
|
+
// 内容审查/敏感拦截属于该模型的失败:按 model 级记录并与冷却(2min×2ⁿ 上限 30min),
|
|
21
|
+
// chooseCandidate 会排除后自动切换到其他模型,而不是同一内容原样重试到本轮上限。
|
|
22
|
+
if (content.test(text)) return { kind: 'content_policy', scope: 'model', roundOnly: false }
|
|
21
23
|
if (context.test(text)) return { kind: 'context_overflow', roundOnly: true }
|
|
22
24
|
if (status === 429) return { kind: 'rate_limit', scope: 'provider', roundOnly: false, retryAfterMs: observation.retryAfterMs }
|
|
23
25
|
if (status === 401 || status === 403) return { kind: 'auth', scope: 'provider', roundOnly: false }
|
package/index.ts
CHANGED
|
@@ -32,6 +32,8 @@ interface Round {
|
|
|
32
32
|
observation?: FailureObservation
|
|
33
33
|
model?: ModelRef
|
|
34
34
|
endpointFails?: EndpointFailTracker
|
|
35
|
+
/** 本轮内最后一次成功切换的时间;用于刷新 ROUND_LIMIT 窗口,避免供应商内部重试耗时导致误判“达到上限” */
|
|
36
|
+
lastSwitchAt?: number
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function key(model: ModelRef | undefined): string | undefined { return model && modelKey(model) }
|
|
@@ -67,13 +69,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
67
69
|
const { models } = candidateSnapshot(ctx.scopedModels, ctx.modelRegistry.getAvailable())
|
|
68
70
|
const counts = summarizeCandidateHealth(models, state)
|
|
69
71
|
const activeModel = round?.model ?? ctx.model
|
|
72
|
+
// 恒显完整状态:健康/总数 · 冷却 · 禁用 · 本session切换 · 当前模型(均为 0 时也显示,便于确认扩展在监控中)
|
|
70
73
|
const plain = round?.phase === 'switching' ? `CCS ↻${round.attempts}/${MAX_ATTEMPTS} ${modelTag(round.model)}` :
|
|
71
|
-
|
|
74
|
+
round?.phase === 'exhausted' ? `CCS ⏸切换停止 ${modelTag(round.model ?? ctx.model)}` :
|
|
75
|
+
`CCS ✓${counts.healthy}/${counts.total} · ⏳${counts.cooling} · ⛔${counts.disabled}${switchSuffix()} · ${modelTag(activeModel)}`
|
|
72
76
|
const theme = ctx.ui.theme
|
|
73
|
-
|
|
77
|
+
const tone = round?.phase === 'exhausted' ? 'error' : counts.cooling || counts.disabled ? 'warning' : 'success'
|
|
78
|
+
ctx.ui.setStatus('ccswitch-ha', theme ? theme.fg(tone, plain) : plain)
|
|
74
79
|
}
|
|
75
|
-
// 本次 session 成功切换计数:始终显示
|
|
76
|
-
const switchSuffix = () => ` ·
|
|
80
|
+
// 本次 session 成功切换计数:始终显示 🔄N(N=0 也显示,用于确认扩展在监控中);累计切换数在面板/自检中可见
|
|
81
|
+
const switchSuffix = () => ` · 🔄${sessionSwitches}`
|
|
77
82
|
const notify = (ctx: ExtensionContext, message: string, type: 'info' | 'warning' | 'error' = 'info') => {
|
|
78
83
|
if (ctx.hasUI) ctx.ui.notify(message, type)
|
|
79
84
|
}
|
|
@@ -115,7 +120,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
120
|
const switchSummary = `本session切换:${sessionSwitches} · 累计切换:${state.switches ?? 0}`
|
|
116
121
|
const recentSwitches = (state.switchLog ?? []).slice(0, 5).map(entry => {
|
|
117
122
|
const time = new Date(entry.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
118
|
-
return ` ${time} ${entry.from}
|
|
123
|
+
return ` ${time} ${entry.from} 🔄 ${entry.to}`
|
|
119
124
|
})
|
|
120
125
|
const switchPanel = recentSwitches.length ? `\n最近切换:\n${recentSwitches.join('\n')}` : ''
|
|
121
126
|
const action = await ctx.ui.select(`CCSwitch 健康面板\n当前:${key(ctx.model) ?? '无'}\n${scopeLabel} · 唯一模型:${counts.total} · 健康:${counts.healthy} · 自动冷却:${counts.cooling} · 手动禁用:${counts.disabled}\n熔断记录:${counts.breakerRecords}\n${switchSummary}${switchPanel}\n${rows.join('\n') || '没有可用模型'}`, ['刷新', '重新激活当前模型', '禁用当前模型', '重置当前模型历史', '关闭'])
|
|
@@ -139,7 +144,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
139
144
|
}
|
|
140
145
|
const failover = async (ctx: ExtensionContext) => {
|
|
141
146
|
if (!round || !round.observation || !round.model || !canRetry(ctx)) return
|
|
142
|
-
|
|
147
|
+
// 窗口从上一次成功切换(或本轮开始)起算:供应商内部重试耗时不应消耗整轮限额
|
|
148
|
+
const windowStart = Math.max(round.startedAt, round.lastSwitchAt ?? 0)
|
|
149
|
+
if (round.attempts >= MAX_ATTEMPTS || Date.now() - windowStart >= ROUND_LIMIT) return exhaust(ctx, '达到本轮切换上限')
|
|
143
150
|
const classification = classifyFailure(round.observation)
|
|
144
151
|
if (round.observation.aborted && !round.observation.watchdog) { round.phase = 'idle'; clearWatchdog(); status(ctx); return }
|
|
145
152
|
round.phase = 'switching'
|
|
@@ -172,6 +179,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
172
179
|
}
|
|
173
180
|
round.model = next
|
|
174
181
|
round.phase = 'redispatching'
|
|
182
|
+
// 刷新本轮切换时间窗:成功切换后重新起算 ROUND_LIMIT,避免长重试轮被误判为“达到上限”
|
|
183
|
+
round.lastSwitchAt = Date.now()
|
|
175
184
|
// 本次 session 成功切换计数 + 持久化累计/日志(衡量扩展有效程度)
|
|
176
185
|
sessionSwitches += 1
|
|
177
186
|
const fromKey = key(round.model ?? ctx.model)
|