fix(model): custom model choose error (#164)

This commit is contained in:
Haze
2026-02-25 16:38:03 +08:00
committed by GitHub
Unverified
parent 1166074a52
commit 265b12281c
4 changed files with 98 additions and 64 deletions

View File

@@ -498,8 +498,14 @@ export class GatewayManager extends EventEmitter {
if (pids.length > 0) { if (pids.length > 0) {
if (!this.process || !pids.includes(String(this.process.pid))) { if (!this.process || !pids.includes(String(this.process.pid))) {
logger.info(`Found orphaned process listening on port ${port} (PIDs: ${pids.join(', ')}), attempting to kill...`); logger.info(`Found orphaned process listening on port ${port} (PIDs: ${pids.join(', ')}), attempting to kill...`);
// SIGTERM first so the gateway can clean up its lock file.
for (const pid of pids) { for (const pid of pids) {
try { process.kill(parseInt(pid), 'SIGKILL'); } catch { /* ignore */ } try { process.kill(parseInt(pid), 'SIGTERM'); } catch { /* ignore */ }
}
await new Promise(r => setTimeout(r, 3000));
// SIGKILL any survivors.
for (const pid of pids) {
try { process.kill(parseInt(pid), 0); process.kill(parseInt(pid), 'SIGKILL'); } catch { /* already exited */ }
} }
await new Promise(r => setTimeout(r, 1000)); await new Promise(r => setTimeout(r, 1000));
return null; return null;

View File

@@ -962,6 +962,8 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
if (provider.type === 'custom' || provider.type === 'ollama') { if (provider.type === 'custom' || provider.type === 'ollama') {
// For runtime-configured providers, use user-entered base URL/api. // For runtime-configured providers, use user-entered base URL/api.
// Do NOT set apiKeyEnv — the OpenClaw gateway resolves custom
// provider keys via auth-profiles, not the config apiKey field.
setOpenClawDefaultModelWithOverride(provider.type, modelOverride, { setOpenClawDefaultModelWithOverride(provider.type, modelOverride, {
baseUrl: provider.baseUrl, baseUrl: provider.baseUrl,
api: 'openai-completions', api: 'openai-completions',

View File

@@ -3,7 +3,7 @@
* Writes API keys to ~/.openclaw/agents/main/agent/auth-profiles.json * Writes API keys to ~/.openclaw/agents/main/agent/auth-profiles.json
* so the OpenClaw Gateway can load them for AI provider calls. * so the OpenClaw Gateway can load them for AI provider calls.
*/ */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { homedir } from 'os'; import { homedir } from 'os';
import { import {
@@ -81,51 +81,74 @@ function writeAuthProfiles(store: AuthProfilesStore, agentId = 'main'): void {
writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8'); writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8');
} }
/**
* Discover all agent IDs that have an agent/ subdirectory.
*/
function discoverAgentIds(): string[] {
const agentsDir = join(homedir(), '.openclaw', 'agents');
try {
if (!existsSync(agentsDir)) return ['main'];
return readdirSync(agentsDir, { withFileTypes: true })
.filter((d) => d.isDirectory() && existsSync(join(agentsDir, d.name, 'agent')))
.map((d) => d.name);
} catch {
return ['main'];
}
}
/** /**
* Save a provider API key to OpenClaw's auth-profiles.json * Save a provider API key to OpenClaw's auth-profiles.json
* This writes the key in the format OpenClaw expects so the gateway * This writes the key in the format OpenClaw expects so the gateway
* can use it for AI provider calls. * can use it for AI provider calls.
*
* Writes to ALL discovered agent directories so every agent
* (including non-"main" agents like "dev") stays in sync.
* *
* @param provider - Provider type (e.g., 'anthropic', 'openrouter', 'openai', 'google') * @param provider - Provider type (e.g., 'anthropic', 'openrouter', 'openai', 'google')
* @param apiKey - The API key to store * @param apiKey - The API key to store
* @param agentId - Agent ID (defaults to 'main') * @param agentId - Optional single agent ID. When omitted, writes to every agent.
*/ */
export function saveProviderKeyToOpenClaw( export function saveProviderKeyToOpenClaw(
provider: string, provider: string,
apiKey: string, apiKey: string,
agentId = 'main' agentId?: string
): void { ): void {
const store = readAuthProfiles(agentId); const agentIds = agentId ? [agentId] : discoverAgentIds();
if (agentIds.length === 0) agentIds.push('main');
// Profile ID follows OpenClaw convention: <provider>:default
const profileId = `${provider}:default`; for (const id of agentIds) {
const store = readAuthProfiles(id);
// Upsert the profile entry
store.profiles[profileId] = { // Profile ID follows OpenClaw convention: <provider>:default
type: 'api_key', const profileId = `${provider}:default`;
provider,
key: apiKey, // Upsert the profile entry
}; store.profiles[profileId] = {
type: 'api_key',
// Update order to include this profile provider,
if (!store.order) { key: apiKey,
store.order = {}; };
// Update order to include this profile
if (!store.order) {
store.order = {};
}
if (!store.order[provider]) {
store.order[provider] = [];
}
if (!store.order[provider].includes(profileId)) {
store.order[provider].push(profileId);
}
// Set as last good
if (!store.lastGood) {
store.lastGood = {};
}
store.lastGood[provider] = profileId;
writeAuthProfiles(store, id);
} }
if (!store.order[provider]) { console.log(`Saved API key for provider "${provider}" to OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`);
store.order[provider] = [];
}
if (!store.order[provider].includes(profileId)) {
store.order[provider].push(profileId);
}
// Set as last good
if (!store.lastGood) {
store.lastGood = {};
}
store.lastGood[provider] = profileId;
writeAuthProfiles(store, agentId);
console.log(`Saved API key for provider "${provider}" to OpenClaw auth-profiles (agent: ${agentId})`);
} }
/** /**
@@ -133,26 +156,31 @@ export function saveProviderKeyToOpenClaw(
*/ */
export function removeProviderKeyFromOpenClaw( export function removeProviderKeyFromOpenClaw(
provider: string, provider: string,
agentId = 'main' agentId?: string
): void { ): void {
const store = readAuthProfiles(agentId); const agentIds = agentId ? [agentId] : discoverAgentIds();
const profileId = `${provider}:default`; if (agentIds.length === 0) agentIds.push('main');
delete store.profiles[profileId]; for (const id of agentIds) {
const store = readAuthProfiles(id);
const profileId = `${provider}:default`;
if (store.order?.[provider]) { delete store.profiles[profileId];
store.order[provider] = store.order[provider].filter((id) => id !== profileId);
if (store.order[provider].length === 0) { if (store.order?.[provider]) {
delete store.order[provider]; store.order[provider] = store.order[provider].filter((aid) => aid !== profileId);
if (store.order[provider].length === 0) {
delete store.order[provider];
}
} }
}
if (store.lastGood?.[provider] === profileId) { if (store.lastGood?.[provider] === profileId) {
delete store.lastGood[provider]; delete store.lastGood[provider];
} }
writeAuthProfiles(store, agentId); writeAuthProfiles(store, id);
console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agent: ${agentId})`); }
console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`);
} }
/** /**
@@ -244,7 +272,7 @@ export function setOpenClawDefaultModel(provider: string, modelOverride?: string
...existingProvider, ...existingProvider,
baseUrl: providerCfg.baseUrl, baseUrl: providerCfg.baseUrl,
api: providerCfg.api, api: providerCfg.api,
apiKey: `\${${providerCfg.apiKeyEnv}}`, apiKey: providerCfg.apiKeyEnv,
models: mergedModels, models: mergedModels,
}; };
console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`); console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`);
@@ -329,27 +357,22 @@ export function setOpenClawDefaultModelWithOverride(
const models = (config.models || {}) as Record<string, unknown>; const models = (config.models || {}) as Record<string, unknown>;
const providers = (models.providers || {}) as Record<string, unknown>; const providers = (models.providers || {}) as Record<string, unknown>;
const existingProvider = // Replace the provider entry entirely rather than merging.
providers[provider] && typeof providers[provider] === 'object' // Different custom/ollama provider instances have different baseUrls,
? (providers[provider] as Record<string, unknown>) // so merging models from a previous instance creates an inconsistent
: {}; // config (models pointing at the wrong endpoint).
const nextModels: Array<Record<string, unknown>> = [];
const existingModels = Array.isArray(existingProvider.models) if (modelId) {
? (existingProvider.models as Array<Record<string, unknown>>) nextModels.push({ id: modelId, name: modelId });
: [];
const mergedModels = [...existingModels];
if (modelId && !mergedModels.some((m) => m.id === modelId)) {
mergedModels.push({ id: modelId, name: modelId });
} }
const nextProvider: Record<string, unknown> = { const nextProvider: Record<string, unknown> = {
...existingProvider,
baseUrl: override.baseUrl, baseUrl: override.baseUrl,
api: override.api, api: override.api,
models: mergedModels, models: nextModels,
}; };
if (override.apiKeyEnv) { if (override.apiKeyEnv) {
nextProvider.apiKey = `\${${override.apiKeyEnv}}`; nextProvider.apiKey = override.apiKeyEnv;
} }
providers[provider] = nextProvider; providers[provider] = nextProvider;

View File

@@ -52,7 +52,7 @@ const REGISTRY: Record<string, ProviderBackendMeta> = {
}, },
google: { google: {
envVar: 'GEMINI_API_KEY', envVar: 'GEMINI_API_KEY',
defaultModel: 'google/gemini-3-pro-preview', defaultModel: 'google/gemini-3.1-pro-preview',
// google is built-in to OpenClaw's pi-ai catalog, no providerConfig needed. // google is built-in to OpenClaw's pi-ai catalog, no providerConfig needed.
// Adding models.providers.google overrides the built-in and can break Gemini. // Adding models.providers.google overrides the built-in and can break Gemini.
}, },
@@ -94,6 +94,9 @@ const REGISTRY: Record<string, ProviderBackendMeta> = {
apiKeyEnv: 'SILICONFLOW_API_KEY', apiKeyEnv: 'SILICONFLOW_API_KEY',
}, },
}, },
custom: {
envVar: 'CUSTOM_API_KEY',
},
// Additional providers with env var mappings but no default model // Additional providers with env var mappings but no default model
groq: { envVar: 'GROQ_API_KEY' }, groq: { envVar: 'GROQ_API_KEY' },
deepgram: { envVar: 'DEEPGRAM_API_KEY' }, deepgram: { envVar: 'DEEPGRAM_API_KEY' },