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 {
evaluateOpenCodeSupport,
OPENCODE_TEAM_LAUNCH_VERSION_POLICY,
@ -34,7 +36,7 @@ export interface OpenCodeRuntimeInventory {
}
export interface OpenCodeModelExecutionProbeResult {
outcome: 'available' | 'unavailable' | 'unknown';
outcome: 'available' | 'unavailable' | 'not_authenticated' | 'unknown';
reason: string | null;
diagnostics: string[];
}
@ -103,8 +105,20 @@ const OPENCODE_RUNTIME_BINARY_UNREACHABLE_DIAGNOSTIC =
'OpenCode runtime binary is not installed or not reachable by launch preflight.';
const OPENCODE_UNAUTHENTICATED_FREE_MODEL_DIAGNOSTIC =
'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 =
'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 {
constructor(
@ -155,8 +169,14 @@ export class OpenCodeTeamLaunchReadinessService {
const usingFreeModelWithoutProvider =
!hasConnectedProvider && isFreeOpenCodeModelRoute(modelId);
const usingConfiguredAuthlessModelWithoutProvider =
!hasConnectedProvider && isConfiguredAuthlessOpenCodeModelRoute(modelId);
if (!hasConnectedProvider && !usingFreeModelWithoutProvider) {
if (
!hasConnectedProvider &&
!usingFreeModelWithoutProvider &&
!(usingConfiguredAuthlessModelWithoutProvider && input.requireExecutionProbe)
) {
return readiness({
state: 'not_authenticated',
inventory,
@ -233,7 +253,10 @@ export class OpenCodeTeamLaunchReadinessService {
});
if (modelProbe.outcome !== 'available') {
return readiness({
state: 'model_unavailable',
state:
modelProbe.outcome === 'not_authenticated'
? 'not_authenticated'
: 'model_unavailable',
inventory,
modelId,
capabilities,
@ -259,7 +282,9 @@ export class OpenCodeTeamLaunchReadinessService {
? appendDiagnostics(inventory.diagnostics, [
OPENCODE_UNAUTHENTICATED_FREE_MODEL_DIAGNOSTIC,
])
: inventory.diagnostics,
: usingConfiguredAuthlessModelWithoutProvider
? appendDiagnostics(inventory.diagnostics, [OPENCODE_AUTHLESS_LOCAL_MODEL_DIAGNOSTIC])
: inventory.diagnostics,
});
} catch (error) {
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: {
state: OpenCodeTeamLaunchReadinessState;
inventory: OpenCodeRuntimeInventory | null;

View file

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

View file

@ -114,6 +114,91 @@ describe('OpenCodeTeamLaunchReadinessService', () => {
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 () => {
const ports = createPorts({
inventory: { version: '1.4.0' },
@ -271,7 +356,7 @@ function createPorts(
toolProof?: OpenCodeMcpToolProof;
runtimeStores?: RuntimeStoreReadinessCheck;
modelProbe?: {
outcome: 'available' | 'unavailable' | 'unknown';
outcome: 'available' | 'unavailable' | 'not_authenticated' | 'unknown';
reason: string | null;
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'
);
});
});