klaude-code 1.8.0__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. klaude_code/auth/base.py +97 -0
  2. klaude_code/auth/claude/__init__.py +6 -0
  3. klaude_code/auth/claude/exceptions.py +9 -0
  4. klaude_code/auth/claude/oauth.py +172 -0
  5. klaude_code/auth/claude/token_manager.py +26 -0
  6. klaude_code/auth/codex/token_manager.py +10 -50
  7. klaude_code/cli/auth_cmd.py +127 -46
  8. klaude_code/cli/config_cmd.py +4 -2
  9. klaude_code/cli/cost_cmd.py +14 -9
  10. klaude_code/cli/list_model.py +248 -200
  11. klaude_code/cli/main.py +1 -1
  12. klaude_code/cli/runtime.py +7 -5
  13. klaude_code/cli/self_update.py +1 -1
  14. klaude_code/cli/session_cmd.py +1 -1
  15. klaude_code/command/clear_cmd.py +6 -2
  16. klaude_code/command/command_abc.py +2 -2
  17. klaude_code/command/debug_cmd.py +4 -4
  18. klaude_code/command/export_cmd.py +2 -2
  19. klaude_code/command/export_online_cmd.py +12 -12
  20. klaude_code/command/fork_session_cmd.py +29 -23
  21. klaude_code/command/help_cmd.py +4 -4
  22. klaude_code/command/model_cmd.py +4 -4
  23. klaude_code/command/model_select.py +1 -1
  24. klaude_code/command/prompt-commit.md +82 -0
  25. klaude_code/command/prompt_command.py +3 -3
  26. klaude_code/command/refresh_cmd.py +2 -2
  27. klaude_code/command/registry.py +7 -5
  28. klaude_code/command/release_notes_cmd.py +4 -4
  29. klaude_code/command/resume_cmd.py +15 -11
  30. klaude_code/command/status_cmd.py +4 -4
  31. klaude_code/command/terminal_setup_cmd.py +8 -8
  32. klaude_code/command/thinking_cmd.py +4 -4
  33. klaude_code/config/assets/builtin_config.yaml +52 -3
  34. klaude_code/config/builtin_config.py +16 -5
  35. klaude_code/config/config.py +31 -7
  36. klaude_code/config/thinking.py +4 -4
  37. klaude_code/const.py +146 -91
  38. klaude_code/core/agent.py +3 -12
  39. klaude_code/core/executor.py +21 -13
  40. klaude_code/core/manager/sub_agent_manager.py +71 -7
  41. klaude_code/core/prompt.py +1 -1
  42. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  43. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  44. klaude_code/core/reminders.py +88 -69
  45. klaude_code/core/task.py +44 -45
  46. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  47. klaude_code/core/tool/file/diff_builder.py +3 -5
  48. klaude_code/core/tool/file/edit_tool.py +23 -23
  49. klaude_code/core/tool/file/move_tool.py +43 -43
  50. klaude_code/core/tool/file/read_tool.py +44 -39
  51. klaude_code/core/tool/file/write_tool.py +14 -14
  52. klaude_code/core/tool/report_back_tool.py +4 -4
  53. klaude_code/core/tool/shell/bash_tool.py +23 -23
  54. klaude_code/core/tool/skill/skill_tool.py +7 -7
  55. klaude_code/core/tool/sub_agent_tool.py +38 -9
  56. klaude_code/core/tool/todo/todo_write_tool.py +8 -8
  57. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  58. klaude_code/core/tool/tool_abc.py +2 -2
  59. klaude_code/core/tool/tool_context.py +27 -0
  60. klaude_code/core/tool/tool_runner.py +88 -42
  61. klaude_code/core/tool/truncation.py +38 -20
  62. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  63. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  64. klaude_code/core/tool/web/web_search_tool.py +15 -17
  65. klaude_code/core/turn.py +120 -73
  66. klaude_code/llm/anthropic/client.py +104 -44
  67. klaude_code/llm/anthropic/input.py +116 -108
  68. klaude_code/llm/bedrock/client.py +8 -5
  69. klaude_code/llm/claude/__init__.py +3 -0
  70. klaude_code/llm/claude/client.py +105 -0
  71. klaude_code/llm/client.py +4 -3
  72. klaude_code/llm/codex/client.py +16 -10
  73. klaude_code/llm/google/client.py +122 -60
  74. klaude_code/llm/google/input.py +94 -108
  75. klaude_code/llm/image.py +123 -0
  76. klaude_code/llm/input_common.py +136 -189
  77. klaude_code/llm/openai_compatible/client.py +17 -7
  78. klaude_code/llm/openai_compatible/input.py +36 -66
  79. klaude_code/llm/openai_compatible/stream.py +119 -67
  80. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  81. klaude_code/llm/openrouter/client.py +34 -9
  82. klaude_code/llm/openrouter/input.py +63 -64
  83. klaude_code/llm/openrouter/reasoning.py +22 -24
  84. klaude_code/llm/registry.py +20 -15
  85. klaude_code/llm/responses/client.py +107 -45
  86. klaude_code/llm/responses/input.py +115 -98
  87. klaude_code/llm/usage.py +52 -25
  88. klaude_code/protocol/__init__.py +1 -0
  89. klaude_code/protocol/events.py +16 -12
  90. klaude_code/protocol/llm_param.py +22 -3
  91. klaude_code/protocol/message.py +250 -0
  92. klaude_code/protocol/model.py +94 -281
  93. klaude_code/protocol/op.py +2 -2
  94. klaude_code/protocol/sub_agent/__init__.py +2 -2
  95. klaude_code/protocol/sub_agent/explore.py +10 -0
  96. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  97. klaude_code/protocol/sub_agent/task.py +10 -0
  98. klaude_code/protocol/sub_agent/web.py +10 -0
  99. klaude_code/session/codec.py +6 -6
  100. klaude_code/session/export.py +261 -62
  101. klaude_code/session/selector.py +7 -24
  102. klaude_code/session/session.py +125 -53
  103. klaude_code/session/store.py +5 -32
  104. klaude_code/session/templates/export_session.html +1 -1
  105. klaude_code/session/templates/mermaid_viewer.html +1 -1
  106. klaude_code/trace/log.py +11 -6
  107. klaude_code/ui/core/input.py +1 -1
  108. klaude_code/ui/core/stage_manager.py +1 -8
  109. klaude_code/ui/modes/debug/display.py +2 -2
  110. klaude_code/ui/modes/repl/clipboard.py +2 -2
  111. klaude_code/ui/modes/repl/completers.py +18 -10
  112. klaude_code/ui/modes/repl/event_handler.py +136 -127
  113. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  114. klaude_code/ui/modes/repl/key_bindings.py +1 -1
  115. klaude_code/ui/modes/repl/renderer.py +107 -15
  116. klaude_code/ui/renderers/assistant.py +2 -2
  117. klaude_code/ui/renderers/common.py +65 -7
  118. klaude_code/ui/renderers/developer.py +7 -6
  119. klaude_code/ui/renderers/diffs.py +11 -11
  120. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  121. klaude_code/ui/renderers/metadata.py +39 -31
  122. klaude_code/ui/renderers/sub_agent.py +57 -16
  123. klaude_code/ui/renderers/thinking.py +37 -2
  124. klaude_code/ui/renderers/tools.py +180 -165
  125. klaude_code/ui/rich/live.py +3 -1
  126. klaude_code/ui/rich/markdown.py +39 -7
  127. klaude_code/ui/rich/quote.py +76 -1
  128. klaude_code/ui/rich/status.py +14 -8
  129. klaude_code/ui/rich/theme.py +13 -6
  130. klaude_code/ui/terminal/image.py +34 -0
  131. klaude_code/ui/terminal/notifier.py +2 -1
  132. klaude_code/ui/terminal/progress_bar.py +4 -4
  133. klaude_code/ui/terminal/selector.py +22 -4
  134. klaude_code/ui/utils/common.py +55 -0
  135. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +28 -6
  136. klaude_code-2.0.0.dist-info/RECORD +229 -0
  137. klaude_code/command/prompt-jj-describe.md +0 -32
  138. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
  139. klaude_code/protocol/sub_agent/oracle.py +0 -91
  140. klaude_code-1.8.0.dist-info/RECORD +0 -219
  141. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
  142. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from pydantic import BaseModel
