# frozen_string_literal: true module DiscourseAi module Completions module Dialects class ChatGpt < Dialect class << self def can_translate?(llm_model) llm_model.provider == "open_router" || llm_model.provider == "open_ai" || llm_model.provider == "azure" end end VALID_ID_REGEX = /\A[a-zA-Z0-9_]+\z/ def native_tool_support? llm_model.provider == "open_ai" || llm_model.provider == "azure" end def embed_user_ids? return @embed_user_ids if defined?(@embed_user_ids) @embed_user_ids = prompt.messages.any? do |m| m[:id] && m[:type] == :user && !m[:id].to_s.match?(VALID_ID_REGEX) end end def max_prompt_tokens # provide a buffer of 120 tokens - our function counting is not # 100% accurate and getting numbers to align exactly is very hard buffer = (opts[:max_tokens] || 2500) + 50 if tools.present? # note this is about 100 tokens over, OpenAI have a more optimal representation @function_size ||= llm_model.tokenizer_class.size(tools.to_json.to_s) buffer += @function_size end llm_model.max_prompt_tokens - buffer end def disable_native_tools? return @disable_native_tools if defined?(@disable_native_tools) !!@disable_native_tools = llm_model.lookup_custom_param("disable_native_tools") end private def tools_dialect if disable_native_tools? super else @tools_dialect ||= DiscourseAi::Completions::Dialects::OpenAiTools.new(prompt.tools) end end # developer messages are preferred on recent reasoning models def supports_developer_messages? !legacy_reasoning_model? && llm_model.provider == "open_ai" && (llm_model.name.start_with?("o1") || llm_model.name.start_with?("o3")) end def legacy_reasoning_model? llm_model.provider == "open_ai" && (llm_model.name.start_with?("o1-preview") || llm_model.name.start_with?("o1-mini")) end def system_msg(msg) content = msg[:content] if disable_native_tools? && tools_dialect.instructions.present? content = content + "\n\n" + tools_dialect.instructions end if supports_developer_messages? { role: "developer", content: content } elsif legacy_reasoning_model? { role: "user", content: content } else { role: "system", content: content } end end def model_msg(msg) { role: "assistant", content: msg[:content] } end def tool_call_msg(msg) if disable_native_tools? super else tools_dialect.from_raw_tool_call(msg) end end def tool_msg(msg) if disable_native_tools? super else tools_dialect.from_raw_tool(msg) end end def user_msg(msg) content_array = [] user_message = { role: "user" } if msg[:id] if embed_user_ids? content_array << "#{msg[:id]}: " else user_message[:name] = msg[:id] end end content_array << msg[:content] content_array = to_encoded_content_array( content: content_array.flatten, image_encoder: ->(details) { image_node(details) }, text_encoder: ->(text) { { type: "text", text: text } }, allow_vision: vision_support?, ) user_message[:content] = no_array_if_only_text(content_array) user_message end def no_array_if_only_text(content_array) if content_array.size == 1 && content_array.first[:type] == "text" content_array.first[:text] else content_array end end def image_node(details) { type: "image_url", image_url: { url: "data:#{details[:mime_type]};base64,#{details[:base64]}", }, } end def per_message_overhead # open ai defines about 4 tokens per message of overhead 4 end def calculate_message_token(context) llm_model.tokenizer_class.size(context[:content].to_s + context[:name].to_s) end end end end end