Skip to content

Commit 3e84ab5

Browse files
authored
Merge pull request #239 from wsp1911/main
fix: align AI connection tests with real chat behavior
2 parents c3be5b4 + f3f658c commit 3e84ab5

File tree

10 files changed

+105
-93
lines changed

10 files changed

+105
-93
lines changed

src/apps/desktop/src/api/commands.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -402,17 +402,12 @@ pub async fn test_ai_config_connection(
402402
result.response_time_ms + image_result.response_time_ms;
403403

404404
if !image_result.success {
405-
let image_error = image_result
406-
.error_details
407-
.unwrap_or_else(|| "Unknown image input test error".to_string());
408405
let merged = bitfun_core::util::types::ConnectionTestResult {
409406
success: false,
410407
response_time_ms,
411408
model_response: image_result.model_response.or(result.model_response),
412-
error_details: Some(format!(
413-
"Basic connection passed, but multimodal image input test failed: {}",
414-
image_error
415-
)),
409+
message_code: image_result.message_code,
410+
error_details: image_result.error_details,
416411
};
417412
info!(
418413
"AI config connection test completed: model={}, success={}, response_time={}ms",
@@ -425,7 +420,8 @@ pub async fn test_ai_config_connection(
425420
success: true,
426421
response_time_ms,
427422
model_response: image_result.model_response.or(result.model_response),
428-
error_details: None,
423+
message_code: result.message_code,
424+
error_details: result.error_details,
429425
};
430426
info!(
431427
"AI config connection test completed: model={}, success={}, response_time={}ms",

src/crates/core/src/infrastructure/ai/client.rs

Lines changed: 11 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -156,33 +156,6 @@ impl AIClient {
156156
)
157157
}
158158

159-
fn build_test_connection_extra_body(&self) -> Option<serde_json::Value> {
160-
let provider = self.config.format.to_ascii_lowercase();
161-
if !matches!(
162-
provider.as_str(),
163-
"openai" | "response" | "responses" | "nvidia" | "openrouter"
164-
) {
165-
return self.config.custom_request_body.clone();
166-
}
167-
168-
let mut extra_body = self
169-
.config
170-
.custom_request_body
171-
.clone()
172-
.unwrap_or_else(|| serde_json::json!({}));
173-
174-
if let Some(extra_obj) = extra_body.as_object_mut() {
175-
extra_obj
176-
.entry("temperature".to_string())
177-
.or_insert_with(|| serde_json::json!(0));
178-
extra_obj
179-
.entry("tool_choice".to_string())
180-
.or_insert_with(|| serde_json::json!("required"));
181-
}
182-
183-
Some(extra_body)
184-
}
185-
186159
fn is_gemini_api_format(api_format: &str) -> bool {
187160
matches!(
188161
api_format.to_ascii_lowercase().as_str(),
@@ -1942,8 +1915,8 @@ impl AIClient {
19421915
pub async fn test_connection(&self) -> Result<ConnectionTestResult> {
19431916
let start_time = std::time::Instant::now();
19441917

1945-
// Force a tool call to avoid false negatives: some models may answer directly when
1946-
// `tool_choice=auto`, even if they support tool calls.
1918+
// Reuse the normal chat request path so the test matches real conversations, even when
1919+
// a provider rejects stricter tool_choice settings such as "required".
19471920
let test_messages = vec![Message::user(
19481921
"Call the get_weather tool for city=Beijing. Do not answer with plain text."
19491922
.to_string(),
@@ -1961,14 +1934,7 @@ impl AIClient {
19611934
}),
19621935
}]);
19631936

1964-
let extra_body = self.build_test_connection_extra_body();
1965-
1966-
let result = if extra_body.is_some() {
1967-
self.send_message_with_extra_body(test_messages, tools, extra_body)
1968-
.await
1969-
} else {
1970-
self.send_message(test_messages, tools).await
1971-
};
1937+
let result = self.send_message(test_messages, tools).await;
19721938

19731939
match result {
19741940
Ok(response) => {
@@ -1978,16 +1944,16 @@ impl AIClient {
19781944
success: true,
19791945
response_time_ms,
19801946
model_response: Some(response.text),
1947+
message_code: None,
19811948
error_details: None,
19821949
})
19831950
} else {
19841951
Ok(ConnectionTestResult {
1985-
success: false,
1952+
success: true,
19861953
response_time_ms,
19871954
model_response: Some(response.text),
1988-
error_details: Some(
1989-
"Model did not return tool calls (tool_choice=required).".to_string(),
1990-
),
1955+
message_code: Some(ConnectionTestMessageCode::ToolCallsNotDetected),
1956+
error_details: None,
19911957
})
19921958
}
19931959
}
@@ -1999,6 +1965,7 @@ impl AIClient {
19991965
success: false,
20001966
response_time_ms,
20011967
model_response: None,
1968+
message_code: None,
20021969
error_details: Some(error_msg),
20031970
})
20041971
}
@@ -2059,6 +2026,7 @@ impl AIClient {
20592026
success: true,
20602027
response_time_ms: start_time.elapsed().as_millis() as u64,
20612028
model_response: Some(response.text),
2029+
message_code: None,
20622030
error_details: None,
20632031
})
20642032
} else {
@@ -2071,6 +2039,7 @@ impl AIClient {
20712039
success: false,
20722040
response_time_ms: start_time.elapsed().as_millis() as u64,
20732041
model_response: Some(response.text),
2042+
message_code: Some(ConnectionTestMessageCode::ImageInputCheckFailed),
20742043
error_details: Some(detail),
20752044
})
20762045
}
@@ -2082,6 +2051,7 @@ impl AIClient {
20822051
success: false,
20832052
response_time_ms: start_time.elapsed().as_millis() as u64,
20842053
model_response: None,
2054+
message_code: None,
20852055
error_details: Some(error_msg),
20862056
})
20872057
}
@@ -2130,44 +2100,6 @@ mod tests {
21302100
})
21312101
}
21322102

