Source file stream.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
type tool_call_delta = {
  index : int;
  id : string option;
  name : string option;
  arguments_delta : string;
  raw : Chatoyant_runtime.Json.t option;
}

type frame = {
  content_delta : string option;
  reasoning_delta : string option;
  tool_call_deltas : tool_call_delta list;
  usage : Chatoyant_tokens.Cost.usage option;
  usage_source : Chatoyant_tokens.Cost.source;
  finish_reason : string option;
  raw : Chatoyant_runtime.Json.t option;
}

type partial_tool_call = {
  index : int;
  id : string option;
  name : string option;
  arguments_buffer : string;
  raw : Chatoyant_runtime.Json.t option;
}

type state = {
  stream_content : string;
  stream_reasoning_content : string;
  stream_tool_calls : partial_tool_call list;
  stream_usage : Chatoyant_tokens.Cost.usage;
  stream_usage_source : Chatoyant_tokens.Cost.source;
  stream_finish_reason : string option;
  first_token_ms : int option;
  raw_frames : Chatoyant_runtime.Json.t list;
}

let empty =
  {
    stream_content = "";
    stream_reasoning_content = "";
    stream_tool_calls = [];
    stream_usage = Chatoyant_tokens.Cost.empty_usage;
    stream_usage_source = Chatoyant_tokens.Cost.Unknown;
    stream_finish_reason = None;
    first_token_ms = None;
    raw_frames = [];
  }

let merge_opt incoming current =
  match incoming with Some _ -> incoming | None -> current

let apply_tool_delta tools (delta : tool_call_delta) =
  let rec loop acc = function
    | [] ->
        List.rev
          ({
             index = delta.index;
             id = delta.id;
             name = delta.name;
             arguments_buffer = delta.arguments_delta;
             raw = delta.raw;
           }
          :: acc)
    | tool :: rest when tool.index = delta.index ->
        List.rev_append acc
          ({
             index = tool.index;
             id = merge_opt delta.id tool.id;
             name = merge_opt delta.name tool.name;
             arguments_buffer = tool.arguments_buffer ^ delta.arguments_delta;
             raw = merge_opt delta.raw tool.raw;
           }
          :: rest)
    | tool :: rest -> loop (tool :: acc) rest
  in
  loop [] tools

let apply state (frame : frame) =
  let state =
    {
      state with
      stream_content =
        state.stream_content ^ Option.value frame.content_delta ~default:"";
      stream_reasoning_content =
        state.stream_reasoning_content
        ^ Option.value frame.reasoning_delta ~default:"";
      stream_tool_calls =
        List.fold_left apply_tool_delta state.stream_tool_calls
          frame.tool_call_deltas;
      stream_finish_reason =
        merge_opt frame.finish_reason state.stream_finish_reason;
      raw_frames =
        (match frame.raw with
        | None -> state.raw_frames
        | Some raw -> raw :: state.raw_frames);
    }
  in
  match frame.usage with
  | None -> state
  | Some usage ->
      {
        state with
        stream_usage = Chatoyant_tokens.Cost.normalize_total usage;
        stream_usage_source = frame.usage_source;
      }

let note_first_token ~now_ms state =
  match state.first_token_ms with
  | Some _ -> state
  | None -> { state with first_token_ms = Some now_ms }

let content state = state.stream_content
let reasoning_content state = state.stream_reasoning_content
let usage state = state.stream_usage
let usage_source state = state.stream_usage_source
let finish_reason state = state.stream_finish_reason

let provider_tool_call_of_partial tool =
  let arguments =
    match Chatoyant_runtime.Json.parse tool.arguments_buffer with
    | Ok json -> json
    | Error _ -> Chatoyant_runtime.Json.Null
  in
  {
    Chatoyant_provider.Provider.id = Option.value tool.id ~default:"";
    name = Option.value tool.name ~default:"";
    arguments;
    arguments_json = tool.arguments_buffer;
    raw = tool.raw;
  }

let tool_calls state =
  state.stream_tool_calls
  |> List.sort (fun left right -> compare left.index right.index)
  |> List.map provider_tool_call_of_partial

let to_generation ~provider ~model ~started_ms ~finished_ms state =
  let latency_ms = max 0 (finished_ms - started_ms) in
  let timing =
    {
      Result.latency_ms;
      time_to_first_token_ms =
        Option.map
          (fun first -> max 0 (first - started_ms))
          state.first_token_ms;
    }
  in
  let usage = Chatoyant_tokens.Cost.normalize_total state.stream_usage in
  let cost_result =
    Chatoyant_tokens.Cost.calculate
      ~pricing:(Chatoyant_tokens.Pricing.get model)
      usage
  in
  {
    Result.content = state.stream_content;
    reasoning_content = state.stream_reasoning_content;
    usage;
    usage_source = state.stream_usage_source;
    timing;
    token_speed = Result.token_speed ~latency_ms usage;
    cost =
      { estimated_usd = cost_result.total; actual_usd = cost_result.actual_usd };
    provider;
    model;
    tool_calls = tool_calls state;
    finish_reason = state.stream_finish_reason;
    cached = usage.cached_tokens > 0;
    iterations = 1;
  }

let string value = Chatoyant_runtime.Json.String value
let int value = Chatoyant_runtime.Json.Float (Float.of_int value)

let add_opt name value fields =
  match value with None -> fields | Some value -> (name, value) :: fields

let tool_call_delta_to_json (delta : tool_call_delta) =
  [
    ("index", int delta.index); ("arguments_delta", string delta.arguments_delta);
  ]
  |> add_opt "id" (Option.map string delta.id)
  |> add_opt "name" (Option.map string delta.name)
  |> add_opt "raw" delta.raw |> List.rev
  |> fun fields -> Chatoyant_runtime.Json.Object fields

let frame_to_json (frame : frame) =
  [
    ( "tool_call_deltas",
      Chatoyant_runtime.Json.Array
        (List.map tool_call_delta_to_json frame.tool_call_deltas) );
    ( "usage_source",
      string (Chatoyant_tokens.Cost.source_to_string frame.usage_source) );
  ]
  |> add_opt "content_delta" (Option.map string frame.content_delta)
  |> add_opt "reasoning_delta" (Option.map string frame.reasoning_delta)
  |> add_opt "usage"
       (Option.map Chatoyant_tokens.Cost.usage_to_json frame.usage)
  |> add_opt "finish_reason" (Option.map string frame.finish_reason)
  |> add_opt "raw" frame.raw |> List.rev
  |> fun fields -> Chatoyant_runtime.Json.Object fields

let state_to_json (state : state) =
  [
    ("content", string state.stream_content);
    ("reasoning_content", string state.stream_reasoning_content);
    ( "tool_calls",
      Chatoyant_runtime.Json.Array
        (List.map Chatoyant_provider.Provider.tool_call_to_json
           (tool_calls state)) );
    ("usage", Chatoyant_tokens.Cost.usage_to_json state.stream_usage);
    ( "usage_source",
      string (Chatoyant_tokens.Cost.source_to_string state.stream_usage_source)
    );
    ("raw_frames", Chatoyant_runtime.Json.Array (List.rev state.raw_frames));
  ]
  |> add_opt "finish_reason" (Option.map string state.stream_finish_reason)
  |> add_opt "first_token_ms" (Option.map int state.first_token_ms)
  |> List.rev
  |> fun fields -> Chatoyant_runtime.Json.Object fields