illusion-code 0.1.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.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet" />
|
|
9
|
+
<title>Illusion Code</title>
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-C_0ZWMuW.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BseIw-ik.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body class="bg-white">
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
illusion/api/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API 模块
|
|
3
|
+
========
|
|
4
|
+
|
|
5
|
+
本模块提供 IllusionCode 与各种 LLM 提供商的 API 集成。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- AnthropicApiClient: Anthropic API 客户端
|
|
9
|
+
- OpenAICompatibleClient: OpenAI 兼容 API 客户端
|
|
10
|
+
- CodexApiClient: OpenAI Codex 客户端
|
|
11
|
+
- IllusionCodeApiError: API 异常基类
|
|
12
|
+
- ProviderInfo: 提供商元数据
|
|
13
|
+
- UsageSnapshot: 使用量追踪
|
|
14
|
+
|
|
15
|
+
使用示例:
|
|
16
|
+
>>> from illusion.api import AnthropicApiClient
|
|
17
|
+
>>> client = AnthropicApiClient(api_key="sk-...")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from illusion.api.client import AnthropicApiClient
|
|
21
|
+
from illusion.api.codex_client import CodexApiClient
|
|
22
|
+
from illusion.api.errors import IllusionCodeApiError
|
|
23
|
+
from illusion.api.openai_client import OpenAICompatibleClient
|
|
24
|
+
from illusion.api.provider import ProviderInfo, auth_status, detect_provider
|
|
25
|
+
from illusion.api.usage import UsageSnapshot
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AnthropicApiClient",
|
|
29
|
+
"CodexApiClient",
|
|
30
|
+
"OpenAICompatibleClient",
|
|
31
|
+
"IllusionCodeApiError",
|
|
32
|
+
"ProviderInfo",
|
|
33
|
+
"UsageSnapshot",
|
|
34
|
+
"auth_status",
|
|
35
|
+
"detect_provider",
|
|
36
|
+
]
|
illusion/api/client.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anthropic API 客户端模块
|
|
3
|
+
=======================
|
|
4
|
+
|
|
5
|
+
本模块提供 Anthropic API 客户端封装,带有重试逻辑。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 流式文本增量生成
|
|
9
|
+
- 自动重试 transient 错误
|
|
10
|
+
- OAuth 支持
|
|
11
|
+
- 错误转换
|
|
12
|
+
|
|
13
|
+
类说明:
|
|
14
|
+
- AnthropicApiClient: Anthropic 异步 SDK 封装类
|
|
15
|
+
- ApiMessageRequest: 模型调用输入参数
|
|
16
|
+
- ApiTextDeltaEvent: 增量文本事件
|
|
17
|
+
- ApiMessageCompleteEvent: 完整消息事件
|
|
18
|
+
- ApiRetryEvent: 重试事件
|
|
19
|
+
|
|
20
|
+
使用示例:
|
|
21
|
+
>>> from illusion.api.client import AnthropicApiClient, ApiMessageRequest
|
|
22
|
+
>>> client = AnthropicApiClient(api_key="sk-...")
|
|
23
|
+
>>> request = ApiMessageRequest(model="claude-3-sonnet", messages=[])
|
|
24
|
+
>>> async for event in client.stream_message(request):
|
|
25
|
+
>>> print(event)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import asyncio
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
import uuid
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any, AsyncIterator, Callable, Protocol
|
|
36
|
+
|
|
37
|
+
from anthropic import APIError, APIStatusError, AsyncAnthropic
|
|
38
|
+
from anthropic.types import ThinkingBlock as _SDKThinkingBlock
|
|
39
|
+
|
|
40
|
+
# 兼容第三方 API(如 MiMo)返回 "signature": null 的情况
|
|
41
|
+
# anthropic SDK 的 ThinkingBlock 要求 signature 为 str,但部分提供商返回 null
|
|
42
|
+
_sdk_sig_field = _SDKThinkingBlock.model_fields["signature"]
|
|
43
|
+
_sdk_sig_field.annotation = str | None
|
|
44
|
+
_SDKThinkingBlock.model_rebuild()
|
|
45
|
+
|
|
46
|
+
from illusion.api.effort import EffortLevel # noqa: E402
|
|
47
|
+
from illusion.api.errors import ( # noqa: E402
|
|
48
|
+
AuthenticationFailure,
|
|
49
|
+
IllusionCodeApiError,
|
|
50
|
+
RateLimitFailure,
|
|
51
|
+
RequestFailure,
|
|
52
|
+
)
|
|
53
|
+
from illusion.auth.external import ( # noqa: E402
|
|
54
|
+
claude_attribution_header,
|
|
55
|
+
claude_oauth_betas,
|
|
56
|
+
claude_oauth_headers,
|
|
57
|
+
get_claude_code_session_id,
|
|
58
|
+
)
|
|
59
|
+
from illusion.api.usage import UsageSnapshot # noqa: E402
|
|
60
|
+
from illusion.engine.messages import ( # noqa: E402
|
|
61
|
+
ConversationMessage,
|
|
62
|
+
_messages_have_media,
|
|
63
|
+
_strip_media_from_messages,
|
|
64
|
+
assistant_message_from_api,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 模块级日志记录器
|
|
68
|
+
log = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
# 重试配置常量
|
|
71
|
+
MAX_RETRIES = 3 # 最大重试次数
|
|
72
|
+
BASE_DELAY = 1.0 # 基础延迟(秒)
|
|
73
|
+
MAX_DELAY = 30.0 # 最大延迟(秒)
|
|
74
|
+
RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 529} # 可重试的状态码集合
|
|
75
|
+
OAUTH_BETA_HEADER = "oauth-2025-04-20" # OAuth beta 版本头
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class ApiMessageRequest:
|
|
80
|
+
"""模型调用输入参数
|
|
81
|
+
|
|
82
|
+
包含调用模型所需的所有参数。
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
model: 模型名称
|
|
86
|
+
messages: 对话消息列表
|
|
87
|
+
system_prompt: 系统提示词(可选)
|
|
88
|
+
max_tokens: 最大令牌数(默认 4096)
|
|
89
|
+
tools: 工具定义列表(默认空列表)
|
|
90
|
+
effort: 推理强度级别(可选,支持 low/medium/high/xhigh/max)
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
model: str
|
|
94
|
+
messages: list[ConversationMessage]
|
|
95
|
+
system_prompt: str | None = None
|
|
96
|
+
max_tokens: int = 4096
|
|
97
|
+
tools: list[dict[str, Any]] = field(default_factory=list)
|
|
98
|
+
effort: EffortLevel | None = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class ApiTextDeltaEvent:
|
|
103
|
+
"""增量文本事件
|
|
104
|
+
|
|
105
|
+
模型产生的增量文本输出。
|
|
106
|
+
|
|
107
|
+
Attributes:
|
|
108
|
+
text: 增量文本内容
|
|
109
|
+
reasoning: 增量思考内容(可选)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
text: str
|
|
113
|
+
reasoning: str | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class ApiMessageCompleteEvent:
|
|
118
|
+
"""完整消息事件
|
|
119
|
+
|
|
120
|
+
包含最终助手消息和完整使用量信息的事件。
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
message: 对话消息对象
|
|
124
|
+
usage: 使用量快照
|
|
125
|
+
stop_reason: 停止原因
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
message: ConversationMessage
|
|
129
|
+
usage: UsageSnapshot
|
|
130
|
+
stop_reason: str | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class ApiRetryEvent:
|
|
135
|
+
"""重试事件
|
|
136
|
+
|
|
137
|
+
表示可恢复的上游错误,将自动重试。
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
message: 错误消息
|
|
141
|
+
attempt: 当前尝试次数
|
|
142
|
+
max_attempts: 最大尝试次数
|
|
143
|
+
delay_seconds: 延迟秒数
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
message: str
|
|
147
|
+
attempt: int
|
|
148
|
+
max_attempts: int
|
|
149
|
+
delay_seconds: float
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class ApiToolCallStartedEvent:
|
|
154
|
+
"""工具调用开始生成事件
|
|
155
|
+
|
|
156
|
+
当模型开始生成工具调用时产生,包含工具名称和调用ID。
|
|
157
|
+
此事件在模型开始输出工具参数之前发出,使前端能够
|
|
158
|
+
立即显示工具调用指示器,而不必等待整个工具参数生成完毕。
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
tool_name: 工具名称
|
|
162
|
+
tool_use_id: 工具调用ID(可选)
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
tool_name: str
|
|
166
|
+
tool_use_id: str = ""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# 流事件联合类型
|
|
170
|
+
ApiStreamEvent = ApiTextDeltaEvent | ApiMessageCompleteEvent | ApiRetryEvent | ApiToolCallStartedEvent
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class SupportsStreamingMessages(Protocol):
|
|
174
|
+
"""流式消息协议
|
|
175
|
+
|
|
176
|
+
查询引擎在测试和生产中使用的协议。
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
async def stream_message(self, request: ApiMessageRequest) -> AsyncIterator[ApiStreamEvent]:
|
|
180
|
+
"""为请求产生流式事件"""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _is_media_related_error(exc: Exception) -> bool:
|
|
184
|
+
"""检查错误是否可能由图片内容导致
|
|
185
|
+
|
|
186
|
+
Anthropic API 在不支持图片时可能返回:
|
|
187
|
+
- 400 invalid_request_error
|
|
188
|
+
- 404 "No endpoints found that support image input"
|
|
189
|
+
|
|
190
|
+
注意:错误可能已被 _translate_api_error 转为 IllusionCodeApiError,
|
|
191
|
+
此时 status_code 属性丢失,需从消息字符串中判断。
|
|
192
|
+
"""
|
|
193
|
+
error_msg = str(exc).lower()
|
|
194
|
+
status = getattr(exc, "status_code", None)
|
|
195
|
+
|
|
196
|
+
# 从错误消息字符串中提取状态码(适配已翻译的异常)
|
|
197
|
+
if status is None:
|
|
198
|
+
for code in (404, 400):
|
|
199
|
+
if f"error code: {code}" in error_msg:
|
|
200
|
+
status = code
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
# 明确的图片不支持错误(404 或 400)
|
|
204
|
+
if status in {400, 404} and any(
|
|
205
|
+
kw in error_msg for kw in ("image", "media", "unsupported")
|
|
206
|
+
):
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
# 某些提供商返回的通用错误
|
|
210
|
+
if "does not support" in error_msg and "image" in error_msg:
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _is_retryable(exc: Exception) -> bool:
|
|
217
|
+
"""检查异常是否可重试
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
exc: 待检查的异常
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
bool: 是否可重试
|
|
224
|
+
"""
|
|
225
|
+
# API 状态错误:检查状态码
|
|
226
|
+
if isinstance(exc, APIStatusError):
|
|
227
|
+
return exc.status_code in RETRYABLE_STATUS_CODES
|
|
228
|
+
# API 错误:网络错误可重试
|
|
229
|
+
if isinstance(exc, APIError):
|
|
230
|
+
return True
|
|
231
|
+
# 连接错误可重试
|
|
232
|
+
if isinstance(exc, (ConnectionError, TimeoutError, OSError)):
|
|
233
|
+
return True
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _get_retry_delay(attempt: int, exc: Exception | None = None) -> float:
|
|
238
|
+
"""计算指数退避延迟(带抖动)
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
attempt: 当前尝试次数
|
|
242
|
+
exc: 异常对象(可选)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
float: 延迟秒数
|
|
246
|
+
"""
|
|
247
|
+
import random
|
|
248
|
+
|
|
249
|
+
# 检查 Retry-After 头
|
|
250
|
+
if isinstance(exc, APIStatusError):
|
|
251
|
+
retry_after = getattr(exc, "headers", {})
|
|
252
|
+
if hasattr(retry_after, "get"):
|
|
253
|
+
val = retry_after.get("retry-after")
|
|
254
|
+
if val:
|
|
255
|
+
try:
|
|
256
|
+
return min(float(val), MAX_DELAY)
|
|
257
|
+
except (ValueError, TypeError):
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
# 指数退避计算
|
|
261
|
+
delay = min(BASE_DELAY * (2 ** attempt), MAX_DELAY)
|
|
262
|
+
# 添加随机抖动(0-25%)
|
|
263
|
+
jitter = random.uniform(0, delay * 0.25)
|
|
264
|
+
return delay + jitter
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class AnthropicApiClient:
|
|
268
|
+
"""Anthropic 异步 SDK 封装类
|
|
269
|
+
|
|
270
|
+
带重试逻辑的 Anthropic API 薄封装。
|
|
271
|
+
|
|
272
|
+
Attributes:
|
|
273
|
+
_api_key: API 密钥
|
|
274
|
+
_auth_token: 认证令牌
|
|
275
|
+
_base_url: 基础 URL
|
|
276
|
+
_claude_oauth: 是否使用 OAuth
|
|
277
|
+
_auth_token_resolver: 认证令牌解析器
|
|
278
|
+
_session_id: 会话 ID
|
|
279
|
+
_client: AsyncAnthropic 客户端实例
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(
|
|
283
|
+
self,
|
|
284
|
+
api_key: str | None = None,
|
|
285
|
+
*,
|
|
286
|
+
auth_token: str | None = None,
|
|
287
|
+
base_url: str | None = None,
|
|
288
|
+
claude_oauth: bool = False,
|
|
289
|
+
auth_token_resolver: Callable[[], str] | None = None,
|
|
290
|
+
) -> None:
|
|
291
|
+
self._api_key = api_key
|
|
292
|
+
self._auth_token = auth_token
|
|
293
|
+
self._base_url = base_url
|
|
294
|
+
self._claude_oauth = claude_oauth
|
|
295
|
+
self._auth_token_resolver = auth_token_resolver
|
|
296
|
+
self._session_id = get_claude_code_session_id() if claude_oauth else ""
|
|
297
|
+
self._client = self._create_client()
|
|
298
|
+
|
|
299
|
+
def _create_client(self) -> AsyncAnthropic:
|
|
300
|
+
"""创建 Anthropic 客户端
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
AsyncAnthropic: 配置好的客户端实例
|
|
304
|
+
"""
|
|
305
|
+
kwargs: dict[str, Any] = {}
|
|
306
|
+
if self._api_key:
|
|
307
|
+
kwargs["api_key"] = self._api_key
|
|
308
|
+
if self._auth_token:
|
|
309
|
+
kwargs["auth_token"] = self._auth_token
|
|
310
|
+
kwargs["default_headers"] = (
|
|
311
|
+
claude_oauth_headers()
|
|
312
|
+
if self._claude_oauth
|
|
313
|
+
else {"anthropic-beta": OAUTH_BETA_HEADER}
|
|
314
|
+
)
|
|
315
|
+
if self._base_url:
|
|
316
|
+
kwargs["base_url"] = self._base_url
|
|
317
|
+
return AsyncAnthropic(**kwargs)
|
|
318
|
+
|
|
319
|
+
def _refresh_client_auth(self) -> None:
|
|
320
|
+
"""刷新客户端认证
|
|
321
|
+
|
|
322
|
+
如果使用 OAuth 且有令牌解析器,则刷新认证令牌。
|
|
323
|
+
"""
|
|
324
|
+
if not self._claude_oauth or self._auth_token_resolver is None:
|
|
325
|
+
return
|
|
326
|
+
next_token = self._auth_token_resolver()
|
|
327
|
+
if next_token and next_token != self._auth_token:
|
|
328
|
+
self._auth_token = next_token
|
|
329
|
+
self._client = self._create_client()
|
|
330
|
+
|
|
331
|
+
async def stream_message(self, request: ApiMessageRequest) -> AsyncIterator[ApiStreamEvent]:
|
|
332
|
+
"""流式生成文本增量并在 transient 错误时自动重试
|
|
333
|
+
|
|
334
|
+
当消息中包含图片但模型不支持时,自动降级为文本描述并重试。
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
request: API 消息请求
|
|
338
|
+
|
|
339
|
+
Yields:
|
|
340
|
+
ApiStreamEvent: 流式事件(文本增量或完整消息)
|
|
341
|
+
"""
|
|
342
|
+
last_error: Exception | None = None
|
|
343
|
+
media_stripped = False
|
|
344
|
+
|
|
345
|
+
for attempt in range(MAX_RETRIES + 1):
|
|
346
|
+
try:
|
|
347
|
+
self._refresh_client_auth()
|
|
348
|
+
async for event in self._stream_once(request):
|
|
349
|
+
yield event
|
|
350
|
+
return # 成功
|
|
351
|
+
except IllusionCodeApiError as exc:
|
|
352
|
+
# 如果消息包含图片且错误可能是模型不支持图片导致的,尝试降级
|
|
353
|
+
if (
|
|
354
|
+
not media_stripped
|
|
355
|
+
and _messages_have_media(request.messages)
|
|
356
|
+
and _is_media_related_error(exc)
|
|
357
|
+
):
|
|
358
|
+
log.warning(
|
|
359
|
+
"Request failed, possibly due to unsupported image content. "
|
|
360
|
+
"Retrying with text descriptions instead of images.",
|
|
361
|
+
)
|
|
362
|
+
request = ApiMessageRequest(
|
|
363
|
+
model=request.model,
|
|
364
|
+
messages=_strip_media_from_messages(request.messages),
|
|
365
|
+
system_prompt=request.system_prompt,
|
|
366
|
+
tools=request.tools,
|
|
367
|
+
max_tokens=request.max_tokens,
|
|
368
|
+
effort=request.effort,
|
|
369
|
+
)
|
|
370
|
+
media_stripped = True
|
|
371
|
+
continue
|
|
372
|
+
raise
|
|
373
|
+
except Exception as exc:
|
|
374
|
+
last_error = exc
|
|
375
|
+
# 如果消息包含图片且错误可能是模型不支持图片导致的,尝试降级
|
|
376
|
+
if (
|
|
377
|
+
not media_stripped
|
|
378
|
+
and _messages_have_media(request.messages)
|
|
379
|
+
and _is_media_related_error(exc)
|
|
380
|
+
):
|
|
381
|
+
log.warning(
|
|
382
|
+
"Request failed, possibly due to unsupported image content. "
|
|
383
|
+
"Retrying with text descriptions instead of images.",
|
|
384
|
+
)
|
|
385
|
+
request = ApiMessageRequest(
|
|
386
|
+
model=request.model,
|
|
387
|
+
messages=_strip_media_from_messages(request.messages),
|
|
388
|
+
system_prompt=request.system_prompt,
|
|
389
|
+
tools=request.tools,
|
|
390
|
+
max_tokens=request.max_tokens,
|
|
391
|
+
effort=request.effort,
|
|
392
|
+
)
|
|
393
|
+
media_stripped = True
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
# 超过最大重试次数或不可重试
|
|
397
|
+
if attempt >= MAX_RETRIES or not _is_retryable(exc):
|
|
398
|
+
if isinstance(exc, APIError):
|
|
399
|
+
raise _translate_api_error(exc) from exc
|
|
400
|
+
raise RequestFailure(str(exc)) from exc
|
|
401
|
+
|
|
402
|
+
# 计算延迟并发送重试事件
|
|
403
|
+
delay = _get_retry_delay(attempt, exc)
|
|
404
|
+
status = getattr(exc, "status_code", "?")
|
|
405
|
+
log.warning(
|
|
406
|
+
"API request failed (attempt %d/%d, status=%s), retrying in %.1fs: %s",
|
|
407
|
+
attempt + 1, MAX_RETRIES + 1, status, delay, exc,
|
|
408
|
+
)
|
|
409
|
+
yield ApiRetryEvent(
|
|
410
|
+
message=str(exc),
|
|
411
|
+
attempt=attempt + 1,
|
|
412
|
+
max_attempts=MAX_RETRIES + 1,
|
|
413
|
+
delay_seconds=delay,
|
|
414
|
+
)
|
|
415
|
+
await asyncio.sleep(delay)
|
|
416
|
+
|
|
417
|
+
# 最终错误处理
|
|
418
|
+
if last_error is not None:
|
|
419
|
+
if isinstance(last_error, APIError):
|
|
420
|
+
raise _translate_api_error(last_error) from last_error
|
|
421
|
+
raise RequestFailure(str(last_error)) from last_error
|
|
422
|
+
|
|
423
|
+
async def _stream_once(self, request: ApiMessageRequest) -> AsyncIterator[ApiStreamEvent]:
|
|
424
|
+
"""单次尝试流式消息
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
request: API 消息请求
|
|
428
|
+
|
|
429
|
+
Yields:
|
|
430
|
+
ApiStreamEvent: 流式事件
|
|
431
|
+
"""
|
|
432
|
+
# 构建请求参数
|
|
433
|
+
params: dict[str, Any] = {
|
|
434
|
+
"model": request.model,
|
|
435
|
+
"messages": [message.to_api_param(provider_type="anthropic") for message in request.messages],
|
|
436
|
+
"max_tokens": request.max_tokens,
|
|
437
|
+
}
|
|
438
|
+
# 添加系统提示词
|
|
439
|
+
if request.system_prompt:
|
|
440
|
+
params["system"] = request.system_prompt
|
|
441
|
+
# OAuth 认证:添加归属头
|
|
442
|
+
if self._claude_oauth:
|
|
443
|
+
attribution = claude_attribution_header()
|
|
444
|
+
params["system"] = (
|
|
445
|
+
f"{attribution}\n{params['system']}"
|
|
446
|
+
if params.get("system")
|
|
447
|
+
else attribution
|
|
448
|
+
)
|
|
449
|
+
# 添加工具定义
|
|
450
|
+
if request.tools:
|
|
451
|
+
params["tools"] = request.tools
|
|
452
|
+
# Anthropic API 不支持 reasoning_effort 参数,effort 通过系统提示词传递
|
|
453
|
+
# OAuth 附加参数
|
|
454
|
+
if self._claude_oauth:
|
|
455
|
+
params["betas"] = claude_oauth_betas()
|
|
456
|
+
params["metadata"] = {
|
|
457
|
+
"user_id": json.dumps(
|
|
458
|
+
{
|
|
459
|
+
"device_id": "illusion",
|
|
460
|
+
"session_id": self._session_id,
|
|
461
|
+
"account_uuid": "",
|
|
462
|
+
},
|
|
463
|
+
separators=(",", ":"),
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
params["extra_headers"] = {"x-client-request-id": str(uuid.uuid4())}
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
# 根据是否使用 OAuth 选择 API 端点
|
|
470
|
+
stream_api = self._client.beta.messages if self._claude_oauth else self._client.messages
|
|
471
|
+
async with stream_api.stream(**params) as stream:
|
|
472
|
+
async for event in stream:
|
|
473
|
+
event_type = getattr(event, "type", None)
|
|
474
|
+
# 处理工具调用开始事件:模型开始生成工具调用时立即通知
|
|
475
|
+
if event_type == "content_block_start":
|
|
476
|
+
block = getattr(event, "content_block", None)
|
|
477
|
+
if getattr(block, "type", None) == "tool_use":
|
|
478
|
+
yield ApiToolCallStartedEvent(
|
|
479
|
+
tool_name=getattr(block, "name", ""),
|
|
480
|
+
tool_use_id=getattr(block, "id", ""),
|
|
481
|
+
)
|
|
482
|
+
continue
|
|
483
|
+
# 处理文本/思考增量事件
|
|
484
|
+
if event_type != "content_block_delta":
|
|
485
|
+
continue
|
|
486
|
+
delta = getattr(event, "delta", None)
|
|
487
|
+
delta_type = getattr(delta, "type", None)
|
|
488
|
+
if delta_type == "text_delta":
|
|
489
|
+
text = getattr(delta, "text", "")
|
|
490
|
+
if text:
|
|
491
|
+
yield ApiTextDeltaEvent(text=text)
|
|
492
|
+
continue
|
|
493
|
+
if delta_type == "thinking_delta":
|
|
494
|
+
thinking = getattr(delta, "thinking", "") or getattr(delta, "text", "")
|
|
495
|
+
if thinking:
|
|
496
|
+
yield ApiTextDeltaEvent(text="", reasoning=thinking)
|
|
497
|
+
|
|
498
|
+
# 获取最终消息
|
|
499
|
+
final_message = await stream.get_final_message()
|
|
500
|
+
except APIError as exc:
|
|
501
|
+
# 检查是否为 effort 不支持错误
|
|
502
|
+
if _is_effort_unsupported_error(exc) and request.effort is not None:
|
|
503
|
+
# 直接向用户反馈错误,不进行降级
|
|
504
|
+
raise RequestFailure(
|
|
505
|
+
f"当前模型不支持推理强度 '{request.effort.value}',请尝试使用其他推理强度级别(如 low/medium/high)"
|
|
506
|
+
) from exc
|
|
507
|
+
# 可重试状态码直接抛出,让重试逻辑处理
|
|
508
|
+
if isinstance(exc, APIStatusError) and exc.status_code in RETRYABLE_STATUS_CODES:
|
|
509
|
+
raise
|
|
510
|
+
raise _translate_api_error(exc) from exc
|
|
511
|
+
|
|
512
|
+
# 提取使用量并发送完成事件
|
|
513
|
+
usage = getattr(final_message, "usage", None)
|
|
514
|
+
yield ApiMessageCompleteEvent(
|
|
515
|
+
message=assistant_message_from_api(final_message),
|
|
516
|
+
usage=UsageSnapshot(
|
|
517
|
+
input_tokens=int(getattr(usage, "input_tokens", 0) or 0),
|
|
518
|
+
output_tokens=int(getattr(usage, "output_tokens", 0) or 0),
|
|
519
|
+
),
|
|
520
|
+
stop_reason=getattr(final_message, "stop_reason", None),
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _is_effort_unsupported_error(exc: Exception) -> bool:
|
|
525
|
+
"""检测是否为 effort 字段不支持导致的错误
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
exc: 异常对象
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
bool: 是否为 effort 不支持错误
|
|
532
|
+
"""
|
|
533
|
+
error_msg = str(exc).lower()
|
|
534
|
+
# 检测常见的 effort 不支持错误消息
|
|
535
|
+
effort_keywords = ["effort", "reasoning_effort", "reasoning effort"]
|
|
536
|
+
unsupported_keywords = ["not supported", "unsupported", "invalid", "unknown"]
|
|
537
|
+
|
|
538
|
+
# 检查是否包含 effort 相关关键词
|
|
539
|
+
has_effort_keyword = any(keyword in error_msg for keyword in effort_keywords)
|
|
540
|
+
# 检查是否包含不支持相关关键词
|
|
541
|
+
has_unsupported_keyword = any(keyword in error_msg for keyword in unsupported_keywords)
|
|
542
|
+
|
|
543
|
+
# 检查特定的错误模式:unknown variant `max`/`xhigh` 等
|
|
544
|
+
has_variant_error = "unknown variant" in error_msg and any(
|
|
545
|
+
level in error_msg for level in ["max", "xhigh", "low", "medium", "high"]
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
return (has_effort_keyword and has_unsupported_keyword) or has_variant_error
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _translate_api_error(exc: APIError) -> IllusionCodeApiError:
|
|
552
|
+
"""转换 API 错误为统一异常类型
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
exc: Anthropic API 错误
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
IllusionCodeApiError: 统一异常类型
|
|
559
|
+
"""
|
|
560
|
+
name = exc.__class__.__name__
|
|
561
|
+
# 认证错误
|
|
562
|
+
if name in {"AuthenticationError", "PermissionDeniedError"}:
|
|
563
|
+
return AuthenticationFailure(str(exc))
|
|
564
|
+
# 速率限制错误
|
|
565
|
+
if name == "RateLimitError":
|
|
566
|
+
return RateLimitFailure(str(exc))
|
|
567
|
+
# 请求失败
|
|
568
|
+
return RequestFailure(str(exc))
|