pi-ccswitch-auto-switch 0.3.13 → 0.3.15

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 CHANGED
@@ -144,7 +144,7 @@ The state machine waits for Pi's native retry cycle to settle before switching.
144
144
  | Failure | Scope | Initial cooldown |
145
145
  | --- | --- | --- |
146
146
  | `401`, `403`, quota, billing | Provider | 30 minutes |
147
- | `429` | Provider | `Retry-After` when present; otherwise 5 minutes |
147
+ | `429` | Provider | quota/billing body (e.g. `AccountQuotaExceeded`) → 30 minutes; otherwise `Retry-After` when present, else 5 minutes |
148
148
  | DNS, network, `408`, `5xx`, interrupted stream | Endpoint | 2 minutes |
149
149
  | `404`, invalid model, incompatible parameters | Model | 15 minutes |
150
150
  | Content-policy or sensitive-content rejection | Current model + learned model-family constraint | 2-minute model cooldown; family evidence is session-scoped (cleared at `session_start`) |
package/README.zh-CN.md CHANGED
@@ -132,7 +132,7 @@ CCS v0.3.5 ✓70/131 · ⏳61 · ⛔0 · 🔄3 · provider/model-id
132
132
  | 失败类型 | 熔断范围 | 初始冷却 |
133
133
  | --- | --- | --- |
134
134
  | `401`、`403`、额度或账单问题 | Provider | 30 分钟 |
135
- | `429` | Provider | 优先使用 `Retry-After`,否则 5 分钟 |
135
+ | `429` | Provider | 配额/余额正文(如 `AccountQuotaExceeded`)→ 30 分钟;否则优先使用 `Retry-After`,无则 5 分钟 |
136
136
  | DNS、网络、`408`、`5xx`、流中断 | 端点 | 2 分钟 |
137
137
  | `404`、模型不存在、参数不兼容 | 模型 | 15 分钟 |
138
138
  | 内容审查/敏感拦截 | 当前模型 + 模型系列约束 | 模型冷却 2 分钟;系列约束按 session 隔离(`session_start` 时清除) |
package/classify.ts CHANGED
@@ -25,7 +25,13 @@ export function classifyFailure(observation: FailureObservation): Classification
25
25
  if (content.test(text)) return { kind: 'content_policy', scope: 'model', roundOnly: false }
26
26
  if (context.test(text)) return { kind: 'context_overflow', roundOnly: true }
27
27
  if (status === 413) return { kind: 'context_overflow', roundOnly: true }
28
- if (status === 429) return { kind: 'rate_limit', scope: 'provider', roundOnly: false, retryAfterMs: observation.retryAfterMs }
28
+ // 账户级配额耗尽常以 429 上报(如 AccountQuotaExceeded + "exceeded the 5-hour usage quota")。
29
+ // quota 文本必须优先于瞬时 rate_limit:否则死 provider 只按 5min 冷却停放,
30
+ // 在数小时后才重置的配额窗口内被放回健康池重试。
31
+ if (status === 429) {
32
+ if (quota.test(text)) return { kind: 'quota', scope: 'provider', roundOnly: false }
33
+ return { kind: 'rate_limit', scope: 'provider', roundOnly: false, retryAfterMs: observation.retryAfterMs }
34
+ }
29
35
  if (status === 402) return { kind: 'quota', scope: 'provider', roundOnly: false }
30
36
  if (status === 403 && quota.test(text)) return { kind: 'quota', scope: 'provider', roundOnly: false }
31
37
  if (status === 401 || status === 403) return { kind: 'auth', scope: 'provider', roundOnly: false }
package/index.ts CHANGED
@@ -17,7 +17,7 @@ const roundLimitMs = (): number => {
17
17
  return Number.isFinite(override) && override > 0 ? override : ROUND_LIMIT_DEFAULT
18
18
  }
19
19
  const RPC_PROTOCOL_VERSION = 1
20
- const EXTENSION_VERSION = '0.3.13'
20
+ const EXTENSION_VERSION = '0.3.15'
21
21
  // 同端点(BaseURL 相同)连续失败达到该次数即隔离该端点,避免同一个平台的多个模型逐个试错耗尽本轮切换
22
22
  const ENDPOINT_FAIL_THRESHOLD = 3
23
23
 
@@ -309,9 +309,11 @@ export default function (pi: ExtensionAPI) {
309
309
  // 下一轮还会重试同一个刚失败的模型/provider
310
310
  if (!classification.roundOnly && classification.scope) health.recordFailure(classification.scope, classification.scope === 'model' ? failedKey : classification.scope === 'provider' ? round.model.provider : endpointKey(round.model), classification.kind, round.observation.message, classification.retryAfterMs)
311
311
  await health.flush()
312
- // 窗口从上一次成功切换(或本轮开始)起算:供应商内部重试耗时不应消耗整轮限额
312
+ // 窗口从上一次成功切换(或本轮开始)起算:供应商内部重试耗时不应消耗整轮限额。
313
+ // 仅在本轮已成功切换过(attempts > 0)后才生效:窗口的本意是限制连续切换 churn,
314
+ // 长健康轮末尾的第一次失败也必须拿到兜底切换,否则轮时限会拦掉唯一一次 fallback。
313
315
  const windowStart = Math.max(round.startedAt, round.lastSwitchAt ?? 0)
314
- if (Date.now() - windowStart >= roundLimitMs()) return exhaust(ctx, '超过本轮时间限制')
316
+ if (round.attempts > 0 && Date.now() - windowStart >= roundLimitMs()) return exhaust(ctx, '超过本轮时间限制')
315
317
  await refresh(ctx)
316
318
  const candidates = effectiveCandidates(ctx.scopedModels, ctx.modelRegistry.getAvailable())
317
319
  const candidateOptions = () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ccswitch-auto-switch",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "Provider-first automatic model failover extension for Pi and CC Switch",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/runner.mjs CHANGED
@@ -376,5 +376,6 @@ export async function main(argv = process.argv.slice(2)) {
376
376
  }
377
377
  }
378
378
 
379
- const invoked = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
379
+ const invokedPath = process.argv[1] && await realpath(resolve(process.argv[1])).catch(() => undefined)
380
+ const invoked = invokedPath === fileURLToPath(import.meta.url)
380
381
  if (invoked) main().then(code => { process.exitCode = code })