mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: add API compatibility check feature
- Add check_api_compatibility Tauri command - Add API compatibility check UI in Settings page - Test all configured models with health check - Show last successful check time - Display warnings for 401/403/400 errors - Fix clippy format string warnings
This commit is contained in:
@@ -763,6 +763,168 @@ struct ModelInfo {
|
||||
owned_by: String,
|
||||
}
|
||||
|
||||
// ============ API Compatibility Check ============
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ApiCheckResult {
|
||||
model: String,
|
||||
available: bool,
|
||||
status: u16,
|
||||
error_type: Option<String>,
|
||||
error_message: Option<String>,
|
||||
time_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ApiCompatibilityResult {
|
||||
provider: String,
|
||||
overall_status: String,
|
||||
checked_at: String,
|
||||
results: Vec<ApiCheckResult>,
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn check_api_compatibility(
|
||||
state: tauri::State<'_, AppState>,
|
||||
logs: tauri::State<'_, LogState>,
|
||||
provider: String,
|
||||
) -> Result<ApiCompatibilityResult, String> {
|
||||
logs.write().await.add(
|
||||
"info",
|
||||
&format!("[API检测] 开始检测 {provider} API 兼容性..."),
|
||||
);
|
||||
|
||||
let s = state.read().await;
|
||||
let mut results: Vec<ApiCheckResult> = Vec::new();
|
||||
let mut warnings: Vec<String> = Vec::new();
|
||||
|
||||
let models_to_check = match provider.as_str() {
|
||||
"kiro" => vec!["claude-sonnet-4-5", "claude-3-7-sonnet-20250219"],
|
||||
"gemini" => vec!["gemini-2.5-flash", "gemini-2.5-pro"],
|
||||
"qwen" => vec!["qwen3-coder-plus", "qwen3-coder-flash"],
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for model in models_to_check {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// 构建简单的测试请求
|
||||
let test_request = crate::models::openai::ChatCompletionRequest {
|
||||
model: model.to_string(),
|
||||
messages: vec![crate::models::openai::ChatMessage {
|
||||
role: "user".to_string(),
|
||||
content: Some(crate::models::openai::MessageContent::Text(
|
||||
"Hi".to_string(),
|
||||
)),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
}],
|
||||
temperature: None,
|
||||
max_tokens: Some(10),
|
||||
stream: false,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
};
|
||||
|
||||
let result = match provider.as_str() {
|
||||
"kiro" => s.kiro_provider.call_api(&test_request).await,
|
||||
_ => Err("Provider not supported for direct API check".into()),
|
||||
};
|
||||
|
||||
let time_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
match result {
|
||||
Ok(resp) => {
|
||||
let status = resp.status().as_u16();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
|
||||
let (available, error_type, error_message) = if (200..300).contains(&status) {
|
||||
(true, None, None)
|
||||
} else {
|
||||
let err_type = match status {
|
||||
401 => {
|
||||
warnings.push(format!("模型 {model} 返回 401: Token 可能已过期或无效"));
|
||||
Some("AUTH_ERROR".to_string())
|
||||
}
|
||||
403 => {
|
||||
warnings.push(format!(
|
||||
"模型 {model} 返回 403: 无权访问,可能需要刷新 Token"
|
||||
));
|
||||
Some("FORBIDDEN".to_string())
|
||||
}
|
||||
400 => {
|
||||
warnings.push(format!("模型 {model} 返回 400: 请求格式可能已变更"));
|
||||
Some("BAD_REQUEST".to_string())
|
||||
}
|
||||
404 => {
|
||||
warnings.push(format!("模型 {model} 返回 404: 模型或接口可能已下线"));
|
||||
Some("NOT_FOUND".to_string())
|
||||
}
|
||||
429 => {
|
||||
warnings.push(format!("模型 {model} 返回 429: 请求过于频繁"));
|
||||
Some("RATE_LIMITED".to_string())
|
||||
}
|
||||
500..=599 => {
|
||||
warnings.push(format!("模型 {model} 返回 {status}: 服务端错误"));
|
||||
Some("SERVER_ERROR".to_string())
|
||||
}
|
||||
_ => Some("UNKNOWN_ERROR".to_string()),
|
||||
};
|
||||
(
|
||||
false,
|
||||
err_type,
|
||||
Some(body[..body.len().min(200)].to_string()),
|
||||
)
|
||||
};
|
||||
|
||||
results.push(ApiCheckResult {
|
||||
model: model.to_string(),
|
||||
available,
|
||||
status,
|
||||
error_type,
|
||||
error_message,
|
||||
time_ms,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
warnings.push(format!("模型 {model} 请求失败: {e}"));
|
||||
results.push(ApiCheckResult {
|
||||
model: model.to_string(),
|
||||
available: false,
|
||||
status: 0,
|
||||
error_type: Some("REQUEST_FAILED".to_string()),
|
||||
error_message: Some(e.to_string()),
|
||||
time_ms,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let overall_status = if results.iter().all(|r| r.available) {
|
||||
"healthy".to_string()
|
||||
} else if results.iter().any(|r| r.available) {
|
||||
"partial".to_string()
|
||||
} else {
|
||||
"error".to_string()
|
||||
};
|
||||
|
||||
let checked_at = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
logs.write().await.add(
|
||||
"info",
|
||||
&format!("[API检测] {provider} 检测完成: {overall_status}"),
|
||||
);
|
||||
|
||||
Ok(ApiCompatibilityResult {
|
||||
provider,
|
||||
overall_status,
|
||||
checked_at,
|
||||
results,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_available_models() -> Result<Vec<ModelInfo>, String> {
|
||||
Ok(vec![
|
||||
@@ -957,6 +1119,8 @@ pub fn run() {
|
||||
clear_logs,
|
||||
test_api,
|
||||
get_available_models,
|
||||
// API Compatibility
|
||||
check_api_compatibility,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -343,7 +343,7 @@ export function Dashboard() {
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||
自动监测中 (5s)
|
||||
自动监测中
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 text-sm">
|
||||
|
||||
+108
-68
@@ -122,7 +122,7 @@ export function Providers() {
|
||||
const [qwenLastSync, setQwenLastSync] = useState<Date | null>(null);
|
||||
|
||||
// Last check time (used for display)
|
||||
const [_lastCheckTime, setLastCheckTime] = useState<Date | null>(null);
|
||||
const [lastCheckTime, setLastCheckTime] = useState<Date | null>(null);
|
||||
|
||||
// OpenAI Custom state
|
||||
const [openaiStatus, setOpenaiStatus] = useState<OpenAICustomStatus | null>(
|
||||
@@ -166,80 +166,102 @@ export function Providers() {
|
||||
await loadQwenStatus();
|
||||
await loadOpenAICustomStatus();
|
||||
await loadClaudeCustomStatus();
|
||||
|
||||
// Get initial hashes
|
||||
try {
|
||||
kiroHashRef.current = await getTokenFileHash();
|
||||
geminiHashRef.current = await getGeminiTokenFileHash();
|
||||
qwenHashRef.current = await getQwenTokenFileHash();
|
||||
const kiroHash = await getTokenFileHash();
|
||||
const geminiHash = await getGeminiTokenFileHash();
|
||||
const qwenHash = await getQwenTokenFileHash();
|
||||
kiroHashRef.current = kiroHash;
|
||||
geminiHashRef.current = geminiHash;
|
||||
qwenHashRef.current = qwenHash;
|
||||
console.log("[Init] Kiro hash:", kiroHash);
|
||||
console.log("[Init] Gemini hash:", geminiHash);
|
||||
console.log("[Init] Qwen hash:", qwenHash);
|
||||
} catch (e) {
|
||||
console.error("Failed to get initial hash:", e);
|
||||
}
|
||||
};
|
||||
init();
|
||||
|
||||
const interval = setInterval(checkFileChanges, 5000);
|
||||
// Define checkFileChanges inside useEffect to avoid stale closure
|
||||
const checkFiles = async () => {
|
||||
const now = new Date();
|
||||
setLastCheckTime(now);
|
||||
console.log("[Check] Running file check at", now.toLocaleTimeString());
|
||||
|
||||
// Check Kiro
|
||||
try {
|
||||
console.log("[Check] Kiro current hash:", kiroHashRef.current);
|
||||
const kiroResult = await checkAndReloadCredentials(kiroHashRef.current);
|
||||
console.log("[Check] Kiro result:", kiroResult);
|
||||
|
||||
if (kiroResult.new_hash !== kiroHashRef.current) {
|
||||
console.log(
|
||||
"[Check] Kiro hash changed:",
|
||||
kiroHashRef.current,
|
||||
"->",
|
||||
kiroResult.new_hash,
|
||||
);
|
||||
}
|
||||
kiroHashRef.current = kiroResult.new_hash;
|
||||
|
||||
if (kiroResult.changed && kiroResult.reloaded) {
|
||||
await loadKiroStatus();
|
||||
setKiroLastSync(new Date());
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "[Kiro] 检测到凭证文件变化,已自动重新加载",
|
||||
});
|
||||
setTimeout(() => setMessage(null), 5000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Kiro check error:", e);
|
||||
}
|
||||
|
||||
// Check Gemini
|
||||
try {
|
||||
const geminiResult = await checkAndReloadGeminiCredentials(
|
||||
geminiHashRef.current,
|
||||
);
|
||||
geminiHashRef.current = geminiResult.new_hash;
|
||||
if (geminiResult.changed && geminiResult.reloaded) {
|
||||
await loadGeminiStatus();
|
||||
setGeminiLastSync(new Date());
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "[Gemini] 检测到凭证文件变化,已自动重新加载",
|
||||
});
|
||||
setTimeout(() => setMessage(null), 5000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Gemini check error:", e);
|
||||
}
|
||||
|
||||
// Check Qwen
|
||||
try {
|
||||
const qwenResult = await checkAndReloadQwenCredentials(
|
||||
qwenHashRef.current,
|
||||
);
|
||||
qwenHashRef.current = qwenResult.new_hash;
|
||||
if (qwenResult.changed && qwenResult.reloaded) {
|
||||
await loadQwenStatus();
|
||||
setQwenLastSync(new Date());
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "[Qwen] 检测到凭证文件变化,已自动重新加载",
|
||||
});
|
||||
setTimeout(() => setMessage(null), 5000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Qwen check error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(checkFiles, 5000);
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const checkFileChanges = async () => {
|
||||
setLastCheckTime(new Date());
|
||||
|
||||
// Check Kiro
|
||||
try {
|
||||
const kiroResult = await checkAndReloadCredentials(kiroHashRef.current);
|
||||
kiroHashRef.current = kiroResult.new_hash;
|
||||
if (kiroResult.changed && kiroResult.reloaded) {
|
||||
await loadKiroStatus();
|
||||
setKiroLastSync(new Date());
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "[Kiro] 检测到凭证文件变化,已自动重新加载",
|
||||
});
|
||||
setTimeout(() => setMessage(null), 5000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Kiro check error:", e);
|
||||
}
|
||||
|
||||
// Check Gemini
|
||||
try {
|
||||
const geminiResult = await checkAndReloadGeminiCredentials(
|
||||
geminiHashRef.current,
|
||||
);
|
||||
geminiHashRef.current = geminiResult.new_hash;
|
||||
if (geminiResult.changed && geminiResult.reloaded) {
|
||||
await loadGeminiStatus();
|
||||
setGeminiLastSync(new Date());
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "[Gemini] 检测到凭证文件变化,已自动重新加载",
|
||||
});
|
||||
setTimeout(() => setMessage(null), 5000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Gemini check error:", e);
|
||||
}
|
||||
|
||||
// Check Qwen
|
||||
try {
|
||||
const qwenResult = await checkAndReloadQwenCredentials(
|
||||
qwenHashRef.current,
|
||||
);
|
||||
qwenHashRef.current = qwenResult.new_hash;
|
||||
if (qwenResult.changed && qwenResult.reloaded) {
|
||||
await loadQwenStatus();
|
||||
setQwenLastSync(new Date());
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "[Qwen] 检测到凭证文件变化,已自动重新加载",
|
||||
});
|
||||
setTimeout(() => setMessage(null), 5000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Qwen check error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const loadKiroStatus = async () => {
|
||||
try {
|
||||
const status = await getKiroCredentials();
|
||||
@@ -557,9 +579,15 @@ export function Providers() {
|
||||
{formatTime(kiroLastSync)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
最后检测:{" "}
|
||||
<span className="text-foreground">
|
||||
{formatTime(lastCheckTime)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||
监测中 (5s)
|
||||
监测中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -634,9 +662,15 @@ export function Providers() {
|
||||
{formatTime(geminiLastSync)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
最后检测:{" "}
|
||||
<span className="text-foreground">
|
||||
{formatTime(lastCheckTime)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||
监测中 (5s)
|
||||
监测中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -714,9 +748,15 @@ export function Providers() {
|
||||
{formatTime(qwenLastSync)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
最后检测:{" "}
|
||||
<span className="text-foreground">
|
||||
{formatTime(lastCheckTime)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||
监测中 (5s)
|
||||
监测中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+160
-2
@@ -1,6 +1,22 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Eye, EyeOff, Copy, Check } from "lucide-react";
|
||||
import { getConfig, saveConfig, Config } from "@/hooks/useTauri";
|
||||
import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
Copy,
|
||||
Check,
|
||||
Shield,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getConfig,
|
||||
saveConfig,
|
||||
Config,
|
||||
checkApiCompatibility,
|
||||
ApiCompatibilityResult,
|
||||
} from "@/hooks/useTauri";
|
||||
|
||||
export function Settings() {
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
@@ -9,6 +25,13 @@ export function Settings() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
// API Compatibility Check
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [checkResult, setCheckResult] = useState<ApiCompatibilityResult | null>(
|
||||
null,
|
||||
);
|
||||
const [lastCheckTime, setLastCheckTime] = useState<Date | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
@@ -44,6 +67,45 @@ export function Settings() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckApiCompatibility = async (provider: string) => {
|
||||
setChecking(true);
|
||||
setCheckResult(null);
|
||||
try {
|
||||
const result = await checkApiCompatibility(provider);
|
||||
setCheckResult(result);
|
||||
setLastCheckTime(new Date());
|
||||
} catch (e) {
|
||||
setMessage(`API 检测失败: ${e}`);
|
||||
}
|
||||
setChecking(false);
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return <CheckCircle2 className="h-5 w-5 text-green-500" />;
|
||||
case "partial":
|
||||
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
|
||||
case "error":
|
||||
return <XCircle className="h-5 w-5 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return "所有模型可用";
|
||||
case "partial":
|
||||
return "部分模型可用";
|
||||
case "error":
|
||||
return "API 不可用";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
return <div>加载中...</div>;
|
||||
}
|
||||
@@ -150,6 +212,102 @@ export function Settings() {
|
||||
{saving ? "保存中..." : "保存设置"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* API 兼容性检测 */}
|
||||
<div className="max-w-2xl space-y-4 rounded-lg border bg-card p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
<h3 className="font-semibold">API 兼容性检测</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
检测当前配置的模型是否可用,识别 API 变更或认证问题
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => handleCheckApiCompatibility("kiro")}
|
||||
disabled={checking}
|
||||
className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium hover:bg-muted disabled:opacity-50"
|
||||
>
|
||||
{checking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Shield className="h-4 w-4" />
|
||||
)}
|
||||
检测 Kiro API
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{lastCheckTime && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
最后检测时间: {lastCheckTime.toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{checkResult && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(checkResult.overall_status)}
|
||||
<span className="font-medium">
|
||||
{checkResult.provider.toUpperCase()} -{" "}
|
||||
{getStatusText(checkResult.overall_status)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(checkResult.checked_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 模型检测结果 */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">模型状态:</p>
|
||||
{checkResult.results.map((r) => (
|
||||
<div
|
||||
key={r.model}
|
||||
className={`flex items-center justify-between rounded p-2 text-sm ${
|
||||
r.available ? "bg-green-50" : "bg-red-50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{r.available ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<span>{r.model}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{r.status > 0 && <span>HTTP {r.status}</span>}
|
||||
<span>{r.time_ms}ms</span>
|
||||
{r.error_type && (
|
||||
<span className="rounded bg-red-100 px-1 text-red-600">
|
||||
{r.error_type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 警告信息 */}
|
||||
{checkResult.warnings.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-yellow-600">警告:</p>
|
||||
{checkResult.warnings.map((w, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-start gap-2 rounded bg-yellow-50 p-2 text-sm text-yellow-700"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<span>{w}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -289,3 +289,28 @@ export interface ModelInfo {
|
||||
export async function getAvailableModels(): Promise<ModelInfo[]> {
|
||||
return invoke("get_available_models");
|
||||
}
|
||||
|
||||
// ============ API Compatibility Check ============
|
||||
|
||||
export interface ApiCheckResult {
|
||||
model: string;
|
||||
available: boolean;
|
||||
status: number;
|
||||
error_type: string | null;
|
||||
error_message: string | null;
|
||||
time_ms: number;
|
||||
}
|
||||
|
||||
export interface ApiCompatibilityResult {
|
||||
provider: string;
|
||||
overall_status: string;
|
||||
checked_at: string;
|
||||
results: ApiCheckResult[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export async function checkApiCompatibility(
|
||||
provider: string,
|
||||
): Promise<ApiCompatibilityResult> {
|
||||
return invoke("check_api_compatibility", { provider });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user