2
2
 
3
3
  from klaude_code.llm.openai_compatible.stream import ReasoningDeltaResult, ReasoningHandlerABC
4
- from klaude_code.protocol import model
4
+ from klaude_code.protocol import message
5
5
  from klaude_code.trace import log
6
6
 
7
7
 
@@ -42,7 +42,7 @@ class ReasoningStreamHandler(ReasoningHandlerABC):
42
42
  if not reasoning_details:
43
43
  return ReasoningDeltaResult(handled=False, outputs=[])
44
44
 
45
- outputs: list[str | model.ConversationItem] = []
45
+ outputs: list[str | message.Part] = []
46
46
  for item in reasoning_details:
47
47
  try:
48
48
  reasoning_detail = ReasoningDetail.model_validate(item)
@@ -56,16 +56,16 @@ class ReasoningStreamHandler(ReasoningHandlerABC):
56
56
 
57
57
  return ReasoningDeltaResult(handled=True, outputs=outputs)
58
58
 
59
- def on_detail(self, detail: ReasoningDetail) -> list[model.ConversationItem]:
60
- """Process a single reasoning detail and return streamable items."""
61
- items: list[model.ConversationItem] = []
59
+ def on_detail(self, detail: ReasoningDetail) -> list[message.Part]:
60
+ """Process a single reasoning detail and return streamable parts."""
61
+ items: list[message.Part] = []
62
62
 
