Skip to content

Commit d24c4e6

Browse files
igorcostaAutohand Evolve
andcommitted
Add a native Anthropic provider and stop sending Claude malformed requests
Two auto-reported session failures traced to the same place: Autohand sends Claude requests that Anthropic rejects. The first named a model OpenRouter does not serve; the second named a valid one and still failed, because OpenRouter forwarded a payload Anthropic refused. Anthropic validates strictly and rejects unsupported parameters rather than ignoring them, so anything we send loosely comes back as an opaque 400. Adds `anthropic` as a first-class provider on the official TypeScript SDK, with the Messages API contract handled at the provider boundary: system prompts, images, tool calls and results, structured output, adaptive thinking, and Anthropic usage fields. The bundled catalog gains the current Claude lineup for both the native provider and OpenRouter, and drops two OpenRouter IDs that never existed. Setup, /model, /settings, ACP, and the answer-only RPC surface all know about the provider. Every optional request parameter is now gated on the target model. Temperature is dropped for the Claude 5 family and Opus 4.7/4.8, output effort is clamped or omitted where unsupported, and the thinking parameter is omitted entirely for models that always think. Sending temperature unconditionally had been breaking the default OpenRouter model on every request. Thinking blocks are captured from each response and replayed unchanged on the next one. Thinking level defaults to normal, which omits the parameter, but the current models think adaptively regardless — so blocks come back every turn, and dropping them risks ordering and signature rejections inside the tool loop. `reasoningBlocks` is a provider-opaque passthrough on the message type, so other providers needing the same replay contract can adopt it later. Conversation history is repaired on the way out rather than trusted. A tool-only assistant turn now sends null content instead of an empty string, which aggregators translate into an empty text block; blank turns are dropped; blank tool results become a placeholder; and tool calls or results orphaned by context compaction are backfilled or removed. This is shared by OpenRouter and LLM Gateway, and the equivalent repair is applied to Bedrock's Converse payload. Vertex AI's native Claude branch was posting OpenAI-shaped messages, system role included, straight to streamRawPredict. It could not have worked with a system prompt. It now uses the shared Messages translation. OpenRouter's error.metadata.raw carries the upstream provider's actual complaint, while its own message is often the wrapper "Provider returned error". Discarding it is why both reports were undiagnosable; it is now included, which also lets a prompt-too-long rejection classify as context overflow and trigger auto-compaction instead of failing the session. Co-authored-by: Autohand Evolve <code-noreply@autohand.ai>
1 parent 0d6cb2b commit d24c4e6

50 files changed

Lines changed: 2772 additions & 155 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/ISSUE_TEMPLATE/model_catalog.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ body:
1616
options:
1717
- autohandai
1818
- openrouter
19+
- anthropic
1920
- ollama
2021
- openai
2122
- llmgateway