2133-
#[test]
2134-
fn build_test_connection_extra_body_merges_custom_body_defaults() {
2135-
let client = make_test_client(
2136-
"responses",
2137-
Some(json!({
2138-
"metadata": {
2139-
"source": "test"
2140-
}
2141-
})),
2142-
);
2143-
2144-
let extra_body = client
2145-
.build_test_connection_extra_body()
2146-
.expect("extra body");
2147-
2148-
assert_eq!(extra_body["metadata"]["source"], "test");
2149-
assert_eq!(extra_body["temperature"], 0);
2150-
assert_eq!(extra_body["tool_choice"], "required");
2151-
}
2152-
2153-
#[test]
2154-
fn build_test_connection_extra_body_preserves_existing_tool_choice() {
2155-
let client = make_test_client(
2156-
"response",
2157-
Some(json!({
2158-
"tool_choice": "auto",
2159-
"temperature": 0.3
2160-
})),
2161-
);
2162-
2163-
let extra_body = client
2164-
.build_test_connection_extra_body()
2165-
.expect("extra body");
2166-
2167-
assert_eq!(extra_body["tool_choice"], "auto");
2168-
assert_eq!(extra_body["temperature"], 0.3);
2169-
}
2170-
21712103
#[test]
21722104
fn resolves_openai_models_url_from_completion_endpoint() {
21732105
let client = AIClient::new(AIConfig {

src/crates/core/src/util/types/ai.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ pub struct GeminiUsage {
3434
pub cached_content_token_count: Option<u32>,
3535
}
3636

37+
/// Structured message codes for localized connection test messaging.
38+
#[derive(Debug, Clone, Serialize, Deserialize)]
39+
#[serde(rename_all = "snake_case")]
40+
pub enum ConnectionTestMessageCode {
41+
ToolCallsNotDetected,
42+
ImageInputCheckFailed,
43+
}
44+
3745
/// AI connection test result
3846
#[derive(Debug, Clone, Serialize, Deserialize)]
3947
pub struct ConnectionTestResult {
@@ -44,7 +52,10 @@ pub struct ConnectionTestResult {
4452
/// Model response content (if successful)
4553
#[serde(skip_serializing_if = "Option::is_none")]
4654
pub model_response: Option<String>,
47-
/// Error details (if failed)
55+
/// Structured message code for localized frontend messaging
56+
#[serde(skip_serializing_if = "Option::is_none")]
57+
pub message_code: Option<ConnectionTestMessageCode>,
58+
/// Raw error or diagnostic details
4859
#[serde(skip_serializing_if = "Option::is_none")]
4960
pub error_details: Option<String>,
5061
}

src/web-ui/src/features/onboarding/components/steps/ModelConfigStep.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { systemAPI } from '@/infrastructure/api';
1111
import { Select, Checkbox, Button, IconButton } from '@/component-library';
1212
import { PROVIDER_TEMPLATES } from '@/infrastructure/config/services/modelConfigs';
1313
import { createLogger } from '@/shared/utils/logger';
14+
import { translateConnectionTestMessage } from '@/shared/utils/aiConnectionTestMessages';
1415

1516
const log = createLogger('ModelConfigStep');
1617

@@ -39,6 +40,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
3940
);
4041
const [testStatus, setTestStatus] = useState<TestStatus>('idle');
4142
const [testError, setTestError] = useState<string>('');
43+
const [testNotice, setTestNotice] = useState<string>('');
4244
const [remoteModelOptions, setRemoteModelOptions] = useState<RemoteModelOption[]>([]);
4345
const [isFetchingRemoteModels, setIsFetchingRemoteModels] = useState(false);
4446
const [remoteModelsError, setRemoteModelsError] = useState<string>('');
@@ -250,6 +252,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
250252
setSelectedProviderId(newProviderId);
251253
setTestStatus('idle');
252254
setTestError('');
255+
setTestNotice('');
253256

254257
if (newProviderId === 'custom') {
255258
setBaseUrl('');
@@ -274,6 +277,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
274277
setModelName(value);
275278
setTestStatus('idle');
276279
setTestError('');
280+
setTestNotice('');
277281
}, []);
278282

279283
// Open help URL
@@ -309,6 +313,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
309313

310314
setTestStatus('testing');
311315
setTestError('');
316+
setTestNotice('');
312317

313318
try {
314319
const effectiveBaseUrl = baseUrl || (currentTemplate?.baseUrl || '');
@@ -321,23 +326,31 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
321326
model_name: effectiveModelName,
322327
provider: format
323328
});
329+
const localizedMessage = translateConnectionTestMessage(result.message_code, tAiModel);
324330

325331
if (result.success) {
326332
setTestStatus('success');
333+
setTestNotice(localizedMessage || result.error_details || '');
327334
log.info('Connection test passed', {
328335
provider: selectedProviderId,
329336
modelName: effectiveModelName
330337
});
331338
} else {
332339
setTestStatus('error');
333-
const errorMsg = result.error_details
334-
? `${t('model.testFailed')}\n${result.error_details}`
340+
setTestNotice('');
341+
const detailLines = [
342+
localizedMessage,
343+
result.error_details ? `${tAiModel('messages.errorDetails')}: ${result.error_details}` : undefined
344+
].filter((line): line is string => Boolean(line));
345+
const errorMsg = detailLines.length > 0
346+
? `${t('model.testFailed')}\n${detailLines.join('\n')}`
335347
: t('model.testFailed');
336348
setTestError(errorMsg);
337349
}
338350
} catch (error) {
339351
log.error('Connection test failed', error);
340352
setTestStatus('error');
353+
setTestNotice('');
341354
const rawMsg = error instanceof Error ? error.message : String(error);
342355
// Tauri command errors often have "Connection test failed: " prefix, extract the actual cause
343356
const cleanMsg = rawMsg.replace(/^Connection test failed:\s*/i, '');
@@ -511,6 +524,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
511524
setApiKey(e.target.value);
512525
setTestStatus('idle');
513526
setTestError('');
527+
setTestNotice('');
514528
}}
515529
/>
516530
{currentTemplate.helpUrl && (
@@ -542,6 +556,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
542556
}
543557
setTestStatus('idle');
544558
setTestError('');
559+
setTestNotice('');
545560
}}
546561
placeholder={t('model.baseUrl.placeholder')}
547562
options={currentTemplate.baseUrlOptions.map(opt => ({
@@ -561,6 +576,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
561576
setBaseUrl(e.target.value);
562577
setTestStatus('idle');
563578
setTestError('');
579+
setTestNotice('');
564580
}}
565581
onFocus={(e) => e.target.select()}
566582
/>
@@ -587,6 +603,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
587603
setBaseUrl(e.target.value);
588604
setTestStatus('idle');
589605
setTestError('');
606+
setTestNotice('');
590607
}}
591608
/>
592609
</div>
@@ -602,6 +619,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
602619
setModelName(value as string);
603620
setTestStatus('idle');
604621
setTestError('');
622+
setTestNotice('');
605623
}}
606624
placeholder={t('model.modelName.placeholder')}
607625
options={availableModelOptions}
@@ -644,6 +662,7 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
644662
setApiKey(e.target.value);
645663
setTestStatus('idle');
646664
setTestError('');
665+
setTestNotice('');
647666
}}
648667
/>
649668
</div>
@@ -830,6 +849,13 @@ export const ModelConfigStep: React.FC<ModelConfigStepProps> = ({ onSkipForNow }
830849
{testError}
831850
</div>
832851
)}
852+
853+
{testStatus === 'success' && testNotice && (
854+
<div className="bitfun-onboarding-model__warning">
855+
<AlertTriangle size={14} />
856+
<span>{testNotice}</span>
857+
</div>
858+
)}
833859
</>
834860
)}
835861

src/web-ui/src/infrastructure/api/service-api/AIApi.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { api } from './ApiClient';
44
import { createTauriCommandError } from '../errors/TauriCommandError';
55
import type { SendMessageRequest } from './tauri-commands';
6+
import type { ConnectionTestMessageCode } from '@/shared/utils/aiConnectionTestMessages';
67

78
export interface CreateAISessionRequest {
89
session_id?: string;
@@ -19,6 +20,7 @@ export interface ConnectionTestResult {
1920
success: boolean;
2021
response_time_ms: number;
2122
model_response?: string;
23+
message_code?: ConnectionTestMessageCode;
2224
error_details?: string;
2325
}
2426

0 commit comments

Comments
 (0)