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
klaude_code/llm/input_common.py
CHANGED
|
@@ -1,204 +1,166 @@
|
|
|
1
|
-
"""Common utilities for converting
|
|
1
|
+
"""Common utilities for converting message history to LLM input formats."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
(Anthropic, OpenAI-compatible, OpenRouter). The Responses API doesn't need this
|
|
5
|
-
since it uses a flat item list matching our internal protocol.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from collections.abc import Iterable, Iterator
|
|
3
|
+
from collections.abc import Callable, Iterable
|
|
9
4
|
from dataclasses import dataclass, field
|
|
10
|
-
from enum import Enum
|
|
11
5
|
from typing import TYPE_CHECKING
|
|
12
6
|
|
|
13
|
-
from klaude_code import const
|
|
14
|
-
|
|
15
7
|
if TYPE_CHECKING:
|
|
16
8
|
from klaude_code.protocol.llm_param import LLMCallParameter, LLMConfigParameter
|
|
17
9
|
|
|
18
|
-
from klaude_code.
|
|
10
|
+
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
11
|
+
from klaude_code.protocol import message
|
|
19
12
|
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
USER = "user"
|
|
24
|
-
TOOL = "tool"
|
|
25
|
-
DEVELOPER = "developer"
|
|
26
|
-
OTHER = "other"
|
|
14
|
+
def _empty_image_parts() -> list[message.ImageURLPart]:
|
|
15
|
+
return []
|
|
27
16
|
|
|
28
17
|
|
|
29
18
|
@dataclass
|
|
30
|
-
class
|
|
31
|
-
|
|
19
|
+
class DeveloperAttachment:
|
|
20
|
+
text: str = ""
|
|
21
|
+
images: list[message.ImageURLPart] = field(default_factory=_empty_image_parts)
|
|
32
22
|
|
|
33
|
-
text_parts: list[str] = field(default_factory=lambda: [])
|
|
34
|
-
images: list[model.ImageURLPart] = field(default_factory=lambda: [])
|
|
35
23
|
|
|
24
|
+
def _extract_developer_content(msg: message.DeveloperMessage) -> tuple[str, list[message.ImageURLPart]]:
|
|
25
|
+
text_parts: list[str] = []
|
|
26
|
+
images: list[message.ImageURLPart] = []
|
|
27
|
+
for part in msg.parts:
|
|
28
|
+
if isinstance(part, message.TextPart):
|
|
29
|
+
text_parts.append(part.text + "\n")
|
|
30
|
+
elif isinstance(part, message.ImageURLPart):
|
|
31
|
+
images.append(part)
|
|
32
|
+
return "".join(text_parts), images
|
|
36
33
|
|
|
37
|
-
@dataclass
|
|
38
|
-
class ToolGroup:
|
|
39
|
-
"""Aggregated tool result group (ToolResultItem + trailing DeveloperMessageItems)."""
|
|
40
|
-
|
|
41
|
-
tool_result: model.ToolResultItem
|
|
42
|
-
reminder_texts: list[str] = field(default_factory=lambda: [])
|
|
43
|
-
reminder_images: list[model.ImageURLPart] = field(default_factory=lambda: [])
|
|
44
34
|
|
|
35
|
+
def attach_developer_messages(
|
|
36
|
+
messages: Iterable[message.Message],
|
|
37
|
+
) -> list[tuple[message.Message, DeveloperAttachment]]:
|
|
38
|
+
"""Attach developer messages to the most recent user/tool message.
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"""Aggregated assistant message group."""
|
|
49
|
-
|
|
50
|
-
text_content: str | None = None
|
|
51
|
-
tool_calls: list[model.ToolCallItem] = field(default_factory=lambda: [])
|
|
52
|
-
reasoning_items: list[model.ReasoningTextItem | model.ReasoningEncryptedItem] = field(default_factory=lambda: [])
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
MessageGroup = UserGroup | ToolGroup | AssistantGroup
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _kind_of(item: model.ConversationItem) -> GroupKind:
|
|
59
|
-
if isinstance(
|
|
60
|
-
item,
|
|
61
|
-
(model.ReasoningTextItem, model.ReasoningEncryptedItem, model.AssistantMessageItem, model.ToolCallItem),
|
|
62
|
-
):
|
|
63
|
-
return GroupKind.ASSISTANT
|
|
64
|
-
if isinstance(item, model.UserMessageItem):
|
|
65
|
-
return GroupKind.USER
|
|
66
|
-
if isinstance(item, model.ToolResultItem):
|
|
67
|
-
return GroupKind.TOOL
|
|
68
|
-
if isinstance(item, model.DeveloperMessageItem):
|
|
69
|
-
return GroupKind.DEVELOPER
|
|
70
|
-
return GroupKind.OTHER
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def group_response_items_gen(
|
|
74
|
-
items: Iterable[model.ConversationItem],
|
|
75
|
-
) -> Iterator[tuple[GroupKind, list[model.ConversationItem]]]:
|
|
76
|
-
"""Group response items into sublists with predictable attachment rules.
|
|
77
|
-
|
|
78
|
-
- Consecutive assistant-side items (ReasoningTextItem | ReasoningEncryptedItem |
|
|
79
|
-
AssistantMessageItem | ToolCallItem) group together.
|
|
80
|
-
- Consecutive UserMessage group together.
|
|
81
|
-
- Each ToolMessage (ToolResultItem) is a single group, but allow following
|
|
82
|
-
DeveloperMessage to attach to it.
|
|
83
|
-
- DeveloperMessage only attaches to the previous UserMessage/ToolMessage group.
|
|
40
|
+
Developer messages are removed from the output list and their text/images are
|
|
41
|
+
attached to the previous user/tool message as out-of-band content for provider input.
|
|
84
42
|
"""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def flush() -> Iterator[tuple[GroupKind, list[model.ConversationItem]]]:
|
|
89
|
-
"""Yield current group and reset buffer state."""
|
|
90
|
-
|
|
91
|
-
nonlocal buffer, buffer_kind
|
|
92
|
-
if buffer_kind is not None and buffer:
|
|
93
|
-
yield (buffer_kind, buffer)
|
|
94
|
-
buffer = []
|
|
95
|
-
buffer_kind = None
|
|
43
|
+
message_list = list(messages)
|
|
44
|
+
attachments = [DeveloperAttachment() for _ in message_list]
|
|
45
|
+
last_user_tool_idx: int | None = None
|
|
96
46
|
|
|
97
|
-
for
|
|
98
|
-
|
|
99
|
-
|
|
47
|
+
for idx, msg in enumerate(message_list):
|
|
48
|
+
if isinstance(msg, (message.UserMessage, message.ToolResultMessage)):
|
|
49
|
+
last_user_tool_idx = idx
|
|
100
50
|
continue
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
continue
|
|
113
|
-
|
|
114
|
-
# Tool messages always form a standalone group.
|
|
115
|
-
if item_kind == GroupKind.TOOL:
|
|
116
|
-
yield from flush()
|
|
117
|
-
buffer_kind = GroupKind.TOOL
|
|
118
|
-
buffer = [item]
|
|
119
|
-
continue
|
|
120
|
-
|
|
121
|
-
# Same non-tool kind: extend current group.
|
|
122
|
-
if item_kind == buffer_kind:
|
|
123
|
-
buffer.append(item)
|
|
51
|
+
if isinstance(msg, message.DeveloperMessage):
|
|
52
|
+
if last_user_tool_idx is None:
|
|
53
|
+
continue
|
|
54
|
+
dev_text, dev_images = _extract_developer_content(msg)
|
|
55
|
+
attachment = attachments[last_user_tool_idx]
|
|
56
|
+
attachment.text += dev_text
|
|
57
|
+
attachment.images.extend(dev_images)
|
|
58
|
+
|
|
59
|
+
result: list[tuple[message.Message, DeveloperAttachment]] = []
|
|
60
|
+
for idx, msg in enumerate(message_list):
|
|
61
|
+
if isinstance(msg, message.DeveloperMessage):
|
|
124
62
|
continue
|
|
63
|
+
result.append((msg, attachments[idx]))
|
|
125
64
|
|
|
126
|
-
|
|
127
|
-
yield from flush()
|
|
128
|
-
buffer_kind = item_kind
|
|
129
|
-
buffer = [item]
|
|
65
|
+
return result
|
|
130
66
|
|
|
131
|
-
if buffer_kind is not None and buffer:
|
|
132
|
-
yield (buffer_kind, buffer)
|
|
133
67
|
|
|
68
|
+
def merge_reminder_text(tool_output: str | None, reminder_text: str) -> str:
|
|
69
|
+
"""Merge tool output with reminder text."""
|
|
70
|
+
base = tool_output or ""
|
|
71
|
+
if reminder_text:
|
|
72
|
+
base += "\n" + reminder_text
|
|
73
|
+
return base
|
|
134
74
|
|
|
135
|
-
def parse_message_groups(history: list[model.ConversationItem]) -> list[MessageGroup]:
|
|
136
|
-
"""Parse conversation history into aggregated message groups.
|
|
137
75
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
76
|
+
def collect_text_content(parts: list[message.Part]) -> str:
|
|
77
|
+
return "".join(part.text for part in parts if isinstance(part, message.TextPart))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_chat_content_parts(
|
|
81
|
+
msg: message.UserMessage,
|
|
82
|
+
attachment: DeveloperAttachment,
|
|
83
|
+
) -> list[dict[str, object]]:
|
|
84
|
+
parts: list[dict[str, object]] = []
|
|
85
|
+
for part in msg.parts:
|
|
86
|
+
if isinstance(part, message.TextPart):
|
|
87
|
+
parts.append({"type": "text", "text": part.text})
|
|
88
|
+
elif isinstance(part, message.ImageURLPart):
|
|
89
|
+
parts.append({"type": "image_url", "image_url": {"url": part.url}})
|
|
90
|
+
if attachment.text:
|
|
91
|
+
parts.append({"type": "text", "text": attachment.text})
|
|
92
|
+
for image in attachment.images:
|
|
93
|
+
parts.append({"type": "image_url", "image_url": {"url": image.url}})
|
|
94
|
+
if not parts:
|
|
95
|
+
parts.append({"type": "text", "text": ""})
|
|
96
|
+
return parts
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_tool_message(
|
|
100
|
+
msg: message.ToolResultMessage,
|
|
101
|
+
attachment: DeveloperAttachment,
|
|
102
|
+
) -> dict[str, object]:
|
|
103
|
+
merged_text = merge_reminder_text(
|
|
104
|
+
msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
|
|
105
|
+
attachment.text,
|
|
106
|
+
)
|
|
107
|
+
return {
|
|
108
|
+
"role": "tool",
|
|
109
|
+
"content": [{"type": "text", "text": merged_text}],
|
|
110
|
+
"tool_call_id": msg.call_id,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_assistant_common_fields(
|
|
115
|
+
msg: message.AssistantMessage,
|
|
116
|
+
*,
|
|
117
|
+
image_to_data_url: Callable[[message.ImageFilePart], str],
|
|
118
|
+
) -> dict[str, object]:
|
|
119
|
+
result: dict[str, object] = {}
|
|
120
|
+
images = [part for part in msg.parts if isinstance(part, message.ImageFilePart)]
|
|
121
|
+
if images:
|
|
122
|
+
result["images"] = [
|
|
123
|
+
{
|
|
124
|
+
"image_url": {
|
|
125
|
+
"url": image_to_data_url(image),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for image in images
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
tool_calls = [part for part in msg.parts if isinstance(part, message.ToolCallPart)]
|
|
132
|
+
if tool_calls:
|
|
133
|
+
result["tool_calls"] = [
|
|
134
|
+
{
|
|
135
|
+
"id": tc.call_id,
|
|
136
|
+
"type": "function",
|
|
137
|
+
"function": {
|
|
138
|
+
"name": tc.tool_name,
|
|
139
|
+
"arguments": tc.arguments_json,
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
for tc in tool_calls
|
|
143
|
+
]
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def split_thinking_parts(
|
|
148
|
+
msg: message.AssistantMessage,
|
|
149
|
+
model_name: str | None,
|
|
150
|
+
) -> tuple[list[message.ThinkingTextPart | message.ThinkingSignaturePart], list[str]]:
|
|
151
|
+
native_parts: list[message.ThinkingTextPart | message.ThinkingSignaturePart] = []
|
|
152
|
+
degraded_texts: list[str] = []
|
|
153
|
+
for part in msg.parts:
|
|
154
|
+
if isinstance(part, message.ThinkingTextPart):
|
|
155
|
+
if part.model_id and model_name and part.model_id != model_name:
|
|
156
|
+
degraded_texts.append(part.text)
|
|
146
157
|
continue
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if item.images:
|
|
154
|
-
group.images.extend(item.images)
|
|
155
|
-
groups.append(group)
|
|
156
|
-
|
|
157
|
-
case GroupKind.TOOL:
|
|
158
|
-
if not items or not isinstance(items[0], model.ToolResultItem):
|
|
159
|
-
continue
|
|
160
|
-
tool_result = items[0]
|
|
161
|
-
group = ToolGroup(tool_result=tool_result)
|
|
162
|
-
for item in items[1:]:
|
|
163
|
-
if isinstance(item, model.DeveloperMessageItem):
|
|
164
|
-
if item.content:
|
|
165
|
-
group.reminder_texts.append(item.content)
|
|
166
|
-
if item.images:
|
|
167
|
-
group.reminder_images.extend(item.images)
|
|
168
|
-
groups.append(group)
|
|
169
|
-
|
|
170
|
-
case GroupKind.ASSISTANT:
|
|
171
|
-
group = AssistantGroup()
|
|
172
|
-
for item in items:
|
|
173
|
-
match item:
|
|
174
|
-
case model.AssistantMessageItem():
|
|
175
|
-
if item.content:
|
|
176
|
-
if group.text_content is None:
|
|
177
|
-
group.text_content = item.content
|
|
178
|
-
else:
|
|
179
|
-
group.text_content += item.content
|
|
180
|
-
case model.ToolCallItem():
|
|
181
|
-
group.tool_calls.append(item)
|
|
182
|
-
case model.ReasoningTextItem():
|
|
183
|
-
group.reasoning_items.append(item)
|
|
184
|
-
case model.ReasoningEncryptedItem():
|
|
185
|
-
group.reasoning_items.append(item)
|
|
186
|
-
case _:
|
|
187
|
-
pass
|
|
188
|
-
groups.append(group)
|
|
189
|
-
|
|
190
|
-
case GroupKind.DEVELOPER:
|
|
191
|
-
pass
|
|
192
|
-
|
|
193
|
-
return groups
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def merge_reminder_text(tool_output: str | None, reminder_texts: list[str]) -> str:
|
|
197
|
-
"""Merge tool output with reminder texts."""
|
|
198
|
-
base = tool_output or ""
|
|
199
|
-
if reminder_texts:
|
|
200
|
-
base += "\n" + "\n".join(reminder_texts)
|
|
201
|
-
return base
|
|
158
|
+
native_parts.append(part)
|
|
159
|
+
elif isinstance(part, message.ThinkingSignaturePart):
|
|
160
|
+
if part.model_id and model_name and part.model_id != model_name:
|
|
161
|
+
continue
|
|
162
|
+
native_parts.append(part)
|
|
163
|
+
return native_parts, degraded_texts
|
|
202
164
|
|
|
203
165
|
|
|
204
166
|
def apply_config_defaults(param: "LLMCallParameter", config: "LLMConfigParameter") -> "LLMCallParameter":
|
|
@@ -215,19 +177,4 @@ def apply_config_defaults(param: "LLMCallParameter", config: "LLMConfigParameter
|
|
|
215
177
|
param.verbosity = config.verbosity
|
|
216
178
|
if param.thinking is None:
|
|
217
179
|
param.thinking = config.thinking
|
|
218
|
-
if param.provider_routing is None:
|
|
219
|
-
param.provider_routing = config.provider_routing
|
|
220
|
-
|
|
221
|
-
if param.model is None:
|
|
222
|
-
raise ValueError("Model is required")
|
|
223
|
-
if param.max_tokens is None:
|
|
224
|
-
param.max_tokens = const.DEFAULT_MAX_TOKENS
|
|
225
|
-
if param.temperature is None:
|
|
226
|
-
param.temperature = const.DEFAULT_TEMPERATURE
|
|
227
|
-
if param.thinking is not None and param.thinking.type == "enabled" and param.thinking.budget_tokens is None:
|
|
228
|
-
param.thinking.budget_tokens = const.DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS
|
|
229
|
-
|
|
230
|
-
if param.model and "gpt-5" in param.model:
|
|
231
|
-
param.temperature = 1.0 # Required for GPT-5
|
|
232
|
-
|
|
233
180
|
return param
|
|
@@ -6,13 +6,14 @@ import httpx
|
|
|
6
6
|
import openai
|
|
7
7
|
from openai.types.chat.completion_create_params import CompletionCreateParamsStreaming
|
|
8
8
|
|
|
9
|
+
from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
|
|
9
10
|
from klaude_code.llm.client import LLMClientABC
|
|
10
11
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
11
12
|
from klaude_code.llm.openai_compatible.input import convert_history_to_input, convert_tool_schema
|
|
12
13
|
from klaude_code.llm.openai_compatible.stream import DefaultReasoningHandler, parse_chat_completions_stream
|
|
13
14
|
from klaude_code.llm.registry import register
|
|
14
15
|
from klaude_code.llm.usage import MetadataTracker
|
|
15
|
-
from klaude_code.protocol import llm_param,
|
|
16
|
+
from klaude_code.protocol import llm_param, message
|
|
16
17
|
from klaude_code.trace import DebugType, log_debug
|
|
17
18
|
|
|
18
19
|
|
|
@@ -56,13 +57,17 @@ class OpenAICompatibleClient(LLMClientABC):
|
|
|
56
57
|
api_key=config.api_key,
|
|
57
58
|
azure_endpoint=str(config.base_url),
|
|
58
59
|
api_version=config.azure_api_version,
|
|
59
|
-
timeout=httpx.Timeout(
|
|
60
|
+
timeout=httpx.Timeout(
|
|
61
|
+
LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ
|
|
62
|
+
),
|
|
60
63
|
)
|
|
61
64
|
else:
|
|
62
65
|
client = openai.AsyncOpenAI(
|
|
63
66
|
api_key=config.api_key,
|
|
64
67
|
base_url=config.base_url,
|
|
65
|
-
timeout=httpx.Timeout(
|
|
68
|
+
timeout=httpx.Timeout(
|
|
69
|
+
LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ
|
|
70
|
+
),
|
|
66
71
|
)
|
|
67
72
|
self.client: openai.AsyncAzureOpenAI | openai.AsyncOpenAI = client
|
|
68
73
|
|
|
@@ -72,12 +77,17 @@ class OpenAICompatibleClient(LLMClientABC):
|
|
|
72
77
|
return cls(config)
|
|
73
78
|
|
|
74
79
|
@override
|
|
75
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[
|
|
80
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
|
|
76
81
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
77
82
|
|
|
78
83
|
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
79
84
|
|
|
80
|
-
|
|
85
|
+
try:
|
|
86
|
+
payload, extra_body = build_payload(param)
|
|
87
|
+
except (ValueError, OSError) as e:
|
|
88
|
+
yield message.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
|
|
89
|
+
yield message.AssistantMessage(parts=[], response_id=None, usage=metadata_tracker.finalize())
|
|
90
|
+
return
|
|
81
91
|
extra_headers: dict[str, str] = {"extra": json.dumps({"session_id": param.session_id}, sort_keys=True)}
|
|
82
92
|
|
|
83
93
|
log_debug(
|
|
@@ -93,8 +103,8 @@ class OpenAICompatibleClient(LLMClientABC):
|
|
|
93
103
|
extra_headers=extra_headers,
|
|
94
104
|
)
|
|
95
105
|
except (openai.OpenAIError, httpx.HTTPError) as e:
|
|
96
|
-
yield
|
|
97
|
-
yield metadata_tracker.finalize()
|
|
106
|
+
yield message.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
|
|
107
|
+
yield message.AssistantMessage(parts=[], response_id=None, usage=metadata_tracker.finalize())
|
|
98
108
|
return
|
|
99
109
|
|
|
100
110
|
reasoning_handler = DefaultReasoningHandler(
|
|
@@ -6,89 +6,59 @@
|
|
|
6
6
|
from openai.types import chat
|
|
7
7
|
from openai.types.chat import ChatCompletionContentPartParam
|
|
8
8
|
|
|
9
|
-
from klaude_code.llm.
|
|
10
|
-
from klaude_code.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
parts.append({"type": "text", "text": ""})
|
|
22
|
-
return {"role": "user", "content": parts}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def tool_group_to_openai_message(group: ToolGroup) -> chat.ChatCompletionMessageParam:
|
|
26
|
-
"""Convert a ToolGroup to an OpenAI-compatible chat message."""
|
|
27
|
-
merged_text = merge_reminder_text(
|
|
28
|
-
group.tool_result.output or "<system-reminder>Tool ran without output or errors</system-reminder>",
|
|
29
|
-
group.reminder_texts,
|
|
30
|
-
)
|
|
31
|
-
return {
|
|
32
|
-
"role": "tool",
|
|
33
|
-
"content": [{"type": "text", "text": merged_text}],
|
|
34
|
-
"tool_call_id": group.tool_result.call_id,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _assistant_group_to_message(
|
|
39
|
-
group: AssistantGroup,
|
|
40
|
-
) -> chat.ChatCompletionMessageParam:
|
|
9
|
+
from klaude_code.llm.image import assistant_image_to_data_url
|
|
10
|
+
from klaude_code.llm.input_common import (
|
|
11
|
+
attach_developer_messages,
|
|
12
|
+
build_assistant_common_fields,
|
|
13
|
+
build_chat_content_parts,
|
|
14
|
+
build_tool_message,
|
|
15
|
+
collect_text_content,
|
|
16
|
+
)
|
|
17
|
+
from klaude_code.protocol import llm_param, message
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _assistant_message_to_openai(msg: message.AssistantMessage) -> chat.ChatCompletionMessageParam:
|
|
41
21
|
assistant_message: dict[str, object] = {"role": "assistant"}
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if group.tool_calls:
|
|
47
|
-
assistant_message["tool_calls"] = [
|
|
48
|
-
{
|
|
49
|
-
"id": tc.call_id,
|
|
50
|
-
"type": "function",
|
|
51
|
-
"function": {
|
|
52
|
-
"name": tc.name,
|
|
53
|
-
"arguments": tc.arguments,
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
for tc in group.tool_calls
|
|
57
|
-
]
|
|
23
|
+
text_content = collect_text_content(msg.parts)
|
|
24
|
+
if text_content:
|
|
25
|
+
assistant_message["content"] = text_content
|
|
58
26
|
|
|
27
|
+
assistant_message.update(build_assistant_common_fields(msg, image_to_data_url=assistant_image_to_data_url))
|
|
59
28
|
return assistant_message
|
|
60
29
|
|
|
61
30
|
|
|
62
31
|
def build_user_content_parts(
|
|
63
|
-
images: list[
|
|
32
|
+
images: list[message.ImageURLPart],
|
|
64
33
|
) -> list[ChatCompletionContentPartParam]:
|
|
65
34
|
"""Build content parts for images only. Used by OpenRouter."""
|
|
66
|
-
return [{"type": "image_url", "image_url": {"url": image.
|
|
35
|
+
return [{"type": "image_url", "image_url": {"url": image.url}} for image in images]
|
|
67
36
|
|
|
68
37
|
|
|
69
38
|
def convert_history_to_input(
|
|
70
|
-
history: list[
|
|
39
|
+
history: list[message.Message],
|
|
71
40
|
system: str | None = None,
|
|
72
41
|
model_name: str | None = None,
|
|
73
42
|
) -> list[chat.ChatCompletionMessageParam]:
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
history: List of conversation items.
|
|
79
|
-
system: System message.
|
|
80
|
-
model_name: Model name. Not used in OpenAI-compatible, kept for API consistency.
|
|
81
|
-
"""
|
|
43
|
+
"""Convert a list of messages to chat completion params."""
|
|
44
|
+
del model_name
|
|
82
45
|
messages: list[chat.ChatCompletionMessageParam] = [{"role": "system", "content": system}] if system else []
|
|
83
46
|
|
|
84
|
-
for
|
|
85
|
-
match
|
|
86
|
-
case
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
case
|
|
91
|
-
|
|
47
|
+
for msg, attachment in attach_developer_messages(history):
|
|
48
|
+
match msg:
|
|
49
|
+
case message.SystemMessage():
|
|
50
|
+
system_text = "\n".join(part.text for part in msg.parts)
|
|
51
|
+
if system_text:
|
|
52
|
+
messages.append({"role": "system", "content": system_text})
|
|
53
|
+
case message.UserMessage():
|
|
54
|
+
parts = build_chat_content_parts(msg, attachment)
|
|
55
|
+
messages.append({"role": "user", "content": parts})
|
|
56
|
+
case message.ToolResultMessage():
|
|
57
|
+
messages.append(build_tool_message(msg, attachment))
|
|
58
|
+
case message.AssistantMessage():
|
|
59
|
+
messages.append(_assistant_message_to_openai(msg))
|
|
60
|
+
case _:
|
|
61
|
+
continue
|
|
92
62
|
|
|
93
63
|
return messages
|
|
94
64
|
|