Commit 9bdb70a
authored
🤖 fix: handle GitHub Copilot context limits and retry behavior (#2431)
## Summary
Fix GitHub Copilot models getting stuck in infinite retry loops when the
prompt exceeds the provider's context window. Three root causes
addressed: model stats lookup failures, error misclassification, and
incorrect tokenizer fallbacks.
## Background
A user reported Mux showing repeated **Stream Error (API)** messages
with Copilot models (e.g., `prompt token count of 128067 exceeds the
limit of 128000`) and auto-retrying endlessly (attempt 9+), never
surfacing the existing **"Compact & retry"** recovery UI.
Three independent issues combined to cause this:
1. **Model stats lookup failed for Copilot models.** `models.json` has
Copilot entries under `github_copilot/` keys (with underscore) but
without cost fields. Our `isValidModelData()` required cost fields, and
`generateLookupKeys()` checked bare model names first (matching OpenAI
entries) and didn't normalize `github-copilot` → `github_copilot`.
2. **Token-limit errors were misclassified.** `categorizeError()` only
detected Anthropic-style context errors (`prompt is too long`).
Copilot's `prompt token count ... exceeds the limit ...` message was
classified as `"api"` (retryable), not `"context_exceeded"`
(non-retryable).
3. **Wrong tokenizer fallback.** Copilot hosts models from multiple
providers (Claude, Gemini, GPT), but all `github-copilot:*` models fell
back to the OpenAI tokenizer regardless of the underlying model.
## Implementation
### 1. `modelStats.ts` — Fix Copilot token limit resolution
- Added provider alias mapping (`github-copilot` → `github_copilot`) for
LiteLLM key generation
- Reversed lookup priority: provider-prefixed keys checked first, bare
model name last
- Relaxed `isValidModelData()` to only require `max_input_tokens` (not
cost fields)
- Default missing costs to `0` (Copilot is subscription-based)
- Added `parseNum()` helper for safe numeric string parsing
### 2. `streamManager.ts` — Classify Copilot context errors correctly
- Expanded `categorizeError()` to detect `"token" + "exceeds" + "limit"`
pattern
- This makes Copilot token-limit errors return `context_exceeded`,
which:
- Stops auto-retry (already in `NON_RETRYABLE_STREAM_ERRORS`)
- Shows existing **"Compact & retry"** UI (already in
`StreamErrorMessage`)
### 3. `tokenizer.ts` — Smart fallback for Copilot model tokenizers
- When provider is `github-copilot`, infer tokenizer from model name
prefix:
- `claude-*` → Anthropic tokenizer
- `gemini-*` → Google tokenizer
- `gpt-*` / others → OpenAI tokenizer
## Validation
- `make static-check` — all checks pass
- `bun test src/common/utils/tokens/modelStats.test.ts` — 27/27 pass (4
new Copilot-specific tests)
---
_Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking:
`xhigh` • Cost: `$1.99`_
<!-- mux-attribution: model=anthropic:claude-opus-4-6 thinking=xhigh
costs=1.99 -->1 parent 62deee1 commit 9bdb70a
4 files changed
Lines changed: 89 additions & 26 deletions
File tree
- src
- common/utils/tokens
- node
- services
- utils/main
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
86 | 113 | | |
87 | 114 | | |
88 | 115 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
24 | 42 | | |
25 | 43 | | |
26 | 44 | | |
27 | 45 | | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
| 46 | + | |
| 47 | + | |
33 | 48 | | |
34 | 49 | | |
35 | 50 | | |
36 | 51 | | |
37 | 52 | | |
38 | 53 | | |
39 | | - | |
40 | | - | |
41 | 54 | | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
47 | 62 | | |
48 | 63 | | |
49 | 64 | | |
| |||
53 | 68 | | |
54 | 69 | | |
55 | 70 | | |
56 | | - | |
57 | 71 | | |
58 | 72 | | |
59 | 73 | | |
| |||
64 | 78 | | |
65 | 79 | | |
66 | 80 | | |
| 81 | + | |
67 | 82 | | |
68 | | - | |
69 | | - | |
70 | | - | |
| 83 | + | |
71 | 84 | | |
72 | | - | |
| 85 | + | |
73 | 86 | | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
| 87 | + | |
78 | 88 | | |
79 | 89 | | |
80 | 90 | | |
81 | 91 | | |
82 | 92 | | |
83 | | - | |
| 93 | + | |
84 | 94 | | |
85 | 95 | | |
86 | 96 | | |
| 97 | + | |
| 98 | + | |
87 | 99 | | |
88 | 100 | | |
89 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2353 | 2353 | | |
2354 | 2354 | | |
2355 | 2355 | | |
2356 | | - | |
| 2356 | + | |
2357 | 2357 | | |
2358 | | - | |
| 2358 | + | |
| 2359 | + | |
| 2360 | + | |
| 2361 | + | |
| 2362 | + | |
| 2363 | + | |
| 2364 | + | |
| 2365 | + | |
| 2366 | + | |
2359 | 2367 | | |
2360 | 2368 | | |
2361 | 2369 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
84 | 100 | | |
85 | | - | |
| 101 | + | |
86 | 102 | | |
87 | | - | |
| 103 | + | |
88 | 104 | | |
89 | 105 | | |
90 | 106 | | |
| |||
0 commit comments