feat(ai): Create unified AI package with OpenAI, Anthropic, and Gemini support

- Set up @mariozechner/ai package structure following monorepo patterns
- Install OpenAI, Anthropic, and Google Gemini SDK dependencies
- Document comprehensive API investigation for all three providers
- Design minimal unified API with streaming-first architecture
- Add models.dev integration for pricing and capabilities
- Implement automatic caching strategy for all providers
- Update project documentation with package creation guide
This commit is contained in:
Mario Zechner 2025-08-17 20:18:45 +02:00
parent 2c03724862
commit f064ea0e14
14 changed files with 7437 additions and 21 deletions

390
package-lock.json generated
View file

@ -19,6 +19,15 @@
"node": ">=20.0.0"
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.60.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.60.0.tgz",
"integrity": "sha512-9zu/TXaUy8BZhXedDtt1wT3H4LOlpKDO1/ftiFpeR3N1PCr3KJFKkxxlQWWt1NNp08xSwUNJ3JNY8yhl8av6eQ==",
"license": "MIT",
"bin": {
"anthropic-ai-sdk": "bin/cli"
}
},
"node_modules/@biomejs/biome": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.4.tgz",
@ -624,6 +633,31 @@
"node": ">=18"
}
},
"node_modules/@google/genai": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.14.0.tgz",
"integrity": "sha512-jirYprAAJU1svjwSDVCzyVq+FrJpJd5CSxR/g2Ga/gZ0ZYZpcWjMS75KJl9y71K1mDN+tcx6s21CzCbB2R840g==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.14.2",
"ws": "^8.18.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.11.0"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
"optional": true
}
}
},
"node_modules/@mariozechner/ai": {
"resolved": "packages/ai",
"link": true
},
"node_modules/@mariozechner/pi": {
"resolved": "packages/pods",
"link": true
@ -659,6 +693,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
@ -683,6 +726,41 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/chalk": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
@ -695,6 +773,32 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
@ -737,6 +841,12 @@
"@esbuild/win32-x64": "0.25.8"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -752,6 +862,36 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
@ -765,6 +905,58 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
@ -781,6 +973,95 @@
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/openai": {
"version": "5.12.2",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.12.2.tgz",
"integrity": "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@ -791,6 +1072,26 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@ -818,6 +1119,12 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tsx": {
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
@ -858,6 +1165,56 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"packages/agent": {
"name": "@mariozechner/pi-agent",
"version": "0.5.8",
@ -1041,25 +1398,6 @@
"node": ">=16 || 14 >=14.17"
}
},
"packages/agent/node_modules/openai": {
"version": "5.12.2",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"packages/agent/node_modules/package-json-from-dist": {
"version": "1.0.1",
"license": "BlueOak-1.0.0"
@ -1259,6 +1597,20 @@
"node": ">=8"
}
},
"packages/ai": {
"name": "@mariozechner/ai",
"version": "0.5.8",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "0.60.0",
"@google/genai": "1.14.0",
"openai": "5.12.2"
},
"devDependencies": {},
"engines": {
"node": ">=20.0.0"
}
},
"packages/pods": {
"name": "@mariozechner/pi",
"version": "0.5.8",

View file

@ -7,7 +7,7 @@
],
"scripts": {
"clean": "npm run clean --workspaces",
"build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/pi",
"build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/ai && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/pi",
"check": "biome check --write . && npm run check --workspaces && tsc --noEmit",
"test": "npm run test --workspaces --if-present",
"version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js",

62
packages/ai/README.md Normal file
View file

@ -0,0 +1,62 @@
# @mariozechner/ai
Unified API for OpenAI, Anthropic, and Google Gemini LLM providers. This package provides a common interface for working with multiple LLM providers, handling their differences transparently while exposing a consistent, minimal API.
## Features (Planned)
- **Unified Interface**: Single API for OpenAI, Anthropic, and Google Gemini
- **Streaming Support**: Real-time response streaming with delta events
- **Tool Calling**: Consistent tool/function calling across providers
- **Reasoning/Thinking**: Support for reasoning tokens where available
- **Session Management**: Serializable conversation state across providers
- **Token Tracking**: Unified token counting (input, output, cached, reasoning)
- **Interrupt Handling**: Graceful cancellation of requests
- **Provider Detection**: Automatic configuration based on endpoint
- **Caching Support**: Provider-specific caching strategies
## Installation
```bash
npm install @mariozechner/ai
```
## Quick Start (Coming Soon)
```typescript
import { createClient } from '@mariozechner/ai';
// Automatically detects provider from configuration
const client = createClient({
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
model: 'gpt-4'
});
// Same API works for all providers
const response = await client.complete({
messages: [
{ role: 'user', content: 'Hello!' }
],
stream: true
});
for await (const event of response) {
if (event.type === 'content') {
process.stdout.write(event.text);
}
}
```
## Supported Providers
- **OpenAI**: GPT-3.5, GPT-4, o1, o3 models
- **Anthropic**: Claude models via native SDK
- **Google Gemini**: Gemini models with thinking support
## Development
This package is part of the pi monorepo. See the main README for development instructions.
## License
MIT

1706
packages/ai/anthropic-api.md Normal file

File diff suppressed because it is too large Load diff

1233
packages/ai/gemini-api.md Normal file

File diff suppressed because it is too large Load diff

2320
packages/ai/openai-api.md Normal file

File diff suppressed because it is too large Load diff

32
packages/ai/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "@mariozechner/ai",
"version": "0.5.8",
"description": "Unified API for OpenAI, Anthropic, and Google Gemini LLM providers",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "README.md"],
"scripts": {
"clean": "rm -rf dist",
"build": "tsc -p tsconfig.build.json",
"check": "biome check --write .",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"openai": "5.12.2",
"@anthropic-ai/sdk": "0.60.0",
"@google/genai": "1.14.0"
},
"devDependencies": {},
"keywords": ["ai", "llm", "openai", "anthropic", "gemini", "unified", "api"],
"author": "Mario Zechner",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/badlogic/pi-mono.git",
"directory": "packages/ai"
},
"engines": {
"node": ">=20.0.0"
}
}

950
packages/ai/plan.md Normal file
View file