63
63
  if detail.type == "reasoning.encrypted":
64
64
  self._reasoning_id = detail.id
65
65
  # Flush accumulated text before encrypted content
66
66
  items.extend(self._flush_text())
67
- if encrypted_item := self._build_encrypted_item(detail.data, detail):
68
- items.append(encrypted_item)
67
+ if signature_part := self._build_signature_part(detail.data, detail):
68
+ items.append(signature_part)
69
69
  return items
70
70
 
71
71
  if detail.type in ("reasoning.text", "reasoning.summary"):
@@ -77,42 +77,40 @@ class ReasoningStreamHandler(ReasoningHandlerABC):
77
77
  # Flush on signature (encrypted content)
78
78
  if detail.signature:
79
79
  items.extend(self._flush_text())
80
- if encrypted_item := self._build_encrypted_item(detail.signature, detail):
81
- items.append(encrypted_item)
80
+ if signature_part := self._build_signature_part(detail.signature, detail):
81
+ items.append(signature_part)
82
82
 
83
83
  return items
84
84
 
85
- def flush(self) -> list[model.ConversationItem]:
85
+ def flush(self) -> list[message.Part]:
86
86
  """Flush buffered reasoning text on finalize."""
87
87
  return self._flush_text()
88
88
 
89
- def _flush_text(self) -> list[model.ConversationItem]:
90
- """Flush accumulated reasoning text as a single item."""
89
+ def _flush_text(self) -> list[message.Part]:
90
+ """Flush accumulated reasoning text as a single part."""
91
91
  if not self._accumulated_reasoning:
92
92
  return []
93
- item = self._build_text_item("".join(self._accumulated_reasoning))
93
+ item = self._build_text_part("".join(self._accumulated_reasoning))
94
94
  self._accumulated_reasoning = []
95
95
  return [item]
96
96
 
