fix(opencode): accept openrouter nested model aliases
This commit is contained in:
parent
523d450bc8
commit
661f308ab4
4 changed files with 209 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue