mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
Maintenance: Improving API-Token management for Zammad AI
Co-authored-by: Mantas Masalskis <mm@zammad.com> Co-authored-by: Benjamin Scharf <bs@zammad.com>
This commit is contained in:
parent
bdb6758daf
commit
52f6589a61
76 changed files with 651 additions and 875 deletions
|
|
@ -15,11 +15,8 @@ class App.ControllerAIFeatureBase extends App.ControllerSubContent
|
|||
|
||||
@controllerBind('config_update', @aiProviderConfigHasChanged)
|
||||
|
||||
missingProvider: ->
|
||||
_.isEmpty(App.Config.get('ai_provider'))
|
||||
|
||||
showAlert: =>
|
||||
@missingProvider()
|
||||
showAlert: ->
|
||||
!App.Config.get('ai_provider')
|
||||
|
||||
renderAlert: =>
|
||||
@el.find('.js-missingProviderAlert').remove()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class ChannelAiProvider extends App.ControllerTabs
|
|||
|
||||
@render()
|
||||
|
||||
|
||||
class AiProviderSettings extends App.Controller
|
||||
@requiredPermission: 'admin.ai_provider'
|
||||
description : __('This service allows you to connect Zammad with an AI provider.')
|
||||
|
|
@ -112,27 +111,13 @@ class ProviderForm extends App.Controller
|
|||
constructor: (content) ->
|
||||
super
|
||||
|
||||
@providers = @activeProviders()
|
||||
@providers = App.Config.get('AIProviders')
|
||||
@sortedProviders = @getSortedProviderOptions()
|
||||
|
||||
@render(content)
|
||||
|
||||
|
||||
activeProviders: ->
|
||||
allProviders = App.Config.get('AIProviders')
|
||||
|
||||
Object.entries(allProviders)
|
||||
.filter(([_, provider]) ->
|
||||
if typeof provider.active is 'function'
|
||||
provider.active()
|
||||
else
|
||||
provider.active
|
||||
)
|
||||
.reduce((acc, [key, provider]) ->
|
||||
acc[key] = provider
|
||||
acc
|
||||
, {})
|
||||
|
||||
getProviderOptions: ->
|
||||
getSortedProviderOptions: ->
|
||||
Object
|
||||
.entries(@providers)
|
||||
.sort(([_, a], [__, b]) -> a.prio - b.prio)
|
||||
|
|
@ -207,7 +192,7 @@ class ProviderForm extends App.Controller
|
|||
name: 'provider'
|
||||
display: __('Provider')
|
||||
tag: 'select'
|
||||
options: @getProviderOptions()
|
||||
options: @sortedProviders
|
||||
null: true
|
||||
nulloption: true
|
||||
value: provider
|
||||
|
|
@ -219,14 +204,21 @@ class ProviderForm extends App.Controller
|
|||
|
||||
return result if !currentProvider
|
||||
|
||||
providerParams = if App.Setting.get('ai_provider') == provider then params else {}
|
||||
savedProvider = App.Setting.get('ai_provider_config')['provider']
|
||||
|
||||
providerParams = if savedProvider == provider then params else {}
|
||||
fields = @getInputFields(currentProvider, providerParams)
|
||||
|
||||
result.concat _.map(currentProvider.fields, (field) -> fields[field])
|
||||
currentProviderFields = if typeof currentProvider.fields is 'function'
|
||||
currentProvider.fields()
|
||||
else
|
||||
currentProvider.fields
|
||||
|
||||
result.concat _.map(currentProviderFields, (field) -> fields[field])
|
||||
|
||||
render: (provider) ->
|
||||
config = App.Setting.get('ai_provider_config') || {}
|
||||
current_provider = if provider != undefined then provider else App.Setting.get('ai_provider')
|
||||
current_provider = if provider != undefined then provider else config['provider']
|
||||
|
||||
configure_attributes = @providerConfiguration(current_provider, config)
|
||||
|
||||
|
|
@ -244,8 +236,6 @@ class ProviderForm extends App.Controller
|
|||
$('select[name=provider]').on('change', (e) =>
|
||||
@render($(e.target).val()))
|
||||
|
||||
|
||||
|
||||
update: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
|
|
@ -271,14 +261,16 @@ class ProviderForm extends App.Controller
|
|||
@validateAndSave(params)
|
||||
|
||||
validateAndSave: (params) ->
|
||||
provider = params.provider
|
||||
has_provider = !_.isEmpty(params.provider)
|
||||
|
||||
if !has_provider
|
||||
delete params.provider
|
||||
|
||||
delete params.provider
|
||||
if !params.model || params.model.trim() == ''
|
||||
delete params.model
|
||||
|
||||
config = params
|
||||
|
||||
App.Setting.set('ai_provider', provider, done: -> App.Setting.set('ai_provider_config', config, notify: true))
|
||||
App.Setting.set('ai_provider_config', params, done: ->
|
||||
App.Setting.set('ai_provider', has_provider, notify: true)
|
||||
)
|
||||
|
||||
App.Config.set('Provider', { prio: 1000, name: __('Provider'), parent: '#ai', target: '#ai/provider', controller: ChannelAiProvider, permission: ['admin.ai_provider'] }, 'NavBarAdmin')
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ class TextTool extends App.ControllerAIFeatureBase
|
|||
|
||||
@genericController.paginate(@page || 1, params)
|
||||
|
||||
showAlert: =>
|
||||
App.Config.get('ai_assistance_text_tools') and @missingProvider()
|
||||
showAlert: ->
|
||||
App.Config.get('ai_assistance_text_tools') and !App.Config.get('ai_provider')
|
||||
|
||||
pageHeaderTitle: =>
|
||||
@$('.page-header-title')
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ class App.TicketSummary extends App.ControllerAIFeatureBase
|
|||
field.prop('checked', value)
|
||||
)
|
||||
|
||||
showAlert: =>
|
||||
App.Config.get('ai_assistance_ticket_summary') && @missingProvider()
|
||||
showAlert: ->
|
||||
App.Config.get('ai_assistance_ticket_summary') && !App.Config.get('ai_provider')
|
||||
|
||||
render: =>
|
||||
service_config = App.Setting.get('ai_assistance_ticket_summary_config') || {}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class App.WidgetTextTools extends App.Controller
|
|||
$(element).data().plugin_texttools.collection = @all
|
||||
|
||||
@enabled: ->
|
||||
App.Config.get('ai_assistance_text_tools') and not _.isEmpty(App.Config.get('ai_provider'))
|
||||
App.Config.get('ai_assistance_text_tools') and App.Config.get('ai_provider')
|
||||
|
||||
@availableTextTools: (ce) ->
|
||||
$(ce.element).data().plugin_texttools?.collection or []
|
||||
|
|
|
|||
|
|
@ -4,6 +4,5 @@ App.Config.set('anthropic', {
|
|||
label: __('Anthropic')
|
||||
prio: 4000
|
||||
fields: ['token', 'model']
|
||||
active: true
|
||||
default_model: 'claude-3-7-sonnet-latest'
|
||||
}, 'AIProviders')
|
||||
|
|
|
|||
|
|
@ -3,5 +3,4 @@ App.Config.set('azure', {
|
|||
label: __('Azure AI')
|
||||
prio: 5000
|
||||
fields: ['url_completions', 'token'] # TODO: Add url_embeddings when needed.
|
||||
active: true
|
||||
}, 'AIProviders')
|
||||
|
|
|
|||
|
|
@ -4,6 +4,5 @@ App.Config.set('mistral', {
|
|||
label: __('Mistral AI')
|
||||
prio: 6000
|
||||
fields: ['token', 'model']
|
||||
active: true
|
||||
default_model: 'mistral-medium-latest'
|
||||
}, 'AIProviders')
|
||||
|
|
|
|||
|
|
@ -4,6 +4,5 @@ App.Config.set('ollama', {
|
|||
label: __('Ollama')
|
||||
prio: 3000
|
||||
fields: ['url', 'model']
|
||||
active: true
|
||||
default_model: 'llama3.2'
|
||||
}, 'AIProviders')
|
||||
|
|
|
|||
|
|
@ -4,6 +4,5 @@ App.Config.set('open_ai', {
|
|||
label: __('OpenAI')
|
||||
prio: 2000
|
||||
fields: ['token', 'model']
|
||||
active: true
|
||||
default_model: 'gpt-4.1'
|
||||
}, 'AIProviders')
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ App.Config.set('zammad_ai', {
|
|||
key: 'zammad_ai'
|
||||
label: __('Zammad AI')
|
||||
prio: 1000
|
||||
fields: ['token']
|
||||
active: -> App.Config.get('system_online_service') || App.Config.get('developer_mode')
|
||||
fields: ->
|
||||
if App.Config.get('system_online_service') || App.Config.get('developer_mode')
|
||||
[]
|
||||
else
|
||||
['token']
|
||||
}, 'AIProviders')
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ describe('basic toolbar testing', () => {
|
|||
it('hides feature if flag is not set', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_assistance_text_tools: false,
|
||||
ai_provider: 'openai',
|
||||
ai_provider: true,
|
||||
})
|
||||
|
||||
mockPermissions(['ticket.agent'])
|
||||
|
|
@ -205,7 +205,7 @@ describe('basic toolbar testing', () => {
|
|||
it('hides the feature if user is customer', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_assistance_text_tools: true,
|
||||
ai_provider: 'openai',
|
||||
ai_provider: true,
|
||||
})
|
||||
|
||||
mockPermissions(['ticket.customer'])
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
})
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -145,7 +145,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -187,7 +187,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -231,7 +231,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -292,7 +292,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
generate_on: EnumTicketSummaryGeneration.OnTicketDetailOpening,
|
||||
|
|
@ -327,7 +327,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: false,
|
||||
})
|
||||
|
||||
|
|
@ -358,7 +358,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
generate_on: EnumTicketSummaryGeneration.OnTicketDetailOpening,
|
||||
|
|
@ -388,7 +388,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
generate_on: EnumTicketSummaryGeneration.OnTicketDetailOpening,
|
||||
|
|
@ -444,7 +444,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
},
|
||||
})
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -485,7 +485,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
})
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -514,7 +514,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
|
||||
it('shows no ai provider is selected', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_provider: '',
|
||||
ai_provider: false,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -538,7 +538,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: false,
|
||||
})
|
||||
|
||||
|
|
@ -556,7 +556,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -583,7 +583,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -612,7 +612,7 @@ describe('Ticket detail view - Ticket summary', () => {
|
|||
mockPermissions(['ticket.agent'])
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ const ticketAIAssistanceSummarizeMock = {
|
|||
describe('TicketSidebarSummary', () => {
|
||||
it('displays correctly', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -181,7 +181,7 @@ describe('TicketSidebarSummary', () => {
|
|||
|
||||
it('does not display headings which are disabled,', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: false,
|
||||
|
|
@ -261,7 +261,7 @@ describe('TicketSidebarSummary', () => {
|
|||
})
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -289,7 +289,7 @@ describe('TicketSidebarSummary', () => {
|
|||
})
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
ai_assistance_ticket_summary_config: {
|
||||
open_questions: true,
|
||||
|
|
@ -323,7 +323,7 @@ describe('TicketSidebarSummary', () => {
|
|||
|
||||
it('shows message that user has provided already feedback', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
})
|
||||
|
||||
|
|
@ -354,7 +354,7 @@ describe('TicketSidebarSummary', () => {
|
|||
const runId = convertToGraphQLId('AIAnalyticsRun', 12345)
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
})
|
||||
|
||||
|
|
@ -390,7 +390,7 @@ describe('TicketSidebarSummary', () => {
|
|||
const runId = convertToGraphQLId('AIAnalyticsRun', 12345)
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
})
|
||||
|
||||
|
|
@ -423,7 +423,7 @@ describe('TicketSidebarSummary', () => {
|
|||
const runId = convertToGraphQLId('AIAnalyticsRun', 12345)
|
||||
|
||||
mockApplicationConfig({
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
ai_assistance_ticket_summary: true,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ vi.mock('prosemirror-model', () => {
|
|||
}
|
||||
})
|
||||
|
||||
describe.todo('keyboard interactions', () => {
|
||||
describe('keyboard interactions', () => {
|
||||
it('can use arrows to traverse toolbar', async () => {
|
||||
const view = renderComponent(FieldEditorActionBar, {
|
||||
props: {
|
||||
|
|
@ -223,7 +223,7 @@ describe('basic toolbar testing', () => {
|
|||
it('hides feature if flag is not set', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_assistance_text_tools: false,
|
||||
ai_provider: 'openai',
|
||||
ai_provider: true,
|
||||
})
|
||||
|
||||
mockPermissions(['ticket.agent'])
|
||||
|
|
@ -245,7 +245,7 @@ describe('basic toolbar testing', () => {
|
|||
it('hides the feature if user is customer', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_assistance_text_tools: true,
|
||||
ai_provider: 'openai',
|
||||
ai_provider: true,
|
||||
})
|
||||
|
||||
mockPermissions(['ticket.customer'])
|
||||
|
|
@ -289,7 +289,7 @@ describe('basic toolbar testing', () => {
|
|||
it.todo('can use custom text tools', async () => {
|
||||
mockApplicationConfig({
|
||||
ai_assistance_text_tools: true,
|
||||
ai_provider: 'openai',
|
||||
ai_provider: true,
|
||||
})
|
||||
|
||||
mockAiAssistanceTextToolsListQuery({
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ describe('Testing AI text tools', { retries: 2 }, () => {
|
|||
|
||||
mountEditor({}, ['ticket.agent'], {
|
||||
ai_assistance_text_tools: true,
|
||||
ai_provider: 'zammad_ai',
|
||||
ai_provider: true,
|
||||
})
|
||||
|
||||
cy.findByRole('textbox').type('Some text which should be checked.{selectall}')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface ConfigList {
|
|||
ai_assistance_text_tools_fixed_instructions: string
|
||||
ai_assistance_ticket_summary: boolean
|
||||
ai_assistance_ticket_summary_config: unknown
|
||||
ai_provider: string
|
||||
ai_provider: boolean
|
||||
api_password_access?: boolean | null
|
||||
api_token_access?: boolean | null
|
||||
auth_facebook?: boolean | null
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class CoreWorkflow::Custom::AdminGroupSummaryGeneration < CoreWorkflow::Custom::
|
|||
end
|
||||
|
||||
def visibility
|
||||
return 'show' if Setting.get('ai_provider').present? && Setting.get('ai_assistance_ticket_summary')
|
||||
return 'show' if Setting.get('ai_provider') && Setting.get('ai_assistance_ticket_summary')
|
||||
|
||||
'remove'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class Setting::Validation::AIProvider < Setting::Validation::Base
|
||||
|
||||
PROVIDERS = AI::Provider.list.map { |provider| provider.name.demodulize.underscore }.freeze
|
||||
|
||||
def run
|
||||
return result_success if value.blank?
|
||||
|
||||
msg = validate_provider
|
||||
return result_failed(msg) if !msg.nil?
|
||||
|
||||
result_success
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_provider
|
||||
return __('AI provider is not supported') if PROVIDERS.exclude?(value)
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
@ -3,61 +3,77 @@
|
|||
class Setting::Validation::AIProviderConfig < Setting::Validation::Base
|
||||
attr_reader :provider
|
||||
|
||||
ERROR_MESSAGE_OLLAMA = __('AI provider Ollama URL is not set').freeze
|
||||
ERROR_MESSAGE_AZURE = __('AI provider Azure configuration is incomplete').freeze
|
||||
ERROR_MESSAGE_TOKEN = __('AI provider token is not set').freeze
|
||||
|
||||
class AIProviderConfigError < StandardError; end
|
||||
|
||||
def initialize(record)
|
||||
super
|
||||
|
||||
@provider = Setting.get('ai_provider')
|
||||
@provider = value[:provider]
|
||||
end
|
||||
|
||||
def run
|
||||
return result_success if value.blank? || provider.blank?
|
||||
return result_success if value.blank?
|
||||
|
||||
msg = verify_configuration
|
||||
return result_failed(msg) if !msg.nil?
|
||||
verify_configuration
|
||||
|
||||
result_success
|
||||
rescue AIProviderConfigError => e
|
||||
result_failed(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_configuration
|
||||
msg = required_attributes
|
||||
return msg if !msg.nil?
|
||||
|
||||
validate_provider
|
||||
required_attributes
|
||||
accessible
|
||||
end
|
||||
|
||||
def required_attributes
|
||||
case provider
|
||||
when 'ollama'
|
||||
return __('AI provider Ollama URL is not set') if value['url'].blank?
|
||||
required_attributes_ollama
|
||||
when 'azure'
|
||||
return __('AI provider Azure configuration is incomplete') if !required_attributes_azure
|
||||
required_attributes_azure
|
||||
when 'zammad_ai'
|
||||
required_attributes_zammad
|
||||
else
|
||||
return __('AI provider token is not set') if value['token'].blank?
|
||||
required_attributes_token
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def accessible
|
||||
provider_class = AI::Provider.by_name(provider)
|
||||
|
||||
provider_class.ping!(value)
|
||||
|
||||
nil
|
||||
rescue => e
|
||||
__("AI provider is not accessible: #{e.message}")
|
||||
end
|
||||
|
||||
def required_attributes_azure
|
||||
return false if value['url_completions'].blank?
|
||||
raise AIProviderConfigError, ERROR_MESSAGE_AZURE if %w[url_completions token].any? { |key| value[key].blank? }
|
||||
end
|
||||
|
||||
# TODO: Enable it when needed.
|
||||
# return false if value['url_embeddings'].blank?
|
||||
def required_attributes_ollama
|
||||
raise AIProviderConfigError, ERROR_MESSAGE_OLLAMA if value['url'].blank?
|
||||
end
|
||||
|
||||
return false if value['token'].blank?
|
||||
def required_attributes_token
|
||||
raise AIProviderConfigError, ERROR_MESSAGE_TOKEN if value['token'].blank?
|
||||
end
|
||||
|
||||
true
|
||||
def required_attributes_zammad
|
||||
return if Setting.get('system_online_service') || Setting.get('developer_mode')
|
||||
|
||||
required_attributes_token
|
||||
end
|
||||
|
||||
def validate_provider
|
||||
raise AIProviderConfigError, __('AI provider is missing') if provider.blank?
|
||||
raise AIProviderConfigError, __('AI provider is not supported') if !AI::Provider.by_name(provider)
|
||||
end
|
||||
|
||||
def accessible
|
||||
AI::Provider
|
||||
.by_name(provider)
|
||||
.ping!(value)
|
||||
rescue => e
|
||||
raise AIProviderConfigError, __("AI provider is not accessible: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ module Service::AI::VectorDB
|
|||
private
|
||||
|
||||
def embedding_size
|
||||
provider = AI::Provider.by_name(Setting.get('ai_provider'))
|
||||
provider = AI::Provider.current
|
||||
|
||||
embedding_sizes = provider.const_get(:EMBEDDING_SIZES)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module Service::AI::VectorDB::Item
|
|||
end
|
||||
|
||||
def execute
|
||||
embedding = AI::Provider.by_name(Setting.get('ai_provider')).new.embed(input: content)
|
||||
embedding = AI::Provider.current.new.embed(input: content)
|
||||
|
||||
ai_vector_db.create(object_id: o_id, object_name:, content:, metadata:, embedding:)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module Service::AI::VectorDB::Item
|
|||
end
|
||||
|
||||
def execute
|
||||
embedding = AI::Provider.by_name(Setting.get('ai_provider')).new.embed(input: content)
|
||||
embedding = AI::Provider.current.new.embed(input: content)
|
||||
|
||||
ai_vector_db.upsert(object_id: o_id, object_name:, content:, metadata:, embedding:) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module Service::AI::VectorDB
|
|||
ai_vector_db.ping!
|
||||
|
||||
# First we need to embed the text.
|
||||
embedding = AI::Provider.by_name(Setting.get('ai_provider')).new.embed(input: text)
|
||||
embedding = AI::Provider.current.new.embed(input: text)
|
||||
|
||||
# Then we need to search the vector database for the most similar items.
|
||||
ai_vector_db.knn(embedding:, k: 2)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ class AddAIAssistanceTicketSummarize < ActiveRecord::Migration[7.2]
|
|||
authentication: true,
|
||||
permission: ['admin.ai'],
|
||||
validations: [
|
||||
'Setting::Validation::AIProvider',
|
||||
# This validaiton is now gone
|
||||
# 'Setting::Validation::AIProvider',
|
||||
],
|
||||
},
|
||||
frontend: true,
|
||||
|
|
|
|||
49
db/migrate/20251024103016_change_to_ai_provider_flag.rb
Normal file
49
db/migrate/20251024103016_change_to_ai_provider_flag.rb
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class ChangeToAIProviderFlag < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
# return if it's a new setup
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
||||
is_provider_set = Setting.get('ai_provider').present?
|
||||
|
||||
copy_provider_name
|
||||
drop_old_provider_setting
|
||||
add_new_provider_setting(is_provider_set)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def copy_provider_name
|
||||
config_setting = Setting.get('ai_provider_config')
|
||||
ai_provider_name = Setting.get('ai_provider')
|
||||
|
||||
return if ai_provider_name.blank?
|
||||
|
||||
config_setting[:provider] = ai_provider_name
|
||||
|
||||
Setting.set('ai_provider_config', config_setting, validate: false)
|
||||
end
|
||||
|
||||
def drop_old_provider_setting
|
||||
Setting
|
||||
.find_by!(name: 'ai_provider')
|
||||
.destroy!
|
||||
end
|
||||
|
||||
def add_new_provider_setting(state)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'AI provider',
|
||||
name: 'ai_provider',
|
||||
area: 'AI::Provider',
|
||||
description: 'Defines if the AI provider is configured.',
|
||||
options: {},
|
||||
state:,
|
||||
preferences: {
|
||||
authentication: true,
|
||||
permission: ['admin.ai_provider'],
|
||||
},
|
||||
frontend: true,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -5983,15 +5983,12 @@ Setting.create_if_not_exists(
|
|||
title: __('AI provider'),
|
||||
name: 'ai_provider',
|
||||
area: 'AI::Provider',
|
||||
description: __('Stores the AI provider.'),
|
||||
description: __('Defines if the AI provider is configured.'),
|
||||
options: {},
|
||||
state: '',
|
||||
state: false,
|
||||
preferences: {
|
||||
authentication: true,
|
||||
permission: ['admin.ai_provider'],
|
||||
validations: [
|
||||
'Setting::Validation::AIProvider',
|
||||
],
|
||||
},
|
||||
frontend: true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -669,7 +669,7 @@ msgstr ""
|
|||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:5993
|
||||
#: db/seeds/settings.rb:5990
|
||||
msgid "AI Provider Config"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -702,14 +702,18 @@ msgstr ""
|
|||
msgid "AI provider"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/setting/validation/ai_provider_config.rb:35
|
||||
#: app/models/setting/validation/ai_provider_config.rb:7
|
||||
msgid "AI provider Azure configuration is incomplete"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/setting/validation/ai_provider_config.rb:33
|
||||
#: app/models/setting/validation/ai_provider_config.rb:6
|
||||
msgid "AI provider Ollama URL is not set"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/setting/validation/ai_provider_config.rb:68
|
||||
msgid "AI provider is missing"
|
||||
msgstr ""
|
||||
|
||||
#: app/controllers/ticket/summarize_controller.rb:8
|
||||
#: app/graphql/gql/mutations/ticket/ai_assistance/summarize.rb:16
|
||||
#: app/services/service/ai/agent/run.rb:17
|
||||
|
|
@ -719,11 +723,11 @@ msgstr ""
|
|||
msgid "AI provider is not configured."
|
||||
msgstr ""
|
||||
|
||||
#: app/models/setting/validation/ai_provider.rb:19
|
||||
#: app/models/setting/validation/ai_provider_config.rb:69
|
||||
msgid "AI provider is not supported"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/setting/validation/ai_provider_config.rb:37
|
||||
#: app/models/setting/validation/ai_provider_config.rb:8
|
||||
msgid "AI provider token is not set"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -4667,6 +4671,10 @@ msgstr ""
|
|||
msgid "Defines if sipgate.io (http://www.sipgate.io) is enabled or not."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:5979
|
||||
msgid "Defines if the AI provider is configured."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:5058
|
||||
msgid "Defines if the GitHub (http://www.github.com) integration is enabled or not."
|
||||
msgstr ""
|
||||
|
|
@ -4687,7 +4695,7 @@ msgstr ""
|
|||
msgid "Defines if the application is in developer mode (all users have the same password and password reset will work without email delivery)."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6073
|
||||
#: db/seeds/settings.rb:6070
|
||||
msgid "Defines if the bubble menu feature of the richtext editor is enabled. Note that this setting will be ignored if the writing assistant is turned on."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -4951,7 +4959,7 @@ msgstr ""
|
|||
msgid "Defines the duration of customer activity (in seconds) on a call until the user profile dialog is shown."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6059
|
||||
#: db/seeds/settings.rb:6056
|
||||
msgid "Defines the fixed instructions that guide the AI Writing Assistant on e.g. how to format its output."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6151,11 +6159,11 @@ msgstr ""
|
|||
msgid "Enable or disable the maintenance mode of Zammad. If enabled, all non-administrators get logged out and only administrators can start a new session."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6012
|
||||
#: db/seeds/settings.rb:6009
|
||||
msgid "Enable or disable the ticket summary."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6045
|
||||
#: db/seeds/settings.rb:6042
|
||||
msgid "Enable or disable the writing assistant text tools."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10057,7 +10065,7 @@ msgstr ""
|
|||
msgid "Mode"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:159
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:144
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13221,7 +13229,7 @@ msgstr ""
|
|||
msgid "Rewrite complex section and make it easy to understand"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6070
|
||||
#: db/seeds/settings.rb:6067
|
||||
msgid "Richtext Bubble Menu"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14956,14 +14964,10 @@ msgstr ""
|
|||
msgid "Store"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:5996
|
||||
#: db/seeds/settings.rb:5993
|
||||
msgid "Stores the AI provider configuration."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:5979
|
||||
msgid "Stores the AI provider."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:5085
|
||||
msgid "Stores the GitHub configuration."
|
||||
msgstr ""
|
||||
|
|
@ -14972,7 +14976,7 @@ msgstr ""
|
|||
msgid "Stores the GitLab configuration."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6026
|
||||
#: db/seeds/settings.rb:6023
|
||||
msgid "Stores the ticket summarization options (e.g. which content is visible)."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15424,7 +15428,7 @@ msgstr ""
|
|||
msgid "The 24 hour customer service window is now closed, no further WhatsApp messages can be sent."
|
||||
msgstr ""
|
||||
|
||||
#: lib/monitoring_helper/health_checker/ai_provider_accessible.rb:17
|
||||
#: lib/monitoring_helper/health_checker/ai_provider_accessible.rb:15
|
||||
msgid "The AI Provider is not accessible."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15781,7 +15785,7 @@ msgstr ""
|
|||
msgid "The divider between TicketHook and ticket number. E. g. ': '."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:93
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:92
|
||||
#: app/assets/javascripts/app/lib/app_post/utils.coffee:1688
|
||||
msgid "The download could not be started. Please try again later."
|
||||
msgstr ""
|
||||
|
|
@ -16320,7 +16324,7 @@ msgstr ""
|
|||
msgid "The required value 'group_id' is missing."
|
||||
msgstr ""
|
||||
|
||||
#: lib/ai/provider.rb:50
|
||||
#: lib/ai/provider.rb:59
|
||||
msgid "The response could not be processed."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17060,11 +17064,11 @@ msgstr ""
|
|||
msgid "This service allows you to connect %s with %s."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:26
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:25
|
||||
msgid "This service allows you to connect Zammad with an AI provider."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:44
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:43
|
||||
msgid "This service allows you to download feedback agents provide on AI features and error details about failed AI requests."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17354,11 +17358,11 @@ msgstr ""
|
|||
#: app/assets/javascripts/app/controllers/_ai/ticket_summary.coffee:2
|
||||
#: app/assets/javascripts/app/views/ai/ticket_summary.jst.eco:7
|
||||
#: db/seeds/permissions.rb:227
|
||||
#: db/seeds/settings.rb:6009
|
||||
#: db/seeds/settings.rb:6006
|
||||
msgid "Ticket Summary"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6023
|
||||
#: db/seeds/settings.rb:6020
|
||||
msgid "Ticket Summary Config"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17821,7 +17825,7 @@ msgstr ""
|
|||
msgid "Toggle the overlay"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:148
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:133
|
||||
#: app/assets/javascripts/app/controllers/api.coffee:11
|
||||
#: app/assets/javascripts/app/views/channel/sms_account_overview.jst.eco:48
|
||||
#: app/models/channel/driver/sms/massenversand.rb:44
|
||||
|
|
@ -18155,7 +18159,7 @@ msgstr ""
|
|||
msgid "UID Field"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:170
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:155
|
||||
#: app/assets/javascripts/app/views/api.jst.eco:63
|
||||
#: app/assets/javascripts/app/views/integration/check_mk.jst.eco:12
|
||||
#: app/assets/javascripts/app/views/integration/cti.jst.eco:12
|
||||
|
|
@ -18172,11 +18176,11 @@ msgstr ""
|
|||
msgid "URL (AJAX endpoint)"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:180
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:165
|
||||
msgid "URL (Completions)"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:190
|
||||
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:175
|
||||
msgid "URL (Embeddings)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19268,11 +19272,11 @@ msgstr ""
|
|||
#: app/assets/javascripts/app/controllers/_ai/text_tool.coffee:16
|
||||
#: app/frontend/shared/components/Form/fields/FieldEditor/features/ai-assistant-text-tools/AiAssistantLoadingBanner/AiAssistantLoadingBanner.vue:35
|
||||
#: db/seeds/permissions.rb:233
|
||||
#: db/seeds/settings.rb:6042
|
||||
#: db/seeds/settings.rb:6039
|
||||
msgid "Writing Assistant"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/settings.rb:6056
|
||||
#: db/seeds/settings.rb:6053
|
||||
msgid "Writing Assistant Fixed Instructions"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -23,14 +23,23 @@ class AI::Provider
|
|||
end
|
||||
|
||||
class << self
|
||||
def list
|
||||
@list ||= descendants.sort_by(&:name)
|
||||
end
|
||||
|
||||
def by_name(name)
|
||||
"AI::Provider::#{name.classify}".safe_constantize
|
||||
end
|
||||
|
||||
def by_config(config)
|
||||
provider_name = config&.dig(:provider)
|
||||
return if provider_name.blank?
|
||||
|
||||
by_name(provider_name)
|
||||
end
|
||||
|
||||
def current
|
||||
return nil if !Setting.get('ai_provider')
|
||||
|
||||
by_config(Setting.get('ai_provider_config'))
|
||||
end
|
||||
|
||||
def ping!(_config)
|
||||
raise 'not implemented'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class AI::Provider::ZammadAI < AI::Provider
|
|||
open_timeout: 4,
|
||||
read_timeout: 60,
|
||||
verify_ssl: true,
|
||||
bearer_token: config[:token],
|
||||
bearer_token: self.class.token(config),
|
||||
total_timeout: 60,
|
||||
json: true,
|
||||
log: {
|
||||
|
|
@ -49,7 +49,7 @@ class AI::Provider::ZammadAI < AI::Provider
|
|||
open_timeout: 4,
|
||||
read_timeout: 60,
|
||||
verify_ssl: true,
|
||||
bearer_token: config[:token],
|
||||
bearer_token: token(config),
|
||||
total_timeout: 60,
|
||||
json: true,
|
||||
log: {
|
||||
|
|
@ -68,6 +68,10 @@ class AI::Provider::ZammadAI < AI::Provider
|
|||
ENV['ZAMMAD_AI_API_URL'] || config[:url] || ZAMMAD_AI_API_BASE_URL
|
||||
end
|
||||
|
||||
def self.token(config)
|
||||
config[:token].presence || ENV['ZAMMAD_AI_TOKEN']
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_response_metadata(data)
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class AI::Service
|
|||
end
|
||||
|
||||
def provider_name
|
||||
@provider_name ||= Setting.get('ai_provider')
|
||||
@provider_name ||= Setting.get('ai_provider_config')&.dig(:provider)
|
||||
end
|
||||
|
||||
def save_result(result, ai_analytics_run:)
|
||||
|
|
|
|||
|
|
@ -5,14 +5,12 @@ module MonitoringHelper
|
|||
class AIProviderAccessible < Backend
|
||||
|
||||
def run_health_check
|
||||
provider = Setting.get('ai_provider')
|
||||
return if provider.blank?
|
||||
return if !Setting.get('ai_provider')
|
||||
|
||||
provider_config = Setting.get('ai_provider_config')
|
||||
return if provider_config.blank?
|
||||
|
||||
begin
|
||||
AI::Provider.by_name(provider).ping!(provider_config)
|
||||
AI::Provider.by_config(provider_config).ping!(provider_config)
|
||||
rescue AI::Provider::ResponseError
|
||||
response.issues.push __('The AI Provider is not accessible.')
|
||||
end
|
||||
|
|
|
|||
78
spec/db/migrate/change_to_ai_provider_flag_spec.rb
Normal file
78
spec/db/migrate/change_to_ai_provider_flag_spec.rb
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ChangeToAIProviderFlag, type: :db_migration do
|
||||
before do
|
||||
Setting
|
||||
.find_by!(name: 'ai_provider')
|
||||
.destroy!
|
||||
|
||||
Setting.new(
|
||||
title: __('AI provider'),
|
||||
name: 'ai_provider',
|
||||
area: 'AI::Provider',
|
||||
description: __('Stores the AI provider.'),
|
||||
options: {},
|
||||
state: '',
|
||||
preferences: {
|
||||
authentication: true,
|
||||
permission: ['admin.ai_provider'],
|
||||
validations: [
|
||||
'Setting::Validation::AIProvider',
|
||||
],
|
||||
},
|
||||
frontend: true,
|
||||
).save(validate: false)
|
||||
end
|
||||
|
||||
shared_examples 'migrates ai provider setting' do
|
||||
it 'setting does not have validations anymore' do
|
||||
expect { migrate }
|
||||
.to change { Setting.find_by(name: 'ai_provider').preferences[:validations] }
|
||||
.to(nil)
|
||||
end
|
||||
|
||||
it 'Changes description' do
|
||||
expect { migrate }
|
||||
.to change { Setting.find_by(name: 'ai_provider').description }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provider was configured' do
|
||||
before do
|
||||
Setting.set('ai_provider', 'example', validate: false)
|
||||
Setting.set('ai_provider_config', { test: 'value' }, validate: false)
|
||||
end
|
||||
|
||||
it 'moves provider to ai_provider_config setting' do
|
||||
expect { migrate }
|
||||
.to change { Setting.get('ai_provider_config') }
|
||||
.to include(provider: 'example', test: 'value')
|
||||
end
|
||||
|
||||
it 'sets ai_provider flag to true' do
|
||||
expect { migrate }
|
||||
.to change { Setting.get('ai_provider') }
|
||||
.to(true)
|
||||
end
|
||||
|
||||
include_examples 'migrates ai provider setting'
|
||||
end
|
||||
|
||||
context 'when provider was empty' do
|
||||
it 'keeps ai_provider_config setting empty' do
|
||||
expect { migrate }
|
||||
.not_to change { Setting.get('ai_provider_config') }
|
||||
.from({})
|
||||
end
|
||||
|
||||
it 'sets ai_provider flag to false' do
|
||||
expect { migrate }
|
||||
.to change { Setting.get('ai_provider') }
|
||||
.to(false)
|
||||
end
|
||||
|
||||
include_examples 'migrates ai provider setting'
|
||||
end
|
||||
end
|
||||
|
|
@ -37,7 +37,7 @@ RSpec.describe Gql::Mutations::AIAssistance::TextTools::Run, :aggregate_failures
|
|||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_assistance_text_tools', true)
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
|
||||
allow_any_instance_of(AI::Service::TextTool)
|
||||
.to receive(:execute)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ RSpec.describe Gql::Mutations::Ticket::AIAssistance::Summarize, :aggregate_failu
|
|||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_assistance_ticket_summary', true)
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
|
||||
ticket_article
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ RSpec.describe Gql::Subscriptions::Ticket::AIAssistance::SummaryUpdates, authent
|
|||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_assistance_ticket_summary', true)
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
|
||||
gql.execute(subscription, variables: variables, context: { channel: mock_channel })
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ RSpec.describe AI::Provider::Anthropic, required_envs: %w[ANTHROPIC_API_KEY], us
|
|||
|
||||
Setting.set('ai_provider', 'anthropic')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['ANTHROPIC_API_KEY'],
|
||||
token: ENV['ANTHROPIC_API_KEY'],
|
||||
provider: 'anthropic',
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,11 @@ RSpec.describe AI::Provider::Azure, required_envs: %w[AZURE_TOKEN AZURE_URL_COMP
|
|||
setting = Setting.find_by(name: 'ai_provider_config')
|
||||
setting.update!(preferences: {})
|
||||
|
||||
Setting.set('ai_provider', 'azure')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['AZURE_TOKEN'],
|
||||
url_completions: ENV['AZURE_URL_COMPLETIONS'],
|
||||
|
||||
# TODO: Enable it when needed.
|
||||
# url_embeddings: ENV['AZURE_URL_EMBEDDINGS']
|
||||
})
|
||||
setup_ai_provider('azure',
|
||||
token: ENV['AZURE_TOKEN'],
|
||||
url_completions: ENV['AZURE_URL_COMPLETIONS'],)
|
||||
# TODO: Enable it when needed.
|
||||
# url_embeddings: ENV['AZURE_URL_EMBEDDINGS']
|
||||
end
|
||||
|
||||
include_examples 'provider/ping!'
|
||||
|
|
|
|||
|
|
@ -17,10 +17,7 @@ RSpec.describe AI::Provider::Mistral, required_envs: %w[MISTRAL_API_KEY], use_vc
|
|||
setting = Setting.find_by(name: 'ai_provider_config')
|
||||
setting.update!(preferences: {})
|
||||
|
||||
Setting.set('ai_provider', 'mistral')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['MISTRAL_API_KEY'],
|
||||
})
|
||||
setup_ai_provider('mistral', token: ENV['MISTRAL_API_KEY'])
|
||||
end
|
||||
|
||||
include_examples 'provider/ping!'
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ RSpec.describe AI::Provider::OpenAI, required_envs: %w[OPEN_AI_TOKEN], use_vcr:
|
|||
setting = Setting.find_by(name: 'ai_provider_config')
|
||||
setting.update!(preferences: {})
|
||||
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['OPEN_AI_TOKEN'],
|
||||
})
|
||||
setup_ai_provider('open_ai', token: ENV['OPEN_AI_TOKEN'])
|
||||
end
|
||||
|
||||
include_examples 'provider/ping!'
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ RSpec.describe AI::Provider::ZammadAI, required_envs: %w[ZAMMAD_AI_TOKEN], use_v
|
|||
setting = Setting.find_by(name: 'ai_provider_config')
|
||||
setting.update!(preferences: {})
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['ZAMMAD_AI_TOKEN'],
|
||||
})
|
||||
setup_ai_provider('zammad_ai', token: ENV['ZAMMAD_AI_TOKEN'])
|
||||
end
|
||||
|
||||
include_examples 'provider/ping!'
|
||||
|
|
|
|||
|
|
@ -45,16 +45,51 @@ RSpec.describe AI::Provider do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.list' do
|
||||
it 'returns a list of providers' do
|
||||
expect(described_class.list).to eq([
|
||||
AI::Provider::Anthropic,
|
||||
AI::Provider::Azure,
|
||||
AI::Provider::Mistral,
|
||||
AI::Provider::Ollama,
|
||||
AI::Provider::OpenAI,
|
||||
AI::Provider::ZammadAI,
|
||||
])
|
||||
describe '.by_config' do
|
||||
it 'returns the correct class' do
|
||||
config = { provider: 'open_ai' }
|
||||
expect(described_class.by_config(config)).to eq(AI::Provider::OpenAI)
|
||||
end
|
||||
|
||||
it 'returns nil when provider is blank' do
|
||||
config = {}
|
||||
expect(described_class.by_config(config)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '.current' do
|
||||
before do
|
||||
Setting.set('ai_provider_config', config, validate: false)
|
||||
Setting.set('ai_provider', flag)
|
||||
end
|
||||
|
||||
context 'when config is provided' do
|
||||
let(:config) { { provider: 'open_ai' } }
|
||||
|
||||
context 'when AI provider flag is true' do
|
||||
let(:flag) { true }
|
||||
|
||||
it 'returns the correct class' do
|
||||
expect(described_class.current).to eq(AI::Provider::OpenAI)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when AI provider flag is false' do
|
||||
let(:flag) { false }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(described_class.current).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config is blank' do
|
||||
let(:config) { {} }
|
||||
let(:flag) { true }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(described_class.current).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ RSpec.describe AI::Service::AIAgent, :aggregate_failures do
|
|||
let(:mock_result) { { 'state_id' => 1, 'priority_id' => 2 } }
|
||||
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
|
||||
# Mock the provider to avoid real API calls
|
||||
allow(AI::Provider::OpenAI).to receive(:new).and_return(mock_provider)
|
||||
|
|
|
|||
|
|
@ -55,10 +55,7 @@ RSpec.describe 'AI text tool result verification', :aggregate_failures, integrat
|
|||
setting = Setting.find_by(name: 'ai_provider_config')
|
||||
setting.update!(preferences: {})
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['ZAMMAD_AI_TOKEN'],
|
||||
})
|
||||
setup_ai_provider('zammad_ai', token: ENV['ZAMMAD_AI_TOKEN'])
|
||||
end
|
||||
|
||||
context 'when "Rewrite complex section and make it easy to understand" is used' do
|
||||
|
|
@ -88,10 +85,7 @@ RSpec.describe 'AI text tool result verification', :aggregate_failures, integrat
|
|||
|
||||
# context 'with Open AI provider' do
|
||||
# before do
|
||||
# Setting.set('ai_provider', 'open_ai')
|
||||
# Setting.set('ai_provider_config', {
|
||||
# token: ENV['OPEN_AI_TOKEN'],
|
||||
# })
|
||||
# setup_ai_provider('open_ai', token: ENV['OPEN_AI_TOKEN'])
|
||||
# end
|
||||
|
||||
# context 'when "Summarize section to about half its current size" is used' do
|
||||
|
|
@ -103,10 +97,7 @@ RSpec.describe 'AI text tool result verification', :aggregate_failures, integrat
|
|||
|
||||
# context 'with Mistral provider' do
|
||||
# before do
|
||||
# Setting.set('ai_provider', 'mistral')
|
||||
# Setting.set('ai_provider_config', {
|
||||
# token: ENV['MISTRAL_API_KEY'],
|
||||
# })
|
||||
# setup_ai_provider('mistral_ai', token: ENV['MISTRAL_API_KEY'])
|
||||
# end
|
||||
|
||||
# context 'when "Summarize section to about half its current size" is used' do
|
||||
|
|
|
|||
|
|
@ -32,10 +32,7 @@ You have to follow these rules:
|
|||
|
||||
context 'when service is executed with OpenAI as provider' do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['OPEN_AI_TOKEN'],
|
||||
})
|
||||
setup_ai_provider('open_ai', token: ENV['OPEN_AI_TOKEN'])
|
||||
end
|
||||
|
||||
it 'check that grammar is correct' do
|
||||
|
|
@ -46,10 +43,7 @@ You have to follow these rules:
|
|||
|
||||
context 'when service is executed with ZammadAI as provider' do
|
||||
before do
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['ZAMMAD_AI_TOKEN'],
|
||||
})
|
||||
setup_ai_provider('zammad_ai', token: ENV['ZAMMAD_AI_TOKEN'])
|
||||
end
|
||||
|
||||
it 'check that grammar is correct' do
|
||||
|
|
@ -63,10 +57,7 @@ You have to follow these rules:
|
|||
setting = Setting.find_by(name: 'ai_provider_config')
|
||||
setting.update!(preferences: {})
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['ZAMMAD_AI_TOKEN'],
|
||||
})
|
||||
setup_ai_provider('zammad_ai', token: ENV['ZAMMAD_AI_TOKEN'])
|
||||
end
|
||||
|
||||
context 'when neither prompt nor result contain paragraphs but have line breaks' do
|
||||
|
|
|
|||
|
|
@ -36,8 +36,7 @@ RSpec.describe AI::Service do
|
|||
|
||||
before do
|
||||
stub_const('AI::Service::PROMPT_PATH_STRING', Rails.root.join('test/data/ai/prompts/%{service}_%{type}.text.erb').to_s)
|
||||
stub_const('Setting::Validation::AIProvider::PROVIDERS', %w[sample_provider])
|
||||
Setting.set('ai_provider', 'sample_provider')
|
||||
setup_ai_provider 'sample_provider'
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
|
|
|
|||
|
|
@ -7,15 +7,8 @@ RSpec.describe MonitoringHelper::HealthChecker::AIProviderAccessible, integratio
|
|||
|
||||
describe '#check_health' do
|
||||
context 'when AI integration is not configured' do
|
||||
it 'reports no issue' do
|
||||
expect(instance.check_health.issues).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context 'when AI integration has an empty configuration' do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
Setting.set('ai_provider_config', {})
|
||||
unset_ai_provider
|
||||
end
|
||||
|
||||
it 'reports no issue' do
|
||||
|
|
@ -28,8 +21,7 @@ RSpec.describe MonitoringHelper::HealthChecker::AIProviderAccessible, integratio
|
|||
# Reset preferences to avoid validation errors.
|
||||
ai_config = Setting.find_by(name: 'ai_provider_config')
|
||||
ai_config.update!(preferences: {})
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
Setting.set('ai_provider_config', { 'token' => '123' })
|
||||
setup_ai_provider('open_ai', token: '123')
|
||||
end
|
||||
|
||||
context 'when AI provider is accessible' do
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ RSpec.describe CoreWorkflow::Custom::AdminGroupSummaryGeneration, type: :model d
|
|||
before do
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
Setting.set('ai_assistance_ticket_summary', true)
|
||||
end
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ RSpec.describe CoreWorkflow::Custom::AdminGroupSummaryGeneration, type: :model d
|
|||
|
||||
context 'when settings disabled' do
|
||||
before do
|
||||
Setting.set('ai_provider', '')
|
||||
unset_ai_provider
|
||||
end
|
||||
|
||||
it 'does not show ticket generation field for group' do
|
||||
|
|
@ -39,7 +39,7 @@ RSpec.describe CoreWorkflow::Custom::AdminGroupSummaryGeneration, type: :model d
|
|||
before do
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
Setting.set('ai_assistance_ticket_summary', false)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Setting::Validation::AIProviderConfig, required_envs: %w[OPEN_AI_TOKEN], use_vcr: true do
|
||||
RSpec.describe Setting::Validation::AIProviderConfig do
|
||||
|
||||
let(:setting_name) { 'ai_provider_config' }
|
||||
|
||||
|
|
@ -10,56 +10,204 @@ RSpec.describe Setting::Validation::AIProviderConfig, required_envs: %w[OPEN_AI_
|
|||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, {}) }.not_to raise_error
|
||||
end
|
||||
|
||||
context 'when config is present but provider is missing' do
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, { url: 'http://ai.example.com' }) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with missing provider' do
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, { 'token' => nil }) }.not_to raise_error
|
||||
context 'with unsupported provider' do
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, { provider: 'unsupported' }) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provider is ollama' do
|
||||
let(:config) { { provider: 'ollama', url: } }
|
||||
|
||||
before do
|
||||
allow(AI::Provider::Ollama).to receive(:ping!).and_return(true)
|
||||
end
|
||||
|
||||
context 'with missing url' do
|
||||
before do
|
||||
Setting.set('ai_provider', 'ollama')
|
||||
end
|
||||
let(:url) { nil }
|
||||
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, { 'url' => nil }) }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid url' do
|
||||
before do
|
||||
Setting.set('ai_provider', 'ollama')
|
||||
end
|
||||
let(:url) { 'https://ollama.ai' }
|
||||
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, { 'url' => 'https://ollama.ai' }) }.not_to raise_error
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provider is ZammadAI' do
|
||||
let(:config) { { provider: 'zammad_ai', token: } }
|
||||
|
||||
before do
|
||||
allow(UserAgent).to receive(:get) do |_, _, options|
|
||||
success = options[:bearer_token] == 'valid'
|
||||
|
||||
UserAgent::Result.new(
|
||||
error: '',
|
||||
success:,
|
||||
code: success ? 200 : 400,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with missing token' do
|
||||
let(:token) { nil }
|
||||
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid token' do
|
||||
let(:token) { 'valid' }
|
||||
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid token' do
|
||||
let(:token) { 'invalid_token' }
|
||||
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in SaaS or developer mode' do
|
||||
before do
|
||||
Setting.set('system_online_service', true)
|
||||
Setting.set('developer_mode', true)
|
||||
end
|
||||
|
||||
around do |example|
|
||||
old_env = ENV['ZAMMAD_AI_TOKEN']
|
||||
ENV['ZAMMAD_AI_TOKEN'] = env_value
|
||||
|
||||
example.run
|
||||
|
||||
ENV['ZAMMAD_AI_TOKEN'] = old_env
|
||||
end
|
||||
|
||||
context 'when ENV variable is present' do
|
||||
let(:env_value) { 'valid' }
|
||||
|
||||
context 'with missing token' do
|
||||
let(:token) { nil }
|
||||
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid token' do
|
||||
let(:token) { 'valid' }
|
||||
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid token' do
|
||||
let(:token) { 'invalid_token' }
|
||||
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ENV variable is missing' do
|
||||
let(:env_value) { nil }
|
||||
|
||||
context 'with missing token' do
|
||||
let(:token) { nil }
|
||||
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid token' do
|
||||
let(:token) { 'valid' }
|
||||
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid token' do
|
||||
let(:token) { 'invalid_token' }
|
||||
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provider is not ollama' do
|
||||
let(:config) { { provider: 'open_ai', token: } }
|
||||
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
allow(AI::Provider::OpenAI).to receive(:ping!).with(hash_including(token:)) do |hash|
|
||||
next if hash[:token] == 'valid'
|
||||
|
||||
raise AI::Provider::ResponseError, 'API server not accessible'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with missing token' do
|
||||
let(:token) { nil }
|
||||
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, { 'token' => nil }) }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid token' do
|
||||
let(:token) { 'valid' }
|
||||
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, { 'token' => ENV['OPEN_AI_TOKEN'] }) }.not_to raise_error
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid token' do
|
||||
let(:token) { 'invalid_token' }
|
||||
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, { 'token' => 'invalid_token' }) }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
expect { Setting.set(setting_name, config) }
|
||||
.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Setting::Validation::AIProvider do
|
||||
|
||||
let(:setting_name) { 'ai_provider' }
|
||||
|
||||
context 'with blank settings' do
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, '') }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unsupported provider' do
|
||||
it 'raises error' do
|
||||
expect { Setting.set(setting_name, 'unsupported') }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with supported provider' do
|
||||
it 'does not raise error' do
|
||||
expect { Setting.set(setting_name, 'open_ai') }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,7 +18,7 @@ RSpec.describe 'AI Assistance API endpoint', authenticated_as: :user, type: :req
|
|||
before do
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
Setting.set('ai_assistance_text_tools', true)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe 'Ticket Summarize API endpoints', authenticated_as: :user, perfor
|
|||
before do
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
Setting.set('ai_assistance_ticket_summary', ai_assistance_ticket_summary)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,8 @@ RSpec.describe Service::AI::Agent::Run::Context, type: :service do
|
|||
|
||||
before do
|
||||
articles
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
|
||||
setup_ai_provider('open_ai')
|
||||
|
||||
# Mock the AI provider to avoid real API calls
|
||||
allow(AI::Provider::OpenAI).to receive(:new).and_return(mock_provider)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,11 @@ RSpec.describe Service::AI::Agent::Run do
|
|||
let(:ai_provider) { 'open_ai' }
|
||||
|
||||
before do
|
||||
Setting.set('ai_provider', ai_provider)
|
||||
if ai_provider.present?
|
||||
setup_ai_provider(ai_provider, token: ENV['OPEN_AI_TOKEN'])
|
||||
else
|
||||
unset_ai_provider
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Service::AI::VectorDB::Available do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'Checks if vector database is available' do
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Service::AI::VectorDB::CreateTable do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'Create vector database table' do
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Service::AI::VectorDB::DropTable do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'Drop vector database table' do
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ RSpec.describe Service::AI::VectorDB::Item::Create do
|
|||
let(:object) { create(:ticket) }
|
||||
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'creates a vector database item' do
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ RSpec.describe Service::AI::VectorDB::Item::Destroy do
|
|||
let(:object) { create(:ticket) }
|
||||
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'destroys a vector database item' do
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ RSpec.describe Service::AI::VectorDB::Item::Upsert do
|
|||
let(:object) { create(:ticket) }
|
||||
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'Upserts a vector database item' do
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Service::AI::VectorDB::Rebuild do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'Rebuild vector database table', :aggregate_failures do
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Service::AI::VectorDB::Reload do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'Rebuild vector database table' do
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Service::AI::VectorDB::SimilaritySearch do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
end
|
||||
|
||||
it 'Rebuild vector database table' do
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ RSpec.describe Service::AIAssistance::TextTools do
|
|||
|
||||
context 'when text tool service is used' do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
setup_ai_provider('open_ai')
|
||||
Setting.set('ai_assistance_text_tools', true)
|
||||
|
||||
allow_any_instance_of(AI::Service::TextTool)
|
||||
|
|
|
|||
29
spec/support/ai_provider.rb
Normal file
29
spec/support/ai_provider.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module AIProviderHelper
|
||||
# @param provider [String] (e.g., 'zammad_ai'[DEFAULT], 'open_ai', 'azure', 'anthropic', 'mistral')
|
||||
# @param token [String] API token for the AI provider
|
||||
# @param additional_config [Hash] Additional configuration options including :token
|
||||
def setup_ai_provider(provider = 'zammad_ai', token: nil, **additional_config)
|
||||
Setting.set('ai_provider', true)
|
||||
|
||||
config = {
|
||||
provider:,
|
||||
token:,
|
||||
}
|
||||
.merge(additional_config)
|
||||
.compact_blank!
|
||||
|
||||
# Disable validation to avoid ping!
|
||||
Setting.set('ai_provider_config', config, validate: false)
|
||||
end
|
||||
|
||||
def unset_ai_provider
|
||||
Setting.set('ai_provider', false)
|
||||
Setting.set('ai_provider_config', {})
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include AIProviderHelper
|
||||
end
|
||||
|
|
@ -3,11 +3,18 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'AI > Provider', authenticated_as: :admin, type: :system do
|
||||
let(:admin) { create(:admin) }
|
||||
let(:admin) { create(:admin) }
|
||||
let(:self_hosted) { false }
|
||||
let(:initial_ai_provider_config) { {} }
|
||||
|
||||
before do
|
||||
setting = Setting.find_by(name: 'ai_provider_config')
|
||||
setting.update!(preferences: {})
|
||||
Setting.set('ai_provider_config', initial_ai_provider_config, validate: false)
|
||||
Setting.set('ai_provider', initial_ai_provider_config.present?, validate: false)
|
||||
|
||||
if self_hosted
|
||||
Setting.set('system_online_service', false)
|
||||
Setting.set('developer_mode', false)
|
||||
end
|
||||
|
||||
result = UserAgent::Result.new(
|
||||
success: true,
|
||||
|
|
@ -33,8 +40,8 @@ RSpec.describe 'AI > Provider', authenticated_as: :admin, type: :system do
|
|||
await_empty_ajax_queue
|
||||
|
||||
# Verify settings were saved
|
||||
expect(Setting.get('ai_provider')).to eq('open_ai')
|
||||
expect(Setting.get('ai_provider_config')).to eq({ 'token' => '1234111' })
|
||||
expect(Setting.get('ai_provider')).to be(true)
|
||||
expect(Setting.get('ai_provider_config')).to include({ provider: 'open_ai', token: '1234111' })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -56,7 +63,28 @@ RSpec.describe 'AI > Provider', authenticated_as: :admin, type: :system do
|
|||
.and(have_field('Model', placeholder: AI::Provider::Anthropic::DEFAULT_OPTIONS[:model]))
|
||||
|
||||
find('select[name=provider]').select('Azure AI')
|
||||
expect(page).to have_field('URL').and(have_field('Token')).and(have_no_field('Model'))
|
||||
expect(page)
|
||||
.to have_field('URL')
|
||||
.and(have_field('Token'))
|
||||
.and(have_no_field('Model'))
|
||||
|
||||
find('select[name=provider]').select('Zammad AI')
|
||||
expect(page)
|
||||
.to have_no_field('Token')
|
||||
.and(have_no_field('Model'))
|
||||
.and(have_no_field('URL'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when self-hosted' do
|
||||
let(:self_hosted) { true }
|
||||
|
||||
it 'shows Zammad AI Token field' do
|
||||
find('select[name=provider]').select('Zammad AI')
|
||||
expect(page)
|
||||
.to have_field('Token')
|
||||
.and(have_no_field('Model'))
|
||||
.and(have_no_field('URL'))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -86,8 +114,24 @@ RSpec.describe 'AI > Provider', authenticated_as: :admin, type: :system do
|
|||
|
||||
expect(page).to have_text('Update successful.')
|
||||
|
||||
expect(Setting.get('ai_provider')).to eq('open_ai')
|
||||
expect(Setting.get('ai_provider_config')).to include(token: token, model: model)
|
||||
expect(Setting.get('ai_provider')).to be(true)
|
||||
expect(Setting.get('ai_provider_config')).to include(provider: 'open_ai', token:, model:)
|
||||
end
|
||||
|
||||
context 'with initial configuration' do
|
||||
let(:initial_ai_provider_config) do
|
||||
{ provider: 'open_ai', token: '123' }
|
||||
end
|
||||
|
||||
it 'saves clear configuration' do
|
||||
find('select[name=provider]').select('-')
|
||||
click '.js-provider-submit'
|
||||
|
||||
expect(page).to have_text('Update successful.')
|
||||
|
||||
expect(Setting.get('ai_provider')).to be(false)
|
||||
expect(Setting.get('ai_provider_config')).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows feedback & logs tab with downloads and entries' do
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ RSpec.describe 'Manage > AI > Text Tool', type: :system do
|
|||
before do
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
Setting.set('ai_assistance_text_tools', true)
|
||||
end
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ RSpec.describe 'Manage > AI > Text Tool', type: :system do
|
|||
|
||||
context 'without provider configured' do
|
||||
before do
|
||||
Setting.set('ai_provider', '')
|
||||
unset_ai_provider
|
||||
Setting.set('ai_assistance_text_tools', true)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ RSpec.describe 'Manage > AI > Ticket Summary', type: :system do
|
|||
|
||||
context 'without provider configured' do
|
||||
before do
|
||||
Setting.set('ai_provider', '')
|
||||
unset_ai_provider
|
||||
visit '/#ai/ticket_summary'
|
||||
page.refresh
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ require 'system/examples/pagination_examples'
|
|||
|
||||
RSpec.describe 'AI > AI Agents > Types > Ticket Categorizer', db_strategy: :reset, type: :system do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['OPEN_AI_TOKEN'],
|
||||
})
|
||||
setup_ai_provider('open_ai', token: ENV['OPEN_AI_TOKEN'])
|
||||
|
||||
create(:object_manager_attribute_select, name: 'example_category', display: 'Example Category')
|
||||
create(:object_manager_attribute_select, name: 'example_industry', display: 'Example Industry')
|
||||
|
|
|
|||
|
|
@ -18,10 +18,7 @@ RSpec.describe 'AI > AI Agents', type: :system do
|
|||
|
||||
context 'when AI provider is configured', required_envs: %w[OPEN_AI_TOKEN] do
|
||||
before do
|
||||
Setting.set('ai_provider', 'open_ai')
|
||||
Setting.set('ai_provider_config', {
|
||||
token: ENV['OPEN_AI_TOKEN'],
|
||||
})
|
||||
setup_ai_provider('open_ai', token: ENV['OPEN_AI_TOKEN'])
|
||||
end
|
||||
|
||||
context 'with existing AI agent(s)' do
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ RSpec.describe 'Ticket create > Inline Image Replacement for AI Text Tools', aut
|
|||
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
Setting.set('ai_assistance_text_tools', true)
|
||||
Setting.set('ui_richtext_bubble_menu', true)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@ RSpec.describe 'Richtext Bubble Menu', authenticated_as: :authenticate, type: :s
|
|||
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', ai_provider)
|
||||
if ai_provider
|
||||
setup_ai_provider(ai_provider)
|
||||
else
|
||||
unset_ai_provider
|
||||
end
|
||||
|
||||
Setting.set('ai_assistance_text_tools', ai_assistance_text_tools)
|
||||
Setting.set('ui_richtext_bubble_menu', ui_richtext_bubble_menu)
|
||||
|
||||
|
|
@ -58,7 +63,7 @@ RSpec.describe 'Richtext Bubble Menu', authenticated_as: :authenticate, type: :s
|
|||
|
||||
shared_examples 'not showing text tools button' do
|
||||
context 'when ai provider is not set' do
|
||||
let(:ai_provider) { '' }
|
||||
let(:ai_provider) { false }
|
||||
|
||||
it 'does not show text tools button' do
|
||||
set_editor_field_value('body', input)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ RSpec.describe 'Ticket Summary', authenticated_as: :authenticate, type: :system
|
|||
def authenticate
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', ai_provider)
|
||||
setup_ai_provider(ai_provider)
|
||||
Setting.set('ai_assistance_ticket_summary', ai_assistance_ticket_summary)
|
||||
Setting.set('ai_assistance_ticket_summary_config', {
|
||||
open_questions: true,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ RSpec.describe 'Ticket Zoom > Text Tools Analytics', authenticated_as: :authenti
|
|||
|
||||
allow(AI::Provider::ZammadAI).to receive(:ping!).and_return(true)
|
||||
|
||||
Setting.set('ai_provider', 'zammad_ai')
|
||||
setup_ai_provider
|
||||
Setting.set('ai_assistance_text_tools', true)
|
||||
Setting.set('ui_richtext_bubble_menu', true)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,444 +0,0 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.openai.com/v1/models
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
User-Agent:
|
||||
- Zammad User Agent
|
||||
Host:
|
||||
- api.openai.com
|
||||
Authorization:
|
||||
- Bearer <OPEN_AI_TOKEN>
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Date:
|
||||
- Wed, 02 Apr 2025 20:20:36 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Openai-Version:
|
||||
- '2020-10-01'
|
||||
X-Request-Id:
|
||||
- a9ce9bcef3633fd57d2f3c07c3b0c7a4
|
||||
Openai-Processing-Ms:
|
||||
- '493'
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Cf-Cache-Status:
|
||||
- DYNAMIC
|
||||
Set-Cookie:
|
||||
- __cf_bm=qL5GgNpNw1SDaT3I3FnOtRzDPj1GOROHoJNBC9SD_rk-1743625236-1.0.1.1-QKYXkbOrvBUCb9RIDlYJFlLT5s_hxokAZaBe6H1HPrfZ50RrtHmVLJ7Jm9qbI1r2jcpIv4j4aqe.xOLFWVcmv18pJig4xQLoOCPil.VqiwE;
|
||||
path=/; expires=Wed, 02-Apr-25 20:50:36 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=DMCyLnwAlq95eaPhhekjLBvMRF4BrVrJ_c1_LXOiC.k-1743625236294-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
Server:
|
||||
- cloudflare
|
||||
Cf-Ray:
|
||||
- 92a3071b084ad2de-FRA
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=86400
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: |-
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "gpt-4o-realtime-preview-2024-12-17",
|
||||
"object": "model",
|
||||
"created": 1733945430,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-audio-preview-2024-12-17",
|
||||
"object": "model",
|
||||
"created": 1734034239,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "dall-e-3",
|
||||
"object": "model",
|
||||
"created": 1698785189,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "dall-e-2",
|
||||
"object": "model",
|
||||
"created": 1698798177,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-audio-preview-2024-10-01",
|
||||
"object": "model",
|
||||
"created": 1727389042,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-realtime-preview-2024-12-17",
|
||||
"object": "model",
|
||||
"created": 1734112601,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o3-mini",
|
||||
"object": "model",
|
||||
"created": 1737146383,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-realtime-preview",
|
||||
"object": "model",
|
||||
"created": 1734387380,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-realtime-preview-2024-10-01",
|
||||
"object": "model",
|
||||
"created": 1727131766,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-transcribe",
|
||||
"object": "model",
|
||||
"created": 1742068463,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-transcribe",
|
||||
"object": "model",
|
||||
"created": 1742068596,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-realtime-preview",
|
||||
"object": "model",
|
||||
"created": 1727659998,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "babbage-002",
|
||||
"object": "model",
|
||||
"created": 1692634615,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-tts",
|
||||
"object": "model",
|
||||
"created": 1742403959,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o3-mini-2025-01-31",
|
||||
"object": "model",
|
||||
"created": 1738010200,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "tts-1-hd-1106",
|
||||
"object": "model",
|
||||
"created": 1699053533,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "text-embedding-3-large",
|
||||
"object": "model",
|
||||
"created": 1705953180,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4",
|
||||
"object": "model",
|
||||
"created": 1687882411,
|
||||
"owned_by": "openai"
|
||||
},
|
||||
{
|
||||
"id": "text-embedding-ada-002",
|
||||
"object": "model",
|
||||
"created": 1671217299,
|
||||
"owned_by": "openai-internal"
|
||||
},
|
||||
{
|
||||
"id": "omni-moderation-latest",
|
||||
"object": "model",
|
||||
"created": 1731689265,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "tts-1-hd",
|
||||
"object": "model",
|
||||
"created": 1699046015,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-audio-preview",
|
||||
"object": "model",
|
||||
"created": 1734387424,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-audio-preview",
|
||||
"object": "model",
|
||||
"created": 1727460443,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o1-preview-2024-09-12",
|
||||
"object": "model",
|
||||
"created": 1725648865,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-3.5-turbo-instruct-0914",
|
||||
"object": "model",
|
||||
"created": 1694122472,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-search-preview",
|
||||
"object": "model",
|
||||
"created": 1741391161,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "tts-1-1106",
|
||||
"object": "model",
|
||||
"created": 1699053241,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "davinci-002",
|
||||
"object": "model",
|
||||
"created": 1692634301,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-3.5-turbo-1106",
|
||||
"object": "model",
|
||||
"created": 1698959748,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-turbo",
|
||||
"object": "model",
|
||||
"created": 1712361441,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-0125-preview",
|
||||
"object": "model",
|
||||
"created": 1706037612,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-3.5-turbo-instruct",
|
||||
"object": "model",
|
||||
"created": 1692901427,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-3.5-turbo",
|
||||
"object": "model",
|
||||
"created": 1677610602,
|
||||
"owned_by": "openai"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-turbo-preview",
|
||||
"object": "model",
|
||||
"created": 1706037777,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "chatgpt-4o-latest",
|
||||
"object": "model",
|
||||
"created": 1723515131,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-search-preview-2025-03-11",
|
||||
"object": "model",
|
||||
"created": 1741390858,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-2024-11-20",
|
||||
"object": "model",
|
||||
"created": 1739331543,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "whisper-1",
|
||||
"object": "model",
|
||||
"created": 1677532384,
|
||||
"owned_by": "openai-internal"
|
||||
},
|
||||
{
|
||||
"id": "gpt-3.5-turbo-0125",
|
||||
"object": "model",
|
||||
"created": 1706048358,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-2024-05-13",
|
||||
"object": "model",
|
||||
"created": 1715368132,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-3.5-turbo-16k",
|
||||
"object": "model",
|
||||
"created": 1683758102,
|
||||
"owned_by": "openai-internal"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-turbo-2024-04-09",
|
||||
"object": "model",
|
||||
"created": 1712601677,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-1106-preview",
|
||||
"object": "model",
|
||||
"created": 1698957206,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o1-preview",
|
||||
"object": "model",
|
||||
"created": 1725648897,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-0613",
|
||||
"object": "model",
|
||||
"created": 1686588896,
|
||||
"owned_by": "openai"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-search-preview",
|
||||
"object": "model",
|
||||
"created": 1741388720,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4.5-preview",
|
||||
"object": "model",
|
||||
"created": 1740623059,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4.5-preview-2025-02-27",
|
||||
"object": "model",
|
||||
"created": 1740623304,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-search-preview-2025-03-11",
|
||||
"object": "model",
|
||||
"created": 1741388170,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "tts-1",
|
||||
"object": "model",
|
||||
"created": 1681940951,
|
||||
"owned_by": "openai-internal"
|
||||
},
|
||||
{
|
||||
"id": "omni-moderation-2024-09-26",
|
||||
"object": "model",
|
||||
"created": 1732734466,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o1-2024-12-17",
|
||||
"object": "model",
|
||||
"created": 1734326976,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o1",
|
||||
"object": "model",
|
||||
"created": 1734375816,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o1-pro",
|
||||
"object": "model",
|
||||
"created": 1742251791,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "text-embedding-3-small",
|
||||
"object": "model",
|
||||
"created": 1705948997,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o1-pro-2025-03-19",
|
||||
"object": "model",
|
||||
"created": 1742251504,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o",
|
||||
"object": "model",
|
||||
"created": 1715367049,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini",
|
||||
"object": "model",
|
||||
"created": 1721172741,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-2024-08-06",
|
||||
"object": "model",
|
||||
"created": 1722814719,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-2024-07-18",
|
||||
"object": "model",
|
||||
"created": 1721172717,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o1-mini",
|
||||
"object": "model",
|
||||
"created": 1725649008,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini-audio-preview-2024-12-17",
|
||||
"object": "model",
|
||||
"created": 1734115920,
|
||||
"owned_by": "system"
|
||||
},
|
||||
{
|
||||
"id": "o1-mini-2024-09-12",
|
||||
"object": "model",
|
||||
"created": 1725648979,
|
||||
"owned_by": "system"
|
||||
}
|
||||
]
|
||||
}
|
||||
recorded_at: Wed, 02 Apr 2025 20:20:36 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue