Metadata-Version: 2.5
Name: llm-anthropic-general
Version: 0.0.2
Summary: Register Anthropic-protocol-compatible providers as first-class llm models
Project-URL: Homepage, https://github.com/s2005/llm-anthropic-general
Project-URL: Source, https://github.com/s2005/llm-anthropic-general
Project-URL: Issues, https://github.com/s2005/llm-anthropic-general/issues
Author-email: s2005 <s2005@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Keywords: anthropic,glm,llm,plugin,qwen,zai
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: llm-anthropic>=0.28
Requires-Dist: llm>=0.34
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# llm-anthropic-general

Register Anthropic-protocol-compatible providers as first-class [llm](https://llm.datasette.io/) models.

## Why

`llm-anthropic` hardcodes its model list in `register_models` and reads no configuration of its own. Its `base_url` is only settable as a Python attribute, and `needs_key` is fixed at `anthropic`. The single available override is the process-wide `ANTHROPIC_BASE_URL` environment variable, which redirects every Anthropic model at once and leaves Anthropic's model names in your logs.

This plugin is the per-model equivalent of llm's built-in `extra-openai-models.yaml`, for hosts that speak the Anthropic protocol rather than the OpenAI one:

- Each model gets its own namespace (`zai/glm-5.3-flash`), instead of every model being forced under `anthropic/`.
- Each provider gets its own key entry, so `zai` and `anthropic` keys stay separate.
- Real Anthropic models are untouched.

## Requirements

- Python 3.13 or newer
- [uv](https://docs.astral.sh/uv/)
- `llm` 0.34 or newer, with `llm-anthropic` 0.28 or newer

## Install

Into the `llm` environment:

```bash
llm install llm-anthropic-general
```

From a local checkout:

```bash
llm install -e .
```

Development setup:

```bash
uv sync
```

## Quick start

```bash
llm keys set zai            # paste the key when prompted
llm -m zai/glm-5.3-flash "hello"   # or the alias: llm -m glm "hello"
llm models list | grep glm
```

## Built-in models

Registered with no configuration file present. Any of them can be replaced by a configuration entry using the same `model_id`.

| Registered id | Alias | Key name | Environment variable | Base URL |
| --- | --- | --- | --- | --- |
| `zai/glm-5.3-flash` | `glm` | `zai` | `ZAI_API_KEY` | `https://api.z.ai/api/anthropic` |
| `qwencloud/qwen3.8-flash` | `qwen` | `qwen` | `DASHSCOPE_API_KEY` | `https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic` |

Both base URLs were verified against provider documentation on 2026-09-06.

The Qwen host is the QwenCloud **Token Plan** endpoint for `ap-southeast-1`, not the generic `dashscope-intl` host. A QwenCloud key is bound to both a plan and its base URL, so a Token Plan key (prefix `sk-sp-`) authenticates only against this host, and a pay-as-you-go key (prefix `sk-`) will not. On a different plan or region, override `base_url` in the configuration file.

Providers move endpoints and retire models, so check before assuming a failure is a bug in this plugin.

DashScope publishes many more Anthropic-protocol model ids than the one shipped here, including the `qwen3.8-max`, `qwen3.6-plus`, `qwen3.8-flash` and `qwen3-coder-plus` families. Add whichever you use as configuration entries.

Do not append `/v1` to a base URL. The Anthropic client adds its own path segment, and a base URL ending in `/v1` produces a duplicated `/v1/v1/...` request that returns HTTP 404.

## Configuration

Create `extra-anthropic-models.yaml` in llm's user directory. Find it with:

```bash
dirname "$(llm logs path)"
```

The file is a list of model entries:

```yaml
- model_id: glm-5.3-flash
  prefix: zai
  base_url: https://api.z.ai/api/anthropic
  needs_key: zai
  key_env_var: ZAI_API_KEY
  aliases:
    - glm
  supports_thinking: true
  default_max_tokens: 8192

- model_id: my-proxy-sonnet
  claude_model_id: claude-sonnet-4-5
  base_url: https://gateway.internal.example/anthropic
  needs_key: gateway
```

### Entry fields

| Field | Required | Default | Description |
| --- | --- | --- | --- |
| `model_id` | yes | - | The name llm exposes. Not prefixed with `anthropic/`. |
| `base_url` | yes | - | Anthropic-protocol endpoint. A trailing slash is stripped. |
| `needs_key` | yes | - | Key name for `llm keys set <name>`. |
| `key_env_var` | no | `<NEEDS_KEY>_API_KEY` | Environment variable checked when no stored key is found. |
| `claude_model_id` | no | `model_id` | Model name sent to the provider, when it differs from the id you type. |
| `prefix` | no | none | Namespace for the registered id: `prefix/model_id`. Leading and trailing slashes are stripped. |
| `aliases` | no | none | List of extra names for the model. Must be a list, not a string. |

### Capability fields

All optional, all passed through to `llm-anthropic`. An unrecognized field is reported as an error rather than silently ignored.

| Field | Default | Description |
| --- | --- | --- |
| `supports_images` | `true` | Accept image attachments. |
| `supports_pdf` | `false` | Accept PDF attachments. |
| `supports_thinking` | `false` | Enable the thinking options. |
| `supports_thinking_effort` | `false` | Enable the thinking-effort options. |
| `supports_adaptive_thinking` | `false` | Enable adaptive thinking. |
| `supports_web_search` | `false` | Offer the server-side web search tool. |
| `supports_code_execution` | `false` | Offer the server-side code execution tool. |
| `thinks_by_default` | `false` | Treat thinking as on unless disabled. |
| `always_thinks` | `false` | Treat thinking as never disableable. |
| `use_structured_outputs` | `false` | Use structured outputs for schemas. |
| `supports_system_messages` | `false` | Allow mid-conversation system messages. |
| `default_max_tokens` | `4096` | Max tokens when the prompt does not set one. |

## Model id namespacing

Every provider plugin namespaces its models: `llm-openrouter` registers `openrouter/...`, `llm-gemini` registers `gemini/gemini-2.5-flash`, `llm-anthropic` registers `anthropic/...`. This plugin follows that convention, with the prefix chosen per entry rather than fixed.

It is not cosmetic. `llm` resolves models through a flat dictionary keyed by id (`llm.get_model_aliases`), so two models registering the same id silently overwrite one another - no warning, and the survivor depends on registration order. A prefix is what lets the same model name served by two hosts coexist.

Configuration-file entries are unprefixed unless they set `prefix`, matching llm's own `extra-openai-models.yaml`, which registers `model_id` verbatim. The shipped built-ins set it explicitly.

The prefix affects only the id llm exposes. The provider still receives `claude_model_id` (defaulting to `model_id`), so `zai/glm-5.3-flash` is sent over the wire as `glm-5.3-flash`.

## How it works

`model_id`, `needs_key` and `key_env_var` are llm's documented configuration surface for a model. `model_id` is declared on `llm.models._BaseModel`, and `needs_key` (required) and `key_env_var` (optional) are the attributes the plugin documentation tells authors to set on an `llm.KeyModel` subclass - see "Models that accept API keys" in llm's `docs/plugins/advanced-model-plugins.md`.

`llm_anthropic._Shared.__init__` already accepts `base_url`, but it also forces `self.model_id = "anthropic/" + model_id` and takes `needs_key` / `key_env_var` from class attributes. So this plugin subclasses `ClaudeMessages` and `AsyncClaudeMessages` and sets the three attributes in the subclass constructor, after `super().__init__()`. That is the same thing `llm-gemini` does to build `gemini/{id}` and `llm-openrouter` to build `openrouter/{id}`; the only difference is that the assignment must follow the parent constructor rather than sit in the class body.

A malformed entry is logged and skipped rather than raised, so one bad line cannot stop llm from starting or take the other models down with it.

## Development

```bash
uv sync                      # install dependencies
uv run pytest                # run tests
uv run pytest --cov          # run tests with coverage
uv run ruff check --fix .    # lint and autofix
uv run ruff format .         # format
uv run mypy                  # type check
```

## License

MIT
