From 7ca465850c2917bdc7bc16cb9453dceb18df440a Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:24:01 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + SKILL.md | 530 +++++++++++++++++++++++++++++ plugin.lock.json | 125 +++++++ references/api-reference.md | 224 ++++++++++++ references/prompt-caching-guide.md | 354 +++++++++++++++++++ references/rate-limits.md | 80 +++++ references/tool-use-patterns.md | 114 +++++++ references/top-errors.md | 507 +++++++++++++++++++++++++++ references/vision-capabilities.md | 108 ++++++ scripts/check-versions.sh | 102 ++++++ templates/basic-chat.ts | 115 +++++++ templates/cloudflare-worker.ts | 268 +++++++++++++++ templates/error-handling.ts | 373 ++++++++++++++++++++ templates/extended-thinking.ts | 320 +++++++++++++++++ templates/nextjs-api-route.ts | 304 +++++++++++++++++ templates/nodejs-example.ts | 381 +++++++++++++++++++++ templates/package.json | 48 +++ templates/prompt-caching.ts | 272 +++++++++++++++ templates/streaming-chat.ts | 194 +++++++++++ templates/tool-use-advanced.ts | 296 ++++++++++++++++ templates/tool-use-basic.ts | 310 +++++++++++++++++ templates/vision-image.ts | 392 +++++++++++++++++++++ templates/wrangler.jsonc | 80 +++++ 24 files changed, 5512 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 SKILL.md create mode 100644 plugin.lock.json create mode 100644 references/api-reference.md create mode 100644 references/prompt-caching-guide.md create mode 100644 references/rate-limits.md create mode 100644 references/tool-use-patterns.md create mode 100644 references/top-errors.md create mode 100644 references/vision-capabilities.md create mode 100755 scripts/check-versions.sh create mode 100644 templates/basic-chat.ts create mode 100644 templates/cloudflare-worker.ts create mode 100644 templates/error-handling.ts create mode 100644 templates/extended-thinking.ts create mode 100644 templates/nextjs-api-route.ts create mode 100644 templates/nodejs-example.ts create mode 100644 templates/package.json create mode 100644 templates/prompt-caching.ts create mode 100644 templates/streaming-chat.ts create mode 100644 templates/tool-use-advanced.ts create mode 100644 templates/tool-use-basic.ts create mode 100644 templates/vision-image.ts create mode 100644 templates/wrangler.jsonc diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..e960fff --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "claude-api", + "description": "Integrate Claude API with streaming, prompt caching, tool use, vision, and extended thinking. Use when: building chatbots, AI assistants, content tools in Node.js/Workers/Next.js, or troubleshooting rate_limit_error, overloaded_error, invalid_request_error, 429 errors.", + "version": "1.0.0", + "author": { + "name": "Jeremy Dawes", + "email": "jeremy@jezweb.net" + }, + "skills": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..99aa362 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# claude-api + +Integrate Claude API with streaming, prompt caching, tool use, vision, and extended thinking. Use when: building chatbots, AI assistants, content tools in Node.js/Workers/Next.js, or troubleshooting rate_limit_error, overloaded_error, invalid_request_error, 429 errors. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..24866ab --- /dev/null +++ b/SKILL.md @@ -0,0 +1,530 @@ +--- +name: claude-api +description: | + Build with Claude Messages API using structured outputs (v0.69.0+, Nov 2025) for guaranteed JSON schema validation. Covers prompt caching (90% savings), streaming SSE, tool use, model deprecations (3.5/3.7 retired Oct 2025). Use when: building chatbots/agents with validated JSON responses, or troubleshooting rate_limit_error, structured output validation, prompt caching not activating, streaming SSE parsing. +license: MIT +metadata: + version: 2.0.0 + last_verified: 2025-11-22 + sdk_version: 0.70.1 + token_savings: ~48% + errors_prevented: 12 + breaking_changes: Oct 2025 - Claude 3.5/3.7 models retired, Nov 2025 - Structured outputs beta + keywords: + - claude api + - anthropic api + - messages api + - "@anthropic-ai/sdk" + - structured outputs + - output_format + - strict tool use + - json schema validation + - claude streaming + - prompt caching + - cache_control + - tool use + - vision + - extended thinking + - claude sonnet 4.5 + - claude haiku 4.5 + - claude opus 4 + - function calling + - SSE + - rate limits + - 429 errors + - agent skills api + - context management + - clear thinking + - streaming sse parsing + - prompt caching not working + - structured output validation + - model deprecated + - model retired +--- + +# Claude API - Structured Outputs & Error Prevention Guide + +**Package**: @anthropic-ai/sdk@0.70.1 (Nov 20, 2025) +**Breaking Changes**: Oct 2025 - Claude 3.5/3.7 models retired, Nov 2025 - Structured outputs beta +**Last Updated**: 2025-11-22 + +--- + +## What's New in v0.69.0+ (Nov 2025) + +**Major Features:** + +### 1. Structured Outputs (v0.69.0, Nov 14, 2025) - CRITICAL ⭐ + +**Guaranteed JSON schema conformance** - Claude's responses strictly follow your JSON schema with two modes: + +**JSON Outputs (`output_format`)** - For data extraction and formatting: +```typescript +import Anthropic from '@anthropic-ai/sdk'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Extract contact info: John Doe, john@example.com, 555-1234' }], + betas: ['structured-outputs-2025-11-13'], + output_format: { + type: 'json_schema', + json_schema: { + name: 'Contact', + strict: true, + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' } + }, + required: ['name', 'email', 'phone'], + additionalProperties: false + } + } + } +}); + +// Guaranteed valid JSON matching schema +const contact = JSON.parse(message.content[0].text); +console.log(contact.name); // "John Doe" +``` + +**Strict Tool Use (`strict: true`)** - For validated function parameters: +```typescript +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Get weather for San Francisco' }], + betas: ['structured-outputs-2025-11-13'], + tools: [{ + name: 'get_weather', + description: 'Get current weather', + input_schema: { + type: 'object', + properties: { + location: { type: 'string' }, + unit: { type: 'string', enum: ['celsius', 'fahrenheit'] } + }, + required: ['location'], + additionalProperties: false + }, + strict: true // ← Guarantees schema compliance + }] +}); +``` + +**Requirements:** +- **Beta header**: `structured-outputs-2025-11-13` (via `betas` array) +- **Models**: Claude Sonnet 4.5, Claude Opus 4.1 only +- **SDK**: v0.69.0+ required + +**Limitations:** +- ❌ No recursive schemas +- ❌ No numerical constraints (`minimum`, `maximum`) +- ❌ Limited regex support (no backreferences/lookahead) +- ❌ Incompatible with citations and message prefilling +- ⚠️ Grammar compilation adds latency on first request (cached 24hrs) + +**When to Use:** +- Data extraction from unstructured text +- API response formatting +- Agentic workflows requiring validated tool inputs +- Eliminating JSON parse errors + +### 2. Model Changes (Oct 2025) - BREAKING + +**Retired (return errors):** +- ❌ Claude 3.5 Sonnet (all versions) +- ❌ Claude 3.7 Sonnet - DEPRECATED (Oct 28, 2025) + +**Active Models (Nov 2025):** + +| Model | ID | Context | Best For | Cost (per MTok) | +|-------|-----|---------|----------|-----------------| +| **Claude Sonnet 4.5** | claude-sonnet-4-5-20250929 | 200k | Balanced performance | $3/$15 (in/out) | +| **Claude Opus 4** | claude-opus-4-20250514 | 200k | Highest capability | $15/$75 | +| **Claude Haiku 4.5** | claude-3-5-haiku-20241022 | 200k | Near-frontier, fast | $1/$5 | + +### 3. Context Management (Oct 28, 2025) + +**Clear Thinking Blocks** - Automatic thinking block cleanup: +```typescript +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 4096, + messages: [{ role: 'user', content: 'Solve complex problem' }], + betas: ['clear_thinking_20251015'] +}); +// Thinking blocks automatically managed +``` + +### 4. Agent Skills API (Oct 16, 2025) + +Pre-built skills for Office files (PowerPoint, Excel, Word, PDF): +```typescript +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Analyze this spreadsheet' }], + betas: ['skills-2025-10-02'], + // Requires code execution tool enabled +}); +``` + +📚 **Docs**: https://platform.claude.com/docs/en/build-with-claude/structured-outputs + +--- + +## Streaming Responses (SSE) + +**CRITICAL Error Pattern** - Errors occur AFTER initial 200 response: +```typescript +const stream = anthropic.messages.stream({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Hello' }], +}); + +stream + .on('error', (error) => { + // Error can occur AFTER stream starts + console.error('Stream error:', error); + // Implement fallback or retry logic + }) + .on('abort', (error) => { + console.warn('Stream aborted:', error); + }); +``` + +**Why this matters**: Unlike regular HTTP errors, SSE errors happen mid-stream after 200 OK, requiring error event listeners + +--- + +## Prompt Caching (⭐ 90% Cost Savings) + +**CRITICAL Rule** - `cache_control` MUST be on LAST block: +```typescript +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: 'System instructions...', + }, + { + type: 'text', + text: LARGE_CODEBASE, // 50k tokens + cache_control: { type: 'ephemeral' }, // ← MUST be on LAST block + }, + ], + messages: [{ role: 'user', content: 'Explain auth module' }], +}); + +// Monitor cache usage +console.log('Cache reads:', message.usage.cache_read_input_tokens); +console.log('Cache writes:', message.usage.cache_creation_input_tokens); +``` + +**Minimum requirements:** +- Claude Sonnet 4.5: 1,024 tokens minimum +- Claude Haiku 4.5: 2,048 tokens minimum +- 5-minute TTL (refreshes on each use) +- Cache shared only with IDENTICAL content + +--- + +## Tool Use (Function Calling) + +**CRITICAL Patterns:** + +**Strict Tool Use** (with structured outputs): +```typescript +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + betas: ['structured-outputs-2025-11-13'], + tools: [{ + name: 'get_weather', + description: 'Get weather data', + input_schema: { + type: 'object', + properties: { + location: { type: 'string' }, + unit: { type: 'string', enum: ['celsius', 'fahrenheit'] } + }, + required: ['location'], + additionalProperties: false + }, + strict: true // ← Guarantees schema compliance + }], + messages: [{ role: 'user', content: 'Weather in NYC?' }] +}); +``` + +**Tool Result Pattern** - `tool_use_id` MUST match: +```typescript +const toolResults = []; +for (const block of response.content) { + if (block.type === 'tool_use') { + const result = await executeToolFunction(block.name, block.input); + + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, // ← MUST match tool_use block id + content: JSON.stringify(result), + }); + } +} + +messages.push({ + role: 'user', + content: toolResults, +}); +``` + +**Error Handling** - Handle tool execution failures: +```typescript +try { + const result = await executeToolFunction(block.name, block.input); + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + content: JSON.stringify(result), + }); +} catch (error) { + // Return error to Claude for handling + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + is_error: true, + content: `Tool execution failed: ${error.message}`, + }); +} +``` + +--- + +## Vision (Image Understanding) + +**CRITICAL Rules:** +- **Formats**: JPEG, PNG, WebP, GIF (non-animated) +- **Max size**: 5MB per image +- **Base64 overhead**: ~33% size increase +- **Context impact**: Images count toward token limit +- **Caching**: Consider for repeated image analysis + +**Format validation** - Check before encoding: +```typescript +const validFormats = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; +if (!validFormats.includes(mimeType)) { + throw new Error(`Unsupported format: ${mimeType}`); +} +``` + +--- + +## Extended Thinking Mode + +**⚠️ Model Compatibility:** +- ❌ Claude 3.7 Sonnet - DEPRECATED (Oct 28, 2025) +- ❌ Claude 3.5 Sonnet - RETIRED (not supported) +- ✅ Claude Sonnet 4.5 - Extended thinking supported +- ✅ Claude Opus 4 - Extended thinking supported + +**CRITICAL:** +- Thinking blocks are NOT cacheable +- Requires higher `max_tokens` (thinking consumes tokens) +- Check model before expecting thinking blocks + +--- + +## Rate Limits + +**CRITICAL Pattern** - Respect `retry-after` header with exponential backoff: +```typescript +async function makeRequestWithRetry( + requestFn: () => Promise, + maxRetries = 3, + baseDelay = 1000 +): Promise { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await requestFn(); + } catch (error) { + if (error.status === 429) { + // CRITICAL: Use retry-after header if present + const retryAfter = error.response?.headers?.['retry-after']; + const delay = retryAfter + ? parseInt(retryAfter) * 1000 + : baseDelay * Math.pow(2, attempt); + + console.warn(`Rate limited. Retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + throw new Error('Max retries exceeded'); +} +``` + +**Rate limit headers:** +- `anthropic-ratelimit-requests-limit` - Total RPM allowed +- `anthropic-ratelimit-requests-remaining` - Remaining requests +- `anthropic-ratelimit-requests-reset` - Reset timestamp + +--- + +## Error Handling + +**Common Error Codes:** + +| Status | Error Type | Cause | Solution | +|--------|-----------|-------|----------| +| 400 | invalid_request_error | Bad parameters | Validate request body | +| 401 | authentication_error | Invalid API key | Check env variable | +| 403 | permission_error | No access to feature | Check account tier | +| 404 | not_found_error | Invalid endpoint | Check API version | +| 429 | rate_limit_error | Too many requests | Implement retry logic | +| 500 | api_error | Internal error | Retry with backoff | +| 529 | overloaded_error | System overloaded | Retry later | + +**CRITICAL:** +- Streaming errors occur AFTER initial 200 response +- Always implement error event listeners for streams +- Respect `retry-after` header on 429 errors +- Have fallback strategies for critical operations + +--- + +## Known Issues Prevention + +This skill prevents **12** documented issues: + +### Issue #1: Rate Limit 429 Errors Without Backoff +**Error**: `429 Too Many Requests: Number of request tokens has exceeded your per-minute rate limit` +**Source**: https://docs.claude.com/en/api/errors +**Why It Happens**: Exceeding RPM, TPM, or daily token limits +**Prevention**: Implement exponential backoff with `retry-after` header respect + +### Issue #2: Streaming SSE Parsing Errors +**Error**: Incomplete chunks, malformed SSE events +**Source**: Common SDK issue (GitHub #323) +**Why It Happens**: Network interruptions, improper event parsing +**Prevention**: Use SDK stream helpers, implement error event listeners + +### Issue #3: Prompt Caching Not Activating +**Error**: High costs despite cache_control blocks +**Source**: https://platform.claude.com/docs/en/build-with-claude/prompt-caching +**Why It Happens**: `cache_control` placed incorrectly (must be at END) +**Prevention**: Always place `cache_control` on LAST block of cacheable content + +### Issue #4: Tool Use Response Format Errors +**Error**: `invalid_request_error: tools[0].input_schema is invalid` +**Source**: API validation errors +**Why It Happens**: Invalid JSON Schema, missing required fields +**Prevention**: Validate schemas with JSON Schema validator, test thoroughly + +### Issue #5: Vision Image Format Issues +**Error**: `invalid_request_error: image source must be base64 or url` +**Source**: API documentation +**Why It Happens**: Incorrect encoding, unsupported formats +**Prevention**: Validate format (JPEG/PNG/WebP/GIF), proper base64 encoding + +### Issue #6: Token Counting Mismatches for Billing +**Error**: Unexpected high costs, context window exceeded +**Source**: Token counting differences +**Why It Happens**: Not accounting for special tokens, formatting +**Prevention**: Use official token counter, monitor usage headers + +### Issue #7: System Prompt Ordering Issues +**Error**: System prompt ignored or overridden +**Source**: API behavior +**Why It Happens**: System prompt placed after messages array +**Prevention**: ALWAYS place system prompt before messages + +### Issue #8: Context Window Exceeded (200k) +**Error**: `invalid_request_error: messages: too many tokens` +**Source**: Model limits +**Why It Happens**: Long conversations without pruning +**Prevention**: Implement message history pruning, use caching + +### Issue #9: Extended Thinking on Wrong Model +**Error**: No thinking blocks in response +**Source**: Model capabilities +**Why It Happens**: Using retired/deprecated models (3.5/3.7 Sonnet) +**Prevention**: Only use extended thinking with Claude Sonnet 4.5 or Claude Opus 4 + +### Issue #10: API Key Exposure in Client Code +**Error**: CORS errors, security vulnerability +**Source**: Security best practices +**Why It Happens**: Making API calls from browser +**Prevention**: Server-side only, use environment variables + +### Issue #11: Rate Limit Tier Confusion +**Error**: Lower limits than expected +**Source**: Account tier system +**Why It Happens**: Not understanding tier progression +**Prevention**: Check Console for current tier, auto-scales with usage + +### Issue #12: Message Batches Beta Headers Missing +**Error**: `invalid_request_error: unknown parameter: batches` +**Source**: Beta API requirements +**Why It Happens**: Missing `anthropic-beta` header +**Prevention**: Include `anthropic-beta: message-batches-2024-09-24` header + +--- + +## Official Documentation + +- **Claude API**: https://platform.claude.com/docs/en/api +- **Messages API**: https://platform.claude.com/docs/en/api/messages +- **Structured Outputs**: https://platform.claude.com/docs/en/build-with-claude/structured-outputs +- **Prompt Caching**: https://platform.claude.com/docs/en/build-with-claude/prompt-caching +- **Tool Use**: https://platform.claude.com/docs/en/build-with-claude/tool-use +- **Vision**: https://platform.claude.com/docs/en/build-with-claude/vision +- **Rate Limits**: https://platform.claude.com/docs/en/api/rate-limits +- **Errors**: https://platform.claude.com/docs/en/api/errors +- **TypeScript SDK**: https://github.com/anthropics/anthropic-sdk-typescript +- **Context7 Library ID**: /anthropics/anthropic-sdk-typescript + +--- + +## Package Versions + +**Latest**: @anthropic-ai/sdk@0.70.1 (Nov 20, 2025) + +```json +{ + "dependencies": { + "@anthropic-ai/sdk": "^0.70.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.3.0", + "zod": "^3.23.0" + } +} +``` + +--- + +**Token Efficiency**: +- **Without skill**: ~8,000 tokens (basic setup, streaming, caching, tools, vision, errors) +- **With skill**: ~4,200 tokens (knowledge gaps + error prevention + critical patterns) +- **Savings**: ~48% (~3,800 tokens) + +**Errors prevented**: 12 documented issues with exact solutions +**Key value**: Structured outputs (v0.69.0+), model deprecations (Oct 2025), prompt caching edge cases, streaming error patterns, rate limit retry logic + +--- + +**Last verified**: 2025-11-22 | **Skill version**: 2.0.0 | **Changes**: Added structured outputs (v0.69.0), updated model table (retired 3.5/3.7), context management, agent skills API. Removed basic tutorials (~380 lines). Focused on knowledge gaps + error prevention + advanced patterns. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..dcf062a --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,125 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jezweb/claude-skills:skills/claude-api", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "0b446dc7f17ae1ba47fea924d73fe5b8ac655302", + "treeHash": "0d9003459d6376a2af8544e39932cd7d1bda1d681490397dda3e54de8c5ce071", + "generatedAt": "2025-11-28T10:19:02.213122Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "claude-api", + "description": "Integrate Claude API with streaming, prompt caching, tool use, vision, and extended thinking. Use when: building chatbots, AI assistants, content tools in Node.js/Workers/Next.js, or troubleshooting rate_limit_error, overloaded_error, invalid_request_error, 429 errors.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "030954f810c2215caa27c6e578f96237b015580f14045f03036cb64fb1f906cc" + }, + { + "path": "SKILL.md", + "sha256": "583beabc079f4d049641170507d1526bf69ab7d8a488bafdaf13c14ce760325f" + }, + { + "path": "references/rate-limits.md", + "sha256": "d365e3327c2f9343a7ea2e8c0b085e72624352df04ad7c3b790211bd90fbace9" + }, + { + "path": "references/tool-use-patterns.md", + "sha256": "c08b81d8b74e6dbe481570575e4c6d5452ed1ea6ee58331773d362e4ad0b679f" + }, + { + "path": "references/top-errors.md", + "sha256": "61e316b1dfd7c3459955529e1386471516c0836b9306bd77f717bc8e1181e909" + }, + { + "path": "references/vision-capabilities.md", + "sha256": "232d5aab709580a4684450252943c1291efeaf8f39038dea4a762097f7de97a5" + }, + { + "path": "references/prompt-caching-guide.md", + "sha256": "aad6332727664ba5345be4c433d877f01520bdd4d4ad44e8603405702fd931b2" + }, + { + "path": "references/api-reference.md", + "sha256": "845efb91c97276fe6cc4ac8105eeea736701ef0b3ea2ccab278ea7f8605f2bde" + }, + { + "path": "scripts/check-versions.sh", + "sha256": "09a2d059609ba75e4ffecc688c7075a981c263368c92c31b5ad766f931176aa1" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "fd8b0ba9d8e048ebc7efe84d487920bf856931c4438f06383ba0ae7ff480d20c" + }, + { + "path": "templates/extended-thinking.ts", + "sha256": "32298e07dad6df9116b49280f3a5c763f362fb2b880550538484459e975962d8" + }, + { + "path": "templates/tool-use-basic.ts", + "sha256": "e83db54f9d28b7cfe1e3187873fd955f68017df61dfd91de3208f5659827d819" + }, + { + "path": "templates/tool-use-advanced.ts", + "sha256": "2f486d51f6011224c0ca5cb541fccd450257adfb0b468f051d3a20fcf021f558" + }, + { + "path": "templates/wrangler.jsonc", + "sha256": "f200236b2af9644ae7b95f523cd5cf50afb96eea478bf65bdad15768600fe8cf" + }, + { + "path": "templates/nextjs-api-route.ts", + "sha256": "fa3ee9a8eb426c59bf1be2e1587704497303854cfd675e26088603605421b0c6" + }, + { + "path": "templates/streaming-chat.ts", + "sha256": "a67c02b6ac9eef07812e0abe777182e0f29a4b263ef688b6375e849ed46ead2f" + }, + { + "path": "templates/basic-chat.ts", + "sha256": "3461d28e0d4ae9b34f03b24b6dcea3a033f261721e5b833a0065da04f58f9bc4" + }, + { + "path": "templates/error-handling.ts", + "sha256": "352ec0214260c012b1bce78ecf39026c410769783f8ce25497859b6824f09064" + }, + { + "path": "templates/package.json", + "sha256": "3fedabb44a2d420de2139167ffe4705e7104dfe54479e0fe8d69b673ff30a2d3" + }, + { + "path": "templates/cloudflare-worker.ts", + "sha256": "34a4bea6597f104ced2d49de7508fed05cf0e4e5ac9cab49c2761506b7fa1c63" + }, + { + "path": "templates/nodejs-example.ts", + "sha256": "aed836c3d2fdd05e52d7079b2aa09fa3c0750c9ff468e1bcdbf1533623aab85f" + }, + { + "path": "templates/vision-image.ts", + "sha256": "51418952ec69fdd109c40d04cf0b42487de9edd3dbeb2223aacbfbe66471fef1" + }, + { + "path": "templates/prompt-caching.ts", + "sha256": "df3b729e396f852f0e421d471a01259e3de96b71d85b7c99470b089c1b2cacef" + } + ], + "dirSha256": "0d9003459d6376a2af8544e39932cd7d1bda1d681490397dda3e54de8c5ce071" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/references/api-reference.md b/references/api-reference.md new file mode 100644 index 0000000..0154d02 --- /dev/null +++ b/references/api-reference.md @@ -0,0 +1,224 @@ +# Claude API Reference + +Quick reference for the Anthropic Messages API endpoints and parameters. + +## Base URL + +``` +https://api.anthropic.com/v1 +``` + +## Authentication + +All requests require: +```http +x-api-key: YOUR_API_KEY +anthropic-version: 2023-06-01 +content-type: application/json +``` + +## Endpoints + +### POST /messages + +Create a message with Claude. + +**Request Body:** + +```typescript +{ + model: string, // Required: "claude-sonnet-4-5-20250929", etc. + max_tokens: number, // Required: Maximum tokens to generate (1-8192) + messages: Message[], // Required: Conversation history + system?: string | SystemBlock[], // Optional: System prompt + temperature?: number, // Optional: 0-1 (default: 1) + top_p?: number, // Optional: 0-1 (default: 1) + top_k?: number, // Optional: Sampling parameter + stop_sequences?: string[], // Optional: Stop generation at these sequences + stream?: boolean, // Optional: Enable streaming (default: false) + tools?: Tool[], // Optional: Available tools + tool_choice?: ToolChoice, // Optional: Tool selection strategy + metadata?: Metadata // Optional: Request metadata +} +``` + +**Message Format:** + +```typescript +{ + role: "user" | "assistant", + content: string | ContentBlock[] +} +``` + +**Content Block Types:** + +```typescript +// Text +{ + type: "text", + text: string, + cache_control?: { type: "ephemeral" } // For prompt caching +} + +// Image +{ + type: "image", + source: { + type: "base64" | "url", + media_type: "image/jpeg" | "image/png" | "image/webp" | "image/gif", + data?: string, // base64 encoded + url?: string // publicly accessible URL + }, + cache_control?: { type: "ephemeral" } +} + +// Tool use (assistant messages only) +{ + type: "tool_use", + id: string, + name: string, + input: object +} + +// Tool result (user messages only) +{ + type: "tool_result", + tool_use_id: string, + content: string | ContentBlock[], + is_error?: boolean +} +``` + +**Response:** + +```typescript +{ + id: string, + type: "message", + role: "assistant", + content: ContentBlock[], + model: string, + stop_reason: "end_turn" | "max_tokens" | "stop_sequence" | "tool_use", + stop_sequence?: string, + usage: { + input_tokens: number, + output_tokens: number, + cache_creation_input_tokens?: number, + cache_read_input_tokens?: number + } +} +``` + +## Model IDs + +| Model | ID | Context Window | +|-------|-----|----------------| +| Claude Sonnet 4.5 | claude-sonnet-4-5-20250929 | 200k tokens | +| Claude 3.7 Sonnet | claude-3-7-sonnet-20250228 | 2M tokens | +| Claude Opus 4 | claude-opus-4-20250514 | 200k tokens | +| Claude 3.5 Haiku | claude-3-5-haiku-20241022 | 200k tokens | + +## Tool Definition + +```typescript +{ + name: string, // Tool identifier + description: string, // What the tool does + input_schema: { // JSON Schema + type: "object", + properties: { + [key: string]: { + type: "string" | "number" | "boolean" | "array" | "object", + description?: string, + enum?: any[] + } + }, + required?: string[] + } +} +``` + +## Streaming + +Set `stream: true` in request. Returns Server-Sent Events (SSE): + +**Event Types:** +- `message_start`: Message begins +- `content_block_start`: Content block begins +- `content_block_delta`: Text or JSON delta +- `content_block_stop`: Content block complete +- `message_delta`: Metadata update +- `message_stop`: Message complete +- `ping`: Keep-alive + +**Event Format:** + +``` +event: message_start +data: {"type":"message_start","message":{...}} + +event: content_block_delta +data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}} + +event: message_stop +data: {"type":"message_stop"} +``` + +## Error Responses + +```typescript +{ + type: "error", + error: { + type: string, // Error type identifier + message: string // Human-readable description + } +} +``` + +**Common Error Types:** +- `invalid_request_error` (400) +- `authentication_error` (401) +- `permission_error` (403) +- `not_found_error` (404) +- `rate_limit_error` (429) +- `api_error` (500) +- `overloaded_error` (529) + +## Rate Limit Headers + +Response includes: + +``` +anthropic-ratelimit-requests-limit: 50 +anthropic-ratelimit-requests-remaining: 49 +anthropic-ratelimit-requests-reset: 2025-10-25T12:00:00Z +anthropic-ratelimit-tokens-limit: 50000 +anthropic-ratelimit-tokens-remaining: 49500 +anthropic-ratelimit-tokens-reset: 2025-10-25T12:01:00Z +retry-after: 60 // Only on 429 errors +``` + +## SDK Installation + +```bash +# TypeScript/JavaScript +npm install @anthropic-ai/sdk + +# Python +pip install anthropic + +# Java +# See https://github.com/anthropics/anthropic-sdk-java + +# Go +go get github.com/anthropics/anthropic-sdk-go +``` + +## Official Documentation + +- **API Reference**: https://docs.claude.com/en/api/messages +- **SDK Documentation**: https://github.com/anthropics/anthropic-sdk-typescript +- **Rate Limits**: https://docs.claude.com/en/api/rate-limits +- **Errors**: https://docs.claude.com/en/api/errors diff --git a/references/prompt-caching-guide.md b/references/prompt-caching-guide.md new file mode 100644 index 0000000..1ca69ed --- /dev/null +++ b/references/prompt-caching-guide.md @@ -0,0 +1,354 @@ +# Prompt Caching Guide + +Complete guide to using prompt caching for cost optimization. + +## Overview + +Prompt caching reduces costs by up to 90% and latency by up to 85% by caching frequently used context. + +### Benefits + +- **Cost Savings**: Cache reads = 10% of input token price +- **Latency Reduction**: 85% faster time to first token +- **Use Cases**: Long documents, codebases, system instructions, conversation history + +### Pricing + +| Operation | Cost (per MTok) | vs Regular Input | +|-----------|-----------------|------------------| +| Regular input | $3 | 100% | +| Cache write | $3.75 | 125% | +| Cache read | $0.30 | 10% | + +**Example**: 100k tokens cached, used 10 times +- Without caching: 100k × $3/MTok × 10 = $3.00 +- With caching: (100k × $3.75/MTok) + (100k × $0.30/MTok × 9) = $0.375 + $0.27 = $0.645 +- **Savings: $2.355 (78.5%)** + +## Requirements + +### Minimum Cacheable Content + +- **Claude 3.5 Sonnet**: 1,024 tokens minimum +- **Claude 3.5 Haiku**: 2,048 tokens minimum +- **Claude 3.7 Sonnet**: 1,024 tokens minimum + +### Cache Lifetime + +- **Default**: 5 minutes +- **Extended**: 1 hour (configurable) +- Refreshes on each use + +### Cache Matching + +Cache hits require: +- ✅ **Identical content** (byte-for-byte) +- ✅ **Same position** in request +- ✅ **Within TTL** (5 min or 1 hour) + +## Implementation + +### Basic System Prompt Caching + +```typescript +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: LARGE_SYSTEM_INSTRUCTIONS, // >= 1024 tokens + cache_control: { type: 'ephemeral' }, + }, + ], + messages: [ + { role: 'user', content: 'Your question here' } + ], +}); +``` + +### Caching in User Messages + +```typescript +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'Analyze this document:', + }, + { + type: 'text', + text: LARGE_DOCUMENT, // >= 1024 tokens + cache_control: { type: 'ephemeral' }, + }, + { + type: 'text', + text: 'What are the main themes?', + }, + ], + }, + ], +}); +``` + +### Multi-Turn Conversation Caching + +```typescript +// Turn 1 - Creates cache +const response1 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: SYSTEM_PROMPT, + cache_control: { type: 'ephemeral' }, + }, + ], + messages: [ + { role: 'user', content: 'Hello!' } + ], +}); + +// Turn 2 - Hits cache (same system prompt) +const response2 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: SYSTEM_PROMPT, // Identical - cache hit + cache_control: { type: 'ephemeral' }, + }, + ], + messages: [ + { role: 'user', content: 'Hello!' }, + { role: 'assistant', content: response1.content[0].text }, + { role: 'user', content: 'Tell me more' }, + ], +}); +``` + +### Caching Conversation History + +```typescript +const messages = [ + { role: 'user', content: 'Message 1' }, + { role: 'assistant', content: 'Response 1' }, + { role: 'user', content: 'Message 2' }, + { role: 'assistant', content: 'Response 2' }, +]; + +// Cache last assistant message +messages[messages.length - 1] = { + role: 'assistant', + content: [ + { + type: 'text', + text: 'Response 2', + cache_control: { type: 'ephemeral' }, + }, + ], +}; + +messages.push({ role: 'user', content: 'Message 3' }); + +const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, +}); +``` + +## Best Practices + +### ✅ Do + +- Place `cache_control` on the **last block** of cacheable content +- Cache content >= 1024 tokens (3.5 Sonnet) or >= 2048 tokens (3.5 Haiku) +- Use caching for repeated context (system prompts, documents, code) +- Monitor cache usage in response headers +- Cache conversation history in long chats + +### ❌ Don't + +- Cache content below minimum token threshold +- Place `cache_control` in the middle of text +- Change cached content (breaks cache matching) +- Cache rarely used content (not cost-effective) +- Expect caching to work across different API keys + +## Monitoring Cache Usage + +```typescript +const response = await anthropic.messages.create({...}); + +console.log('Input tokens:', response.usage.input_tokens); +console.log('Cache creation:', response.usage.cache_creation_input_tokens); +console.log('Cache read:', response.usage.cache_read_input_tokens); +console.log('Output tokens:', response.usage.output_tokens); + +// First request +// input_tokens: 1000 +// cache_creation_input_tokens: 5000 +// cache_read_input_tokens: 0 + +// Subsequent requests (within 5 min) +// input_tokens: 1000 +// cache_creation_input_tokens: 0 +// cache_read_input_tokens: 5000 // 90% cost savings! +``` + +## Common Patterns + +### Pattern 1: Document Analysis Chatbot + +```typescript +const document = fs.readFileSync('./document.txt', 'utf-8'); // 10k tokens + +// All requests use same cached document +for (const question of questions) { + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: 'Document:' }, + { + type: 'text', + text: document, + cache_control: { type: 'ephemeral' }, + }, + { type: 'text', text: `Question: ${question}` }, + ], + }, + ], + }); + + // First request: cache_creation_input_tokens: 10000 + // Subsequent: cache_read_input_tokens: 10000 (90% savings) +} +``` + +### Pattern 2: Code Review with Codebase Context + +```typescript +const codebase = await loadCodebase(); // 50k tokens + +const review = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 2048, + system: [ + { type: 'text', text: 'You are a code reviewer.' }, + { + type: 'text', + text: `Codebase context:\n${codebase}`, + cache_control: { type: 'ephemeral' }, + }, + ], + messages: [ + { role: 'user', content: 'Review this PR: ...' } + ], +}); +``` + +### Pattern 3: Customer Support with Knowledge Base + +```typescript +const knowledgeBase = await loadKB(); // 20k tokens + +// Cache persists across all customer queries +const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { type: 'text', text: 'You are a customer support agent.' }, + { + type: 'text', + text: knowledgeBase, + cache_control: { type: 'ephemeral' }, + }, + ], + messages: customerConversation, +}); +``` + +## Troubleshooting + +### Cache Not Activating + +**Problem**: `cache_read_input_tokens` is 0 + +**Solutions**: +1. Ensure content >= 1024 tokens (or 2048 for Haiku) +2. Verify `cache_control` is on **last block** +3. Check content is byte-for-byte identical +4. Confirm requests within 5-minute window + +### Unexpected Cache Misses + +**Problem**: Cache hits intermittently + +**Solutions**: +1. Ensure content doesn't change (even whitespace) +2. Check TTL hasn't expired +3. Verify using same API key +4. Monitor cache headers in responses + +### High Cache Creation Costs + +**Problem**: Frequent `cache_creation_input_tokens` + +**Solutions**: +1. Increase request frequency (use cache before expiry) +2. Consider if caching is cost-effective (need 2+ uses) +3. Extend cache TTL to 1 hour if supported + +## Cost Calculator + +```typescript +function calculateCachingSavings( + cachedTokens: number, + uncachedTokens: number, + requestCount: number +): { + withoutCaching: number; + withCaching: number; + savings: number; + savingsPercent: number; +} { + const inputCostPerMTok = 3; + const cacheCostPerMTok = 3.75; + const cacheReadCostPerMTok = 0.3; + + const withoutCaching = ((cachedTokens + uncachedTokens) / 1_000_000) * + inputCostPerMTok * requestCount; + + const cacheWrite = (cachedTokens / 1_000_000) * cacheCostPerMTok; + const cacheReads = (cachedTokens / 1_000_000) * cacheReadCostPerMTok * (requestCount - 1); + const uncachedInput = (uncachedTokens / 1_000_000) * inputCostPerMTok * requestCount; + const withCaching = cacheWrite + cacheReads + uncachedInput; + + const savings = withoutCaching - withCaching; + const savingsPercent = (savings / withoutCaching) * 100; + + return { withoutCaching, withCaching, savings, savingsPercent }; +} + +// Example: 10k cached tokens, 1k uncached, 20 requests +const result = calculateCachingSavings(10000, 1000, 20); +console.log(`Savings: $${result.savings.toFixed(4)} (${result.savingsPercent.toFixed(1)}%)`); +``` + +## Official Documentation + +- **Prompt Caching Guide**: https://docs.claude.com/en/docs/build-with-claude/prompt-caching +- **Pricing**: https://www.anthropic.com/pricing +- **API Reference**: https://docs.claude.com/en/api/messages diff --git a/references/rate-limits.md b/references/rate-limits.md new file mode 100644 index 0000000..80bdf75 --- /dev/null +++ b/references/rate-limits.md @@ -0,0 +1,80 @@ +# Rate Limits Guide + +Complete guide to Claude API rate limits and how to handle them. + +## Overview + +Claude API uses **token bucket algorithm** for rate limiting: +- Capacity continuously replenishes +- Three types: Requests per minute (RPM), Tokens per minute (TPM), Daily tokens +- Limits vary by account tier and model + +## Rate Limit Tiers + +| Tier | Requirements | Example Limits (Sonnet 3.5) | +|------|--------------|------------------------------| +| Tier 1 | New account | 50 RPM, 40k TPM | +| Tier 2 | $10 spend | 1000 RPM, 100k TPM | +| Tier 3 | $50 spend | 2000 RPM, 200k TPM | +| Tier 4 | $500 spend | 4000 RPM, 400k TPM | + +**Note**: Limits vary by model. Check Console for exact limits. + +## Response Headers + +Every API response includes: + +``` +anthropic-ratelimit-requests-limit: 50 +anthropic-ratelimit-requests-remaining: 49 +anthropic-ratelimit-requests-reset: 2025-10-25T12:00:00Z +anthropic-ratelimit-tokens-limit: 50000 +anthropic-ratelimit-tokens-remaining: 49500 +anthropic-ratelimit-tokens-reset: 2025-10-25T12:01:00Z +``` + +On 429 errors: +``` +retry-after: 60 // Seconds until retry allowed +``` + +## Handling Rate Limits + +### Basic Exponential Backoff + +```typescript +async function withRetry(requestFn, maxRetries = 3) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await requestFn(); + } catch (error) { + if (error.status === 429) { + const delay = 1000 * Math.pow(2, attempt); + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } +} +``` + +### Respecting retry-after Header + +```typescript +const retryAfter = error.response?.headers?.['retry-after']; +const delay = retryAfter ? parseInt(retryAfter) * 1000 : baseDelay; +``` + +## Best Practices + +1. **Monitor headers** - Check remaining requests/tokens +2. **Implement backoff** - Exponential delay on 429 +3. **Respect retry-after** - Use provided wait time +4. **Batch requests** - Group when possible +5. **Use caching** - Reduce duplicate requests +6. **Upgrade tier** - Scale with usage + +## Official Docs + +https://docs.claude.com/en/api/rate-limits diff --git a/references/tool-use-patterns.md b/references/tool-use-patterns.md new file mode 100644 index 0000000..c134593 --- /dev/null +++ b/references/tool-use-patterns.md @@ -0,0 +1,114 @@ +# Tool Use Patterns + +Common patterns for using tools (function calling) with Claude API. + +## Basic Pattern + +```typescript +const tools = [{ + name: 'get_weather', + description: 'Get current weather', + input_schema: { + type: 'object', + properties: { + location: { type: 'string' } + }, + required: ['location'] + } +}]; + +// 1. Send request with tools +const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages: [{ role: 'user', content: 'Weather in NYC?' }] +}); + +// 2. Check if Claude wants to use tools +if (response.stop_reason === 'tool_use') { + for (const block of response.content) { + if (block.type === 'tool_use') { + // 3. Execute tool + const result = await executeToolFunction(block.name, block.input); + + // 4. Return result + const toolResult = { + type: 'tool_result', + tool_use_id: block.id, + content: JSON.stringify(result) + }; + } + } +} +``` + +## Tool Execution Loop + +```typescript +async function chatWithTools(userMessage) { + const messages = [{ role: 'user', content: userMessage }]; + + while (true) { + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages, + }); + + messages.push({ role: 'assistant', content: response.content }); + + if (response.stop_reason === 'tool_use') { + // Execute tools and continue + const toolResults = await executeAllTools(response.content); + messages.push({ role: 'user', content: toolResults }); + } else { + // Final response + return response.content.find(b => b.type === 'text')?.text; + } + } +} +``` + +## With Zod Validation + +```typescript +import { betaZodTool } from '@anthropic-ai/sdk/helpers/zod'; +import { z } from 'zod'; + +const weatherTool = betaZodTool({ + name: 'get_weather', + inputSchema: z.object({ + location: z.string(), + unit: z.enum(['celsius', 'fahrenheit']).optional(), + }), + description: 'Get weather', + run: async (input) => { + return `Weather in ${input.location}: 72°F`; + }, +}); + +// Automatic execution +const finalMessage = await anthropic.beta.messages.toolRunner({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1000, + messages: [{ role: 'user', content: 'Weather in SF?' }], + tools: [weatherTool], +}); +``` + +## Error Handling in Tools + +```typescript +{ + type: 'tool_result', + tool_use_id: block.id, + content: 'Error: API unavailable', + is_error: true // Mark as error +} +``` + +## Official Docs + +https://docs.claude.com/en/docs/build-with-claude/tool-use diff --git a/references/top-errors.md b/references/top-errors.md new file mode 100644 index 0000000..e6ba729 --- /dev/null +++ b/references/top-errors.md @@ -0,0 +1,507 @@ +# Top 12 Common Errors and Solutions + +Complete reference for troubleshooting Claude API errors. + +--- + +## Error #1: Rate Limit 429 - Too Many Requests + +**Error Message:** +``` +429 Too Many Requests: Number of request tokens has exceeded your per-minute rate limit +``` + +**Source**: https://docs.claude.com/en/api/errors + +**Why It Happens:** +- Exceeded requests per minute (RPM) +- Exceeded tokens per minute (TPM) +- Exceeded daily token quota + +**Solution:** +```typescript +async function handleRateLimit(requestFn, maxRetries = 3) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await requestFn(); + } catch (error) { + if (error.status === 429) { + const retryAfter = error.response?.headers?.['retry-after']; + const delay = retryAfter ? parseInt(retryAfter) * 1000 : 1000 * Math.pow(2, attempt); + console.log(`Retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } +} +``` + +**Prevention:** +- Implement exponential backoff +- Respect `retry-after` header +- Monitor rate limit headers +- Upgrade account tier for higher limits + +--- + +## Error #2: Streaming SSE Parsing Errors + +**Error Message:** +``` +Incomplete chunks, malformed events, connection dropped +``` + +**Source**: Community reports, SDK issues + +**Why It Happens:** +- Network interruptions +- Improper SSE event parsing +- Errors occur AFTER initial 200 response + +**Solution:** +```typescript +const stream = anthropic.messages.stream({...}); + +stream + .on('error', (error) => { + console.error('Stream error:', error); + // Implement retry or fallback + }) + .on('abort', (error) => { + console.warn('Stream aborted'); + }) + .on('end', () => { + console.log('Stream completed'); + }); + +await stream.finalMessage(); +``` + +**Prevention:** +- Always implement error event listeners +- Handle stream abortion +- Use SDK helpers (don't parse SSE manually) +- Implement reconnection logic + +--- + +## Error #3: Prompt Caching Not Activating + +**Error Message:** +``` +High costs despite cache_control blocks +cache_read_input_tokens: 0 +``` + +**Source**: https://docs.claude.com/en/docs/build-with-claude/prompt-caching + +**Why It Happens:** +- `cache_control` not on last block +- Content below minimum tokens (1024/2048) +- Content changed (breaks cache match) +- Outside 5-minute TTL + +**Solution:** +```typescript +// ❌ Wrong - cache_control not at end +{ + type: 'text', + text: DOCUMENT, + cache_control: { type: 'ephemeral' }, // Wrong position +}, +{ + type: 'text', + text: 'Additional text', +} + +// ✅ Correct - cache_control at end +{ + type: 'text', + text: DOCUMENT + '\n\nAdditional text', + cache_control: { type: 'ephemeral' }, // Correct position +} +``` + +**Prevention:** +- Place `cache_control` on LAST block +- Ensure content >= 1024 tokens +- Keep cached content identical +- Monitor `cache_read_input_tokens` + +--- + +## Error #4: Tool Use Response Format Errors + +**Error Message:** +``` +invalid_request_error: tools[0].input_schema is invalid +``` + +**Source**: API validation + +**Why It Happens:** +- Invalid JSON Schema +- Missing required fields +- Incorrect tool_use_id in tool_result + +**Solution:** +```typescript +// ✅ Valid tool schema +{ + name: 'get_weather', + description: 'Get current weather', + input_schema: { + type: 'object', // Must be 'object' + properties: { + location: { + type: 'string', // Valid JSON Schema types + description: 'City' // Optional but recommended + } + }, + required: ['location'] // List required fields + } +} + +// ✅ Valid tool result +{ + type: 'tool_result', + tool_use_id: block.id, // Must match tool_use id + content: JSON.stringify(result) // Convert to string +} +``` + +**Prevention:** +- Validate schemas with JSON Schema validator +- Match `tool_use_id` exactly +- Stringify tool results +- Test thoroughly before production + +--- + +## Error #5: Vision Image Format Issues + +**Error Message:** +``` +invalid_request_error: image source must be base64 or url +``` + +**Source**: API documentation + +**Why It Happens:** +- Unsupported image format +- Incorrect base64 encoding +- Invalid media_type + +**Solution:** +```typescript +import fs from 'fs'; + +const imageData = fs.readFileSync('./image.jpg'); +const base64Image = imageData.toString('base64'); + +// ✅ Correct format +{ + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', // Must match actual format + data: base64Image // Pure base64 (no data URI prefix) + } +} +``` + +**Supported Formats:** +- image/jpeg +- image/png +- image/webp +- image/gif + +**Prevention:** +- Validate format before encoding +- Use correct media_type +- Remove data URI prefix if present +- Keep images under 5MB + +--- + +## Error #6: Token Counting Mismatches + +**Error Message:** +``` +invalid_request_error: messages: too many tokens +``` + +**Source**: Token counting differences + +**Why It Happens:** +- Not accounting for special tokens +- Formatting adds hidden tokens +- Context window exceeded + +**Solution:** +```typescript +// Monitor token usage +const response = await anthropic.messages.create({...}); + +console.log('Input tokens:', response.usage.input_tokens); +console.log('Output tokens:', response.usage.output_tokens); +console.log('Total:', response.usage.input_tokens + response.usage.output_tokens); + +// Check against context window +const contextWindow = 200000; // Claude 3.5 Sonnet +if (response.usage.input_tokens > contextWindow) { + console.warn('Approaching context limit'); +} +``` + +**Prevention:** +- Use official token counter +- Monitor usage headers +- Implement message pruning +- Use prompt caching for long context + +--- + +## Error #7: System Prompt Ordering Issues + +**Error Message:** +``` +System prompt ignored or overridden +``` + +**Source**: API behavior + +**Why It Happens:** +- System prompt placed after messages +- System prompt in wrong format + +**Solution:** +```typescript +// ❌ Wrong +const message = await anthropic.messages.create({ + messages: [...], + system: 'You are helpful', // Wrong - after messages +}); + +// ✅ Correct +const message = await anthropic.messages.create({ + system: 'You are helpful', // Correct - before messages + messages: [...], +}); +``` + +**Prevention:** +- Always place `system` before `messages` +- Use system prompt for behavior instructions +- Test system prompt effectiveness + +--- + +## Error #8: Context Window Exceeded + +**Error Message:** +``` +invalid_request_error: messages: too many tokens (210000 > 200000) +``` + +**Source**: Model limits + +**Why It Happens:** +- Long conversations +- Large documents +- Not pruning message history + +**Solution:** +```typescript +function pruneMessages(messages, maxTokens = 150000) { + // Keep most recent messages + let totalTokens = 0; + const prunedMessages = []; + + for (let i = messages.length - 1; i >= 0; i--) { + const msgTokens = estimateTokens(messages[i].content); + if (totalTokens + msgTokens > maxTokens) break; + prunedMessages.unshift(messages[i]); + totalTokens += msgTokens; + } + + return prunedMessages; +} +``` + +**Prevention:** +- Implement message history pruning +- Use summarization for old messages +- Use prompt caching +- Choose model with larger context (3.7 Sonnet: 2M tokens) + +--- + +## Error #9: Extended Thinking on Wrong Model + +**Error Message:** +``` +No thinking blocks in response +``` + +**Source**: Model capabilities + +**Why It Happens:** +- Using Claude 3.5 Sonnet (not supported) +- Should use Claude 3.7 Sonnet or Claude 4 + +**Solution:** +```typescript +// ❌ Wrong model - no extended thinking +model: 'claude-sonnet-4-5-20250929' + +// ✅ Correct models for extended thinking +model: 'claude-3-7-sonnet-20250228' // Has extended thinking +model: 'claude-opus-4-20250514' // Has extended thinking +``` + +**Prevention:** +- Verify model capabilities +- Use 3.7 Sonnet or 4.x models for extended thinking +- Document model requirements + +--- + +## Error #10: API Key Exposure in Client Code + +**Error Message:** +``` +CORS errors, security vulnerability +``` + +**Source**: Security best practices + +**Why It Happens:** +- Making API calls from browser +- API key in client-side code + +**Solution:** +```typescript +// ❌ Never do this +const anthropic = new Anthropic({ + apiKey: 'sk-ant-...', // Exposed in browser! +}); + +// ✅ Use server-side endpoint +async function callClaude(messages) { + const response = await fetch('/api/chat', { + method: 'POST', + body: JSON.stringify({ messages }), + }); + return response.json(); +} +``` + +**Prevention:** +- Server-side only +- Use environment variables +- Implement authentication +- Never expose API key + +--- + +## Error #11: Rate Limit Tier Confusion + +**Error Message:** +``` +Lower limits than expected +``` + +**Source**: Account tier system + +**Why It Happens:** +- Not understanding tier progression +- Expecting higher limits without usage history + +**Solution:** +- Check current tier in Console +- Tiers auto-scale with usage ($10, $50, $500 spend) +- Monitor rate limit headers +- Contact support for custom limits + +**Tier Progression:** +- Tier 1: 50 RPM, 40k TPM +- Tier 2: 1000 RPM, 100k TPM ($10 spend) +- Tier 3: 2000 RPM, 200k TPM ($50 spend) +- Tier 4: 4000 RPM, 400k TPM ($500 spend) + +**Prevention:** +- Review tier requirements +- Plan for gradual scale-up +- Implement proper rate limiting + +--- + +## Error #12: Message Batches Beta Headers Missing + +**Error Message:** +``` +invalid_request_error: unknown parameter: batches +``` + +**Source**: Beta API requirements + +**Why It Happens:** +- Missing `anthropic-beta` header +- Using wrong endpoint + +**Solution:** +```typescript +const response = await fetch('https://api.anthropic.com/v1/messages/batches', { + method: 'POST', + headers: { + 'x-api-key': API_KEY, + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'message-batches-2024-09-24', // Required! + 'content-type': 'application/json', + }, + body: JSON.stringify({...}), +}); +``` + +**Prevention:** +- Include beta headers for beta features +- Check official docs for header requirements +- Test beta features in development first + +--- + +## Quick Diagnosis Checklist + +When encountering errors: + +1. ✅ Check error status code (400, 401, 429, 500, etc.) +2. ✅ Read error message carefully +3. ✅ Verify API key is valid and in environment variable +4. ✅ Confirm model ID is correct +5. ✅ Check request format matches API spec +6. ✅ Monitor rate limit headers +7. ✅ Review recent code changes +8. ✅ Test with minimal example +9. ✅ Check official docs for breaking changes +10. ✅ Search GitHub issues for similar problems + +--- + +## Getting Help + +**Official Resources:** +- **Errors Reference**: https://docs.claude.com/en/api/errors +- **API Documentation**: https://docs.claude.com/en/api +- **Support**: https://support.claude.com/ + +**Community:** +- **GitHub Issues**: https://github.com/anthropics/anthropic-sdk-typescript/issues +- **Developer Forum**: https://support.claude.com/ + +**This Skill:** +- Check other reference files for detailed guides +- Review templates for working examples +- Verify setup checklist in SKILL.md diff --git a/references/vision-capabilities.md b/references/vision-capabilities.md new file mode 100644 index 0000000..23284ff --- /dev/null +++ b/references/vision-capabilities.md @@ -0,0 +1,108 @@ +# Vision Capabilities + +Guide to using Claude's vision capabilities for image understanding. + +## Supported Formats + +- **JPEG** (image/jpeg) +- **PNG** (image/png) +- **WebP** (image/webp) +- **GIF** (image/gif) - non-animated only + +**Max size**: 5MB per image + +## Basic Usage + +```typescript +import fs from 'fs'; + +const imageData = fs.readFileSync('./image.jpg').toString('base64'); + +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: imageData + } + }, + { + type: 'text', + text: 'What is in this image?' + } + ] + }] +}); +``` + +## Multiple Images + +```typescript +content: [ + { type: 'text', text: 'Compare these images:' }, + { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: img1 } }, + { type: 'image', source: { type: 'base64', media_type: 'image/png', data: img2 } }, + { type: 'text', text: 'What are the differences?' } +] +``` + +## With Tools + +```typescript +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools: [searchProductTool], + messages: [{ + role: 'user', + content: [ + { type: 'image', source: {...} }, + { type: 'text', text: 'Find similar products' } + ] + }] +}); +``` + +## With Prompt Caching + +```typescript +{ + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: imageData + }, + cache_control: { type: 'ephemeral' } // Cache image +} +``` + +## Validation + +```typescript +function validateImage(path: string) { + const stats = fs.statSync(path); + const sizeMB = stats.size / (1024 * 1024); + + if (sizeMB > 5) { + throw new Error('Image exceeds 5MB'); + } + + const ext = path.split('.').pop()?.toLowerCase(); + if (!['jpg', 'jpeg', 'png', 'webp', 'gif'].includes(ext)) { + throw new Error('Unsupported format'); + } + + return true; +} +``` + +## Official Docs + +https://docs.claude.com/en/docs/build-with-claude/vision diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh new file mode 100755 index 0000000..e341768 --- /dev/null +++ b/scripts/check-versions.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# Check Claude API package versions +# Usage: ./scripts/check-versions.sh + +echo "=== Claude API Package Version Checker ===" +echo "" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + echo -e "${RED}Error: npm is not installed${NC}" + exit 1 +fi + +# Check @anthropic-ai/sdk +echo "Checking @anthropic-ai/sdk..." +CURRENT_VERSION=$(npm view @anthropic-ai/sdk version 2>/dev/null) + +if [ -z "$CURRENT_VERSION" ]; then + echo -e "${RED}Error: Could not fetch version info${NC}" +else + echo -e "${GREEN}Latest version: $CURRENT_VERSION${NC}" + + # Expected version from skill + EXPECTED="0.67.0" + + if [ "$CURRENT_VERSION" != "$EXPECTED" ]; then + echo -e "${YELLOW}Note: Skill was verified with version $EXPECTED${NC}" + echo -e "${YELLOW}Latest version is $CURRENT_VERSION${NC}" + echo "" + echo "To update skill documentation, run:" + echo " npm view @anthropic-ai/sdk version" + else + echo -e "${GREEN}✓ Version matches skill documentation${NC}" + fi +fi + +echo "" + +# Check other related packages +echo "Checking optional dependencies..." +echo "" + +packages=("zod" "@types/node" "typescript") + +for package in "${packages[@]}"; do + echo "- $package:" + VERSION=$(npm view $package version 2>/dev/null) + if [ -z "$VERSION" ]; then + echo -e " ${YELLOW}Could not fetch version${NC}" + else + echo -e " ${GREEN}Latest: $VERSION${NC}" + fi +done + +echo "" + +# Check if package.json exists locally +if [ -f "package.json" ]; then + echo "Checking local package.json..." + echo "" + + if command -v jq &> /dev/null; then + # Use jq if available + ANTHROPIC_VERSION=$(jq -r '.dependencies."@anthropic-ai/sdk"' package.json 2>/dev/null) + + if [ "$ANTHROPIC_VERSION" != "null" ] && [ ! -z "$ANTHROPIC_VERSION" ]; then + echo "@anthropic-ai/sdk: $ANTHROPIC_VERSION" + fi + else + # Fallback to grep + grep -E '"@anthropic-ai/sdk"' package.json + fi + + echo "" +fi + +# Check for breaking changes +echo "=== Checking for breaking changes ===" +echo "" +echo "Official changelog:" +echo "https://github.com/anthropics/anthropic-sdk-typescript/releases" +echo "" + +# Check npm for recent updates +echo "Recent versions:" +npm view @anthropic-ai/sdk versions --json | tail -10 + +echo "" +echo "=== Version Check Complete ===" +echo "" +echo "To update your local installation:" +echo " npm install @anthropic-ai/sdk@latest" +echo "" +echo "To check what would be installed:" +echo " npm outdated" diff --git a/templates/basic-chat.ts b/templates/basic-chat.ts new file mode 100644 index 0000000..4e6de28 --- /dev/null +++ b/templates/basic-chat.ts @@ -0,0 +1,115 @@ +import Anthropic from '@anthropic-ai/sdk'; + +// Initialize the client +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY || '', +}); + +async function basicChat() { + try { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: 'Hello, Claude! Tell me a fun fact about TypeScript.', + }, + ], + }); + + // Extract text from response + const textContent = message.content.find(block => block.type === 'text'); + if (textContent && textContent.type === 'text') { + console.log('Claude:', textContent.text); + } + + // Log usage information + console.log('\nUsage:'); + console.log('- Input tokens:', message.usage.input_tokens); + console.log('- Output tokens:', message.usage.output_tokens); + } catch (error) { + if (error instanceof Anthropic.APIError) { + console.error(`API Error [${error.status}]:`, error.message); + } else { + console.error('Unexpected error:', error); + } + } +} + +// Multi-turn conversation example +async function multiTurnChat() { + const messages: Array<{ role: 'user' | 'assistant'; content: string }> = []; + + // First turn + messages.push({ + role: 'user', + content: 'What is the capital of France?', + }); + + const response1 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + const text1 = response1.content.find(b => b.type === 'text'); + if (text1 && text1.type === 'text') { + messages.push({ role: 'assistant', content: text1.text }); + console.log('Claude:', text1.text); + } + + // Second turn + messages.push({ + role: 'user', + content: 'What is its population?', + }); + + const response2 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + const text2 = response2.content.find(b => b.type === 'text'); + if (text2 && text2.type === 'text') { + console.log('Claude:', text2.text); + } +} + +// System prompt example +async function chatWithSystemPrompt() { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: 'You are a helpful Python coding assistant. Always provide type hints and docstrings.', + messages: [ + { + role: 'user', + content: 'Write a function to calculate the factorial of a number.', + }, + ], + }); + + const textContent = message.content.find(block => block.type === 'text'); + if (textContent && textContent.type === 'text') { + console.log(textContent.text); + } +} + +// Run examples +if (require.main === module) { + console.log('=== Basic Chat ===\n'); + basicChat() + .then(() => { + console.log('\n=== Multi-turn Chat ===\n'); + return multiTurnChat(); + }) + .then(() => { + console.log('\n=== Chat with System Prompt ===\n'); + return chatWithSystemPrompt(); + }) + .catch(console.error); +} + +export { basicChat, multiTurnChat, chatWithSystemPrompt }; diff --git a/templates/cloudflare-worker.ts b/templates/cloudflare-worker.ts new file mode 100644 index 0000000..e69993a --- /dev/null +++ b/templates/cloudflare-worker.ts @@ -0,0 +1,268 @@ +// Cloudflare Worker with Claude API +// Uses fetch API (no SDK needed) + +export interface Env { + ANTHROPIC_API_KEY: string; +} + +// Basic chat endpoint +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + // CORS headers + const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }; + + if (request.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + const { messages } = await request.json<{ messages: any[] }>(); + + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': env.ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }), + }); + + const data = await response.json(); + + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + ...corsHeaders, + }, + }); + } catch (error) { + return new Response( + JSON.stringify({ error: error.message }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + ...corsHeaders, + }, + } + ); + } + }, +}; + +// Streaming endpoint example +export const streamingEndpoint = { + async fetch(request: Request, env: Env): Promise { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + const { messages } = await request.json<{ messages: any[] }>(); + + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': env.ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + stream: true, + }), + }); + + // Return SSE stream directly + return new Response(response.body, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (error) { + return new Response( + JSON.stringify({ error: error.message }), + { status: 500 } + ); + } + }, +}; + +// With rate limiting using Durable Objects +interface RateLimiterState { + requests: number; + resetTime: number; +} + +export class RateLimiter implements DurableObject { + state: DurableObjectState; + storage: DurableObjectStorage; + + constructor(state: DurableObjectState, env: Env) { + this.state = state; + this.storage = state.storage; + } + + async fetch(request: Request): Promise { + const now = Date.now(); + const limitData = await this.storage.get('limiter') || { + requests: 0, + resetTime: now + 60000, // 1 minute + }; + + // Reset if time window expired + if (now > limitData.resetTime) { + limitData.requests = 0; + limitData.resetTime = now + 60000; + } + + // Check limit (e.g., 10 requests per minute) + if (limitData.requests >= 10) { + return new Response('Rate limit exceeded', { status: 429 }); + } + + limitData.requests++; + await this.storage.put('limiter', limitData); + + return new Response('OK', { status: 200 }); + } +} + +// Complete worker with error handling and rate limiting +export const productionWorker = { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(request.url); + + // Route handling + if (url.pathname === '/chat') { + return handleChat(request, env); + } + + if (url.pathname === '/stream') { + return handleStream(request, env); + } + + return new Response('Not Found', { status: 404 }); + }, +}; + +async function handleChat(request: Request, env: Env): Promise { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + const { messages } = await request.json<{ messages: any[] }>(); + + // Validate input + if (!Array.isArray(messages) || messages.length === 0) { + return new Response( + JSON.stringify({ error: 'Invalid messages array' }), + { status: 400 } + ); + } + + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': env.ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }), + }); + + if (!response.ok) { + const error = await response.text(); + console.error('Claude API error:', error); + return new Response( + JSON.stringify({ error: 'API request failed' }), + { status: response.status } + ); + } + + const data = await response.json(); + + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (error) { + console.error('Worker error:', error); + return new Response( + JSON.stringify({ error: 'Internal server error' }), + { status: 500 } + ); + } +} + +async function handleStream(request: Request, env: Env): Promise { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + const { messages } = await request.json<{ messages: any[] }>(); + + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': env.ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + stream: true, + }), + }); + + if (!response.ok) { + return new Response( + JSON.stringify({ error: 'Stream failed' }), + { status: response.status } + ); + } + + return new Response(response.body, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (error) { + console.error('Stream error:', error); + return new Response( + JSON.stringify({ error: 'Stream error' }), + { status: 500 } + ); + } +} diff --git a/templates/error-handling.ts b/templates/error-handling.ts new file mode 100644 index 0000000..6842499 --- /dev/null +++ b/templates/error-handling.ts @@ -0,0 +1,373 @@ +import Anthropic from '@anthropic-ai/sdk'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +// Example 1: Basic error handling +async function basicErrorHandling(prompt: string) { + try { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: prompt }], + }); + + return message; + } catch (error) { + if (error instanceof Anthropic.APIError) { + console.error(`API Error [${error.status}]:`, error.message); + console.error('Error type:', error.type); + console.error('Error details:', error.error); + + // Handle specific error types + switch (error.status) { + case 400: + console.error('Invalid request. Check your parameters.'); + break; + case 401: + console.error('Authentication failed. Check your API key.'); + break; + case 403: + console.error('Permission denied. Check your account tier.'); + break; + case 404: + console.error('Resource not found. Check the endpoint.'); + break; + case 429: + console.error('Rate limit exceeded. Implement retry logic.'); + break; + case 500: + console.error('Server error. Retry with exponential backoff.'); + break; + case 529: + console.error('API overloaded. Retry later.'); + break; + default: + console.error('Unexpected error occurred.'); + } + } else { + console.error('Non-API error:', error); + } + + throw error; + } +} + +// Example 2: Rate limit handler with retry +async function handleRateLimits( + requestFn: () => Promise, + maxRetries = 3, + baseDelay = 1000 +): Promise { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await requestFn(); + } catch (error) { + if (error instanceof Anthropic.APIError && error.status === 429) { + // Check retry-after header + const retryAfter = error.response?.headers?.['retry-after']; + const delay = retryAfter + ? parseInt(retryAfter) * 1000 + : baseDelay * Math.pow(2, attempt); + + if (attempt < maxRetries - 1) { + console.warn(`Rate limited. Retrying in ${delay}ms... (Attempt ${attempt + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + } + + throw error; + } + } + + throw new Error('Max retries exceeded'); +} + +// Example 3: Comprehensive error handler +class APIErrorHandler { + private maxRetries: number; + private baseDelay: number; + private onError?: (error: Error) => void; + + constructor(options: { + maxRetries?: number; + baseDelay?: number; + onError?: (error: Error) => void; + } = {}) { + this.maxRetries = options.maxRetries || 3; + this.baseDelay = options.baseDelay || 1000; + this.onError = options.onError; + } + + async execute(requestFn: () => Promise): Promise { + for (let attempt = 0; attempt < this.maxRetries; attempt++) { + try { + return await requestFn(); + } catch (error) { + if (this.onError) { + this.onError(error); + } + + if (error instanceof Anthropic.APIError) { + if (this.shouldRetry(error) && attempt < this.maxRetries - 1) { + const delay = this.calculateDelay(error, attempt); + console.warn(`Retrying after ${delay}ms... (${attempt + 1}/${this.maxRetries})`); + await this.sleep(delay); + continue; + } + } + + throw this.enhanceError(error); + } + } + + throw new Error('Max retries exceeded'); + } + + private shouldRetry(error: Anthropic.APIError): boolean { + // Retry on rate limits, server errors, and overload + return error.status === 429 || error.status === 500 || error.status === 529; + } + + private calculateDelay(error: Anthropic.APIError, attempt: number): number { + // Use retry-after header if available + const retryAfter = error.response?.headers?.['retry-after']; + if (retryAfter) { + return parseInt(retryAfter) * 1000; + } + + // Exponential backoff + return this.baseDelay * Math.pow(2, attempt); + } + + private enhanceError(error: any): Error { + if (error instanceof Anthropic.APIError) { + const enhancedError = new Error(`Claude API Error: ${error.message}`); + (enhancedError as any).originalError = error; + (enhancedError as any).status = error.status; + (enhancedError as any).type = error.type; + return enhancedError; + } + + return error; + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} + +// Example 4: Streaming error handling +async function streamWithErrorHandling(prompt: string) { + try { + const stream = anthropic.messages.stream({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: prompt }], + }); + + let hasError = false; + + stream.on('error', (error) => { + hasError = true; + console.error('Stream error:', error); + + if (error instanceof Anthropic.APIError) { + console.error(`Status: ${error.status}`); + console.error(`Type: ${error.type}`); + } + + // Implement fallback or retry logic here + }); + + stream.on('abort', (error) => { + console.warn('Stream aborted:', error); + }); + + stream.on('text', (text) => { + if (!hasError) { + process.stdout.write(text); + } + }); + + await stream.finalMessage(); + + if (hasError) { + throw new Error('Stream completed with errors'); + } + } catch (error) { + console.error('Failed to complete stream:', error); + throw error; + } +} + +// Example 5: Validation errors +function validateRequest(params: { + messages: any[]; + max_tokens?: number; + model?: string; +}): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!Array.isArray(params.messages) || params.messages.length === 0) { + errors.push('Messages must be a non-empty array'); + } + + if (params.max_tokens && (params.max_tokens < 1 || params.max_tokens > 8192)) { + errors.push('max_tokens must be between 1 and 8192'); + } + + if (params.model && !params.model.startsWith('claude-')) { + errors.push('Invalid model name'); + } + + for (const [index, message] of params.messages.entries()) { + if (!message.role || !['user', 'assistant'].includes(message.role)) { + errors.push(`Message ${index}: Invalid role. Must be "user" or "assistant"`); + } + + if (!message.content) { + errors.push(`Message ${index}: Missing content`); + } + } + + return { + valid: errors.length === 0, + errors, + }; +} + +// Example 6: Circuit breaker pattern +class CircuitBreaker { + private failures: number = 0; + private lastFailureTime: number = 0; + private state: 'closed' | 'open' | 'half-open' = 'closed'; + private readonly threshold: number; + private readonly timeout: number; + + constructor(options: { threshold?: number; timeout?: number } = {}) { + this.threshold = options.threshold || 5; + this.timeout = options.timeout || 60000; // 1 minute + } + + async execute(requestFn: () => Promise): Promise { + if (this.state === 'open') { + if (Date.now() - this.lastFailureTime > this.timeout) { + console.log('Circuit breaker: Transitioning to half-open'); + this.state = 'half-open'; + } else { + throw new Error('Circuit breaker is open. Service unavailable.'); + } + } + + try { + const result = await requestFn(); + + // Success - reset failures + if (this.state === 'half-open') { + console.log('Circuit breaker: Transitioning to closed'); + this.state = 'closed'; + } + this.failures = 0; + + return result; + } catch (error) { + this.failures++; + this.lastFailureTime = Date.now(); + + if (this.failures >= this.threshold) { + console.error(`Circuit breaker: Opening after ${this.failures} failures`); + this.state = 'open'; + } + + throw error; + } + } + + getState(): { state: string; failures: number } { + return { + state: this.state, + failures: this.failures, + }; + } +} + +// Example 7: Usage with all patterns +async function robustAPICall(prompt: string) { + const errorHandler = new APIErrorHandler({ + maxRetries: 3, + baseDelay: 1000, + onError: (error) => { + console.error('Error logged:', error); + // Could send to monitoring service here + }, + }); + + const circuitBreaker = new CircuitBreaker({ + threshold: 5, + timeout: 60000, + }); + + try { + const validation = validateRequest({ + messages: [{ role: 'user', content: prompt }], + max_tokens: 1024, + model: 'claude-sonnet-4-5-20250929', + }); + + if (!validation.valid) { + throw new Error(`Validation failed: ${validation.errors.join(', ')}`); + } + + const result = await circuitBreaker.execute(() => + errorHandler.execute(() => + anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: prompt }], + }) + ) + ); + + return result; + } catch (error) { + console.error('Robust API call failed:', error); + console.error('Circuit breaker state:', circuitBreaker.getState()); + throw error; + } +} + +// Run examples +if (require.main === module) { + console.log('=== Error Handling Examples ===\n'); + + // Test basic error handling + basicErrorHandling('Hello, Claude!') + .then(() => { + console.log('\n=== Testing Rate Limit Handler ===\n'); + return handleRateLimits(() => + anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Test message' }], + }) + ); + }) + .then(() => { + console.log('\n=== Testing Robust API Call ===\n'); + return robustAPICall('What is 2+2?'); + }) + .catch(console.error); +} + +export { + basicErrorHandling, + handleRateLimits, + APIErrorHandler, + streamWithErrorHandling, + validateRequest, + CircuitBreaker, + robustAPICall, +}; diff --git a/templates/extended-thinking.ts b/templates/extended-thinking.ts new file mode 100644 index 0000000..c6f9b03 --- /dev/null +++ b/templates/extended-thinking.ts @@ -0,0 +1,320 @@ +import Anthropic from '@anthropic-ai/sdk'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY || '', +}); + +/** + * IMPORTANT: Extended thinking is ONLY available in: + * - Claude 3.7 Sonnet (claude-3-7-sonnet-20250228) + * - Claude 4 models (Opus 4, Sonnet 4) + * + * NOT available in Claude 3.5 Sonnet + */ + +// Example 1: Basic extended thinking +async function basicExtendedThinking() { + const message = await anthropic.messages.create({ + model: 'claude-3-7-sonnet-20250228', // Must use 3.7 or 4.x + max_tokens: 4096, // Higher token limit for thinking + messages: [ + { + role: 'user', + content: `A ball is thrown upward with an initial velocity of 20 m/s. + How high does it go? (Use g = 9.8 m/s²)`, + }, + ], + }); + + console.log('=== Response with Extended Thinking ===\n'); + + // Display thinking blocks separately from answer + for (const block of message.content) { + if (block.type === 'thinking') { + console.log('🤔 Claude is thinking:'); + console.log(block.text); + console.log('\n' + '='.repeat(50) + '\n'); + } else if (block.type === 'text') { + console.log('💡 Final Answer:'); + console.log(block.text); + } + } + + console.log('\nStop reason:', message.stop_reason); + console.log('Token usage:', message.usage); +} + +// Example 2: Complex problem solving +async function complexProblemSolving() { + const message = await anthropic.messages.create({ + model: 'claude-3-7-sonnet-20250228', + max_tokens: 8192, // Even higher for complex reasoning + messages: [ + { + role: 'user', + content: `Debug this Python code and explain what's wrong: + +def fibonacci(n): + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-1) + +print(fibonacci(10)) + +Why is it slow and what's the correct implementation?`, + }, + ], + }); + + for (const block of message.content) { + if (block.type === 'thinking') { + console.log('🔍 Debugging process:'); + console.log(block.text); + console.log(); + } else if (block.type === 'text') { + console.log('✅ Solution:'); + console.log(block.text); + } + } +} + +// Example 3: Multi-step reasoning +async function multiStepReasoning() { + const message = await anthropic.messages.create({ + model: 'claude-3-7-sonnet-20250228', + max_tokens: 6144, + messages: [ + { + role: 'user', + content: `I have a 10-liter jug and a 6-liter jug. How can I measure exactly 8 liters of water? + Think through this step by step.`, + }, + ], + }); + + for (const block of message.content) { + if (block.type === 'thinking') { + console.log('🧠 Reasoning steps:'); + console.log(block.text); + console.log(); + } else if (block.type === 'text') { + console.log('📝 Final solution:'); + console.log(block.text); + } + } +} + +// Example 4: Comparing with and without extended thinking +async function compareThinkingModes() { + const problem = 'What is the sum of all prime numbers less than 100?'; + + // Without extended thinking (Claude 3.5 Sonnet) + console.log('=== Without Extended Thinking (Claude 3.5 Sonnet) ===\n'); + + const response1 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 2048, + messages: [{ role: 'user', content: problem }], + }); + + const text1 = response1.content.find(b => b.type === 'text'); + if (text1 && text1.type === 'text') { + console.log(text1.text); + } + console.log('\nTokens used:', response1.usage.input_tokens + response1.usage.output_tokens); + + // With extended thinking (Claude 3.7 Sonnet) + console.log('\n\n=== With Extended Thinking (Claude 3.7 Sonnet) ===\n'); + + const response2 = await anthropic.messages.create({ + model: 'claude-3-7-sonnet-20250228', + max_tokens: 4096, + messages: [{ role: 'user', content: problem }], + }); + + for (const block of response2.content) { + if (block.type === 'thinking') { + console.log('🤔 Thinking process:'); + console.log(block.text); + console.log(); + } else if (block.type === 'text') { + console.log('💡 Answer:'); + console.log(block.text); + } + } + console.log('\nTokens used:', response2.usage.input_tokens + response2.usage.output_tokens); +} + +// Example 5: Extended thinking with tools +async function extendedThinkingWithTools() { + const tools: Anthropic.Tool[] = [ + { + name: 'calculate', + description: 'Perform mathematical calculations', + input_schema: { + type: 'object', + properties: { + expression: { + type: 'string', + description: 'Mathematical expression to evaluate', + }, + }, + required: ['expression'], + }, + }, + ]; + + const messages: Anthropic.MessageParam[] = [ + { + role: 'user', + content: + 'Calculate the compound interest on $1000 invested at 5% annual interest for 10 years, compounded monthly', + }, + ]; + + const response = await anthropic.messages.create({ + model: 'claude-3-7-sonnet-20250228', + max_tokens: 4096, + tools, + messages, + }); + + console.log('=== Extended Thinking with Tools ===\n'); + + for (const block of response.content) { + if (block.type === 'thinking') { + console.log('🤔 Planning:'); + console.log(block.text); + console.log(); + } else if (block.type === 'tool_use') { + console.log('🔧 Tool use:', block.name); + console.log('Parameters:', block.input); + console.log(); + } else if (block.type === 'text') { + console.log('💡 Response:'); + console.log(block.text); + } + } +} + +// Example 6: Error when using wrong model +async function demonstrateWrongModelError() { + try { + console.log('=== Attempting extended thinking on Claude 3.5 Sonnet ===\n'); + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', // Wrong model! + max_tokens: 4096, + messages: [ + { + role: 'user', + content: 'Solve this complex math problem step by step', + }, + ], + }); + + // No thinking blocks will be present + const hasThinking = message.content.some(block => block.type === 'thinking'); + + if (!hasThinking) { + console.log('⚠️ No thinking blocks found!'); + console.log('Extended thinking is only available in Claude 3.7 Sonnet or Claude 4 models.'); + } + + for (const block of message.content) { + if (block.type === 'text') { + console.log('Regular response:', block.text); + } + } + } catch (error) { + console.error('Error:', error); + } +} + +// Example 7: Check model capabilities +function getModelCapabilities(modelId: string): { + supportsExtendedThinking: boolean; + contextWindow: number; +} { + const models: Record< + string, + { supportsExtendedThinking: boolean; contextWindow: number } + > = { + 'claude-sonnet-4-5-20250929': { + supportsExtendedThinking: false, + contextWindow: 200_000, + }, + 'claude-3-7-sonnet-20250228': { + supportsExtendedThinking: true, + contextWindow: 2_000_000, + }, + 'claude-opus-4-20250514': { + supportsExtendedThinking: true, + contextWindow: 200_000, + }, + 'claude-3-5-haiku-20241022': { + supportsExtendedThinking: false, + contextWindow: 200_000, + }, + }; + + return ( + models[modelId] || { + supportsExtendedThinking: false, + contextWindow: 200_000, + } + ); +} + +// Helper: Validate model for extended thinking +function validateModelForExtendedThinking(modelId: string): void { + const capabilities = getModelCapabilities(modelId); + + if (!capabilities.supportsExtendedThinking) { + throw new Error( + `Model ${modelId} does not support extended thinking. Use Claude 3.7 Sonnet or Claude 4 models.` + ); + } + + console.log(`✅ Model ${modelId} supports extended thinking`); + console.log(`Context window: ${capabilities.contextWindow.toLocaleString()} tokens`); +} + +// Run examples +if (require.main === module) { + console.log('=== Extended Thinking Examples ===\n'); + + // Validate model first + try { + validateModelForExtendedThinking('claude-3-7-sonnet-20250228'); + } catch (error) { + console.error(error.message); + process.exit(1); + } + + basicExtendedThinking() + .then(() => { + console.log('\n\n=== Complex Problem ===\n'); + return complexProblemSolving(); + }) + .then(() => { + console.log('\n\n=== Multi-step Reasoning ===\n'); + return multiStepReasoning(); + }) + .then(() => { + console.log('\n\n=== Wrong Model Demo ===\n'); + return demonstrateWrongModelError(); + }) + .catch(console.error); +} + +export { + basicExtendedThinking, + complexProblemSolving, + multiStepReasoning, + compareThinkingModes, + extendedThinkingWithTools, + demonstrateWrongModelError, + getModelCapabilities, + validateModelForExtendedThinking, +}; diff --git a/templates/nextjs-api-route.ts b/templates/nextjs-api-route.ts new file mode 100644 index 0000000..708c312 --- /dev/null +++ b/templates/nextjs-api-route.ts @@ -0,0 +1,304 @@ +// Next.js API Routes for Claude API + +// ============================================ +// App Router (app/api/chat/route.ts) +// ============================================ + +import Anthropic from '@anthropic-ai/sdk'; +import { NextRequest, NextResponse } from 'next/server'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +// POST /api/chat - Non-streaming +export async function POST(request: NextRequest) { + try { + const { messages } = await request.json(); + + // Validate input + if (!Array.isArray(messages) || messages.length === 0) { + return NextResponse.json({ error: 'Invalid messages' }, { status: 400 }); + } + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + return NextResponse.json(message); + } catch (error) { + console.error('Chat error:', error); + + if (error instanceof Anthropic.APIError) { + return NextResponse.json( + { error: error.message }, + { status: error.status || 500 } + ); + } + + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +// ============================================ +// App Router with Streaming (app/api/stream/route.ts) +// ============================================ + +export async function POST_STREAMING(request: NextRequest) { + try { + const { messages } = await request.json(); + + const stream = anthropic.messages.stream({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + // Convert to ReadableStream + const readableStream = new ReadableStream({ + async start(controller) { + try { + for await (const event of stream) { + if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { + const text = event.delta.text; + controller.enqueue(new TextEncoder().encode(text)); + } + + if (event.type === 'message_stop') { + controller.close(); + } + } + } catch (error) { + console.error('Stream error:', error); + controller.error(error); + } + }, + }); + + return new Response(readableStream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }); + } catch (error) { + console.error('Stream setup error:', error); + return NextResponse.json({ error: 'Stream failed' }, { status: 500 }); + } +} + +// ============================================ +// Pages Router (pages/api/chat.ts) +// ============================================ + +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { messages } = req.body; + + if (!Array.isArray(messages)) { + return res.status(400).json({ error: 'Invalid request' }); + } + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + res.status(200).json(message); + } catch (error) { + console.error('API error:', error); + + if (error instanceof Anthropic.APIError) { + return res.status(error.status || 500).json({ error: error.message }); + } + + res.status(500).json({ error: 'Internal server error' }); + } +} + +// ============================================ +// Pages Router with Streaming (pages/api/stream.ts) +// ============================================ + +export async function streamHandler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { messages } = req.body; + + // Set SSE headers + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + const stream = anthropic.messages.stream({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + stream.on('text', (text) => { + res.write(`data: ${JSON.stringify({ text })}\n\n`); + }); + + stream.on('error', (error) => { + console.error('Stream error:', error); + res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); + res.end(); + }); + + stream.on('end', () => { + res.write('data: [DONE]\n\n'); + res.end(); + }); + + await stream.finalMessage(); + } catch (error) { + console.error('Stream setup error:', error); + res.status(500).json({ error: 'Stream failed' }); + } +} + +// ============================================ +// With Tool Use (App Router) +// ============================================ + +export async function POST_WITH_TOOLS(request: NextRequest) { + try { + const { messages } = await request.json(); + + const tools: Anthropic.Tool[] = [ + { + name: 'get_weather', + description: 'Get the current weather', + input_schema: { + type: 'object', + properties: { + location: { type: 'string' }, + }, + required: ['location'], + }, + }, + ]; + + let conversationMessages = messages; + + while (true) { + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages: conversationMessages, + }); + + conversationMessages.push({ + role: 'assistant', + content: response.content, + }); + + if (response.stop_reason === 'tool_use') { + const toolResults = []; + + for (const block of response.content) { + if (block.type === 'tool_use') { + // Execute tool + const result = await executeToolFunction(block.name, block.input); + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + content: JSON.stringify(result), + }); + } + } + + conversationMessages.push({ + role: 'user', + content: toolResults, + }); + } else { + // Final response + return NextResponse.json(response); + } + } + } catch (error) { + console.error('Tool use error:', error); + return NextResponse.json({ error: 'Tool execution failed' }, { status: 500 }); + } +} + +async function executeToolFunction(name: string, input: any): Promise { + if (name === 'get_weather') { + // Mock implementation + return { temperature: 72, condition: 'Sunny' }; + } + return { error: 'Unknown tool' }; +} + +// ============================================ +// With Prompt Caching (App Router) +// ============================================ + +export async function POST_WITH_CACHING(request: NextRequest) { + try { + const { messages, systemPrompt } = await request.json(); + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: systemPrompt, + cache_control: { type: 'ephemeral' }, + }, + ], + messages, + }); + + return NextResponse.json({ + message, + usage: message.usage, + }); + } catch (error) { + console.error('Caching error:', error); + return NextResponse.json({ error: 'Request failed' }, { status: 500 }); + } +} + +// ============================================ +// Middleware for Rate Limiting +// ============================================ + +import { Ratelimit } from '@upstash/ratelimit'; +import { Redis } from '@upstash/redis'; + +const ratelimit = new Ratelimit({ + redis: Redis.fromEnv(), + limiter: Ratelimit.slidingWindow(10, '1 m'), +}); + +export async function POST_WITH_RATE_LIMIT(request: NextRequest) { + // Get identifier (IP or user ID) + const identifier = request.ip ?? 'anonymous'; + + const { success } = await ratelimit.limit(identifier); + + if (!success) { + return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 }); + } + + // Continue with normal handler + return POST(request); +} diff --git a/templates/nodejs-example.ts b/templates/nodejs-example.ts new file mode 100644 index 0000000..c5452e6 --- /dev/null +++ b/templates/nodejs-example.ts @@ -0,0 +1,381 @@ +// Complete Node.js examples for Claude API + +import Anthropic from '@anthropic-ai/sdk'; +import * as dotenv from 'dotenv'; +import * as readline from 'readline'; + +dotenv.config(); + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY, +}); + +// Example 1: Simple CLI chatbot +async function simpleCLIChatbot() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const messages: Anthropic.MessageParam[] = []; + + console.log('Claude CLI Chatbot (type "exit" to quit)\n'); + + const chat = async () => { + rl.question('You: ', async (userInput) => { + if (userInput.toLowerCase() === 'exit') { + console.log('Goodbye!'); + rl.close(); + return; + } + + messages.push({ role: 'user', content: userInput }); + + try { + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + const textContent = response.content.find(b => b.type === 'text'); + if (textContent && textContent.type === 'text') { + console.log(`\nClaude: ${textContent.text}\n`); + messages.push({ role: 'assistant', content: textContent.text }); + } + } catch (error) { + console.error('Error:', error.message); + } + + chat(); + }); + }; + + chat(); +} + +// Example 2: Streaming CLI chatbot +async function streamingCLIChatbot() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + console.log('Streaming Claude CLI Chatbot (type "exit" to quit)\n'); + + const chat = async () => { + rl.question('You: ', async (userInput) => { + if (userInput.toLowerCase() === 'exit') { + console.log('Goodbye!'); + rl.close(); + return; + } + + try { + process.stdout.write('\nClaude: '); + + const stream = anthropic.messages.stream({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: userInput }], + }); + + let fullText = ''; + + stream.on('text', (text) => { + process.stdout.write(text); + fullText += text; + }); + + await stream.finalMessage(); + console.log('\n'); + } catch (error) { + console.error('\nError:', error.message); + } + + chat(); + }); + }; + + chat(); +} + +// Example 3: Batch processing from file +import * as fs from 'fs'; + +async function batchProcessing(inputFile: string, outputFile: string) { + const lines = fs.readFileSync(inputFile, 'utf-8').split('\n').filter(Boolean); + const results = []; + + console.log(`Processing ${lines.length} prompts...`); + + for (let i = 0; i < lines.length; i++) { + const prompt = lines[i]; + console.log(`\n[${i + 1}/${lines.length}] Processing: ${prompt.substring(0, 50)}...`); + + try { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: prompt }], + }); + + const textContent = message.content.find(b => b.type === 'text'); + if (textContent && textContent.type === 'text') { + results.push({ + prompt, + response: textContent.text, + tokens: message.usage, + }); + } + + // Rate limiting pause + await new Promise(resolve => setTimeout(resolve, 1000)); + } catch (error) { + console.error(`Error processing prompt ${i + 1}:`, error.message); + results.push({ + prompt, + error: error.message, + }); + } + } + + fs.writeFileSync(outputFile, JSON.stringify(results, null, 2)); + console.log(`\nResults written to ${outputFile}`); +} + +// Example 4: Document summarization +async function summarizeDocument(filePath: string) { + const document = fs.readFileSync(filePath, 'utf-8'); + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 2048, + system: 'You are an expert document summarizer. Provide concise, accurate summaries.', + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'Summarize the following document in 3-5 bullet points:', + }, + { + type: 'text', + text: document, + cache_control: { type: 'ephemeral' }, + }, + ], + }, + ], + }); + + const textContent = message.content.find(b => b.type === 'text'); + if (textContent && textContent.type === 'text') { + console.log('Summary:'); + console.log(textContent.text); + console.log('\nToken usage:', message.usage); + } +} + +// Example 5: Code review assistant +async function codeReview(codeContent: string, language: string) { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 2048, + system: `You are an expert ${language} code reviewer. Analyze code for: +- Bugs and potential issues +- Performance optimizations +- Security vulnerabilities +- Best practices +- Code style and readability`, + messages: [ + { + role: 'user', + content: `Review this ${language} code:\n\n\`\`\`${language}\n${codeContent}\n\`\`\``, + }, + ], + }); + + const textContent = message.content.find(b => b.type === 'text'); + if (textContent && textContent.type === 'text') { + console.log('Code Review:'); + console.log(textContent.text); + } +} + +// Example 6: Translation service +async function translateText(text: string, from: string, to: string) { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: `Translate the following text from ${from} to ${to}:\n\n${text}`, + }, + ], + }); + + const textContent = message.content.find(b => b.type === 'text'); + if (textContent && textContent.type === 'text') { + return textContent.text; + } + + return null; +} + +// Example 7: Parallel requests +async function parallelRequests(prompts: string[]) { + console.log(`Processing ${prompts.length} prompts in parallel...`); + + const promises = prompts.map(async (prompt, index) => { + try { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 512, + messages: [{ role: 'user', content: prompt }], + }); + + const textContent = message.content.find(b => b.type === 'text'); + return { + index, + prompt, + response: textContent && textContent.type === 'text' ? textContent.text : null, + }; + } catch (error) { + return { + index, + prompt, + error: error.message, + }; + } + }); + + const results = await Promise.all(promises); + return results; +} + +// Example 8: Retry logic with exponential backoff +async function requestWithRetry( + prompt: string, + maxRetries = 3, + baseDelay = 1000 +): Promise { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [{ role: 'user', content: prompt }], + }); + + const textContent = message.content.find(b => b.type === 'text'); + if (textContent && textContent.type === 'text') { + return textContent.text; + } + + throw new Error('No text content in response'); + } catch (error) { + if (error instanceof Anthropic.APIError && error.status === 429) { + if (attempt < maxRetries - 1) { + const delay = baseDelay * Math.pow(2, attempt); + console.warn(`Rate limited. Retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + } + + throw error; + } + } + + throw new Error('Max retries exceeded'); +} + +// Example 9: Conversation logger +class ConversationLogger { + private messages: Anthropic.MessageParam[] = []; + private logFile: string; + + constructor(logFile: string) { + this.logFile = logFile; + } + + async chat(userMessage: string): Promise { + this.messages.push({ role: 'user', content: userMessage }); + + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: this.messages, + }); + + const textContent = response.content.find(b => b.type === 'text'); + if (textContent && textContent.type === 'text') { + this.messages.push({ role: 'assistant', content: textContent.text }); + this.save(); + return textContent.text; + } + + throw new Error('No response'); + } + + private save() { + fs.writeFileSync(this.logFile, JSON.stringify(this.messages, null, 2)); + } + + load() { + if (fs.existsSync(this.logFile)) { + this.messages = JSON.parse(fs.readFileSync(this.logFile, 'utf-8')); + } + } +} + +// Run examples +if (require.main === module) { + const args = process.argv.slice(2); + const example = args[0]; + + switch (example) { + case 'cli': + simpleCLIChatbot(); + break; + case 'stream': + streamingCLIChatbot(); + break; + case 'batch': + batchProcessing(args[1], args[2] || 'output.json'); + break; + case 'summarize': + summarizeDocument(args[1]); + break; + case 'review': + const code = fs.readFileSync(args[1], 'utf-8'); + codeReview(code, args[2] || 'typescript'); + break; + case 'translate': + translateText(args[1], args[2], args[3]).then(console.log); + break; + default: + console.log('Available examples:'); + console.log('- cli: Interactive chatbot'); + console.log('- stream: Streaming chatbot'); + console.log('- batch [output]: Batch processing'); + console.log('- summarize : Document summarization'); + console.log('- review [language]: Code review'); + console.log('- translate : Translation'); + } +} + +export { + simpleCLIChatbot, + streamingCLIChatbot, + batchProcessing, + summarizeDocument, + codeReview, + translateText, + parallelRequests, + requestWithRetry, + ConversationLogger, +}; diff --git a/templates/package.json b/templates/package.json new file mode 100644 index 0000000..5896212 --- /dev/null +++ b/templates/package.json @@ -0,0 +1,48 @@ +{ + "name": "claude-api-examples", + "version": "1.0.0", + "description": "Examples for using the Claude API with various platforms", + "main": "index.ts", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "cli": "tsx src/nodejs-example.ts cli", + "stream": "tsx src/nodejs-example.ts stream", + "test": "vitest", + "lint": "eslint . --ext .ts", + "format": "prettier --write \"**/*.{ts,tsx,md,json}\"", + "typecheck": "tsc --noEmit" + }, + "keywords": [ + "claude", + "anthropic", + "ai", + "llm", + "chatbot" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.67.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "dotenv": "^16.3.1", + "eslint": "^8.50.0", + "prettier": "^3.0.3", + "tsx": "^4.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0", + "zod": "^3.23.0" + }, + "optionalDependencies": { + "@upstash/ratelimit": "^1.0.0", + "@upstash/redis": "^1.25.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/templates/prompt-caching.ts b/templates/prompt-caching.ts new file mode 100644 index 0000000..bd13bc5 --- /dev/null +++ b/templates/prompt-caching.ts @@ -0,0 +1,272 @@ +import Anthropic from '@anthropic-ai/sdk'; +import fs from 'fs'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY || '', +}); + +// Example 1: Basic prompt caching with system prompt +async function cacheSystemPrompt() { + // Simulate a large system prompt (must be >= 1024 tokens for caching) + const largeSystemPrompt = ` +You are an expert software architect with deep knowledge of: +- Microservices architecture and design patterns +- Cloud-native applications (AWS, GCP, Azure) +- Containerization (Docker, Kubernetes) +- CI/CD pipelines and DevOps practices +- Database design (SQL and NoSQL) +- API design (REST, GraphQL, gRPC) +- Security best practices and compliance +- Performance optimization and scalability +- Monitoring and observability (Prometheus, Grafana) +- Event-driven architectures and message queues + +${' '.repeat(10000)} // Padding to ensure > 1024 tokens + +Always provide detailed, production-ready advice with code examples. +`.trim(); + + // First request - creates cache + const message1 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: largeSystemPrompt, + cache_control: { type: 'ephemeral' }, + }, + ], + messages: [ + { + role: 'user', + content: 'How do I design a scalable authentication system?', + }, + ], + }); + + console.log('=== First Request (Cache Creation) ==='); + console.log('Cache creation tokens:', message1.usage.cache_creation_input_tokens); + console.log('Cache read tokens:', message1.usage.cache_read_input_tokens); + console.log('Input tokens:', message1.usage.input_tokens); + console.log('Output tokens:', message1.usage.output_tokens); + + // Second request - hits cache (within 5 minutes) + const message2 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: largeSystemPrompt, // MUST be identical to hit cache + cache_control: { type: 'ephemeral' }, + }, + ], + messages: [ + { + role: 'user', + content: 'What about rate limiting strategies?', + }, + ], + }); + + console.log('\n=== Second Request (Cache Hit) ==='); + console.log('Cache creation tokens:', message2.usage.cache_creation_input_tokens); + console.log('Cache read tokens:', message2.usage.cache_read_input_tokens); + console.log('Input tokens:', message2.usage.input_tokens); + console.log('Output tokens:', message2.usage.output_tokens); + console.log('Savings: ~90% on cached content'); +} + +// Example 2: Caching large documents +async function cacheLargeDocument() { + // Read a large document (e.g., documentation, codebase) + const largeDocument = fs.readFileSync('./large-document.txt', 'utf-8'); + // Ensure document is >= 1024 tokens + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'Analyze the following documentation:', + }, + { + type: 'text', + text: largeDocument, + cache_control: { type: 'ephemeral' }, + }, + { + type: 'text', + text: 'What are the main API endpoints?', + }, + ], + }, + ], + }); + + console.log('=== Document Analysis with Caching ==='); + console.log('Cache creation:', message.usage.cache_creation_input_tokens); + console.log('Cache read:', message.usage.cache_read_input_tokens); +} + +// Example 3: Multi-turn conversation with caching (chatbot pattern) +async function multiTurnCachingConversation() { + const systemInstructions = ` +You are a customer support AI for TechCorp, specializing in: +- Product troubleshooting +- Account management +- Billing inquiries +- Technical specifications + +${' '.repeat(10000)} // Ensure > 1024 tokens + +Knowledge Base: +- Product A: Cloud storage service +- Product B: Analytics platform +- Product C: AI API service + +Always be polite, helpful, and provide actionable solutions. +`.trim(); + + // Conversation state + const messages: Anthropic.MessageParam[] = []; + + // Turn 1 + messages.push({ + role: 'user', + content: 'How do I reset my password?', + }); + + const response1 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: systemInstructions, + cache_control: { type: 'ephemeral' }, + }, + ], + messages, + }); + + const text1 = response1.content.find(b => b.type === 'text'); + if (text1 && text1.type === 'text') { + messages.push({ role: 'assistant', content: text1.text }); + console.log('Turn 1 - Cache creation:', response1.usage.cache_creation_input_tokens); + } + + // Turn 2 - cache hit + messages.push({ + role: 'user', + content: 'What about two-factor authentication?', + }); + + const response2 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + system: [ + { + type: 'text', + text: systemInstructions, + cache_control: { type: 'ephemeral' }, + }, + ], + messages, + }); + + console.log('Turn 2 - Cache read:', response2.usage.cache_read_input_tokens); + console.log('Turn 2 - New input tokens:', response2.usage.input_tokens); +} + +// Example 4: Caching with conversation history +async function cacheConversationHistory() { + const messages: Anthropic.MessageParam[] = [ + { role: 'user', content: 'Tell me about TypeScript' }, + { role: 'assistant', content: 'TypeScript is a superset of JavaScript...' }, + { role: 'user', content: 'What about interfaces?' }, + { role: 'assistant', content: 'Interfaces in TypeScript define contracts...' }, + { role: 'user', content: 'Can you give examples?' }, + ]; + + // Cache the conversation history + const messagesWithCache: Anthropic.MessageParam[] = messages.slice(0, -1).map((msg, idx) => { + if (idx === messages.length - 2) { + // Cache the last assistant message + return { + ...msg, + content: [ + { + type: 'text', + text: typeof msg.content === 'string' ? msg.content : '', + cache_control: { type: 'ephemeral' }, + }, + ], + }; + } + return msg; + }); + + messagesWithCache.push(messages[messages.length - 1]); + + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: messagesWithCache, + }); + + console.log('=== Caching Conversation History ==='); + console.log('Cache usage:', response.usage); +} + +// Example 5: Cost comparison calculator +function calculateCachingSavings(inputTokens: number, outputTokens: number, turnCount: number) { + const inputCostPerMTok = 3; // $3 per million tokens + const outputCostPerMTok = 15; // $15 per million tokens + const cacheCostPerMTok = 3.75; // $3.75 per million tokens (write) + const cacheReadCostPerMTok = 0.3; // $0.30 per million tokens (read) + + // Without caching + const costWithoutCache = + (inputTokens / 1_000_000) * inputCostPerMTok * turnCount + + (outputTokens / 1_000_000) * outputCostPerMTok * turnCount; + + // With caching + const cacheWriteCost = (inputTokens / 1_000_000) * cacheCostPerMTok; // First request + const cacheReadCost = (inputTokens / 1_000_000) * cacheReadCostPerMTok * (turnCount - 1); // Subsequent + const outputCost = (outputTokens / 1_000_000) * outputCostPerMTok * turnCount; + const costWithCache = cacheWriteCost + cacheReadCost + outputCost; + + const savings = costWithoutCache - costWithCache; + const savingsPercent = (savings / costWithoutCache) * 100; + + console.log('\n=== Cost Comparison ==='); + console.log(`Input tokens: ${inputTokens}, Output tokens: ${outputTokens}, Turns: ${turnCount}`); + console.log(`Without caching: $${costWithoutCache.toFixed(4)}`); + console.log(`With caching: $${costWithCache.toFixed(4)}`); + console.log(`Savings: $${savings.toFixed(4)} (${savingsPercent.toFixed(1)}%)`); +} + +// Run examples +if (require.main === module) { + cacheSystemPrompt() + .then(() => multiTurnCachingConversation()) + .then(() => { + // Example cost calculation + calculateCachingSavings(100000, 5000, 10); // 100k input, 5k output, 10 turns + }) + .catch(console.error); +} + +export { + cacheSystemPrompt, + cacheLargeDocument, + multiTurnCachingConversation, + cacheConversationHistory, + calculateCachingSavings, +}; diff --git a/templates/streaming-chat.ts b/templates/streaming-chat.ts new file mode 100644 index 0000000..df2504f --- /dev/null +++ b/templates/streaming-chat.ts @@ -0,0 +1,194 @@ +import Anthropic from '@anthropic-ai/sdk'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY || '', +}); + +// Method 1: Using SDK stream helper with event listeners +async function streamWithEvents() { + console.log('Claude:'); + + const stream = anthropic.messages.stream({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: 'Write a short poem about coding.', + }, + ], + }); + + stream + .on('text', (text) => { + process.stdout.write(text); + }) + .on('message', (message) => { + console.log('\n\nFinal message:', message); + console.log('Stop reason:', message.stop_reason); + }) + .on('error', (error) => { + console.error('\nStream error:', error); + }) + .on('abort', (error) => { + console.warn('\nStream aborted:', error); + }) + .on('end', () => { + console.log('\n\nStream ended'); + }); + + // Wait for stream to complete + const finalMessage = await stream.finalMessage(); + return finalMessage; +} + +// Method 2: Manual iteration over stream events +async function streamWithManualIteration() { + console.log('Claude:'); + + const stream = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: 'Explain quantum computing in simple terms.', + }, + ], + stream: true, + }); + + let fullText = ''; + + try { + for await (const event of stream) { + if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { + const text = event.delta.text; + fullText += text; + process.stdout.write(text); + } + + if (event.type === 'message_stop') { + console.log('\n\nStream complete'); + } + } + } catch (error) { + console.error('\nStream error:', error); + throw error; + } + + return fullText; +} + +// Method 3: Streaming with abort control +async function streamWithAbort() { + console.log('Claude (can be aborted):'); + + const stream = anthropic.messages.stream({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 2048, + messages: [ + { + role: 'user', + content: 'Write a long essay about the history of computers.', + }, + ], + }); + + let charCount = 0; + const maxChars = 200; // Abort after 200 characters + + stream.on('text', (text) => { + process.stdout.write(text); + charCount += text.length; + + // Abort stream after reaching limit + if (charCount >= maxChars) { + console.log('\n\n[Aborting stream after', charCount, 'characters]'); + stream.abort(); + } + }); + + stream.on('abort', () => { + console.log('Stream was aborted successfully'); + }); + + stream.on('error', (error) => { + console.error('Stream error:', error); + }); + + try { + await stream.done(); + } catch (error) { + // Handle abort error + if (error.name === 'APIUserAbortError') { + console.log('Stream aborted by user'); + } else { + throw error; + } + } +} + +// Method 4: Streaming with retry logic +async function streamWithRetry(maxRetries = 3) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const stream = anthropic.messages.stream({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: 'Tell me a fun fact about space.', + }, + ], + }); + + let fullText = ''; + + stream.on('text', (text) => { + fullText += text; + process.stdout.write(text); + }); + + stream.on('error', (error) => { + console.error(`\nStream error on attempt ${attempt + 1}:`, error); + throw error; + }); + + await stream.finalMessage(); + console.log('\n\nStream completed successfully'); + return fullText; + } catch (error) { + if (attempt < maxRetries - 1) { + const delay = Math.pow(2, attempt) * 1000; + console.log(`\nRetrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + console.error('\nMax retries exceeded'); + throw error; + } + } + } +} + +// Run examples +if (require.main === module) { + console.log('=== Stream with Events ===\n'); + streamWithEvents() + .then(() => { + console.log('\n\n=== Stream with Manual Iteration ===\n'); + return streamWithManualIteration(); + }) + .then(() => { + console.log('\n\n=== Stream with Abort ===\n'); + return streamWithAbort(); + }) + .then(() => { + console.log('\n\n=== Stream with Retry ===\n'); + return streamWithRetry(); + }) + .catch(console.error); +} + +export { streamWithEvents, streamWithManualIteration, streamWithAbort, streamWithRetry }; diff --git a/templates/tool-use-advanced.ts b/templates/tool-use-advanced.ts new file mode 100644 index 0000000..6185ab6 --- /dev/null +++ b/templates/tool-use-advanced.ts @@ -0,0 +1,296 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { betaZodTool } from '@anthropic-ai/sdk/helpers/zod'; +import { z } from 'zod'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY || '', +}); + +// Example 1: Using Zod schemas with betaZodTool +const weatherTool = betaZodTool({ + name: 'get_weather', + inputSchema: z.object({ + location: z.string().describe('The city and state, e.g. San Francisco, CA'), + unit: z.enum(['celsius', 'fahrenheit']).optional().describe('Temperature unit'), + }), + description: 'Get the current weather in a given location', + run: async (input) => { + // Mock implementation - replace with actual API call + console.log(`Fetching weather for ${input.location}...`); + const temp = input.unit === 'celsius' ? 22 : 72; + return `The weather in ${input.location} is sunny and ${temp}°${input.unit || 'F'}`; + }, +}); + +const searchTool = betaZodTool({ + name: 'search_web', + inputSchema: z.object({ + query: z.string().describe('The search query'), + max_results: z.number().int().min(1).max(10).optional().describe('Maximum number of results'), + }), + description: 'Search the web for information', + run: async (input) => { + console.log(`Searching for: ${input.query}...`); + // Mock implementation + return `Found ${input.max_results || 5} results for "${input.query}": +1. Example result 1 +2. Example result 2 +3. Example result 3`; + }, +}); + +const calculatorTool = betaZodTool({ + name: 'calculate', + inputSchema: z.object({ + expression: z.string().describe('Mathematical expression to evaluate'), + }), + description: 'Evaluate a mathematical expression', + run: async (input) => { + try { + // WARNING: eval is dangerous - this is just for demonstration + // In production, use a safe math parser like math.js + const result = eval(input.expression); + return `${input.expression} = ${result}`; + } catch (error) { + throw new Error(`Invalid expression: ${input.expression}`); + } + }, +}); + +// Example 2: Using toolRunner for automatic execution +async function automaticToolExecution() { + const finalMessage = await anthropic.beta.messages.toolRunner({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1000, + messages: [ + { + role: 'user', + content: 'What is the weather in Tokyo? Also, search for "best sushi restaurants in Tokyo"', + }, + ], + tools: [weatherTool, searchTool], + }); + + console.log('\nFinal response:'); + for (const block of finalMessage.content) { + if (block.type === 'text') { + console.log(block.text); + } + } + + return finalMessage; +} + +// Example 3: Streaming with tools +async function streamingWithTools() { + const runner = anthropic.beta.messages.toolRunner({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1000, + messages: [ + { + role: 'user', + content: 'Calculate 123 * 456, then tell me about the result', + }, + ], + tools: [calculatorTool], + stream: true, + }); + + console.log('Streaming response:'); + + // Iterate through messages as they arrive + for await (const messageStream of runner) { + // Each message can have multiple events + for await (const event of messageStream) { + if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { + process.stdout.write(event.delta.text); + } + } + + console.log('\n\nMessage completed'); + } + + // Get final result + const result = await runner; + console.log('\nFinal result:', result); +} + +// Example 4: Complex tool chain +const databaseTool = betaZodTool({ + name: 'query_database', + inputSchema: z.object({ + query: z.string().describe('SQL query to execute'), + }), + description: 'Query the database', + run: async (input) => { + console.log(`Executing SQL: ${input.query}`); + // Mock database response + return JSON.stringify([ + { id: 1, name: 'Product A', price: 29.99 }, + { id: 2, name: 'Product B', price: 49.99 }, + ]); + }, +}); + +const emailTool = betaZodTool({ + name: 'send_email', + inputSchema: z.object({ + to: z.string().email().describe('Recipient email address'), + subject: z.string().describe('Email subject'), + body: z.string().describe('Email body'), + }), + description: 'Send an email', + run: async (input) => { + console.log(`Sending email to ${input.to}...`); + // Mock email sending + return `Email sent successfully to ${input.to}`; + }, +}); + +async function complexToolChain() { + const finalMessage = await anthropic.beta.messages.toolRunner({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 2000, + messages: [ + { + role: 'user', + content: + 'Query the database for all products, calculate their average price, and send me an email with the results to test@example.com', + }, + ], + tools: [databaseTool, calculatorTool, emailTool], + }); + + console.log('\nTool chain completed'); + for (const block of finalMessage.content) { + if (block.type === 'text') { + console.log(block.text); + } + } +} + +// Example 5: Tool with max iterations limit +async function toolsWithMaxIterations() { + try { + const finalMessage = await anthropic.beta.messages.toolRunner({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1000, + max_iterations: 3, // Limit tool execution loops + messages: [ + { + role: 'user', + content: 'Search for "quantum computing" and then search for each result', + }, + ], + tools: [searchTool], + }); + + console.log('Completed with max_iterations limit'); + for (const block of finalMessage.content) { + if (block.type === 'text') { + console.log(block.text); + } + } + } catch (error) { + console.error('Max iterations reached or error occurred:', error); + } +} + +// Example 6: Custom tool runner with manual control +async function manualToolRunner() { + const runner = anthropic.beta.messages.toolRunner({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1000, + messages: [ + { + role: 'user', + content: 'What is 15 * 27?', + }, + ], + tools: [calculatorTool], + }); + + // Manually iterate through messages + for await (const message of runner) { + console.log('\nReceived message'); + console.log('Stop reason:', message.stop_reason); + + // Generate tool response if needed + const toolResponse = await runner.generateToolResponse(); + if (toolResponse) { + console.log('Tool results:', toolResponse.content); + } + + // Can inspect and modify the conversation here + console.log('Current params:', runner.params); + } + + // Wait for completion + const finalMessage = await runner.done(); + console.log('\nFinal message:', finalMessage); +} + +// Example 7: Error recovery in tools +const unreliableTool = betaZodTool({ + name: 'unreliable_api', + inputSchema: z.object({ + data: z.string(), + }), + description: 'An API that sometimes fails', + run: async (input) => { + // Randomly fail to demonstrate error handling + if (Math.random() < 0.3) { + throw new Error('API temporarily unavailable'); + } + return `Processed: ${input.data}`; + }, +}); + +async function toolWithErrorRecovery() { + try { + const finalMessage = await anthropic.beta.messages.toolRunner({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1000, + messages: [ + { + role: 'user', + content: 'Process this data with the unreliable API: "test data"', + }, + ], + tools: [unreliableTool], + }); + + console.log('Success:', finalMessage.content); + } catch (error) { + console.error('Tool execution failed:', error); + // Implement retry logic or fallback + } +} + +// Run examples +if (require.main === module) { + console.log('=== Automatic Tool Execution ===\n'); + automaticToolExecution() + .then(() => { + console.log('\n\n=== Streaming with Tools ===\n'); + return streamingWithTools(); + }) + .then(() => { + console.log('\n\n=== Complex Tool Chain ===\n'); + return complexToolChain(); + }) + .then(() => { + console.log('\n\n=== Manual Tool Runner ===\n'); + return manualToolRunner(); + }) + .catch(console.error); +} + +export { + automaticToolExecution, + streamingWithTools, + complexToolChain, + toolsWithMaxIterations, + manualToolRunner, + toolWithErrorRecovery, +}; diff --git a/templates/tool-use-basic.ts b/templates/tool-use-basic.ts new file mode 100644 index 0000000..37a8995 --- /dev/null +++ b/templates/tool-use-basic.ts @@ -0,0 +1,310 @@ +import Anthropic from '@anthropic-ai/sdk'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY || '', +}); + +// Define tools +const tools: Anthropic.Tool[] = [ + { + name: 'get_weather', + description: 'Get the current weather in a given location', + input_schema: { + type: 'object', + properties: { + location: { + type: 'string', + description: 'The city and state, e.g. San Francisco, CA', + }, + unit: { + type: 'string', + enum: ['celsius', 'fahrenheit'], + description: 'The unit of temperature, either "celsius" or "fahrenheit"', + }, + }, + required: ['location'], + }, + }, + { + name: 'get_time', + description: 'Get the current time in a given timezone', + input_schema: { + type: 'object', + properties: { + timezone: { + type: 'string', + description: 'The IANA timezone name, e.g. America/Los_Angeles', + }, + }, + required: ['timezone'], + }, + }, +]; + +// Mock tool implementations +function executeWeatherTool(location: string, unit?: string): string { + // In production, call actual weather API + const temp = unit === 'celsius' ? 22 : 72; + return `The weather in ${location} is sunny and ${temp}°${unit === 'celsius' ? 'C' : 'F'}`; +} + +function executeTimeTool(timezone: string): string { + // In production, get actual time for timezone + const time = new Date().toLocaleTimeString('en-US', { timeZone: timezone }); + return `The current time in ${timezone} is ${time}`; +} + +// Example 1: Basic tool use detection +async function basicToolUse() { + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages: [ + { + role: 'user', + content: 'What is the weather like in San Francisco?', + }, + ], + }); + + console.log('Stop reason:', message.stop_reason); + + if (message.stop_reason === 'tool_use') { + console.log('\nClaude wants to use tools:'); + + for (const block of message.content) { + if (block.type === 'tool_use') { + console.log(`- Tool: ${block.name}`); + console.log(` ID: ${block.id}`); + console.log(` Input:`, block.input); + } + } + } + + return message; +} + +// Example 2: Tool execution loop +async function toolExecutionLoop(userMessage: string) { + const messages: Anthropic.MessageParam[] = [ + { role: 'user', content: userMessage }, + ]; + + while (true) { + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages, + }); + + console.log('\nStop reason:', response.stop_reason); + + // Add assistant response to messages + messages.push({ + role: 'assistant', + content: response.content, + }); + + // Check if Claude wants to use tools + if (response.stop_reason === 'tool_use') { + const toolResults: Anthropic.ToolResultBlockParam[] = []; + + // Execute each tool + for (const block of response.content) { + if (block.type === 'tool_use') { + console.log(`\nExecuting tool: ${block.name}`); + console.log('Input:', block.input); + + let result: string; + + // Execute the appropriate tool + if (block.name === 'get_weather') { + result = executeWeatherTool( + block.input.location as string, + block.input.unit as string | undefined + ); + } else if (block.name === 'get_time') { + result = executeTimeTool(block.input.timezone as string); + } else { + result = `Unknown tool: ${block.name}`; + } + + console.log('Result:', result); + + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + content: result, + }); + } + } + + // Add tool results to messages + messages.push({ + role: 'user', + content: toolResults, + }); + } else { + // Final response - no more tools needed + const textBlock = response.content.find(block => block.type === 'text'); + if (textBlock && textBlock.type === 'text') { + console.log('\nFinal response:', textBlock.text); + return textBlock.text; + } + break; + } + } +} + +// Example 3: Multiple tools in one turn +async function multipleToolsInOneTurn() { + const messages: Anthropic.MessageParam[] = [ + { + role: 'user', + content: 'What is the weather in New York and what time is it in Tokyo?', + }, + ]; + + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages, + }); + + console.log('Claude requested', response.content.filter(b => b.type === 'tool_use').length, 'tools'); + + if (response.stop_reason === 'tool_use') { + const toolResults: Anthropic.ToolResultBlockParam[] = []; + + for (const block of response.content) { + if (block.type === 'tool_use') { + console.log(`\n- ${block.name}:`, block.input); + + let result: string; + if (block.name === 'get_weather') { + result = executeWeatherTool(block.input.location as string); + } else if (block.name === 'get_time') { + result = executeTimeTool(block.input.timezone as string); + } else { + result = 'Unknown tool'; + } + + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + content: result, + }); + } + } + + // Continue conversation with tool results + messages.push({ role: 'assistant', content: response.content }); + messages.push({ role: 'user', content: toolResults }); + + const finalResponse = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages, + }); + + const textBlock = finalResponse.content.find(b => b.type === 'text'); + if (textBlock && textBlock.type === 'text') { + console.log('\nFinal answer:', textBlock.text); + } + } +} + +// Example 4: Error handling in tool execution +async function toolExecutionWithErrorHandling(userMessage: string) { + const messages: Anthropic.MessageParam[] = [ + { role: 'user', content: userMessage }, + ]; + + try { + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages, + }); + + messages.push({ role: 'assistant', content: response.content }); + + if (response.stop_reason === 'tool_use') { + const toolResults: Anthropic.ToolResultBlockParam[] = []; + + for (const block of response.content) { + if (block.type === 'tool_use') { + try { + let result: string; + + if (block.name === 'get_weather') { + result = executeWeatherTool(block.input.location as string); + } else if (block.name === 'get_time') { + result = executeTimeTool(block.input.timezone as string); + } else { + throw new Error(`Unknown tool: ${block.name}`); + } + + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + content: result, + }); + } catch (error) { + // Return error to Claude + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + content: `Error executing tool: ${error.message}`, + is_error: true, + }); + } + } + } + + messages.push({ role: 'user', content: toolResults }); + + // Get final response + const finalResponse = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools, + messages, + }); + + const textBlock = finalResponse.content.find(b => b.type === 'text'); + if (textBlock && textBlock.type === 'text') { + console.log('Final response:', textBlock.text); + } + } + } catch (error) { + console.error('API error:', error); + throw error; + } +} + +// Run examples +if (require.main === module) { + console.log('=== Basic Tool Use ===\n'); + basicToolUse() + .then(() => { + console.log('\n\n=== Tool Execution Loop ===\n'); + return toolExecutionLoop('What is the weather in London and what time is it there?'); + }) + .then(() => { + console.log('\n\n=== Multiple Tools ===\n'); + return multipleToolsInOneTurn(); + }) + .then(() => { + console.log('\n\n=== Error Handling ===\n'); + return toolExecutionWithErrorHandling('What is the weather in Mars?'); + }) + .catch(console.error); +} + +export { basicToolUse, toolExecutionLoop, multipleToolsInOneTurn, toolExecutionWithErrorHandling }; diff --git a/templates/vision-image.ts b/templates/vision-image.ts new file mode 100644 index 0000000..9038f62 --- /dev/null +++ b/templates/vision-image.ts @@ -0,0 +1,392 @@ +import Anthropic from '@anthropic-ai/sdk'; +import fs from 'fs'; +import path from 'path'; + +const anthropic = new Anthropic({ + apiKey: process.env.ANTHROPIC_API_KEY || '', +}); + +// Example 1: Single image analysis +async function analyzeSingleImage(imagePath: string) { + // Read and encode image as base64 + const imageData = fs.readFileSync(imagePath); + const base64Image = imageData.toString('base64'); + + // Determine media type from file extension + const ext = path.extname(imagePath).toLowerCase(); + const mediaTypeMap: Record = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.webp': 'image/webp', + '.gif': 'image/gif', + }; + const mediaType = mediaTypeMap[ext] || 'image/jpeg'; + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: mediaType, + data: base64Image, + }, + }, + { + type: 'text', + text: 'What is in this image? Describe it in detail.', + }, + ], + }, + ], + }); + + const textContent = message.content.find(block => block.type === 'text'); + if (textContent && textContent.type === 'text') { + console.log('Claude:', textContent.text); + } + + return message; +} + +// Example 2: Multiple images comparison +async function compareImages(image1Path: string, image2Path: string) { + const image1Data = fs.readFileSync(image1Path).toString('base64'); + const image2Data = fs.readFileSync(image2Path).toString('base64'); + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'Compare these two images. What are the similarities and differences?', + }, + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: image1Data, + }, + }, + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: image2Data, + }, + }, + ], + }, + ], + }); + + const textContent = message.content.find(block => block.type === 'text'); + if (textContent && textContent.type === 'text') { + console.log('Comparison:', textContent.text); + } +} + +// Example 3: Vision with tools +const searchTool: Anthropic.Tool = { + name: 'search_product', + description: 'Search for similar products', + input_schema: { + type: 'object', + properties: { + keywords: { + type: 'array', + items: { type: 'string' }, + description: 'Keywords to search for', + }, + }, + required: ['keywords'], + }, +}; + +async function visionWithTools(imagePath: string) { + const imageData = fs.readFileSync(imagePath).toString('base64'); + + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + tools: [searchTool], + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: imageData, + }, + }, + { + type: 'text', + text: 'Identify the objects in this image and search for similar products', + }, + ], + }, + ], + }); + + console.log('Stop reason:', message.stop_reason); + + if (message.stop_reason === 'tool_use') { + for (const block of message.content) { + if (block.type === 'tool_use') { + console.log('Tool requested:', block.name); + console.log('Search keywords:', block.input); + } + } + } +} + +// Example 4: Multi-turn conversation with images +async function multiTurnVision(imagePath: string) { + const imageData = fs.readFileSync(imagePath).toString('base64'); + + const messages: Anthropic.MessageParam[] = [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: imageData, + }, + }, + { + type: 'text', + text: 'What objects are visible in this image?', + }, + ], + }, + ]; + + // First turn + const response1 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + const text1 = response1.content.find(b => b.type === 'text'); + if (text1 && text1.type === 'text') { + console.log('Claude:', text1.text); + messages.push({ role: 'assistant', content: text1.text }); + } + + // Second turn - follow-up question (image still in context) + messages.push({ + role: 'user', + content: 'What color is the largest object?', + }); + + const response2 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages, + }); + + const text2 = response2.content.find(b => b.type === 'text'); + if (text2 && text2.type === 'text') { + console.log('Claude:', text2.text); + } +} + +// Example 5: Vision with prompt caching +async function visionWithCaching(imagePath: string) { + const imageData = fs.readFileSync(imagePath).toString('base64'); + + // First request - cache the image + const response1 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: imageData, + }, + cache_control: { type: 'ephemeral' }, + }, + { + type: 'text', + text: 'Describe the main objects in this image', + }, + ], + }, + ], + }); + + console.log('First request - cache creation:', response1.usage.cache_creation_input_tokens); + + const text1 = response1.content.find(b => b.type === 'text'); + if (text1 && text1.type === 'text') { + console.log('Response 1:', text1.text); + } + + // Second request - use cached image (within 5 minutes) + const response2 = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: imageData, // Same image + }, + cache_control: { type: 'ephemeral' }, + }, + { + type: 'text', + text: 'What colors are prominent in this image?', + }, + ], + }, + ], + }); + + console.log('Second request - cache read:', response2.usage.cache_read_input_tokens); + console.log('Token savings: ~90%'); +} + +// Example 6: Image URL (if accessible) +async function analyzeImageFromURL(imageUrl: string) { + // Note: Image must be publicly accessible + const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20250929', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'url', + url: imageUrl, + }, + }, + { + type: 'text', + text: 'Analyze this image', + }, + ], + }, + ], + }); + + const textContent = message.content.find(block => block.type === 'text'); + if (textContent && textContent.type === 'text') { + console.log('Analysis:', textContent.text); + } +} + +// Example 7: Image validation helper +function validateImage(filePath: string): { valid: boolean; error?: string } { + if (!fs.existsSync(filePath)) { + return { valid: false, error: 'File does not exist' }; + } + + const stats = fs.statSync(filePath); + const fileSizeMB = stats.size / (1024 * 1024); + + if (fileSizeMB > 5) { + return { valid: false, error: 'Image exceeds 5MB limit' }; + } + + const ext = path.extname(filePath).toLowerCase(); + const supportedFormats = ['.jpg', '.jpeg', '.png', '.webp', '.gif']; + + if (!supportedFormats.includes(ext)) { + return { valid: false, error: `Unsupported format. Use: ${supportedFormats.join(', ')}` }; + } + + return { valid: true }; +} + +// Example 8: Batch image analysis +async function analyzeMultipleImages(imagePaths: string[]) { + const results = []; + + for (const imagePath of imagePaths) { + console.log(`\nAnalyzing: ${imagePath}`); + + const validation = validateImage(imagePath); + if (!validation.valid) { + console.error(`Error: ${validation.error}`); + continue; + } + + try { + const result = await analyzeSingleImage(imagePath); + results.push({ imagePath, result }); + } catch (error) { + console.error(`Failed to analyze ${imagePath}:`, error); + } + + // Rate limiting pause + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + return results; +} + +// Run examples (with placeholder paths) +if (require.main === module) { + const exampleImagePath = './example-image.jpg'; + + // Check if example image exists + if (fs.existsSync(exampleImagePath)) { + console.log('=== Single Image Analysis ===\n'); + analyzeSingleImage(exampleImagePath) + .then(() => { + console.log('\n=== Vision with Caching ===\n'); + return visionWithCaching(exampleImagePath); + }) + .catch(console.error); + } else { + console.log('Example image not found. Create example-image.jpg to test.'); + console.log('\nValidation example:'); + const validation = validateImage('./non-existent.jpg'); + console.log(validation); + } +} + +export { + analyzeSingleImage, + compareImages, + visionWithTools, + multiTurnVision, + visionWithCaching, + analyzeImageFromURL, + validateImage, + analyzeMultipleImages, +}; diff --git a/templates/wrangler.jsonc b/templates/wrangler.jsonc new file mode 100644 index 0000000..dd1248a --- /dev/null +++ b/templates/wrangler.jsonc @@ -0,0 +1,80 @@ +// wrangler.jsonc - Cloudflare Workers configuration for Claude API +{ + "name": "claude-api-worker", + "main": "src/index.ts", + "compatibility_date": "2025-01-01", + + // Bindings + "vars": { + "ENVIRONMENT": "production" + }, + + // Environment variables (secrets) + // Set with: wrangler secret put ANTHROPIC_API_KEY + // "ANTHROPIC_API_KEY": "sk-ant-..." // Never commit this! + + // KV namespace (optional - for caching) + "kv_namespaces": [ + { + "binding": "CACHE", + "id": "your-kv-namespace-id", + "preview_id": "your-kv-preview-id" + } + ], + + // D1 database (optional - for conversation storage) + "d1_databases": [ + { + "binding": "DB", + "database_name": "claude-conversations", + "database_id": "your-d1-database-id" + } + ], + + // Durable Objects (optional - for rate limiting) + "durable_objects": { + "bindings": [ + { + "name": "RATE_LIMITER", + "class_name": "RateLimiter", + "script_name": "claude-api-worker" + } + ] + }, + + "migrations": [ + { + "tag": "v1", + "new_classes": ["RateLimiter"] + } + ], + + // Routes (if deploying to custom domain) + "routes": [ + { + "pattern": "api.example.com/chat/*", + "zone_name": "example.com" + } + ], + + // Limits and features + "limits": { + "cpu_ms": 50 // 50ms CPU time + }, + + "compatibility_flags": [ + "nodejs_compat" // If using Node.js APIs + ], + + // Observability + "observability": { + "enabled": true + }, + + // Tail consumers (for logging) + "tail_consumers": [ + { + "service": "logging-worker" + } + ] +}