klaude-code 2.8.1__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.
Files changed (107) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +33 -38
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/claude/oauth.py +34 -49
  6. klaude_code/auth/codex/exceptions.py +0 -4
  7. klaude_code/auth/codex/oauth.py +32 -28
  8. klaude_code/auth/codex/token_manager.py +0 -18
  9. klaude_code/cli/cost_cmd.py +128 -39
  10. klaude_code/cli/list_model.py +27 -10
  11. klaude_code/cli/main.py +14 -3
  12. klaude_code/config/assets/builtin_config.yaml +25 -24
  13. klaude_code/config/config.py +47 -25
  14. klaude_code/config/sub_agent_model_helper.py +18 -13
  15. klaude_code/config/thinking.py +0 -8
  16. klaude_code/const.py +1 -1
  17. klaude_code/core/agent_profile.py +11 -56
  18. klaude_code/core/compaction/overflow.py +0 -4
  19. klaude_code/core/executor.py +33 -5
  20. klaude_code/core/manager/llm_clients.py +9 -1
  21. klaude_code/core/prompts/prompt-claude-code.md +4 -4
  22. klaude_code/core/reminders.py +21 -23
  23. klaude_code/core/task.py +1 -5
  24. klaude_code/core/tool/__init__.py +3 -2
  25. klaude_code/core/tool/file/apply_patch.py +0 -27
  26. klaude_code/core/tool/file/read_tool.md +3 -2
  27. klaude_code/core/tool/file/read_tool.py +27 -3
  28. klaude_code/core/tool/offload.py +0 -35
  29. klaude_code/core/tool/shell/bash_tool.py +1 -1
  30. klaude_code/core/tool/sub_agent/__init__.py +6 -0
  31. klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  32. klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  33. klaude_code/core/tool/sub_agent/task.md +20 -0
  34. klaude_code/core/tool/sub_agent/task.py +205 -0
  35. klaude_code/core/tool/tool_registry.py +0 -16
  36. klaude_code/core/turn.py +1 -1
  37. klaude_code/llm/anthropic/input.py +6 -5
  38. klaude_code/llm/antigravity/input.py +14 -7
  39. klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
  40. klaude_code/llm/google/client.py +8 -6
  41. klaude_code/llm/google/input.py +20 -12
  42. klaude_code/llm/image.py +18 -11
  43. klaude_code/llm/input_common.py +32 -6
  44. klaude_code/llm/json_stable.py +37 -0
  45. klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
  46. klaude_code/llm/{codex → openai_codex}/client.py +24 -2
  47. klaude_code/llm/openai_codex/prompt_sync.py +237 -0
  48. klaude_code/llm/openai_compatible/client.py +3 -1
  49. klaude_code/llm/openai_compatible/input.py +0 -10
  50. klaude_code/llm/openai_compatible/stream.py +35 -10
  51. klaude_code/llm/{responses → openai_responses}/client.py +1 -1
  52. klaude_code/llm/{responses → openai_responses}/input.py +15 -5
  53. klaude_code/llm/registry.py +3 -8
  54. klaude_code/llm/stream_parts.py +3 -1
  55. klaude_code/llm/usage.py +1 -9
  56. klaude_code/protocol/events.py +2 -2
  57. klaude_code/protocol/message.py +3 -2
  58. klaude_code/protocol/model.py +34 -2
  59. klaude_code/protocol/op.py +13 -0
  60. klaude_code/protocol/op_handler.py +5 -0
  61. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  62. klaude_code/protocol/sub_agent/__init__.py +13 -34
  63. klaude_code/protocol/sub_agent/explore.py +7 -34
  64. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  65. klaude_code/protocol/sub_agent/task.py +3 -47
  66. klaude_code/protocol/sub_agent/web.py +8 -52
  67. klaude_code/protocol/tools.py +2 -0
  68. klaude_code/session/session.py +80 -22
  69. klaude_code/session/store.py +0 -4
  70. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  71. klaude_code/skill/system_skills.py +0 -20
  72. klaude_code/tui/command/fork_session_cmd.py +5 -2
  73. klaude_code/tui/command/resume_cmd.py +9 -2
  74. klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  75. klaude_code/tui/components/assistant.py +0 -26
  76. klaude_code/tui/components/bash_syntax.py +4 -0
  77. klaude_code/tui/components/command_output.py +3 -1
  78. klaude_code/tui/components/developer.py +3 -0
  79. klaude_code/tui/components/diffs.py +4 -209
  80. klaude_code/tui/components/errors.py +4 -0
  81. klaude_code/tui/components/mermaid_viewer.py +2 -2
  82. klaude_code/tui/components/metadata.py +0 -3
  83. klaude_code/tui/components/rich/markdown.py +120 -87
  84. klaude_code/tui/components/rich/status.py +2 -2
  85. klaude_code/tui/components/rich/theme.py +11 -6
  86. klaude_code/tui/components/sub_agent.py +2 -46
  87. klaude_code/tui/components/thinking.py +0 -33
  88. klaude_code/tui/components/tools.py +65 -21
  89. klaude_code/tui/components/user_input.py +2 -0
  90. klaude_code/tui/input/images.py +21 -18
  91. klaude_code/tui/input/key_bindings.py +2 -2
  92. klaude_code/tui/input/prompt_toolkit.py +49 -49
  93. klaude_code/tui/machine.py +29 -47
  94. klaude_code/tui/renderer.py +48 -33
  95. klaude_code/tui/runner.py +2 -1
  96. klaude_code/tui/terminal/image.py +27 -34
  97. klaude_code/ui/common.py +0 -70
  98. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/METADATA +3 -6
  99. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/RECORD +103 -99
  100. klaude_code/core/tool/sub_agent_tool.py +0 -126
  101. klaude_code/llm/bedrock/__init__.py +0 -3
  102. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  103. klaude_code/tui/components/rich/searchable_text.py +0 -68
  104. /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
  105. /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
  106. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
  107. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  """Codex LLM client using ChatGPT subscription."""
