perf-lens
Version:
AI-powered frontend performance optimizer
84 lines (83 loc) • 3.44 kB
JavaScript
import Anthropic from '@anthropic-ai/sdk';
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
import { FindingsSchema } from './schema.js';
import { recordUsage } from './cost.js';
/** Single source of truth for the default model. Override via config `ai.model`. */
export const DEFAULT_MODEL = 'claude-opus-4-8';
const DEFAULT_MAX_TOKENS = 16000;
// Adaptive thinking is only supported on Claude 4.6+ models; older/smaller models reject it.
export function thinkingFor(model) {
return /opus-4-[678]|sonnet-4-6|sonnet-5|fable/.test(model)
? { thinking: { type: 'adaptive' } }
: undefined;
}
/**
* Thin wrapper around the Anthropic SDK. Structured outputs for code analysis,
* streaming text for prose, prompt caching on the stable system prefix, and
* usage accounting on every call. Retries are the SDK's built-in backoff.
*/
export class AIClient {
constructor(config) {
this.config = { ...config, model: config.model || DEFAULT_MODEL };
this.client = new Anthropic({ apiKey: config.apiKey, maxRetries: 3 });
}
getConfig() {
return this.config;
}
systemBlocks(systemPrompt, sharedContext) {
const blocks = [];
if (systemPrompt)
blocks.push({ type: 'text', text: systemPrompt });
if (sharedContext)
blocks.push({ type: 'text', text: sharedContext });
if (blocks.length > 0) {
blocks[blocks.length - 1].cache_control = { type: 'ephemeral' };
}
return blocks;
}
/**
* Analyzes code and returns schema-validated findings — no freeform text parsing.
*/
async analyzeCode(prompt, options = {}) {
const response = await this.client.messages.parse({
model: this.config.model,
max_tokens: this.config.maxTokens || DEFAULT_MAX_TOKENS,
...thinkingFor(this.config.model),
system: this.systemBlocks(options.systemPrompt, options.sharedContext),
messages: [{ role: 'user', content: prompt }],
output_config: { format: zodOutputFormat(FindingsSchema) },
});
recordUsage(this.config.model, response.usage);
return response.parsed_output?.findings ?? [];
}
/**
* Streams a prose response (used for the Lighthouse report analysis).
* Keeps the pre-revamp `generateSuggestions` surface so call sites stay small.
*/
async generateSuggestions(prompt, options = {}) {
const stream = this.client.messages.stream({
model: this.config.model,
max_tokens: this.config.maxTokens || DEFAULT_MAX_TOKENS,
...thinkingFor(this.config.model),
system: this.systemBlocks(options.systemPrompt),
messages: [{ role: 'user', content: prompt }],
});
if (options.onChunk) {
let firstChunk = true;
stream.on('text', text => {
options.onChunk(text, firstChunk);
firstChunk = false;
});
}
const message = await stream.finalMessage();
recordUsage(this.config.model, message.usage);
return message.content
.filter((block) => block.type === 'text')
.map(block => block.text)
.join('');
}
/** Raw SDK client — used by the agent loop, which drives its own tool-use turns. */
get anthropic() {
return this.client;
}
}