witbitz-code 1.9.1__tar.gz → 1.9.2__tar.gz

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 (117) hide show
  1. {witbitz_code-1.9.1/src/witbitz_code.egg-info → witbitz_code-1.9.2}/PKG-INFO +3 -3
  2. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/README.md +2 -2
  3. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/pyproject.toml +1 -1
  4. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/__init__.py +1 -1
  5. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/confidential.py +34 -1
  6. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/confidential_models.json +0 -11
  7. {witbitz_code-1.9.1 → witbitz_code-1.9.2/src/witbitz_code.egg-info}/PKG-INFO +3 -3
  8. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_confidential.py +37 -7
  9. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/LICENSE +0 -0
  10. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/setup.cfg +0 -0
  11. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/__main__.py +0 -0
  12. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/_js.py +0 -0
  13. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/account.py +0 -0
  14. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/asks.py +0 -0
  15. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/attachments.py +0 -0
  16. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/auto.py +0 -0
  17. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/auto_runner.py +0 -0
  18. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/browse.py +0 -0
  19. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/browsetree.py +0 -0
  20. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/cli.py +0 -0
  21. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/codekeys.py +0 -0
  22. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/codetools.py +0 -0
  23. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/compress.py +0 -0
  24. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/computers.py +0 -0
  25. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/connector.py +0 -0
  26. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/devicelink.py +0 -0
  27. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/dirs.py +0 -0
  28. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/folders.py +0 -0
  29. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/gateway.py +0 -0
  30. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  31. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/icons/witbitz-code.ico +0 -0
  32. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/images.py +0 -0
  33. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/keys.py +0 -0
  34. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/link.py +0 -0
  35. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/mkdir.py +0 -0
  36. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/models.py +0 -0
  37. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/net.py +0 -0
  38. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/offered_models.json +0 -0
  39. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  40. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
  41. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
  42. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +0 -0
  43. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +0 -0
  44. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
  45. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  46. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  47. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  48. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/outputs.py +0 -0
  49. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/pair.py +0 -0
  50. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/pairings.py +0 -0
  51. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/payers.py +0 -0
  52. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/plugins.py +0 -0
  53. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/policy.py +0 -0
  54. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/ports.py +0 -0
  55. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/qr.py +0 -0
  56. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/receipt.py +0 -0
  57. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/relay.py +0 -0
  58. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/remind.py +0 -0
  59. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/seccheck.py +0 -0
  60. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/seen.py +0 -0
  61. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/selfupdate.py +0 -0
  62. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/tinfoil_read.py +0 -0
  63. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/tools_catalog.json +0 -0
  64. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/tools_probe.py +0 -0
  65. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/toolsetup.py +0 -0
  66. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/ui.py +0 -0
  67. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/app.css +0 -0
  68. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/app.js +0 -0
  69. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/ui_static/index.html +0 -0
  70. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/uitools.py +0 -0
  71. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/uninstall.py +0 -0
  72. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/winagent.py +0 -0
  73. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/winbg.py +0 -0
  74. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/winconsole.py +0 -0
  75. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/wizard.py +0 -0
  76. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code/wslstart.py +0 -0
  77. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  78. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  79. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  80. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/requires.txt +0 -0
  81. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/src/witbitz_code.egg-info/top_level.txt +0 -0
  82. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_asks.py +0 -0
  83. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_attachments.py +0 -0
  84. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_auto.py +0 -0
  85. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_browse.py +0 -0
  86. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_browsetree.py +0 -0
  87. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_cli.py +0 -0
  88. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_codekeys.py +0 -0
  89. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_codetools.py +0 -0
  90. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_connector_e2e.py +0 -0
  91. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_devicelink.py +0 -0
  92. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_dirs.py +0 -0
  93. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_files_and_compress.py +0 -0
  94. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_folders.py +0 -0
  95. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_images.py +0 -0
  96. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_outputs.py +0 -0
  97. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_pair_flows.py +0 -0
  98. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_plugins.py +0 -0
  99. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_policy.py +0 -0
  100. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_ports.py +0 -0
  101. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_ports_win.py +0 -0
  102. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_registry.py +0 -0
  103. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_relay_codec.py +0 -0
  104. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_remind.py +0 -0
  105. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_seccheck.py +0 -0
  106. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_selfupdate.py +0 -0
  107. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_setup.py +0 -0
  108. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_tools_probe.py +0 -0
  109. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_toolsetup.py +0 -0
  110. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_ui.py +0 -0
  111. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_uitools.py +0 -0
  112. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_uninstall.py +0 -0
  113. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_winagent.py +0 -0
  114. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_winbg.py +0 -0
  115. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_winconsole.py +0 -0
  116. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_windows_files.py +0 -0
  117. {witbitz_code-1.9.1 → witbitz_code-1.9.2}/tests/test_wslstart.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.9.1
