klaude-code 2.9.0__py3-none-any.whl → 2.9.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/antigravity/oauth.py +33 -29
- klaude_code/auth/claude/oauth.py +34 -49
- klaude_code/config/assets/builtin_config.yaml +17 -0
- klaude_code/core/agent_profile.py +2 -5
- klaude_code/core/task.py +1 -1
- klaude_code/core/tool/file/read_tool.py +13 -2
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
- klaude_code/llm/input_common.py +18 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +3 -3
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/stream.py +19 -9
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/registry.py +3 -3
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -1
- klaude_code/protocol/events.py +0 -1
- klaude_code/protocol/message.py +1 -0
- klaude_code/protocol/model.py +14 -1
- klaude_code/session/session.py +22 -1
- klaude_code/tui/components/bash_syntax.py +4 -0
- klaude_code/tui/components/diffs.py +3 -2
- klaude_code/tui/components/metadata.py +0 -3
- klaude_code/tui/components/rich/markdown.py +120 -33
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +9 -6
- klaude_code/tui/components/tools.py +22 -0
- klaude_code/tui/components/user_input.py +2 -0
- klaude_code/tui/machine.py +25 -47
- klaude_code/tui/renderer.py +37 -13
- klaude_code/tui/terminal/image.py +24 -3
- {klaude_code-2.9.0.dist-info → klaude_code-2.9.1.dist-info}/METADATA +1 -1
- {klaude_code-2.9.0.dist-info → klaude_code-2.9.1.dist-info}/RECORD +40 -40
- klaude_code/llm/bedrock/__init__.py +0 -3
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{codex → openai_codex}/prompt_sync.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/input.py +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -258,42 +258,46 @@ class AntigravityOAuth:
|
|
|
258
258
|
)
|
|
259
259
|
|
|
260
260
|
def refresh(self) -> AntigravityAuthState:
|
|
261
|
-
"""Refresh the access token using refresh token.
|
|
262
|
-
state = self.token_manager.get_state()
|
|
263
|
-
if state is None:
|
|
264
|
-
raise AntigravityNotLoggedInError("Not logged in to Antigravity. Run 'klaude login antigravity' first.")
|
|
261
|
+
"""Refresh the access token using refresh token with file locking.
|
|
265
262
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"refresh_token": state.refresh_token,
|
|
270
|
-
"grant_type": "refresh_token",
|
|
271
|
-
}
|
|
263
|
+
Uses file locking to prevent multiple instances from refreshing simultaneously.
|
|
264
|
+
If another instance has already refreshed, returns the updated state.
|
|
265
|
+
"""
|
|
272
266
|
|
|
273
|
-
|
|
274
|
-
|
|
267
|
+
def do_refresh(current_state: AntigravityAuthState) -> AntigravityAuthState:
|
|
268
|
+
data = {
|
|
269
|
+
"client_id": CLIENT_ID,
|
|
270
|
+
"client_secret": CLIENT_SECRET,
|
|
271
|
+
"refresh_token": current_state.refresh_token,
|
|
272
|
+
"grant_type": "refresh_token",
|
|
273
|
+
}
|
|
275
274
|
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
with httpx.Client() as client:
|
|
276
|
+
response = client.post(TOKEN_URL, data=data, timeout=30)
|
|
278
277
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
refresh_token = tokens.get("refresh_token", state.refresh_token)
|
|
282
|
-
expires_in = tokens.get("expires_in", 3600)
|
|
278
|
+
if response.status_code != 200:
|
|
279
|
+
raise AntigravityTokenExpiredError(f"Token refresh failed: {response.text}")
|
|
283
280
|
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
tokens = response.json()
|
|
282
|
+
access_token = tokens["access_token"]
|
|
283
|
+
refresh_token = tokens.get("refresh_token", current_state.refresh_token)
|
|
284
|
+
expires_in = tokens.get("expires_in", 3600)
|
|
286
285
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
286
|
+
# Calculate expiry time with 5 minute buffer
|
|
287
|
+
expires_at = int(time.time()) + expires_in - 300
|
|
288
|
+
|
|
289
|
+
return AntigravityAuthState(
|
|
290
|
+
access_token=access_token,
|
|
291
|
+
refresh_token=refresh_token,
|
|
292
|
+
expires_at=expires_at,
|
|
293
|
+
project_id=current_state.project_id,
|
|
294
|
+
email=current_state.email,
|
|
295
|
+
)
|
|
294
296
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
try:
|
|
298
|
+
return self.token_manager.refresh_with_lock(do_refresh)
|
|
299
|
+
except ValueError as e:
|
|
300
|
+
raise AntigravityNotLoggedInError(str(e)) from e
|
|
297
301
|
|
|
298
302
|
def ensure_valid_token(self) -> tuple[str, str]:
|
|
299
303
|
"""Ensure we have a valid access token, refreshing if needed.
|
klaude_code/auth/claude/oauth.py
CHANGED
|
@@ -125,60 +125,45 @@ class ClaudeOAuth:
|
|
|
125
125
|
expires_at=int(time.time()) + int(expires_in),
|
|
126
126
|
)
|
|
127
127
|
|
|
128
|
-
def _do_refresh_request(self, refresh_token: str) -> httpx.Response:
|
|
129
|
-
"""Send token refresh request to OAuth server."""
|
|
130
|
-
payload = {
|
|
131
|
-
"grant_type": "refresh_token",
|
|
132
|
-
"client_id": CLIENT_ID,
|
|
133
|
-
"refresh_token": refresh_token,
|
|
134
|
-
}
|
|
135
|
-
with httpx.Client() as client:
|
|
136
|
-
return client.post(
|
|
137
|
-
TOKEN_URL,
|
|
138
|
-
json=payload,
|
|
139
|
-
headers={"Content-Type": "application/json"},
|
|
140
|
-
)
|
|
141
|
-
|
|
142
128
|
def refresh(self) -> ClaudeAuthState:
|
|
143
|
-
"""Refresh the access token using refresh token.
|
|
129
|
+
"""Refresh the access token using refresh token with file locking.
|
|
144
130
|
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
Uses file locking to prevent multiple instances from refreshing simultaneously.
|
|
132
|
+
If another instance has already refreshed, returns the updated state.
|
|
147
133
|
"""
|
|
148
|
-
state = self.token_manager.get_state()
|
|
149
|
-
if state is None:
|
|
150
|
-
raise ClaudeNotLoggedInError("Not logged in to Claude. Run 'klaude login claude' first.")
|
|
151
134
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
135
|
+
def do_refresh(current_state: ClaudeAuthState) -> ClaudeAuthState:
|
|
136
|
+
payload = {
|
|
137
|
+
"grant_type": "refresh_token",
|
|
138
|
+
"client_id": CLIENT_ID,
|
|
139
|
+
"refresh_token": current_state.refresh_token,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
with httpx.Client() as client:
|
|
143
|
+
response = client.post(
|
|
144
|
+
TOKEN_URL,
|
|
145
|
+
json=payload,
|
|
146
|
+
headers={"Content-Type": "application/json"},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if response.status_code != 200:
|
|
150
|
+
raise ClaudeAuthError(f"Token refresh failed: {response.text}")
|
|
151
|
+
|
|
152
|
+
tokens = response.json()
|
|
153
|
+
access_token = tokens["access_token"]
|
|
154
|
+
refresh_token = tokens.get("refresh_token", current_state.refresh_token)
|
|
155
|
+
expires_in = tokens.get("expires_in", 3600)
|
|
156
|
+
|
|
157
|
+
return ClaudeAuthState(
|
|
158
|
+
access_token=access_token,
|
|
159
|
+
refresh_token=refresh_token,
|
|
160
|
+
expires_at=int(time.time()) + int(expires_in),
|
|
161
|
+
)
|
|
174
162
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
180
|
-
self.token_manager.save(new_state)
|
|
181
|
-
return new_state
|
|
163
|
+
try:
|
|
164
|
+
return self.token_manager.refresh_with_lock(do_refresh)
|
|
165
|
+
except ValueError as e:
|
|
166
|
+
raise ClaudeNotLoggedInError(str(e)) from e
|
|
182
167
|
|
|
183
168
|
def ensure_valid_token(self) -> str:
|
|
184
169
|
"""Ensure we have a valid access token, refreshing if needed."""
|
|
@@ -148,6 +148,8 @@ provider_list:
|
|
|
148
148
|
modalities:
|
|
149
149
|
- image
|
|
150
150
|
- text
|
|
151
|
+
image_config:
|
|
152
|
+
image_size: "4K"
|
|
151
153
|
cost: {input: 2, output: 12, cache_read: 0.2, image: 120}
|
|
152
154
|
|
|
153
155
|
- model_name: nano-banana
|
|
@@ -221,6 +223,8 @@ provider_list:
|
|
|
221
223
|
modalities:
|
|
222
224
|
- image
|
|
223
225
|
- text
|
|
226
|
+
image_config:
|
|
227
|
+
image_size: "4K"
|
|
224
228
|
cost: {input: 2, output: 12, cache_read: 0.2, image: 120}
|
|
225
229
|
|
|
226
230
|
- model_name: nano-banana
|
|
@@ -275,6 +279,19 @@ provider_list:
|
|
|
275
279
|
cost: {input: 4, output: 16, cache_read: 1, currency: CNY}
|
|
276
280
|
|
|
277
281
|
|
|
282
|
+
- provider_name: cerebras
|
|
283
|
+
protocol: openai
|
|
284
|
+
api_key: ${CEREBRAS_API_KEY}
|
|
285
|
+
base_url: https://api.cerebras.ai/v1
|
|
286
|
+
model_list:
|
|
287
|
+
|
|
288
|
+
- model_name: glm
|
|
289
|
+
model_id: zai-glm-4.7
|
|
290
|
+
context_limit: 131072
|
|
291
|
+
max_tokens: 12800
|
|
292
|
+
cost: {input: 2.25, output: 2.75}
|
|
293
|
+
|
|
294
|
+
|
|
278
295
|
- provider_name: claude-max
|
|
279
296
|
protocol: claude_oauth
|
|
280
297
|
disabled: true
|
|
@@ -132,7 +132,7 @@ def load_system_prompt(
|
|
|
132
132
|
|
|
133
133
|
# For codex_oauth protocol, use dynamic prompts from GitHub (no additions).
|
|
134
134
|
if protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
|
|
135
|
-
from klaude_code.llm.
|
|
135
|
+
from klaude_code.llm.openai_codex.prompt_sync import get_codex_instructions
|
|
136
136
|
|
|
137
137
|
return get_codex_instructions(model_name)
|
|
138
138
|
|
|
@@ -176,8 +176,6 @@ def load_agent_tools(
|
|
|
176
176
|
# Main agent tools
|
|
177
177
|
if "gpt-5" in model_name:
|
|
178
178
|
tool_names: list[str] = [tools.BASH, tools.READ, tools.APPLY_PATCH, tools.UPDATE_PLAN]
|
|
179
|
-
elif "gemini-3" in model_name:
|
|
180
|
-
tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE]
|
|
181
179
|
else:
|
|
182
180
|
tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE, tools.TODO_WRITE]
|
|
183
181
|
|
|
@@ -189,8 +187,7 @@ def load_agent_tools(
|
|
|
189
187
|
else:
|
|
190
188
|
tool_names.append(tools.IMAGE_GEN)
|
|
191
189
|
|
|
192
|
-
tool_names.
|
|
193
|
-
# tool_names.extend([tools.MEMORY])
|
|
190
|
+
tool_names.append(tools.MERMAID)
|
|
194
191
|
return get_tool_schemas(tool_names)
|
|
195
192
|
|
|
196
193
|
|
klaude_code/core/task.py
CHANGED
|
@@ -210,7 +210,7 @@ class TaskExecutor:
|
|
|
210
210
|
accumulated = self._metadata_accumulator.get_partial_item(task_duration_s)
|
|
211
211
|
if accumulated is not None:
|
|
212
212
|
session_id = self._context.session_ctx.session_id
|
|
213
|
-
ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id
|
|
213
|
+
ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id))
|
|
214
214
|
self._context.session_ctx.append_history([accumulated])
|
|
215
215
|
|
|
216
216
|
return ui_events
|
|
@@ -22,7 +22,7 @@ from klaude_code.core.tool.file._utils import file_exists, is_directory
|
|
|
22
22
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
23
23
|
from klaude_code.core.tool.tool_registry import register
|
|
24
24
|
from klaude_code.protocol import llm_param, message, model, tools
|
|
25
|
-
from klaude_code.protocol.model import ImageUIExtra
|
|
25
|
+
from klaude_code.protocol.model import ImageUIExtra, ReadPreviewLine, ReadPreviewUIExtra
|
|
26
26
|
|
|
27
27
|
_IMAGE_MIME_TYPES: dict[str, str] = {
|
|
28
28
|
".png": "image/png",
|
|
@@ -346,4 +346,15 @@ class ReadTool(ToolABC):
|
|
|
346
346
|
read_result_str = "\n".join(lines_out)
|
|
347
347
|
_track_file_access(context.file_tracker, file_path, content_sha256=read_result.content_sha256)
|
|
348
348
|
|
|
349
|
-
|
|
349
|
+
# When offset > 1, show a preview of the first 5 lines in UI
|
|
350
|
+
ui_extra = None
|
|
351
|
+
if args.offset is not None and args.offset > 1:
|
|
352
|
+
preview_count = 5
|
|
353
|
+
preview_lines = [
|
|
354
|
+
ReadPreviewLine(line_no=line_no, content=content)
|
|
355
|
+
for line_no, content in read_result.selected_lines[:preview_count]
|
|
356
|
+
]
|
|
357
|
+
remaining = len(read_result.selected_lines) - len(preview_lines)
|
|
358
|
+
ui_extra = ReadPreviewUIExtra(lines=preview_lines, remaining_lines=remaining)
|
|
359
|
+
|
|
360
|
+
return message.ToolResultMessage(status="success", output_text=read_result_str, ui_extra=ui_extra)
|
|
@@ -342,7 +342,7 @@ class BashTool(ToolABC):
|
|
|
342
342
|
if not combined:
|
|
343
343
|
combined = f"Command exited with code {rc}"
|
|
344
344
|
return message.ToolResultMessage(
|
|
345
|
-
status="
|
|
345
|
+
status="success",
|
|
346
346
|
# Preserve leading whitespace; only trim trailing newlines.
|
|
347
347
|
output_text=combined.rstrip("\n"),
|
|
348
348
|
)
|
klaude_code/llm/input_common.py
CHANGED
|
@@ -149,6 +149,14 @@ def build_assistant_common_fields(
|
|
|
149
149
|
}
|
|
150
150
|
for tc in tool_calls
|
|
151
151
|
]
|
|
152
|
+
|
|
153
|
+
thinking_parts = [part for part in msg.parts if isinstance(part, message.ThinkingTextPart)]
|
|
154
|
+
if thinking_parts:
|
|
155
|
+
thinking_text = "".join(part.text for part in thinking_parts)
|
|
156
|
+
reasoning_field = next((p.reasoning_field for p in thinking_parts if p.reasoning_field), None)
|
|
157
|
+
if thinking_text and reasoning_field:
|
|
158
|
+
result[reasoning_field] = thinking_text
|
|
159
|
+
|
|
152
160
|
return result
|
|
153
161
|
|
|
154
162
|
|
|
@@ -185,4 +193,14 @@ def apply_config_defaults(param: "LLMCallParameter", config: "LLMConfigParameter
|
|
|
185
193
|
param.verbosity = config.verbosity
|
|
186
194
|
if param.thinking is None:
|
|
187
195
|
param.thinking = config.thinking
|
|
196
|
+
if param.modalities is None:
|
|
197
|
+
param.modalities = config.modalities
|
|
198
|
+
if param.image_config is None:
|
|
199
|
+
param.image_config = config.image_config
|
|
200
|
+
elif config.image_config is not None:
|
|
201
|
+
# Merge field-level: param overrides config defaults
|
|
202
|
+
if param.image_config.aspect_ratio is None:
|
|
203
|
+
param.image_config.aspect_ratio = config.image_config.aspect_ratio
|
|
204
|
+
if param.image_config.image_size is None:
|
|
205
|
+
param.image_config.image_size = config.image_config.image_size
|
|
188
206
|
return param
|
|
@@ -20,9 +20,9 @@ from klaude_code.const import (
|
|
|
20
20
|
)
|
|
21
21
|
from klaude_code.llm.client import LLMClientABC, LLMStreamABC
|
|
22
22
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
23
|
+
from klaude_code.llm.openai_responses.client import ResponsesLLMStream
|
|
24
|
+
from klaude_code.llm.openai_responses.input import convert_history_to_input, convert_tool_schema
|
|
23
25
|
from klaude_code.llm.registry import register
|
|
24
|
-
from klaude_code.llm.responses.client import ResponsesLLMStream
|
|
25
|
-
from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
|
|
26
26
|
from klaude_code.llm.usage import MetadataTracker, error_llm_stream
|
|
27
27
|
from klaude_code.log import DebugType, log_debug
|
|
28
28
|
from klaude_code.protocol import llm_param
|
|
@@ -164,7 +164,7 @@ def _is_invalid_instruction_error(e: Exception) -> bool:
|
|
|
164
164
|
|
|
165
165
|
def _invalidate_prompt_cache_for_model(model_id: str) -> None:
|
|
166
166
|
"""Invalidate the cached prompt for a model to force refresh."""
|
|
167
|
-
from klaude_code.llm.
|
|
167
|
+
from klaude_code.llm.openai_codex.prompt_sync import invalidate_cache
|
|
168
168
|
|
|
169
169
|
log_debug(
|
|
170
170
|
f"Invalidating prompt cache for model {model_id} due to invalid instruction error",
|
|
@@ -39,9 +39,11 @@ def build_payload(param: llm_param.LLMCallParameter) -> tuple[CompletionCreatePa
|
|
|
39
39
|
"max_tokens": param.max_tokens,
|
|
40
40
|
"tools": tools,
|
|
41
41
|
"reasoning_effort": param.thinking.reasoning_effort if param.thinking else None,
|
|
42
|
-
"verbosity": param.verbosity,
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
if param.verbosity:
|
|
45
|
+
payload["verbosity"] = param.verbosity
|
|
46
|
+
|
|
45
47
|
return payload, extra_body
|
|
46
48
|
|
|
47
49
|
|
|
@@ -76,9 +76,11 @@ class StreamStateManager:
|
|
|
76
76
|
"""Set the response ID once received from the stream."""
|
|
77
77
|
self.response_id = response_id
|
|
78
78
|
|
|
79
|
-
def append_thinking_text(self, text: str) -> None:
|
|
79
|
+
def append_thinking_text(self, text: str, *, reasoning_field: str | None = None) -> None:
|
|
80
80
|
"""Append thinking text, merging with the previous ThinkingTextPart when possible."""
|
|
81
|
-
append_thinking_text_part(
|
|
81
|
+
append_thinking_text_part(
|
|
82
|
+
self.assistant_parts, text, model_id=self.param_model, reasoning_field=reasoning_field
|
|
83
|
+
)
|
|
82
84
|
|
|
83
85
|
def append_text(self, text: str) -> None:
|
|
84
86
|
"""Append assistant text, merging with the previous TextPart when possible."""
|
|
@@ -150,6 +152,7 @@ class ReasoningDeltaResult:
|
|
|
150
152
|
|
|
151
153
|
handled: bool
|
|
152
154
|
outputs: list[str | message.Part]
|
|
155
|
+
reasoning_field: str | None = None # Original field name: reasoning_content, reasoning, reasoning_text
|
|
153
156
|
|
|
154
157
|
|
|
155
158
|
class ReasoningHandlerABC(ABC):
|
|
@@ -168,8 +171,11 @@ class ReasoningHandlerABC(ABC):
|
|
|
168
171
|
"""Flush buffered reasoning content (usually at stage transition/finalize)."""
|
|
169
172
|
|
|
170
173
|
|
|
174
|
+
REASONING_FIELDS = ("reasoning_content", "reasoning", "reasoning_text")
|
|
175
|
+
|
|
176
|
+
|
|
171
177
|
class DefaultReasoningHandler(ReasoningHandlerABC):
|
|
172
|
-
"""Handles OpenAI-compatible reasoning fields (reasoning_content / reasoning)."""
|
|
178
|
+
"""Handles OpenAI-compatible reasoning fields (reasoning_content / reasoning / reasoning_text)."""
|
|
173
179
|
|
|
174
180
|
def __init__(
|
|
175
181
|
self,
|
|
@@ -179,16 +185,20 @@ class DefaultReasoningHandler(ReasoningHandlerABC):
|
|
|
179
185
|
) -> None:
|
|
180
186
|
self._param_model = param_model
|
|
181
187
|
self._response_id = response_id
|
|
188
|
+
self._reasoning_field: str | None = None
|
|
182
189
|
|
|
183
190
|
def set_response_id(self, response_id: str | None) -> None:
|
|
184
191
|
self._response_id = response_id
|
|
185
192
|
|
|
186
193
|
def on_delta(self, delta: object) -> ReasoningDeltaResult:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
for field_name in REASONING_FIELDS:
|
|
195
|
+
content = getattr(delta, field_name, None)
|
|
196
|
+
if content:
|
|
197
|
+
if self._reasoning_field is None:
|
|
198
|
+
self._reasoning_field = field_name
|
|
199
|
+
text = str(content)
|
|
200
|
+
return ReasoningDeltaResult(handled=True, outputs=[text], reasoning_field=self._reasoning_field)
|
|
201
|
+
return ReasoningDeltaResult(handled=False, outputs=[])
|
|
192
202
|
|
|
193
203
|
def flush(self) -> list[message.Part]:
|
|
194
204
|
return []
|
|
@@ -282,7 +292,7 @@ async def parse_chat_completions_stream(
|
|
|
282
292
|
if not output:
|
|
283
293
|
continue
|
|
284
294
|
metadata_tracker.record_token()
|
|
285
|
-
state.append_thinking_text(output)
|
|
295
|
+
state.append_thinking_text(output, reasoning_field=reasoning_result.reasoning_field)
|
|
286
296
|
yield message.ThinkingTextDelta(content=output, response_id=state.response_id)
|
|
287
297
|
else:
|
|
288
298
|
state.assistant_parts.append(output)
|
|
@@ -11,8 +11,8 @@ from openai.types.responses.response_create_params import ResponseCreateParamsSt
|
|
|
11
11
|
from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
|
|
12
12
|
from klaude_code.llm.client import LLMClientABC, LLMStreamABC
|
|
13
13
|
from klaude_code.llm.input_common import apply_config_defaults
|
|
14
|
+
from klaude_code.llm.openai_responses.input import convert_history_to_input, convert_tool_schema
|
|
14
15
|
from klaude_code.llm.registry import register
|
|
15
|
-
from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
|
|
16
16
|
from klaude_code.llm.stream_parts import (
|
|
17
17
|
append_text_part,
|
|
18
18
|
append_thinking_text_part,
|
klaude_code/llm/registry.py
CHANGED
|
@@ -15,11 +15,11 @@ _REGISTRY: dict[llm_param.LLMClientProtocol, type["LLMClientABC"]] = {}
|
|
|
15
15
|
_PROTOCOL_MODULES: dict[llm_param.LLMClientProtocol, str] = {
|
|
16
16
|
llm_param.LLMClientProtocol.ANTHROPIC: "klaude_code.llm.anthropic",
|
|
17
17
|
llm_param.LLMClientProtocol.CLAUDE_OAUTH: "klaude_code.llm.claude",
|
|
18
|
-
llm_param.LLMClientProtocol.BEDROCK: "klaude_code.llm.
|
|
19
|
-
llm_param.LLMClientProtocol.CODEX_OAUTH: "klaude_code.llm.
|
|
18
|
+
llm_param.LLMClientProtocol.BEDROCK: "klaude_code.llm.bedrock_anthropic",
|
|
19
|
+
llm_param.LLMClientProtocol.CODEX_OAUTH: "klaude_code.llm.openai_codex",
|
|
20
20
|
llm_param.LLMClientProtocol.OPENAI: "klaude_code.llm.openai_compatible",
|
|
21
21
|
llm_param.LLMClientProtocol.OPENROUTER: "klaude_code.llm.openrouter",
|
|
22
|
-
llm_param.LLMClientProtocol.RESPONSES: "klaude_code.llm.
|
|
22
|
+
llm_param.LLMClientProtocol.RESPONSES: "klaude_code.llm.openai_responses",
|
|
23
23
|
llm_param.LLMClientProtocol.GOOGLE: "klaude_code.llm.google",
|
|
24
24
|
llm_param.LLMClientProtocol.ANTIGRAVITY: "klaude_code.llm.antigravity",
|
|
25
25
|
}
|
klaude_code/llm/stream_parts.py
CHANGED
|
@@ -24,6 +24,7 @@ def append_thinking_text_part(
|
|
|
24
24
|
text: str,
|
|
25
25
|
*,
|
|
26
26
|
model_id: str,
|
|
27
|
+
reasoning_field: str | None = None,
|
|
27
28
|
force_new: bool = False,
|
|
28
29
|
) -> int | None:
|
|
29
30
|
if not text:
|
|
@@ -35,10 +36,11 @@ def append_thinking_text_part(
|
|
|
35
36
|
parts[-1] = message.ThinkingTextPart(
|
|
36
37
|
text=last.text + text,
|
|
37
38
|
model_id=model_id,
|
|
39
|
+
reasoning_field=reasoning_field or last.reasoning_field,
|
|
38
40
|
)
|
|
39
41
|
return len(parts) - 1
|
|
40
42
|
|
|
41
|
-
parts.append(message.ThinkingTextPart(text=text, model_id=model_id))
|
|
43
|
+
parts.append(message.ThinkingTextPart(text=text, model_id=model_id, reasoning_field=reasoning_field))
|
|
42
44
|
return len(parts) - 1
|
|
43
45
|
|
|
44
46
|
|
klaude_code/llm/usage.py
CHANGED
|
@@ -28,7 +28,7 @@ def calculate_cost(usage: model.Usage, cost_config: llm_param.Cost | None) -> No
|
|
|
28
28
|
usage.output_cost = (usage.output_tokens / 1_000_000) * cost_config.output
|
|
29
29
|
|
|
30
30
|
# Cache read cost
|
|
31
|
-
usage.cache_read_cost = (usage.cached_tokens / 1_000_000) * cost_config.cache_read
|
|
31
|
+
usage.cache_read_cost = (usage.cached_tokens / 1_000_000) * (cost_config.cache_read or cost_config.input)
|
|
32
32
|
|
|
33
33
|
# Image generation cost
|
|
34
34
|
usage.image_cost = (usage.image_tokens / 1_000_000) * cost_config.image
|
klaude_code/protocol/events.py
CHANGED
klaude_code/protocol/message.py
CHANGED
|
@@ -112,6 +112,7 @@ class ThinkingTextPart(BaseModel):
|
|
|
112
112
|
id: str | None = None
|
|
113
113
|
text: str
|
|
114
114
|
model_id: str | None = None
|
|
115
|
+
reasoning_field: str | None = None # Original field name: reasoning_content, reasoning, reasoning_text
|
|
115
116
|
|
|
116
117
|
|
|
117
118
|
class ThinkingSignaturePart(BaseModel):
|
klaude_code/protocol/model.py
CHANGED
|
@@ -228,6 +228,17 @@ class MarkdownDocUIExtra(BaseModel):
|
|
|
228
228
|
content: str
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
class ReadPreviewLine(BaseModel):
|
|
232
|
+
line_no: int
|
|
233
|
+
content: str
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class ReadPreviewUIExtra(BaseModel):
|
|
237
|
+
type: Literal["read_preview"] = "read_preview"
|
|
238
|
+
lines: list[ReadPreviewLine]
|
|
239
|
+
remaining_lines: int # lines not shown in preview
|
|
240
|
+
|
|
241
|
+
|
|
231
242
|
class SessionStatusUIExtra(BaseModel):
|
|
232
243
|
type: Literal["session_status"] = "session_status"
|
|
233
244
|
usage: "Usage"
|
|
@@ -243,6 +254,7 @@ MultiUIExtraItem = (
|
|
|
243
254
|
| ImageUIExtra
|
|
244
255
|
| MarkdownDocUIExtra
|
|
245
256
|
| SessionStatusUIExtra
|
|
257
|
+
| ReadPreviewUIExtra
|
|
246
258
|
)
|
|
247
259
|
|
|
248
260
|
|
|
@@ -265,7 +277,8 @@ ToolResultUIExtra = Annotated[
|
|
|
265
277
|
| ImageUIExtra
|
|
266
278
|
| MarkdownDocUIExtra
|
|
267
279
|
| SessionStatusUIExtra
|
|
268
|
-
| MultiUIExtra
|
|
280
|
+
| MultiUIExtra
|
|
281
|
+
| ReadPreviewUIExtra,
|
|
269
282
|
Field(discriminator="type"),
|
|
270
283
|
]
|
|
271
284
|
|
klaude_code/session/session.py
CHANGED
|
@@ -316,10 +316,15 @@ class Session(BaseModel):
|
|
|
316
316
|
prev_item: message.HistoryEvent | None = None
|
|
317
317
|
last_assistant_content: str = ""
|
|
318
318
|
report_back_result: str | None = None
|
|
319
|
+
pending_tool_calls: dict[str, events.ToolCallEvent] = {}
|
|
319
320
|
history = self.conversation_history
|
|
320
321
|
history_len = len(history)
|
|
321
322
|
yield events.TaskStartEvent(session_id=self.id, sub_agent_state=self.sub_agent_state)
|
|
322
323
|
for idx, it in enumerate(history):
|
|
324
|
+
# Flush pending tool calls if current item won't consume them
|
|
325
|
+
if pending_tool_calls and not isinstance(it, message.ToolResultMessage):
|
|
326
|
+
yield from pending_tool_calls.values()
|
|
327
|
+
pending_tool_calls.clear()
|
|
323
328
|
if self.need_turn_start(prev_item, it):
|
|
324
329
|
yield events.TurnStartEvent(session_id=self.id)
|
|
325
330
|
match it:
|
|
@@ -331,6 +336,7 @@ class Session(BaseModel):
|
|
|
331
336
|
# Reconstruct streaming boundaries from saved parts.
|
|
332
337
|
# This allows replay to reuse the same TUI state machine as live events.
|
|
333
338
|
thinking_open = False
|
|
339
|
+
thinking_had_content = False
|
|
334
340
|
assistant_open = False
|
|
335
341
|
|
|
336
342
|
for part in am.parts:
|
|
@@ -342,15 +348,23 @@ class Session(BaseModel):
|
|
|
342
348
|
thinking_open = True
|
|
343
349
|
yield events.ThinkingStartEvent(response_id=am.response_id, session_id=self.id)
|
|
344
350
|
if part.text:
|
|
351
|
+
if thinking_had_content:
|
|
352
|
+
yield events.ThinkingDeltaEvent(
|
|
353
|
+
content=" \n \n",
|
|
354
|
+
response_id=am.response_id,
|
|
355
|
+
session_id=self.id,
|
|
356
|
+
)
|
|
345
357
|
yield events.ThinkingDeltaEvent(
|
|
346
358
|
content=part.text,
|
|
347
359
|
response_id=am.response_id,
|
|
348
360
|
session_id=self.id,
|
|
349
361
|
)
|
|
362
|
+
thinking_had_content = True
|
|
350
363
|
continue
|
|
351
364
|
|
|
352
365
|
if thinking_open:
|
|
353
366
|
thinking_open = False
|
|
367
|
+
thinking_had_content = False
|
|
354
368
|
yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
|
|
355
369
|
|
|
356
370
|
if isinstance(part, message.TextPart):
|
|
@@ -380,7 +394,7 @@ class Session(BaseModel):
|
|
|
380
394
|
continue
|
|
381
395
|
if part.tool_name == tools.REPORT_BACK:
|
|
382
396
|
report_back_result = part.arguments_json
|
|
383
|
-
|
|
397
|
+
pending_tool_calls[part.call_id] = events.ToolCallEvent(
|
|
384
398
|
tool_call_id=part.call_id,
|
|
385
399
|
tool_name=part.tool_name,
|
|
386
400
|
arguments=part.arguments_json,
|
|
@@ -390,6 +404,8 @@ class Session(BaseModel):
|
|
|
390
404
|
if am.stop_reason == "aborted":
|
|
391
405
|
yield events.InterruptEvent(session_id=self.id)
|
|
392
406
|
case message.ToolResultMessage() as tr:
|
|
407
|
+
if tr.call_id in pending_tool_calls:
|
|
408
|
+
yield pending_tool_calls.pop(tr.call_id)
|
|
393
409
|
status = "success" if tr.status == "success" else "error"
|
|
394
410
|
# Check if this is the last tool result in the current turn
|
|
395
411
|
next_item = history[idx + 1] if idx + 1 < history_len else None
|
|
@@ -437,6 +453,11 @@ class Session(BaseModel):
|
|
|
437
453
|
pass
|
|
438
454
|
prev_item = it
|
|
439
455
|
|
|
456
|
+
# Flush any remaining pending tool calls (e.g., from aborted or incomplete sessions)
|
|
457
|
+
if pending_tool_calls:
|
|
458
|
+
yield from pending_tool_calls.values()
|
|
459
|
+
pending_tool_calls.clear()
|
|
460
|
+
|
|
440
461
|
has_structured_output = report_back_result is not None
|
|
441
462
|
task_result = report_back_result if has_structured_output else last_assistant_content
|
|
442
463
|
|
|
@@ -187,6 +187,10 @@ def highlight_bash_command(command: str) -> Text:
|
|
|
187
187
|
expect_subcommand = False
|
|
188
188
|
elif token_type in (Token.Text.Whitespace,):
|
|
189
189
|
result.append(token_value)
|
|
190
|
+
# Newline starts a new command context (like ; or &&)
|
|
191
|
+
if "\n" in token_value:
|
|
192
|
+
expect_command = True
|
|
193
|
+
expect_subcommand = False
|
|
190
194
|
elif token_type == Token.Name.Builtin:
|
|
191
195
|
# Built-in commands are always commands
|
|
192
196
|
result.append(token_value, style=ThemeKey.BASH_COMMAND)
|