@oneciel-ai/claude-any 0.1.25 → 0.1.27

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.
package/README.md CHANGED
@@ -34,7 +34,7 @@ arguments through unchanged.
34
34
 
35
35
  Credits: One Ciel LLC
36
36
 
37
- Current version: `0.1.25`
37
+ Current version: `0.1.27`
38
38
 
39
39
  ## Why This Exists
40
40
 
@@ -249,12 +249,24 @@ steps under that larger model's supervision.
249
249
 
250
250
  ## Changelog
251
251
 
252
+ ### 0.1.27
253
+
254
+ - **Plan mode support for non-Anthropic providers**: the router now keeps
255
+ `EnterPlanMode` available and supports Claude Code Plan mode even when the
256
+ upstream model does not reliably choose that internal tool. Forced
257
+ `tool_choice=EnterPlanMode` is answered locally with a valid Anthropic
258
+ `tool_use`, and long implementation requests that receive only a short or
259
+ empty non-actionable text response are promoted to `EnterPlanMode` using
260
+ language-agnostic structure checks.
261
+ - **Plan-mode self-tool handling**: unsupported Claude Code self-tools are
262
+ still stripped for non-Anthropic providers, but Plan-mode tools are handled
263
+ separately so planning can work instead of being disabled.
264
+
252
265
  ### 0.1.25
253
266
 
254
- - **Plan-mode guard + diagnostics**: non-Anthropic providers now strip Claude
255
- Code self-tools such as `EnterPlanMode` before forwarding requests upstream.
256
- Set `~/.config/claude-any/log-level` to `TRACE` to capture redacted request
257
- and response summaries in `requests.jsonl` / `responses.jsonl`.
267
+ - **Plan-mode diagnostics**: set `~/.config/claude-any/log-level` to `TRACE`
268
+ to capture redacted request and response summaries in `requests.jsonl` /
269
+ `responses.jsonl`.
258
270
  - **Headless agent chat service**: the router exposes a small HTTP control
259
271
  plane for sub coding agents. Agents can post messages, poll updates after
260
272
  the last seen message id, or wait on an SSE stream when they do not have
package/claude_any.py CHANGED
@@ -80,7 +80,7 @@ PROVIDER_LABELS = {
80
80
  "self-hosted-nim": "Self Hosted NIM",
81
81
  }
82
82
  APP_NAME = "Claude Any"
83
- VERSION = "0.1.25"
83
+ VERSION = "0.1.27"
84
84
  CREDITS = "Credits: One Ciel LLC"
85
85
 
