fix(model): custom model choose error (#164)
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
Reference in New Issue
Block a user