klaude-code 1.9.0__py3-none-any.whl → 2.0.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/base.py +2 -6
- klaude_code/cli/auth_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -1
- klaude_code/cli/main.py +1 -1
- klaude_code/cli/runtime.py +7 -5
- klaude_code/cli/self_update.py +1 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +2 -2
- klaude_code/command/debug_cmd.py +4 -4
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +12 -12
- klaude_code/command/fork_session_cmd.py +29 -23
- klaude_code/command/help_cmd.py +4 -4
- klaude_code/command/model_cmd.py +4 -4
- klaude_code/command/model_select.py +1 -1
- klaude_code/command/prompt-commit.md +11 -2
- klaude_code/command/prompt_command.py +3 -3
- klaude_code/command/refresh_cmd.py +2 -2
- klaude_code/command/registry.py +7 -5
- klaude_code/command/release_notes_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +15 -11
- klaude_code/command/status_cmd.py +4 -4
- klaude_code/command/terminal_setup_cmd.py +8 -8
- klaude_code/command/thinking_cmd.py +4 -4
- klaude_code/config/assets/builtin_config.yaml +16 -0
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +7 -2
- klaude_code/const.py +146 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +21 -13
- klaude_code/core/manager/sub_agent_manager.py +71 -7
- klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
- klaude_code/core/reminders.py +88 -69
- klaude_code/core/task.py +44 -45
- klaude_code/core/tool/file/apply_patch_tool.py +9 -9
- klaude_code/core/tool/file/diff_builder.py +3 -5
- klaude_code/core/tool/file/edit_tool.py +23 -23
- klaude_code/core/tool/file/move_tool.py +43 -43
- klaude_code/core/tool/file/read_tool.py +44 -39
- klaude_code/core/tool/file/write_tool.py +14 -14
- klaude_code/core/tool/report_back_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +23 -23
- klaude_code/core/tool/skill/skill_tool.py +7 -7
- klaude_code/core/tool/sub_agent_tool.py +38 -9
- klaude_code/core/tool/todo/todo_write_tool.py +8 -8
- klaude_code/core/tool/todo/update_plan_tool.py +6 -6
- klaude_code/core/tool/tool_abc.py +2 -2
- klaude_code/core/tool/tool_context.py +27 -0
- klaude_code/core/tool/tool_runner.py +88 -42
- klaude_code/core/tool/truncation.py +38 -20
- klaude_code/core/tool/web/mermaid_tool.py +6 -7
- klaude_code/core/tool/web/web_fetch_tool.py +68 -30
- klaude_code/core/tool/web/web_search_tool.py +15 -17
- klaude_code/core/turn.py +120 -73
- klaude_code/llm/anthropic/client.py +79 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/client.py +18 -8
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +15 -9
- klaude_code/llm/google/client.py +122 -60
- klaude_code/llm/google/input.py +94 -108
- klaude_code/llm/image.py +123 -0
- klaude_code/llm/input_common.py +136 -189
- klaude_code/llm/openai_compatible/client.py +17 -7
- klaude_code/llm/openai_compatible/input.py +36 -66
- klaude_code/llm/openai_compatible/stream.py +119 -67
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
- klaude_code/llm/openrouter/client.py +34 -9
- klaude_code/llm/openrouter/input.py +63 -64
- klaude_code/llm/openrouter/reasoning.py +22 -24
- klaude_code/llm/registry.py +20 -17
- klaude_code/llm/responses/client.py +107 -45
- klaude_code/llm/responses/input.py +115 -98
- klaude_code/llm/usage.py +52 -25
- klaude_code/protocol/__init__.py +1 -0
- klaude_code/protocol/events.py +16 -12
- klaude_code/protocol/llm_param.py +20 -2
- klaude_code/protocol/message.py +250 -0
- klaude_code/protocol/model.py +94 -281
- klaude_code/protocol/op.py +2 -2
- klaude_code/protocol/sub_agent/__init__.py +1 -0
- klaude_code/protocol/sub_agent/explore.py +10 -0
- klaude_code/protocol/sub_agent/image_gen.py +119 -0
- klaude_code/protocol/sub_agent/task.py +10 -0
- klaude_code/protocol/sub_agent/web.py +10 -0
- klaude_code/session/codec.py +6 -6
- klaude_code/session/export.py +261 -62
- klaude_code/session/selector.py +7 -24
- klaude_code/session/session.py +126 -54
- klaude_code/session/store.py +5 -32
- klaude_code/session/templates/export_session.html +1 -1
- klaude_code/session/templates/mermaid_viewer.html +1 -1
- klaude_code/trace/log.py +11 -6
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +1 -8
- klaude_code/ui/modes/debug/display.py +2 -2
- klaude_code/ui/modes/repl/clipboard.py +2 -2
- klaude_code/ui/modes/repl/completers.py +18 -10
- klaude_code/ui/modes/repl/event_handler.py +136 -127
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/common.py +65 -7
- klaude_code/ui/renderers/developer.py +7 -6
- klaude_code/ui/renderers/diffs.py +11 -11
- klaude_code/ui/renderers/mermaid_viewer.py +49 -2
- klaude_code/ui/renderers/metadata.py +33 -5
- klaude_code/ui/renderers/sub_agent.py +57 -16
- klaude_code/ui/renderers/thinking.py +37 -2
- klaude_code/ui/renderers/tools.py +180 -165
- klaude_code/ui/rich/live.py +3 -1
- klaude_code/ui/rich/markdown.py +39 -7
- klaude_code/ui/rich/quote.py +76 -1
- klaude_code/ui/rich/status.py +14 -8
- klaude_code/ui/rich/theme.py +8 -2
- klaude_code/ui/terminal/image.py +34 -0
- klaude_code/ui/terminal/notifier.py +2 -1
- klaude_code/ui/terminal/progress_bar.py +4 -4
- klaude_code/ui/terminal/selector.py +22 -4
- klaude_code/ui/utils/common.py +11 -2
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code-1.9.0.dist-info/RECORD +0 -224
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -19,16 +19,38 @@ from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
|
|
|
19
19
|
from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
|
|
20
20
|
from anthropic.types.beta.message_create_params import MessageCreateParamsStreaming
|
|
21
21
|
|
|
22
|
-
from klaude_code import
|
|
22
|
+
from klaude_code.const import (
|
|
23
|
+
ANTHROPIC_BETA_INTERLEAVED_THINKING,
|
|
24
|
+
CLAUDE_CODE_IDENTITY,
|
|
25
|
+
DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
|
|
26
|
+
DEFAULT_MAX_TOKENS,
|
|
27
|
+
DEFAULT_TEMPERATURE,
|
|
28
|
+
LLM_HTTP_TIMEOUT_CONNECT,
|
|
29
|
+
LLM_HTTP_TIMEOUT_READ,
|
|
30
|
+
LLM_HTTP_TIMEOUT_TOTAL,
|
|
31
|
+
)
|
|
23
32
|
from klaude_code.llm.anthropic.input import convert_history_to_input, convert_system_to_input, convert_tool_schema
|
|
24
33
|
from klaude_code.llm.client import LLMClientABC
|
|
25
34
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
26
35
|
from klaude_code.llm.registry import register
|
|
27
|
-
from klaude_code.llm.usage import MetadataTracker
|
|
28
|
-
from klaude_code.protocol import llm_param, model
|
|
36
|
+
from klaude_code.llm.usage import MetadataTracker, error_stream_items
|
|
37
|
+
from klaude_code.protocol import llm_param, message, model
|
|
29
38
|
from klaude_code.trace import DebugType, log_debug
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
|
|
41
|
+
def _map_anthropic_stop_reason(reason: str) -> model.StopReason | None:
|
|
42
|
+
mapping: dict[str, model.StopReason] = {
|
|
43
|
+
"end_turn": "stop",
|
|
44
|
+
"stop_sequence": "stop",
|
|
45
|
+
"max_tokens": "length",
|
|
46
|
+
"tool_use": "tool_use",
|
|
47
|
+
"content_filter": "error",
|
|
48
|
+
"error": "error",
|
|
49
|
+
"cancelled": "aborted",
|
|
50
|
+
"canceled": "aborted",
|
|
51
|
+
"aborted": "aborted",
|
|
52
|
+
}
|
|
53
|
+
return mapping.get(reason)
|
|
32
54
|
|
|
33
55
|
|
|
34
56
|
def build_payload(
|
|
@@ -44,17 +66,18 @@ def build_payload(
|
|
|
44
66
|
"""
|
|
45
67
|
messages = convert_history_to_input(param.input, param.model)
|
|
46
68
|
tools = convert_tool_schema(param.tools)
|
|
47
|
-
|
|
69
|
+
system_messages = [msg for msg in param.input if isinstance(msg, message.SystemMessage)]
|
|
70
|
+
system = convert_system_to_input(param.system, system_messages)
|
|
48
71
|
|
|
49
72
|
# Add identity block at the beginning of the system prompt
|
|
50
73
|
identity_block: BetaTextBlockParam = {
|
|
51
74
|
"type": "text",
|
|
52
|
-
"text":
|
|
75
|
+
"text": CLAUDE_CODE_IDENTITY,
|
|
53
76
|
"cache_control": {"type": "ephemeral"},
|
|
54
77
|
}
|
|
55
78
|
system = [identity_block, *system]
|
|
56
79
|
|
|
57
|
-
betas = [
|
|
80
|
+
betas = [ANTHROPIC_BETA_INTERLEAVED_THINKING]
|
|
58
81
|
if extra_betas:
|
|
59
82
|
# Prepend extra betas, avoiding duplicates
|
|
60
83
|
betas = [b for b in extra_betas if b not in betas] + betas
|
|
@@ -66,8 +89,8 @@ def build_payload(
|
|
|
66
89
|
"disable_parallel_tool_use": False,
|
|
67
90
|
},
|
|
68
91
|
"stream": True,
|
|
69
|
-
"max_tokens": param.max_tokens or
|
|
70
|
-
"temperature": param.temperature or
|
|
92
|
+
"max_tokens": param.max_tokens or DEFAULT_MAX_TOKENS,
|
|
93
|
+
"temperature": param.temperature or DEFAULT_TEMPERATURE,
|
|
71
94
|
"messages": messages,
|
|
72
95
|
"system": system,
|
|
73
96
|
"tools": tools,
|
|
@@ -77,7 +100,7 @@ def build_payload(
|
|
|
77
100
|
if param.thinking and param.thinking.type == "enabled":
|
|
78
101
|
payload["thinking"] = anthropic.types.ThinkingConfigEnabledParam(
|
|
79
102
|
type="enabled",
|
|
80
|
-
budget_tokens=param.thinking.budget_tokens or
|
|
103
|
+
budget_tokens=param.thinking.budget_tokens or DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
|
|
81
104
|
)
|
|
82
105
|
|
|
83
106
|
return payload
|
|
@@ -87,14 +110,14 @@ async def parse_anthropic_stream(
|
|
|
87
110
|
stream: Any,
|
|
88
111
|
param: llm_param.LLMCallParameter,
|
|
89
112
|
metadata_tracker: MetadataTracker,
|
|
90
|
-
) -> AsyncGenerator[
|
|
91
|
-
"""Parse Anthropic beta messages stream and yield
|
|
92
|
-
|
|
93
|
-
This function is shared between AnthropicClient and BedrockClient.
|
|
94
|
-
"""
|
|
113
|
+
) -> AsyncGenerator[message.LLMStreamItem]:
|
|
114
|
+
"""Parse Anthropic beta messages stream and yield stream items."""
|
|
95
115
|
accumulated_thinking: list[str] = []
|
|
96
116
|
accumulated_content: list[str] = []
|
|
117
|
+
parts: list[message.Part] = []
|
|
97
118
|
response_id: str | None = None
|
|
119
|
+
stop_reason: model.StopReason | None = None
|
|
120
|
+
pending_signature: str | None = None
|
|
98
121
|
|
|
99
122
|
current_tool_name: str | None = None
|
|
100
123
|
current_tool_call_id: str | None = None
|
|
@@ -115,28 +138,23 @@ async def parse_anthropic_stream(
|
|
|
115
138
|
response_id = event.message.id
|
|
116
139
|
cached_token = event.message.usage.cache_read_input_tokens or 0
|
|
117
140
|
input_token = event.message.usage.input_tokens
|
|
118
|
-
yield model.StartItem(response_id=response_id)
|
|
119
141
|
case BetaRawContentBlockDeltaEvent() as event:
|
|
120
142
|
match event.delta:
|
|
121
143
|
case BetaThinkingDelta() as delta:
|
|
122
144
|
if delta.thinking:
|
|
123
145
|
metadata_tracker.record_token()
|
|
124
146
|
accumulated_thinking.append(delta.thinking)
|
|
125
|
-
yield
|
|
147
|
+
yield message.ThinkingTextDelta(
|
|
126
148
|
content=delta.thinking,
|
|
127
149
|
response_id=response_id,
|
|
128
150
|
)
|
|
129
151
|
case BetaSignatureDelta() as delta:
|
|
130
|
-
|
|
131
|
-
encrypted_content=delta.signature,
|
|
132
|
-
response_id=response_id,
|
|
133
|
-
model=str(param.model),
|
|
134
|
-
)
|
|
152
|
+
pending_signature = delta.signature
|
|
135
153
|
case BetaTextDelta() as delta:
|
|
136
154
|
if delta.text:
|
|
137
155
|
metadata_tracker.record_token()
|
|
138
156
|
accumulated_content.append(delta.text)
|
|
139
|
-
yield
|
|
157
|
+
yield message.AssistantTextDelta(
|
|
140
158
|
content=delta.text,
|
|
141
159
|
response_id=response_id,
|
|
142
160
|
)
|
|
@@ -151,7 +169,7 @@ async def parse_anthropic_stream(
|
|
|
151
169
|
match event.content_block:
|
|
152
170
|
case BetaToolUseBlock() as block:
|
|
153
171
|
metadata_tracker.record_token()
|
|
154
|
-
yield
|
|
172
|
+
yield message.ToolCallStartItem(
|
|
155
173
|
response_id=response_id,
|
|
156
174
|
call_id=block.id,
|
|
157
175
|
name=block.name,
|
|
@@ -162,29 +180,32 @@ async def parse_anthropic_stream(
|
|
|
162
180
|
case _:
|
|
163
181
|
pass
|
|
164
182
|
case BetaRawContentBlockStopEvent():
|
|
165
|
-
if
|
|
183
|
+
if accumulated_thinking:
|
|
166
184
|
metadata_tracker.record_token()
|
|
167
185
|
full_thinking = "".join(accumulated_thinking)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
parts.append(message.ThinkingTextPart(text=full_thinking, model_id=str(param.model)))
|
|
187
|
+
if pending_signature:
|
|
188
|
+
parts.append(
|
|
189
|
+
message.ThinkingSignaturePart(
|
|
190
|
+
signature=pending_signature,
|
|
191
|
+
model_id=str(param.model),
|
|
192
|
+
format="anthropic",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
173
195
|
accumulated_thinking.clear()
|
|
174
|
-
|
|
196
|
+
pending_signature = None
|
|
197
|
+
if accumulated_content:
|
|
175
198
|
metadata_tracker.record_token()
|
|
176
|
-
|
|
177
|
-
content="".join(accumulated_content),
|
|
178
|
-
response_id=response_id,
|
|
179
|
-
)
|
|
199
|
+
parts.append(message.TextPart(text="".join(accumulated_content)))
|
|
180
200
|
accumulated_content.clear()
|
|
181
201
|
if current_tool_name and current_tool_call_id:
|
|
182
202
|
metadata_tracker.record_token()
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
parts.append(
|
|
204
|
+
message.ToolCallPart(
|
|
205
|
+
call_id=current_tool_call_id,
|
|
206
|
+
tool_name=current_tool_name,
|
|
207
|
+
arguments_json="".join(current_tool_inputs) if current_tool_inputs else "",
|
|
208
|
+
)
|
|
188
209
|
)
|
|
189
210
|
current_tool_name = None
|
|
190
211
|
current_tool_call_id = None
|
|
@@ -202,10 +223,20 @@ async def parse_anthropic_stream(
|
|
|
202
223
|
)
|
|
203
224
|
metadata_tracker.set_model_name(str(param.model))
|
|
204
225
|
metadata_tracker.set_response_id(response_id)
|
|
205
|
-
|
|
226
|
+
raw_stop_reason = getattr(event, "stop_reason", None)
|
|
227
|
+
if isinstance(raw_stop_reason, str):
|
|
228
|
+
stop_reason = _map_anthropic_stop_reason(raw_stop_reason)
|
|
206
229
|
case _:
|
|
207
230
|
pass
|
|
208
231
|
|
|
232
|
+
metadata = metadata_tracker.finalize()
|
|
233
|
+
yield message.AssistantMessage(
|
|
234
|
+
parts=parts,
|
|
235
|
+
response_id=response_id,
|
|
236
|
+
usage=metadata,
|
|
237
|
+
stop_reason=stop_reason,
|
|
238
|
+
)
|
|
239
|
+
|
|
209
240
|
|
|
210
241
|
@register(llm_param.LLMClientProtocol.ANTHROPIC)
|
|
211
242
|
class AnthropicClient(LLMClientABC):
|
|
@@ -220,7 +251,9 @@ class AnthropicClient(LLMClientABC):
|
|
|
220
251
|
client = anthropic.AsyncAnthropic(
|
|
221
252
|
api_key=config.api_key,
|
|
222
253
|
base_url=config.base_url,
|
|
223
|
-
timeout=httpx.Timeout(
|
|
254
|
+
timeout=httpx.Timeout(
|
|
255
|
+
LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ
|
|
256
|
+
),
|
|
224
257
|
)
|
|
225
258
|
finally:
|
|
226
259
|
if saved_auth_token is not None:
|
|
@@ -233,7 +266,7 @@ class AnthropicClient(LLMClientABC):
|
|
|
233
266
|
return cls(config)
|
|
234
267
|
|
|
235
268
|
@override
|
|
236
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[
|
|
269
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
|
|
237
270
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
238
271
|
|
|
239
272
|
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
@@ -255,4 +288,6 @@ class AnthropicClient(LLMClientABC):
|
|
|
255
288
|
async for item in parse_anthropic_stream(stream, param, metadata_tracker):
|
|
256
289
|
yield item
|
|
257
290
|
except (APIError, httpx.HTTPError) as e:
|
|
258
|
-
|
|
291
|
+
error_message = f"{e.__class__.__name__} {e!s}"
|
|
292
|
+
for item in error_stream_items(metadata_tracker, error=error_message):
|
|
293
|
+
yield item
|
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
# pyright: reportAttributeAccessIssue=false
|
|
5
5
|
# pyright: reportUnknownVariableType=false
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
import json
|
|
9
|
-
from base64 import b64decode
|
|
10
|
-
from binascii import Error as BinasciiError
|
|
11
8
|
from typing import Literal, cast
|
|
12
9
|
|
|
13
10
|
from anthropic.types.beta.beta_base64_image_source_param import BetaBase64ImageSourceParam
|
|
@@ -17,8 +14,15 @@ from anthropic.types.beta.beta_text_block_param import BetaTextBlockParam
|
|
|
17
14
|
from anthropic.types.beta.beta_tool_param import BetaToolParam
|
|
18
15
|
from anthropic.types.beta.beta_url_image_source_param import BetaURLImageSourceParam
|
|
19
16
|
|
|
20
|
-
from klaude_code.
|
|
21
|
-
from klaude_code.
|
|
17
|
+
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
18
|
+
from klaude_code.llm.image import parse_data_url
|
|
19
|
+
from klaude_code.llm.input_common import (
|
|
20
|
+
DeveloperAttachment,
|
|
21
|
+
attach_developer_messages,
|
|
22
|
+
merge_reminder_text,
|
|
23
|
+
split_thinking_parts,
|
|
24
|
+
)
|
|
25
|
+
from klaude_code.protocol import llm_param, message
|
|
22
26
|
|
|
23
27
|
AllowedMediaType = Literal["image/png", "image/jpeg", "image/gif", "image/webp"]
|
|
24
28
|
_INLINE_IMAGE_MEDIA_TYPES: tuple[AllowedMediaType, ...] = (
|
|
@@ -29,25 +33,12 @@ _INLINE_IMAGE_MEDIA_TYPES: tuple[AllowedMediaType, ...] = (
|
|
|
29
33
|
)
|
|
30
34
|
|
|
31
35
|
|
|
32
|
-
def _image_part_to_block(image:
|
|
33
|
-
url = image.
|
|
36
|
+
def _image_part_to_block(image: message.ImageURLPart) -> BetaImageBlockParam:
|
|
37
|
+
url = image.url
|
|
34
38
|
if url.startswith("data:"):
|
|
35
|
-
|
|
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]
|
|
39
|
+
media_type, base64_payload, _ = parse_data_url(url)
|
|
42
40
|
if media_type not in _INLINE_IMAGE_MEDIA_TYPES:
|
|
43
41
|
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
42
|
source = cast(
|
|
52
43
|
BetaBase64ImageSourceParam,
|
|
53
44
|
{
|
|
@@ -62,97 +53,108 @@ def _image_part_to_block(image: model.ImageURLPart) -> BetaImageBlockParam:
|
|
|
62
53
|
return {"type": "image", "source": source_url}
|
|
63
54
|
|
|
64
55
|
|
|
65
|
-
def
|
|
56
|
+
def _user_message_to_message(
|
|
57
|
+
msg: message.UserMessage,
|
|
58
|
+
attachment: DeveloperAttachment,
|
|
59
|
+
) -> BetaMessageParam:
|
|
66
60
|
blocks: list[BetaTextBlockParam | BetaImageBlockParam] = []
|
|
67
|
-
for
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
for part in msg.parts:
|
|
62
|
+
if isinstance(part, message.TextPart):
|
|
63
|
+
blocks.append({"type": "text", "text": part.text})
|
|
64
|
+
elif isinstance(part, message.ImageURLPart):
|
|
65
|
+
blocks.append(_image_part_to_block(part))
|
|
66
|
+
if attachment.text:
|
|
67
|
+
blocks.append({"type": "text", "text": attachment.text})
|
|
68
|
+
for image in attachment.images:
|
|
70
69
|
blocks.append(_image_part_to_block(image))
|
|
71
70
|
if not blocks:
|
|
72
71
|
blocks.append({"type": "text", "text": ""})
|
|
73
72
|
return {"role": "user", "content": blocks}
|
|
74
73
|
|
|
75
74
|
|
|
76
|
-
def
|
|
77
|
-
|
|
75
|
+
def _tool_message_to_block(
|
|
76
|
+
msg: message.ToolResultMessage,
|
|
77
|
+
attachment: DeveloperAttachment,
|
|
78
|
+
) -> dict[str, object]:
|
|
79
|
+
"""Convert a single tool result message to a tool_result block."""
|
|
78
80
|
tool_content: list[BetaTextBlockParam | BetaImageBlockParam] = []
|
|
79
81
|
merged_text = merge_reminder_text(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
|
|
83
|
+
attachment.text,
|
|
82
84
|
)
|
|
83
85
|
tool_content.append({"type": "text", "text": merged_text})
|
|
84
|
-
for image in
|
|
86
|
+
for image in [part for part in msg.parts if isinstance(part, message.ImageURLPart)]:
|
|
85
87
|
tool_content.append(_image_part_to_block(image))
|
|
86
|
-
for image in
|
|
88
|
+
for image in attachment.images:
|
|
87
89
|
tool_content.append(_image_part_to_block(image))
|
|
88
90
|
return {
|
|
89
91
|
"type": "tool_result",
|
|
90
|
-
"tool_use_id":
|
|
91
|
-
"is_error":
|
|
92
|
+
"tool_use_id": msg.call_id,
|
|
93
|
+
"is_error": msg.status != "success",
|
|
92
94
|
"content": tool_content,
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
|
|
96
|
-
def
|
|
97
|
-
"""Convert one or more
|
|
98
|
+
def _tool_blocks_to_message(blocks: list[dict[str, object]]) -> BetaMessageParam:
|
|
99
|
+
"""Convert one or more tool_result blocks to a single user message."""
|
|
98
100
|
return {
|
|
99
101
|
"role": "user",
|
|
100
|
-
"content":
|
|
102
|
+
"content": blocks,
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
|
|
104
|
-
def
|
|
106
|
+
def _assistant_message_to_message(msg: message.AssistantMessage, model_name: str | None) -> BetaMessageParam:
|
|
105
107
|
content: list[dict[str, object]] = []
|
|
106
|
-
|
|
107
|
-
degraded_thinking_texts
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
108
|
+
current_thinking_content: str | None = None
|
|
109
|
+
native_thinking_parts, degraded_thinking_texts = split_thinking_parts(msg, model_name)
|
|
110
|
+
native_thinking_ids = {id(part) for part in native_thinking_parts}
|
|
111
|
+
|
|
112
|
+
def _flush_thinking() -> None:
|
|
113
|
+
nonlocal current_thinking_content
|
|
114
|
+
if current_thinking_content is None:
|
|
115
|
+
return
|
|
116
|
+
content.append({"type": "thinking", "thinking": current_thinking_content})
|
|
117
|
+
current_thinking_content = None
|
|
118
|
+
|
|
119
|
+
for part in msg.parts:
|
|
120
|
+
if isinstance(part, message.ThinkingTextPart):
|
|
121
|
+
if id(part) not in native_thinking_ids:
|
|
122
|
+
continue
|
|
123
|
+
current_thinking_content = part.text
|
|
124
|
+
continue
|
|
125
|
+
if isinstance(part, message.ThinkingSignaturePart):
|
|
126
|
+
if id(part) not in native_thinking_ids:
|
|
127
|
+
continue
|
|
128
|
+
if part.signature:
|
|
123
129
|
content.append(
|
|
124
130
|
{
|
|
125
131
|
"type": "thinking",
|
|
126
|
-
"thinking":
|
|
127
|
-
"signature":
|
|
132
|
+
"thinking": current_thinking_content or "",
|
|
133
|
+
"signature": part.signature,
|
|
128
134
|
}
|
|
129
135
|
)
|
|
130
|
-
|
|
136
|
+
current_thinking_content = None
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
_flush_thinking()
|
|
140
|
+
if isinstance(part, message.TextPart):
|
|
141
|
+
content.append({"type": "text", "text": part.text})
|
|
142
|
+
elif isinstance(part, message.ToolCallPart):
|
|
143
|
+
content.append(
|
|
144
|
+
{
|
|
145
|
+
"type": "tool_use",
|
|
146
|
+
"id": part.call_id,
|
|
147
|
+
"name": part.tool_name,
|
|
148
|
+
"input": json.loads(part.arguments_json) if part.arguments_json else None,
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
_flush_thinking()
|
|
131
153
|
|
|
132
|
-
# Moonshot.ai's Kimi does not always send reasoning signatures;
|
|
133
|
-
# if we saw reasoning text without any matching encrypted item,
|
|
134
|
-
# emit it as a plain thinking block.
|
|
135
|
-
if len(current_reasoning_content or "") > 0:
|
|
136
|
-
content.insert(0, {"type": "thinking", "thinking": current_reasoning_content})
|
|
137
|
-
|
|
138
|
-
# Cross-model: degrade thinking to plain text with <thinking> tags
|
|
139
154
|
if degraded_thinking_texts:
|
|
140
155
|
degraded_text = "<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"
|
|
141
156
|
content.insert(0, {"type": "text", "text": degraded_text})
|
|
142
157
|
|
|
143
|
-
if group.text_content:
|
|
144
|
-
content.append({"type": "text", "text": group.text_content})
|
|
145
|
-
|
|
146
|
-
for tc in group.tool_calls:
|
|
147
|
-
content.append(
|
|
148
|
-
{
|
|
149
|
-
"type": "tool_use",
|
|
150
|
-
"id": tc.call_id,
|
|
151
|
-
"name": tc.name,
|
|
152
|
-
"input": json.loads(tc.arguments) if tc.arguments else None,
|
|
153
|
-
}
|
|
154
|
-
)
|
|
155
|
-
|
|
156
158
|
return {"role": "assistant", "content": content}
|
|
157
159
|
|
|
158
160
|
|
|
@@ -167,45 +169,51 @@ def _add_cache_control(messages: list[BetaMessageParam]) -> None:
|
|
|
167
169
|
|
|
168
170
|
|
|
169
171
|
def convert_history_to_input(
|
|
170
|
-
history: list[
|
|
172
|
+
history: list[message.Message],
|
|
171
173
|
model_name: str | None,
|
|
172
174
|
) -> list[BetaMessageParam]:
|
|
173
|
-
"""
|
|
174
|
-
Convert a list of conversation items to a list of beta message params.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
history: List of conversation items.
|
|
178
|
-
model_name: Model name. Used to verify that signatures are valid for the same model
|
|
179
|
-
"""
|
|
175
|
+
"""Convert a list of messages to beta message params."""
|
|
180
176
|
messages: list[BetaMessageParam] = []
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def
|
|
184
|
-
nonlocal
|
|
185
|
-
if
|
|
186
|
-
messages.append(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for
|
|
190
|
-
match
|
|
191
|
-
case
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
case
|
|
197
|
-
|
|
198
|
-
messages.append(
|
|
199
|
-
|
|
200
|
-
|
|
177
|
+
pending_tool_blocks: list[dict[str, object]] = []
|
|
178
|
+
|
|
179
|
+
def flush_tool_blocks() -> None:
|
|
180
|
+
nonlocal pending_tool_blocks
|
|
181
|
+
if pending_tool_blocks:
|
|
182
|
+
messages.append(_tool_blocks_to_message(pending_tool_blocks))
|
|
183
|
+
pending_tool_blocks = []
|
|
184
|
+
|
|
185
|
+
for msg, attachment in attach_developer_messages(history):
|
|
186
|
+
match msg:
|
|
187
|
+
case message.ToolResultMessage():
|
|
188
|
+
pending_tool_blocks.append(_tool_message_to_block(msg, attachment))
|
|
189
|
+
case message.UserMessage():
|
|
190
|
+
flush_tool_blocks()
|
|
191
|
+
messages.append(_user_message_to_message(msg, attachment))
|
|
192
|
+
case message.AssistantMessage():
|
|
193
|
+
flush_tool_blocks()
|
|
194
|
+
messages.append(_assistant_message_to_message(msg, model_name))
|
|
195
|
+
case message.SystemMessage():
|
|
196
|
+
continue
|
|
197
|
+
case _:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
flush_tool_blocks()
|
|
201
201
|
_add_cache_control(messages)
|
|
202
202
|
return messages
|
|
203
203
|
|
|
204
204
|
|
|
205
|
-
def convert_system_to_input(
|
|
206
|
-
|
|
205
|
+
def convert_system_to_input(
|
|
206
|
+
system: str | None, system_messages: list[message.SystemMessage] | None = None
|
|
207
|
+
) -> list[BetaTextBlockParam]:
|
|
208
|
+
parts: list[str] = []
|
|
209
|
+
if system:
|
|
210
|
+
parts.append(system)
|
|
211
|
+
if system_messages:
|
|
212
|
+
for msg in system_messages:
|
|
213
|
+
parts.append("\n".join(part.text for part in msg.parts))
|
|
214
|
+
if not parts:
|
|
207
215
|
return []
|
|
208
|
-
return [{"type": "text", "text":
|
|
216
|
+
return [{"type": "text", "text": "\n".join(parts), "cache_control": {"type": "ephemeral"}}]
|
|
209
217
|
|
|
210
218
|
|
|
211
219
|
def convert_tool_schema(
|
|
@@ -8,12 +8,13 @@ import anthropic
|
|
|
8
8
|
import httpx
|
|
9
9
|
from anthropic import APIError
|
|
10
10
|
|
|
11
|
+
from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
|
|
11
12
|
from klaude_code.llm.anthropic.client import build_payload, parse_anthropic_stream
|
|
12
13
|
from klaude_code.llm.client import LLMClientABC
|
|
13
14
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
14
15
|
from klaude_code.llm.registry import register
|
|
15
|
-
from klaude_code.llm.usage import MetadataTracker
|
|
16
|
-
from klaude_code.protocol import llm_param,
|
|
16
|
+
from klaude_code.llm.usage import MetadataTracker, error_stream_items
|
|
17
|
+
from klaude_code.protocol import llm_param, message
|
|
17
18
|
from klaude_code.trace import DebugType, log_debug
|
|
18
19
|
|
|
19
20
|
|
|
@@ -29,7 +30,7 @@ class BedrockClient(LLMClientABC):
|
|
|
29
30
|
aws_region=config.aws_region,
|
|
30
31
|
aws_session_token=config.aws_session_token,
|
|
31
32
|
aws_profile=config.aws_profile,
|
|
32
|
-
timeout=httpx.Timeout(
|
|
33
|
+
timeout=httpx.Timeout(LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ),
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
@classmethod
|
|
@@ -38,7 +39,7 @@ class BedrockClient(LLMClientABC):
|
|
|
38
39
|
return cls(config)
|
|
39
40
|
|
|
40
41
|
@override
|
|
41
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[
|
|
42
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
|
|
42
43
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
43
44
|
|
|
44
45
|
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
@@ -57,4 +58,6 @@ class BedrockClient(LLMClientABC):
|
|
|
57
58
|
async for item in parse_anthropic_stream(stream, param, metadata_tracker):
|
|
58
59
|
yield item
|
|
59
60
|
except (APIError, httpx.HTTPError) as e:
|
|
60
|
-
|
|
61
|
+
error_message = f"{e.__class__.__name__} {e!s}"
|
|
62
|
+
for item in error_stream_items(metadata_tracker, error=error_message):
|
|
63
|
+
yield item
|
klaude_code/llm/claude/client.py
CHANGED
|
@@ -9,17 +9,25 @@ from anthropic import APIError
|
|
|
9
9
|
from klaude_code.auth.claude.exceptions import ClaudeNotLoggedInError
|
|
10
10
|
from klaude_code.auth.claude.oauth import ClaudeOAuth
|
|
11
11
|
from klaude_code.auth.claude.token_manager import ClaudeTokenManager
|
|
12
|
+
from klaude_code.const import (
|
|
13
|
+
ANTHROPIC_BETA_FINE_GRAINED_TOOL_STREAMING,
|
|
14
|
+
ANTHROPIC_BETA_INTERLEAVED_THINKING,
|
|
15
|
+
ANTHROPIC_BETA_OAUTH,
|
|
16
|
+
LLM_HTTP_TIMEOUT_CONNECT,
|
|
17
|
+
LLM_HTTP_TIMEOUT_READ,
|
|
18
|
+
LLM_HTTP_TIMEOUT_TOTAL,
|
|
19
|
+
)
|
|
12
20
|
from klaude_code.llm.anthropic.client import build_payload, parse_anthropic_stream
|
|
13
21
|
from klaude_code.llm.client import LLMClientABC
|
|
14
22
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
15
23
|
from klaude_code.llm.registry import register
|
|
16
|
-
from klaude_code.llm.usage import MetadataTracker
|
|
17
|
-
from klaude_code.protocol import llm_param,
|
|
24
|
+
from klaude_code.llm.usage import MetadataTracker, error_stream_items
|
|
25
|
+
from klaude_code.protocol import llm_param, message
|
|
18
26
|
from klaude_code.trace import DebugType, log_debug
|
|
19
27
|
|
|
20
28
|
_CLAUDE_OAUTH_REQUIRED_BETAS: tuple[str, ...] = (
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
ANTHROPIC_BETA_OAUTH,
|
|
30
|
+
ANTHROPIC_BETA_FINE_GRAINED_TOOL_STREAMING,
|
|
23
31
|
)
|
|
24
32
|
|
|
25
33
|
|
|
@@ -45,7 +53,7 @@ class ClaudeClient(LLMClientABC):
|
|
|
45
53
|
token = self._oauth.ensure_valid_token()
|
|
46
54
|
return anthropic.AsyncAnthropic(
|
|
47
55
|
auth_token=token,
|
|
48
|
-
timeout=httpx.Timeout(
|
|
56
|
+
timeout=httpx.Timeout(LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ),
|
|
49
57
|
)
|
|
50
58
|
|
|
51
59
|
def _ensure_valid_token(self) -> None:
|
|
@@ -63,7 +71,7 @@ class ClaudeClient(LLMClientABC):
|
|
|
63
71
|
return cls(config)
|
|
64
72
|
|
|
65
73
|
@override
|
|
66
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[
|
|
74
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
|
|
67
75
|
self._ensure_valid_token()
|
|
68
76
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
69
77
|
|
|
@@ -75,7 +83,7 @@ class ClaudeClient(LLMClientABC):
|
|
|
75
83
|
|
|
76
84
|
# Keep the interleaved-thinking beta in sync with configured thinking.
|
|
77
85
|
if not (param.thinking and param.thinking.type == "enabled"):
|
|
78
|
-
payload["betas"] = [b for b in payload.get("betas", []) if b !=
|
|
86
|
+
payload["betas"] = [b for b in payload.get("betas", []) if b != ANTHROPIC_BETA_INTERLEAVED_THINKING]
|
|
79
87
|
|
|
80
88
|
log_debug(
|
|
81
89
|
json.dumps(payload, ensure_ascii=False, default=str),
|
|
@@ -92,4 +100,6 @@ class ClaudeClient(LLMClientABC):
|
|
|
92
100
|
async for item in parse_anthropic_stream(stream, param, metadata_tracker):
|
|
93
101
|
yield item
|
|
94
102
|
except (APIError, httpx.HTTPError) as e:
|
|
95
|
-
|
|
103
|
+
error_message = f"{e.__class__.__name__} {e!s}"
|
|
104
|
+
for item in error_stream_items(metadata_tracker, error=error_message):
|
|
105
|
+
yield item
|