86
86
  LOG_LEVELS = {"SILENT": 0, "ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4, "TRACE": 5}
@@ -98,8 +98,8 @@ _CHAT_NEXT_ID: int | None = None
98
98
  # Tools Claude Code injects into every model's tool list that misfire when called
99
99
  # by non-Anthropic models. See docs/notes from anthropics/claude-code issues
100
100
  # #25720, #29950 and Piebald-AI/claude-code-system-prompts for tool semantics.
101
+ PLAN_MODE_SELF_TOOLS: tuple[str, ...] = ("EnterPlanMode", "ExitPlanMode")
101
102
  DEFAULT_BLOCKED_TOOLS_NON_ANTHROPIC: tuple[str, ...] = (
102
- "EnterPlanMode",
103
103
  "EnterWorktree",
104
104
  "ExitWorktree",
105
105
  "TeamCreate",
@@ -1185,15 +1185,130 @@ def resolve_blocked_tools(provider: str, pcfg: dict[str, Any]) -> set[str]:
1185
1185
  return set(DEFAULT_BLOCKED_TOOLS_NON_ANTHROPIC)
1186
1186
 
1187
1187
 
1188
+ def forced_tool_choice_name(body: dict[str, Any]) -> str | None:
1189
+ tool_choice = body.get("tool_choice") if isinstance(body.get("tool_choice"), dict) else None
1190
+ if not tool_choice:
1191
+ return None
1192
+ if tool_choice.get("type") != "tool":
1193
+ return None
1194
+ name = tool_choice.get("name")
1195
+ return name if isinstance(name, str) and name else None
1196
+
1197
+
1198
+ def tool_names_in_body(body: dict[str, Any]) -> set[str]:
1199
+ names: set[str] = set()
1200
+ tools = body.get("tools")
1201
+ if not isinstance(tools, list):
1202
+ return names
1203
+ for tool in tools:
1204
+ if isinstance(tool, dict) and isinstance(tool.get("name"), str):
1205
+ names.add(tool["name"])
1206
+ return names
1207
+
1208
+
1209
+ def synthetic_tool_use_response(model: str, tool_name: str, tool_input: dict[str, Any] | None = None) -> dict[str, Any]:
1210
+ now = int(time.time() * 1000)
1211
+ return {
1212
+ "id": f"msg_claude_any_tool_{now}",
1213
+ "type": "message",
1214
+ "role": "assistant",
1215
+ "model": model or "claude-any-router",
1216
+ "content": [
1217
+ {
1218
+ "type": "tool_use",
1219
+ "id": f"toolu_claude_any_{tool_name}_{now}",
1220
+ "name": tool_name,
1221
+ "input": tool_input or {},
1222
+ }
1223
+ ],
1224
+ "stop_reason": "tool_use",
1225
+ "stop_sequence": None,
1226
+ "usage": {"input_tokens": 0, "output_tokens": 0},
1227
+ }
1228
+
1229
+
1230
+ def has_tool(body: dict[str, Any], name: str) -> bool:
1231
+ return name in tool_names_in_body(body)
1232
+
1233
+
1234
+ def latest_user_text(body: dict[str, Any]) -> str:
1235
+ for message in reversed(body.get("messages") or []):
1236
+ if not isinstance(message, dict) or message.get("role") != "user":
1237
+ continue
1238
+ return anthropic_content_to_text(message.get("content"))
1239
+ return ""
1240
+
1241
+
1242
+ def likely_implementation_planning_request(text: str) -> bool:
1243
+ normalized = re.sub(r"\s+", " ", text or "").strip()
1244
+ if len(normalized) >= 120:
1245
+ return True
1246
+ # Multi-line prompts usually carry enough task structure that a one-line
1247
+ # "I'll make a plan" style response is not a useful final answer.
1248
+ non_empty_lines = [line for line in (text or "").splitlines() if line.strip()]
1249
+ if len(non_empty_lines) >= 3 and len(normalized) >= 80:
1250
+ return True
1251
+ return False
1252
+
1253
+
1254
+ def non_actionable_short_response(text: str) -> bool:
1255
+ normalized = re.sub(r"\s+", " ", text or "").strip()
1256
+ if not normalized:
1257
+ return True
1258
+ # Language-agnostic: for a long implementation request, a short single-line
1259
+ # text response with no tool call is not actionable. Do not inspect words.
1260
+ if len(normalized) <= 80 and "\n" not in (text or ""):
1261
+ return True
1262
+ if len(normalized) <= 160 and "\n" not in (text or "") and not re.search(r"[`{};/\\\\]|https?://", normalized):
1263
+ return True
1264
+ return False
1265
+
1266
+
1267
+ def should_auto_enter_plan_mode(body: dict[str, Any], response_text: str, tool_calls: list[dict[str, Any]]) -> bool:
1268
+ if tool_calls:
1269
+ return False
1270
+ if not has_tool(body, "EnterPlanMode"):
1271
+ return False
1272
+ if not non_actionable_short_response(response_text):
1273
+ return False
1274
+ return likely_implementation_planning_request(latest_user_text(body))
1275
+
1276
+
1277
+ def maybe_handle_plan_mode_tool_choice(handler: BaseHTTPRequestHandler, provider: str, body: dict[str, Any]) -> bool:
1278
+ """Support Claude Code's forced Plan-mode entry without relying on upstream model behavior."""
1279
+ if provider == "anthropic":
1280
+ return False
1281
+ name = forced_tool_choice_name(body)
1282
+ if name != "EnterPlanMode":
1283
+ return False
1284
+ # Claude Code may force this tool when the user uses /plan or toggles Plan mode.
1285
+ # Returning a valid tool_use locally is more reliable than asking arbitrary
1286
+ # OpenAI/Ollama-compatible backends to select an internal Claude Code tool.
1287
+ available = tool_names_in_body(body)
1288
+ if available and name not in available:
1289
+ return False
1290
+ router_log("INFO", f"synthesized {name} tool_use for {provider} forced tool_choice")
1291
+ write_json(handler, synthetic_tool_use_response(str(body.get("model") or ""), name))
1292
+ return True
1293
+
1294
+
1188
1295
  def filter_blocked_tools(provider: str, pcfg: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
1189
1296
  """Strip Claude-Code self-tools the upstream model shouldn't see (e.g. EnterPlanMode).
1190
1297
  Returns a (possibly new) body dict."""
1191
- tools = body.get("tools")
1192
- if not isinstance(tools, list) or not tools:
1193
- return body
1194
1298
  blocked = resolve_blocked_tools(provider, pcfg)
1195
1299
  if not blocked:
1196
1300
  return body
1301
+ tools = body.get("tools")
1302
+ tool_choice = body.get("tool_choice") if isinstance(body.get("tool_choice"), dict) else None
1303
+ tool_choice_name = tool_choice.get("name") if tool_choice else None
1304
+ must_drop_tool_choice = isinstance(tool_choice_name, str) and tool_choice_name in blocked
1305
+ if not isinstance(tools, list) or not tools:
1306
+ if not must_drop_tool_choice:
1307
+ return body
1308
+ new_body = dict(body)
1309
+ new_body.pop("tool_choice", None)
1310
+ router_log("WARN", f"removed blocked tool_choice for {provider}: {tool_choice_name}")
1311
+ return new_body
1197
1312
  kept: list[Any] = []
1198
1313
  dropped: list[str] = []
1199
1314
  for tool in tools:
@@ -1203,10 +1318,18 @@ def filter_blocked_tools(provider: str, pcfg: dict[str, Any], body: dict[str, An
1203
1318
  continue
1204
1319
  kept.append(tool)
1205
1320
  if not dropped:
1206
- return body
1321
+ if not must_drop_tool_choice:
1322
+ return body
1323
+ new_body = dict(body)
1324
+ new_body.pop("tool_choice", None)
1325
+ router_log("WARN", f"removed blocked tool_choice for {provider}: {tool_choice_name}")
1326
+ return new_body
1207
1327
  router_log("INFO", f"filtered upstream tools for {provider}: {', '.join(sorted(set(dropped)))}")
1208
1328
  new_body = dict(body)
1209
1329
  new_body["tools"] = kept
1330
+ if must_drop_tool_choice:
1331
+ new_body.pop("tool_choice", None)
1332
+ router_log("WARN", f"removed blocked tool_choice for {provider}: {tool_choice_name}")
1210
1333
  return new_body
1211
1334
 
1212
1335
 
@@ -2400,7 +2523,7 @@ def normalize_tool_arguments(tool_name: str, args: Any) -> dict[str, Any]:
2400
2523
  return {}
2401
2524
 
2402
2525
 
2403
- def ollama_chat_to_anthropic(data: dict[str, Any], model: str) -> dict[str, Any]:
2526
+ def ollama_chat_to_anthropic(data: dict[str, Any], model: str, source_body: dict[str, Any] | None = None) -> dict[str, Any]:
2404
2527
  message = data.get("message") if isinstance(data.get("message"), dict) else {}
2405
2528
  content: list[dict[str, Any]] = []
2406
2529
  text = message.get("content") or ""
@@ -2435,6 +2558,9 @@ def ollama_chat_to_anthropic(data: dict[str, Any], model: str) -> dict[str, Any]
2435
2558
  "input": fixed_input,
2436
2559
  }
2437
2560
  )
2561
+ if source_body is not None and should_auto_enter_plan_mode(source_body, text, message.get("tool_calls") or []):
2562
+ router_log("WARN", "auto-synthesized EnterPlanMode from short/empty upstream response")
2563
+ return synthetic_tool_use_response(model, "EnterPlanMode")
2438
2564
  done_reason = data.get("done_reason")
2439
2565
  stop_reason = "tool_use" if any(block.get("type") == "tool_use" for block in content) else "end_turn"
2440
2566
  if done_reason == "length":
@@ -2578,7 +2704,7 @@ def _rebatch_anthropic_sse_text(handler: BaseHTTPRequestHandler, resp: Any) -> N
2578
2704
  pass
2579
2705
 
2580
2706
 
2581
- def _ollama_stream_to_anthropic_sse(handler: BaseHTTPRequestHandler, resp: Any, model: str, word_chunking: bool = False, provider: str = "ollama") -> None:
2707
+ def _ollama_stream_to_anthropic_sse(handler: BaseHTTPRequestHandler, resp: Any, model: str, word_chunking: bool = False, provider: str = "ollama", source_body: dict[str, Any] | None = None) -> None:
2582
2708
  """Stream Ollama NDJSON /api/chat response as Anthropic SSE /v1/messages format."""
2583
2709
  handler.send_response(200)
2584
2710
  handler.send_header("content-type", "text/event-stream")
@@ -2588,6 +2714,7 @@ def _ollama_stream_to_anthropic_sse(handler: BaseHTTPRequestHandler, resp: Any,
2588
2714
  msg_id = f"msg_ollama_{int(time.time() * 1000)}"
2589
2715
  started = False
2590
2716
  text_started = False
2717
+ text_suppressed_for_plan = False
2591
2718
  next_content_index = 0
2592
2719
  text_index: int | None = None
2593
2720
  text_so_far = ""
@@ -2632,6 +2759,10 @@ def _ollama_stream_to_anthropic_sse(handler: BaseHTTPRequestHandler, resp: Any,
2632
2759
  # Handle text content
2633
2760
  text_chunk = message.get("content") or ""
2634
2761
  if text_chunk:
2762
+ if source_body is not None and not text_started and not tool_calls and should_auto_enter_plan_mode(source_body, text_so_far + text_chunk, []):
2763
+ text_so_far += text_chunk
2764
+ text_suppressed_for_plan = True
2765
+ continue
2635
2766
  if not text_started:
2636
2767
  text_started = True
2637
2768
  text_index = next_content_index
@@ -2713,6 +2844,50 @@ def _ollama_stream_to_anthropic_sse(handler: BaseHTTPRequestHandler, resp: Any,
2713
2844
  handler.wfile.write(f"event: content_block_delta\ndata: {json.dumps(delta_event, ensure_ascii=False)}\n\n".encode())
2714
2845
  handler.wfile.flush()
2715
2846
  # Flush any remaining buffered text when word-chunking is active
2847
+ if source_body is not None and should_auto_enter_plan_mode(source_body, text_so_far, tool_calls):
2848
+ router_log("WARN", "auto-synthesized EnterPlanMode from short/empty upstream stream")
2849
+ tool_calls.append({"function": {"name": "EnterPlanMode", "arguments": {}}})
2850
+ tool_id = f"toolu_ollama_plan_{int(time.time() * 1000)}"
2851
+ tool_index = next_content_index
2852
+ next_content_index += 1
2853
+ tool_indices.append(tool_index)
2854
+ tool_event = {
2855
+ "type": "content_block_start",
2856
+ "index": tool_index,
2857
+ "content_block": {
2858
+ "type": "tool_use",
2859
+ "id": tool_id,
2860
+ "name": "EnterPlanMode",
2861
+ "input": {},
2862
+ },
2863
+ }
2864
+ handler.wfile.write(f"event: content_block_start\ndata: {json.dumps(tool_event, ensure_ascii=False)}\n\n".encode())
2865
+ handler.wfile.flush()
2866
+ delta_event = {
2867
+ "type": "content_block_delta",
2868
+ "index": tool_index,
2869
+ "delta": {"type": "input_json_delta", "partial_json": "{}"},
2870
+ }
2871
+ handler.wfile.write(f"event: content_block_delta\ndata: {json.dumps(delta_event, ensure_ascii=False)}\n\n".encode())
2872
+ handler.wfile.flush()
2873
+ elif text_suppressed_for_plan and text_so_far:
2874
+ text_started = True
2875
+ text_index = next_content_index
2876
+ next_content_index += 1
2877
+ event = {
2878
+ "type": "content_block_start",
2879
+ "index": text_index,
2880
+ "content_block": {"type": "text", "text": ""},
2881
+ }
2882
+ handler.wfile.write(f"event: content_block_start\ndata: {json.dumps(event, ensure_ascii=False)}\n\n".encode())
2883
+ handler.wfile.flush()
2884
+ event = {
2885
+ "type": "content_block_delta",
2886
+ "index": text_index,
2887
+ "delta": {"type": "text_delta", "text": text_so_far},
2888
+ }
2889
+ handler.wfile.write(f"event: content_block_delta\ndata: {json.dumps(event, ensure_ascii=False)}\n\n".encode())
2890
+ handler.wfile.flush()
2716
2891
  if word_chunking and text_started and text_buffer:
2717
2892
  to_flush, text_buffer = _split_word_buffer(text_buffer, force=True)
2718
2893
  if to_flush:
@@ -2817,7 +2992,7 @@ def forward_ollama_api_chat(handler: BaseHTTPRequestHandler, provider: str, pcfg
2817
2992
  # Check if Claude Code requested SSE streaming
2818
2993
  accept = handler.headers.get("accept", "")
2819
2994
  if "text/event-stream" in accept or stream_requested:
2820
- _ollama_stream_to_anthropic_sse(handler, resp, model, word_chunking=word_chunking, provider=provider)
2995
+ _ollama_stream_to_anthropic_sse(handler, resp, model, word_chunking=word_chunking, provider=provider, source_body=body)
2821
2996
  else:
2822
2997
  # Non-SSE client but streaming from Ollama: collect full response
2823
2998
  chunks = []
@@ -2838,7 +3013,7 @@ def forward_ollama_api_chat(handler: BaseHTTPRequestHandler, provider: str, pcfg
2838
3013
  continue
2839
3014
  if data is None:
2840
3015
  data = {"message": {"content": ""}, "done": True, "done_reason": "end_turn"}
2841
- write_json(handler, ollama_chat_to_anthropic(data, model))
3016
+ write_json(handler, ollama_chat_to_anthropic(data, model, source_body=body))
2842
3017
  return
2843
3018
  # Non-streaming fallback
2844
3019
  try:
@@ -2863,7 +3038,7 @@ def forward_ollama_api_chat(handler: BaseHTTPRequestHandler, provider: str, pcfg
2863
3038
  exc.code,
2864
3039
  )
2865
3040
  return
2866
- write_json(handler, ollama_chat_to_anthropic(data, model))
3041
+ write_json(handler, ollama_chat_to_anthropic(data, model, source_body=body))
2867
3042
 
2868
3043
 
2869
3044
  class RouterHandler(BaseHTTPRequestHandler):
@@ -2917,6 +3092,8 @@ class RouterHandler(BaseHTTPRequestHandler):
2917
3092
  write_json(self, {"type": "error", "error": {"type": "not_found_error", "message": path}}, 404)
2918
3093
  return
2919
3094
  dump_request_for_trace(provider, path, body)
3095
+ if maybe_handle_plan_mode_tool_choice(self, provider, body):
3096
+ return
2920
3097
  body = filter_blocked_tools(provider, pcfg, body)
2921
3098
  router_log("DEBUG", f"POST {path} provider={provider} model={body.get('model')} tools={len(body.get('tools') or [])} msgs={len(body.get('messages') or [])}")
2922
3099
  try:
package/docs/README.ja.md CHANGED
@@ -33,7 +33,7 @@ vLLM、NVIDIA hosted、self-hosted NIM を選択し、通常の Claude Code 引
33
33
 
34
34
  Credits: One Ciel LLC
35
35
 
36
- 現在のバージョン: `0.1.25`
36
+ 現在のバージョン: `0.1.27`
37
37
 
38
38
  ## 作られた理由
39
39
 
@@ -224,9 +224,14 @@ Windows/Linux 管理、クリーンアップスクリプト、定期的なセキ
224
224
 
225
225
  ## 変更履歴
226
226
 
227
+ ### 0.1.27
228
+
229
+ - **non-Anthropic provider の Plan mode 対応**: ルーターは `EnterPlanMode` を残し、上流モデルが Claude Code 内部の Plan tool を安定して選択できない場合でも Claude Code Plan mode へ移行できるようにします。`tool_choice=EnterPlanMode` が強制されたリクエストには、ルーターがローカルで有効な Anthropic `tool_use` を返します。長い実装リクエストに対して短い、または空の実行不能なテキストだけが返った場合は、言語に依存しない構造チェックで `EnterPlanMode` に昇格します。
230
+ - **Plan-mode self-tool 処理**: 未対応の Claude Code self-tool は non-Anthropic provider では引き続き除去しますが、Plan-mode tool は別扱いにして planning 機能を無効化しません。
231
+
227
232
  ### 0.1.25
228
233
 
229
- - **Plan mode guard と診断**: non-Anthropic provider へ転送する前に、Claude Code 内部 self-tool の `EnterPlanMode` などをルーターで除去します。`~/.config/claude-any/log-level` に `TRACE` を書くと、`requests.jsonl` / `responses.jsonl` にリクエスト/レスポンス要約を記録します。
234
+ - **Plan mode 診断**: `~/.config/claude-any/log-level` に `TRACE` を書くと、`requests.jsonl` / `responses.jsonl` にリクエスト/レスポンス要約を記録します。
230
235
  - **ヘッドレスエージェントチャット**: ルーターが `/ca/chat/messages`、`/ca/chat/wait`、`/ca/chat/stream` を提供します。サブ coding agent は最後に見た message id 以降の更新を取得したり、SSE で返信を待機できます。
231
236
  - **Plan artifact 配信**: `/ca/plan/artifacts` で plan ファイルを作成し、ローカル URL として共有できます。Anthropic の内部実装はコピーせず、ファイル/アーティファクト中心の流れだけを独立実装しています。
232
237
 
package/docs/README.ko.md CHANGED
@@ -33,7 +33,7 @@ NVIDIA hosted, self-hosted NIM을 선택하고, Claude Code의 일반 인자는
33
33
 
34
34
  Credits: One Ciel LLC
35
35
 
36
- 현재 버전: `0.1.25`
36
+ 현재 버전: `0.1.27`
37
37
 
38
38
  ## 왜 만들었나
39
39
 
@@ -224,9 +224,14 @@ Windows 이벤트 로그 리뷰, 바이러스/랜섬웨어 침입 시도 정리,
224
224
 
225
225
  ## 변경 이력
226
226
 
227
+ ### 0.1.27
228
+
229
+ - **non-Anthropic provider의 Plan mode 지원**: 라우터가 `EnterPlanMode`를 유지하고, 업스트림 모델이 Claude Code 내부 Plan tool을 안정적으로 선택하지 못해도 Claude Code Plan mode로 전환할 수 있게 처리합니다. `tool_choice=EnterPlanMode`가 강제된 요청은 라우터가 로컬에서 유효한 Anthropic `tool_use`로 응답하고, 긴 구현 요청에 대해 짧거나 빈 비실행 텍스트만 돌아오면 언어에 의존하지 않는 구조 검사로 `EnterPlanMode`로 승격합니다.
230
+ - **Plan-mode self-tool 처리**: 지원하지 않는 Claude Code self-tool은 non-Anthropic provider에서 계속 제거하지만, Plan-mode tool은 별도 처리하여 planning 기능을 비활성화하지 않습니다.
231
+
227
232
  ### 0.1.25
228
233
 
229
- - **Plan mode guard와 진단**: non-Anthropic provider로 보낼 때 Claude Code 내부 self-tool인 `EnterPlanMode` 등을 라우터에서 제거합니다. `~/.config/claude-any/log-level`에 `TRACE`를 쓰면 `requests.jsonl` / `responses.jsonl`에 요청/응답 요약이 남습니다.
234
+ - **Plan mode 진단**: `~/.config/claude-any/log-level`에 `TRACE`를 쓰면 `requests.jsonl` / `responses.jsonl`에 요청/응답 요약이 남습니다.
230
235
  - **헤드리스 에이전트 채팅**: 라우터가 `/ca/chat/messages`, `/ca/chat/wait`, `/ca/chat/stream`을 제공합니다. 서브 코딩 에이전트는 마지막 message id 이후의 업데이트를 받거나 SSE로 답변을 기다릴 수 있습니다.
231
236
  - **Plan artifact 서빙**: `/ca/plan/artifacts`로 plan 파일을 만들고 로컬 URL로 공유할 수 있습니다. Anthropic 내부 구현을 복제하지 않고 파일/아티팩트 중심 흐름만 독립 구현했습니다.
232
237
 
package/docs/README.zh.md CHANGED
@@ -33,7 +33,7 @@ NIM,并把普通 Claude Code 参数原样传递。
33
33
 
34
34
  Credits: One Ciel LLC
35
35
 
36
- 当前版本: `0.1.25`
36
+ 当前版本: `0.1.27`
37
37
 
38
38
  ## 为什么存在
39
39
 
@@ -212,9 +212,14 @@ Hermes 格式模型或部分较旧的 Qwen tool template。
212
212
 
213
213
  ## 更新日志
214
214
 
215
+ ### 0.1.27
216
+
217
+ - **支持 non-Anthropic provider 的 Plan mode**: 路由器会保留 `EnterPlanMode`,即使上游模型不能稳定选择 Claude Code 内部 Plan tool,也能让 Claude Code 进入 Plan mode。对于强制 `tool_choice=EnterPlanMode` 的请求,路由器会在本地返回有效的 Anthropic `tool_use`。当较长的实现请求只得到很短或空的、不可执行的文本响应时,路由器会通过不依赖语言的结构检查将其提升为 `EnterPlanMode`。
218
+ - **Plan-mode self-tool 处理**: 不支持的 Claude Code self-tool 在 non-Anthropic provider 下仍会被移除,但 Plan-mode tool 会单独处理,不再禁用 planning 能力。
219
+
215
220
  ### 0.1.25
216
221
 
217
- - **Plan mode guard 与诊断**: 转发到 non-Anthropic provider 前,路由器会移除 Claude Code 内部 self-tool,例如 `EnterPlanMode`。将 `TRACE` 写入 `~/.config/claude-any/log-level` 后,会在 `requests.jsonl` / `responses.jsonl` 中记录请求/响应摘要。
222
+ - **Plan mode 诊断**: `TRACE` 写入 `~/.config/claude-any/log-level` 后,会在 `requests.jsonl` / `responses.jsonl` 中记录请求/响应摘要。
218
223
  - **Headless agent chat**: 路由器提供 `/ca/chat/messages`、`/ca/chat/wait`、`/ca/chat/stream`。子 coding agent 可以按最后看到的 message id 拉取更新,也可以通过 SSE 等待回复。
219
224
  - **Plan artifact 服务**: 可通过 `/ca/plan/artifacts` 创建 plan 文件并以本地 URL 分享。这里没有复制 Anthropic 的内部实现,只独立实现了文件/artifact 型工作流。
220
225
 
package/docs/manual.md CHANGED
@@ -6,7 +6,7 @@ Code starts, while passing normal Claude Code arguments through unchanged.
6
6
 
7
7
  Credits: One Ciel LLC
8
8
 
9
- Current version: `0.1.25`
9
+ Current version: `0.1.27`
10
10
 
11
11
  ## Install
12
12
 
@@ -381,13 +381,18 @@ Notes for automation:
381
381
 
382
382
  ## Router Chat and Plan Artifacts
383
383
 
384
- Claude Code Plan mode uses internal Claude Code tools and UI state. With
385
- non-Anthropic providers those self-tools can be selected by the upstream model
386
- and leave the CLI stuck in planning instead of continuing work. Claude Any
387
- therefore removes known Claude Code self-tools, including `EnterPlanMode`,
388
- before forwarding requests to non-Anthropic providers. For troubleshooting,
389
- write `TRACE` to `~/.config/claude-any/log-level`; the router then records
390
- redacted request and response summaries in:
384
+ Claude Code Plan mode uses internal Claude Code tools and UI state. Claude Any
385
+ keeps `EnterPlanMode` available for non-Anthropic providers and handles the
386
+ Plan-mode transition in the router when the upstream model does not reliably
387
+ select that internal tool. If Claude Code forces `tool_choice=EnterPlanMode`,
388
+ the router returns a valid Anthropic `tool_use` locally. If a long
389
+ implementation request receives only a short or empty non-actionable text
390
+ response, the router promotes that response to `EnterPlanMode` using
391
+ language-agnostic structure checks. Other unsupported Claude Code self-tools
392
+ are still removed before forwarding requests to non-Anthropic providers.
393
+
394
+ For troubleshooting, write `TRACE` to `~/.config/claude-any/log-level`; the
395
+ router then records redacted request and response summaries in:
391
396
 
392
397
  - `~/.config/claude-any/requests.jsonl`
393
398
  - `~/.config/claude-any/responses.jsonl`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/claude-any",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Claude Code provider selector for Anthropic, Ollama, Ollama Cloud, vLLM, NVIDIA hosted, and self-hosted NIM.",
5
5
  "license": "MIT",
6
6
  "author": "One Ciel LLC",