Skip to content

Commit 2d071fd

Browse files
committed
clean up
1 parent a77764e commit 2d071fd

10 files changed

Lines changed: 53 additions & 126 deletions

File tree

app/web_ui/src/lib/ui/error_with_trace.svelte

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
export let error_title: string = "Error"
88
export let troubleshooting_steps: string[] = []
99
10-
// The generated ErrorWithTrace["trace"] uses the "-Output" variants of the
11-
// chat message schemas while the shared `Trace` alias uses the "-Input"
12-
// variants. Shapes are render-compatible; cast once here so trace.svelte's
13-
// prop typing is satisfied.
10+
// The API response and UI component use slightly different TypeScript types
11+
// for chat messages (same structure, different type names). Cast once here
12+
// to satisfy the Trace component's prop typing.
1413
$: trace_for_viewer = (error.trace ?? []) as TraceType
1514
</script>
1615

app/web_ui/src/routes/(app)/run/run_page_errors.test.ts

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
11
import { describe, it, expect } from "vitest"
22
import { looks_like_error_with_trace } from "./error_with_trace_detection"
33

4-
// Phase 4 wires the run page to distinguish three fetch outcomes:
5-
// 1. 500 with an ErrorWithTrace body -> render <ErrorWithTrace>
6-
// 2. 500 (or 4xx) with a plain HTTPException body -> FormContainer fallback
7-
// 3. Network failure / non-JSON body -> FormContainer fallback
8-
// In all three cases the submit button re-enables because `submitting = false`
9-
// lives inside a `finally` block.
10-
//
11-
// The single decision point driving (1) vs (2)+(3) on the run page is
12-
// `looks_like_error_with_trace(fetch_error)`. `fetch_error` is whatever
13-
// openapi-fetch parsed out of the response body (JSON object, string fallback,
14-
// or {} for empty bodies per `openapi-fetch/dist/index.js`). These tests feed
15-
// each scenario's realistic `fetch_error` shape into the type guard and assert
16-
// the routing decision.
17-
184
describe("looks_like_error_with_trace (type guard)", () => {
195
it("returns true for an ErrorWithTrace body with trace present", () => {
206
const body = {
@@ -108,33 +94,3 @@ describe("looks_like_error_with_trace (type guard)", () => {
10894
expect(looks_like_error_with_trace(body)).toBe(false)
10995
})
11096
})
111-
112-
// Explicit scenario-level mapping tests. These re-assert scenario-to-branch
113-
// mapping so future readers see the three run-page outcomes named directly.
114-
describe("run page error routing scenarios", () => {
115-
it("scenario 1: 500 with ErrorWithTrace body -> renders <ErrorWithTrace>", () => {
116-
const fetch_error = {
117-
message: "The model's output didn't match the expected format.",
118-
error_type: "JSONSchemaValidationError",
119-
trace: [
120-
{ role: "system", content: "You are a helpful assistant." },
121-
{ role: "user", content: "hi" },
122-
{ role: "assistant", content: "not valid json" },
123-
],
124-
}
125-
expect(looks_like_error_with_trace(fetch_error)).toBe(true)
126-
})
127-
128-
it("scenario 2: 500 with plain {detail: ...} body -> FormContainer fallback", () => {
129-
const fetch_error = { detail: "Task configuration was deleted." }
130-
expect(looks_like_error_with_trace(fetch_error)).toBe(false)
131-
})
132-
133-
it("scenario 3: network failure surfaces non-object fetch_error -> FormContainer fallback", () => {
134-
// openapi-fetch returns the raw text when the body isn't JSON; a thrown
135-
// fetch error at the transport layer never reaches this branch (it bubbles
136-
// to the outer catch) but we still guard against it here.
137-
expect(looks_like_error_with_trace("")).toBe(false)
138-
expect(looks_like_error_with_trace(undefined)).toBe(false)
139-
})
140-
})

libs/core/kiln_ai/adapters/errors.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- `KilnRunError`: the exception thrown by the adapter that carries the partial
66
conversation trace across the exception boundary so the API layer can return
77
it to the client.
8-
- `format_user_message`: maps known exceptions to user-friendly text.
8+
- `format_error_message`: maps known exceptions to user-friendly text.
99
"""
1010

1111
from __future__ import annotations
@@ -66,11 +66,11 @@ def _safe_str(exc: Exception) -> str:
6666
return result
6767

6868

69-
def format_user_message(exc: Exception) -> str:
69+
def format_error_message(exc: Exception) -> str:
7070
"""Map an exception to a user-friendly message.
7171
72-
Known exception types get custom messages. Unknown types fall back to
73-
str(exc). Never returns a stack trace or internal details.
72+
Known exception types get custom messages. Unknown types get a generic
73+
fallback to avoid leaking provider internals to the client.
7474
"""
7575
try:
7676
# Order matters: several litellm error classes inherit from each
@@ -117,6 +117,6 @@ def format_user_message(exc: Exception) -> str:
117117
if "specific output schema" in msg or "didn't meet the schema" in msg:
118118
return "The model's output didn't match the task's output schema."
119119

120-
return _safe_str(exc)
120+
return _GENERIC_FALLBACK_MESSAGE
121121
except Exception:
122122
return _GENERIC_FALLBACK_MESSAGE

libs/core/kiln_ai/adapters/model_adapters/base_adapter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
MultiturnFormatter,
1414
get_chat_formatter,
1515
)
16-
from kiln_ai.adapters.errors import KilnRunError, format_user_message
16+
from kiln_ai.adapters.errors import KilnRunError, format_error_message
1717
from kiln_ai.adapters.ml_model_list import (
1818
KilnModelProvider,
1919
StructuredOutputMode,
@@ -287,8 +287,8 @@ async def _run_returning_run_output(
287287
for message in parsed_output.trace
288288
)
289289

290-
# Validate reasoning content is present and required
291-
# We don't require reasoning when using tools as models tend not to return any on the final turn (both Sonnet and Gemini).
290+
# Validate reasoning content is present if required.
291+
# Models often skip reasoning on the final turn when tools are involved, so we don't require it then.
292292
if (
293293
provider.reasoning_capable
294294
and (
@@ -343,7 +343,7 @@ async def _run_returning_run_output(
343343
except Exception:
344344
partial_trace = None
345345
raise KilnRunError(
346-
message=format_user_message(e),
346+
message=format_error_message(e),
347347
partial_trace=partial_trace,
348348
original=e,
349349
) from e

libs/core/kiln_ai/adapters/model_adapters/mcp_adapter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
from typing import Tuple
33

4-
from kiln_ai.adapters.errors import KilnRunError, format_user_message
4+
from kiln_ai.adapters.errors import KilnRunError, format_error_message
55
from kiln_ai.adapters.model_adapters.base_adapter import AdapterConfig, BaseAdapter
66
from kiln_ai.adapters.parsers.json_parser import parse_json_string
77
from kiln_ai.adapters.run_output import RunOutput
@@ -214,7 +214,7 @@ async def _run_and_validate_output(
214214
raise
215215
except Exception as e:
216216
raise KilnRunError(
217-
message=format_user_message(e),
217+
message=format_error_message(e),
218218
partial_trace=None,
219219
original=e,
220220
) from e

libs/core/kiln_ai/adapters/model_adapters/test_base_adapter_errors.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
- The partial trace is preserved across the exception boundary
66
- Already-wrapped KilnRunErrors pass through unmodified
77
- The original exception is accessible via `.original` and `__cause__`
8-
- `format_user_message` is applied to the wrapped message
8+
- `format_error_message` is applied to the wrapped message
99
"""
1010

1111
from __future__ import annotations
1212

1313
from typing import Tuple
14-
from unittest.mock import MagicMock, patch
14+
from unittest.mock import patch
1515

1616
import litellm
1717
import pytest
@@ -161,15 +161,15 @@ async def test_cause_chain_preserved(make_adapter):
161161
assert ei.value.original is inner
162162

163163

164-
async def test_format_user_message_applied_for_known_exception(make_adapter):
164+
async def test_format_error_message_applied_for_known_exception(make_adapter):
165165
inner = RuntimeError("Too many turns (11). Stopping iteration...")
166166
adapter = make_adapter(post_raise=inner)
167167
with pytest.raises(KilnRunError) as ei:
168168
await adapter._run_returning_run_output("hello")
169169
assert str(ei.value) == "The run exceeded the maximum number of turns."
170170

171171

172-
async def test_format_user_message_applied_for_litellm_rate_limit(make_adapter):
172+
async def test_format_error_message_applied_for_litellm_rate_limit(make_adapter):
173173
try:
174174
rate_limit = litellm.RateLimitError(
175175
message="upstream", model="m", llm_provider="openai"
@@ -231,24 +231,3 @@ async def test_messages_to_trace_hook_called(make_adapter):
231231
await adapter._run_returning_run_output("hello")
232232
hook.assert_called_once()
233233
assert ei.value.partial_trace == converted
234-
235-
236-
async def test_happy_path_unchanged(make_adapter, base_task):
237-
"""Sanity check: when `_run` succeeds the adapter still returns normally."""
238-
run_output = RunOutput(output="hi", intermediate_outputs={})
239-
adapter = make_adapter(return_output=(run_output, Usage()))
240-
241-
with (
242-
patch(
243-
"kiln_ai.adapters.model_adapters.base_adapter.model_parser_from_id"
244-
) as mock_parser_from_id,
245-
patch.object(type(adapter), "model_provider") as mock_mp,
246-
):
247-
mock_parser = MagicMock()
248-
mock_parser.parse_output.return_value = run_output
249-
mock_parser_from_id.return_value = mock_parser
250-
mock_mp.return_value = KilnModelProvider(name="openai", formatter=None)
251-
252-
task_run, out = await adapter._run_returning_run_output("hello")
253-
assert out is run_output
254-
assert task_run.output.output == "hi"

libs/core/kiln_ai/adapters/model_adapters/test_mcp_adapter.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,7 @@ async def test_mcp_adapter_runtime_failure_wrapped_in_kiln_run_error(
299299

300300
assert isinstance(ei.value.original, RuntimeError)
301301
assert ei.value.partial_trace is None
302-
# format_user_message falls back to str() for unrecognised RuntimeError.
303-
assert str(ei.value) == "mcp tool blew up"
302+
assert str(ei.value) == "An unexpected error occurred."
304303
assert ei.value.error_type == "RuntimeError"
305304

306305

@@ -313,8 +312,7 @@ async def test_mcp_adapter_output_schema_mismatch_wrapped_in_kiln_run_error(
313312
project_with_local_mcp_server,
314313
local_mcp_tool_id,
315314
):
316-
"""Post-run output schema validation failures are also runtime failures
317-
in scope for Phase 1 and should surface as KilnRunError."""
315+
"""Post-run output schema validation failures should surface as KilnRunError."""
318316
from kiln_ai.adapters.errors import KilnRunError
319317

320318
project, _ = project_with_local_mcp_server

libs/core/kiln_ai/adapters/test_errors.py

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from kiln_ai.adapters.errors import (
1010
ErrorWithTrace,
1111
KilnRunError,
12-
format_user_message,
12+
format_error_message,
1313
)
1414

1515

@@ -59,21 +59,21 @@ class TestFormatUserMessage:
5959
def test_rate_limit_error(self):
6060
exc = _make_litellm_error(litellm.RateLimitError)
6161
assert (
62-
format_user_message(exc)
62+
format_error_message(exc)
6363
== "Rate limit exceeded. Wait a moment and try again."
6464
)
6565

6666
def test_authentication_error(self):
6767
exc = _make_litellm_error(litellm.AuthenticationError)
6868
assert (
69-
format_user_message(exc)
69+
format_error_message(exc)
7070
== "Authentication with the model provider failed. Check your API key."
7171
)
7272

7373
def test_api_connection_error(self):
7474
exc = _make_litellm_error(litellm.APIConnectionError)
7575
assert (
76-
format_user_message(exc)
76+
format_error_message(exc)
7777
== "Could not connect to the model provider. Check your network connection."
7878
)
7979

@@ -88,7 +88,7 @@ def test_api_connection_error(self):
8888
def test_service_unavailable_family(self, cls):
8989
exc = _make_litellm_error(cls)
9090
assert (
91-
format_user_message(exc)
91+
format_error_message(exc)
9292
== "The model provider is currently unavailable. Try again in a moment."
9393
)
9494

@@ -108,7 +108,7 @@ def test_json_schema_validation_error(self):
108108
)
109109
Exception.__init__(exc, "schema mismatch")
110110
assert (
111-
format_user_message(exc)
111+
format_error_message(exc)
112112
== "The model's output didn't match the expected format."
113113
)
114114

@@ -117,15 +117,15 @@ def test_too_many_turns(self):
117117
"Too many turns (11). Stopping iteration to avoid using too many tokens."
118118
)
119119
assert (
120-
format_user_message(exc) == "The run exceeded the maximum number of turns."
120+
format_error_message(exc) == "The run exceeded the maximum number of turns."
121121
)
122122

123123
def test_too_many_tool_calls(self):
124124
exc = RuntimeError(
125125
"Too many tool calls (31). Stopping iteration to avoid using too many tokens."
126126
)
127127
assert (
128-
format_user_message(exc)
128+
format_error_message(exc)
129129
== "The run exceeded the maximum number of tool calls in one turn."
130130
)
131131

@@ -134,7 +134,7 @@ def test_tool_not_available(self):
134134
"A tool named 'foo' was invoked by a model, but was not available."
135135
)
136136
assert (
137-
format_user_message(exc)
137+
format_error_message(exc)
138138
== "The model tried to call a tool that isn't available on this task."
139139
)
140140

@@ -143,7 +143,7 @@ def test_parse_arguments(self):
143143
"Failed to parse arguments for tool 'foo' (should be JSON): blah"
144144
)
145145
assert (
146-
format_user_message(exc)
146+
format_error_message(exc)
147147
== "The model produced invalid arguments for a tool call."
148148
)
149149

@@ -152,7 +152,7 @@ def test_validate_arguments(self):
152152
"Failed to validate arguments for tool 'foo'. The arguments didn't match..."
153153
)
154154
assert (
155-
format_user_message(exc)
155+
format_error_message(exc)
156156
== "The model's tool call arguments didn't match the tool's schema."
157157
)
158158

