Packages

A Gleam framework for building AI agents — type-safe, multi-provider, with tool calling, streaming, and simulation testing

Current section

Files

Jump to
glean src glean@providers@openai_compatible.erl
Raw

src/glean@providers@openai_compatible.erl

-module(glean@providers@openai_compatible).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glean/providers/openai_compatible.gleam").
-export([create/2]).
-export_type([stream_accumulator/0, streaming_tool_call/0, chunk_data/0, tool_call_delta/0, parsed_response/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(false).
-type stream_accumulator() :: {stream_accumulator,
gleam@string_tree:string_tree(),
list(streaming_tool_call()),
glean@error:finish_reason(),
glean@stream:usage()}.
-type streaming_tool_call() :: {streaming_tool_call,
integer(),
binary(),
gleam@string_tree:string_tree(),
gleam@string_tree:string_tree(),
boolean()}.
-type chunk_data() :: {chunk_data,
binary(),
binary(),
list(tool_call_delta()),
binary(),
glean@stream:usage()}.
-type tool_call_delta() :: {tool_call_delta,
integer(),
binary(),
binary(),
binary()}.
-type parsed_response() :: {parsed_response,
list(glean@provider:generated_content()),
glean@error:finish_reason(),
glean@stream:usage()}.
-file("src/glean/providers/openai_compatible.gleam", 233).
?DOC(false).
-spec openai_chunk_decoder() -> gleam@dynamic@decode:decoder(chunk_data()).
openai_chunk_decoder() ->
gleam@dynamic@decode:optional_field(
<<"id"/utf8>>,
<<""/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) ->
Tool_call_delta_decoder = begin
gleam@dynamic@decode:field(
<<"index"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Index) ->
gleam@dynamic@decode:optional_field(
<<"id"/utf8>>,
<<""/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id@1) ->
gleam@dynamic@decode:optional_field(
<<"function"/utf8>>,
<<""/utf8>>,
begin
gleam@dynamic@decode:optional_field(
<<"name"/utf8>>,
<<""/utf8>>,
{decoder,
fun gleam@dynamic@decode:decode_string/1},
fun(N) ->
gleam@dynamic@decode:success(N)
end
)
end,
fun(Name) ->
gleam@dynamic@decode:optional_field(
<<"function"/utf8>>,
<<""/utf8>>,
begin
gleam@dynamic@decode:optional_field(
<<"arguments"/utf8>>,
<<""/utf8>>,
{decoder,
fun gleam@dynamic@decode:decode_string/1},
fun(A) ->
gleam@dynamic@decode:success(
A
)
end
)
end,
fun(Args) ->
gleam@dynamic@decode:success(
{tool_call_delta,
Index,
Id@1,
Name,
Args}
)
end
)
end
)
end
)
end
)
end,
Choice_decoder = begin
gleam@dynamic@decode:optional_field(
<<"delta"/utf8>>,
{<<""/utf8>>, []},
begin
gleam@dynamic@decode:optional_field(
<<"content"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Content) ->
gleam@dynamic@decode:optional_field(
<<"tool_calls"/utf8>>,
[],
gleam@dynamic@decode:list(
Tool_call_delta_decoder
),
fun(Tool_calls) ->
gleam@dynamic@decode:success(
{gleam@option:unwrap(
Content,
<<""/utf8>>
),
Tool_calls}
)
end
)
end
)
end,
fun(Delta) ->
gleam@dynamic@decode:optional_field(
<<"finish_reason"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Finish_reason) ->
gleam@dynamic@decode:success(
{Delta,
gleam@option:unwrap(
Finish_reason,
<<""/utf8>>
)}
)
end
)
end
)
end,
gleam@dynamic@decode:optional_field(
<<"choices"/utf8>>,
[],
gleam@dynamic@decode:list(Choice_decoder),
fun(Choices) ->
gleam@dynamic@decode:optional_field(
<<"usage"/utf8>>,
{usage, 0, 0},
begin
gleam@dynamic@decode:optional_field(
<<"prompt_tokens"/utf8>>,
0,
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Input) ->
gleam@dynamic@decode:optional_field(
<<"completion_tokens"/utf8>>,
0,
{decoder,
fun gleam@dynamic@decode:decode_int/1},
fun(Output) ->
gleam@dynamic@decode:success(
{usage, Input, Output}
)
end
)
end
)
end,
fun(Usage) ->
{Text_delta, Tool_call_deltas, Finish_reason@1} = case Choices of
[{{Text, Tcs}, Fr} | _] ->
{Text, Tcs, Fr};
[] ->
{<<""/utf8>>, [], <<""/utf8>>}
end,
gleam@dynamic@decode:success(
{chunk_data,
Id,
Text_delta,
Tool_call_deltas,
Finish_reason@1,
Usage}
)
end
)
end
)
end
).
-file("src/glean/providers/openai_compatible.gleam", 355).
?DOC(false).
-spec find_tool_call(list(streaming_tool_call()), integer()) -> {ok,
streaming_tool_call()} |
{error, nil}.
find_tool_call(Calls, Index) ->
gleam@list:find(Calls, fun(Tc) -> erlang:element(2, Tc) =:= Index end).
-file("src/glean/providers/openai_compatible.gleam", 362).
?DOC(false).
-spec upsert_tool_call(list(streaming_tool_call()), streaming_tool_call()) -> list(streaming_tool_call()).
upsert_tool_call(Calls, Tc) ->
case gleam@list:any(
Calls,
fun(C) -> erlang:element(2, C) =:= erlang:element(2, Tc) end
) of
true ->
gleam@list:map(
Calls,
fun(C@1) ->
case erlang:element(2, C@1) =:= erlang:element(2, Tc) of
true ->
Tc;
false ->
C@1
end
end
);
false ->
lists:append(Calls, [Tc])
end.
-file("src/glean/providers/openai_compatible.gleam", 297).
?DOC(false).
-spec process_tool_call_delta(
stream_accumulator(),
tool_call_delta(),
binary(),
fun((glean@stream:stream_event()) -> nil)
) -> stream_accumulator().
process_tool_call_delta(Acc, Delta, Chunk_id, On_event) ->
{Tc, Is_new} = case find_tool_call(
erlang:element(3, Acc),
erlang:element(2, Delta)
) of
{ok, Existing} ->
{Existing, false};
{error, _} ->
{{streaming_tool_call,
erlang:element(2, Delta),
<<""/utf8>>,
gleam@string_tree:new(),
gleam@string_tree:new(),
false},
true}
end,
Tc@1 = case erlang:element(3, Delta) of
<<""/utf8>> ->
Tc;
Id ->
{streaming_tool_call,
erlang:element(2, Tc),
Id,
erlang:element(4, Tc),
erlang:element(5, Tc),
erlang:element(6, Tc)}
end,
Tc@2 = case erlang:element(4, Delta) of
<<""/utf8>> ->
Tc@1;
Name ->
{streaming_tool_call,
erlang:element(2, Tc@1),
erlang:element(3, Tc@1),
gleam@string_tree:append(erlang:element(4, Tc@1), Name),
erlang:element(5, Tc@1),
erlang:element(6, Tc@1)}
end,
Tc@3 = case Is_new andalso (erlang:element(3, Tc@2) /= <<""/utf8>>) of
true ->
On_event(
{tool_call_start,
Chunk_id,
erlang:element(3, Tc@2),
unicode:characters_to_binary(erlang:element(4, Tc@2))}
),
{streaming_tool_call,
erlang:element(2, Tc@2),
erlang:element(3, Tc@2),
erlang:element(4, Tc@2),
erlang:element(5, Tc@2),
true};
false ->
Tc@2
end,
Tc@4 = case erlang:element(5, Delta) of
<<""/utf8>> ->
Tc@3;
Args ->
On_event({tool_call_delta, Chunk_id, erlang:element(3, Tc@3), Args}),
{streaming_tool_call,
erlang:element(2, Tc@3),
erlang:element(3, Tc@3),
erlang:element(4, Tc@3),
gleam@string_tree:append(erlang:element(5, Tc@3), Args),
erlang:element(6, Tc@3)}
end,
Tool_calls = upsert_tool_call(erlang:element(3, Acc), Tc@4),
{stream_accumulator,
erlang:element(2, Acc),
Tool_calls,
erlang:element(4, Acc),
erlang:element(5, Acc)}.
-file("src/glean/providers/openai_compatible.gleam", 380).
?DOC(false).
-spec make_http_request(
glean@providers@config:provider_config(),
binary(),
binary(),
binary()
) -> {ok, binary()} | {error, glean@error:glean_error()}.
make_http_request(Config, Provider_name, Url, Body) ->
glean@providers@http_utils:send_post(
Config,
Provider_name,
Url,
Body,
[{<<"authorization"/utf8>>,
<<"Bearer "/utf8, (erlang:element(2, Config))/binary>>}]
).
-file("src/glean/providers/openai_compatible.gleam", 511).
?DOC(false).
-spec encode_user_content(glean@message:user_content()) -> gleam@json:json().
encode_user_content(Content) ->
case Content of
{user_text, Text} ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"text"/utf8>>)},
{<<"text"/utf8>>, gleam@json:string(Text)}]
);
{user_file, Data, Media_type} ->
case Data of
{base64_data, B64} ->
gleam@json:object(
[{<<"type"/utf8>>,
gleam@json:string(<<"image_url"/utf8>>)},
{<<"image_url"/utf8>>,
gleam@json:object(
[{<<"url"/utf8>>,
gleam@json:string(
<<<<<<"data:"/utf8,
Media_type/binary>>/binary,
";base64,"/utf8>>/binary,
B64/binary>>
)}]
)}]
);
{url_data, Url} ->
gleam@json:object(
[{<<"type"/utf8>>,
gleam@json:string(<<"image_url"/utf8>>)},
{<<"image_url"/utf8>>,
gleam@json:object(
[{<<"url"/utf8>>, gleam@json:string(Url)}]
)}]
)
end
end.
-file("src/glean/providers/openai_compatible.gleam", 547).
?DOC(false).
-spec encode_assistant_message(list(glean@message:assistant_content())) -> gleam@json:json().
encode_assistant_message(Content) ->
Text_parts = gleam@list:filter_map(Content, fun(C) -> case C of
{assistant_text, Text} ->
{ok, Text};
_ ->
{error, nil}
end end),
Tool_calls = gleam@list:filter_map(Content, fun(C@1) -> case C@1 of
{assistant_tool_call, Id, Name, Args} ->
{ok,
gleam@json:object(
[{<<"id"/utf8>>, gleam@json:string(Id)},
{<<"type"/utf8>>,
gleam@json:string(<<"function"/utf8>>)},
{<<"function"/utf8>>,
gleam@json:object(
[{<<"name"/utf8>>,
gleam@json:string(Name)},
{<<"arguments"/utf8>>,
gleam@json:string(Args)}]
)}]
)};
_ ->
{error, nil}
end end),
Text@1 = gleam@string:join(Text_parts, <<""/utf8>>),
Fields = [{<<"role"/utf8>>, gleam@json:string(<<"assistant"/utf8>>)}],
Fields@1 = case Text@1 of
<<""/utf8>> ->
case Tool_calls of
[] ->
lists:append(
Fields,
[{<<"content"/utf8>>, gleam@json:null()}]
);
_ ->
lists:append(
Fields,
[{<<"content"/utf8>>, gleam@json:null()}]
)
end;
T ->
lists:append(Fields, [{<<"content"/utf8>>, gleam@json:string(T)}])
end,
Fields@2 = case Tool_calls of
[] ->
Fields@1;
_ ->
lists:append(
Fields@1,
[{<<"tool_calls"/utf8>>,
gleam@json:preprocessed_array(Tool_calls)}]
)
end,
gleam@json:object(Fields@2).
-file("src/glean/providers/openai_compatible.gleam", 603).
?DOC(false).
-spec encode_tool_result(glean@message:tool_result()) -> gleam@json:json().
encode_tool_result(Result) ->
case Result of
{tool_result_ok, Tool_call_id, _, Output} ->
gleam@json:object(
[{<<"role"/utf8>>, gleam@json:string(<<"tool"/utf8>>)},
{<<"tool_call_id"/utf8>>, gleam@json:string(Tool_call_id)},
{<<"content"/utf8>>, gleam@json:string(Output)}]
);
{tool_result_error, Tool_call_id@1, _, Error_message} ->
gleam@json:object(
[{<<"role"/utf8>>, gleam@json:string(<<"tool"/utf8>>)},
{<<"tool_call_id"/utf8>>, gleam@json:string(Tool_call_id@1)},
{<<"content"/utf8>>,
gleam@json:string(
<<"Error: "/utf8, Error_message/binary>>
)}]
)
end.
-file("src/glean/providers/openai_compatible.gleam", 479).
?DOC(false).
-spec encode_message(glean@message:message()) -> list(gleam@json:json()).
encode_message(Msg) ->
case Msg of
{system_message, Content} ->
[gleam@json:object(
[{<<"role"/utf8>>, gleam@json:string(<<"system"/utf8>>)},
{<<"content"/utf8>>, gleam@json:string(Content)}]
)];
{user_message, Content@1} ->
case Content@1 of
[{user_text, Text}] ->
[gleam@json:object(
[{<<"role"/utf8>>,
gleam@json:string(<<"user"/utf8>>)},
{<<"content"/utf8>>, gleam@json:string(Text)}]
)];
_ ->
[gleam@json:object(
[{<<"role"/utf8>>,
gleam@json:string(<<"user"/utf8>>)},
{<<"content"/utf8>>,
gleam@json:preprocessed_array(
gleam@list:map(
Content@1,
fun encode_user_content/1
)
)}]
)]
end;
{assistant_message, Content@2} ->
[encode_assistant_message(Content@2)];
{tool_result_message, Results} ->
gleam@list:map(Results, fun encode_tool_result/1)
end.
-file("src/glean/providers/openai_compatible.gleam", 475).
?DOC(false).
-spec encode_messages(list(glean@message:message())) -> list(gleam@json:json()).
encode_messages(Messages) ->
gleam@list:flat_map(Messages, fun encode_message/1).
-file("src/glean/providers/openai_compatible.gleam", 620).
?DOC(false).
-spec encode_tools(list(glean@provider:tool_spec())) -> list(gleam@json:json()).
encode_tools(Tools) ->
gleam@list:map(
Tools,
fun(T) ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"function"/utf8>>)},
{<<"function"/utf8>>,
gleam@json:object(
[{<<"name"/utf8>>,
gleam@json:string(erlang:element(2, T))},
{<<"description"/utf8>>,
gleam@json:string(erlang:element(3, T))},
{<<"parameters"/utf8>>,
glean@schema:to_json(erlang:element(4, T))}]
)}]
)
end
).
-file("src/glean/providers/openai_compatible.gleam", 636).
?DOC(false).
-spec encode_tool_choice(glean@provider:tool_choice()) -> gleam@json:json().
encode_tool_choice(Choice) ->
case Choice of
auto ->
gleam@json:string(<<"auto"/utf8>>);
required ->
gleam@json:string(<<"required"/utf8>>);
no_tools ->
gleam@json:string(<<"none"/utf8>>);
{specific_tool, Name} ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"function"/utf8>>)},
{<<"function"/utf8>>,
gleam@json:object(
[{<<"name"/utf8>>, gleam@json:string(Name)}]
)}]
)
end.
-file("src/glean/providers/openai_compatible.gleam", 649).
?DOC(false).
-spec encode_response_format(
gleam@option:option(glean@schema:schema()),
gleam@option:option(binary())
) -> gleam@json:json().
encode_response_format(Schema_opt, Name_opt) ->
case Schema_opt of
none ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"json_object"/utf8>>)}]
);
{some, S} ->
Name = case Name_opt of
none ->
<<"response"/utf8>>;
{some, N} ->
N
end,
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"json_schema"/utf8>>)},
{<<"json_schema"/utf8>>,
gleam@json:object(
[{<<"name"/utf8>>, gleam@json:string(Name)},
{<<"schema"/utf8>>,
glean@schema:to_json_strict(S)},
{<<"strict"/utf8>>, gleam@json:bool(true)}]
)}]
)
end.
-file("src/glean/providers/openai_compatible.gleam", 397).
?DOC(false).
-spec encode_request(
glean@providers@config:provider_config(),
glean@provider:generate_request(),
boolean()
) -> binary().
encode_request(Config, Gen_request, Stream) ->
Messages_json = encode_messages(erlang:element(2, Gen_request)),
Tools_json = encode_tools(erlang:element(4, Gen_request)),
Fields = [{<<"model"/utf8>>, gleam@json:string(erlang:element(4, Config))},
{<<"messages"/utf8>>, gleam@json:preprocessed_array(Messages_json)}],
Fields@1 = case Tools_json of
[] ->
Fields;
_ ->
lists:append(
Fields,
[{<<"tools"/utf8>>, gleam@json:preprocessed_array(Tools_json)},
{<<"tool_choice"/utf8>>,
encode_tool_choice(erlang:element(5, Gen_request))}]
)
end,
Fields@2 = case erlang:element(2, erlang:element(3, Gen_request)) of
none ->
Fields@1;
{some, Max} ->
lists:append(
Fields@1,
[{erlang:element(7, Config), gleam@json:int(Max)}]
)
end,
Fields@3 = case erlang:element(3, erlang:element(3, Gen_request)) of
none ->
Fields@2;
{some, Temp} ->
lists:append(
Fields@2,
[{<<"temperature"/utf8>>, gleam@json:float(Temp)}]
)
end,
Fields@4 = case erlang:element(4, erlang:element(3, Gen_request)) of
none ->
Fields@3;
{some, P} ->
lists:append(Fields@3, [{<<"top_p"/utf8>>, gleam@json:float(P)}])
end,
Fields@5 = case erlang:element(7, erlang:element(3, Gen_request)) of
none ->
Fields@4;
{some, S} ->
lists:append(Fields@4, [{<<"seed"/utf8>>, gleam@json:int(S)}])
end,
Fields@6 = case erlang:element(8, erlang:element(3, Gen_request)) of
none ->
Fields@5;
{some, Pp} ->
lists:append(
Fields@5,
[{<<"presence_penalty"/utf8>>, gleam@json:float(Pp)}]
)
end,
Fields@7 = case erlang:element(9, erlang:element(3, Gen_request)) of
none ->
Fields@6;
{some, Fp} ->
lists:append(
Fields@6,
[{<<"frequency_penalty"/utf8>>, gleam@json:float(Fp)}]
)
end,
Fields@8 = case erlang:element(6, erlang:element(3, Gen_request)) of
none ->
Fields@7;
{some, Stops} ->
lists:append(
Fields@7,
[{<<"stop"/utf8>>,
gleam@json:array(Stops, fun gleam@json:string/1)}]
)
end,
Fields@9 = case Stream of
false ->
Fields@8;
true ->
lists:append(Fields@8, [{<<"stream"/utf8>>, gleam@json:bool(true)}])
end,
Fields@10 = case erlang:element(6, Gen_request) of
text_format ->
Fields@9;
{json_format, Schema_opt, Name_opt} ->
lists:append(
Fields@9,
[{<<"response_format"/utf8>>,
encode_response_format(Schema_opt, Name_opt)}]
)
end,
_pipe = gleam@json:object(Fields@10),
gleam@json:to_string(_pipe).
-file("src/glean/providers/openai_compatible.gleam", 775).
?DOC(false).
-spec parse_finish_reason(binary()) -> glean@error:finish_reason().
parse_finish_reason(Reason) ->
case Reason of
<<"stop"/utf8>> ->
stop;
<<"length"/utf8>> ->
length;
<<"tool_calls"/utf8>> ->
tool_calls;
<<"content_filter"/utf8>> ->
content_filter;
Other ->
{other, Other}
end.
-file("src/glean/providers/openai_compatible.gleam", 176).
?DOC(false).
-spec process_openai_chunk(
stream_accumulator(),
binary(),
fun((glean@stream:stream_event()) -> nil)
) -> stream_accumulator().
process_openai_chunk(Acc, Data, On_event) ->
Chunk_decoder = openai_chunk_decoder(),
case gleam@json:parse(Data, Chunk_decoder) of
{error, _} ->
Acc;
{ok, Chunk} ->
Acc@1 = case erlang:element(3, Chunk) of
<<""/utf8>> ->
Acc;
Delta ->
On_event({text_delta, erlang:element(2, Chunk), Delta}),
{stream_accumulator,
gleam@string_tree:append(erlang:element(2, Acc), Delta),
erlang:element(3, Acc),
erlang:element(4, Acc),
erlang:element(5, Acc)}
end,
Acc@2 = gleam@list:fold(
erlang:element(4, Chunk),
Acc@1,
fun(A, Tcd) ->
process_tool_call_delta(
A,
Tcd,
erlang:element(2, Chunk),
On_event
)
end
),
Acc@3 = case erlang:element(5, Chunk) of
<<""/utf8>> ->
Acc@2;
Fr ->
{stream_accumulator,
erlang:element(2, Acc@2),
erlang:element(3, Acc@2),
parse_finish_reason(Fr),
erlang:element(5, Acc@2)}
end,
case erlang:element(6, Chunk) of
{usage, 0, 0} ->
Acc@3;
U ->
{stream_accumulator,
erlang:element(2, Acc@3),
erlang:element(3, Acc@3),
erlang:element(4, Acc@3),
U}
end
end.
-file("src/glean/providers/openai_compatible.gleam", 142).
?DOC(false).
-spec openai_stream_loop(
gleam@erlang@process:subject(httpp@sse:s_s_e_event()),
gleam@erlang@process:subject(httpp@sse:s_s_e_manager_message()),
stream_accumulator(),
fun((glean@stream:stream_event()) -> nil),
integer()
) -> stream_accumulator().
openai_stream_loop(Subject, Manager, Acc, On_event, Timeout_ms) ->
case gleam@erlang@process:'receive'(Subject, Timeout_ms) of
{error, _} ->
gleam@erlang@process:send(Manager, shutdown),
Acc;
{ok, closed} ->
Acc;
{ok, {event, _, _, Data}} ->
case Data of
<<"[DONE]"/utf8>> ->
gleam@erlang@process:send(Manager, shutdown),
Acc;
_ ->
New_acc = process_openai_chunk(Acc, Data, On_event),
openai_stream_loop(
Subject,
Manager,
New_acc,
On_event,
Timeout_ms
)
end
end.
-file("src/glean/providers/openai_compatible.gleam", 77).
?DOC(false).
-spec do_stream(
glean@providers@config:provider_config(),
binary(),
glean@provider:generate_request(),
fun((glean@stream:stream_event()) -> nil)
) -> {ok, glean@provider:generate_result()} | {error, glean@error:glean_error()}.
do_stream(Config, Provider_name, Gen_request, On_event) ->
Body = encode_request(Config, Gen_request, true),
Url = <<(erlang:element(3, Config))/binary, "/chat/completions"/utf8>>,
Headers = [{<<"authorization"/utf8>>,
<<"Bearer "/utf8, (erlang:element(2, Config))/binary>>} |
gleam@list:map(
erlang:element(5, Config),
fun(H) -> {erlang:element(1, H), erlang:element(2, H)} end
)],
case glean@providers@sse:start_stream(
Url,
Headers,
Body,
erlang:element(6, Config)
) of
{error, Msg} ->
{error,
{provider_error,
Provider_name,
erlang:element(4, Config),
0,
<<"Streaming failed: "/utf8, Msg/binary>>}};
{ok, {Event_subject, Manager_subject}} ->
Acc = {stream_accumulator,
gleam@string_tree:new(),
[],
stop,
glean@stream:zero_usage()},
Final_acc = openai_stream_loop(
Event_subject,
Manager_subject,
Acc,
On_event,
erlang:element(6, Config)
),
Text_content = case unicode:characters_to_binary(
erlang:element(2, Final_acc)
) of
<<""/utf8>> ->
[];
T ->
[{generated_text, T}]
end,
Tool_content = gleam@list:map(
erlang:element(3, Final_acc),
fun(Tc) ->
{generated_tool_call,
erlang:element(3, Tc),
unicode:characters_to_binary(erlang:element(4, Tc)),
unicode:characters_to_binary(erlang:element(5, Tc))}
end
),
{ok,
{generate_result,
lists:append(Text_content, Tool_content),
erlang:element(4, Final_acc),
erlang:element(5, Final_acc),
erlang:element(4, Config)}}
end.
-file("src/glean/providers/openai_compatible.gleam", 707).
?DOC(false).
-spec openai_response_decoder() -> gleam@dynamic@decode:decoder(parsed_response()).
openai_response_decoder() ->
Usage_decoder = begin
gleam@dynamic@decode:field(
<<"prompt_tokens"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Input_tokens) ->
gleam@dynamic@decode:field(
<<"completion_tokens"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Output_tokens) ->
gleam@dynamic@decode:success(
{usage, Input_tokens, Output_tokens}
)
end
)
end
)
end,
Tool_call_decoder = begin
gleam@dynamic@decode:field(
<<"id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) ->
gleam@dynamic@decode:field(
<<"function"/utf8>>,
begin
gleam@dynamic@decode:field(
<<"name"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Name) ->
gleam@dynamic@decode:field(
<<"arguments"/utf8>>,
{decoder,
fun gleam@dynamic@decode:decode_string/1},
fun(Arguments) ->
gleam@dynamic@decode:success(
{Name, Arguments}
)
end
)
end
)
end,
fun(Function) ->
gleam@dynamic@decode:success(
{generated_tool_call,
Id,
erlang:element(1, Function),
erlang:element(2, Function)}
)
end
)
end
)
end,
Choice_decoder = begin
gleam@dynamic@decode:field(
<<"message"/utf8>>,
begin
gleam@dynamic@decode:optional_field(
<<"content"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
fun(Content) ->
gleam@dynamic@decode:optional_field(
<<"tool_calls"/utf8>>,
none,
gleam@dynamic@decode:optional(
gleam@dynamic@decode:list(Tool_call_decoder)
),
fun(Tool_calls) ->
gleam@dynamic@decode:success(
{Content, Tool_calls}
)
end
)
end
)
end,
fun(Message) ->
gleam@dynamic@decode:field(
<<"finish_reason"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Finish_reason) ->
gleam@dynamic@decode:success({Message, Finish_reason})
end
)
end
)
end,
gleam@dynamic@decode:field(
<<"choices"/utf8>>,
gleam@dynamic@decode:list(Choice_decoder),
fun(Choices) ->
gleam@dynamic@decode:field(
<<"usage"/utf8>>,
Usage_decoder,
fun(Usage) ->
{Content@1, Finish_reason@1} = case Choices of
[{Message@1, Fr} | _] ->
{Text_opt, Tool_calls_opt} = Message@1,
Text_content = case Text_opt of
{some, Text} when Text =/= <<""/utf8>> ->
[{generated_text, Text}];
_ ->
[]
end,
Tool_content = case Tool_calls_opt of
{some, Calls} ->
Calls;
none ->
[]
end,
{lists:append(Text_content, Tool_content),
parse_finish_reason(Fr)};
[] ->
{[], stop}
end,
gleam@dynamic@decode:success(
{parsed_response, Content@1, Finish_reason@1, Usage}
)
end
)
end
).
-file("src/glean/providers/openai_compatible.gleam", 677).
?DOC(false).
-spec decode_response(binary(), binary()) -> {ok,
glean@provider:generate_result()} |
{error, glean@error:glean_error()}.
decode_response(Model_id, Body) ->
Decoder = openai_response_decoder(),
case gleam@json:parse(Body, Decoder) of
{error, _} ->
{error,
{json_parse_error,
Body,
<<"Failed to decode OpenAI response"/utf8>>}};
{ok, Parsed} ->
{ok,
{generate_result,
erlang:element(2, Parsed),
erlang:element(3, Parsed),
erlang:element(4, Parsed),
Model_id}}
end.
-file("src/glean/providers/openai_compatible.gleam", 41).
?DOC(false).
-spec do_generate(
glean@providers@config:provider_config(),
binary(),
glean@provider:generate_request()
) -> {ok, glean@provider:generate_result()} | {error, glean@error:glean_error()}.
do_generate(Config, Provider_name, Gen_request) ->
Body = encode_request(Config, Gen_request, false),
Url = <<(erlang:element(3, Config))/binary, "/chat/completions"/utf8>>,
case make_http_request(Config, Provider_name, Url, Body) of
{error, Err} ->
{error, Err};
{ok, Response_body} ->
decode_response(erlang:element(4, Config), Response_body)
end.
-file("src/glean/providers/openai_compatible.gleam", 25).
?DOC(false).
-spec create(binary(), glean@providers@config:provider_config()) -> glean@provider:provider().
create(Provider_name, Config) ->
{provider,
Provider_name,
erlang:element(4, Config),
fun(Request) -> do_generate(Config, Provider_name, Request) end,
fun(Request@1, On_event) ->
do_stream(Config, Provider_name, Request@1, On_event)
end}.