All files / api/src/utils evaluation-model-resolver.ts

100% Statements 113/113
100% Branches 32/32
100% Functions 4/4
100% Lines 113/113

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 2151x       1x                                                                                       25x 25x 25x 25x 25x 25x   25x 25x 2x 2x 2x 22x     22x 22x 22x 25x 1x 1x 1x 1x 1x 20x     20x 20x 2x 25x 1x 1x 1x 1x 1x   19x 19x 19x 25x 25x 25x 25x                 26x 26x 26x 26x 26x 26x 26x   25x   25x 26x 21x 21x 26x 1x 1x 26x 1x 1x 26x 2x 2x 26x   26x 2x 2x 2x 2x 2x   23x 23x                         16x 16x 16x 16x 16x 16x     16x 2x 2x     14x 14x                                 6x 6x 6x 6x 6x 6x   6x 2x 2x 2x   4x 4x 4x 5x 1x 1x 1x 1x 1x   3x   5x 1x 1x 1x 1x 1x   2x 2x 2x 2x 2x 2x  
import { isAPIKeyRequiredForProvider } from '@api/ai-providers';
import type { LLMJudgeModelConfig } from '@api/evaluations/llm-judge';
import type { UserDataStorageConnector } from '@api/types/connector';
import type { AppContext } from '@api/types/hono';
import { warn } from '@shared/console-logging';
import type { AIProvider } from '@shared/types/constants';
import type { Model, SkillOptimizationEvaluation } from '@shared/types/data';
 
/**
 * Model configuration resolved from system settings or evaluation.
 */
export interface ResolvedModelConfig {
  model: string;
  provider: AIProvider;
  /**
   * The provider's API key, where it has one. Self-hosted providers such as
   * Ollama are configured without a key and are called without one.
   */
  apiKey?: string;
  /**
   * The provider's configured base URL, where it has one.
   *
   * Internal skills call back through the gateway with a target naming only a
   * provider and a model, so without this a self-hosted provider is sent to its
   * vendor default -- Ollama to `http://localhost:11434` -- no matter what the
   * user configured. The failure is quiet: the call cannot connect, the error
   * is logged, and optimization simply stops happening.
   */
  customHost?: string;
}
 
/**
 * System settings model type for lookup.
 */
export type SystemSettingsModelType =
  | 'judge'
  | 'embedding'
  | 'system_prompt_reflection'
  | 'evaluation_generation';
 
/**
 * Resolves a model configuration from a model ID.
 *
 * @param modelId - The model ID to resolve
 * @param connector - The storage connector to look up models
 * @param logPrefix - Prefix for log messages
 * @returns The model configuration or null if not found
 */
async function resolveModelById(
  c: AppContext,
  modelId: string,
  connector: UserDataStorageConnector,
  logPrefix: string,
): Promise<ResolvedModelConfig | null> {
  // Look up the model
  const models = await connector.getModels(c, { id: modelId });
  if (models.length === 0) {
    warn(`[${logPrefix}] Model not found: ${modelId}`);
    return null;
  }
  const model = models[0];
 
  // Look up the provider to get the API key
  const providers = await connector.getAIProviderAPIKeys(c, {
    id: model.ai_provider_id,
  });
  if (providers.length === 0) {
    warn(
      `[${logPrefix}] Provider not found for model: ${model.ai_provider_id}`,
    );
    return null;
  }
  const providerConfig = providers[0];
 
  // Ensure we have an API key, for the providers that need one
  if (
    !providerConfig.api_key &&
    isAPIKeyRequiredForProvider(providerConfig.ai_provider)
  ) {
    warn(
      `[${logPrefix}] No API key configured for provider: ${model.ai_provider_id}`,
    );
    return null;
  }
 
  return {
    model: model.model_name,
    provider: providerConfig.ai_provider as AIProvider,
    apiKey: providerConfig.api_key ?? undefined,
    customHost: providerConfig.custom_fields?.custom_host as string | undefined,
  };
}
 
/**
 * Resolves a model configuration from system settings.
 *
 * @param modelType - The type of model to resolve from system settings
 * @param connector - The storage connector to look up models and settings
 * @returns The model configuration or null if not configured
 */
export async function resolveSystemSettingsModel(
  c: AppContext,
  modelType: SystemSettingsModelType,
  connector: UserDataStorageConnector,
): Promise<ResolvedModelConfig | null> {
  const logPrefix = `MODEL_RESOLVER_${modelType.toUpperCase()}`;
  const systemSettings = await connector.getSystemSettings(c);
 
  let modelId: string | null = null;
 
  switch (modelType) {
    case 'judge':
      modelId = systemSettings.judge_model_id;
      break;
    case 'embedding':
      modelId = systemSettings.embedding_model_id;
      break;
    case 'system_prompt_reflection':
      modelId = systemSettings.system_prompt_reflection_model_id;
      break;
    case 'evaluation_generation':
      modelId = systemSettings.evaluation_generation_model_id;
      break;
  }
 
  if (!modelId) {
    warn(
      `[${logPrefix}] No ${modelType}_model_id configured in system settings`,
    );
    return null;
  }
 
  return resolveModelById(c, modelId, connector, logPrefix);
}
 
/**
 * Resolves the model configuration for an evaluation.
 *
 * Resolution order:
 * 1. If evaluation.model_id is set, use that model
 * 2. Otherwise, use the judge_model_id from system settings
 *
 * @param evaluation - The evaluation to resolve model for
 * @param connector - The storage connector to look up models and settings
 * @returns The model configuration or null if no model could be resolved
 */
export async function resolveEvaluationModelConfig(
  c: AppContext,
  evaluation: SkillOptimizationEvaluation,
  connector: UserDataStorageConnector,
): Promise<LLMJudgeModelConfig | null> {
  const logPrefix = 'EVAL_MODEL_RESOLVER';
 
  // If evaluation has a model_id, use it
  if (evaluation.model_id) {
    return await resolveModelById(c, evaluation.model_id, connector, logPrefix);
  }
 
  // Fall back to system settings judge_model_id
  return await resolveSystemSettingsModel(c, 'judge', connector);
}
 
/**
 * Embedding model configuration with dimensions.
 */
export interface EmbeddingModelConfig {
  modelId: string;
  model: Model;
  dimensions: number;
}
 
/**
 * Resolves the embedding model configuration from system settings.
 *
 * @param connector - The storage connector to look up models and settings
 * @returns The embedding model config or null if not configured
 */
export async function resolveEmbeddingModelConfig(
  c: AppContext,
  connector: UserDataStorageConnector,
): Promise<EmbeddingModelConfig | null> {
  const logPrefix = 'EMBEDDING_MODEL_RESOLVER';
  const systemSettings = await connector.getSystemSettings(c);
 
  if (!systemSettings.embedding_model_id) {
    warn(`[${logPrefix}] No embedding_model_id configured in system settings`);
    return null;
  }
 
  const models = await connector.getModels(c, {
    id: systemSettings.embedding_model_id,
  });
  if (models.length === 0) {
    warn(
      `[${logPrefix}] Embedding model not found: ${systemSettings.embedding_model_id}`,
    );
    return null;
  }
 
  const model = models[0];
 
  if (!model.embedding_dimensions) {
    warn(
      `[${logPrefix}] Embedding model ${model.model_name} has no dimensions configured`,
    );
    return null;
  }
 
  return {
    modelId: model.id,
    model,
    dimensions: model.embedding_dimensions,
  };
}