Spaces:
Sleeping
Sleeping
Claw Web commited on
Commit ·
a51697c
1
Parent(s): 3705a49
fix: protect API config from corrupted settings — hardcoded fallbacks + sanitization
Browse files- resolveApiConfig: FALLBACK_URL and FALLBACK_MODEL hardcoded
- ENV.forgeApiUrl fallback to https://router.huggingface.co/v1
- ENV.forgeApiKey fallback to process.env.BUILT_IN_FORGE_API_KEY
- Custom provider path: fallback to FALLBACK_URL when no provider match
- chat-endpoint.ts: sanitize all settings before passing to agent
- Empty strings, 'default' provider, non-http URLs all treated as null
- temperature/topP use ?? instead of || to preserve 0 values
- Debug logging in resolveApiConfig for troubleshooting
- server/runtime/agent.ts +44 -34
- server/runtime/chat-endpoint.ts +12 -6
server/runtime/agent.ts
CHANGED
|
@@ -210,48 +210,56 @@ const RETRY_BASE_DELAY_MS = 2000;
|
|
| 210 |
* Resolve the API URL and key based on provider config
|
| 211 |
*/
|
| 212 |
function resolveApiConfig(config: AgentConfig) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
// Treat empty, null, masked, or built-in providers as "use server default"
|
| 214 |
const hasCustomKey = config.apiKey && config.apiKey.length > 4 && !config.apiKey.startsWith("••••");
|
| 215 |
if (config.apiProvider === "claw" || config.apiProvider === "default" || config.apiProvider === "huggingface" || !hasCustomKey) {
|
| 216 |
-
|
| 217 |
-
const aliasMap: Record<string, string> = {
|
| 218 |
-
// Xiaomi MiMo (default)
|
| 219 |
-
mimo: "XiaomiMiMo/MiMo-V2-Flash",
|
| 220 |
-
"mimo-flash": "XiaomiMiMo/MiMo-V2-Flash",
|
| 221 |
-
"mimo-v2": "XiaomiMiMo/MiMo-V2-Flash",
|
| 222 |
-
// Qwen models
|
| 223 |
-
"qwen-coder": "Qwen/Qwen3-Coder-Next",
|
| 224 |
-
"qwen3-8b": "Qwen/Qwen3-8B",
|
| 225 |
-
"qwen3-coder": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
|
| 226 |
-
// Llama
|
| 227 |
-
llama: "meta-llama/Llama-3.3-70B-Instruct",
|
| 228 |
-
"llama-70b": "meta-llama/Llama-3.3-70B-Instruct",
|
| 229 |
-
// DeepSeek
|
| 230 |
-
deepseek: "deepseek-ai/DeepSeek-V3.2",
|
| 231 |
-
"deepseek-r1": "deepseek-ai/DeepSeek-R1",
|
| 232 |
-
// OpenAI GPT-5.x family (if user has OpenAI key)
|
| 233 |
-
"gpt5": "gpt-5.4",
|
| 234 |
-
"gpt-5": "gpt-5.4",
|
| 235 |
-
"gpt54": "gpt-5.4",
|
| 236 |
-
// Anthropic aliases (for compatibility)
|
| 237 |
-
opus: "claude-opus-4-6",
|
| 238 |
-
sonnet: "claude-sonnet-4-6",
|
| 239 |
-
haiku: "claude-haiku-4-5-20251213",
|
| 240 |
-
// xAI
|
| 241 |
-
grok: "grok-3",
|
| 242 |
-
"grok-3": "grok-3",
|
| 243 |
-
};
|
| 244 |
-
const defaultModel = process.env.DEFAULT_MODEL || "XiaomiMiMo/MiMo-V2-Flash";
|
| 245 |
const resolvedModel = aliasMap[config.model] || config.model || defaultModel;
|
| 246 |
// Use BUILT_IN_FORGE_API_URL from env — HuggingFace router or OpenAI
|
| 247 |
-
const baseUrl = ENV.forgeApiUrl.replace(/\/$/, "");
|
|
|
|
|
|
|
| 248 |
return {
|
| 249 |
url: `${baseUrl}/chat/completions`,
|
| 250 |
-
key:
|
| 251 |
-
model: resolvedModel,
|
| 252 |
};
|
| 253 |
}
|
| 254 |
|
|
|
|
| 255 |
let baseUrl = config.apiBaseUrl || "";
|
| 256 |
if (!baseUrl) {
|
| 257 |
const providers: Record<string, string> = {
|
|
@@ -262,13 +270,15 @@ function resolveApiConfig(config: AgentConfig) {
|
|
| 262 |
anthropic: "https://api.anthropic.com/v1",
|
| 263 |
ollama: "http://localhost:11434/v1",
|
| 264 |
};
|
| 265 |
-
baseUrl = providers[config.apiProvider] ||
|
| 266 |
}
|
| 267 |
|
|
|
|
|
|
|
| 268 |
return {
|
| 269 |
url: `${baseUrl.replace(/\/$/, "")}/chat/completions`,
|
| 270 |
key: config.apiKey,
|
| 271 |
-
model:
|
| 272 |
};
|
| 273 |
}
|
| 274 |
|
|
|
|
| 210 |
* Resolve the API URL and key based on provider config
|
| 211 |
*/
|
| 212 |
function resolveApiConfig(config: AgentConfig) {
|
| 213 |
+
// ─── HARDCODED FALLBACK — always works even if settings are corrupted ───
|
| 214 |
+
const FALLBACK_URL = "https://router.huggingface.co/v1";
|
| 215 |
+
const FALLBACK_MODEL = "XiaomiMiMo/MiMo-V2-Flash";
|
| 216 |
+
|
| 217 |
+
// Resolve model aliases (used for both default and custom paths)
|
| 218 |
+
const aliasMap: Record<string, string> = {
|
| 219 |
+
// Xiaomi MiMo (default)
|
| 220 |
+
mimo: "XiaomiMiMo/MiMo-V2-Flash",
|
| 221 |
+
"mimo-flash": "XiaomiMiMo/MiMo-V2-Flash",
|
| 222 |
+
"mimo-v2": "XiaomiMiMo/MiMo-V2-Flash",
|
| 223 |
+
// Qwen models
|
| 224 |
+
"qwen-coder": "Qwen/Qwen3-Coder-Next",
|
| 225 |
+
"qwen3-8b": "Qwen/Qwen3-8B",
|
| 226 |
+
"qwen3-coder": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
|
| 227 |
+
// Llama
|
| 228 |
+
llama: "meta-llama/Llama-3.3-70B-Instruct",
|
| 229 |
+
"llama-70b": "meta-llama/Llama-3.3-70B-Instruct",
|
| 230 |
+
// DeepSeek
|
| 231 |
+
deepseek: "deepseek-ai/DeepSeek-V3.2",
|
| 232 |
+
"deepseek-r1": "deepseek-ai/DeepSeek-R1",
|
| 233 |
+
// OpenAI GPT-5.x family (if user has OpenAI key)
|
| 234 |
+
"gpt5": "gpt-5.4",
|
| 235 |
+
"gpt-5": "gpt-5.4",
|
| 236 |
+
"gpt54": "gpt-5.4",
|
| 237 |
+
// Anthropic aliases (for compatibility)
|
| 238 |
+
opus: "claude-opus-4-6",
|
| 239 |
+
sonnet: "claude-sonnet-4-6",
|
| 240 |
+
haiku: "claude-haiku-4-5-20251213",
|
| 241 |
+
// xAI
|
| 242 |
+
grok: "grok-3",
|
| 243 |
+
"grok-3": "grok-3",
|
| 244 |
+
};
|
| 245 |
+
|
| 246 |
// Treat empty, null, masked, or built-in providers as "use server default"
|
| 247 |
const hasCustomKey = config.apiKey && config.apiKey.length > 4 && !config.apiKey.startsWith("••••");
|
| 248 |
if (config.apiProvider === "claw" || config.apiProvider === "default" || config.apiProvider === "huggingface" || !hasCustomKey) {
|
| 249 |
+
const defaultModel = process.env.DEFAULT_MODEL || FALLBACK_MODEL;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
const resolvedModel = aliasMap[config.model] || config.model || defaultModel;
|
| 251 |
// Use BUILT_IN_FORGE_API_URL from env — HuggingFace router or OpenAI
|
| 252 |
+
const baseUrl = (ENV.forgeApiUrl || FALLBACK_URL).replace(/\/$/, "");
|
| 253 |
+
const apiKey = ENV.forgeApiKey || process.env.BUILT_IN_FORGE_API_KEY || "";
|
| 254 |
+
console.log(`[agent] resolveApiConfig: using server default. URL=${baseUrl}, model=${resolvedModel}, hasKey=${!!apiKey}`);
|
| 255 |
return {
|
| 256 |
url: `${baseUrl}/chat/completions`,
|
| 257 |
+
key: apiKey,
|
| 258 |
+
model: resolvedModel || FALLBACK_MODEL,
|
| 259 |
};
|
| 260 |
}
|
| 261 |
|
| 262 |
+
// Custom provider path — user has their own API key
|
| 263 |
let baseUrl = config.apiBaseUrl || "";
|
| 264 |
if (!baseUrl) {
|
| 265 |
const providers: Record<string, string> = {
|
|
|
|
| 270 |
anthropic: "https://api.anthropic.com/v1",
|
| 271 |
ollama: "http://localhost:11434/v1",
|
| 272 |
};
|
| 273 |
+
baseUrl = providers[config.apiProvider] || FALLBACK_URL;
|
| 274 |
}
|
| 275 |
|
| 276 |
+
const resolvedModel = aliasMap[config.model] || config.model || FALLBACK_MODEL;
|
| 277 |
+
console.log(`[agent] resolveApiConfig: custom provider. URL=${baseUrl}, model=${resolvedModel}`);
|
| 278 |
return {
|
| 279 |
url: `${baseUrl.replace(/\/$/, "")}/chat/completions`,
|
| 280 |
key: config.apiKey,
|
| 281 |
+
model: resolvedModel,
|
| 282 |
};
|
| 283 |
}
|
| 284 |
|
server/runtime/chat-endpoint.ts
CHANGED
|
@@ -105,14 +105,20 @@ export async function handleChatStream(req: Request, res: Response) {
|
|
| 105 |
const effortLevel = getEffortLevel(sessionId);
|
| 106 |
|
| 107 |
// Build config from settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
const config = {
|
| 109 |
-
model:
|
| 110 |
-
apiProvider:
|
| 111 |
-
apiKey:
|
| 112 |
-
apiBaseUrl:
|
| 113 |
maxTokens: settings?.maxTokens || 16384,
|
| 114 |
-
temperature: settings?.temperature
|
| 115 |
-
topP: settings?.topP
|
| 116 |
systemPrompt: buildSystemPrompt({
|
| 117 |
memory: settings?.memoryContent,
|
| 118 |
effortLevel,
|
|
|
|
| 105 |
const effortLevel = getEffortLevel(sessionId);
|
| 106 |
|
| 107 |
// Build config from settings
|
| 108 |
+
// Sanitize settings — treat empty strings, "default", and masked values as null
|
| 109 |
+
const sanitizedModel = session.model || (settings?.model && settings.model.trim()) || process.env.DEFAULT_MODEL || "XiaomiMiMo/MiMo-V2-Flash";
|
| 110 |
+
const sanitizedProvider = session.provider || (settings?.apiProvider && settings.apiProvider !== "default" && settings.apiProvider.trim()) || "huggingface";
|
| 111 |
+
const sanitizedKey = (settings?.apiKey && settings.apiKey.length > 4 && !settings.apiKey.startsWith("••••")) ? settings.apiKey : null;
|
| 112 |
+
const sanitizedBaseUrl = (settings?.apiBaseUrl && settings.apiBaseUrl.trim() && settings.apiBaseUrl.startsWith("http")) ? settings.apiBaseUrl : null;
|
| 113 |
+
|
| 114 |
const config = {
|
| 115 |
+
model: sanitizedModel,
|
| 116 |
+
apiProvider: sanitizedProvider,
|
| 117 |
+
apiKey: sanitizedKey,
|
| 118 |
+
apiBaseUrl: sanitizedBaseUrl,
|
| 119 |
maxTokens: settings?.maxTokens || 16384,
|
| 120 |
+
temperature: settings?.temperature ?? 0.7,
|
| 121 |
+
topP: settings?.topP ?? 1,
|
| 122 |
systemPrompt: buildSystemPrompt({
|
| 123 |
memory: settings?.memoryContent,
|
| 124 |
effortLevel,
|