@@ -161,7 +161,7 @@ def test_reasoning_required(self):
161161
"Reasoning is required for this model, but no reasoning was returned."
162162
)
163163
assert (
164-
format_user_message(exc)
164+
format_error_message(exc)
165165
== "The model should have returned reasoning but didn't."
166166
)
167167

@@ -171,29 +171,28 @@ def test_schema_mismatch_value_error(self):
171171
"that JSON didn't meet the schema. Search 'Troubleshooting Structured Data Issues' in our docs for more information."
172172
)
173173
assert (
174-
format_user_message(exc)
174+
format_error_message(exc)
175175
== "The model's output didn't match the task's output schema."
176176
)
177177

178-
def test_unknown_exception_falls_back_to_str(self):
178+
def test_unknown_exception_uses_generic_message(self):
179179
exc = KeyError("missing_key")
180-
# KeyError's str() wraps in quotes; just assert the key name is present
181-
assert "missing_key" in format_user_message(exc)
180+
assert format_error_message(exc) == "An unexpected error occurred."
182181

183-
def test_unknown_exception_type_falls_back_to_str(self):
182+
def test_unknown_exception_type_uses_generic_message(self):
184183
class WeirdError(Exception):
185184
pass
186185

187186
exc = WeirdError("something odd")
188-
assert format_user_message(exc) == "something odd"
187+
assert format_error_message(exc) == "An unexpected error occurred."
189188

