coderouter-cli 2.5.4__py3-none-any.whl → 2.5.5__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.
- coderouter/translation/anthropic.py +124 -1
- {coderouter_cli-2.5.4.dist-info → coderouter_cli-2.5.5.dist-info}/METADATA +1 -1
- {coderouter_cli-2.5.4.dist-info → coderouter_cli-2.5.5.dist-info}/RECORD +6 -6
- {coderouter_cli-2.5.4.dist-info → coderouter_cli-2.5.5.dist-info}/WHEEL +0 -0
- {coderouter_cli-2.5.4.dist-info → coderouter_cli-2.5.5.dist-info}/entry_points.txt +0 -0
- {coderouter_cli-2.5.4.dist-info → coderouter_cli-2.5.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,9 +10,12 @@ through unchanged if a client sends them.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import logging
|
|
13
14
|
from typing import Any, Literal
|
|
14
15
|
|
|
15
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
16
19
|
|
|
17
20
|
# ============================================================
|
|
18
21
|
# Content blocks
|
|
@@ -105,6 +108,113 @@ class AnthropicTool(BaseModel):
|
|
|
105
108
|
input_schema: dict[str, Any] = Field(default_factory=dict)
|
|
106
109
|
|
|
107
110
|
|
|
111
|
+
# ============================================================
|
|
112
|
+
# Role normalization (Claude Code CLI >= 2.1.154 workaround)
|
|
113
|
+
# ============================================================
|
|
114
|
+
|
|
115
|
+
_SPEC_MESSAGE_ROLES = frozenset({"user", "assistant"})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _content_as_text(content: Any) -> str:
|
|
119
|
+
"""Best-effort plain-text extraction from a message ``content`` field.
|
|
120
|
+
|
|
121
|
+
Strings pass through; block lists contribute their ``text`` blocks
|
|
122
|
+
joined with newlines; anything else yields "".
|
|
123
|
+
"""
|
|
124
|
+
if isinstance(content, str):
|
|
125
|
+
return content
|
|
126
|
+
if isinstance(content, list):
|
|
127
|
+
parts: list[str] = []
|
|
128
|
+
for block in content:
|
|
129
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
130
|
+
parts.append(str(block.get("text", "")))
|
|
131
|
+
return "\n".join(p for p in parts if p)
|
|
132
|
+
return ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def normalize_message_roles(payload: dict[str, Any]) -> dict[str, Any]:
|
|
136
|
+
"""Normalize non-spec roles inside ``messages`` before validation.
|
|
137
|
+
|
|
138
|
+
Claude Code CLI >= 2.1.154 has a regression where it emits messages
|
|
139
|
+
with ``role: "system"`` (and reportedly ``ctx`` / ``msg``) inside the
|
|
140
|
+
``messages`` array. The Anthropic Messages API spec allows only
|
|
141
|
+
``user`` / ``assistant`` there, so without this hop those requests
|
|
142
|
+
die in validation with "Input should be 'user' or 'assistant'"
|
|
143
|
+
(see anthropics/claude-code#63469, vllm-project/vllm#44000).
|
|
144
|
+
|
|
145
|
+
Policy:
|
|
146
|
+
- ``role: "system"`` → text content merged into the top-level
|
|
147
|
+
``system`` field (appended after any existing system prompt;
|
|
148
|
+
same join rule as ``convert.to_anthropic_request``).
|
|
149
|
+
- any other non-spec role (``ctx``, ``msg``, ...) → coerced to
|
|
150
|
+
``user`` so conversation position is preserved. Anthropic
|
|
151
|
+
merges consecutive same-role turns, so this is safe.
|
|
152
|
+
- messages whose salvaged content is empty are dropped entirely
|
|
153
|
+
(Anthropic rejects empty turns).
|
|
154
|
+
|
|
155
|
+
Returns a shallow-copied payload; the caller's dict is not mutated.
|
|
156
|
+
Non-dict message entries (already-validated models) pass through.
|
|
157
|
+
"""
|
|
158
|
+
messages = payload.get("messages")
|
|
159
|
+
if not isinstance(messages, list):
|
|
160
|
+
return payload
|
|
161
|
+
|
|
162
|
+
system_texts: list[str] = []
|
|
163
|
+
messages_out: list[Any] = []
|
|
164
|
+
coerced_roles: list[str] = []
|
|
165
|
+
|
|
166
|
+
for msg in messages:
|
|
167
|
+
if not isinstance(msg, dict):
|
|
168
|
+
# Already a validated AnthropicMessage (internal construction
|
|
169
|
+
# path, e.g. convert.to_anthropic_request) — spec roles only.
|
|
170
|
+
messages_out.append(msg)
|
|
171
|
+
continue
|
|
172
|
+
role = msg.get("role")
|
|
173
|
+
if role in _SPEC_MESSAGE_ROLES:
|
|
174
|
+
messages_out.append(msg)
|
|
175
|
+
continue
|
|
176
|
+
if role == "system":
|
|
177
|
+
text = _content_as_text(msg.get("content"))
|
|
178
|
+
if text:
|
|
179
|
+
system_texts.append(text)
|
|
180
|
+
coerced_roles.append("system")
|
|
181
|
+
continue
|
|
182
|
+
# Unknown role (ctx / msg / future surprises): keep its position
|
|
183
|
+
# in the conversation as a user turn; drop if nothing salvageable.
|
|
184
|
+
text = _content_as_text(msg.get("content"))
|
|
185
|
+
coerced_roles.append(str(role))
|
|
186
|
+
if text:
|
|
187
|
+
messages_out.append({"role": "user", "content": text})
|
|
188
|
+
|
|
189
|
+
if not coerced_roles:
|
|
190
|
+
return payload
|
|
191
|
+
|
|
192
|
+
out = dict(payload)
|
|
193
|
+
out["messages"] = messages_out
|
|
194
|
+
|
|
195
|
+
if system_texts:
|
|
196
|
+
joined = "\n".join(system_texts)
|
|
197
|
+
existing = out.get("system")
|
|
198
|
+
if existing is None:
|
|
199
|
+
out["system"] = joined
|
|
200
|
+
elif isinstance(existing, str):
|
|
201
|
+
out["system"] = f"{existing}\n{joined}" if existing else joined
|
|
202
|
+
elif isinstance(existing, list):
|
|
203
|
+
out["system"] = [*existing, {"type": "text", "text": joined}]
|
|
204
|
+
else: # unexpected shape — don't lose the client's value
|
|
205
|
+
out["system"] = existing
|
|
206
|
+
|
|
207
|
+
logger.warning(
|
|
208
|
+
"normalized-nonspec-message-roles",
|
|
209
|
+
extra={
|
|
210
|
+
"roles": coerced_roles,
|
|
211
|
+
"system_merged": bool(system_texts),
|
|
212
|
+
"hint": "client is likely Claude Code CLI >= 2.1.154 (known regression)",
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
108
218
|
# ============================================================
|
|
109
219
|
# Request
|
|
110
220
|
# ============================================================
|
|
@@ -147,6 +257,19 @@ class AnthropicRequest(BaseModel):
|
|
|
147
257
|
# `thinking` beyond what the default minor version accepts.
|
|
148
258
|
anthropic_beta: str | None = Field(default=None, exclude=True)
|
|
149
259
|
|
|
260
|
+
@model_validator(mode="before")
|
|
261
|
+
@classmethod
|
|
262
|
+
def _normalize_roles(cls, data: Any) -> Any:
|
|
263
|
+
"""Claude Code >= 2.1.154 sends system/ctx/msg roles in messages.
|
|
264
|
+
|
|
265
|
+
Normalize them before field validation so the request doesn't
|
|
266
|
+
422 at ingress (and doesn't 400 upstream at api.anthropic.com
|
|
267
|
+
via the native adapter). See ``normalize_message_roles``.
|
|
268
|
+
"""
|
|
269
|
+
if isinstance(data, dict):
|
|
270
|
+
return normalize_message_roles(data)
|
|
271
|
+
return data
|
|
272
|
+
|
|
150
273
|
|
|
151
274
|
# ============================================================
|
|
152
275
|
# Response
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.5
|
|
4
4
|
Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zephel01/CodeRouter
|
|
6
6
|
Project-URL: Repository, https://github.com/zephel01/CodeRouter
|
|
@@ -63,11 +63,11 @@ coderouter/state/request_log.py,sha256=bR814sOn--U_sKVtbezwS3bkZaNt4FGnboX75_2LL
|
|
|
63
63
|
coderouter/state/store.py,sha256=h-rsMJq8GILsOfCP94nI40cuHaj4Vqycsm9UNN77REI,7445
|
|
64
64
|
coderouter/state/suggest_rules.py,sha256=FvdhEvao5NvdKp9zs8AkcoFKHY4yqqXY2HekvSjpDFA,16670
|
|
65
65
|
coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8IYOtG8,1788
|
|
66
|
-
coderouter/translation/anthropic.py,sha256=
|
|
66
|
+
coderouter/translation/anthropic.py,sha256=aZkcYH4x82b0x7efJgJb9RWn9Hbyc9pEOthXe4vjUdU,11113
|
|
67
67
|
coderouter/translation/convert.py,sha256=-qyzFzmmr9hhQV6_Sg75kJnvCZvHe3n7vRdaZtk_JqQ,47269
|
|
68
68
|
coderouter/translation/tool_repair.py,sha256=Ok2PF947Liegc5oaytfptv5MWMkpfJYQie-zdP1y3cY,9946
|
|
69
|
-
coderouter_cli-2.5.
|
|
70
|
-
coderouter_cli-2.5.
|
|
71
|
-
coderouter_cli-2.5.
|
|
72
|
-
coderouter_cli-2.5.
|
|
73
|
-
coderouter_cli-2.5.
|
|
69
|
+
coderouter_cli-2.5.5.dist-info/METADATA,sha256=1A8zDyh8_kEIFafq1l3uKVyJikkJ8QOmwOlaEaSz_qI,11674
|
|
70
|
+
coderouter_cli-2.5.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
71
|
+
coderouter_cli-2.5.5.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
|
|
72
|
+
coderouter_cli-2.5.5.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
|
|
73
|
+
coderouter_cli-2.5.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|