diff --git a/docs/llm.markdown b/docs/llm.markdown index c4e1e999a..158e0566b 100644 --- a/docs/llm.markdown +++ b/docs/llm.markdown @@ -73,9 +73,6 @@ jsonschema llm path/to/schema.json \ --header "Authorization: Bearer $DOUBLEWORD_API_KEY" ``` -Doubleword can sometimes be slow to serve a request, independently of the -model, which is what the generous default `--timeout/-T` leaves room for. - ### OpenAI ```sh diff --git a/src/error.h b/src/error.h index 9d9f061d9..737786d6d 100644 --- a/src/error.h +++ b/src/error.h @@ -18,6 +18,7 @@ #include #include +#include // std::chrono::milliseconds #include // std::same_as, std::convertible_to #include // std::size_t #include // std::uint64_t @@ -1326,6 +1327,23 @@ inline auto print_exception(const bool is_json, const Exception &exception) } } + // A backend does not report which of its bounds elapsed, so this is the + // total that was configured rather than what the request actually took + if constexpr (requires(const Exception ¤t) { + { + current.timeout() + } -> std::same_as; + }) { + const auto timeout{exception.timeout()}; + if (is_json) { + error_json.assign( + "timeoutMilliseconds", + sourcemeta::core::JSON{static_cast(timeout.count())}); + } else { + std::cerr << " after " << timeout.count() << " milliseconds\n"; + } + } + if constexpr (requires(const Exception ¤t) { { current.method() @@ -2305,6 +2323,15 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::HTTPTimeoutError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nPass --timeout/-T with a larger number of seconds to " + "wait longer\n"; + } + + return EXIT_UNEXPECTED_ERROR; } catch (const sourcemeta::core::HTTPStatusError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 17e37c09a..1b331f6e9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1643,6 +1643,7 @@ add_jsonschema_test_unix_ci(pass_llm_request_response) add_jsonschema_test_unix_ci(fail_llm_response_invalid) add_jsonschema_test_unix_ci(fail_llm_response_rejected) add_jsonschema_test_unix_ci(fail_llm_response_unreadable) +add_jsonschema_test_unix_ci(fail_llm_timeout) add_jsonschema_test_unix_ci(precommit_lint) if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT JSONSCHEMA_USE_SYSTEM_CURL) diff --git a/test/ci/fail_llm_timeout.sh b/test/ci/fail_llm_timeout.sh new file mode 100755 index 000000000..8b2c38b1c --- /dev/null +++ b/test/ci/fail_llm_timeout.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +PORT=5899 + +# A server that accepts the connection and never answers, which is the one +# failure the caller can do something about by waiting longer +cat << 'EOF' > "$TMP/server.js" +const http = require('http'); +const server = http.createServer(() => {}); +server.listen(parseInt(process.argv[2], 10)); +EOF + +node "$TMP/server.js" "$PORT" & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +sleep 2 + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +"$1" llm "$TMP/schema.json" --ask hello \ + --url "http://localhost:${PORT}/v1/chat/completions" --model my-model \ + --timeout 2 \ + > "$TMP/output.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?" +# Unexpected error +test "$EXIT_CODE" = "1" + +cat << EOF > "$TMP/expected.txt" +error: The HTTP request timed out + after 2000 milliseconds + with method POST + at url http://localhost:${PORT}/v1/chat/completions + +Pass --timeout/-T with a larger number of seconds to wait longer +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" + +"$1" llm "$TMP/schema.json" --ask hello \ + --url "http://localhost:${PORT}/v1/chat/completions" --model my-model \ + --timeout 2 --json \ + > "$TMP/output_json.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?" +# Unexpected error +test "$EXIT_CODE" = "1" + +cat << EOF > "$TMP/expected_json.txt" +{ + "error": "The HTTP request timed out", + "timeoutMilliseconds": 2000, + "method": "POST", + "url": "http://localhost:${PORT}/v1/chat/completions" +} +EOF + +diff "$TMP/output_json.txt" "$TMP/expected_json.txt"