{"_id":"@cargo-cult/pi-ai","_rev":"7-6dc9b4aeac028fcc0869effd867974e3","name":"@cargo-cult/pi-ai","dist-tags":{"latest":"0.47.0"},"versions":{"0.40.0":{"name":"@cargo-cult/pi-ai","version":"0.40.0","keywords":["ai","llm","openai","anthropic","gemini","unified","api"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-ai@0.40.0","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"bin":{"pi-ai":"dist/cli.js"},"dist":{"shasum":"4a4c038755ac5c2609a4362f9e51a3a1c4010e35","tarball":"https://registry.npmjs.org/@cargo-cult/pi-ai/-/pi-ai-0.40.0.tgz","fileCount":147,"integrity":"sha512-iobXag7CUiusau72vRRbm021u34JJTW66lcUZCjIjofJQb+f/5OCWj45+gwm6djpsuRXvyHckrx3VGcNn9zHxg==","signatures":[{"sig":"MEUCIQDEBDypBVBfpgAkXE5mieDh8gib7eCXBpJVlzcnWU2DqwIgDgyQMWMvcNDa0GQ2inplsOckaeDIli9xu4EXdPZV4D4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-ai@0.40.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2236745},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"94ba25b6aee13ac8f98935b3536dab5192ed0399","scripts":{"dev":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","test":"vitest --run","build":"npm run generate-models && tsgo -p tsconfig.build.json && node scripts/copy-assets.js","clean":"rm -rf dist","dev:tsc":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","prepublishOnly":"npm run clean && npm run build","generate-models":"npx tsx scripts/generate-models.ts"},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/ai"},"_npmVersion":"10.9.4","description":"Unified LLM API with automatic model discovery and provider configuration","directories":{},"_nodeVersion":"22.21.1","dependencies":{"ajv":"^8.17.1","chalk":"^5.6.2","openai":"6.10.0","ajv-formats":"^3.0.1","partial-json":"^0.1.7","@google/genai":"1.34.0","@anthropic-ai/sdk":"0.71.2","@sinclair/typebox":"^0.34.41","zod-to-json-schema":"^3.24.6","@mistralai/mistralai":"1.10.0"},"_hasShrinkwrap":false,"devDependencies":{"canvas":"^3.2.0","vitest":"^3.2.4","@types/node":"^24.3.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-ai_0.40.0_1767983651155_0.755529306511391","host":"s3://npm-registry-packages-npm-production"}},"0.40.1":{"name":"@cargo-cult/pi-ai","version":"0.40.1","keywords":["ai","llm","openai","anthropic","gemini","unified","api"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-ai@0.40.1","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"bin":{"pi-ai":"dist/cli.js"},"dist":{"shasum":"dc1b1248e05746aec0894e440dc88ae9ab31aece","tarball":"https://registry.npmjs.org/@cargo-cult/pi-ai/-/pi-ai-0.40.1.tgz","fileCount":147,"integrity":"sha512-g6FzbQlyUvcPBxdn4g6aTSVXHRIY32cQ95g3MMIVSl3IziTpXBLaEQMyy2FY7nyqMte66iStJuQM2GGock8RFg==","signatures":[{"sig":"MEQCIBJL5Vp3e1egi2uuY9q6w9xKMDEtAi1nhzqqiRj6P1ohAiAZ+LR6R+vs8sb+oZzXzRA1uAzEESMJeXXZvejNeD1vkQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-ai@0.40.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2236745},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"4d64ff64d64172c9c7ac87db756bf6044503396f","scripts":{"dev":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","test":"vitest --run","build":"npm run generate-models && tsgo -p tsconfig.build.json && node scripts/copy-assets.js","clean":"rm -rf dist","dev:tsc":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","prepublishOnly":"npm run clean && npm run build","generate-models":"npx tsx scripts/generate-models.ts"},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/ai"},"_npmVersion":"10.9.4","description":"Unified LLM API with automatic model discovery and provider configuration","directories":{},"_nodeVersion":"22.21.1","dependencies":{"ajv":"^8.17.1","chalk":"^5.6.2","openai":"6.10.0","ajv-formats":"^3.0.1","partial-json":"^0.1.7","@google/genai":"1.34.0","@anthropic-ai/sdk":"0.71.2","@sinclair/typebox":"^0.34.41","zod-to-json-schema":"^3.24.6","@mistralai/mistralai":"1.10.0"},"_hasShrinkwrap":false,"devDependencies":{"canvas":"^3.2.0","vitest":"^3.2.4","@types/node":"^24.3.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-ai_0.40.1_1767984131623_0.763230102797392","host":"s3://npm-registry-packages-npm-production"}},"0.42.0":{"name":"@cargo-cult/pi-ai","version":"0.42.0","keywords":["ai","llm","openai","anthropic","gemini","unified","api"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-ai@0.42.0","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"bin":{"pi-ai":"dist/cli.js"},"dist":{"shasum":"3c790c04b1b5557668f54cc3731f07cae825ca47","tarball":"https://registry.npmjs.org/@cargo-cult/pi-ai/-/pi-ai-0.42.0.tgz","fileCount":147,"integrity":"sha512-SlPNP/ayHhyMp1HfgN4siyfz0YmSiQSmRyLJXtmFvaypqAERLK6ATBLfaLbv3RzRmb/JC6b24/lf0c20G0H/Hw==","signatures":[{"sig":"MEUCIQCQe8BtcbgVCO6tFv5WI6Y1QzLxgjvSRjxyLwSs2bmVLQIgYx8FCwXb8xQ1cj7cCv+oX2Ht5aD4mQ/vfThD1b4arcM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-ai@0.42.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2301294},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"84996508517bf91a4cbe8fbaa09d5b12b19f38cf","scripts":{"dev":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","test":"vitest --run","build":"npm run generate-models && tsgo -p tsconfig.build.json && node scripts/copy-assets.js","clean":"rm -rf dist","dev:tsc":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","prepublishOnly":"npm run clean && npm run build","generate-models":"npx tsx scripts/generate-models.ts"},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/ai"},"_npmVersion":"10.9.4","description":"Unified LLM API with automatic model discovery and provider configuration","directories":{},"_nodeVersion":"22.21.1","dependencies":{"ajv":"^8.17.1","chalk":"^5.6.2","openai":"6.10.0","ajv-formats":"^3.0.1","partial-json":"^0.1.7","@google/genai":"1.34.0","@anthropic-ai/sdk":"0.71.2","@sinclair/typebox":"^0.34.41","zod-to-json-schema":"^3.24.6","@mistralai/mistralai":"1.10.0"},"_hasShrinkwrap":false,"devDependencies":{"canvas":"^3.2.0","vitest":"^3.2.4","@types/node":"^24.3.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-ai_0.42.0_1767985874825_0.02594439988360997","host":"s3://npm-registry-packages-npm-production"}},"0.42.1":{"name":"@cargo-cult/pi-ai","version":"0.42.1","keywords":["ai","llm","openai","anthropic","gemini","unified","api"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-ai@0.42.1","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"bin":{"pi-ai":"dist/cli.js"},"dist":{"shasum":"837dd89f03b248d1485fd0afec6aa8c5f5370d94","tarball":"https://registry.npmjs.org/@cargo-cult/pi-ai/-/pi-ai-0.42.1.tgz","fileCount":147,"integrity":"sha512-FHDkvLtVzgJ6CR+rkx+/sOmhfNZ7KEGeEmBaNviYwnYdHDUc2XadMsxfREiFBmSygRtdlH3elRjaD/1+wJYw/Q==","signatures":[{"sig":"MEQCIHPS9pKLRjf213nXg1nTpmx+rWQMObUZuO6Z4ca9zzS9AiBKmpPyg/78ROK+8BBhEHxz1CE9oto51vl6IfafCpZ2ig==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-ai@0.42.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2301294},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"5198ee434057587f815953b57e154b8f71e1db3c","scripts":{"dev":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","test":"vitest --run","build":"npm run generate-models && tsgo -p tsconfig.build.json && node scripts/copy-assets.js","clean":"rm -rf dist","dev:tsc":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","prepublishOnly":"npm run clean && npm run build","generate-models":"npx tsx scripts/generate-models.ts"},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/ai"},"_npmVersion":"10.9.4","description":"Unified LLM API with automatic model discovery and provider configuration","directories":{},"_nodeVersion":"22.21.1","dependencies":{"ajv":"^8.17.1","chalk":"^5.6.2","openai":"6.10.0","ajv-formats":"^3.0.1","partial-json":"^0.1.7","@google/genai":"1.34.0","@anthropic-ai/sdk":"0.71.2","@sinclair/typebox":"^0.34.41","zod-to-json-schema":"^3.24.6","@mistralai/mistralai":"1.10.0"},"_hasShrinkwrap":false,"devDependencies":{"canvas":"^3.2.0","vitest":"^3.2.4","@types/node":"^24.3.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-ai_0.42.1_1767986266757_0.5276293051247869","host":"s3://npm-registry-packages-npm-production"}},"0.45.3":{"name":"@cargo-cult/pi-ai","version":"0.45.3","keywords":["ai","llm","openai","anthropic","gemini","bedrock","unified","api"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-ai@0.45.3","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"bin":{"pi-ai":"dist/cli.js"},"dist":{"shasum":"a5b23ff7a770bcf5e9374b83f5b5fa785572e0ce","tarball":"https://registry.npmjs.org/@cargo-cult/pi-ai/-/pi-ai-0.45.3.tgz","fileCount":150,"integrity":"sha512-XwwtN205JHjg99YDP3BDAj2THFmDk2uBkyxjkhZE3/tsm3J5tZFX2EntHhYu1gZRu0vNOCGIrVCyJY1JkcVuRA==","signatures":[{"sig":"MEUCIBY8ANW1OJnJ7pfSN8veXAQkz/fT3DAHpI9ukLOXnCphAiEAqg0040pYfrk8miBrPpjnH1+jlrgw6PPv1WVsYKScQP4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-ai@0.45.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2968813},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"c565ce93e848a6a833849cebebec6178585abed7","scripts":{"dev":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","test":"vitest --run","build":"npm run generate-models && tsgo -p tsconfig.build.json","clean":"rm -rf dist","dev:tsc":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","prepublishOnly":"npm run clean && npm run build","generate-models":"npx tsx scripts/generate-models.ts"},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/ai"},"_npmVersion":"10.9.4","description":"Unified LLM API with automatic model discovery and provider configuration","directories":{},"_nodeVersion":"22.21.1","dependencies":{"ajv":"^8.17.1","chalk":"^5.6.2","openai":"6.10.0","ajv-formats":"^3.0.1","partial-json":"^0.1.7","@google/genai":"1.34.0","@anthropic-ai/sdk":"0.71.2","@sinclair/typebox":"^0.34.41","zod-to-json-schema":"^3.24.6","@mistralai/mistralai":"1.10.0","@aws-sdk/client-bedrock-runtime":"^3.966.0"},"_hasShrinkwrap":false,"devDependencies":{"canvas":"^3.2.0","vitest":"^3.2.4","@types/node":"^24.3.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-ai_0.45.3_1768326668581_0.7625552747161461","host":"s3://npm-registry-packages-npm-production"}},"0.45.7":{"name":"@cargo-cult/pi-ai","version":"0.45.7","keywords":["ai","llm","openai","anthropic","gemini","bedrock","unified","api"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-ai@0.45.7","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"bin":{"pi-ai":"dist/cli.js"},"dist":{"shasum":"00da7ad230363336a3df543a2986a3afe76e7c34","tarball":"https://registry.npmjs.org/@cargo-cult/pi-ai/-/pi-ai-0.45.7.tgz","fileCount":150,"integrity":"sha512-9GQUyXBjuzpWb5JH2tDsC4Xnds4LN9UKGzY7X3o6pLKtY0oGDW30f2isULeG5Bx2KNoxeVIcYrV9nMTn/n57KA==","signatures":[{"sig":"MEUCIELtGtxysb8v1j1HV6C62MkJ3cg691lChrq8wqPqymn3AiEAo5DWgWIO8W3G1j9/1HP8Qc1NxBwfhFio8k1Lv0N96II=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-ai@0.45.7","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2973738},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"gitHead":"0854b993f8933023232e238f1e8eb14c6768817f","scripts":{"dev":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","test":"vitest --run","build":"npm run generate-models && tsgo -p tsconfig.build.json","clean":"rm -rf dist","dev:tsc":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","prepublishOnly":"npm run clean && npm run build","generate-models":"npx tsx scripts/generate-models.ts"},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/ai"},"_npmVersion":"10.9.4","description":"Unified LLM API with automatic model discovery and provider configuration","directories":{},"_nodeVersion":"22.21.1","dependencies":{"ajv":"^8.17.1","chalk":"^5.6.2","openai":"6.10.0","ajv-formats":"^3.0.1","partial-json":"^0.1.7","@google/genai":"1.34.0","@anthropic-ai/sdk":"0.71.2","@sinclair/typebox":"^0.34.41","zod-to-json-schema":"^3.24.6","@mistralai/mistralai":"1.10.0","@aws-sdk/client-bedrock-runtime":"^3.966.0"},"_hasShrinkwrap":false,"devDependencies":{"canvas":"^3.2.0","vitest":"^3.2.4","@types/node":"^24.3.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-ai_0.45.7_1768473921527_0.2762583766495623","host":"s3://npm-registry-packages-npm-production"}},"0.47.0":{"name":"@cargo-cult/pi-ai","version":"0.47.0","description":"Unified LLM API with automatic model discovery and provider configuration","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","bin":{"pi-ai":"dist/cli.js"},"scripts":{"clean":"rm -rf dist","generate-models":"npx tsx scripts/generate-models.ts","build":"npm run generate-models && tsgo -p tsconfig.build.json","dev":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","dev:tsc":"tsgo -p tsconfig.build.json --watch --preserveWatchOutput","test":"vitest --run","prepublishOnly":"npm run clean && npm run build"},"dependencies":{"@anthropic-ai/sdk":"0.71.2","@aws-sdk/client-bedrock-runtime":"^3.966.0","@google/genai":"1.34.0","@mistralai/mistralai":"1.10.0","@sinclair/typebox":"^0.34.41","ajv":"^8.17.1","ajv-formats":"^3.0.1","chalk":"^5.6.2","openai":"6.10.0","partial-json":"^0.1.7","zod-to-json-schema":"^3.24.6"},"keywords":["ai","llm","openai","anthropic","gemini","bedrock","unified","api"],"author":{"name":"Mario Zechner"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/TUstudents/pi-mono.git","directory":"packages/ai"},"engines":{"node":">=20.0.0"},"devDependencies":{"@types/node":"^24.3.0","canvas":"^3.2.0","vitest":"^3.2.4"},"_id":"@cargo-cult/pi-ai@0.47.0","gitHead":"639988d4bcbe25693775ce39cd905382eb58b7d4","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"homepage":"https://github.com/TUstudents/pi-mono#readme","_nodeVersion":"22.21.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-yePq7ii2EVS9SkLeLIizQy7VOy8hl6CcCoCgj+8tBxBXTMdN/RhDC8D9n7n84qytGL1O4Qi4xSeOlGjO+l439A==","shasum":"cddc74731e999c3d2bb23c9a025116e1b5d477aa","tarball":"https://registry.npmjs.org/@cargo-cult/pi-ai/-/pi-ai-0.47.0.tgz","fileCount":126,"unpackedSize":2800339,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-ai@0.47.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCBlJiugkpAWd0h8pWWe7v9yjgmZN5nvGfJsATMQRraSgIhAOAL+Alnjf5TiHJJs6ARG2NpgOEbutMEaIOzGA13N7so"}]},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"directories":{},"maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pi-ai_0.47.0_1768544529614_0.11030382498384816"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-09T18:34:11.070Z","modified":"2026-01-16T06:22:10.067Z","0.40.0":"2026-01-09T18:34:11.302Z","0.40.1":"2026-01-09T18:42:11.798Z","0.42.0":"2026-01-09T19:11:15.046Z","0.42.1":"2026-01-09T19:17:46.970Z","0.45.3":"2026-01-13T17:51:08.737Z","0.45.7":"2026-01-15T10:45:21.745Z","0.47.0":"2026-01-16T06:22:09.768Z"},"bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"author":{"name":"Mario Zechner"},"license":"MIT","homepage":"https://github.com/TUstudents/pi-mono#readme","keywords":["ai","llm","openai","anthropic","gemini","bedrock","unified","api"],"repository":{"type":"git","url":"git+https://github.com/TUstudents/pi-mono.git","directory":"packages/ai"},"description":"Unified LLM API with automatic model discovery and provider configuration","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"readme":"# @cargo-cult/pi-ai\n\nUnified LLM API with automatic model discovery, provider configuration, token and cost tracking, and simple context persistence and hand-off to other models mid-session.\n\n**Note**: This library only includes models that support tool calling (function calling), as this is essential for agentic workflows.\n\n## Table of Contents\n\n- [Supported Providers](#supported-providers)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Tools](#tools)\n  - [Defining Tools](#defining-tools)\n  - [Handling Tool Calls](#handling-tool-calls)\n  - [Streaming Tool Calls with Partial JSON](#streaming-tool-calls-with-partial-json)\n  - [Validating Tool Arguments](#validating-tool-arguments)\n  - [Complete Event Reference](#complete-event-reference)\n- [Image Input](#image-input)\n- [Thinking/Reasoning](#thinkingreasoning)\n  - [Unified Interface](#unified-interface-streamsimplecompletesimple)\n  - [Provider-Specific Options](#provider-specific-options-streamcomplete)\n  - [Streaming Thinking Content](#streaming-thinking-content)\n- [Stop Reasons](#stop-reasons)\n- [Error Handling](#error-handling)\n  - [Aborting Requests](#aborting-requests)\n  - [Continuing After Abort](#continuing-after-abort)\n- [APIs, Models, and Providers](#apis-models-and-providers)\n  - [Providers and Models](#providers-and-models)\n  - [Querying Providers and Models](#querying-providers-and-models)\n  - [Custom Models](#custom-models)\n  - [OpenAI Compatibility Settings](#openai-compatibility-settings)\n  - [Type Safety](#type-safety)\n- [Cross-Provider Handoffs](#cross-provider-handoffs)\n- [Context Serialization](#context-serialization)\n- [Browser Usage](#browser-usage)\n  - [Environment Variables](#environment-variables-nodejs-only)\n  - [Checking Environment Variables](#checking-environment-variables)\n- [OAuth Providers](#oauth-providers)\n  - [Vertex AI (ADC)](#vertex-ai-adc)\n  - [CLI Login](#cli-login)\n  - [Programmatic OAuth](#programmatic-oauth)\n  - [Login Flow Example](#login-flow-example)\n  - [Using OAuth Tokens](#using-oauth-tokens)\n  - [Provider Notes](#provider-notes)\n- [License](#license)\n\n## Supported Providers\n\n- **OpenAI**\n- **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)\n- **Anthropic**\n- **Google**\n- **Vertex AI** (Gemini via Vertex AI)\n- **Mistral**\n- **Groq**\n- **Cerebras**\n- **xAI**\n- **OpenRouter**\n- **Vercel AI Gateway**\n- **MiniMax**\n- **GitHub Copilot** (requires OAuth, see below)\n- **Google Gemini CLI** (requires OAuth, see below)\n- **Antigravity** (requires OAuth, see below)\n- **Amazon Bedrock**\n- **Any OpenAI-compatible API**: Ollama, vLLM, LM Studio, etc.\n\n## Installation\n\n```bash\nnpm install @cargo-cult/pi-ai\n```\n\n## Quick Start\n\n```typescript\nimport { Type, getModel, stream, complete, Context, Tool, StringEnum } from '@cargo-cult/pi-ai';\n\n// Fully typed with auto-complete support for both providers and models\nconst model = getModel('openai', 'gpt-4o-mini');\n\n// Define tools with TypeBox schemas for type safety and validation\nconst tools: Tool[] = [{\n  name: 'get_time',\n  description: 'Get the current time',\n  parameters: Type.Object({\n    timezone: Type.Optional(Type.String({ description: 'Optional timezone (e.g., America/New_York)' }))\n  })\n}];\n\n// Build a conversation context (easily serializable and transferable between models)\nconst context: Context = {\n  systemPrompt: 'You are a helpful assistant.',\n  messages: [{ role: 'user', content: 'What time is it?' }],\n  tools\n};\n\n// Option 1: Streaming with all event types\nconst s = stream(model, context);\n\nfor await (const event of s) {\n  switch (event.type) {\n    case 'start':\n      console.log(`Starting with ${event.partial.model}`);\n      break;\n    case 'text_start':\n      console.log('\\n[Text started]');\n      break;\n    case 'text_delta':\n      process.stdout.write(event.delta);\n      break;\n    case 'text_end':\n      console.log('\\n[Text ended]');\n      break;\n    case 'thinking_start':\n      console.log('[Model is thinking...]');\n      break;\n    case 'thinking_delta':\n      process.stdout.write(event.delta);\n      break;\n    case 'thinking_end':\n      console.log('[Thinking complete]');\n      break;\n    case 'toolcall_start':\n      console.log(`\\n[Tool call started: index ${event.contentIndex}]`);\n      break;\n    case 'toolcall_delta':\n      // Partial tool arguments are being streamed\n      const partialCall = event.partial.content[event.contentIndex];\n      if (partialCall.type === 'toolCall') {\n        console.log(`[Streaming args for ${partialCall.name}]`);\n      }\n      break;\n    case 'toolcall_end':\n      console.log(`\\nTool called: ${event.toolCall.name}`);\n      console.log(`Arguments: ${JSON.stringify(event.toolCall.arguments)}`);\n      break;\n    case 'done':\n      console.log(`\\nFinished: ${event.reason}`);\n      break;\n    case 'error':\n      console.error(`Error: ${event.error}`);\n      break;\n  }\n}\n\n// Get the final message after streaming, add it to the context\nconst finalMessage = await s.result();\ncontext.messages.push(finalMessage);\n\n// Handle tool calls if any\nconst toolCalls = finalMessage.content.filter(b => b.type === 'toolCall');\nfor (const call of toolCalls) {\n  // Execute the tool\n  const result = call.name === 'get_time'\n    ? new Date().toLocaleString('en-US', {\n        timeZone: call.arguments.timezone || 'UTC',\n        dateStyle: 'full',\n        timeStyle: 'long'\n      })\n    : 'Unknown tool';\n\n  // Add tool result to context (supports text and images)\n  context.messages.push({\n    role: 'toolResult',\n    toolCallId: call.id,\n    toolName: call.name,\n    content: [{ type: 'text', text: result }],\n    isError: false,\n    timestamp: Date.now()\n  });\n}\n\n// Continue if there were tool calls\nif (toolCalls.length > 0) {\n  const continuation = await complete(model, context);\n  context.messages.push(continuation);\n  console.log('After tool execution:', continuation.content);\n}\n\nconsole.log(`Total tokens: ${finalMessage.usage.input} in, ${finalMessage.usage.output} out`);\nconsole.log(`Cost: $${finalMessage.usage.cost.total.toFixed(4)}`);\n\n// Option 2: Get complete response without streaming\nconst response = await complete(model, context);\n\nfor (const block of response.content) {\n  if (block.type === 'text') {\n    console.log(block.text);\n  } else if (block.type === 'toolCall') {\n    console.log(`Tool: ${block.name}(${JSON.stringify(block.arguments)})`);\n  }\n}\n```\n\n## Tools\n\nTools enable LLMs to interact with external systems. This library uses TypeBox schemas for type-safe tool definitions with automatic validation using AJV. TypeBox schemas can be serialized and deserialized as plain JSON, making them ideal for distributed systems.\n\n### Defining Tools\n\n```typescript\nimport { Type, Tool, StringEnum } from '@cargo-cult/pi-ai';\n\n// Define tool parameters with TypeBox\nconst weatherTool: Tool = {\n  name: 'get_weather',\n  description: 'Get current weather for a location',\n  parameters: Type.Object({\n    location: Type.String({ description: 'City name or coordinates' }),\n    units: StringEnum(['celsius', 'fahrenheit'], { default: 'celsius' })\n  })\n};\n\n// Note: For Google API compatibility, use StringEnum helper instead of Type.Enum\n// Type.Enum generates anyOf/const patterns that Google doesn't support\n\nconst bookMeetingTool: Tool = {\n  name: 'book_meeting',\n  description: 'Schedule a meeting',\n  parameters: Type.Object({\n    title: Type.String({ minLength: 1 }),\n    startTime: Type.String({ format: 'date-time' }),\n    endTime: Type.String({ format: 'date-time' }),\n    attendees: Type.Array(Type.String({ format: 'email' }), { minItems: 1 })\n  })\n};\n```\n\n### Handling Tool Calls\n\nTool results use content blocks and can include both text and images:\n\n```typescript\nimport { readFileSync } from 'fs';\n\nconst context: Context = {\n  messages: [{ role: 'user', content: 'What is the weather in London?' }],\n  tools: [weatherTool]\n};\n\nconst response = await complete(model, context);\n\n// Check for tool calls in the response\nfor (const block of response.content) {\n  if (block.type === 'toolCall') {\n    // Execute your tool with the arguments\n    // See \"Validating Tool Arguments\" section for validation\n    const result = await executeWeatherApi(block.arguments);\n\n    // Add tool result with text content\n    context.messages.push({\n      role: 'toolResult',\n      toolCallId: block.id,\n      toolName: block.name,\n      content: [{ type: 'text', text: JSON.stringify(result) }],\n      isError: false,\n      timestamp: Date.now()\n    });\n  }\n}\n\n// Tool results can also include images (for vision-capable models)\nconst imageBuffer = readFileSync('chart.png');\ncontext.messages.push({\n  role: 'toolResult',\n  toolCallId: 'tool_xyz',\n  toolName: 'generate_chart',\n  content: [\n    { type: 'text', text: 'Generated chart showing temperature trends' },\n    { type: 'image', data: imageBuffer.toString('base64'), mimeType: 'image/png' }\n  ],\n  isError: false,\n  timestamp: Date.now()\n});\n```\n\n### Streaming Tool Calls with Partial JSON\n\nDuring streaming, tool call arguments are progressively parsed as they arrive. This enables real-time UI updates before the complete arguments are available:\n\n```typescript\nconst s = stream(model, context);\n\nfor await (const event of s) {\n  if (event.type === 'toolcall_delta') {\n    const toolCall = event.partial.content[event.contentIndex];\n\n    // toolCall.arguments contains partially parsed JSON during streaming\n    // This allows for progressive UI updates\n    if (toolCall.type === 'toolCall' && toolCall.arguments) {\n      // BE DEFENSIVE: arguments may be incomplete\n      // Example: Show file path being written even before content is complete\n      if (toolCall.name === 'write_file' && toolCall.arguments.path) {\n        console.log(`Writing to: ${toolCall.arguments.path}`);\n\n        // Content might be partial or missing\n        if (toolCall.arguments.content) {\n          console.log(`Content preview: ${toolCall.arguments.content.substring(0, 100)}...`);\n        }\n      }\n    }\n  }\n\n  if (event.type === 'toolcall_end') {\n    // Here toolCall.arguments is complete (but not yet validated)\n    const toolCall = event.toolCall;\n    console.log(`Tool completed: ${toolCall.name}`, toolCall.arguments);\n  }\n}\n```\n\n**Important notes about partial tool arguments:**\n- During `toolcall_delta` events, `arguments` contains the best-effort parse of partial JSON\n- Fields may be missing or incomplete - always check for existence before use\n- String values may be truncated mid-word\n- Arrays may be incomplete\n- Nested objects may be partially populated\n- At minimum, `arguments` will be an empty object `{}`, never `undefined`\n- The Google provider does not support function call streaming. Instead, you will receive a single `toolcall_delta` event with the full arguments.\n\n### Validating Tool Arguments\n\nWhen using `agentLoop`, tool arguments are automatically validated against your TypeBox schemas before execution. If validation fails, the error is returned to the model as a tool result, allowing it to retry.\n\nWhen implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:\n\n```typescript\nimport { stream, validateToolCall, Tool } from '@cargo-cult/pi-ai';\n\nconst tools: Tool[] = [weatherTool, calculatorTool];\nconst s = stream(model, { messages, tools });\n\nfor await (const event of s) {\n  if (event.type === 'toolcall_end') {\n    const toolCall = event.toolCall;\n\n    try {\n      // Validate arguments against the tool's schema (throws on invalid args)\n      const validatedArgs = validateToolCall(tools, toolCall);\n      const result = await executeMyTool(toolCall.name, validatedArgs);\n      // ... add tool result to context\n    } catch (error) {\n      // Validation failed - return error as tool result so model can retry\n      context.messages.push({\n        role: 'toolResult',\n        toolCallId: toolCall.id,\n        toolName: toolCall.name,\n        content: [{ type: 'text', text: error.message }],\n        isError: true,\n        timestamp: Date.now()\n      });\n    }\n  }\n}\n```\n\n### Complete Event Reference\n\nAll streaming events emitted during assistant message generation:\n\n| Event Type | Description | Key Properties |\n|------------|-------------|----------------|\n| `start` | Stream begins | `partial`: Initial assistant message structure |\n| `text_start` | Text block starts | `contentIndex`: Position in content array |\n| `text_delta` | Text chunk received | `delta`: New text, `contentIndex`: Position |\n| `text_end` | Text block complete | `content`: Full text, `contentIndex`: Position |\n| `thinking_start` | Thinking block starts | `contentIndex`: Position in content array |\n| `thinking_delta` | Thinking chunk received | `delta`: New text, `contentIndex`: Position |\n| `thinking_end` | Thinking block complete | `content`: Full thinking, `contentIndex`: Position |\n| `toolcall_start` | Tool call begins | `contentIndex`: Position in content array |\n| `toolcall_delta` | Tool arguments streaming | `delta`: JSON chunk, `partial.content[contentIndex].arguments`: Partial parsed args |\n| `toolcall_end` | Tool call complete | `toolCall`: Complete validated tool call with `id`, `name`, `arguments` |\n| `done` | Stream complete | `reason`: Stop reason (\"stop\", \"length\", \"toolUse\"), `message`: Final assistant message |\n| `error` | Error occurred | `reason`: Error type (\"error\" or \"aborted\"), `error`: AssistantMessage with partial content |\n\n## Image Input\n\nModels with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored.\n\n```typescript\nimport { readFileSync } from 'fs';\nimport { getModel, complete } from '@cargo-cult/pi-ai';\n\nconst model = getModel('openai', 'gpt-4o-mini');\n\n// Check if model supports images\nif (model.input.includes('image')) {\n  console.log('Model supports vision');\n}\n\nconst imageBuffer = readFileSync('image.png');\nconst base64Image = imageBuffer.toString('base64');\n\nconst response = await complete(model, {\n  messages: [{\n    role: 'user',\n    content: [\n      { type: 'text', text: 'What is in this image?' },\n      { type: 'image', data: base64Image, mimeType: 'image/png' }\n    ]\n  }]\n});\n\n// Access the response\nfor (const block of response.content) {\n  if (block.type === 'text') {\n    console.log(block.text);\n  }\n}\n```\n\n## Thinking/Reasoning\n\nMany models support thinking/reasoning capabilities where they can show their internal thought process. You can check if a model supports reasoning via the `reasoning` property. If you pass reasoning options to a non-reasoning model, they are silently ignored.\n\n### Unified Interface (streamSimple/completeSimple)\n\n```typescript\nimport { getModel, streamSimple, completeSimple } from '@cargo-cult/pi-ai';\n\n// Many models across providers support thinking/reasoning\nconst model = getModel('anthropic', 'claude-sonnet-4-20250514');\n// or getModel('openai', 'gpt-5-mini');\n// or getModel('google', 'gemini-2.5-flash');\n// or getModel('xai', 'grok-code-fast-1');\n// or getModel('groq', 'openai/gpt-oss-20b');\n// or getModel('cerebras', 'gpt-oss-120b');\n// or getModel('openrouter', 'z-ai/glm-4.5v');\n\n// Check if model supports reasoning\nif (model.reasoning) {\n  console.log('Model supports reasoning/thinking');\n}\n\n// Use the simplified reasoning option\nconst response = await completeSimple(model, {\n  messages: [{ role: 'user', content: 'Solve: 2x + 5 = 13' }]\n}, {\n  reasoning: 'medium'  // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to high on non-OpenAI providers)\n});\n\n// Access thinking and text blocks\nfor (const block of response.content) {\n  if (block.type === 'thinking') {\n    console.log('Thinking:', block.thinking);\n  } else if (block.type === 'text') {\n    console.log('Response:', block.text);\n  }\n}\n```\n\n### Provider-Specific Options (stream/complete)\n\nFor fine-grained control, use the provider-specific options:\n\n```typescript\nimport { getModel, complete } from '@cargo-cult/pi-ai';\n\n// OpenAI Reasoning (o1, o3, gpt-5)\nconst openaiModel = getModel('openai', 'gpt-5-mini');\nawait complete(openaiModel, context, {\n  reasoningEffort: 'medium',\n  reasoningSummary: 'detailed'  // OpenAI Responses API only\n});\n\n// Anthropic Thinking (Claude Sonnet 4)\nconst anthropicModel = getModel('anthropic', 'claude-sonnet-4-20250514');\nawait complete(anthropicModel, context, {\n  thinkingEnabled: true,\n  thinkingBudgetTokens: 8192  // Optional token limit\n});\n\n// Google Gemini Thinking\nconst googleModel = getModel('google', 'gemini-2.5-flash');\nawait complete(googleModel, context, {\n  thinking: {\n    enabled: true,\n    budgetTokens: 8192  // -1 for dynamic, 0 to disable\n  }\n});\n```\n\n### Streaming Thinking Content\n\nWhen streaming, thinking content is delivered through specific events:\n\n```typescript\nconst s = streamSimple(model, context, { reasoning: 'high' });\n\nfor await (const event of s) {\n  switch (event.type) {\n    case 'thinking_start':\n      console.log('[Model started thinking]');\n      break;\n    case 'thinking_delta':\n      process.stdout.write(event.delta);  // Stream thinking content\n      break;\n    case 'thinking_end':\n      console.log('\\n[Thinking complete]');\n      break;\n  }\n}\n```\n\n## Stop Reasons\n\nEvery `AssistantMessage` includes a `stopReason` field that indicates how the generation ended:\n\n- `\"stop\"` - Normal completion, the model finished its response\n- `\"length\"` - Output hit the maximum token limit\n- `\"toolUse\"` - Model is calling tools and expects tool results\n- `\"error\"` - An error occurred during generation\n- `\"aborted\"` - Request was cancelled via abort signal\n\n## Error Handling\n\nWhen a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event:\n\n```typescript\n// In streaming\nfor await (const event of stream) {\n  if (event.type === 'error') {\n    // event.reason is either \"error\" or \"aborted\"\n    // event.error is the AssistantMessage with partial content\n    console.error(`Error (${event.reason}):`, event.error.errorMessage);\n    console.log('Partial content:', event.error.content);\n  }\n}\n\n// The final message will have the error details\nconst message = await stream.result();\nif (message.stopReason === 'error' || message.stopReason === 'aborted') {\n  console.error('Request failed:', message.errorMessage);\n  // message.content contains any partial content received before the error\n  // message.usage contains partial token counts and costs\n}\n```\n\n### Aborting Requests\n\nThe abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`:\n\n```typescript\nimport { getModel, stream } from '@cargo-cult/pi-ai';\n\nconst model = getModel('openai', 'gpt-4o-mini');\nconst controller = new AbortController();\n\n// Abort after 2 seconds\nsetTimeout(() => controller.abort(), 2000);\n\nconst s = stream(model, {\n  messages: [{ role: 'user', content: 'Write a long story' }]\n}, {\n  signal: controller.signal\n});\n\nfor await (const event of s) {\n  if (event.type === 'text_delta') {\n    process.stdout.write(event.delta);\n  } else if (event.type === 'error') {\n    // event.reason tells you if it was \"error\" or \"aborted\"\n    console.log(`${event.reason === 'aborted' ? 'Aborted' : 'Error'}:`, event.error.errorMessage);\n  }\n}\n\n// Get results (may be partial if aborted)\nconst response = await s.result();\nif (response.stopReason === 'aborted') {\n  console.log('Request was aborted:', response.errorMessage);\n  console.log('Partial content received:', response.content);\n  console.log('Tokens used:', response.usage);\n}\n```\n\n### Continuing After Abort\n\nAborted messages can be added to the conversation context and continued in subsequent requests:\n\n```typescript\nconst context = {\n  messages: [\n    { role: 'user', content: 'Explain quantum computing in detail' }\n  ]\n};\n\n// First request gets aborted after 2 seconds\nconst controller1 = new AbortController();\nsetTimeout(() => controller1.abort(), 2000);\n\nconst partial = await complete(model, context, { signal: controller1.signal });\n\n// Add the partial response to context\ncontext.messages.push(partial);\ncontext.messages.push({ role: 'user', content: 'Please continue' });\n\n// Continue the conversation\nconst continuation = await complete(model, context);\n```\n\n## APIs, Models, and Providers\n\nThe library implements 4 API interfaces, each with its own streaming function and options:\n\n- **`anthropic-messages`**: Anthropic's Messages API (`streamAnthropic`, `AnthropicOptions`)\n- **`google-generative-ai`**: Google's Generative AI API (`streamGoogle`, `GoogleOptions`)\n- **`openai-completions`**: OpenAI's Chat Completions API (`streamOpenAICompletions`, `OpenAICompletionsOptions`)\n- **`openai-responses`**: OpenAI's Responses API (`streamOpenAIResponses`, `OpenAIResponsesOptions`)\n\n### Providers and Models\n\nA **provider** offers models through a specific API. For example:\n- **Anthropic** models use the `anthropic-messages` API\n- **Google** models use the `google-generative-ai` API\n- **OpenAI** models use the `openai-responses` API\n- **Mistral, xAI, Cerebras, Groq, etc.** models use the `openai-completions` API (OpenAI-compatible)\n\n### Querying Providers and Models\n\n```typescript\nimport { getProviders, getModels, getModel } from '@cargo-cult/pi-ai';\n\n// Get all available providers\nconst providers = getProviders();\nconsole.log(providers); // ['openai', 'anthropic', 'google', 'xai', 'groq', ...]\n\n// Get all models from a provider (fully typed)\nconst anthropicModels = getModels('anthropic');\nfor (const model of anthropicModels) {\n  console.log(`${model.id}: ${model.name}`);\n  console.log(`  API: ${model.api}`); // 'anthropic-messages'\n  console.log(`  Context: ${model.contextWindow} tokens`);\n  console.log(`  Vision: ${model.input.includes('image')}`);\n  console.log(`  Reasoning: ${model.reasoning}`);\n}\n\n// Get a specific model (both provider and model ID are auto-completed in IDEs)\nconst model = getModel('openai', 'gpt-4o-mini');\nconsole.log(`Using ${model.name} via ${model.api} API`);\n```\n\n### Custom Models\n\nYou can create custom models for local inference servers or custom endpoints:\n\n```typescript\nimport { Model, stream } from '@cargo-cult/pi-ai';\n\n// Example: Ollama using OpenAI-compatible API\nconst ollamaModel: Model<'openai-completions'> = {\n  id: 'llama-3.1-8b',\n  name: 'Llama 3.1 8B (Ollama)',\n  api: 'openai-completions',\n  provider: 'ollama',\n  baseUrl: 'http://localhost:11434/v1',\n  reasoning: false,\n  input: ['text'],\n  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n  contextWindow: 128000,\n  maxTokens: 32000\n};\n\n// Example: LiteLLM proxy with explicit compat settings\nconst litellmModel: Model<'openai-completions'> = {\n  id: 'gpt-4o',\n  name: 'GPT-4o (via LiteLLM)',\n  api: 'openai-completions',\n  provider: 'litellm',\n  baseUrl: 'http://localhost:4000/v1',\n  reasoning: false,\n  input: ['text', 'image'],\n  cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },\n  contextWindow: 128000,\n  maxTokens: 16384,\n  compat: {\n    supportsStore: false,  // LiteLLM doesn't support the store field\n  }\n};\n\n// Example: Custom endpoint with headers (bypassing Cloudflare bot detection)\nconst proxyModel: Model<'anthropic-messages'> = {\n  id: 'claude-sonnet-4',\n  name: 'Claude Sonnet 4 (Proxied)',\n  api: 'anthropic-messages',\n  provider: 'custom-proxy',\n  baseUrl: 'https://proxy.example.com/v1',\n  reasoning: true,\n  input: ['text', 'image'],\n  cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },\n  contextWindow: 200000,\n  maxTokens: 8192,\n  headers: {\n    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',\n    'X-Custom-Auth': 'bearer-token-here'\n  }\n};\n\n// Use the custom model\nconst response = await stream(ollamaModel, context, {\n  apiKey: 'dummy' // Ollama doesn't need a real key\n});\n```\n\n### OpenAI Compatibility Settings\n\nThe `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for known providers (Cerebras, xAI, Mistral, Chutes, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field:\n\n```typescript\ninterface OpenAICompat {\n  supportsStore?: boolean;           // Whether provider supports the `store` field (default: true)\n  supportsDeveloperRole?: boolean;   // Whether provider supports `developer` role vs `system` (default: true)\n  supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)\n  maxTokensField?: 'max_completion_tokens' | 'max_tokens';  // Which field name to use (default: max_completion_tokens)\n  thinkingFormat?: 'openai' | 'zai'; // Format for reasoning param: 'openai' uses reasoning_effort, 'zai' uses thinking: { type: \"enabled\" } (default: openai)\n}\n```\n\nIf `compat` is not set, the library falls back to URL-based detection. If `compat` is partially set, unspecified fields use the detected defaults. This is useful for:\n\n- **LiteLLM proxies**: May not support `store` field\n- **Custom inference servers**: May use non-standard field names\n- **Self-hosted endpoints**: May have different feature support\n\n### Type Safety\n\nModels are typed by their API, ensuring type-safe options:\n\n```typescript\n// TypeScript knows this is an Anthropic model\nconst claude = getModel('anthropic', 'claude-sonnet-4-20250514');\n\n// So these options are type-checked for AnthropicOptions\nawait stream(claude, context, {\n  thinkingEnabled: true,      // ✓ Valid for anthropic-messages\n  thinkingBudgetTokens: 2048, // ✓ Valid for anthropic-messages\n  // reasoningEffort: 'high'  // ✗ TypeScript error: not valid for anthropic-messages\n});\n```\n\n## Cross-Provider Handoffs\n\nThe library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including thinking blocks, tool calls, and tool results.\n\n### How It Works\n\nWhen messages from one provider are sent to a different provider, the library automatically transforms them for compatibility:\n\n- **User and tool result messages** are passed through unchanged\n- **Assistant messages from the same provider/API** are preserved as-is\n- **Assistant messages from different providers** have their thinking blocks converted to text with `<thinking>` tags\n- **Tool calls and regular text** are preserved unchanged\n\n### Example: Multi-Provider Conversation\n\n```typescript\nimport { getModel, complete, Context } from '@cargo-cult/pi-ai';\n\n// Start with Claude\nconst claude = getModel('anthropic', 'claude-sonnet-4-20250514');\nconst context: Context = {\n  messages: []\n};\n\ncontext.messages.push({ role: 'user', content: 'What is 25 * 18?' });\nconst claudeResponse = await complete(claude, context, {\n  thinkingEnabled: true\n});\ncontext.messages.push(claudeResponse);\n\n// Switch to GPT-5 - it will see Claude's thinking as <thinking> tagged text\nconst gpt5 = getModel('openai', 'gpt-5-mini');\ncontext.messages.push({ role: 'user', content: 'Is that calculation correct?' });\nconst gptResponse = await complete(gpt5, context);\ncontext.messages.push(gptResponse);\n\n// Switch to Gemini\nconst gemini = getModel('google', 'gemini-2.5-flash');\ncontext.messages.push({ role: 'user', content: 'What was the original question?' });\nconst geminiResponse = await complete(gemini, context);\n```\n\n### Provider Compatibility\n\nAll providers can handle messages from other providers, including:\n- Text content\n- Tool calls and tool results (including images in tool results)\n- Thinking/reasoning blocks (transformed to tagged text for cross-provider compatibility)\n- Aborted messages with partial content\n\nThis enables flexible workflows where you can:\n- Start with a fast model for initial responses\n- Switch to a more capable model for complex reasoning\n- Use specialized models for specific tasks\n- Maintain conversation continuity across provider outages\n\n## Context Serialization\n\nThe `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:\n\n```typescript\nimport { Context, getModel, complete } from '@cargo-cult/pi-ai';\n\n// Create and use a context\nconst context: Context = {\n  systemPrompt: 'You are a helpful assistant.',\n  messages: [\n    { role: 'user', content: 'What is TypeScript?' }\n  ]\n};\n\nconst model = getModel('openai', 'gpt-4o-mini');\nconst response = await complete(model, context);\ncontext.messages.push(response);\n\n// Serialize the entire context\nconst serialized = JSON.stringify(context);\nconsole.log('Serialized context size:', serialized.length, 'bytes');\n\n// Save to database, localStorage, file, etc.\nlocalStorage.setItem('conversation', serialized);\n\n// Later: deserialize and continue the conversation\nconst restored: Context = JSON.parse(localStorage.getItem('conversation')!);\nrestored.messages.push({ role: 'user', content: 'Tell me more about its type system' });\n\n// Continue with any model\nconst newModel = getModel('anthropic', 'claude-3-5-haiku-20241022');\nconst continuation = await complete(newModel, restored);\n```\n\n> **Note**: If the context contains images (encoded as base64 as shown in the Image Input section), those will also be serialized.\n\n## Browser Usage\n\nThe library supports browser environments. You must pass the API key explicitly since environment variables are not available in browsers:\n\n```typescript\nimport { getModel, complete } from '@cargo-cult/pi-ai';\n\n// API key must be passed explicitly in browser\nconst model = getModel('anthropic', 'claude-3-5-haiku-20241022');\n\nconst response = await complete(model, {\n  messages: [{ role: 'user', content: 'Hello!' }]\n}, {\n  apiKey: 'your-api-key'\n});\n```\n\n> **Security Warning**: Exposing API keys in frontend code is dangerous. Anyone can extract and abuse your keys. Only use this approach for internal tools or demos. For production applications, use a backend proxy that keeps your API keys secure.\n\n### Environment Variables (Node.js only)\n\nIn Node.js environments, you can set environment variables to avoid passing API keys:\n\n| Provider | Environment Variable(s) |\n|----------|------------------------|\n| OpenAI | `OPENAI_API_KEY` |\n| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |\n| Google | `GEMINI_API_KEY` |\n| Vertex AI | `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |\n| Mistral | `MISTRAL_API_KEY` |\n| Groq | `GROQ_API_KEY` |\n| Cerebras | `CEREBRAS_API_KEY` |\n| xAI | `XAI_API_KEY` |\n| OpenRouter | `OPENROUTER_API_KEY` |\n| Vercel AI Gateway | `AI_GATEWAY_API_KEY` |\n| zAI | `ZAI_API_KEY` |\n| MiniMax | `MINIMAX_API_KEY` |\n| GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` |\n\nWhen set, the library automatically uses these keys:\n\n```typescript\n// Uses OPENAI_API_KEY from environment\nconst model = getModel('openai', 'gpt-4o-mini');\nconst response = await complete(model, context);\n\n// Or override with explicit key\nconst response = await complete(model, context, {\n  apiKey: 'sk-different-key'\n});\n```\n\n### Checking Environment Variables\n\n```typescript\nimport { getEnvApiKey } from '@cargo-cult/pi-ai';\n\n// Check if an API key is set in environment variables\nconst key = getEnvApiKey('openai');  // checks OPENAI_API_KEY\n```\n\n## OAuth Providers\n\nSeveral providers require OAuth authentication instead of static API keys:\n\n- **Anthropic** (Claude Pro/Max subscription)\n- **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)\n- **GitHub Copilot** (Copilot subscription)\n- **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription)\n- **Antigravity** (Free Gemini 3, Claude, GPT-OSS via Google Cloud)\n\nFor paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID.\n\n### Vertex AI (ADC)\n\nVertex AI models use Application Default Credentials (ADC):\n\n- **Local development**: Run `gcloud auth application-default login`\n- **CI/Production**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to a service account JSON key file\n\nAlso set `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) and `GOOGLE_CLOUD_LOCATION`. You can also pass `project`/`location` in the call options.\n\nExample:\n\n```bash\n# Local (uses your user credentials)\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=\"my-project\"\nexport GOOGLE_CLOUD_LOCATION=\"us-central1\"\n\n# CI/Production (service account key file)\nexport GOOGLE_APPLICATION_CREDENTIALS=\"/path/to/service-account.json\"\n```\n\n```typescript\nimport { getModel, complete } from '@cargo-cult/pi-ai';\n\n(async () => {\n  const model = getModel('google-vertex', 'gemini-2.5-flash');\n  const response = await complete(model, {\n    messages: [{ role: 'user', content: 'Hello from Vertex AI' }]\n  });\n\n  for (const block of response.content) {\n    if (block.type === 'text') console.log(block.text);\n  }\n})().catch(console.error);\n```\n\nOfficial docs: [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials)\n\n### CLI Login\n\nThe quickest way to authenticate:\n\n```bash\nnpx @cargo-cult/pi-ai login              # interactive provider selection\nnpx @cargo-cult/pi-ai login anthropic    # login to specific provider\nnpx @cargo-cult/pi-ai list               # list available providers\n```\n\nCredentials are saved to `auth.json` in the current directory.\n\n### Programmatic OAuth\n\nThe library provides login and token refresh functions. Credential storage is the caller's responsibility.\n\n```typescript\nimport {\n  // Login functions (return credentials, do not store)\n  loginAnthropic,\n  loginOpenAICodex,\n  loginGitHubCopilot,\n  loginGeminiCli,\n  loginAntigravity,\n\n  // Token management\n  refreshOAuthToken,   // (provider, credentials) => new credentials\n  getOAuthApiKey,      // (provider, credentialsMap) => { newCredentials, apiKey } | null\n\n  // Types\n  type OAuthProvider,  // 'anthropic' | 'openai-codex' | 'github-copilot' | 'google-gemini-cli' | 'google-antigravity'\n  type OAuthCredentials,\n} from '@cargo-cult/pi-ai';\n```\n\n### Login Flow Example\n\n```typescript\nimport { loginGitHubCopilot } from '@cargo-cult/pi-ai';\nimport { writeFileSync } from 'fs';\n\nconst credentials = await loginGitHubCopilot({\n  onAuth: (url, instructions) => {\n    console.log(`Open: ${url}`);\n    if (instructions) console.log(instructions);\n  },\n  onPrompt: async (prompt) => {\n    return await getUserInput(prompt.message);\n  },\n  onProgress: (message) => console.log(message)\n});\n\n// Store credentials yourself\nconst auth = { 'github-copilot': { type: 'oauth', ...credentials } };\nwriteFileSync('auth.json', JSON.stringify(auth, null, 2));\n```\n\n### Using OAuth Tokens\n\nUse `getOAuthApiKey()` to get an API key, automatically refreshing if expired:\n\n```typescript\nimport { getModel, complete, getOAuthApiKey } from '@cargo-cult/pi-ai';\nimport { readFileSync, writeFileSync } from 'fs';\n\n// Load your stored credentials\nconst auth = JSON.parse(readFileSync('auth.json', 'utf-8'));\n\n// Get API key (refreshes if expired)\nconst result = await getOAuthApiKey('github-copilot', auth);\nif (!result) throw new Error('Not logged in');\n\n// Save refreshed credentials\nauth['github-copilot'] = { type: 'oauth', ...result.newCredentials };\nwriteFileSync('auth.json', JSON.stringify(auth, null, 2));\n\n// Use the API key\nconst model = getModel('github-copilot', 'gpt-4o');\nconst response = await complete(model, {\n  messages: [{ role: 'user', content: 'Hello!' }]\n}, { apiKey: result.apiKey });\n```\n\n### Provider Notes\n\n**OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options.\n\n**GitHub Copilot**: If you get \"The requested model is not supported\" error, enable the model manually in VS Code: open Copilot Chat, click the model selector, select the model (warning icon), and click \"Enable\".\n\n**Google Gemini CLI / Antigravity**: These use Google Cloud OAuth. The `apiKey` returned by `getOAuthApiKey()` is a JSON string containing both the token and project ID, which the library handles automatically.\n\n## Development\n\n### Adding a New Provider\n\nAdding a new LLM provider requires changes across multiple files. This checklist covers all necessary steps:\n\n#### 1. Core Types (`src/types.ts`)\n\n- Add the API identifier to the `Api` type union (e.g., `\"bedrock-converse-stream\"`)\n- Create an options interface extending `StreamOptions` (e.g., `BedrockOptions`)\n- Add the mapping to `ApiOptionsMap`\n- Add the provider name to `KnownProvider` type union (e.g., `\"amazon-bedrock\"`)\n\n#### 2. Provider Implementation (`src/providers/`)\n\nCreate a new provider file (e.g., `amazon-bedrock.ts`) that exports:\n\n- `stream<Provider>()` function returning `AssistantMessageEventStream`\n- Provider-specific options interface\n- Message conversion functions to transform `Context` to provider format\n- Tool conversion if the provider supports tools\n- Response parsing to emit standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`)\n\n#### 3. Stream Integration (`src/stream.ts`)\n\n- Import the provider's stream function and options type\n- Add credential detection in `getEnvApiKey()` for the new provider\n- Add a case in `mapOptionsForApi()` to map `SimpleStreamOptions` to provider options\n- Add the provider's stream function to the `streamFunctions` map\n\n#### 4. Model Generation (`scripts/generate-models.ts`)\n\n- Add logic to fetch and parse models from the provider's source (e.g., models.dev API)\n- Map provider model data to the standardized `Model` interface\n- Handle provider-specific quirks (pricing format, capability flags, model ID transformations)\n\n#### 5. Tests (`test/`)\n\nCreate or update test files to cover the new provider:\n\n- `stream.test.ts` - Basic streaming and tool use\n- `tokens.test.ts` - Token usage reporting\n- `abort.test.ts` - Request cancellation\n- `empty.test.ts` - Empty message handling\n- `context-overflow.test.ts` - Context limit errors\n- `image-limits.test.ts` - Image support (if applicable)\n- `unicode-surrogate.test.ts` - Unicode handling\n- `tool-call-without-result.test.ts` - Orphaned tool calls\n- `image-tool-result.test.ts` - Images in tool results\n- `total-tokens.test.ts` - Token counting accuracy\n\nFor providers with non-standard auth (AWS, Google Vertex), create a utility like `bedrock-utils.ts` with credential detection helpers.\n\n#### 6. Coding Agent Integration (`../coding-agent/`)\n\nUpdate `src/core/model-resolver.ts`:\n\n- Add a default model ID for the provider in `DEFAULT_MODELS`\n\nUpdate `src/cli/args.ts`:\n\n- Add environment variable documentation in the help text\n\nUpdate `README.md`:\n\n- Add the provider to the providers section with setup instructions\n\n#### 7. Documentation\n\nUpdate `packages/ai/README.md`:\n\n- Add to the Supported Providers table\n- Document any provider-specific options or authentication requirements\n- Add environment variable to the Environment Variables section\n\n#### 8. Changelog\n\nAdd an entry to `packages/ai/CHANGELOG.md` under `## [Unreleased]`:\n\n```markdown\n### Added\n- Added support for [Provider Name] provider ([#PR](link) by [@author](link))\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}