рд╣рдо cookies рдЗрд╕реНрддреЗрдорд╛рд▓ рдХрд░рддреЗ рд╣реИрдВ. Cookies рдФрд░ рдмреНрд░рд╛рдЙрдЬрд╝рд░ рд╕реНрдЯреЛрд░реЗрдЬ рдЖрдкрдХреЛ рд╕рд╛рдЗрди рдЗрди рд░рдЦрддреЗ рд╣реИрдВ рдФрд░ рдЖрдкрдХреА рдкреНрд░рд╛рдердорд┐рдХрддрд╛рдПрдВ рдпрд╛рдж рд░рдЦрддреЗ рд╣реИрдВред рдЖрдкрдХреА рдЕрдиреБрдорддрд┐ рд╕реЗ рд╣рдо рдпрд╣ рднреА рдорд╛рдкрддреЗ рд╣реИрдВ рдХрд┐ рд╕рд╛рдЗрдЯ рдХреИрд╕реЗ рдЗрд╕реНрддреЗрдорд╛рд▓ рд╣реЛрддреА рд╣реИред рдЖрдк рдХрднреА рднреА ред рд╣рдорд╛рд░реА cookie рдиреАрддрд┐ рджреЗрдЦреЗрдВред

рдЧрд╛рдЗрдб: рдПрдореНрдмреЗрдбрд┐рдВрдЧ рдХреЗ рд╕рд╛рде RAG

рд╢реБрд░реБрдЖрдд

рдЧрд╛рдЗрдб: рдПрдореНрдмреЗрдбрд┐рдВрдЧ рдХреЗ рд╕рд╛рде RAG

рджрд╕реНрддрд╛рд╡реЗрдЬрд╝реЛрдВ рдХреЛ рдЪрдВрдХ рдореЗрдВ рдмрд╛рдБрдЯреЗрдВ, рдПрдореНрдмреЗрдб рдХрд░реЗрдВ, рдХреЛрд╕рд╛рдЗрди рд╕рдорд╛рдирддрд╛ рд╕реЗ рдЦреЛрдЬреЗрдВ, рд░реАрд░реИрдВрдХ рдХрд░реЗрдВ рдФрд░ рдЙрджреНрдзрд░рдгреЛрдВ (citations) рдХреЗ рд╕рд╛рде рдЙрддреНрддрд░ рджреЗрдВ, рдкреВрд░реЗ рдХреЛрдб рдХреЗ рд╕рд╛рдеред

рд░рд┐рдЯреНрд░реАрд╡рд▓-рдСрдЧрдореЗрдВрдЯреЗрдб рдЬрдирд░реЗрд╢рди (RAG) рдЖрдкрдХреЗ рдЕрдкрдиреЗ рджрд╕реНрддрд╛рд╡реЗрдЬрд╝реЛрдВ рд╕реЗ рд╕рд╡рд╛рд▓реЛрдВ рдХреЗ рдЬрд╡рд╛рдм рджреЗрддрд╛ рд╣реИред рдЖрдк рджрд╕реНрддрд╛рд╡реЗрдЬрд╝реЛрдВ рдХреЛ рдЪрдВрдХ рдореЗрдВ рдмрд╛рдБрдЯрддреЗ рд╣реИрдВ, рд╣рд░ рдЪрдВрдХ рдХреЛ рдПрдХ рдмрд╛рд░ рдПрдореНрдмреЗрдб рдХрд░рддреЗ рд╣реИрдВ, рдФрд░ рд╕рд╡рд╛рд▓ рдЖрдиреЗ рдкрд░ рд╕рд╡рд╛рд▓ рдХреЛ рдПрдореНрдмреЗрдб рдХрд░рдХреЗ рд╕рдмрд╕реЗ рдирдЬрд╝рджреАрдХреА рдЪрдВрдХ рдвреВрдБрдврддреЗ рд╣реИрдВ, рдЪрд╛рд╣реЗрдВ рддреЛ рдЙрдиреНрд╣реЗрдВ рд░реАрд░реИрдВрдХ рдХрд░рддреЗ рд╣реИрдВ, рдФрд░ рд╕рдмрд╕реЗ рдЕрдЪреНрдЫреЗ рдЪрдВрдХ рдПрдХ рдЪреИрдЯ рдореЙрдбрд▓ рдХреЛ рдЗрд╕ рдирд┐рд░реНрджреЗрд╢ рдХреЗ рд╕рд╛рде рднреЗрдЬрддреЗ рд╣реИрдВ рдХрд┐ рд╡рд╣ рдЙрдирдХрд╛ рд╣рд╡рд╛рд▓рд╛ рджреЗред

