Downstream API key policy plugin for CLIProxyAPI.
In plain words: you issue your own cpa_… keys to clients. Each key only sees the models you allow, can be rate-limited and budget-limited, and is routed to real CPA upstream providers (Codex, Claude, OpenAI-compat channels, etc.). CPA’s own api-keys can still exist for admin use — do not put plugin-issued keys into api-keys, or you bypass this plugin.
| Repo | origin652/cpa-plugin-key-policy |
| License | MIT |
| Install | CLIProxyAPI Plugins Store or build from source |
| 中文说明 | README.zh-CN.md |
- Issue keys — create many downstream keys; each has an allow-list of models (or shared aliases).
- Route — client calls with alias name
fast; plugin rewrites to e.g.codex+gpt-5.4-mini. - Limit — per-key RPM, optional daily/weekly USD caps, token or per-call billing.
- Isolate credentials (tiers / groups) — pin a request to Codex free/team/… or to a custom classify group so it never lands on the wrong auth file.
- Multi-target aliases — one alias can point at several backends (priority or round-robin).
- Web UI — manage keys, global aliases, and credential classification inside CPA.
A plugin-owned secret (cpa_…). Authenticated only by this plugin. Holds:
- allowed models and/or aliases
- RPM
- optional daily / weekly dollar limits
- optional
allow_models_endpoint(see below)
A reusable name like fast that expands to one or more targets:
| Field | Meaning |
|---|---|
provider |
CPA provider id (codex, claude, or an openai-compatibility name such as cerebras) |
target_model |
Real upstream model id |
group |
Optional credential filter (see Credential groups) |
dispatch |
priority (always first usable target) or round-robin |
| billing | tokens (per-million prices) or per_call (fixed USD) |
Keys can reference aliases instead of duplicating targets. Multi-target aliases expand to several rules with the same alias name; auth and routing share one pick per request so the group filter matches the chosen target.
Two sources of “which auth file may serve this request”:
| Kind | How it appears in the picker | Stored in mapping as |
|---|---|---|
Built-in tier (Codex plan_type, Antigravity tier) |
e.g. Free tier / Team | bare name: free, team, supported |
| Custom classify rule | e.g. Custom · vip | prefixed: classify:vip |
Runtime rule: if a mapping sets a group, the plugin scheduler only picks auth files in that group. No match → hard failure (auth_not_found), never silently fall back to another tier.
Scheduling: the plugin keeps the highest available Priority tier, then applies smooth weighted round-robin using each credential's weight:
- Weight is read from the CPA credential candidate's
Weight,Attributes.weight, orMetadata.weightfield. - Missing or invalid weight defaults to
1; non-positive weight stops new requests; values are capped at1000000. - State is shared globally by
provider + model + group + Priority, so all downstreamcpa_…keys contribute to one distribution. An empty group is one global pool containing every candidate CPA offers for that provider/model. - Lower-priority credentials participate only when every higher-priority credential is unavailable or has non-positive weight.
- When CPA does not propagate frontend-auth group metadata (including CPA
7.2.140), plugin-owned keys still use global weighted round-robin instead of falling back to CPA'srouting.strategy; native CPA keys remain untouched. - With
global_weighted_round_robin: true, the plugin deliberately ignores group even when CPA propagates it, then schedules by Weight across every current provider/model candidate.
Custom classification (Web UI → Mapping → Credential Classification):
- Match auth-file fields (
filename,provider,plan_type,tier, …) with a regex. - Assign a group name you choose (stored bare on the rule).
- Catalog and mappings use
classify:<name>so it never collides with built-infree/team. - One file can match multiple custom groups (shown under each).
- If no custom rule matches → built-in tier (for Codex/Antigravity) or flat (no group) for other auth-file providers.
- OpenAI-compat / API-key channels stay flat (no groups).
Configure classify rules in the UI, or via management API (/classify-rules, /classify-preview, /catalog). You do not need to hand-edit state JSON for normal use.
Channels under CPA openai-compatibility (e.g. a named proxy) use the channel name as provider. The plugin maps it to CPA’s internal key openai-compatible-<name> when routing. Models must be listed on that channel in CPA config, or the host reports no auth for that model.
| Hook | Role |
|---|---|
| Frontend auth | Know plugin keys; enforce alias allow-list, RPM, budget; stamp route + group metadata |
| Model router | Alias → provider + target model |
| Scheduler | Optionally filter candidates by group, then smooth-weight them within the highest Priority tier |
| Response interceptor | Non-stream JSON: rewrite top-level model back to the alias |
| Usage | Token / per-call billing into the state file |
| Management API + embedded Web UI | Keys, aliases, classify rules, status |
Linux .so needs cgo and a matching toolchain:
make test
make build-linux # builds web UI, then linux amd64/arm64 .so
# or
make web-build
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -buildvcs=false -tags cshared \
-buildmode=c-shared -o dist/cpa-key-policy_linux_amd64.so ./cmd/cpa-key-policyOn Windows, build the .so via WSL/Linux. go test ./... uses a non-cgo stub so unit tests run without a shared-library toolchain.
Copy the .so into CPA plugins.dir and enable the plugin in config.
Minimal shape (see also config.example.yaml):
plugins:
enabled: true
dir: "plugins"
configs:
cpa-key-policy:
enabled: true
priority: 10
state_file: "cpa-key-policy-state.json"
global_weighted_round_robin: trueNotes:
- If
state_fileexists, it is the source of truth for keys / aliases / classify rules / usage. global_weighted_round_robin: trueignores the selected alias target group and places every current provider/model candidate in one global pool. Distribution then follows the Weight values on CPA's credential page. The default isfalse.- With this option enabled, alias-level group rotation no longer restricts the final credential. Native CPA keys remain unaffected.
- Prefer creating keys and aliases in the Web UI or Management API; seed YAML
keysis mainly for first boot. - Never commit real key hashes, management secrets, or live host URLs into public docs.
Embedded in the plugin. After load, open:
http://<your-cpa-host>:<api-port>/v0/resource/plugins/cpa-key-policy/index.html
Login with CPA management secret (remote-management.secret-key / management password). The secret stays in memory only (not localStorage); refresh → re-login.
UI areas:
| Tab / page | Use for |
|---|---|
| Keys | Create / edit / rotate / delete keys; bind models or aliases; RPM & budgets |
| Mapping → Aliases | Global multi-target aliases, dispatch, pricing |
| Mapping → Classification | Custom credential groups + match preview |
| Model picker | Catalog of providers; tier / Custom · … subgroups |
Dev UI without rebuilding the .so:
cd web
npm install
VITE_CPA_BASE=http://127.0.0.1:8317 npm run devExact paths (no path templates). Auth: CPA management bearer token.
Keys
GET/POST/PATCH/DELETE …/keys(idin query or body for mutate)POST …/keys/rotate?id=…POST …/keys/reset-rpm?id=…GET …/keys/usage?id=…GET …/status
Aliases
GET/POST/DELETE …/aliases
Classify
GET/POST/DELETE …/classify-rulesPOST …/classify-rules/reorderPOST …/classify-preview— group → credential ids (UI preview; bare group names)POST …/catalog— body: auth-file credentials + models; response: pickerentrieswithclassify:groups
Create key (plain key returned once):
curl -X POST "$CPA/v0/management/plugins/cpa-key-policy/keys" \
-H "Authorization: Bearer $MANAGEMENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "team-a",
"name": "Team A",
"rpm": 60,
"models": [
{"alias":"fast","provider":"codex","target_model":"gpt-5.4-mini","group":"free"}
]
}'Create a multi-target alias:
curl -X POST "$CPA/v0/management/plugins/cpa-key-policy/aliases" \
-H "Authorization: Bearer $MANAGEMENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"alias": "cheap-chat",
"dispatch": "priority",
"billing_mode": "tokens",
"targets": [
{"provider":"cerebras","target_model":"gpt-oss-120b"},
{"provider":"codex","target_model":"gpt-5.4-mini","group":"free"}
]
}'| Case | Result |
|---|---|
| Known key + allowed alias | Auth OK → route → optional group filter → upstream |
| Known key + unknown model | Auth rejected |
| RPM / budget exceeded | Rejected |
| Group set, no matching auth file | auth_not_found / unavailable (no cross-tier leak) |
| Unknown key | Plugin declines; CPA may try native api-keys |
| Non-stream chat response | Top-level model rewritten to alias |
| Stream | Body not rewritten (v1) |
Per-key allow_models_endpoint: binary — deny (401) or full global list. CPA cannot filter that list per plugin key on the main port.
- Build / install the
.sointo CPAplugins.dir. - Enable
plugins+cpa-key-policyin CPA config; setstate_file. - Open the Web UI with the management secret.
- (Optional) Define classify rules if you need custom credential buckets.
- Create aliases (multi-target / pricing) and/or pick models per key (with tier or Custom group).
- Create keys, save the one-time
plain_key, hand out to clients. - Client: OpenAI-compatible base URL = CPA;
Authorization: Bearer cpa_…;model= alias name. - Ensure openai-compat channels list the models you map; empty model lists → host “no auth” errors.
go test ./...
cd web && npm test && npm run build