fix: make AI resume analysis schema provider-compatible (#2939)

This commit is contained in:
Kuchizu
2026-04-29 21:16:29 +03:00
committed by GitHub
parent e035088269
commit dd94d070af
4 changed files with 45 additions and 15 deletions
+9
View File
@@ -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",
};
+8 -7
View File
@@ -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
View File
@@ -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({