рдиреАрдЪреЗ рд╕рдм рдХреБрдЫ рдмреЗрд╕ URL https://api14.com/api/v1, рдЖрдкрдХреА sk-api14-... рдХреБрдВрдЬреА рдФрд░ рдЖрдзрд┐рдХрд╛рд░рд┐рдХ openai рдкреИрдХреЗрдЬ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддрд╛ рд╣реИред рд▓рд╛рдЧрдд рдЖрдкрдХреЗ рдкреНрд░реАрдкреЗрдб USD рдмреИрд▓реЗрдВрд╕ рд╕реЗ рдХрдЯрддреА рд╣реИ рдФрд░ рд╣рд░ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдореЗрдВ usage.cost рдореЗрдВ рд▓реМрдЯрд╛рдИ рдЬрд╛рддреА рд╣реИред

import os
from openai import OpenAI

client = OpenAI(base_url="https://api14.com/api/v1", api_key=os.environ["API14_API_KEY"])
EMBED_MODEL = "openai/text-embedding-3-small"
CHAT_MODEL = "openai/gpt-5-mini"

1. рджрд╕реНрддрд╛рд╡реЗрдЬрд╝реЛрдВ рдХреЛ рдЪрдВрдХ рдореЗрдВ рдмрд╛рдБрдЯреЗрдВ

рдЪрдВрдХ рдЗрддрдиреЗ рдЫреЛрдЯреЗ рд╣реЛрдВ рдХрд┐ рд╡рд┐рд╢рд┐рд╖реНрдЯ рд░рд╣реЗрдВ рдФрд░ рдЗрддрдиреЗ рдмрдбрд╝реЗ рдХрд┐ рдЕрдкрдиреЗ рдЖрдк рдореЗрдВ рдЕрд░реНрдердкреВрд░реНрдг рд╣реЛрдВред рд╢реБрд░реБрдЖрдд рдХреЗ рд▓рд┐рдП 500-1,000 рдЕрдХреНрд╖рд░, рдХреБрдЫ рдУрд╡рд░рд▓реИрдк рдХреЗ рд╕рд╛рде, рдкреИрд░рд╛рдЧреНрд░рд╛рдлрд╝ рдХреА рд╕реАрдорд╛рдУрдВ рдкрд░ рдмрд╛рдБрдЯрдирд╛ рдЕрдЪреНрдЫрд╛ рд░рд╣рддрд╛ рд╣реИ:

def chunk(text, size=800, overlap=150):
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    chunks, current = [], ""
    for p in paragraphs:
        if current and len(current) + len(p) > size:
            chunks.append(current)
            current = current[-overlap:]  # рд╕рдВрджрд░реНрдн рдХреЛ рдЕрдЧрд▓реЗ рдЪрдВрдХ рдореЗрдВ рд▓реЗ рдЬрд╛рдПрдБ
        current = f"{current}\n\n{p}".strip()
    if current:
        chunks.append(current)
    return chunks

docs = {"handbook.md": open("handbook.md").read(), "faq.md": open("faq.md").read()}
chunks = [{"source": name, "text": c} for name, text in docs.items() for c in chunk(text)]

