{"_id":"@awfixerai/ai","name":"@awfixerai/ai","dist-tags":{"latest":"0.0.1-rc.1"},"versions":{"0.0.1-rc.1":{"type":"module","name":"@awfixerai/ai","version":"0.0.1-rc.1","description":"Unified LLM API with automatic model discovery and provider configuration","homepage":"https://agent.awfixer.codes","license":"MIT","repository":{"type":"git","url":"git+https://github.com/awfixers-stuff/awfixer-agent.git","directory":"packages/ai"},"bugs":{"url":"https://github.com/awfixers-stuff/awfixer-agent/issues"},"keywords":["ai","llm","openai","anthropic","gemini","unified","api"],"main":"./src/index.ts","types":"./src/index.ts","scripts":{"check":"biome check . && bun run check:types","check:types":"tsgo -p tsconfig.json --noEmit","lint":"biome lint .","test":"bun test --parallel","fix":"biome check --write --unsafe .","fmt":"biome format --write ."},"dependencies":{"@bufbuild/protobuf":"^2.12.0","@awfixerai/catalog":"0.0.1-rc.1","@awfixerai/utils":"0.0.1-rc.1","@awfixerai/wire":"0.0.1-rc.1","arktype":"^2.2.0","zod":"^4"},"devDependencies":{"@bufbuild/protoc-gen-es":"^2.12.0","@types/bun":"^1.3.14"},"engines":{"bun":">=1.3.14"},"exports":{".":{"types":"./src/index.ts","import":"./src/index.ts"},"./error":{"types":"./src/error/index.ts","import":"./src/error/index.ts"},"./*":{"types":"./src/*.ts","import":"./src/*.ts"},"./auth-broker":{"types":"./src/auth-broker/index.ts","import":"./src/auth-broker/index.ts"},"./auth-broker/*":{"types":"./src/auth-broker/*.ts","import":"./src/auth-broker/*.ts"},"./auth-gateway":{"types":"./src/auth-gateway/index.ts","import":"./src/auth-gateway/index.ts"},"./auth-gateway/*":{"types":"./src/auth-gateway/*.ts","import":"./src/auth-gateway/*.ts"},"./providers/*":{"types":"./src/providers/*.ts","import":"./src/providers/*.ts"},"./providers/openai-codex/*":{"types":"./src/providers/openai-codex/*.ts","import":"./src/providers/openai-codex/*.ts"},"./usage/*":{"types":"./src/usage/*.ts","import":"./src/usage/*.ts"},"./utils/harmony-leak":{"types":"./src/utils/harmony-leak.ts","import":"./src/utils/harmony-leak.ts"},"./dialect":{"types":"./src/dialect/index.ts","import":"./src/dialect/index.ts"},"./utils/*":{"types":"./src/utils/*.ts","import":"./src/utils/*.ts"},"./oauth":{"types":"./src/registry/oauth/index.ts","import":"./src/registry/oauth/index.ts"},"./oauth/*":{"types":"./src/registry/oauth/*.ts","import":"./src/registry/oauth/*.ts"},"./registry":{"types":"./src/registry/index.ts","import":"./src/registry/index.ts"},"./registry/oauth":{"types":"./src/registry/oauth/index.ts","import":"./src/registry/oauth/index.ts"},"./utils/schema":{"types":"./src/utils/schema/index.ts","import":"./src/utils/schema/index.ts"},"./utils/schema/*":{"types":"./src/utils/schema/*.ts","import":"./src/utils/schema/*.ts"},"./*.js":"./src/*.ts"},"_id":"@awfixerai/ai@0.0.1-rc.1","_integrity":"sha512-ApIwBrBVp9udEXeAE0kxNtHdjSZAg1YQtp8AWP95BeHMkM7+UfQ7wCyl3zTWM3n9CDL+bxlUXanMmKNm99dpCw==","_nodeVersion":"24.3.0","_npmVersion":"10.8.3","shasum":"670b517b8c82454dc98d39160baceff041db702f","dist":{"integrity":"sha512-ApIwBrBVp9udEXeAE0kxNtHdjSZAg1YQtp8AWP95BeHMkM7+UfQ7wCyl3zTWM3n9CDL+bxlUXanMmKNm99dpCw==","shasum":"670b517b8c82454dc98d39160baceff041db702f","tarball":"https://registry.npmjs.org/@awfixerai/ai/-/ai-0.0.1-rc.1.tgz","fileCount":331,"unpackedSize":4107725,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCpI0oLPPblP5oF9chzu3jXGk+lmpE2E9R0gQ7GbHIL5QIgeXuhosB9QXk5OAq+Y3WLyZSJPVNpO50Ghz5dxLvdDII="}]},"_npmUser":{"name":"awfixer","email":"wise.jet2897@fastmail.com"},"directories":{},"maintainers":[{"name":"awfixer","email":"wise.jet2897@fastmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ai_0.0.1-rc.1_1783014933388_0.9028127506875943"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-02T17:55:33.241Z","0.0.1-rc.1":"2026-07-02T17:55:33.580Z","modified":"2026-07-02T17:55:33.816Z"},"maintainers":[{"name":"awfixer","email":"wise.jet2897@fastmail.com"}],"description":"Unified LLM API with automatic model discovery and provider configuration","homepage":"https://agent.awfixer.codes","keywords":["ai","llm","openai","anthropic","gemini","unified","api"],"repository":{"type":"git","url":"git+https://github.com/awfixers-stuff/awfixer-agent.git","directory":"packages/ai"},"bugs":{"url":"https://github.com/awfixers-stuff/awfixer-agent/issues"},"license":"MIT","readme":"# @awfixerai/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- **Together**\n- **Moonshot** (requires `MOONSHOT_API_KEY`)\n- **Qianfan** (requires `QIANFAN_API_KEY`)\n- **NVIDIA** (requires `NVIDIA_API_KEY`)\n- **NanoGPT** (requires `NANO_GPT_API_KEY`)\n- **Hugging Face Inference**\n- **xAI**\n- **Venice** (requires `VENICE_API_KEY`)\n- **Wafer Serverless** (requires `WAFER_SERVERLESS_API_KEY`; pay-as-you-go)\n- **OpenRouter**\n- **Kilo Gateway** (supports OAuth `/login kilo` or `KILO_API_KEY`)\n- **LiteLLM** (requires `LITELLM_API_KEY`)\n- **zAI** (requires `ZAI_API_KEY`)\n- **Umans AI Coding Plan** (supports `/login umans` or `UMANS_AI_CODING_PLAN_API_KEY`)\n- **MiniMax Token Plan** (requires `MINIMAX_CODE_API_KEY` or `MINIMAX_CODE_CN_API_KEY`)\n- **Xiaomi MiMo** (requires `XIAOMI_API_KEY`)\n- **ZenMux** (requires `ZENMUX_API_KEY`)\n- **Qwen Portal** (supports `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY`)\n- **Cloudflare AI Gateway** (requires `CLOUDFLARE_AI_GATEWAY_API_KEY` and provider-specific gateway base URL)\n- **Ollama** (local OpenAI-compatible runtime; optional `OLLAMA_API_KEY`)\n- **Ollama Cloud** (hosted native Ollama API; requires `OLLAMA_CLOUD_API_KEY`)\n- **llama.cpp** (local OpenAI and Anthropic compatible inference server)\n- **vLLM** (OpenAI-compatible server; `VLLM_API_KEY` for secured deployments)\n- **GitHub Copilot** (requires OAuth, see below)\n- **Google Gemini CLI** (requires OAuth, see below)\n- **Antigravity** (requires OAuth, see below)\n- **Any OpenAI-compatible API**: LM Studio, custom proxies, etc.\n\n## Installation\n\n```bash\nnpm install @awfixerai/ai\n```\n\n## Quick Start\n\n```typescript\nimport { z, getModel, stream, complete, Context, Tool } from \"@awfixerai/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 Zod schemas for type safety and validation\nconst tools: Tool[] = [\n\t{\n\t\tname: \"get_time\",\n\t\tdescription: \"Get the current time\",\n\t\tparameters: z.object({\n\t\t\ttimezone: z\n\t\t\t\t.string()\n\t\t\t\t.optional()\n\t\t\t\t.describe(\"Optional timezone (e.g., America/New_York)\"),\n\t\t}),\n\t},\n];\n\n// Build a conversation context (easily serializable and transferable between models)\nconst context: Context = {\n\tsystemPrompt: [\"You are a helpful assistant.\"],\n\tmessages: [{ role: \"user\", content: \"What time is it?\" }],\n\ttools,\n};\n\n// Option 1: Streaming with all event types\nconst s = stream(model, context);\n\nfor await (const event of s) {\n\tswitch (event.type) {\n\t\tcase \"start\":\n\t\t\tconsole.log(`Starting with ${event.partial.model}`);\n\t\t\tbreak;\n\t\tcase \"text_start\":\n\t\t\tconsole.log(\"\\n[Text started]\");\n\t\t\tbreak;\n\t\tcase \"text_delta\":\n\t\t\tprocess.stdout.write(event.delta);\n\t\t\tbreak;\n\t\tcase \"text_end\":\n\t\t\tconsole.log(\"\\n[Text ended]\");\n\t\t\tbreak;\n\t\tcase \"thinking_start\":\n\t\t\tconsole.log(\"[Model is thinking...]\");\n\t\t\tbreak;\n\t\tcase \"thinking_delta\":\n\t\t\tprocess.stdout.write(event.delta);\n\t\t\tbreak;\n\t\tcase \"thinking_end\":\n\t\t\tconsole.log(\"[Thinking complete]\");\n\t\t\tbreak;\n\t\tcase \"toolcall_start\":\n\t\t\tconsole.log(`\\n[Tool call started: index ${event.contentIndex}]`);\n\t\t\tbreak;\n\t\tcase \"toolcall_delta\":\n\t\t\t// Partial tool arguments are being streamed\n\t\t\tconst partialCall = event.partial.content[event.contentIndex];\n\t\t\tif (partialCall.type === \"toolCall\") {\n\t\t\t\tconsole.log(`[Streaming args for ${partialCall.name}]`);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"toolcall_end\":\n\t\t\tconsole.log(`\\nTool called: ${event.toolCall.name}`);\n\t\t\tconsole.log(`Arguments: ${JSON.stringify(event.toolCall.arguments)}`);\n\t\t\tbreak;\n\t\tcase \"done\":\n\t\t\tconsole.log(`\\nFinished: ${event.reason}`);\n\t\t\tbreak;\n\t\tcase \"error\":\n\t\t\tconsole.error(`Error: ${event.error}`);\n\t\t\tbreak;\n\t}\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\t// Execute the tool\n\tconst result =\n\t\tcall.name === \"get_time\"\n\t\t\t? new Date().toLocaleString(\"en-US\", {\n\t\t\t\t\ttimeZone: call.arguments.timezone || \"UTC\",\n\t\t\t\t\tdateStyle: \"full\",\n\t\t\t\t\ttimeStyle: \"long\",\n\t\t\t\t})\n\t\t\t: \"Unknown tool\";\n\n\t// Add tool result to context (supports text and images)\n\tcontext.messages.push({\n\t\trole: \"toolResult\",\n\t\ttoolCallId: call.id,\n\t\ttoolName: call.name,\n\t\tcontent: [{ type: \"text\", text: result }],\n\t\tisError: false,\n\t\ttimestamp: Date.now(),\n\t});\n}\n\n// Continue if there were tool calls\nif (toolCalls.length > 0) {\n\tconst continuation = await complete(model, context);\n\tcontext.messages.push(continuation);\n\tconsole.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\tif (block.type === \"text\") {\n\t\tconsole.log(block.text);\n\t} else if (block.type === \"toolCall\") {\n\t\tconsole.log(`Tool: ${block.name}(${JSON.stringify(block.arguments)})`);\n\t}\n}\n```\n\n## Tools\n\nTools enable LLMs to interact with external systems. This library uses **Zod** schemas for type-safe tool definitions with automatic validation. Schemas are converted to JSON Schema for providers as needed.\n\n### Defining Tools\n\n```typescript\nimport { z, Tool } from \"@awfixerai/ai\";\n\n// Define tool parameters with Zod\nconst weatherTool: Tool = {\n\tname: \"get_weather\",\n\tdescription: \"Get current weather for a location\",\n\tparameters: z.object({\n\t\tlocation: z.string().describe(\"City name or coordinates\"),\n\t\tunits: z.enum([\"celsius\", \"fahrenheit\"]).default(\"celsius\"),\n\t}),\n};\n\nconst bookMeetingTool: Tool = {\n\tname: \"book_meeting\",\n\tdescription: \"Schedule a meeting\",\n\tparameters: z.object({\n\t\ttitle: z.string().min(1),\n\t\tstartTime: z.string().describe(\"ISO 8601 date-time\"),\n\t\tendTime: z.string().describe(\"ISO 8601 date-time\"),\n\t\tattendees: z.array(z.email()).min(1),\n\t}),\n};\n```\n\n### Handling Tool Calls\n\nTool results use content blocks and can include both text and images:\n\n```typescript\nimport * as fs from \"node:fs\";\n\nconst context: Context = {\n\tmessages: [{ role: \"user\", content: \"What is the weather in London?\" }],\n\ttools: [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\tif (block.type === \"toolCall\") {\n\t\t// Execute your tool with the arguments\n\t\t// See \"Validating Tool Arguments\" section for validation\n\t\tconst result = await executeWeatherApi(block.arguments);\n\n\t\t// Add tool result with text content\n\t\tcontext.messages.push({\n\t\t\trole: \"toolResult\",\n\t\t\ttoolCallId: block.id,\n\t\t\ttoolName: block.name,\n\t\t\tcontent: [{ type: \"text\", text: JSON.stringify(result) }],\n\t\t\tisError: false,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n}\n\n// Tool results can also include images (for vision-capable models)\nconst imageBuffer = fs.readFileSync(\"chart.png\");\ncontext.messages.push({\n\trole: \"toolResult\",\n\ttoolCallId: \"tool_xyz\",\n\ttoolName: \"generate_chart\",\n\tcontent: [\n\t\t{ type: \"text\", text: \"Generated chart showing temperature trends\" },\n\t\t{ type: \"image\", data: imageBuffer.toBase64(), mimeType: \"image/png\" },\n\t],\n\tisError: false,\n\ttimestamp: 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\tif (event.type === \"toolcall_delta\") {\n\t\tconst toolCall = event.partial.content[event.contentIndex];\n\n\t\t// toolCall.arguments contains partially parsed JSON during streaming\n\t\t// This allows for progressive UI updates\n\t\tif (toolCall.type === \"toolCall\" && toolCall.arguments) {\n\t\t\t// BE DEFENSIVE: arguments may be incomplete\n\t\t\t// Example: Show file path being written even before content is complete\n\t\t\tif (toolCall.name === \"write_file\" && toolCall.arguments.path) {\n\t\t\t\tconsole.log(`Writing to: ${toolCall.arguments.path}`);\n\n\t\t\t\t// Content might be partial or missing\n\t\t\t\tif (toolCall.arguments.content) {\n\t\t\t\t\tconsole.log(`Content preview: ${toolCall.arguments.content.substring(0, 100)}...`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (event.type === \"toolcall_end\") {\n\t\t// Here toolCall.arguments is complete (but not yet validated)\n\t\tconst toolCall = event.toolCall;\n\t\tconsole.log(`Tool completed: ${toolCall.name}`, toolCall.arguments);\n\t}\n}\n```\n\n**Important notes about partial tool arguments:**\n\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 Zod parameter 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 \"@awfixerai/ai\";\n\nconst tools: Tool[] = [weatherTool, calculatorTool];\nconst s = stream(model, { messages, tools });\n\nfor await (const event of s) {\n\tif (event.type === \"toolcall_end\") {\n\t\tconst toolCall = event.toolCall;\n\n\t\ttry {\n\t\t\t// Validate arguments against the tool's schema (throws on invalid args)\n\t\t\tconst validatedArgs = validateToolCall(tools, toolCall);\n\t\t\tconst result = await executeMyTool(toolCall.name, validatedArgs);\n\t\t\t// ... add tool result to context\n\t\t} catch (error) {\n\t\t\t// Validation failed - return error as tool result so model can retry\n\t\t\tcontext.messages.push({\n\t\t\t\trole: \"toolResult\",\n\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\ttoolName: toolCall.name,\n\t\t\t\tcontent: [{ type: \"text\", text: error.message }],\n\t\t\t\tisError: true,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t});\n\t\t}\n\t}\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 * as fs from \"node:fs\";\nimport { getModel, complete } from \"@awfixerai/ai\";\n\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\n\n// Check if model supports images\nif (model.input.includes(\"image\")) {\n\tconsole.log(\"Model supports vision\");\n}\n\nconst imageBuffer = fs.readFileSync(\"image.png\");\nconst base64Image = imageBuffer.toBase64();\n\nconst response = await complete(model, {\n\tmessages: [\n\t\t{\n\t\t\trole: \"user\",\n\t\t\tcontent: [\n\t\t\t\t{ type: \"text\", text: \"What is in this image?\" },\n\t\t\t\t{ type: \"image\", data: base64Image, mimeType: \"image/png\" },\n\t\t\t],\n\t\t},\n\t],\n});\n\n// Access the response\nfor (const block of response.content) {\n\tif (block.type === \"text\") {\n\t\tconsole.log(block.text);\n\t}\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 \"@awfixerai/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\tconsole.log(\"Model supports reasoning/thinking\");\n}\n\n// Use the simplified reasoning option\nconst response = await completeSimple(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Solve: 2x + 5 = 13\" }],\n\t},\n\t{\n\t\treasoning: \"medium\", // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to high on non-OpenAI providers)\n\t}\n);\n\n// Access thinking and text blocks\nfor (const block of response.content) {\n\tif (block.type === \"thinking\") {\n\t\tconsole.log(\"Thinking:\", block.thinking);\n\t} else if (block.type === \"text\") {\n\t\tconsole.log(\"Response:\", block.text);\n\t}\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 \"@awfixerai/ai\";\n\n// OpenAI Reasoning (o1, o3, gpt-5)\nconst openaiModel = getModel(\"openai\", \"gpt-5-mini\");\nawait complete(openaiModel, context, {\n\treasoningEffort: \"medium\",\n\treasoningSummary: \"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\tthinkingEnabled: true,\n\tthinkingBudgetTokens: 8192, // Optional token limit\n});\n\n// Google Gemini Thinking\nconst googleModel = getModel(\"google\", \"gemini-2.5-flash\");\nawait complete(googleModel, context, {\n\tthinking: {\n\t\tenabled: true,\n\t\tbudgetTokens: 8192, // -1 for dynamic, 0 to disable\n\t},\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\tswitch (event.type) {\n\t\tcase \"thinking_start\":\n\t\t\tconsole.log(\"[Model started thinking]\");\n\t\t\tbreak;\n\t\tcase \"thinking_delta\":\n\t\t\tprocess.stdout.write(event.delta); // Stream thinking content\n\t\t\tbreak;\n\t\tcase \"thinking_end\":\n\t\t\tconsole.log(\"\\n[Thinking complete]\");\n\t\t\tbreak;\n\t}\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\tif (event.type === \"error\") {\n\t\t// event.reason is either \"error\" or \"aborted\"\n\t\t// event.error is the AssistantMessage with partial content\n\t\tconsole.error(`Error (${event.reason}):`, event.error.errorMessage);\n\t\tconsole.log(\"Partial content:\", event.error.content);\n\t}\n}\n\n// The final message will have the error details\nconst message = await stream.result();\nif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\tconsole.error(\"Request failed:\", message.errorMessage);\n\t// message.content contains any partial content received before the error\n\t// 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 \"@awfixerai/ai\";\n\nconst model = getModel(\"openai\", \"gpt-4o-mini\");\n\n// Abort after 2 seconds\nconst signal = AbortSignal.timeout(2000);\n\nconst s = stream(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Write a long story\" }],\n\t},\n\t{\n\t\tsignal,\n\t}\n);\n\nfor await (const event of s) {\n\tif (event.type === \"text_delta\") {\n\t\tprocess.stdout.write(event.delta);\n\t} else if (event.type === \"error\") {\n\t\t// event.reason tells you if it was \"error\" or \"aborted\"\n\t\tconsole.log(`${event.reason === \"aborted\" ? \"Aborted\" : \"Error\"}:`, event.error.errorMessage);\n\t}\n}\n\n// Get results (may be partial if aborted)\nconst response = await s.result();\nif (response.stopReason === \"aborted\") {\n\tconsole.log(\"Request was aborted:\", response.errorMessage);\n\tconsole.log(\"Partial content received:\", response.content);\n\tconsole.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\tmessages: [{ role: \"user\", content: \"Explain quantum computing in detail\" }],\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### Common Stream Options\n\nAll providers accept the base `StreamOptions` (in addition to provider-specific options):\n\n- `apiKey`: Override the provider API key\n- `headers`: Extra request headers merged on top of model-defined headers\n- `sessionId`: Provider-specific session identifier (prompt caching/routing)\n- `signal`: Abort in-flight requests\n- `onPayload`: Callback invoked with the provider request payload just before sending\n\nExample:\n\n```typescript\nconst response = await complete(model, context, {\n\tapiKey: \"sk-live\",\n\theaders: { \"X-Debug-Trace\": \"true\" },\n\tonPayload: (payload) => {\n\t\tconsole.log(\"request payload\", payload);\n\t},\n});\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\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 \"@awfixerai/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\tconsole.log(`${model.id}: ${model.name}`);\n\tconsole.log(`  API: ${model.api}`); // 'anthropic-messages'\n\tconsole.log(`  Context: ${model.contextWindow} tokens`);\n\tconsole.log(`  Vision: ${model.input.includes(\"image\")}`);\n\tconsole.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\nFor local Ollama, `OLLAMA_API_KEY` is optional and mainly needed for authenticated/self-hosted gateways. `ollama` remains the local OpenAI-compatible runtime integration.\n\n```typescript\nimport { Model, stream } from \"@awfixerai/ai\";\n\n// Example: local Ollama using the OpenAI-compatible API\nconst ollamaModel: Model<\"openai-completions\"> = {\n\tid: \"llama-3.1-8b\",\n\tname: \"Llama 3.1 8B (Ollama)\",\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 32000,\n};\n\nconst localResponse = await stream(ollamaModel, context, {\n\tapiKey: process.env.OLLAMA_API_KEY, // Optional; local Ollama usually runs without auth\n});\n\n// Example: Ollama Cloud using the native /api/chat transport\nconst ollamaCloudModel: Model<\"ollama-chat\"> = {\n\tid: \"gpt-oss:120b\",\n\tname: \"GPT OSS 120B (Ollama Cloud)\",\n\tapi: \"ollama-chat\",\n\tprovider: \"ollama-cloud\",\n\tbaseUrl: \"https://ollama.com\",\n\treasoning: true,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 262144,\n\tmaxTokens: 8192,\n};\n\nconst cloudResponse = await stream(ollamaCloudModel, context, {\n\tapiKey: process.env.OLLAMA_CLOUD_API_KEY,\n});\n\n// Example: LiteLLM proxy with explicit compat settings\nconst litellmModel: Model<\"openai-completions\"> = {\n\tid: \"gpt-4o\",\n\tname: \"GPT-4o (via LiteLLM)\",\n\tapi: \"openai-completions\",\n\tprovider: \"litellm\",\n\tbaseUrl: \"http://localhost:4000/v1\",\n\treasoning: false,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 16384,\n\tcompat: {\n\t\tsupportsStore: false, // LiteLLM doesn't support the store field\n\t},\n};\n\n// Example: Custom endpoint with headers (bypassing Cloudflare bot detection)\nconst proxyModel: Model<\"anthropic-messages\"> = {\n\tid: \"claude-sonnet-4\",\n\tname: \"Claude Sonnet 4 (Proxied)\",\n\tapi: \"anthropic-messages\",\n\tprovider: \"custom-proxy\",\n\tbaseUrl: \"https://proxy.example.com/v1\",\n\treasoning: true,\n\tinput: [\"text\", \"image\"],\n\tcost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },\n\tcontextWindow: 200000,\n\tmaxTokens: 8192,\n\theaders: {\n\t\t\"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36\",\n\t\t\"X-Custom-Auth\": \"bearer-token-here\",\n\t},\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\tsupportsStore?: boolean; // Whether provider supports the `store` field (default: true)\n\tsupportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)\n\tsupportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)\n\tmaxTokensField?: \"max_completion_tokens\" | \"max_tokens\"; // Which field name to use (default: max_completion_tokens)\n\textraBody?: Record<string, unknown>; // Extra request-body fields for custom proxy routing or provider-specific options\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\tthinkingEnabled: true, // ✓ Valid for anthropic-messages\n\tthinkingBudgetTokens: 2048, // ✓ Valid for anthropic-messages\n\t// 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 \"@awfixerai/ai\";\n\n// Start with Claude\nconst claude = getModel(\"anthropic\", \"claude-sonnet-4-20250514\");\nconst context: Context = {\n\tmessages: [],\n};\n\ncontext.messages.push({ role: \"user\", content: \"What is 25 * 18?\" });\nconst claudeResponse = await complete(claude, context, {\n\tthinkingEnabled: 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\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\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 \"@awfixerai/ai\";\n\n// Create and use a context\nconst context: Context = {\n\tsystemPrompt: [\"You are a helpful assistant.\"],\n\tmessages: [{ role: \"user\", content: \"What is TypeScript?\" }],\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-haiku-4-5-20251001\");\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 \"@awfixerai/ai\";\n\n// API key must be passed explicitly in browser\nconst model = getModel(\"anthropic\", \"claude-haiku-4-5-20251001\");\n\nconst response = await complete(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Hello!\" }],\n\t},\n\t{\n\t\tapiKey: \"your-api-key\",\n\t}\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` (or `ANTHROPIC_FOUNDRY_API_KEY` when `CLAUDE_CODE_USE_FOUNDRY=true`) |\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| Together       | `TOGETHER_API_KEY`                                                           |\n| Qianfan        | `QIANFAN_API_KEY`                                                            |\n| Hugging Face   | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN`                                        |\n| Synthetic      | `SYNTHETIC_API_KEY`                                                          |\n| NVIDIA         | `NVIDIA_API_KEY`                                                             |\n| NanoGPT        | `NANO_GPT_API_KEY`                                                          |\n| Venice         | `VENICE_API_KEY`                                                             |\n| Moonshot       | `MOONSHOT_API_KEY`                                                           |\n| xAI            | `XAI_API_KEY`                                                                |\n| OpenRouter     | `OPENROUTER_API_KEY`                                                         |\n| LiteLLM        | `LITELLM_API_KEY`                                                            |\n| Ollama         | `OLLAMA_API_KEY` (optional for local deployments)                            |\n| Ollama Cloud   | `OLLAMA_CLOUD_API_KEY`                                                     |\n| Qwen Portal    | `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY`                                  |\n| zAI            | `ZAI_API_KEY`                                                                |\n| Umans AI Coding Plan | `UMANS_AI_CODING_PLAN_API_KEY`                                           |\n| MiniMax Code   | `MINIMAX_CODE_API_KEY` (international) or `MINIMAX_CODE_CN_API_KEY` (China) |\n| Xiaomi MiMo    | `XIAOMI_API_KEY`                                                             |\n| ZenMux         | `ZENMUX_API_KEY`                                                             |\n| vLLM           | `VLLM_API_KEY`                                                               |\n| Cloudflare AI Gateway | `CLOUDFLARE_AI_GATEWAY_API_KEY`                                      |\n| GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN`                      |\n\nFor Cloudflare AI Gateway models, use provider base URL format\n`https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic`.\n\nFor Anthropic Foundry routing, set `CLAUDE_CODE_USE_FOUNDRY=true` plus:\n`FOUNDRY_BASE_URL`, `ANTHROPIC_FOUNDRY_API_KEY`, optional `ANTHROPIC_CUSTOM_HEADERS`,\nand optional mTLS material (`CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `NODE_EXTRA_CA_CERTS`).\n\nProvider endpoint defaults for the current OpenAI-compatible integrations:\n\n- Together: `https://api.together.xyz/v1`\n- Moonshot: `https://api.moonshot.ai/v1`\n- Qianfan: `https://qianfan.baidubce.com/v2`\n- NVIDIA: `https://integrate.api.nvidia.com/v1`\n- NanoGPT: `https://nano-gpt.com/api/v1`\n- Hugging Face Inference: `https://router.huggingface.co/v1`\n- Venice: `https://api.venice.ai/api/v1`\n- Xiaomi MiMo: `https://api.xiaomimimo.com/anthropic`\n- ZenMux (OpenAI): `https://zenmux.ai/api/v1`\n- ZenMux (Anthropic models): `https://zenmux.ai/api/anthropic`\n- Umans AI Coding Plan: `https://api.code.umans.ai`\n- vLLM: `http://127.0.0.1:8000/v1`\n- Ollama: local OpenAI-compatible runtime (`http://127.0.0.1:11434/v1`)\n- Ollama Cloud: native Ollama API host (`https://ollama.com/api`, configured here as base URL `https://ollama.com`)\n- LiteLLM: `http://localhost:4000/v1`\n- Cloudflare AI Gateway: `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic`\n- Qwen Portal: `https://portal.qwen.ai/v1`\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\tapiKey: \"sk-different-key\",\n});\n```\n\n### Checking Environment Variables\n\n```typescript\nimport { getEnvApiKey } from \"@awfixerai/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 support OAuth authentication (some also support 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- **Qwen Portal** (Qwen OAuth token or API key)\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 \"@awfixerai/ai\";\n\n(async () => {\n\tconst model = getModel(\"google-vertex\", \"gemini-2.5-flash\");\n\tconst response = await complete(model, {\n\t\tmessages: [{ role: \"user\", content: \"Hello from Vertex AI\" }],\n\t});\n\n\tfor (const block of response.content) {\n\t\tif (block.type === \"text\") console.log(block.text);\n\t}\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\nAuthenticate via the [`agent`](https://agent.awfixer.codes) coding-agent CLI, which drives this library's OAuth/API-key flows in-process and persists into `agent.db`:\n\n```bash\nagent auth-broker login              # interactive provider selection\nagent auth-broker login anthropic    # login to a specific provider\nagent auth-broker login vllm         # store vLLM API key (or placeholder for local no-auth)\nagent auth-broker list               # list supported providers\nagent auth-broker logout             # interactive — pick a stored credential to remove\n```\n\nCredentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.\n\n`login` supports OAuth providers (Anthropic, OpenAI Codex, GitHub Copilot, Gemini CLI, Antigravity) and API-key onboarding flows.\n\nFor the current API-key onboarding flows, the library covers Together, Moonshot, Qianfan, NVIDIA, NanoGPT, Hugging Face, Venice, Xiaomi, vLLM, LiteLLM, Cloudflare AI Gateway, Qwen Portal, and Ollama Cloud. Ollama remains the local runtime integration; set `OLLAMA_API_KEY` only when your local or self-hosted deployment enforces bearer auth.\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\t// Login functions (return credentials, do not store)\n\tloginAnthropic,\n\tloginOpenAICodex,\n\tloginGitHubCopilot,\n\tloginGeminiCli,\n\tloginAntigravity,\n\tloginCloudflareAiGateway,\n\tloginHuggingface,\n\tloginLiteLLM,\n\tloginMoonshot,\n\tloginNvidia,\n\tloginNanoGPT,\n\tloginQianfan,\n\tloginQwenPortal,\n\tloginTogether,\n\tloginVenice,\n\tloginVllm,\n\tloginXiaomi,\n\n\t// Token management\n\trefreshOAuthToken, // (provider, credentials) => new credentials\n\tgetOAuthApiKey, // (provider, credentialsMap) => { newCredentials, apiKey } | null\n\n\t// Types\n\ttype OAuthProvider, // includes 'anthropic', 'openai-codex', 'github-copilot', 'google-gemini-cli', 'google-antigravity', 'together', 'moonshot', 'qianfan', 'nvidia', 'nanogpt', 'huggingface', 'venice', 'xiaomi', 'vllm', 'litellm', 'cloudflare-ai-gateway', 'qwen-portal', ...\n\ttype OAuthCredentials,\n} from \"@awfixerai/ai\";\n```\n\n`loginOpenAICodex` accepts an optional `originator` value used in the OAuth flow:\n\n```typescript\nawait loginOpenAICodex({\n\tonAuth: ({ url }) => console.log(url),\n\toriginator: \"my-cli\",\n});\n```\n\n### Login Flow Example\n\n```typescript\nimport { loginGitHubCopilot } from \"@awfixerai/ai\";\nimport * as fs from \"node:fs\";\n\nconst credentials = await loginGitHubCopilot({\n\tonAuth: (url, instructions) => {\n\t\tconsole.log(`Open: ${url}`);\n\t\tif (instructions) console.log(instructions);\n\t},\n\tonPrompt: async (prompt) => {\n\t\treturn await getUserInput(prompt.message);\n\t},\n\tonProgress: (message) => console.log(message),\n});\n\n// Store credentials yourself\nconst auth = { \"github-copilot\": { type: \"oauth\", ...credentials } };\nfs.writeFileSync(\"credentials.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 \"@awfixerai/ai\";\nimport * as fs from \"node:fs\";\n\n// Load your stored credentials\nconst auth = JSON.parse(fs.readFileSync(\"credentials.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 };\nfs.writeFileSync(\"credentials.json\", JSON.stringify(auth, null, 2));\n\n// Use the API key\nconst model = getModel(\"github-copilot\", \"gpt-4o\");\nconst response = await complete(\n\tmodel,\n\t{\n\t\tmessages: [{ role: \"user\", content: \"Hello!\" }],\n\t},\n\t{ apiKey: result.apiKey }\n);\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## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-db9c5ad847399d32ddbd21e2d57c25e8"}