okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petopenai_api: fast paths for chat tool fragments and claude deltas, requests wait for a draining connection, parallel and tool-call benchmark6b24755

main
8.6 KiB242 linesraw
1-module(albedo_openai_transport_test_server).
2
3-include_lib("public_key/include/public_key.hrl").
4
5-export([start/1, start_tls/1, url/1, trust/1, distrust/0, await_body/1, await_closed/1,
6    stop/1, owner_after_headers/1, kill_owner/1]).
7
8start(Mode) ->
9    {ok, Listen} = gen_tcp:listen(0, [binary, {active, false},
10        {reuseaddr, true}, {packet, raw}]),
11    {ok, {_Address, Port}} = inet:sockname(Listen),
12    Owner = self(),
13    Pid = spawn(fun() -> serve({gen_tcp, Listen}, Owner, Mode) end),
14    URL = iolist_to_binary(io_lib:format("http://127.0.0.1:~B/v1/chat?fixture=yes", [Port])),
15    {fixture, Pid, URL, none}.
16
17%% A TLS server for localhost under a fresh test CA, which no system trusts
18%% until trust/1 loads it.
19start_tls(Mode) ->
20    {ok, _} = application:ensure_all_started(ssl),
21    Name = #'Extension'{extnID = ?'id-ce-subjectAltName', critical = false,
22                        extnValue = [{dNSName, "localhost"}]},
23    #{server_config := Server, client_config := Client} = public_key:pkix_test_data(#{
24        server_chain => #{root => key(), intermediates => [], peer => [{extensions, [Name]} | key()]},
25        client_chain => #{root => key(), intermediates => [], peer => key()}}),
26    CaFile = filename:join(os:getenv("TMPDIR", "/tmp"),
27        "albedo-test-ca-" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".pem"),
28    Roots = [{'Certificate', Der, not_encrypted} || Der <- proplists:get_value(cacerts, Client)],
29    ok = file:write_file(CaFile, public_key:pem_encode(Roots)),
30    {ok, Listen} = ssl:listen(0, [binary, {active, false}, {reuseaddr, true} | Server]),
31    {ok, {_Address, Port}} = ssl:sockname(Listen),
32    Owner = self(),
33    Pid = spawn(fun() -> serve({ssl, Listen}, Owner, Mode) end),
34    URL = iolist_to_binary(io_lib:format("https://localhost:~B/v1/chat", [Port])),
35    {fixture, Pid, URL, CaFile}.
36
37key() -> [{key, {namedCurve, ?secp256r1}}, {digest, sha256}].
38
39url({fixture, _Pid, URL, _}) -> URL.
40
41trust({fixture, _Pid, _URL, CaFile}) ->
42    ok = public_key:cacerts_load(CaFile),
43    nil.
44
45distrust() ->
46    public_key:cacerts_clear(),
47    nil.
48
49await_body({fixture, Pid, _URL, _}) ->
50    receive
51        {fixture_body, Pid, Body} -> Body
52    after 2000 ->
53        timeout
54    end.
55
56await_closed({fixture, Pid, _URL, _}) ->
57    receive
58        {fixture_closed, Pid} -> true;
59        {fixture_error, Pid, _} -> false
60    after 2000 ->
61        false
62    end.
63
64stop({fixture, Pid, _URL, CaFile}) ->
65    exit(Pid, shutdown),
66    _ = is_list(CaFile) andalso file:delete(CaFile),
67    nil.
68
69owner_after_headers(Fixture) ->
70    Parent = self(),
71    {Owner, Monitor} = spawn_monitor(fun() ->
72        {ok, Connection} = albedo_openai_transport:open(url(Fixture), [], <<>>, 2000),
73        {ok, {headers, 200, _, false}} =
74            albedo_openai_transport:receive_message(Connection),
75        Parent ! {transport_owner_ready, self()},
76        receive stop -> albedo_openai_transport:close(Connection) end
77    end),
78    receive
79        {transport_owner_ready, Owner} ->
80            demonitor(Monitor, [flush]),
81            Owner;
82        {'DOWN', Monitor, process, Owner, Reason} ->
83            error({transport_owner_failed, Reason})
84    after 2000 ->
85        exit(Owner, kill),
86        error(transport_owner_not_ready)
87    end.
88
89kill_owner(Owner) ->
90    Monitor = monitor(process, Owner),
91    exit(Owner, kill),
92    receive
93        {'DOWN', Monitor, process, Owner, killed} -> true;
94        {'DOWN', Monitor, process, Owner, _} -> false
95    after 2000 ->
96        demonitor(Monitor, [flush]),
97        false
98    end.
99
100%% Every mode but drop_second takes one connection and closes the listener,
101%% so a second request can only arrive on a kept-alive connection.
102serve(Listen, Owner, drop_second) ->
103    case accept(Listen) of
104        {ok, First} ->
105            {ok, Body} = read_request(First, <<>>),
106            Owner ! {fixture_body, self(), Body},
107            ok = send(First, length_response()),
108            {ok, _} = read_request(First, <<>>),
109            close(First),
110            serve(Listen, Owner, keep_alive);
111        _ -> close(Listen)
112    end;
113serve(Listen, Owner, Mode) ->
114    case accept(Listen) of
115        {ok, Socket} ->
116            close(Listen),
117            case read_request(Socket, <<>>) of
118                {ok, Body} ->
119                    Owner ! {fixture_body, self(), Body},
120                    respond(Socket, Mode, Owner);
121                {error, Reason} ->
122                    Owner ! {fixture_error, self(), Reason}
123            end;
124        _ ->
125            close(Listen)
126    end.
127
128accept({gen_tcp, Listen}) ->
129    case gen_tcp:accept(Listen, 2000) of
130        {ok, Socket} -> {ok, {gen_tcp, Socket}};
131        Error -> Error
132    end;
133accept({ssl, Listen}) ->
134    case ssl:transport_accept(Listen, 2000) of
135        {ok, Socket} ->
136            case ssl:handshake(Socket, 2000) of
137                {ok, Tls} -> {ok, {ssl, Tls}};
138                Error -> Error
139            end;
140        Error -> Error
141    end.
142
143send({Module, Socket}, Data) -> Module:send(Socket, Data).
144recv({Module, Socket}, Length) -> Module:recv(Socket, Length, 2000).
145close({Module, Socket}) -> Module:close(Socket).
146
147read_request(Socket, Acc) ->
148    case binary:match(Acc, <<"\r\n\r\n">>) of
149        {HeaderEnd, 4} ->
150            <<Headers:HeaderEnd/binary, _Separator:4/binary, Rest/binary>> = Acc,
151            Length = content_length(Headers),
152            read_body(Socket, Rest, Length);
153        nomatch ->
154            case recv(Socket, 0) of
155                {ok, Bytes} -> read_request(Socket, <<Acc/binary, Bytes/binary>>);
156                Error -> Error
157            end
158    end.
159
160content_length(Headers) ->
161    Lines = binary:split(Headers, <<"\r\n">>, [global]),
162    content_length_lines(Lines).
163
164content_length_lines([]) -> 0;
165content_length_lines([Line | Rest]) ->
166    case binary:split(Line, <<":">>) of
167        [Name, Value] ->
168            case string:lowercase(string:trim(Name)) of
169                <<"content-length">> -> binary_to_integer(string:trim(Value));
170                _ -> content_length_lines(Rest)
171            end;
172        _ -> content_length_lines(Rest)
173    end.
174
175read_body(_Socket, Body, Length) when byte_size(Body) >= Length ->
176    {ok, binary:part(Body, 0, Length)};
177read_body(Socket, Body, Length) ->
178    case recv(Socket, Length - byte_size(Body)) of
179        {ok, Bytes} -> read_body(Socket, <<Body/binary, Bytes/binary>>, Length);
180        Error -> Error
181    end.
182
183length_response() ->
184    <<"HTTP/1.1 200 OK\r\ncontent-length: 16\r\n\r\nhello world, all">>.
185
186respond(Socket, stream, Owner) ->
187    ok = send(Socket, [
188        <<"HTTP/1.1 200 OK\r\n">>,
189        <<"content-type: application/octet-stream\r\n">>,
190        <<"transfer-encoding: chunked\r\n\r\n">>,
191        <<"3\r\none\r\n">>
192    ]),
193    receive after 40 -> ok end,
194    ok = send(Socket, <<"3\r\ntwo\r\n0\r\n\r\n">>),
195    wait_for_close(Socket, Owner);
196respond({gen_tcp, Port} = Socket, chunked_trickle, Owner) ->
197    ok = inet:setopts(Port, [{nodelay, true}]),
198    Response = <<"HTTP/1.1 100 Continue\r\n\r\n",
199        "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\n",
200        "Transfer-Encoding: chunked\r\n\r\n",
201        "5;name=value\r\nhello\r\n1\r\n \r\nA\r\nworld, all\r\n",
202        "0\r\nx-trailer: yes\r\n\r\n">>,
203    [begin ok = send(Socket, <<Byte>>), receive after 1 -> ok end end
204     || <<Byte>> <= Response],
205    wait_for_close(Socket, Owner);
206respond(Socket, content_length, Owner) ->
207    ok = send(Socket, length_response()),
208    wait_for_close(Socket, Owner);
209respond(Socket, keep_alive, Owner) ->
210    ok = send(Socket, length_response()),
211    next_request(Socket, keep_alive, Owner);
212respond(Socket, slow_end, Owner) ->
213    ok = send(Socket, <<"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n10\r\nhello world, all\r\n">>),
214    receive after 10 -> ok end,
215    case send(Socket, <<"0\r\n\r\n">>) of
216        ok -> next_request(Socket, slow_end, Owner);
217        {error, closed} -> Owner ! {fixture_closed, self()}
218    end;
219respond(Socket, close_delimited, Owner) ->
220    ok = send(Socket, [
221        <<"HTTP/1.0 200 OK\r\n">>,
222        <<"content-type: application/octet-stream\r\n\r\n">>
223    ]),
224    wait_for_close(Socket, Owner).
225
226next_request(Socket, Mode, Owner) ->
227    case read_request(Socket, <<>>) of
228        {ok, Body} ->
229            Owner ! {fixture_body, self(), Body},
230            respond(Socket, Mode, Owner);
231        {error, closed} -> Owner ! {fixture_closed, self()};
232        {error, Reason} -> Owner ! {fixture_error, self(), Reason}
233    end.
234
235wait_for_close(Socket, Owner) ->
236    case recv(Socket, 0) of
237        {error, closed} -> Owner ! {fixture_closed, self()};
238        {error, timeout} -> wait_for_close(Socket, Owner);
239        {error, Reason} -> Owner ! {fixture_error, self(), Reason};
240        {ok, _} -> wait_for_close(Socket, Owner)
241    end,
242    close(Socket).