рд╣рд░ рдЪрдВрдХ рдХреЗ рд╕рд╛рде рдЙрд╕рдХрд╛ рд╕реНрд░реЛрдд (рдлрд╝рд╛рдЗрд▓ рдирд╛рдо, URL, рдкреЗрдЬ рдирдВрдмрд░) рд░рдЦреЗрдВ, рдЙрджреНрдзрд░рдгреЛрдВ рдХреЗ рд▓рд┐рдП рдпрд╣ рдЪрд╛рд╣рд┐рдПред

2. POST /embeddings рд╕реЗ рдПрдореНрдмреЗрдб рдХрд░реЗрдВ

рдПрдХ рдЕрдиреБрд░реЛрдз рдореЗрдВ рдХрдИ рдЪрдВрдХ рднреЗрдЬреЗрдВред 100 рдЗрдирдкреБрдЯ рд╡рд╛рд▓рд╛ рдПрдХ рдЕрдиреБрд░реЛрдз рдЖрдкрдХреА рд░реЗрдЯ рд▓рд┐рдорд┐рдЯ рдореЗрдВ рдПрдХ рдЕрдиреБрд░реЛрдз рдЧрд┐рдирд╛ рдЬрд╛рддрд╛ рд╣реИ:

def embed(texts):
    vectors = []
    for i in range(0, len(texts), 100):
        r = client.embeddings.create(model=EMBED_MODEL, input=texts[i:i + 100])
        vectors += [d.embedding for d in sorted(r.data, key=lambda d: d.index)]
    return vectors

for c, v in zip(chunks, embed([c["text"] for c in chunks])):
    c["vector"] = v

рд╡реЗрдХреНрдЯрд░ рдФрд░ рдореЙрдбрд▓ id рдХреЛ рд╕рд╛рде рдореЗрдВ рд╕рд╣реЗрдЬреЗрдВред рдЕрд▓рдЧ-рдЕрд▓рдЧ рдореЙрдбрд▓ рдХреЗ рд╡реЗрдХреНрдЯрд░ рддреБрд▓рдиреАрдп рдирд╣реАрдВ рд╣реЛрддреЗ, рдЗрд╕рд▓рд┐рдП EMBED_MODEL рдмрджрд▓рдиреЗ рдкрд░ рдкреВрд░рд╛ рдХреЙрд░реНрдкрд╕ рджреЛрдмрд╛рд░рд╛ рдПрдореНрдмреЗрдб рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рджреЗрдЦреЗрдВ рдПрдореНрдмреЗрдбрд┐рдВрдЧред

3. рдХреЛрд╕рд╛рдЗрди рд╕рдорд╛рдирддрд╛ рд╕реЗ рдЦреЛрдЬреЗрдВ

рдХреБрдЫ рд╣рдЬрд╝рд╛рд░ рдЪрдВрдХ рдХреЗ рд▓рд┐рдП рдореЗрдореЛрд░реА рдореЗрдВ рд▓реАрдирд┐рдпрд░ рд╕реНрдХреИрди рдкрд░реНрдпрд╛рдкреНрдд рддреЗрдЬрд╝ рд╣реИред рдЙрд╕рд╕реЗ рдЕрдзрд┐рдХ рд╣реЛрдиреЗ рдкрд░ рд╡реЗрдХреНрдЯрд░ рдХреЛ рд╡реЗрдХреНрдЯрд░ рдбреЗрдЯрд╛рдмреЗрд╕ рдпрд╛ pgvector рд╡рд╛рд▓реА Postgres рдЯреЗрдмрд▓ рдореЗрдВ рд░рдЦреЗрдВред

import math

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))

def search(question, k=20):
    q = embed([question])[0]
    return sorted(chunks, key=lambda c: cosine(q, c["vector"]), reverse=True)[:k]

4. POST /rerank рд╕реЗ рд░реАрд░реИрдВрдХ рдХрд░реЗрдВ

рдПрдореНрдмреЗрдбрд┐рдВрдЧ рдЦреЛрдЬ рд░рд┐рдХреЙрд▓ рдореЗрдВ рдЕрдЪреНрдЫреА рд╣реИ рд▓реЗрдХрд┐рди рдХреНрд░рдо рддрдп рдХрд░рдиреЗ рдореЗрдВ рдореЛрдЯреАред рд░реАрд░реИрдВрдХрд░ рд╕рд╡рд╛рд▓ рдФрд░ рд╣рд░ рдЙрдореНрдореАрджрд╡рд╛рд░ рдХреЛ рд╕рд╛рде рдкрдврд╝рдХрд░ рдкреНрд░рд╛рд╕рдВрдЧрд┐рдХрддрд╛ рдХрд╛ рдЬрд╝реНрдпрд╛рджрд╛ рд╕рдЯреАрдХ рд╕реНрдХреЛрд░ рджреЗрддрд╛ рд╣реИред 20-100 рдЙрдореНрдореАрджрд╡рд╛рд░ рдирд┐рдХрд╛рд▓реЗрдВ, рд░реАрд░реИрдВрдХ рдХрд░реЗрдВ, рдФрд░ рд╢реАрд░реНрд╖ 3-8 рд░рдЦреЗрдВред OpenAI SDK рдореЗрдВ rerank рдореЗрдердб рдирд╣реАрдВ рд╣реИ, рдЗрд╕рд▓рд┐рдП рдЗрд╕реЗ HTTP рд╕реЗ рдХреЙрд▓ рдХрд░реЗрдВ (httpx openai рдХреЗ рд╕рд╛рде рдЗрдВрд╕реНрдЯреЙрд▓ рд╣реЛрддрд╛ рд╣реИ):

import httpx

def rerank(question, candidates, top_n=5):
    r = httpx.post(
        "https://api14.com/api/v1/rerank",
        headers={"Authorization": f"Bearer {os.environ['API14_API_KEY']}"},
        json={
            "model": "cohere/rerank-v3.5",
            "query": question,
            "documents": [c["text"] for c in candidates],
            "top_n": top_n,
        },
        timeout=30,
    )
    r.raise_for_status()
    return [candidates[x["index"]] for x in r.json()["results"]]

index рдЖрдкрдХреЗ рднреЗрдЬреЗ documents рдРрд░реЗ рдХреА рд╕реНрдерд┐рддрд┐ рдмрддрд╛рддрд╛ рд╣реИред рджреЗрдЦреЗрдВ рд░реАрд░реИрдВрдХред

5. рдЙрджреНрдзрд░рдгреЛрдВ рдХреЗ рд╕рд╛рде рдЙрддреНрддрд░ рджреЗрдВ

рдкреНрд░реЙрдореНрдкреНрдЯ рдореЗрдВ рдЪрдВрдХ рдХреЛ рдХреНрд░рдорд╛рдВрдХрд┐рдд рдХрд░реЗрдВ рдФрд░ рдореЙрдбрд▓ рд╕реЗ рдЙрдиреНрд╣реАрдВ рдХреНрд░рдорд╛рдВрдХреЛрдВ рдХрд╛ рд╣рд╡рд╛рд▓рд╛ рджреЗрдиреЗ рдХреЛ рдХрд╣реЗрдВред рдЙрд╕реЗ рдмрддрд╛рдПрдБ рдХрд┐ рд╕рдВрджрд░реНрдн рдореЗрдВ рдЙрддреНрддрд░ рди рд╣реЛ рддреЛ рд╡реИрд╕рд╛ рдХрд╣ рджреЗ:

