@wess/atlas/ai#

Unified AI provider abstraction with chat, embeddings, streaming, RAG, and agent support. Zero external dependencies -- uses fetch for all API calls.

Exports#

Provider:

  • createProvider({ provider, key?, baseUrl? }) → AiProvider — provider is "openai" | "anthropic" | "ollama". key is required for openai/anthropic, optional for ollama (sent as Authorization: Bearer, needed for Ollama Cloud). baseUrl is optional for all three (anthropic default https://api.anthropic.com, trailing slash trimmed)
  • AiProvider exposes .chat(opts), .chatStream(opts), .embed(opts)

Chat:

  • createConversation(system?) → Conversation
  • addMessage(conv, msg) → Conversation (immutable)
  • send(provider, conv, content, opts?) → { conversation, response }
  • userMessage, assistantMessage, systemMessage, toolMessage

Stream:

  • parseSSE(text) → events
  • collectStream(stream) → ChatResponse
  • streamToSse(stream) → ReadableStream (for HTTP responses)

Embeddings:

  • embed(provider, inputs) → number[][] — inputs: string | string[]
  • cosineSimilarity(a, b) → number
  • createVectorStore() → { add, search, size }

Structured / tools:

  • generateJson<T>(provider, prompt, opts?) → T
  • tool(name, description, parameters) → ToolDef

RAG:

  • index(rag, id, text) → Promise<void> — rag = { ai, store, topK? }
  • query(rag, question) → { answer, sources }

Agents:

  • runAgent({ ai, system?, tools, maxIterations?, approve? }, prompt) → result
    • approve(call) => boolean | Promise<boolean> runs before each tool call; false refuses it and the model gets Error: tool call <name> was not approved as the result
    • A handler that throws, an unknown tool, or unparseable arguments (ToolCall.error) becomes an Error: <message> tool result; the loop keeps going and no stack is sent

Server pipe:

  • withAi(provider) → PipeFn — adds provider to conn.assigns.ai

Types#

  • ProviderConfig, ImageAttachment, Message, ChatOptions, ChatResponse, StreamChunk
  • EmbedOptions, EmbedResponse, ToolDef, ToolCall
  • Conversation, VectorStore, VectorEntry, RagOptions
  • AgentTool, AgentOptions

Provider setup#

import { createProvider } from "@wess/atlas/ai"

const openai = createProvider({ provider: "openai", key: process.env.OPENAI_API_KEY! })
const anthropic = createProvider({ provider: "anthropic", key: process.env.ANTHROPIC_API_KEY! })
const ollama = createProvider({ provider: "ollama" }) // local, no key needed
const cloud = createProvider({ provider: "ollama", baseUrl: "https://ollama.com", key: process.env.OLLAMA_API_KEY }) // Ollama Cloud: sends Authorization: Bearer

Chat#

import { createConversation, send } from "@wess/atlas/ai"

const conv = createConversation("You are a helpful assistant")
const { conversation, response } = await send(openai, conv, "Hello!")
// conversation tracks full message history immutably

Image input#

User messages can include images: [{ mediaType: "image/jpeg", data: base64 }]. The same attachment shape works with chat and streaming. Supported media types are JPEG, PNG, WebP, and GIF. Pass base64 bytes, without a data-URL prefix; image URLs are not fetched by the library. Image input requires a compatible model. System, assistant, and tool messages cannot carry image attachments. Existing text-only messages keep their original wire format.

Streaming#

import { collectStream, streamToSse } from "@wess/atlas/ai"

const stream = openai.chatStream({ messages: [{ role: "user", content: "Hi" }] })
const full = await collectStream(stream) // collect into ChatResponse
const sse = streamToSse(stream) // convert to ReadableStream for HTTP responses
import { embed, cosineSimilarity, createVectorStore } from "@wess/atlas/ai"

const vectors = await embed(openai, ["hello", "world"])
const score = cosineSimilarity(vectors[0]!, vectors[1]!)

const store = createVectorStore()
store.add("id1", vectors[0]!, { text: "hello" })
const results = store.search(vectors[1]!, 5) // top 5 nearest

Structured output#

import { generateJson, tool } from "@wess/atlas/ai"

const user = await generateJson<{ name: string }>(openai, "Generate a user")
const searchTool = tool("search", "Search the web", { type: "object", properties: { query: { type: "string" } } })

RAG#

import { index, query, createVectorStore, createProvider } from "@wess/atlas/ai"

const store = createVectorStore()
const rag = { ai: openai, store, topK: 3 }

await index(rag, "doc1", "Document text here...")
const result = await query(rag, "What does the document say?")
// result.answer, result.sources

Agent loop#

import { runAgent, tool } from "@wess/atlas/ai"

const result = await runAgent({
  ai: openai,
  system: "You are a calculator assistant",
  tools: [{
    definition: tool("multiply", "Multiply two numbers", {
      type: "object",
      properties: { a: { type: "number" }, b: { type: "number" } },
    }),
    handler: async (args) => String((args.a as number) * (args.b as number)),
  }],
  maxIterations: 5,
  // optional: gate calls, e.g. refuse anything destructive without a human
  approve: (call) => call.name !== "delete_account",
}, "What is 6 times 7?")

Tool arguments are model output, and a prompt-injected document can choose them. Never eval, new Function, shell out, or interpolate them into SQL; parse them into the narrow shape the tool needs and reject the rest.

Server pipe#

import { withAi } from "@wess/atlas/ai"

// Add AI provider to conn.assigns.ai in a server pipeline
const aiPipe = withAi(openai)

Architecture#

  • provider/ -- Provider interface and adapters (openai, anthropic, ollama)
  • chat/ -- Immutable conversation management
  • stream/ -- SSE parsing, stream collection, SSE response generation
  • embeddings/ -- Vector embeddings and in-memory vector store
  • structured/ -- JSON mode and tool definitions
  • rag/ -- Retrieval-augmented generation pipeline
  • agents/ -- Tool-use agent loop with iteration limits
  • pipes/ -- Server middleware integration

Dependencies#

  • @wess/atlas/server — only for the withAi pipe; the rest of the package stands alone.
  • External: none. All provider calls go through fetch.

Testing#

All tests use mock providers. No real API calls.

bun test packages/ai/
Canonical sourcepackages/ai/AGENTS.md
Type to search guides and package references.