feat(opencode): support local model readiness

This commit is contained in:
777genius 2026-05-20 13:03:24 +03:00
parent 948e00aedb
commit c3b6d2dea8
4 changed files with 175 additions and 13 deletions

View file

@ -1,3 +1,5 @@
import { parseOpenCodeQualifiedModelRef } from '@shared/utils/opencodeModelRef';
import { import {
evaluateOpenCodeSupport, evaluateOpenCodeSupport,
OPENCODE_TEAM_LAUNCH_VERSION_POLICY, OPENCODE_TEAM_LAUNCH_VERSION_POLICY,
@ -34,7 +36,7 @@ export interface OpenCodeRuntimeInventory {
} }
export interface OpenCodeModelExecutionProbeResult { export interface OpenCodeModelExecutionProbeResult {
outcome: 'available' | 'unavailable' | 'unknown'; outcome: 'available' | 'unavailable' | 'not_authenticated' | 'unknown';
reason: string | null; reason: string | null;
diagnostics: string[]; diagnostics: string[];
} }
@ -103,8 +105,20 @@ const OPENCODE_RUNTIME_BINARY_UNREACHABLE_DIAGNOSTIC =
'OpenCode runtime binary is not installed or not reachable by launch preflight.'; 'OpenCode runtime binary is not installed or not reachable by launch preflight.';
const OPENCODE_UNAUTHENTICATED_FREE_MODEL_DIAGNOSTIC = const OPENCODE_UNAUTHENTICATED_FREE_MODEL_DIAGNOSTIC =
'No connected OpenCode provider found. Proceeding with a free OpenCode model route that does not require provider authentication.'; 'No connected OpenCode provider found. Proceeding with a free OpenCode model route that does not require provider authentication.';
const OPENCODE_AUTHLESS_LOCAL_MODEL_DIAGNOSTIC =
'No connected OpenCode provider found. Proceeding with a configured local OpenCode model route after execution proof.';
const OPENCODE_UNAUTHENTICATED_PAID_MODEL_DIAGNOSTIC = const OPENCODE_UNAUTHENTICATED_PAID_MODEL_DIAGNOSTIC =
'No connected OpenCode provider found. Choose a free OpenCode model such as Big Pickle, or connect a provider in OpenCode for provider-backed models.'; 'No connected OpenCode provider found. Choose a free OpenCode model such as Big Pickle, or connect a provider in OpenCode for provider-backed models.';
const AUTHLESS_LOCAL_OPEN_CODE_PROVIDER_IDS = new Set([
'atomic-chat',
'llama.cpp',
'llamacpp',
'lmstudio',
'lm-studio',
'local',
'ollama',
'vllm',
]);
export class OpenCodeTeamLaunchReadinessService { export class OpenCodeTeamLaunchReadinessService {
constructor( constructor(
@ -155,8 +169,14 @@ export class OpenCodeTeamLaunchReadinessService {
const usingFreeModelWithoutProvider = const usingFreeModelWithoutProvider =
!hasConnectedProvider && isFreeOpenCodeModelRoute(modelId); !hasConnectedProvider && isFreeOpenCodeModelRoute(modelId);
const usingConfiguredAuthlessModelWithoutProvider =
!hasConnectedProvider && isConfiguredAuthlessOpenCodeModelRoute(modelId);
if (!hasConnectedProvider && !usingFreeModelWithoutProvider) { if (
!hasConnectedProvider &&
!usingFreeModelWithoutProvider &&
!(usingConfiguredAuthlessModelWithoutProvider && input.requireExecutionProbe)
) {
return readiness({ return readiness({
state: 'not_authenticated', state: 'not_authenticated',
inventory, inventory,
@ -233,7 +253,10 @@ export class OpenCodeTeamLaunchReadinessService {
}); });
if (modelProbe.outcome !== 'available') { if (modelProbe.outcome !== 'available') {
return readiness({ return readiness({
state: 'model_unavailable', state:
modelProbe.outcome === 'not_authenticated'
? 'not_authenticated'
: 'model_unavailable',
inventory, inventory,
modelId, modelId,
capabilities, capabilities,
@ -259,7 +282,9 @@ export class OpenCodeTeamLaunchReadinessService {
? appendDiagnostics(inventory.diagnostics, [ ? appendDiagnostics(inventory.diagnostics, [
OPENCODE_UNAUTHENTICATED_FREE_MODEL_DIAGNOSTIC, OPENCODE_UNAUTHENTICATED_FREE_MODEL_DIAGNOSTIC,
]) ])
: inventory.diagnostics, : usingConfiguredAuthlessModelWithoutProvider
? appendDiagnostics(inventory.diagnostics, [OPENCODE_AUTHLESS_LOCAL_MODEL_DIAGNOSTIC])
: inventory.diagnostics,
}); });
} catch (error) { } catch (error) {
return readiness({ return readiness({
@ -282,6 +307,11 @@ function isFreeOpenCodeModelRoute(modelId: string): boolean {
); );
} }
function isConfiguredAuthlessOpenCodeModelRoute(modelId: string): boolean {
const parsed = parseOpenCodeQualifiedModelRef(modelId);
return parsed ? AUTHLESS_LOCAL_OPEN_CODE_PROVIDER_IDS.has(parsed.sourceId) : false;
}
function readiness(input: { function readiness(input: {
state: OpenCodeTeamLaunchReadinessState; state: OpenCodeTeamLaunchReadinessState;
inventory: OpenCodeRuntimeInventory | null; inventory: OpenCodeRuntimeInventory | null;

View file

@ -4,7 +4,7 @@ export interface OpenCodeQualifiedModelRef {
raw: string; raw: string;
} }
const OPEN_CODE_MODEL_REF_PATTERN = /^(?<source>[a-z0-9-]+)\/(?<model>\S.*)$/i; const OPEN_CODE_SOURCE_ID_PATTERN = /^[a-z0-9._-]+$/i;
const OPEN_CODE_SOURCE_LABELS: Record<string, string> = { const OPEN_CODE_SOURCE_LABELS: Record<string, string> = {
anthropic: 'Anthropic', anthropic: 'Anthropic',
@ -14,6 +14,10 @@ const OPEN_CODE_SOURCE_LABELS: Record<string, string> = {
gemini: 'Gemini', gemini: 'Gemini',
google: 'Google', google: 'Google',
groq: 'Groq', groq: 'Groq',
'llama.cpp': 'llama.cpp',
llamacpp: 'llama.cpp',
lmstudio: 'LM Studio',
'lm-studio': 'LM Studio',
minimax: 'MiniMax', minimax: 'MiniMax',
mistral: 'Mistral', mistral: 'Mistral',
moonshot: 'Moonshot', moonshot: 'Moonshot',
@ -24,6 +28,7 @@ const OPEN_CODE_SOURCE_LABELS: Record<string, string> = {
openrouter: 'OpenRouter', openrouter: 'OpenRouter',
together: 'Together', together: 'Together',
vertex: 'Vertex', vertex: 'Vertex',
vllm: 'vLLM',
xai: 'xAI', xai: 'xAI',
'z-ai': 'Z.AI', 'z-ai': 'Z.AI',
}; };
@ -40,7 +45,7 @@ function humanizeOpenCodeSourceId(sourceId: string): string {
} }
return normalized return normalized
.split('-') .split(/[-._]/g)
.filter(Boolean) .filter(Boolean)
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join(' '); .join(' ');
@ -50,19 +55,25 @@ export function parseOpenCodeQualifiedModelRef(
model: string | undefined | null model: string | undefined | null
): OpenCodeQualifiedModelRef | null { ): OpenCodeQualifiedModelRef | null {
const trimmed = model?.trim(); const trimmed = model?.trim();
if (!trimmed) { if (!trimmed || /\s/.test(trimmed)) {
return null; return null;
} }
const match = OPEN_CODE_MODEL_REF_PATTERN.exec(trimmed); const separatorIndex = trimmed.indexOf('/');
if (!match?.groups?.source || !match.groups.model) { if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) {
return null;
}
const sourceId = trimmed.slice(0, separatorIndex).toLowerCase();
const modelId = trimmed.slice(separatorIndex + 1);
if (!OPEN_CODE_SOURCE_ID_PATTERN.test(sourceId) || !modelId) {
return null; return null;
} }
return { return {
raw: trimmed, raw: `${sourceId}/${modelId}`,
sourceId: match.groups.source.toLowerCase(), sourceId,
modelId: match.groups.model, modelId,
}; };
} }

View file

@ -114,6 +114,91 @@ describe('OpenCodeTeamLaunchReadinessService', () => {
expect(ports.mcpTools.prove).not.toHaveBeenCalled(); expect(ports.mcpTools.prove).not.toHaveBeenCalled();
}); });
it('requires execution probe before allowing unauthenticated configured local models', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['llama.cpp/qwen-test:0.5b'],
},
});
await expect(
service(ports).check(
readinessInput({
selectedModel: 'llama.cpp/qwen-test:0.5b',
requireExecutionProbe: false,
})
)
).resolves.toMatchObject({
state: 'not_authenticated',
launchAllowed: false,
modelId: 'llama.cpp/qwen-test:0.5b',
});
expect(ports.modelExecution.verify).not.toHaveBeenCalled();
});
it('allows unauthenticated configured local models after execution proof', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['llama.cpp/qwen-test:0.5b'],
},
});
await expect(
service(ports).check(
readinessInput({
selectedModel: 'llama.cpp/qwen-test:0.5b',
requireExecutionProbe: true,
})
)
).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
modelId: 'llama.cpp/qwen-test:0.5b',
diagnostics: [
'No connected OpenCode provider found. Proceeding with a configured local OpenCode model route after execution proof.',
],
});
expect(ports.modelExecution.verify).toHaveBeenCalledWith({
projectPath: '/repo',
modelId: 'llama.cpp/qwen-test:0.5b',
inventory: expect.objectContaining({
connectedProviders: [],
}),
});
});
it('maps execution probe authentication failures to not_authenticated', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['llama.cpp/qwen-test:0.5b'],
},
modelProbe: {
outcome: 'not_authenticated',
reason: 'local server rejected request',
diagnostics: ['local server rejected request'],
},
});
await expect(
service(ports).check(
readinessInput({
selectedModel: 'llama.cpp/qwen-test:0.5b',
requireExecutionProbe: true,
})
)
).resolves.toMatchObject({
state: 'not_authenticated',
launchAllowed: false,
missing: ['local server rejected request'],
});
});
it('blocks unsupported versions before MCP and model probes', async () => { it('blocks unsupported versions before MCP and model probes', async () => {
const ports = createPorts({ const ports = createPorts({
inventory: { version: '1.4.0' }, inventory: { version: '1.4.0' },
@ -271,7 +356,7 @@ function createPorts(
toolProof?: OpenCodeMcpToolProof; toolProof?: OpenCodeMcpToolProof;
runtimeStores?: RuntimeStoreReadinessCheck; runtimeStores?: RuntimeStoreReadinessCheck;
modelProbe?: { modelProbe?: {
outcome: 'available' | 'unavailable' | 'unknown'; outcome: 'available' | 'unavailable' | 'not_authenticated' | 'unknown';
reason: string | null; reason: string | null;
diagnostics: string[]; diagnostics: string[];
}; };

View file

@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import {
getOpenCodeQualifiedModelSourceLabel,
parseOpenCodeQualifiedModelRef,
} from '../../../src/shared/utils/opencodeModelRef';
describe('opencodeModelRef', () => {
it('parses dotted providers and model ids with nested slashes', () => {
expect(parseOpenCodeQualifiedModelRef('llama.cpp/qwen3-coder:a3b')).toEqual({
raw: 'llama.cpp/qwen3-coder:a3b',
sourceId: 'llama.cpp',
modelId: 'qwen3-coder:a3b',
});
expect(parseOpenCodeQualifiedModelRef('lmstudio/google/gemma-3n-e4b')).toEqual({
raw: 'lmstudio/google/gemma-3n-e4b',
sourceId: 'lmstudio',
modelId: 'google/gemma-3n-e4b',
});
});
it('rejects whitespace and unscoped OpenCode model refs', () => {
expect(parseOpenCodeQualifiedModelRef('llama.cpp/qwen test')).toBeNull();
expect(parseOpenCodeQualifiedModelRef('qwen3-coder:a3b')).toBeNull();
});
it('labels common local OpenCode providers', () => {
expect(getOpenCodeQualifiedModelSourceLabel('llama.cpp/qwen3-coder:a3b')).toBe(
'llama.cpp'
);
expect(getOpenCodeQualifiedModelSourceLabel('lmstudio/google/gemma-3n-e4b')).toBe(
'LM Studio'
);
});
});