返回 DeepSeek-Reasonix
providerModels.ts
根目录 / desktop / frontend / src / lib / providerModels.ts
1 import type { ProviderModelOverrideView } from "./types";
2
3 export function mergedFetchedProviderModels(current: string[], fetched: string[], options: { preserveCurated?: boolean } = {}): string[] {
4 const saved = uniqueStrings(current);
5 if (options.preserveCurated && saved.length > 0) return saved;
6 return uniqueStrings([...saved, ...fetched]);
7 }
8
9 export function providerModelCandidates(current: string[], fetched: string[]): string[] {
10 return uniqueStrings([...current, ...fetched]).filter(isLikelyChatModel);
11 }
12
13 export function inferredVisionModels(models: string[]): string[] {
14 return uniqueStrings(models).filter((model) => isLikelyChatModel(model) && isLikelyVisionModel(model));
15 }
16
17 export function providerDefaultModel(currentDefault: string, models: string[]): string {
18 return currentDefault && models.includes(currentDefault) ? currentDefault : models[0] ?? "";
19 }
20
21 export function providerModelContextWindowDrafts(overrides: ProviderModelOverrideView[] | null | undefined): Record<string, string> {
22 const drafts: Record<string, string> = {};
23 for (const override of overrides ?? []) {
24 const model = override.model.trim();
25 const contextWindow = normalizedContextWindow(override.contextWindow);
26 if (model && contextWindow > 0) drafts[model] = String(contextWindow);
27 }
28 return drafts;
29 }
30
31 export function providerModelContextWindowIsSmall(value: unknown): boolean {
32 const contextWindow = normalizedContextWindow(value);
33 return contextWindow > 0 && contextWindow < 16_384;
34 }
35
36 export function mergeProviderModelContextWindows(
37 overrides: ProviderModelOverrideView[] | null | undefined,
38 models: string[],
39 drafts: Record<string, string>,
40 ): ProviderModelOverrideView[] {
41 const existing = new Map((overrides ?? []).map((override) => [override.model.trim(), override]));
42 const merged: ProviderModelOverrideView[] = [];
43 for (const model of uniqueStrings(models)) {
44 const previous = existing.get(model);
45 const parsedContextWindow = normalizedContextWindow(drafts[model]);
46 const override: ProviderModelOverrideView = {
47 model,
48 reasoningProtocol: previous?.reasoningProtocol ?? "",
49 supportedEfforts: previous?.supportedEfforts ?? [],
50 defaultEffort: previous?.defaultEffort ?? "",
51 vision: previous?.vision ?? null,
52 contextWindow: Math.max(parsedContextWindow, 0),
53 };
54 if (
55 override.reasoningProtocol.trim()
56 || override.supportedEfforts.length > 0
57 || override.defaultEffort.trim()
58 || override.vision != null
59 || (override.contextWindow ?? 0) > 0
60 ) {
61 merged.push(override);
62 }
63 }
64 return merged;
65 }
66
67 function normalizedContextWindow(value: unknown): number {
68 const parsed = Number(value);
69 if (!Number.isFinite(parsed) || parsed <= 0) return 0;
70 return Math.min(Math.trunc(parsed), Number.MAX_SAFE_INTEGER);
71 }
72
73 export function providerRequiresKey(provider: { requiresKey?: boolean; apiKeyEnv?: string }): boolean {
74 if (typeof provider.requiresKey === "boolean") return provider.requiresKey;
75 return Boolean((provider.apiKeyEnv ?? "").trim());
76 }
77
78 export function providerIsConfigured(provider: { configured?: boolean; requiresKey?: boolean; apiKeyEnv?: string; keySet?: boolean }): boolean {
79 if (typeof provider.configured === "boolean") return provider.configured;
80 return !providerRequiresKey(provider) || Boolean(provider.keySet);
81 }
82
83 export function providerApiKeyEnvForSave(name: string, apiKeyEnv: string, keyDraft: string): string {
84 const explicit = apiKeyEnv.trim();
85 if (explicit) return explicit;
86 return keyDraft.trim() ? apiKeyEnvFromProviderName(name) : "";
87 }
88
89 export function apiKeyEnvFromProviderName(name: string): string {
90 const stem = name
91 .trim()
92 .toUpperCase()
93 .replace(/[^A-Z0-9]+/g, "_")
94 .replace(/^_+|_+$/g, "");
95 if (stem) {
96 // Dotenv/environment variable names cannot start with a digit. Keep the
97 // readable name while giving digit-leading providers (for example
98 // "9router") a valid, stable credential slot.
99 const validStem = /^[0-9]/.test(stem) ? `CUSTOM_${stem}` : stem;
100 return `${validStem}_API_KEY`;
101 }
102 // When the provider name is entirely non-ASCII (e.g. Chinese characters),
103 // generate a stable hash suffix so each custom provider gets a unique slot.
104 const hash = fnv1a32(name.trim());
105 return `CUSTOM_${hash}_API_KEY`;
106 }
107
108 /** 32-bit FNV-1a hash, returns 8-char lowercase hex. Stable and deterministic. */
109 function fnv1a32(s: string): string {
110 let hash = 0x811c9dc5 >>> 0;
111 for (let i = 0; i < s.length; i++) {
112 hash ^= s.charCodeAt(i);
113 hash = Math.imul(hash, 0x01000193) >>> 0;
114 }
115 return hash.toString(16).padStart(8, "0");
116 }
117
118 export function isLikelyChatModel(model: string): boolean {
119 const lower = model.trim().toLowerCase();
120 if (!lower) return false;
121 for (const term of ["text-embedding", "text-to-speech", "speech-to-text"]) {
122 if (lower.includes(term)) return false;
123 }
124 const nonChatTokens = new Set([
125 "asr",
126 "stt",
127 "tts",
128 "whisper",
129 "embedding",
130 "moderation",
131 "rerank",
132 "dall",
133 "transcription",
134 ]);
135 return !lower.split(/[-_./:]+/).some((token) => nonChatTokens.has(token));
136 }
137
138 export function isLikelyVisionModel(model: string): boolean {
139 const lower = model.trim().toLowerCase();
140 if (!lower) return false;
141 if (lower === "mimo-v2.5" || lower === "mimo-v2-omni") return true;
142 const tokens = lower.split(/[-_./:]+/);
143 if (tokens.includes("audio")) return false;
144 if (lower.startsWith("gpt-4o")) return true;
145 const visionTokens = new Set(["vl", "vision", "visual", "multimodal", "omni"]);
146 return tokens.some((token) => visionTokens.has(token));
147 }
148
149 function uniqueStrings(values: string[]): string[] {
150 const seen = new Set<string>();
151 const out: string[] = [];
152 for (const value of values) {
153 const model = value.trim();
154 if (!model || seen.has(model)) continue;
155 seen.add(model);
156 out.push(model);
157 }
158 return out;
159 }
160
160 lines TYPESCRIPT