def answer(question):
    context = rerank(question, search(question))
    sources = "\n\n".join(f"[{i + 1}] ({c['source']})\n{c['text']}" for i, c in enumerate(context))
    r = client.chat.completions.create(
        model=CHAT_MODEL,
        messages=[
            {"role": "system", "content": (
                "Answer only from the numbered sources. Cite them like [1] or [2][3] after each claim. "
                "If the sources do not contain the answer, say you don't know."
            )},
            {"role": "user", "content": f"Sources:\n\n{sources}\n\nQuestion: {question}"},
        ],
    )
    return r.choices[0].message.content, context, r.usage.cost

рдпрджрд┐ рдЖрдкрдХреЗ рдРрдк рдХреЛ рдЙрджреНрдзрд░рдг рдбреЗрдЯрд╛ рдХреЗ рд░реВрдк рдореЗрдВ рдЪрд╛рд╣рд┐рдП, рддреЛ рдмреНрд░реИрдХреЗрдЯ рдкрд╛рд░реНрд╕ рдХрд░рдиреЗ рдХреЗ рдмрдЬрд╛рдп { "answer": string, "citations": integer[] } рдЬреИрд╕реЗ schema рдХреЗ рд╕рд╛рде рд╕реНрдЯреНрд░рдХреНрдЪрд░реНрдб рдЖрдЙрдЯрдкреБрдЯ рдЗрд╕реНрддреЗрдорд╛рд▓ рдХрд░реЗрдВред

Node рдореЗрдВ рд╡рд╣реА рдкрд╛рдЗрдкрд▓рд╛рдЗрди

import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api14.com/api/v1", apiKey: process.env.API14_API_KEY });

const embed = async (input) =>
  (await client.embeddings.create({ model: "openai/text-embedding-3-small", input })).data.map((d) => d.embedding);

const cosine = (a, b) => {
  let dot = 0, na = 0, nb = 0;
  for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2; }
  return dot / Math.sqrt(na * nb);
};

async function answer(question, chunks /* [{ source, text, vector }] */) {
  const [q] = await embed([question]);
  const candidates = chunks.map((c) => ({ ...c, score: cosine(q, c.vector) }))
    .sort((a, b) => b.score - a.score).slice(0, 20);

  const ranked = await client.post("/rerank", {
    body: { model: "cohere/rerank-v3.5", query: question, documents: candidates.map((c) => c.text), top_n: 5 },
  });
  const context = ranked.results.map((r) => candidates[r.index]);

  const sources = context.map((c, i) => `[${i + 1}] (${c.source})\n${c.text}`).join("\n\n");
  const res = await client.chat.completions.create({
    model: "openai/gpt-5-mini",
    messages: [
      { role: "system", content: "Answer only from the numbered sources and cite them like [1]. If they do not contain the answer, say so." },
      { role: "user", content: `Sources:\n\n${sources}\n\nQuestion: ${question}` },
    ],
  });
  return res.choices[0].message.content;
}

рд▓рд╛рдЧрдд рдХрд╛ рдЕрдиреБрдорд╛рди

RAG рд╕рд┐рд╕реНрдЯрдо рдореЗрдВ рддреАрди рд▓рд╛рдЧрдд рдорджреЗрдВ рд╣реЛрддреА рд╣реИрдВ, рд╕рдм USD рдореЗрдВ рдФрд░ рд╕рдм usage.cost рдореЗрдВ рд░рд┐рдкреЛрд░реНрдЯ рд╣реЛрддреА рд╣реИрдВ:

рдЪрд░рдгрдХрдмрдмрд┐рд▓рд┐рдВрдЧ рдЗрдХрд╛рдИ
рдХреЙрд░реНрдкрд╕ рдХреА рдПрдореНрдмреЗрдбрд┐рдВрдЧрдПрдХ рдмрд╛рд░, рдФрд░ рджрд╕реНрддрд╛рд╡реЗрдЬрд╝ рдмрджрд▓рдиреЗ рдкрд░ рдлрд┐рд░ рд╕реЗрдЗрдирдкреБрдЯ рдЯреЛрдХрди
рд╕рд╡рд╛рд▓ рдХреА рдПрдореНрдмреЗрдбрд┐рдВрдЧ + рд░реАрд░реИрдВрдХрд╣рд░ рд╕рд╡рд╛рд▓ рдкрд░рдЗрдирдкреБрдЯ рдЯреЛрдХрди / рд╕рд░реНрдЪ
рдЪреИрдЯ рдЙрддреНрддрд░рд╣рд░ рд╕рд╡рд╛рд▓ рдкрд░рдЗрдирдкреБрдЯ рдФрд░ рдЖрдЙрдЯрдкреБрдЯ рдЯреЛрдХрди

