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.
@@ -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.4
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=JpvIWNXHUPVqOGvps7o_6ZADhXuJuvpU7RdMqQFtwwM,6421
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.4.dist-info/METADATA,sha256=cpwFo9rILUr99bq2K1bRH62s-hhVQqmed4psTvG-XFM,11674
70
- coderouter_cli-2.5.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
71
- coderouter_cli-2.5.4.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
72
- coderouter_cli-2.5.4.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
73
- coderouter_cli-2.5.4.dist-info/RECORD,,
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,,