97
- def _build_text_item(self, content: str) -> model.ReasoningTextItem:
98
- return model.ReasoningTextItem(
97
+ def _build_text_part(self, content: str) -> message.ThinkingTextPart:
98
+ return message.ThinkingTextPart(
99
99
  id=self._reasoning_id,
100
- content=content,
101
- response_id=self._response_id,
102
- model=self._param_model,
100
+ text=content,
101
+ model_id=self._param_model,
103
102
  )
104
103
 
105
- def _build_encrypted_item(
104
+ def _build_signature_part(
106
105
  self,
107
106
  content: str | None,
108
107
  detail: ReasoningDetail,
109
- ) -> model.ReasoningEncryptedItem | None:
108
+ ) -> message.ThinkingSignaturePart | None:
110
109
  if not content:
111
110
  return None
112
- return model.ReasoningEncryptedItem(
111
+ return message.ThinkingSignaturePart(
113
112
  id=detail.id,
114
- encrypted_content=content,
113
+ signature=content,
115
114
  format=detail.format,
116
- response_id=self._response_id,
117
- model=self._param_model,
115
+ model_id=self._param_model,
118
116
  )
@@ -12,29 +12,34 @@ _T = TypeVar("_T", bound=type["LLMClientABC"])
12
12
  # Track which protocols have been loaded
13
13
  _loaded_protocols: set[llm_param.LLMClientProtocol] = set()
14
14
  _REGISTRY: dict[llm_param.LLMClientProtocol, type["LLMClientABC"]] = {}
15
+ _PROTOCOL_MODULES: dict[llm_param.LLMClientProtocol, str] = {
16
+ llm_param.LLMClientProtocol.ANTHROPIC: "klaude_code.llm.anthropic",
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",
20
+ llm_param.LLMClientProtocol.OPENAI: "klaude_code.llm.openai_compatible",
21
+ llm_param.LLMClientProtocol.OPENROUTER: "klaude_code.llm.openrouter",
22
+ llm_param.LLMClientProtocol.RESPONSES: "klaude_code.llm.responses",
23
+ llm_param.LLMClientProtocol.GOOGLE: "klaude_code.llm.google",
24
+ }
15
25
 
16
26
 
17
27
  def _load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
18
28
  """Load the module for a specific protocol on demand."""
19
29
  if protocol in _loaded_protocols:
20
30
  return
21
- _loaded_protocols.add(protocol)
31
+ module_path = _PROTOCOL_MODULES.get(protocol)
32
+ if module_path is None:
33
+ raise ValueError(f"Unknown LLMClient protocol: {protocol}")
22
34
 
23
35
  # Import only the needed module to trigger @register decorator
24
- if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
25
- importlib.import_module("klaude_code.llm.anthropic")
26
- elif protocol == llm_param.LLMClientProtocol.BEDROCK:
27
- importlib.import_module("klaude_code.llm.bedrock")
28
- elif protocol == llm_param.LLMClientProtocol.CODEX:
29
- importlib.import_module("klaude_code.llm.codex")
30
- elif protocol == llm_param.LLMClientProtocol.OPENAI:
31
- importlib.import_module("klaude_code.llm.openai_compatible")
32
- elif protocol == llm_param.LLMClientProtocol.OPENROUTER:
33
- importlib.import_module("klaude_code.llm.openrouter")
34
- elif protocol == llm_param.LLMClientProtocol.RESPONSES:
35
- importlib.import_module("klaude_code.llm.responses")
36
- elif protocol == llm_param.LLMClientProtocol.GOOGLE:
37
- importlib.import_module("klaude_code.llm.google")
36
+ importlib.import_module(module_path)
37
+ _loaded_protocols.add(protocol)
38
+
39
+
40
+ def load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
41
+ """Load the module for a specific protocol on demand."""
42
+ _load_protocol(protocol)
38
43
 
39
44
 
40
45
  def register(name: llm_param.LLMClientProtocol) -> Callable[[_T], _T]:
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from collections.abc import AsyncGenerator
3
- from typing import TYPE_CHECKING, override
3
+ from typing import TYPE_CHECKING, Literal, override
4
4
 
5
5
  import httpx
6
6
  import openai
@@ -8,12 +8,13 @@ from openai import AsyncAzureOpenAI, AsyncOpenAI
8
8
  from openai.types import responses
9
9
  from openai.types.responses.response_create_params import ResponseCreateParamsStreaming
10
10
 
11
+ from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
11
12
  from klaude_code.llm.client import LLMClientABC
12
13
  from klaude_code.llm.input_common import apply_config_defaults
13
14
  from klaude_code.llm.registry import register
14
15
  from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
15
- from klaude_code.llm.usage import MetadataTracker
16
- from klaude_code.protocol import llm_param, model
16
+ from klaude_code.llm.usage import MetadataTracker, error_stream_items
17
+ from klaude_code.protocol import llm_param, message, model
17
18
  from klaude_code.trace import DebugType, log_debug
18
19
 
19
20
  if TYPE_CHECKING:
@@ -59,9 +60,57 @@ async def parse_responses_stream(
59
60
  stream: "AsyncStream[ResponseStreamEvent]",
60
61
  param: llm_param.LLMCallParameter,
61
62
  metadata_tracker: MetadataTracker,
62
- ) -> AsyncGenerator[model.ConversationItem]:
63
- """Parse OpenAI Responses API stream events into ConversationItems."""
63
+ ) -> AsyncGenerator[message.LLMStreamItem]:
64
+ """Parse OpenAI Responses API stream events into stream items."""
64
65
  response_id: str | None = None
66
+ stage: Literal["waiting", "thinking", "assistant", "tool"] = "waiting"
67
+
68
+ accumulated_thinking: list[str] = []
69
+ accumulated_text: list[str] = []
70
+ pending_signature: str | None = None
71
+ assistant_parts: list[message.Part] = []
72
+ stop_reason: model.StopReason | None = None
73
+
74
+ def flush_thinking() -> None:
75
+ nonlocal pending_signature
76
+ if accumulated_thinking:
77
+ assistant_parts.append(
78
+ message.ThinkingTextPart(
79
+ text="".join(accumulated_thinking),
80
+ model_id=str(param.model),
81
+ )
82
+ )
83
+ accumulated_thinking.clear()
84
+ if pending_signature:
85
+ assistant_parts.append(
86
+ message.ThinkingSignaturePart(
87
+ signature=pending_signature,
88
+ model_id=str(param.model),
89
+ format="openai_reasoning",
90
+ )
91
+ )
92
+ pending_signature = None
93
+
94
+ def flush_text() -> None:
95
+ if not accumulated_text:
96
+ return
97
+ assistant_parts.append(message.TextPart(text="".join(accumulated_text)))
98
+ accumulated_text.clear()
99
+
100
+ def map_stop_reason(status: str | None, reason: str | None) -> model.StopReason | None:
101
+ if reason:
102
+ normalized = reason.strip().lower()
103
+ if normalized in {"max_output_tokens", "length", "max_tokens"}:
104
+ return "length"
105
+ if normalized in {"content_filter", "safety"}:
106
+ return "error"
107
+ if normalized in {"cancelled", "canceled", "aborted"}:
108
+ return "aborted"
109
+ if status == "completed":
110
+ return "stop"
111
+ if status in {"failed", "error"}:
112
+ return "error"
113
+ return None
65
114
 
66
115
  try:
67
116
  async for event in stream:
@@ -74,29 +123,29 @@ async def parse_responses_stream(
74
123
  match event:
75
124
  case responses.ResponseCreatedEvent() as event:
76
125
  response_id = event.response.id
77
- yield model.StartItem(response_id=response_id)
78
126
  case responses.ResponseReasoningSummaryTextDeltaEvent() as event:
79
127
  if event.delta:
80
128
  metadata_tracker.record_token()
81
- yield model.ReasoningTextDelta(
82
- content=event.delta,
83
- response_id=response_id,
84
- )
129
+ if stage == "assistant":
130
+ flush_text()
131
+ stage = "thinking"
132
+ accumulated_thinking.append(event.delta)
133
+ yield message.ThinkingTextDelta(content=event.delta, response_id=response_id)
85
134
  case responses.ResponseReasoningSummaryTextDoneEvent() as event:
86
- if event.text:
87
- yield model.ReasoningTextItem(
88
- content=event.text,
89
- response_id=response_id,
90
- model=str(param.model),
91
- )
135
+ if event.text and not accumulated_thinking:
136
+ accumulated_thinking.append(event.text)
92
137
  case responses.ResponseTextDeltaEvent() as event:
93
138
  if event.delta:
94
139
  metadata_tracker.record_token()
95
- yield model.AssistantMessageDelta(content=event.delta, response_id=response_id)
140
+ if stage == "thinking":
141
+ flush_thinking()
142
+ stage = "assistant"
143
+ accumulated_text.append(event.delta)
144
+ yield message.AssistantTextDelta(content=event.delta, response_id=response_id)
96
145
  case responses.ResponseOutputItemAddedEvent() as event:
97
146
  if isinstance(event.item, responses.ResponseFunctionToolCall):
98
147
  metadata_tracker.record_token()
99
- yield model.ToolCallStartItem(
148
+ yield message.ToolCallStartItem(
100
149
  response_id=response_id,
101
150
  call_id=event.item.call_id,
102
151
  name=event.item.name,
@@ -105,34 +154,30 @@ async def parse_responses_stream(
105
154
  match event.item:
106
155
  case responses.ResponseReasoningItem() as item:
107
156
  if item.encrypted_content:
108
- metadata_tracker.record_token()
109
- yield model.ReasoningEncryptedItem(
110
- id=item.id,
111
- encrypted_content=item.encrypted_content,
112
- response_id=response_id,
113
- model=str(param.model),
114
- )
157
+ pending_signature = item.encrypted_content
115
158
  case responses.ResponseOutputMessage() as item:
116
- metadata_tracker.record_token()
117
- yield model.AssistantMessageItem(
118
- content="\n".join(
159
+ if not accumulated_text:
160
+ text_content = "\n".join(
119
161
  [
120
162
  part.text
121
163
  for part in item.content
122
164
  if isinstance(part, responses.ResponseOutputText)
123
165
  ]
124
- ),
125
- id=item.id,
126
- response_id=response_id,
127
- )
166
+ )
167
+ if text_content:
168
+ accumulated_text.append(text_content)
128
169
  case responses.ResponseFunctionToolCall() as item:
129
170
  metadata_tracker.record_token()
130
- yield model.ToolCallItem(
131
- name=item.name,
132
- arguments=item.arguments.strip(),
133
- call_id=item.call_id,
134
- id=item.id,
135
- response_id=response_id,
171
+ flush_thinking()
172
+ flush_text()
173
+ stage = "tool"
174
+ assistant_parts.append(
175
+ message.ToolCallPart(
176
+ call_id=item.call_id,
177
+ id=item.id,
178
+ tool_name=item.name,
179
+ arguments_json=item.arguments.strip(),
180
+ )
136
181
  )
137
182
  case _:
138
183
  pass
@@ -154,7 +199,7 @@ async def parse_responses_stream(
154
199
  )
155
200
  metadata_tracker.set_model_name(str(param.model))
156
201
  metadata_tracker.set_response_id(response_id)
157
- yield metadata_tracker.finalize()
202
+ stop_reason = map_stop_reason(event.response.status, error_reason)
158
203
  if event.response.status != "completed":
159
204
  error_message = f"LLM response finished with status '{event.response.status}'"
160
205
  if error_reason:
@@ -165,7 +210,7 @@ async def parse_responses_stream(
165
210
  style="red",
166
211
  debug_type=DebugType.LLM_STREAM,
167
212
  )
168
- yield model.StreamErrorItem(error=error_message)
213
+ yield message.StreamErrorItem(error=error_message)
169
214
  case _:
170
215
  log_debug(
171
216
  "[Unhandled stream event]",
@@ -174,7 +219,18 @@ async def parse_responses_stream(
174
219
  debug_type=DebugType.LLM_STREAM,
175
220
  )
176
221
  except (openai.OpenAIError, httpx.HTTPError) as e:
177
- yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
222
+ yield message.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
223
+
224
+ flush_thinking()
225
+ flush_text()
226
+ metadata_tracker.set_response_id(response_id)
227
+ metadata = metadata_tracker.finalize()
228
+ yield message.AssistantMessage(
229
+ parts=assistant_parts,
230
+ response_id=response_id,
231
+ usage=metadata,
232
+ stop_reason=stop_reason,
233
+ )
178
234
 
179
235
 
180
236
  @register(llm_param.LLMClientProtocol.RESPONSES)
@@ -188,13 +244,17 @@ class ResponsesClient(LLMClientABC):
188
244
  api_key=config.api_key,
189
245
  azure_endpoint=str(config.base_url),
190
246
  api_version=config.azure_api_version,
191
- timeout=httpx.Timeout(300.0, connect=15.0, read=285.0),
247
+ timeout=httpx.Timeout(
248
+ LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ
249
+ ),
192
250
  )
193
251
  else:
194
252
  client = AsyncOpenAI(
195
253
  api_key=config.api_key,
196
254
  base_url=config.base_url,
197
- timeout=httpx.Timeout(300.0, connect=15.0, read=285.0),
255
+ timeout=httpx.Timeout(
256
+ LLM_HTTP_TIMEOUT_TOTAL, connect=LLM_HTTP_TIMEOUT_CONNECT, read=LLM_HTTP_TIMEOUT_READ
257
+ ),
198
258
  )
199
259
  self.client: AsyncAzureOpenAI | AsyncOpenAI = client
200
260
 
@@ -204,7 +264,7 @@ class ResponsesClient(LLMClientABC):
204
264
  return cls(config)
205
265
 
206
266
  @override
207
- async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
267
+ async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[message.LLMStreamItem]:
208
268
  param = apply_config_defaults(param, self.get_llm_config())
209
269
 
210
270
  metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
@@ -222,7 +282,9 @@ class ResponsesClient(LLMClientABC):
222
282
  extra_headers={"extra": json.dumps({"session_id": param.session_id}, sort_keys=True)},
223
283
  )
224
284
  except (openai.OpenAIError, httpx.HTTPError) as e:
225
- yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
285
+ error_message = f"{e.__class__.__name__} {e!s}"
286
+ for item in error_stream_items(metadata_tracker, error=error_message):
287
+ yield item
226
288
  return
227
289
 
228
290
  async for item in parse_responses_stream(stream, param, metadata_tracker):