From 351ae4f4edd1a5886fccb52da232263f76757621 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 25 Apr 2026 18:01:49 +0300 Subject: [PATCH] fix(opencode): explain missing openrouter catalog provider --- .../services/team/TeamProvisioningService.ts | 36 +++++++++++ .../TeamProvisioningServicePrepare.test.ts | 59 +++++++++++++++++++ ...RuntimeProviderManagementPanelView.test.ts | 15 +++-- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index ce19c902..8a8f818b 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -10133,6 +10133,24 @@ export class TeamProvisioningService { } if (trimmedModelId.includes('/')) { + const requestedProviderId = this.extractOpenCodeCatalogProviderId(trimmedModelId); + const availableProviderIds = this.getOpenCodeCatalogProviderIds(availableModels); + if ( + requestedProviderId === 'openrouter' && + !availableProviderIds.includes(requestedProviderId) + ) { + const availableProviderList = + availableProviderIds.length > 0 ? availableProviderIds.join(', ') : 'none'; + return { + ok: false, + reason: + `OpenCode provider "openrouter" for selected model "${trimmedModelId}" ` + + 'is not available in the current runtime catalog for this project/profile. ' + + `Live catalog providers: ${availableProviderList}. ` + + 'Connect OpenRouter in OpenCode provider management or choose one of the listed OpenCode models.', + }; + } + return { ok: false, reason: `Selected model ${trimmedModelId} was not found in the live provider catalog.`, @@ -10163,6 +10181,24 @@ export class TeamProvisioningService { }; } + private extractOpenCodeCatalogProviderId(modelId: string): string | null { + const separatorIndex = modelId.indexOf('/'); + if (separatorIndex <= 0) { + return null; + } + return modelId.slice(0, separatorIndex).trim().toLowerCase() || null; + } + + private getOpenCodeCatalogProviderIds(availableModels: readonly string[]): string[] { + return Array.from( + new Set( + availableModels + .map((modelId) => this.extractOpenCodeCatalogProviderId(modelId.trim())) + .filter((providerId): providerId is string => Boolean(providerId)) + ) + ).sort((left, right) => left.localeCompare(right)); + } + private findEquivalentOpenRouterModelIds( requestedModelId: string, availableModels: readonly string[] diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 86c58e32..0990bf71 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -836,6 +836,65 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(prepare).toHaveBeenCalledTimes(1); }); + it('explains OpenRouter selected-model failures when the current OpenCode catalog has no OpenRouter provider', async () => { + const prepare = vi.fn(async (input: { model?: string; runtimeOnly?: boolean }) => ({ + ok: true as const, + providerId: 'opencode' as const, + modelId: input.model ?? null, + diagnostics: [], + warnings: [], + })); + const registry = new TeamRuntimeAdapterRegistry([ + { + providerId: 'opencode', + prepare, + getLastOpenCodeTeamLaunchReadiness: vi.fn(() => ({ + state: 'ready', + launchAllowed: true, + modelId: 'opencode/minimax-m2.5-free', + availableModels: ['opencode/minimax-m2.5-free', 'openai/gpt-5.4'], + opencodeVersion: '1.0.0', + installMethod: 'unknown', + binaryPath: 'opencode', + hostHealthy: true, + appMcpConnected: true, + requiredToolsPresent: true, + permissionBridgeReady: true, + runtimeStoresReady: true, + supportLevel: 'production_supported', + missing: [], + diagnostics: [], + evidence: { + capabilitiesReady: true, + mcpToolProofRoute: 'mcp:tools/list', + observedMcpTools: [], + runtimeStoreReadinessReason: 'runtime_store_manifest_valid', + }, + })), + launch: vi.fn(), + reconcile: vi.fn(), + stop: vi.fn(), + } as any, + ]); + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(registry); + + const result = await svc.prepareForProvisioning(tempRoot, { + providerId: 'opencode', + forceFresh: true, + modelIds: ['openrouter/qwen/qwen3-coder'], + modelVerificationMode: 'compatibility', + }); + + expect(result.ready).toBe(false); + expect(result.message).toContain( + 'OpenCode provider "openrouter" for selected model "openrouter/qwen/qwen3-coder" is not available' + ); + expect(result.message).toContain('Live catalog providers: openai, opencode.'); + expect(result.message).toContain('Connect OpenRouter in OpenCode provider management'); + expect(prepare).toHaveBeenCalledTimes(1); + }); + it('treats retryable OpenCode compatibility failures as blocking selected-model diagnostics', async () => { const prepare = vi.fn(async () => ({ ok: false as const, diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts index f7fe561a..08f631ed 100644 --- a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts +++ b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts @@ -352,11 +352,16 @@ describe('RuntimeProviderManagementPanelView', () => { ) as HTMLElement | undefined; expect(connectedBadge?.style.color).toBeTruthy(); expect( - host.querySelector('[data-testid="runtime-provider-model-search"]')?.style.paddingLeft + ( + host.querySelector( + '[data-testid="runtime-provider-model-search"]' + ) as HTMLElement | null + )?.style.paddingLeft ).toBe('42px'); - expect(host.querySelector('[data-testid="runtime-provider-model-list"]')?.style.maxHeight).toBe( - '300px' - ); + expect( + (host.querySelector('[data-testid="runtime-provider-model-list"]') as HTMLElement | null) + ?.style.maxHeight + ).toBe('300px'); expect(host.textContent).not.toContain('OpenRouterfree'); const firstTestButton = Array.from(host.querySelectorAll('button')).find( (button) => button.textContent?.trim() === 'Test' @@ -364,7 +369,7 @@ describe('RuntimeProviderManagementPanelView', () => { expect(firstTestButton?.className).toContain('border'); const modelResult = host.querySelector( '[data-testid="runtime-provider-model-result-openrouter/openai/gpt-oss-20b:free"]' - ); + ) as HTMLElement | null; expect(modelResult?.style.color).toBe('#86efac'); expect((host.textContent ?? '').indexOf('qwen/qwen3-coder-flash')).toBeLessThan( (host.textContent ?? '').indexOf('opencode/big-pickle')