2
2
 
3
- from klaude_code.llm.codex.client import CodexClient
3
+ from klaude_code.llm.openai_codex.client import CodexClient
4
4
 
5
5
  __all__ = ["CodexClient"]
@@ -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
@@ -146,6 +146,28 @@ class CodexClient(LLMClientABC):
146
146
  )
147
147
  except (openai.OpenAIError, httpx.HTTPError) as e:
148
148
  error_message = f"{e.__class__.__name__} {e!s}"
149
+
150
+ # Check for invalid instruction error and invalidate prompt cache
151
+ if _is_invalid_instruction_error(e) and param.model_id:
152
+ _invalidate_prompt_cache_for_model(param.model_id)
153
+
149
154
  return error_llm_stream(metadata_tracker, error=error_message)
150
155
 
151
156
  return ResponsesLLMStream(stream, param=param, metadata_tracker=metadata_tracker)
157
+
158
+
159
+ def _is_invalid_instruction_error(e: Exception) -> bool:
160
+ """Check if the error is related to invalid instructions."""
161
+ error_str = str(e).lower()
162
+ return "invalid instruction" in error_str or "invalid_instruction" in error_str
163
+
164
+
165
+ def _invalidate_prompt_cache_for_model(model_id: str) -> None:
166
+ """Invalidate the cached prompt for a model to force refresh."""
167
+ from klaude_code.llm.openai_codex.prompt_sync import invalidate_cache
168
+
169
+ log_debug(
170
+ f"Invalidating prompt cache for model {model_id} due to invalid instruction error",
171
+ debug_type=DebugType.GENERAL,
172
+ )
173
+ invalidate_cache(model_id)
@@ -0,0 +1,237 @@
1
+ """Dynamic prompt synchronization from OpenAI Codex GitHub repository."""
2
+
3
+ import json
4
+ import time
5
+ from functools import cache
6
+ from importlib.resources import files
7
+ from pathlib import Path
8
+ from typing import Any, Literal
9
+
10
+ import httpx
11
+
12
+ from klaude_code.log import DebugType, log_debug
13
+
14
+ GITHUB_API_RELEASES = "https://api.github.com/repos/openai/codex/releases/latest"
15
+ GITHUB_HTML_RELEASES = "https://github.com/openai/codex/releases/latest"
16
+ GITHUB_RAW_BASE = "https://raw.githubusercontent.com/openai/codex"
17
+
18
+ CACHE_DIR = Path.home() / ".klaude" / "codex-prompts"
19
+ CACHE_TTL_SECONDS = 24 * 60 * 60 # 24 hours
20
+
21
+ type ModelFamily = Literal["gpt-5.2-codex", "codex-max", "codex", "gpt-5.2", "gpt-5.1"]
22
+
23
+ PROMPT_FILES: dict[ModelFamily, str] = {
24
+ "gpt-5.2-codex": "gpt-5.2-codex_prompt.md",
25
+ "codex-max": "gpt-5.1-codex-max_prompt.md",
26
+ "codex": "gpt_5_codex_prompt.md",
27
+ "gpt-5.2": "gpt_5_2_prompt.md",
28
+ "gpt-5.1": "gpt_5_1_prompt.md",
29
+ }
30
+
31
+ CACHE_FILES: dict[ModelFamily, str] = {
32
+ "gpt-5.2-codex": "gpt-5.2-codex-instructions.md",
33
+ "codex-max": "codex-max-instructions.md",
34
+ "codex": "codex-instructions.md",
35
+ "gpt-5.2": "gpt-5.2-instructions.md",
36
+ "gpt-5.1": "gpt-5.1-instructions.md",
37
+ }
38
+
39
+
40
+ @cache
41
+ def _load_bundled_prompt(prompt_path: str) -> str:
42
+ """Load bundled prompt from package resources."""
43
+ return files("klaude_code.core").joinpath(prompt_path).read_text(encoding="utf-8").strip()
44
+
45
+
46
+ class CacheMetadata:
47
+ def __init__(self, etag: str | None, tag: str, last_checked: int, url: str):
48
+ self.etag = etag
49
+ self.tag = tag
50
+ self.last_checked = last_checked
51
+ self.url = url
52
+
53
+ def to_dict(self) -> dict[str, str | int | None]:
54
+ return {
55
+ "etag": self.etag,
56
+ "tag": self.tag,
57
+ "last_checked": self.last_checked,
58
+ "url": self.url,
59
+ }
60
+
61
+ @classmethod
62
+ def from_dict(cls, data: dict[str, object]) -> "CacheMetadata":
63
+ etag = data.get("etag")
64
+ last_checked = data.get("last_checked")
65
+ return cls(
66
+ etag=etag if isinstance(etag, str) else None,
67
+ tag=str(data.get("tag", "")),
68
+ last_checked=int(last_checked) if isinstance(last_checked, int | float) else 0,
69
+ url=str(data.get("url", "")),
70
+ )
71
+
72
+
73
+ def get_model_family(model: str) -> ModelFamily:
74
+ """Determine model family from model name."""
75
+ if "gpt-5.2-codex" in model or "gpt 5.2 codex" in model:
76
+ return "gpt-5.2-codex"
77
+ if "codex-max" in model:
78
+ return "codex-max"
79
+ if "codex" in model or model.startswith("codex-"):
80
+ return "codex"
81
+ if "gpt-5.2" in model:
82
+ return "gpt-5.2"
83
+ return "gpt-5.1"
84
+
85
+
86
+ def _get_latest_release_tag(client: httpx.Client) -> str:
87
+ """Get latest release tag from GitHub."""
88
+ try:
89
+ response = client.get(GITHUB_API_RELEASES)
90
+ if response.status_code == 200:
91
+ data: dict[str, Any] = response.json()
92
+ tag_name: Any = data.get("tag_name")
93
+ if isinstance(tag_name, str):
94
+ return tag_name
95
+ except httpx.HTTPError:
96
+ pass
97
+
98
+ # Fallback: follow redirect from releases/latest
99
+ response = client.get(GITHUB_HTML_RELEASES, follow_redirects=True)
100
+ if response.status_code == 200:
101
+ final_url = str(response.url)
102
+ if "/tag/" in final_url:
103
+ parts = final_url.split("/tag/")
104
+ if len(parts) > 1 and "/" not in parts[-1]:
105
+ return parts[-1]
106
+
107
+ raise RuntimeError("Failed to determine latest release tag from GitHub")
108
+
109
+
110
+ def _load_cache_metadata(meta_file: Path) -> CacheMetadata | None:
111
+ if not meta_file.exists():
112
+ return None
113
+ try:
114
+ data = json.loads(meta_file.read_text())
115
+ return CacheMetadata.from_dict(data)
116
+ except (json.JSONDecodeError, ValueError):
117
+ return None
118
+
119
+
120
+ def _save_cache_metadata(meta_file: Path, metadata: CacheMetadata) -> None:
121
+ meta_file.parent.mkdir(parents=True, exist_ok=True)
122
+ meta_file.write_text(json.dumps(metadata.to_dict(), indent=2))
123
+
124
+
125
+ def get_codex_instructions(model: str = "gpt-5.1-codex", force_refresh: bool = False) -> str:
126
+ """Get Codex instructions for the given model.
127
+
128
+ Args:
129
+ model: Model name to get instructions for.
130
+ force_refresh: If True, bypass cache TTL and fetch fresh instructions.
131
+
132
+ Returns:
133
+ The Codex system prompt instructions.
134
+ """
135
+ model_family = get_model_family(model)
136
+ prompt_file = PROMPT_FILES[model_family]
137
+ cache_file = CACHE_DIR / CACHE_FILES[model_family]
138
+ meta_file = CACHE_DIR / f"{CACHE_FILES[model_family].replace('.md', '-meta.json')}"
139
+
140
+ # Check cache unless force refresh
141
+ if not force_refresh:
142
+ metadata = _load_cache_metadata(meta_file)
143
+ if metadata and cache_file.exists():
144
+ age = int(time.time()) - metadata.last_checked
145
+ if age < CACHE_TTL_SECONDS:
146
+ log_debug(f"Using cached {model_family} instructions (age: {age}s)", debug_type=DebugType.GENERAL)
147
+ return cache_file.read_text()
148
+
149
+ try:
150
+ with httpx.Client(timeout=30.0) as client:
151
+ latest_tag = _get_latest_release_tag(client)
152
+ instructions_url = f"{GITHUB_RAW_BASE}/{latest_tag}/codex-rs/core/{prompt_file}"
153
+
154
+ # Load existing metadata for conditional request
155
+ metadata = _load_cache_metadata(meta_file)
156
+ headers: dict[str, str] = {}
157
+
158
+ # Only use ETag if tag matches (different release = different content)
159
+ if metadata and metadata.tag == latest_tag and metadata.etag:
160
+ headers["If-None-Match"] = metadata.etag
161
+
162
+ response = client.get(instructions_url, headers=headers)
163
+
164
+ if response.status_code == 304 and cache_file.exists():
165
+ # Not modified, update last_checked and return cached
166
+ if metadata:
167
+ metadata.last_checked = int(time.time())
168
+ _save_cache_metadata(meta_file, metadata)
169
+ log_debug(f"Codex {model_family} instructions not modified", debug_type=DebugType.GENERAL)
170
+ return cache_file.read_text()
171
+
172
+ if response.status_code == 200:
173
+ instructions = response.text
174
+ new_etag = response.headers.get("etag")
175
+
176
+ # Save to cache
177
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
178
+ cache_file.write_text(instructions)
179
+ _save_cache_metadata(
180
+ meta_file,
181
+ CacheMetadata(
182
+ etag=new_etag,
183
+ tag=latest_tag,
184
+ last_checked=int(time.time()),
185
+ url=instructions_url,
186
+ ),
187
+ )
188
+
189
+ log_debug(f"Updated {model_family} instructions from GitHub", debug_type=DebugType.GENERAL)
190
+ return instructions
191
+
192
+ raise RuntimeError(f"HTTP {response.status_code}")
193
+
194
+ except Exception as e:
195
+ log_debug(f"Failed to fetch {model_family} instructions: {e}", debug_type=DebugType.GENERAL)
196
+
197
+ # Fallback to cached version
198
+ if cache_file.exists():
199
+ log_debug(f"Using cached {model_family} instructions (fallback)", debug_type=DebugType.GENERAL)
200
+ return cache_file.read_text()
201
+
202
+ # Last resort: use bundled prompt
203
+ bundled_path = _get_bundled_prompt_path(model_family)
204
+ if bundled_path:
205
+ log_debug(f"Using bundled {model_family} instructions (fallback)", debug_type=DebugType.GENERAL)
206
+ return _load_bundled_prompt(bundled_path)
207
+
208
+ raise RuntimeError(f"No Codex instructions available for {model_family}") from e
209
+
210
+
211
+ def _get_bundled_prompt_path(model_family: ModelFamily) -> str | None:
212
+ """Get bundled prompt path for model family."""
213
+ if model_family == "gpt-5.2-codex":
214
+ return "prompts/prompt-codex-gpt-5-2-codex.md"
215
+ if model_family == "gpt-5.2":
216
+ return "prompts/prompt-codex-gpt-5-2.md"
217
+ if model_family in ("codex", "codex-max", "gpt-5.1"):
218
+ return "prompts/prompt-codex.md"
219
+ return None
220
+
221
+
222
+ def invalidate_cache(model: str | None = None) -> None:
223
+ """Invalidate cached instructions to force refresh on next access.
224
+
225
+ Args:
226
+ model: If provided, only invalidate cache for this model's family.
227
+ If None, invalidate all cached instructions.
228
+ """
229
+ if model:
230
+ model_family = get_model_family(model)
231
+ meta_file = CACHE_DIR / f"{CACHE_FILES[model_family].replace('.md', '-meta.json')}"
232
+ if meta_file.exists():
233
+ meta_file.unlink()
234
+ else:
235
+ if CACHE_DIR.exists():
236
+ for meta_file in CACHE_DIR.glob("*-meta.json"):
237
+ meta_file.unlink()
@@ -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
 
@@ -6,7 +6,6 @@
6
6
  from typing import cast
7
7
 
8
8
  from openai.types import chat
9
- from openai.types.chat import ChatCompletionContentPartParam
10
9
 
11
10
  from klaude_code.llm.image import assistant_image_to_data_url
12
11
  from klaude_code.llm.input_common import (
@@ -30,15 +29,6 @@ def _assistant_message_to_openai(msg: message.AssistantMessage) -> chat.ChatComp
30
29
  return cast(chat.ChatCompletionMessageParam, assistant_message)
31
30
 
32
31
 
33
- def build_user_content_parts(
34
- images: list[message.ImageURLPart],
35
- ) -> list[ChatCompletionContentPartParam]:
36
- """Build content parts for images only. Used by OpenRouter."""
37
- return [
38
- cast(ChatCompletionContentPartParam, {"type": "image_url", "image_url": {"url": image.url}}) for image in images
39
- ]
40
-
41
-
42
32
  def convert_history_to_input(
43
33
  history: list[message.Message],
44
34
  system: str | None = None,
@@ -12,6 +12,7 @@ how reasoning is represented (``reasoning_details`` vs ``reasoning_content``).
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import re
15
16
  from abc import ABC, abstractmethod
16
17
  from collections.abc import AsyncGenerator, Callable
17
18
  from dataclasses import dataclass
@@ -26,7 +27,6 @@ from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
26
27
 
27
28
  from klaude_code.llm.client import LLMStreamABC
28
29
  from klaude_code.llm.image import save_assistant_image
29
- from klaude_code.llm.openai_compatible.tool_call_accumulator import normalize_tool_name
30
30
  from klaude_code.llm.stream_parts import (
31
31
  append_text_part,
32
32
  append_thinking_text_part,
@@ -34,9 +34,24 @@ from klaude_code.llm.stream_parts import (
34
34
  build_partial_parts,
35
35
  )
36
36
  from klaude_code.llm.usage import MetadataTracker, convert_usage
37
+ from klaude_code.log import log_debug
37
38
  from klaude_code.protocol import llm_param, message, model
38
39
 
39
40
 
41
+ def normalize_tool_name(name: str) -> str:
42
+ """Normalize tool name from Gemini-3 format.
43
+
44
+ Gemini-3 sometimes returns tool names in format like 'tool_Edit_mUoY2p3W3r3z8uO5P2nZ'.
45
+ This function extracts the actual tool name (e.g., 'Edit').
46
+ """
47
+ match = re.match(r"^tool_([A-Za-z]+)_[A-Za-z0-9]+$", name)
48
+ if match:
49
+ normalized = match.group(1)
50
+ log_debug(f"Gemini-3 tool name normalized: {name} -> {normalized}", style="yellow")
51
+ return normalized
52
+ return name
53
+
54
+
40
55
  class StreamStateManager:
41
56
  """Manages streaming state and accumulates parts in stream order.
42
57
 
@@ -61,9 +76,11 @@ class StreamStateManager:
61
76
  """Set the response ID once received from the stream."""
62
77
  self.response_id = response_id
63
78
 
64
- def append_thinking_text(self, text: str) -> None:
79
+ def append_thinking_text(self, text: str, *, reasoning_field: str | None = None) -> None:
65
80
  """Append thinking text, merging with the previous ThinkingTextPart when possible."""
66
- append_thinking_text_part(self.assistant_parts, text, model_id=self.param_model)
81
+ append_thinking_text_part(
82
+ self.assistant_parts, text, model_id=self.param_model, reasoning_field=reasoning_field
83
+ )
67
84
 
68
85
  def append_text(self, text: str) -> None:
69
86
  """Append assistant text, merging with the previous TextPart when possible."""
@@ -135,6 +152,7 @@ class ReasoningDeltaResult:
135
152
 
136
153
  handled: bool
137
154
  outputs: list[str | message.Part]
155
+ reasoning_field: str | None = None # Original field name: reasoning_content, reasoning, reasoning_text
138
156
 
139
157
 
140
158
  class ReasoningHandlerABC(ABC):
@@ -153,8 +171,11 @@ class ReasoningHandlerABC(ABC):
153
171
  """Flush buffered reasoning content (usually at stage transition/finalize)."""
154
172
 
155
173
 
174
+ REASONING_FIELDS = ("reasoning_content", "reasoning", "reasoning_text")
175
+
176
+
156
177
  class DefaultReasoningHandler(ReasoningHandlerABC):
157
- """Handles OpenAI-compatible reasoning fields (reasoning_content / reasoning)."""
178
+ """Handles OpenAI-compatible reasoning fields (reasoning_content / reasoning / reasoning_text)."""
158
179
 
159
180
  def __init__(
160
181
  self,
@@ -164,16 +185,20 @@ class DefaultReasoningHandler(ReasoningHandlerABC):
164
185
  ) -> None:
165
186
  self._param_model = param_model
166
187
  self._response_id = response_id
188
+ self._reasoning_field: str | None = None
167
189
 
168
190
  def set_response_id(self, response_id: str | None) -> None:
169
191
  self._response_id = response_id
170
192
 
171
193
  def on_delta(self, delta: object) -> ReasoningDeltaResult:
172
- reasoning_content = getattr(delta, "reasoning_content", None) or getattr(delta, "reasoning", None) or ""
173
- if not reasoning_content:
174
- return ReasoningDeltaResult(handled=False, outputs=[])
175
- text = str(reasoning_content)
176
- return ReasoningDeltaResult(handled=True, outputs=[text])
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=[])
177
202
 
178
203
  def flush(self) -> list[message.Part]:
179
204
  return []
@@ -267,7 +292,7 @@ async def parse_chat_completions_stream(
267
292
  if not output:
268
293
  continue
269
294
  metadata_tracker.record_token()
270
- state.append_thinking_text(output)
295
+ state.append_thinking_text(output, reasoning_field=reasoning_result.reasoning_field)
271
296
  yield message.ThinkingTextDelta(content=output, response_id=state.response_id)
272
297
  else:
273
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,
@@ -7,6 +7,7 @@ from typing import Any, cast
7
7
  from openai.types import responses
8
8
 
9
9
  from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
10
+ from klaude_code.llm.image import image_file_to_data_url
10
11
  from klaude_code.llm.input_common import (
11
12
  DeveloperAttachment,
12
13
  attach_developer_messages,
@@ -16,6 +17,12 @@ from klaude_code.llm.input_common import (
16
17
  from klaude_code.protocol import llm_param, message
17
18
 
18
19
 
20
+ def _image_to_url(image: message.ImageURLPart | message.ImageFilePart) -> str:
21
+ if isinstance(image, message.ImageFilePart):
22
+ return image_file_to_data_url(image)
23
+ return image.url
24
+
25
+
19
26
  def _build_user_content_parts(
20
27
  user: message.UserMessage,
21
28
  attachment: DeveloperAttachment,
@@ -24,11 +31,11 @@ def _build_user_content_parts(
24
31
  for part in user.parts:
25
32
  if isinstance(part, message.TextPart):
26
33
  parts.append(cast(responses.ResponseInputContentParam, {"type": "input_text", "text": part.text}))
27
- elif isinstance(part, message.ImageURLPart):
34
+ elif isinstance(part, (message.ImageURLPart, message.ImageFilePart)):
28
35
  parts.append(
29
36
  cast(
30
37
  responses.ResponseInputContentParam,
31
- {"type": "input_image", "detail": "auto", "image_url": part.url},
38
+ {"type": "input_image", "detail": "auto", "image_url": _image_to_url(part)},
32
39
  )
33
40
  )
34
41
  if attachment.text:
@@ -37,7 +44,7 @@ def _build_user_content_parts(
37
44
  parts.append(
38
45
  cast(
39
46
  responses.ResponseInputContentParam,
40
- {"type": "input_image", "detail": "auto", "image_url": image.url},
47
+ {"type": "input_image", "detail": "auto", "image_url": _image_to_url(image)},
41
48
  )
42
49
  )
43
50
  if not parts:
@@ -56,12 +63,15 @@ def _build_tool_result_item(
56
63
  )
57
64
  if text_output:
58
65
  content_parts.append(cast(responses.ResponseInputContentParam, {"type": "input_text", "text": text_output}))
59
- images = [part for part in tool.parts if isinstance(part, message.ImageURLPart)] + attachment.images
66
+ images: list[message.ImageURLPart | message.ImageFilePart] = [
67
+ part for part in tool.parts if isinstance(part, (message.ImageURLPart, message.ImageFilePart))
68
+ ]
69
+ images.extend(attachment.images)
60
70
  for image in images:
61
71
  content_parts.append(
62
72
  cast(
63
73
  responses.ResponseInputContentParam,
64
- {"type": "input_image", "detail": "auto", "image_url": image.url},
74
+ {"type": "input_image", "detail": "auto", "image_url": _image_to_url(image)},
65
75
  )
66
76
  )
67
77
 
@@ -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.bedrock",
19
- llm_param.LLMClientProtocol.CODEX_OAUTH: "klaude_code.llm.codex",
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.responses",
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
  }
@@ -38,11 +38,6 @@ def _load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
38
38
  _loaded_protocols.add(protocol)
39
39
 
40
40
 
41
- def load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
42
- """Load the module for a specific protocol on demand."""
43
- _load_protocol(protocol)
44
-
45
-
46
41
  def register(name: llm_param.LLMClientProtocol) -> Callable[[_T], _T]:
47
42
  """Decorator to register an LLM client class for a protocol."""
48
43
 
@@ -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
@@ -44,14 +44,6 @@ class MetadataTracker:
44
44
  self._usage = model.Usage()
45
45
  self._cost_config = cost_config
46
46
 
47
- @property
48
- def first_token_time(self) -> float | None:
49
- return self._first_token_time
50
-
51
- @property
52
- def last_token_time(self) -> float | None:
53
- return self._last_token_time
54
-
55
47
  def record_token(self) -> None:
56
48
  """Record a token arrival, updating first/last token times."""
57
49
  now = time.time()
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
+ from collections.abc import Sequence
4
5
  from typing import Literal
5
6
 
6
7
  from pydantic import BaseModel, Field
@@ -58,7 +59,7 @@ class ResponseEvent(Event):
58
59
 
59
60
  class UserMessageEvent(Event):
60
61
  content: str
61
- images: list[message.ImageURLPart] | None = None
62
+ images: Sequence[message.ImageURLPart | message.ImageFilePart] | None = None
62
63
 
63
64
 
64
65
  class DeveloperMessageEvent(Event):
@@ -118,7 +119,6 @@ class UsageEvent(ResponseEvent):
118
119
 
119
120
  class TaskMetadataEvent(Event):
120
121
  metadata: model.TaskMetadataItem
121
- cancelled: bool = False
122
122
 
123
123
 
124
124
  class ThinkingStartEvent(ResponseEvent):
@@ -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):
@@ -213,7 +214,7 @@ class UserInputPayload(BaseModel):
213
214
  """
214
215
 
215
216
  text: str
216
- images: list[ImageURLPart] | None = None
217
+ images: Sequence[ImageURLPart | ImageFilePart] | None = None
217
218
 
218
219
 
219
220
  # Helper functions
@@ -225,7 +226,7 @@ def text_parts_from_str(text: str | None) -> list[Part]:
225
226
  return [TextPart(text=text)]
226
227
 
227
228
 
228
- def parts_from_text_and_images(text: str | None, images: list[ImageURLPart] | None) -> list[Part]:
229
+ def parts_from_text_and_images(text: str | None, images: Sequence[ImageURLPart | ImageFilePart] | None) -> list[Part]:
229
230
  parts: list[Part] = []
230
231
  if text:
231
232
  parts.append(TextPart(text=text))