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('/')) {
|
if (trimmedModelId.includes('/')) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
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: {
|
private resolveProviderCompatibilityModel(params: {
|
||||||
providerId: TeamProviderId;
|
providerId: TeamProviderId;
|
||||||
requestedModelId: string;
|
requestedModelId: string;
|
||||||
|
|
|
||||||
|
|
@ -189,8 +189,9 @@ function writeMockMcpServer(
|
||||||
| 'lead-briefing-error'
|
| 'lead-briefing-error'
|
||||||
): string {
|
): string {
|
||||||
const scriptPath = path.join(targetDir, `mock-mcp-${variant}.js`);
|
const scriptPath = path.join(targetDir, `mock-mcp-${variant}.js`);
|
||||||
const tools = REQUIRED_MOCK_AGENT_TEAMS_TOOLS
|
const tools = REQUIRED_MOCK_AGENT_TEAMS_TOOLS.filter(
|
||||||
.filter((name) => variant !== 'missing-member-briefing' || name !== 'member_briefing')
|
(name) => variant !== 'missing-member-briefing' || name !== 'member_briefing'
|
||||||
|
)
|
||||||
.filter((name) => variant !== 'missing-lead-briefing' || name !== 'lead_briefing')
|
.filter((name) => variant !== 'missing-lead-briefing' || name !== 'lead_briefing')
|
||||||
.map((name) => ({ name }));
|
.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 () => {
|
it('treats retryable OpenCode compatibility failures as blocking selected-model diagnostics', async () => {
|
||||||
const prepare = vi.fn(async () => ({
|
const prepare = vi.fn(async () => ({
|
||||||
ok: false as const,
|
ok: false as const,
|
||||||
|
|
|
||||||
|
|
@ -559,7 +559,7 @@ describe('SkillsPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain(
|
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');
|
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 () => {
|
it('keeps the runtime-reported Codex model list visible during a background refresh', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
storeState.cliStatus = {
|
storeState.cliStatus = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue