feat: add dynamic model discovery and routing for OpenCode provider

- Update isOpencodeModel() to detect dynamic models with provider/model format
  (e.g., github-copilot/gpt-4o, google/gemini-2.5-pro, zai-coding-plan/glm-4.7)
- Update resolveModelString() to recognize and pass through OpenCode models
- Update enhance route to route OpenCode models to OpenCode provider
- Fix OpenCode CLI command format: use --format json (not stream-json)
- Remove unsupported -q and - flags from CLI arguments
- Update normalizeEvent() to handle actual OpenCode JSON event format
- Add dynamic model configuration UI with provider grouping
- Cache providers and models in app store for snappier navigation
- Show authenticated providers in OpenCode CLI status card

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Stefan de Vogelaere
2026-01-11 20:08:25 +01:00
committed by DhanushSantosh
parent ed65f70315
commit 6c5206daf4
13 changed files with 2247 additions and 265 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import { resolveModelString } from '@automaker/model-resolver';
import {
CLAUDE_MODEL_MAP,
isCursorModel,
isOpencodeModel,
stripProviderPrefix,
ThinkingLevel,
getThinkingTokenBudget,
@@ -91,13 +92,13 @@ async function extractTextFromStream(
}
/**
* Execute enhancement using Cursor provider
* Execute enhancement using a provider (Cursor, OpenCode, etc.)
*
* @param prompt - The enhancement prompt
* @param model - The Cursor model to use
* @param model - The model to use
* @returns The enhanced text
*/
async function executeWithCursor(prompt: string, model: string): Promise<string> {
async function executeWithProvider(prompt: string, model: string): Promise<string> {
const provider = ProviderFactory.getProviderForModel(model);
// Strip provider prefix - providers expect bare model IDs
const bareModel = stripProviderPrefix(model);
@@ -212,7 +213,14 @@ export function createEnhanceHandler(
// Cursor doesn't have a separate system prompt concept, so combine them
const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`;
enhancedText = await executeWithCursor(combinedPrompt, resolvedModel);
enhancedText = await executeWithProvider(combinedPrompt, resolvedModel);
} else if (isOpencodeModel(resolvedModel)) {
// Use OpenCode provider for OpenCode models (static and dynamic)
logger.info(`Using OpenCode provider for model: ${resolvedModel}`);
// OpenCode CLI handles the system prompt, so combine them
const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`;
enhancedText = await executeWithProvider(combinedPrompt, resolvedModel);
} else {
// Use Claude SDK for Claude models
logger.info(`Using Claude provider for model: ${resolvedModel}`);

View File

@@ -24,6 +24,12 @@ import { createDeauthCursorHandler } from './routes/deauth-cursor.js';
import { createAuthOpencodeHandler } from './routes/auth-opencode.js';
import { createDeauthOpencodeHandler } from './routes/deauth-opencode.js';
import { createOpencodeStatusHandler } from './routes/opencode-status.js';
import {
createGetOpencodeModelsHandler,
createRefreshOpencodeModelsHandler,
createGetOpencodeProvidersHandler,
createClearOpencodeCacheHandler,
} from './routes/opencode-models.js';
import {
createGetCursorConfigHandler,
createSetCursorDefaultModelHandler,
@@ -65,6 +71,12 @@ export function createSetupRoutes(): Router {
router.get('/opencode-status', createOpencodeStatusHandler());
router.post('/auth-opencode', createAuthOpencodeHandler());
router.post('/deauth-opencode', createDeauthOpencodeHandler());
// OpenCode Dynamic Model Discovery routes
router.get('/opencode/models', createGetOpencodeModelsHandler());
router.post('/opencode/models/refresh', createRefreshOpencodeModelsHandler());
router.get('/opencode/providers', createGetOpencodeProvidersHandler());
router.post('/opencode/cache/clear', createClearOpencodeCacheHandler());
router.get('/cursor-config', createGetCursorConfigHandler());
router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler());
router.post('/cursor-config/models', createSetCursorModelsHandler());

View File

@@ -0,0 +1,191 @@
/**
* OpenCode Dynamic Models API Routes
*
* Provides endpoints for:
* - GET /api/setup/opencode/models - Get available models (cached or refreshed)
* - POST /api/setup/opencode/models/refresh - Force refresh models from CLI
* - GET /api/setup/opencode/providers - Get authenticated providers
*/
import type { Request, Response } from 'express';
import {
OpencodeProvider,
type OpenCodeProviderInfo,
} from '../../../providers/opencode-provider.js';
import { getErrorMessage, logError } from '../common.js';
import type { ModelDefinition } from '@automaker/types';
import { createLogger } from '@automaker/utils';
const logger = createLogger('OpenCodeModelsRoute');
// Singleton provider instance for caching
let providerInstance: OpencodeProvider | null = null;
function getProvider(): OpencodeProvider {
if (!providerInstance) {
providerInstance = new OpencodeProvider();
}
return providerInstance;
}
/**
* Response type for models endpoint
*/
interface ModelsResponse {
success: boolean;
models?: ModelDefinition[];
count?: number;
cached?: boolean;
error?: string;
}
/**
* Response type for providers endpoint
*/
interface ProvidersResponse {
success: boolean;
providers?: OpenCodeProviderInfo[];
authenticated?: OpenCodeProviderInfo[];
error?: string;
}
/**
* Creates handler for GET /api/setup/opencode/models
*
* Returns currently available models (from cache if available).
* Query params:
* - refresh=true: Force refresh from CLI before returning
*
* Note: If cache is empty, this will trigger a refresh to get dynamic models.
*/
export function createGetOpencodeModelsHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const provider = getProvider();
const forceRefresh = req.query.refresh === 'true';
let models: ModelDefinition[];
let cached = true;
if (forceRefresh) {
models = await provider.refreshModels();
cached = false;
} else {
// Check if we have cached models
const cachedModels = provider.getAvailableModels();
// If cache only has default models (provider.hasCachedModels() would be false),
// trigger a refresh to get dynamic models
if (!provider.hasCachedModels()) {
models = await provider.refreshModels();
cached = false;
} else {
models = cachedModels;
}
}
const response: ModelsResponse = {
success: true,
models,
count: models.length,
cached,
};
res.json(response);
} catch (error) {
logError(error, 'Get OpenCode models failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
} as ModelsResponse);
}
};
}
/**
* Creates handler for POST /api/setup/opencode/models/refresh
*
* Forces a refresh of models from the OpenCode CLI.
*/
export function createRefreshOpencodeModelsHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
const provider = getProvider();
const models = await provider.refreshModels();
const response: ModelsResponse = {
success: true,
models,
count: models.length,
cached: false,
};
res.json(response);
} catch (error) {
logger.error('Refresh OpenCode models failed:', error);
logError(error, 'Refresh OpenCode models failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
} as ModelsResponse);
}
};
}
/**
* Creates handler for GET /api/setup/opencode/providers
*
* Returns authenticated providers from OpenCode CLI.
* This calls `opencode auth list` to get provider status.
*/
export function createGetOpencodeProvidersHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
const provider = getProvider();
const providers = await provider.fetchAuthenticatedProviders();
// Filter to only authenticated providers
const authenticated = providers.filter((p) => p.authenticated);
const response: ProvidersResponse = {
success: true,
providers,
authenticated,
};
res.json(response);
} catch (error) {
logger.error('Get OpenCode providers failed:', error);
logError(error, 'Get OpenCode providers failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
} as ProvidersResponse);
}
};
}
/**
* Creates handler for POST /api/setup/opencode/cache/clear
*
* Clears the model cache, forcing a fresh fetch on next access.
*/
export function createClearOpencodeCacheHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
const provider = getProvider();
provider.clearModelCache();
res.json({
success: true,
message: 'OpenCode model cache cleared',
});
} catch (error) {
logError(error, 'Clear OpenCode cache failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}