Class: Legion::LLM::API::ClientTranslators::OpenAIResponses::Events

Inherits:
Object
  • Object
show all
Includes:
Legion::Logging::Helper
Defined in:
lib/legion/llm/api/client_translators/openai_responses.rb

Overview

SSE Events emitter for /v1/responses. Emits sequence_number’d events following the OpenAI Responses API spec.

Instance Method Summary collapse

Constructor Details

#initialize(out:, request_id:, model:, conv_id: nil) ⇒ Events

Returns a new instance of Events.



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 253

def initialize(out:, request_id:, model:, conv_id: nil)
  @out = out
  @request_id = request_id
  @model = model
  @conv_id = conv_id
  @seq = 0
  @output_items = []
  @msg_id = "msg_#{SecureRandom.hex(12)}"
  @msg_index = nil
  @msg_opened = false
  @full_text = +''
  @full_reasoning = +''
  @thinking_state = nil
  @pending_tool_calls = {} # block_index => { id, name, arguments_str, output_index }
  @created_at = Time.now.to_i
end

Instance Method Details

#on_done(stop_reason:, usage:, model:) ⇒ Object



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 427

def on_done(stop_reason:, usage:, model:)
  close_thinking_item

  if @msg_opened
    emit('response.output_text.done', {
           type: 'response.output_text.done', sequence_number: next_seq,
           output_index: @msg_index, content_index: 0, item_id: @msg_id, text: @full_text
         })
    emit('response.content_part.done', {
           type: 'response.content_part.done', sequence_number: next_seq,
           output_index: @msg_index, content_index: 0, item_id: @msg_id,
           part: { type: 'output_text', text: @full_text, annotations: [] }
         })
    completed_item = { id: @msg_id, type: 'message', role: 'assistant', status: 'completed',
                       content: [{ type: 'output_text', text: @full_text, annotations: [] }] }
    emit('response.output_item.done', {
           type: 'response.output_item.done', sequence_number: next_seq,
           output_index: @msg_index, item: completed_item
         })
    @output_items[@msg_index] = completed_item
  end

  has_tool_calls = @pending_tool_calls.any?
  status = stop_reason == :tool_use || has_tool_calls ? 'requires_action' : 'completed'
  event = status == 'requires_action' ? 'response.done' : 'response.completed'
  payload = {
    type: event, sequence_number: next_seq,
    response: {
      id: @request_id, object: 'response', created_at: @created_at,
      status: status, model: model.to_s,
      output: @output_items, usage: format_usage(usage)
    }
  }
  if status == 'requires_action'
    payload[:response][:action_required] = {
      type:           'function_calls',
      function_calls: @output_items.select { |i| i[:type] == 'function_call' }
    }
  end
  emit(event, payload)
end

#on_error(message:, type:, status_code:) ⇒ Object



469
470
471
472
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 469

def on_error(message:, type:, status_code:)
  _ = status_code
  emit('error', { type: 'error', message: message, code: type })
end

#on_keep_aliveObject



415
416
417
418
419
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 415

def on_keep_alive
  # SSE comment line — keeps the connection alive without issuing
  # a typed event.
  @out << ": keep-alive\n\n"
end

#on_message_delta(stop_reason:, output_tokens:) ⇒ Object



421
422
423
424
425
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 421

def on_message_delta(stop_reason:, output_tokens:)
  _ = stop_reason
  _ = output_tokens
  # Consolidated trailer happens in on_done.
end

#on_server_tool_result(block_index:, tool_call_id:, result_text:) ⇒ Object



406
407
408
409
410
411
412
413
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 406

def on_server_tool_result(block_index:, tool_call_id:, result_text:)
  _ = block_index
  _ = tool_call_id
  _ = result_text
  # /v1/responses doesn't expose server tool results inline today —
  # the function_call output item carries id/args; result is part
  # of the assistant message that follows on the next turn.
end

#on_start(model:, request_id:, input_tokens:) ⇒ Object



270
271
272
273
274
275
276
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 270

def on_start(model:, request_id:, input_tokens:)
  _ = input_tokens
  base = { id: request_id, object: 'response', created_at: @created_at,
           status: 'in_progress', model: model.to_s, output: [], usage: nil }
  emit('response.created',     { type: 'response.created',     sequence_number: next_seq, response: base })
  emit('response.in_progress', { type: 'response.in_progress', sequence_number: next_seq, response: base })
end

#on_text_close(block_index:) ⇒ Object



294
295
296
297
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 294

def on_text_close(block_index:)
  _ = block_index
  # Closure happens at on_done.
end

#on_text_delta(block_index:, text:) ⇒ Object



283
284
285
286
287
288
289
290
291
292
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 283