рдЪрд▓рд╛рдиреЗ рд╕реЗ рдкрд╣рд▓реЗ рдЕрдиреБрдорд╛рди рд▓рдЧрд╛рдиреЗ рдХреЗ рд▓рд┐рдП GET /models/{id} рд╕реЗ рдореЙрдбрд▓ рдХреА рдХреАрдорддреЗрдВ рд▓реЗрдВ (pricing.prompt рдФрд░ pricing.completion рдкреНрд░рддрд┐ рдЯреЛрдХрди USD рд╣реИрдВ) рдФрд░ рдЕрдВрдЧреНрд░реЗрдЬрд╝реА рдЯреЗрдХреНрд╕реНрдЯ рдХреЗ рд▓рд┐рдП рд▓рдЧрднрдЧ 4 рдЕрдХреНрд╖рд░ рдкреНрд░рддрд┐ рдЯреЛрдХрди рдорд╛рдиреЗрдВ:

import httpx

def price(model_id):
    return httpx.get(f"https://api14.com/api/v1/models/{model_id}").json()["pricing"]

corpus_tokens = sum(len(c["text"]) for c in chunks) / 4
print("One-off embedding:", corpus_tokens * float(price(EMBED_MODEL)["prompt"]), "USD")

p = price(CHAT_MODEL)
prompt_tokens, answer_tokens = 5 * 800 / 4 + 200, 300  # 5 рдЪрдВрдХ + рдирд┐рд░реНрджреЗрд╢, рдЫреЛрдЯрд╛ рдЙрддреНрддрд░
print("Per answer:", prompt_tokens * float(p["prompt"]) + answer_tokens * float(p["completion"]), "USD")

рдЪреИрдЯ рдХреЙрд▓ рдЖрдорддреМрд░ рдкрд░ рд╕рдмрд╕реЗ рдмрдбрд╝реА рдордж рд╣реЛрддреА рд╣реИред рдЗрд╕реЗ рдШрдЯрд╛рдиреЗ рдХреЗ рд▓рд┐рдП рдХрдо рдФрд░ рдЫреЛрдЯреЗ рдЪрдВрдХ рднреЗрдЬреЗрдВ (рд░реАрд░реИрдВрдХрд┐рдВрдЧ рдЗрд╕реЗ рд╕реБрд░рдХреНрд╖рд┐рдд рдмрдирд╛рддреА рд╣реИ), рд╕рд┐рд╕реНрдЯрдо рдкреНрд░реЙрдореНрдкреНрдЯ рдХреЛ рд╢реБрд░реБрдЖрдд рдореЗрдВ рд╕реНрдерд┐рд░ рд░рдЦреЗрдВ рддрд╛рдХрд┐ рдкреНрд░реЙрдореНрдкреНрдЯ рдХреИрд╢рд┐рдВрдЧ рд▓рд╛рдЧреВ рд╣реЛ рд╕рдХреЗ, рдФрд░ max_tokens рд╕реЗрдЯ рдХрд░реЗрдВред рдмрд╛рдж рдореЗрдВ рдЕрд╕рд▓реА рд▓рд╛рдЧрдд usage.cost рдпрд╛ рдЬрдирд░реЗрд╢рди рд▓реБрдХрдЕрдк рд╕реЗ рдЬрд╛рдБрдЪреЗрдВред