From ed8c38539206b60e9c7fc9a2913c44bbe85b3fa1 Mon Sep 17 00:00:00 2001 From: Garret Premo Date: Tue, 22 Sep 2026 09:37:00 -0400 Subject: [PATCH] Widen ScoreAnswer.legend to Map so object and array levels read back Score levels are sent as any JSON value and the API echoes them back as sent, but the legend was typed Map. A Score with an object or array level, including the one in Score's own javadoc example, failed the whole response and every other answer with it (#11). --- CHANGELOG.md | 4 ++++ .../premocloud/typesafe/ScoreAnswer.java | 6 ++--- .../typesafe/TypeSafeClientTest.java | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22642bb..59895b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- `ScoreAnswer.legend()` is now `Map`, since levels are sent as any JSON value and echoed back as sent. A `Score` with an object or array level previously failed the whole response, including its other answers and usage, with `Cannot deserialize value of type java.lang.String` (#11). + ## 0.4.0 - 2026-09-22 - Logging through slf4j on the `io.github.premocloud.typesafe` logger: `DEBUG` for one line per request with status and elapsed, plus retries and connection failures; `TRACE` for the wire in both directions. Nothing at `INFO` or above. Credential headers are masked. Adds `org.slf4j:slf4j-api` (#6, #7). diff --git a/typesafe-sdk/src/main/java/io/github/premocloud/typesafe/ScoreAnswer.java b/typesafe-sdk/src/main/java/io/github/premocloud/typesafe/ScoreAnswer.java index 64a031c..00983ef 100644 --- a/typesafe-sdk/src/main/java/io/github/premocloud/typesafe/ScoreAnswer.java +++ b/typesafe-sdk/src/main/java/io/github/premocloud/typesafe/ScoreAnswer.java @@ -10,14 +10,14 @@ * @param score probability-weighted position from 0 to the top level index * @param probabilities probability per level index (keys are the index as a string) * @param confidence 0 to 1, how concentrated the distribution is - * @param legend level index back to the description that was sent + * @param legend level index back to the description that was sent: a String, or the Map or List it was given as */ @JsonIgnoreProperties(ignoreUnknown = true) public record ScoreAnswer( double score, Map probabilities, double confidence, - Map legend + Map legend ) implements TypeSafeAnswer { /** Jackson entry point: a score answer missing its score, probabilities, or confidence is malformed. */ @@ -26,7 +26,7 @@ public record ScoreAnswer( @JsonProperty("score") Double score, @JsonProperty("probabilities") Map probabilities, @JsonProperty("confidence") Double confidence, - @JsonProperty("legend") Map legend, + @JsonProperty("legend") Map legend, @JsonProperty("type") String ignoredType ) { this(TypeSafeAnswer.required(score, "score", "score").doubleValue(), diff --git a/typesafe-sdk/src/test/java/io/github/premocloud/typesafe/TypeSafeClientTest.java b/typesafe-sdk/src/test/java/io/github/premocloud/typesafe/TypeSafeClientTest.java index b706c4b..9fef716 100644 --- a/typesafe-sdk/src/test/java/io/github/premocloud/typesafe/TypeSafeClientTest.java +++ b/typesafe-sdk/src/test/java/io/github/premocloud/typesafe/TypeSafeClientTest.java @@ -334,6 +334,28 @@ void systemOneRejectsScoreAnswerMissingItsScore() { assertTrue(exception.getMessage().contains("score"), exception.getMessage()); } + @Test + void systemOneReadsAScoreLegendWithObjectAndArrayLevels() { + // Levels are sent as any JSON value and echoed back as sent, so the legend holds whatever was asked (#11). + server.reply(200, """ + {"model": "jev-1.13.0", "answers": { + "is_phishing": {"type": "noul", "noul": 0.93}, + "spam_category": {"type": "choice", "choice": "PHISHING", "probabilities": {"PHISHING": 1.0}, "confidence": 1.0}, + "urgency": {"type": "score", "score": 1.0, "probabilities": {"0": 0.2, "1": 0.5, "2": 0.3}, "confidence": 0.5, + "legend": {"0": "none", "1": {"what": "Threatens loss within hours", "examples": ["final notice"]}, "2": ["a", "b"]}}}, + "usage": {"input_tokens": 1, "output_tokens": 1}} + """); + + TypeSafeResponse response = client.systemOne(spamRequest()); + + Map legend = response.score("urgency").legend(); + assertEquals("none", legend.get("0")); + assertEquals(Map.of("what", "Threatens loss within hours", "examples", List.of("final notice")), legend.get("1")); + assertEquals(List.of("a", "b"), legend.get("2")); + assertEquals(0.93, response.noul("is_phishing")); + assertEquals(1, response.usage().inputTokens()); + } + @Test void systemOneRejectsAResponseMissingAnAnswerForAQuestionThatWasAsked() { server.reply(200, """