From 66692f177768066012f588c750b001f2d052924b Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:46:06 +0800 Subject: [PATCH] fix: use openai-chatgpt-responses for Codex OAuth and hide stale OpenAI API slot after logout (#1113) --- .../providers/provider-runtime-sync.ts | 21 +++++ .../services/providers/provider-service.ts | 29 ++++--- electron/shared/providers/types.ts | 16 +++- electron/utils/openclaw-auth.ts | 36 +++++++- electron/utils/provider-keys.ts | 13 ++- ...provider-switch-api-protocol-validation.md | 2 +- shared/host-api/contract.ts | 2 +- src/lib/providers.ts | 2 +- tests/unit/openclaw-auth.test.ts | 56 +++++++++++-- tests/unit/provider-keys.test.ts | 4 + tests/unit/provider-runtime-sync.test.ts | 40 +++++++++ .../provider-service-stale-cleanup.test.ts | 84 ++++++++++++++++--- 12 files changed, 263 insertions(+), 42 deletions(-) diff --git a/electron/services/providers/provider-runtime-sync.ts b/electron/services/providers/provider-runtime-sync.ts index c5c1ea1a..fe0832bc 100644 --- a/electron/services/providers/provider-runtime-sync.ts +++ b/electron/services/providers/provider-runtime-sync.ts @@ -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 { diff --git a/electron/services/providers/provider-service.ts b/electron/services/providers/provider-service.ts index cc7aefff..65fd83cf 100644 --- a/electron/services/providers/provider-service.ts +++ b/electron/services/providers/provider-service.ts @@ -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); } diff --git a/electron/shared/providers/types.ts b/electron/shared/providers/types.ts index 29ce0d2e..08beb43c 100644 --- a/electron/shared/providers/types.ts +++ b/electron/shared/providers/types.ts @@ -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; + +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)[api]; + return migrated; +} + export class InvalidApiProtocolError extends Error { constructor(public readonly api: unknown, public readonly providerKey?: string) { super( diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index 54b9e6de..1c7d84c9 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -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): string[] { + const migrated: string[] = []; + const models = (config.models || {}) as Record; + const providers = (models.providers || {}) as Record; + + for (const [key, entry] of Object.entries(providers)) { + if (!isPlainRecord(entry)) continue; + const entryObj = entry as Record; + 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 { const removed: string[] = []; await withConfigLock(async () => { @@ -1127,9 +1146,14 @@ export async function pruneInvalidApiProviderEntries(): Promise { const providers = (models.providers || {}) as Record; 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).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 = { /** 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 { } } + 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; diff --git a/electron/utils/provider-keys.ts b/electron/utils/provider-keys.ts index 7d8f0fbc..4ccf6124 100644 --- a/electron/utils/provider-keys.ts +++ b/electron/utils/provider-keys.ts @@ -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 { diff --git a/harness/specs/tasks/provider-switch-api-protocol-validation.md b/harness/specs/tasks/provider-switch-api-protocol-validation.md index a9924032..fc65e3a4 100644 --- a/harness/specs/tasks/provider-switch-api-protocol-validation.md +++ b/harness/specs/tasks/provider-switch-api-protocol-validation.md @@ -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 diff --git a/shared/host-api/contract.ts b/shared/host-api/contract.ts index 4fedd358..d2401219 100644 --- a/shared/host-api/contract.ts +++ b/shared/host-api/contract.ts @@ -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' diff --git a/src/lib/providers.ts b/src/lib/providers.ts index a7fea8e4..d692fef5 100644 --- a/src/lib/providers.ts +++ b/src/lib/providers.ts @@ -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' diff --git a/tests/unit/openclaw-auth.test.ts b/tests/unit/openclaw-auth.test.ts index b37958d8..14ef227d 100644 --- a/tests/unit/openclaw-auth.test.ts +++ b/tests/unit/openclaw-auth.test.ts @@ -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).providers as Record; + 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).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).providers as Record; + 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: { diff --git a/tests/unit/provider-keys.test.ts b/tests/unit/provider-keys.test.ts index 1e937810..7c604029 100644 --- a/tests/unit/provider-keys.test.ts +++ b/tests/unit/provider-keys.test.ts @@ -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']); + }); }); diff --git a/tests/unit/provider-runtime-sync.test.ts b/tests/unit/provider-runtime-sync.test.ts index 673ce157..a3aefcfd 100644 --- a/tests/unit/provider-runtime-sync.test.ts +++ b/tests/unit/provider-runtime-sync.test.ts @@ -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', diff --git a/tests/unit/provider-service-stale-cleanup.test.ts b/tests/unit/provider-service-stale-cleanup.test.ts index 4c979ffa..363e22b1 100644 --- a/tests/unit/provider-service-stale-cleanup.test.ts +++ b/tests/unit/provider-service-stale-cleanup.test.ts @@ -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', }), - ])); + ]); }); });