Fixes #6060 - Missing temperature support by some models for some providers.

Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
This commit is contained in:
Dusan Vuckovic
2026-04-15 12:42:16 +01:00
parent 13ce640dc3
commit 281f39ead8
18 changed files with 517 additions and 83 deletions

View File

@@ -85,14 +85,14 @@ class AiProviderSettings extends App.Controller
release: =>
App.Setting.unsubscribe(@subscribeId)
render: =>
return if _.isEqual(@aiProviderConfigAtRender, App.Setting.get('ai_provider_config'))
@aiProviderConfigAtRender = App.Setting.get('ai_provider_config')
render: (_, localOrServer) =>
return if localOrServer isnt 'refresh'
@html App.view('ai/provider')(
description: @description,
)
@form = new ProviderForm()
new ProviderForm()
class AiProviderFeedbackAndLogs extends App.Controller
@requiredPermission: 'admin.ai_provider'
@@ -159,11 +159,7 @@ class AiProviderFeedbackAndLogs extends App.Controller
@httpLog = null
super
class ProviderForm extends App.Controller
events:
'.js-provider-submit': 'update'
constructor: (content) ->
super
@@ -172,7 +168,6 @@ class ProviderForm extends App.Controller
@render(content)
getSortedProviderOptions: ->
Object
.entries(@providers)
@@ -191,7 +186,7 @@ class ProviderForm extends App.Controller
type: 'password'
single: true
null: not _.contains(provider.required, 'token')
autocomplete: 'off'
autocomplete: 'new-password'
value: params.token
}
model: {
@@ -315,6 +310,7 @@ class ProviderForm extends App.Controller
)
$('.js-provider-submit').off('click.provider').on('click.provider', @update)
$('select[name=provider]').off('change.provider').on('change.provider', (e) =>
@render($(e.target).val())
)
@@ -335,6 +331,8 @@ class ProviderForm extends App.Controller
@validateAndSave(params)
validateAndSave: (params) ->
App.ControllerForm.disable(@providerSettingsForm.form)
has_provider = not _.isEmpty(params.provider)
if not has_provider
@@ -350,25 +348,32 @@ class ProviderForm extends App.Controller
if has_provider && !params.hasOwnProperty('token') && savedProviderConfig.provider == params.provider && savedProviderConfig.token
params.token = savedProviderConfig.token
App.Setting.set('ai_provider_config', params, done: =>
# If the provider configuration is being updated, or the provider is already disabled,
# do not touch the provider switch.
if has_provider or not App.Config.get('ai_provider')
App.Event.trigger 'notify', {
type: 'success'
msg: __('Update successful.')
timeout: 2000
}
App.Setting.set(
'ai_provider_config',
params,
done: =>
App.ControllerForm.enable(@providerSettingsForm.form)
return
# If the provider configuration is being updated, or the provider is already disabled,
# do not touch the provider switch.
if has_provider or not App.Config.get('ai_provider')
App.Event.trigger 'notify', {
type: 'success'
msg: __('Update successful.')
timeout: 2000
}
# Turn off the provider switch when the provider configuration is emptied.
App.Setting.set('ai_provider', false, done: =>
@notify(
type: 'success'
msg: __('AI provider disabled successfully.')
return
# Turn off the provider switch when the provider configuration is emptied.
App.Setting.set('ai_provider', false, done: =>
@notify(
type: 'success'
msg: __('AI provider disabled successfully.')
)
)
)
fail: =>
App.ControllerForm.enable(@providerSettingsForm.form)
)
App.Config.set('Provider', { prio: 1000, name: __('Provider'), parent: '#ai', target: '#ai/provider', controller: ChannelAiProvider, permission: ['admin.ai_provider'] }, 'NavBarAdmin')

View File

@@ -1,6 +1,6 @@
App.Config.set('azure', {
key: 'azure'
label: __('Azure AI')
label: __('Azure AI (legacy deployment-based endpoints)')
prio: 5000
fields: ['token', 'url_completions', 'ocr_active', 'url_ocr'] # TODO: Add url_embeddings when needed.
required: ['token', 'url_completions']

View File

@@ -77,10 +77,17 @@ class Setting::Validation::AIProviderConfig < Setting::Validation::Base
end
def accessible
AI::Provider
.by_name(provider)
.ping!(value)
provider_class = AI::Provider.by_name(provider)
provider_class.ping!(value)
check_temperature_support!(provider_class)
rescue AIProviderConfigError
raise
rescue => e
raise AIProviderConfigError, __("AI provider is not accessible: #{e.message}")
end
def check_temperature_support!(provider_class)
value['model_temperature_support'] = provider_class.check_temperature_support!(value)
end
end

View File

@@ -835,7 +835,7 @@ msgstr ""
msgid "API server error: %s"
msgstr ""
#: lib/ai/provider/azure.rb:128
#: lib/ai/provider/azure.rb:168
msgid "API server not accessible"
msgstr ""
@@ -2449,7 +2449,7 @@ msgid "Average: %s%"
msgstr ""
#: app/assets/javascripts/app/lib/app_post/ai_provider/azure_ai.coffee:3
msgid "Azure AI"
msgid "Azure AI (legacy deployment-based endpoints)"
msgstr ""
#: db/seeds/settings.rb:3493
@@ -9560,11 +9560,11 @@ msgstr ""
msgid "Leave BETA UI feedback program?"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:263
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:258
msgid "Leave empty to use URL (Completions)"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:253
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:248
msgid "Leave empty to use the base model"
msgstr ""
@@ -10663,7 +10663,7 @@ msgstr ""
msgid "Mode"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:199
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:194
msgid "Model"
msgstr ""
@@ -11819,7 +11819,7 @@ msgstr ""
msgid "OAuth2 token is expired!"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:246
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:241
msgid "OCR Model"
msgstr ""
@@ -13416,7 +13416,7 @@ msgstr ""
msgid "Recipient"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:237
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:232
msgid "Recognize image text (OCR)"
msgstr ""
@@ -17200,7 +17200,7 @@ msgstr ""
msgid "The required value 'group_id' is missing."
msgstr ""
#: lib/ai/provider.rb:65
#: lib/ai/provider.rb:69
msgid "The response could not be processed."
msgstr ""
@@ -18721,7 +18721,7 @@ msgstr ""
msgid "Toggle the overlay"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:189
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:184
#: 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
@@ -19071,7 +19071,7 @@ msgstr ""
msgid "UID Field"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:209
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:204
#: 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
@@ -19088,15 +19088,15 @@ msgstr ""
msgid "URL (AJAX endpoint)"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:219
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:214
msgid "URL (Completions)"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:228
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:223
msgid "URL (Embeddings)"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:257
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:252
msgid "URL (OCR)"
msgstr ""
@@ -19282,7 +19282,7 @@ msgstr ""
msgid "Update shared draft"
msgstr ""
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:359
#: app/assets/javascripts/app/controllers/_ai/provider.coffee:362
#: app/assets/javascripts/app/controllers/_profile/calendar_subscriptions.coffee:81
#: app/assets/javascripts/app/controllers/_profile/language.coffee:64
#: app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee:48

View File

@@ -43,6 +43,10 @@ class AI::Provider
def ping!(_config)
raise 'not implemented'
end
def check_temperature_support!(_config)
true
end
end
def ask(prompt_system:, prompt_user:, prompt_image: nil)
@@ -79,6 +83,10 @@ class AI::Provider
private
def model_supports_temperature?
config[:model_temperature_support] != false
end
def transform_json_response(result)
return result if result.blank?
@@ -117,4 +125,5 @@ class AI::Provider
class RequestError < StandardError; end
class ResponseError < StandardError; end
class OutputFormatError < ResponseError; end
class CheckTemperatureSupportError < ResponseError; end
end

View File

@@ -34,21 +34,24 @@ class AI::Provider::Anthropic < AI::Provider
prompt_user
end
request_body = {
max_tokens: options[:max_tokens],
messages: [
{
role: 'user',
content:,
},
],
model: model_for(prompt_image:),
stream: false,
system: prompt_system,
}
request_body[:temperature] = options[:temperature] if model_supports_temperature?
response = UserAgent.post(
"#{ANTHROPIC_API_BASE_URL}/messages",
{
max_tokens: options[:max_tokens],
messages: [
{
role: 'user',
content:,
},
],
model: model_for(prompt_image:),
stream: false,
system: prompt_system,
temperature: options[:temperature],
},
request_body,
{
open_timeout: 4,
read_timeout: 60,

View File

@@ -4,17 +4,20 @@ class AI::Provider::Azure < AI::Provider
include AI::Provider::Concerns::HandlesOpenAIMessages
def chat(prompt_system:, prompt_user:, prompt_image:)
request_body = {
messages: messages_for(prompt_system:, prompt_user:, prompt_image:),
response_format: {
type: options[:json_response] ? 'json_object' : 'text'
},
stream: false,
store: false,
}
request_body[:temperature] = options[:temperature] if model_supports_temperature?
response = UserAgent.post(
chat_url_for(prompt_image:),
{
messages: messages_for(prompt_system:, prompt_user:, prompt_image:),
temperature: options[:temperature],
response_format: {
type: options[:json_response] ? 'json_object' : 'text'
},
stream: false,
store: false,
},
request_body,
{
open_timeout: 4,
read_timeout: 60,
@@ -67,6 +70,44 @@ class AI::Provider::Azure < AI::Provider
nil
end
def self.check_temperature_support!(config)
response = UserAgent.post(
config[:url_completions],
{
messages: [{ role: 'user', content: 'Hello' }],
temperature: 0.1,
stream: false,
store: false,
},
{
open_timeout: 4,
read_timeout: 60,
verify_ssl: true,
bearer_token: config[:token],
total_timeout: 60,
json: true,
log: {
facility: 'AI::Provider',
log_only_on_error: true,
},
},
)
return true if response.success?
data = JSON.parse(response.body)
data = data.pop if data.is_a?(Array) # Handle case when response is an array of errors
message = data.dig('error', 'message')
type = data.dig('error', 'type')
param = data.dig('error', 'param')
code = data.dig('error', 'code')
return false if type == 'invalid_request_error' && param == 'temperature' && code == 'unsupported_value'
raise message
rescue => e
raise CheckTemperatureSupportError, e.message
end
def self.ping_chat!(config)
response = UserAgent.post(
config[:url_completions],
@@ -81,7 +122,6 @@ class AI::Provider::Azure < AI::Provider
content: 'Ping pong in JSON', # rubocop:disable Zammad/DetectTranslatableString
},
],
temperature: 0,
response_format: {
type: 'json_object'
},

View File

@@ -0,0 +1,20 @@
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
class AI::Provider
module Concerns::HasModelsWithoutTemperatureFallback
extend ActiveSupport::Concern
included do
private
def model_supports_temperature?
# Use the dynamically detected flag if it has been set.
return config[:model_temperature_support] if config[:model_temperature_support].present?
# Fall back to the hardcoded list for backward compatibility.
current_model = options[:model]
options[:models_without_temperature].none? { |model_pattern| current_model.start_with?(model_pattern) }
end
end
end
end

View File

@@ -3,9 +3,11 @@
class AI::Provider::CustomOpenAI < AI::Provider
include AI::Provider::Concerns::HandlesOpenAIMessages
include AI::Provider::Concerns::HasConfigurableModel
include AI::Provider::Concerns::HasModelsWithoutTemperatureFallback
DEFAULT_OPTIONS = {
temperature: 0.1,
temperature: 0.1,
models_without_temperature: ['gpt-5']
}.freeze
def chat(prompt_system:, prompt_user:, prompt_image:)
@@ -15,7 +17,7 @@ class AI::Provider::CustomOpenAI < AI::Provider
stream: false,
}
request_body[:temperature] = options[:temperature]
request_body[:temperature] = options[:temperature] if model_supports_temperature?
request_options = {
open_timeout: 4,
@@ -74,6 +76,50 @@ class AI::Provider::CustomOpenAI < AI::Provider
nil
end
def self.check_temperature_support!(config)
request_body = {
model: config[:model],
messages: [{ role: 'user', content: 'Hello' }],
temperature: DEFAULT_OPTIONS[:temperature],
stream: false,
}
request_options = {
open_timeout: 4,
read_timeout: 60,
verify_ssl: true,
total_timeout: 60,
json: true,
log: {
facility: 'AI::Provider',
log_only_on_error: true,
},
}
# Token is optional since target host might not require authentication
request_options[:bearer_token] = config[:token] if config[:token].present?
response = UserAgent.post(
"#{config[:url]}/chat/completions",
request_body,
request_options,
)
return true if response.success?
data = JSON.parse(response.body)
data = data.pop if data.is_a?(Array) # Handle case when response is an array of errors
message = data.dig('error', 'message')
type = data.dig('error', 'type')
param = data.dig('error', 'param')
code = data.dig('error', 'code')
return false if type == 'invalid_request_error' && param == 'temperature' && code == 'unsupported_value'
raise message
rescue => e
raise CheckTemperatureSupportError, e.message
end
private
def specific_metadata

View File

@@ -18,16 +18,17 @@ class AI::Provider::Ollama < AI::Provider
def chat(prompt_system:, prompt_user:, prompt_image:)
params = {
model: model_for(prompt_image:),
system: prompt_system,
prompt: prompt_user,
stream: false,
think: false,
options: {
temperature: options[:temperature],
},
model: model_for(prompt_image:),
system: prompt_system,
prompt: prompt_user,
stream: false,
think: false,
}
if model_supports_temperature?
params[:options] = { temperature: options[:temperature] }
end
if prompt_image.is_a?(::Store)
params[:images] = [Base64.strict_encode64(prompt_image.content_ocr)]
end

View File

@@ -3,6 +3,7 @@
class AI::Provider::OpenAI < AI::Provider
include AI::Provider::Concerns::HandlesOpenAIMessages
include AI::Provider::Concerns::HasConfigurableModel
include AI::Provider::Concerns::HasModelsWithoutTemperatureFallback
OPENAI_API_BASE_URL = 'https://api.openai.com/v1'.freeze
@@ -98,15 +99,46 @@ class AI::Provider::OpenAI < AI::Provider
nil
end
private
def self.check_temperature_support!(config)
response = UserAgent.post(
"#{OPENAI_API_BASE_URL}/chat/completions",
{
model: config[:model] || DEFAULT_OPTIONS[:model],
messages: [{ role: 'user', content: 'Hello' }],
temperature: DEFAULT_OPTIONS[:temperature],
stream: false,
store: false,
},
{
open_timeout: 4,
read_timeout: 60,
verify_ssl: true,
bearer_token: config[:token],
total_timeout: 60,
json: true,
log: {
facility: 'AI::Provider',
log_only_on_error: true,
},
},
)
def model_supports_temperature?
current_model = options[:model]
return true if response.success?
# Check if any model in the list starts with the current model name
options[:models_without_temperature].none? { |model_pattern| current_model.start_with?(model_pattern) }
data = JSON.parse(response.body)
message = data.dig('error', 'message')
type = data.dig('error', 'type')
param = data.dig('error', 'param')
code = data.dig('error', 'code')
return false if type == 'invalid_request_error' && param == 'temperature' && code == 'unsupported_value'
raise message
rescue => e
raise CheckTemperatureSupportError, e.message
end
private
def specific_metadata
{
model: options[:model],

View File

@@ -2,6 +2,7 @@
require 'rails_helper'
require_relative 'shared_examples/ping'
require_relative 'shared_examples/check_temperature_support'
# TODO: Add AZURE_URL_EMBEDDINGS when needed.
RSpec.describe AI::Provider::Azure, integration: true, required_envs: %w[AZURE_TOKEN AZURE_URL_COMPLETIONS AZURE_HOST], use_vcr: true do
@@ -23,6 +24,7 @@ RSpec.describe AI::Provider::Azure, integration: true, required_envs: %w[AZURE_T
end
include_examples 'provider/ping!'
include_examples 'provider/check_temperature_support'
it 'does exchange data with azure ai endpoint' do
expect(ai_provider.ask(prompt_system:, prompt_user:)).to match({ 'connected' => 'true' })

View File

@@ -2,6 +2,7 @@
require 'rails_helper'
require_relative 'shared_examples/ping'
require_relative 'shared_examples/check_temperature_support'
RSpec.describe AI::Provider::OpenAI, integration: true, required_envs: %w[OPEN_AI_TOKEN], use_vcr: true do
subject(:ai_provider) { described_class.new(options: { json_response: true }) }
@@ -18,6 +19,7 @@ RSpec.describe AI::Provider::OpenAI, integration: true, required_envs: %w[OPEN_A
end
include_examples 'provider/ping!'
include_examples 'provider/check_temperature_support'
context 'when specifying a model' do
context 'without a model' do
@@ -44,6 +46,16 @@ RSpec.describe AI::Provider::OpenAI, integration: true, required_envs: %w[OPEN_A
expect(ai_provider.ask(prompt_system:, prompt_user:)).to match({ 'connected' => 'true' })
end
end
context 'with model_temperature_support flag set to false' do
before do
Setting.set('ai_provider_config', Setting.get('ai_provider_config').merge(model_temperature_support: false))
end
it 'does exchange data with open ai endpoint without temperature' do
expect(ai_provider.ask(prompt_system:, prompt_user:)).to match({ 'connected' => 'true' })
end
end
end
context 'with an invalid model' do

View File

@@ -0,0 +1,49 @@
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
RSpec.shared_examples 'provider/check_temperature_support' do
describe '.check_temperature_support!' do
let(:provider_config) { Setting.get('ai_provider_config') }
context 'when API accepts temperature' do
before do
allow(UserAgent).to receive(:post).and_return(
UserAgent::Result.new(
success: true,
code: 200,
data: {},
)
)
end
it 'returns true' do
expect(described_class.check_temperature_support!(provider_config)).to be true
end
end
context 'when API rejects temperature' do
before do
allow(UserAgent).to receive(:post).and_return(
UserAgent::Result.new(
body: '{ "error": { "message": "Unsupported value: \'temperature\' does not support 0.1 with this model. Only the default (1) value is supported.", "type": "invalid_request_error", "param": "temperature", "code": "unsupported_value" } }',
success: false,
code: 400,
)
)
end
it 'returns false' do
expect(described_class.check_temperature_support!(provider_config)).to be false
end
end
context 'when an exception occurs' do
before do
allow(UserAgent).to receive(:post).and_raise(StandardError, 'connection error')
end
it 'raises an error' do
expect { described_class.check_temperature_support!(provider_config) }.to raise_error(AI::Provider::CheckTemperatureSupportError, 'connection error')
end
end
end
end

View File

@@ -0,0 +1,36 @@
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
RSpec.shared_examples 'provider/model_supports_temperature' do
describe 'temperature support flag' do
let(:prompt_system) { '' }
let(:prompt_user) { 'Hi' }
before do
allow(UserAgent).to receive(:post).and_return(
UserAgent::Result.new(
success: true,
code: 200,
data: chat_response_data,
)
)
end
context 'when model_temperature_support is false' do
before do
Setting.set('ai_provider_config', Setting.get('ai_provider_config').merge(model_temperature_support: false), validate: false)
end
it 'does not include temperature in the request', :aggregate_failures do
subject.ask(prompt_system:, prompt_user:)
request_body = nil
expect(UserAgent).to have_received(:post) do |_url, body, _options|
request_body = body
end
expect(request_body).not_to include(:temperature)
expect(request_body.dig(:options, :temperature)).to be_nil if request_body.key?(:options)
end
end
end
end

View File

@@ -182,6 +182,7 @@ RSpec.describe Setting::Validation::AIProviderConfig do
raise AI::Provider::ResponseError, 'API server not accessible'
end
allow(AI::Provider::OpenAI).to receive(:check_temperature_support!).and_return(true)
end
context 'with missing token' do
@@ -211,4 +212,55 @@ RSpec.describe Setting::Validation::AIProviderConfig do
end
end
end
describe 'temperature support check' do
let(:config) { { provider: 'open_ai', token: 'valid' } }
before do
allow(AI::Provider::OpenAI).to receive(:ping!)
end
context 'when temperature is supported' do
before do
allow(AI::Provider::OpenAI).to receive(:check_temperature_support!).and_return(true)
end
it 'persists model_temperature_support as true' do
Setting.set(setting_name, config)
expect(Setting.get(setting_name)).to include('model_temperature_support' => true)
end
end
context 'when temperature is not supported' do
before do
allow(AI::Provider::OpenAI).to receive(:check_temperature_support!).and_return(false)
end
it 'persists model_temperature_support as false' do
Setting.set(setting_name, config)
expect(Setting.get(setting_name)).to include('model_temperature_support' => false)
end
end
context 'when temperature check raises an error' do
before do
allow(AI::Provider::OpenAI).to receive(:check_temperature_support!).and_raise(AI::Provider::CheckTemperatureSupportError, 'check failed')
end
it 'does cause validation failure' do
expect { Setting.set(setting_name, config) }
.to raise_error(ActiveRecord::RecordInvalid)
end
end
it 'calls check_temperature_support! after successful ping' do
allow(AI::Provider::OpenAI).to receive(:check_temperature_support!).and_return(true)
Setting.set(setting_name, config)
expect(AI::Provider::OpenAI).to have_received(:check_temperature_support!)
end
end
end

View File

@@ -70,7 +70,7 @@ RSpec.describe 'AI > Provider', authenticated_as: :admin, type: :system do
.and(have_field('OCR Model'))
check_switch_field_value('ocr_active', false)
find('select[name=provider]').select('Azure AI')
find('select[name=provider]').select('Azure AI (legacy deployment-based endpoints)')
expect(page)
.to have_field('URL')
.and(have_field('Token'))

View File

@@ -0,0 +1,120 @@
---
http_interactions:
- request:
method: post
uri: https://api.openai.com/v1/chat/completions
body:
encoding: UTF-8
string: '{"model":"gpt-4.1","messages":[{"role":"user","content":"This is a
connection test. Return in unprettified JSON ''{ \"connected\": \"true\" }''
if you got the message. Respond in plain JSON format only and do not wrap
it in code block markers."}],"response_format":{"type":"json_object"},"stream":false,"store":false}'
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
Content-Type:
- application/json; charset=utf-8
Authorization:
- Bearer <OPEN_AI_TOKEN>
response:
status:
code: 200
message: OK
headers:
Date:
- Mon, 06 Apr 2026 09:25:01 GMT
Content-Type:
- application/json
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cf-Ray:
- 9e7fbe22c8dc3477-MAN
Cf-Cache-Status:
- DYNAMIC
Server:
- cloudflare
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
X-Content-Type-Options:
- nosniff
Access-Control-Expose-Headers:
- X-Request-ID
Openai-Organization:
- zammad-gmbh
Openai-Processing-Ms:
- '335'
Openai-Project:
- proj_3ITPzFJI71Ao0y739ux6T85o
Openai-Version:
- '2020-10-01'
X-Openai-Proxy-Wasm:
- v0.1
X-Ratelimit-Limit-Requests:
- '5000'
X-Ratelimit-Limit-Tokens:
- '800000'
X-Ratelimit-Remaining-Requests:
- '4999'
X-Ratelimit-Remaining-Tokens:
- '799953'
X-Ratelimit-Reset-Requests:
- 12ms
X-Ratelimit-Reset-Tokens:
- 3ms
X-Request-Id:
- req_e23447a583ab427b89185a2397323324
Set-Cookie:
- __cf_bm=tNzJDqUIA1cyn6KMNdHJXQlJoToMv8V7.kY3Fe9JslE-1775467499.9651556-1.0.1.1-wutSST9yqPpq0YHxnLaSQWz5nzpwyKmHCr7zVbI8ZxBcqKdO3w_wUP8SEhnckpECT6GWaZTsXxUxhMahwtMhW2X2rXc1VmpvPZ.vHSUXIbtEUYbMhxJh1p68h7b0MlLh;
HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Mon, 06 Apr 2026
09:55:01 GMT
Alt-Svc:
- h3=":443"; ma=86400
body:
encoding: ASCII-8BIT
string: |
{
"id": "chatcmpl-DRaUW8SLWk9pnnuET4BB6siBDEK4c",
"object": "chat.completion",
"created": 1775467500,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{ \"connected\": \"true\" }",
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 51,
"completion_tokens": 8,
"total_tokens": 59,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_966651883e"
}
recorded_at: Mon, 06 Apr 2026 09:25:01 GMT
recorded_with: VCR 6.4.0