fix: properly sync Ollama provider config to gateway runtime (#747)

This commit is contained in:
paisley
2026-04-02 14:42:57 +08:00
committed by GitHub
Unverified
parent fa2131ab13
commit b9fd5f6a78
2 changed files with 65 additions and 9 deletions

View File

@@ -23,6 +23,14 @@ const GOOGLE_OAUTH_DEFAULT_MODEL_REF = `${GOOGLE_OAUTH_RUNTIME_PROVIDER}/gemini-
const OPENAI_OAUTH_RUNTIME_PROVIDER = 'openai-codex';
const OPENAI_OAUTH_DEFAULT_MODEL_REF = `${OPENAI_OAUTH_RUNTIME_PROVIDER}/gpt-5.4`;
/**
* Provider types that are not in the built-in provider registry (no `providerConfig.api`).
* They require explicit api-protocol defaulting to `openai-completions`.
*/
function isUnregisteredProviderType(type: string): boolean {
return type === 'custom' || type === 'ollama';
}
type RuntimeProviderSyncContext = {
runtimeProviderKey: string;
meta: ReturnType<typeof getProviderConfig>;
@@ -44,7 +52,7 @@ function normalizeProviderBaseUrl(
return normalized.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
}
if (config.type === 'custom' || config.type === 'ollama') {
if (isUnregisteredProviderType(config.type)) {
const protocol = apiProtocol || config.apiProtocol || 'openai-completions';
if (protocol === 'openai-responses') {
return normalized.replace(/\/responses?$/i, '');
@@ -65,7 +73,7 @@ function shouldUseExplicitDefaultOverride(config: ProviderConfig, runtimeProvide
}
export function getOpenClawProviderKey(type: string, providerId: string): string {
if (type === 'custom' || type === 'ollama') {
if (isUnregisteredProviderType(type)) {
// If the providerId is already a runtime key (e.g. re-seeded from openclaw.json
// as "custom-XXXXXXXX"), return it directly to avoid double-hashing.
const prefix = `${type}-`;
@@ -286,7 +294,7 @@ async function syncProviderSecretToRuntime(
async function resolveRuntimeSyncContext(config: ProviderConfig): Promise<RuntimeProviderSyncContext | null> {
const runtimeProviderKey = await resolveRuntimeProviderKey(config);
const meta = getProviderConfig(config.type);
const api = config.apiProtocol || ((config.type === 'custom' || config.type === 'ollama') ? 'openai-completions' : meta?.api);
const api = config.apiProtocol || (isUnregisteredProviderType(config.type) ? 'openai-completions' : meta?.api);
if (!api) {
return null;
}
@@ -315,7 +323,7 @@ async function syncCustomProviderAgentModel(
runtimeProviderKey: string,
apiKey: string | undefined,
): Promise<void> {
if (config.type !== 'custom' && config.type !== 'ollama') {
if (!isUnregisteredProviderType(config.type)) {
return;
}
@@ -402,7 +410,7 @@ async function buildAgentModelProviderEntry(
authHeader?: boolean;
} | null> {
const meta = getProviderConfig(config.type);
const api = config.apiProtocol || ((config.type === 'custom' || config.type === 'ollama') ? 'openai-completions' : meta?.api);
const api = config.apiProtocol || (isUnregisteredProviderType(config.type) ? 'openai-completions' : meta?.api);
const baseUrl = normalizeProviderBaseUrl(config, config.baseUrl || meta?.baseUrl, api);
if (!api || !baseUrl) {
return null;
@@ -411,7 +419,7 @@ async function buildAgentModelProviderEntry(
let apiKey: string | undefined;
let authHeader: boolean | undefined;
if (config.type === 'custom') {
if (isUnregisteredProviderType(config.type)) {
apiKey = (await getApiKey(config.id)) || undefined;
} else if (config.type === 'minimax-portal' || config.type === 'minimax-portal-cn') {
const accountApiKey = await getApiKey(config.id);
@@ -507,7 +515,7 @@ export async function syncUpdatedProviderToRuntime(
const defaultProviderId = await getDefaultProvider();
if (defaultProviderId === config.id) {
const modelOverride = config.model ? `${ock}/${config.model}` : undefined;
if (config.type !== 'custom') {
if (!isUnregisteredProviderType(config.type)) {
if (shouldUseExplicitDefaultOverride(config, ock)) {
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl, context.api),
@@ -593,7 +601,7 @@ export async function syncDefaultProviderToRuntime(
? (provider.model.startsWith(`${ock}/`) ? provider.model : `${ock}/${provider.model}`)
: undefined;
if (provider.type === 'custom' || provider.type === 'ollama') {
if (isUnregisteredProviderType(provider.type)) {
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl, provider.apiProtocol || 'openai-completions'),
api: provider.apiProtocol || 'openai-completions',
@@ -689,7 +697,7 @@ export async function syncDefaultProviderToRuntime(
}
if (
(provider.type === 'custom' || provider.type === 'ollama') &&
isUnregisteredProviderType(provider.type) &&
providerKey &&
provider.baseUrl
) {

View File

@@ -76,6 +76,7 @@ import {
syncDeletedProviderApiKeyToRuntime,
syncDeletedProviderToRuntime,
syncSavedProviderToRuntime,
syncUpdatedProviderToRuntime,
} from '@electron/services/providers/provider-runtime-sync';
function createProvider(overrides: Partial<ProviderConfig> = {}): ProviderConfig {
@@ -310,4 +311,51 @@ describe('provider-runtime-sync refresh strategy', () => {
expect.any(Array),
);
});
it('syncs updated Ollama provider as default with correct override config', async () => {
const ollamaProvider = createProvider({
id: 'ollamafd',
type: 'ollama',
name: 'Ollama',
model: 'qwen3:30b',
baseUrl: 'http://localhost:11434/v1',
});
mocks.getProviderConfig.mockReturnValue(undefined);
mocks.getProviderSecret.mockResolvedValue({ type: 'local', apiKey: 'ollama-local' });
mocks.getDefaultProvider.mockResolvedValue('ollamafd');
const gateway = createGateway('running');
await syncUpdatedProviderToRuntime(ollamaProvider, undefined, gateway as GatewayManager);
// Should use the custom/ollama branch with explicit override
expect(mocks.setOpenClawDefaultModelWithOverride).toHaveBeenCalledWith(
'ollama-ollamafd',
'ollama-ollamafd/qwen3:30b',
expect.objectContaining({
baseUrl: 'http://localhost:11434/v1',
api: 'openai-completions',
}),
expect.any(Array),
);
// Should NOT call the non-override path
expect(mocks.setOpenClawDefaultModel).not.toHaveBeenCalled();
expect(gateway.debouncedReload).toHaveBeenCalledTimes(1);
});
it('removes Ollama provider from runtime on delete', async () => {
const ollamaProvider = createProvider({
id: 'ollamafd',
type: 'ollama',
name: 'Ollama',
model: 'qwen3:30b',
baseUrl: 'http://localhost:11434/v1',
});
const gateway = createGateway('running');
await syncDeletedProviderToRuntime(ollamaProvider, 'ollamafd', gateway as GatewayManager);
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('ollama-ollamafd');
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('ollamafd');
expect(gateway.debouncedRestart).toHaveBeenCalledTimes(1);
});
});