fix(opencode): explain missing openrouter catalog provider

This commit is contained in:
777genius 2026-04-25 18:01:49 +03:00
parent 661f308ab4
commit 351ae4f4ed
3 changed files with 105 additions and 5 deletions

View file

@ -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[]

View file

@ -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,

View file

@ -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')