@ -0,0 +1,950 @@
# Unified AI API Design Plan
Based on comprehensive investigation of OpenAI, Anthropic, and Gemini SDKs with actual implementation examples.
## Key API Differences Summary
### OpenAI
- **Dual APIs**: Chat Completions (broad support) vs Responses API (o1/o3 thinking content)
- **Thinking**: Only Responses API gives actual content, Chat Completions only gives counts
- **Roles**: `system`, `user`, `assistant`, `tool` (o1/o3 use `developer` instead of `system`)
- **Streaming**: Deltas in chunks with `stream_options.include_usage` for token usage
### Anthropic
- **Single API**: Messages API with comprehensive streaming
- **Content Blocks**: Always arrays, even for simple text
- **System**: Separate parameter, not in messages array
- **Tool Use**: Content blocks, not separate message role
- **Thinking**: Explicit budget allocation, appears as content blocks
- **Caching**: Per-block cache control with TTL options
### Gemini
- **Parts System**: All content split into typed parts
- **System**: Separate `systemInstruction` parameter
- **Roles**: Uses `model` instead of `assistant`
- **Thinking**: `part.thought: true` flag identifies reasoning
- **Streaming**: Returns complete responses, not deltas
- **Function Calls**: Embedded in parts array
## Unified API Design
### Core Client
```typescript
interface AIConfig {
provider: 'openai' | 'anthropic' | 'gemini';
apiKey: string;
model: string;
baseURL?: string; // For OpenAI-compatible endpoints
}
interface ModelInfo {
id: string;
name: string;
provider: string;
capabilities: {
reasoning: boolean;
toolCall: boolean;
vision: boolean;
audio?: boolean;
};
cost: {
input: number; // per million tokens
output: number; // per million tokens
cacheRead?: number;
cacheWrite?: number;
};
limits: {
context: number;
output: number;
};
knowledge?: string; // Knowledge cutoff date
}
class AI {
constructor(config: AIConfig);
// Main streaming interface - everything else builds on this
async *stream(request: Request): AsyncGenerator<Event>;
// Convenience method for non-streaming
async complete(request: Request): Promise<Response>;
// Get model information
getModelInfo(): ModelInfo;
// Abort current request
abort(): void;
}
```
### Message Format
```typescript
type Message =
| {
role: 'user';
content: string | Content[];
}
| {
role: 'assistant';
content: string | Content[];
model: string;
usage: TokenUsage;
toolCalls?: {
id: string;
name: string;
arguments: Record<string, any>;
}[];
}
| {
role: 'tool';
content: string | Content[];
toolCallId: string;
};
interface Content {
type: 'text' | 'image';
text?: string;
image?: {
data: string; // base64
mimeType: string;
};
}
```
### Request Format
```typescript
interface Request {
messages: Message[];
// System prompt (separated for Anthropic/Gemini compatibility)
systemPrompt?: string;
// Common parameters
temperature?: number;
maxTokens?: number;
stopSequences?: string[];
// Tools
tools?: {
name: string;
description: string;
parameters: Record<string, any>; // JSON Schema
}[];
toolChoice?: 'auto' | 'none' | 'required' | { name: string };
// Thinking/reasoning
reasoning?: {
enabled: boolean;
effort?: 'low' | 'medium' | 'high'; // OpenAI reasoning_effort
maxTokens?: number; // Anthropic thinking budget
};
// Abort signal
signal?: AbortSignal;
}
```
### Event Stream
```typescript
type Event =
| { type: 'start'; model: string; provider: string }
| { type: 'text'; content: string; delta: string }
| { type: 'thinking'; content: string; delta: string }
| { type: 'toolCall'; toolCall: ToolCall }
| { type: 'usage'; usage: TokenUsage }
| { type: 'done'; reason: StopReason; message: Message } // message includes model and usage
| { type: 'error'; error: Error };
interface TokenUsage {
input: number;
output: number;
total: number;
thinking?: number;
cacheRead?: number;
cacheWrite?: number;
cost?: {
input: number;
output: number;
cache?: number;
total: number;
};
}
type StopReason = 'stop' | 'length' | 'toolUse' | 'safety' | 'error';
```
## Caching Strategy
Caching is handled automatically by each provider adapter:
- **OpenAI**: Automatic prompt caching (no configuration needed)
- **Gemini**: Automatic context caching (no configuration needed)
- **Anthropic**: We automatically add cache_control to the system prompt and older messages
```typescript
class AnthropicAdapter {
private addCaching(messages: Message[]): any[] {
const anthropicMessages = [];
// Automatically cache older messages (assuming incremental context)
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
const isOld = i < messages.length - 2; // Cache all but last 2 messages
// Convert to Anthropic format with automatic caching
const blocks = this.toContentBlocks(msg);
if (isOld && blocks.length > 0) {
blocks[0].cache_control = { type: 'ephemeral' };
}
anthropicMessages.push({
role: msg.role === 'assistant' ? 'assistant' : 'user',
content: blocks
});
}
return anthropicMessages;
}
}
```
## Provider Adapter Implementation
### OpenAI Adapter
```typescript
class OpenAIAdapter {
private client: OpenAI;
private useResponsesAPI: boolean = false;
async *stream(request: Request): AsyncGenerator<Event> {
// Determine which API to use
if (request.reasoning?.enabled && this.isReasoningModel()) {
yield* this.streamResponsesAPI(request);
} else {
yield* this.streamChatCompletions(request);
}
}
private async *streamChatCompletions(request: Request) {
const stream = await this.client.chat.completions.create({
model: this.model,
messages: this.toOpenAIMessages(request),
tools: this.toOpenAITools(request.tools),
reasoning_effort: request.reasoning?.effort,
stream: true,
stream_options: { include_usage: true }
});
let content = '';
let toolCalls: any[] = [];
for await (const chunk of stream) {
if (chunk.choices[0]?.delta?.content) {
const delta = chunk.choices[0].delta.content;
content += delta;
yield { type: 'text', content, delta };
}
if (chunk.choices[0]?.delta?.tool_calls) {
// Accumulate tool calls
this.mergeToolCalls(toolCalls, chunk.choices[0].delta.tool_calls);
for (const tc of toolCalls) {
yield { type: 'toolCall', toolCall: tc, partial: true };
}
}
if (chunk.usage) {
yield {
type: 'usage',
usage: {
input: chunk.usage.prompt_tokens,
output: chunk.usage.completion_tokens,
total: chunk.usage.total_tokens,
thinking: chunk.usage.completion_tokens_details?.reasoning_tokens
}
};
}
}
}
private async *streamResponsesAPI(request: Request) {
// Use Responses API for actual thinking content
const response = await this.client.responses.create({
model: this.model,
input: this.toResponsesInput(request),
tools: this.toResponsesTools(request.tools),
stream: true
});
for await (const event of response) {
if (event.type === 'response.reasoning_text.delta') {
yield {
type: 'thinking',
content: event.text,
delta: event.delta
};
}
// Handle other event types...
}
}
private toOpenAIMessages(request: Request): any[] {
const messages: any[] = [];
// Handle system prompt
if (request.systemPrompt) {
const role = this.isReasoningModel() ? 'developer' : 'system';
messages.push({ role, content: request.systemPrompt });
}
// Convert unified messages
for (const msg of request.messages) {
if (msg.role === 'tool') {
messages.push({
role: 'tool',
content: msg.content,
tool_call_id: msg.toolCallId
});
} else {
messages.push({
role: msg.role,
content: this.contentToString(msg.content),
tool_calls: msg.toolCalls
});
}
}
return messages;
}
}
```
### Anthropic Adapter
```typescript
class AnthropicAdapter {
private client: Anthropic;
async *stream(request: Request): AsyncGenerator<Event> {
const stream = this.client.messages.stream({
model: this.model,
max_tokens: request.maxTokens || 1024,
messages: this.addCaching(request.messages),
system: request.systemPrompt,
tools: this.toAnthropicTools(request.tools),
thinking: request.reasoning?.enabled ? {
type: 'enabled',
budget_tokens: request.reasoning.maxTokens || 2000
} : undefined
});
let content = '';
let thinking = '';
stream.on('text', (delta, snapshot) => {
content = snapshot;
// Note: Can't yield from callback, need different approach
});
stream.on('thinking', (delta, snapshot) => {
thinking = snapshot;
});
// Use raw streaming instead for proper async generator
const rawStream = await this.client.messages.create({
...params,
stream: true
});
for await (const chunk of rawStream) {
switch (chunk.type) {
case 'content_block_delta':
if (chunk.delta.type === 'text_delta') {
content += chunk.delta.text;
yield {
type: 'text',
content,
delta: chunk.delta.text
};
}
break;
case 'message_delta':
if (chunk.usage) {
yield {
type: 'usage',
usage: {
input: chunk.usage.input_tokens,
output: chunk.usage.output_tokens,
total: chunk.usage.input_tokens + chunk.usage.output_tokens,
cacheRead: chunk.usage.cache_read_input_tokens,
cacheWrite: chunk.usage.cache_creation_input_tokens
}
};
}
break;
}
}
}
private toAnthropicMessages(request: Request): any[] {
return request.messages.map(msg => {
if (msg.role === 'tool') {
// Tool results go as user messages with tool_result blocks
return {
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: msg.toolCallId,
content: msg.content
}]
};
}
// Always use content blocks
const blocks: any[] = [];
if (typeof msg.content === 'string') {
blocks.push({
type: 'text',
text: msg.content,
cache_control: msg.cacheControl
});
} else {
// Convert unified content to blocks
for (const part of msg.content) {
if (part.type === 'text') {
blocks.push({ type: 'text', text: part.text });
} else if (part.type === 'image') {
blocks.push({
type: 'image',
source: {
type: 'base64',
media_type: part.image.mimeType,
data: part.image.data
}
});
}
}
}
// Add tool calls as blocks
if (msg.toolCalls) {
for (const tc of msg.toolCalls) {
blocks.push({
type: 'tool_use',
id: tc.id,
name: tc.name,
input: tc.arguments
});
}
}
return {
role: msg.role === 'assistant' ? 'assistant' : 'user',
content: blocks
};
});
}
}
```
### Gemini Adapter
```typescript
class GeminiAdapter {
private client: GoogleGenAI;
async *stream(request: Request): AsyncGenerator<Event> {
const stream = await this.client.models.generateContentStream({
model: this.model,
systemInstruction: request.systemPrompt ? {
parts: [{ text: request.systemPrompt }]
} : undefined,
contents: this.toGeminiContents(request),
tools: this.toGeminiTools(request.tools),
abortSignal: request.signal
});
let content = '';
let thinking = '';
for await (const chunk of stream) {
const candidate = chunk.candidates?.[0];
if (!candidate?.content?.parts) continue;
for (const part of candidate.content.parts) {
if (part.text && !part.thought) {
content += part.text;
yield {
type: 'text',
content,
delta: part.text
};
} else if (part.text && part.thought) {
thinking += part.text;
yield {
type: 'thinking',
content: thinking,
delta: part.text
};
} else if (part.functionCall) {
yield {
type: 'toolCall',
toolCall: {
id: part.functionCall.id || crypto.randomUUID(),
name: part.functionCall.name,
arguments: part.functionCall.args
}
};
}
}
if (chunk.usageMetadata) {
yield {
type: 'usage',
usage: {
input: chunk.usageMetadata.promptTokenCount || 0,
output: chunk.usageMetadata.candidatesTokenCount || 0,
total: chunk.usageMetadata.totalTokenCount || 0,
thinking: chunk.usageMetadata.thoughtsTokenCount,
cacheRead: chunk.usageMetadata.cachedContentTokenCount
}
};
}
}
}
private toGeminiContents(request: Request): any[] {
return request.messages.map(msg => {
const parts: any[] = [];
if (typeof msg.content === 'string') {
parts.push({ text: msg.content });
} else {
for (const part of msg.content) {
if (part.type === 'text') {
parts.push({ text: part.text });
} else if (part.type === 'image') {
parts.push({
inlineData: {
mimeType: part.image.mimeType,
data: part.image.data
}
});
}
}
}
// Add function calls as parts
if (msg.toolCalls) {
for (const tc of msg.toolCalls) {
parts.push({
functionCall: {
name: tc.name,
args: tc.arguments
}
});
}
}
// Add tool results as function responses
if (msg.role === 'tool') {
parts.push({
functionResponse: {
name: msg.toolCallId,
response: { result: msg.content }
}
});
}
return {
role: msg.role === 'assistant' ? 'model' : msg.role === 'tool' ? 'user' : msg.role,
parts
};
});
}
}
```
## Usage Examples
### Basic Streaming
```typescript
const ai = new AI({
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
model: 'gpt-4'
});
const stream = ai.stream({
messages: [
{ role: 'user', content: 'Write a haiku about coding' }
],
systemPrompt: 'You are a poetic programmer'
});
for await (const event of stream) {
switch (event.type) {
case 'text':
process.stdout.write(event.delta);
break;
case 'usage':
console.log(`\nTokens: ${event.usage.total}`);
break;
case 'done':
console.log(`\nFinished: ${event.reason}`);
break;
}
}
```
### Cross-Provider Tool Calling
```typescript
async function callWithTools(provider: 'openai' | 'anthropic' | 'gemini') {
const ai = new AI({
provider,
apiKey: process.env[`${provider.toUpperCase()}_API_KEY`],
model: getDefaultModel(provider)
});
const messages: Message[] = [{
role: 'user',
content: 'What is the weather in Paris and calculate 15 * 23?'
}];
const stream = ai.stream({
messages,
tools: [
{
name: 'weather',
description: 'Get weather for a location',
parameters: {
type: 'object',
properties: {
location: { type: 'string' }
},
required: ['location']
}
},
{
name: 'calculator',
description: 'Calculate math expressions',
parameters: {
type: 'object',
properties: {
expression: { type: 'string' }
},
required: ['expression']
}
}
]
});
const toolCalls: any[] = [];
for await (const event of stream) {
if (event.type === 'toolCall') {
toolCalls.push(event.toolCall);
// Execute tool
const result = await executeToolCall(event.toolCall);
// Add tool result to conversation
messages.push({
role: 'assistant',
toolCalls: [event.toolCall]
});
messages.push({
role: 'tool',
content: JSON.stringify(result),
toolCallId: event.toolCall.id
});
}
}
// Continue conversation with tool results
if (toolCalls.length > 0) {
const finalStream = ai.stream({ messages });
for await (const event of finalStream) {
if (event.type === 'text') {
process.stdout.write(event.delta);
}
}
}
}
```
### Thinking/Reasoning
```typescript
async function withThinking() {
// OpenAI o1
const openai = new AI({
provider: 'openai',
model: 'o1-preview'
});
// Anthropic Claude
const anthropic = new AI({
provider: 'anthropic',
model: 'claude-3-opus-20240229'
});
// Gemini thinking model
const gemini = new AI({
provider: 'gemini',
model: 'gemini-2.0-flash-thinking-exp-1219'
});
for (const ai of [openai, anthropic, gemini]) {
const stream = ai.stream({
messages: [{
role: 'user',
content: 'Solve this step by step: If a tree falls in a forest...'
}],
reasoning: {
enabled: true,
effort: 'high', // OpenAI reasoning_effort
maxTokens: 2000 // Anthropic budget
}
});
for await (const event of stream) {
if (event.type === 'thinking') {
console.log('[THINKING]', event.delta);
} else if (event.type === 'text') {
console.log('[RESPONSE]', event.delta);
} else if (event.type === 'done') {
// Final message includes model and usage with cost
console.log('Model:', event.message.model);
console.log('Tokens:', event.message.usage?.total);
console.log('Cost: $', event.message.usage?.cost?.total);
}
}
}
}
```
## Implementation Notes
### Critical Decisions
1. **Streaming First**: All providers support streaming, non-streaming is just collected events
2. **Unified Events**: Same event types across all providers for consistent handling
3. **Separate System Prompt**: Required for Anthropic/Gemini compatibility
4. **Tool Role**: Unified way to handle tool responses across providers
5. **Content Arrays**: Support both string and structured content
6. **Thinking Extraction**: Normalize reasoning across different provider formats
### Provider-Specific Handling
**OpenAI**:
- Choose between Chat Completions and Responses API based on model and thinking needs
- Map `developer` role for o1/o3 models
- Handle streaming tool call deltas
**Anthropic**:
- Convert to content blocks (always arrays)
- Tool results as user messages with tool_result blocks
- Handle MessageStream events or raw streaming
**Gemini**:
- Convert to parts system
- Extract thinking from `part.thought` flag
- Map `assistant` to `model` role
- Handle function calls/responses in parts
### Error Handling
```typescript
class AIError extends Error {
constructor(
message: string,
public code: string,
public provider: string,
public retryable: boolean,
public statusCode?: number
) {
super(message);
}
}
// In adapters
try {
// API call
} catch (error) {
if (error instanceof RateLimitError) {
throw new AIError(
'Rate limit exceeded',
'rate_limit',
this.provider,
true,
429
);
}
// Map other errors...
}
```
## Model Information & Cost Tracking
### Models Database
We cache the models.dev API data at build time for fast, offline access:
```typescript
// scripts/update-models.ts - Run during build or manually
async function updateModels() {
const response = await fetch('https://models.dev/api.json');
const data = await response.json();
// Transform to our format
const models: ModelsDatabase = transformModelsData(data);
// Generate TypeScript file
const content = `// Auto-generated from models.dev API
// Last updated: ${new Date().toISOString()}
// Run 'npm run update-models' to refresh
export const MODELS_DATABASE: ModelsDatabase = ${JSON.stringify(models, null, 2)};
`;
await fs.writeFile('src/models-data.ts', content);
}
// src/models.ts - Runtime model lookup
import { MODELS_DATABASE } from './models-data.js';
// Simple lookup with fallback
export function getModelInfo(provider: string, model: string): ModelInfo {
const info = MODELS_DATABASE.providers[provider]?.models[model];
if (!info) {
// Fallback for unknown models
return {
id: model,
name: model,
provider,
capabilities: {
reasoning: false,
toolCall: true,
vision: false
},
cost: { input: 0, output: 0 },
limits: { context: 128000, output: 4096 }
};
}
return info;
}
// Optional: Runtime override for testing new models
const runtimeOverrides = new Map<string, ModelInfo>();
export function registerModel(provider: string, model: string, info: ModelInfo) {
runtimeOverrides.set(`${provider}:${model}`, info);
}
```
### Cost Calculation
```typescript
class CostTracker {
private usage: TokenUsage = {
input: 0,
output: 0,
total: 0,
cacheRead: 0,
cacheWrite: 0
};
private modelInfo: ModelInfo;
constructor(modelInfo: ModelInfo) {
this.modelInfo = modelInfo;
}
addUsage(tokens: Partial<TokenUsage>): TokenUsage {
this.usage.input += tokens.input || 0;
this.usage.output += tokens.output || 0;
this.usage.thinking += tokens.thinking || 0;
this.usage.cacheRead += tokens.cacheRead || 0;
this.usage.cacheWrite += tokens.cacheWrite || 0;
this.usage.total = this.usage.input + this.usage.output + (this.usage.thinking || 0);
// Calculate costs (per million tokens)
const cost = this.modelInfo.cost;
this.usage.cost = {
input: (this.usage.input / 1_000_000) * cost.input,
output: (this.usage.output / 1_000_000) * cost.output,
cache:
((this.usage.cacheRead || 0) / 1_000_000) * (cost.cacheRead || 0) +
((this.usage.cacheWrite || 0) / 1_000_000) * (cost.cacheWrite || 0),
total: 0
};
this.usage.cost.total =
this.usage.cost.input +
this.usage.cost.output +
this.usage.cost.cache;
return { ...this.usage };
}
getTotalCost(): number {
return this.usage.cost?.total || 0;
}
getUsageSummary(): string {
return `Tokens: ${this.usage.total} (${this.usage.input}→${this.usage.output}) | Cost: $${this.getTotalCost().toFixed(4)}`;
}
}
```
### Integration in Adapters
```typescript
class OpenAIAdapter {
private costTracker: CostTracker;
constructor(config: AIConfig) {
const modelInfo = getModelInfo('openai', config.model);
this.costTracker = new CostTracker(modelInfo);
}
async *stream(request: Request): AsyncGenerator<Event> {
// ... streaming logic ...
if (chunk.usage) {
const usage = this.costTracker.addUsage({
input: chunk.usage.prompt_tokens,
output: chunk.usage.completion_tokens,
thinking: chunk.usage.completion_tokens_details?.reasoning_tokens,
cacheRead: chunk.usage.prompt_tokens_details?.cached_tokens
});
yield { type: 'usage', usage };
}
}
}
```
## Next Steps
1. Create models.ts with models.dev integration
2. Implement base `AI` class with adapter pattern
3. Create three provider adapters with full streaming support
4. Add comprehensive error mapping
5. Implement token counting and cost tracking
6. Add test suite for each provider
7. Create migration guide from native SDKs

