mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 00:48:10 +00:00
fix: use openai-chatgpt-responses for Codex OAuth and hide stale OpenAI API slot after logout (#1113)
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
syncProviderConfigToOpenClaw,
|
||||
updateAgentModelProvider,
|
||||
updateSingleAgentModelProvider,
|
||||
getProviderApiKeyFromOpenClaw,
|
||||
} from '../../utils/openclaw-auth';
|
||||
import {
|
||||
piAiModelsJsonModelEntry,
|
||||
@@ -376,6 +377,26 @@ async function removeDeletedProviderFromOpenClaw(
|
||||
for (const key of keys) {
|
||||
await removeProviderFromOpenClaw(key);
|
||||
}
|
||||
|
||||
// Codex OAuth uses runtime key openai-codex but may leave a bare models.providers.openai
|
||||
// entry behind. Drop that slot when no API key credentials remain.
|
||||
if (runtimeProviderKey === OPENAI_OAUTH_RUNTIME_PROVIDER) {
|
||||
const openClawKey = await getProviderApiKeyFromOpenClaw('openai');
|
||||
if (openClawKey) {
|
||||
return;
|
||||
}
|
||||
const storeAccounts = await listProviderAccounts();
|
||||
for (const account of storeAccounts) {
|
||||
if (account.vendorId !== 'openai' || account.authMode === 'oauth_browser') {
|
||||
continue;
|
||||
}
|
||||
const apiKey = await getApiKey(account.id);
|
||||
if (apiKey) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await removeProviderFromOpenClaw('openai');
|
||||
}
|
||||
}
|
||||
|
||||
function parseModelRef(modelRef: string): { providerKey: string; modelId: string } | null {
|
||||
|
||||
@@ -112,15 +112,19 @@ export class ProviderService {
|
||||
|
||||
let hasConfiguredOpenAiApiKey = false;
|
||||
if (activeProviders.has('openai')) {
|
||||
for (const account of storeByKey.get('openai') ?? []) {
|
||||
if (account.authMode === 'oauth_browser') {
|
||||
continue;
|
||||
}
|
||||
const apiKey = await getApiKey(account.id);
|
||||
const openClawKey = await getProviderApiKeyFromOpenClaw('openai');
|
||||
if (apiKey || openClawKey) {
|
||||
hasConfiguredOpenAiApiKey = true;
|
||||
break;
|
||||
const openClawKey = await getProviderApiKeyFromOpenClaw('openai');
|
||||
if (openClawKey) {
|
||||
hasConfiguredOpenAiApiKey = true;
|
||||
} else {
|
||||
for (const account of storeByKey.get('openai') ?? []) {
|
||||
if (account.authMode === 'oauth_browser') {
|
||||
continue;
|
||||
}
|
||||
const apiKey = await getApiKey(account.id);
|
||||
if (apiKey) {
|
||||
hasConfiguredOpenAiApiKey = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,7 +178,7 @@ export class ProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
if (activeProviders.has(OPENAI_CODEX_RUNTIME_PROVIDER_KEY)) {
|
||||
if (activeProviders.has(OPENAI_CODEX_RUNTIME_PROVIDER_KEY) || !hasConfiguredOpenAiApiKey) {
|
||||
const openaiStoreAccounts = storeByKey.get('openai') ?? [];
|
||||
for (const account of openaiStoreAccounts) {
|
||||
if (account.authMode !== 'api_key' && account.authMode !== undefined) {
|
||||
@@ -184,7 +188,10 @@ export class ProviderService {
|
||||
const openClawKey = await getProviderApiKeyFromOpenClaw('openai');
|
||||
if (!apiKey && !openClawKey) {
|
||||
logger.info(
|
||||
`[provider-sync] Removing unconfigured OpenAI API key account "${account.id}" (OAuth uses ${OPENAI_CODEX_RUNTIME_PROVIDER_KEY})`,
|
||||
`[provider-sync] Removing unconfigured OpenAI API key account "${account.id}"`
|
||||
+ (activeProviders.has(OPENAI_CODEX_RUNTIME_PROVIDER_KEY)
|
||||
? ` (OAuth uses ${OPENAI_CODEX_RUNTIME_PROVIDER_KEY})`
|
||||
: ' (Codex OAuth removed)'),
|
||||
);
|
||||
await deleteProviderAccount(account.id);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const OLLAMA_PLACEHOLDER_API_KEY = 'ollama-local';
|
||||
export const OPENCLAW_API_PROTOCOLS = [
|
||||
'openai-completions',
|
||||
'openai-responses',
|
||||
'openai-codex-responses',
|
||||
'openai-chatgpt-responses',
|
||||
'anthropic-messages',
|
||||
'google-generative-ai',
|
||||
'github-copilot',
|
||||
@@ -59,6 +59,20 @@ export const OPENCLAW_API_PROTOCOLS = [
|
||||
|
||||
export type OpenClawApiProtocol = (typeof OPENCLAW_API_PROTOCOLS)[number];
|
||||
|
||||
/** Legacy api values ClawX previously wrote that OpenClaw no longer accepts. */
|
||||
export const LEGACY_OPENCLAW_API_PROTOCOL_MIGRATIONS = {
|
||||
'openai-codex-responses': 'openai-chatgpt-responses',
|
||||
} as const satisfies Record<string, OpenClawApiProtocol>;
|
||||
|
||||
export function normalizeOpenClawApiProtocol(api: unknown): OpenClawApiProtocol | undefined {
|
||||
if (typeof api !== 'string') return undefined;
|
||||
if ((OPENCLAW_API_PROTOCOLS as readonly string[]).includes(api)) {
|
||||
return api as OpenClawApiProtocol;
|
||||
}
|
||||
const migrated = (LEGACY_OPENCLAW_API_PROTOCOL_MIGRATIONS as Record<string, OpenClawApiProtocol>)[api];
|
||||
return migrated;
|
||||
}
|
||||
|
||||
export class InvalidApiProtocolError extends Error {
|
||||
constructor(public readonly api: unknown, public readonly providerKey?: string) {
|
||||
super(
|
||||
|
||||
@@ -32,8 +32,8 @@ import { withConfigLock } from './config-mutex';
|
||||
import { PORTS } from './config';
|
||||
import { getSetting } from './store';
|
||||
import {
|
||||
OPENCLAW_API_PROTOCOLS,
|
||||
assertValidApiProtocol,
|
||||
normalizeOpenClawApiProtocol,
|
||||
} from '../shared/providers/types';
|
||||
import { inferCustomModelInputModalities } from '../shared/providers/model-capabilities';
|
||||
import {
|
||||
@@ -1119,6 +1119,25 @@ export async function removeProviderFromOpenClaw(provider: string): Promise<void
|
||||
*
|
||||
* Returns the list of pruned provider keys for logging.
|
||||
*/
|
||||
function repairLegacyApiProtocolEntriesInConfig(config: Record<string, unknown>): string[] {
|
||||
const migrated: string[] = [];
|
||||
const models = (config.models || {}) as Record<string, unknown>;
|
||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||
|
||||
for (const [key, entry] of Object.entries(providers)) {
|
||||
if (!isPlainRecord(entry)) continue;
|
||||
const entryObj = entry as Record<string, unknown>;
|
||||
const api = entryObj.api;
|
||||
const normalized = normalizeOpenClawApiProtocol(api);
|
||||
if (normalized && normalized !== api) {
|
||||
entryObj.api = normalized;
|
||||
migrated.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
export async function pruneInvalidApiProviderEntries(): Promise<string[]> {
|
||||
const removed: string[] = [];
|
||||
await withConfigLock(async () => {
|
||||
@@ -1127,9 +1146,14 @@ export async function pruneInvalidApiProviderEntries(): Promise<string[]> {
|
||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||
let modified = false;
|
||||
|
||||
const migrated = repairLegacyApiProtocolEntriesInConfig(config);
|
||||
if (migrated.length > 0) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
for (const [key, entry] of Object.entries(providers)) {
|
||||
const api = isPlainRecord(entry) ? (entry as Record<string, unknown>).api : undefined;
|
||||
if (typeof api !== 'string' || !(OPENCLAW_API_PROTOCOLS as readonly string[]).includes(api)) {
|
||||
if (!normalizeOpenClawApiProtocol(api)) {
|
||||
delete providers[key];
|
||||
removed.push(key);
|
||||
modified = true;
|
||||
@@ -1493,7 +1517,7 @@ const OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME: Record<string, string> = {
|
||||
/** Runtime models.providers entry for OpenAI Codex OAuth accounts. */
|
||||
export const OPENAI_CODEX_OAUTH_PROVIDER_CONFIG = {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-codex-responses' as const,
|
||||
api: 'openai-chatgpt-responses' as const,
|
||||
};
|
||||
|
||||
function applyPinnedAgentRuntime(
|
||||
@@ -3136,6 +3160,12 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const migratedApiProtocols = repairLegacyApiProtocolEntriesInConfig(config);
|
||||
if (migratedApiProtocols.length > 0) {
|
||||
modified = true;
|
||||
console.log(`[sanitize] Migrated legacy models.providers api protocol for: ${migratedApiProtocols.join(', ')}`);
|
||||
}
|
||||
|
||||
const pinnedProviderRuntimes = applyOpenClawProviderAgentRuntimePinsToConfig(config);
|
||||
if (pinnedProviderRuntimes.length > 0) {
|
||||
modified = true;
|
||||
|
||||
@@ -74,14 +74,13 @@ export function filterActiveProviderKeysForUi(
|
||||
options?: { hasConfiguredOpenAiApiKey?: boolean },
|
||||
): string[] {
|
||||
const keys = Array.from(activeKeys).filter((key) => !HIDDEN_PROVIDER_KEYS_FOR_UI.has(key));
|
||||
const active = new Set(keys);
|
||||
if (!active.has('openai') || !active.has(OPENAI_CODEX_RUNTIME_PROVIDER_KEY)) {
|
||||
return keys;
|
||||
// Bare `openai` is the API-key slot. Hide it unless a real API key exists —
|
||||
// including after Codex OAuth is removed and openclaw.json still lists
|
||||
// models.providers.openai from an earlier OAuth setup.
|
||||
if (!options?.hasConfiguredOpenAiApiKey) {
|
||||
return keys.filter((key) => key !== 'openai');
|
||||
}
|
||||
if (options?.hasConfiguredOpenAiApiKey) {
|
||||
return keys;
|
||||
}
|
||||
return keys.filter((key) => key !== 'openai');
|
||||
return keys;
|
||||
}
|
||||
|
||||
export function isOAuthProviderType(type: string): boolean {
|
||||
|
||||
@@ -39,7 +39,7 @@ docs:
|
||||
|
||||
A historical bug in [electron/shared/providers/registry.ts](electron/shared/providers/registry.ts) set the OpenRouter
|
||||
`providerConfig.api` to the literal string `'openrouter'`, which is not in OpenClaw's allowed
|
||||
`api` enum (`openai-completions | openai-responses | openai-codex-responses | anthropic-messages |
|
||||
`api` enum (`openai-completions | openai-responses | openai-chatgpt-responses | anthropic-messages |
|
||||
google-generative-ai | github-copilot | bedrock-converse-stream | ollama | azure-openai-responses`).
|
||||
|
||||
When the user selected OpenRouter as their default provider, ClawX wrote that invalid value into
|
||||
|
||||
@@ -266,7 +266,7 @@ export type ProviderVendorCategory = 'official' | 'compatible' | 'local' | 'cust
|
||||
export type ProviderProtocol =
|
||||
| 'openai-completions'
|
||||
| 'openai-responses'
|
||||
| 'openai-codex-responses'
|
||||
| 'openai-chatgpt-responses'
|
||||
| 'anthropic-messages'
|
||||
| 'google-generative-ai'
|
||||
| 'github-copilot'
|
||||
|
||||
@@ -27,7 +27,7 @@ export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
export type ProviderProtocol =
|
||||
| 'openai-completions'
|
||||
| 'openai-responses'
|
||||
| 'openai-codex-responses'
|
||||
| 'openai-chatgpt-responses'
|
||||
| 'anthropic-messages'
|
||||
| 'google-generative-ai'
|
||||
| 'github-copilot'
|
||||
|
||||
@@ -1782,6 +1782,31 @@ describe('pruneInvalidApiProviderEntries', () => {
|
||||
const after = await readOpenClawJson();
|
||||
expect(after).toEqual(before);
|
||||
});
|
||||
|
||||
it('migrates legacy openai-codex-responses api values instead of pruning them', async () => {
|
||||
await writeOpenClawJson({
|
||||
models: {
|
||||
providers: {
|
||||
'openai-codex': {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-codex-responses',
|
||||
},
|
||||
openrouter: {
|
||||
baseUrl: 'https://openrouter.ai/api/v1',
|
||||
api: 'openrouter',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { pruneInvalidApiProviderEntries } = await import('@electron/utils/openclaw-auth');
|
||||
const removed = await pruneInvalidApiProviderEntries();
|
||||
|
||||
expect(removed).toEqual(['openrouter']);
|
||||
const result = await readOpenClawJson();
|
||||
const providers = (result.models as Record<string, unknown>).providers as Record<string, unknown>;
|
||||
expect((providers['openai-codex'] as { api: string }).api).toBe('openai-chatgpt-responses');
|
||||
});
|
||||
});
|
||||
|
||||
describe('openai agentRuntime pin', () => {
|
||||
@@ -1824,7 +1849,7 @@ describe('openai agentRuntime pin', () => {
|
||||
|
||||
await syncProviderConfigToOpenClaw('openai-codex', 'gpt-5.5', {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-codex-responses',
|
||||
api: 'openai-chatgpt-responses',
|
||||
});
|
||||
|
||||
const result = await readOpenClawJson();
|
||||
@@ -1833,7 +1858,7 @@ describe('openai agentRuntime pin', () => {
|
||||
|
||||
expect(codex).toBeDefined();
|
||||
expect(codex.agentRuntime).toEqual({ id: 'pi' });
|
||||
expect(codex.api).toBe('openai-codex-responses');
|
||||
expect(codex.api).toBe('openai-chatgpt-responses');
|
||||
});
|
||||
|
||||
it('preserves a user-provided agentRuntime override on the openai entry', async () => {
|
||||
@@ -1967,7 +1992,7 @@ describe('setOpenClawDefaultModel for openai-codex OAuth', () => {
|
||||
|
||||
expect(defaults.primary).toBe('openai-codex/gpt-5.5');
|
||||
expect(codex.agentRuntime).toEqual({ id: 'pi' });
|
||||
expect(codex.api).toBe('openai-codex-responses');
|
||||
expect(codex.api).toBe('openai-chatgpt-responses');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2010,7 +2035,7 @@ describe('ensureOpenClawProviderAgentRuntimePins', () => {
|
||||
providers: {
|
||||
'openai-codex': {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-codex-responses',
|
||||
api: 'openai-chatgpt-responses',
|
||||
models: [{ id: 'gpt-5.5', name: 'gpt-5.5' }],
|
||||
},
|
||||
},
|
||||
@@ -2038,7 +2063,7 @@ describe('ensureOpenClawProviderAgentRuntimePins', () => {
|
||||
},
|
||||
'openai-codex': {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-codex-responses',
|
||||
api: 'openai-chatgpt-responses',
|
||||
models: [{ id: 'gpt-5.5', name: 'gpt-5.5' }],
|
||||
},
|
||||
},
|
||||
@@ -2054,6 +2079,27 @@ describe('ensureOpenClawProviderAgentRuntimePins', () => {
|
||||
expect((providers['openai-codex'] as Record<string, unknown>).agentRuntime).toEqual({ id: 'pi' });
|
||||
});
|
||||
|
||||
it('migrates legacy openai-codex-responses api values during sanitizeOpenClawConfig', async () => {
|
||||
await writeOpenClawJson({
|
||||
models: {
|
||||
providers: {
|
||||
'openai-codex': {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-codex-responses',
|
||||
models: [{ id: 'gpt-5.5', name: 'gpt-5.5' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth');
|
||||
await sanitizeOpenClawConfig();
|
||||
|
||||
const result = await readOpenClawJson();
|
||||
const providers = (result.models as Record<string, unknown>).providers as Record<string, unknown>;
|
||||
expect((providers['openai-codex'] as { api: string }).api).toBe('openai-chatgpt-responses');
|
||||
});
|
||||
|
||||
it('leaves entries untouched when the openai entry already has any agentRuntime.id', async () => {
|
||||
const initial = {
|
||||
models: {
|
||||
|
||||
@@ -42,4 +42,8 @@ describe('provider-keys', () => {
|
||||
hasConfiguredOpenAiApiKey: true,
|
||||
})).toEqual(['openai', 'openai-codex']);
|
||||
});
|
||||
|
||||
it('drops bare openai after Codex OAuth is removed and no API key remains', () => {
|
||||
expect(filterActiveProviderKeysForUi(['openai', 'minimax-portal'])).toEqual(['minimax-portal']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ const mocks = vi.hoisted(() => ({
|
||||
syncProviderConfigToOpenClaw: vi.fn(),
|
||||
updateAgentModelProvider: vi.fn(),
|
||||
updateSingleAgentModelProvider: vi.fn(),
|
||||
getProviderApiKeyFromOpenClaw: vi.fn(),
|
||||
listAgentsSnapshot: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -55,6 +56,7 @@ vi.mock('@electron/utils/openclaw-auth', () => ({
|
||||
syncProviderConfigToOpenClaw: mocks.syncProviderConfigToOpenClaw,
|
||||
updateAgentModelProvider: mocks.updateAgentModelProvider,
|
||||
updateSingleAgentModelProvider: mocks.updateSingleAgentModelProvider,
|
||||
getProviderApiKeyFromOpenClaw: mocks.getProviderApiKeyFromOpenClaw,
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/agent-config', () => ({
|
||||
@@ -123,6 +125,8 @@ describe('provider-runtime-sync refresh strategy', () => {
|
||||
mocks.removeProviderKeyFromOpenClaw.mockResolvedValue(undefined);
|
||||
mocks.updateAgentModelProvider.mockResolvedValue(undefined);
|
||||
mocks.updateSingleAgentModelProvider.mockResolvedValue(undefined);
|
||||
mocks.getProviderApiKeyFromOpenClaw.mockResolvedValue(null);
|
||||
mocks.listProviderAccounts.mockResolvedValue([]);
|
||||
mocks.listAgentsSnapshot.mockResolvedValue({ agents: [] });
|
||||
});
|
||||
|
||||
@@ -158,6 +162,42 @@ describe('provider-runtime-sync refresh strategy', () => {
|
||||
expect(gateway.debouncedRestart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('also removes bare openai config when deleting Codex OAuth without an API key', async () => {
|
||||
const gateway = createGateway('running');
|
||||
const openaiOAuthProvider = createProvider({
|
||||
id: 'openai-oauth-1',
|
||||
type: 'openai',
|
||||
model: 'gpt-5.5',
|
||||
});
|
||||
|
||||
mocks.getProviderApiKeyFromOpenClaw.mockResolvedValue(null);
|
||||
mocks.listProviderAccounts.mockResolvedValue([
|
||||
{
|
||||
id: 'openai-oauth-1',
|
||||
vendorId: 'openai',
|
||||
authMode: 'oauth_browser',
|
||||
label: 'OpenAI Codex',
|
||||
enabled: true,
|
||||
isDefault: false,
|
||||
createdAt: '2026-03-14T00:00:00.000Z',
|
||||
updatedAt: '2026-03-14T00:00:00.000Z',
|
||||
},
|
||||
]);
|
||||
mocks.getApiKey.mockResolvedValue(null);
|
||||
|
||||
await syncDeletedProviderToRuntime(
|
||||
openaiOAuthProvider,
|
||||
'openai-oauth-1',
|
||||
gateway as GatewayManager,
|
||||
'openai-codex',
|
||||
);
|
||||
|
||||
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('openai-codex');
|
||||
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('openai-oauth-1');
|
||||
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('openai');
|
||||
expect(gateway.debouncedRestart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('only clears the api-key profile when deleting a provider api key', async () => {
|
||||
const openaiProvider = createProvider({
|
||||
id: 'openai-personal',
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('ProviderService.listAccounts (openclaw.json as sole source of truth)',
|
||||
mocks.getOpenClawProvidersConfig.mockResolvedValue({
|
||||
providers: {
|
||||
openai: { baseUrl: 'https://api.openai.com/v1', api: 'openai-responses' },
|
||||
'openai-codex': { baseUrl: 'https://api.openai.com/v1', api: 'openai-codex-responses' },
|
||||
'openai-codex': { baseUrl: 'https://api.openai.com/v1', api: 'openai-chatgpt-responses' },
|
||||
},
|
||||
defaultModel: 'openai-codex/gpt-5.5',
|
||||
});
|
||||
@@ -278,6 +278,73 @@ describe('ProviderService.listAccounts (openclaw.json as sole source of truth)',
|
||||
expect(result[0].authMode).toBe('oauth_browser');
|
||||
});
|
||||
|
||||
it('hides bare openai after Codex OAuth is removed and no API key is configured', async () => {
|
||||
mocks.listProviderAccounts.mockResolvedValue([
|
||||
makeAccount({
|
||||
id: 'openai',
|
||||
vendorId: 'openai' as ProviderAccount['vendorId'],
|
||||
authMode: 'api_key',
|
||||
label: 'OpenAI',
|
||||
}),
|
||||
]);
|
||||
mocks.getApiKey.mockResolvedValue(null);
|
||||
mocks.getProviderApiKeyFromOpenClaw.mockResolvedValue(null);
|
||||
mocks.getActiveOpenClawProviders.mockResolvedValue(new Set(['openai']));
|
||||
mocks.getOpenClawProvidersConfig.mockResolvedValue({
|
||||
providers: {
|
||||
openai: { baseUrl: 'https://api.openai.com/v1', api: 'openai-responses' },
|
||||
},
|
||||
defaultModel: 'minimax-portal/MiniMax-M3',
|
||||
});
|
||||
|
||||
const result = await service.listAccounts();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
expect(mocks.deleteProviderAccount).toHaveBeenCalledWith('openai');
|
||||
expect(mocks.saveProviderAccount).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps openai visible when only OpenClaw auth-profiles has the API key', async () => {
|
||||
mocks.listProviderAccounts.mockResolvedValue([]);
|
||||
mocks.getApiKey.mockResolvedValue(null);
|
||||
mocks.getProviderApiKeyFromOpenClaw.mockImplementation(async (provider: string) => (
|
||||
provider === 'openai' ? 'sk-openclaw-imported' : null
|
||||
));
|
||||
mocks.getActiveOpenClawProviders.mockResolvedValue(new Set(['openai']));
|
||||
mocks.getOpenClawProvidersConfig.mockResolvedValue({
|
||||
providers: {
|
||||
openai: { baseUrl: 'https://api.openai.com/v1', api: 'openai-responses' },
|
||||
},
|
||||
defaultModel: 'openai/gpt-5.5',
|
||||
});
|
||||
mocks.getProviderDefinition.mockImplementation((key: string) => {
|
||||
if (key === 'openai') {
|
||||
return {
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
defaultAuthMode: 'api_key',
|
||||
defaultModelId: 'gpt-5.5',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-responses',
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const result = await service.listAccounts();
|
||||
|
||||
expect(mocks.saveProviderAccount).toHaveBeenCalledTimes(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual(expect.objectContaining({
|
||||
id: 'openai',
|
||||
vendorId: 'openai',
|
||||
authMode: 'api_key',
|
||||
}));
|
||||
expect(mocks.deleteProviderAccount).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('matches UUID-based store account to openclaw key via getOpenClawProviderKeyForType', async () => {
|
||||
mocks.listProviderAccounts.mockResolvedValue([
|
||||
makeAccount({ id: 'openrouter-uuid-1234', vendorId: 'openrouter' as ProviderAccount['vendorId'] }),
|
||||
@@ -481,23 +548,16 @@ describe('ProviderService.listAccounts (openclaw.json as sole source of truth)',
|
||||
|
||||
const result = await service.listAccounts();
|
||||
|
||||
expect(mocks.saveProviderAccount).toHaveBeenCalledTimes(2);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'openai',
|
||||
vendorId: 'openai',
|
||||
authMode: 'oauth_browser',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-5.2',
|
||||
}),
|
||||
expect(mocks.saveProviderAccount).toHaveBeenCalledTimes(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'anthropic',
|
||||
vendorId: 'anthropic',
|
||||
authMode: 'api_key',
|
||||
model: 'claude-opus-4-6',
|
||||
}),
|
||||
]));
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user