diff models for diff work

This commit is contained in:
DaKheera47 2026-01-11 15:30:23 +00:00
parent e954cba4b1
commit 5d7c394650
9 changed files with 162 additions and 4 deletions

View File

@ -143,6 +143,9 @@ export async function getProfileProjects(): Promise<ResumeProjectCatalogItem[]>
export async function updateSettings(update: {
model?: string | null
modelScorer?: string | null
modelTailoring?: string | null
modelProjectSelection?: string | null
pipelineWebhookUrl?: string | null
jobCompleteWebhookUrl?: string | null
resumeProjects?: ResumeProjectsSettings | null

View File

@ -57,6 +57,9 @@ function clampInt(value: number, min: number, max: number) {
export const SettingsPage: React.FC = () => {
const [settings, setSettings] = useState<AppSettings | null>(null)
const [modelDraft, setModelDraft] = useState("")
const [modelScorerDraft, setModelScorerDraft] = useState("")
const [modelTailoringDraft, setModelTailoringDraft] = useState("")
const [modelProjectSelectionDraft, setModelProjectSelectionDraft] = useState("")
const [pipelineWebhookUrlDraft, setPipelineWebhookUrlDraft] = useState("")
const [jobCompleteWebhookUrlDraft, setJobCompleteWebhookUrlDraft] = useState("")
const [resumeProjectsDraft, setResumeProjectsDraft] = useState<ResumeProjectsSettings | null>(null)
@ -79,6 +82,9 @@ export const SettingsPage: React.FC = () => {
if (!isMounted) return
setSettings(data)
setModelDraft(data.overrideModel ?? "")
setModelScorerDraft(data.overrideModelScorer ?? "")
setModelTailoringDraft(data.overrideModelTailoring ?? "")
setModelProjectSelectionDraft(data.overrideModelProjectSelection ?? "")
setPipelineWebhookUrlDraft(data.overridePipelineWebhookUrl ?? "")
setJobCompleteWebhookUrlDraft(data.overrideJobCompleteWebhookUrl ?? "")
setResumeProjectsDraft(data.resumeProjects)
@ -107,6 +113,12 @@ export const SettingsPage: React.FC = () => {
const effectiveModel = settings?.model ?? ""
const defaultModel = settings?.defaultModel ?? ""
const overrideModel = settings?.overrideModel
const effectiveModelScorer = settings?.modelScorer ?? ""
const overrideModelScorer = settings?.overrideModelScorer
const effectiveModelTailoring = settings?.modelTailoring ?? ""
const overrideModelTailoring = settings?.overrideModelTailoring
const effectiveModelProjectSelection = settings?.modelProjectSelection ?? ""
const overrideModelProjectSelection = settings?.overrideModelProjectSelection
const effectivePipelineWebhookUrl = settings?.pipelineWebhookUrl ?? ""
const defaultPipelineWebhookUrl = settings?.defaultPipelineWebhookUrl ?? ""
const overridePipelineWebhookUrl = settings?.overridePipelineWebhookUrl
@ -142,6 +154,12 @@ export const SettingsPage: React.FC = () => {
if (!settings || !resumeProjectsDraft) return false
const next = modelDraft.trim()
const current = (overrideModel ?? "").trim()
const nextScorer = modelScorerDraft.trim()
const currentScorer = (overrideModelScorer ?? "").trim()
const nextTailoring = modelTailoringDraft.trim()
const currentTailoring = (overrideModelTailoring ?? "").trim()
const nextProjectSelection = modelProjectSelectionDraft.trim()
const currentProjectSelection = (overrideModelProjectSelection ?? "").trim()
const nextWebhook = pipelineWebhookUrlDraft.trim()
const currentWebhook = (overridePipelineWebhookUrl ?? "").trim()
const nextJobCompleteWebhook = jobCompleteWebhookUrlDraft.trim()
@ -150,6 +168,9 @@ export const SettingsPage: React.FC = () => {
const searchTermsChanged = JSON.stringify(searchTermsDraft) !== JSON.stringify(overrideSearchTerms ?? null)
return (
next !== current ||
nextScorer !== currentScorer ||
nextTailoring !== currentTailoring ||
nextProjectSelection !== currentProjectSelection ||
nextWebhook !== currentWebhook ||
nextJobCompleteWebhook !== currentJobCompleteWebhook ||
!resumeProjectsEqual(resumeProjectsDraft, settings.resumeProjects) ||
@ -164,9 +185,15 @@ export const SettingsPage: React.FC = () => {
}, [
settings,
modelDraft,
modelScorerDraft,
modelTailoringDraft,
modelProjectSelectionDraft,
pipelineWebhookUrlDraft,
jobCompleteWebhookUrlDraft,
overrideModel,
overrideModelScorer,
overrideModelTailoring,
overrideModelProjectSelection,
overridePipelineWebhookUrl,
overrideJobCompleteWebhookUrl,
resumeProjectsDraft,
@ -191,6 +218,9 @@ export const SettingsPage: React.FC = () => {
try {
setIsSaving(true)
const trimmed = modelDraft.trim()
const trimmedScorer = modelScorerDraft.trim()
const trimmedTailoring = modelTailoringDraft.trim()
const trimmedProjectSelection = modelProjectSelectionDraft.trim()
const webhookTrimmed = pipelineWebhookUrlDraft.trim()
const jobCompleteTrimmed = jobCompleteWebhookUrlDraft.trim()
const resumeProjectsOverride = resumeProjectsEqual(resumeProjectsDraft, settings.defaultResumeProjects)
@ -205,6 +235,9 @@ export const SettingsPage: React.FC = () => {
const jobspyLinkedinFetchDescriptionOverride = jobspyLinkedinFetchDescriptionDraft === defaultJobspyLinkedinFetchDescription ? null : jobspyLinkedinFetchDescriptionDraft
const updated = await api.updateSettings({
model: trimmed.length > 0 ? trimmed : null,
modelScorer: trimmedScorer.length > 0 ? trimmedScorer : null,
modelTailoring: trimmedTailoring.length > 0 ? trimmedTailoring : null,
modelProjectSelection: trimmedProjectSelection.length > 0 ? trimmedProjectSelection : null,
pipelineWebhookUrl: webhookTrimmed.length > 0 ? webhookTrimmed : null,
jobCompleteWebhookUrl: jobCompleteTrimmed.length > 0 ? jobCompleteTrimmed : null,
resumeProjects: resumeProjectsOverride,
@ -218,6 +251,9 @@ export const SettingsPage: React.FC = () => {
})
setSettings(updated)
setModelDraft(updated.overrideModel ?? "")
setModelScorerDraft(updated.overrideModelScorer ?? "")
setModelTailoringDraft(updated.overrideModelTailoring ?? "")
setModelProjectSelectionDraft(updated.overrideModelProjectSelection ?? "")
setPipelineWebhookUrlDraft(updated.overridePipelineWebhookUrl ?? "")
setJobCompleteWebhookUrlDraft(updated.overrideJobCompleteWebhookUrl ?? "")
setResumeProjectsDraft(updated.resumeProjects)
@ -266,6 +302,9 @@ export const SettingsPage: React.FC = () => {
setIsSaving(true)
const updated = await api.updateSettings({
model: null,
modelScorer: null,
modelTailoring: null,
modelProjectSelection: null,
pipelineWebhookUrl: null,
jobCompleteWebhookUrl: null,
resumeProjects: null,
@ -279,6 +318,9 @@ export const SettingsPage: React.FC = () => {
})
setSettings(updated)
setModelDraft("")
setModelScorerDraft("")
setModelTailoringDraft("")
setModelProjectSelectionDraft("")
setPipelineWebhookUrlDraft("")
setJobCompleteWebhookUrlDraft("")
setResumeProjectsDraft(updated.resumeProjects)
@ -327,9 +369,56 @@ export const SettingsPage: React.FC = () => {
<Separator />
<div className="space-y-4">
<div className="text-sm font-medium">Task-Specific Overrides</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="space-y-2">
<div className="text-sm">Scoring Model</div>
<Input
value={modelScorerDraft}
onChange={(event) => setModelScorerDraft(event.target.value)}
placeholder={effectiveModel || "inherit"}
disabled={isLoading || isSaving}
/>
<div className="text-xs text-muted-foreground">
Effective: <span className="font-mono">{effectiveModelScorer || effectiveModel}</span>
</div>
</div>
<div className="space-y-2">
<div className="text-sm">Tailoring Model</div>
<Input
value={modelTailoringDraft}
onChange={(event) => setModelTailoringDraft(event.target.value)}
placeholder={effectiveModel || "inherit"}
disabled={isLoading || isSaving}
/>
<div className="text-xs text-muted-foreground">
Effective: <span className="font-mono">{effectiveModelTailoring || effectiveModel}</span>
</div>
</div>
<div className="space-y-2">
<div className="text-sm">Project Selection Model</div>
<Input
value={modelProjectSelectionDraft}
onChange={(event) => setModelProjectSelectionDraft(event.target.value)}
placeholder={effectiveModel || "inherit"}
disabled={isLoading || isSaving}
/>
<div className="text-xs text-muted-foreground">
Effective: <span className="font-mono">{effectiveModelProjectSelection || effectiveModel}</span>
</div>
</div>
</div>
</div>
<Separator />
<div className="grid gap-2 text-sm sm:grid-cols-2">
<div>
<div className="text-xs text-muted-foreground">Effective</div>
<div className="text-xs text-muted-foreground">Global Effective</div>
<div className="break-words font-mono text-xs">{effectiveModel || "—"}</div>
</div>
<div>

View File

@ -270,6 +270,16 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => {
const defaultModel = process.env.MODEL || 'openai/gpt-4o-mini';
const model = overrideModel || defaultModel;
// Specific AI models
const overrideModelScorer = await settingsRepo.getSetting('modelScorer');
const modelScorer = overrideModelScorer || model;
const overrideModelTailoring = await settingsRepo.getSetting('modelTailoring');
const modelTailoring = overrideModelTailoring || model;
const overrideModelProjectSelection = await settingsRepo.getSetting('modelProjectSelection');
const modelProjectSelection = overrideModelProjectSelection || model;
const overridePipelineWebhookUrl = await settingsRepo.getSetting('pipelineWebhookUrl');
const defaultPipelineWebhookUrl = process.env.PIPELINE_WEBHOOK_URL || process.env.WEBHOOK_URL || '';
const pipelineWebhookUrl = overridePipelineWebhookUrl || defaultPipelineWebhookUrl;
@ -326,6 +336,12 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => {
model,
defaultModel,
overrideModel,
modelScorer,
overrideModelScorer,
modelTailoring,
overrideModelTailoring,
modelProjectSelection,
overrideModelProjectSelection,
pipelineWebhookUrl,
defaultPipelineWebhookUrl,
overridePipelineWebhookUrl,
@ -364,6 +380,9 @@ apiRouter.get('/settings', async (_req: Request, res: Response) => {
const updateSettingsSchema = z.object({
model: z.string().trim().min(1).max(200).nullable().optional(),
modelScorer: z.string().trim().min(1).max(200).nullable().optional(),
modelTailoring: z.string().trim().min(1).max(200).nullable().optional(),
modelProjectSelection: z.string().trim().min(1).max(200).nullable().optional(),
pipelineWebhookUrl: z.string().trim().min(1).max(2000).nullable().optional(),
jobCompleteWebhookUrl: z.string().trim().min(1).max(2000).nullable().optional(),
resumeProjects: z.object({
@ -392,6 +411,16 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => {
await settingsRepo.setSetting('model', model);
}
if ('modelScorer' in input) {
await settingsRepo.setSetting('modelScorer', input.modelScorer ?? null);
}
if ('modelTailoring' in input) {
await settingsRepo.setSetting('modelTailoring', input.modelTailoring ?? null);
}
if ('modelProjectSelection' in input) {
await settingsRepo.setSetting('modelProjectSelection', input.modelProjectSelection ?? null);
}
if ('pipelineWebhookUrl' in input) {
const pipelineWebhookUrl = input.pipelineWebhookUrl ?? null;
await settingsRepo.setSetting('pipelineWebhookUrl', pipelineWebhookUrl);
@ -455,6 +484,15 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => {
const defaultModel = process.env.MODEL || 'openai/gpt-4o-mini';
const model = overrideModel || defaultModel;
const overrideModelScorer = await settingsRepo.getSetting('modelScorer');
const modelScorer = overrideModelScorer || model;
const overrideModelTailoring = await settingsRepo.getSetting('modelTailoring');
const modelTailoring = overrideModelTailoring || model;
const overrideModelProjectSelection = await settingsRepo.getSetting('modelProjectSelection');
const modelProjectSelection = overrideModelProjectSelection || model;
const overridePipelineWebhookUrl = await settingsRepo.getSetting('pipelineWebhookUrl');
const defaultPipelineWebhookUrl = process.env.PIPELINE_WEBHOOK_URL || process.env.WEBHOOK_URL || '';
const pipelineWebhookUrl = overridePipelineWebhookUrl || defaultPipelineWebhookUrl;
@ -512,6 +550,12 @@ apiRouter.patch('/settings', async (req: Request, res: Response) => {
model,
defaultModel,
overrideModel,
modelScorer,
overrideModelScorer,
modelTailoring,
overrideModelTailoring,
modelProjectSelection,
overrideModelProjectSelection,
pipelineWebhookUrl,
defaultPipelineWebhookUrl,
overridePipelineWebhookUrl,

View File

@ -241,6 +241,8 @@ function mapRowToJob(row: typeof jobs.$inferSelect): Job {
suitabilityScore: row.suitabilityScore,
suitabilityReason: row.suitabilityReason,
tailoredSummary: row.tailoredSummary,
tailoredHeadline: row.tailoredHeadline ?? null,
tailoredSkills: row.tailoredSkills ?? null,
selectedProjectIds: row.selectedProjectIds ?? null,
pdfPath: row.pdfPath,
notionPageId: row.notionPageId,

View File

@ -8,6 +8,9 @@ import { db, schema } from '../db/index.js'
const { settings } = schema
export type SettingKey = 'model'
| 'modelScorer'
| 'modelTailoring'
| 'modelProjectSelection'
| 'pipelineWebhookUrl'
| 'jobCompleteWebhookUrl'
| 'resumeProjects'

View File

@ -21,7 +21,9 @@ export async function pickProjectIdsForJob(args: {
}
const overrideModel = await getSetting('model');
const model = overrideModel || process.env.MODEL || 'openai/gpt-4o-mini';
const overrideModelProjectSelection = await getSetting('modelProjectSelection');
// Precedence: Project-specific override > Global override > Env var > Default
const model = overrideModelProjectSelection || overrideModel || process.env.MODEL || 'openai/gpt-4o-mini';
const prompt = buildProjectSelectionPrompt({
jobDescription: args.jobDescription,

View File

@ -26,7 +26,9 @@ export async function scoreJobSuitability(
}
const overrideModel = await getSetting('model');
const model = overrideModel || process.env.MODEL || 'openai/gpt-4o-mini';
const overrideModelScorer = await getSetting('modelScorer');
// Precedence: Scorer-specific override > Global override > Env var > Default
const model = overrideModelScorer || overrideModel || process.env.MODEL || 'openai/gpt-4o-mini';
const prompt = buildScoringPrompt(job, profile);

View File

@ -2,6 +2,8 @@
* Service for generating tailored resume content (Summary, Headline, Skills).
*/
import { getSetting } from '../repositories/settings.js';
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
export interface TailoredData {
@ -30,7 +32,10 @@ export async function generateTailoring(
return { success: false, error: 'API key not configured' };
}
const model = process.env.MODEL || 'openai/gpt-4o-mini';
const overrideModel = await getSetting('model');
const overrideModelTailoring = await getSetting('modelTailoring');
// Precedence: Tailoring-specific override > Global override > Env var > Default
const model = overrideModelTailoring || overrideModel || process.env.MODEL || 'openai/gpt-4o-mini';
const prompt = buildTailoringPrompt(profile, jobDescription);
try {

View File

@ -211,6 +211,14 @@ export interface AppSettings {
model: string;
defaultModel: string;
overrideModel: string | null;
// Specific model overrides
modelScorer: string; // resolved
overrideModelScorer: string | null;
modelTailoring: string; // resolved
overrideModelTailoring: string | null;
modelProjectSelection: string; // resolved
overrideModelProjectSelection: string | null;
pipelineWebhookUrl: string;
defaultPipelineWebhookUrl: string;
overridePipelineWebhookUrl: string | null;