devcopilot 0.2.0__py3-none-any.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 (189) hide show
  1. api/__init__.py +17 -0
  2. api/admin_config.py +1303 -0
  3. api/admin_routes.py +287 -0
  4. api/admin_static/admin.css +459 -0
  5. api/admin_static/admin.js +497 -0
  6. api/admin_static/index.html +77 -0
  7. api/admin_urls.py +34 -0
  8. api/app.py +194 -0
  9. api/command_utils.py +164 -0
  10. api/dependencies.py +144 -0
  11. api/detection.py +152 -0
  12. api/gateway_model_ids.py +54 -0
  13. api/model_catalog.py +133 -0
  14. api/model_router.py +125 -0
  15. api/models/__init__.py +45 -0
  16. api/models/anthropic.py +234 -0
  17. api/models/openai_responses.py +28 -0
  18. api/models/responses.py +60 -0
  19. api/optimization_handlers.py +154 -0
  20. api/request_pipeline.py +424 -0
  21. api/routes.py +156 -0
  22. api/runtime.py +334 -0
  23. api/validation_log.py +48 -0
  24. api/web_server_tools.py +22 -0
  25. api/web_tools/__init__.py +17 -0
  26. api/web_tools/constants.py +15 -0
  27. api/web_tools/egress.py +99 -0
  28. api/web_tools/outbound.py +278 -0
  29. api/web_tools/parsers.py +104 -0
  30. api/web_tools/request.py +87 -0
  31. api/web_tools/streaming.py +206 -0
  32. cli/__init__.py +5 -0
  33. cli/claude_env.py +12 -0
  34. cli/entrypoints.py +166 -0
  35. cli/env.example +209 -0
  36. cli/launchers/__init__.py +1 -0
  37. cli/launchers/claude.py +84 -0
  38. cli/launchers/codex.py +204 -0
  39. cli/launchers/codex_model_catalog.py +186 -0
  40. cli/launchers/common.py +93 -0
  41. cli/managed/__init__.py +6 -0
  42. cli/managed/claude.py +215 -0
  43. cli/managed/manager.py +157 -0
  44. cli/managed/session.py +260 -0
  45. cli/process_registry.py +78 -0
  46. config/__init__.py +5 -0
  47. config/constants.py +13 -0
  48. config/logging_config.py +159 -0
  49. config/nim.py +118 -0
  50. config/paths.py +91 -0
  51. config/provider_catalog.py +259 -0
  52. config/provider_ids.py +7 -0
  53. config/settings.py +538 -0
  54. core/__init__.py +1 -0
  55. core/anthropic/__init__.py +46 -0
  56. core/anthropic/content.py +31 -0
  57. core/anthropic/conversion.py +587 -0
  58. core/anthropic/emitted_sse_tracker.py +346 -0
  59. core/anthropic/errors.py +70 -0
  60. core/anthropic/native_messages_request.py +280 -0
  61. core/anthropic/native_sse_block_policy.py +313 -0
  62. core/anthropic/provider_stream_error.py +34 -0
  63. core/anthropic/server_tool_sse.py +14 -0
  64. core/anthropic/sse.py +440 -0
  65. core/anthropic/stream_contracts.py +205 -0
  66. core/anthropic/stream_recovery.py +346 -0
  67. core/anthropic/stream_recovery_session.py +133 -0
  68. core/anthropic/thinking.py +140 -0
  69. core/anthropic/tokens.py +117 -0
  70. core/anthropic/tools.py +212 -0
  71. core/anthropic/utils.py +9 -0
  72. core/openai_responses/__init__.py +5 -0
  73. core/openai_responses/adapter.py +31 -0
  74. core/openai_responses/anthropic_sse.py +59 -0
  75. core/openai_responses/errors.py +22 -0
  76. core/openai_responses/events.py +19 -0
  77. core/openai_responses/ids.py +21 -0
  78. core/openai_responses/input.py +258 -0
  79. core/openai_responses/items.py +37 -0
  80. core/openai_responses/reasoning.py +52 -0
  81. core/openai_responses/stream.py +25 -0
  82. core/openai_responses/stream_state.py +654 -0
  83. core/openai_responses/tools.py +374 -0
  84. core/openai_responses/usage.py +37 -0
  85. core/rate_limit.py +60 -0
  86. core/trace.py +216 -0
  87. devcopilot-0.2.0.dist-info/METADATA +687 -0
  88. devcopilot-0.2.0.dist-info/RECORD +189 -0
  89. devcopilot-0.2.0.dist-info/WHEEL +4 -0
  90. devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
  91. devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
  92. messaging/__init__.py +26 -0
  93. messaging/cli_event_constants.py +67 -0
  94. messaging/command_context.py +66 -0
  95. messaging/command_dispatcher.py +37 -0
  96. messaging/commands.py +275 -0
  97. messaging/event_parser.py +181 -0
  98. messaging/limiter.py +300 -0
  99. messaging/models.py +36 -0
  100. messaging/node_event_pipeline.py +127 -0
  101. messaging/node_runner.py +342 -0
  102. messaging/platforms/__init__.py +15 -0
  103. messaging/platforms/base.py +228 -0
  104. messaging/platforms/discord.py +567 -0
  105. messaging/platforms/factory.py +103 -0
  106. messaging/platforms/outbox.py +144 -0
  107. messaging/platforms/telegram.py +688 -0
  108. messaging/platforms/voice_flow.py +295 -0
  109. messaging/rendering/__init__.py +3 -0
  110. messaging/rendering/discord_markdown.py +318 -0
  111. messaging/rendering/markdown_tables.py +49 -0
  112. messaging/rendering/profiles.py +55 -0
  113. messaging/rendering/telegram_markdown.py +327 -0
  114. messaging/safe_diagnostics.py +17 -0
  115. messaging/session.py +334 -0
  116. messaging/transcript.py +581 -0
  117. messaging/transcription.py +164 -0
  118. messaging/trees/__init__.py +15 -0
  119. messaging/trees/data.py +482 -0
  120. messaging/trees/manager.py +433 -0
  121. messaging/trees/processor.py +179 -0
  122. messaging/trees/repository.py +177 -0
  123. messaging/turn_intake.py +235 -0
  124. messaging/ui_updates.py +101 -0
  125. messaging/voice.py +76 -0
  126. messaging/workflow.py +200 -0
  127. providers/__init__.py +31 -0
  128. providers/base.py +152 -0
  129. providers/cerebras/__init__.py +7 -0
  130. providers/cerebras/client.py +31 -0
  131. providers/cerebras/request.py +55 -0
  132. providers/codestral/__init__.py +7 -0
  133. providers/codestral/client.py +34 -0
  134. providers/deepseek/__init__.py +11 -0
  135. providers/deepseek/client.py +51 -0
  136. providers/deepseek/request.py +475 -0
  137. providers/defaults.py +41 -0
  138. providers/error_mapping.py +309 -0
  139. providers/exceptions.py +113 -0
  140. providers/fireworks/__init__.py +5 -0
  141. providers/fireworks/client.py +45 -0
  142. providers/fireworks/request.py +48 -0
  143. providers/gemini/__init__.py +7 -0
  144. providers/gemini/client.py +49 -0
  145. providers/gemini/request.py +199 -0
  146. providers/groq/__init__.py +7 -0
  147. providers/groq/client.py +31 -0
  148. providers/groq/request.py +83 -0
  149. providers/kimi/__init__.py +10 -0
  150. providers/kimi/client.py +53 -0
  151. providers/kimi/request.py +42 -0
  152. providers/llamacpp/__init__.py +3 -0
  153. providers/llamacpp/client.py +16 -0
  154. providers/lmstudio/__init__.py +5 -0
  155. providers/lmstudio/client.py +16 -0
  156. providers/mistral/__init__.py +7 -0
  157. providers/mistral/client.py +31 -0
  158. providers/mistral/request.py +37 -0
  159. providers/model_listing.py +133 -0
  160. providers/nvidia_nim/__init__.py +7 -0
  161. providers/nvidia_nim/client.py +91 -0
  162. providers/nvidia_nim/request.py +430 -0
  163. providers/nvidia_nim/voice.py +95 -0
  164. providers/ollama/__init__.py +7 -0
  165. providers/ollama/client.py +39 -0
  166. providers/open_router/__init__.py +7 -0
  167. providers/open_router/client.py +124 -0
  168. providers/open_router/request.py +42 -0
  169. providers/opencode/__init__.py +11 -0
  170. providers/opencode/client.py +31 -0
  171. providers/opencode/request.py +35 -0
  172. providers/rate_limit.py +300 -0
  173. providers/registry.py +527 -0
  174. providers/transports/__init__.py +1 -0
  175. providers/transports/anthropic_messages/__init__.py +5 -0
  176. providers/transports/anthropic_messages/http.py +118 -0
  177. providers/transports/anthropic_messages/recovery.py +206 -0
  178. providers/transports/anthropic_messages/stream.py +295 -0
  179. providers/transports/anthropic_messages/transport.py +236 -0
  180. providers/transports/openai_chat/__init__.py +5 -0
  181. providers/transports/openai_chat/recovery.py +217 -0
  182. providers/transports/openai_chat/stream.py +384 -0
  183. providers/transports/openai_chat/tool_calls.py +293 -0
  184. providers/transports/openai_chat/transport.py +156 -0
  185. providers/wafer/__init__.py +10 -0
  186. providers/wafer/client.py +50 -0
  187. providers/zai/__init__.py +10 -0
  188. providers/zai/client.py +46 -0
  189. providers/zai/request.py +42 -0
