* feat: fix open-source build and add Ollama model picker - Fix build failures by stubbing 62+ missing Anthropic-internal modules with a catch-all plugin in scripts/build.ts - Add runtime shim exports (isReplBridgeActive, getReplBridgeHandle) in bootstrap/state.ts for feature-gated code references - Add /model picker support for Ollama: fetches available models from Ollama server at startup and displays them in the model selection menu - Add Ollama model validation against cached server model list Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback for Ollama integration - Move Ollama validation before enterprise allowlist check in validateModel - Truncate model list in error messages to first 5 entries - Fix isOllamaProvider() to detect OLLAMA_BASE_URL-only configurations - Reuse getOllamaApiBaseUrl() from providerDiscovery instead of duplicating - Reset fetchPromise on failure to allow retry in prefetchOllamaModels - Include Default option in Ollama model picker, prevent Claude model fallthrough - Add file existence check for src/tasks/ stubs in build script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use pre-scanned exact-match resolvers to avoid Bun bundler corruption Bun's onResolve plugin corrupts the module graph even when returning null for non-matching imports. This caused lodash-es memoize and zod's util namespace to be incorrectly tree-shaken, producing runtime ReferenceErrors. Replace all pattern-based onResolve hooks with a pre-build scan that identifies missing modules upfront, then registers exact-match resolvers only for confirmed missing imports. This avoids touching any valid module resolution paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: move Ollama model prefetch outside startup throttle gate prefetchOllamaModels() was inside the skipStartupPrefetches condition, so it would be skipped on subsequent launches due to the bgRefresh throttle timestamp. Ollama model fetch targets a local/remote server and is fast & cheap, so it should always run at startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
180 lines
5.5 KiB
TypeScript
180 lines
5.5 KiB
TypeScript
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
|
import { MODEL_ALIASES } from './aliases.js'
|
|
import { isModelAllowed } from './modelAllowlist.js'
|
|
import { getAPIProvider } from './providers.js'
|
|
import { sideQuery } from '../sideQuery.js'
|
|
import {
|
|
NotFoundError,
|
|
APIError,
|
|
APIConnectionError,
|
|
AuthenticationError,
|
|
} from '@anthropic-ai/sdk'
|
|
import { getModelStrings } from './modelStrings.js'
|
|
import { getCachedOllamaModelOptions, isOllamaProvider } from './ollamaModels.js'
|
|
|
|
// Cache valid models to avoid repeated API calls
|
|
const validModelCache = new Map<string, boolean>()
|
|
|
|
/**
|
|
* Validates a model by attempting an actual API call.
|
|
*/
|
|
export async function validateModel(
|
|
model: string,
|
|
): Promise<{ valid: boolean; error?: string }> {
|
|
const normalizedModel = model.trim()
|
|
|
|
// Empty model is invalid
|
|
if (!normalizedModel) {
|
|
return { valid: false, error: 'Model name cannot be empty' }
|
|
}
|
|
|
|
// For Ollama provider, validate against cached model list instead of API call
|
|
// (skip enterprise allowlist since Ollama models are user-managed)
|
|
if (getAPIProvider() === 'openai' && isOllamaProvider()) {
|
|
const ollamaModels = getCachedOllamaModelOptions()
|
|
const found = ollamaModels.some(m => m.value === normalizedModel)
|
|
if (found) {
|
|
validModelCache.set(normalizedModel, true)
|
|
return { valid: true }
|
|
}
|
|
if (ollamaModels.length > 0) {
|
|
const MAX_SHOWN = 5
|
|
const names = ollamaModels.map(m => m.value)
|
|
const shown = names.slice(0, MAX_SHOWN).join(', ')
|
|
const suffix = names.length > MAX_SHOWN ? ` and ${names.length - MAX_SHOWN} more` : ''
|
|
return { valid: false, error: `Model '${normalizedModel}' not found on Ollama server. Available: ${shown}${suffix}` }
|
|
}
|
|
// If cache is empty, fall through to API validation
|
|
}
|
|
|
|
// Check against availableModels allowlist before any API call
|
|
if (!isModelAllowed(normalizedModel)) {
|
|
return {
|
|
valid: false,
|
|
error: `Model '${normalizedModel}' is not in the list of available models`,
|
|
}
|
|
}
|
|
|
|
// Check if it's a known alias (these are always valid)
|
|
const lowerModel = normalizedModel.toLowerCase()
|
|
if ((MODEL_ALIASES as readonly string[]).includes(lowerModel)) {
|
|
return { valid: true }
|
|
}
|
|
|
|
// Check if it matches ANTHROPIC_CUSTOM_MODEL_OPTION (pre-validated by the user)
|
|
if (normalizedModel === process.env.ANTHROPIC_CUSTOM_MODEL_OPTION) {
|
|
return { valid: true }
|
|
}
|
|
|
|
// Check cache first
|
|
if (validModelCache.has(normalizedModel)) {
|
|
return { valid: true }
|
|
}
|
|
|
|
|
|
// Try to make an actual API call with minimal parameters
|
|
try {
|
|
await sideQuery({
|
|
model: normalizedModel,
|
|
max_tokens: 1,
|
|
maxRetries: 0,
|
|
querySource: 'model_validation',
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: 'Hi',
|
|
cache_control: { type: 'ephemeral' },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
})
|
|
|
|
// If we got here, the model is valid
|
|
validModelCache.set(normalizedModel, true)
|
|
return { valid: true }
|
|
} catch (error) {
|
|
return handleValidationError(error, normalizedModel)
|
|
}
|
|
}
|
|
|
|
function handleValidationError(
|
|
error: unknown,
|
|
modelName: string,
|
|
): { valid: boolean; error: string } {
|
|
// NotFoundError (404) means the model doesn't exist
|
|
if (error instanceof NotFoundError) {
|
|
const fallback = get3PFallbackSuggestion(modelName)
|
|
const suggestion = fallback ? `. Try '${fallback}' instead` : ''
|
|
return {
|
|
valid: false,
|
|
error: `Model '${modelName}' not found${suggestion}`,
|
|
}
|
|
}
|
|
|
|
// For other API errors, provide context-specific messages
|
|
if (error instanceof APIError) {
|
|
if (error instanceof AuthenticationError) {
|
|
return {
|
|
valid: false,
|
|
error: 'Authentication failed. Please check your API credentials.',
|
|
}
|
|
}
|
|
|
|
if (error instanceof APIConnectionError) {
|
|
return {
|
|
valid: false,
|
|
error: 'Network error. Please check your internet connection.',
|
|
}
|
|
}
|
|
|
|
// Check error body for model-specific errors
|
|
const errorBody = error.error as unknown
|
|
if (
|
|
errorBody &&
|
|
typeof errorBody === 'object' &&
|
|
'type' in errorBody &&
|
|
errorBody.type === 'not_found_error' &&
|
|
'message' in errorBody &&
|
|
typeof errorBody.message === 'string' &&
|
|
errorBody.message.includes('model:')
|
|
) {
|
|
return { valid: false, error: `Model '${modelName}' not found` }
|
|
}
|
|
|
|
// Generic API error
|
|
return { valid: false, error: `API error: ${error.message}` }
|
|
}
|
|
|
|
// For unknown errors, be safe and reject
|
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
return {
|
|
valid: false,
|
|
error: `Unable to validate model: ${errorMessage}`,
|
|
}
|
|
}
|
|
|
|
// @[MODEL LAUNCH]: Add a fallback suggestion chain for the new model → previous version
|
|
/**
|
|
* Suggest a fallback model for 3P users when the selected model is unavailable.
|
|
*/
|
|
function get3PFallbackSuggestion(model: string): string | undefined {
|
|
if (getAPIProvider() === 'firstParty') {
|
|
return undefined
|
|
}
|
|
const lowerModel = model.toLowerCase()
|
|
if (lowerModel.includes('opus-4-6') || lowerModel.includes('opus_4_6')) {
|
|
return getModelStrings().opus41
|
|
}
|
|
if (lowerModel.includes('sonnet-4-6') || lowerModel.includes('sonnet_4_6')) {
|
|
return getModelStrings().sonnet45
|
|
}
|
|
if (lowerModel.includes('sonnet-4-5') || lowerModel.includes('sonnet_4_5')) {
|
|
return getModelStrings().sonnet40
|
|
}
|
|
return undefined
|
|
}
|