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
klaude_code/llm/client.py
CHANGED
|
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
|
|
|
2
2
|
from collections.abc import AsyncGenerator
|
|
3
3
|
from typing import ParamSpec, TypeVar, cast
|
|
4
4
|
|
|
5
|
-
from klaude_code.protocol import llm_param,
|
|
5
|
+
from klaude_code.protocol import llm_param, message
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class LLMClientABC(ABC):
|
|
@@ -15,9 +15,10 @@ class LLMClientABC(ABC):
|
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
@abstractmethod
|
|
18
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[
|
|
18
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
|
|
19
|
+
if False: # pragma: no cover
|
|
20
|
+
yield cast(message.LLMStreamItem, None)
|
|
19
21
|
raise NotImplementedError
|
|
20
|
-
yield cast(model.ConversationItem, None)
|
|
21
22
|
|
|
22
23
|
def get_llm_config(self) -> llm_param.LLMConfigParameter:
|
|
23
24
|
return self._config
|
klaude_code/llm/codex/client.py
CHANGED
|
@@ -12,13 +12,20 @@ from openai.types.responses.response_create_params import ResponseCreateParamsSt
|
|
|
12
12
|
from klaude_code.auth.codex.exceptions import CodexNotLoggedInError
|
|
13
13
|
from klaude_code.auth.codex.oauth import CodexOAuth
|
|
14
14
|
from klaude_code.auth.codex.token_manager import CodexTokenManager
|
|
15
|
+
from klaude_code.const import (
|
|
16
|
+
CODEX_BASE_URL,
|
|
17
|
+
CODEX_USER_AGENT,
|
|
18
|
+
LLM_HTTP_TIMEOUT_CONNECT,
|
|
19
|
+
LLM_HTTP_TIMEOUT_READ,
|
|
20
|
+
LLM_HTTP_TIMEOUT_TOTAL,
|
|
21
|
+
)
|
|
15
22
|
from klaude_code.llm.client import LLMClientABC
|
|
16
23
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
17
24
|
from klaude_code.llm.registry import register
|
|
18
25
|
from klaude_code.llm.responses.client import parse_responses_stream
|
|
19
26
|
from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
|
|
20
|
-
from klaude_code.llm.usage import MetadataTracker
|
|
21
|
-
from klaude_code.protocol import llm_param,
|
|
27
|
+
from klaude_code.llm.usage import MetadataTracker, error_stream_items
|
|
28
|
+
from klaude_code.protocol import llm_param, message
|
|
22
29
|
from klaude_code.trace import DebugType, log_debug
|
|
23
30
|
|
|
24
31
|
|
|
@@ -57,12 +64,9 @@ def build_payload(param: llm_param.LLMCallParameter) -> ResponseCreateParamsStre
|
|
|
57
64
|
return payload
|
|
58
65
|
|
|
59
66
|
|
|
60
|
-
# Codex API configuration
|
|
61
|
-
CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"
|
|
62
67
|
CODEX_HEADERS = {
|
|
63
68
|
"originator": "codex_cli_rs",
|
|
64
|
-
|
|
65
|
-
"User-Agent": "codex_cli_rs/0.0.0-klaude",
|
|
69
|
+
"User-Agent": CODEX_USER_AGENT,
|
|
66
70
|
"OpenAI-Beta": "responses=experimental",
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -90,7 +94,7 @@ class CodexClient(LLMClientABC):
|
|
|
90
94
|
return AsyncOpenAI(
|
|
91
95
|
api_key=state.access_token,
|
|
92
96
|
base_url=CODEX_BASE_URL,
|
|
93
|
-
timeout=httpx.Timeout(
|
|
97
|
+
timeout=httpx.Timeout(LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ),
|
|
94
98
|
default_headers={
|
|
95
99
|
**CODEX_HEADERS,
|
|
96
100
|
"chatgpt-account-id": state.account_id,
|
|
@@ -114,7 +118,7 @@ class CodexClient(LLMClientABC):
|
|
|
114
118
|
return cls(config)
|
|
115
119
|
|
|
116
120
|
@override
|
|
117
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[
|
|
121
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
|
|
118
122
|
# Ensure token is valid before API call
|
|
119
123
|
self._ensure_valid_token()
|
|
120
124
|
|
|
@@ -142,7 +146,9 @@ class CodexClient(LLMClientABC):
|
|
|
142
146
|
extra_headers=extra_headers,
|
|
143
147
|
)
|
|
144
148
|
except (openai.OpenAIError, httpx.HTTPError) as e:
|
|
145
|
-
|
|
149
|
+
error_message = f"{e.__class__.__name__} {e!s}"
|
|
150
|
+
for item in error_stream_items(metadata_tracker, error=error_message):
|
|
151
|
+
yield item
|
|
146
152
|
return
|
|
147
153
|
|
|
148
154
|
async for item in parse_responses_stream(stream, param, metadata_tracker):
|
klaude_code/llm/google/client.py
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
from collections.abc import AsyncGenerator, AsyncIterator
|
|
8
|
-
from typing import Any, cast, override
|
|
8
|
+
from typing import Any, Literal, cast, override
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
@@ -26,7 +26,7 @@ from klaude_code.llm.google.input import convert_history_to_contents, convert_to
|
|
|
26
26
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
27
27
|
from klaude_code.llm.registry import register
|
|
28
28
|
from klaude_code.llm.usage import MetadataTracker
|
|
29
|
-
from klaude_code.protocol import llm_param, model
|
|
29
|
+
from klaude_code.protocol import llm_param, message, model
|
|
30
30
|
from klaude_code.trace import DebugType, log_debug
|
|
31
31
|
|
|
32
32
|
|
|
@@ -114,25 +114,74 @@ def _merge_partial_args(dst: dict[str, Any], partial_args: list[Any] | None) ->
|
|
|
114
114
|
dst[key] = _partial_arg_value(partial)
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def _map_finish_reason(reason: str) -> model.StopReason | None:
|
|
118
|
+
normalized = reason.strip().lower()
|
|
119
|
+
mapping: dict[str, model.StopReason] = {
|
|
120
|
+
"stop": "stop",
|
|
121
|
+
"end_turn": "stop",
|
|
122
|
+
"max_tokens": "length",
|
|
123
|
+
"length": "length",
|
|
124
|
+
"tool_use": "tool_use",
|
|
125
|
+
"safety": "error",
|
|
126
|
+
"recitation": "error",
|
|
127
|
+
"other": "error",
|
|
128
|
+
"content_filter": "error",
|
|
129
|
+
"blocked": "error",
|
|
130
|
+
"blocklist": "error",
|
|
131
|
+
"cancelled": "aborted",
|
|
132
|
+
"canceled": "aborted",
|
|
133
|
+
"aborted": "aborted",
|
|
134
|
+
}
|
|
135
|
+
return mapping.get(normalized)
|
|
136
|
+
|
|
137
|
+
|
|
117
138
|
async def parse_google_stream(
|
|
118
139
|
stream: AsyncIterator[Any],
|
|
119
140
|
param: llm_param.LLMCallParameter,
|
|
120
141
|
metadata_tracker: MetadataTracker,
|
|
121
|
-
) -> AsyncGenerator[
|
|
142
|
+
) -> AsyncGenerator[message.LLMStreamItem]:
|
|
122
143
|
response_id: str | None = None
|
|
123
|
-
|
|
144
|
+
stage: Literal["waiting", "thinking", "assistant", "tool"] = "waiting"
|
|
124
145
|
|
|
125
146
|
accumulated_text: list[str] = []
|
|
126
147
|
accumulated_thoughts: list[str] = []
|
|
127
148
|
thought_signature: str | None = None
|
|
149
|
+
assistant_parts: list[message.Part] = []
|
|
128
150
|
|
|
129
151
|
# Track tool calls where args arrive as partial updates.
|
|
130
152
|
partial_args_by_call: dict[str, dict[str, Any]] = {}
|
|
131
153
|
started_tool_calls: dict[str, str] = {} # call_id -> name
|
|
132
154
|
started_tool_items: set[str] = set()
|
|
133
|
-
|
|
155
|
+
completed_tool_items: set[str] = set()
|
|
134
156
|
|
|
135
157
|
last_usage_metadata: UsageMetadata | None = None
|
|
158
|
+
stop_reason: model.StopReason | None = None
|
|
159
|
+
|
|
160
|
+
def flush_thinking() -> None:
|
|
161
|
+
nonlocal thought_signature
|
|
162
|
+
if accumulated_thoughts:
|
|
163
|
+
assistant_parts.append(
|
|
164
|
+
message.ThinkingTextPart(
|
|
165
|
+
text="".join(accumulated_thoughts),
|
|
166
|
+
model_id=str(param.model),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
accumulated_thoughts.clear()
|
|
170
|
+
if thought_signature:
|
|
171
|
+
assistant_parts.append(
|
|
172
|
+
message.ThinkingSignaturePart(
|
|
173
|
+
signature=thought_signature,
|
|
174
|
+
model_id=str(param.model),
|
|
175
|
+
format="google_thought_signature",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
thought_signature = None
|
|
179
|
+
|
|
180
|
+
def flush_text() -> None:
|
|
181
|
+
if not accumulated_text:
|
|
182
|
+
return
|
|
183
|
+
assistant_parts.append(message.TextPart(text="".join(accumulated_text)))
|
|
184
|
+
accumulated_text.clear()
|
|
136
185
|
|
|
137
186
|
async for chunk in stream:
|
|
138
187
|
log_debug(
|
|
@@ -143,33 +192,44 @@ async def parse_google_stream(
|
|
|
143
192
|
|
|
144
193
|
if response_id is None:
|
|
145
194
|
response_id = getattr(chunk, "response_id", None) or uuid4().hex
|
|
146
|
-
assert response_id is not None
|
|
147
|
-
if not started:
|
|
148
|
-
started = True
|
|
149
|
-
yield model.StartItem(response_id=response_id)
|
|
150
195
|
|
|
151
196
|
if getattr(chunk, "usage_metadata", None) is not None:
|
|
152
197
|
last_usage_metadata = chunk.usage_metadata
|
|
153
198
|
|
|
154
199
|
candidates = getattr(chunk, "candidates", None) or []
|
|
155
200
|
candidate0 = candidates[0] if candidates else None
|
|
201
|
+
finish_reason = getattr(candidate0, "finish_reason", None) if candidate0 else None
|
|
202
|
+
if finish_reason is not None:
|
|
203
|
+
if isinstance(finish_reason, str):
|
|
204
|
+
reason_value = finish_reason
|
|
205
|
+
else:
|
|
206
|
+
reason_value = getattr(finish_reason, "name", None) or str(finish_reason)
|
|
207
|
+
stop_reason = _map_finish_reason(reason_value)
|
|
156
208
|
content = getattr(candidate0, "content", None) if candidate0 else None
|
|
157
|
-
|
|
158
|
-
if not
|
|
209
|
+
content_parts = getattr(content, "parts", None) if content else None
|
|
210
|
+
if not content_parts:
|
|
159
211
|
continue
|
|
160
212
|
|
|
161
|
-
for part in
|
|
213
|
+
for part in content_parts:
|
|
162
214
|
if getattr(part, "text", None) is not None:
|
|
163
|
-
metadata_tracker.record_token()
|
|
164
215
|
text = part.text
|
|
216
|
+
if not text:
|
|
217
|
+
continue
|
|
218
|
+
metadata_tracker.record_token()
|
|
165
219
|
if getattr(part, "thought", False) is True:
|
|
220
|
+
if stage == "assistant":
|
|
221
|
+
flush_text()
|
|
222
|
+
stage = "thinking"
|
|
166
223
|
accumulated_thoughts.append(text)
|
|
167
224
|
if getattr(part, "thought_signature", None):
|
|
168
225
|
thought_signature = part.thought_signature
|
|
169
|
-
yield
|
|
226
|
+
yield message.ThinkingTextDelta(content=text, response_id=response_id)
|
|
170
227
|
else:
|
|
228
|
+
if stage == "thinking":
|
|
229
|
+
flush_thinking()
|
|
230
|
+
stage = "assistant"
|
|
171
231
|
accumulated_text.append(text)
|
|
172
|
-
yield
|
|
232
|
+
yield message.AssistantTextDelta(content=text, response_id=response_id)
|
|
173
233
|
|
|
174
234
|
function_call = getattr(part, "function_call", None)
|
|
175
235
|
if function_call is None:
|
|
@@ -182,17 +242,23 @@ async def parse_google_stream(
|
|
|
182
242
|
|
|
183
243
|
if call_id not in started_tool_items:
|
|
184
244
|
started_tool_items.add(call_id)
|
|
185
|
-
yield
|
|
245
|
+
yield message.ToolCallStartItem(response_id=response_id, call_id=call_id, name=name)
|
|
186
246
|
|
|
187
247
|
args_obj = getattr(function_call, "args", None)
|
|
188
248
|
if args_obj is not None:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
249
|
+
if stage == "thinking":
|
|
250
|
+
flush_thinking()
|
|
251
|
+
if stage == "assistant":
|
|
252
|
+
flush_text()
|
|
253
|
+
stage = "tool"
|
|
254
|
+
assistant_parts.append(
|
|
255
|
+
message.ToolCallPart(
|
|
256
|
+
call_id=call_id,
|
|
257
|
+
tool_name=name,
|
|
258
|
+
arguments_json=json.dumps(args_obj, ensure_ascii=False),
|
|
259
|
+
)
|
|
195
260
|
)
|
|
261
|
+
completed_tool_items.add(call_id)
|
|
196
262
|
continue
|
|
197
263
|
|
|
198
264
|
partial_args = getattr(function_call, "partial_args", None)
|
|
@@ -201,53 +267,49 @@ async def parse_google_stream(
|
|
|
201
267
|
_merge_partial_args(acc, partial_args)
|
|
202
268
|
|
|
203
269
|
will_continue = getattr(function_call, "will_continue", None)
|
|
204
|
-
if will_continue is False and call_id in partial_args_by_call and call_id not in
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
270
|
+
if will_continue is False and call_id in partial_args_by_call and call_id not in completed_tool_items:
|
|
271
|
+
if stage == "thinking":
|
|
272
|
+
flush_thinking()
|
|
273
|
+
if stage == "assistant":
|
|
274
|
+
flush_text()
|
|
275
|
+
stage = "tool"
|
|
276
|
+
assistant_parts.append(
|
|
277
|
+
message.ToolCallPart(
|
|
278
|
+
call_id=call_id,
|
|
279
|
+
tool_name=name,
|
|
280
|
+
arguments_json=json.dumps(partial_args_by_call[call_id], ensure_ascii=False),
|
|
281
|
+
)
|
|
211
282
|
)
|
|
283
|
+
completed_tool_items.add(call_id)
|
|
212
284
|
|
|
213
285
|
# Flush any pending tool calls that never produced args.
|
|
214
286
|
for call_id, name in started_tool_calls.items():
|
|
215
|
-
if call_id in
|
|
287
|
+
if call_id in completed_tool_items:
|
|
216
288
|
continue
|
|
217
289
|
args = partial_args_by_call.get(call_id, {})
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
arguments=json.dumps(args, ensure_ascii=False),
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
if accumulated_thoughts:
|
|
227
|
-
metadata_tracker.record_token()
|
|
228
|
-
yield model.ReasoningTextItem(
|
|
229
|
-
content="".join(accumulated_thoughts),
|
|
230
|
-
response_id=response_id,
|
|
231
|
-
model=str(param.model),
|
|
232
|
-
)
|
|
233
|
-
if thought_signature:
|
|
234
|
-
yield model.ReasoningEncryptedItem(
|
|
235
|
-
encrypted_content=thought_signature,
|
|
236
|
-
response_id=response_id,
|
|
237
|
-
model=str(param.model),
|
|
238
|
-
format="google_thought_signature",
|
|
290
|
+
assistant_parts.append(
|
|
291
|
+
message.ToolCallPart(
|
|
292
|
+
call_id=call_id,
|
|
293
|
+
tool_name=name,
|
|
294
|
+
arguments_json=json.dumps(args, ensure_ascii=False),
|
|
239
295
|
)
|
|
296
|
+
)
|
|
240
297
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
yield model.AssistantMessageItem(content="".join(accumulated_text), response_id=response_id)
|
|
298
|
+
flush_thinking()
|
|
299
|
+
flush_text()
|
|
244
300
|
|
|
245
301
|
usage = _usage_from_metadata(last_usage_metadata, context_limit=param.context_limit, max_tokens=param.max_tokens)
|
|
246
302
|
if usage is not None:
|
|
247
303
|
metadata_tracker.set_usage(usage)
|
|
248
304
|
metadata_tracker.set_model_name(str(param.model))
|
|
249
305
|
metadata_tracker.set_response_id(response_id)
|
|
250
|
-
|
|
306
|
+
metadata = metadata_tracker.finalize()
|
|
307
|
+
yield message.AssistantMessage(
|
|
308
|
+
parts=assistant_parts,
|
|
309
|
+
response_id=response_id,
|
|
310
|
+
usage=metadata,
|
|
311
|
+
stop_reason=stop_reason,
|
|
312
|
+
)
|
|
251
313
|
|
|
252
314
|
|
|
253
315
|
@register(llm_param.LLMClientProtocol.GOOGLE)
|
|
@@ -270,7 +332,7 @@ class GoogleClient(LLMClientABC):
|
|
|
270
332
|
return cls(config)
|
|
271
333
|
|
|
272
334
|
@override
|
|
273
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[
|
|
335
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
|
|
274
336
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
275
337
|
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
276
338
|
|
|
@@ -297,13 +359,13 @@ class GoogleClient(LLMClientABC):
|
|
|
297
359
|
config=config,
|
|
298
360
|
)
|
|
299
361
|
except (APIError, ClientError, ServerError, httpx.HTTPError) as e:
|
|
300
|
-
yield
|
|
301
|
-
yield metadata_tracker.finalize()
|
|
362
|
+
yield message.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
|
|
363
|
+
yield message.AssistantMessage(parts=[], response_id=None, usage=metadata_tracker.finalize())
|
|
302
364
|
return
|
|
303
365
|
|
|
304
366
|
try:
|
|
305
367
|
async for item in parse_google_stream(stream, param=param, metadata_tracker=metadata_tracker):
|
|
306
368
|
yield item
|
|
307
369
|
except (APIError, ClientError, ServerError, httpx.HTTPError) as e:
|
|
308
|
-
yield
|
|
309
|
-
yield metadata_tracker.finalize()
|
|
370
|
+
yield message.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
|
|
371
|
+
yield message.AssistantMessage(parts=[], response_id=None, usage=metadata_tracker.finalize())
|
klaude_code/llm/google/input.py
CHANGED
|
@@ -4,89 +4,80 @@
|
|
|
4
4
|
# pyright: reportAttributeAccessIssue=false
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
-
from base64 import b64decode
|
|
8
|
-
from binascii import Error as BinasciiError
|
|
9
7
|
from typing import Any
|
|
10
8
|
|
|
11
9
|
from google.genai import types
|
|
12
10
|
|
|
13
|
-
from klaude_code.
|
|
14
|
-
from klaude_code.
|
|
11
|
+
from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
|
|
12
|
+
from klaude_code.llm.image import parse_data_url
|
|
13
|
+
from klaude_code.llm.input_common import (
|
|
14
|
+
DeveloperAttachment,
|
|
15
|
+
attach_developer_messages,
|
|
16
|
+
merge_reminder_text,
|
|
17
|
+
split_thinking_parts,
|
|
18
|
+
)
|
|
19
|
+
from klaude_code.protocol import llm_param, message
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
def _data_url_to_blob(url: str) -> types.Blob:
|
|
18
|
-
|
|
19
|
-
if len(header_and_media) != 2:
|
|
20
|
-
raise ValueError("Invalid data URL for image: missing comma separator")
|
|
21
|
-
header, base64_data = header_and_media
|
|
22
|
-
if not header.startswith("data:"):
|
|
23
|
-
raise ValueError("Invalid data URL for image: missing data: prefix")
|
|
24
|
-
if ";base64" not in header:
|
|
25
|
-
raise ValueError("Invalid data URL for image: missing base64 marker")
|
|
26
|
-
|
|
27
|
-
media_type = header[5:].split(";", 1)[0]
|
|
28
|
-
base64_payload = base64_data.strip()
|
|
29
|
-
if base64_payload == "":
|
|
30
|
-
raise ValueError("Inline image data is empty")
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
decoded = b64decode(base64_payload, validate=True)
|
|
34
|
-
except (BinasciiError, ValueError) as exc:
|
|
35
|
-
raise ValueError("Inline image data is not valid base64") from exc
|
|
36
|
-
|
|
23
|
+
media_type, _, decoded = parse_data_url(url)
|
|
37
24
|
return types.Blob(data=decoded, mime_type=media_type)
|
|
38
25
|
|
|
39
26
|
|
|
40
|
-
def _image_part_to_part(image:
|
|
41
|
-
url = image.
|
|
27
|
+
def _image_part_to_part(image: message.ImageURLPart) -> types.Part:
|
|
28
|
+
url = image.url
|
|
42
29
|
if url.startswith("data:"):
|
|
43
30
|
return types.Part(inline_data=_data_url_to_blob(url))
|
|
44
31
|
# Best-effort: Gemini supports file URIs, and may accept public HTTPS URLs.
|
|
45
32
|
return types.Part(file_data=types.FileData(file_uri=url))
|
|
46
33
|
|
|
47
34
|
|
|
48
|
-
def
|
|
35
|
+
def _user_message_to_content(msg: message.UserMessage, attachment: DeveloperAttachment) -> types.Content:
|
|
49
36
|
parts: list[types.Part] = []
|
|
50
|
-
for
|
|
51
|
-
|
|
52
|
-
|
|
37
|
+
for part in msg.parts:
|
|
38
|
+
if isinstance(part, message.TextPart):
|
|
39
|
+
parts.append(types.Part(text=part.text))
|
|
40
|
+
elif isinstance(part, message.ImageURLPart):
|
|
41
|
+
parts.append(_image_part_to_part(part))
|
|
42
|
+
if attachment.text:
|
|
43
|
+
parts.append(types.Part(text=attachment.text))
|
|
44
|
+
for image in attachment.images:
|
|
53
45
|
parts.append(_image_part_to_part(image))
|
|
54
46
|
if not parts:
|
|
55
47
|
parts.append(types.Part(text=""))
|
|
56
48
|
return types.Content(role="user", parts=parts)
|
|
57
49
|
|
|
58
50
|
|
|
59
|
-
def
|
|
51
|
+
def _tool_messages_to_contents(
|
|
52
|
+
msgs: list[tuple[message.ToolResultMessage, DeveloperAttachment]], model_name: str | None
|
|
53
|
+
) -> list[types.Content]:
|
|
60
54
|
supports_multimodal_function_response = bool(model_name and "gemini-3" in model_name.lower())
|
|
61
55
|
|
|
62
56
|
response_parts: list[types.Part] = []
|
|
63
57
|
extra_image_contents: list[types.Content] = []
|
|
64
58
|
|
|
65
|
-
for
|
|
59
|
+
for msg, attachment in msgs:
|
|
66
60
|
merged_text = merge_reminder_text(
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
|
|
62
|
+
attachment.text,
|
|
69
63
|
)
|
|
70
64
|
has_text = merged_text.strip() != ""
|
|
71
65
|
|
|
72
|
-
images =
|
|
66
|
+
images = [part for part in msg.parts if isinstance(part, message.ImageURLPart)] + attachment.images
|
|
73
67
|
image_parts: list[types.Part] = []
|
|
74
68
|
for image in images:
|
|
75
69
|
try:
|
|
76
70
|
image_parts.append(_image_part_to_part(image))
|
|
77
71
|
except ValueError:
|
|
78
|
-
# Skip invalid data URLs
|
|
79
72
|
continue
|
|
80
73
|
|
|
81
74
|
has_images = len(image_parts) > 0
|
|
82
75
|
response_value = merged_text if has_text else "(see attached image)" if has_images else ""
|
|
83
|
-
response_payload =
|
|
84
|
-
{"error": response_value} if group.tool_result.status == "error" else {"output": response_value}
|
|
85
|
-
)
|
|
76
|
+
response_payload = {"error": response_value} if msg.status != "success" else {"output": response_value}
|
|
86
77
|
|
|
87
78
|
function_response = types.FunctionResponse(
|
|
88
|
-
id=
|
|
89
|
-
name=
|
|
79
|
+
id=msg.call_id,
|
|
80
|
+
name=msg.tool_name,
|
|
90
81
|
response=response_payload,
|
|
91
82
|
parts=image_parts if (has_images and supports_multimodal_function_response) else None,
|
|
92
83
|
)
|
|
@@ -104,100 +95,95 @@ def _tool_groups_to_content(groups: list[ToolGroup], model_name: str | None) ->
|
|
|
104
95
|
return contents
|
|
105
96
|
|
|
106
97
|
|
|
107
|
-
def
|
|
98
|
+
def _assistant_message_to_content(msg: message.AssistantMessage, model_name: str | None) -> types.Content | None:
|
|
108
99
|
parts: list[types.Part] = []
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
native_thinking_parts, degraded_thinking_texts = split_thinking_parts(msg, model_name)
|
|
101
|
+
native_thinking_ids = {id(part) for part in native_thinking_parts}
|
|
111
102
|
pending_thought_text: str | None = None
|
|
112
103
|
pending_thought_signature: str | None = None
|
|
113
104
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
continue
|
|
119
|
-
if model_name is not None and item.model is not None and item.model != model_name:
|
|
120
|
-
degraded_thinking_texts.append(item.content)
|
|
121
|
-
else:
|
|
122
|
-
pending_thought_text = item.content
|
|
123
|
-
case model.ReasoningEncryptedItem():
|
|
124
|
-
if not (
|
|
125
|
-
model_name is not None
|
|
126
|
-
and item.model == model_name
|
|
127
|
-
and item.encrypted_content
|
|
128
|
-
and (item.format or "").startswith("google")
|
|
129
|
-
and pending_thought_text
|
|
130
|
-
):
|
|
131
|
-
continue
|
|
132
|
-
pending_thought_signature = item.encrypted_content
|
|
133
|
-
parts.append(
|
|
134
|
-
types.Part(
|
|
135
|
-
text=pending_thought_text,
|
|
136
|
-
thought=True,
|
|
137
|
-
thought_signature=pending_thought_signature,
|
|
138
|
-
)
|
|
139
|
-
)
|
|
140
|
-
pending_thought_text = None
|
|
141
|
-
pending_thought_signature = None
|
|
142
|
-
|
|
143
|
-
if pending_thought_text:
|
|
105
|
+
def flush_thought() -> None:
|
|
106
|
+
nonlocal pending_thought_text, pending_thought_signature
|
|
107
|
+
if pending_thought_text is None and pending_thought_signature is None:
|
|
108
|
+
return
|
|
144
109
|
parts.append(
|
|
145
110
|
types.Part(
|
|
146
|
-
text=pending_thought_text,
|
|
111
|
+
text=pending_thought_text or "",
|
|
147
112
|
thought=True,
|
|
148
113
|
thought_signature=pending_thought_signature,
|
|
149
114
|
)
|
|
150
115
|
)
|
|
116
|
+
pending_thought_text = None
|
|
117
|
+
pending_thought_signature = 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
|
+
pending_thought_text = 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 and (part.format or "").startswith("google"):
|
|
129
|
+
pending_thought_signature = part.signature
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
flush_thought()
|
|
133
|
+
if isinstance(part, message.TextPart):
|
|
134
|
+
parts.append(types.Part(text=part.text))
|
|
135
|
+
elif isinstance(part, message.ToolCallPart):
|
|
136
|
+
args: dict[str, Any]
|
|
137
|
+
if part.arguments_json:
|
|
138
|
+
try:
|
|
139
|
+
args = json.loads(part.arguments_json)
|
|
140
|
+
except json.JSONDecodeError:
|
|
141
|
+
args = {"_raw": part.arguments_json}
|
|
142
|
+
else:
|
|
143
|
+
args = {}
|
|
144
|
+
parts.append(types.Part(function_call=types.FunctionCall(id=part.call_id, name=part.tool_name, args=args)))
|
|
145
|
+
|
|
146
|
+
flush_thought()
|
|
151
147
|
|
|
152
148
|
if degraded_thinking_texts:
|
|
153
149
|
parts.insert(0, types.Part(text="<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"))
|
|
154
150
|
|
|
155
|
-
if group.text_content:
|
|
156
|
-
parts.append(types.Part(text=group.text_content))
|
|
157
|
-
|
|
158
|
-
for tc in group.tool_calls:
|
|
159
|
-
args: dict[str, Any]
|
|
160
|
-
if tc.arguments:
|
|
161
|
-
try:
|
|
162
|
-
args = json.loads(tc.arguments)
|
|
163
|
-
except json.JSONDecodeError:
|
|
164
|
-
args = {"_raw": tc.arguments}
|
|
165
|
-
else:
|
|
166
|
-
args = {}
|
|
167
|
-
parts.append(types.Part(function_call=types.FunctionCall(id=tc.call_id, name=tc.name, args=args)))
|
|
168
|
-
|
|
169
151
|
if not parts:
|
|
170
152
|
return None
|
|
171
153
|
return types.Content(role="model", parts=parts)
|
|
172
154
|
|
|
173
155
|
|
|
174
156
|
def convert_history_to_contents(
|
|
175
|
-
history: list[
|
|
157
|
+
history: list[message.Message],
|
|
176
158
|
model_name: str | None,
|
|
177
159
|
) -> list[types.Content]:
|
|
178
160
|
contents: list[types.Content] = []
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def
|
|
182
|
-
nonlocal
|
|
183
|
-
if
|
|
184
|
-
contents.extend(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
for
|
|
188
|
-
match
|
|
189
|
-
case
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
case
|
|
195
|
-
|
|
196
|
-
content =
|
|
161
|
+
pending_tool_messages: list[tuple[message.ToolResultMessage, DeveloperAttachment]] = []
|
|
162
|
+
|
|
163
|
+
def flush_tool_messages() -> None:
|
|
164
|
+
nonlocal pending_tool_messages
|
|
165
|
+
if pending_tool_messages:
|
|
166
|
+
contents.extend(_tool_messages_to_contents(pending_tool_messages, model_name=model_name))
|
|
167
|
+
pending_tool_messages = []
|
|
168
|
+
|
|
169
|
+
for msg, attachment in attach_developer_messages(history):
|
|
170
|
+
match msg:
|
|
171
|
+
case message.ToolResultMessage():
|
|
172
|
+
pending_tool_messages.append((msg, attachment))
|
|
173
|
+
case message.UserMessage():
|
|
174
|
+
flush_tool_messages()
|
|
175
|
+
contents.append(_user_message_to_content(msg, attachment))
|
|
176
|
+
case message.AssistantMessage():
|
|
177
|
+
flush_tool_messages()
|
|
178
|
+
content = _assistant_message_to_content(msg, model_name=model_name)
|
|
197
179
|
if content is not None:
|
|
198
180
|
contents.append(content)
|
|
181
|
+
case message.SystemMessage():
|
|
182
|
+
continue
|
|
183
|
+
case _:
|
|
184
|
+
continue
|
|
199
185
|
|
|
200
|
-
|
|
186
|
+
flush_tool_messages()
|
|
201
187
|
return contents
|
|
202
188
|
|
|
203
189
|
|