klaude-code 1.8.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 +97 -0
- klaude_code/auth/claude/__init__.py +6 -0
- klaude_code/auth/claude/exceptions.py +9 -0
- klaude_code/auth/claude/oauth.py +172 -0
- klaude_code/auth/claude/token_manager.py +26 -0
- klaude_code/auth/codex/token_manager.py +10 -50
- klaude_code/cli/auth_cmd.py +127 -46
- klaude_code/cli/config_cmd.py +4 -2
- klaude_code/cli/cost_cmd.py +14 -9
- klaude_code/cli/list_model.py +248 -200
- 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 +82 -0
- 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 +52 -3
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +31 -7
- klaude_code/config/thinking.py +4 -4
- 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/prompt.py +1 -1
- 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 +104 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/__init__.py +3 -0
- klaude_code/llm/claude/client.py +105 -0
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +16 -10
- 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 -15
- 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 +22 -3
- 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 +2 -2
- 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 +125 -53
- 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 +39 -31
- 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 +13 -6
- 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 +55 -0
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +28 -6
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code/command/prompt-jj-describe.md +0 -32
- klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
- klaude_code/protocol/sub_agent/oracle.py +0 -91
- klaude_code-1.8.0.dist-info/RECORD +0 -219
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,6 +6,7 @@ from typing import Any, override
|
|
|
6
6
|
import anthropic
|
|
7
7
|
import httpx
|
|
8
8
|
from anthropic import APIError
|
|
9
|
+
from anthropic.types.beta import BetaTextBlockParam
|
|
9
10
|
from anthropic.types.beta.beta_input_json_delta import BetaInputJSONDelta
|
|
10
11
|
from anthropic.types.beta.beta_raw_content_block_delta_event import BetaRawContentBlockDeltaEvent
|
|
11
12
|
from anthropic.types.beta.beta_raw_content_block_start_event import BetaRawContentBlockStartEvent
|
|
@@ -18,21 +19,68 @@ from anthropic.types.beta.beta_thinking_delta import BetaThinkingDelta
|
|
|
18
19
|
from anthropic.types.beta.beta_tool_use_block import BetaToolUseBlock
|
|
19
20
|
from anthropic.types.beta.message_create_params import MessageCreateParamsStreaming
|
|
20
21
|
|
|
21
|
-
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
|
+
)
|
|
22
32
|
from klaude_code.llm.anthropic.input import convert_history_to_input, convert_system_to_input, convert_tool_schema
|
|
23
33
|
from klaude_code.llm.client import LLMClientABC
|
|
24
34
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
25
35
|
from klaude_code.llm.registry import register
|
|
26
|
-
from klaude_code.llm.usage import MetadataTracker
|
|
27
|
-
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
|
|
28
38
|
from klaude_code.trace import DebugType, log_debug
|
|
29
39
|
|
|
30
40
|
|
|
31
|
-
def
|
|
32
|
-
|
|
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)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_payload(
|
|
57
|
+
param: llm_param.LLMCallParameter,
|
|
58
|
+
*,
|
|
59
|
+
extra_betas: list[str] | None = None,
|
|
60
|
+
) -> MessageCreateParamsStreaming:
|
|
61
|
+
"""Build Anthropic API request parameters.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
param: LLM call parameters.
|
|
65
|
+
extra_betas: Additional beta flags to prepend to the betas list.
|
|
66
|
+
"""
|
|
33
67
|
messages = convert_history_to_input(param.input, param.model)
|
|
34
68
|
tools = convert_tool_schema(param.tools)
|
|
35
|
-
|
|
69
|
+
system_messages = [msg for msg in param.input if isinstance(msg, message.SystemMessage)]
|
|
70
|
+
system = convert_system_to_input(param.system, system_messages)
|
|
71
|
+
|
|
72
|
+
# Add identity block at the beginning of the system prompt
|
|
73
|
+
identity_block: BetaTextBlockParam = {
|
|
74
|
+
"type": "text",
|
|
75
|
+
"text": CLAUDE_CODE_IDENTITY,
|
|
76
|
+
"cache_control": {"type": "ephemeral"},
|
|
77
|
+
}
|
|
78
|
+
system = [identity_block, *system]
|
|
79
|
+
|
|
80
|
+
betas = [ANTHROPIC_BETA_INTERLEAVED_THINKING]
|
|
81
|
+
if extra_betas:
|
|
82
|
+
# Prepend extra betas, avoiding duplicates
|
|
83
|
+
betas = [b for b in extra_betas if b not in betas] + betas
|
|
36
84
|
|
|
37
85
|
payload: MessageCreateParamsStreaming = {
|
|
38
86
|
"model": str(param.model),
|
|
@@ -41,18 +89,18 @@ def build_payload(param: llm_param.LLMCallParameter) -> MessageCreateParamsStrea
|
|
|
41
89
|
"disable_parallel_tool_use": False,
|
|
42
90
|
},
|
|
43
91
|
"stream": True,
|
|
44
|
-
"max_tokens": param.max_tokens or
|
|
45
|
-
"temperature": param.temperature or
|
|
92
|
+
"max_tokens": param.max_tokens or DEFAULT_MAX_TOKENS,
|
|
93
|
+
"temperature": param.temperature or DEFAULT_TEMPERATURE,
|
|
46
94
|
"messages": messages,
|
|
47
95
|
"system": system,
|
|
48
96
|
"tools": tools,
|
|
49
|
-
"betas":
|
|
97
|
+
"betas": betas,
|
|
50
98
|
}
|
|
51
99
|
|
|
52
100
|
if param.thinking and param.thinking.type == "enabled":
|
|
53
101
|
payload["thinking"] = anthropic.types.ThinkingConfigEnabledParam(
|
|
54
102
|
type="enabled",
|
|
55
|
-
budget_tokens=param.thinking.budget_tokens or
|
|
103
|
+
budget_tokens=param.thinking.budget_tokens or DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS,
|
|
56
104
|
)
|
|
57
105
|
|
|
58
106
|
return payload
|
|
@@ -62,14 +110,14 @@ async def parse_anthropic_stream(
|
|
|
62
110
|
stream: Any,
|
|
63
111
|
param: llm_param.LLMCallParameter,
|
|
64
112
|
metadata_tracker: MetadataTracker,
|
|
65
|
-
) -> AsyncGenerator[
|
|
66
|
-
"""Parse Anthropic beta messages stream and yield
|
|
67
|
-
|
|
68
|
-
This function is shared between AnthropicClient and BedrockClient.
|
|
69
|
-
"""
|
|
113
|
+
) -> AsyncGenerator[message.LLMStreamItem]:
|
|
114
|
+
"""Parse Anthropic beta messages stream and yield stream items."""
|
|
70
115
|
accumulated_thinking: list[str] = []
|
|
71
116
|
accumulated_content: list[str] = []
|
|
117
|
+
parts: list[message.Part] = []
|
|
72
118
|
response_id: str | None = None
|
|
119
|
+
stop_reason: model.StopReason | None = None
|
|
120
|
+
pending_signature: str | None = None
|
|
73
121
|
|
|
74
122
|
current_tool_name: str | None = None
|
|
75
123
|
current_tool_call_id: str | None = None
|
|
@@ -90,28 +138,23 @@ async def parse_anthropic_stream(
|
|
|
90
138
|
response_id = event.message.id
|
|
91
139
|
cached_token = event.message.usage.cache_read_input_tokens or 0
|
|
92
140
|
input_token = event.message.usage.input_tokens
|
|
93
|
-
yield model.StartItem(response_id=response_id)
|
|
94
141
|
case BetaRawContentBlockDeltaEvent() as event:
|
|
95
142
|
match event.delta:
|
|
96
143
|
case BetaThinkingDelta() as delta:
|
|
97
144
|
if delta.thinking:
|
|
98
145
|
metadata_tracker.record_token()
|
|
99
146
|
accumulated_thinking.append(delta.thinking)
|
|
100
|
-
yield
|
|
147
|
+
yield message.ThinkingTextDelta(
|
|
101
148
|
content=delta.thinking,
|
|
102
149
|
response_id=response_id,
|
|
103
150
|
)
|
|
104
151
|
case BetaSignatureDelta() as delta:
|
|
105
|
-
|
|
106
|
-
encrypted_content=delta.signature,
|
|
107
|
-
response_id=response_id,
|
|
108
|
-
model=str(param.model),
|
|
109
|
-
)
|
|
152
|
+
pending_signature = delta.signature
|
|
110
153
|
case BetaTextDelta() as delta:
|
|
111
154
|
if delta.text:
|
|
112
155
|
metadata_tracker.record_token()
|
|
113
156
|
accumulated_content.append(delta.text)
|
|
114
|
-
yield
|
|
157
|
+
yield message.AssistantTextDelta(
|
|
115
158
|
content=delta.text,
|
|
116
159
|
response_id=response_id,
|
|
117
160
|
)
|
|
@@ -126,7 +169,7 @@ async def parse_anthropic_stream(
|
|
|
126
169
|
match event.content_block:
|
|
127
170
|
case BetaToolUseBlock() as block:
|
|
128
171
|
metadata_tracker.record_token()
|
|
129
|
-
yield
|
|
172
|
+
yield message.ToolCallStartItem(
|
|
130
173
|
response_id=response_id,
|
|
131
174
|
call_id=block.id,
|
|
132
175
|
name=block.name,
|
|
@@ -137,29 +180,32 @@ async def parse_anthropic_stream(
|
|
|
137
180
|
case _:
|
|
138
181
|
pass
|
|
139
182
|
case BetaRawContentBlockStopEvent():
|
|
140
|
-
if
|
|
183
|
+
if accumulated_thinking:
|
|
141
184
|
metadata_tracker.record_token()
|
|
142
185
|
full_thinking = "".join(accumulated_thinking)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
)
|
|
148
195
|
accumulated_thinking.clear()
|
|
149
|
-
|
|
196
|
+
pending_signature = None
|
|
197
|
+
if accumulated_content:
|
|
150
198
|
metadata_tracker.record_token()
|
|
151
|
-
|
|
152
|
-
content="".join(accumulated_content),
|
|
153
|
-
response_id=response_id,
|
|
154
|
-
)
|
|
199
|
+
parts.append(message.TextPart(text="".join(accumulated_content)))
|
|
155
200
|
accumulated_content.clear()
|
|
156
201
|
if current_tool_name and current_tool_call_id:
|
|
157
202
|
metadata_tracker.record_token()
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
)
|
|
163
209
|
)
|
|
164
210
|
current_tool_name = None
|
|
165
211
|
current_tool_call_id = None
|
|
@@ -177,10 +223,20 @@ async def parse_anthropic_stream(
|
|
|
177
223
|
)
|
|
178
224
|
metadata_tracker.set_model_name(str(param.model))
|
|
179
225
|
metadata_tracker.set_response_id(response_id)
|
|
180
|
-
|
|
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)
|
|
181
229
|
case _:
|
|
182
230
|
pass
|
|
183
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
|
+
|
|
184
240
|
|
|
185
241
|
@register(llm_param.LLMClientProtocol.ANTHROPIC)
|
|
186
242
|
class AnthropicClient(LLMClientABC):
|
|
@@ -195,7 +251,9 @@ class AnthropicClient(LLMClientABC):
|
|
|
195
251
|
client = anthropic.AsyncAnthropic(
|
|
196
252
|
api_key=config.api_key,
|
|
197
253
|
base_url=config.base_url,
|
|
198
|
-
timeout=httpx.Timeout(
|
|
254
|
+
timeout=httpx.Timeout(
|
|
255
|
+
LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ
|
|
256
|
+
),
|
|
199
257
|
)
|
|
200
258
|
finally:
|
|
201
259
|
if saved_auth_token is not None:
|
|
@@ -208,7 +266,7 @@ class AnthropicClient(LLMClientABC):
|
|
|
208
266
|
return cls(config)
|
|
209
267
|
|
|
210
268
|
@override
|
|
211
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[
|
|
269
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
|
|
212
270
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
213
271
|
|
|
214
272
|
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
@@ -230,4 +288,6 @@ class AnthropicClient(LLMClientABC):
|
|
|
230
288
|
async for item in parse_anthropic_stream(stream, param, metadata_tracker):
|
|
231
289
|
yield item
|
|
232
290
|
except (APIError, httpx.HTTPError) as e:
|
|
233
|
-
|
|
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
|