flowent 0.2.0 → 0.2.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/backend/pyproject.toml +31 -5
- package/backend/src/flowent/agent.py +13 -4
- package/backend/src/flowent/compact.py +35 -14
- package/backend/src/flowent/llm.py +73 -7
- package/backend/src/flowent/main.py +260 -59
- package/backend/src/flowent/static/assets/index-CRSV2xu1.css +2 -0
- package/backend/src/flowent/static/assets/index-DUYj6rgD.js +82 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +135 -3
- package/backend/src/flowent/usage.py +315 -0
- package/backend/uv.lock +971 -3
- package/dist/frontend/assets/index-CRSV2xu1.css +2 -0
- package/dist/frontend/assets/index-DUYj6rgD.js +82 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +24 -3
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/approval.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/compact.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/permissions.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/static/assets/index-BlaCigkZ.js +0 -82
- package/backend/src/flowent/static/assets/index-CRvbsH4K.css +0 -2
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_approval.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_patch.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_permissions.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/conftest.py +0 -60
- package/backend/tests/test_agent_tools.py +0 -1124
- package/backend/tests/test_approval.py +0 -283
- package/backend/tests/test_channels.py +0 -360
- package/backend/tests/test_health.py +0 -12
- package/backend/tests/test_llm_providers.py +0 -548
- package/backend/tests/test_logging.py +0 -212
- package/backend/tests/test_mcp.py +0 -788
- package/backend/tests/test_patch.py +0 -112
- package/backend/tests/test_permissions.py +0 -588
- package/backend/tests/test_persistence.py +0 -249
- package/backend/tests/test_skills.py +0 -462
- package/backend/tests/test_startup_requirements.py +0 -144
- package/backend/tests/test_workspace_chat.py +0 -2174
- package/dist/frontend/assets/index-BlaCigkZ.js +0 -82
- package/dist/frontend/assets/index-CRvbsH4K.css +0 -2
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from flowent.llm import (
|
|
6
|
-
ChatMessage,
|
|
7
|
-
LLMStreamError,
|
|
8
|
-
ProviderConnection,
|
|
9
|
-
ProviderFormat,
|
|
10
|
-
ReasoningEffort,
|
|
11
|
-
build_litellm_request,
|
|
12
|
-
chunk_delta_reasoning,
|
|
13
|
-
complete_chat,
|
|
14
|
-
list_provider_models,
|
|
15
|
-
normalize_system_messages,
|
|
16
|
-
stream_chat,
|
|
17
|
-
stream_chat_chunks,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def read_single_llm_request_diagnostic(tmp_path):
|
|
22
|
-
files = sorted((tmp_path / "logs" / "llm-requests").glob("llm-request-*.json"))
|
|
23
|
-
assert len(files) == 1
|
|
24
|
-
return json.loads(files[0].read_text())
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_supported_provider_formats_match_product_choices() -> None:
|
|
28
|
-
assert [provider.value for provider in ProviderFormat] == [
|
|
29
|
-
"openai",
|
|
30
|
-
"openai_responses",
|
|
31
|
-
"anthropic",
|
|
32
|
-
"gemini",
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_build_litellm_request_maps_provider_connection_to_completion_args() -> None:
|
|
37
|
-
connection = ProviderConnection(
|
|
38
|
-
name="Primary",
|
|
39
|
-
provider=ProviderFormat.ANTHROPIC,
|
|
40
|
-
model="claude-sonnet-4-5",
|
|
41
|
-
secret_reference="connection-primary",
|
|
42
|
-
base_url="https://example.test/v1",
|
|
43
|
-
)
|
|
44
|
-
messages = [
|
|
45
|
-
ChatMessage(role="system", content="Keep answers direct."),
|
|
46
|
-
ChatMessage(role="user", content="Draft a launch checklist."),
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
request = build_litellm_request(connection, messages)
|
|
50
|
-
|
|
51
|
-
assert request == {
|
|
52
|
-
"api_base": "https://example.test/v1",
|
|
53
|
-
"api_key": "connection-primary",
|
|
54
|
-
"messages": [
|
|
55
|
-
{"role": "system", "content": "Keep answers direct."},
|
|
56
|
-
{"role": "user", "content": "Draft a launch checklist."},
|
|
57
|
-
],
|
|
58
|
-
"model": "anthropic/claude-sonnet-4-5",
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def test_build_litellm_request_appends_default_api_version_to_base_url() -> None:
|
|
63
|
-
connection = ProviderConnection(
|
|
64
|
-
name="OpenAI Compatible",
|
|
65
|
-
provider=ProviderFormat.OPENAI,
|
|
66
|
-
model="gpt-5.1",
|
|
67
|
-
secret_reference="connection-primary",
|
|
68
|
-
base_url="https://example.test",
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
request = build_litellm_request(
|
|
72
|
-
connection, [ChatMessage(role="user", content="Draft a checklist.")]
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
assert request["api_base"] == "https://example.test/v1"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_build_litellm_request_appends_anthropic_api_version_to_base_url() -> None:
|
|
79
|
-
connection = ProviderConnection(
|
|
80
|
-
name="Anthropic",
|
|
81
|
-
provider=ProviderFormat.ANTHROPIC,
|
|
82
|
-
model="claude-sonnet-4-5",
|
|
83
|
-
secret_reference="connection-primary",
|
|
84
|
-
base_url="https://example.test",
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
request = build_litellm_request(
|
|
88
|
-
connection, [ChatMessage(role="user", content="Draft a checklist.")]
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
assert request["api_base"] == "https://example.test/v1"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_build_litellm_request_does_not_duplicate_existing_api_version() -> None:
|
|
95
|
-
connection = ProviderConnection(
|
|
96
|
-
name="OpenAI Compatible",
|
|
97
|
-
provider=ProviderFormat.OPENAI,
|
|
98
|
-
model="gpt-5.1",
|
|
99
|
-
secret_reference="connection-primary",
|
|
100
|
-
base_url="https://example.test/v1/",
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
request = build_litellm_request(
|
|
104
|
-
connection, [ChatMessage(role="user", content="Draft a checklist.")]
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
assert request["api_base"] == "https://example.test/v1"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_build_litellm_request_preserves_custom_versioned_base_path() -> None:
|
|
111
|
-
connection = ProviderConnection(
|
|
112
|
-
name="OpenAI Compatible",
|
|
113
|
-
provider=ProviderFormat.OPENAI,
|
|
114
|
-
model="gpt-5.1",
|
|
115
|
-
secret_reference="connection-primary",
|
|
116
|
-
base_url="https://example.test/api/v3",
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
request = build_litellm_request(
|
|
120
|
-
connection, [ChatMessage(role="user", content="Draft a checklist.")]
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
assert request["api_base"] == "https://example.test/api/v3"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def test_build_litellm_request_allows_base_url_version_opt_out() -> None:
|
|
127
|
-
connection = ProviderConnection(
|
|
128
|
-
name="OpenAI Compatible",
|
|
129
|
-
provider=ProviderFormat.OPENAI,
|
|
130
|
-
model="gpt-5.1",
|
|
131
|
-
secret_reference="connection-primary",
|
|
132
|
-
base_url="https://example.test#",
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
request = build_litellm_request(
|
|
136
|
-
connection, [ChatMessage(role="user", content="Draft a checklist.")]
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
assert request["api_base"] == "https://example.test"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def test_build_litellm_request_appends_gemini_api_version_to_base_url() -> None:
|
|
143
|
-
connection = ProviderConnection(
|
|
144
|
-
name="Gemini",
|
|
145
|
-
provider=ProviderFormat.GEMINI,
|
|
146
|
-
model="gemini-3-pro-preview",
|
|
147
|
-
secret_reference="connection-primary",
|
|
148
|
-
base_url="https://example.test",
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
request = build_litellm_request(
|
|
152
|
-
connection, [ChatMessage(role="user", content="Draft a checklist.")]
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
assert request["api_base"] == "https://example.test/v1beta"
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def test_list_provider_models_uses_normalized_base_url() -> None:
|
|
159
|
-
captured_request: dict[str, object] = {}
|
|
160
|
-
|
|
161
|
-
def fake_model_lister(**request: object) -> list[str]:
|
|
162
|
-
captured_request.update(request)
|
|
163
|
-
return ["openai/gpt-5.1", "gpt-5.1-mini"]
|
|
164
|
-
|
|
165
|
-
models = list_provider_models(
|
|
166
|
-
provider=ProviderFormat.OPENAI,
|
|
167
|
-
secret_reference="connection-primary",
|
|
168
|
-
base_url="https://example.test",
|
|
169
|
-
model_lister=fake_model_lister,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
assert captured_request["api_base"] == "https://example.test/v1"
|
|
173
|
-
assert models == ["gpt-5.1", "gpt-5.1-mini"]
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def test_build_litellm_request_omits_default_reasoning_effort() -> None:
|
|
177
|
-
connection = ProviderConnection(
|
|
178
|
-
name="Primary",
|
|
179
|
-
provider=ProviderFormat.OPENAI,
|
|
180
|
-
model="gpt-5.1",
|
|
181
|
-
secret_reference="connection-primary",
|
|
182
|
-
reasoning_effort=ReasoningEffort.DEFAULT,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
request = build_litellm_request(
|
|
186
|
-
connection, [ChatMessage(role="user", content="Draft a checklist.")]
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
assert "reasoning_effort" not in request
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def test_build_litellm_request_includes_selected_reasoning_effort() -> None:
|
|
193
|
-
connection = ProviderConnection(
|
|
194
|
-
name="Primary",
|
|
195
|
-
provider=ProviderFormat.OPENAI,
|
|
196
|
-
model="gpt-5.1",
|
|
197
|
-
secret_reference="connection-primary",
|
|
198
|
-
reasoning_effort=ReasoningEffort.XHIGH,
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
request = build_litellm_request(
|
|
202
|
-
connection, [ChatMessage(role="user", content="Draft a checklist.")]
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
assert request["reasoning_effort"] == "xhigh"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def test_chunk_delta_reasoning_reads_litellm_reasoning_fields() -> None:
|
|
209
|
-
assert (
|
|
210
|
-
chunk_delta_reasoning(
|
|
211
|
-
{"choices": [{"delta": {"reasoning_content": "Checking files."}}]}
|
|
212
|
-
)
|
|
213
|
-
== "Checking files."
|
|
214
|
-
)
|
|
215
|
-
assert (
|
|
216
|
-
chunk_delta_reasoning(
|
|
217
|
-
{
|
|
218
|
-
"choices": [
|
|
219
|
-
{
|
|
220
|
-
"delta": {
|
|
221
|
-
"thinking_blocks": [{"thinking": "Read files."}],
|
|
222
|
-
"reasoning_items": [{"summary": "Summarize."}],
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
]
|
|
226
|
-
}
|
|
227
|
-
)
|
|
228
|
-
== "Read files.Summarize."
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
@pytest.mark.anyio
|
|
233
|
-
async def test_complete_chat_uses_injected_litellm_completion() -> None:
|
|
234
|
-
captured_request: dict[str, object] = {}
|
|
235
|
-
|
|
236
|
-
async def fake_completion(**request: object) -> dict[str, object]:
|
|
237
|
-
captured_request.update(request)
|
|
238
|
-
return {
|
|
239
|
-
"choices": [
|
|
240
|
-
{
|
|
241
|
-
"message": {
|
|
242
|
-
"content": "Here is the checklist.",
|
|
243
|
-
"role": "assistant",
|
|
244
|
-
},
|
|
245
|
-
}
|
|
246
|
-
]
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
connection = ProviderConnection(
|
|
250
|
-
name="Responses",
|
|
251
|
-
provider=ProviderFormat.OPENAI_RESPONSES,
|
|
252
|
-
model="gpt-5.1",
|
|
253
|
-
secret_reference="connection-responses",
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
answer = await complete_chat(
|
|
257
|
-
connection,
|
|
258
|
-
[ChatMessage(role="user", content="Create a checklist.")],
|
|
259
|
-
completion=fake_completion,
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
assert captured_request["model"] == "openai/gpt-5.1"
|
|
263
|
-
assert answer == ChatMessage(role="assistant", content="Here is the checklist.")
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
@pytest.mark.anyio
|
|
267
|
-
async def test_development_mode_writes_completion_request_diagnostic_file(
|
|
268
|
-
tmp_path, monkeypatch
|
|
269
|
-
) -> None:
|
|
270
|
-
monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
|
|
271
|
-
monkeypatch.setenv("DEBUG", "true")
|
|
272
|
-
|
|
273
|
-
async def fake_completion(**request: object) -> dict[str, object]:
|
|
274
|
-
return {
|
|
275
|
-
"choices": [
|
|
276
|
-
{
|
|
277
|
-
"message": {
|
|
278
|
-
"content": "Here is the checklist.",
|
|
279
|
-
"role": "assistant",
|
|
280
|
-
},
|
|
281
|
-
}
|
|
282
|
-
]
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
connection = ProviderConnection(
|
|
286
|
-
name="Responses",
|
|
287
|
-
provider=ProviderFormat.OPENAI_RESPONSES,
|
|
288
|
-
model="gpt-5.1",
|
|
289
|
-
secret_reference="sk-request-secret",
|
|
290
|
-
)
|
|
291
|
-
messages = [ChatMessage(role="user", content="Create a checklist.")]
|
|
292
|
-
tools = [
|
|
293
|
-
{
|
|
294
|
-
"type": "function",
|
|
295
|
-
"function": {
|
|
296
|
-
"name": "create_checklist",
|
|
297
|
-
"description": "Create a checklist.",
|
|
298
|
-
},
|
|
299
|
-
}
|
|
300
|
-
]
|
|
301
|
-
|
|
302
|
-
await complete_chat(
|
|
303
|
-
connection,
|
|
304
|
-
messages,
|
|
305
|
-
completion=fake_completion,
|
|
306
|
-
tools=tools,
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
diagnostic = read_single_llm_request_diagnostic(tmp_path)
|
|
310
|
-
|
|
311
|
-
assert diagnostic == {
|
|
312
|
-
"base_url": None,
|
|
313
|
-
"litellm_model": "openai/gpt-5.1",
|
|
314
|
-
"messages": [{"content": "Create a checklist.", "role": "user"}],
|
|
315
|
-
"model": "gpt-5.1",
|
|
316
|
-
"provider": "openai_responses",
|
|
317
|
-
"reasoning_effort": "default",
|
|
318
|
-
"stream": False,
|
|
319
|
-
"tools": tools,
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
@pytest.mark.anyio
|
|
324
|
-
async def test_stream_chat_uses_litellm_streaming() -> None:
|
|
325
|
-
captured_request: dict[str, object] = {}
|
|
326
|
-
|
|
327
|
-
async def fake_completion(**request: object) -> object:
|
|
328
|
-
captured_request.update(request)
|
|
329
|
-
|
|
330
|
-
async def chunks() -> object:
|
|
331
|
-
yield {"choices": [{"delta": {"content": "Here is "}}]}
|
|
332
|
-
yield {"choices": [{"delta": {"content": "the checklist."}}]}
|
|
333
|
-
|
|
334
|
-
return chunks()
|
|
335
|
-
|
|
336
|
-
connection = ProviderConnection(
|
|
337
|
-
name="Responses",
|
|
338
|
-
provider=ProviderFormat.OPENAI_RESPONSES,
|
|
339
|
-
model="gpt-5.1",
|
|
340
|
-
secret_reference="connection-responses",
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
chunks = [
|
|
344
|
-
chunk
|
|
345
|
-
async for chunk in stream_chat(
|
|
346
|
-
connection,
|
|
347
|
-
[ChatMessage(role="user", content="Create a checklist.")],
|
|
348
|
-
completion=fake_completion,
|
|
349
|
-
)
|
|
350
|
-
]
|
|
351
|
-
|
|
352
|
-
assert captured_request["stream"] is True
|
|
353
|
-
assert captured_request["model"] == "openai/gpt-5.1"
|
|
354
|
-
assert chunks == ["Here is ", "the checklist."]
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
@pytest.mark.anyio
|
|
358
|
-
async def test_stream_chat_chunks_raises_when_responses_stream_fails(
|
|
359
|
-
fake_litellm_responses_transformer,
|
|
360
|
-
) -> None:
|
|
361
|
-
async def fake_completion(**request: object) -> object:
|
|
362
|
-
async def chunks() -> object:
|
|
363
|
-
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
|
364
|
-
OpenAiResponsesToChatCompletionStreamIterator,
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
yield {"choices": [{"delta": {"content": "Partial answer."}}]}
|
|
368
|
-
yield OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(
|
|
369
|
-
{
|
|
370
|
-
"response": {
|
|
371
|
-
"error": {
|
|
372
|
-
"code": "upstream_error",
|
|
373
|
-
"message": "Upstream request failed",
|
|
374
|
-
},
|
|
375
|
-
"status": "failed",
|
|
376
|
-
},
|
|
377
|
-
"type": "response.failed",
|
|
378
|
-
}
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
return chunks()
|
|
382
|
-
|
|
383
|
-
connection = ProviderConnection(
|
|
384
|
-
name="Responses",
|
|
385
|
-
provider=ProviderFormat.OPENAI_RESPONSES,
|
|
386
|
-
model="gpt-5.1",
|
|
387
|
-
secret_reference="connection-responses",
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
with pytest.raises(LLMStreamError, match="Upstream request failed"):
|
|
391
|
-
[
|
|
392
|
-
chunk
|
|
393
|
-
async for chunk in stream_chat_chunks(
|
|
394
|
-
connection,
|
|
395
|
-
[ChatMessage(role="user", content="Create a checklist.")],
|
|
396
|
-
completion=fake_completion,
|
|
397
|
-
)
|
|
398
|
-
]
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
@pytest.mark.anyio
|
|
402
|
-
async def test_development_mode_writes_one_streaming_request_diagnostic_file(
|
|
403
|
-
tmp_path, monkeypatch
|
|
404
|
-
) -> None:
|
|
405
|
-
monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
|
|
406
|
-
monkeypatch.setenv("DEBUG", "true")
|
|
407
|
-
|
|
408
|
-
async def fake_completion(**request: object) -> object:
|
|
409
|
-
async def chunks() -> object:
|
|
410
|
-
yield {"choices": [{"delta": {"content": "Here is "}}]}
|
|
411
|
-
yield {"choices": [{"delta": {"content": "the checklist."}}]}
|
|
412
|
-
|
|
413
|
-
return chunks()
|
|
414
|
-
|
|
415
|
-
connection = ProviderConnection(
|
|
416
|
-
name="Responses",
|
|
417
|
-
provider=ProviderFormat.OPENAI_RESPONSES,
|
|
418
|
-
model="gpt-5.1",
|
|
419
|
-
secret_reference="sk-request-secret",
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
chunks = [
|
|
423
|
-
chunk
|
|
424
|
-
async for chunk in stream_chat(
|
|
425
|
-
connection,
|
|
426
|
-
[ChatMessage(role="user", content="Create a checklist.")],
|
|
427
|
-
completion=fake_completion,
|
|
428
|
-
)
|
|
429
|
-
]
|
|
430
|
-
diagnostic = read_single_llm_request_diagnostic(tmp_path)
|
|
431
|
-
|
|
432
|
-
assert chunks == ["Here is ", "the checklist."]
|
|
433
|
-
assert diagnostic["stream"] is True
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
@pytest.mark.anyio
|
|
437
|
-
async def test_development_request_diagnostic_omits_api_key_and_secret_values(
|
|
438
|
-
tmp_path, monkeypatch
|
|
439
|
-
) -> None:
|
|
440
|
-
monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
|
|
441
|
-
monkeypatch.setenv("DEBUG", "true")
|
|
442
|
-
|
|
443
|
-
async def fake_completion(**request: object) -> dict[str, object]:
|
|
444
|
-
return {
|
|
445
|
-
"choices": [
|
|
446
|
-
{
|
|
447
|
-
"message": {
|
|
448
|
-
"content": "Here is the checklist.",
|
|
449
|
-
"role": "assistant",
|
|
450
|
-
},
|
|
451
|
-
}
|
|
452
|
-
]
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
connection = ProviderConnection(
|
|
456
|
-
name="Responses",
|
|
457
|
-
provider=ProviderFormat.OPENAI_RESPONSES,
|
|
458
|
-
model="gpt-5.1",
|
|
459
|
-
secret_reference="sk-provider-secret",
|
|
460
|
-
)
|
|
461
|
-
tools = [
|
|
462
|
-
{
|
|
463
|
-
"type": "function",
|
|
464
|
-
"function": {
|
|
465
|
-
"name": "create_checklist",
|
|
466
|
-
"description": "Uses api_key=sk-tool-secret when configured.",
|
|
467
|
-
},
|
|
468
|
-
}
|
|
469
|
-
]
|
|
470
|
-
|
|
471
|
-
await complete_chat(
|
|
472
|
-
connection,
|
|
473
|
-
[ChatMessage(role="user", content="authorization=Bearer sk-message-secret")],
|
|
474
|
-
completion=fake_completion,
|
|
475
|
-
tools=tools,
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
rendered = next(
|
|
479
|
-
(tmp_path / "logs" / "llm-requests").glob("llm-request-*.json")
|
|
480
|
-
).read_text()
|
|
481
|
-
|
|
482
|
-
assert "api_key" not in rendered
|
|
483
|
-
assert "sk-provider-secret" not in rendered
|
|
484
|
-
assert "sk-tool-secret" not in rendered
|
|
485
|
-
assert "sk-message-secret" not in rendered
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
@pytest.mark.anyio
|
|
489
|
-
async def test_non_development_mode_skips_request_diagnostic_file(
|
|
490
|
-
tmp_path, monkeypatch
|
|
491
|
-
) -> None:
|
|
492
|
-
monkeypatch.setenv("FLOWENT_DATA_DIR", str(tmp_path))
|
|
493
|
-
monkeypatch.delenv("DEBUG", raising=False)
|
|
494
|
-
|
|
495
|
-
async def fake_completion(**request: object) -> dict[str, object]:
|
|
496
|
-
return {
|
|
497
|
-
"choices": [
|
|
498
|
-
{
|
|
499
|
-
"message": {
|
|
500
|
-
"content": "Here is the checklist.",
|
|
501
|
-
"role": "assistant",
|
|
502
|
-
},
|
|
503
|
-
}
|
|
504
|
-
]
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
connection = ProviderConnection(
|
|
508
|
-
name="Responses",
|
|
509
|
-
provider=ProviderFormat.OPENAI_RESPONSES,
|
|
510
|
-
model="gpt-5.1",
|
|
511
|
-
secret_reference="sk-request-secret",
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
await complete_chat(
|
|
515
|
-
connection,
|
|
516
|
-
[ChatMessage(role="user", content="Create a checklist.")],
|
|
517
|
-
completion=fake_completion,
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
assert not (tmp_path / "logs" / "llm-requests").exists()
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
def test_normalize_system_messages_keeps_multiple_system_messages_for_openai() -> None:
|
|
524
|
-
messages = [
|
|
525
|
-
{"role": "system", "content": "Base prompt."},
|
|
526
|
-
{"role": "system", "content": "Configured prompt."},
|
|
527
|
-
{"role": "user", "content": "Hello."},
|
|
528
|
-
]
|
|
529
|
-
|
|
530
|
-
assert normalize_system_messages(messages, ProviderFormat.OPENAI) == messages
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
def test_normalize_system_messages_converts_additional_system_messages_for_anthropic() -> (
|
|
534
|
-
None
|
|
535
|
-
):
|
|
536
|
-
messages = [
|
|
537
|
-
{"role": "system", "content": "Base prompt."},
|
|
538
|
-
{"role": "system", "content": "Configured prompt."},
|
|
539
|
-
{"role": "system", "content": "Project prompt."},
|
|
540
|
-
{"role": "user", "content": "Hello."},
|
|
541
|
-
]
|
|
542
|
-
|
|
543
|
-
assert normalize_system_messages(messages, ProviderFormat.ANTHROPIC) == [
|
|
544
|
-
{"role": "system", "content": "Base prompt."},
|
|
545
|
-
{"role": "user", "content": "Configured prompt."},
|
|
546
|
-
{"role": "user", "content": "Project prompt."},
|
|
547
|
-
{"role": "user", "content": "Hello."},
|
|
548
|
-
]
|