Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4023d3a
test(e2e+unit): failing CSRF + loopback regression for Deep Audit POS…
sharonds Apr 22, 2026
40c9d7b
test(cost): failing regression for premium-tier sync estimate (#46 re…
sharonds Apr 22, 2026
99843e0
test(llm): failing regression for Gemini maxOutputTokens clamp (#46 r…
sharonds Apr 22, 2026
c107668
test(utils): failing regression for URL encoding in Gemini interactio…
sharonds Apr 22, 2026
4b48433
test(dashboard-format): failing regression for invalid date input (#4…
sharonds Apr 22, 2026
6285ee3
fix(dashboard): add CSRF + loopback guard to Deep Audit POST (#44 rev…
sharonds Apr 22, 2026
5d06536
ci: run browser E2E lane so new UI regressions are caught (#46 review)
sharonds Apr 22, 2026
36e3d27
fix(cost): premium tier uses basic sync cost; Deep Audit stays separa…
sharonds Apr 22, 2026
b503314
fix(llm): Gemini maxOutputTokens respects caller budget, capped at 81…
sharonds Apr 22, 2026
b77fb40
fix(dashboard): formatShortDate fallback on invalid date input (#47 r…
sharonds Apr 22, 2026
bb572e2
fix(gemini): URL-encode api key and interaction id in all Gemini endp…
sharonds Apr 22, 2026
656e372
fix(providers): gemini tier costs are per-claim, aligned with 4-claim…
sharonds Apr 22, 2026
64a7624
docs(readme): align skills table with shipped defaults and costs (#43…
sharonds Apr 22, 2026
c1488e9
docs(changelog): document PR-review-findings cleanup under [Unreleased]
sharonds Apr 22, 2026
779c5b1
fix(grounded): emit per-claim cost from registry (Copilot PR #50 review)
sharonds Apr 22, 2026
e319a30
test(e2e): bump CSRF-test post-stop delay to 500ms (Copilot PR #50 re…
sharonds Apr 22, 2026
0fe7c44
ci: pin agent-browser to 0.26.0 (Copilot PR #50 review)
sharonds Apr 22, 2026
19a9f89
refactor(dashboard): hoist formatShortDate result in ClaimDrillDown (…
sharonds Apr 22, 2026
792337f
test(e2e): poll for skills list before asserting body text (fixes CI …
sharonds Apr 22, 2026
52a3a45
Merge branch 'main' into fix/pr-review-cleanup
sharonds Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ jobs:
- name: Run dashboard tests
run: cd dashboard && bunx vitest run

- name: Install agent-browser
run: npm install -g agent-browser@0.26.0 && agent-browser install

- name: Run browser E2E lane
run: bun run test:e2e:browser
timeout-minutes: 5
Comment on lines +38 to +43
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI installs agent-browser globally without a version pin. This can make CI non-reproducible (sudden upstream releases breaking the lane) and increases supply-chain risk. Prefer pinning the version (e.g. install agent-browser@<known-good>), or add it as a devDependency and run it via a lockfile-managed runner (bunx/npx).

Copilot uses AI. Check for mistakes.

- name: Build dashboard
run: cd dashboard && bun run build

Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to CheckApp are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [1.3.1] - 2026-04-23

### Security

- Deep Audit POST route (`/api/reports/[id]/deep-audit`) now enforces the same loopback + CSRF guard as other mutation routes. Previously a caller that could reach the dev server could trigger a paid Gemini Deep Research job without localhost + CSRF checks.

### Fixed

- Cost estimator no longer over-reports premium fact-check tier: `factCheckTier: "premium"` runtime routes to basic sync + async Deep Audit, but the estimator was adding $1.50/check as if Deep Audit ran synchronously. Preflight totals are now accurate.
- Gemini `maxOutputTokens` now honors the caller's token budget, capped at 8192. Previously any caller asking for less than 8192 silently got 8192, up to 16Γ— response-side cost.
- `formatShortDate` / `formatDateTime` return an empty string instead of throwing a `RangeError` when given non-ISO input (e.g. Exa's occasional `publishedDate: "unknown"`); `ClaimDrillDown` also omits the badge entirely when the fallback kicks in.
- Gemini API key and interaction id are now URL-encoded in all Gemini interaction calls (`interactions-api.ts`, `factcheck-grounded.ts`, `llm.ts`), preventing broken requests when a key or id contains `+` or `/`.

### Changed

- Provider registry `costPerCheckUsd` for `gemini-grounded` and `gemini-deep-research` now express per-claim pricing ($0.04 / $0.375) so the per-article 4-claim total matches the tier estimator and user-facing cost labels ($0.16 / $1.50).
- CI now runs the browser E2E lane (`bun run test:e2e:browser`) so new UI regressions are caught before merge.

### Docs

- README skills table: AI Detection cost corrected to ~$0.03 (shipped cost constant). Grammar / Academic / Self-Plagiarism now accurately marked as disabled by default, matching `DEFAULT_SKILLS`. Mirrored in `docs/features.md`.

## [Unreleased]

### Changed
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ See [docs/security.md](docs/security.md) for the BYOK-alpha threat model.
| Skill | Engine | Cost/check | Enabled by default |
|-------|--------|-----------|-------------------|
| **Plagiarism** | Copyscape | ~$0.09 | βœ… |
| **AI Detection** | Copyscape | ~$0.09 | βœ… |
| **AI Detection** | Copyscape | ~$0.03 | βœ… |
| **SEO** | Offline (no API) | free | βœ… |
| **Grammar & Style** | LanguageTool + LLM fallback | free tier / ~$0.002 | βœ… (free tier) |
| **Academic Citations** | OpenAlex (default) / Semantic Scholar (legacy) | free | βœ… |
| **Self-Plagiarism** | Cloudflare Vectorize + OpenRouter embeddings | ~$0.0001 | ❌ requires index (`checkapp index <dir>`) |
| **Grammar & Style** | LanguageTool + LLM fallback | free tier / ~$0.002 | ❌ disabled by default (enable in Settings; LanguageTool free tier works without any API key) |
| **Academic Citations** | OpenAlex (default) / Semantic Scholar (legacy) | free | ❌ disabled by default (augments fact-check findings when enabled; OpenAlex/SS both free) |
| **Self-Plagiarism** | Cloudflare Vectorize + OpenRouter embeddings | ~$0.0001 | ❌ disabled by default β€” requires index (`checkapp index <dir>`), `OPENROUTER_API_KEY`, and Cloudflare Vectorize provider config |
| **Fact Check** | Tiered: Basic = Exa + LLM; Standard = Gemini + Google Search; Deep Audit = Gemini Deep Research | varies | Basic is available by default; Standard is opt-in; Deep Audit is async |
| **Tone of Voice** | Claude/MiniMax | ~$0.002 | ❌ requires LLM key + tone guide file |
| **Legal Risk** | Claude/MiniMax | ~$0.002 | ❌ requires LLM key |
Expand Down
2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dashboard",
"version": "1.3.0",
"version": "1.3.1",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
36 changes: 36 additions & 0 deletions dashboard/src/__tests__/api/deep-audit-guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, test, expect, vi } from "vitest";
import { NextRequest } from "next/server";

vi.mock("@/lib/csrf", () => ({
getCsrfToken: vi.fn(() => "test-csrf-token"),
}));

import { POST } from "@/app/api/reports/[id]/deep-audit/route";

describe("POST /api/reports/[id]/deep-audit β€” loopback + CSRF guard", () => {
test("rejects POST from spoofed Host header (uses nextUrl.hostname instead)", async () => {
const req = new NextRequest(new URL("http://203.0.113.5:3000/api/reports/1/deep-audit"), {
method: "POST",
headers: { host: "localhost", "x-checkapp-csrf": "test-csrf-token" },
});
const res = await POST(req, { params: Promise.resolve({ id: "1" }) });
expect(res.status).toBe(403);
});

test("rejects POST without CSRF token", async () => {
const req = new NextRequest(new URL("http://localhost:3000/api/reports/1/deep-audit"), {
method: "POST",
});
const res = await POST(req, { params: Promise.resolve({ id: "1" }) });
expect(res.status).toBe(403);
});

test("rejects POST with wrong CSRF token", async () => {
const req = new NextRequest(new URL("http://localhost:3000/api/reports/1/deep-audit"), {
method: "POST",
headers: { "x-checkapp-csrf": "wrong" },
});
const res = await POST(req, { params: Promise.resolve({ id: "1" }) });
expect(res.status).toBe(403);
});
});
7 changes: 7 additions & 0 deletions dashboard/src/__tests__/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,11 @@ describe("format helpers", () => {
it("formats numbers with a fixed grouping style", () => {
expect(formatNumber(1234567)).toBe("1,234,567");
});

it("returns a fallback for invalid date input instead of throwing or garbage", () => {
expect(() => formatShortDate("unknown")).not.toThrow();
expect(formatShortDate("unknown")).toBe("");
expect(() => formatDateTime("not-a-date")).not.toThrow();
expect(formatDateTime("not-a-date")).toBe("");
});
});
6 changes: 5 additions & 1 deletion dashboard/src/app/api/reports/[id]/deep-audit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { sql } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { jsonWithCors } from "@/lib/cors";
import { getDb } from "@/lib/db";
import { guardLocalMutation } from "@/lib/guard-local";
import { readConfig } from "../../../../../../../src/config";
import {
emitAuditCompletedEvent,
Expand Down Expand Up @@ -149,9 +150,12 @@ export async function GET(
}

export async function POST(
_request: NextRequest,
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const blocked = guardLocalMutation(request);
if (blocked) return blocked;

try {
ensureDeepAuditSchema();
await initializeDeepAuditRuntime();
Expand Down
11 changes: 7 additions & 4 deletions dashboard/src/components/ClaimDrillDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export function ClaimDrillDown({ finding }: Props) {
</SheetTitle>
</SheetHeader>
<div className="mt-4 space-y-4">
{finding.sources?.map((s, i) => (
{finding.sources?.map((s, i) => {
const publishedLabel = s.publishedDate ? formatShortDate(s.publishedDate) : "";
return (
<article key={`s-${i}`} className="rounded border p-3">
<a
href={safeHref(s.url)}
Expand All @@ -38,9 +40,9 @@ export function ClaimDrillDown({ finding }: Props) {
>
{sanitizeText(s.title) || safeHref(s.url)}
</a>
{s.publishedDate && (
{publishedLabel && (
<Badge variant="secondary" className="ml-2">
{formatShortDate(s.publishedDate)}
{publishedLabel}
</Badge>
)}
{typeof s.relevanceScore === "number" && (
Expand All @@ -54,7 +56,8 @@ export function ClaimDrillDown({ finding }: Props) {
</p>
)}
</article>
))}
);
})}

{finding.citations?.map((c, i) => (
<article key={`c-${i}`} className="rounded border bg-blue-50/30 p-3">
Expand Down
13 changes: 7 additions & 6 deletions dashboard/src/lib/estimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,14 @@ export function estimateRunCost(cfg: AppConfigForEstimate, wordCount: number): E
const s = cfg.skills ?? {};

if (s.factCheck) {
const effectiveTier = cfg.factCheckTierFlag === true ? (cfg.factCheckTier ?? "basic") : null;
perSkill["fact-check"] = effectiveTier
? effectiveTier === "standard"
const configuredTier = cfg.factCheckTierFlag === true ? (cfg.factCheckTier ?? "basic") : null;
// Runtime routes "premium" to basic sync + async Deep Audit; don't charge
// $1.50 synchronously here (mirrors ~/checkapp/src/cost/estimator.ts).
const syncTier = configuredTier === "premium" ? "basic" : configuredTier;
perSkill["fact-check"] = syncTier
? syncTier === "standard"
? 0.16
: effectiveTier === "premium"
? 1.5
: 0.04
: 0.04
: providerBase(cfg, "fact-check") * FACT_CHECK_MAX_CLAIMS;
}
if ((s as any).aiDetection) {
Expand Down
6 changes: 5 additions & 1 deletion dashboard/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ export function formatDate(
value: DateInput,
options: Intl.DateTimeFormatOptions = {}
): string {
const date = normalizeDateInput(value);
if (Number.isNaN(date.getTime())) {
return "";
}
return new Intl.DateTimeFormat(DEFAULT_LOCALE, {
...options,
timeZone: DEFAULT_TIME_ZONE,
}).format(normalizeDateInput(value));
}).format(date);
}

export function formatDateTime(value: DateInput): string {
Expand Down
4 changes: 2 additions & 2 deletions dashboard/src/lib/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export const PROVIDER_REGISTRY: Partial<Record<SkillId, ProviderMetadata[]>> = {
"fact-check": [
{ id: "exa-search", label: "Exa Search", speed: "fast", costPerCheckUsd: 0.008, costLabel: "$0.008/check", depth: "standard", freeTier: false, requiresKey: true },
{ id: "exa-deep-reasoning", label: "Exa Deep Reasoning", speed: "slow", costPerCheckUsd: 0.025, costLabel: "$0.025/check", depth: "deep", freeTier: false, requiresKey: true },
{ id: "gemini-grounded", label: "Gemini 3.1 Pro + Google Search", speed: "medium", costPerCheckUsd: 0.01, costLabel: "$0.01/check", depth: "standard", freeTier: false, requiresKey: true },
{ id: "gemini-deep-research", label: "Gemini Deep Research (Premium Audit)", speed: "slow", costPerCheckUsd: 0.05, costLabel: "$0.05/check", depth: "deep", freeTier: false, requiresKey: true },
{ id: "gemini-grounded", label: "Gemini 3.1 Pro + Google Search", speed: "medium", costPerCheckUsd: 0.04, costLabel: "$0.04/claim", depth: "standard", freeTier: false, requiresKey: true },
{ id: "gemini-deep-research", label: "Gemini Deep Research (Premium Audit)", speed: "slow", costPerCheckUsd: 0.375, costLabel: "$0.375/claim", depth: "deep", freeTier: false, requiresKey: true },
{ id: "parallel-task", label: "Parallel Task", speed: "slow", costPerCheckUsd: 0.03, costLabel: "$0.03/check", depth: "deep", freeTier: true, requiresKey: true },
],
grammar: [
Expand Down
8 changes: 4 additions & 4 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
| Skill | Engine | Cost/check | Default |
|-------|--------|-----------|---------|
| Plagiarism Check | Copyscape | ~$0.09 | Enabled |
| AI Detection | Copyscape | ~$0.09 | Enabled |
| AI Detection | Copyscape | ~$0.03 | Enabled |
| SEO Analysis | Offline | Free | Enabled |
| Fact Check | Tiered: Basic = Exa + LLM; Standard = Gemini + Google Search; Deep Audit = Gemini Deep Research | varies | Basic is default; Standard is opt-in; Deep Audit is async |
| Tone of Voice | MiniMax/Claude | ~$0.002 | Requires API keys + tone guide |
| Legal Risk | MiniMax/Claude | ~$0.002 | Requires API keys |
| Content Summary | MiniMax/Claude | ~$0.002 | Requires API keys |
| Brief Matching | MiniMax/Claude | ~$0.002 | Requires API keys + brief context |
| Content Purpose | MiniMax/Claude | ~$0.002 | Requires API keys |
| Grammar & Style | LanguageTool (default) / Sapling / LLM-fallback | Free / $0.0008/100w / LLM | Optional β€” LT free, no key required |
| Academic Citations | Semantic Scholar | Free (100 req/5min) | Optional β€” no key required |
| Self-Plagiarism | Cloudflare Vectorize / Pinecone / Upstash | ~$0.0002/article | Optional β€” requires one-time `checkapp index <dir>` |
| Grammar & Style | LanguageTool (default) / Sapling / LLM-fallback | Free / $0.0008/100w / LLM | Disabled by default β€” enable in Settings; LT free tier works without any API key |
| Academic Citations | Semantic Scholar | Free (100 req/5min) | Disabled by default β€” augments fact-check findings when enabled; no key required |
| Self-Plagiarism | Cloudflare Vectorize / Pinecone / Upstash | ~$0.0002/article | Disabled by default β€” requires one-time `checkapp index <dir>`, `OPENROUTER_API_KEY`, and provider config |

All enabled skills run in parallel. Skills with missing API keys skip gracefully.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "checkapp",
"version": "1.3.0",
"version": "1.3.1",
"description": "AI content quality gate β€” plagiarism, AI detection, SEO, fact-check, tone, legal risk, and more. CLI + web dashboard.",
"type": "module",
"bin": {
Expand Down
18 changes: 18 additions & 0 deletions src/cost/estimator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,22 @@ describe("estimateRunCost", () => {
}, 800);
expect(r.perSkill.aiDetection).toBeCloseTo(0.03);
});

test("premium tier estimate is basic sync cost, not Deep Audit cost", () => {
// Runtime routes premium β†’ basic sync + async Deep Audit. The sync-run
// estimate must NOT add $1.50 as if Deep Audit ran synchronously.
const cfg: Config = {
...base({ factCheck: true }),
factCheckTierFlag: true,
factCheckTier: "premium",
providers: { "fact-check": { provider: "exa-search", apiKey: "k" } },
};
const r = estimateRunCost(cfg, 500);
expect(r.perSkill["fact-check"]).toBe(0.04); // basic, NOT 1.5
});

test("estimateFactCheckCost('premium') still returns 1.5 as a pure helper", () => {
// Kept for separate Deep Audit pricing display (e.g. --estimate-cost).
expect(estimateFactCheckCost("premium")).toBe(1.5);
});
});
10 changes: 7 additions & 3 deletions src/cost/estimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,15 @@ export function estimateRunCost(config: Config, wordCount: number): EstimateResu
};

if (config.skills.factCheck) {
const effectiveTier = config.factCheckTierFlag === true
const configuredTier = config.factCheckTierFlag === true
? (config.factCheckTier ?? "basic")
: null;
perSkill["fact-check"] = effectiveTier
? estimateFactCheckCost(effectiveTier)
// Runtime routes "premium" to basic sync + async Deep Audit. Don't charge
// $1.50 here. estimateFactCheckCost("premium") stays as a pure helper for
// displaying Deep Audit pricing separately (see --estimate-cost output).
const syncTierForEstimate = configuredTier === "premium" ? "basic" : configuredTier;
perSkill["fact-check"] = syncTierForEstimate
? estimateFactCheckCost(syncTierForEstimate)
: providerBase("fact-check") * FACT_CHECK_MAX_CLAIMS;
}
if ((config.skills as any).aiDetection) {
Expand Down
4 changes: 2 additions & 2 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export const PROVIDER_REGISTRY: Partial<Record<SkillId, ProviderMetadata[]>> = {
"fact-check": [
{ id: "exa-search", label: "Exa Search", speed: "fast", costPerCheckUsd: 0.008, costLabel: "$0.008/check", depth: "standard", freeTier: false, requiresKey: true },
{ id: "exa-deep-reasoning", label: "Exa Deep Reasoning", speed: "slow", costPerCheckUsd: 0.025, costLabel: "$0.025/check", depth: "deep", freeTier: false, requiresKey: true },
{ id: "gemini-grounded", label: "Gemini 3.1 Pro + Google Search", speed: "medium", costPerCheckUsd: 0.01, costLabel: "$0.01/check", depth: "standard", freeTier: false, requiresKey: true },
{ id: "gemini-deep-research", label: "Gemini Deep Research (Premium Audit)", speed: "slow", costPerCheckUsd: 0.05, costLabel: "$0.05/check", depth: "deep", freeTier: false, requiresKey: true },
{ id: "gemini-grounded", label: "Gemini 3.1 Pro + Google Search", speed: "medium", costPerCheckUsd: 0.04, costLabel: "$0.04/claim", depth: "standard", freeTier: false, requiresKey: true },
{ id: "gemini-deep-research", label: "Gemini Deep Research (Premium Audit)", speed: "slow", costPerCheckUsd: 0.375, costLabel: "$0.375/claim", depth: "deep", freeTier: false, requiresKey: true },
{ id: "parallel-task", label: "Parallel Task", speed: "slow", costPerCheckUsd: 0.03, costLabel: "$0.03/check", depth: "deep", freeTier: true, requiresKey: true },
],
grammar: [
Expand Down
2 changes: 1 addition & 1 deletion src/skills/factcheck-grounded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe("FactCheckGroundedSkill", () => {
provider: "gemini-grounded",
model: "gemini-3.1-pro-preview",
httpStatus: 200,
costUsd: 0.01,
costUsd: 0.04,
inputTokens: 111,
outputTokens: 22,
totalTokens: 133,
Expand Down
18 changes: 12 additions & 6 deletions src/skills/factcheck-grounded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ export class FactCheckGroundedSkill implements Skill {
}

const findings: Finding[] = [];
const perClaimCost = resolved.metadata?.costPerCheckUsd ?? 0.01;
const perClaimCost = resolved.metadata?.costPerCheckUsd ?? 0.04;
let costUsd = 0.001;

const groundedResults: GroundedClaimResult[] = [];
for (const claim of claims.slice(0, 4)) {
const grounded = await assessClaimGrounded(claim, resolved.apiKey);
const grounded = await assessClaimGrounded(claim, resolved.apiKey, perClaimCost);
costUsd += perClaimCost;
groundedResults.push({ claim, ...grounded });
}
Expand Down Expand Up @@ -173,7 +173,11 @@ async function extractClaims(
let _e2eGroundedCursor = 0;
let _e2eGroundedScenarioName: string | null = null;

async function assessClaimGrounded(claim: string, apiKey: string): Promise<Omit<GroundedClaimResult, "claim">> {
async function assessClaimGrounded(
claim: string,
apiKey: string,
perClaimCost: number,
): Promise<Omit<GroundedClaimResult, "claim">> {
if (isE2E()) {
const s = loadScenario();
if (s.name !== _e2eGroundedScenarioName) {
Expand Down Expand Up @@ -203,6 +207,7 @@ async function assessClaimGrounded(claim: string, apiKey: string): Promise<Omit<
apiKey,
createGeminiCapability({ apiKey }).getModel("grounded"),
1,
perClaimCost,
);
const candidate = response.candidates?.[0];
const text = (candidate?.content?.parts ?? [])
Expand All @@ -225,6 +230,7 @@ async function fetchGroundedAssessment(
apiKey: string,
model: string,
retriesLeft: number,
perClaimCost: number,
): Promise<GeminiGroundedResponse> {
const startedAt = Date.now();
const emitAttempt = (payload: Record<string, unknown>) =>
Expand All @@ -233,14 +239,14 @@ async function fetchGroundedAssessment(
model,
claimPreview: claim.slice(0, 160),
retriesLeft,
costUsd: 0.01,
costUsd: perClaimCost,
...payload,
});

let response: Response;
try {
response = await fetch(
`${GEMINI_BASE_URL}/${model}:generateContent?key=${apiKey}`,
`${GEMINI_BASE_URL}/${model}:generateContent?key=${encodeURIComponent(apiKey)}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
Expand Down Expand Up @@ -282,7 +288,7 @@ async function fetchGroundedAssessment(
totalTokens: null,
});
await sleep(3_000);
return fetchGroundedAssessment(claim, apiKey, model, retriesLeft - 1);
return fetchGroundedAssessment(claim, apiKey, model, retriesLeft - 1, perClaimCost);
}

if (!response.ok) {
Expand Down
Loading
Loading