Skip to main content
Glama
rewriteClient.ts15.2 kB
import { generateObject, generateText } from "ai"; import { z } from "zod"; import type { AppConfig, ClaudeModel, LLMProvider } from "../config"; import { detectProviderFromApiKeys, log } from "../config"; export const RewriterToneSchema = z.enum([ "neutral", "formal", "informal", "academic", "custom", ]); export type RewriterTone = z.infer<typeof RewriterToneSchema>; export type RewriteProvider = LLMProvider; export interface RewriteParams { originalText: string; lastAiPercent: number | null; lastPlagiarismPercent: number | null; targetMaxAiPercent: number; targetMaxPlagiarismPercent: number; tone: RewriterTone; domainHint?: string; customInstructions?: string; maxIterations: number; } export interface RewriteResult { rewrittenText: string; reasoning: string; } const RewriteSchema = z.object({ rewrittenText: z.string().describe("The full rewritten text."), reasoning: z .string() .describe( "Short explanation of modifications and strategies used to reduce AI and plagiarism scores.", ), }); const AnalysisSchema = z.object({ analysis: z .string() .describe( "Concise analysis of AI detection and plagiarism risk and suggestions for improvement.", ), }); /** * Detect which LLM provider to use for rewriting based on config. * Priority: explicit REWRITE_LLM_PROVIDER > API key detection > claude-code */ export function detectRewriteProvider(config: AppConfig): RewriteProvider { if (config.rewriteLlmProvider) { return config.rewriteLlmProvider; } return detectProviderFromApiKeys(config); } /** * Choose Claude model based on text length and iteration count. * @internal Exported for testing */ export function chooseClaudeModel( textLength: number, maxIterations: number, forcedModel?: ClaudeModel, ): "haiku" | "sonnet" | "opus" { if (forcedModel && forcedModel !== "auto") { return forcedModel; } // Heuristic: prefer opus for very long texts or high iteration counts. // 12k characters ≈ 8–9k tokens, where opus maintains quality and longer context. // >8 iterations implies heavier rewrite loops; opus reduces retries and instability. if (textLength > 12000 || maxIterations > 8) { return "opus"; } // Haiku: cost optimization for short texts with few iterations. // <3k characters is typically 1-2k tokens; ≤3 iterations is light rewrite work. if (textLength < 3000 && maxIterations <= 3) { return "haiku"; } // Sonnet: default for moderate complexity. return "sonnet"; } /** * Get the appropriate model instance based on provider and config. */ async function getRewriteModel( config: AppConfig, provider: RewriteProvider, textLength = 0, maxIterations = 5, ) { switch (provider) { case "claude-code": { const { claudeCode } = await import("ai-sdk-provider-claude-code"); const modelId = chooseClaudeModel( textLength, maxIterations, config.claudeModel, ); return { model: claudeCode(modelId), modelId: `claude-code/${modelId}` }; } case "openai": { const { openai } = await import("@ai-sdk/openai"); return { model: openai(config.openaiModel), modelId: config.openaiModel }; } case "google": { const { google } = await import("@ai-sdk/google"); return { model: google(config.googleModel), modelId: config.googleModel }; } case "anthropic": { const { anthropic } = await import("@ai-sdk/anthropic"); const modelId = config.anthropicModel; return { model: anthropic(modelId), modelId }; } default: { const exhaustiveCheck: never = provider; throw new Error(`Unknown rewrite provider: ${exhaustiveCheck}`); } } } /** * Log which authentication method will be used. */ function logAuthMethod(provider: RewriteProvider, apiKey?: string): void { if (provider === "claude-code") { if (apiKey) { log("debug", "Using Claude API key authentication"); } else { log( "debug", "Using Claude CLI authentication (Pro/Max subscription via 'claude login')", ); } } else { log("debug", `Using ${provider} provider`); } } /** Rewrite text with the configured provider to reduce AI detection and plagiarism. */ export async function rewriteText( appConfig: AppConfig, params: RewriteParams, ): Promise<RewriteResult> { const { originalText, lastAiPercent, lastPlagiarismPercent, targetMaxAiPercent, targetMaxPlagiarismPercent, tone, domainHint, customInstructions, maxIterations, } = params; const provider = detectRewriteProvider(appConfig); logAuthMethod(provider, appConfig.claudeApiKey); const { model, modelId } = await getRewriteModel( appConfig, provider, originalText.length, maxIterations, ); // Use "an" for tones starting with a vowel sound (informal, academic) const article = /^[aeiou]/i.test(tone) ? "an" : "a"; const toneDescription = tone === "custom" ? "Use a natural human tone guided by the custom instructions." : `Use ${article} ${tone} tone that feels like a human wrote it.`; const domainText = domainHint ? `Domain: ${domainHint.trim()}.\n` : ""; const lastAiText = lastAiPercent === null ? "The last AI detection score was unavailable." : `The last AI detection score from Grammarly was approximately ${lastAiPercent}%.`; const lastPlagiarismText = lastPlagiarismPercent === null ? "The last plagiarism / originality score was unavailable." : `The last plagiarism score from Grammarly was approximately ${lastPlagiarismPercent}%.`; const targetText = [ `Target: AI detection ≤ ${targetMaxAiPercent}%`, `Target: plagiarism ≤ ${targetMaxPlagiarismPercent}%`, ].join(", "); const customText = customInstructions ? `Additional constraints from the user: ${customInstructions.trim()}` : "No additional custom constraints were provided."; const prompt = [ "You are an expert human-writing optimizer.", "You rewrite text so that:", "- It reads as naturally human as possible.", "- It avoids obvious AI-writing patterns: template-like phrasing, overuse of buzzwords,", " repetitive sentence openings, or exaggerated enthusiasm.", "- It preserves all important factual content and structure.", "- It avoids copying long phrases from common AI models or from Grammarly's own rewrite style.", "- It adapts the writing to feel personal and aligned with the author's intent and audience.", "", "Context:", domainText, lastAiText, lastPlagiarismText, `${targetText}.`, "", toneDescription, customText, "", "Do NOT:", "- Add citations or references that do not exist in the original.", "- Fabricate sources or numeric data.", "- Change code blocks, inline code, or math expressions other than trivial formatting.", "- Use em dashes (—) or emojis in the rewritten text.", "Examples of what NOT to change:", "- Code block (delimited by triple backticks):", " ```js", " const x = 5;", " ```", "- Inline code (single backticks): `const x = 5;`", "- Math expression (e.g., $E=mc^2$ or \\\\(\\int_0^1 x^2 dx\\\\)).", "", "When you rewrite:", "- Prefer varied sentence lengths.", "- Occasionally use short, direct sentences.", "- Remove filler phrases like 'in today's world', 'in conclusion', and similar clichés,", " unless they are essential to the content.", "- Make the text sound like a specific human author wrote it for a specific audience,", " not like a generic AI assistant voice.", "", "Return strictly in the JSON schema you were given.", "", "Original text:", "-----", originalText, "-----", ].join("\n"); const timeoutMs = appConfig.llmRequestTimeoutMs; log("info", "Calling for rewrite", { provider, modelId }); let timeoutId: ReturnType<typeof setTimeout> | undefined; try { const result = await Promise.race([ generateObject({ model, schema: RewriteSchema, prompt, }), new Promise<never>((_, reject) => { timeoutId = setTimeout(() => { log("error", "Rewrite timed out", { provider, modelId, timeoutMs, promptPreview: prompt.slice(0, 500), }); reject( new Error(`Rewrite request exceeded timeout of ${timeoutMs}ms`), ); }, timeoutMs); }), ]); const object = result.object; log("debug", "Rewrite completed", { provider, modelId }); return { rewrittenText: object.rewrittenText, reasoning: object.reasoning, }; } catch (error: unknown) { log("error", "Rewrite failed", { provider, modelId, error }); throw new Error( `Rewrite failed: ${error instanceof Error ? error.message : String(error)}`, ); } finally { if (timeoutId) { clearTimeout(timeoutId); } } } /** Analyze text for AI detection and plagiarism risk. */ export async function analyzeText( appConfig: AppConfig, text: string, aiPercent: number | null, plagiarismPercent: number | null, targetMaxAiPercent: number, targetMaxPlagiarismPercent: number, tone: RewriterTone, domainHint?: string, ): Promise<string> { const provider = detectRewriteProvider(appConfig); logAuthMethod(provider, appConfig.claudeApiKey); const { model, modelId } = await getRewriteModel( appConfig, provider, text.length, 1, ); const aiText = aiPercent === null ? "Current Grammarly AI detection score is unknown (not available)." : `Current Grammarly AI detection score is approximately ${aiPercent}%.`; const plagText = plagiarismPercent === null ? "Current Grammarly plagiarism / originality score is unknown (not available)." : `Current Grammarly plagiarism / originality score is approximately ${plagiarismPercent}%.`; const targetText = [ `Target AI detection ≤ ${targetMaxAiPercent}%`, `Target plagiarism ≤ ${targetMaxPlagiarismPercent}%`, ].join(", "); const domainText = domainHint ? `Domain: ${domainHint.trim()}` : "Domain not specified."; const prompt = [ "You are analyzing a piece of text for the risk of being flagged by Grammarly's AI Detector", "and Plagiarism Checker.", "", aiText, plagText, `${targetText}.`, domainText, `Desired tone: ${tone}.`, "", "Tasks:", "1. Briefly assess how likely this text is to be flagged as AI-generated by a typical detector.", "2. Briefly assess plagiarism risk given the score (if available).", "3. Suggest 3-5 specific, concrete changes that would make the text feel more human-written", " while preserving the meaning.", "4. Call out any obviously AI-ish phrases or structures to avoid.", "5. Note whether the text sounds like a specific human author or like a generic AI assistant.", "", "Respond with a few short paragraphs and bullet points, suitable for showing directly to a user.", "Return strictly in the JSON schema you were given.", "", "Text to analyze:", "-----", text, "-----", ].join("\n"); log("info", "Calling for analysis", { provider, modelId }); const timeoutMs = appConfig.llmRequestTimeoutMs; let timeoutId: ReturnType<typeof setTimeout> | undefined; try { const result = await Promise.race([ generateObject({ model, schema: AnalysisSchema, prompt, }), new Promise<never>((_, reject) => { timeoutId = setTimeout(() => { log("error", "Analysis timed out", { provider, modelId, timeoutMs, promptPreview: prompt.slice(0, 500), }); reject( new Error(`Analysis request exceeded timeout of ${timeoutMs}ms`), ); }, timeoutMs); }), ]); return result.object.analysis; } catch (error: unknown) { log("error", "Analysis failed", { provider, modelId, error }); throw new Error( `Analysis failed: ${error instanceof Error ? error.message : String(error)}`, ); } finally { if (timeoutId) { clearTimeout(timeoutId); } } } /** Generate a user-facing summary of the optimization run. */ export async function summarizeOptimization( appConfig: AppConfig, summaryInput: { mode: "score_only" | "optimize" | "analyze"; iterationsUsed: number; thresholdsMet: boolean; history: Array<{ iteration: number; ai_detection_percent: number | null; plagiarism_percent: number | null; note: string; }>; finalText: string; maxAiPercent: number; maxPlagiarismPercent: number; }, ): Promise<string> { const provider = detectRewriteProvider(appConfig); logAuthMethod(provider, appConfig.claudeApiKey); const { model, modelId } = await getRewriteModel( appConfig, provider, summaryInput.finalText.length, 1, ); const timeoutMs = appConfig.llmRequestTimeoutMs; let timeoutId: ReturnType<typeof setTimeout> | undefined; const prompt = [ "You are summarizing the outcome of a Grammarly-based AI detection and plagiarism optimization run.", "", `Mode: ${summaryInput.mode}`, `Iterations used (excluding initial scoring at iteration 0): ${summaryInput.iterationsUsed}`, `Thresholds met: ${summaryInput.thresholdsMet}`, `Targets: AI ≤ ${summaryInput.maxAiPercent}%, plagiarism ≤ ${summaryInput.maxPlagiarismPercent}%`, "", "History entries:", JSON.stringify(summaryInput.history, null, 2), "", "Final text (for context only, do not quote large passages):", "-----", summaryInput.finalText.slice(0, 4000), "-----", "", "Produce a short summary with:", "- A one-line verdict of how safe the text is with respect to AI and plagiarism detection.", "- A bullet list of the most important changes made across iterations.", "- A note if scores are missing or thresholds were not met.", "", "Keep the response under 250 words.", ].join("\n"); log("debug", "Calling for optimization summary", { provider, modelId }); try { const result = await Promise.race([ generateText({ model, prompt, }), new Promise<never>((_, reject) => { timeoutId = setTimeout(() => { log("error", "Optimization summary timed out", { provider, modelId, timeoutMs, promptPreview: prompt.slice(0, 500), }); reject( new Error( `Optimization summary request exceeded timeout of ${timeoutMs}ms`, ), ); }, timeoutMs); }), ]); return result.text; } catch (error: unknown) { log("error", "Summary failed", { provider, modelId, error }); throw new Error( `Optimization summary failed: ${error instanceof Error ? error.message : String(error)}`, ); } finally { if (timeoutId) { clearTimeout(timeoutId); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BjornMelin/grammarly-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server