cade-cli 0.16.2__tar.gz → 0.16.4__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 (202) hide show
  1. {cade_cli-0.16.2 → cade_cli-0.16.4}/PKG-INFO +2 -2
  2. {cade_cli-0.16.2 → cade_cli-0.16.4}/README.md +1 -1
  3. {cade_cli-0.16.2 → cade_cli-0.16.4}/pyproject.toml +1 -1
  4. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/mcp.py +64 -0
  5. cade_cli-0.16.4/src/cadecoder/cli/mcp_hosts.py +351 -0
  6. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/onboarding.py +16 -0
  7. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/providers/anthropic.py +20 -2
  8. cade_cli-0.16.4/src/cadecoder/providers/http_client.py +52 -0
  9. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/providers/openai.py +25 -2
  10. {cade_cli-0.16.2 → cade_cli-0.16.4}/.gitignore +0 -0
  11. {cade_cli-0.16.2 → cade_cli-0.16.4}/LICENSE +0 -0
  12. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/__init__.py +0 -0
  13. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/__main__.py +0 -0
  14. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/_metadata.py +0 -0
  15. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/config.py +0 -0
  16. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/errors.py +0 -0
  17. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/grants.py +0 -0
  18. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/resources.py +0 -0
  19. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/sandbox.py +0 -0
  20. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/server.py +0 -0
  21. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/__init__.py +0 -0
  22. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/_read_cache.py +0 -0
  23. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/access.py +0 -0
  24. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/agents.py +0 -0
  25. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/context.py +0 -0
  26. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/filesystem.py +0 -0
  27. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/git.py +0 -0
  28. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/notifications.py +0 -0
  29. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/questions.py +0 -0
  30. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/results.py +0 -0
  31. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/schemas.py +0 -0
  32. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/search.py +0 -0
  33. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/shell.py +0 -0
  34. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/snippets.py +0 -0
  35. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/tasks.py +0 -0
  36. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/web_fetch.py +0 -0
  37. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/tools/web_search.py +0 -0
  38. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/utils.py +0 -0
  39. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/__init__.py +0 -0
  40. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/arcade_client.py +0 -0
  41. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/cache.py +0 -0
  42. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/extract.py +0 -0
  43. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/markdown.py +0 -0
  44. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/preapproved.py +0 -0
  45. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/prompts.py +0 -0
  46. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/scrape.py +0 -0
  47. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/search/__init__.py +0 -0
  48. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/search/arcade.py +0 -0
  49. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/search/base.py +0 -0
  50. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cade_mcp_local/web/search/http.py +0 -0
  51. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/__init__.py +0 -0
  52. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ai/__init__.py +0 -0
  53. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ai/prompts.py +0 -0
  54. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/__init__.py +0 -0
  55. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/app.py +0 -0
  56. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/auth.py +0 -0
  57. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/__init__.py +0 -0
  58. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/account.py +0 -0
  59. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/agents.py +0 -0
  60. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/auth.py +0 -0
  61. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/chat.py +0 -0
  62. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/cron.py +0 -0
  63. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/daemon.py +0 -0
  64. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/grant.py +0 -0
  65. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/hooks.py +0 -0
  66. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/mem.py +0 -0
  67. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/tasks.py +0 -0
  68. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/thread.py +0 -0
  69. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/cli/commands/tools.py +0 -0
  70. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/__init__.py +0 -0
  71. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/agent.py +0 -0
  72. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/comms.py +0 -0
  73. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/config.py +0 -0
  74. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/constants.py +0 -0
  75. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/errors.py +0 -0
  76. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/frozen.py +0 -0
  77. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/git.py +0 -0
  78. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/inbox.py +0 -0
  79. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/logging.py +0 -0
  80. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/memory.py +0 -0
  81. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/model_specs.py +0 -0
  82. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/names.py +0 -0
  83. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/paths.py +0 -0
  84. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/runtime.py +0 -0
  85. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/secrets.py +0 -0
  86. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/spawn.py +0 -0
  87. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/core/types.py +0 -0
  88. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/__init__.py +0 -0
  89. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/adapters/__init__.py +0 -0
  90. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/adapters/base.py +0 -0
  91. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/adapters/factory.py +0 -0
  92. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/adapters/imessage.py +0 -0
  93. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/adapters/registry.py +0 -0
  94. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/adapters/stub.py +0 -0
  95. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/adapters/telegram.py +0 -0
  96. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/allowlist.py +0 -0
  97. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/attach.py +0 -0
  98. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/chunker.py +0 -0
  99. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/client.py +0 -0
  100. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/commands.py +0 -0
  101. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/config.py +0 -0
  102. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/control.py +0 -0
  103. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/cron.py +0 -0
  104. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/elicitation.py +0 -0
  105. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/ensure.py +0 -0
  106. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/format.py +0 -0
  107. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/__init__.py +0 -0
  108. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/app.py +0 -0
  109. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/arcade_export.py +0 -0
  110. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/auth.py +0 -0
  111. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/constants.py +0 -0
  112. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/elicitation.py +0 -0
  113. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/exposure.py +0 -0
  114. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/limits.py +0 -0
  115. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/middleware.py +0 -0
  116. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/resources.py +0 -0
  117. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/subscriptions.py +0 -0
  118. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/gateway/turn_bridge.py +0 -0
  119. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/grants.py +0 -0
  120. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/inbox_cron.py +0 -0
  121. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/install.py +0 -0
  122. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/live.py +0 -0
  123. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/lock.py +0 -0
  124. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/mesh.py +0 -0
  125. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/message_pump.py +0 -0
  126. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/observability.py +0 -0
  127. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/owner.py +0 -0
  128. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/progress_sink.py +0 -0
  129. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/registry.py +0 -0
  130. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/router.py +0 -0
  131. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/runner.py +0 -0
  132. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/runner_main.py +0 -0
  133. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/secrets.py +0 -0
  134. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/secrets_broker.py +0 -0
  135. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/server.py +0 -0
  136. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/services.py +0 -0
  137. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/shim.py +0 -0
  138. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/sinks.py +0 -0
  139. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/supervisor.py +0 -0
  140. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/tokens.py +0 -0
  141. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/traffic.py +0 -0
  142. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/turns.py +0 -0
  143. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/daemon/wake.py +0 -0
  144. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/execution/__init__.py +0 -0
  145. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/execution/context_window.py +0 -0
  146. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/execution/mcp_schema_index.py +0 -0
  147. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/execution/orchestrator.py +0 -0
  148. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/execution/parallel.py +0 -0
  149. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/execution/tool_result_store.py +0 -0
  150. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/execution/tool_schema_cache.py +0 -0
  151. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/__init__.py +0 -0
  152. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/config.py +0 -0
  153. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/engine.py +0 -0
  154. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/executors/__init__.py +0 -0
  155. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/executors/callback.py +0 -0
  156. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/executors/command.py +0 -0
  157. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/executors/http.py +0 -0
  158. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/executors/prompt.py +0 -0
  159. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/executors/ssrf_guard.py +0 -0
  160. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/matchers.py +0 -0
  161. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/registry.py +0 -0
  162. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/hooks/types.py +0 -0
  163. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/providers/__init__.py +0 -0
  164. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/providers/base.py +0 -0
  165. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/providers/ollama.py +0 -0
  166. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/serve/__init__.py +0 -0
  167. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/storage/__init__.py +0 -0
  168. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/storage/threads.py +0 -0
  169. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tasks/__init__.py +0 -0
  170. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tasks/scheduler.py +0 -0
  171. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tasks/store.py +0 -0
  172. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tasks/types.py +0 -0
  173. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/templates/login_failed.html +0 -0
  174. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/templates/login_success.html +0 -0
  175. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/templates/serve/launchd.plist.tmpl +0 -0
  176. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/templates/serve/serve.toml.example +0 -0
  177. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/templates/serve/systemd.service.tmpl +0 -0
  178. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/templates/styles.css +0 -0
  179. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/__init__.py +0 -0
  180. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/manager/__init__.py +0 -0
  181. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/manager/_request_ctx.py +0 -0
  182. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/manager/base.py +0 -0
  183. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/manager/composite.py +0 -0
  184. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/manager/config.py +0 -0
  185. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/manager/mcp.py +0 -0
  186. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/search/__init__.py +0 -0
  187. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/search/discovered.py +0 -0
  188. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/search/scoring.py +0 -0
  189. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/tools/search/service.py +0 -0
  190. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ui/__init__.py +0 -0
  191. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ui/completion.py +0 -0
  192. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ui/display.py +0 -0
  193. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ui/elicitation.py +0 -0
  194. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ui/input.py +0 -0
  195. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ui/session.py +0 -0
  196. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/ui/slash.py +0 -0
  197. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/voice/__init__.py +0 -0
  198. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/voice/audio.py +0 -0
  199. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/voice/cleanup.py +0 -0
  200. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/voice/session.py +0 -0
  201. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/voice/stt.py +0 -0
  202. {cade_cli-0.16.2 → cade_cli-0.16.4}/src/cadecoder/voice/tts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cade-cli