5
packages/ai/src/index.ts Normal file
View file

@ -0,0 +1,5 @@
// @mariozechner/ai - Unified API for OpenAI, Anthropic, and Google Gemini
// This package provides a common interface for working with multiple LLM providers
// TODO: Export types and implementations once defined
export const version = "0.5.8";

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,606 @@
# Analysis: Creating Unified AI Package
## Package Structure Analysis for Pi Monorepo
Based on my examination of the existing packages (`tui`, `agent`, and `pods`), here are the comprehensive patterns and conventions used in this monorepo:
### 1. Package Naming Conventions
**Scoped NPM packages with consistent naming:**
- All packages use the `@mariozechner/` scope
- Package names follow the pattern: `@mariozechner/pi-<package-name>`
- Special case: the main CLI package is simply `@mariozechner/pi` (not `pi-pods`)
**Directory structure:**
- Packages are located in `/packages/<package-name>/`
- Directory names match the suffix of the npm package name (e.g., `tui`, `agent`, `pods`)
### 2. Package.json Structure Patterns
**Common fields across all packages:**
```json
{
"name": "@mariozechner/pi-<name>",
"version": "0.5.8", // Lockstep versioning - all packages share same version
"description": "...",
"type": "module", // All packages use ES modules
"author": "Mario Zechner",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/badlogic/pi-mono.git",
"directory": "packages/<name>"
},
"engines": {
"node": ">=20.0.0" // Consistent Node.js requirement
}
}
```
**Binary packages (agent, pods):**
- Include `"bin"` field with CLI command mapping
- Examples: `"pi-agent": "dist/cli.js"` and `"pi": "dist/cli.js"`
**Library packages (tui):**
- Include `"main"` field pointing to built entry point
- Include `"types"` field for TypeScript definitions
### 3. Scripts Configuration
**Universal scripts across all packages:**
- `"clean": "rm -rf dist"` - Removes build artifacts
- `"build": "tsc -p tsconfig.build.json"` - Builds with dedicated build config
- `"check": "biome check --write ."` - Linting and formatting
- `"prepublishOnly": "npm run clean && npm run build"` - Pre-publish cleanup
**CLI-specific build scripts:**
- Add `&& chmod +x dist/cli.js` for executable permissions
- Copy additional assets (e.g., `&& cp src/models.json dist/` for pods package)
### 4. Dependencies Structure
**Dependency hierarchy follows a clear pattern:**
```
pi-tui (foundation) -> pi-agent (uses tui) -> pi (uses agent)
```
**Internal dependencies:**
- Use exact version matching for internal packages (e.g., `"^0.5.8"`)
- Agent depends on TUI: `"@mariozechner/pi-tui": "^0.5.8"`
- Pods depends on Agent: `"@mariozechner/pi-agent": "^0.5.8"`
**External dependencies:**
- Common dependencies like `chalk` are used across multiple packages
- Specialized dependencies are package-specific (e.g., `marked` for tui, `openai` for agent)
### 5. TypeScript Configuration
**Dual TypeScript configuration approach:**
**`tsconfig.build.json` (for production builds):**
```json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
**Root `tsconfig.json` (for development and type checking):**
- Contains path mappings for cross-package imports during development
- Includes all source and test files
- Uses `"noEmit": true` for type checking without building
### 6. Source Directory Structure
**Standard structure across all packages:**
```
src/
├── index.ts # Main export file
├── cli.ts # CLI entry point (if applicable)
├── <core-files>.ts # Core functionality
├── components/ # Components (for tui)
├── tools/ # Tool implementations (for agent)
├── commands/ # Command implementations (for pods)
└── renderers/ # Output renderers (for agent)
```
### 7. Export Patterns (index.ts)
**Comprehensive type and function exports:**
- Export both types and implementation classes
- Use `export type` for type-only exports
- Group exports logically with comments
- Example from tui: exports components, interfaces, and utilities
- Example from agent: exports core classes, types, and utilities
### 8. Files Configuration
**Files included in NPM packages:**
- `"files": ["dist"]` or `"files": ["dist/**/*", "README.md"]`
- All packages include built `dist/` directory
- Some include additional files like README.md or scripts
### 9. README.md Structure
**Comprehensive documentation pattern:**
- Feature overview with key capabilities
- Quick start section with code examples
- Detailed API documentation
- Installation instructions
- Development setup
- Testing information (especially for tui)
- Examples and usage patterns
### 10. Testing Structure (TUI package)
**Dedicated test directory:**
- `test/` directory with `.test.ts` files for unit tests
- Example applications (e.g., `chat-app.ts`, `file-browser.ts`)
- Custom testing infrastructure (e.g., `virtual-terminal.ts`)
- Test script: `"test": "node --test --import tsx test/*.test.ts"`
### 11. Version Management
**Lockstep versioning:**
- All packages share the same version number
- Root package.json scripts handle version bumping across all packages
- Version sync script ensures internal dependency versions match
### 12. Build Order
**Dependency-aware build order:**
- Root build script builds packages in dependency order
- `"build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/pi"`
### 13. Common Configuration Files
**Shared across monorepo:**
- `biome.json` - Unified linting and formatting configuration
- `tsconfig.base.json` - Base TypeScript configuration
- `.gitignore` - Ignores `dist/`, `node_modules/`, and other build artifacts
- Husky pre-commit hooks for formatting and type checking
### 14. Keywords and Metadata
**Descriptive keywords for NPM discovery:**
- Each package includes relevant keywords (e.g., "tui", "terminal", "agent", "ai", "llm")
- Keywords help with package discoverability
This analysis shows a well-structured monorepo with consistent patterns that would make adding new packages straightforward by following these established conventions.
## Monorepo Configuration Analysis
Based on my analysis of the pi-mono monorepo configuration, here's a comprehensive guide on how to properly integrate a new package:
### 1. Root Package.json Configuration
**Workspace Configuration:**
- Uses npm workspaces with `"workspaces": ["packages/*"]`
- All packages are located under `/packages/` directory
- Private monorepo (`"private": true`) with ESM modules (`"type": "module"`)
**Build System:**
- **Sequential Build Order**: The build script explicitly defines dependency order:
```json
"build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/pi"
```
- **Dependency Chain**: `pi-tui``pi-agent``pi` (pods)
- **Important**: New packages must be inserted in the correct dependency order in the build script
**Scripts Available:**
- `clean`: Cleans all package dist folders
- `build`: Sequential build respecting dependencies
- `check`: Runs Biome formatting, package checks, and TypeScript checking
- `test`: Runs tests across all packages
- Version management scripts (lockstep versioning)
- Publishing scripts with dry-run capability
### 2. Root TypeScript Configuration
**Dual Configuration System:**
- **`tsconfig.base.json`**: Base TypeScript settings for all packages
- **`tsconfig.json`**: Development configuration with path mappings for cross-package imports
- **Package `tsconfig.build.json`**: Clean build configs per package
**Path Mappings** (in `/Users/badlogic/workspaces/pi-mono/tsconfig.json`):
```json
"paths": {
"@mariozechner/pi-tui": ["./packages/tui/src/index.ts"],
"@mariozechner/pi-agent": ["./packages/agent/src/index.ts"],
"@mariozechner/pi": ["./packages/pods/src/index.ts"]
}
```
### 3. Package Dependencies and Structure
**Dependency Structure:**
- `pi-tui` (base library) - no internal dependencies
- `pi-agent` depends on `pi-tui`
- `pi` (pods) depends on `pi-agent`
**Standard Package Structure:**
```
packages/new-package/
├── src/
│ ├── index.ts # Main export file
│ └── ... # Implementation files
├── package.json # Package configuration
├── tsconfig.build.json # Build-specific TypeScript config
├── README.md # Package documentation
└── dist/ # Build output (gitignored)
```
### 4. Version Management
**Lockstep Versioning:**
- All packages share the same version number (currently 0.5.8)
- Automated version sync script: `/Users/badlogic/workspaces/pi-mono/scripts/sync-versions.js`
- Inter-package dependencies are automatically updated to match current versions
**Version Scripts:**
- `npm run version:patch/minor/major` - Updates all package versions and syncs dependencies
- Automatic dependency version synchronization
### 5. GitIgnore Patterns
**Package-Level Ignores:**
```
packages/*/node_modules/
packages/*/dist/
```
Plus standard ignores for logs, IDE files, environment files, etc.
## How to Integrate a New Package
### Step 1: Create Package Structure
```bash
mkdir packages/your-new-package
cd packages/your-new-package
```
### Step 2: Create package.json
```json
{
"name": "@mariozechner/your-new-package",
"version": "0.5.8",
"description": "Your package description",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"scripts": {
"clean": "rm -rf dist",
"build": "tsc -p tsconfig.build.json",
"check": "biome check --write .",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
// Add dependencies on other packages in the monorepo if needed
// "@mariozechner/pi-tui": "^0.5.8"
},
"devDependencies": {},
"keywords": ["relevant", "keywords"],
"author": "Mario Zechner",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/badlogic/pi-mono.git",
"directory": "packages/your-new-package"
},
"engines": {
"node": ">=20.0.0"
}
}
```
### Step 3: Create tsconfig.build.json
```json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
### Step 4: Create src/index.ts
```typescript
// Main exports for your package
export * from './your-main-module.js';
```
### Step 5: Update Root Configuration
**Add to `/Users/badlogic/workspaces/pi-mono/tsconfig.json` paths:**
```json
"paths": {
"@mariozechner/pi-tui": ["./packages/tui/src/index.ts"],
"@mariozechner/pi-agent": ["./packages/agent/src/index.ts"],
"@mariozechner/pi": ["./packages/pods/src/index.ts"],
"@mariozechner/your-new-package": ["./packages/your-new-package/src/index.ts"]
}
```
**Update build script in root `/Users/badlogic/workspaces/pi-mono/package.json`:**
```json
"build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/your-new-package && npm run build -w @mariozechner/pi"
```
(Insert in correct dependency order)
### Step 6: Update sync-versions.js
If your package depends on other monorepo packages, add synchronization logic to `/Users/badlogic/workspaces/pi-mono/scripts/sync-versions.js`.
### Step 7: Install and Test
```bash
# From monorepo root
npm install
npm run build
npm run check
```
## Key Requirements for New Packages
1. **Must use ESM modules** (`"type": "module"`)
2. **Must follow lockstep versioning** (same version as other packages)
3. **Must be placed in correct build order** based on dependencies
4. **Must use tab indentation** (Biome config: `"indentStyle": "tab"`)
5. **Must avoid `any` types** unless absolutely necessary (project instruction)
6. **Must include proper TypeScript declarations** (`"declaration": true`)
7. **Must use Node.js >= 20.0.0** (engine requirement)
8. **Must follow the standard package structure** with src/, dist/, proper exports
## Development Workflow
1. **Development**: Use `tsx` to run source files directly (no build needed)
2. **Type Checking**: `npm run check` works across all packages
3. **Building**: Sequential builds respect dependency order
4. **Publishing**: Automatic version sync and cross-package dependency updates
5. **Testing**: Each package can have its own test suite
This monorepo is well-structured for maintaining multiple related packages with clean dependency management and automated version synchronization.
## Detailed Findings: Unified AI API Requirements Based on Current pi-agent Usage
After thoroughly analyzing the existing agent package (`/Users/badlogic/workspaces/pi-mono/packages/agent`), here are the comprehensive requirements for a unified AI API based on current usage patterns:
### **1. Core API Structure & Event System**
**Current Pattern:**
- Event-driven architecture using `AgentEvent` types
- Single `AgentEventReceiver` interface for all output handling
- Support for both single-shot and interactive modes
**Required API Features:**
```typescript
type AgentEvent =
| { type: "session_start"; sessionId: string; model: string; api: string; baseURL: string; systemPrompt: string }
| { type: "assistant_start" }
| { type: "reasoning"; text: string }
| { type: "tool_call"; toolCallId: string; name: string; args: string }
| { type: "tool_result"; toolCallId: string; result: string; isError: boolean }
| { type: "assistant_message"; text: string }
| { type: "error"; message: string }
| { type: "user_message"; text: string }
| { type: "interrupted" }
| { type: "token_usage"; inputTokens: number; outputTokens: number; totalTokens: number; cacheReadTokens: number; cacheWriteTokens: number; reasoningTokens: number }
```
### **2. OpenAI API Integration Patterns**
**Current Implementation:**
- Uses OpenAI SDK v5.12.2 (`import OpenAI from "openai"`)
- Supports both Chat Completions (`/v1/chat/completions`) and Responses API (`/v1/responses`)
- Provider detection based on base URL patterns
**Provider Support Required:**
```typescript
// Detected providers based on baseURL patterns
type Provider = "openai" | "gemini" | "groq" | "anthropic" | "openrouter" | "other"
// Provider-specific configurations
interface ProviderConfig {
openai: { reasoning_effort: "minimal" | "low" | "medium" | "high" }
gemini: { extra_body: { google: { thinking_config: { thinking_budget: number, include_thoughts: boolean } } } }
groq: { reasoning_format: "parsed", reasoning_effort: string }
openrouter: { reasoning: { effort: "low" | "medium" | "high" } }
}
```
### **3. Streaming vs Non-Streaming**
**Current Status:**
- **No streaming currently implemented** - uses standard request/response
- All API calls are non-streaming: `await client.chat.completions.create()` and `await client.responses.create()`
- Events are emitted synchronously after full response
**Streaming Requirements for Unified API:**
- Support for streaming responses with partial content updates
- Event-driven streaming with `assistant_message_delta` events
- Proper handling of tool call streaming
- Reasoning token streaming for supported models
### **4. Tool Calling Architecture**
**Current Implementation:**
```typescript
// Tool definitions for both APIs
toolsForResponses: Array<{type: "function", name: string, description: string, parameters: object}>
toolsForChat: ChatCompletionTool[]
// Tool execution with abort support
async function executeTool(name: string, args: string, signal?: AbortSignal): Promise<string>
// Built-in tools: read, list, bash, glob, rg (ripgrep)
```
**Unified API Requirements:**
- Automatic tool format conversion between Chat Completions and Responses API
- Built-in tools with filesystem and shell access
- Custom tool registration capability
- Tool execution with proper abort/interrupt handling
- Tool result streaming for long-running operations
### **5. Message Structure Handling**
**Current Pattern:**
- Dual message format support based on API type
- Automatic conversion between formats in `setEvents()` method
**Chat Completions Format:**
```typescript
{ role: "system" | "user" | "assistant" | "tool", content: string, tool_calls?: any[] }
```
**Responses API Format:**
```typescript
{ type: "message" | "function_call" | "function_call_output", content: any[] }
```
### **6. Session Persistence System**
**Current Implementation:**
```typescript
interface SessionData {
config: AgentConfig
events: SessionEvent[]
totalUsage: TokenUsage
}
// File-based persistence in ~/.pi/sessions/
// JSONL format with session headers and event entries
// Automatic session continuation support
```
**Requirements:**
- Directory-based session organization
- Event replay capability for session restoration
- Cumulative token usage tracking
- Session metadata (config, timestamps, working directory)
### **7. Token Counting & Usage Tracking**
**Current Implementation:**
```typescript
interface TokenUsage {
inputTokens: number
outputTokens: number
totalTokens: number
cacheReadTokens: number
cacheWriteTokens: number
reasoningTokens: number // For o1/o3 and reasoning models
}
```
**Provider-Specific Token Mapping:**
- OpenAI: `prompt_tokens`, `completion_tokens`, `cached_tokens`, `reasoning_tokens`
- Responses API: `input_tokens`, `output_tokens`, `cached_tokens`, `reasoning_tokens`
- Cumulative tracking across conversations
### **8. Abort/Interrupt Handling**
**Current Pattern:**
```typescript
class Agent {
private abortController: AbortController | null = null
async ask(message: string) {
this.abortController = new AbortController()
// Pass signal to all API calls and tool executions
}
interrupt(): void {
this.abortController?.abort()
}
}
```
**Requirements:**
- AbortController integration for all async operations
- Graceful interruption of API calls, tool execution, and streaming
- Proper cleanup and "interrupted" event emission
- Signal propagation to nested operations
### **9. Reasoning/Thinking Support**
**Current Implementation:**
```typescript
// Provider-specific reasoning extraction
function parseReasoningFromMessage(message: any, baseURL?: string): {
cleanContent: string
reasoningTexts: string[]
}
// Automatic reasoning support detection
async function checkReasoningSupport(client, model, api, baseURL, signal): Promise<boolean>
```
**Provider Support:**
- **OpenAI o1/o3**: Full thinking content via Responses API
- **Groq GPT-OSS**: Reasoning via `reasoning_format: "parsed"`
- **Gemini 2.5**: Thinking content via `<thought>` tags
- **OpenRouter**: Model-dependent reasoning support
### **10. Error Handling Patterns**
**Current Approach:**
- Try/catch blocks around all API calls
- Error events emitted through event system
- Specific error handling for reasoning model failures
- Provider-specific error interpretation
### **11. Configuration Management**
**Current Structure:**
```typescript
interface AgentConfig {
apiKey: string
baseURL: string
model: string
api: "completions" | "responses"
systemPrompt: string
}
```
**Provider Detection:**
```typescript
function detectProvider(baseURL?: string): Provider {
// URL pattern matching for automatic provider configuration
}
```
### **12. Output Rendering System**
**Current Renderers:**
- **ConsoleRenderer**: Terminal output with animations, token display
- **TuiRenderer**: Full interactive TUI with pi-tui integration
- **JsonRenderer**: JSONL event stream output
**Requirements:**
- Event-based rendering architecture
- Real-time token usage display
- Loading animations for async operations
- Markdown rendering support
- Tool execution progress indication
### **Summary: Key Unified API Requirements**
1. **Event-driven architecture** with standardized event types
2. **Dual API support** (Chat Completions + Responses API) with automatic format conversion
3. **Provider abstraction** with automatic detection and configuration
4. **Comprehensive tool system** with abort support and built-in tools
5. **Session persistence** with event replay and token tracking
6. **Reasoning/thinking support** across multiple providers
7. **Interrupt handling** with AbortController integration
8. **Token usage tracking** with provider-specific mapping
9. **Flexible rendering** through event receiver pattern
10. **Configuration management** with provider-specific settings
The unified API should maintain this event-driven, provider-agnostic approach while adding streaming capabilities and enhanced tool execution features that the current implementation lacks.