3
+ Version: 1.9.2
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -67,8 +67,8 @@ opened (a one-time key traded for a private cookie; no other website can drive i
67
67
  **No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
68
68
  providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
69
69
  `~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
70
- still go from this computer straight to TrustedRouter and Tinfoil. Coding is by invitation for now — an account that is not
71
- invited gets no keys, and the pairing stops with a sentence saying so. A computer paired before 1.3 has no account keys:
70
+ still go from this computer straight to TrustedRouter and Tinfoil. An account that is not invited gets the free allowance —
71
+ one computer, $1 of models and $0.25 of tools a month; when it is used up, Code says so and when it comes back. A computer paired before 1.3 has no account keys:
72
72
  pair it once more (`witbitz-code pair`).
73
73
 
74
74
  **Several accounts on one computer:** each pays for its own prompts. OpenCode names the conversation on every call it
@@ -40,8 +40,8 @@ opened (a one-time key traded for a private cookie; no other website can drive i
40
40
  **No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
41
41
  providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
42
42
  `~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
43
- still go from this computer straight to TrustedRouter and Tinfoil. Coding is by invitation for now — an account that is not
44
- invited gets no keys, and the pairing stops with a sentence saying so. A computer paired before 1.3 has no account keys:
43
+ still go from this computer straight to TrustedRouter and Tinfoil. An account that is not invited gets the free allowance —
44
+ one computer, $1 of models and $0.25 of tools a month; when it is used up, Code says so and when it comes back. A computer paired before 1.3 has no account keys:
45
45
  pair it once more (`witbitz-code pair`).
46
46
 
47
47
  **Several accounts on one computer:** each pays for its own prompts. OpenCode names the conversation on every call it
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.9.1"
7
+ version = "1.9.2"
8
8
  description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.9.1"
7
+ __version__ = "1.9.2"
@@ -45,6 +45,22 @@ _SUBJECT = re.compile(r'"(filePath|path|command|pattern|url)"\s*:\s*"((?:[^"\\]|
45
45
  _DATA_URL = re.compile(r"^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$", re.S)
46
46
 
47
47
 
48
+ # THE MONTH'S ALLOWANCE IS USED (the owner, 2026-10-20: the free Code allowance — "treat it like any other end of budget"). At a
49
+ # key's monthly cap TrustedRouter answers 429 "API key monthly spend limit exceeded; resets at 2026-11-01T00:00:00Z" (measured
50
+ # 2026-10-20). Passed on as a 429, OpenCode reads "rate limited" and RETRIES — the turn hangs. So it becomes a 402 with a
51
+ # sentence a person can act on. tools/code-confidential.mjs allowanceUsed says the same words.
52
+ PLAN_URL = "https://getspaces.witbitz.chat/account"
53
+ _MONTHS = ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December")
54
+
55
+
56
+ def allowance_used(status: int, text: str) -> "Refusal | None":
57
+ if status != 429 or not re.search(r"monthly spend limit exceeded", text or "", re.I):
58
+ return None
59
+ m = re.search(r"resets at (\d{4})-(\d{2})-(\d{2})", text or "")
60
+ day = f"{int(m.group(3))} {_MONTHS[int(m.group(2)) - 1]}" if m and 1 <= int(m.group(2)) <= 12 else "the 1st of next month"
61
+ return Refusal(402, f"This computer's Code allowance for the month is used up — it comes back on {day}. Your plan and use: {PLAN_URL}", "allowance_used")
62
+
63
+
48
64
  class Refusal(Exception):
49
65
  def __init__(self, status: int, message: str, kind: str = "confidential_refused") -> None:
50
66
  super().__init__(message)
@@ -117,9 +133,14 @@ async def convert_unreadable(body: dict, label: str, to_text: Callable | None) -
117
133
  return {**body, "messages": messages}
118
134
 
119
135
 
136
+ # Only the providers whose enclaves we verify (2026-10-20, ⇄ agent/providers/trustedrouter.mjs CONFIDENTIAL_PROVIDERS):
137
+ # TrustedRouter's min_privacy=confidential alone began answering some models from an ordinary server (receipt `tls-webpki`).
138
+ CONFIDENTIAL_PROVIDERS = ("tinfoil",)
139
+
140
+
120
141
  def stamp_floor(req: dict) -> dict:
121
142
  """The confidential floor, as a confidential room stamps it. Key ORDER matters — the receipt signs these exact bytes."""
122
- return {**req, "stream": True, "stream_options": {"include_usage": True}, "provider": {**(req.get("provider") or {}), "min_privacy": "confidential"}}
143
+ return {**req, "stream": True, "stream_options": {"include_usage": True}, "provider": {**(req.get("provider") or {}), "min_privacy": "confidential", "only": list(CONFIDENTIAL_PROVIDERS)}}
123
144
 
124
145
 
125
146
  # ── the answer ───────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -596,6 +617,15 @@ class ConfidentialProxy:
596
617
  async with self.client.stream(req.method, target, headers=_forward_headers(req.headers), content=None if req.method in ("GET", "HEAD") else out) as up:
597
618
  headers = {k: v for k, v in up.headers.items() if k.lower() not in ("content-encoding", "content-length", "transfer-encoding", "connection")}
598
619
  self._key_refused(up.status_code)
620
+ if up.status_code == 429: # the month's allowance, said plainly — never a "rate limit" OpenCode would retry
621
+ text = (await up.aread()).decode("utf-8", "replace")
622
+ used = allowance_used(up.status_code, text)
623
+ if used:
624
+ await self._fail(res, used, False)
625
+ return
626
+ await res.head(up.status_code, headers)
627
+ await res.end(text)
628
+ return
599
629
  await res.head(up.status_code, headers)
600
630
  async for c in up.aiter_bytes():
601
631
  await res.write(c)
@@ -637,6 +667,9 @@ class ConfidentialProxy:
637
667
  if not up.is_success:
638
668
  self._key_refused(up.status_code)
639
669
  text = (await up.aread()).decode("utf-8", "replace")
670
+ used = allowance_used(up.status_code, text)
671
+ if used:
672
+ raise used
640
673
  if res.headers_sent:
641
674
  raise Refusal(502, f"{label}: TrustedRouter answered {up.status_code} when asked again ({text[:160]})", "upstream_error")
642
675
  await res.head(up.status_code, {"content-type": up.headers.get("content-type", "application/json")})
@@ -11,17 +11,6 @@
11
11
  ],
12
12
  "maxTokens": 24000
13
13
  },
