From 661f308ab4effa40b442d29ab5e78d5e225aa4e6 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 25 Apr 2026 17:44:28 +0300 Subject: [PATCH] fix(opencode): accept openrouter nested model aliases --- .../services/team/TeamProvisioningService.ts | 40 ++++++ .../TeamProvisioningServicePrepare.test.ts | 119 +++++++++++++++++- .../extensions/skills/SkillsPanel.test.ts | 2 +- .../TeamModelSelectorDisabledState.test.ts | 51 ++++++++ 4 files changed, 209 insertions(+), 3 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 1357dab0..ce19c902 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -10113,6 +10113,25 @@ export class TeamProvisioningService { }; } + const equivalentOpenRouterMatches = this.findEquivalentOpenRouterModelIds( + trimmedModelId, + availableModels + ); + if (equivalentOpenRouterMatches.length === 1) { + return { + ok: true, + resolvedModelId: equivalentOpenRouterMatches[0], + }; + } + if (equivalentOpenRouterMatches.length > 1) { + return { + ok: false, + reason: + `Selected model ${trimmedModelId} matched multiple live provider models: ` + + equivalentOpenRouterMatches.join(', '), + }; + } + if (trimmedModelId.includes('/')) { return { ok: false, @@ -10144,6 +10163,27 @@ export class TeamProvisioningService { }; } + private findEquivalentOpenRouterModelIds( + requestedModelId: string, + availableModels: readonly string[] + ): string[] { + const equivalentIds = new Set(); + + if (requestedModelId.startsWith('openrouter/')) { + equivalentIds.add(requestedModelId.slice('openrouter/'.length)); + } else if (requestedModelId.includes('/')) { + equivalentIds.add(`openrouter/${requestedModelId}`); + } + + if (equivalentIds.size === 0) { + return []; + } + + return Array.from( + new Set(availableModels.filter((candidate) => equivalentIds.has(candidate.trim()))) + ); + } + private resolveProviderCompatibilityModel(params: { providerId: TeamProviderId; requestedModelId: string; diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 5505c405..86c58e32 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -189,8 +189,9 @@ function writeMockMcpServer( | 'lead-briefing-error' ): string { const scriptPath = path.join(targetDir, `mock-mcp-${variant}.js`); - const tools = REQUIRED_MOCK_AGENT_TEAMS_TOOLS - .filter((name) => variant !== 'missing-member-briefing' || name !== 'member_briefing') + const tools = REQUIRED_MOCK_AGENT_TEAMS_TOOLS.filter( + (name) => variant !== 'missing-member-briefing' || name !== 'member_briefing' + ) .filter((name) => variant !== 'missing-lead-briefing' || name !== 'lead_briefing') .map((name) => ({ name })); @@ -721,6 +722,120 @@ describe('TeamProvisioningService prepare/auth behavior', () => { ); }); + it('accepts OpenRouter-selected models when OpenCode reports the nested model id without provider prefix', 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: 'qwen/qwen3-coder', + availableModels: ['qwen/qwen3-coder'], + 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(true); + expect(result.details).toEqual([ + 'Selected model openrouter/qwen/qwen3-coder is compatible. Deep verification pending.', + ]); + expect(prepare).toHaveBeenCalledTimes(1); + }); + + it('accepts saved nested OpenRouter model ids when OpenCode reports the provider-scoped id', 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: 'openrouter/qwen/qwen3-coder', + availableModels: ['openrouter/qwen/qwen3-coder'], + 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: ['qwen/qwen3-coder'], + modelVerificationMode: 'compatibility', + }); + + expect(result.ready).toBe(true); + expect(result.details).toEqual([ + 'Selected model qwen/qwen3-coder is compatible. Deep verification pending.', + ]); + 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/components/extensions/skills/SkillsPanel.test.ts b/test/renderer/components/extensions/skills/SkillsPanel.test.ts index 079fc18f..a19ce837 100644 --- a/test/renderer/components/extensions/skills/SkillsPanel.test.ts +++ b/test/renderer/components/extensions/skills/SkillsPanel.test.ts @@ -559,7 +559,7 @@ describe('SkillsPanel', () => { }); expect(host.textContent).toContain( - 'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic, Codex, and OpenCode.' + 'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic, Codex, and OpenCode (75+ LLM providers).' ); expect(host.textContent).toContain('Codex only'); diff --git a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts index a8afbf61..04d8ec97 100644 --- a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts +++ b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts @@ -315,6 +315,57 @@ describe('TeamModelSelector disabled Codex models', () => { }); }); + it('constrains long runtime model lists so the selector scrolls', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + storeState.cliStatus = { + providers: [ + { + providerId: 'codex', + models: [ + 'gpt-5.4', + 'gpt-5.4-mini', + 'gpt-5.3-codex', + 'gpt-5.3-codex-spark', + 'gpt-5.2', + 'gpt-5.1-codex', + 'gpt-5.1-codex-mini', + 'gpt-5', + 'gpt-4.1', + ], + }, + ], + }; + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(TeamModelSelector, { + providerId: 'codex', + onProviderChange: () => undefined, + value: '', + onValueChange: () => undefined, + }) + ); + await Promise.resolve(); + }); + + const modelGrid = host.querySelector( + '[data-testid="team-model-selector-model-grid"]' + ) as HTMLElement | null; + + expect(modelGrid).toBeTruthy(); + expect(modelGrid?.style.maxHeight).toBe('400px'); + expect(modelGrid?.className).toContain('overflow-y-auto'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + it('keeps the runtime-reported Codex model list visible during a background refresh', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliStatus = {