fix(opencode): accept openrouter nested model aliases

This commit is contained in:
777genius 2026-04-25 17:44:28 +03:00
parent 523d450bc8
commit 661f308ab4
4 changed files with 209 additions and 3 deletions

View file

@ -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<string>();
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;

View file

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

View file

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

View file

@ -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 = {