klaude-code 1.2.6__py3-none-any.whl → 1.8.0__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.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -1,19 +1,69 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from collections.abc import AsyncGenerator
|
|
2
|
-
from typing import
|
|
3
|
+
from typing import Any, override
|
|
3
4
|
|
|
4
5
|
import httpx
|
|
5
6
|
import openai
|
|
7
|
+
from openai.types.chat.completion_create_params import CompletionCreateParamsStreaming
|
|
6
8
|
|
|
7
|
-
from klaude_code.llm.client import LLMClientABC
|
|
9
|
+
from klaude_code.llm.client import LLMClientABC
|
|
8
10
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
9
11
|
from klaude_code.llm.openai_compatible.input import convert_tool_schema
|
|
10
|
-
from klaude_code.llm.openai_compatible.
|
|
12
|
+
from klaude_code.llm.openai_compatible.stream import parse_chat_completions_stream
|
|
11
13
|
from klaude_code.llm.openrouter.input import convert_history_to_input, is_claude_model
|
|
12
|
-
from klaude_code.llm.openrouter.
|
|
14
|
+
from klaude_code.llm.openrouter.reasoning import ReasoningStreamHandler
|
|
13
15
|
from klaude_code.llm.registry import register
|
|
14
|
-
from klaude_code.llm.usage import MetadataTracker
|
|
16
|
+
from klaude_code.llm.usage import MetadataTracker
|
|
15
17
|
from klaude_code.protocol import llm_param, model
|
|
16
|
-
from klaude_code.trace import DebugType,
|
|
18
|
+
from klaude_code.trace import DebugType, is_debug_enabled, log_debug
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_payload(
|
|
22
|
+
param: llm_param.LLMCallParameter,
|
|
23
|
+
) -> tuple[CompletionCreateParamsStreaming, dict[str, object], dict[str, str]]:
|
|
24
|
+
"""Build OpenRouter API request parameters."""
|
|
25
|
+
messages = convert_history_to_input(param.input, param.system, param.model)
|
|
26
|
+
tools = convert_tool_schema(param.tools)
|
|
27
|
+
|
|
28
|
+
extra_body: dict[str, object] = {
|
|
29
|
+
"usage": {"include": True}, # To get the cache tokens at the end of the response
|
|
30
|
+
}
|
|
31
|
+
if is_debug_enabled():
|
|
32
|
+
extra_body["debug"] = {
|
|
33
|
+
"echo_upstream_body": True
|
|
34
|
+
} # https://openrouter.ai/docs/api/reference/errors-and-debugging#debug-option-shape
|
|
35
|
+
extra_headers: dict[str, str] = {}
|
|
36
|
+
|
|
37
|
+
if param.thinking:
|
|
38
|
+
if param.thinking.type != "disabled" and param.thinking.budget_tokens is not None:
|
|
39
|
+
extra_body["reasoning"] = {
|
|
40
|
+
"max_tokens": param.thinking.budget_tokens,
|
|
41
|
+
"enable": True,
|
|
42
|
+
} # OpenRouter: https://openrouter.ai/docs/use-cases/reasoning-tokens#anthropic-models-with-reasoning-tokens
|
|
43
|
+
elif param.thinking.reasoning_effort is not None:
|
|
44
|
+
extra_body["reasoning"] = {
|
|
45
|
+
"effort": param.thinking.reasoning_effort,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if param.provider_routing:
|
|
49
|
+
extra_body["provider"] = param.provider_routing.model_dump(exclude_none=True)
|
|
50
|
+
|
|
51
|
+
if is_claude_model(param.model):
|
|
52
|
+
extra_headers["x-anthropic-beta"] = "fine-grained-tool-streaming-2025-05-14,interleaved-thinking-2025-05-14"
|
|
53
|
+
|
|
54
|
+
payload: CompletionCreateParamsStreaming = {
|
|
55
|
+
"model": str(param.model),
|
|
56
|
+
"tool_choice": "auto",
|
|
57
|
+
"parallel_tool_calls": True,
|
|
58
|
+
"stream": True,
|
|
59
|
+
"messages": messages,
|
|
60
|
+
"temperature": param.temperature,
|
|
61
|
+
"max_tokens": param.max_tokens,
|
|
62
|
+
"tools": tools,
|
|
63
|
+
"verbosity": param.verbosity,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return payload, extra_body, extra_headers
|
|
17
67
|
|
|
18
68
|
|
|
19
69
|
@register(llm_param.LLMClientProtocol.OPENROUTER)
|
|
@@ -33,168 +83,47 @@ class OpenRouterClient(LLMClientABC):
|
|
|
33
83
|
return cls(config)
|
|
34
84
|
|
|
35
85
|
@override
|
|
36
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem
|
|
86
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
|
|
37
87
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
38
|
-
messages = convert_history_to_input(param.input, param.system, param.model)
|
|
39
|
-
tools = convert_tool_schema(param.tools)
|
|
40
|
-
|
|
41
|
-
metadata_tracker = MetadataTracker(cost_config=self._config.cost)
|
|
42
|
-
|
|
43
|
-
extra_body: dict[str, object] = {
|
|
44
|
-
"usage": {"include": True} # To get the cache tokens at the end of the response
|
|
45
|
-
}
|
|
46
|
-
extra_headers = {}
|
|
47
|
-
|
|
48
|
-
if param.thinking:
|
|
49
|
-
if param.thinking.budget_tokens is not None:
|
|
50
|
-
extra_body["reasoning"] = {
|
|
51
|
-
"max_tokens": param.thinking.budget_tokens,
|
|
52
|
-
"enable": True,
|
|
53
|
-
} # OpenRouter: https://openrouter.ai/docs/use-cases/reasoning-tokens#anthropic-models-with-reasoning-tokens
|
|
54
|
-
elif param.thinking.reasoning_effort is not None:
|
|
55
|
-
extra_body["reasoning"] = {
|
|
56
|
-
"effort": param.thinking.reasoning_effort,
|
|
57
|
-
}
|
|
58
|
-
if param.provider_routing:
|
|
59
|
-
extra_body["provider"] = param.provider_routing.model_dump(exclude_none=True)
|
|
60
|
-
if is_claude_model(param.model):
|
|
61
|
-
extra_headers["anthropic-beta"] = (
|
|
62
|
-
"interleaved-thinking-2025-05-14" # Not working yet, maybe OpenRouter's issue, or Anthropic: Interleaved thinking is only supported for tools used via the Messages API.
|
|
63
|
-
)
|
|
64
88
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
max_tokens=param.max_tokens,
|
|
74
|
-
tools=tools,
|
|
75
|
-
verbosity=param.verbosity,
|
|
76
|
-
extra_body=extra_body, # pyright: ignore[reportUnknownArgumentType]
|
|
77
|
-
extra_headers=extra_headers, # pyright: ignore[reportUnknownArgumentType]
|
|
89
|
+
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
90
|
+
|
|
91
|
+
payload, extra_body, extra_headers = build_payload(param)
|
|
92
|
+
|
|
93
|
+
log_debug(
|
|
94
|
+
json.dumps({**payload, **extra_body}, ensure_ascii=False, default=str),
|
|
95
|
+
style="yellow",
|
|
96
|
+
debug_type=DebugType.LLM_PAYLOAD,
|
|
78
97
|
)
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
try:
|
|
100
|
+
stream = await self.client.chat.completions.create(
|
|
101
|
+
**payload,
|
|
102
|
+
extra_body=extra_body,
|
|
103
|
+
extra_headers=extra_headers,
|
|
104
|
+
)
|
|
105
|
+
except (openai.OpenAIError, httpx.HTTPError) as e:
|
|
106
|
+
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
|
|
107
|
+
yield metadata_tracker.finalize()
|
|
108
|
+
return
|
|
109
|
+
|
|
85
110
|
reasoning_handler = ReasoningStreamHandler(
|
|
86
111
|
param_model=str(param.model),
|
|
87
|
-
response_id=
|
|
112
|
+
response_id=None,
|
|
88
113
|
)
|
|
89
114
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if len(accumulated_content) == 0:
|
|
96
|
-
return []
|
|
97
|
-
item = model.AssistantMessageItem(
|
|
98
|
-
content="".join(accumulated_content),
|
|
99
|
-
response_id=response_id,
|
|
115
|
+
def on_event(event: Any) -> None:
|
|
116
|
+
log_debug(
|
|
117
|
+
event.model_dump_json(exclude_none=True),
|
|
118
|
+
style="blue",
|
|
119
|
+
debug_type=DebugType.LLM_STREAM,
|
|
100
120
|
)
|
|
101
|
-
accumulated_content = []
|
|
102
|
-
return [item]
|
|
103
|
-
|
|
104
|
-
def flush_tool_call_items() -> list[model.ToolCallItem]:
|
|
105
|
-
nonlocal accumulated_tool_calls
|
|
106
|
-
items: list[model.ToolCallItem] = accumulated_tool_calls.get()
|
|
107
|
-
if items:
|
|
108
|
-
accumulated_tool_calls.chunks_by_step = [] # pyright: ignore[reportAttributeAccessIssue]
|
|
109
|
-
return items
|
|
110
121
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if not response_id and event.id:
|
|
119
|
-
response_id = event.id
|
|
120
|
-
reasoning_handler.set_response_id(response_id)
|
|
121
|
-
accumulated_tool_calls.response_id = response_id
|
|
122
|
-
yield model.StartItem(response_id=response_id)
|
|
123
|
-
if (
|
|
124
|
-
event.usage is not None and event.usage.completion_tokens is not None # pyright: ignore[reportUnnecessaryComparison]
|
|
125
|
-
): # gcp gemini will return None usage field
|
|
126
|
-
metadata_tracker.set_usage(convert_usage(event.usage, param.context_limit))
|
|
127
|
-
if event.model:
|
|
128
|
-
metadata_tracker.set_model_name(event.model)
|
|
129
|
-
if provider := getattr(event, "provider", None):
|
|
130
|
-
metadata_tracker.set_provider(str(provider))
|
|
131
|
-
|
|
132
|
-
if len(event.choices) == 0:
|
|
133
|
-
continue
|
|
134
|
-
delta = event.choices[0].delta
|
|
135
|
-
|
|
136
|
-
# Reasoning
|
|
137
|
-
if hasattr(delta, "reasoning_details") and getattr(delta, "reasoning_details"):
|
|
138
|
-
reasoning_details = getattr(delta, "reasoning_details")
|
|
139
|
-
for item in reasoning_details:
|
|
140
|
-
try:
|
|
141
|
-
reasoning_detail = ReasoningDetail.model_validate(item)
|
|
142
|
-
metadata_tracker.record_token()
|
|
143
|
-
stage = "reasoning"
|
|
144
|
-
for conversation_item in reasoning_handler.on_detail(reasoning_detail):
|
|
145
|
-
yield conversation_item
|
|
146
|
-
except Exception as e:
|
|
147
|
-
log("reasoning_details error", str(e), style="red")
|
|
148
|
-
|
|
149
|
-
# Assistant
|
|
150
|
-
if delta.content and (
|
|
151
|
-
stage == "assistant" or delta.content.strip()
|
|
152
|
-
): # Process all content in assistant stage, filter empty content in reasoning stage
|
|
153
|
-
metadata_tracker.record_token()
|
|
154
|
-
if stage == "reasoning":
|
|
155
|
-
for item in flush_reasoning_items():
|
|
156
|
-
yield item
|
|
157
|
-
stage = "assistant"
|
|
158
|
-
accumulated_content.append(delta.content)
|
|
159
|
-
yield model.AssistantMessageDelta(
|
|
160
|
-
content=delta.content,
|
|
161
|
-
response_id=response_id,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Tool
|
|
165
|
-
if delta.tool_calls and len(delta.tool_calls) > 0:
|
|
166
|
-
metadata_tracker.record_token()
|
|
167
|
-
if stage == "reasoning":
|
|
168
|
-
for item in flush_reasoning_items():
|
|
169
|
-
yield item
|
|
170
|
-
elif stage == "assistant":
|
|
171
|
-
for item in flush_assistant_items():
|
|
172
|
-
yield item
|
|
173
|
-
stage = "tool"
|
|
174
|
-
# Emit ToolCallStartItem for new tool calls
|
|
175
|
-
for tc in delta.tool_calls:
|
|
176
|
-
if tc.index not in emitted_tool_start_indices and tc.function and tc.function.name:
|
|
177
|
-
emitted_tool_start_indices.add(tc.index)
|
|
178
|
-
yield model.ToolCallStartItem(
|
|
179
|
-
response_id=response_id,
|
|
180
|
-
call_id=tc.id or "",
|
|
181
|
-
name=tc.function.name,
|
|
182
|
-
)
|
|
183
|
-
accumulated_tool_calls.add(delta.tool_calls)
|
|
184
|
-
|
|
185
|
-
except (openai.OpenAIError, httpx.HTTPError) as e:
|
|
186
|
-
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {str(e)}")
|
|
187
|
-
|
|
188
|
-
# Finalize
|
|
189
|
-
for item in flush_reasoning_items():
|
|
122
|
+
async for item in parse_chat_completions_stream(
|
|
123
|
+
stream,
|
|
124
|
+
param=param,
|
|
125
|
+
metadata_tracker=metadata_tracker,
|
|
126
|
+
reasoning_handler=reasoning_handler,
|
|
127
|
+
on_event=on_event,
|
|
128
|
+
):
|
|
190
129
|
yield item
|
|
191
|
-
|
|
192
|
-
for item in flush_assistant_items():
|
|
193
|
-
yield item
|
|
194
|
-
|
|
195
|
-
if stage == "tool":
|
|
196
|
-
for tool_call_item in flush_tool_call_items():
|
|
197
|
-
yield tool_call_item
|
|
198
|
-
|
|
199
|
-
metadata_tracker.set_response_id(response_id)
|
|
200
|
-
yield metadata_tracker.finalize()
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
# pyright: reportGeneralTypeIssues=false
|
|
8
8
|
|
|
9
9
|
from openai.types import chat
|
|
10
|
-
from openai.types.chat import ChatCompletionContentPartParam
|
|
11
10
|
|
|
12
|
-
from klaude_code.llm.input_common import AssistantGroup, ToolGroup, UserGroup,
|
|
11
|
+
from klaude_code.llm.input_common import AssistantGroup, ToolGroup, UserGroup, parse_message_groups
|
|
12
|
+
from klaude_code.llm.openai_compatible.input import tool_group_to_openai_message, user_group_to_openai_message
|
|
13
13
|
from klaude_code.protocol import model
|
|
14
14
|
|
|
15
15
|
|
|
@@ -25,35 +25,9 @@ def is_gemini_model(model_name: str | None) -> bool:
|
|
|
25
25
|
return model_name is not None and model_name.startswith("google/gemini")
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def _user_group_to_message(group: UserGroup) -> chat.ChatCompletionMessageParam:
|
|
29
|
-
parts: list[ChatCompletionContentPartParam] = []
|
|
30
|
-
for text in group.text_parts:
|
|
31
|
-
parts.append({"type": "text", "text": text + "\n"})
|
|
32
|
-
for image in group.images:
|
|
33
|
-
parts.append({"type": "image_url", "image_url": {"url": image.image_url.url}})
|
|
34
|
-
if not parts:
|
|
35
|
-
parts.append({"type": "text", "text": ""})
|
|
36
|
-
return {"role": "user", "content": parts}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _tool_group_to_message(group: ToolGroup) -> chat.ChatCompletionMessageParam:
|
|
40
|
-
merged_text = merge_reminder_text(
|
|
41
|
-
group.tool_result.output or "<system-reminder>Tool ran without output or errors</system-reminder>",
|
|
42
|
-
group.reminder_texts,
|
|
43
|
-
)
|
|
44
|
-
return {
|
|
45
|
-
"role": "tool",
|
|
46
|
-
"content": [{"type": "text", "text": merged_text}],
|
|
47
|
-
"tool_call_id": group.tool_result.call_id,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
28
|
def _assistant_group_to_message(group: AssistantGroup, model_name: str | None) -> chat.ChatCompletionMessageParam:
|
|
52
29
|
assistant_message: dict[str, object] = {"role": "assistant"}
|
|
53
30
|
|
|
54
|
-
if group.text_content:
|
|
55
|
-
assistant_message["content"] = group.text_content
|
|
56
|
-
|
|
57
31
|
if group.tool_calls:
|
|
58
32
|
assistant_message["tool_calls"] = [
|
|
59
33
|
{
|
|
@@ -71,9 +45,14 @@ def _assistant_group_to_message(group: AssistantGroup, model_name: str | None) -
|
|
|
71
45
|
# The order of items in reasoning_details must match the original
|
|
72
46
|
# stream order from the provider, so we iterate reasoning_items
|
|
73
47
|
# instead of the separated reasoning_text / reasoning_encrypted lists.
|
|
48
|
+
# For cross-model scenarios, degrade thinking to plain text.
|
|
74
49
|
reasoning_details: list[dict[str, object]] = []
|
|
50
|
+
degraded_thinking_texts: list[str] = []
|
|
75
51
|
for item in group.reasoning_items:
|
|
76
52
|
if model_name != item.model:
|
|
53
|
+
# Cross-model: collect thinking text for degradation
|
|
54
|
+
if isinstance(item, model.ReasoningTextItem) and item.content:
|
|
55
|
+
degraded_thinking_texts.append(item.content)
|
|
77
56
|
continue
|
|
78
57
|
if isinstance(item, model.ReasoningEncryptedItem):
|
|
79
58
|
if item.encrypted_content and len(item.encrypted_content) > 0:
|
|
@@ -98,6 +77,15 @@ def _assistant_group_to_message(group: AssistantGroup, model_name: str | None) -
|
|
|
98
77
|
if reasoning_details:
|
|
99
78
|
assistant_message["reasoning_details"] = reasoning_details
|
|
100
79
|
|
|
80
|
+
# Build content with optional degraded thinking prefix
|
|
81
|
+
content_parts: list[str] = []
|
|
82
|
+
if degraded_thinking_texts:
|
|
83
|
+
content_parts.append("<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>")
|
|
84
|
+
if group.text_content:
|
|
85
|
+
content_parts.append(group.text_content)
|
|
86
|
+
if content_parts:
|
|
87
|
+
assistant_message["content"] = "\n".join(content_parts)
|
|
88
|
+
|
|
101
89
|
return assistant_message
|
|
102
90
|
|
|
103
91
|
|
|
@@ -150,9 +138,9 @@ def convert_history_to_input(
|
|
|
150
138
|
for group in parse_message_groups(history):
|
|
151
139
|
match group:
|
|
152
140
|
case UserGroup():
|
|
153
|
-
messages.append(
|
|
141
|
+
messages.append(user_group_to_openai_message(group))
|
|
154
142
|
case ToolGroup():
|
|
155
|
-
messages.append(
|
|
143
|
+
messages.append(tool_group_to_openai_message(group))
|
|
156
144
|
case AssistantGroup():
|
|
157
145
|
messages.append(_assistant_group_to_message(group, model_name))
|
|
158
146
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from klaude_code.llm.openai_compatible.stream import ReasoningDeltaResult, ReasoningHandlerABC
|
|
4
|
+
from klaude_code.protocol import model
|
|
5
|
+
from klaude_code.trace import log
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReasoningDetail(BaseModel):
|
|
9
|
+
"""OpenRouter's https://openrouter.ai/docs/use-cases/reasoning-tokens#reasoning_details-array-structure"""
|
|
10
|
+
|
|
11
|
+
type: str
|
|
12
|
+
format: str
|
|
13
|
+
index: int
|
|
14
|
+
id: str | None = None
|
|
15
|
+
data: str | None = None # OpenAI's encrypted content
|
|
16
|
+
summary: str | None = None
|
|
17
|
+
text: str | None = None
|
|
18
|
+
signature: str | None = None # Claude's signature
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReasoningStreamHandler(ReasoningHandlerABC):
|
|
22
|
+
"""Accumulates OpenRouter reasoning details and emits ordered outputs."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
param_model: str,
|
|
27
|
+
response_id: str | None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._param_model = param_model
|
|
30
|
+
self._response_id = response_id
|
|
31
|
+
|
|
32
|
+
self._reasoning_id: str | None = None
|
|
33
|
+
self._accumulated_reasoning: list[str] = []
|
|
34
|
+
|
|
35
|
+
def set_response_id(self, response_id: str | None) -> None:
|
|
36
|
+
"""Update the response identifier used for emitted items."""
|
|
37
|
+
self._response_id = response_id
|
|
38
|
+
|
|
39
|
+
def on_delta(self, delta: object) -> ReasoningDeltaResult:
|
|
40
|
+
"""Parse OpenRouter's reasoning_details and return ordered stream outputs."""
|
|
41
|
+
reasoning_details = getattr(delta, "reasoning_details", None)
|
|
42
|
+
if not reasoning_details:
|
|
43
|
+
return ReasoningDeltaResult(handled=False, outputs=[])
|
|
44
|
+
|
|
45
|
+
outputs: list[str | model.ConversationItem] = []
|
|
46
|
+
for item in reasoning_details:
|
|
47
|
+
try:
|
|
48
|
+
reasoning_detail = ReasoningDetail.model_validate(item)
|
|
49
|
+
if reasoning_detail.text:
|
|
50
|
+
outputs.append(reasoning_detail.text)
|
|
51
|
+
if reasoning_detail.summary:
|
|
52
|
+
outputs.append(reasoning_detail.summary)
|
|
53
|
+
outputs.extend(self.on_detail(reasoning_detail))
|
|
54
|
+
except Exception as e:
|
|
55
|
+
log("reasoning_details error", str(e), style="red")
|
|
56
|
+
|
|
57
|
+
return ReasoningDeltaResult(handled=True, outputs=outputs)
|
|
58
|
+
|
|
59
|
+
def on_detail(self, detail: ReasoningDetail) -> list[model.ConversationItem]:
|
|
60
|
+
"""Process a single reasoning detail and return streamable items."""
|
|
61
|
+
items: list[model.ConversationItem] = []
|
|
62
|
+
|
|
63
|
+
if detail.type == "reasoning.encrypted":
|
|
64
|
+
self._reasoning_id = detail.id
|
|
65
|
+
# Flush accumulated text before encrypted content
|
|
66
|
+
items.extend(self._flush_text())
|
|
67
|
+
if encrypted_item := self._build_encrypted_item(detail.data, detail):
|
|
68
|
+
items.append(encrypted_item)
|
|
69
|
+
return items
|
|
70
|
+
|
|
71
|
+
if detail.type in ("reasoning.text", "reasoning.summary"):
|
|
72
|
+
self._reasoning_id = detail.id
|
|
73
|
+
# Accumulate text
|
|
74
|
+
text = detail.text if detail.type == "reasoning.text" else detail.summary
|
|
75
|
+
if text:
|
|
76
|
+
self._accumulated_reasoning.append(text)
|
|
77
|
+
# Flush on signature (encrypted content)
|
|
78
|
+
if detail.signature:
|
|
79
|
+
items.extend(self._flush_text())
|
|
80
|
+
if encrypted_item := self._build_encrypted_item(detail.signature, detail):
|
|
81
|
+
items.append(encrypted_item)
|
|
82
|
+
|
|
83
|
+
return items
|
|
84
|
+
|
|
85
|
+
def flush(self) -> list[model.ConversationItem]:
|
|
86
|
+
"""Flush buffered reasoning text on finalize."""
|
|
87
|
+
return self._flush_text()
|
|
88
|
+
|
|
89
|
+
def _flush_text(self) -> list[model.ConversationItem]:
|
|
90
|
+
"""Flush accumulated reasoning text as a single item."""
|
|
91
|
+
if not self._accumulated_reasoning:
|
|
92
|
+
return []
|
|
93
|
+
item = self._build_text_item("".join(self._accumulated_reasoning))
|
|
94
|
+
self._accumulated_reasoning = []
|
|
95
|
+
return [item]
|
|
96
|
+
|
|
97
|
+
def _build_text_item(self, content: str) -> model.ReasoningTextItem:
|
|
98
|
+
return model.ReasoningTextItem(
|
|
99
|
+
id=self._reasoning_id,
|
|
100
|
+
content=content,
|
|
101
|
+
response_id=self._response_id,
|
|
102
|
+
model=self._param_model,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _build_encrypted_item(
|
|
106
|
+
self,
|
|
107
|
+
content: str | None,
|
|
108
|
+
detail: ReasoningDetail,
|
|
109
|
+
) -> model.ReasoningEncryptedItem | None:
|
|
110
|
+
if not content:
|
|
111
|
+
return None
|
|
112
|
+
return model.ReasoningEncryptedItem(
|
|
113
|
+
id=detail.id,
|
|
114
|
+
encrypted_content=content,
|
|
115
|
+
format=detail.format,
|
|
116
|
+
response_id=self._response_id,
|
|
117
|
+
model=self._param_model,
|
|
118
|
+
)
|
klaude_code/llm/registry.py
CHANGED
|
@@ -1,22 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
import importlib
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
2
4
|
|
|
3
|
-
from klaude_code.llm.client import LLMClientABC
|
|
4
5
|
from klaude_code.protocol import llm_param
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from klaude_code.llm.client import LLMClientABC
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
_T = TypeVar("_T", bound=type["LLMClientABC"])
|
|
9
11
|
|
|
12
|
+
# Track which protocols have been loaded
|
|
13
|
+
_loaded_protocols: set[llm_param.LLMClientProtocol] = set()
|
|
14
|
+
_REGISTRY: dict[llm_param.LLMClientProtocol, type["LLMClientABC"]] = {}
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
|
|
17
|
+
def _load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
|
|
18
|
+
"""Load the module for a specific protocol on demand."""
|
|
19
|
+
if protocol in _loaded_protocols:
|
|
20
|
+
return
|
|
21
|
+
_loaded_protocols.add(protocol)
|
|
22
|
+
|
|
23
|
+
# Import only the needed module to trigger @register decorator
|
|
24
|
+
if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
|
|
25
|
+
importlib.import_module("klaude_code.llm.anthropic")
|
|
26
|
+
elif protocol == llm_param.LLMClientProtocol.BEDROCK:
|
|
27
|
+
importlib.import_module("klaude_code.llm.bedrock")
|
|
28
|
+
elif protocol == llm_param.LLMClientProtocol.CODEX:
|
|
29
|
+
importlib.import_module("klaude_code.llm.codex")
|
|
30
|
+
elif protocol == llm_param.LLMClientProtocol.OPENAI:
|
|
31
|
+
importlib.import_module("klaude_code.llm.openai_compatible")
|
|
32
|
+
elif protocol == llm_param.LLMClientProtocol.OPENROUTER:
|
|
33
|
+
importlib.import_module("klaude_code.llm.openrouter")
|
|
34
|
+
elif protocol == llm_param.LLMClientProtocol.RESPONSES:
|
|
35
|
+
importlib.import_module("klaude_code.llm.responses")
|
|
36
|
+
elif protocol == llm_param.LLMClientProtocol.GOOGLE:
|
|
37
|
+
importlib.import_module("klaude_code.llm.google")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register(name: llm_param.LLMClientProtocol) -> Callable[[_T], _T]:
|
|
41
|
+
"""Decorator to register an LLM client class for a protocol."""
|
|
42
|
+
|
|
43
|
+
def _decorator(cls: _T) -> _T:
|
|
13
44
|
_REGISTRY[name] = cls
|
|
14
45
|
return cls
|
|
15
46
|
|
|
16
47
|
return _decorator
|
|
17
48
|
|
|
18
49
|
|
|
19
|
-
def create_llm_client(config: llm_param.LLMConfigParameter) -> LLMClientABC:
|
|
50
|
+
def create_llm_client(config: llm_param.LLMConfigParameter) -> "LLMClientABC":
|
|
51
|
+
_load_protocol(config.protocol)
|
|
20
52
|
if config.protocol not in _REGISTRY:
|
|
21
53
|
raise ValueError(f"Unknown LLMClient protocol: {config.protocol}")
|
|
22
54
|
return _REGISTRY[config.protocol].create(config)
|