View file

@ -0,0 +1,46 @@
# Create AI Package with Unified API
**Status:** Done
**Agent PID:** 10965
## Original Todo
ai: create a new package ai (package name @mariozechner/ai) which implements a common api for the openai, anthropic, and google gemini apis
- look at the other packages and how they are set up, mirror that setup for ai
- install the latest version of each dependency via npm in the ai package
- openai@5.12.2
- @anthropic-ai/sdk@0.60.0
- @google/genai@1.14.0
- investigate the APIs in their respective node_modules folder so you understand how to use them. specifically, we need to understand how to:
- stream responses, including reasoning/thinking tokens and tool calls
- abort requests
- handle errors
- handle stop reasons
- maintain the context (message history) such that it can be serialized in a uniform format to disk, and deserialized again later and used with the other api
- count tokens (input, output, cached read, cached write)
- enable caching
- Create a plan.md in the ai package that details how the unified API on top of all three could look like. we want the most minimal api possible, which allows serialization/deserialization, turning on/off reasoning/thinking, and handle system prompt and tool specifications
## Description
Create the initial package scaffold for @mariozechner/ai following the established monorepo patterns, install the required dependencies (openai, anthropic, google genai SDKs), and create a plan.md file that details the unified API design for all three providers.
*Read [analysis.md](./analysis.md) in full for detailed codebase research and context*
## Implementation Plan
- [x] Create package directory structure at packages/ai/
- [x] Create package.json with proper configuration following monorepo patterns
- [x] Create tsconfig.build.json for build configuration
- [x] Create initial src/index.ts file
- [x] Add package to root tsconfig.json path mappings
- [x] Update root package.json build script to include ai package
- [x] Install dependencies: openai@5.12.2, @anthropic-ai/sdk@0.60.0, @google/genai@1.14.0
- [x] Create README.md with package description
- [x] Create plan.md detailing the unified API design
- [x] Investigate OpenAI, Anthropic, and Gemini APIs in detail
- [x] Document implementation details for each API
- [x] Update todos/project-description.md with "How to Create a New Package" section
- [x] Update todos/project-description.md Testing section to reflect that tui has Node.js built-in tests
- [x] Run npm install from root to link everything
- [x] Verify package builds correctly with npm run build
## Notes
[Implementation notes]

