From dd94d070af54ac4b8afbe7fc2bf84349379f943e Mon Sep 17 00:00:00 2001 From: Kuchizu <70284260+Kuchizu@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:16:29 +0300 Subject: [PATCH] fix: make AI resume analysis schema provider-compatible (#2939) --- src/integrations/ai/types.ts | 9 ++++++++ src/integrations/orpc/services/ai.ts | 15 ++++++------- .../integrations/-components/ai-section.tsx | 15 +++++++------ src/schema/resume/analysis.ts | 21 +++++++++++++++++++ 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/integrations/ai/types.ts b/src/integrations/ai/types.ts index fd5ad9d80..3db72e089 100644 --- a/src/integrations/ai/types.ts +++ b/src/integrations/ai/types.ts @@ -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 = { + 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", +}; diff --git a/src/integrations/orpc/services/ai.ts b/src/integrations/orpc/services/ai.ts index 6ab8efac7..e349e407e 100644 --- a/src/integrations/orpc/services/ai.ts +++ b/src/integrations/orpc/services/ai.ts @@ -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, string[]> = { +const defaultProviderHosts: Record = { 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 const result = await generateText({ model, - output: Output.object({ schema: resumeAnalysisSchema }), + output: Output.object({ schema: resumeAnalysisOutputSchema }), messages: [ { role: "system", content: systemPrompt }, { diff --git a/src/routes/dashboard/settings/integrations/-components/ai-section.tsx b/src/routes/dashboard/settings/integrations/-components/ai-section.tsx index 876e2acc3..58cd7bd6a 100644 --- a/src/routes/dashboard/settings/integrations/-components/ai-section.tsx +++ b/src/routes/dashboard/settings/integrations/-components/ai-section.tsx @@ -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, }, ]; diff --git a/src/schema/resume/analysis.ts b/src/schema/resume/analysis.ts index 52f07144e..db9265401 100644 --- a/src/schema/resume/analysis.ts +++ b/src/schema/resume/analysis.ts @@ -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({