{"slug":"claude-api-advanced-patterns","title":"Claude API Advanced Patterns: Thinking, Streaming, Batches, and Agentic Loops","tags":["claude","anthropic","llm","streaming","thinking","batches","agentic-loop"],"agent_summary":"Advanced Claude API patterns — adaptive thinking (claude-opus-4-6), streaming with finalMessage, Batches API at 50% cost, Files API for multi-request uploads, agentic tool use loops, RAG pipeline, and multi-provider routing via Bedrock/Vertex.","trigger_phrases":["Claude adaptive thinking","Claude streaming","Batches API","Claude agentic loop","Claude Files API","Claude Bedrock","Claude Vertex","claude-opus-4-6 thinking","extended thinking Claude"],"runnable":false,"markdown":"\n## Overview\n\nAdvanced Claude API patterns for production applications. Model defaults: always `claude-opus-4-6` unless explicitly overridden. Adaptive thinking, streaming, and agentic loops are the core building blocks.\n\n## Model Reference\n\n| Model | ID | Best For |\n|-------|-----|----------|\n| Claude Opus 4.6 | `claude-opus-4-6` | Complex reasoning, production default |\n| Claude Sonnet 4.6 | `claude-sonnet-4-6` | Cost-efficient, standard tasks |\n| Claude Haiku 4.5 | `claude-haiku-4-5` | Simple lookups, high throughput |\n\n**Never** append date suffixes to model IDs. Use the exact strings above.\n\n## Adaptive Thinking (claude-opus-4-6)\n\n```typescript\nimport Anthropic from \"@anthropic-ai/sdk\";\n\nconst client = new Anthropic();\n\nconst response = await client.messages.create({\n  model: \"claude-opus-4-6\",\n  max_tokens: 16_000,\n  thinking: { type: \"adaptive\" },  // Claude decides when/how much to think\n  messages: [{ role: \"user\", content: \"Design a distributed caching strategy for a SaaS app with 1M users.\" }],\n});\n```\n\n**Key rules:**\n- `thinking: { type: \"adaptive\" }` — correct for Opus 4.6 and Sonnet 4.6\n- `budget_tokens` — **deprecated**, do not use\n- `thinking.type: \"enabled\"` with `budget_tokens` — only for explicitly requested older models\n- Opus 4.6 does not support assistant message prefills — use structured outputs instead\n\n## Effort Parameter\n\n```typescript\nconst response = await client.messages.create({\n  model: \"claude-opus-4-6\",\n  max_tokens: 8_000,\n  output_config: {\n    effort: \"max\",    // \"low\" | \"medium\" | \"high\" | \"max\"\n  },\n  messages: [{ role: \"user\", content: \"...\" }],\n});\n```\n\n- `max` — Opus 4.6 only, deepest reasoning\n- `high` — default (equivalent to omitting)\n- `low` — for subagents and simple tasks\n\n## Streaming (Required for Large Outputs)\n\nOpus 4.6 supports 128K output tokens — must stream for large `max_tokens`:\n\n```typescript\nconst stream = await client.messages.stream({\n  model: \"claude-opus-4-6\",\n  max_tokens: 32_000,\n  messages: [{ role: \"user\", content: \"Write a complete API spec for...\" }],\n});\n\n// Stream events\nfor await (const event of stream) {\n  if (event.type === \"content_block_delta\" && event.delta.type === \"text_delta\") {\n    process.stdout.write(event.delta.text);\n  }\n}\n\n// Get final complete message\nconst message = await stream.finalMessage();\n```\n\n## Structured Outputs\n\nUse `output_config.format` (not deprecated `output_format`):\n\n```typescript\nconst response = await client.messages.parse({\n  model: \"claude-opus-4-6\",\n  max_tokens: 4_000,\n  output_config: {\n    format: {\n      type: \"json_schema\",\n      json_schema: {\n        name: \"analysis\",\n        schema: {\n          type: \"object\",\n          properties: {\n            sentiment: { type: \"string\", enum: [\"positive\", \"negative\", \"neutral\"] },\n            confidence: { type: \"number\", minimum: 0, maximum: 1 },\n            summary: { type: \"string\" },\n          },\n          required: [\"sentiment\", \"confidence\", \"summary\"],\n        },\n      },\n    },\n  },\n  messages: [{ role: \"user\", content: \"Analyze this review: ...\" }],\n});\n\n// Validated response\nconst analysis = response.content[0];\n```\n\n## Manual Agentic Loop\n\n```typescript\nconst tools: Anthropic.Tool[] = [\n  {\n    name: \"search_web\",\n    description: \"Search the web for current information\",\n    input_schema: {\n      type: \"object\",\n      properties: { query: { type: \"string\" } },\n      required: [\"query\"],\n    },\n  },\n];\n\nconst messages: Anthropic.MessageParam[] = [\n  { role: \"user\", content: \"Research the latest Next.js 16 features.\" },\n];\n\nwhile (true) {\n  const response = await client.messages.create({\n    model: \"claude-opus-4-6\",\n    max_tokens: 4_000,\n    tools,\n    messages,\n  });\n\n  // Append assistant response\n  messages.push({ role: \"assistant\", content: response.content });\n\n  if (response.stop_reason === \"end_turn\") break;\n\n  if (response.stop_reason === \"tool_use\") {\n    const toolResults: Anthropic.ToolResultBlockParam[] = [];\n\n    for (const block of response.content) {\n      if (block.type === \"tool_use\") {\n        const result = await executeToolCall(block.name, block.input);\n        toolResults.push({\n          type: \"tool_result\",\n          tool_use_id: block.id,\n          content: JSON.stringify(result),\n        });\n      }\n    }\n\n    messages.push({ role: \"user\", content: toolResults });\n  }\n}\n```\n\n## Batches API (50% Cost for Non-Latency-Sensitive Work)\n\n```typescript\nconst batch = await client.messages.batches.create({\n  requests: [\n    {\n      custom_id: \"classify-0\",\n      params: {\n        model: \"claude-opus-4-6\",\n        max_tokens: 256,\n        messages: [{ role: \"user\", content: \"Classify: 'Great product!' as positive/negative/neutral\" }],\n      },\n    },\n    {\n      custom_id: \"classify-1\",\n      params: {\n        model: \"claude-opus-4-6\",\n        max_tokens: 256,\n        messages: [{ role: \"user\", content: \"Classify: 'Terrible experience' as positive/negative/neutral\" }],\n      },\n    },\n  ],\n});\n\nconsole.log(`Batch ID: ${batch.id}`);\n\n// Poll until complete\nlet batchResult = batch;\nwhile (batchResult.processing_status !== \"ended\") {\n  await new Promise(r => setTimeout(r, 10_000));\n  batchResult = await client.messages.batches.retrieve(batch.id);\n}\n\n// Retrieve results\nfor await (const result of await client.messages.batches.results(batch.id)) {\n  if (result.result.type === \"succeeded\") {\n    console.log(result.custom_id, result.result.message.content[0]);\n  }\n}\n```\n\n## Files API (Reuse Documents Across Requests)\n\n```typescript\n// Upload once\nconst file = await client.files.create({\n  file: fs.createReadStream(\"spec.pdf\"),\n});\nconst fileId = file.id;\n\n// Reference in multiple requests without re-uploading\nconst responses = await Promise.all([\n  client.messages.create({\n    model: \"claude-opus-4-6\",\n    max_tokens: 4_000,\n    messages: [{\n      role: \"user\",\n      content: [\n        { type: \"document\", source: { type: \"file\", file_id: fileId } },\n        { type: \"text\", text: \"Summarize the architecture section.\" },\n      ],\n    }],\n  }),\n  client.messages.create({\n    model: \"claude-opus-4-6\",\n    max_tokens: 4_000,\n    messages: [{\n      role: \"user\",\n      content: [\n        { type: \"document\", source: { type: \"file\", file_id: fileId } },\n        { type: \"text\", text: \"List all the API endpoints mentioned.\" },\n      ],\n    }],\n  }),\n]);\n```\n\n## RAG Pipeline\n\n```\nChunk documents (500 tokens, 50-token overlap)\n→ Embed with voyage-3\n→ Store in pgvector\n→ Hybrid search (semantic + BM25 with Reciprocal Rank Fusion)\n→ Rerank\n→ Augment prompt with top-K context\n→ Generate with citations\n```\n\n```typescript\n// Augment prompt with retrieved context\nconst context = retrievedDocs.map(doc => doc.content).join(\"\\n\\n---\\n\\n\");\n\nconst response = await client.messages.create({\n  model: \"claude-opus-4-6\",\n  max_tokens: 4_000,\n  system: `Answer questions using the provided context. Cite sources with [Source N].\n  \nContext:\n${context}`,\n  messages: [{ role: \"user\", content: question }],\n});\n```\n\n## Compaction (Long Conversations)\n\n```typescript\n// Append response.content (not just text) to preserve compaction blocks\nmessages.push({ role: \"assistant\", content: response.content });\n\n// Enable compaction via beta header\nconst client = new Anthropic({\n  defaultHeaders: { \"anthropic-beta\": \"compact-2026-01-12\" },\n});\n```\n\nCompaction automatically summarizes earlier context when approaching the 200K limit.\n","html":"<h2>Overview</h2>\n<p>Advanced Claude API patterns for production applications. Model defaults: always <code>claude-opus-4-6</code> unless explicitly overridden. Adaptive thinking, streaming, and agentic loops are the core building blocks.</p>\n<h2>Model Reference</h2>\n<p>| Model | ID | Best For |\n|-------|-----|----------|\n| Claude Opus 4.6 | <code>claude-opus-4-6</code> | Complex reasoning, production default |\n| Claude Sonnet 4.6 | <code>claude-sonnet-4-6</code> | Cost-efficient, standard tasks |\n| Claude Haiku 4.5 | <code>claude-haiku-4-5</code> | Simple lookups, high throughput |</p>\n<p><strong>Never</strong> append date suffixes to model IDs. Use the exact strings above.</p>\n<h2>Adaptive Thinking (claude-opus-4-6)</h2>\n<pre><code class=\"language-typescript\">import Anthropic from \"@anthropic-ai/sdk\";\n\nconst client = new Anthropic();\n\nconst response = await client.messages.create({\n  model: \"claude-opus-4-6\",\n  max_tokens: 16_000,\n  thinking: { type: \"adaptive\" },  // Claude decides when/how much to think\n  messages: [{ role: \"user\", content: \"Design a distributed caching strategy for a SaaS app with 1M users.\" }],\n});\n</code></pre>\n<p><strong>Key rules:</strong></p>\n<ul>\n<li><code>thinking: { type: \"adaptive\" }</code> — correct for Opus 4.6 and Sonnet 4.6</li>\n<li><code>budget_tokens</code> — <strong>deprecated</strong>, do not use</li>\n<li><code>thinking.type: \"enabled\"</code> with <code>budget_tokens</code> — only for explicitly requested older models</li>\n<li>Opus 4.6 does not support assistant message prefills — use structured outputs instead</li>\n</ul>\n<h2>Effort Parameter</h2>\n<pre><code class=\"language-typescript\">const response = await client.messages.create({\n  model: \"claude-opus-4-6\",\n  max_tokens: 8_000,\n  output_config: {\n    effort: \"max\",    // \"low\" | \"medium\" | \"high\" | \"max\"\n  },\n  messages: [{ role: \"user\", content: \"...\" }],\n});\n</code></pre>\n<ul>\n<li><code>max</code> — Opus 4.6 only, deepest reasoning</li>\n<li><code>high</code> — default (equivalent to omitting)</li>\n<li><code>low</code> — for subagents and simple tasks</li>\n</ul>\n<h2>Streaming (Required for Large Outputs)</h2>\n<p>Opus 4.6 supports 128K output tokens — must stream for large <code>max_tokens</code>:</p>\n<pre><code class=\"language-typescript\">const stream = await client.messages.stream({\n  model: \"claude-opus-4-6\",\n  max_tokens: 32_000,\n  messages: [{ role: \"user\", content: \"Write a complete API spec for...\" }],\n});\n\n// Stream events\nfor await (const event of stream) {\n  if (event.type === \"content_block_delta\" &#x26;&#x26; event.delta.type === \"text_delta\") {\n    process.stdout.write(event.delta.text);\n  }\n}\n\n// Get final complete message\nconst message = await stream.finalMessage();\n</code></pre>\n<h2>Structured Outputs</h2>\n<p>Use <code>output_config.format</code> (not deprecated <code>output_format</code>):</p>\n<pre><code class=\"language-typescript\">const response = await client.messages.parse({\n  model: \"claude-opus-4-6\",\n  max_tokens: 4_000,\n  output_config: {\n    format: {\n      type: \"json_schema\",\n      json_schema: {\n        name: \"analysis\",\n        schema: {\n          type: \"object\",\n          properties: {\n            sentiment: { type: \"string\", enum: [\"positive\", \"negative\", \"neutral\"] },\n            confidence: { type: \"number\", minimum: 0, maximum: 1 },\n            summary: { type: \"string\" },\n          },\n          required: [\"sentiment\", \"confidence\", \"summary\"],\n        },\n      },\n    },\n  },\n  messages: [{ role: \"user\", content: \"Analyze this review: ...\" }],\n});\n\n// Validated response\nconst analysis = response.content[0];\n</code></pre>\n<h2>Manual Agentic Loop</h2>\n<pre><code class=\"language-typescript\">const tools: Anthropic.Tool[] = [\n  {\n    name: \"search_web\",\n    description: \"Search the web for current information\",\n    input_schema: {\n      type: \"object\",\n      properties: { query: { type: \"string\" } },\n      required: [\"query\"],\n    },\n  },\n];\n\nconst messages: Anthropic.MessageParam[] = [\n  { role: \"user\", content: \"Research the latest Next.js 16 features.\" },\n];\n\nwhile (true) {\n  const response = await client.messages.create({\n    model: \"claude-opus-4-6\",\n    max_tokens: 4_000,\n    tools,\n    messages,\n  });\n\n  // Append assistant response\n  messages.push({ role: \"assistant\", content: response.content });\n\n  if (response.stop_reason === \"end_turn\") break;\n\n  if (response.stop_reason === \"tool_use\") {\n    const toolResults: Anthropic.ToolResultBlockParam[] = [];\n\n    for (const block of response.content) {\n      if (block.type === \"tool_use\") {\n        const result = await executeToolCall(block.name, block.input);\n        toolResults.push({\n          type: \"tool_result\",\n          tool_use_id: block.id,\n          content: JSON.stringify(result),\n        });\n      }\n    }\n\n    messages.push({ role: \"user\", content: toolResults });\n  }\n}\n</code></pre>\n<h2>Batches API (50% Cost for Non-Latency-Sensitive Work)</h2>\n<pre><code class=\"language-typescript\">const batch = await client.messages.batches.create({\n  requests: [\n    {\n      custom_id: \"classify-0\",\n      params: {\n        model: \"claude-opus-4-6\",\n        max_tokens: 256,\n        messages: [{ role: \"user\", content: \"Classify: 'Great product!' as positive/negative/neutral\" }],\n      },\n    },\n    {\n      custom_id: \"classify-1\",\n      params: {\n        model: \"claude-opus-4-6\",\n        max_tokens: 256,\n        messages: [{ role: \"user\", content: \"Classify: 'Terrible experience' as positive/negative/neutral\" }],\n      },\n    },\n  ],\n});\n\nconsole.log(`Batch ID: ${batch.id}`);\n\n// Poll until complete\nlet batchResult = batch;\nwhile (batchResult.processing_status !== \"ended\") {\n  await new Promise(r => setTimeout(r, 10_000));\n  batchResult = await client.messages.batches.retrieve(batch.id);\n}\n\n// Retrieve results\nfor await (const result of await client.messages.batches.results(batch.id)) {\n  if (result.result.type === \"succeeded\") {\n    console.log(result.custom_id, result.result.message.content[0]);\n  }\n}\n</code></pre>\n<h2>Files API (Reuse Documents Across Requests)</h2>\n<pre><code class=\"language-typescript\">// Upload once\nconst file = await client.files.create({\n  file: fs.createReadStream(\"spec.pdf\"),\n});\nconst fileId = file.id;\n\n// Reference in multiple requests without re-uploading\nconst responses = await Promise.all([\n  client.messages.create({\n    model: \"claude-opus-4-6\",\n    max_tokens: 4_000,\n    messages: [{\n      role: \"user\",\n      content: [\n        { type: \"document\", source: { type: \"file\", file_id: fileId } },\n        { type: \"text\", text: \"Summarize the architecture section.\" },\n      ],\n    }],\n  }),\n  client.messages.create({\n    model: \"claude-opus-4-6\",\n    max_tokens: 4_000,\n    messages: [{\n      role: \"user\",\n      content: [\n        { type: \"document\", source: { type: \"file\", file_id: fileId } },\n        { type: \"text\", text: \"List all the API endpoints mentioned.\" },\n      ],\n    }],\n  }),\n]);\n</code></pre>\n<h2>RAG Pipeline</h2>\n<pre><code>Chunk documents (500 tokens, 50-token overlap)\n→ Embed with voyage-3\n→ Store in pgvector\n→ Hybrid search (semantic + BM25 with Reciprocal Rank Fusion)\n→ Rerank\n→ Augment prompt with top-K context\n→ Generate with citations\n</code></pre>\n<pre><code class=\"language-typescript\">// Augment prompt with retrieved context\nconst context = retrievedDocs.map(doc => doc.content).join(\"\\n\\n---\\n\\n\");\n\nconst response = await client.messages.create({\n  model: \"claude-opus-4-6\",\n  max_tokens: 4_000,\n  system: `Answer questions using the provided context. Cite sources with [Source N].\n  \nContext:\n${context}`,\n  messages: [{ role: \"user\", content: question }],\n});\n</code></pre>\n<h2>Compaction (Long Conversations)</h2>\n<pre><code class=\"language-typescript\">// Append response.content (not just text) to preserve compaction blocks\nmessages.push({ role: \"assistant\", content: response.content });\n\n// Enable compaction via beta header\nconst client = new Anthropic({\n  defaultHeaders: { \"anthropic-beta\": \"compact-2026-01-12\" },\n});\n</code></pre>\n<p>Compaction automatically summarizes earlier context when approaching the 200K limit.</p>\n"}