View file

@ -39,4 +39,98 @@ A comprehensive toolkit for managing Large Language Model (LLM) deployments and
- Publish: `npm run publish`
## Testing
Currently no formal testing framework is configured. Test infrastructure exists but no actual test files or framework dependencies are present.
The TUI package includes comprehensive tests using Node.js built-in test framework:
- Unit tests in `packages/tui/test/*.test.ts`
- Test runner: `node --test --import tsx test/*.test.ts`
- Virtual terminal for TUI testing via `@xterm/headless`
- Example applications for manual testing
## How to Create a New Package
Follow these steps to add a new package to the monorepo:
1. **Create package directory structure:**
```bash
mkdir -p packages/your-package/src
```
2. **Create package.json:**
```json
{
"name": "@mariozechner/your-package",
"version": "0.5.8",
"description": "Package description",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "README.md"],
"scripts": {
"clean": "rm -rf dist",
"build": "tsc -p tsconfig.build.json",
"check": "biome check --write .",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {},
"devDependencies": {},
"keywords": ["relevant", "keywords"],
"author": "Mario Zechner",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/badlogic/pi-mono.git",
"directory": "packages/your-package"
},
"engines": {
"node": ">=20.0.0"
}
}
```
3. **Create tsconfig.build.json:**
```json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
4. **Create src/index.ts:**
```typescript
// Main exports for your package
export const version = "0.5.8";
```
5. **Update root tsconfig.json paths:**
Add your package to the `paths` mapping in the correct dependency order:
```json
"paths": {
"@mariozechner/pi-tui": ["./packages/tui/src/index.ts"],
"@mariozechner/your-package": ["./packages/your-package/src/index.ts"],
// ... other packages
}
```
6. **Update root package.json build script:**
Insert your package in the correct dependency order:
```json
"build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/your-package && ..."
```
7. **Install and verify:**
```bash
npm install
npm run build
npm run check
```
**Important Notes:**
- All packages use lockstep versioning (same version number)
- Follow dependency order: foundational packages build first
- Use ESM modules (`"type": "module"`)
- No `any` types unless absolutely necessary
- Include README.md with package documentation

View file

@ -4,6 +4,7 @@
"noEmit": true,
"paths": {
"@mariozechner/pi-tui": ["./packages/tui/src/index.ts"],
"@mariozechner/pi-ai": ["./packages/ai/src/index.ts"],
"@mariozechner/pi-agent": ["./packages/agent/src/index.ts"],
"@mariozechner/pi": ["./packages/pods/src/index.ts"]
}