@@ -0,0 +1,54 @@
1
+ """Gateway-safe model id encoding for Claude Code model discovery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ GATEWAY_MODEL_ID_PREFIX = "anthropic"
8
+
9
+ # Claude Code currently treats any model id containing ``claude-3-`` as not
10
+ # supporting thinking. This intentionally uses that client-side capability
11
+ # heuristic while keeping the real provider/model ref reversible for routing.
12
+ NO_THINKING_GATEWAY_MODEL_ID_PREFIX = "claude-3-freecc-no-thinking"
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class DecodedGatewayModelId:
17
+ provider_id: str
18
+ provider_model: str
19
+ force_thinking_enabled: bool | None = None
20
+
21
+
22
+ def gateway_model_id(provider_model_ref: str) -> str:
23
+ """Return the normal Claude Code-discoverable id for a provider/model ref."""
24
+ return f"{GATEWAY_MODEL_ID_PREFIX}/{provider_model_ref}"
25
+
26
+
27
+ def no_thinking_gateway_model_id(provider_model_ref: str) -> str:
28
+ """Return a Claude Code-discoverable id that disables client thinking."""
29
+ return f"{NO_THINKING_GATEWAY_MODEL_ID_PREFIX}/{provider_model_ref}"
30
+
31
+
32
+ def decode_gateway_model_id(model_name: str) -> DecodedGatewayModelId | None:
33
+ """Decode a model id advertised by this gateway, if it is one."""
34
+ prefix, separator, remainder = model_name.partition("/")
35
+ if not separator:
36
+ return None
37
+
38
+ force_thinking_enabled: bool | None
39
+ if prefix == GATEWAY_MODEL_ID_PREFIX:
40
+ force_thinking_enabled = None
41
+ elif prefix == NO_THINKING_GATEWAY_MODEL_ID_PREFIX:
42
+ force_thinking_enabled = False
43
+ else:
44
+ return None
45
+
46
+ provider_id, provider_separator, provider_model = remainder.partition("/")
47
+ if not provider_separator or not provider_model:
48
+ return None
49
+
50
+ return DecodedGatewayModelId(
51
+ provider_id=provider_id,
52
+ provider_model=provider_model,
53
+ force_thinking_enabled=force_thinking_enabled,
54
+ )
api/model_catalog.py ADDED
@@ -0,0 +1,133 @@
1
+ """Model-list response construction for Claude-compatible clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from config.settings import Settings
6
+ from providers.registry import ProviderRegistry
7
+
8
+ from .gateway_model_ids import gateway_model_id, no_thinking_gateway_model_id
9
+ from .models.responses import ModelResponse, ModelsListResponse
10
+
11
+ DISCOVERED_MODEL_CREATED_AT = "1970-01-01T00:00:00Z"
12
+
13
+
14
+ SUPPORTED_CLAUDE_MODELS = [
15
+ ModelResponse(
16
+ id="claude-opus-4-20250514",
17
+ display_name="Claude Opus 4",
18
+ created_at="2025-05-14T00:00:00Z",
19
+ ),
20
+ ModelResponse(
21
+ id="claude-sonnet-4-20250514",
22
+ display_name="Claude Sonnet 4",
23
+ created_at="2025-05-14T00:00:00Z",
24
+ ),
25
+ ModelResponse(
26
+ id="claude-haiku-4-20250514",
27
+ display_name="Claude Haiku 4",
28
+ created_at="2025-05-14T00:00:00Z",
29
+ ),
30
+ ModelResponse(
31
+ id="claude-3-opus-20240229",
32
+ display_name="Claude 3 Opus",
33
+ created_at="2024-02-29T00:00:00Z",
34
+ ),
35
+ ModelResponse(
36
+ id="claude-3-5-sonnet-20241022",
37
+ display_name="Claude 3.5 Sonnet",
38
+ created_at="2024-10-22T00:00:00Z",
39
+ ),
40
+ ModelResponse(
41
+ id="claude-3-haiku-20240307",
42
+ display_name="Claude 3 Haiku",
43
+ created_at="2024-03-07T00:00:00Z",
44
+ ),
45
+ ModelResponse(
46
+ id="claude-3-5-haiku-20241022",
47
+ display_name="Claude 3.5 Haiku",
48
+ created_at="2024-10-22T00:00:00Z",
49
+ ),
50
+ ]
51
+
52
+
53
+ def build_models_list_response(
54
+ settings: Settings, provider_registry: ProviderRegistry | None
55
+ ) -> ModelsListResponse:
56
+ """Return configured, cached, and compatibility model ids."""
57
+ models: list[ModelResponse] = []
58
+ seen: set[str] = set()
59
+
60
+ for ref in settings.configured_chat_model_refs():
61
+ supports_thinking = None
62
+ if provider_registry is not None:
63
+ supports_thinking = provider_registry.cached_model_supports_thinking(
64
+ ref.provider_id, ref.model_id
65
+ )
66
+ _append_provider_model_variants(
67
+ models,
68
+ seen,
69
+ ref.model_ref,
70
+ supports_thinking=supports_thinking,
71
+ )
72
+
73
+ if provider_registry is not None:
74
+ for model_info in provider_registry.cached_prefixed_model_infos():
75
+ _append_provider_model_variants(
76
+ models,
77
+ seen,
78
+ model_info.model_id,
79
+ supports_thinking=model_info.supports_thinking,
80
+ )
81
+
82
+ for model in SUPPORTED_CLAUDE_MODELS:
83
+ _append_unique_model(models, seen, model)
84
+
85
+ return ModelsListResponse(
86
+ data=models,
87
+ first_id=models[0].id if models else None,
88
+ has_more=False,
89
+ last_id=models[-1].id if models else None,
90
+ )
91
+
92
+
93
+ def _discovered_model_response(model_id: str, *, display_name: str) -> ModelResponse:
94
+ return ModelResponse(
95
+ id=model_id,
96
+ display_name=display_name,
97
+ created_at=DISCOVERED_MODEL_CREATED_AT,
98
+ )
99
+
100
+
101
+ def _append_unique_model(
102
+ models: list[ModelResponse], seen: set[str], model: ModelResponse
103
+ ) -> None:
104
+ if model.id in seen:
105
+ return
106
+ seen.add(model.id)
107
+ models.append(model)
108
+
109
+
110
+ def _append_provider_model_variants(
111
+ models: list[ModelResponse],
112
+ seen: set[str],
113
+ provider_model_ref: str,
114
+ *,
115
+ supports_thinking: bool | None = None,
116
+ ) -> None:
117
+ if supports_thinking is not False:
118
+ _append_unique_model(
119
+ models,
120
+ seen,
121
+ _discovered_model_response(
122
+ gateway_model_id(provider_model_ref),
123
+ display_name=provider_model_ref,
124
+ ),
125
+ )
126
+ _append_unique_model(
127
+ models,
128
+ seen,
129
+ _discovered_model_response(
130
+ no_thinking_gateway_model_id(provider_model_ref),
131
+ display_name=f"{provider_model_ref} (no thinking)",
132
+ ),
133
+ )
api/model_router.py ADDED
@@ -0,0 +1,125 @@
1
+ """Model routing for Claude-compatible requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from loguru import logger
8
+
9
+ from config.provider_ids import SUPPORTED_PROVIDER_IDS
10
+ from config.settings import Settings
11
+
12
+ from .gateway_model_ids import decode_gateway_model_id
13
+ from .models.anthropic import MessagesRequest, TokenCountRequest
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ResolvedModel:
18
+ original_model: str
19
+ provider_id: str
20
+ provider_model: str
21
+ provider_model_ref: str
22
+ thinking_enabled: bool
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class RoutedMessagesRequest:
27
+ request: MessagesRequest
28
+ resolved: ResolvedModel
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class RoutedTokenCountRequest:
33
+ request: TokenCountRequest
34
+ resolved: ResolvedModel
35
+
36
+
37
+ class ModelRouter:
38
+ """Resolve incoming Claude model names to configured provider/model pairs."""
39
+
40
+ def __init__(self, settings: Settings):
41
+ self._settings = settings
42
+
43
+ def resolve(self, claude_model_name: str) -> ResolvedModel:
44
+ (
45
+ direct_provider_id,
46
+ direct_provider_model,
47
+ force_thinking_enabled,
48
+ ) = self._direct_provider_model(claude_model_name)
49
+ if direct_provider_id is not None and direct_provider_model is not None:
50
+ thinking_enabled = (
51
+ force_thinking_enabled
52
+ if force_thinking_enabled is not None
53
+ else self._settings.resolve_thinking(direct_provider_model)
54
+ )
55
+ logger.debug(
56
+ "MODEL DIRECT: '{}' -> provider='{}' model='{}' thinking={}",
57
+ claude_model_name,
58
+ direct_provider_id,
59
+ direct_provider_model,
60
+ thinking_enabled,
61
+ )
62
+ return ResolvedModel(
63
+ original_model=claude_model_name,
64
+ provider_id=direct_provider_id,
65
+ provider_model=direct_provider_model,
66
+ provider_model_ref=claude_model_name,
67
+ thinking_enabled=thinking_enabled,
68
+ )
69
+
70
+ provider_model_ref = self._settings.resolve_model(claude_model_name)
71
+ thinking_enabled = self._settings.resolve_thinking(claude_model_name)
72
+ provider_id = Settings.parse_provider_type(provider_model_ref)
73
+ provider_model = Settings.parse_model_name(provider_model_ref)
74
+ if provider_model != claude_model_name:
75
+ logger.debug(
76
+ "MODEL MAPPING: '{}' -> '{}'", claude_model_name, provider_model
77
+ )
78
+ return ResolvedModel(
79
+ original_model=claude_model_name,
80
+ provider_id=provider_id,
81
+ provider_model=provider_model,
82
+ provider_model_ref=provider_model_ref,
83
+ thinking_enabled=thinking_enabled,
84
+ )
85
+
86
+ def _direct_provider_model(
87
+ self, model_name: str
88
+ ) -> tuple[str | None, str | None, bool | None]:
89
+ decoded = decode_gateway_model_id(model_name)
90
+ if decoded is not None:
91
+ if decoded.provider_id not in SUPPORTED_PROVIDER_IDS:
92
+ return None, None, None
93
+ return (
94
+ decoded.provider_id,
95
+ decoded.provider_model,
96
+ decoded.force_thinking_enabled,
97
+ )
98
+
99
+ provider_id, separator, provider_model = model_name.partition("/")
100
+ if not separator:
101
+ return None, None, None
102
+ if provider_id not in SUPPORTED_PROVIDER_IDS:
103
+ return None, None, None
104
+ if not provider_model:
105
+ return None, None, None
106
+ return provider_id, provider_model, None
107
+
108
+ def resolve_messages_request(
109
+ self, request: MessagesRequest
110
+ ) -> RoutedMessagesRequest:
111
+ """Return an internal routed request context."""
112
+ resolved = self.resolve(request.model)
113
+ routed = request.model_copy(deep=True)
114
+ routed.model = resolved.provider_model
115
+ return RoutedMessagesRequest(request=routed, resolved=resolved)
116
+
117
+ def resolve_token_count_request(
118
+ self, request: TokenCountRequest
119
+ ) -> RoutedTokenCountRequest:
120
+ """Return an internal token-count request context."""
121
+ resolved = self.resolve(request.model)
122
+ routed = request.model_copy(
123
+ update={"model": resolved.provider_model}, deep=True
124
+ )
125
+ return RoutedTokenCountRequest(request=routed, resolved=resolved)
api/models/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ """API models exports."""
2
+
3
+ from .anthropic import (
4
+ ContentBlockImage,
5
+ ContentBlockRedactedThinking,
6
+ ContentBlockText,
7
+ ContentBlockThinking,
8
+ ContentBlockToolResult,
9
+ ContentBlockToolUse,
10
+ Message,
11
+ MessagesRequest,
12
+ Role,
13
+ SystemContent,
14
+ ThinkingConfig,
15
+ TokenCountRequest,
16
+ Tool,
17
+ )
18
+ from .responses import (
19
+ MessagesResponse,
20
+ ModelResponse,
21
+ ModelsListResponse,
22
+ TokenCountResponse,
23
+ Usage,
24
+ )
25
+
26
+ __all__ = [
27
+ "ContentBlockImage",
28
+ "ContentBlockRedactedThinking",
29
+ "ContentBlockText",
30
+ "ContentBlockThinking",
31
+ "ContentBlockToolResult",
32
+ "ContentBlockToolUse",
33
+ "Message",
34
+ "MessagesRequest",
35
+ "MessagesResponse",
36
+ "ModelResponse",
37
+ "ModelsListResponse",
38
+ "Role",
39
+ "SystemContent",
40
+ "ThinkingConfig",
41
+ "TokenCountRequest",
42
+ "TokenCountResponse",
43
+ "Tool",
44
+ "Usage",
45
+ ]
@@ -0,0 +1,234 @@
1
+ """Pydantic models for Anthropic-compatible requests."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
7
+
8
+
9
+ # =============================================================================
10
+ # Content Block Types
11
+ # =============================================================================
12
+ class Role(StrEnum):
13
+ user = "user"
14
+ assistant = "assistant"
15
+ system = "system"
16
+
17
+
18
+ class _AnthropicBlockBase(BaseModel):
19
+ """Pass through provider fields (e.g. ``cache_control``) for native transports."""
20
+
21
+ model_config = ConfigDict(extra="allow")
22
+
23
+
24
+ class ContentBlockText(_AnthropicBlockBase):
25
+ type: Literal["text"]
26
+ text: str
27
+
28
+
29
+ class ContentBlockImage(_AnthropicBlockBase):
30
+ type: Literal["image"]
31
+ source: dict[str, Any]
32
+
33
+
34
+ class ContentBlockDocument(_AnthropicBlockBase):
35
+ """Anthropic document block (e.g. PDF files via the Files API)."""
36
+
37
+ type: Literal["document"]
38
+ source: dict[str, Any]
39
+
40
+
41
+ class ContentBlockToolUse(_AnthropicBlockBase):
42
+ type: Literal["tool_use"]
43
+ id: str
44
+ name: str
45
+ input: dict[str, Any]
46
+
47
+
48
+ class ContentBlockToolResult(_AnthropicBlockBase):
49
+ type: Literal["tool_result"]
50
+ tool_use_id: str
51
+ content: str | list[Any] | dict[str, Any]
52
+
53
+
54
+ class ContentBlockThinking(_AnthropicBlockBase):
55
+ type: Literal["thinking"]
56
+ thinking: str
57
+ signature: str | None = None
58
+
59
+
60
+ class ContentBlockRedactedThinking(_AnthropicBlockBase):
61
+ type: Literal["redacted_thinking"]
62
+ data: str
63
+
64
+
65
+ class ContentBlockServerToolUse(_AnthropicBlockBase):
66
+ """Anthropic server-side tool invocation (e.g. ``web_search``, ``web_fetch``)."""
67
+
68
+ type: Literal["server_tool_use"]
69
+ id: str
70
+ name: str
71
+ input: dict[str, Any]
72
+
73
+
74
+ class ContentBlockWebSearchToolResult(_AnthropicBlockBase):
75
+ type: Literal["web_search_tool_result"]
76
+ tool_use_id: str
77
+ content: Any
78
+
79
+
80
+ class ContentBlockWebFetchToolResult(_AnthropicBlockBase):
81
+ type: Literal["web_fetch_tool_result"]
82
+ tool_use_id: str
83
+ content: Any
84
+
85
+
86
+ class SystemContent(_AnthropicBlockBase):
87
+ type: Literal["text"]
88
+ text: str
89
+
90
+
91
+ def _text_system_block(text: str) -> dict[str, str]:
92
+ return {"type": "text", "text": text}
93
+
94
+
95
+ def _merge_system_values(existing: Any, additions: list[Any]) -> Any:
96
+ values = [existing] if existing is not None else []
97
+ values.extend(additions)
98
+
99
+ if all(isinstance(value, str) for value in values):
100
+ return "\n\n".join(value for value in values if value)
101
+
102
+ blocks: list[Any] = []
103
+ for value in values:
104
+ if isinstance(value, str):
105
+ blocks.append(_text_system_block(value))
106
+ elif isinstance(value, list):
107
+ blocks.extend(value)
108
+ else:
109
+ blocks.append(value)
110
+ return blocks
111
+
112
+
113
+ def _normalize_system_role_messages(data: Any) -> Any:
114
+ if not isinstance(data, dict):
115
+ return data
116
+
117
+ messages = data.get("messages")
118
+ if not isinstance(messages, list):
119
+ return data
120
+
121
+ system_contents: list[Any] = []
122
+ normalized_messages: list[Any] = []
123
+ for message in messages:
124
+ role = message.get("role") if isinstance(message, dict) else None
125
+ if role == Role.system:
126
+ system_contents.append(message.get("content", ""))
127
+ continue
128
+ normalized_messages.append(message)
129
+
130
+ if not system_contents:
131
+ return data
132
+
133
+ normalized = dict(data)
134
+ normalized["messages"] = normalized_messages
135
+ normalized["system"] = _merge_system_values(
136
+ normalized.get("system"), system_contents
137
+ )
138
+ return normalized
139
+
140
+
141
+ # =============================================================================
142
+ # Message Types
143
+ # =============================================================================
144
+ class Message(BaseModel):
145
+ role: Literal["user", "assistant"]
146
+ content: (
147
+ str
148
+ | list[
149
+ ContentBlockText
150
+ | ContentBlockImage
151
+ | ContentBlockDocument
152
+ | ContentBlockToolUse
153
+ | ContentBlockToolResult
154
+ | ContentBlockThinking
155
+ | ContentBlockRedactedThinking
156
+ | ContentBlockServerToolUse
157
+ | ContentBlockWebSearchToolResult
158
+ | ContentBlockWebFetchToolResult
159
+ ]
160
+ )
161
+ reasoning_content: str | None = None
162
+
163
+
164
+ class Tool(_AnthropicBlockBase):
165
+ name: str
166
+ # Anthropic server tools (e.g. web_search beta tools) include a ``type`` and
167
+ # may omit ``input_schema`` because the provider owns the schema.
168
+ type: str | None = None
169
+ description: str | None = None
170
+ input_schema: dict[str, Any] | None = None
171
+
172
+
173
+ class ThinkingConfig(BaseModel):
174
+ enabled: bool | None = True
175
+ type: str | None = None
176
+ budget_tokens: int | None = None
177
+
178
+
179
+ # =============================================================================
180
+ # Request Models
181
+ # =============================================================================
182
+ class MessagesRequest(BaseModel):
183
+ model_config = ConfigDict(extra="allow")
184
+
185
+ @model_validator(mode="before")
186
+ @classmethod
187
+ def normalize_system_role_messages(cls, data: Any) -> Any:
188
+ return _normalize_system_role_messages(data)
189
+
190
+ model: str
191
+ # Internal routing / debug: accepted on parse but not serialized to providers.
192
+ original_model: str | None = Field(default=None, exclude=True)
193
+ resolved_provider_model: str | None = Field(default=None, exclude=True)
194
+ max_tokens: int | None = None
195
+ messages: list[Message]
196
+ system: str | list[SystemContent] | None = None
197
+ stop_sequences: list[str] | None = None
198
+ stream: bool | None = True
199
+ temperature: float | None = None
200
+ top_p: float | None = None
201
+ top_k: int | None = None
202
+ metadata: dict[str, Any] | None = None
203
+ tools: list[Tool] | None = None
204
+ tool_choice: dict[str, Any] | None = None
205
+ thinking: ThinkingConfig | None = None
206
+ # Native Anthropic / SDK client hints: ignored (not forwarded) for OpenAI Chat conversion.
207
+ context_management: dict[str, Any] | None = None
208
+ output_config: dict[str, Any] | None = None
209
+ mcp_servers: list[dict[str, Any]] | None = None
210
+ extra_body: dict[str, Any] | None = None
211
+ # Beta feature flags sent by Claude Code as a body field; accepted but never forwarded.
212
+ betas: list[str] | None = Field(default=None, exclude=True)
213
+
214
+
215
+ class TokenCountRequest(BaseModel):
216
+ model_config = ConfigDict(extra="allow")
217
+
218
+ @model_validator(mode="before")
219
+ @classmethod
220
+ def normalize_system_role_messages(cls, data: Any) -> Any:
221
+ return _normalize_system_role_messages(data)
222
+
223
+ model: str
224
+ original_model: str | None = Field(default=None, exclude=True)
225
+ resolved_provider_model: str | None = Field(default=None, exclude=True)
226
+ messages: list[Message]
227
+ system: str | list[SystemContent] | None = None
228
+ tools: list[Tool] | None = None
229
+ thinking: ThinkingConfig | None = None
230
+ tool_choice: dict[str, Any] | None = None
231
+ context_management: dict[str, Any] | None = None
232
+ output_config: dict[str, Any] | None = None
233
+ mcp_servers: list[dict[str, Any]] | None = None
234
+ betas: list[str] | None = Field(default=None, exclude=True)
@@ -0,0 +1,28 @@
1
+ """Pydantic models for OpenAI Responses-compatible ingress."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+
10
+ class OpenAIResponsesRequest(BaseModel):
11
+ """Permissive subset of the OpenAI Responses API request shape."""
12
+
13
+ model_config = ConfigDict(extra="allow")
14
+
15
+ model: str
16
+ input: Any = None
17
+ instructions: str | None = None
18
+ tools: list[dict[str, Any]] | None = None
19
+ tool_choice: Any = None
20
+ parallel_tool_calls: bool | None = None
21
+ stream: bool | None = True
22
+ temperature: float | None = None
23
+ top_p: float | None = None
24
+ max_output_tokens: int | None = None
25
+ metadata: dict[str, Any] | None = None
26
+ reasoning: dict[str, Any] | None = None
27
+ previous_response_id: str | None = None
28
+ store: bool | None = None
@@ -0,0 +1,60 @@
1
+ """Pydantic models for API responses."""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from .anthropic import (
8
+ ContentBlockRedactedThinking,
9
+ ContentBlockText,
10
+ ContentBlockThinking,
11
+ ContentBlockToolUse,
12
+ )
13
+
14
+
15
+ class TokenCountResponse(BaseModel):
16
+ input_tokens: int
17
+
18
+
19
+ class ModelResponse(BaseModel):
20
+ object: Literal["model"] = "model"
21
+ created: int = 0
22
+ owned_by: str = "devcopilot"
23
+ created_at: str
24
+ display_name: str
25
+ id: str
26
+ type: Literal["model"] = "model"
27
+
28
+
29
+ class ModelsListResponse(BaseModel):
30
+ object: Literal["list"] = "list"
31
+ data: list[ModelResponse]
32
+ first_id: str | None
33
+ has_more: bool
34
+ last_id: str | None
35
+
36
+
37
+ class Usage(BaseModel):
38
+ input_tokens: int
39
+ output_tokens: int
40
+ cache_creation_input_tokens: int = 0
41
+ cache_read_input_tokens: int = 0
42
+
43
+
44
+ class MessagesResponse(BaseModel):
45
+ id: str
46
+ model: str
47
+ role: Literal["assistant"] = "assistant"
48
+ content: list[
49
+ ContentBlockText
50
+ | ContentBlockToolUse
51
+ | ContentBlockThinking
52
+ | ContentBlockRedactedThinking
53
+ | dict[str, Any]
54
+ ]
55
+ type: Literal["message"] = "message"
56
+ stop_reason: (
57
+ Literal["end_turn", "max_tokens", "stop_sequence", "tool_use"] | None
58
+ ) = None
59
+ stop_sequence: str | None = None
60
+ usage: Usage