fix: properly sync Ollama provider config to gateway runtime (#747)
This commit is contained in:
committed by
GitHub
Unverified
parent
fa2131ab13
commit
b9fd5f6a78
@@ -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_RUNTIME_PROVIDER = 'openai-codex';
|
||||||
const OPENAI_OAUTH_DEFAULT_MODEL_REF = `${OPENAI_OAUTH_RUNTIME_PROVIDER}/gpt-5.4`;
|
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 = {
|
type RuntimeProviderSyncContext = {
|
||||||
runtimeProviderKey: string;
|
runtimeProviderKey: string;
|
||||||
meta: ReturnType<typeof getProviderConfig>;
|
meta: ReturnType<typeof getProviderConfig>;
|
||||||
@@ -44,7 +52,7 @@ function normalizeProviderBaseUrl(
|
|||||||
return normalized.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
|
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';
|
const protocol = apiProtocol || config.apiProtocol || 'openai-completions';
|
||||||
if (protocol === 'openai-responses') {
|
if (protocol === 'openai-responses') {
|
||||||
return normalized.replace(/\/responses?$/i, '');
|
return normalized.replace(/\/responses?$/i, '');
|
||||||
@@ -65,7 +73,7 @@ function shouldUseExplicitDefaultOverride(config: ProviderConfig, runtimeProvide
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getOpenClawProviderKey(type: string, providerId: string): string {
|
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
|
// 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.
|
// as "custom-XXXXXXXX"), return it directly to avoid double-hashing.
|
||||||
const prefix = `${type}-`;
|
const prefix = `${type}-`;
|
||||||
@@ -286,7 +294,7 @@ async function syncProviderSecretToRuntime(
|
|||||||
async function resolveRuntimeSyncContext(config: ProviderConfig): Promise<RuntimeProviderSyncContext | null> {
|
async function resolveRuntimeSyncContext(config: ProviderConfig): Promise<RuntimeProviderSyncContext | null> {
|
||||||
const runtimeProviderKey = await resolveRuntimeProviderKey(config);
|
const runtimeProviderKey = await resolveRuntimeProviderKey(config);
|
||||||
const meta = getProviderConfig(config.type);
|
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) {
|
if (!api) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -315,7 +323,7 @@ async function syncCustomProviderAgentModel(
|
|||||||
runtimeProviderKey: string,
|
runtimeProviderKey: string,
|
||||||
apiKey: string | undefined,
|
apiKey: string | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (config.type !== 'custom' && config.type !== 'ollama') {
|
if (!isUnregisteredProviderType(config.type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +410,7 @@ async function buildAgentModelProviderEntry(
|
|||||||
authHeader?: boolean;
|
authHeader?: boolean;
|
||||||
} | null> {
|
} | null> {
|
||||||
const meta = getProviderConfig(config.type);
|
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);
|
const baseUrl = normalizeProviderBaseUrl(config, config.baseUrl || meta?.baseUrl, api);
|
||||||
if (!api || !baseUrl) {
|
if (!api || !baseUrl) {
|
||||||
return null;
|
return null;
|
||||||
@@ -411,7 +419,7 @@ async function buildAgentModelProviderEntry(
|
|||||||
let apiKey: string | undefined;
|
let apiKey: string | undefined;
|
||||||
let authHeader: boolean | undefined;
|
let authHeader: boolean | undefined;
|
||||||
|
|
||||||
if (config.type === 'custom') {
|
if (isUnregisteredProviderType(config.type)) {
|
||||||
apiKey = (await getApiKey(config.id)) || undefined;
|
apiKey = (await getApiKey(config.id)) || undefined;
|
||||||
} else if (config.type === 'minimax-portal' || config.type === 'minimax-portal-cn') {
|
} else if (config.type === 'minimax-portal' || config.type === 'minimax-portal-cn') {
|
||||||
const accountApiKey = await getApiKey(config.id);
|
const accountApiKey = await getApiKey(config.id);
|
||||||
@@ -507,7 +515,7 @@ export async function syncUpdatedProviderToRuntime(
|
|||||||
const defaultProviderId = await getDefaultProvider();
|
const defaultProviderId = await getDefaultProvider();
|
||||||
if (defaultProviderId === config.id) {
|
if (defaultProviderId === config.id) {
|
||||||
const modelOverride = config.model ? `${ock}/${config.model}` : undefined;
|
const modelOverride = config.model ? `${ock}/${config.model}` : undefined;
|
||||||
if (config.type !== 'custom') {
|
if (!isUnregisteredProviderType(config.type)) {
|
||||||
if (shouldUseExplicitDefaultOverride(config, ock)) {
|
if (shouldUseExplicitDefaultOverride(config, ock)) {
|
||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl, context.api),
|
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}`)
|
? (provider.model.startsWith(`${ock}/`) ? provider.model : `${ock}/${provider.model}`)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (provider.type === 'custom' || provider.type === 'ollama') {
|
if (isUnregisteredProviderType(provider.type)) {
|
||||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||||
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl, provider.apiProtocol || 'openai-completions'),
|
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl, provider.apiProtocol || 'openai-completions'),
|
||||||
api: provider.apiProtocol || 'openai-completions',
|
api: provider.apiProtocol || 'openai-completions',
|
||||||
@@ -689,7 +697,7 @@ export async function syncDefaultProviderToRuntime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(provider.type === 'custom' || provider.type === 'ollama') &&
|
isUnregisteredProviderType(provider.type) &&
|
||||||
providerKey &&
|
providerKey &&
|
||||||
provider.baseUrl
|
provider.baseUrl
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import {
|
|||||||
syncDeletedProviderApiKeyToRuntime,
|
syncDeletedProviderApiKeyToRuntime,
|
||||||
syncDeletedProviderToRuntime,
|
syncDeletedProviderToRuntime,
|
||||||
syncSavedProviderToRuntime,
|
syncSavedProviderToRuntime,
|
||||||
|
syncUpdatedProviderToRuntime,
|
||||||
} from '@electron/services/providers/provider-runtime-sync';
|
} from '@electron/services/providers/provider-runtime-sync';
|
||||||
|
|
||||||
function createProvider(overrides: Partial<ProviderConfig> = {}): ProviderConfig {
|
function createProvider(overrides: Partial<ProviderConfig> = {}): ProviderConfig {
|
||||||
@@ -310,4 +311,51 @@ describe('provider-runtime-sync refresh strategy', () => {
|
|||||||
expect.any(Array),
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user