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.
- api/__init__.py +17 -0
- api/admin_config.py +1303 -0
- api/admin_routes.py +287 -0
- api/admin_static/admin.css +459 -0
- api/admin_static/admin.js +497 -0
- api/admin_static/index.html +77 -0
- api/admin_urls.py +34 -0
- api/app.py +194 -0
- api/command_utils.py +164 -0
- api/dependencies.py +144 -0
- api/detection.py +152 -0
- api/gateway_model_ids.py +54 -0
- api/model_catalog.py +133 -0
- api/model_router.py +125 -0
- api/models/__init__.py +45 -0
- api/models/anthropic.py +234 -0
- api/models/openai_responses.py +28 -0
- api/models/responses.py +60 -0
- api/optimization_handlers.py +154 -0
- api/request_pipeline.py +424 -0
- api/routes.py +156 -0
- api/runtime.py +334 -0
- api/validation_log.py +48 -0
- api/web_server_tools.py +22 -0
- api/web_tools/__init__.py +17 -0
- api/web_tools/constants.py +15 -0
- api/web_tools/egress.py +99 -0
- api/web_tools/outbound.py +278 -0
- api/web_tools/parsers.py +104 -0
- api/web_tools/request.py +87 -0
- api/web_tools/streaming.py +206 -0
- cli/__init__.py +5 -0
- cli/claude_env.py +12 -0
- cli/entrypoints.py +166 -0
- cli/env.example +209 -0
- cli/launchers/__init__.py +1 -0
- cli/launchers/claude.py +84 -0
- cli/launchers/codex.py +204 -0
- cli/launchers/codex_model_catalog.py +186 -0
- cli/launchers/common.py +93 -0
- cli/managed/__init__.py +6 -0
- cli/managed/claude.py +215 -0
- cli/managed/manager.py +157 -0
- cli/managed/session.py +260 -0
- cli/process_registry.py +78 -0
- config/__init__.py +5 -0
- config/constants.py +13 -0
- config/logging_config.py +159 -0
- config/nim.py +118 -0
- config/paths.py +91 -0
- config/provider_catalog.py +259 -0
- config/provider_ids.py +7 -0
- config/settings.py +538 -0
- core/__init__.py +1 -0
- core/anthropic/__init__.py +46 -0
- core/anthropic/content.py +31 -0
- core/anthropic/conversion.py +587 -0
- core/anthropic/emitted_sse_tracker.py +346 -0
- core/anthropic/errors.py +70 -0
- core/anthropic/native_messages_request.py +280 -0
- core/anthropic/native_sse_block_policy.py +313 -0
- core/anthropic/provider_stream_error.py +34 -0
- core/anthropic/server_tool_sse.py +14 -0
- core/anthropic/sse.py +440 -0
- core/anthropic/stream_contracts.py +205 -0
- core/anthropic/stream_recovery.py +346 -0
- core/anthropic/stream_recovery_session.py +133 -0
- core/anthropic/thinking.py +140 -0
- core/anthropic/tokens.py +117 -0
- core/anthropic/tools.py +212 -0
- core/anthropic/utils.py +9 -0
- core/openai_responses/__init__.py +5 -0
- core/openai_responses/adapter.py +31 -0
- core/openai_responses/anthropic_sse.py +59 -0
- core/openai_responses/errors.py +22 -0
- core/openai_responses/events.py +19 -0
- core/openai_responses/ids.py +21 -0
- core/openai_responses/input.py +258 -0
- core/openai_responses/items.py +37 -0
- core/openai_responses/reasoning.py +52 -0
- core/openai_responses/stream.py +25 -0
- core/openai_responses/stream_state.py +654 -0
- core/openai_responses/tools.py +374 -0
- core/openai_responses/usage.py +37 -0
- core/rate_limit.py +60 -0
- core/trace.py +216 -0
- devcopilot-0.2.0.dist-info/METADATA +687 -0
- devcopilot-0.2.0.dist-info/RECORD +189 -0
- devcopilot-0.2.0.dist-info/WHEEL +4 -0
- devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
- devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
- messaging/__init__.py +26 -0
- messaging/cli_event_constants.py +67 -0
- messaging/command_context.py +66 -0
- messaging/command_dispatcher.py +37 -0
- messaging/commands.py +275 -0
- messaging/event_parser.py +181 -0
- messaging/limiter.py +300 -0
- messaging/models.py +36 -0
- messaging/node_event_pipeline.py +127 -0
- messaging/node_runner.py +342 -0
- messaging/platforms/__init__.py +15 -0
- messaging/platforms/base.py +228 -0
- messaging/platforms/discord.py +567 -0
- messaging/platforms/factory.py +103 -0
- messaging/platforms/outbox.py +144 -0
- messaging/platforms/telegram.py +688 -0
- messaging/platforms/voice_flow.py +295 -0
- messaging/rendering/__init__.py +3 -0
- messaging/rendering/discord_markdown.py +318 -0
- messaging/rendering/markdown_tables.py +49 -0
- messaging/rendering/profiles.py +55 -0
- messaging/rendering/telegram_markdown.py +327 -0
- messaging/safe_diagnostics.py +17 -0
- messaging/session.py +334 -0
- messaging/transcript.py +581 -0
- messaging/transcription.py +164 -0
- messaging/trees/__init__.py +15 -0
- messaging/trees/data.py +482 -0
- messaging/trees/manager.py +433 -0
- messaging/trees/processor.py +179 -0
- messaging/trees/repository.py +177 -0
- messaging/turn_intake.py +235 -0
- messaging/ui_updates.py +101 -0
- messaging/voice.py +76 -0
- messaging/workflow.py +200 -0
- providers/__init__.py +31 -0
- providers/base.py +152 -0
- providers/cerebras/__init__.py +7 -0
- providers/cerebras/client.py +31 -0
- providers/cerebras/request.py +55 -0
- providers/codestral/__init__.py +7 -0
- providers/codestral/client.py +34 -0
- providers/deepseek/__init__.py +11 -0
- providers/deepseek/client.py +51 -0
- providers/deepseek/request.py +475 -0
- providers/defaults.py +41 -0
- providers/error_mapping.py +309 -0
- providers/exceptions.py +113 -0
- providers/fireworks/__init__.py +5 -0
- providers/fireworks/client.py +45 -0
- providers/fireworks/request.py +48 -0
- providers/gemini/__init__.py +7 -0
- providers/gemini/client.py +49 -0
- providers/gemini/request.py +199 -0
- providers/groq/__init__.py +7 -0
- providers/groq/client.py +31 -0
- providers/groq/request.py +83 -0
- providers/kimi/__init__.py +10 -0
- providers/kimi/client.py +53 -0
- providers/kimi/request.py +42 -0
- providers/llamacpp/__init__.py +3 -0
- providers/llamacpp/client.py +16 -0
- providers/lmstudio/__init__.py +5 -0
- providers/lmstudio/client.py +16 -0
- providers/mistral/__init__.py +7 -0
- providers/mistral/client.py +31 -0
- providers/mistral/request.py +37 -0
- providers/model_listing.py +133 -0
- providers/nvidia_nim/__init__.py +7 -0
- providers/nvidia_nim/client.py +91 -0
- providers/nvidia_nim/request.py +430 -0
- providers/nvidia_nim/voice.py +95 -0
- providers/ollama/__init__.py +7 -0
- providers/ollama/client.py +39 -0
- providers/open_router/__init__.py +7 -0
- providers/open_router/client.py +124 -0
- providers/open_router/request.py +42 -0
- providers/opencode/__init__.py +11 -0
- providers/opencode/client.py +31 -0
- providers/opencode/request.py +35 -0
- providers/rate_limit.py +300 -0
- providers/registry.py +527 -0
- providers/transports/__init__.py +1 -0
- providers/transports/anthropic_messages/__init__.py +5 -0
- providers/transports/anthropic_messages/http.py +118 -0
- providers/transports/anthropic_messages/recovery.py +206 -0
- providers/transports/anthropic_messages/stream.py +295 -0
- providers/transports/anthropic_messages/transport.py +236 -0
- providers/transports/openai_chat/__init__.py +5 -0
- providers/transports/openai_chat/recovery.py +217 -0
- providers/transports/openai_chat/stream.py +384 -0
- providers/transports/openai_chat/tool_calls.py +293 -0
- providers/transports/openai_chat/transport.py +156 -0
- providers/wafer/__init__.py +10 -0
- providers/wafer/client.py +50 -0
- providers/zai/__init__.py +10 -0
- providers/zai/client.py +46 -0
- providers/zai/request.py +42 -0
api/gateway_model_ids.py
ADDED
|
@@ -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
|
+
]
|
api/models/anthropic.py
ADDED
|
@@ -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
|
api/models/responses.py
ADDED
|
@@ -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
|