.github/scripts/generate-model-catalog.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { readFileSync, writeFileSync } from "node:fs";
55
const PROVIDER_DEFAULTS = {
66
autohandai: { api: "openai-completions", baseUrl: "https://api.autohand.ai/v1", contextWindow: 64000 },
77
openrouter: { api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1", contextWindow: 131072 },
8+
anthropic: { api: "anthropic-messages", baseUrl: "https://api.anthropic.com", contextWindow: 1000000 },
89
ollama: { api: "openai-completions", baseUrl: "http://127.0.0.1:11434/v1", contextWindow: 131072 },
910
llamacpp: { api: "openai-completions", baseUrl: "http://127.0.0.1:8080/v1", contextWindow: 131072 },
1011
openai: { api: "openai-responses", baseUrl: "https://api.openai.com/v1", contextWindow: 400000 },

bun.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/config-reference.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ Active LLM provider to use.
188188
| -------------- | ---------------------------- |
189189
| `"autohandai"` | Autohand AI Cloud or Local |
190190
| `"openrouter"` | OpenRouter API (default) |
191+
| `"anthropic"` | Anthropic Messages API |
191192
| `"ollama"` | Local Ollama instance |
192193
| `"llamacpp"` | Local llama.cpp server |
193194
| `"openai"` | OpenAI API directly |
@@ -280,6 +281,29 @@ OpenRouter provider configuration.
280281
| `model` | string | Yes | - | Model identifier (e.g., `your-modelcard-id-here`) |
281282
| `contextWindow` | number | No | Auto | Exact model context window. Autohand fills this from OpenRouter when known. |
282283

284+
### `anthropic`
285+
286+
Native Anthropic Messages API configuration. Autohand sends system prompts, tool definitions, tool uses, and tool results using Anthropic's native request format.
287+
288+
```json
289+
{
290+
"anthropic": {
291+
"apiKey": "sk-ant-xxx",
292+
"baseUrl": "https://api.anthropic.com",
293+
"model": "claude-sonnet-5",
294+
"contextWindow": 1000000
295+
}
296+
}
297+
```
298+
299+
| Field | Type | Required | Default | Description |
300+
| --------------- | ------ | -------- | --------------------------- | ------------------------------------------------ |
301+
| `apiKey` | string | Yes | - | Your Anthropic Console API key |
302+
| `baseUrl` | string | No | `https://api.anthropic.com` | Anthropic API origin; `/v1/messages` is appended |
303+
| `model` | string | Yes | `claude-sonnet-5` | Native Anthropic model identifier |
304+
| `contextWindow` | number | No | Catalog value | Model context window used by Autohand accounting |
305+
| `reasoningEffort` | string | No | Provider default | Native output effort (`low` through `xhigh`) |
306+
283307
### `zai`
284308

285309
Z.ai provider configuration.

docs/providers.md

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Autohand supports multiple LLM providers, giving you flexibility to choose betwe
88
- [Provider Comparison](#provider-comparison)
99
- [Cloud Providers](#cloud-providers)
1010
- [OpenRouter](#openrouter)
11+
- [Anthropic](#anthropic)
1112
- [OpenAI](#openai)
1213
- [LLM Gateway](#llm-gateway)
1314
- [DeepSeek](#deepseek)
@@ -54,6 +55,7 @@ Bundled provider model choices live in `src/providers/models.json` and packaged
5455
| --------------- | ----- | ----------- | ------- | ----------------------------------------------- |
5556
| **Autohand AI** | Cloud/Local | Account, API key, or local | Low | Fantail ultra-fast coding, Moa thinking, or local MLX coding models |
5657
| **OpenRouter** | Cloud | Pay-per-use | Low | Access to 100+ models, recommended default |
58+
| **Anthropic** | Cloud | Pay-per-use | Low | Native Claude Messages API and tool use |
5759
| **OpenAI** | Cloud | Pay-per-use | Low | Direct OpenAI access, GPT-5, o3 models |
5860
| **LLM Gateway** | Cloud | Pay-per-use | Low | Unified API for multiple providers |
5961
| **DeepSeek** | Cloud | Pay-per-use | Low | DeepSeek V4 Flash and V4 Pro models |
@@ -150,21 +152,67 @@ OpenRouter provides a unified API to access 100+ models from various providers (
150152
}
151153
```
152154

153-
**Popular Models:**
155+
**Current Anthropic Models:**
154156
| Model | Description |
155157
|-------|-------------|
156-
| `your-modelcard-id-here` | Best balance of speed and capability |
157-
| `anthropic/claude-5-opus` | Most capable Claude model |
158+
| `anthropic/claude-sonnet-5` | Claude Sonnet 5 |
159+
| `anthropic/claude-opus-5` | Claude Opus 5 |
160+
| `anthropic/claude-fable-5` | Claude Fable 5 |
161+
| `anthropic/claude-opus-4.8` | Claude Opus 4.8 |
162+
| `anthropic/claude-haiku-4.5` | Claude Haiku 4.5 |
158163
| `openai/gpt-5` | OpenAI's flagship model |
159164
| `google/gemini-3.0-pro` | Google's latest model |
160165
| `meta-llama/llama-3.1-70b-instruct` | Open-source alternative |
161166

162167
**Switching Models:**
163168

164169
```
165-
/model anthropic/claude-5-opus
170+
/model anthropic/claude-opus-5
166171
```
167172

173+
**Claude models through OpenRouter:** OpenRouter forwards requests to Anthropic
174+
largely unchanged, so Anthropic's stricter validation applies. Autohand omits
175+
`temperature` for the Claude 5 family and Opus 4.7/4.8 (which reject sampling
176+
parameters), requests extended thinking through OpenRouter's `reasoning` field,
177+
and repairs turn shapes that Anthropic rejects — blank turns, blank tool
178+
results, and tool calls or results orphaned by context compaction. When an
179+
upstream provider does reject a request, the provider's own error text is now
180+
included rather than only OpenRouter's generic "Provider returned error".
181+
182+
---
183+
184+
### Anthropic
185+
186+
The native Anthropic provider uses the official TypeScript SDK and the Messages API rather than routing Claude requests through the OpenAI-compatible provider layer. Native system prompts, images, tool calls, tool results, structured output, adaptive thinking, and Anthropic usage fields are translated at the provider boundary.
187+
188+
1. Create a key in the [Anthropic Console](https://console.anthropic.com/settings/keys).
189+
2. Run `autohand --setup` or select Anthropic from `/model`.
190+
3. Choose a model from the bundled and remotely updated catalog.
191+
192+
```json
193+
{
194+
"provider": "anthropic",
195+
"anthropic": {
196+
"apiKey": "sk-ant-your-key-here",
197+
"baseUrl": "https://api.anthropic.com",
198+
"model": "claude-sonnet-5",
199+
"contextWindow": 1000000
200+
}
201+
}
202+
```
203+
204+
Current catalog choices include `claude-sonnet-5`, `claude-opus-5`, `claude-fable-5`, `claude-opus-4-8`, `claude-sonnet-4-6`, and `claude-haiku-4-5`. Use the unprefixed model IDs for the native provider; use `anthropic/...` IDs only with OpenRouter.
205+
206+
**Request behavior:**
207+
208+
- Anthropic rejects unsupported parameters with a `400` rather than ignoring them, so Autohand gates each optional parameter on the selected model. `temperature` is dropped for the Claude 5 family and Opus 4.7/4.8, `output_config.effort` is clamped to `high` on Opus 4.5/4.6 and omitted for models without effort support, and `thinking` is omitted entirely for models where thinking is always on.
209+
- Prompt caching is on by default. The stable prefix (tools, system prompt, and prior turns) is cached automatically; check `/usage` for cache read and write counts.
210+
- Thinking blocks are captured from each response and replayed unchanged on the following request. Anthropic validates their ordering and signatures, so they are never edited or reconstructed.
211+
- Only the leading system messages become the top-level `system` prompt. Later system notes stay in conversation order as `<system-reminder>` blocks so the cached prefix is not invalidated mid-session.
212+
- Requests use a 10-minute floor for the client timeout because the Anthropic SDK applies `network.timeout` to the whole request, not just the response headers. A larger `network.timeout` is honored; a smaller one is not.
213+
214+
See Anthropic's [model overview](https://platform.claude.com/docs/en/about-claude/models/overview) and [TypeScript Messages API](https://platform.claude.com/docs/en/api/typescript/messages/create) for the upstream contracts.
215+
168216
---
169217

170218
### OpenAI
@@ -238,8 +286,8 @@ LLM Gateway provides a unified API for multiple LLM providers with a single inte
238286
| `gpt-5` | OpenAI |
239287
| `gpt-5-mini` | OpenAI |
240288
| `gpt-4-turbo` | OpenAI |
241-
| `claude-5-sonnet` | Anthropic |
242-
| `claude-5-haiku` | Anthropic |
289+
| `claude-sonnet-5` | Anthropic |
290+
| `claude-haiku-4-5` | Anthropic |
243291
| `gemini-3.0-pro` | Google |
244292
| `gemini-3.0-flash` | Google |
245293

@@ -608,7 +656,7 @@ Use the `/model` command to switch providers or models:
608656
```
609657
/model # List available models
610658
/model gpt-5 # Switch to GPT-5
611-
/model anthropic/claude-5-opus # Switch to Claude Opus
659+
/model anthropic/claude-opus-5 # Switch OpenRouter to Claude Opus 5
612660
```
613661

614662
When you pick `openai`, Autohand now lets you choose between `API key` and `ChatGPT account` authentication.
@@ -627,10 +675,10 @@ Update `~/.autohand/config.json`:
627675

628676
```json
629677
{
630-
"provider": "llmgateway",
631-
"llmgateway": {
632-
"apiKey": "your-key",
633-
"model": "claude-5-sonnet"
678+
"provider": "anthropic",
679+
"anthropic": {
680+
"apiKey": "sk-ant-your-key",
681+
"model": "claude-sonnet-5"
634682
}
635683
}
636684
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
},
7474
"dependencies": {
7575
"@agentclientprotocol/sdk": "1.3.0",
76+
"@anthropic-ai/sdk": "^0.120.0",
7677
"@aws-sdk/client-bedrock": "^3.1106.0",
7778
"@aws-sdk/client-bedrock-runtime": "^3.1086.0",
7879
"@aws-sdk/credential-providers": "^3.1090.0",

src/commands/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const SETTING_KEY_ALIASES: Record<string, string> = {
5959

6060
const CONFIG_PROVIDER_NAMES: readonly BuiltInProviderName[] = [
6161
'openrouter',
62+
'anthropic',
6263
'ollama',
6364
'llamacpp',
6465
'openai',

src/config.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ import { autoInitTheme, configureThemeSources, themeExists } from "./ui/theme/in
2828
import { loadLocalProjectSettings, type LocalProjectSettings } from "./permissions/localProjectPermissions.js";
2929
import { isAwsBedrockProviderEnabled } from "./features/featureRegistry.js";
3030
import { getCustomProviderConfig, isCustomProviderName } from "./providers/customProviders.js";
31-
import { getProviderDefaultModel, getProviderModelOptions, getProviderRuntimeDefaultModel } from "./providers/modelCatalog.js";
31+
import { getProviderDefaultModel, getProviderModelOptions, getProviderRuntimeDefaultModel, normalizeOpenRouterModelId } from "./providers/modelCatalog.js";
3232

3333
const DEFAULT_CONFIG_PATH = AUTOHAND_FILES.configJson;
3434
const TOML_CONFIG_PATH = AUTOHAND_FILES.configToml;
3535
const YAML_CONFIG_PATH = AUTOHAND_FILES.configYaml;
3636
const YML_CONFIG_PATH = AUTOHAND_FILES.configYml;
3737
const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
38+
const DEFAULT_ANTHROPIC_URL = "https://api.anthropic.com";
3839
const DEFAULT_OLLAMA_URL = "http://localhost:11434";
3940
const DEFAULT_LLAMACPP_URL = "http://localhost:8080";
4041
const DEFAULT_OPENAI_URL = "https://api.openai.com/v1";
@@ -85,6 +86,7 @@ function normalizeProviderName(provider: unknown): ProviderName | undefined {
8586
const validProviders: readonly BuiltInProviderName[] = [
8687
"autohandai",
8788
"openrouter",
89+
"anthropic",
8890
"ollama",
8991
"llamacpp",
9092
"openai",
@@ -749,7 +751,18 @@ function normalizeConfig(
749751

750752
if (isModernConfig(config)) {
751753
const provider = normalizeProviderName(config.provider) ?? "openrouter";
752-
return { ...config, provider };
754+
return {
755+
...config,
756+
provider,
757+
...(config.openrouter
758+
? {
759+
openrouter: {
760+
...config.openrouter,
761+
model: normalizeOpenRouterModelId(config.openrouter.model),
762+
},
763+
}
764+
: {}),
765+
};
753766
}
754767

755768
if (isLegacyConfig(config)) {
@@ -758,7 +771,7 @@ function normalizeConfig(
758771
openrouter: {
759772
apiKey: config.api_key ?? "replace-me",
760773
baseUrl: config.base_url ?? DEFAULT_BASE_URL,
761-
model: getProviderDefaultModel("openrouter", "anthropic/claude-4-sonnet"),
774+
model: getProviderDefaultModel("openrouter", "anthropic/claude-sonnet-5"),
762775
},
763776
workspace: {
764777
defaultRoot: process.cwd(),
@@ -784,6 +797,7 @@ function isModernConfig(
784797
): config is AutohandConfig {
785798
return (
786799
typeof (config as AutohandConfig).openrouter === "object" ||
800+
typeof (config as AutohandConfig).anthropic === "object" ||
787801
typeof (config as AutohandConfig).blueprintLocal === "object" ||
788802
typeof (config as AutohandConfig).autohandai === "object" ||
789803
typeof (config as AutohandConfig).ollama === "object" ||
@@ -1107,6 +1121,7 @@ export function getProviderConfig(
11071121
const configByProvider: Record<BuiltInProviderName, ProviderSettings | undefined> = {
11081122
autohandai: config.autohandai,
11091123
openrouter: config.openrouter,
1124+
anthropic: config.anthropic,
11101125
ollama: config.ollama,
11111126
llamacpp: config.llamacpp,
11121127
openai: config.openai,
@@ -1177,6 +1192,7 @@ export function getProviderConfig(
11771192
}
11781193
} else if (
11791194
builtInProvider === "openrouter" ||
1195+
builtInProvider === "anthropic" ||
11801196
builtInProvider === "llmgateway" ||
11811197
builtInProvider === "zai" ||
11821198
builtInProvider === "sakana" ||
@@ -1225,6 +1241,7 @@ function defaultBaseUrlFor(
12251241
port?: number,
12261242
): string | undefined {
12271243
if (provider === "openrouter") return DEFAULT_BASE_URL;
1244+
if (provider === "anthropic") return DEFAULT_ANTHROPIC_URL;
12281245
if (provider === "autohandai") return DEFAULT_AUTOHAND_AI_URL;
12291246
if (provider === "llmgateway") return DEFAULT_LLMGATEWAY_URL;
12301247
if (provider === "zai") return DEFAULT_ZAI_URL;

0 commit comments

Comments
 (0)