@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 +17 -5
- package/claude_any.py +188 -11
- package/docs/README.ja.md +7 -2
- package/docs/README.ko.md +7 -2
- package/docs/README.zh.md +7 -2
- package/docs/manual.md +13 -8
- package/package.json +1 -1
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.
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
385
|
-
non-Anthropic providers
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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