detector.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. /**
  2. * AIGC detection — calls external API (GPTZero, Originality, or custom endpoint).
  3. * Not a BaseAgent subclass since it doesn't use the LLM provider.
  4. */
  5. import type { DetectionConfig } from "../models/project.js";
  6. export interface DetectionResult {
  7. readonly score: number; // 0-1, higher = more likely AI
  8. readonly provider: string;
  9. readonly detectedAt: string;
  10. readonly raw?: Record<string, unknown>;
  11. }
  12. /**
  13. * Detect AI-generated content by calling an external detection API.
  14. * Returns a normalized score between 0 (human) and 1 (AI).
  15. */
  16. export async function detectAIContent(
  17. config: DetectionConfig,
  18. content: string,
  19. ): Promise<DetectionResult> {
  20. const apiKey = process.env[config.apiKeyEnv];
  21. if (!apiKey) {
  22. throw new Error(
  23. `Detection API key not found. Set ${config.apiKeyEnv} in your environment.`,
  24. );
  25. }
  26. const detectedAt = new Date().toISOString();
  27. switch (config.provider) {
  28. case "gptzero":
  29. return detectGPTZero(config.apiUrl, apiKey, content, detectedAt);
  30. case "originality":
  31. return detectOriginality(config.apiUrl, apiKey, content, detectedAt);
  32. case "custom":
  33. return detectCustom(config.apiUrl, apiKey, content, detectedAt);
  34. }
  35. }
  36. async function detectGPTZero(
  37. apiUrl: string,
  38. apiKey: string,
  39. content: string,
  40. detectedAt: string,
  41. ): Promise<DetectionResult> {
  42. const response = await fetch(apiUrl, {
  43. method: "POST",
  44. headers: {
  45. "Content-Type": "application/json",
  46. "X-Api-Key": apiKey,
  47. },
  48. body: JSON.stringify({ document: content }),
  49. });
  50. if (!response.ok) {
  51. const body = await response.text();
  52. throw new Error(`GPTZero API failed: ${response.status} ${body}`);
  53. }
  54. const data = await response.json() as Record<string, unknown>;
  55. const documents = data.documents as Array<Record<string, unknown>> | undefined;
  56. const score = documents?.[0]?.completely_generated_prob as number ?? 0;
  57. return { score, provider: "gptzero", detectedAt, raw: data };
  58. }
  59. async function detectOriginality(
  60. apiUrl: string,
  61. apiKey: string,
  62. content: string,
  63. detectedAt: string,
  64. ): Promise<DetectionResult> {
  65. const response = await fetch(apiUrl, {
  66. method: "POST",
  67. headers: {
  68. "Content-Type": "application/json",
  69. Authorization: `Bearer ${apiKey}`,
  70. },
  71. body: JSON.stringify({ content }),
  72. });
  73. if (!response.ok) {
  74. const body = await response.text();
  75. throw new Error(`Originality API failed: ${response.status} ${body}`);
  76. }
  77. const data = await response.json() as Record<string, unknown>;
  78. const score = (data.score as Record<string, unknown>)?.ai as number ?? 0;
  79. return { score, provider: "originality", detectedAt, raw: data };
  80. }
  81. async function detectCustom(
  82. apiUrl: string,
  83. apiKey: string,
  84. content: string,
  85. detectedAt: string,
  86. ): Promise<DetectionResult> {
  87. const response = await fetch(apiUrl, {
  88. method: "POST",
  89. headers: {
  90. "Content-Type": "application/json",
  91. Authorization: `Bearer ${apiKey}`,
  92. },
  93. body: JSON.stringify({ content }),
  94. });
  95. if (!response.ok) {
  96. const body = await response.text();
  97. throw new Error(`Detection API failed: ${response.status} ${body}`);
  98. }
  99. const data = await response.json() as Record<string, unknown>;
  100. // Custom endpoint must return { score: number } at minimum
  101. const score = typeof data.score === "number" ? data.score : 0;
  102. return { score, provider: "custom", detectedAt, raw: data };
  103. }