captchakraken 2.6.0 → 2.8.0

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.
Files changed (36) hide show
  1. package/README.md +126 -16
  2. package/dist/cli-invocation.d.ts +2 -0
  3. package/dist/cli-invocation.js +5 -1
  4. package/dist/errors.d.ts +1 -1
  5. package/dist/humanize.d.ts +377 -0
  6. package/dist/humanize.js +716 -0
  7. package/dist/index.d.ts +33 -0
  8. package/dist/index.js +44 -1
  9. package/dist/playwright-types.d.ts +18 -0
  10. package/dist/solver.d.ts +115 -5
  11. package/dist/solver.js +583 -190
  12. package/dist/timing.d.ts +48 -0
  13. package/dist/timing.js +103 -0
  14. package/dist/trajectory.d.ts +27 -0
  15. package/dist/trajectory.js +104 -0
  16. package/dist/types.d.ts +129 -0
  17. package/package.json +11 -3
  18. package/python/README.md +76 -5
  19. package/python/pyproject.toml +21 -4
  20. package/python/src/captchakraken/__init__.py +36 -1
  21. package/python/src/captchakraken/cli.py +62 -4
  22. package/python/src/captchakraken/config.py +28 -2
  23. package/python/src/captchakraken/errors.py +24 -0
  24. package/python/src/captchakraken/humanize.py +929 -0
  25. package/python/src/captchakraken/keyframes.py +99 -0
  26. package/python/src/captchakraken/models.json +149 -13
  27. package/python/src/captchakraken/page_solver.py +588 -291
  28. package/python/src/captchakraken/pinned_model.json +3 -2
  29. package/python/src/captchakraken/planner.py +118 -9
  30. package/python/src/captchakraken/prompts.py +182 -0
  31. package/python/src/captchakraken/server_manager.py +9 -4
  32. package/python/src/captchakraken/solver.py +97 -57
  33. package/python/src/captchakraken/timing.py +8 -3
  34. package/python/src/captchakraken/tool_calls/find_grid.py +357 -20
  35. package/python/src/captchakraken/trajectory.py +117 -0
  36. package/python/src/captchakraken/updater.py +62 -1
