Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions crates/agent-gateway/internal/proto/v2/gateway.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/agent-gateway/proto/v2/gateway.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,8 @@ message ProviderModelsRequest {
string base_url = 2;
string api_key = 3;
bool use_system_proxy = 4;
// 可选的模型列表完整地址;非空时跳过基于 base_url 的端点推导。
string models_url = 5;
}

message ProviderModelsResponse {
Expand Down
13 changes: 13 additions & 0 deletions crates/agent-gateway/web/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,12 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.compatible": "兼容",
"settings.providerName": "分组名称",
"settings.baseUrl": "Base URL",
"settings.providerFullUrl": "完整 URL",
"settings.providerFullUrlHint": "开启后,此地址将作为最终请求地址使用,不再自动拼接接口路径。",
"settings.providerModelsUrl": "模型列表 URL(可选)",
"settings.providerModelsUrlPlaceholder": "例如:https://api.example.com/v1/models",
"settings.providerModelsUrlHint":
"留空时根据 Base URL 自动推导;填写后仅用此地址刷新模型,不影响聊天请求。",
"settings.apiKey": "API Key",
"settings.customHeaders": "自定义请求头",
"settings.providerDialogNavigation": "供应商配置导航",
Expand Down Expand Up @@ -3673,6 +3679,13 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.compatible": "Compatible",
"settings.providerName": "Name",
"settings.baseUrl": "Base URL",
"settings.providerFullUrl": "Full URL",
"settings.providerFullUrlHint":
"When enabled, this address is used as the final request URL without appending an API path.",
"settings.providerModelsUrl": "Models URL (optional)",
"settings.providerModelsUrlPlaceholder": "For example: https://api.example.com/v1/models",
"settings.providerModelsUrlHint":
"Leave blank to derive it from the Base URL. This address is only used to refresh models and does not affect chat requests.",
"settings.apiKey": "API Key",
"settings.customHeaders": "Custom request headers",
"settings.providerDialogNavigation": "Provider configuration",
Expand Down
3 changes: 3 additions & 0 deletions crates/agent-gateway/web/src/lib/gatewaySocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2760,12 +2760,14 @@ export class GatewayWebSocketClient {
baseUrl: string,
apiKey: string,
useSystemProxy = false,
modelsUrl = "",
): Promise<unknown> {
return this.requestWithRecovery("provider.models", {
type,
base_url: baseUrl,
api_key: apiKey,
use_system_proxy: useSystemProxy,
models_url: modelsUrl,
});
}

Expand Down Expand Up @@ -3903,6 +3905,7 @@ export type GatewayWebSocketClientLike = {
baseUrl: string,
apiKey: string,
useSystemProxy?: boolean,
modelsUrl?: string,
): Promise<unknown>;
providerUsageQuery<T = unknown>(providerId: string, refresh: boolean): Promise<T>;
providerUsageTest<T = unknown>(providerId: string, configJson: string): Promise<T>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ function agentRequestPayload(type: string, body: J): GatewayEnvelope["payload"]
baseUrl: trimStr(body.base_url),
apiKey: trimStr(body.api_key),
useSystemProxy: bool(body.use_system_proxy),
modelsUrl: trimStr(body.models_url),
}),
};
case "provider.usage.query":
Expand Down

Large diffs are not rendered by default.

40 changes: 33 additions & 7 deletions crates/agent-gateway/web/src/lib/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,10 @@ export type CustomProvider = {
name: string;
type: ProviderId;
baseUrl: string;
/** 将 baseUrl 作为最终请求地址,本地反代不再追加协议端点路径。 */
isFullUrl: boolean;
/** 可选的模型列表完整地址;Gemini 始终使用自动端点发现。 */
modelsUrl?: string;
apiKey: string;
apiKeyConfigured?: boolean;
customHeaders?: { key: string; value: string }[];
Expand Down Expand Up @@ -521,22 +525,31 @@ function normalizeCodexRequestFormat(input: unknown): CodexRequestFormat | undef
function normalizeCodexRouting(
baseUrlInput: unknown,
requestFormatInput: unknown,
isFullUrl = false,
): {
baseUrl: string;
requestFormat: CodexRequestFormat;
} {
let baseUrl = normalizeBaseUrl(typeof baseUrlInput === "string" ? baseUrlInput : "");
let requestFormat = normalizeCodexRequestFormat(requestFormatInput);
const lower = baseUrl.toLowerCase();
let routePath = lower;
if (isFullUrl) {
try {
routePath = new URL(baseUrl).pathname.replace(/\/+$/, "").toLowerCase();
} catch {
// URL validation is handled by the desktop request proxy.
}
}

if (lower.endsWith(CODEX_CHAT_COMPLETIONS_SUFFIX)) {
baseUrl = baseUrl.slice(0, -CODEX_CHAT_COMPLETIONS_SUFFIX.length);
if (routePath.endsWith(CODEX_CHAT_COMPLETIONS_SUFFIX)) {
if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_CHAT_COMPLETIONS_SUFFIX.length);
requestFormat ??= "openai-completions";
} else if (lower.endsWith(CODEX_RESPONSES_SUFFIX)) {
baseUrl = baseUrl.slice(0, -CODEX_RESPONSES_SUFFIX.length);
} else if (routePath.endsWith(CODEX_RESPONSES_SUFFIX)) {
if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_RESPONSES_SUFFIX.length);
requestFormat ??= "openai-responses";
} else if (lower.endsWith(CODEX_RESPONSE_SUFFIX)) {
baseUrl = baseUrl.slice(0, -CODEX_RESPONSE_SUFFIX.length);
} else if (routePath.endsWith(CODEX_RESPONSE_SUFFIX)) {
if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_RESPONSE_SUFFIX.length);
requestFormat ??= "openai-responses";
}

