SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
11.0 KB · 211 lines python
Raw Blame History
1"""DeepSeek — API docs (Docusaurus): models & pricing table, change log, news posts.23Sources (tier 1):4  * pricing   → api-docs.deepseek.com/quick_start/pricing : one transposed table (models as columns): API ids, model versions, context length,5                max output, feature matrix (JSON output, tool calls, vision…), peak / off-peak prices (cache hit, cache miss, output)6  * change log → api-docs.deepseek.com/updates : "Date: YYYY-MM-DD" sections → ANNOUNCEMENT events (release posts queued for LLM)7  * news       → the docs top-nav "News" link (points to the latest post; /news/ itself redirects to the quick start); its sidebar lists every8                news post with its date → events + follow-ups910www.deepseek.com is a client-rendered Next.js site (Chinese landing page, no model list in the HTML) — not crawled.11"""12from __future__ import annotations1314import re15from typing import Any1617from aiatlas.registry import org_ref, provider_ref18from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext19from aiatlas.sdk.extract.dates import parse_datetime20from aiatlas.sdk.extract.feeds import FeedItem21from aiatlas.sdk.extract.html import node_text22from aiatlas.sdk.facts import EntityRef, Facts, Target23from aiatlas.sdk.fetch import FetchResult2425from ._common import MODEL_WORDS, RELEASE_WORDS, announcement_events, clean_cell, model_ref, money, tokens2627DOCS = "https://api-docs.deepseek.com"28PROVIDER_KEY = "deepseek"29ID_SCHEME = "deepseek_model_id"30FOOTNOTE = re.compile(r"\s*\(\d+\)\s*$")31NEWS_LINK = re.compile(r"/news/news\d+/?$")32NEWS_DATE = re.compile(r"(\d{4})/(\d{2})/(\d{2})\s*$")333435class DeepSeekConnector(BaseConnector):36    name = "deepseek"37    label = "DeepSeek — API models & pricing, change log, news"38    description = "DeepSeek API docs: models & pricing table (ids, context, max output, features, peak/off-peak prices), change log and news posts."39    source_key = "deepseek.com"40    version = "1"41    parser_version = "1"42    interval_seconds = 360043    min_interval_seconds = 180044    rate_per_min = 1245    tier = 146    priority = 047    expected_min_records = 348    concurrency = 24950    async def discover(self, ctx: RunContext) -> list[Target]:51        return [52            Target(url=f"{DOCS}/quick_start/pricing", doc_type="pricing", key="pricing", min_bytes=5000),53            Target(url=f"{DOCS}/updates", doc_type="listing", key="changelog", min_bytes=5000),54        ]5556    async def extract(self, ctx: RunContext, target: Target, res: FetchResult, parsed: Parsed) -> Facts:57        facts = Facts()58        org = org_ref("deepseek")59        facts.entities.append(org)60        key = target.key or ""61        if key == "pricing" and parsed.html:62            self._pricing(facts, org, parsed, res.final_url or res.url)63        elif key == "changelog" and parsed.html:64            self._changelog(facts, org, parsed, res.final_url or res.url)65        elif target.doc_type == "news_index" and parsed.html:66            self._news_index(facts, org, parsed)67        return facts6869    # ------------------------------------------------------------------------------------------ models & pricing70    def _pricing(self, facts: Facts, org: EntityRef, parsed: Parsed, url: str) -> None:71        html = parsed.html72        assert html73        provider = provider_ref(PROVIDER_KEY)74        facts.entities.append(provider)75        facts.document_title, facts.document_entity = "DeepSeek models & pricing", provider76        table = next((t for t in html.tables if any(clean_cell(r[0]).upper().startswith("MODEL") for r in t["rows"] if r)), None)77        if not table:78            return79        rows = [[clean_cell(c) for c in r] for r in table["rows"]]80        ids = next(([FOOTNOTE.sub("", c) for c in r[1:]] for r in rows if r and r[0].upper().startswith("MODEL") and not r[0].upper().startswith("MODEL VERSION")), [])81        ids = [i for i in ids if re.fullmatch(r"[a-z0-9][a-z0-9.\-]*", i)]82        if not ids:83            return84        n = len(ids)85        versions = next(([c for c in r[1:]] for r in rows if r and r[0].upper().startswith("MODEL VERSION")), [])86        refs: list[EntityRef] = []87        for i, api_id in enumerate(ids):88            display = versions[i] if i < len(versions) and versions[i] else api_id89            ref = model_ref(facts, display, org, api_id=api_id, provider_key=PROVIDER_KEY, family="DeepSeek", aliases=[api_id])90            facts.claim(ref, "api_model_id", api_id)91            facts.claim(ref, "official_url", url)92            refs.append(ref)93        prices = [facts.price(model=ref, provider=provider, provider_model_id=api_id, meta={"from": "pricing page", "note": "peak-hour rates; off-peak in features"})94                  for ref, api_id in zip(refs, ids, strict=True)]9596        def spread(values: list[str]) -> list[str]:97            return values * n if len(values) == 1 else values9899        kind: str | None = None100        for r in rows:101            if not r:102                continue103            label = r[0].upper()104            if label.startswith("CONTEXT LENGTH"):105                for ref, v in zip(refs, spread(r[1:]), strict=False):106                    facts.claim(ref, "context_length", tokens(v), unit="tokens")107            elif label.startswith("MAX OUTPUT"):108                for ref, v in zip(refs, spread(r[1:]), strict=False):109                    facts.claim(ref, "max_output_tokens", tokens(re.sub(r"(?i)maximum:?\s*|default:?\s*\d+\w*", "", v)), unit="tokens")110            elif label.startswith("THINKING"):111                for ref in refs:112                    facts.claim(ref, "reasoning", True)113            cells = r[1:] if not label.startswith("FEATURES") else r[2:]114            feat_label = r[1] if label.startswith("FEATURES") and len(r) > 1 else r[0]115            if feat_label in ("Json Output", "JSON Output", "Tool Calls", "Vision", "FIM Completion(Beta)", "Chat Prefix Completion(Beta)", "Responses API", "Anthropic API"):116                vals = spread(cells)117                for ref, v in zip(refs, vals, strict=False):118                    supported = "✓" in v or v.lower().startswith(("yes", "support"))119                    if feat_label == "Tool Calls":120                        facts.claim(ref, "tool_calling", supported)121                    elif feat_label.lower() == "json output":122                        facts.claim(ref, "structured_output", supported)123                    elif feat_label == "Vision":124                        facts.claim(ref, "vision", supported)125                        if supported:126                            facts.claim(ref, "modalities_input", ["text", "image"])127            # pricing rows: a "kind" cell then PEAK / OFF-PEAK rows with one money cell per model128            joined = " ".join(r).upper()129            if "INPUT TOKENS" in joined and "CACHE HIT" in joined:130                kind = "cached_input_per_mtok"131            elif "INPUT TOKENS" in joined and "CACHE MISS" in joined:132                kind = "input_per_mtok"133            elif "OUTPUT TOKENS" in joined:134                kind = "output_per_mtok"135            monies = [money(c) for c in r if c.startswith("$")]136            if kind and len(monies) == n:137                period = next((c.upper() for c in r if c.upper() in ("PEAK", "OFF-PEAK", "STANDARD")), "PEAK")138                for p, v in zip(prices, monies, strict=True):139                    if period == "OFF-PEAK":140                        p.features[f"off_peak_{kind}"] = v141                    else:142                        setattr(p, kind, v)143        for ref, api_id in zip(refs, ids, strict=True):144            for p in html.css("p"):145                t = node_text(p)146                if t.startswith("(") and api_id in t and "legacy" in t.lower():147                    for legacy in re.findall(r"\b(deepseek-[a-z0-9.\-]+)\b", t):148                        if legacy != api_id and legacy not in ref.aliases:149                            ref.aliases.append(legacy)150        facts.prices = [p for p in facts.prices if any(v is not None for v in p.price_tuple()[:-1])]151152    # ------------------------------------------------------------------------------------------ change log153    def _changelog(self, facts: Facts, org: EntityRef, parsed: Parsed, url: str) -> None:154        html = parsed.html155        assert html156        facts.document_title, facts.document_entity = "DeepSeek API change log", org157        for h2 in html.css("h2"):158            m = re.search(r"Date:\s*(\d{4}-\d{2}-\d{2})", node_text(h2))159            if not m:160                continue161            date = parse_datetime(m.group(1))162            anchor = h2.attributes.get("id") or m.group(1)163            sib = h2.next164            title: str | None = None165            body: list[str] = []166            n = 0167            while sib is not None and sib.tag != "h2":168                if sib.tag == "h3":169                    if title:170                        n += 1171                        self._changelog_event(facts, org, url, anchor, n, date, title, " ".join(body))172                        body = []173                    title = node_text(sib).replace("\u200b", "").strip()174                elif sib.tag in ("p", "ul", "ol", "div"):175                    body.append(node_text(sib))176                sib = sib.next177            if title:178                n += 1179                self._changelog_event(facts, org, url, anchor, n, date, title, " ".join(body))180        news = next((href for href, text in html.links if text.strip() == "News" and NEWS_LINK.search(href)), None)181        if news:182            facts.follow(news, doc_type="news_index", key="news_index", priority=1, min_bytes=5000)183184    def _changelog_event(self, facts: Facts, org: EntityRef, url: str, anchor: str, n: int, date: Any, title: str, body: str) -> None:185        is_release = bool(MODEL_WORDS.search(title) or RELEASE_WORDS.search(title))186        facts.event("ANNOUNCEMENT", "release" if is_release else "company", f"DeepSeek: {title}", entity=org, importance=2 if is_release else 1,187                    effective_at=date, dedupe_key=f"ANNOUNCEMENT:{url}#{anchor}:{n}", source_url=f"{url}#{anchor}",188                    meta={"source": "api-docs.deepseek.com/updates", "summary": body[:300], "is_release": is_release})189190    # ------------------------------------------------------------------------------------------ news index (sidebar)191    def _news_index(self, facts: Facts, org: EntityRef, parsed: Parsed) -> None:192        html = parsed.html193        assert html194        items: list[FeedItem] = []195        seen: set[str] = set()196        for href, text in html.links:197            if not NEWS_LINK.search(href) or "/zh-cn/" in href or href in seen:198                continue199            m = NEWS_DATE.search(text)200            title = NEWS_DATE.sub("", text).strip()201            if not title or title in ("News", "English"):202                continue203            seen.add(href)204            date = parse_datetime(f"{m.group(1)}-{m.group(2)}-{m.group(3)}") if m else None205            items.append(FeedItem(id=href, url=href, title=title, summary=None, published_at=date, updated_at=None))206        announcement_events(facts, org, items, source_name="api-docs.deepseek.com/news", max_follow=10)207        facts.document_title, facts.document_entity = "DeepSeek news", org208209210CONNECTORS = [DeepSeekConnector]211