DEV Community

GrahamduesCN
GrahamduesCN

Posted on

Building a Multi-Provider AI Agent in TypeScript — No SDKs, Just Fetch

Building a Multi-Provider AI Agent in TypeScript — No SDKs, Just Fetch

I built ai-agent-starter — a lightweight TypeScript library for calling OpenAI, Anthropic, and Ollama through one unified API. No SDK dependencies. Just fetch.

Here's how it works.


The Problem

Most AI SDKs are either:

  • Provider-locked (only OpenAI, or only Anthropic)
  • Heavy on dependencies (openai alone pulls 15+ packages)
  • Difficult to swap providers without rewriting

I wanted: one API, any provider, zero heavy dependencies.

The Design

// One interface for all providers
interface AIProvider {
  chat(messages: AIMessage[], options?: ProviderOptions): Promise<AIResponse>;
  stream?(messages: AIMessage[], options?: ProviderOptions): AsyncIterable<StreamChunk>;
}
Enter fullscreen mode Exit fullscreen mode

Three implementations: OpenAIProvider, AnthropicProvider, OllamaProvider. All use native fetch.

Real Function Calling (Not String Matching)

The old version used regex to parse [TOOL:name(args)] from the response text. LLMs don't naturally output this format. It barely worked.

The fixed version uses the native tool/function calling API:

// OpenAI
body.tools = tools.map(t => ({
  type: 'function',
  function: { name: t.name, description: "t.description, parameters: t.parameters }"
}));
body.tool_choice = 'auto';

// Anthropic  
body.tools = tools.map(t => ({
  name: t.name,
  description: "t.description,"
  input_schema: { type: 'object', properties: t.parameters.properties }
}));
Enter fullscreen mode Exit fullscreen mode

The provider returns toolCalls in the response, and the Agent handles the execution loop — feeding results back to the LLM for the final answer.

Streaming with SSE

All three providers support streaming. The API is simple:

for await (const chunk of agent.stream("Tell me a story")) {
  process.stdout.write(chunk);
}
Enter fullscreen mode Exit fullscreen mode

Under the hood, each provider's stream() method is an async generator that parses SSE events — data: {...} lines — and yields content chunks.

Memory Management

The MemoryManager keeps conversation history within a token budget:

const mem = new MemoryManager({ maxTokens: 8000 });
mem.add({ role: 'user', content: 'Hello' });
mem.tokenCount(); // ~2

// When it exceeds the limit, old messages drop automatically
// System prompt is always preserved
Enter fullscreen mode Exit fullscreen mode

CLI

# Interactive chat
npx ai-agent-starter chat

# One-shot
npx ai-agent-starter run "Explain monads"

# HTTP API
npx ai-agent-starter serve --port 3000
curl -X POST localhost:3000/chat -d '{"messages":"hello"}'
Enter fullscreen mode Exit fullscreen mode

Try It

npm install ai-agent-starter
Enter fullscreen mode Exit fullscreen mode
import { Agent, AIClient, OpenAIProvider } from 'ai-agent-starter';

const agent = new Agent(
  new AIClient(new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! })),
  { systemPrompt: 'Be helpful.' }
);

console.log(await agent.run('Hello!'));
Enter fullscreen mode Exit fullscreen mode

GitHub: github.com/GrahamduesCN/ai-agent-starter


Also check out dev-cli-kit and ai-chat-saas — built in the same sprint.

Top comments (0)