@@ -511,6 +511,96 @@ def _detect_cycle(
511
511
  return state_reps, per_frame
512
512
 
513
513
 
514
+ def _drop_smeared(
515
+ frames: Sequence[np.ndarray], indices: Sequence[int], params: KeyframeParams
516
+ ) -> List[int]:
517
+ """Drop an index caught BETWEEN two holds, when both of them are already
518
+ represented in the cut.
519
+
520
+ These clips hold a picture, swap, hold the next. An evenly spaced sample can
521
+ land on the instant of the swap — a board part-way between two states, which
522
+ is a picture the puzzle never actually shows. If the model already has the
523
+ board before the swap AND the board after it, that frame adds nothing but a
524
+ smear; if either endpoint is missing, it is the only thing standing in for a
525
+ state and must stay.
526
+
527
+ THE MACHINERY ALREADY EXISTED AND THIS PATH NEVER ASKED IT. `_anchor_runs`
528
+ plus `min_hold_frames` is how `_detect_cycle` separates holds from
529
+ transitions (it labels the latter -1 in `frame_states`), and
530
+ `min_hold_frames` had exactly one reader — inside `_detect_cycle`. On the
531
+ `even` path the parameter was declared, serialised into every manifest, and
532
+ inert.
533
+
534
+ WHY THE TEST IS "BOTH NEIGHBOURING HOLDS ARE KEPT" AND NOT A PIXEL DIFF.
535
+ Two earlier versions were measured against both trees and both were wrong:
536
+
537
+ * *Snap to the nearest held frame* turned `[0, 16, 39]` into `[0, 16]` on
538
+ six eval clips, dropping a SCREEN. A page that never reaches the model is
539
+ unanswerable whenever the target lived on it.
540
+ * *Drop when some other still is within `distinct_ratio`* cut
541
+ `hcaptcha_tile_flip_video` from 6 stills to 3, because two boards of that
542
+ puzzle differ by ONE TILE — well inside `distinct_ratio` — so a diff
543
+ threshold cannot tell "the same picture again" from "the next board".
544
+ EXPECTED_SLICING says it directly: fewer than 6 "means dedup merged two
545
+ boards that differ only by the tile mid-flip, and that tile is frequently
546
+ the answer".
547
+
548
+ Anchoring on the HOLDS either side asks the question that actually matters —
549
+ does the model already see both ends of this swap — and needs no threshold of
550
+ its own.
551
+
552
+ A clip with no holds at all (a rotation, a continuous fade, anything in
553
+ permanent motion) has no transitions to speak of and is returned untouched.
554
+ """
555
+ runs = _anchor_runs(frames, params.steady_ratio)
556
+ holds = [(s, e) for (s, e) in runs if (e - s + 1) >= params.min_hold_frames]
557
+ if not holds:
558
+ return list(indices) # nothing holds; nothing is a smear
559
+
560
+ # Holds showing the same picture are ONE state — the same merge
561
+ # `_detect_cycle` does, at the same threshold. It matters here because a
562
+ # cycle revisits: on a real `icon_2x2` clip the hold at 53..74 is the
563
+ # opening board coming back, so the still at index 0 already covers it.
564
+ # Counting holds instead of states kept a smear whose neighbouring picture
565
+ # was on screen the whole time.
566
+ state_of_hold: List[int] = []
567
+ reps: List[int] = []
568
+ for start, end in holds:
569
+ rep = _medoid(frames, list(range(start, end + 1)))
570
+ for k, existing in enumerate(reps):
571
+ if frame_diff_ratio(frames[rep], frames[existing]) <= params.steady_ratio:
572
+ state_of_hold.append(k)
573
+ break
574
+ else:
575
+ reps.append(rep)
576
+ state_of_hold.append(len(reps) - 1)
577
+
578
+ def hold_of(i: int) -> Optional[int]:
579
+ for k, (s, e) in enumerate(holds):
580
+ if s <= i <= e:
581
+ return k
582
+ return None
583
+
584
+ covered = {state_of_hold[h] for h in (hold_of(i) for i in indices)
585
+ if h is not None}
586
+ kept: List[int] = []
587
+ for i in indices:
588
+ if hold_of(i) is not None:
589
+ kept.append(i)
590
+ continue
591
+ before = max((k for k, (s, e) in enumerate(holds) if e < i), default=None)
592
+ after = min((k for k, (s, e) in enumerate(holds) if s > i), default=None)
593
+ # Only a swap BETWEEN two kept holds is redundant. A transition at the
594
+ # very start or end of a clip has an endpoint that was never recorded,
595
+ # so it is evidence of a state and stays.
596
+ if before is not None and after is not None \
597
+ and state_of_hold[before] in covered \
598
+ and state_of_hold[after] in covered:
599
+ continue
600
+ kept.append(i)
601
+ return kept
602
+
603
+
514
604
  def extract_keyframes(
515
605
  frames: Sequence[np.ndarray],
516
606
  *,
@@ -548,12 +638,21 @@ def extract_keyframes(
548
638
  )
549
639
 
550
640
  indices = _even_indices(n, p.max_keyframes)
641
+ # BEFORE dedup, so a smear does not spend budget a real state could use...
642
+ indices = _drop_smeared(frames, indices, p)
551
643
  if p.dedupe:
552
644
  # Applied to `even` only. `cycle` states are distinct by construction (the
553
645
  # merge in `_detect_cycle` uses this same threshold) and `static` is one
554
646
  # frame, so there is nothing to remove; and dropping a cycle keyframe here
555
647
  # would desynchronise `frame_states`, which indexes keyframes by position.
556
648
  indices = _distinct_indices(frames, indices, p)
649
+ # ...AND AFTER, because `_distinct_indices` BACKFILLS. Dedup frees
650
+ # budget and the backfill walks the clip for anything not already
651
+ # represented — which is exactly what a smeared frame is, so it gets
652
+ # picked straight back up. Measured on a 3-hold fixture: filtering only
653
+ # before dedup returned [0, 10, 12, 21, 25] with 10 and 21 the two
654
+ # smears the first pass had just removed.
655
+ indices = _drop_smeared(frames, indices, p)
557
656
  return KeyframeSet(
558
657
  mode="even",
559
658
  keyframes=[
@@ -6,10 +6,9 @@
6
6
  "Why a map and not a constant: a model answers in whatever schema its prompt",
7
7
  "asks for. Send a version-1 model the version-2 prompt and nothing errors —",
8
8
  "it just answers worse, on every puzzle, silently. That is exactly the",
9
- "2026-07-18 regression (failure-mode 3 in the finetune repo's",
10
- "scripts/check_prompt_parity.py), where the shipped prompt kept asking for",
11
- "the legacy output/simulate_drag/PascalCase schema long after the LoRA had",
12
- "been retrained on action: drag / drags[].",
9
+ "2026-07-18 regression, where the shipped prompt kept asking for the legacy",
10
+ "output/simulate_drag/PascalCase schema long after the LoRA had been",
11
+ "retrained on action: drag / drags[].",
13
12
  "",
14
13
  "Hardcoding one generation makes that inevitable the moment a new model",
15
14
  "trains on different prompts: updating the constants breaks every already-",
@@ -27,10 +26,9 @@
27
26
  "never see `latest`, so they are not silently moved onto prompts they cannot",
28
27
  "produce.",
29
28
  "",
30
- "Adding a model: add its entry here IN THE SAME COMMIT as publishing it. The",
31
- "finetune repo's check_prompt_parity.py fails if any registered model names a",
32
- "prompt_version this client ships no built-ins for, so an unmapped model",
33
- "cannot reach a release.",
29
+ "Adding a model: add its entry here IN THE SAME COMMIT as publishing it. A",
30
+ "release gate fails if any registered model names a prompt_version this",
31
+ "client ships no built-ins for, so an unmapped model cannot reach a release.",
34
32
  "",
35
33
  "`pixel_budget` is the same idea for RESOLUTION. Qwen reads MIN_PIXELS /",
36
34
  "MAX_PIXELS from the environment and clamps each image's AREA into that band,",
@@ -47,7 +45,80 @@
47
45
  "",
48
46
  "Omit it and the client keeps its historical behaviour (floor at 448 squared,",
49
47
  "no ceiling). Absent means UNKNOWN, not zero — set it only where it has been",
50
- "measured or the training run is known."
48
+ "measured or the training run is known.",
49
+ "",
50
+ "`availability` says whether the WEIGHTS are obtainable. Two values:",
51
+ " \"public\" — on the Hub; anyone may download and self-host it. This is",
52
+ " the default when the field is absent, so every entry that",
53
+ " predates the field keeps its meaning exactly.",
54
+ " \"private\" — ON THE HUB, in a PRIVATE repo. The bytes exist and",
55
+ " `captchakraken fetch` pulls them; the Hub answers 401 to",
56
+ " everyone whose token this account has not authorised. That",
57
+ " refusal belongs to the Hub, not to us, so the client does",
58
+ " NOT pre-empt it — doing so would stop our own admin token",
59
+ " from fetching weights it is entitled to.",
60
+ "",
61
+ " It is NOT a softer `licensed`, and the difference is what an",
62
+ " authorised holder can do: a licensed model has nothing to",
63
+ " download however good your credentials are. `latest` may",
64
+ " never be private either — an unpinned self-hoster would get",
65
+ " a 401 instead of a model — and check_prompt_parity.py says so.",
66
+ "",
67
+ " \"licensed\" — NOT downloadable by anyone. Reachable only through",
68
+ " api.captchakraken.com, or by a holder of a licence we issue.",
69
+ " There is no Hub repo to point CAPTCHA_LORA_ADAPTER at, and",
70
+ " `captchakraken fetch` refuses rather than 404ing at the Hub.",
71
+ "",
72
+ "Why it lives HERE and not only in a policy doc: this file is already the one",
73
+ "place that decides what an unpinned client downloads and what prompts it",
74
+ "sends. A second list of 'models you may not publish' kept somewhere else",
75
+ "would be a list that drifts, and the drift is silent in the direction that",
76
+ "costs the most. The finetune repo's publisher reads this field and REFUSES",
77
+ "to upload a licensed model to the Hub; check_prompt_parity.py refuses to let",
78
+ "`latest` be one, because an unpinned self-hoster would resolve to weights",
79
+ "they cannot obtain.",
80
+ "",
81
+ "`experts` is the fourth axis, and the newest. A ROUTED model is not one",
82
+ "adapter: Abyss is four LoRAs behind one endpoint, and the thing that picks",
83
+ "between them is the PROMPT FAMILY the request is about to send — grid,",
84
+ "pixel, video, text. See docs/MOE_LORA_DESIGN.md §11: the router is the",
85
+ "prompt family and it already exists, so no DOM access, no image classifier",
86
+ "and no cooperation from the caller is needed to reach the right expert.",
87
+ "",
88
+ "It lives here for the same reason prompt_version does. WHICH EXPERTS EXIST",
89
+ "IS A FACT ABOUT THE MODEL. A client that hardcoded four names would send",
90
+ "them to v1.2 — which serves one adapter under one name — and every request",
91
+ "would 404 on a model the endpoint has never heard of. ABSENT MEANS NOT",
92
+ "ROUTED, and that is the whole backwards-compatibility story: every model",
93
+ "published so far declares no `experts`, so the client sends the single",
94
+ "`lora_name` on every request exactly as it did before this field existed.",
95
+ "",
96
+ "An unmapped or unrecognised family degrades to the model's own",
97
+ "`lora_name` — the GENERALIST — never to another expert and never to an",
98
+ "error. The day the generation-2 `text` family reached ckgate without a",
99
+ "marker, refusing it cost every distorted-text solve in production for a",
100
+ "day; a caller prompting in their own words is the expected case for a",
101
+ "shipped model, not a 400.",
102
+ "",
103
+ "Each expert name must resolve — through `served_aliases` or as a repo id —",
104
+ "to a REGISTERED entry declaring the same `prompt_version` and the same",
105
+ "`pixel_budget` as the model that routes to it. check_prompt_parity.py",
106
+ "enforces that. It is what makes aliasing four arms onto one entry safe",
107
+ "rather than convenient: the four share a base, a schema and a band today",
108
+ "(measured, not assumed — runs 20260906-0142xx all carry prompt generation",
109
+ "2 and a flat 518400 band), and the day one of them trains at a different",
110
+ "generation or a different band the gate refuses the alias and that arm has",
111
+ "to graduate to an entry of its own. A divergence that resolved silently",
112
+ "through a shared entry would be the 2026-07-18 regression with four models",
113
+ "in it instead of one.",
114
+ "",
115
+ "The four arms are on the wire training as this is written (runs",
116
+ "20260906-0142xx) and none of them is published yet, so the `experts` map",
117
+ "below names weights that do not exist. That is the same deliberate",
118
+ "arming as `Abyss` itself — registered before it existed so the licence",
119
+ "gates were armed rather than retrofitted — and it costs nothing, because",
120
+ "an expert name only ever reaches the wire for a caller who asked for a",
121
+ "routed model in the first place."
51
122
  ],
52
123
  "latest": "CaptchaKraken/CaptchaKraken-Lora-v1.2",
53
124
  "models": {
@@ -61,7 +132,7 @@
61
132
  "min": 518400,
62
133
  "max": 518400
63
134
  },
64
- "note": "Run 20260812-005302 (ready-to-deploy). PUBLIC on the Hub under Source-Available License v1.1, and the first published generation-2 model. TRAINED with floor 448^2 / ceiling 720^2, but MEASURED best served at a flat 720^2 — floor == ceiling, so every image arrives at exactly one size. Tier 2 overall 0.6115 -> 0.6412 across the full 1458-sample real eval and reCAPTCHA 3x3 exact 0.648 -> 0.715, against run-to-run noise of +/-0.001. Swept 2026-08-18 over 448/576/640/704/720/736/768/896/1024^2: the plateau is 704-736^2 and it falls off above 768^2, where a 400x580 portrait capture runs more than 40% past the ViT's 48x48 position grid. Note 640^2 is where that grid is exactly saturated and it scores WORSE (0.6357) — mild extrapolation plus real pixels beats staying inside the grid. Was served as `captcha-v12` while absent from this file, which resolved it to generation 1 prompts against generation-2 weights — the failure this file exists to prevent."
135
+ "note": "PUBLIC on the Hub under Source-Available License v1.1, and the first published generation-2 model. TRAINED with floor 448^2 / ceiling 720^2, but MEASURED best served at a flat 720^2 — floor == ceiling, so every image arrives at exactly one size and the band cannot drift. The band was chosen by sweeping the serving floor over 448/576/640/704/720/736/768/896/1024^2 and scoring each arm: the plateau is 704-736^2, and it falls off above 768^2, where a 400x580 portrait capture runs more than 40% past the ViT's 48x48 position grid. Do not \"just use the ViT's native grid\": 640^2 is where that grid is exactly saturated for a portrait capture, and it scores WORSE than 720^2 — mild position-embedding extrapolation plus real pixels beats staying inside the grid. Was served as `captcha-v12` while absent from this file, which resolved it to generation 1 prompts against generation-2 weights — the failure this file exists to prevent."
65
136
  },
66
137
  "CaptchaKraken/Twilight-v1.2-FP8": {
67
138
  "prompt_version": "2",
@@ -110,11 +181,76 @@
110
181
  "kind": "merged",
111
182
  "lora_name": "captcha",
112
183
  "note": "Ships its own prompts.json; this entry keeps it resolvable offline."
184
+ },
185
+ "CaptchaKraken/Abyss": {
186
+ "availability": "licensed",
187
+ "prompt_version": "2",
188
+ "kind": "lora",
189
+ "base_model": "Qwen/Qwen3.5-9B",
190
+ "lora_name": "abyss-general",
191
+ "pixel_budget": {
192
+ "min": 518400,
193
+ "max": 518400
194
+ },
195
+ "experts": {
196
+ "grid": "abyss-grid",
197
+ "pixel": "abyss-general",
198
+ "video": "abyss-video",
199
+ "text": "abyss-text"
200
+ },
201
+ "note": "IN DEVELOPMENT. Never published, never downloadable — see `availability`. Registered before it exists so the licence gates are armed rather than retrofitted after the first artifact is sitting on a disk: the publisher, the parity gate and `captchakraken fetch` all read this entry, and a rule added the day it is first needed is a rule that was absent on the day it was first needed. Being nameable is not being downloadable: `availability` is what refuses the weights, and the two are independent on purpose — a licence holder DOES name this model, which is why it and each of its experts has a served_aliases entry. ABYSS IS THE ROUTED 9B, NOT THE 27B. It was going to be a single Qwen3.8-27B adapter; that model was trained (run 20260903-235101) and did not earn its cost, so the product became the four-expert mixture on Qwen3.5-9B that docs/MOE_LORA_DESIGN.md §7 and §10 specify. A routed 27B may be a later generation of this same name; it would be a new entry when it exists, because the base model is what a self-hoster loads. `lora_name` is `abyss-general` rather than a fifth name: the generalist IS the fallback for any family `experts` does not map, so pointing the two at one adapter is what makes §11’s “degrade to the generalist, never 400” true with one fewer resident LoRA slot — and slots are the binding constraint (§10, five names against `--max-loras`). prompt_version 2 and the flat 518400 band are what all four arms are training at TODAY (verified against training_runs/20260906-0142xx/run.json, min_pixels == max_pixels == 518400); CONFIRM BOTH from the run records at publish time and re-record here in the publishing commit, and if any arm has diverged give that arm its own entry — check_prompt_parity.py refuses the shared alias rather than resolving it silently."
202
+ },
203
+ "CaptchaKraken/Abyss-27B": {
204
+ "availability": "private",
205
+ "prompt_version": "2",
206
+ "kind": "lora",
207
+ "base_model": "Qwen/Qwen3.8-27B",
208
+ "lora_revision": "main",
209
+ "lora_name": "abyss-27b",
210
+ "pixel_budget": {
211
+ "min": 518400,
212
+ "max": 518400
213
+ },
214
+ "note": "PRIVATE on the Hub. The single-adapter 27B that Abyss started as — run 20260903-235101, re-gated as 20260906-025040. NOT the routed model and not one of its arms: it is a Qwen3.8-27B, and the base model is what a self-hoster loads, so it could never share the `CaptchaKraken/Abyss` entry however similar the product story is. Kept because it is measured and it wins: Tier 2 gated_overall 0.6699 against the v1.2 baseline's 0.6456, solved 0.7113, forgetting 0.0052. What it did not win was its COST — see the `CaptchaKraken/Abyss` note — which is a statement about serving economics, not about the weights, and is exactly why the weights are archived rather than deleted. Band and generation are the run's own (min_pixels == max_pixels == 518400, PROMPT_VERSION 2), read from training_runs/20260906-025040/model.json rather than assumed."
215
+ },
216
+ "CaptchaKraken/Abyss-grid": {
217
+ "availability": "private",
218
+ "prompt_version": "2",
219
+ "kind": "lora",
220
+ "base_model": "Qwen/Qwen3.5-9B",
221
+ "lora_revision": "main",
222
+ "lora_name": "abyss-grid",
223
+ "pixel_budget": {
224
+ "min": 518400,
225
+ "max": 518400
226
+ },
227
+ "note": "PRIVATE on the Hub. The 'grid' expert arm of the routed CaptchaKraken/Abyss, from run 20260906-014318. Tier 2 gated_overall 0.7223 against a 0.6644 baseline, solved 0.6719, forgetting 0; Tier 3 9/10 pairs. Its run reads `blocked` for a TIER 1 failure that was this repo's own test harness (a module its sys.path insertion made unclassifiable, fixed in 79201f9aa) and not the model — the tier that judges the weights passed. It has its OWN entry rather than an alias onto the router because it now has its own weights at its own repo id, and `experts` names what goes ON THE WIRE — an alias would resolve a real, separately fetchable adapter to another model's entry. It still declares the router's prompt generation and pixel band, which is what check_prompt_parity.py requires of every expert and what makes the mixture coherent."
228
+ },
229
+ "CaptchaKraken/Abyss-text": {
230
+ "availability": "private",
231
+ "prompt_version": "2",
232
+ "kind": "lora",
233
+ "base_model": "Qwen/Qwen3.5-9B",
234
+ "lora_revision": "main",
235
+ "lora_name": "abyss-text",
236
+ "pixel_budget": {
237
+ "min": 518400,
238
+ "max": 518400
239
+ },
240
+ "note": "PRIVATE on the Hub. The 'text' expert arm of the routed CaptchaKraken/Abyss, from run 20260906-014253. Tier 2 gated_overall 0.8750 against a 0.8375 baseline, solved 0.8750, forgetting 0; Tier 3 5/6 pairs. `passed-partial` because the run is type-narrowed by construction — an arm trains on its own families — which disqualifies `ready-to-deploy` and says nothing bad about it. It has its OWN entry rather than an alias onto the router because it now has its own weights at its own repo id, and `experts` names what goes ON THE WIRE — an alias would resolve a real, separately fetchable adapter to another model's entry. It still declares the router's prompt generation and pixel band, which is what check_prompt_parity.py requires of every expert and what makes the mixture coherent."
113
241
  }
114
242
  },
115
243
  "served_aliases": {
116
- "_comment": "A hosted endpoint serves a bare name, not a repo id. Map it back so prompt resolution works for callers who only ever see the alias.",
117
- "captcha": "CaptchaKraken/CaptchaKraken_v1.1",
118
- "captcha-v12": "CaptchaKraken/CaptchaKraken-Lora-v1.2"
244
+ "_comment": "A hosted endpoint serves a bare name, not a repo id. Map it back so prompt resolution works for callers who only ever see the alias. api.captchakraken.com now serves ONE model behind the permanent public name `captcha`, whatever the caller sends, so `captcha` means whichever adapter is current there — today CaptchaKraken-Lora-v1.2 — and NOT the v1.1 weights that share the name as a local vLLM alias. Self-hosters resolve by repo id via CAPTCHA_LORA_ADAPTER and are unaffected. Move this in the same change as the gateway's CKGATE_UPSTREAM_MODEL or the hosted model answers a prompt generation it was not trained on.",
245
+ "captcha": "CaptchaKraken/CaptchaKraken-Lora-v1.2",
246
+ "captcha-v12": "CaptchaKraken/CaptchaKraken-Lora-v1.2",
247
+ "_comment_abyss": "`abyss` is here for the OPPOSITE reason to the two above, and it is not a contradiction of `availability: licensed`. An alias says which model a served NAME means; availability says whether its weights can be downloaded. A licence holder does name this model — the hosted API accepts it from a licensed key and lists it to them on /v1/models so their SDK can — and without this line `canonical_model_id('abyss')` is None, so their prompts resolve through the `latest` fallback: the exact model/generation mispairing this file exists to prevent, silent, and only accidentally right while `latest` happens to share the generation. It does NOT make the weights fetchable; `captchakraken fetch` reads availability and refuses this name, which is also the clear error a self-hoster who typed it deserves.",
248
+ "abyss": "CaptchaKraken/Abyss",
249
+ "_comment_abyss_experts": "The four expert arms. These are the names that go ON THE WIRE — a routed request for a grid names `abyss-grid`, never `abyss` — so without these lines `canonical_model_id` returns None for the string the client actually sent, and prompt resolution falls through to the `latest` fallback on every routed solve. That is the mispairing `abyss` itself is here to prevent, one level down and four times over. Aliasing four arms onto one entry rather than giving each its own is deliberate and CHECKED: they share a base, a prompt generation and a pixel band today, and check_prompt_parity.py requires every expert name to resolve to a registered entry declaring the SAME prompt_version and pixel_budget as the router — so the day an arm trains at another generation or another band, the gate refuses the alias and that arm has to graduate to an entry of its own.",
250
+ "abyss-grid": "CaptchaKraken/Abyss-grid",
251
+ "abyss-general": "CaptchaKraken/Abyss",
252
+ "abyss-video": "CaptchaKraken/Abyss",
253
+ "abyss-text": "CaptchaKraken/Abyss-text",
254
+ "abyss-27b": "CaptchaKraken/Abyss-27B"
119
255
  }
120
256
  }