- name
- ngx-ai-devtools-angular-llm-debugger
- description
- Floating DevTools panel for intercepting and debugging OpenAI, Anthropic, Gemini, Mistral, Groq, and Cohere API calls in Angular apps with cost tracking and streaming support
- triggers
- ["add AI devtools to my Angular app","debug LLM calls in Angular","track OpenAI API costs in development","intercept Anthropic requests in my app","show me AI API tokens and costs","set up ngx-ai-devtools","monitor streaming AI responses","replay LLM calls in Angular"]
# ngx-ai-devtools Angular LLM Debugger
> Skill by [ara.so](https://ara.so) — Devtools Skills collection.
Network-tab-style DevTools for debugging LLM API calls in Angular applications. Intercepts and displays prompts, responses, token counts, costs, and streaming data for OpenAI, Anthropic, Google Gemini, Mistral, Groq, and Cohere — all without leaving the browser.
## What It Does
`ngx-ai-devtools` automatically intercepts all LLM API calls made from your Angular app (via `fetch`, `HttpClient`, or official SDKs) and displays them in a floating DevTools panel. It:
- Auto-detects provider (OpenAI, Anthropic, Gemini, etc.) and parses request/response
- Calculates cost per call using embedded pricing tables
- Handles Server-Sent Events (SSE) streaming with delta accumulation
- Shows tool/function calls with arguments
- Provides one-click replay functionality
- Tracks time-to-first-token and running session totals
- Persists history across reloads (optional)
- Works with any SDK or raw `fetch` calls
## Installation
```bash
npm install ngx-ai-devtools
```
**Requirements:** Angular 18.1+ (uses signals, standalone components, `@let` syntax)
## Basic Setup
Add `provideAiDevtools()` to your application config:
```typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAiDevtools } from 'ngx-ai-devtools';
import { environment } from './environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
provideAiDevtools({
enabled: !environment.production, // Disable in production
}),
],
};
```
A floating launcher pill appears in the bottom-right corner. All LLM calls are now intercepted and displayed.
## Configuration Options
All options are optional. Pass them to `provideAiDevtools()`:
```typescript
provideAiDevtools({
// When false, no patching or UI overhead
enabled: !environment.production,
// Maximum calls retained in memory (FIFO)
maxCalls: 200,
// Persist call history to localStorage across reloads
persist: true,
// Launcher position on screen
position: 'bottom-left', // 'bottom-right' | 'top-right' | 'top-left'
// Hide request/response bodies in UI (still recorded)
redact: false,
// Auto-inject UI into document.body
autoMount: true,
// Additional URL patterns to treat as LLM endpoints (for proxies/custom gateways)
additionalEndpoints: [
'/api/llm-proxy',
'/v1/internal-gateway',
'custom-llm.example.com'
],
});
```
## Usage with Different SDKs
### OpenAI SDK
```typescript
import OpenAI from 'openai';
import { Component } from '@angular/core';
@Component({
selector: 'app-chat',
template: `<button (click)="sendMessage()">Send</button>`,
})
export class ChatComponent {
private openai = new OpenAI({
apiKey: process.env['OPENAI_API_KEY'],
dangerouslyAllowBrowser: true, // Only for dev/demo
});
async sendMessage() {
const completion = await this.openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Explain quantum computing in simple terms' }
],
temperature: 0.7,
});
console.log(completion.choices[0].message.content);
// Call automatically appears in ngx-ai-devtools panel
}
}
```
### Streaming with OpenAI
```typescript
async streamResponse() {
const stream = await this.openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Count to 10' }],
stream: true, // Enable streaming
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
console.log(content);
// Deltas accumulate in real-time in the devtools panel
}
}
```
### Anthropic SDK
```typescript
import Anthropic from '@anthropic-ai/sdk';
@Component({
selector: 'app-claude',
template: `<button (click)="askClaude()">Ask Claude</button>`,
})
export class ClaudeComponent {
private anthropic = new Anthropic({
apiKey: process.env['ANTHROPIC_API_KEY'],
});
async askClaude() {
const message = await this.anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [
{ role: 'user', content: 'Write a haiku about TypeScript' }
],
});
console.log(message.content[0].text);
}
}
```
### Angular HttpClient (Raw API Calls)
```typescript
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { inject, Component } from '@angular/core';
@Component({
selector: 'app-gemini',
template: `<button (click)="callGemini()">Call Gemini</button>`,
})
export class GeminiComponent {
private http = inject(HttpClient);
callGemini() {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
});
const body = {
contents: [{
parts: [{ text: 'Explain the theory of relativity' }]
}]
};
this.http.post(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${process.env['GEMINI_API_KEY']}`,
body,
{ headers }
).subscribe(response => {
console.log(response);
// Appears in devtools automatically
});
}
}
```
### Raw Fetch (Works Anywhere)
```typescript
async callMistral() {
const response = await fetch('https://api.mistral.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env['MISTRAL_API_KEY']}`
},
body: JSON.stringify({
model: 'mistral-large-latest',
messages: [{ role: 'user', content: 'Hello!' }]
})
});
const data = await response.json();
console.log(data);
// Intercepted and displayed in devtools
}
```
## Programmatic API
Access the devtools service directly in your components:
```typescript
import { Component, computed, inject } from '@angular/core';
import { AiDevtoolsService } from 'ngx-ai-devtools';
@Component({
selector: 'app-cost-dashboard',
template: `
<div class="stats">
<div>Total Calls: {{ stats().count }}</div>
<div>Total Cost: ${{ stats().totalCost | number:'1.4-4' }}</div>
<div>Total Tokens: {{ stats().totalTokens | number }}</div>
<div>Avg Latency: {{ stats().avgLatency }}ms</div>
</div>
<button (click)="clearHistory()">Clear History</button>
<button (click)="togglePanel()">Toggle Panel</button>
@if (selectedCall()) {
<div>Selected: {{ selectedCall()?.model }}</div>
}
`,
})
export class CostDashboard {
private devtools = inject(AiDevtoolsService);
// Signal-based reactive state
stats = computed(() => this.devtools.stats());
calls = this.devtools.calls;
selectedCall = this.devtools.selected;
clearHistory() {
this.devtools.clear();
}
togglePanel() {
const currentState = this.devtools.ui().open;
this.devtools.setOpen(!currentState);
}
}
```
### Service API Reference
| Member | Type | Description |
|--------|------|-------------|
| `calls` | `Signal<LlmCall[]>` | All recorded calls, newest first |
| `filtered` | `Signal<LlmCall[]>` | Filtered view matching search |
| `selected` | `Signal<LlmCall \| null>` | Currently selected call |
| `stats` | `Signal<Stats>` | `{ count, totalCost, totalTokens, avgLatency }` |
| `ui` | `Signal<UiState>` | `{ open, selectedId, filter }` |
| `clear()` | `() => void` | Drop all recorded calls |
| `setOpen(boolean)` | `(boolean) => void` | Open/close panel |
| `select(id)` | `(string \| null) => void` | Select a call by ID |
| `setFilter(string)` | `(string) => void` | Set search filter |
| `replay(id)` | `(string) => Promise<string \| null>` | Re-issue a call |
## Common Patterns
### Budget Alerts
```typescript
import { Component, effect, inject } from '@angular/core';
import { AiDevtoolsService } from 'ngx-ai-devtools';
@Component({
selector: 'app-budget-monitor',
template: `<div>Session cost: ${{ currentCost() }}</div>`,
})
export class BudgetMonitor {
private devtools = inject(AiDevtoolsService);
private readonly BUDGET_LIMIT = 5.00; // $5 limit
currentCost = this.devtools.stats().totalCost;
constructor() {
effect(() => {
const cost = this.devtools.stats().totalCost;
if (cost > this.BUDGET_LIMIT) {
console.warn(`⚠️ Budget exceeded: $${cost.toFixed(4)}`);
// Disable AI features, show warning, etc.
}
});
}
}
```
### Call Replay for Testing
```typescript
async testPromptVariation(callId: string) {
// Replay exact same request
const newCallId = await this.devtools.replay(callId);
if (newCallId) {
console.log('Replayed call ID:', newCallId);
// Compare results in the UI
}
}
```
### Filter Calls Programmatically
```typescript
searchForExpensiveCalls() {
this.devtools.setFilter('gpt-4'); // Show only GPT-4 calls
}
showOnlyErrors() {
const errorCalls = this.devtools.calls().filter(call => call.error);
console.log('Failed calls:', errorCalls);
}
```
### Custom Component Integration
If `autoMount: false`, manually place the component:
```typescript
// app.config.ts
provideAiDevtools({
enabled: true,
autoMount: false, // Manual placement
});
// app.component.ts
import { Component } from '@angular/core';
import { NgxAiDevtoolsComponent } from 'ngx-ai-devtools';
@Component({
selector: 'app-root',
standalone: true,
imports: [NgxAiDevtoolsComponent],
template: `
<main>
<!-- Your app content -->
</main>
<!-- Devtools in custom position -->
<ngx-ai-devtools />
`,
})
export class AppComponent {}
```
## Tool/Function Calling
The devtools automatically parse and display function calls:
```typescript
const completion = await this.openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
tools: [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get current weather for a location',
parameters: {
type: 'object',
properties: {
location: { type: 'string', description: 'City name' },
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] }
},
required: ['location']
}
}
}
],
});
// Tool calls appear in dedicated "Tool Use" tab in the panel
```
## Provider Support
Auf GitHub ansehen