mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 20:50:20 +08:00
fix(provider): split partial model fetch warnings from errors
This commit is contained in:
@@ -99,6 +99,7 @@ static PROVIDER_QUERY_POOL_LOAD_BALANCE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
struct ProviderQueryKeyFetchResult {
|
||||
models: Vec<Value>,
|
||||
error: Option<String>,
|
||||
warning: Option<String>,
|
||||
from_cache: bool,
|
||||
has_success: bool,
|
||||
}
|
||||
@@ -288,6 +289,7 @@ fn provider_query_codex_preset_fallback(
|
||||
Some(ProviderQueryKeyFetchResult {
|
||||
models: aggregate_models_for_cache(&models),
|
||||
error: None,
|
||||
warning: None,
|
||||
from_cache: false,
|
||||
has_success: true,
|
||||
})
|
||||
@@ -427,6 +429,7 @@ async fn provider_query_fetch_models_for_key(
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models,
|
||||
error: None,
|
||||
warning: None,
|
||||
from_cache: true,
|
||||
has_success: true,
|
||||
});
|
||||
@@ -444,6 +447,7 @@ async fn provider_query_fetch_models_for_key(
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models,
|
||||
error: None,
|
||||
warning: None,
|
||||
from_cache: false,
|
||||
has_success: true,
|
||||
});
|
||||
@@ -451,6 +455,7 @@ async fn provider_query_fetch_models_for_key(
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models: Vec::new(),
|
||||
error: Some(ADMIN_PROVIDER_QUERY_NO_ACTIVE_ENDPOINT_DETAIL.to_string()),
|
||||
warning: None,
|
||||
from_cache: false,
|
||||
has_success: false,
|
||||
});
|
||||
@@ -477,6 +482,7 @@ async fn provider_query_fetch_models_for_key(
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models: Vec::new(),
|
||||
error: Some(all_errors.join("; ")),
|
||||
warning: None,
|
||||
from_cache: false,
|
||||
has_success: false,
|
||||
});
|
||||
@@ -492,6 +498,7 @@ async fn provider_query_fetch_models_for_key(
|
||||
return Ok(ProviderQueryKeyFetchResult {
|
||||
models: Vec::new(),
|
||||
error: Some(all_errors.join("; ")),
|
||||
warning: None,
|
||||
from_cache: false,
|
||||
has_success: false,
|
||||
});
|
||||
@@ -516,18 +523,25 @@ async fn provider_query_fetch_models_for_key(
|
||||
}
|
||||
}
|
||||
|
||||
let mut error = if all_errors.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let has_models = !unique_models.is_empty();
|
||||
let mut error = if !has_models && !all_errors.is_empty() {
|
||||
Some(all_errors.join("; "))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if unique_models.is_empty() && error.is_none() {
|
||||
let warning = if has_models && !all_errors.is_empty() {
|
||||
Some(all_errors.join("; "))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !has_models && error.is_none() {
|
||||
error = Some(ADMIN_PROVIDER_QUERY_NO_MODELS_FROM_ENDPOINT_DETAIL.to_string());
|
||||
}
|
||||
|
||||
Ok(ProviderQueryKeyFetchResult {
|
||||
models: provider_query_filter_models_for_key(provider, key, unique_models),
|
||||
error,
|
||||
warning,
|
||||
from_cache: false,
|
||||
has_success: outcome.has_success,
|
||||
})
|
||||
@@ -588,6 +602,7 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
"data": {
|
||||
"models": models,
|
||||
"error": result.error,
|
||||
"warning": result.warning,
|
||||
"from_cache": result.from_cache,
|
||||
},
|
||||
"provider": provider_query_provider_payload(&provider),
|
||||
@@ -620,6 +635,7 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
"data": {
|
||||
"models": models,
|
||||
"error": serde_json::Value::Null,
|
||||
"warning": serde_json::Value::Null,
|
||||
"from_cache": true,
|
||||
"keys_total": active_key_count,
|
||||
"keys_cached": active_key_count,
|
||||
@@ -643,6 +659,7 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
|
||||
let mut all_models = Vec::new();
|
||||
let mut all_errors = Vec::new();
|
||||
let mut all_warnings = Vec::new();
|
||||
let mut cache_hit_count = 0usize;
|
||||
let mut fetch_count = 0usize;
|
||||
for key in &ordered_keys {
|
||||
@@ -657,6 +674,13 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
error
|
||||
));
|
||||
}
|
||||
if let Some(warning) = result.warning {
|
||||
all_warnings.push(format!(
|
||||
"Key {}: {}",
|
||||
provider_query_key_display_name(key),
|
||||
warning
|
||||
));
|
||||
}
|
||||
if result.from_cache {
|
||||
cache_hit_count += 1;
|
||||
} else {
|
||||
@@ -682,10 +706,17 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
provider_query_write_provider_cached_models(state, &provider.id, &models).await;
|
||||
}
|
||||
let success = !models.is_empty();
|
||||
let mut error = if all_errors.is_empty() {
|
||||
None
|
||||
let mut all_issues = all_errors;
|
||||
all_issues.extend(all_warnings);
|
||||
let mut error = if !success && !all_issues.is_empty() {
|
||||
Some(all_issues.join("; "))
|
||||
} else {
|
||||
Some(all_errors.join("; "))
|
||||
None
|
||||
};
|
||||
let warning = if success && !all_issues.is_empty() {
|
||||
Some(all_issues.join("; "))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !success && error.is_none() {
|
||||
error = Some(ADMIN_PROVIDER_QUERY_NO_MODELS_FROM_KEY_DETAIL.to_string());
|
||||
@@ -697,6 +728,7 @@ pub(crate) async fn build_admin_provider_query_models_response(
|
||||
"data": {
|
||||
"models": models,
|
||||
"error": error,
|
||||
"warning": warning,
|
||||
"from_cache": fetch_count == 0 && cache_hit_count > 0,
|
||||
"keys_total": active_key_count,
|
||||
"keys_cached": cache_hit_count,
|
||||
|
||||
@@ -12,9 +12,9 @@ pub use config::{
|
||||
};
|
||||
pub use logic::{
|
||||
aggregate_models_for_cache, apply_model_filters, build_models_fetch_url,
|
||||
endpoint_supports_rust_models_fetch, extract_error_message, json_string_list,
|
||||
merge_upstream_metadata, parse_models_response, parse_models_response_page,
|
||||
parse_windsurf_model_configs_response, preset_models_for_provider,
|
||||
deepseek_anthropic_models_fetch_uses_openai_auth, endpoint_supports_rust_models_fetch,
|
||||
extract_error_message, json_string_list, merge_upstream_metadata, parse_models_response,
|
||||
parse_models_response_page, parse_windsurf_model_configs_response, preset_models_for_provider,
|
||||
provider_type_uses_preset_models, select_models_fetch_endpoint,
|
||||
selected_models_fetch_endpoints, ModelFetchRunSummary, ModelsFetchPage, ModelsFetchSuccess,
|
||||
};
|
||||
|
||||
@@ -74,8 +74,10 @@ pub fn build_models_fetch_url(
|
||||
let provider_type = provider_type.trim().to_ascii_lowercase();
|
||||
let url = if provider_type == "codex" && api_format.starts_with("openai:") {
|
||||
build_codex_models_url(base_url)
|
||||
} else if api_format.starts_with("openai:") || api_format.starts_with("claude:") {
|
||||
} else if api_format.starts_with("openai:") {
|
||||
build_v1_models_url(base_url)
|
||||
} else if api_format.starts_with("claude:") {
|
||||
build_claude_models_url(base_url)
|
||||
} else if api_format.starts_with("gemini:") {
|
||||
build_gemini_models_url(base_url)
|
||||
} else {
|
||||
@@ -608,6 +610,51 @@ fn build_v1_models_url(base_url: &str) -> Option<String> {
|
||||
build_openai_compatible_models_url(base_url)
|
||||
}
|
||||
|
||||
fn build_claude_models_url(base_url: &str) -> Option<String> {
|
||||
if let Some(url) = build_deepseek_anthropic_models_url(base_url) {
|
||||
return Some(url);
|
||||
}
|
||||
|
||||
let (trimmed_base_url, base_query) = split_url_query(base_url);
|
||||
let trimmed_base_url = trimmed_base_url.trim_end_matches('/');
|
||||
if trimmed_base_url.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut url = if trimmed_base_url.ends_with("/v1") {
|
||||
format!("{trimmed_base_url}/models")
|
||||
} else {
|
||||
format!("{trimmed_base_url}/v1/models")
|
||||
};
|
||||
if let Some(query) = base_query.filter(|value| !value.trim().is_empty()) {
|
||||
url.push('?');
|
||||
url.push_str(query);
|
||||
}
|
||||
Some(url)
|
||||
}
|
||||
|
||||
pub fn deepseek_anthropic_models_fetch_uses_openai_auth(base_url: &str) -> bool {
|
||||
build_deepseek_anthropic_models_url(base_url).is_some()
|
||||
}
|
||||
|
||||
fn build_deepseek_anthropic_models_url(base_url: &str) -> Option<String> {
|
||||
let (trimmed_base_url, base_query) = split_url_query(base_url);
|
||||
let trimmed_base_url = trimmed_base_url.trim_end_matches('/');
|
||||
let normalized = trimmed_base_url.to_ascii_lowercase();
|
||||
if normalized != "https://api.deepseek.com/anthropic"
|
||||
&& normalized != "https://api.deepseek.com/anthropic/v1"
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut url = "https://api.deepseek.com/models".to_string();
|
||||
if let Some(query) = base_query.filter(|value| !value.trim().is_empty()) {
|
||||
url.push('?');
|
||||
url.push_str(query);
|
||||
}
|
||||
Some(url)
|
||||
}
|
||||
|
||||
fn build_codex_models_url(base_url: &str) -> Option<String> {
|
||||
if let Some(url) = build_bigmodel_coding_models_url(base_url) {
|
||||
return Some(url);
|
||||
@@ -1029,7 +1076,22 @@ mod tests {
|
||||
"https://proxy.example.com/api"
|
||||
),
|
||||
Some((
|
||||
"https://proxy.example.com/api/models".to_string(),
|
||||
"https://proxy.example.com/api/v1/models".to_string(),
|
||||
"claude:messages".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_models_fetch_url_uses_deepseek_openai_models_for_anthropic_base() {
|
||||
assert_eq!(
|
||||
build_models_fetch_url(
|
||||
"custom",
|
||||
"claude:messages",
|
||||
"https://api.deepseek.com/anthropic"
|
||||
),
|
||||
Some((
|
||||
"https://api.deepseek.com/models".to_string(),
|
||||
"claude:messages".to_string()
|
||||
))
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ use aether_provider_transport::{
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::build_models_fetch_url;
|
||||
use crate::{build_models_fetch_url, deepseek_anthropic_models_fetch_uses_openai_auth};
|
||||
|
||||
const OPENAI_RESPONSES_USER_AGENT: &str = "openai-codex/1.0";
|
||||
const CLAUDE_CLI_USER_AGENT: &str = "claude-code/1.0.1";
|
||||
@@ -98,19 +98,29 @@ pub async fn build_standard_models_fetch_execution_plan(
|
||||
let provider_type = transport.provider.provider_type.trim().to_ascii_lowercase();
|
||||
let is_codex_openai_models_fetch =
|
||||
provider_type == "codex" && api_format.starts_with("openai:");
|
||||
let is_deepseek_anthropic_models_fetch = api_format.starts_with("claude:")
|
||||
&& deepseek_anthropic_models_fetch_uses_openai_auth(&transport.endpoint.base_url);
|
||||
let mut headers = standard_models_fetch_headers(&api_format, &provider_type);
|
||||
if is_codex_openai_models_fetch {
|
||||
headers.insert("accept".to_string(), "application/json".to_string());
|
||||
}
|
||||
if is_deepseek_anthropic_models_fetch {
|
||||
headers.remove("anthropic-version");
|
||||
headers.insert("accept".to_string(), "application/json".to_string());
|
||||
}
|
||||
let mut protected_headers = Vec::<String>::new();
|
||||
|
||||
if api_format.starts_with("openai:") || api_format.starts_with("claude:") {
|
||||
let (auth_header_name, auth_header_value) =
|
||||
resolve_standard_header_auth(runtime, transport)
|
||||
let resolved_auth = if is_deepseek_anthropic_models_fetch {
|
||||
resolve_oauth_header_auth(runtime, transport)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
"Rust models fetch auth resolution is not supported for this key".to_string()
|
||||
})?;
|
||||
.or_else(|| resolve_local_openai_bearer_auth(transport))
|
||||
} else {
|
||||
resolve_standard_header_auth(runtime, transport).await?
|
||||
};
|
||||
let (auth_header_name, auth_header_value) = resolved_auth.ok_or_else(|| {
|
||||
"Rust models fetch auth resolution is not supported for this key".to_string()
|
||||
})?;
|
||||
insert_non_empty_auth_header(
|
||||
&mut headers,
|
||||
&mut protected_headers,
|
||||
@@ -590,7 +600,9 @@ fn build_standard_models_fetch_url(
|
||||
)
|
||||
.ok_or_else(|| "Rust models fetch does not support this provider format yet".to_string())?;
|
||||
|
||||
if api_format.starts_with("claude:") {
|
||||
if api_format.starts_with("claude:")
|
||||
&& !deepseek_anthropic_models_fetch_uses_openai_auth(&transport.endpoint.base_url)
|
||||
{
|
||||
url = append_query_param(url, "limit", "100");
|
||||
if let Some(after_id) = after_id.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
url = append_query_param(url, "after_id", after_id);
|
||||
@@ -914,6 +926,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn builds_deepseek_anthropic_models_fetch_plan_with_openai_models_endpoint() {
|
||||
let runtime = TestRuntime {
|
||||
oauth_auth: None,
|
||||
proxy: None,
|
||||
};
|
||||
let mut transport = sample_transport("custom", "claude:messages", "api_key");
|
||||
transport.endpoint.base_url = "https://api.deepseek.com/anthropic".to_string();
|
||||
transport.key.decrypted_auth_config = None;
|
||||
let plan = build_models_fetch_execution_plan(&runtime, &transport)
|
||||
.await
|
||||
.expect("plan");
|
||||
|
||||
assert_eq!(plan.url, "https://api.deepseek.com/models");
|
||||
assert_eq!(
|
||||
plan.headers.get("authorization").map(String::as_str),
|
||||
Some("Bearer secret")
|
||||
);
|
||||
assert!(!plan.headers.contains_key("x-api-key"));
|
||||
assert!(!plan.headers.contains_key("anthropic-version"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn builds_gemini_models_fetch_plan_with_browser_headers_and_query_auth() {
|
||||
let runtime = TestRuntime {
|
||||
|
||||
@@ -80,7 +80,7 @@ fn openai_image_base_includes_operation_path(base_url: &str) -> bool {
|
||||
pub fn build_claude_messages_url(upstream_base_url: &str, query: Option<&str>) -> String {
|
||||
let (trimmed, base_query) = split_base_url_query(upstream_base_url);
|
||||
let trimmed = trimmed.trim_end_matches('/');
|
||||
let mut url = if v1_compatible_base_includes_api_root(trimmed) {
|
||||
let mut url = if trimmed.ends_with("/v1") {
|
||||
format!("{trimmed}/messages")
|
||||
} else {
|
||||
format!("{trimmed}/v1/messages")
|
||||
@@ -524,11 +524,11 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
build_claude_messages_url("https://proxy.example.com/api", None),
|
||||
"https://proxy.example.com/api/messages"
|
||||
"https://proxy.example.com/api/v1/messages"
|
||||
);
|
||||
assert_eq!(
|
||||
build_claude_messages_url("https://proxy.example.com/anthropic", None),
|
||||
"https://proxy.example.com/anthropic/messages"
|
||||
"https://proxy.example.com/anthropic/v1/messages"
|
||||
);
|
||||
assert_eq!(
|
||||
build_claude_messages_url("https://api.anthropic.example", None),
|
||||
|
||||
@@ -568,6 +568,7 @@ export interface ProviderModelsQueryResponse {
|
||||
model_test_capabilities?: ModelTestCapabilities | null
|
||||
}>
|
||||
error?: string
|
||||
warning?: string
|
||||
from_cache?: boolean
|
||||
}
|
||||
provider: {
|
||||
|
||||
@@ -357,8 +357,8 @@ async function applyAutoMatchFromKey(key: AutoMatchKey) {
|
||||
const result = await fetchCachedModels(props.providerId, key.id, true)
|
||||
if (!props.open) return
|
||||
|
||||
if (result.error && result.models.length > 0) {
|
||||
showWarning(`部分格式获取失败: ${result.error}`)
|
||||
if (result.warning) {
|
||||
showWarning(`部分格式获取失败: ${result.warning}`)
|
||||
}
|
||||
|
||||
if (result.models.length === 0) {
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
<Input
|
||||
:model-value="getDisplayedPath(endpoint)"
|
||||
:placeholder="getEndpointDefaultPath(endpoint) || '留空使用默认'"
|
||||
:disabled="isFixedProvider"
|
||||
@update:model-value="(v) => updateEndpointField(endpoint.id, 'path', v)"
|
||||
/>
|
||||
<p
|
||||
@@ -1878,9 +1877,6 @@ function getEndpointDefaultPath(endpoint: ProviderEndpoint): string {
|
||||
}
|
||||
|
||||
function getDisplayedPath(endpoint: ProviderEndpoint): string {
|
||||
if (isFixedProvider.value) {
|
||||
return getDefaultPath(endpoint.api_format, endpoint.base_url)
|
||||
}
|
||||
return getEndpointEditState(endpoint.id)?.path ?? (endpoint.custom_path || '')
|
||||
}
|
||||
|
||||
@@ -3217,13 +3213,13 @@ async function saveEndpoint(endpoint: ProviderEndpoint) {
|
||||
|
||||
savingEndpointId.value = endpoint.id
|
||||
try {
|
||||
// 仅提交变更字段,避免 fixed provider 因 base_url/custom_path 被锁定而更新失败
|
||||
// 仅提交变更字段;fixed provider 锁定 base_url,但允许覆盖 custom_path。
|
||||
const payload: Record<string, unknown> = {}
|
||||
|
||||
if (!isFixedProvider.value) {
|
||||
if (state.url !== endpoint.base_url) payload.base_url = state.url
|
||||
if (state.path !== (endpoint.custom_path || '')) payload.custom_path = state.path || null
|
||||
}
|
||||
if (state.path !== (endpoint.custom_path || '')) payload.custom_path = state.path || null
|
||||
|
||||
if (hasRulesChanges(endpoint)) payload.header_rules = rulesToHeaderRules(state.rules)
|
||||
if (hasResponseHeaderRulesChanges(endpoint)) {
|
||||
|
||||
@@ -295,9 +295,8 @@ async function fetchUpstreamModels() {
|
||||
.filter((m: UpstreamModel) => !existingModelIds.value.has(m.id))
|
||||
.map((m: UpstreamModel) => m.id)
|
||||
hasQueried.value = true
|
||||
// 如果有部分失败,显示警告提示
|
||||
if (result.error) {
|
||||
showWarning(result.error, '部分格式获取失败')
|
||||
if (result.warning) {
|
||||
showWarning(result.warning, '部分格式获取失败')
|
||||
}
|
||||
} else if (result.error) {
|
||||
errorMessage.value = result.error
|
||||
|
||||
@@ -691,8 +691,8 @@ async function fetchUpstreamModels(forceRefresh = false) {
|
||||
// 获取上游模型后,从自定义模型列表中移除已变成已知的模型
|
||||
const upstreamIds = new Set(result.models.map((m: UpstreamModel) => m.id))
|
||||
allCustomModels.value = allCustomModels.value.filter(m => !upstreamIds.has(m))
|
||||
if (result.error) {
|
||||
showWarning(result.error, '部分格式获取失败')
|
||||
if (result.warning) {
|
||||
showWarning(result.warning, '部分格式获取失败')
|
||||
}
|
||||
} else if (result.error) {
|
||||
showError(result.error, '获取上游模型失败')
|
||||
|
||||
@@ -564,12 +564,11 @@ async function fetchUpstreamModels() {
|
||||
const mergedCustom = new Set([...allCustomNames.value, ...customFromSelected])
|
||||
allCustomNames.value = Array.from(mergedCustom).filter(name => !upstreamIds.has(name))
|
||||
}
|
||||
if (result.warning) {
|
||||
showWarning(result.warning, '部分格式获取失败')
|
||||
}
|
||||
if (result.error) {
|
||||
if (result.models.length > 0) {
|
||||
showWarning(result.error, '部分格式获取失败')
|
||||
} else {
|
||||
showError(result.error, '获取上游模型失败')
|
||||
}
|
||||
showError(result.error, '获取上游模型失败')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '获取上游模型列表失败'), '错误')
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('endpoint default paths', () => {
|
||||
})).toBe('/responses')
|
||||
})
|
||||
|
||||
it('drops /v1 from Claude Messages defaults for v1 and unversioned api roots', () => {
|
||||
it('keeps /v1 for Claude Messages defaults unless base URL already ends with v1', () => {
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'claude:messages',
|
||||
providerType: 'custom',
|
||||
@@ -107,13 +107,13 @@ describe('endpoint default paths', () => {
|
||||
providerType: 'custom',
|
||||
baseUrl: 'https://proxy.example.com/api',
|
||||
apiFormats,
|
||||
})).toBe('/messages')
|
||||
})).toBe('/v1/messages')
|
||||
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'claude:messages',
|
||||
providerType: 'custom',
|
||||
baseUrl: 'https://proxy.example.com/anthropic',
|
||||
apiFormats,
|
||||
})).toBe('/messages')
|
||||
})).toBe('/v1/messages')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,7 +69,7 @@ function openAiCompatibleBaseIncludesApiRoot(baseUrl?: string | null): boolean {
|
||||
}
|
||||
|
||||
function v1CompatibleBaseIncludesApiRoot(baseUrl?: string | null): boolean {
|
||||
return baseUrlEndsWithV1Root(baseUrl) || baseUrlHasPathApiRoot(baseUrl)
|
||||
return baseUrlEndsWithV1Root(baseUrl)
|
||||
}
|
||||
|
||||
function stripV1PrefixForApiRoot(path: string): string {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { UpstreamModel } from '@/api/endpoints/types'
|
||||
|
||||
export type { UpstreamModel }
|
||||
|
||||
type FetchResult = { models: UpstreamModel[]; error?: string; fromCache?: boolean }
|
||||
type FetchResult = { models: UpstreamModel[]; error?: string; warning?: string; fromCache?: boolean }
|
||||
|
||||
// 进行中的请求(用于去重并发请求)
|
||||
const pendingRequests = new Map<string, Promise<FetchResult>>()
|
||||
@@ -54,14 +54,14 @@ export function useUpstreamModelsCache() {
|
||||
const response = await adminApi.queryProviderModels(providerId, apiKeyId, forceRefresh)
|
||||
|
||||
if (response.success && response.data?.models) {
|
||||
const partialWarning = response.data.warning ?? response.data.error
|
||||
return {
|
||||
models: response.data.models,
|
||||
// 传递部分格式获取失败的 warning(后端 success=true 但仍可能附带 error)
|
||||
error: response.data.error ? parseUpstreamModelError(response.data.error) : undefined,
|
||||
warning: partialWarning ? parseUpstreamModelError(partialWarning) : undefined,
|
||||
fromCache: response.data.from_cache
|
||||
}
|
||||
} else {
|
||||
const rawError = response.data?.error || '获取上游模型失败'
|
||||
const rawError = response.data?.error || response.data?.warning || '获取上游模型失败'
|
||||
return { models: [], error: parseUpstreamModelError(rawError) }
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user