crabcode 0.1.6__py3-none-win_amd64.whl

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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,132 @@
1
+ """Provider error diagnostics shared by CLI and Gateway-backed clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from typing import Any
9
+
10
+ from crabcode_core.logging_utils import get_logger
11
+ from crabcode_core.utf8_sanitize import safe_utf8_str
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ _CREDENTIAL_KEYS = {
16
+ "authorization", "proxyauthorization", "apikey", "xapikey", "token",
17
+ "accesstoken", "refreshtoken", "idtoken", "password", "secret",
18
+ "clientsecret", "cookie", "setcookie",
19
+ }
20
+
21
+
22
+ def _credential_key(key: str) -> bool:
23
+ return re.sub(r"[^a-z0-9]", "", key.lower()) in _CREDENTIAL_KEYS
24
+
25
+
26
+ def response_request_id(response: Any) -> str | None:
27
+ headers = getattr(response, "headers", {})
28
+ return next((headers.get(key) for key in (
29
+ "x-request-id", "request-id", "x-amzn-requestid",
30
+ ) if headers.get(key)), None)
31
+
32
+
33
+ def _exception_details(exc: BaseException) -> list[dict[str, Any]]:
34
+ details = []
35
+ pending = [exc]
36
+ seen: set[int] = set()
37
+ while pending:
38
+ current = pending.pop()
39
+ if id(current) in seen:
40
+ continue
41
+ seen.add(id(current))
42
+ response = getattr(current, "response", None)
43
+ body = getattr(current, "body", None)
44
+ # HTTP errors may contain an envelope beyond SDK exception.body.
45
+ # Only inspect an already-read response; never consume a live stream.
46
+ if response is not None:
47
+ try:
48
+ body = response.text or body
49
+ except Exception:
50
+ pass
51
+ details.append({
52
+ "exception": type(current).__name__,
53
+ # SDK messages often repeat body as a Python repr, which would
54
+ # bypass structured credential redaction. The body retains it.
55
+ "message": str(current) if body is None else None,
56
+ "status_code": getattr(current, "status_code", None)
57
+ or getattr(response, "status_code", None),
58
+ "request_id": getattr(current, "request_id", None)
59
+ or response_request_id(response),
60
+ "body": body,
61
+ })
62
+ pending.extend(item for item in (current.__context__, current.__cause__)
63
+ if item is not None)
64
+ return details
65
+
66
+
67
+ def _redact(value: Any, secrets: set[str]) -> Any:
68
+ if hasattr(value, "model_dump"):
69
+ value = value.model_dump(mode="json")
70
+ if isinstance(value, dict):
71
+ return {str(key): "[REDACTED]" if _credential_key(str(key))
72
+ else _redact(item, secrets) for key, item in value.items()}
73
+ if isinstance(value, (list, tuple)):
74
+ return [_redact(item, secrets) for item in value]
75
+ if isinstance(value, bytes):
76
+ value = value.decode("utf-8", "replace")
77
+ if isinstance(value, str):
78
+ # Raw HTTP bodies can be JSON too. Preserve every diagnostic field.
79
+ try:
80
+ parsed = json.loads(value)
81
+ except (ValueError, TypeError):
82
+ parsed = None
83
+ if isinstance(parsed, (dict, list)):
84
+ return _redact(parsed, secrets)
85
+ for secret in sorted(secrets, key=len, reverse=True):
86
+ value = value.replace(secret, "[REDACTED]")
87
+ return re.sub(r"(?i)\bBearer\s+[^\s\"'<>]+", "Bearer [REDACTED]", value)
88
+ return value
89
+
90
+
91
+ def log_api_error(
92
+ adapter: Any, *, session_id: str | None, model: str, attempt_id: str,
93
+ purpose: str, exception: BaseException | None = None, chunk: Any = None,
94
+ ) -> None:
95
+ """Log the full error body without dumping requests or credential headers.
96
+
97
+ Diagnostics must never replace the original error or change retry behavior.
98
+ Bodies are not truncated; credentials are the only redacted fields.
99
+ """
100
+ try:
101
+ config = getattr(adapter, "config", None)
102
+ secrets = set()
103
+ for name in (getattr(config, "api_key_env", None),
104
+ "OPENAI_API_KEY", "ANTHROPIC_API_KEY"):
105
+ if name and os.environ.get(name):
106
+ secrets.add(os.environ[name])
107
+ for value in (getattr(adapter, "_api_key", None),
108
+ getattr(getattr(adapter, "client", None), "api_key", None)):
109
+ if isinstance(value, str) and value:
110
+ secrets.add(value)
111
+ for key, value in (getattr(config, "http_headers", None) or {}).items():
112
+ if _credential_key(key) and value:
113
+ secrets.add(value)
114
+ if value.lower().startswith("bearer "):
115
+ secrets.add(value[7:])
116
+ details = {
117
+ "session_id": session_id, "model": model,
118
+ "provider": getattr(config, "provider", None) or type(adapter).__name__,
119
+ "attempt_id": attempt_id, "purpose": purpose,
120
+ "errors": _exception_details(exception) if exception is not None else [{
121
+ "message": chunk.error if chunk.error_body is None else None,
122
+ "status_code": chunk.error_status_code,
123
+ "request_id": chunk.error_request_id,
124
+ "body": chunk.error_body,
125
+ }],
126
+ }
127
+ logger.warning("API error details: %s", safe_utf8_str(json.dumps(
128
+ _redact(details, secrets), ensure_ascii=False, default=str,
129
+ )))
130
+ except Exception:
131
+ logger.warning("Unable to serialize API error details: session=%s attempt=%s",
132
+ session_id, attempt_id)
@@ -0,0 +1,26 @@
1
+ """Gemini API adapter — uses Google's OpenAI-compatible endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from crabcode_core.api.openai_adapter import OpenAIAdapter
6
+ from crabcode_core.types.config import ApiConfig
7
+
8
+
9
+ class GeminiAdapter(OpenAIAdapter):
10
+ """Adapter for Google Gemini via its OpenAI-compatible endpoint.
11
+
12
+ Google Gemini has provided an OpenAI-compatible endpoint since late 2024
13
+ (``https://generativelanguage.googleapis.com/v1beta/openai/``), so we
14
+ simply inherit from :class:`OpenAIAdapter` and pre-fill the base URL,
15
+ API key environment variable, and disable thinking (not supported on the
16
+ OpenAI-compatible surface).
17
+ """
18
+
19
+ def __init__(self, config: ApiConfig):
20
+ if not config.base_url:
21
+ config = config.model_copy(update={"base_url": "https://generativelanguage.googleapis.com/v1beta/openai/"})
22
+ if not config.api_key_env:
23
+ config = config.model_copy(update={"api_key_env": "GEMINI_API_KEY"})
24
+ if config.thinking_enabled is True:
25
+ config = config.model_copy(update={"thinking_enabled": False})
26
+ super().__init__(config)
@@ -0,0 +1,257 @@
1
+ """Known model context window sizes and resolution helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Maps model ID (or prefix) to context window size in tokens.
6
+ # Used as fallback when the API doesn't provide model metadata.
7
+ KNOWN_CONTEXT_WINDOWS: dict[str, int] = {
8
+ # Anthropic
9
+ "claude-mythos-5-1": 1_000_000,
10
+ "claude-fable-5-1": 1_000_000,
11
+ "claude-mythos-preview": 1_000_000,
12
+ "claude-mythos-5": 1_000_000,
13
+ "claude-fable-5": 1_000_000,
14
+ "claude-opus-5-5": 1_000_000,
15
+ "claude-sonnet-5-5": 1_000_000,
16
+ "claude-haiku-5-5": 1_000_000,
17
+ "claude-opus-5": 1_000_000,
18
+ "claude-sonnet-5": 1_000_000,
19
+ "claude-opus-4-8": 1_000_000,
20
+ "claude-opus-4-7": 1_000_000,
21
+ "claude-opus-4-6": 1_000_000,
22
+ "claude-sonnet-4-6": 1_000_000,
23
+ "claude-opus-4-5": 200_000,
24
+ "claude-sonnet-4-5": 200_000,
25
+ "claude-haiku-4-5": 200_000,
26
+ "claude-sonnet-4-20250514": 200_000,
27
+ "claude-opus-4-20250514": 200_000,
28
+ "claude-haiku-3-5-20241022": 200_000,
29
+ "claude-3-5-sonnet-20241022": 200_000,
30
+ "claude-3-5-haiku-20241022": 200_000,
31
+ "claude-3-opus-20240229": 200_000,
32
+ "claude-3-sonnet-20240229": 200_000,
33
+ "claude-3-haiku-20240307": 200_000,
34
+ # OpenAI
35
+ "gpt-daybreak-blue-latest": 1_050_000,
36
+ "gpt-daybreak-red-latest": 400_000,
37
+ "gpt-6-astra": 1_050_000,
38
+ "gpt-6.1-sol": 1_050_000,
39
+ "gpt-6-sol": 1_050_000,
40
+ "gpt-6-luna": 1_050_000,
41
+ "gpt-5.6-cyber": 400_000,
42
+ "gpt-5.6-sol": 1_050_000,
43
+ "gpt-5.6-terra": 1_050_000,
44
+ "gpt-5.6-luna": 1_050_000,
45
+ "gpt-5.6": 1_050_000,
46
+ "gpt-5.5-pro": 1_050_000,
47
+ "gpt-5.5": 1_050_000,
48
+ "gpt-5.4-mini": 400_000,
49
+ "gpt-5.4-nano": 400_000,
50
+ "gpt-5.4-pro": 1_050_000,
51
+ "gpt-5.4": 1_050_000,
52
+ "gpt-5.3-chat-latest": 128_000,
53
+ "gpt-5.3-codex": 400_000,
54
+ "gpt-5.2-chat-latest": 128_000,
55
+ "gpt-5.2-codex": 400_000,
56
+ "gpt-5.2-pro": 400_000,
57
+ "gpt-5.2": 400_000,
58
+ "gpt-5.1-chat-latest": 128_000,
59
+ "gpt-5.1-codex-mini": 400_000,
60
+ "gpt-5.1-codex-max": 400_000,
61
+ "gpt-5.1-codex": 400_000,
62
+ "gpt-5.1": 400_000,
63
+ "gpt-5-chat-latest": 128_000,
64
+ "gpt-5-codex": 400_000,
65
+ "gpt-5-mini": 400_000,
66
+ "gpt-5-nano": 400_000,
67
+ "gpt-5-pro": 400_000,
68
+ "gpt-5": 400_000,
69
+ "gpt-4.5-preview": 128_000,
70
+ "gpt-4.1-mini": 1_047_576,
71
+ "gpt-4.1-nano": 1_047_576,
72
+ "gpt-4.1": 1_047_576,
73
+ "gpt-4o-mini-realtime-preview": 16_000,
74
+ "gpt-4o-mini-transcribe": 16_000,
75
+ "gpt-4o-realtime-preview": 32_000,
76
+ "gpt-4o-transcribe-diarize": 16_000,
77
+ "gpt-4o-transcribe": 16_000,
78
+ "gpt-4o-mini": 128_000,
79
+ "gpt-4o": 128_000,
80
+ "gpt-4-turbo": 128_000,
81
+ "gpt-4": 8_192,
82
+ "o1-preview": 128_000,
83
+ "o1-mini": 128_000,
84
+ "o1-pro": 200_000,
85
+ "o1": 200_000,
86
+ "o3-deep-research": 200_000,
87
+ "o3-mini": 200_000,
88
+ "o3-pro": 200_000,
89
+ "o3": 200_000,
90
+ "o4-mini-deep-research": 200_000,
91
+ "o4-mini": 200_000,
92
+ "codex-mini-latest": 200_000,
93
+ # xAI
94
+ "grok-4.7": 500_000,
95
+ "grok-4.20": 1_000_000,
96
+ "grok-4.6": 500_000,
97
+ "grok-4.5": 500_000,
98
+ "grok-4.3": 1_000_000,
99
+ "grok-build-latest": 500_000,
100
+ "grok-build-0.1": 256_000,
101
+ "grok-code-fast": 256_000,
102
+ # DeepSeek
103
+ "deepseek-flash": 1_000_000,
104
+ "deepseek-v4-pro": 1_000_000,
105
+ "deepseek-v4-flash": 1_000_000,
106
+ "deepseek-v4.1": 1_000_000,
107
+ "deep-research-preview-04-2026": 1_048_576,
108
+ "deep-research-max-preview-04-2026": 1_048_576,
109
+ "antigravity-preview-09-2026": 1_048_576,
110
+ "deepseek-v4.1-flash": 1_000_000,
111
+ "deepseek-chat": 128_000,
112
+ "deepseek-reasoner": 128_000,
113
+ # GLM (Zhipu)
114
+ "glm-5.3-flash": 1_000_000,
115
+ "glm-5.3": 1_000_000,
116
+ "glm-5.2": 1_000_000,
117
+ "glm-5.1-fp8": 202_752,
118
+ "glm-5.1": 202_752,
119
+ "glm-4-plus": 128_000,
120
+ "glm-4": 128_000,
121
+ # Other OpenAI-compatible models
122
+ "qwen3.8-max": 1_000_000,
123
+ "qwen3.8-flash": 1_000_000,
124
+ "qwen3.7-plus": 1_000_000,
125
+ "qwen3.7-flash": 1_000_000,
126
+ "qwen3.6-plus": 1_000_000,
127
+ "qwen3.6-flash": 1_000_000,
128
+ "qwen3.5-plus": 1_000_000,
129
+ "qwen3.5-flash": 1_000_000,
130
+ "kimi-k3": 1_000_000,
131
+ "kimi-k2.7-code": 262_144,
132
+ "kimi-k2.6": 262_144,
133
+ "kimi-k2.5": 262_144,
134
+ "moonshot-v1-128k": 128_000,
135
+ "moonshot-v1-32k": 32_000,
136
+ "moonshot-v1-8k": 8_000,
137
+ "MiniMax-M3": 1_000_000,
138
+ "MiniMax-M2.7-highspeed": 204_800,
139
+ "MiniMax-M2.7": 204_800,
140
+ "minimax-m2.7": 204_800,
141
+ "skyclaw-v1-lite": 1_000_000,
142
+ "skyclaw-v1": 1_000_000,
143
+ # Meta
144
+ "Llama-4-Maverick-17B-128E-Instruct": 1_000_000,
145
+ "Llama-4-Scout-17B-16E-Instruct": 10_000_000,
146
+ "Llama-3.3": 128_000,
147
+ "Llama-3.2": 128_000,
148
+ "Llama-3.1": 128_000,
149
+ "Llama-3": 8_000,
150
+ "Llama-2": 4_000,
151
+ "Llama-1": 2_048,
152
+ "Llama-4-Maverick-17B-128E-Instruct": 1_000_000,
153
+ "Llama-4-Scout-17B-16E-Instruct": 10_000_000,
154
+ "muse-spark-1.3-contributor": 1_000_000,
155
+ "muse-spark-1.3": 1_000_000,
156
+ "mimo-v2.6-pro": 1_000_000,
157
+ "mimo-v2.6-pro-ultraspeed": 1_000_000,
158
+ "mimo-v2.6-flash": 1_000_000,
159
+ "mimo-v2.5": 1_000_000,
160
+ "mimo-v2.5-pro": 1_000_000,
161
+ "mimo-v2.5-asr": 8_000,
162
+ "mimo-v2.5-tts": 8_000,
163
+ "mimo-v2-flash": 256_000,
164
+ "mimo-v2-pro": 1_000_000,
165
+ "mimo-v2-omni": 256_000,
166
+ "mimo-7b": 32_768,
167
+ "IFM/K2-Horizon-375B-A23B": 524_288,
168
+ "IFM/K2-Horizon-MoVA-36B-A4B": 524_288,
169
+ "IFM/K2-Horizon-32B": 524_288,
170
+ "IFM/K2-Horizon-7B-Uno": 524_288,
171
+ "IFM/K2-Horizon-7B": 524_288,
172
+ "IFM/K2-Horizon-3.7B": 524_288,
173
+ "IFM/K2-Horizon-0.9B": 131_072,
174
+ "thinkingmachines/Inkling": 1_048_576,
175
+ "thinkingmachines/Inkling-Small": 1_048_576,
176
+ "inkling": 1_048_576,
177
+ "inkling-small": 1_048_576,
178
+ "NVIDIA-Nemotron-3-Ultra-550B-A55B": 1_000_000,
179
+ "NVIDIA-Nemotron-3.5-Lightning-30B-A3B": 1_000_000,
180
+ "NVIDIA-Nemotron-3-Nano-30B-A3B": 1_000_000,
181
+ "Nemotron-Cascade-2-30B-A3B": 1_000_000,
182
+ "NVIDIA-Nemotron-Nano-12B-v2-VL": 128_000,
183
+ "NVIDIA-Nemotron-Nano-12B-v2": 128_000,
184
+ "Llama-3.1-Nemotron-Nano-4B-v1.1": 131_072,
185
+ "Nemotron-H-4B-Instruct-128K": 128_000,
186
+ "Nemotron-Mini-4B-Instruct": 4_096,
187
+ "Nemotron-4-340B-Instruct": 4_096,
188
+ "nemotron-3-8b-base-4k": 4_096,
189
+ # Mistral
190
+ "mistral-medium-3-5": 256_000,
191
+ "mistral-medium-3": 256_000,
192
+ "mistral-medium-latest": 256_000,
193
+ "mistral-small-2603": 256_000,
194
+ "mistral-small-latest": 256_000,
195
+ "mistral-large-2512": 256_000,
196
+ "mistral-large-latest": 256_000,
197
+ "Mixtral-8x22B-Instruct-v0.1": 64_000,
198
+ "Mixtral-8x22B-v0.1": 64_000,
199
+ "Mixtral-8x7B-Instruct-v0.1": 32_000,
200
+ "Mixtral-8x7B-v0.1": 32_000,
201
+ "Mixtral-8x22B-Instruct-v0.1": 64_000,
202
+ "Mixtral-8x22B-v0.1": 64_000,
203
+ "Mixtral-8x7B-Instruct-v0.1": 32_000,
204
+ "Mixtral-8x7B-v0.1": 32_000,
205
+ "open-mixtral-8x22b": 64_000,
206
+ "open-mixtral-8x7b": 32_000,
207
+ # Ollama
208
+ "qwen3:32b": 128_000,
209
+ "qwen2.5-coder:32b": 128_000,
210
+ "deepseek-coder-v2": 128_000,
211
+ "llama3.1:8b": 128_000,
212
+ "codellama": 16_000,
213
+ "mistral": 32_000,
214
+ "mixtral": 32_000,
215
+ # Gemini
216
+ "gemini-3.8-live-extended-thinking": 131_072,
217
+ "gemini-3.8-live": 131_072,
218
+ "gemini-3.8-flash": 1_048_576,
219
+ "gemini-3.8-flash-tts": 1_048_576,
220
+ "gemini-3.8-flash-lite-tts": 1_048_576,
221
+ "gemini-nano-banana-2.1": 1_048_576,
222
+ "gemini-3.7-flash": 1_048_576,
223
+ "gemini-3.6-flash": 1_048_576,
224
+ "gemini-3.5-flash-lite": 1_048_576,
225
+ "gemini-3.5-flash": 1_048_576,
226
+ "gemini-3.1-pro": 1_048_576,
227
+ "gemini-3.1-flash-lite": 1_048_576,
228
+ "gemini-3-flash": 1_048_576,
229
+ "gemini-2.5-pro": 1_048_576,
230
+ "gemini-2.5-flash-lite": 1_048_576,
231
+ "gemini-2.5-flash": 1_048_576,
232
+ "gemini-2.0-flash-lite": 1_048_576,
233
+ "gemini-2.0-flash": 1_048_576,
234
+ "gemini-1.5-pro": 2_097_152,
235
+ "gemini-1.5-flash": 1_048_576,
236
+ }
237
+
238
+ DEFAULT_CONTEXT_WINDOW = 200_000
239
+
240
+
241
+ def lookup_context_window(model: str | None) -> int | None:
242
+ """Look up the context window for a model ID.
243
+
244
+ Tries exact match first, then prefix matching for versioned model IDs
245
+ (e.g. "gpt-4o-2024-11-20" falls back to "gpt-4o").
246
+ """
247
+ if not model:
248
+ return None
249
+
250
+ if model in KNOWN_CONTEXT_WINDOWS:
251
+ return KNOWN_CONTEXT_WINDOWS[model]
252
+
253
+ for known_model in sorted(KNOWN_CONTEXT_WINDOWS, key=len, reverse=True):
254
+ if model.startswith((f"{known_model}-", f"{known_model}:")):
255
+ return KNOWN_CONTEXT_WINDOWS[known_model]
256
+
257
+ return None
@@ -0,0 +1,62 @@
1
+ """Shared HTTP routing policy. Never fall back from a proxy to direct access."""
2
+ from __future__ import annotations
3
+
4
+ import ssl
5
+ from typing import Any
6
+ from urllib.parse import urlsplit
7
+
8
+ import httpx
9
+
10
+
11
+ def http_options(config: Any) -> dict[str, Any]:
12
+ mode = getattr(config, "network_mode", "inherit")
13
+ if mode == "inherit":
14
+ return {}
15
+ if mode == "direct":
16
+ return {"trust_env": False}
17
+ proxy = getattr(config, "proxy_url", None)
18
+ try:
19
+ parsed = urlsplit(proxy or "")
20
+ valid = parsed.scheme in {"http", "https"} and bool(parsed.hostname) and parsed.port != 0
21
+ valid = valid and not parsed.username and not parsed.password
22
+ except ValueError:
23
+ valid = False
24
+ if not valid:
25
+ raise ValueError("Proxy mode requires an HTTP(S) proxy URL without embedded credentials")
26
+ return {"trust_env": False, "proxy": proxy}
27
+
28
+
29
+ def request_timeout(config: Any) -> httpx.Timeout:
30
+ timeout = float(getattr(config, "timeout", 300))
31
+ return httpx.Timeout(timeout, connect=min(timeout, 10.0))
32
+
33
+
34
+ def sdk_options(config: Any) -> dict[str, Any]:
35
+ # The query loop owns retry budgets; nested SDK retries hide progress.
36
+ options: dict[str, Any] = {"max_retries": 0, "timeout": request_timeout(config)}
37
+ routing = http_options(config)
38
+ if routing:
39
+ options["http_client"] = httpx.AsyncClient(**routing, timeout=request_timeout(config))
40
+ return options
41
+
42
+
43
+ def certificate_failure(exc: BaseException) -> bool:
44
+ seen: set[int] = set()
45
+ pending = [exc]
46
+ while pending:
47
+ current = pending.pop()
48
+ if id(current) in seen:
49
+ continue
50
+ seen.add(id(current))
51
+ if isinstance(current, ssl.SSLCertVerificationError) or "CERTIFICATE_VERIFY_FAILED" in str(current):
52
+ return True
53
+ pending.extend(item for item in (current.__cause__, current.__context__) if item is not None)
54
+ return False
55
+
56
+
57
+ def network_error_message(exc: BaseException, config: Any) -> str:
58
+ if certificate_failure(exc):
59
+ return "模型连接证书校验失败,请检查证书或网络拦截;未自动重试"
60
+ mode = getattr(config, "network_mode", "inherit")
61
+ route = {"inherit": "继承进程代理环境", "direct": "直连", "proxy": "指定代理"}.get(mode, mode)
62
+ return f"模型连接中断({type(exc).__name__},{route})。请检查网络或代理状态后重试;更改系统环境变量需重启 Gateway。"
@@ -0,0 +1,27 @@
1
+ """Ollama API adapter — delegates to OpenAIAdapter with Ollama defaults."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from crabcode_core.api.openai_adapter import OpenAIAdapter
6
+ from crabcode_core.types.config import ApiConfig
7
+
8
+
9
+ class OllamaAdapter(OpenAIAdapter):
10
+ """Adapter for Ollama via its OpenAI-compatible Chat Completions API.
11
+
12
+ Ollama natively exposes an OpenAI-compatible endpoint at
13
+ ``http://localhost:11434/v1`` and does not require a real API key,
14
+ but the OpenAI SDK still expects *some* value for ``api_key``.
15
+ This adapter pre-fills those defaults and disables extended thinking
16
+ (local models do not support it).
17
+ """
18
+
19
+ def __init__(self, config: ApiConfig):
20
+ # Pre-fill Ollama defaults before handing off to OpenAIAdapter
21
+ if not config.base_url:
22
+ config = config.model_copy(update={"base_url": "http://localhost:11434/v1"})
23
+ if not config.api_key_env:
24
+ config = config.model_copy(update={"api_key_env": "OLLAMA_API_KEY"})
25
+ if config.thinking_enabled is True:
26
+ config = config.model_copy(update={"thinking_enabled": False})
27
+ super().__init__(config)