-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Expand file tree
/
Copy pathproviderRegistry.ts
More file actions
321 lines (290 loc) · 11.2 KB
/
Copy pathproviderRegistry.ts
File metadata and controls
321 lines (290 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/**
* Provider Registry — Single source of truth for all provider configuration.
* Modularized into `open-sse/config/providers/`
*/
export * from "./providers/shared.ts";
export {
ALIBABA_MODEL_STUDIO_MODELS,
ALIBABA_MODEL_STUDIO_MODELS as ALIBABA_DASHSCOPE_MODELS,
} from "./providers/registry/alibaba/index.ts";
export { REGISTRY } from "./providers/index.ts";
import { REGISTRY } from "./providers/index.ts";
// Imported from `privateHost` rather than `outboundUrlGuard`: this module is reachable from
// `ProviderDetailPageClient.tsx`, so anything it pulls in has to survive a browser bundle
// (#11122). `privateHost` is platform-free by contract; the guard module is not.
import { isPrivateHost } from "@/shared/network/privateHost";
import {
RegistryModel,
REASONING_UNSUPPORTED,
RegistryOAuth,
RegistryEntry,
LegacyProvider,
buildModels,
GPT_5_5_CONTEXT_LENGTH,
GPT_5_5_CODEX_CAPABILITIES,
CHAT_OPENAI_COMPAT_MODELS,
mapStainlessOs,
mapStainlessArch,
} from "./providers/shared.ts";
// ── Generator Functions ───────────────────────────────────────────────────
/** Generate legacy PROVIDERS object shape for constants.js backward compatibility */
export function generateLegacyProviders(): Record<string, LegacyProvider> {
const providers: Record<string, LegacyProvider> = {};
for (const [id, entry] of Object.entries(REGISTRY)) {
const p: LegacyProvider = { format: entry.format };
// URL(s)
if (entry.baseUrls) {
p.baseUrls = entry.baseUrls;
} else if (entry.baseUrl) {
p.baseUrl = entry.baseUrl;
}
if (entry.responsesBaseUrl) {
p.responsesBaseUrl = entry.responsesBaseUrl;
}
if (entry.messagesUrl) {
p.messagesUrl = entry.messagesUrl;
}
if (entry.requestDefaults) {
p.requestDefaults = entry.requestDefaults;
}
if (typeof entry.timeoutMs === "number") {
p.timeoutMs = entry.timeoutMs;
}
// Headers
const mergedHeaders = {
...(entry.headers || {}),
...(entry.extraHeaders || {}),
};
if (Object.keys(mergedHeaders).length > 0) {
p.headers = mergedHeaders;
}
// OAuth
if (entry.oauth) {
if (entry.oauth.clientIdEnv) {
p.clientId = process.env[entry.oauth.clientIdEnv] || entry.oauth.clientIdDefault;
}
if (entry.oauth.clientSecretEnv) {
p.clientSecret =
process.env[entry.oauth.clientSecretEnv] || entry.oauth.clientSecretDefault;
}
if (entry.oauth.tokenUrl) p.tokenUrl = entry.oauth.tokenUrl;
if (entry.oauth.refreshUrl) p.refreshUrl = entry.oauth.refreshUrl;
if (entry.oauth.authUrl) p.authUrl = entry.oauth.authUrl;
}
// Cursor-specific
if (entry.chatPath) p.chatPath = entry.chatPath;
if (entry.clientVersion) p.clientVersion = entry.clientVersion;
providers[id] = p;
}
return providers;
}
/** Generate PROVIDER_MODELS map (alias → model list) */
export function generateModels(): Record<string, RegistryModel[]> {
const models: Record<string, RegistryModel[]> = {};
for (const entry of Object.values(REGISTRY)) {
if (entry.models && entry.models.length > 0) {
const key = entry.alias || entry.id;
// If alias already exists, don't overwrite (first wins)
if (!models[key]) {
models[key] = entry.models;
}
}
}
return models;
}
/** Generate PROVIDER_ID_TO_ALIAS map */
export function generateAliasMap(): Record<string, string> {
const map: Record<string, string> = {};
for (const entry of Object.values(REGISTRY)) {
map[entry.id] = entry.alias || entry.id;
}
return map;
}
// ── Local Provider Detection ──────────────────────────────────────────────
// Evaluated once at module load time — process restart required for env var changes.
const LOCAL_HOSTNAMES = new Set([
"localhost",
"127.0.0.1",
...(typeof process !== "undefined" && process.env.LOCAL_HOSTNAMES
? process.env.LOCAL_HOSTNAMES.split(",")
.map((h) => h.trim())
.filter(Boolean)
: []),
]);
/**
* Detect if a base URL points to a local inference backend.
* Used for shorter 404 cooldowns (model-only, not connection) and health check targets.
*
* Operators can extend via LOCAL_HOSTNAMES env var (comma-separated) for Docker
* hostnames (e.g., LOCAL_HOSTNAMES=omlx,mlx-audio).
*/
export function isLocalProvider(baseUrl?: string | null): boolean {
if (!baseUrl) return false;
try {
const url = new URL(baseUrl);
const hostname = url.hostname;
if (!hostname) return false;
return LOCAL_HOSTNAMES.has(hostname) || isPrivateHost(hostname);
} catch {
return false;
}
}
/** Set of provider IDs with passthroughModels enabled — 404s are model-specific, not account-level. */
let _passthroughProviderIds: Set<string> | null = null;
function ensurePassthroughProviderIds(): Set<string> {
if (_passthroughProviderIds) return _passthroughProviderIds;
try {
const ids = new Set<string>();
for (const entry of Object.values(REGISTRY)) {
if (entry.passthroughModels) ids.add(entry.id);
}
_passthroughProviderIds = ids;
} catch {
_passthroughProviderIds = new Set<string>();
}
return _passthroughProviderIds;
}
export function getPassthroughProviders(): Set<string> {
return ensurePassthroughProviderIds();
}
// ── Registry Lookup Helpers ───────────────────────────────────────────────
const _byAlias = new Map<string, RegistryEntry>();
let _byAliasPopulated = false;
function ensureByAliasPopulated(): void {
if (_byAliasPopulated) return;
_byAliasPopulated = true;
for (const entry of Object.values(REGISTRY)) {
if (entry.alias && entry.alias !== entry.id) {
_byAlias.set(entry.alias, entry);
}
}
}
/** Get registry entry by provider ID or alias */
export function getRegistryEntry(provider: string): RegistryEntry | null {
ensureByAliasPopulated();
return REGISTRY[provider] || _byAlias.get(provider) || null;
}
/** Resolve only a model's explicit reasoning vocabulary. */
export function getRegistryModelThinkingEfforts(
provider: string,
modelId: string
): readonly string[] | undefined {
const entry = getRegistryEntry(provider);
if (!entry) return undefined;
const model = entry.models.find((candidate) => candidate.id === modelId);
return model?.supportedThinkingEfforts;
}
/** Resolve a model's explicit reasoning vocabulary before its provider fallback. */
export function getRegistryThinkingEfforts(
provider: string,
modelId: string
): readonly string[] | undefined {
const entry = getRegistryEntry(provider);
if (!entry) return undefined;
const modelEfforts = getRegistryModelThinkingEfforts(provider, modelId);
if (modelEfforts !== undefined) return modelEfforts;
return entry.defaultSupportedThinkingEfforts;
}
/**
* Decide whether a non-empty live catalog may exclude omitted static models
* during request routing and wildcard expansion.
*
* Live discovery is authoritative by default, including for dynamic providers.
* Providers with intentionally partial discovery must explicitly opt out in
* their registry entry.
*/
export function providerUsesAuthoritativeLiveCatalog(provider: string): boolean {
const entry = getRegistryEntry(provider);
if (entry && typeof entry.liveCatalogAuthoritative === "boolean") {
return entry.liveCatalogAuthoritative;
}
return true;
}
/** Get all registered provider IDs */
export function getRegisteredProviders(): string[] {
return Object.keys(REGISTRY);
}
// Precomputed map: modelId → unsupportedParams (O(1) lookup instead of O(N×M) scan).
// Built once at module load from all registry entries.
const _unsupportedParamsMap = new Map<string, readonly string[]>();
let _unsupportedParamsPopulated = false;
function ensureUnsupportedParamsPopulated(): void {
if (_unsupportedParamsPopulated) return;
_unsupportedParamsPopulated = true;
for (const entry of Object.values(REGISTRY)) {
// Some entries (e.g. the `mimocode` proxy) legitimately have no model catalogue.
for (const model of entry.models ?? []) {
if (model.unsupportedParams && !_unsupportedParamsMap.has(model.id)) {
_unsupportedParamsMap.set(model.id, model.unsupportedParams);
}
}
}
}
/**
* Get unsupported parameters for a specific model.
* Uses O(1) precomputed lookup. Also handles prefixed model IDs
* (e.g., "openai/o3" → strips prefix and looks up "o3").
* Returns empty array if no restrictions are defined.
*/
export function getUnsupportedParams(provider: string, modelId: string): readonly string[] {
ensureUnsupportedParamsPopulated();
// 1. Check current provider's registry (exact match)
const entry = getRegistryEntry(provider);
const modelEntry = entry?.models?.find((m) => m.id === modelId);
if (modelEntry?.unsupportedParams) return modelEntry.unsupportedParams;
// 2. O(1) lookup in precomputed map (handles cross-provider routing)
const cached = _unsupportedParamsMap.get(modelId);
if (cached) return cached;
// 3. Handle prefixed model IDs (e.g., "openai/o3" → "o3")
if (modelId.includes("/")) {
const bareId = modelId.split("/").pop() || "";
const bare = _unsupportedParamsMap.get(bareId);
if (bare) return bare;
}
// 4. Provider-wide fallback for providers whose limitation applies to every
// model they serve, not just the ones statically catalogued (e.g. AI Horde's
// `passthroughModels: true` roster changes as workers come and go, but no
// model it hosts supports tool calling — see RegistryEntry.unsupportedParams).
if (entry?.unsupportedParams) return entry.unsupportedParams;
return [];
}
/**
* True for providers whose OpenAI-compatible facade rejects a single-text-part
* content array and only accepts the equivalent plain string (RegistryEntry.
* requiresPlainStringContent). Used by the Responses→Chat translator to scope
* its content-collapse workaround to just these providers.
*/
export function requiresPlainStringContent(provider: string): boolean {
return getRegistryEntry(provider)?.requiresPlainStringContent === true;
}
/**
* Get provider category: "oauth" or "apikey"
* Used by the resilience layer to apply different cooldown/backoff profiles.
* @param {string} provider - Provider ID or alias
* @returns {"oauth"|"apikey"}
*/
export function getProviderCategory(provider: string): "oauth" | "apikey" {
const entry = getRegistryEntry(provider);
if (!entry) return "apikey"; // Safe default for unknown providers
return entry.authType === "apikey" ? "apikey" : "oauth";
}
/**
* Derive the latest fable/opus/sonnet/haiku model IDs from the `claude` registry entry.
* Picks the first model whose ID matches each family pattern — registry order
* determines precedence, so newer models should be listed first.
*/
export function getClaudeCodeDefaultModels(): {
fable: string;
opus: string;
sonnet: string;
haiku: string;
} {
const models = REGISTRY.claude?.models ?? [];
const find = (pattern: RegExp) => models.find((m) => pattern.test(m.id))?.id ?? "";
return {
fable: find(/fable/i),
opus: find(/opus/i),
sonnet: find(/sonnet/i),
haiku: find(/haiku/i),
};
}