klaude-code 1.9.0__py3-none-any.whl → 2.0.1__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/cost_cmd.py +1 -1
- 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 +20 -0
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +7 -2
- klaude_code/const.py +147 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +18 -39
- 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 +9 -10
- 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 +95 -285
- klaude_code/protocol/op.py +2 -15
- klaude_code/protocol/op_handler.py +0 -5
- 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 +138 -132
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +136 -2
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/bash_syntax.py +36 -4
- klaude_code/ui/renderers/common.py +70 -10
- 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 +188 -178
- 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 +20 -14
- 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.1.dist-info}/METADATA +4 -2
- klaude_code-2.0.1.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.1.dist-info}/WHEEL +0 -0
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.1.dist-info}/entry_points.txt +0 -0
|
@@ -6,29 +6,49 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from openai.types import responses
|
|
8
8
|
|
|
9
|
-
from klaude_code.
|
|
9
|
+
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
10
|
+
from klaude_code.llm.input_common import (
|
|
11
|
+
DeveloperAttachment,
|
|
12
|
+
attach_developer_messages,
|
|
13
|
+
merge_reminder_text,
|
|
14
|
+
split_thinking_parts,
|
|
15
|
+
)
|
|
16
|
+
from klaude_code.protocol import llm_param, message
|
|
10
17
|
|
|
11
18
|
|
|
12
19
|
def _build_user_content_parts(
|
|
13
|
-
user:
|
|
20
|
+
user: message.UserMessage,
|
|
21
|
+
attachment: DeveloperAttachment,
|
|
14
22
|
) -> list[responses.ResponseInputContentParam]:
|
|
15
23
|
parts: list[responses.ResponseInputContentParam] = []
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
for part in user.parts:
|
|
25
|
+
if isinstance(part, message.TextPart):
|
|
26
|
+
parts.append({"type": "input_text", "text": part.text})
|
|
27
|
+
elif isinstance(part, message.ImageURLPart):
|
|
28
|
+
parts.append({"type": "input_image", "detail": "auto", "image_url": part.url})
|
|
29
|
+
if attachment.text:
|
|
30
|
+
parts.append({"type": "input_text", "text": attachment.text})
|
|
31
|
+
for image in attachment.images:
|
|
32
|
+
parts.append({"type": "input_image", "detail": "auto", "image_url": image.url})
|
|
20
33
|
if not parts:
|
|
21
34
|
parts.append({"type": "input_text", "text": ""})
|
|
22
35
|
return parts
|
|
23
36
|
|
|
24
37
|
|
|
25
|
-
def _build_tool_result_item(
|
|
38
|
+
def _build_tool_result_item(
|
|
39
|
+
tool: message.ToolResultMessage,
|
|
40
|
+
attachment: DeveloperAttachment,
|
|
41
|
+
) -> responses.ResponseInputItemParam:
|
|
26
42
|
content_parts: list[responses.ResponseInputContentParam] = []
|
|
27
|
-
text_output =
|
|
43
|
+
text_output = merge_reminder_text(
|
|
44
|
+
tool.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
|
|
45
|
+
attachment.text,
|
|
46
|
+
)
|
|
28
47
|
if text_output:
|
|
29
48
|
content_parts.append({"type": "input_text", "text": text_output})
|
|
30
|
-
for
|
|
31
|
-
|
|
49
|
+
images = [part for part in tool.parts if isinstance(part, message.ImageURLPart)] + attachment.images
|
|
50
|
+
for image in images:
|
|
51
|
+
content_parts.append({"type": "input_image", "detail": "auto", "image_url": image.url})
|
|
32
52
|
|
|
33
53
|
item: dict[str, Any] = {
|
|
34
54
|
"type": "function_call_output",
|
|
@@ -39,103 +59,105 @@ def _build_tool_result_item(tool: model.ToolResultItem) -> responses.ResponseInp
|
|
|
39
59
|
|
|
40
60
|
|
|
41
61
|
def convert_history_to_input(
|
|
42
|
-
history: list[
|
|
62
|
+
history: list[message.Message],
|
|
43
63
|
model_name: str | None = None,
|
|
44
64
|
) -> responses.ResponseInputParam:
|
|
45
|
-
"""
|
|
46
|
-
Convert a list of conversation items to a list of response input params.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
history: List of conversation items.
|
|
50
|
-
model_name: Model name. Used to verify that signatures are valid for the same model.
|
|
51
|
-
"""
|
|
65
|
+
"""Convert a list of messages to response input params."""
|
|
52
66
|
items: list[responses.ResponseInputItemParam] = []
|
|
53
67
|
|
|
54
|
-
pending_reasoning_text: str | None = None
|
|
55
68
|
degraded_thinking_texts: list[str] = []
|
|
56
69
|
|
|
57
|
-
for
|
|
58
|
-
match
|
|
59
|
-
case
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
pending_reasoning_text = None
|
|
76
|
-
|
|
77
|
-
case model.ToolCallItem() as t:
|
|
78
|
-
items.append(
|
|
79
|
-
{
|
|
80
|
-
"type": "function_call",
|
|
81
|
-
"name": t.name,
|
|
82
|
-
"arguments": t.arguments,
|
|
83
|
-
"call_id": t.call_id,
|
|
84
|
-
"id": t.id,
|
|
85
|
-
}
|
|
86
|
-
)
|
|
87
|
-
case model.ToolResultItem() as t:
|
|
88
|
-
items.append(_build_tool_result_item(t))
|
|
89
|
-
case model.AssistantMessageItem() as a:
|
|
90
|
-
items.append(
|
|
91
|
-
{
|
|
92
|
-
"type": "message",
|
|
93
|
-
"role": "assistant",
|
|
94
|
-
"id": a.id,
|
|
95
|
-
"content": [
|
|
96
|
-
{
|
|
97
|
-
"type": "output_text",
|
|
98
|
-
"text": a.content,
|
|
99
|
-
}
|
|
100
|
-
],
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
case model.UserMessageItem() as u:
|
|
70
|
+
for msg, attachment in attach_developer_messages(history):
|
|
71
|
+
match msg:
|
|
72
|
+
case message.SystemMessage():
|
|
73
|
+
system_text = "\n".join(part.text for part in msg.parts)
|
|
74
|
+
if system_text:
|
|
75
|
+
items.append(
|
|
76
|
+
{
|
|
77
|
+
"type": "message",
|
|
78
|
+
"role": "system",
|
|
79
|
+
"content": [
|
|
80
|
+
{
|
|
81
|
+
"type": "input_text",
|
|
82
|
+
"text": system_text,
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
case message.UserMessage():
|
|
104
88
|
items.append(
|
|
105
89
|
{
|
|
106
90
|
"type": "message",
|
|
107
91
|
"role": "user",
|
|
108
|
-
"id":
|
|
109
|
-
"content": _build_user_content_parts(
|
|
92
|
+
"id": msg.id,
|
|
93
|
+
"content": _build_user_content_parts(msg, attachment),
|
|
110
94
|
}
|
|
111
95
|
)
|
|
112
|
-
case
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
96
|
+
case message.ToolResultMessage():
|
|
97
|
+
items.append(_build_tool_result_item(msg, attachment))
|
|
98
|
+
case message.AssistantMessage():
|
|
99
|
+
assistant_text_parts: list[responses.ResponseInputContentParam] = []
|
|
100
|
+
pending_thinking_text: str | None = None
|
|
101
|
+
pending_signature: str | None = None
|
|
102
|
+
native_thinking_parts, degraded_for_message = split_thinking_parts(msg, model_name)
|
|
103
|
+
native_thinking_ids = {id(part) for part in native_thinking_parts}
|
|
104
|
+
degraded_thinking_texts.extend(degraded_for_message)
|
|
105
|
+
|
|
106
|
+
def flush_text(*, _message_id: str = msg.id) -> None:
|
|
107
|
+
nonlocal assistant_text_parts
|
|
108
|
+
if not assistant_text_parts:
|
|
109
|
+
return
|
|
110
|
+
items.append(
|
|
118
111
|
{
|
|
119
|
-
"type": "
|
|
120
|
-
"
|
|
121
|
-
"
|
|
112
|
+
"type": "message",
|
|
113
|
+
"role": "assistant",
|
|
114
|
+
"id": _message_id,
|
|
115
|
+
"content": assistant_text_parts,
|
|
122
116
|
}
|
|
123
117
|
)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
118
|
+
assistant_text_parts = []
|
|
119
|
+
|
|
120
|
+
def emit_reasoning() -> None:
|
|
121
|
+
nonlocal pending_thinking_text, pending_signature
|
|
122
|
+
if pending_thinking_text is None and pending_signature is None:
|
|
123
|
+
return
|
|
124
|
+
items.append(convert_reasoning_inputs(pending_thinking_text, pending_signature))
|
|
125
|
+
pending_thinking_text = None
|
|
126
|
+
pending_signature = None
|
|
127
|
+
|
|
128
|
+
for part in msg.parts:
|
|
129
|
+
if isinstance(part, message.ThinkingTextPart):
|
|
130
|
+
if id(part) not in native_thinking_ids:
|
|
131
|
+
continue
|
|
132
|
+
emit_reasoning()
|
|
133
|
+
pending_thinking_text = part.text
|
|
134
|
+
continue
|
|
135
|
+
if isinstance(part, message.ThinkingSignaturePart):
|
|
136
|
+
if id(part) not in native_thinking_ids:
|
|
137
|
+
continue
|
|
138
|
+
pending_signature = part.signature
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
emit_reasoning()
|
|
142
|
+
if isinstance(part, message.TextPart):
|
|
143
|
+
assistant_text_parts.append({"type": "output_text", "text": part.text})
|
|
144
|
+
elif isinstance(part, message.ToolCallPart):
|
|
145
|
+
flush_text()
|
|
146
|
+
items.append(
|
|
147
|
+
{
|
|
148
|
+
"type": "function_call",
|
|
149
|
+
"name": part.tool_name,
|
|
150
|
+
"arguments": part.arguments_json,
|
|
151
|
+
"call_id": part.call_id,
|
|
152
|
+
"id": part.id,
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
emit_reasoning()
|
|
157
|
+
flush_text()
|
|
134
158
|
case _:
|
|
135
|
-
# Other items may be Metadata
|
|
136
159
|
continue
|
|
137
160
|
|
|
138
|
-
# Cross-model: degrade thinking to plain text with <thinking> tags
|
|
139
161
|
if degraded_thinking_texts:
|
|
140
162
|
degraded_item: responses.ResponseInputItemParam = {
|
|
141
163
|
"type": "message",
|
|
@@ -152,21 +174,16 @@ def convert_history_to_input(
|
|
|
152
174
|
return items
|
|
153
175
|
|
|
154
176
|
|
|
155
|
-
def convert_reasoning_inputs(
|
|
156
|
-
|
|
157
|
-
) -> responses.ResponseInputItemParam:
|
|
158
|
-
result = {"type": "reasoning", "content": None}
|
|
159
|
-
|
|
177
|
+
def convert_reasoning_inputs(text_content: str | None, signature: str | None) -> responses.ResponseInputItemParam:
|
|
178
|
+
result: dict[str, Any] = {"type": "reasoning", "content": None}
|
|
160
179
|
result["summary"] = [
|
|
161
180
|
{
|
|
162
181
|
"type": "summary_text",
|
|
163
182
|
"text": text_content or "",
|
|
164
183
|
}
|
|
165
184
|
]
|
|
166
|
-
if
|
|
167
|
-
result["encrypted_content"] =
|
|
168
|
-
if encrypted_item.id is not None:
|
|
169
|
-
result["id"] = encrypted_item.id
|
|
185
|
+
if signature:
|
|
186
|
+
result["encrypted_content"] = signature
|
|
170
187
|
return result
|
|
171
188
|
|
|
172
189
|
|
klaude_code/llm/usage.py
CHANGED
|
@@ -2,7 +2,8 @@ import time
|
|
|
2
2
|
|
|
3
3
|
import openai.types
|
|
4
4
|
|
|
5
|
-
from klaude_code.
|
|
5
|
+
from klaude_code.const import THROUGHPUT_MIN_DURATION_SEC
|
|
6
|
+
from klaude_code.protocol import llm_param, message, model
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def calculate_cost(usage: model.Usage, cost_config: llm_param.Cost | None) -> None:
|
|
@@ -18,7 +19,7 @@ def calculate_cost(usage: model.Usage, cost_config: llm_param.Cost | None) -> No
|
|
|
18
19
|
usage.currency = cost_config.currency
|
|
19
20
|
|
|
20
21
|
# Non-cached input tokens cost
|
|
21
|
-
non_cached_input = usage.input_tokens - usage.cached_tokens
|
|
22
|
+
non_cached_input = max(0, usage.input_tokens - usage.cached_tokens)
|
|
22
23
|
usage.input_cost = (non_cached_input / 1_000_000) * cost_config.input
|
|
23
24
|
|
|
24
25
|
# Output tokens cost (includes reasoning tokens)
|
|
@@ -27,6 +28,9 @@ def calculate_cost(usage: model.Usage, cost_config: llm_param.Cost | None) -> No
|
|
|
27
28
|
# Cache read cost
|
|
28
29
|
usage.cache_read_cost = (usage.cached_tokens / 1_000_000) * cost_config.cache_read
|
|
29
30
|
|
|
31
|
+
# Image generation cost
|
|
32
|
+
usage.image_cost = (usage.image_tokens / 1_000_000) * cost_config.image
|
|
33
|
+
|
|
30
34
|
|
|
31
35
|
class MetadataTracker:
|
|
32
36
|
"""Tracks timing and metadata for LLM responses."""
|
|
@@ -35,13 +39,9 @@ class MetadataTracker:
|
|
|
35
39
|
self._request_start_time: float = time.time()
|
|
36
40
|
self._first_token_time: float | None = None
|
|
37
41
|
self._last_token_time: float | None = None
|
|
38
|
-
self.
|
|
42
|
+
self._usage = model.Usage()
|
|
39
43
|
self._cost_config = cost_config
|
|
40
44
|
|
|
41
|
-
@property
|
|
42
|
-
def metadata_item(self) -> model.ResponseMetadataItem:
|
|
43
|
-
return self._metadata_item
|
|
44
|
-
|
|
45
45
|
@property
|
|
46
46
|
def first_token_time(self) -> float | None:
|
|
47
47
|
return self._first_token_time
|
|
@@ -59,37 +59,59 @@ class MetadataTracker:
|
|
|
59
59
|
|
|
60
60
|
def set_usage(self, usage: model.Usage) -> None:
|
|
61
61
|
"""Set the usage information."""
|
|
62
|
-
|
|
62
|
+
preserved = {
|
|
63
|
+
"response_id": self._usage.response_id,
|
|
64
|
+
"model_name": self._usage.model_name,
|
|
65
|
+
"provider": self._usage.provider,
|
|
66
|
+
"task_duration_s": self._usage.task_duration_s,
|
|
67
|
+
"created_at": self._usage.created_at,
|
|
68
|
+
}
|
|
69
|
+
self._usage = usage.model_copy(update=preserved)
|
|
63
70
|
|
|
64
71
|
def set_model_name(self, model_name: str) -> None:
|
|
65
72
|
"""Set the model name."""
|
|
66
|
-
self.
|
|
73
|
+
self._usage.model_name = model_name
|
|
67
74
|
|
|
68
75
|
def set_provider(self, provider: str) -> None:
|
|
69
76
|
"""Set the provider name."""
|
|
70
|
-
self.
|
|
77
|
+
self._usage.provider = provider
|
|
71
78
|
|
|
72
79
|
def set_response_id(self, response_id: str | None) -> None:
|
|
73
80
|
"""Set the response ID."""
|
|
74
|
-
self.
|
|
81
|
+
self._usage.response_id = response_id
|
|
75
82
|
|
|
76
|
-
def finalize(self) -> model.
|
|
77
|
-
"""Finalize and return the
|
|
78
|
-
if self.
|
|
79
|
-
self.
|
|
80
|
-
self._first_token_time - self._request_start_time
|
|
81
|
-
) * 1000
|
|
83
|
+
def finalize(self) -> model.Usage:
|
|
84
|
+
"""Finalize and return the usage item with calculated performance metrics."""
|
|
85
|
+
if self._first_token_time is not None:
|
|
86
|
+
self._usage.first_token_latency_ms = (self._first_token_time - self._request_start_time) * 1000
|
|
82
87
|
|
|
83
|
-
if self._last_token_time is not None and self.
|
|
88
|
+
if self._last_token_time is not None and self._usage.output_tokens > 0:
|
|
84
89
|
time_duration = self._last_token_time - self._request_start_time
|
|
85
|
-
if time_duration >=
|
|
86
|
-
self.
|
|
90
|
+
if time_duration >= THROUGHPUT_MIN_DURATION_SEC:
|
|
91
|
+
self._usage.throughput_tps = self._usage.output_tokens / time_duration
|
|
87
92
|
|
|
88
93
|
# Calculate cost if config is available
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
calculate_cost(self._usage, self._cost_config)
|
|
95
|
+
|
|
96
|
+
return self._usage
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def usage(self) -> model.Usage:
|
|
100
|
+
return self._usage
|
|
91
101
|
|
|
92
|
-
|
|
102
|
+
|
|
103
|
+
def error_stream_items(
|
|
104
|
+
metadata_tracker: MetadataTracker,
|
|
105
|
+
*,
|
|
106
|
+
error: str,
|
|
107
|
+
response_id: str | None = None,
|
|
108
|
+
) -> list[message.LLMStreamItem]:
|
|
109
|
+
metadata_tracker.set_response_id(response_id)
|
|
110
|
+
metadata = metadata_tracker.finalize()
|
|
111
|
+
return [
|
|
112
|
+
message.StreamErrorItem(error=error),
|
|
113
|
+
message.AssistantMessage(parts=[], response_id=response_id, usage=metadata),
|
|
114
|
+
]
|
|
93
115
|
|
|
94
116
|
|
|
95
117
|
def convert_usage(
|
|
@@ -102,12 +124,17 @@ def convert_usage(
|
|
|
102
124
|
context_token is set to total_tokens from the API response,
|
|
103
125
|
representing the actual context window usage for this turn.
|
|
104
126
|
"""
|
|
127
|
+
completion_details = usage.completion_tokens_details
|
|
128
|
+
image_tokens = 0
|
|
129
|
+
if completion_details is not None:
|
|
130
|
+
image_tokens = getattr(completion_details, "image_tokens", 0) or 0
|
|
131
|
+
|
|
105
132
|
return model.Usage(
|
|
106
133
|
input_tokens=usage.prompt_tokens,
|
|
107
134
|
cached_tokens=(usage.prompt_tokens_details.cached_tokens if usage.prompt_tokens_details else 0) or 0,
|
|
108
|
-
reasoning_tokens=(
|
|
109
|
-
or 0,
|
|
135
|
+
reasoning_tokens=(completion_details.reasoning_tokens if completion_details else 0) or 0,
|
|
110
136
|
output_tokens=usage.completion_tokens,
|
|
137
|
+
image_tokens=image_tokens,
|
|
111
138
|
context_size=usage.total_tokens,
|
|
112
139
|
context_limit=context_limit,
|
|
113
140
|
max_tokens=max_tokens,
|
klaude_code/protocol/__init__.py
CHANGED
klaude_code/protocol/events.py
CHANGED
|
@@ -2,7 +2,7 @@ from typing import Literal
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
-
from klaude_code.protocol import llm_param, model
|
|
5
|
+
from klaude_code.protocol import llm_param, message, model
|
|
6
6
|
|
|
7
7
|
"""
|
|
8
8
|
Event is how Agent Executor and UI Display communicate.
|
|
@@ -50,35 +50,36 @@ class TurnToolCallStartEvent(BaseModel):
|
|
|
50
50
|
arguments: str
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
class
|
|
53
|
+
class ThinkingDeltaEvent(BaseModel):
|
|
54
54
|
session_id: str
|
|
55
55
|
response_id: str | None = None
|
|
56
56
|
content: str
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
class
|
|
59
|
+
class AssistantTextDeltaEvent(BaseModel):
|
|
60
60
|
session_id: str
|
|
61
61
|
response_id: str | None = None
|
|
62
62
|
content: str
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
class
|
|
65
|
+
class AssistantImageDeltaEvent(BaseModel):
|
|
66
66
|
session_id: str
|
|
67
67
|
response_id: str | None = None
|
|
68
|
-
|
|
68
|
+
file_path: str
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
class AssistantMessageEvent(BaseModel):
|
|
72
72
|
response_id: str | None = None
|
|
73
73
|
session_id: str
|
|
74
74
|
content: str
|
|
75
|
+
thinking_text: str | None = None
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
class DeveloperMessageEvent(BaseModel):
|
|
78
79
|
"""DeveloperMessages are reminders in user messages or tool results, see: core/reminders.py"""
|
|
79
80
|
|
|
80
81
|
session_id: str
|
|
81
|
-
item:
|
|
82
|
+
item: message.DeveloperMessage
|
|
82
83
|
|
|
83
84
|
|
|
84
85
|
class ToolCallEvent(BaseModel):
|
|
@@ -98,13 +99,16 @@ class ToolResultEvent(BaseModel):
|
|
|
98
99
|
ui_extra: model.ToolResultUIExtra | None = None
|
|
99
100
|
status: Literal["success", "error"]
|
|
100
101
|
task_metadata: model.TaskMetadata | None = None # Sub-agent task metadata
|
|
102
|
+
# Whether this tool result is the last one emitted in the current turn.
|
|
103
|
+
# Used by UI renderers to close tree-style prefixes.
|
|
104
|
+
is_last_in_turn: bool = True
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
class ResponseMetadataEvent(BaseModel):
|
|
104
108
|
"""Internal event for turn-level metadata. Not exposed to UI directly."""
|
|
105
109
|
|
|
106
110
|
session_id: str
|
|
107
|
-
metadata: model.
|
|
111
|
+
metadata: model.Usage
|
|
108
112
|
|
|
109
113
|
|
|
110
114
|
class TaskMetadataEvent(BaseModel):
|
|
@@ -117,7 +121,7 @@ class TaskMetadataEvent(BaseModel):
|
|
|
117
121
|
class UserMessageEvent(BaseModel):
|
|
118
122
|
session_id: str
|
|
119
123
|
content: str
|
|
120
|
-
images: list[
|
|
124
|
+
images: list[message.ImageURLPart] | None = None
|
|
121
125
|
|
|
122
126
|
|
|
123
127
|
class WelcomeEvent(BaseModel):
|
|
@@ -142,10 +146,10 @@ class ContextUsageEvent(BaseModel):
|
|
|
142
146
|
|
|
143
147
|
|
|
144
148
|
HistoryItemEvent = (
|
|
145
|
-
|
|
146
|
-
| TaskStartEvent
|
|
149
|
+
TaskStartEvent
|
|
147
150
|
| TaskFinishEvent
|
|
148
151
|
| TurnStartEvent # This event is used for UI to print new empty line
|
|
152
|
+
| AssistantImageDeltaEvent
|
|
149
153
|
| AssistantMessageEvent
|
|
150
154
|
| ToolCallEvent
|
|
151
155
|
| ToolResultEvent
|
|
@@ -167,9 +171,9 @@ class ReplayHistoryEvent(BaseModel):
|
|
|
167
171
|
Event = (
|
|
168
172
|
TaskStartEvent
|
|
169
173
|
| TaskFinishEvent
|
|
170
|
-
| ThinkingEvent
|
|
171
174
|
| ThinkingDeltaEvent
|
|
172
|
-
|
|
|
175
|
+
| AssistantTextDeltaEvent
|
|
176
|
+
| AssistantImageDeltaEvent
|
|
173
177
|
| AssistantMessageEvent
|
|
174
178
|
| ToolCallEvent
|
|
175
179
|
| ToolResultEvent
|
|
@@ -4,7 +4,7 @@ from typing import Any, Literal
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
from pydantic.json_schema import JsonSchemaValue
|
|
6
6
|
|
|
7
|
-
from klaude_code.protocol.
|
|
7
|
+
from klaude_code.protocol.message import Message
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class LLMClientProtocol(Enum):
|
|
@@ -39,6 +39,18 @@ class Thinking(BaseModel):
|
|
|
39
39
|
budget_tokens: int | None = None
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
class ImageConfig(BaseModel):
|
|
43
|
+
"""Image generation config (OpenRouter-compatible fields).
|
|
44
|
+
|
|
45
|
+
This is intentionally small and extensible. Additional vendor/model
|
|
46
|
+
parameters can be stored in `extra`.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
aspect_ratio: str | None = None
|
|
50
|
+
image_size: Literal["1K", "2K", "4K"] | None = None
|
|
51
|
+
extra: dict[str, Any] | None = None
|
|
52
|
+
|
|
53
|
+
|
|
42
54
|
class Cost(BaseModel):
|
|
43
55
|
"""Cost configuration per million tokens."""
|
|
44
56
|
|
|
@@ -46,6 +58,7 @@ class Cost(BaseModel):
|
|
|
46
58
|
output: float # Output token price per million tokens
|
|
47
59
|
cache_read: float = 0.0 # Cache read price per million tokens
|
|
48
60
|
cache_write: float = 0.0 # Cache write price per million tokens (ignored in calculation for now)
|
|
61
|
+
image: float = 0.0 # Image generation token price per million tokens
|
|
49
62
|
currency: Literal["USD", "CNY"] = "USD" # Currency for cost display
|
|
50
63
|
|
|
51
64
|
|
|
@@ -114,6 +127,11 @@ class LLMConfigModelParameter(BaseModel):
|
|
|
114
127
|
# OpenAI GPT-5
|
|
115
128
|
verbosity: Literal["low", "medium", "high"] | None = None
|
|
116
129
|
|
|
130
|
+
# Multimodal output control (OpenRouter image generation)
|
|
131
|
+
modalities: list[Literal["text", "image"]] | None = None
|
|
132
|
+
|
|
133
|
+
image_config: ImageConfig | None = None
|
|
134
|
+
|
|
117
135
|
# Unified Thinking & Reasoning
|
|
118
136
|
thinking: Thinking | None = None
|
|
119
137
|
|
|
@@ -145,7 +163,7 @@ class LLMCallParameter(LLMConfigModelParameter):
|
|
|
145
163
|
"""
|
|
146
164
|
|
|
147
165
|
# Agent
|
|
148
|
-
input: list[
|
|
166
|
+
input: list[Message]
|
|
149
167
|
system: str | None = None
|
|
150
168
|
tools: list[ToolSchema] | None = None
|
|
151
169
|
session_id: str | None = None
|