@oneciel-ai/claude-any 0.1.36 → 0.1.37
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 +9 -1
- package/claude_any.py +117 -14
- package/docs/README.ja.md +9 -1
- package/docs/README.ko.md +9 -1
- package/docs/README.zh.md +9 -1
- package/docs/manual.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ arguments through unchanged.
|
|
|
48
48
|
|
|
49
49
|
Credits: One Ciel LLC
|
|
50
50
|
|
|
51
|
-
Current version: `0.1.
|
|
51
|
+
Current version: `0.1.37`
|
|
52
52
|
|
|
53
53
|
## Why This Exists
|
|
54
54
|
|
|
@@ -381,6 +381,14 @@ steps under that larger model's supervision.
|
|
|
381
381
|
|
|
382
382
|
## Changelog
|
|
383
383
|
|
|
384
|
+
### 0.1.37
|
|
385
|
+
|
|
386
|
+
- **Pseudo tool-call recovery**: the NVIDIA/OpenAI-compatible stream path now
|
|
387
|
+
suppresses `<|tool_calls_section_begin|>...` pseudo tool-call text and
|
|
388
|
+
converts it back into Claude `tool_use` blocks when possible.
|
|
389
|
+
- **Streaming defaults**: provider streaming defaults to on; NVIDIA hosted
|
|
390
|
+
remains forced to the streaming upstream path for stability.
|
|
391
|
+
|
|
384
392
|
### 0.1.36
|
|
385
393
|
|
|
386
394
|
- **NVIDIA upstream streaming**: NVIDIA hosted router calls now use upstream
|
package/claude_any.py
CHANGED
|
@@ -85,7 +85,7 @@ PROVIDER_LABELS = {
|
|
|
85
85
|
"self-hosted-nim": "Self Hosted NIM",
|
|
86
86
|
}
|
|
87
87
|
APP_NAME = "Claude Any"
|
|
88
|
-
VERSION = "0.1.
|
|
88
|
+
VERSION = "0.1.37"
|
|
89
89
|
CREDITS = "Credits: One Ciel LLC"
|
|
90
90
|
|
|
91
91
|
LOG_LEVELS = {"SILENT": 0, "ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4, "TRACE": 5}
|
|
@@ -790,6 +790,13 @@ def apply_config_migrations(cfg: dict[str, Any]) -> None:
|
|
|
790
790
|
if isinstance(pcfg, dict) and not positive_int(pcfg.get("context_window")):
|
|
791
791
|
pcfg["context_window"] = 32768
|
|
792
792
|
migrations[marker] = True
|
|
793
|
+
|
|
794
|
+
marker = "stream_enabled_default_true_20260513"
|
|
795
|
+
if not migrations.get(marker):
|
|
796
|
+
for pcfg in (cfg.get("providers") or {}).values():
|
|
797
|
+
if isinstance(pcfg, dict) and "stream_enabled" not in pcfg:
|
|
798
|
+
pcfg["stream_enabled"] = True
|
|
799
|
+
migrations[marker] = True
|
|
793
800
|
|
|
794
801
|
|
|
795
802
|
_config_cache: dict[str, Any] | None = None
|
|
@@ -3988,7 +3995,7 @@ def maybe_handle_advisor_request(handler: BaseHTTPRequestHandler, provider: str,
|
|
|
3988
3995
|
return True
|
|
3989
3996
|
|
|
3990
3997
|
|
|
3991
|
-
def normalize_tool_arguments(tool_name: str, args: Any) -> dict[str, Any]:
|
|
3998
|
+
def normalize_tool_arguments(tool_name: str, args: Any) -> dict[str, Any]:
|
|
3992
3999
|
if isinstance(args, dict):
|
|
3993
4000
|
return args
|
|
3994
4001
|
if isinstance(args, str):
|
|
@@ -4003,17 +4010,86 @@ def normalize_tool_arguments(tool_name: str, args: Any) -> dict[str, Any]:
|
|
|
4003
4010
|
pass
|
|
4004
4011
|
if tool_name == "Bash":
|
|
4005
4012
|
return {"command": text}
|
|
4006
|
-
return {}
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4013
|
+
return {}
|
|
4014
|
+
|
|
4015
|
+
|
|
4016
|
+
PSEUDO_TOOL_START = "<|tool_calls_section_begin|>"
|
|
4017
|
+
PSEUDO_TOOL_END = "<|tool_calls_section_end|>"
|
|
4018
|
+
PSEUDO_CALL_BEGIN = "<|tool_call_begin|>"
|
|
4019
|
+
PSEUDO_ARG_BEGIN = "<|tool_call_argument_begin|>"
|
|
4020
|
+
PSEUDO_CALL_END = "<|tool_call_end|>"
|
|
4021
|
+
|
|
4022
|
+
|
|
4023
|
+
def infer_tool_name_from_args(args: dict[str, Any]) -> str:
|
|
4024
|
+
keys = set(args)
|
|
4025
|
+
if "command" in keys:
|
|
4026
|
+
return "Bash"
|
|
4027
|
+
if {"file_path", "content"}.issubset(keys):
|
|
4028
|
+
return "Write"
|
|
4029
|
+
if {"file_path", "old_string", "new_string"}.issubset(keys):
|
|
4030
|
+
return "Edit"
|
|
4031
|
+
if "file_path" in keys:
|
|
4032
|
+
return "Read"
|
|
4033
|
+
if "taskId" in keys and "status" in keys:
|
|
4034
|
+
return "TaskUpdate"
|
|
4035
|
+
return "TaskList" if not args else "Write"
|
|
4036
|
+
|
|
4037
|
+
|
|
4038
|
+
def parse_pseudo_tool_calls(text: str) -> tuple[str, list[dict[str, Any]]]:
|
|
4039
|
+
if PSEUDO_TOOL_START not in text:
|
|
4040
|
+
return text, []
|
|
4041
|
+
visible_parts: list[str] = []
|
|
4042
|
+
calls: list[dict[str, Any]] = []
|
|
4043
|
+
pos = 0
|
|
4044
|
+
while True:
|
|
4045
|
+
start = text.find(PSEUDO_TOOL_START, pos)
|
|
4046
|
+
if start < 0:
|
|
4047
|
+
visible_parts.append(text[pos:])
|
|
4048
|
+
break
|
|
4049
|
+
visible_parts.append(text[pos:start])
|
|
4050
|
+
end = text.find(PSEUDO_TOOL_END, start)
|
|
4051
|
+
if end < 0:
|
|
4052
|
+
section = text[start + len(PSEUDO_TOOL_START):]
|
|
4053
|
+
pos = len(text)
|
|
4054
|
+
else:
|
|
4055
|
+
section = text[start + len(PSEUDO_TOOL_START):end]
|
|
4056
|
+
pos = end + len(PSEUDO_TOOL_END)
|
|
4057
|
+
for match in re.finditer(
|
|
4058
|
+
re.escape(PSEUDO_CALL_BEGIN) + r"(.*?)" + re.escape(PSEUDO_ARG_BEGIN) + r"(.*?)" + re.escape(PSEUDO_CALL_END),
|
|
4059
|
+
section,
|
|
4060
|
+
flags=re.DOTALL,
|
|
4061
|
+
):
|
|
4062
|
+
raw_header = match.group(1).strip()
|
|
4063
|
+
raw_args = match.group(2).strip()
|
|
4064
|
+
try:
|
|
4065
|
+
args = json.loads(raw_args)
|
|
4066
|
+
except Exception:
|
|
4067
|
+
continue
|
|
4068
|
+
if not isinstance(args, dict):
|
|
4069
|
+
continue
|
|
4070
|
+
name = ""
|
|
4071
|
+
for part in re.split(r"[\s:|,]+", raw_header):
|
|
4072
|
+
candidate = _fuzzy_match_tool_name(part)
|
|
4073
|
+
if candidate:
|
|
4074
|
+
name = candidate
|
|
4075
|
+
break
|
|
4076
|
+
if not name:
|
|
4077
|
+
name = infer_tool_name_from_args(args)
|
|
4078
|
+
calls.append({"function": {"name": name, "arguments": args}, "id": raw_header})
|
|
4079
|
+
if end < 0:
|
|
4080
|
+
break
|
|
4081
|
+
return "".join(visible_parts), calls
|
|
4082
|
+
|
|
4083
|
+
|
|
4084
|
+
def ollama_chat_to_anthropic(data: dict[str, Any], model: str, source_body: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
4085
|
+
message = data.get("message") if isinstance(data.get("message"), dict) else {}
|
|
4086
|
+
content: list[dict[str, Any]] = []
|
|
4087
|
+
text = message.get("content") or ""
|
|
4088
|
+
text, pseudo_tool_calls = parse_pseudo_tool_calls(text)
|
|
4089
|
+
if text:
|
|
4090
|
+
content.append({"type": "text", "text": text})
|
|
4091
|
+
tool_id_prefix = f"toolu_ollama_{int(time.time() * 1000)}_{os.getpid()}"
|
|
4092
|
+
for i, call in enumerate(list(message.get("tool_calls") or []) + pseudo_tool_calls):
|
|
4017
4093
|
fn = call.get("function") if isinstance(call, dict) else {}
|
|
4018
4094
|
if not isinstance(fn, dict) or not fn.get("name"):
|
|
4019
4095
|
continue
|
|
@@ -4609,6 +4685,8 @@ def stream_openai_chat_to_anthropic_sse(
|
|
|
4609
4685
|
text_suppressed_for_plan = False
|
|
4610
4686
|
text_index: int | None = None
|
|
4611
4687
|
text_so_far = ""
|
|
4688
|
+
pseudo_text = ""
|
|
4689
|
+
pseudo_mode = False
|
|
4612
4690
|
text_buffer = ""
|
|
4613
4691
|
tool_fragments: dict[int, dict[str, Any]] = {}
|
|
4614
4692
|
output_tokens = 0
|
|
@@ -4667,6 +4745,21 @@ def stream_openai_chat_to_anthropic_sse(
|
|
|
4667
4745
|
delta = choice.get("delta") if isinstance(choice.get("delta"), dict) else {}
|
|
4668
4746
|
text_chunk = delta.get("content") or ""
|
|
4669
4747
|
if text_chunk:
|
|
4748
|
+
if pseudo_mode or PSEUDO_TOOL_START in text_chunk:
|
|
4749
|
+
before, sep, after = text_chunk.partition(PSEUDO_TOOL_START)
|
|
4750
|
+
if before and not pseudo_mode:
|
|
4751
|
+
text_so_far += before
|
|
4752
|
+
if word_chunking:
|
|
4753
|
+
text_buffer += before
|
|
4754
|
+
to_flush, text_buffer = _split_word_buffer(text_buffer, force=False)
|
|
4755
|
+
emit_text_delta(to_flush)
|
|
4756
|
+
else:
|
|
4757
|
+
emit_text_delta(before)
|
|
4758
|
+
pseudo_mode = True
|
|
4759
|
+
pseudo_text += (sep + after) if sep else text_chunk
|
|
4760
|
+
if PSEUDO_TOOL_END in pseudo_text:
|
|
4761
|
+
pseudo_mode = False
|
|
4762
|
+
continue
|
|
4670
4763
|
if source_body is not None and not text_started and not tool_fragments and should_auto_enter_plan_mode(source_body, text_so_far + text_chunk, []):
|
|
4671
4764
|
text_so_far += text_chunk
|
|
4672
4765
|
text_suppressed_for_plan = True
|
|
@@ -4698,6 +4791,15 @@ def stream_openai_chat_to_anthropic_sse(
|
|
|
4698
4791
|
emit_text_delta(to_flush)
|
|
4699
4792
|
|
|
4700
4793
|
tool_calls: list[dict[str, Any]] = []
|
|
4794
|
+
_, pseudo_tool_calls = parse_pseudo_tool_calls(pseudo_text)
|
|
4795
|
+
for i, pseudo in enumerate(pseudo_tool_calls):
|
|
4796
|
+
fn = pseudo.get("function") if isinstance(pseudo, dict) else {}
|
|
4797
|
+
if isinstance(fn, dict):
|
|
4798
|
+
tool_fragments.setdefault(100000 + i, {
|
|
4799
|
+
"id": str(pseudo.get("id") or ""),
|
|
4800
|
+
"name": str(fn.get("name") or ""),
|
|
4801
|
+
"arguments": json.dumps(fn.get("arguments") or {}, ensure_ascii=False),
|
|
4802
|
+
})
|
|
4701
4803
|
for _, fragment in sorted(tool_fragments.items()):
|
|
4702
4804
|
raw_name = str(fragment.get("name") or "")
|
|
4703
4805
|
if not raw_name:
|
|
@@ -4964,7 +5066,8 @@ def forward_openai_compatible_chat(handler: BaseHTTPRequestHandler, provider: st
|
|
|
4964
5066
|
model = ncp_model_id_for_nvidia_hosted(model)
|
|
4965
5067
|
url = join_url(provider_upstream_request_base(provider, pcfg), "/chat/completions")
|
|
4966
5068
|
waited, rpm_used, rpm_limit = apply_router_rate_limit(provider, pcfg, model)
|
|
4967
|
-
|
|
5069
|
+
stream_enabled = bool(pcfg.get("stream_enabled", True))
|
|
5070
|
+
stream = True if provider == "nvidia-hosted" else bool(body.get("stream", stream_enabled)) and stream_enabled
|
|
4968
5071
|
notice = rate_limit_notice(waited, rpm_used, rpm_limit, bool(pcfg.get("rate_limit_status", True)))
|
|
4969
5072
|
if stream:
|
|
4970
5073
|
req_body = openai_compatible_chat_request(provider, model, body, pcfg, stream=True)
|
package/docs/README.ja.md
CHANGED
|
@@ -47,7 +47,7 @@ vLLM、NVIDIA hosted、self-hosted NIM を選択し、通常の Claude Code 引
|
|
|
47
47
|
|
|
48
48
|
Credits: One Ciel LLC
|
|
49
49
|
|
|
50
|
-
現在のバージョン: `0.1.
|
|
50
|
+
現在のバージョン: `0.1.37`
|
|
51
51
|
|
|
52
52
|
## 作られた理由
|
|
53
53
|
|
|
@@ -351,6 +351,14 @@ Windows/Linux 管理、クリーンアップスクリプト、定期的なセキ
|
|
|
351
351
|
|
|
352
352
|
## 変更履歴
|
|
353
353
|
|
|
354
|
+
### 0.1.37
|
|
355
|
+
|
|
356
|
+
- **Pseudo tool-call recovery**: NVIDIA/OpenAI-compatible stream 経路で
|
|
357
|
+
`<|tool_calls_section_begin|>...` pseudo tool-call テキストを画面に出さず、
|
|
358
|
+
可能な場合は Claude `tool_use` ブロックへ復元します。
|
|
359
|
+
- **Streaming defaults**: provider streaming の既定値は on です。NVIDIA hosted
|
|
360
|
+
は安定性のため upstream streaming 経路に固定されます。
|
|
361
|
+
|
|
354
362
|
### 0.1.36
|
|
355
363
|
|
|
356
364
|
- **NVIDIA upstream streaming**: NVIDIA hosted router 呼び出しは upstream にも
|
package/docs/README.ko.md
CHANGED
|
@@ -47,7 +47,7 @@ NVIDIA hosted, self-hosted NIM을 선택하고, Claude Code의 일반 인자는
|
|
|
47
47
|
|
|
48
48
|
Credits: One Ciel LLC
|
|
49
49
|
|
|
50
|
-
현재 버전: `0.1.
|
|
50
|
+
현재 버전: `0.1.37`
|
|
51
51
|
|
|
52
52
|
## 왜 만들었나
|
|
53
53
|
|
|
@@ -351,6 +351,14 @@ Windows 이벤트 로그 리뷰, 바이러스/랜섬웨어 침입 시도 정리,
|
|
|
351
351
|
|
|
352
352
|
## 변경 이력
|
|
353
353
|
|
|
354
|
+
### 0.1.37
|
|
355
|
+
|
|
356
|
+
- **Pseudo tool-call recovery**: NVIDIA/OpenAI-compatible stream 경로에서
|
|
357
|
+
`<|tool_calls_section_begin|>...` pseudo tool-call 텍스트를 화면에 출력하지
|
|
358
|
+
않고 가능한 경우 Claude `tool_use` 블록으로 복구합니다.
|
|
359
|
+
- **Streaming defaults**: provider streaming 기본값은 on이며, NVIDIA hosted는
|
|
360
|
+
안정성을 위해 upstream streaming 경로로 고정됩니다.
|
|
361
|
+
|
|
354
362
|
### 0.1.36
|
|
355
363
|
|
|
356
364
|
- **NVIDIA upstream streaming**: NVIDIA hosted router 호출은 이제 upstream에도
|
package/docs/README.zh.md
CHANGED
|
@@ -47,7 +47,7 @@ NIM,并把普通 Claude Code 参数原样传递。
|
|
|
47
47
|
|
|
48
48
|
Credits: One Ciel LLC
|
|
49
49
|
|
|
50
|
-
当前版本: `0.1.
|
|
50
|
+
当前版本: `0.1.37`
|
|
51
51
|
|
|
52
52
|
## 为什么存在
|
|
53
53
|
|
|
@@ -337,6 +337,14 @@ Hermes 格式模型或部分较旧的 Qwen tool template。
|
|
|
337
337
|
|
|
338
338
|
## 更新日志
|
|
339
339
|
|
|
340
|
+
### 0.1.37
|
|
341
|
+
|
|
342
|
+
- **Pseudo tool-call recovery**:NVIDIA/OpenAI-compatible stream 路径现在会
|
|
343
|
+
隐藏 `<|tool_calls_section_begin|>...` pseudo tool-call 文本,并尽可能恢复为
|
|
344
|
+
Claude `tool_use` block。
|
|
345
|
+
- **Streaming defaults**:provider streaming 默认开启;NVIDIA hosted 为了稳定性
|
|
346
|
+
固定使用 upstream streaming 路径。
|
|
347
|
+
|
|
340
348
|
### 0.1.36
|
|
341
349
|
|
|
342
350
|
- **NVIDIA upstream streaming**:NVIDIA hosted router 调用现在也会向 upstream
|
package/docs/manual.md
CHANGED
package/package.json
CHANGED