From 0ecfce5c27cf8095c2837c28ad42acc8c4fd7b12 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Wed, 22 Apr 2026 22:47:09 -0300 Subject: [PATCH] fix(captain): translate response_format to text.format on Codex proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sem isso o Codex devolvia texto puro e o reaction_emoji do JSON estruturado nunca chegava ao ResponseBuilderJob — quebrava a ferramenta de reagir mensagens com emoji. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/services/captain/codex/translator.rb | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/enterprise/app/services/captain/codex/translator.rb b/enterprise/app/services/captain/codex/translator.rb index b83f68bf5..67437aebf 100644 --- a/enterprise/app/services/captain/codex/translator.rb +++ b/enterprise/app/services/captain/codex/translator.rb @@ -18,9 +18,14 @@ class Captain::Codex::Translator # chat_body: hash no formato OpenAI Chat Completions. # Retorna hash pro POST /responses. def self.chat_to_responses(chat_body) - messages = chat_body['messages'] || chat_body[:messages] || [] - instructions, input = split_system(messages) + instructions, input = split_system(chat_body['messages'] || chat_body[:messages] || []) + body = base_body(chat_body, instructions, input) + apply_tools!(body, chat_body) + apply_response_format!(body, chat_body['response_format'] || chat_body[:response_format]) + body + end + def self.base_body(chat_body, instructions, input) body = { model: override_model(chat_body['model'] || chat_body[:model]), input: input, @@ -31,16 +36,43 @@ class Captain::Codex::Translator stream: true # Codex exige streaming sempre — o Client agrega. } body[:max_output_tokens] = chat_body['max_tokens'] if chat_body['max_tokens'] - - tools = chat_body['tools'] || chat_body[:tools] - if tools.present? - body[:tools] = tools.map { |t| translate_tool(t) } - body[:tool_choice] = translate_tool_choice(chat_body['tool_choice'] || chat_body[:tool_choice]) - end - body end + def self.apply_tools!(body, chat_body) + tools = chat_body['tools'] || chat_body[:tools] + return if tools.blank? + + body[:tools] = tools.map { |t| translate_tool(t) } + body[:tool_choice] = translate_tool_choice(chat_body['tool_choice'] || chat_body[:tool_choice]) + end + + def self.apply_response_format!(body, response_format) + text_format = translate_response_format(response_format) + body[:text] = { format: text_format } if text_format + end + + # Chat Completions: { type: 'json_schema', json_schema: { name, schema, strict } } + # Responses API: { type: 'json_schema', name, schema, strict } (sem wrapper json_schema) + # Também aceita { type: 'json_object' } (sem schema). + def self.translate_response_format(format) + return nil if format.blank? + + format = format.stringify_keys + case format['type'] + when 'json_schema' + js = (format['json_schema'] || {}).stringify_keys + { + type: 'json_schema', + name: js['name'] || 'response', + schema: js['schema'] || {}, + strict: js.fetch('strict', true) + } + when 'json_object' + { type: 'json_object' } + end + end + # Separa a(s) mensagem(ns) system do resto. # Várias system messages viram uma única instruction com \n\n. def self.split_system(messages)