返回 DeepSeek-Reasonix
providerEndpoint.ts
根目录 / desktop / frontend / src / lib / providerEndpoint.ts
1 import type { ProviderCatalog } from "./providerCatalogTypes";
2
3 export interface ProviderEndpointConfig {
4 kind: string;
5 baseUrl: string;
6 requestUrl?: string;
7 chatUrl?: string;
8 }
9
10 export function trimmedBaseURL(value: string): string {
11 return value.trim().replace(/\/+$/, "");
12 }
13
14 export function providerRequestURLFromConfig(
15 kind: string,
16 baseUrl: string,
17 requestUrl: string,
18 legacyChatUrl = "",
19 ): string {
20 const exactRequestURL = requestUrl.trim();
21 if (exactRequestURL) return exactRequestURL;
22 if (kind.trim().toLowerCase() === "openai") {
23 const legacyOpenAIRequestURL = legacyChatUrl.trim().replace(/\/+$/, "");
24 if (legacyOpenAIRequestURL) return legacyOpenAIRequestURL;
25 }
26 const base = trimmedBaseURL(baseUrl);
27 if (!base) return "";
28 switch (kind.trim().toLowerCase()) {
29 case "anthropic":
30 return base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
31 case "responses":
32 case "dashscope-responses":
33 return `${base}/responses`;
34 default:
35 return `${base}/chat/completions`;
36 }
37 }
38
39 export function providerBaseURLFromRequestURL(kind: string, requestUrl: string): string {
40 const exactRequestURL = requestUrl.trim();
41 if (!exactRequestURL) return "";
42 const normalizedKind = kind.trim().toLowerCase();
43 const suffixes = normalizedKind === "anthropic"
44 ? ["/v1/messages"]
45 : normalizedKind === "responses" || normalizedKind === "dashscope-responses"
46 ? ["/responses"]
47 : ["/chat/completions"];
48 try {
49 const parsed = new URL(exactRequestURL);
50 const pathname = parsed.pathname.replace(/\/+$/, "");
51 const suffix = suffixes.find((candidate) => pathname.endsWith(candidate));
52 parsed.pathname = suffix ? pathname.slice(0, -suffix.length) || "/" : pathname || "/";
53 parsed.search = "";
54 parsed.hash = "";
55 return trimmedBaseURL(parsed.toString());
56 } catch {
57 const suffix = suffixes.find((candidate) => exactRequestURL.endsWith(candidate));
58 if (suffix) return trimmedBaseURL(exactRequestURL.slice(0, -suffix.length));
59 }
60 return trimmedBaseURL(exactRequestURL);
61 }
62
63 export function providerBaseURLForSave(
64 initial: ProviderEndpointConfig | undefined,
65 effectiveKind: string,
66 effectiveRequestUrl: string,
67 ): string {
68 const requestUrl = effectiveRequestUrl.trim();
69 if (initial) {
70 const initialRequestUrl = providerRequestURLFromConfig(
71 initial.kind,
72 initial.baseUrl,
73 initial.requestUrl ?? "",
74 initial.chatUrl ?? "",
75 );
76 const kindUnchanged = initial.kind.trim().toLowerCase() === effectiveKind.trim().toLowerCase();
77 if (kindUnchanged && initialRequestUrl === requestUrl) {
78 return initial.baseUrl.trim();
79 }
80 }
81 return providerBaseURLFromRequestURL(effectiveKind, requestUrl);
82 }
83
84 // Only rewrite a standard API suffix on an explicit protocol selection. Custom
85 // paths, queries and fragments are user-owned and must stay byte-for-byte intact.
86 export function providerRequestURLForFormatChange(previousKind: string, nextKind: string, requestUrl: string): string {
87 if (previousKind === nextKind || !requestUrl) return requestUrl;
88 try {
89 const url = new URL(requestUrl);
90 if (url.search || url.hash) return requestUrl;
91 const previousSuffix = previousKind === "anthropic" ? "/messages"
92 : previousKind === "responses" ? "/responses" : previousKind === "openai" ? "/chat/completions" : "";
93 if (!previousSuffix || !url.pathname.endsWith(previousSuffix)) return requestUrl;
94 const nextSuffix = nextKind === "anthropic" ? "/messages"
95 : nextKind === "responses" ? "/responses" : nextKind === "openai" ? "/chat/completions" : "";
96 if (!nextSuffix) return requestUrl;
97 url.pathname = url.pathname.slice(0, -previousSuffix.length) + nextSuffix;
98 return url.toString();
99 } catch { return requestUrl; }
100 }
101
102 function normalizedProtocol(kind: string): string {
103 const normalized = kind.trim().toLowerCase();
104 return normalized === "dashscope-responses" ? "responses" : normalized;
105 }
106
107 function protocolSuffix(kind: string): string {
108 switch (normalizedProtocol(kind)) {
109 case "anthropic": return "/messages";
110 case "responses": return "/responses";
111 case "openai": return "/chat/completions";
112 default: return "";
113 }
114 }
115
116 export function providerCatalogRequestURL(catalog: ProviderCatalog | undefined, kind: string): string {
117 const route = catalog?.protocols?.[normalizedProtocol(kind)];
118 return route?.baseUrl ? providerRequestURLFromConfig(kind, route.baseUrl, "") : "";
119 }
120
121 function normalizedExactURL(value: string): string {
122 try {
123 const parsed = new URL(value.trim());
124 if (parsed.search || parsed.hash || parsed.username || parsed.password) return "";
125 parsed.pathname = parsed.pathname.replace(/\/+$/, "") || "/";
126 return parsed.toString();
127 } catch {
128 return "";
129 }
130 }
131
132 // Switch to a catalog route only when the current URL is one of that catalog's
133 // registered official routes. Custom gateways and query-bearing routes keep the
134 // conservative suffix-only behavior.
135 export function providerRequestURLForCatalogFormatChange(
136 previousKind: string,
137 nextKind: string,
138 requestUrl: string,
139 catalog?: ProviderCatalog,
140 ): string {
141 const current = normalizedExactURL(requestUrl);
142 if (current && catalog?.protocols) {
143 const matchesOfficialRoute = Object.entries(catalog.protocols).some(([kind, route]) =>
144 normalizedExactURL(providerRequestURLFromConfig(kind, route.baseUrl, "")) === current,
145 );
146 if (matchesOfficialRoute) {
147 const next = providerCatalogRequestURL(catalog, nextKind);
148 if (next) return next;
149 }
150 }
151 if (catalog?.protocols) return requestUrl;
152 return providerRequestURLForFormatChange(previousKind, nextKind, requestUrl);
153 }
154
155 export interface ProviderEndpointMismatchDetail {
156 mismatch: boolean;
157 recommendedUrl: string;
158 }
159
160 export function providerEndpointMismatchDetail(
161 kind: string,
162 address: string,
163 catalog?: ProviderCatalog,
164 ): ProviderEndpointMismatchDetail {
165 const recommendedUrl = providerCatalogRequestURL(catalog, kind);
166 let parsed: URL;
167 try { parsed = new URL(address); } catch { return { mismatch: false, recommendedUrl }; }
168 if (parsed.search || parsed.hash || parsed.username || parsed.password) return { mismatch: false, recommendedUrl };
169 const path = parsed.pathname.replace(/\/+$/, "");
170 const expected = protocolSuffix(kind);
171 if (!expected) return { mismatch: false, recommendedUrl };
172 if (recommendedUrl && normalizedExactURL(address) === normalizedExactURL(recommendedUrl)) return { mismatch: false, recommendedUrl };
173 const knownSuffix = ["/messages", "/chat/completions", "/responses"].find(suffix => path.endsWith(suffix));
174 if (knownSuffix && !path.endsWith(expected)) return { mismatch: true, recommendedUrl };
175 if (!catalog?.protocols || !path.endsWith(expected)) return { mismatch: false, recommendedUrl };
176
177 const selectedRoute = catalog.protocols[normalizedProtocol(kind)];
178 if (!selectedRoute) return { mismatch: false, recommendedUrl };
179 let selectedBase: URL;
180 try { selectedBase = new URL(selectedRoute.baseUrl); } catch { return { mismatch: false, recommendedUrl }; }
181 if (selectedBase.host.toLowerCase() !== parsed.host.toLowerCase()) return { mismatch: false, recommendedUrl };
182 for (const [otherKind, route] of Object.entries(catalog.protocols)) {
183 if (normalizedProtocol(otherKind) === normalizedProtocol(kind)) continue;
184 let foreignBase: URL;
185 try { foreignBase = new URL(providerRequestURLFromConfig(otherKind, route.baseUrl, "")); } catch { continue; }
186 if (foreignBase.host.toLowerCase() !== parsed.host.toLowerCase()) continue;
187 const otherSuffix = protocolSuffix(otherKind);
188 const foreignRequestPath = foreignBase.pathname.replace(/\/+$/, "");
189 const foreignRoot = foreignRequestPath.slice(0, -otherSuffix.length);
190 if (!foreignRoot || foreignRoot === "/") continue;
191 if (path === `${foreignRoot}${expected}`) {
192 return { mismatch: true, recommendedUrl };
193 }
194 }
195 return { mismatch: false, recommendedUrl };
196 }
197
197 lines TYPESCRIPT