def on_text_delta(block_index:, text:)
  _ = block_index
  close_thinking_item
  open_message
  @full_text << text
  emit('response.output_text.delta', {
         type: 'response.output_text.delta', sequence_number: next_seq,
         output_index: @msg_index, content_index: 0, item_id: @msg_id, delta: text
       })
end

#on_text_open(block_index:) ⇒ Object



278
279
280
281
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 278

def on_text_open(block_index:)
  _ = block_index
  open_message
end

#on_thinking_close(block_index:, signature:) ⇒ Object



337
338
339
340
341
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 337

def on_thinking_close(block_index:, signature:)
  _ = block_index
  _ = signature
  close_thinking_item
end

#on_thinking_delta(block_index:, text:, signature:) ⇒ Object



323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 323

def on_thinking_delta(block_index:, text:, signature:)
  _ = block_index
  _ = signature
  return if text.to_s.empty? || @thinking_state.nil?

  @full_reasoning << text
  @thinking_state[:thinking] << text
  output_index = @output_items.index(@thinking_state)
  emit('response.thinking.delta', {
         type: 'response.thinking.delta', sequence_number: next_seq,
         output_index: output_index, item_id: @thinking_state[:id], delta: text
       })
end

#on_thinking_open(block_index:) ⇒ Object



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 299

def on_thinking_open(block_index:)
  _ = block_index
  return if @thinking_state

  state = {
    type:     'thinking',
    id:       "thnk_#{SecureRandom.hex(12)}",
    thinking: +'',
    status:   'in_progress'
  }
  @output_items << state
  @thinking_state = state
  output_index = @output_items.length - 1
  emit('response.output_item.added', {
         type: 'response.output_item.added', sequence_number: next_seq,
         output_index: output_index, item: state
       })
  emit('response.thinking_part.added', {
         type: 'response.thinking_part.added', sequence_number: next_seq,
         output_index: output_index, item_id: state[:id],
         part: { type: 'thinking', thinking: '' }
       })
end

#on_tool_call_abort(block_index:, reason:) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



402
403
404
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 402

def on_tool_call_abort(block_index:, reason:) # rubocop:disable Lint/UnusedMethodArgument
  nil
end

#on_tool_call_close(block_index:) ⇒ Object



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 384

def on_tool_call_close(block_index:)
  state = @pending_tool_calls[block_index]
  return if state.nil?

  emit('response.function_call_arguments.done', {
         type: 'response.function_call_arguments.done', sequence_number: next_seq,
         output_index: state[:output_index], item_id: state[:id],
         arguments: state[:arguments_str]
       })
  completed = { id: state[:id], type: 'function_call', name: state[:name],
                call_id: state[:id], arguments: state[:arguments_str], status: 'completed' }
  emit('response.output_item.done', {
         type: 'response.output_item.done', sequence_number: next_seq,
         output_index: state[:output_index], item: completed
       })
  @output_items[state[:output_index]] = completed
end

#on_tool_call_delta(block_index:, partial_arguments_json:) ⇒ Object



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 363

def on_tool_call_delta(block_index:, partial_arguments_json:)
  state = @pending_tool_calls[block_index]
  return if state.nil?

  # If the assembler hands us a cumulative string (buffered close)
  # only emit the diff vs already emitted.
  new_part = if partial_arguments_json.start_with?(state[:args_emitted])
               partial_arguments_json[state[:args_emitted].length..]
             else
               partial_arguments_json
             end
  return if new_part.to_s.empty?

  state[:args_emitted] << new_part
  state[:arguments_str] = state[:args_emitted]
  emit('response.function_call_arguments.delta', {
         type: 'response.function_call_arguments.delta', sequence_number: next_seq,
         output_index: state[:output_index], item_id: state[:id], delta: new_part
       })
end

#on_tool_call_open(block_index:, tool_call:, server_tool:) ⇒ Object



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/legion/llm/api/client_translators/openai_responses.rb', line 343

def on_tool_call_open(block_index:, tool_call:, server_tool:)
  _ = server_tool
  tc_id = tool_call[:id] || "call_#{SecureRandom.hex(12)}"
  idx = @output_items.length
  item = { id: tc_id, type: 'function_call', name: tool_call[:name].to_s,
           call_id: tc_id, arguments: '', status: 'in_progress' }
  @output_items << item
  @pending_tool_calls[block_index] = {
    id:            tc_id,
    name:          tool_call[:name].to_s,
    arguments_str: '',
    output_index:  idx,
    args_emitted:  +''
  }
  emit('response.output_item.added', {
         type: 'response.output_item.added', sequence_number: next_seq,
         output_index: idx, item: item
       })
end