14
- {
15
- "model": "z-ai/glm-5.3-flash",
16
- "label": "GLM 5.3 Flash",
17
- "vision": true,
18
- "efforts": [
19
- "low",
20
- "high",
21
- "max"
22
- ],
23
- "maxTokens": 16000
24
- },
25
14
  {
26
15
  "model": "z-ai/glm-5.3",
27
16
  "label": "GLM 5.3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.9.1
3
+ Version: 1.9.2
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -67,8 +67,8 @@ opened (a one-time key traded for a private cookie; no other website can drive i
67
67
  **No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
68
68
  providers (today up to $50 of models and $5 of tools a month per computer). They are sent sealed to this computer, kept in
69
69
  `~/.witbitz/code/account-keys.json` (0600) and never given to OpenCode: the local proxy puts them on each call. Model calls
70
- still go from this computer straight to TrustedRouter and Tinfoil. Coding is by invitation for now — an account that is not
71
- invited gets no keys, and the pairing stops with a sentence saying so. A computer paired before 1.3 has no account keys:
70
+ still go from this computer straight to TrustedRouter and Tinfoil. An account that is not invited gets the free allowance —
71
+ one computer, $1 of models and $0.25 of tools a month; when it is used up, Code says so and when it comes back. A computer paired before 1.3 has no account keys:
72
72
  pair it once more (`witbitz-code pair`).
73
73
 
74
74
  **Several accounts on one computer:** each pays for its own prompts. OpenCode names the conversation on every call it
@@ -61,7 +61,8 @@ def test_any_change_to_what_was_sent_or_received_is_refused():
61
61
  assert v(request_body=request + b" ")["error"] == "receipt_req_hash"
62
62
  assert v(nonce="someone-elses-nonce")["error"] == "receipt_nonce_mismatch"
63
63
  tampered = capture_of(stream)
64
- tampered.datas[1] = tampered.datas[1].replace(b'"OK"', b'"NO"')
64
+ at = next(i for i, d in enumerate(tampered.datas) if b'"OK"' in d) # wherever the captured answer says OK
65
+ tampered.datas[at] = tampered.datas[at].replace(b'"OK"', b'"NO"')
65
66
  assert v(capture=tampered)["error"] == "receipt_resp_hash"
66
67
  dropped = capture_of(stream)
67
68
  dropped.datas.pop()
@@ -91,14 +92,14 @@ from conftest import requires_node, run_node # noqa: E402
91
92
 
92
93
  from witbitz_code import confidential as cf, models as mdl # noqa: E402
93
94
 
94
- SAY_OK = {"model": "deepseek/deepseek-v4-flash", "messages": [{"role": "user", "content": "Say OK."}], "max_tokens": 5, "stream": True}
95
+ SAY_OK = {"model": "deepseek/deepseek-v4.1-flash", "messages": [{"role": "user", "content": "Say OK."}], "max_tokens": 200, "stream": True}
95
96
  PNG = "data:image/png;base64,bm90IHJlYWxseSBhIHBuZw=="
96
97
 
97
98
 
98
99
  def replay_models():
99
- # The captured exchange is deepseek-v4-flash's (retired from the confidential tier since), and its receipt signs those
100
+ # The captured exchange is deepseek-v4.1-flash's (recaptured 2026-10-20 with the Tinfoil-only floor), and its receipt signs those
100
101
  # exact bytes — so the replay hands the proxy that model, as the Node test does.
101
- return {**mdl.confidential_models(), "deepseek/deepseek-v4-flash": {"vision": False, "label": "DeepSeek V4 Flash"}}
102
+ return {**mdl.confidential_models()}
102
103
 
103
104
 
104
105
  class FakeNet:
@@ -217,9 +218,9 @@ def test_a_lapsed_proof_before_anything_reached_opencode_is_asked_again_with_a_f
217
218
 
218
219
  async def t(post, net, _p):
219
220
  r = await post({**SAY_OK, "stream": False})
220
- assert r.status_code == 200 and r.json()["choices"][0]["message"]["content"] == "OK."
221
+ assert r.status_code == 200 and r.json()["choices"][0]["message"]["content"] == "OK"
221
222
  assert len(net.calls) == 2 and seen == {"verify": 2, "nonce": 2}
222
- assert "REFUSED deepseek/deepseek-v4-flash — receipt_verification_window" in "\n".join(logs) and "↻" in "\n".join(logs)
223
+ assert "REFUSED deepseek/deepseek-v4.1-flash — receipt_verification_window" in "\n".join(logs) and "↻" in "\n".join(logs)
223
224
  with_proxy(t, verify=lapsing, nonce=nonce, log=logs.append)
224
225
 
225
226
 
@@ -246,7 +247,7 @@ def test_the_gateway_must_prove_its_attestation_before_anything_leaves():
246
247
 
247
248
  @needs_fixture
248
249
  def test_an_image_for_a_text_only_model_is_refused_without_a_tinfoil_key_and_read_with_one():
249
- img = {**SAY_OK, "messages": [{"role": "user", "content": [{"type": "text", "text": "what is this?"}, {"type": "image_url", "image_url": {"url": PNG}}]}]}
250
+ img = {**SAY_OK, "model": "z-ai/glm-5.3", "messages": [{"role": "user", "content": [{"type": "text", "text": "what is this?"}, {"type": "image_url", "image_url": {"url": PNG}}]}]} # a TEXT-ONLY confidential model (as the Node test)
250
251
 
251
252
  async def no_key(post, net, _p):
252
253
  r = await post(img)
@@ -380,3 +381,32 @@ def test_opencode_starts_on_the_catalog_s_confidential_default():
380
381
  entry = next(m for m in models.catalog() if m["model"] == models.DEFAULT_MODEL)
381
382
  assert want in (entry.get("id"), entry["model"].split("/")[-1]), f"catalog default {want!r} vs {models.DEFAULT_MODEL!r}"
382
383
  assert models.default_model_config() == {"model": "trustedrouter/deepseek/deepseek-v4.1-flash"}
384
+
385
+
386
+ # THE MONTH'S ALLOWANCE IS USED (the free Code allowance, 2026-10-20): TrustedRouter's answer at a key's cap, as measured.
387
+ SPEND_LIMIT = {"error": {"message": "API key monthly spend limit exceeded; resets at 2026-11-01T00:00:00Z", "source": "router", "status": 429}}
388
+
389
+
390
+ class LimitNet(FakeNet):
391
+ def handler(self, req: httpx.Request):
392
+ if "googleapis.com/service_accounts" in str(req.url):
393
+ return httpx.Response(200, json=self.meta["googleJwks"])
394
+ self.calls.append({"url": str(req.url)})
395
+ return httpx.Response(429, json=SPEND_LIMIT)
396
+
397
+
398
+ @needs_fixture
399
+ def test_the_months_allowance_used_is_a_402_that_says_so_never_a_429_opencode_would_retry():
400
+ meta, stream, _ = fixture()
401
+ models = replay_models()
402
+ for model in ["openai/gpt-6-sol", next(iter(models))]: # pass-through, and a confidential model
403
+ async def t(post, _net, _p, model=model):
404
+ r = await post({"model": model, "messages": [{"role": "user", "content": "hi"}], "stream": True})
405
+ assert r.status_code == 402, model
406
+ e = r.json()["error"]
407
+ assert e["type"] == "allowance_used", model
408
+ assert "allowance for the month is used up — it comes back on 1 November" in e["message"], e["message"]
409
+ assert "getspaces.witbitz.chat/account" in e["message"]
410
+ with_proxy(t, net=LimitNet(meta, stream.decode("utf-8")), models=models)
411
+ assert cf.allowance_used(429, '{"error":{"message":"rate limited"}}') is None, "a real rate limit stays a rate limit"
412
+ assert cf.allowance_used(400, json.dumps(SPEND_LIMIT)) is None
File without changes
File without changes