3
- Version: 0.16.2
3
+ Version: 0.16.4
4
4
  Summary: Cade - The CLI Agent from Arcade.dev (install: curl -fsSL https://arcadeagent.dev/install | sh)
5
5
  Project-URL: Homepage, https://arcade.dev
6
6
  Project-URL: Documentation, https://arcadeagent.dev/docs
@@ -129,7 +129,7 @@ The script detects your OS/arch, downloads the matching prebuilt binary
129
129
  `~/.local/bin`. Pin a version or install location:
130
130
 
131
131
  ```bash
132
- CADE_VERSION=0.16.2 CADE_INSTALL_DIR=/usr/local/bin \
132
+ CADE_VERSION=0.16.4 CADE_INSTALL_DIR=/usr/local/bin \
133
133
  sh -c "$(curl -fsSL https://arcadeagent.dev/install)"
134
134
  ```
135
135
 
@@ -38,7 +38,7 @@ The script detects your OS/arch, downloads the matching prebuilt binary
38
38
  `~/.local/bin`. Pin a version or install location:
39
39
 
40
40
  ```bash
41
- CADE_VERSION=0.16.2 CADE_INSTALL_DIR=/usr/local/bin \
41
+ CADE_VERSION=0.16.4 CADE_INSTALL_DIR=/usr/local/bin \
42
42
  sh -c "$(curl -fsSL https://arcadeagent.dev/install)"
43
43
  ```
44
44
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cade-cli"
3
- version = "0.16.2"
3
+ version = "0.16.4"
4
4
  description = "Cade - The CLI Agent from Arcade.dev (install: curl -fsSL https://arcadeagent.dev/install | sh)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -617,6 +617,70 @@ def authorize_server(
617
617
  raise typer.Exit(1)
618
618
 
619
619
 
620
+ @mcp_app.command("install-hosts")
621
+ def install_hosts_cmd(
622
+ agent: Annotated[
623
+ str,
624
+ typer.Option("--agent", "-a", help="Agent the stdio shim targets."),
625
+ ] = "default",
626
+ force: Annotated[
627
+ bool,
628
+ typer.Option("--force", "-f", help="Overwrite an existing cade MCP entry."),
629
+ ] = False,
630
+ dry_run: Annotated[
631
+ bool,
632
+ typer.Option("--dry-run", help="Show what would change without writing."),
633
+ ] = False,
634
+ ) -> None:
635
+ """Register Cade as an MCP server in Claude Desktop, Cursor, Claude Code, etc.
636
+
637
+ Only touches hosts that already look installed (their config dir/file exists
638
+ under your home directory). Safe to re-run — updates the command path after
639
+ upgrades. The curl installer runs this automatically unless
640
+ ``CADE_SKIP_MCP_HOSTS=1``.
641
+ """
642
+ from cadecoder.cli.mcp_hosts import install_into_hosts, resolve_cade_command
643
+
644
+ command = resolve_cade_command()
645
+ updates = install_into_hosts(command=command, agent=agent, force=force, dry_run=dry_run)
646
+ if not updates:
647
+ console.print(
648
+ "[dim]No MCP hosts detected (looked for Cursor, Claude Desktop, "
649
+ "Claude Code, OpenCode, Windsurf).[/dim]"
650
+ )
651
+ console.print(
652
+ "[dim]After installing a host, re-run [bold]cade mcp install-hosts[/bold].[/dim]"
653
+ )
654
+ return
655
+
656
+ console.print(f"[dim]Using binary:[/dim] {command}")
657
+ changed = 0
658
+ for u in updates:
659
+ style = {
660
+ "added": "green",
661
+ "updated": "cyan",
662
+ "unchanged": "dim",
663
+ "would-added": "yellow",
664
+ "would-updated": "yellow",
665
+ "error": "red",
666
+ "skipped": "dim",
667
+ }.get(u.status, "white")
668
+ msg = f"[{style}]{u.status}[/{style}] {u.name} [dim]{u.path}[/dim]"
669
+ if u.detail:
670
+ msg += f" ({u.detail})"
671
+ console.print(msg)
672
+ if u.status in ("added", "updated", "would-added", "would-updated"):
673
+ changed += 1
674
+
675
+ if changed and not dry_run:
676
+ console.print(
677
+ "\n[green]Done.[/green] Restart those apps, then run "
678
+ "[bold]cade daemon start[/bold] (or just [bold]cade[/bold] — it autostarts)."
679
+ )
680
+ elif dry_run:
681
+ console.print(f"\n[dim]Dry run — {changed} host(s) would change.[/dim]")
682
+
683
+
620
684
  @mcp_app.command("token-info")
621
685
  def token_info(
622
686
  name: Annotated[str, typer.Argument(help="Name of the server")],
@@ -0,0 +1,351 @@
1
+ """Register Cade as an MCP server in detected AI host apps.
2
+
3
+ Scans typical home-directory config locations for Claude Desktop, Cursor,
4
+ Claude Code, OpenCode, and Windsurf. When a host's config dir (or file)
5
+ already exists, merges a ``cade`` stdio entry that points at
6
+ ``cade __mcp-adapter`` — using an absolute path so GUI apps find the binary
7
+ even when ``PATH`` is incomplete.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import platform
15
+ import re
16
+ import shutil
17
+ import sys
18
+ from dataclasses import dataclass
19
+ from enum import Enum
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+
24
+ SERVER_KEY = "cade"
25
+ DEFAULT_AGENT = "default"
26
+
27
+
28
+ class HostKind(str, Enum):
29
+ """How a host stores MCP server entries."""
30
+
31
+ MCP_SERVERS = "mcpServers" # Claude Desktop / Cursor / Claude Code
32
+ OPENCODE = "opencode" # OpenCode ``mcp`` map with type/command
33
+ WINDSURF = "windsurf" # Windsurf mcp_config.json
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class HostTarget:
38
+ """One MCP host config file we may update."""
39
+
40
+ name: str
41
+ path: Path
42
+ kind: HostKind
43
+ # Parent path that must exist (app installed / used before).
44
+ marker: Path
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class HostUpdate:
49
+ """Result of attempting to register Cade with one host."""
50
+
51
+ name: str
52
+ path: Path
53
+ status: str # added | updated | unchanged | skipped | error
54
+ detail: str = ""
55
+
56
+
57
+ def resolve_cade_command() -> str:
58
+ """Absolute path to the ``cade`` binary when possible."""
59
+ if getattr(sys, "frozen", False):
60
+ return str(Path(sys.executable).resolve())
61
+ which = shutil.which("cade")
62
+ if which:
63
+ return str(Path(which).resolve())
64
+ # Dev / module invocation fallback — hosts need a real executable.
65
+ return sys.executable
66
+
67
+
68
+ def stdio_entry(*, command: str | None = None, agent: str = DEFAULT_AGENT) -> dict[str, Any]:
69
+ """Claude/Cursor/Claude Code mcpServers entry."""
70
+ cmd = command or resolve_cade_command()
71
+ return {
72
+ "command": cmd,
73
+ "args": ["__mcp-adapter", "--agent", agent],
74
+ }
75
+
76
+
77
+ def opencode_entry(*, command: str | None = None, agent: str = DEFAULT_AGENT) -> dict[str, Any]:
78
+ """OpenCode ``mcp.<name>`` local server entry."""
79
+ cmd = command or resolve_cade_command()
80
+ return {
81
+ "type": "local",
82
+ "command": [cmd, "__mcp-adapter", "--agent", agent],
83
+ "enabled": True,
84
+ }
85
+
86
+
87
+ def discover_hosts(home: Path | None = None) -> list[HostTarget]:
88
+ """Return host configs that look installed (marker path exists)."""
89
+ home = home or Path.home()
90
+ system = platform.system()
91
+ targets: list[HostTarget] = []
92
+
93
+ # Cursor (all platforms)
94
+ cursor_dir = home / ".cursor"
95
+ targets.append(
96
+ HostTarget(
97
+ name="Cursor",
98
+ path=cursor_dir / "mcp.json",
99
+ kind=HostKind.MCP_SERVERS,
100
+ marker=cursor_dir,
101
+ )
102
+ )
103
+
104
+ # Claude Code
105
+ claude_json = home / ".claude.json"
106
+ claude_dir = home / ".claude"
107
+ targets.append(
108
+ HostTarget(
109
+ name="Claude Code",
110
+ path=claude_json,
111
+ kind=HostKind.MCP_SERVERS,
112
+ marker=claude_json if claude_json.exists() else claude_dir,
113
+ )
114
+ )
115
+
116
+ # Claude Desktop
117
+ if system == "Darwin":
118
+ desktop_dir = home / "Library" / "Application Support" / "Claude"
119
+ elif system == "Windows":
120
+ appdata = os.environ.get("APPDATA", str(home / "AppData" / "Roaming"))
121
+ desktop_dir = Path(appdata) / "Claude"
122
+ else:
123
+ desktop_dir = home / ".config" / "Claude"
124
+ targets.append(
125
+ HostTarget(
126
+ name="Claude Desktop",
127
+ path=desktop_dir / "claude_desktop_config.json",
128
+ kind=HostKind.MCP_SERVERS,
129
+ marker=desktop_dir,
130
+ )
131
+ )
132
+
133
+ # OpenCode — prefer XDG config; fall back to ~/.opencode
134
+ opencode_xdg = home / ".config" / "opencode"
135
+ opencode_legacy = home / ".opencode"
136
+ if opencode_xdg.exists():
137
+ # Prefer .json over .jsonc (we write strict JSON).
138
+ oc_path = opencode_xdg / "opencode.json"
139
+ if not oc_path.exists() and (opencode_xdg / "opencode.jsonc").exists():
140
+ oc_path = opencode_xdg / "opencode.jsonc"
141
+ targets.append(
142
+ HostTarget(
143
+ name="OpenCode",
144
+ path=oc_path,
145
+ kind=HostKind.OPENCODE,
146
+ marker=opencode_xdg,
147
+ )
148
+ )
149
+ elif opencode_legacy.exists():
150
+ oc_path = opencode_legacy / "opencode.json"
151
+ targets.append(
152
+ HostTarget(
153
+ name="OpenCode",
154
+ path=oc_path,
155
+ kind=HostKind.OPENCODE,
156
+ marker=opencode_legacy,
157
+ )
158
+ )
159
+
160
+ # Windsurf
161
+ windsurf = home / ".codeium" / "windsurf" / "mcp_config.json"
162
+ targets.append(
163
+ HostTarget(
164
+ name="Windsurf",
165
+ path=windsurf,
166
+ kind=HostKind.WINDSURF,
167
+ marker=home / ".codeium" / "windsurf",
168
+ )
169
+ )
170
+
171
+ return [t for t in targets if t.marker.exists()]
172
+
173
+
174
+ def _strip_jsonc(text: str) -> str:
175
+ """Best-effort strip of // and /* */ comments for JSONC files."""
176
+ # Remove /* ... */ blocks
177
+ text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
178
+ # Remove // line comments (not inside strings — good enough for config)
179
+ out_lines: list[str] = []
180
+ for line in text.splitlines():
181
+ in_str = False
182
+ escaped = False
183
+ cut = len(line)
184
+ for i, ch in enumerate(line):
185
+ if escaped:
186
+ escaped = False
187
+ continue
188
+ if ch == "\\":
189
+ escaped = True
190
+ continue
191
+ if ch == '"':
192
+ in_str = not in_str
193
+ continue
194
+ if not in_str and ch == "/" and i + 1 < len(line) and line[i + 1] == "/":
195
+ cut = i
196
+ break
197
+ out_lines.append(line[:cut])
198
+ return "\n".join(out_lines)
199
+
200
+
201
+ def _load_config(path: Path) -> dict[str, Any]:
202
+ if not path.exists():
203
+ return {}
204
+ raw = path.read_text(encoding="utf-8")
205
+ if path.suffix == ".jsonc":
206
+ raw = _strip_jsonc(raw)
207
+ data = json.loads(raw) if raw.strip() else {}
208
+ if not isinstance(data, dict):
209
+ raise ValueError(f"{path} root must be a JSON object")
210
+ return data
211
+
212
+
213
+ def _write_config(path: Path, data: dict[str, Any], *, dry_run: bool) -> None:
214
+ if dry_run:
215
+ return
216
+ path.parent.mkdir(parents=True, exist_ok=True)
217
+ if path.exists():
218
+ backup = path.with_suffix(path.suffix + ".bak")
219
+ shutil.copy2(path, backup)
220
+ # Always write .json when source was .jsonc so we don't re-emit comments.
221
+ out = path if path.suffix != ".jsonc" else path.with_suffix(".json")
222
+ out.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
223
+
224
+
225
+ def _merge_mcp_servers(
226
+ data: dict[str, Any],
227
+ entry: dict[str, Any],
228
+ *,
229
+ force: bool,
230
+ ) -> tuple[dict[str, Any], str]:
231
+ servers = data.get("mcpServers")
232
+ if servers is None:
233
+ servers = {}
234
+ data["mcpServers"] = servers
235
+ if not isinstance(servers, dict):
236
+ raise ValueError("mcpServers must be an object")
237
+
238
+ existing = servers.get(SERVER_KEY)
239
+ if existing == entry:
240
+ return data, "unchanged"
241
+ if existing is not None and not force:
242
+ # Update in place when only the command path drifted (common after upgrade).
243
+ if isinstance(existing, dict) and existing.get("args") == entry.get("args"):
244
+ servers[SERVER_KEY] = entry
245
+ return data, "updated"
246
+ if isinstance(existing, dict):
247
+ servers[SERVER_KEY] = entry
248
+ return data, "updated"
249
+ servers[SERVER_KEY] = entry
250
+ return data, "added" if existing is None else "updated"
251
+
252
+
253
+ def _merge_opencode(
254
+ data: dict[str, Any],
255
+ entry: dict[str, Any],
256
+ *,
257
+ force: bool,
258
+ ) -> tuple[dict[str, Any], str]:
259
+ mcp = data.get("mcp")
260
+ if mcp is None:
261
+ mcp = {}
262
+ data["mcp"] = mcp
263
+ if not isinstance(mcp, dict):
264
+ raise ValueError("mcp must be an object")
265
+ existing = mcp.get(SERVER_KEY)
266
+ if existing == entry:
267
+ return data, "unchanged"
268
+ if existing is not None and not force:
269
+ mcp[SERVER_KEY] = entry
270
+ return data, "updated"
271
+ mcp[SERVER_KEY] = entry
272
+ return data, "added" if existing is None else "updated"
273
+
274
+
275
+ def _merge_windsurf(
276
+ data: dict[str, Any],
277
+ entry: dict[str, Any],
278
+ *,
279
+ force: bool,
280
+ ) -> tuple[dict[str, Any], str]:
281
+ # Windsurf uses the same mcpServers shape as Cursor in current builds.
282
+ return _merge_mcp_servers(data, entry, force=force)
283
+
284
+
285
+ def register_host(
286
+ target: HostTarget,
287
+ *,
288
+ command: str | None = None,
289
+ agent: str = DEFAULT_AGENT,
290
+ force: bool = False,
291
+ dry_run: bool = False,
292
+ ) -> HostUpdate:
293
+ """Merge the Cade MCP entry into one host config."""
294
+ cmd = command or resolve_cade_command()
295
+ try:
296
+ data = _load_config(target.path)
297
+ if target.kind == HostKind.OPENCODE:
298
+ entry = opencode_entry(command=cmd, agent=agent)
299
+ data, status = _merge_opencode(data, entry, force=force)
300
+ elif target.kind in (HostKind.MCP_SERVERS, HostKind.WINDSURF):
301
+ entry = stdio_entry(command=cmd, agent=agent)
302
+ merge = _merge_windsurf if target.kind == HostKind.WINDSURF else _merge_mcp_servers
303
+ data, status = merge(data, entry, force=force)
304
+ else:
305
+ return HostUpdate(target.name, target.path, "skipped", "unknown host kind")
306
+
307
+ if status != "unchanged":
308
+ _write_config(target.path, data, dry_run=dry_run)
309
+ if dry_run:
310
+ status = f"would-{status}"
311
+ return HostUpdate(target.name, target.path, status)
312
+ except Exception as e: # noqa: BLE001
313
+ return HostUpdate(target.name, target.path, "error", str(e))
314
+
315
+
316
+ def install_into_hosts(
317
+ *,
318
+ home: Path | None = None,
319
+ command: str | None = None,
320
+ agent: str = DEFAULT_AGENT,
321
+ force: bool = False,
322
+ dry_run: bool = False,
323
+ ) -> list[HostUpdate]:
324
+ """Discover installed hosts and register Cade with each."""
325
+ updates: list[HostUpdate] = []
326
+ for target in discover_hosts(home):
327
+ updates.append(
328
+ register_host(
329
+ target,
330
+ command=command,
331
+ agent=agent,
332
+ force=force,
333
+ dry_run=dry_run,
334
+ )
335
+ )
336
+ return updates
337
+
338
+
339
+ __all__ = [
340
+ "HostKind",
341
+ "HostTarget",
342
+ "HostUpdate",
343
+ "DEFAULT_AGENT",
344
+ "SERVER_KEY",
345
+ "discover_hosts",
346
+ "install_into_hosts",
347
+ "opencode_entry",
348
+ "register_host",
349
+ "resolve_cade_command",
350
+ "stdio_entry",
351
+ ]
@@ -312,6 +312,22 @@ def finish_first_run_cloud(console: Console, *, prefer_skip_login: bool = False)
312
312
  "[bold]cade mcp authorize hub[/bold].[/dim]"
313
313
  )
314
314
 
315
+ # Register Cade in Claude Desktop / Cursor / Claude Code / … when present.
316
+ try:
317
+ from cadecoder.cli.mcp_hosts import install_into_hosts
318
+
319
+ updates = install_into_hosts()
320
+ touched = [u for u in updates if u.status in ("added", "updated")]
321
+ if touched:
322
+ names = ", ".join(u.name for u in touched)
323
+ console.print(
324
+ f"\n[green]✓[/green] Registered Cade as an MCP server in: {names}\n"
325
+ "[dim]Restart those apps, and keep [bold]cade daemon[/bold] running "
326
+ "(or just use [bold]cade[/bold] — it autostarts).[/dim]"
327
+ )
328
+ except Exception: # noqa: BLE001
329
+ pass
330
+
315
331
 
316
332
  def run_onboarding(console: Console, *, force: bool = False) -> OnboardingResult:
317
333
  """Run the interactive model-setup wizard (step 1 of first-run).
@@ -31,6 +31,11 @@ from cadecoder.providers.base import (
31
31
  StreamEvent,
32
32
  stream_with_retry,
33
33
  )
34
+ from cadecoder.providers.http_client import (
35
+ is_decompress_error,
36
+ make_async_http_client,
37
+ make_sync_http_client,
38
+ )
34
39
 
35
40
  # HTTP statuses from Anthropic that indicate a transient server-side issue
36
41
  # and are safe to retry. 529 is Anthropic's "Overloaded" code; 408/429/5xx
@@ -87,13 +92,18 @@ class AnthropicProvider(Provider):
87
92
  if not self.api_key:
88
93
  raise ProviderError("Anthropic API key not configured", ProviderType.ANTHROPIC)
89
94
 
90
- self.client = AsyncAnthropic(api_key=self.api_key)
95
+ # Own the httpx client so we can pin Accept-Encoding (avoids brotli /
96
+ # zlib "incorrect header check" failures in frozen PyInstaller builds).
97
+ self._http_client = make_async_http_client()
98
+ self.client = AsyncAnthropic(api_key=self.api_key, http_client=self._http_client)
99
+ self._sync_http_client = None
91
100
  self._sync_client: Anthropic | None = None
92
101
 
93
102
  def _get_sync_client(self) -> Anthropic:
94
103
  """Get or create synchronous client for model fetching."""
95
104
  if self._sync_client is None:
96
- self._sync_client = Anthropic(api_key=self.api_key)
105
+ self._sync_http_client = make_sync_http_client()
106
+ self._sync_client = Anthropic(api_key=self.api_key, http_client=self._sync_http_client)
97
107
  return self._sync_client
98
108
 
99
109
  @property
@@ -220,6 +230,9 @@ class AnthropicProvider(Provider):
220
230
  return ErrorClass.TRANSIENT
221
231
  if isinstance(exc, APIStatusError) and exc.status_code in _RETRIABLE_STATUSES:
222
232
  return ErrorClass.TRANSIENT
233
+ # Broken Content-Encoding on SSE streams (seen in frozen builds).
234
+ if is_decompress_error(exc):
235
+ return ErrorClass.TRANSIENT
223
236
  return ErrorClass.PERMANENT
224
237
 
225
238
  def format_error_for_user(self, exc: BaseException) -> str:
@@ -233,6 +246,11 @@ class AnthropicProvider(Provider):
233
246
  return f"Anthropic service error ({exc.status_code}) — please retry."
234
247
  if isinstance(exc, APITimeoutError | APIConnectionError):
235
248
  return "Network error talking to Anthropic — please retry."
249
+ if is_decompress_error(exc):
250
+ return (
251
+ "Network/encoding error talking to Anthropic (response decompress failed) — "
252
+ "please retry."
253
+ )
236
254
  return str(exc)
237
255
 
238
256
  def stream(self, request: ProviderRequest) -> AsyncIterator[StreamEvent]:
@@ -0,0 +1,52 @@
1
+ """Shared httpx client helpers for LLM providers.
2
+
3
+ Frozen/PyInstaller builds have hit ``zlib.error: Error -3 while decompressing
4
+ data: incorrect header check`` on Anthropic SSE streams when httpx negotiates
5
+ brotli (``br``) or otherwise mishandles ``Content-Encoding``. Pinning
6
+ ``Accept-Encoding`` to gzip/deflate avoids that path while keeping compression.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import httpx
12
+
13
+ # Do not advertise ``br`` / ``zstd`` — those decoders are fragile in frozen
14
+ # binaries and have produced "incorrect header check" failures on streams.
15
+ _SAFE_ACCEPT_ENCODING = "gzip, deflate"
16
+
17
+
18
+ def make_async_http_client(*, timeout: float = 600.0) -> httpx.AsyncClient:
19
+ """Return an AsyncClient safe for Anthropic/OpenAI streaming in frozen apps."""
20
+ return httpx.AsyncClient(
21
+ headers={"Accept-Encoding": _SAFE_ACCEPT_ENCODING},
22
+ timeout=httpx.Timeout(timeout, connect=10.0),
23
+ follow_redirects=True,
24
+ )
25
+
26
+
27
+ def make_sync_http_client(*, timeout: float = 60.0) -> httpx.Client:
28
+ """Return a sync Client with the same encoding constraints."""
29
+ return httpx.Client(
30
+ headers={"Accept-Encoding": _SAFE_ACCEPT_ENCODING},
31
+ timeout=httpx.Timeout(timeout, connect=10.0),
32
+ follow_redirects=True,
33
+ )
34
+
35
+
36
+ def is_decompress_error(exc: BaseException) -> bool:
37
+ """True when ``exc`` looks like a broken Content-Encoding decode."""
38
+ import zlib
39
+
40
+ if isinstance(exc, zlib.error):
41
+ return True
42
+ if isinstance(exc, httpx.DecodingError):
43
+ return True
44
+ msg = str(exc).lower()
45
+ return "incorrect header check" in msg or "while decompressing" in msg
46
+
47
+
48
+ __all__ = [
49
+ "is_decompress_error",
50
+ "make_async_http_client",
51
+ "make_sync_http_client",
52
+ ]
@@ -30,6 +30,11 @@ from cadecoder.providers.base import (
30
30
  StreamEvent,
31
31
  stream_with_retry,
32
32
  )
33
+ from cadecoder.providers.http_client import (
34
+ is_decompress_error,
35
+ make_async_http_client,
36
+ make_sync_http_client,
37
+ )
33
38
 
34
39
  # Retriable HTTP statuses — 408 timeout, 429 rate limit, 500/502/503/504 5xx,
35
40
  # 529 seen occasionally behind OpenAI-compatible proxies (Anthropic's code).
@@ -57,7 +62,13 @@ class OpenAIProvider(Provider):
57
62
  if not self.api_key:
58
63
  raise ProviderError("OpenAI API key not configured", ProviderType.OPENAI)
59
64
 
60
- self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
65
+ self._http_client = make_async_http_client()
66
+ self.client = AsyncOpenAI(
67
+ api_key=self.api_key,
68
+ base_url=self.base_url,
69
+ http_client=self._http_client,
70
+ )
71
+ self._sync_http_client = None
61
72
  self._sync_client: OpenAI | None = None
62
73
 
63
74
  # Curated fallback list of OpenAI cloud chat models, used when the live API
@@ -83,7 +94,12 @@ class OpenAIProvider(Provider):
83
94
  def _get_sync_client(self) -> OpenAI:
84
95
  """Get or create a synchronous client for model listing."""
85
96
  if self._sync_client is None:
86
- self._sync_client = OpenAI(api_key=self.api_key, base_url=self.base_url)
97
+ self._sync_http_client = make_sync_http_client()
98
+ self._sync_client = OpenAI(
99
+ api_key=self.api_key,
100
+ base_url=self.base_url,
101
+ http_client=self._sync_http_client,
102
+ )
87
103
  return self._sync_client
88
104
 
89
105
  @property
@@ -173,6 +189,8 @@ class OpenAIProvider(Provider):
173
189
  return ErrorClass.TRANSIENT
174
190
  if isinstance(exc, APIStatusError) and exc.status_code in _RETRIABLE_STATUSES:
175
191
  return ErrorClass.TRANSIENT
192
+ if is_decompress_error(exc):
193
+ return ErrorClass.TRANSIENT
176
194
  return ErrorClass.PERMANENT
177
195
 
178
196
  def format_error_for_user(self, exc: BaseException) -> str:
@@ -184,6 +202,11 @@ class OpenAIProvider(Provider):
184
202
  return f"OpenAI service error ({exc.status_code}) — please retry."
185
203
  if isinstance(exc, APITimeoutError | APIConnectionError):
186
204
  return "Network error talking to OpenAI — please retry."
205
+ if is_decompress_error(exc):
206
+ return (
207
+ "Network/encoding error talking to OpenAI (response decompress failed) — "
208
+ "please retry."
209
+ )
187
210
  return str(exc)
188
211
 
189
212
  def stream(self, request: ProviderRequest) -> AsyncIterator[StreamEvent]:
File without changes
File without changes