mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
fix: make AI resume analysis schema provider-compatible (#2939)
This commit is contained in:
@@ -5,3 +5,12 @@ const AI_PROVIDERS = ["openai", "anthropic", "gemini", "vercel-ai-gateway", "ope
|
||||
export type AIProvider = (typeof AI_PROVIDERS)[number];
|
||||
|
||||
export const aiProviderSchema = z.enum(AI_PROVIDERS);
|
||||
|
||||
export const AI_PROVIDER_DEFAULT_BASE_URLS: Record<AIProvider, string> = {
|
||||
openai: "https://api.openai.com/v1",
|
||||
anthropic: "https://api.anthropic.com/v1",
|
||||
gemini: "https://generativelanguage.googleapis.com/v1beta",
|
||||
"vercel-ai-gateway": "https://ai-gateway.vercel.sh/v3/ai",
|
||||
openrouter: "https://openrouter.ai/api/v1",
|
||||
ollama: "https://ollama.com/api",
|
||||
};
|
||||
|
||||
@@ -35,8 +35,8 @@ import {
|
||||
patchResumeDescription,
|
||||
patchResumeInputSchema,
|
||||
} from "@/integrations/ai/tools/patch-resume";
|
||||
import { aiProviderSchema, type AIProvider } from "@/integrations/ai/types";
|
||||
import { resumeAnalysisSchema, type ResumeAnalysis } from "@/schema/resume/analysis";
|
||||
import { AI_PROVIDER_DEFAULT_BASE_URLS, aiProviderSchema, type AIProvider } from "@/integrations/ai/types";
|
||||
import { resumeAnalysisOutputSchema, resumeAnalysisSchema, type ResumeAnalysis } from "@/schema/resume/analysis";
|
||||
import { defaultResumeData, resumeDataSchema } from "@/schema/resume/data";
|
||||
import { tailorOutputSchema, type TailorOutput } from "@/schema/tailor";
|
||||
import { buildAiExtractionTemplate } from "@/utils/ai-template";
|
||||
@@ -213,20 +213,21 @@ type GetModelInput = {
|
||||
const MAX_AI_FILE_BYTES = 10 * 1024 * 1024; // 10MB
|
||||
const MAX_AI_FILE_BASE64_CHARS = Math.ceil((MAX_AI_FILE_BYTES * 4) / 3) + 4;
|
||||
const adminAllowedBaseUrls = parseAllowedHostList(env.AI_ALLOWED_BASE_URLS);
|
||||
const defaultProviderHosts: Record<Exclude<AIProvider, "ollama">, string[]> = {
|
||||
const defaultProviderHosts: Record<AIProvider, string[]> = {
|
||||
openai: ["api.openai.com"],
|
||||
anthropic: ["api.anthropic.com"],
|
||||
gemini: ["generativelanguage.googleapis.com"],
|
||||
"vercel-ai-gateway": ["gateway.ai.vercel.com"],
|
||||
"vercel-ai-gateway": ["ai-gateway.vercel.sh"],
|
||||
openrouter: ["openrouter.ai"],
|
||||
ollama: ["ollama.com"],
|
||||
};
|
||||
|
||||
function resolveBaseUrl(input: GetModelInput): string {
|
||||
const baseURL = input.baseURL?.trim();
|
||||
const baseURL = input.baseURL?.trim() || AI_PROVIDER_DEFAULT_BASE_URLS[input.provider];
|
||||
|
||||
if (!baseURL) throw new Error("INVALID_AI_BASE_URL");
|
||||
|
||||
const providerHosts = input.provider === "ollama" ? [] : defaultProviderHosts[input.provider];
|
||||
const providerHosts = defaultProviderHosts[input.provider];
|
||||
const allowedHosts = new Set([...providerHosts, ...adminAllowedBaseUrls]);
|
||||
if (!isAllowedExternalUrl(baseURL, allowedHosts)) {
|
||||
throw new Error("INVALID_AI_BASE_URL");
|
||||
@@ -421,7 +422,7 @@ async function analyzeResume(input: AnalyzeResumeInput): Promise<ResumeAnalysis>
|
||||
|
||||
const result = await generateText({
|
||||
model,
|
||||
output: Output.object({ schema: resumeAnalysisSchema }),
|
||||
output: Output.object({ schema: resumeAnalysisOutputSchema }),
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
|
||||
@@ -5,14 +5,13 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import type { AIProvider } from "@/integrations/ai/types";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Combobox, type ComboboxOption } from "@/components/ui/combobox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { AI_PROVIDER_DEFAULT_BASE_URLS, type AIProvider } from "@/integrations/ai/types";
|
||||
import { useAIStore } from "@/integrations/ai/store";
|
||||
import { orpc } from "@/integrations/orpc/client";
|
||||
import { getOrpcErrorMessage } from "@/utils/error-message";
|
||||
@@ -28,7 +27,7 @@ const providerOptions: AIProviderOption[] = [
|
||||
message: "OpenAI",
|
||||
}),
|
||||
keywords: ["openai", "gpt", "chatgpt"],
|
||||
defaultBaseURL: "https://api.openai.com/v1",
|
||||
defaultBaseURL: AI_PROVIDER_DEFAULT_BASE_URLS.openai,
|
||||
},
|
||||
{
|
||||
value: "anthropic",
|
||||
@@ -37,7 +36,7 @@ const providerOptions: AIProviderOption[] = [
|
||||
message: "Anthropic Claude",
|
||||
}),
|
||||
keywords: ["anthropic", "claude", "ai"],
|
||||
defaultBaseURL: "https://api.anthropic.com/v1",
|
||||
defaultBaseURL: AI_PROVIDER_DEFAULT_BASE_URLS.anthropic,
|
||||
},
|
||||
{
|
||||
value: "gemini",
|
||||
@@ -46,7 +45,7 @@ const providerOptions: AIProviderOption[] = [
|
||||
message: "Google Gemini",
|
||||
}),
|
||||
keywords: ["gemini", "google", "bard"],
|
||||
defaultBaseURL: "https://generativelanguage.googleapis.com/v1beta",
|
||||
defaultBaseURL: AI_PROVIDER_DEFAULT_BASE_URLS.gemini,
|
||||
},
|
||||
{
|
||||
value: "vercel-ai-gateway",
|
||||
@@ -55,7 +54,7 @@ const providerOptions: AIProviderOption[] = [
|
||||
message: "Vercel AI Gateway",
|
||||
}),
|
||||
keywords: ["vercel", "gateway", "ai"],
|
||||
defaultBaseURL: "https://ai-gateway.vercel.sh/v1/ai",
|
||||
defaultBaseURL: AI_PROVIDER_DEFAULT_BASE_URLS["vercel-ai-gateway"],
|
||||
},
|
||||
{
|
||||
value: "openrouter",
|
||||
@@ -64,7 +63,7 @@ const providerOptions: AIProviderOption[] = [
|
||||
message: "OpenRouter",
|
||||
}),
|
||||
keywords: ["openrouter", "router", "multi", "proxy"],
|
||||
defaultBaseURL: "https://openrouter.ai/api/v1",
|
||||
defaultBaseURL: AI_PROVIDER_DEFAULT_BASE_URLS.openrouter,
|
||||
},
|
||||
{
|
||||
value: "ollama",
|
||||
@@ -73,7 +72,7 @@ const providerOptions: AIProviderOption[] = [
|
||||
message: "Ollama",
|
||||
}),
|
||||
keywords: ["ollama", "ai", "local"],
|
||||
defaultBaseURL: "https://ollama.com/api",
|
||||
defaultBaseURL: AI_PROVIDER_DEFAULT_BASE_URLS.ollama,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -21,6 +21,27 @@ export const resumeAnalysisSchema = z.object({
|
||||
strengths: z.array(z.string().min(1)).max(10),
|
||||
});
|
||||
|
||||
export const resumeAnalysisOutputSchema = z.object({
|
||||
overallScore: z.number(),
|
||||
scorecard: z.array(
|
||||
z.object({
|
||||
dimension: z.string(),
|
||||
score: z.number(),
|
||||
rationale: z.string(),
|
||||
}),
|
||||
),
|
||||
suggestions: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
impact: z.enum(["high", "medium", "low"]),
|
||||
why: z.string(),
|
||||
exampleRewrite: z.string().nullable(),
|
||||
copyPrompt: z.string(),
|
||||
}),
|
||||
),
|
||||
strengths: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const storedResumeAnalysisSchema = resumeAnalysisSchema.extend({
|
||||
updatedAt: z.coerce.date(),
|
||||
modelMeta: z.object({
|
||||
|
||||
Reference in New Issue
Block a user