import type { FetchResponse, Provider, ProviderMeta } from "./types.js"; // Jina AI provider — reader only (search endpoint deliberately not exposed, // see ADR-0003) // // r.jina.ai returns SSE (Server-Sent Events) format by default. // We parse the first data event to extract clean markdown content. export const JINA_META = { name: "jina", label: "Jina", envVar: "JINA_API_KEY", capabilities: { generalSearch: false, verticalSearch: false, contentExtraction: true, crawl: false, siteMap: false, deepResearch: false, batchSearch: false, hasMetadata: false, }, // No searchHint — deliberately excluded from search routing fetchHint: "Fast, lightweight reader that converts web pages and PDF files into LLM-friendly Markdown. Features native PDF parsing and low latency, best for static articles and documents.", // No verticals // No searchFallbackPriority — excluded from search chain fetchFallbackPriority: 15, apiKeyRequired: false, } as const satisfies ProviderMeta; interface JinaSSEData { title?: string; content?: string; } function parseSSE(text: string): JinaSSEData | null { // SSE format: // event: data // data: {"title":"...","content":"..."} // // event: data // data: {...} // // We extract the first data event only. const lines = text.split("\n"); let dataLine = ""; for (const line of lines) { if (line.startsWith("data:")) { dataLine = line.slice(5).trim(); break; } } if (!dataLine) return null; try { return JSON.parse(dataLine) as JinaSSEData; } catch { return null; } } export class JinaProvider implements Provider { readonly name = JINA_META.name; readonly label = JINA_META.label; readonly capabilities = JINA_META.capabilities; constructor(private readonly apiKey: string | undefined) {} async fetch(url: string, signal?: AbortSignal): Promise { const headers: Record = { Accept: "text/event-stream" }; if (this.apiKey) { headers.Authorization = `Bearer ${this.apiKey}`; } const res = await fetch(`https://r.jina.ai/${url}`, { headers, signal }); if (!res.ok) throw new Error(`Jina fetch error (${res.status}): ${await res.text()}`); const raw = await res.text(); if (!raw.trim()) throw new Error(`Jina fetch: no content for ${url}`); // Parse SSE if the response looks like SSE const sse = parseSSE(raw); if (sse?.content) { return { text: sse.content, title: sse.title, contentType: "text/markdown", }; } // Fallback: plain text response (backward compatible) return { text: raw, contentType: "text/markdown" }; } }