Expand All @@ -553,6 +566,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] {
name: "Anthropic",
type: "claude_code",
baseUrl: "https://api.anthropic.com/v1",
isFullUrl: false,
apiKey: "",
customHeaders: [],
models: [],
Expand All @@ -568,6 +582,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] {
name: "OpenAI",
type: "codex",
baseUrl: "https://api.openai.com/v1",
isFullUrl: false,
apiKey: "",
customHeaders: [],
models: [],
Expand All @@ -584,6 +599,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] {
name: "Gemini",
type: "gemini",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
isFullUrl: false,
apiKey: "",
customHeaders: [],
models: [],
Expand All @@ -599,6 +615,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] {
name: "Grok",
type: "xai",
baseUrl: "https://api.x.ai/v1",
isFullUrl: false,
apiKey: "",
customHeaders: [],
models: [],
Expand Down Expand Up @@ -1610,9 +1627,14 @@ function normalizeUsageQueryConfig(input: unknown): UsageQueryConfig {
export function normalizeCustomProvider(input: unknown): CustomProvider {
const obj = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
const type = normalizeProviderId(obj.type);
const isFullUrl = obj.isFullUrl === true;
const codexRouting =
type === "codex" || type === "xai"
? normalizeCodexRouting(obj.baseUrl, type === "xai" ? "openai-responses" : obj.requestFormat)
? normalizeCodexRouting(
obj.baseUrl,
type === "xai" ? "openai-responses" : obj.requestFormat,
isFullUrl,
)
: undefined;
const models = normalizeProviderModelConfigs(obj.models, type);
const modelOrder = normalizeProviderModelOrder(obj.modelOrder, models);
Expand All @@ -1627,6 +1649,10 @@ export function normalizeCustomProvider(input: unknown): CustomProvider {
baseUrl: codexRouting
? codexRouting.baseUrl
: normalizeBaseUrl(typeof obj.baseUrl === "string" ? obj.baseUrl : ""),
isFullUrl,
...(type !== "gemini" && typeof obj.modelsUrl === "string" && obj.modelsUrl.trim()
? { modelsUrl: obj.modelsUrl.trim() }
: {}),
apiKey,
apiKeyConfigured: apiKey.length > 0 || obj.apiKeyConfigured === true,
customHeaders: normalizeCustomHeaders(obj.customHeaders),
Expand Down
1 change: 1 addition & 0 deletions crates/agent-gateway/web/src/shims/tauriCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export async function invoke<T>(command: string, args?: Record<string, unknown>)
String(args?.base_url ?? ""),
String(args?.api_key ?? ""),
args?.use_system_proxy === true,
String(args?.models_url ?? ""),
)) as T;
case "settings_reset_ssh_known_host": {
const host = String(args?.host ?? "").trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub struct CcsProviderImportItem {
pub provider_type: String,
pub name: String,
pub base_url: String,
pub is_full_url: bool,
pub models_url: String,
pub api_key: String,
pub request_format: String,
pub models: Vec<String>,
Expand Down Expand Up @@ -187,6 +189,19 @@ fn ccs_provider_from_value(
provider_type: provider_type.to_string(),
name: strip_ccswitch_suffix(name).to_string(),
base_url,
is_full_url: config
.get("meta")
.and_then(|meta| meta.get("isFullUrl").or_else(|| meta.get("is_full_url")))
.and_then(Value::as_bool)
.unwrap_or(false),
models_url: config
.get("meta")
.and_then(|meta| meta.get("modelsUrl").or_else(|| meta.get("models_url")))
.or_else(|| config.get("modelsUrl").or_else(|| config.get("models_url")))
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_string(),
api_key,
request_format: if provider_type == "xai" {
// Grok / xAI 在 LiveAgent 固定 Responses。
Expand Down
1 change: 1 addition & 0 deletions crates/agent-gui/src-tauri/src/services/gateway_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ pub async fn handle_provider_models(
request.base_url.trim(),
request.api_key.trim(),
request.use_system_proxy,
Some(request.models_url.trim()).filter(|value| !value.is_empty()),
)
.await?;
Ok(proto::ProviderModelsResponse { models_json })
Expand Down
Loading
Loading