coderouter-cli 1.10.0__py3-none-any.whl → 1.10.1__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.
@@ -498,6 +498,23 @@ class RuleMatcher(BaseModel):
498
498
  workloads can compensate by tuning the threshold, since the
499
499
  char/4 heuristic is conservative for CJK and looser for
500
500
  English code.
501
+
502
+ Variants ([Unreleased] / tool-aware routing, OpenClaw + Pi 由来):
503
+
504
+ - ``has_tools: True`` — the request body declares one or more
505
+ tools (OpenAI ``tools[]`` / Anthropic ``tools[]`` / OpenAI legacy
506
+ ``functions[]``). Lets operators send tool-laden requests to a
507
+ tool-capable cloud profile while keeping plain chat on a small
508
+ local model (typical Raspberry Pi / low-spec deployment shape:
509
+ a 1-4B local model that cannot reliably tool-call paired with a
510
+ free-tier cloud chain that can). Distinct from the
511
+ ``capabilities.tools`` flag on a provider — that flag is read by
512
+ ``coderouter doctor`` for diagnostics but does NOT gate the
513
+ fallback chain (the chain just iterates providers in order and
514
+ engages the v0.3-D tool-downgrade path on non-native ones with
515
+ ``request.tools`` set). The ``has_tools`` matcher is the
516
+ profile-level lever for steering tool-laden traffic to the right
517
+ chain entirely.
501
518
  """
502
519
 
503
520
  model_config = ConfigDict(extra="forbid")
@@ -508,6 +525,13 @@ class RuleMatcher(BaseModel):
508
525
  content_regex: str | None = None
509
526
  model_pattern: str | None = None
510
527
  content_token_count_min: int | None = Field(default=None, ge=1)
528
+ # [Unreleased]: tool-aware routing (OpenClaw + Raspberry Pi 由来).
529
+ # See class docstring "Variants ([Unreleased] / tool-aware routing)"
530
+ # above for the full rationale. Boolean shape mirrors ``has_image`` —
531
+ # only the ``True`` value is meaningful (matches when the body
532
+ # declares any tools); ``False`` is rejected by ``_exactly_one``
533
+ # since a "no-tools" rule would shadow the default fall-through.
534
+ has_tools: bool | None = None
511
535
 
512
536
  _MATCHER_FIELDS: tuple[str, ...] = (
513
537
  "has_image",
@@ -516,6 +540,7 @@ class RuleMatcher(BaseModel):
516
540
  "content_regex",
517
541
  "model_pattern",
518
542
  "content_token_count_min",
543
+ "has_tools",
519
544
  )
520
545
 
521
546
  @model_validator(mode="after")
@@ -142,12 +142,40 @@ def _code_fence_ratio(text: str) -> float:
142
142
  return fenced / len(text)
143
143
 
144
144
 
145
+ def _has_tools_in_body(body: dict[str, Any]) -> bool:
146
+ """True iff the request body declares one or more callable tools.
147
+
148
+ Recognized declaration shapes:
149
+
150
+ * ``tools: [...]`` — OpenAI Chat Completions ``tools[]`` AND
151
+ Anthropic Messages API ``tools[]``. Both wire formats put the
152
+ array at the same top-level key, so a single membership check
153
+ covers both ingresses.
154
+ * ``functions: [...]`` — OpenAI legacy ``functions[]`` (deprecated
155
+ since 2023-11 but still emitted by some agents that pinned old
156
+ SDK versions). Treated as equivalent to ``tools[]`` for routing
157
+ purposes.
158
+
159
+ A non-list value (or a value of ``None`` / empty list) returns
160
+ False — agents that initialize the field but populate it lazily
161
+ are still on the no-tools path until a tool actually appears.
162
+ """
163
+ tools = body.get("tools")
164
+ if isinstance(tools, list) and len(tools) > 0:
165
+ return True
166
+ functions = body.get("functions")
167
+ if isinstance(functions, list) and len(functions) > 0:
168
+ return True
169
+ return False
170
+
171
+
145
172
  def _match_rule(
146
173
  rule: AutoRouteRule,
147
174
  message: dict[str, Any] | None,
148
175
  text: str,
149
176
  model: str | None,
150
177
  estimated_tokens: int,
178
+ has_tools: bool,
151
179
  ) -> bool:
152
180
  m = rule.match
153
181
  if m.has_image is True:
@@ -177,6 +205,14 @@ def _match_rule(
177
205
  # for English code; operators tune the threshold to match
178
206
  # their input distribution.
179
207
  return estimated_tokens >= m.content_token_count_min
208
+ if m.has_tools is True:
209
+ # [Unreleased]: tool-aware routing (OpenClaw + Pi 由来).
210
+ # Computed once in ``classify`` from ``body.tools`` /
211
+ # ``body.functions`` so per-rule evaluation is O(1). See
212
+ # ``_has_tools_in_body`` for the recognized declaration shapes
213
+ # and ``RuleMatcher`` docstring for why this is profile-level
214
+ # routing (not a provider capability gate).
215
+ return has_tools
180
216
  return False # pragma: no cover — _exactly_one guards against this
181
217
 
182
218
 
@@ -259,6 +295,11 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
259
295
  # contribute 0. See ``_estimate_total_tokens`` for the heuristic
260
296
  # rationale and the 5-deps tradeoff.
261
297
  estimated_tokens = _estimate_total_tokens(body)
298
+ # [Unreleased]: tool-aware routing (OpenClaw + Pi 由来). Computed
299
+ # once for both the ``has_tools`` matcher and the signals payload.
300
+ # See ``_has_tools_in_body`` for the recognized declaration shapes
301
+ # (OpenAI/Anthropic ``tools[]``, OpenAI legacy ``functions[]``).
302
+ has_tools = _has_tools_in_body(body)
262
303
 
263
304
  auto_cfg = config.auto_router
264
305
  if auto_cfg is not None and auto_cfg.disabled:
@@ -267,6 +308,7 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
267
308
  text,
268
309
  model,
269
310
  estimated_tokens,
311
+ has_tools,
270
312
  disabled=True,
271
313
  )
272
314
  return auto_cfg.default_rule_profile
@@ -278,17 +320,18 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
278
320
  else BUNDLED_DEFAULT_RULE_PROFILE
279
321
  )
280
322
 
281
- # ``model_pattern`` and ``content_token_count_min`` matchers can
282
- # fire even without a user message (e.g. system-only prompts or a
283
- # request body that carries only a model field). Other matchers
284
- # still require ``user_msg`` to be present — they short out via
285
- # ``_match_rule``'s message-None handling.
323
+ # ``model_pattern``, ``content_token_count_min`` and ``has_tools``
324
+ # matchers can fire even without a user message (e.g. system-only
325
+ # prompts or a request body that carries only a model field +
326
+ # tools array). Other matchers still require ``user_msg`` to be
327
+ # present — they short out via ``_match_rule``'s message-None
328
+ # handling.
286
329
  for rule in rules:
287
- if _match_rule(rule, user_msg, text, model, estimated_tokens):
288
- _emit_resolved(rule, user_msg, text, model, estimated_tokens)
330
+ if _match_rule(rule, user_msg, text, model, estimated_tokens, has_tools):
331
+ _emit_resolved(rule, user_msg, text, model, estimated_tokens, has_tools)
289
332
  return rule.profile
290
333
 
291
- _emit_fallthrough(default_profile, text, model, estimated_tokens)
334
+ _emit_fallthrough(default_profile, text, model, estimated_tokens, has_tools)
292
335
  return default_profile
293
336
 
294
337
 
@@ -298,6 +341,7 @@ def _emit_resolved(
298
341
  text: str,
299
342
  model: str | None,
300
343
  estimated_tokens: int,
344
+ has_tools: bool,
301
345
  ) -> None:
302
346
  logger.info(
303
347
  "auto-router-resolved",
@@ -310,6 +354,7 @@ def _emit_resolved(
310
354
  "content_len": len(text),
311
355
  "model": model,
312
356
  "estimated_tokens": estimated_tokens,
357
+ "has_tools": has_tools,
313
358
  },
314
359
  },
315
360
  )
@@ -320,6 +365,7 @@ def _emit_fallthrough(
320
365
  text: str,
321
366
  model: str | None,
322
367
  estimated_tokens: int,
368
+ has_tools: bool,
323
369
  disabled: bool = False,
324
370
  ) -> None:
325
371
  logger.info(
@@ -331,6 +377,7 @@ def _emit_fallthrough(
331
377
  "content_len": len(text),
332
378
  "model": model,
333
379
  "estimated_tokens": estimated_tokens,
380
+ "has_tools": has_tools,
334
381
  "disabled": disabled,
335
382
  },
336
383
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 1.10.0
3
+ Version: 1.10.1
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
@@ -18,7 +18,7 @@ coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g
18
18
  coderouter/config/capability_registry.py,sha256=F6DetVL5oM03R4QeK1g6h_Q_zrXH0opnYDp3duZmkN4,15808
19
19
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
20
20
  coderouter/config/loader.py,sha256=FUEe8m4Tnmj_aul0vSctD8vKvNW-oLRoMRbTpSKqSmc,4077
21
- coderouter/config/schemas.py,sha256=c3eR1iqyJq_x6kwkyONgDIyVCFbfk20A49B131Ar_0k,36741
21
+ coderouter/config/schemas.py,sha256=OI_78vSvjWTSVJMPER-JhtMIXOVJZ-8QaLH8wj3Snoc,38204
22
22
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
23
23
  coderouter/data/model-capabilities.yaml,sha256=b0CJYfBqnKA8uaOxOdgNf5z9opTZcrC-N2FtsJvxPgw,16911
24
24
  coderouter/guards/__init__.py,sha256=eYjuKo0OvE-GjJo7drYtU2XavUPF9OdiTB5IS76c92Y,855
@@ -36,7 +36,7 @@ coderouter/metrics/collector.py,sha256=y4MUAAF_nf9nml6c_NwRwt_1BsIqCXVL1oLrxGmXF
36
36
  coderouter/metrics/prometheus.py,sha256=GveI6OyWCACOet05Enpv1gxGrJ-wqpMWLiTRceT0bAw,16913
37
37
  coderouter/routing/__init__.py,sha256=g2vhutbozRx5QBThReqwPN3imk5qXdpDiaogILd3IRc,257
38
38
  coderouter/routing/adaptive.py,sha256=pRUphp2_NJ7i6I2HRNk3AkB7Wiu0amZ3vtgBHgmVLBU,20233
39
- coderouter/routing/auto_router.py,sha256=Lb3ej2mjoqjD5cbvBPmHb8kCvg1ikpau85bei2_CrbI,12503
39
+ coderouter/routing/auto_router.py,sha256=1szbaFMa9mTZPuEOxdxHmVfMNfuH4v4FXkfD4LG3vsc,14571
40
40
  coderouter/routing/budget.py,sha256=XZQL24YIkusB2MwfgbIJ1uhW1ODtMMdpbZ5e_A7oSU4,7164
41
41
  coderouter/routing/capability.py,sha256=ziIDuE5keH_jxYDlXSKufRVxxSYOAvUxJ6Rw5QkYDDU,18436
42
42
  coderouter/routing/fallback.py,sha256=M0jiUJzzvXcHHW-7YhOiDLAx-UJTL5EA_j97w5Ao-10,66540
@@ -44,8 +44,8 @@ coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8I
44
44
  coderouter/translation/anthropic.py,sha256=JpvIWNXHUPVqOGvps7o_6ZADhXuJuvpU7RdMqQFtwwM,6421
45
45
  coderouter/translation/convert.py,sha256=-qyzFzmmr9hhQV6_Sg75kJnvCZvHe3n7vRdaZtk_JqQ,47269
46
46
  coderouter/translation/tool_repair.py,sha256=fyxDb4kWHytO5JWq5y0i4tinJUtWqhMCkyfoCf5BjeM,8314
47
- coderouter_cli-1.10.0.dist-info/METADATA,sha256=sXwBm1KLnTHo5i3rSNUwqlNURCRZ0u6LgLQdYWrQ5d8,47838
48
- coderouter_cli-1.10.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
49
- coderouter_cli-1.10.0.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
50
- coderouter_cli-1.10.0.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
51
- coderouter_cli-1.10.0.dist-info/RECORD,,
47
+ coderouter_cli-1.10.1.dist-info/METADATA,sha256=H1f0BXj6vUXy05qjEY1lAah8ItzI_zrnj3rWkWjgU9w,47838
48
+ coderouter_cli-1.10.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
49
+ coderouter_cli-1.10.1.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
50
+ coderouter_cli-1.10.1.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
51
+ coderouter_cli-1.10.1.dist-info/RECORD,,