# prime-alibaba

Prime Agent extension that registers Alibaba Model Studio **token plan** models
(`https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1`) as the
`alibaba` provider over the OpenAI-compatible API.

## Install

Installed as an auto-discovered global extension:

```
~/.prime/agent/extensions/prime-alibaba -> /Users/dawn/proj/prime-alibaba
```

Edits here are picked up by `/reload` in a running session; new sessions load it at startup.
For a one-off run without installing: `prime-agent -e /Users/dawn/proj/prime-alibaba`.

## Auth

Key resolution order:

1. key stored by `/login` -> pick **Alibaba Model Studio (token plan)**, paste the key
2. `ALIBABA_API_KEY`

There is no key file. Store the key with `/login` and it lives in
`~/.prime/agent/auth.json`, which Prime Agent checks before the extension's own config;
that is the durable source. `ALIBABA_API_KEY` exists for throwaway shells, and when
neither is present the provider reports an unresolved key rather than sending a
placeholder as the credential.

`ALIBABA_BASE_URL` overrides the endpoint (other region or gateway).

## Model list

`models.json` is the model list, committed and read at startup. Nothing about a model's
capabilities is looked up at runtime.

Regenerate it after a plan or catalog change:

```
npx tsx regen.ts
```

That asks the endpoint for `/models` (what the key is entitled to) and models.dev for
metadata (context window, output limit, reasoning, image input), then writes the file.
Non-chat entitlements (audio, image generation) are skipped, and the ids it refused are
recorded in `skipped`. Ids the `alibaba-token-plan` entry has not caught up with are filled
from another Alibaba catalog listing the same id (`alibaba-cn` and the `-cn` variants
describe the same models) and never from an unrelated provider, where a shared id is a name
collision: `auto` is a route on this gateway and model-oracle-ai's model elsewhere, so that
entry would hand the route someone else's 1M context and image input. Freezing the choice in
a committed file is the point, because resolving it live made `deepseek-v4.1-flash` gain and
lose image input between runs depending on which provider models.dev happened to list first.

At startup the extension still asks `/models` for entitlement only, and hides models the key
cannot call. The list is cached in `~/.prime/agent/alibaba-entitlement.json`, so a slow or
failed call reuses the last known list. Entitlement can only hide, never add, so an id the
account gains after the last bake is invisible until `regen.ts` runs again; a session that
finds one notifies with the ids and says to regenerate, rather than dropping them in silence.

## Thinking

| family | switch | depth | levels offered |
| --- | --- | --- | --- |
| deepseek | `thinking: {type}` (verified) or `enable_thinking` (documented) | `reasoning_effort`: low, medium, high, xhigh, max, default high | off, low, medium, high, xhigh, max |
| qwen | `enable_thinking` | `thinking_budget` (token cap) | off, high |
| glm | none, thinking is on by default and glm-5.3 refuses to turn it off | `reasoning_effort`: glm-5.2 none..max, glm-5.3 low/high/max only | glm-5.2 off, low, medium, high, xhigh, max; glm-5.3 low, high, max |

DeepSeek on Model Studio takes the full `reasoning_effort` ladder, unlike Prime's
built-in DeepSeek entries, which expose only high and max - so the levels are remapped
here rather than inherited.

GLM depth is `reasoning_effort`, measured against the endpoint rather than assumed. glm-5.3
answers `invalid_parameter_error` for every value outside low/high/max and for
`enable_thinking: false` ("restricted to True"), so it never sends `enable_thinking` and
hides off; a request already pinned to off clamps up to low instead of failing. glm-5.2
takes none through max, and none - what off maps to - suppresses reasoning the same way
`enable_thinking: false` does. The docs give `enable_thinking` priority over
`reasoning_effort`, which is the other reason not to send both.

Qwen still has no effort parameter. Its depth is a token cap (`thinking_budget`), which
Prime's OpenAI-completions path does not send, so every level above off is the same
request and only off/high are offered.

Sources: [deep thinking](https://help.aliyun.com/en/model-studio/deep-thinking),
[DeepSeek API](https://help.aliyun.com/en/model-studio/deepseek-api),
[GLM](https://www.alibabacloud.com/help/en/model-studio/glm).

## Prompt caching

Model Studio has two modes, and they are mutually exclusive per request:

- implicit: always on, no markers, 1024-token minimum, hit not guaranteed
- explicit: `cache_control: {"type": "ephemeral"}` markers, 5 minute TTL that resets on
  each hit, deterministic, 1024-token minimum

Qwen honours the markers here (usage comes back with `cache_type: ephemeral` and
`cache_creation_input_tokens`), so qwen models get `cacheControlFormat: "anthropic"` and
Prime marks the system prompt, the last tool definition, and the last conversation
message. GLM and DeepSeek on this gateway ignore the markers and cache implicitly, so
they are left unmarked.

Measured in-session: turn 2 of a session reported `cacheRead: 15042` of ~15.1K prompt
tokens. Separate `--print` runs never share a cache because the system prompt embeds the
per-session conversation log path about 2.6K characters in, which is under the 1024-token
minimum for the shared prefix.

## Rate limits

Model Studio limits are per account, per model, per minute (TPM) and per second (TPS =
TPM/60). A 429 reads `Allocated quota exceeded` and means TPM, not the 7-day plan quota.
The gateway sends no `Retry-After` and no `x-ratelimit-*` headers, so the only signal is
the failure itself.

Recovery is fast: in a measured session, 32 of 124 calls hit 429, never more than two in
a row, and the next successful call landed 2.6-3.1s later. Prime handles this with its
own retry, tuned in `~/.prime/agent/settings.json`:

```json
{ "retry": { "maxRetries": 5, "baseDelayMs": 1000 } }
```

That gives 1s, 2s, 4s, 8s, 16s. Sustained throughput in that session was 0.7-1.6M
tokens/minute with 129K-token prompts, which is what pushes it over the limit; prompt
caching does not help here, because cached tokens still count as prompt tokens.
