From 3815558af6840d529ca4ef6e8a24cd93fa7b8d14 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Fri, 2 Oct 2026 11:43:48 -0400 Subject: [PATCH] perf(stream): validate delta maps without rebuilding them OTP 28 medians used 2.4% fewer reductions for text and 4.9% fewer with reasoning; plain-text timing was unchanged. Keep string-key validation; all 212 Gleam tests passed. --- src/albedo/openai_api/albedo_openai_json.erl | 17 ++++++++++++++++- src/albedo/openai_api/chat.gleam | 7 ++++++- test/openai_api/albedo_openai_json_test.erl | 7 +++++++ test/openai_api/stream_test.gleam | 8 ++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/albedo/openai_api/albedo_openai_json.erl b/src/albedo/openai_api/albedo_openai_json.erl index 065a7be..b3955e1 100644 --- a/src/albedo/openai_api/albedo_openai_json.erl +++ b/src/albedo/openai_api/albedo_openai_json.erl @@ -1,5 +1,5 @@ -module(albedo_openai_json). --export([encode/1, null/0, flatten/1, data_url/2, base64_string/1, semantically_empty/1]). +-export([encode/1, null/0, flatten/1, data_url/2, base64_string/1, semantically_empty/1, object_fields/1]). encode(Value) -> json:encode(Value). @@ -15,6 +15,21 @@ semantically_empty({}) -> true; semantically_empty(Value) when is_map(Value) -> map_size(Value) =:= 0; semantically_empty(_) -> false. +%% Validate the typed key contract while retaining the original map and values. +object_fields(Value) when is_map(Value) -> + case has_string_keys(maps:iterator(Value)) of + true -> {ok, Value}; + false -> {error, #{}} + end; +object_fields(_) -> {error, #{}}. + +has_string_keys(Iterator) -> + case maps:next(Iterator) of + none -> true; + {Key, _, Next} when is_binary(Key) -> has_string_keys(Next); + _ -> false + end. + %% Binaries at least this large cross process boundaries by reference. -define(SHARED, 512). diff --git a/src/albedo/openai_api/chat.gleam b/src/albedo/openai_api/chat.gleam index bc2720f..09adbce 100644 --- a/src/albedo/openai_api/chat.gleam +++ b/src/albedo/openai_api/chat.gleam @@ -120,7 +120,7 @@ fn choice_decoder() -> decode.Decoder(Choice) { } fn delta_decoder() -> decode.Decoder(Delta) { - use fields <- decode.then(decode.dict(decode.string, decode.dynamic)) + use fields <- decode.then(decode.new_primitive_decoder("Dict", object_fields)) case unsupported_field(fields) { Some(field) -> decode.failure( @@ -154,6 +154,11 @@ fn delta_decoder() -> decode.Decoder(Delta) { } } +@external(erlang, "albedo_openai_json", "object_fields") +fn object_fields( + value: dynamic.Dynamic, +) -> Result(Dict(String, dynamic.Dynamic), Dict(String, dynamic.Dynamic)) + fn non_empty_string() -> decode.Decoder(Option(String)) { decode.optional(decode.string) |> decode.map(fn(value) { diff --git a/test/openai_api/albedo_openai_json_test.erl b/test/openai_api/albedo_openai_json_test.erl index f6dce5c..8ec993c 100644 --- a/test/openai_api/albedo_openai_json_test.erl +++ b/test/openai_api/albedo_openai_json_test.erl @@ -7,3 +7,10 @@ semantically_empty_test_() -> || Value <- [null, nil, undefined, <<>>, [], {}, #{}]] ++ [?_assertEqual(false, albedo_openai_json:semantically_empty(Value)) || Value <- [false, true, 0, <<"x">>, [null], {null}, #{<<"id">> => null}]]. + +object_fields_test_() -> + [?_assertEqual({ok, Value}, albedo_openai_json:object_fields(Value)) + || Value <- [#{}, #{<<"content">> => <<"hello">>, <<"opaque">> => [1, #{}]}]] ++ + [?_assertEqual({error, #{}}, albedo_openai_json:object_fields(Value)) + || Value <- [null, [], 42, <<"x">>, #{content => <<"hello">>}, + #{<<"content">> => <<"hello">>, 1 => null}]]. diff --git a/test/openai_api/stream_test.gleam b/test/openai_api/stream_test.gleam index def48a2..6d4e7e3 100644 --- a/test/openai_api/stream_test.gleam +++ b/test/openai_api/stream_test.gleam @@ -215,6 +215,14 @@ pub fn chat_ignores_only_empty_unknown_fields_test() -> Nil { }) } +pub fn chat_rejects_non_object_deltas_test() -> Nil { + list.each(["null", "[]", "42", "\"x\"", "false"], fn(value) { + let data = "{\"choices\":[{\"index\":0,\"delta\":" <> value <> "}]}" + let assert Error(types.InvalidEvent(_)) = + send(stream.new(types.ChatCompletions), "", data) + }) +} + pub fn chat_requires_finish_and_suppresses_partial_tools_on_limits_test() -> Nil { assert send(stream.new(types.ChatCompletions), "", "[DONE]") == Error(types.InvalidEvent("[DONE] before chat finish reason")) -- knotmirror 0.0.1