klaude-code 1.2.6__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/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from typing import override
|
|
5
|
+
|
|
6
|
+
import anthropic
|
|
7
|
+
import httpx
|
|
8
|
+
from anthropic import RateLimitError
|
|
9
|
+
from anthropic.types.beta.beta_input_json_delta import BetaInputJSONDelta
|
|
10
|
+
from anthropic.types.beta.beta_raw_content_block_delta_event import BetaRawContentBlockDeltaEvent
|
|
11
|
+
from anthropic.types.beta.beta_raw_content_block_start_event import BetaRawContentBlockStartEvent
|
|
12
|
+
from anthropic.types.beta.beta_raw_content_block_stop_event import BetaRawContentBlockStopEvent
|
|
13
|
+
from anthropic.types.beta.beta_raw_message_delta_event import BetaRawMessageDeltaEvent
|
|
14
|
+
from anthropic.types.beta.beta_raw_message_start_event import BetaRawMessageStartEvent
|
|
15
|
+
from anthropic.types.beta.beta_signature_delta import BetaSignatureDelta
|
|
16
|
+
from anthropic.types.beta.beta_text_delta import BetaTextDelta
|
|
17
|
+
from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
|
|
18
|
+
from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
|
|
19
|
+
|
|
20
|
+
from klaude_code import const
|
|
21
|
+
from klaude_code.llm.anthropic.input import convert_history_to_input, convert_system_to_input, convert_tool_schema
|
|
22
|
+
from klaude_code.llm.client import LLMClientABC, call_with_logged_payload
|
|
23
|
+
from klaude_code.llm.input_common import apply_config_defaults
|
|
24
|
+
from klaude_code.llm.registry import register
|
|
25
|
+
from klaude_code.llm.usage import calculate_cost
|
|
26
|
+
from klaude_code.protocol import llm_param, model
|
|
27
|
+
from klaude_code.trace import DebugType, log_debug
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@register(llm_param.LLMClientProtocol.ANTHROPIC)
|
|
31
|
+
class AnthropicClient(LLMClientABC):
|
|
32
|
+
def __init__(self, config: llm_param.LLMConfigParameter):
|
|
33
|
+
super().__init__(config)
|
|
34
|
+
client = anthropic.AsyncAnthropic(
|
|
35
|
+
api_key=config.api_key,
|
|
36
|
+
base_url=config.base_url,
|
|
37
|
+
timeout=httpx.Timeout(300.0, connect=15.0, read=285.0),
|
|
38
|
+
)
|
|
39
|
+
self.client: anthropic.AsyncAnthropic = client
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
@override
|
|
43
|
+
def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
|
|
44
|
+
return cls(config)
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
48
|
+
param = apply_config_defaults(param, self.get_llm_config())
|
|
49
|
+
|
|
50
|
+
request_start_time = time.time()
|
|
51
|
+
first_token_time: float | None = None
|
|
52
|
+
last_token_time: float | None = None
|
|
53
|
+
|
|
54
|
+
messages = convert_history_to_input(param.input, param.model)
|
|
55
|
+
tools = convert_tool_schema(param.tools)
|
|
56
|
+
system = convert_system_to_input(param.system)
|
|
57
|
+
|
|
58
|
+
stream = call_with_logged_payload(
|
|
59
|
+
self.client.beta.messages.create,
|
|
60
|
+
model=str(param.model),
|
|
61
|
+
tool_choice={
|
|
62
|
+
"type": "auto",
|
|
63
|
+
"disable_parallel_tool_use": False,
|
|
64
|
+
},
|
|
65
|
+
stream=True,
|
|
66
|
+
max_tokens=param.max_tokens or const.DEFAULT_MAX_TOKENS,
|
|
67
|
+
temperature=param.temperature or const.DEFAULT_TEMPERATURE,
|
|
68
|
+
messages=messages,
|
|
69
|
+
system=system,
|
|
70
|
+
tools=tools,
|
|
71
|
+
betas=["interleaved-thinking-2025-05-14", "context-1m-2025-08-07"],
|
|
72
|
+
thinking=anthropic.types.ThinkingConfigEnabledParam(
|
|
73
|
+
type=param.thinking.type,
|
|
74
|
+
budget_tokens=param.thinking.budget_tokens or const.DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
|
|
75
|
+
)
|
|
76
|
+
if param.thinking and param.thinking.type == "enabled"
|
|
77
|
+
else anthropic.types.ThinkingConfigDisabledParam(
|
|
78
|
+
type="disabled",
|
|
79
|
+
),
|
|
80
|
+
extra_headers={"extra": json.dumps({"session_id": param.session_id})},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
accumulated_thinking: list[str] = []
|
|
84
|
+
accumulated_content: list[str] = []
|
|
85
|
+
response_id: str | None = None
|
|
86
|
+
|
|
87
|
+
current_tool_name: str | None = None
|
|
88
|
+
current_tool_call_id: str | None = None
|
|
89
|
+
current_tool_inputs: list[str] | None = None
|
|
90
|
+
|
|
91
|
+
input_tokens = 0
|
|
92
|
+
cached_tokens = 0
|
|
93
|
+
output_tokens = 0
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
async for event in await stream:
|
|
97
|
+
log_debug(
|
|
98
|
+
f"[{event.type}]",
|
|
99
|
+
event.model_dump_json(exclude_none=True),
|
|
100
|
+
style="blue",
|
|
101
|
+
debug_type=DebugType.LLM_STREAM,
|
|
102
|
+
)
|
|
103
|
+
match event:
|
|
104
|
+
case BetaRawMessageStartEvent() as event:
|
|
105
|
+
response_id = event.message.id
|
|
106
|
+
cached_tokens = event.message.usage.cache_read_input_tokens or 0
|
|
107
|
+
input_tokens = (event.message.usage.input_tokens or 0) + (
|
|
108
|
+
event.message.usage.cache_creation_input_tokens or 0
|
|
109
|
+
)
|
|
110
|
+
output_tokens = event.message.usage.output_tokens or 0
|
|
111
|
+
yield model.StartItem(response_id=response_id)
|
|
112
|
+
case BetaRawContentBlockDeltaEvent() as event:
|
|
113
|
+
match event.delta:
|
|
114
|
+
case BetaThinkingDelta() as delta:
|
|
115
|
+
if first_token_time is None:
|
|
116
|
+
first_token_time = time.time()
|
|
117
|
+
last_token_time = time.time()
|
|
118
|
+
accumulated_thinking.append(delta.thinking)
|
|
119
|
+
case BetaSignatureDelta() as delta:
|
|
120
|
+
if first_token_time is None:
|
|
121
|
+
first_token_time = time.time()
|
|
122
|
+
last_token_time = time.time()
|
|
123
|
+
yield model.ReasoningEncryptedItem(
|
|
124
|
+
encrypted_content=delta.signature,
|
|
125
|
+
response_id=response_id,
|
|
126
|
+
model=str(param.model),
|
|
127
|
+
)
|
|
128
|
+
case BetaTextDelta() as delta:
|
|
129
|
+
if first_token_time is None:
|
|
130
|
+
first_token_time = time.time()
|
|
131
|
+
last_token_time = time.time()
|
|
132
|
+
accumulated_content.append(delta.text)
|
|
133
|
+
yield model.AssistantMessageDelta(
|
|
134
|
+
content=delta.text,
|
|
135
|
+
response_id=response_id,
|
|
136
|
+
)
|
|
137
|
+
case BetaInputJSONDelta() as delta:
|
|
138
|
+
if first_token_time is None:
|
|
139
|
+
first_token_time = time.time()
|
|
140
|
+
last_token_time = time.time()
|
|
141
|
+
if current_tool_inputs is not None:
|
|
142
|
+
current_tool_inputs.append(delta.partial_json)
|
|
143
|
+
case _:
|
|
144
|
+
pass
|
|
145
|
+
case BetaRawContentBlockStartEvent() as event:
|
|
146
|
+
match event.content_block:
|
|
147
|
+
case BetaToolUseBlock() as block:
|
|
148
|
+
yield model.ToolCallStartItem(
|
|
149
|
+
response_id=response_id,
|
|
150
|
+
call_id=block.id,
|
|
151
|
+
name=block.name,
|
|
152
|
+
)
|
|
153
|
+
current_tool_name = block.name
|
|
154
|
+
current_tool_call_id = block.id
|
|
155
|
+
current_tool_inputs = []
|
|
156
|
+
case _:
|
|
157
|
+
pass
|
|
158
|
+
case BetaRawContentBlockStopEvent() as event:
|
|
159
|
+
if len(accumulated_thinking) > 0:
|
|
160
|
+
full_thinking = "".join(accumulated_thinking)
|
|
161
|
+
yield model.ReasoningTextItem(
|
|
162
|
+
content=full_thinking,
|
|
163
|
+
response_id=response_id,
|
|
164
|
+
model=str(param.model),
|
|
165
|
+
)
|
|
166
|
+
accumulated_thinking.clear()
|
|
167
|
+
if len(accumulated_content) > 0:
|
|
168
|
+
yield model.AssistantMessageItem(
|
|
169
|
+
content="".join(accumulated_content),
|
|
170
|
+
response_id=response_id,
|
|
171
|
+
)
|
|
172
|
+
accumulated_content.clear()
|
|
173
|
+
if current_tool_name and current_tool_call_id:
|
|
174
|
+
yield model.ToolCallItem(
|
|
175
|
+
name=current_tool_name,
|
|
176
|
+
call_id=current_tool_call_id,
|
|
177
|
+
arguments="".join(current_tool_inputs) if current_tool_inputs else "",
|
|
178
|
+
response_id=response_id,
|
|
179
|
+
)
|
|
180
|
+
current_tool_name = None
|
|
181
|
+
current_tool_call_id = None
|
|
182
|
+
current_tool_inputs = None
|
|
183
|
+
case BetaRawMessageDeltaEvent() as event:
|
|
184
|
+
input_tokens += (event.usage.input_tokens or 0) + (event.usage.cache_creation_input_tokens or 0)
|
|
185
|
+
output_tokens += event.usage.output_tokens or 0
|
|
186
|
+
cached_tokens += event.usage.cache_read_input_tokens or 0
|
|
187
|
+
total_tokens = input_tokens + cached_tokens + output_tokens
|
|
188
|
+
context_usage_percent = (
|
|
189
|
+
(total_tokens / param.context_limit) * 100 if param.context_limit else None
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
throughput_tps: float | None = None
|
|
193
|
+
first_token_latency_ms: float | None = None
|
|
194
|
+
|
|
195
|
+
if first_token_time is not None:
|
|
196
|
+
first_token_latency_ms = (first_token_time - request_start_time) * 1000
|
|
197
|
+
|
|
198
|
+
if first_token_time is not None and last_token_time is not None and output_tokens > 0:
|
|
199
|
+
time_duration = last_token_time - first_token_time
|
|
200
|
+
if time_duration >= 0.15:
|
|
201
|
+
throughput_tps = output_tokens / time_duration
|
|
202
|
+
|
|
203
|
+
usage = model.Usage(
|
|
204
|
+
input_tokens=input_tokens,
|
|
205
|
+
output_tokens=output_tokens,
|
|
206
|
+
cached_tokens=cached_tokens,
|
|
207
|
+
total_tokens=total_tokens,
|
|
208
|
+
context_usage_percent=context_usage_percent,
|
|
209
|
+
throughput_tps=throughput_tps,
|
|
210
|
+
first_token_latency_ms=first_token_latency_ms,
|
|
211
|
+
)
|
|
212
|
+
calculate_cost(usage, self._config.cost)
|
|
213
|
+
yield model.ResponseMetadataItem(
|
|
214
|
+
usage=usage,
|
|
215
|
+
response_id=response_id,
|
|
216
|
+
model_name=str(param.model),
|
|
217
|
+
)
|
|
218
|
+
case _:
|
|
219
|
+
pass
|
|
220
|
+
except RateLimitError as e:
|
|
221
|
+
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {str(e)}")
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# pyright: reportReturnType=false
|
|
2
|
+
# pyright: reportArgumentType=false
|
|
3
|
+
# pyright: reportUnknownMemberType=false
|
|
4
|
+
# pyright: reportAttributeAccessIssue=false
|
|
5
|
+
# pyright: reportUnknownVariableType=false
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from base64 import b64decode
|
|
10
|
+
from binascii import Error as BinasciiError
|
|
11
|
+
from typing import Literal, cast
|
|
12
|
+
|
|
13
|
+
from anthropic.types.beta.beta_base64_image_source_param import BetaBase64ImageSourceParam
|
|
14
|
+
from anthropic.types.beta.beta_image_block_param import BetaImageBlockParam
|
|
15
|
+
from anthropic.types.beta.beta_message_param import BetaMessageParam
|
|
16
|
+
from anthropic.types.beta.beta_text_block_param import BetaTextBlockParam
|
|
17
|
+
from anthropic.types.beta.beta_tool_param import BetaToolParam
|
|
18
|
+
from anthropic.types.beta.beta_url_image_source_param import BetaURLImageSourceParam
|
|
19
|
+
|
|
20
|
+
from klaude_code.llm.input_common import AssistantGroup, ToolGroup, UserGroup, merge_reminder_text, parse_message_groups
|
|
21
|
+
from klaude_code.protocol import llm_param, model
|
|
22
|
+
|
|
23
|
+
AllowedMediaType = Literal["image/png", "image/jpeg", "image/gif", "image/webp"]
|
|
24
|
+
_INLINE_IMAGE_MEDIA_TYPES: tuple[AllowedMediaType, ...] = (
|
|
25
|
+
"image/png",
|
|
26
|
+
"image/jpeg",
|
|
27
|
+
"image/gif",
|
|
28
|
+
"image/webp",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _image_part_to_block(image: model.ImageURLPart) -> BetaImageBlockParam:
|
|
33
|
+
url = image.image_url.url
|
|
34
|
+
if url.startswith("data:"):
|
|
35
|
+
header_and_media = url.split(",", 1)
|
|
36
|
+
if len(header_and_media) != 2:
|
|
37
|
+
raise ValueError("Invalid data URL for image: missing comma separator")
|
|
38
|
+
header, base64_data = header_and_media
|
|
39
|
+
if ";base64" not in header:
|
|
40
|
+
raise ValueError("Invalid data URL for image: missing base64 marker")
|
|
41
|
+
media_type = header[5:].split(";", 1)[0]
|
|
42
|
+
if media_type not in _INLINE_IMAGE_MEDIA_TYPES:
|
|
43
|
+
raise ValueError(f"Unsupported inline image media type: {media_type}")
|
|
44
|
+
base64_payload = base64_data.strip()
|
|
45
|
+
if base64_payload == "":
|
|
46
|
+
raise ValueError("Inline image data is empty")
|
|
47
|
+
try:
|
|
48
|
+
b64decode(base64_payload, validate=True)
|
|
49
|
+
except (BinasciiError, ValueError) as exc:
|
|
50
|
+
raise ValueError("Inline image data is not valid base64") from exc
|
|
51
|
+
source = cast(
|
|
52
|
+
BetaBase64ImageSourceParam,
|
|
53
|
+
{
|
|
54
|
+
"type": "base64",
|
|
55
|
+
"media_type": media_type,
|
|
56
|
+
"data": base64_payload,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
return {"type": "image", "source": source}
|
|
60
|
+
|
|
61
|
+
source_url: BetaURLImageSourceParam = {"type": "url", "url": url}
|
|
62
|
+
return {"type": "image", "source": source_url}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _user_group_to_message(group: UserGroup) -> BetaMessageParam:
|
|
66
|
+
blocks: list[BetaTextBlockParam | BetaImageBlockParam] = []
|
|
67
|
+
for text in group.text_parts:
|
|
68
|
+
blocks.append({"type": "text", "text": text + "\n"})
|
|
69
|
+
for image in group.images:
|
|
70
|
+
blocks.append(_image_part_to_block(image))
|
|
71
|
+
if not blocks:
|
|
72
|
+
blocks.append({"type": "text", "text": ""})
|
|
73
|
+
return {"role": "user", "content": blocks}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _tool_group_to_message(group: ToolGroup) -> BetaMessageParam:
|
|
77
|
+
tool_content: list[BetaTextBlockParam | BetaImageBlockParam] = []
|
|
78
|
+
merged_text = merge_reminder_text(
|
|
79
|
+
group.tool_result.output or "<system-reminder>Tool ran without output or errors</system-reminder>",
|
|
80
|
+
group.reminder_texts,
|
|
81
|
+
)
|
|
82
|
+
tool_content.append({"type": "text", "text": merged_text})
|
|
83
|
+
for image in group.tool_result.images or []:
|
|
84
|
+
tool_content.append(_image_part_to_block(image))
|
|
85
|
+
for image in group.reminder_images:
|
|
86
|
+
tool_content.append(_image_part_to_block(image))
|
|
87
|
+
return {
|
|
88
|
+
"role": "user",
|
|
89
|
+
"content": [
|
|
90
|
+
{
|
|
91
|
+
"type": "tool_result",
|
|
92
|
+
"tool_use_id": group.tool_result.call_id,
|
|
93
|
+
"is_error": group.tool_result.status == "error",
|
|
94
|
+
"content": tool_content,
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _assistant_group_to_message(group: AssistantGroup, model_name: str | None) -> BetaMessageParam:
|
|
101
|
+
content: list[dict[str, object]] = []
|
|
102
|
+
current_reasoning_content: str | None = None
|
|
103
|
+
|
|
104
|
+
# Process reasoning items in original order so that text and
|
|
105
|
+
# encrypted parts are paired correctly for the given model.
|
|
106
|
+
for item in group.reasoning_items:
|
|
107
|
+
if isinstance(item, model.ReasoningTextItem):
|
|
108
|
+
if model_name != item.model:
|
|
109
|
+
continue
|
|
110
|
+
current_reasoning_content = item.content
|
|
111
|
+
else:
|
|
112
|
+
if model_name != item.model:
|
|
113
|
+
continue
|
|
114
|
+
if item.encrypted_content and len(item.encrypted_content) > 0:
|
|
115
|
+
content.append(
|
|
116
|
+
{
|
|
117
|
+
"type": "thinking",
|
|
118
|
+
"thinking": current_reasoning_content or "",
|
|
119
|
+
"signature": item.encrypted_content,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
current_reasoning_content = None
|
|
123
|
+
|
|
124
|
+
# Moonshot.ai's Kimi does not always send reasoning signatures;
|
|
125
|
+
# if we saw reasoning text without any matching encrypted item,
|
|
126
|
+
# emit it as a plain thinking block.
|
|
127
|
+
if len(current_reasoning_content or "") > 0:
|
|
128
|
+
content.insert(0, {"type": "thinking", "thinking": current_reasoning_content})
|
|
129
|
+
|
|
130
|
+
if group.text_content:
|
|
131
|
+
content.append({"type": "text", "text": group.text_content})
|
|
132
|
+
|
|
133
|
+
for tc in group.tool_calls:
|
|
134
|
+
content.append(
|
|
135
|
+
{
|
|
136
|
+
"type": "tool_use",
|
|
137
|
+
"id": tc.call_id,
|
|
138
|
+
"name": tc.name,
|
|
139
|
+
"input": json.loads(tc.arguments) if tc.arguments else None,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return {"role": "assistant", "content": content}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _add_cache_control(messages: list[BetaMessageParam]) -> None:
|
|
147
|
+
if len(messages) > 0:
|
|
148
|
+
last_message = messages[-1]
|
|
149
|
+
content_list = list(last_message.get("content", []))
|
|
150
|
+
if content_list:
|
|
151
|
+
last_content_part = content_list[-1]
|
|
152
|
+
if last_content_part.get("type", "") in ["text", "tool_result", "tool_use"]:
|
|
153
|
+
last_content_part["cache_control"] = {"type": "ephemeral"} # type: ignore
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def convert_history_to_input(
|
|
157
|
+
history: list[model.ConversationItem],
|
|
158
|
+
model_name: str | None,
|
|
159
|
+
) -> list[BetaMessageParam]:
|
|
160
|
+
"""
|
|
161
|
+
Convert a list of conversation items to a list of beta message params.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
history: List of conversation items.
|
|
165
|
+
model_name: Model name. Used to verify that signatures are valid for the same model
|
|
166
|
+
"""
|
|
167
|
+
messages: list[BetaMessageParam] = []
|
|
168
|
+
for group in parse_message_groups(history):
|
|
169
|
+
match group:
|
|
170
|
+
case UserGroup():
|
|
171
|
+
messages.append(_user_group_to_message(group))
|
|
172
|
+
case ToolGroup():
|
|
173
|
+
messages.append(_tool_group_to_message(group))
|
|
174
|
+
case AssistantGroup():
|
|
175
|
+
messages.append(_assistant_group_to_message(group, model_name))
|
|
176
|
+
|
|
177
|
+
_add_cache_control(messages)
|
|
178
|
+
return messages
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def convert_system_to_input(system: str | None) -> list[BetaTextBlockParam]:
|
|
182
|
+
if system is None:
|
|
183
|
+
return []
|
|
184
|
+
return [{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def convert_tool_schema(
|
|
188
|
+
tools: list[llm_param.ToolSchema] | None,
|
|
189
|
+
) -> list[BetaToolParam]:
|
|
190
|
+
if tools is None:
|
|
191
|
+
return []
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
"input_schema": tool.parameters,
|
|
195
|
+
"type": "custom",
|
|
196
|
+
"name": tool.name,
|
|
197
|
+
"description": tool.description,
|
|
198
|
+
}
|
|
199
|
+
for tool in tools
|
|
200
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from typing import Callable, ParamSpec, TypeVar, cast
|
|
5
|
+
|
|
6
|
+
from klaude_code.protocol import llm_param, model
|
|
7
|
+
from klaude_code.trace import DebugType, log_debug
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LLMClientABC(ABC):
|
|
11
|
+
def __init__(self, config: llm_param.LLMConfigParameter) -> None:
|
|
12
|
+
self._config = config
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def create(cls, config: llm_param.LLMConfigParameter) -> "LLMClientABC":
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
yield cast(model.ConversationItem, None) # pyright: ignore[reportUnreachable]
|
|
23
|
+
|
|
24
|
+
def get_llm_config(self) -> llm_param.LLMConfigParameter:
|
|
25
|
+
return self._config
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def model_name(self) -> str:
|
|
29
|
+
return self._config.model or ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
P = ParamSpec("P")
|
|
33
|
+
R = TypeVar("R")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def call_with_logged_payload(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
|
|
37
|
+
"""Call an SDK function while logging the JSON payload.
|
|
38
|
+
|
|
39
|
+
The function reuses the original callable's type signature via ParamSpec
|
|
40
|
+
so static type checkers can validate arguments at the call site.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
payload = {k: v for k, v in kwargs.items() if v is not None}
|
|
44
|
+
log_debug(
|
|
45
|
+
json.dumps(payload, ensure_ascii=False, default=str),
|
|
46
|
+
style="yellow",
|
|
47
|
+
debug_type=DebugType.LLM_PAYLOAD,
|
|
48
|
+
)
|
|
49
|
+
return func(*args, **kwargs)
|