flowent 0.2.4 → 0.3.1
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.
- package/README.md +3 -3
- package/backend/README.md +3 -3
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +1 -1
- package/backend/src/flowent/api_models.py +108 -0
- package/backend/src/flowent/app.py +151 -0
- package/backend/src/flowent/cli.py +13 -4
- package/backend/src/flowent/compact.py +34 -13
- package/backend/src/flowent/llm.py +52 -6
- package/backend/src/flowent/main.py +18 -1994
- package/backend/src/flowent/mcp.py +100 -2
- package/backend/src/flowent/network.py +5 -0
- package/backend/src/flowent/provider_connections.py +42 -0
- package/backend/src/flowent/routes/__init__.py +0 -0
- package/backend/src/flowent/routes/integrations.py +105 -0
- package/backend/src/flowent/routes/permissions.py +36 -0
- package/backend/src/flowent/routes/providers.py +53 -0
- package/backend/src/flowent/routes/system.py +48 -0
- package/backend/src/flowent/routes/workflow_routes.py +63 -0
- package/backend/src/flowent/routes/workspace.py +115 -0
- package/backend/src/flowent/state/__init__.py +53 -0
- package/backend/src/flowent/state/models.py +258 -0
- package/backend/src/flowent/state/schema.py +191 -0
- package/backend/src/flowent/state/store.py +1019 -0
- package/backend/src/flowent/static/assets/index-BaZmIi2Y.js +98 -0
- package/backend/src/flowent/static/assets/index-EC37agAH.css +2 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +52 -1318
- package/backend/src/flowent/system_tools.py +25 -0
- package/backend/src/flowent/tools.py +4 -2
- package/backend/src/flowent/usage.py +9 -4
- package/backend/src/flowent/workflows.py +282 -0
- package/backend/src/flowent/workspace/__init__.py +0 -0
- package/backend/src/flowent/workspace/context.py +335 -0
- package/backend/src/flowent/workspace/events.py +178 -0
- package/backend/src/flowent/workspace/output.py +396 -0
- package/backend/src/flowent/workspace/runtime.py +1160 -0
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-BaZmIi2Y.js +98 -0
- package/dist/frontend/assets/index-EC37agAH.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-BH30iLzb.css +0 -2
- package/backend/src/flowent/static/assets/index-sBFt3ORj.js +0 -84
- package/dist/frontend/assets/index-BH30iLzb.css +0 -2
- package/dist/frontend/assets/index-sBFt3ORj.js +0 -84
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ A workflow orchestration platform for multi-agent collaboration.
|
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
Flowent requires Bubblewrap for local tool isolation
|
|
21
|
-
first:
|
|
20
|
+
Flowent requires Bubblewrap for local tool isolation and ripgrep for file
|
|
21
|
+
search. Install the system packages first:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
sudo apt-get install bubblewrap
|
|
24
|
+
sudo apt-get install bubblewrap ripgrep
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Install the CLI globally:
|
package/backend/README.md
CHANGED
|
@@ -17,11 +17,11 @@ A workflow orchestration platform for multi-agent collaboration.
|
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
Flowent requires Bubblewrap for local tool isolation
|
|
21
|
-
first:
|
|
20
|
+
Flowent requires Bubblewrap for local tool isolation and ripgrep for file
|
|
21
|
+
search. Install the system packages first:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
sudo apt-get install bubblewrap
|
|
24
|
+
sudo apt-get install bubblewrap ripgrep
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Install the CLI globally:
|
package/backend/pyproject.toml
CHANGED
|
@@ -45,7 +45,7 @@ Use tools deliberately:
|
|
|
45
45
|
- Search the web only when current external information is needed.
|
|
46
46
|
- Update the plan when a task has multiple meaningful steps.
|
|
47
47
|
|
|
48
|
-
After each tool result, decide whether the task is complete, whether another tool is needed, or whether you need to explain a blocker. When no more tool work is needed, provide the final response."""
|
|
48
|
+
After each tool result, decide whether the task is complete, whether another tool is needed, or whether you need to explain a blocker. A tool call is not a final response. After every tool result, continue the same turn until you either call another tool, explain a blocker, or provide a final response. If a tool fails, use the error as context and continue deciding whether to retry, use another tool, or explain the blocker. When no more tool work is needed, provide the final response."""
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class AgentStreamEvent(BaseModel):
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from flowent.llm import ProviderFormat
|
|
6
|
+
from flowent.storage import StoredMessage, StoredWritablePath
|
|
7
|
+
from flowent.usage import TokenUsageInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProviderModelsRequest(BaseModel):
|
|
11
|
+
model_config = ConfigDict(extra="forbid")
|
|
12
|
+
|
|
13
|
+
provider: ProviderFormat
|
|
14
|
+
secret_reference: str
|
|
15
|
+
base_url: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProviderModelsResponse(BaseModel):
|
|
19
|
+
models: list[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProviderModelsFailureResponse(BaseModel):
|
|
23
|
+
model_config = ConfigDict(extra="forbid")
|
|
24
|
+
|
|
25
|
+
code: Literal[
|
|
26
|
+
"connection_failed",
|
|
27
|
+
"access_denied",
|
|
28
|
+
"rate_limited",
|
|
29
|
+
"provider_unavailable",
|
|
30
|
+
"request_failed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WorkspaceMessagesRequest(BaseModel):
|
|
35
|
+
model_config = ConfigDict(extra="forbid")
|
|
36
|
+
|
|
37
|
+
messages: list[StoredMessage]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WorkspaceMessageEditRequest(BaseModel):
|
|
41
|
+
model_config = ConfigDict(extra="forbid")
|
|
42
|
+
|
|
43
|
+
action: Literal["resend", "save"]
|
|
44
|
+
content: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WorkspaceMessageEditResponse(BaseModel):
|
|
48
|
+
model_config = ConfigDict(extra="forbid")
|
|
49
|
+
|
|
50
|
+
is_responding: bool = False
|
|
51
|
+
messages: list[StoredMessage]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class WorkspaceRespondRequest(BaseModel):
|
|
55
|
+
model_config = ConfigDict(extra="forbid")
|
|
56
|
+
|
|
57
|
+
content: str
|
|
58
|
+
message_id: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class WorkspaceClearResponse(BaseModel):
|
|
62
|
+
model_config = ConfigDict(extra="forbid")
|
|
63
|
+
|
|
64
|
+
messages: list[StoredMessage]
|
|
65
|
+
usage_info: TokenUsageInfo | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AboutResponse(BaseModel):
|
|
69
|
+
model_config = ConfigDict(extra="forbid")
|
|
70
|
+
|
|
71
|
+
version: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TelegramSessionApproveRequest(BaseModel):
|
|
75
|
+
model_config = ConfigDict(extra="forbid")
|
|
76
|
+
|
|
77
|
+
chat_id: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SkillSettingsRequest(BaseModel):
|
|
81
|
+
model_config = ConfigDict(extra="forbid")
|
|
82
|
+
|
|
83
|
+
enabled: bool
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class McpImportRequest(BaseModel):
|
|
87
|
+
model_config = ConfigDict(extra="forbid")
|
|
88
|
+
|
|
89
|
+
server_id: str
|
|
90
|
+
source: Literal["claude_code", "codex"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class McpImportPreviewRequest(BaseModel):
|
|
94
|
+
model_config = ConfigDict(extra="forbid")
|
|
95
|
+
|
|
96
|
+
source: Literal["claude_code", "codex"]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class WritablePathRequest(BaseModel):
|
|
100
|
+
model_config = ConfigDict(extra="forbid")
|
|
101
|
+
|
|
102
|
+
path: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class WritablePathListResponse(BaseModel):
|
|
106
|
+
model_config = ConfigDict(extra="forbid")
|
|
107
|
+
|
|
108
|
+
writable_paths: list[StoredWritablePath]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import AsyncIterator, Awaitable
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.responses import FileResponse
|
|
9
|
+
from fastapi.staticfiles import StaticFiles
|
|
10
|
+
|
|
11
|
+
from flowent.channels import TelegramBotManager, TelegramTransport
|
|
12
|
+
from flowent.compact import LocalSummaryCompactProvider
|
|
13
|
+
from flowent.llm import CompletionCallable
|
|
14
|
+
from flowent.logging import ensure_logging_configured
|
|
15
|
+
from flowent.mcp import McpManager, McpTransport
|
|
16
|
+
from flowent.paths import resolve_workdir
|
|
17
|
+
from flowent.routes.integrations import register_integration_routes
|
|
18
|
+
from flowent.routes.permissions import register_permission_routes
|
|
19
|
+
from flowent.routes.providers import register_provider_routes
|
|
20
|
+
from flowent.routes.system import register_system_routes
|
|
21
|
+
from flowent.routes.workflow_routes import register_workflow_routes
|
|
22
|
+
from flowent.routes.workspace import register_workspace_routes
|
|
23
|
+
from flowent.sandbox import ensure_sandbox_available
|
|
24
|
+
from flowent.storage import StateStore
|
|
25
|
+
from flowent.system_tools import ensure_ripgrep_available
|
|
26
|
+
from flowent.workspace.runtime import WorkspaceRuntime
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("flowent.app")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DEFAULT_STATIC_DIR = Path(__file__).parent / "static"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def frontend_static_directory() -> Path:
|
|
35
|
+
configured_directory = os.environ.get("FLOWENT_STATIC_DIR")
|
|
36
|
+
if configured_directory:
|
|
37
|
+
return Path(configured_directory)
|
|
38
|
+
repository_frontend_dist = Path(__file__).resolve().parents[3] / "frontend" / "dist"
|
|
39
|
+
if repository_frontend_dist.is_dir():
|
|
40
|
+
return repository_frontend_dist
|
|
41
|
+
return DEFAULT_STATIC_DIR
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_app(
|
|
45
|
+
*,
|
|
46
|
+
serve_frontend: bool = True,
|
|
47
|
+
chat_completion: CompletionCallable | None = None,
|
|
48
|
+
mcp_transport: McpTransport | None = None,
|
|
49
|
+
telegram_transport: TelegramTransport | None = None,
|
|
50
|
+
workdir: Path | str | None = None,
|
|
51
|
+
) -> FastAPI:
|
|
52
|
+
ensure_logging_configured()
|
|
53
|
+
ensure_sandbox_available()
|
|
54
|
+
ensure_ripgrep_available()
|
|
55
|
+
|
|
56
|
+
cwd = resolve_workdir(workdir)
|
|
57
|
+
store = StateStore()
|
|
58
|
+
compact_provider = LocalSummaryCompactProvider()
|
|
59
|
+
mcp_manager = McpManager(store=store, transport=mcp_transport)
|
|
60
|
+
|
|
61
|
+
static_dir = frontend_static_directory().resolve(strict=False)
|
|
62
|
+
logger.debug("Flowent app created serve_frontend=%s", serve_frontend)
|
|
63
|
+
logger.info("Workdir: %s", cwd)
|
|
64
|
+
logger.info("Static directory: %s", static_dir)
|
|
65
|
+
|
|
66
|
+
runtime = WorkspaceRuntime(
|
|
67
|
+
chat_completion=chat_completion,
|
|
68
|
+
compact_provider=compact_provider,
|
|
69
|
+
cwd=cwd,
|
|
70
|
+
mcp_manager=mcp_manager,
|
|
71
|
+
store=store,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
telegram_bot_manager = TelegramBotManager(
|
|
75
|
+
message_handler=runtime.reply_text,
|
|
76
|
+
store=store,
|
|
77
|
+
telegram_transport=telegram_transport,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def run_shutdown_step(label: str, cleanup: Awaitable[object]) -> None:
|
|
81
|
+
try:
|
|
82
|
+
await cleanup
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception("%s cleanup failed during shutdown", label)
|
|
85
|
+
|
|
86
|
+
async def graceful_shutdown() -> None:
|
|
87
|
+
await run_shutdown_step("Workspace", runtime.stop_for_shutdown())
|
|
88
|
+
await run_shutdown_step("Telegram", telegram_bot_manager.stop_all())
|
|
89
|
+
await run_shutdown_step("MCP", mcp_manager.stop_all())
|
|
90
|
+
|
|
91
|
+
@asynccontextmanager
|
|
92
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
93
|
+
app.state.mcp_manager = mcp_manager
|
|
94
|
+
app.state.telegram_bot_manager = telegram_bot_manager
|
|
95
|
+
await mcp_manager.start_enabled()
|
|
96
|
+
await telegram_bot_manager.start_enabled()
|
|
97
|
+
try:
|
|
98
|
+
yield
|
|
99
|
+
finally:
|
|
100
|
+
await graceful_shutdown()
|
|
101
|
+
|
|
102
|
+
app = FastAPI(title="Flowent", lifespan=lifespan)
|
|
103
|
+
app.state.mcp_manager = mcp_manager
|
|
104
|
+
app.state.telegram_bot_manager = telegram_bot_manager
|
|
105
|
+
|
|
106
|
+
register_system_routes(
|
|
107
|
+
app,
|
|
108
|
+
cwd=cwd,
|
|
109
|
+
mcp_manager=mcp_manager,
|
|
110
|
+
runtime=runtime,
|
|
111
|
+
store=store,
|
|
112
|
+
telegram_bot_manager=telegram_bot_manager,
|
|
113
|
+
)
|
|
114
|
+
register_provider_routes(app, store=store)
|
|
115
|
+
register_integration_routes(
|
|
116
|
+
app,
|
|
117
|
+
cwd=cwd,
|
|
118
|
+
mcp_manager=mcp_manager,
|
|
119
|
+
store=store,
|
|
120
|
+
telegram_bot_manager=telegram_bot_manager,
|
|
121
|
+
)
|
|
122
|
+
register_workflow_routes(
|
|
123
|
+
app,
|
|
124
|
+
chat_completion=chat_completion,
|
|
125
|
+
store=store,
|
|
126
|
+
)
|
|
127
|
+
register_permission_routes(app, cwd=cwd, store=store)
|
|
128
|
+
register_workspace_routes(app, runtime=runtime, store=store)
|
|
129
|
+
|
|
130
|
+
if serve_frontend and static_dir.is_dir():
|
|
131
|
+
assets_dir = static_dir / "assets"
|
|
132
|
+
if assets_dir.is_dir():
|
|
133
|
+
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
|
134
|
+
|
|
135
|
+
@app.get("/{path:path}")
|
|
136
|
+
async def spa_fallback(path: str) -> FileResponse:
|
|
137
|
+
file = (static_dir / path).resolve(strict=False)
|
|
138
|
+
if file.is_file() and file.is_relative_to(static_dir):
|
|
139
|
+
return FileResponse(file)
|
|
140
|
+
return FileResponse(static_dir / "index.html")
|
|
141
|
+
|
|
142
|
+
return app
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
app = create_app()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
import uvicorn
|
|
150
|
+
|
|
151
|
+
uvicorn.run(app)
|
|
@@ -59,13 +59,22 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
59
59
|
|
|
60
60
|
if args.command == "doctor":
|
|
61
61
|
from flowent.sandbox import SANDBOX_INSTALL_HINT, sandbox_binary
|
|
62
|
+
from flowent.system_tools import RIPGREP_INSTALL_HINT, ripgrep_binary
|
|
62
63
|
|
|
63
64
|
bwrap = sandbox_binary()
|
|
65
|
+
rg = ripgrep_binary()
|
|
66
|
+
|
|
64
67
|
if bwrap:
|
|
65
68
|
print(f"Sandbox: {bwrap}")
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
else:
|
|
70
|
+
print(f"Sandbox: missing. {SANDBOX_INSTALL_HINT}", file=sys.stderr)
|
|
71
|
+
|
|
72
|
+
if rg:
|
|
73
|
+
print(f"Search: {rg}")
|
|
74
|
+
else:
|
|
75
|
+
print(f"Search: missing. {RIPGREP_INSTALL_HINT}", file=sys.stderr)
|
|
76
|
+
|
|
77
|
+
raise SystemExit(0 if bwrap and rg else 1)
|
|
69
78
|
|
|
70
79
|
if args.version:
|
|
71
80
|
try:
|
|
@@ -95,7 +104,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
95
104
|
import uvicorn
|
|
96
105
|
|
|
97
106
|
uvicorn.run(
|
|
98
|
-
"flowent.
|
|
107
|
+
"flowent.app:app",
|
|
99
108
|
host=args.host,
|
|
100
109
|
port=args.port,
|
|
101
110
|
)
|
|
@@ -10,7 +10,7 @@ from flowent.llm import (
|
|
|
10
10
|
ProviderConnection,
|
|
11
11
|
complete_chat_with_usage,
|
|
12
12
|
)
|
|
13
|
-
from flowent.usage import TokenUsage
|
|
13
|
+
from flowent.usage import APPROX_BYTES_PER_TOKEN, TokenUsage, approximate_token_count
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from flowent.storage import StoredMessage
|
|
@@ -172,14 +172,41 @@ def retained_recent_user_messages(
|
|
|
172
172
|
def truncate_text_to_token_budget(content: str, token_budget: int) -> str:
|
|
173
173
|
if token_budget <= 0 or not content:
|
|
174
174
|
return ""
|
|
175
|
-
|
|
176
|
-
if len(content) <=
|
|
175
|
+
byte_budget = max(token_budget * APPROX_BYTES_PER_TOKEN, 1)
|
|
176
|
+
if len(content.encode("utf-8")) <= byte_budget:
|
|
177
177
|
return content
|
|
178
|
-
left_budget =
|
|
179
|
-
right_budget =
|
|
180
|
-
|
|
178
|
+
left_budget = byte_budget // 2
|
|
179
|
+
right_budget = byte_budget - left_budget
|
|
180
|
+
prefix = text_prefix_for_byte_budget(content, left_budget)
|
|
181
|
+
suffix = text_suffix_for_byte_budget(content, right_budget)
|
|
182
|
+
middle_end = -len(suffix) if suffix else len(content)
|
|
183
|
+
removed_tokens = approximate_token_count(content[len(prefix) : middle_end])
|
|
181
184
|
marker = f"…{removed_tokens} tokens truncated…"
|
|
182
|
-
return f"{
|
|
185
|
+
return f"{prefix}{marker}{suffix}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def text_prefix_for_byte_budget(content: str, byte_budget: int) -> str:
|
|
189
|
+
used_bytes = 0
|
|
190
|
+
prefix: list[str] = []
|
|
191
|
+
for character in content:
|
|
192
|
+
character_bytes = len(character.encode("utf-8"))
|
|
193
|
+
if used_bytes + character_bytes > byte_budget:
|
|
194
|
+
break
|
|
195
|
+
prefix.append(character)
|
|
196
|
+
used_bytes += character_bytes
|
|
197
|
+
return "".join(prefix)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def text_suffix_for_byte_budget(content: str, byte_budget: int) -> str:
|
|
201
|
+
used_bytes = 0
|
|
202
|
+
suffix: list[str] = []
|
|
203
|
+
for character in reversed(content):
|
|
204
|
+
character_bytes = len(character.encode("utf-8"))
|
|
205
|
+
if used_bytes + character_bytes > byte_budget:
|
|
206
|
+
break
|
|
207
|
+
suffix.append(character)
|
|
208
|
+
used_bytes += character_bytes
|
|
209
|
+
return "".join(reversed(suffix))
|
|
183
210
|
|
|
184
211
|
|
|
185
212
|
def transcript_messages_after(
|
|
@@ -196,9 +223,3 @@ def transcript_messages_after(
|
|
|
196
223
|
|
|
197
224
|
def approximate_tokens_for_messages(messages: Sequence[ChatMessage]) -> int:
|
|
198
225
|
return sum(approximate_token_count(message.content) for message in messages)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def approximate_token_count(content: str) -> int:
|
|
202
|
-
if not content:
|
|
203
|
-
return 0
|
|
204
|
-
return max(1, (len(content) + 3) // 4)
|
|
@@ -13,6 +13,7 @@ from flowent.logging import (
|
|
|
13
13
|
configure_litellm_logging,
|
|
14
14
|
write_llm_request_diagnostic,
|
|
15
15
|
)
|
|
16
|
+
from flowent.network import flowent_user_agent
|
|
16
17
|
from flowent.usage import TokenUsage, token_usage_from_response
|
|
17
18
|
|
|
18
19
|
|
|
@@ -84,6 +85,20 @@ class LLMStreamError(RuntimeError):
|
|
|
84
85
|
pass
|
|
85
86
|
|
|
86
87
|
|
|
88
|
+
class ProviderModelFetchFailure(StrEnum):
|
|
89
|
+
CONNECTION_FAILED = "connection_failed"
|
|
90
|
+
ACCESS_DENIED = "access_denied"
|
|
91
|
+
RATE_LIMITED = "rate_limited"
|
|
92
|
+
PROVIDER_UNAVAILABLE = "provider_unavailable"
|
|
93
|
+
REQUEST_FAILED = "request_failed"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ProviderModelFetchError(RuntimeError):
|
|
97
|
+
def __init__(self, failure: ProviderModelFetchFailure) -> None:
|
|
98
|
+
super().__init__(failure.value)
|
|
99
|
+
self.failure = failure
|
|
100
|
+
|
|
101
|
+
|
|
87
102
|
async def wait_before_llm_retry(attempt_number: int) -> None:
|
|
88
103
|
await asyncio.sleep(LLM_RETRY_BASE_DELAY_SECONDS * attempt_number)
|
|
89
104
|
|
|
@@ -247,6 +262,31 @@ def unique_model_names(provider: ProviderFormat, models: Sequence[str]) -> list[
|
|
|
247
262
|
return normalized_models
|
|
248
263
|
|
|
249
264
|
|
|
265
|
+
def classify_provider_model_fetch_failure(
|
|
266
|
+
exception: Exception,
|
|
267
|
+
) -> ProviderModelFetchFailure:
|
|
268
|
+
try:
|
|
269
|
+
import litellm
|
|
270
|
+
except Exception:
|
|
271
|
+
return ProviderModelFetchFailure.REQUEST_FAILED
|
|
272
|
+
|
|
273
|
+
if isinstance(exception, (litellm.APIConnectionError, litellm.Timeout)):
|
|
274
|
+
return ProviderModelFetchFailure.CONNECTION_FAILED
|
|
275
|
+
if isinstance(
|
|
276
|
+
exception, (litellm.AuthenticationError, litellm.PermissionDeniedError)
|
|
277
|
+
):
|
|
278
|
+
return ProviderModelFetchFailure.ACCESS_DENIED
|
|
279
|
+
if isinstance(exception, litellm.RateLimitError):
|
|
280
|
+
return ProviderModelFetchFailure.RATE_LIMITED
|
|
281
|
+
if isinstance(
|
|
282
|
+
exception, (litellm.ServiceUnavailableError, litellm.InternalServerError)
|
|
283
|
+
):
|
|
284
|
+
return ProviderModelFetchFailure.PROVIDER_UNAVAILABLE
|
|
285
|
+
if isinstance(exception, (ConnectionError, TimeoutError)):
|
|
286
|
+
return ProviderModelFetchFailure.CONNECTION_FAILED
|
|
287
|
+
return ProviderModelFetchFailure.REQUEST_FAILED
|
|
288
|
+
|
|
289
|
+
|
|
250
290
|
def list_provider_models(
|
|
251
291
|
*,
|
|
252
292
|
provider: ProviderFormat,
|
|
@@ -260,12 +300,17 @@ def list_provider_models(
|
|
|
260
300
|
configure_litellm_logging()
|
|
261
301
|
model_lister = get_valid_models
|
|
262
302
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
303
|
+
try:
|
|
304
|
+
models = model_lister(
|
|
305
|
+
api_base=normalize_provider_base_url(provider, base_url),
|
|
306
|
+
api_key=secret_reference,
|
|
307
|
+
check_provider_endpoint=True,
|
|
308
|
+
custom_llm_provider=provider_litellm_name(provider),
|
|
309
|
+
)
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
raise ProviderModelFetchError(
|
|
312
|
+
classify_provider_model_fetch_failure(exc)
|
|
313
|
+
) from exc
|
|
269
314
|
return unique_model_names(provider, models)
|
|
270
315
|
|
|
271
316
|
|
|
@@ -300,6 +345,7 @@ def build_litellm_request(
|
|
|
300
345
|
)
|
|
301
346
|
request: dict[str, Any] = {
|
|
302
347
|
"api_key": connection.secret_reference,
|
|
348
|
+
"extra_headers": {"User-Agent": flowent_user_agent()},
|
|
303
349
|
"messages": request_messages,
|
|
304
350
|
"model": provider_model_name(connection),
|
|
305
351
|
}
|