190-
def test_runtime_error_without_known_prefix_falls_back(self):
189+
def test_runtime_error_without_known_prefix_uses_generic_message(self):
191190
exc = RuntimeError("something random happened")
192-
assert format_user_message(exc) == "something random happened"
191+
assert format_error_message(exc) == "An unexpected error occurred."
193192

194-
def test_value_error_without_schema_hint_falls_back(self):
193+
def test_value_error_without_schema_hint_uses_generic_message(self):
195194
exc = ValueError("just a regular value error")
196-
assert format_user_message(exc) == "just a regular value error"
195+
assert format_error_message(exc) == "An unexpected error occurred."
197196

198197
def test_survives_broken_str(self):
199198
class BrokenStrError(Exception):
@@ -202,20 +201,18 @@ def __str__(self):
202201

203202
exc = BrokenStrError("hidden")
204203
# Should not raise; should return the generic fallback.
205-
assert format_user_message(exc) == "An unexpected error occurred."
204+
assert format_error_message(exc) == "An unexpected error occurred."
206205

207-
def test_empty_message_falls_back_to_class_name(self):
206+
def test_empty_message_falls_back_to_generic(self):
208207
exc = RuntimeError("")
209-
# RuntimeError with no recognised prefix and empty string → class name
210-
# (more useful to the user than a totally generic message).
211-
assert format_user_message(exc) == "RuntimeError"
208+
assert format_error_message(exc) == "An unexpected error occurred."
212209

213-
def test_empty_message_custom_class_uses_class_name(self):
210+
def test_empty_message_custom_class_uses_generic(self):
214211
class WeirdError(Exception):
215212
pass
216213

217214
exc = WeirdError("")
218-
assert format_user_message(exc) == "WeirdError"
215+
assert format_error_message(exc) == "An unexpected error occurred."
219216

220217

221218
class TestKilnRunError:

0 commit comments

Comments
 (0)