fix: Preserve Captain LLM defaults (#14858)

# Pull Request Template

## Description

Adjusts the Captain LLM feature defaults after feature routing so the
defaults stay intentional and avoid unintended high-cost model upgrades.
Assistant, copilot, and onboarding content generation now default to
`gpt-4.1`; audio transcription keeps `gpt-4o-mini-transcribe` as the
default while exposing `whisper-1` as an available account override
option.

Related: https://linear.app/chatwoot/issue/CW-7425/test-new-models

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- `ruby -ryaml -e 'yaml = YAML.load_file("config/llm.yml"); features =
yaml.fetch("features"); models = yaml.fetch("models"); features.each {
|name, cfg| missing = Array(cfg["models"]) - models.keys; raise
"#{name}: missing #{missing.join(",")}" if missing.any?; raise "#{name}:
default not in models" unless
Array(cfg["models"]).include?(cfg["default"]) }'`
- `node -e
"JSON.parse(require('fs').readFileSync('app/javascript/dashboard/i18n/locale/en/settings.json',
'utf8'))"`
- `pnpm exec prettier --check
app/javascript/dashboard/i18n/locale/en/settings.json`
- `bundle exec rspec spec/lib/llm/feature_router_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/models/concerns/agentable_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb
spec/models/concerns/captain_featurable_spec.rb
spec/models/account_spec.rb
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/controllers/super_admin/accounts_controller_spec.rb`
- `bundle exec rspec
spec/enterprise/services/captain/llm/article_translation_service_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb`
- `RUBOCOP_CACHE_ROOT=/private/tmp/rubocop_cache bundle exec rubocop
enterprise/app/services/captain/llm/article_translation_service.rb
enterprise/app/services/messages/audio_transcription_service.rb
spec/enterprise/services/captain/llm/article_translation_service_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb`
- `git diff --check`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Sony Mathew
2026-06-25 21:35:09 +05:30
committed by GitHub
parent b8b62ad0f1
commit cf134deb37
7 changed files with 37 additions and 15 deletions
@@ -440,7 +440,9 @@
"DESCRIPTION": "Enable or disable AI-powered features.",
"AUDIO_TRANSCRIPTION": {
"TITLE": "Audio Transcription",
"DESCRIPTION": "Automatically convert voice messages and call recordings into searchable text transcripts."
"DESCRIPTION": "Automatically convert voice messages and call recordings into searchable text transcripts.",
"MODEL_TITLE": "Audio Transcription Model",
"MODEL_DESCRIPTION": "Select the AI model to use for converting audio messages into text transcripts"
},
"HELP_CENTER_SEARCH": {
"TITLE": "Help Center Search Indexing",
+11 -5
View File
@@ -59,6 +59,10 @@ models:
provider: openai
display_name: 'Whisper'
credit_multiplier: 1
gpt-4o-mini-transcribe:
provider: openai
display_name: 'GPT-4o Mini Transcribe'
credit_multiplier: 1
text-embedding-3-small:
provider: openai
display_name: 'Text Embedding 3 Small'
@@ -82,6 +86,7 @@ features:
assistant:
models:
[
gpt-4.1-mini,
gpt-5-mini,
gpt-4.1,
gpt-5.1,
@@ -91,10 +96,11 @@ features:
gemini-3-flash,
gemini-3-pro,
]
default: gpt-5.1
default: gpt-4.1
copilot:
models:
[
gpt-4.1-mini,
gpt-5-mini,
gpt-4.1,
gpt-5.1,
@@ -104,11 +110,11 @@ features:
gemini-3-flash,
gemini-3-pro,
]
default: gpt-5.1
default: gpt-4.1
label_suggestion:
models:
[gpt-4.1-nano, gpt-4.1-mini, gpt-5-mini, gemini-3-flash, claude-haiku-4.5]
default: gpt-4.1-nano
default: gpt-4.1-mini
document_faq_generation:
models:
[
@@ -148,8 +154,8 @@ features:
models: [gpt-4.1-nano, gpt-4.1-mini, gpt-5-mini]
default: gpt-4.1-nano
audio_transcription:
models: [whisper-1]
default: whisper-1
models: [gpt-4o-mini-transcribe, whisper-1]
default: gpt-4o-mini-transcribe
help_center_search:
models: [text-embedding-3-small]
default: text-embedding-3-small
@@ -6,7 +6,7 @@ class Captain::Llm::ArticleTranslationService < Captain::BaseTaskService
def perform
raise ArgumentError, "Invalid type: #{type}" unless TYPES.include?(type)
response = make_api_call(feature: 'help_center_article_generation', messages: messages)
response = make_api_call(feature: 'help_center_article_generation', model: translation_model, messages: messages)
return response if response[:error]
response.merge(message: response[:message].strip)
@@ -33,6 +33,10 @@ class Captain::Llm::ArticleTranslationService < Captain::BaseTaskService
@llm_credential ||= system_llm_credential
end
def translation_model
@translation_model ||= InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_MODEL')&.value.presence || GPT_MODEL
end
def title_system_prompt
<<~SYSTEM_PROMPT_MESSAGE
You are a professional translator.
@@ -1,14 +1,13 @@
class Messages::AudioTranscriptionService< Llm::LegacyBaseOpenAiService
include Integrations::LlmInstrumentation
TRANSCRIPTION_MODEL = 'whisper-1'.freeze
# OpenAI's transcription endpoint hard limit is 25 MB *decimal* (25_000_000), not
# binary (25.megabytes = 26_214_400) — using the binary form leaks the 25.026.2 MB
# range to the API as 413s. Long audio (~70+ min Opus) keeps the attachment but skips
# transcription.
TRANSCRIPTION_BYTE_LIMIT = 25_000_000
attr_reader :attachment, :message, :account
attr_reader :attachment, :message, :account, :transcription_model
def initialize(attachment)
super()
@@ -128,8 +127,4 @@ class Messages::AudioTranscriptionService< Llm::LegacyBaseOpenAiService
'x-mp3' => 'mp3'
}.fetch(subtype, subtype)
end
def transcription_model
@transcription_model || TRANSCRIPTION_MODEL
end
end
@@ -5,6 +5,7 @@ RSpec.describe Captain::Llm::ArticleTranslationService do
let(:target_language) { 'Spanish' }
before do
InstallationConfig.where(name: %w[CAPTAIN_OPEN_AI_API_KEY CAPTAIN_OPEN_AI_MODEL]).destroy_all
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
allow(account).to receive(:feature_enabled?).and_call_original
allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(true)
@@ -18,6 +19,7 @@ RSpec.describe Captain::Llm::ArticleTranslationService do
it 'returns the stripped translated title' do
expect(service).to receive(:make_api_call) do |args|
expect(args[:feature]).to eq('help_center_article_generation')
expect(args[:model]).to eq(Llm::Config::DEFAULT_MODEL)
expect(args[:messages][0][:content]).to include('professional translator')
expect(args[:messages][0][:content]).to include(target_language)
expect(args[:messages][1][:content]).to eq('Getting Started')
@@ -26,6 +28,19 @@ RSpec.describe Captain::Llm::ArticleTranslationService do
expect(service.perform).to include(message: 'Primeros pasos')
end
it 'uses the installation model when no account override is configured' do
create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4.1-nano')
expect(service).to receive(:make_api_call).with(
hash_including(
feature: 'help_center_article_generation',
model: 'gpt-4.1-nano'
)
).and_return(message: 'Primeros pasos')
expect(service.perform).to include(message: 'Primeros pasos')
end
end
describe '#perform with type: :content' do
@@ -31,7 +31,7 @@ RSpec.describe Llm::BaseAiService do
end
it 'uses the feature default when feature context has no account override or installation model' do
expect(described_class.new(feature: 'assistant', account: account).model).to eq('gpt-5.1')
expect(described_class.new(feature: 'assistant', account: account).model).to eq(Llm::Models.default_model_for('assistant'))
end
end
@@ -120,7 +120,7 @@ RSpec.describe Messages::AudioTranscriptionService, type: :service do
it 'uses the audio transcription feature model' do
expect(audio_api).to receive(:transcribe).with(
parameters: hash_including(model: 'whisper-1', temperature: 0.0)
parameters: hash_including(model: 'gpt-4o-mini-transcribe', temperature: 0.0)
).and_return({ 'text' => 'Audio transcript' })
expect(service.send(:transcribe_audio)).to eq('Audio transcript')