Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable user-facing changes should be documented in this file.

This changelog is maintained from source. Published release artifacts and tags remain the source of truth for shipped binaries and npm packages.

## Unreleased

### Added

- **Custom API-key providers without code changes**: define OpenAI-compatible vendors in `~/.cligate/provider-presets.json` (same schema as the built-in presets — `id`, `label`, `baseUrl`, `apiFormat`, optional `models`/`tiers`/`nativeModelPrefixes`/`pricing`/`validateModel`) and they become first-class everywhere: the add-key form, model mapping with their own tier maps, model discovery, protocol compatibility, pricing. Invalid entries are skipped with a warning; ids colliding with built-ins are rejected.

### Changed

- Zhipu (GLM), Moonshot (Kimi), and MiniMax are now declarative presets served by the generic OpenAI-compatible provider instead of bespoke classes (behavior preserved: same base URLs, ids, and pricing; Zhipu key validation probes a 1-token chat request via the new preset `validateModel` field, since Zhipu serves no `/models` endpoint).
- Zhipu/Moonshot/MiniMax keys now also serve `/v1/chat/completions` and the Codex Responses fallback (they previously only served `/v1/messages`).
- Zhipu/Moonshot/MiniMax now have their own tier maps and model lists, so non-native models are remapped instead of passing through verbatim, and the model-mapping page gives each its own editor instead of folding them into the OpenAI one (which the runtime never consulted for these keys).

## 1.3.1 - 2026-06-14

### Changed
Expand Down
73 changes: 73 additions & 0 deletions docs/CUSTOM_PROVIDERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Custom API-Key Providers

Add an OpenAI-compatible vendor **without touching the code**: describe it in
`~/.cligate/provider-presets.json` and it becomes a first-class provider —
selectable in the add-key form, visible on the model-mapping page with its own
tier map, included in model discovery, protocol compatibility, and pricing.

## Quick start

`~/.cligate/provider-presets.json` (full file, not a merge — include every
custom provider you want):

```json
[
{
"id": "clinepass",
"label": "ClinePass",
"baseUrl": "https://api.clinepass.example/v1",
"models": ["vendor-x", "vendor-y"],
"tiers": {
"flagship": "vendor-x",
"standard": "vendor-y",
"fast": "vendor-y",
"reasoning": "vendor-x"
},
"nativeModelPrefixes": ["vendor-"]
}
]
```

Restart the server. The new type appears in the dashboard's **Add API Key**
form; keys you add with it route through the generic OpenAI-compatible
provider. `nativeModelPrefixes` keeps ids the caller explicitly chose
(`vendor-x`) from being tier-remapped; everything else (e.g. an incoming
`claude-sonnet-4-6` from Claude Code) resolves through the tier map.

## Fields

Same schema as the built-in presets in `src/providers/provider-presets.js`:

| Field | Required | Meaning |
|----------------------|----------|---------|
| `id` | yes | Provider type key. Lowercase letters/digits/`.`/`_`/`-`; must not collide with a built-in id (`qwen`, `openrouter`, `zhipu`, `moonshot`, `minimax`, `clinepass`, `ollama-cloud`, `openai`, `anthropic`, `gemini`, `azure-openai`, `vertex-ai`, `deepseek`). |
| `baseUrl` | yes | Upstream API base URL (chat-completions compatible). |
| `label`, `badge` | no | Display name (defaults to `id`) and short UI tag (defaults to first two chars uppercased). `labelKey` adds i18n. |
| `apiFormat` | no | `openai_chat` (default) \| `openai_responses` \| `anthropic` \| `gemini_native`. `openai_chat` vendors serve all three gateway surfaces (chat, Codex Responses fallback, Claude Code bridge). |
| `website`, `keyHint` | no | Console link and key placeholder shown in the add-key form. |
| `models` | no | Static model ids for UI dropdowns and tier auto-fill (supplemented by discovery when the vendor serves `/models`). |
| `tiers` | no | `{ flagship, standard, fast, reasoning }` → model id. Unknown tier keys are rejected. |
| `nativeModelPrefixes`| no | Model ids starting with any prefix pass through without tier remap. |
| `passthroughModelMatch` | no | Model ids containing this substring pass through verbatim (e.g. `"/"` for `vendor/model` slugs). |
| `pricing` | no | `{ "<model>": { input, output, cacheRead?, cacheWrite? } }` in USD / 1M tokens. |
| `validateModel` | no | For vendors without `GET /models` (e.g. Zhipu): "test key" probes a 1-token chat request with this model instead. |

## Validation and error handling

The loader runs at server start. A bad file never blocks startup — each
invalid entry is skipped with a console warning explaining why:

- collides with a built-in id → skipped (built-ins always win),
- invalid id shape / missing `baseUrl` / unknown `apiFormat` / unknown tier key → skipped,
- duplicate ids → first entry wins.

The file must be a JSON array (or `{"presets": [...]}`).

## Limitations

- Only `openai_chat` user presets are wired into the protocol-compatibility
sets today (`openai_responses` / `anthropic` / `gemini_native` formats are
accepted but only serve the surfaces the generic provider implements).
- Model discovery probes one provider per type; with a custom `id` your vendor
is always probed — the limitation only affects two keys of the *same* type
with different base URLs.
10 changes: 10 additions & 0 deletions public/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,11 @@ export const i18n = {
provider: 'Provider',
providerQwen: 'Qwen',
providerOpenrouter: 'OpenRouter',
providerZhipu: 'Zhipu (GLM)',
providerMoonshot: 'Moonshot (Kimi)',
providerMinimax: 'MiniMax',
providerClinepass: 'ClinePass',
providerOllamaCloud: 'Ollama Cloud',
apiKeyLabel: 'API Key',
keyName: 'Name',
totalKeysLabel: 'Total Keys',
Expand Down Expand Up @@ -2330,6 +2335,11 @@ export const i18n = {
provider: '供应商',
providerQwen: '通义千问',
providerOpenrouter: 'OpenRouter',
providerZhipu: '智谱 (GLM)',
providerMoonshot: '月之暗面 (Kimi)',
providerMinimax: 'MiniMax',
providerClinepass: 'ClinePass',
providerOllamaCloud: 'Ollama Cloud',
apiKeyLabel: 'API 密钥',
keyName: '名称',
totalKeysLabel: '总密钥数',
Expand Down
6 changes: 3 additions & 3 deletions public/js/modules/settings-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -1033,11 +1033,11 @@ export function createSettingsPageModule() {
},

normalizeModelMappingProvider(type) {
// Provider identity must match runtime resolveModel(providerType, ...)
// 1:1 — collapsing zhipu/moonshot/minimax into 'openai' made the mapping
// page edit a tier map the runtime never consulted for those keys.
switch (type) {
case 'openai':
case 'moonshot':
case 'minimax':
case 'zhipu':
return 'openai';
case 'anthropic':
return 'anthropic';
Expand Down
9 changes: 3 additions & 6 deletions src/api-key-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,21 @@ import { AnthropicProvider } from './providers/anthropic.js';
import { GeminiProvider } from './providers/gemini.js';
import { AzureOpenAIProvider } from './providers/azure-openai.js';
import { VertexAIProvider } from './providers/vertex-ai.js';
import { MiniMaxProvider } from './providers/minimax.js';
import { MoonshotProvider } from './providers/moonshot.js';
import { ZhipuProvider } from './providers/zhipu.js';
import { DeepSeekProvider } from './providers/deepseek.js';
import { PROVIDER_PRESETS } from './providers/provider-presets.js';
import { makeOpenAICompatibleProvider } from './providers/openai-compatible.js';

const API_KEYS_FILE = join(CONFIG_DIR, 'api-keys.json');

// zhipu/moonshot/minimax used to be bespoke classes here; they are now
// declarative presets (provider-presets.js) served by the generic
// OpenAI-compatible class, like qwen and openrouter before them.
const PROVIDER_CLASSES = {
openai: OpenAIProvider,
anthropic: AnthropicProvider,
gemini: GeminiProvider,
'azure-openai': AzureOpenAIProvider,
'vertex-ai': VertexAIProvider,
minimax: MiniMaxProvider,
moonshot: MoonshotProvider,
zhipu: ZhipuProvider,
deepseek: DeepSeekProvider
};

Expand Down
24 changes: 13 additions & 11 deletions src/pricing-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,29 @@ const DEFAULT_PRICING = {
'claude-haiku-4-5': { input: 1.00, output: 5.00, cacheRead: 0, cacheWrite: 0 }
},
minimax: {
'MiniMax-M2.7': { input: 0.30, output: 1.20, cacheRead: 0, cacheWrite: 0 },
'MiniMax-M2.7-highspeed': { input: 0.30, output: 2.40, cacheRead: 0, cacheWrite: 0 },
'MiniMax-M3': { input: 0.30, output: 1.20, cacheRead: 0.06, cacheWrite: 0 },
'MiniMax-M2.7': { input: 0.30, output: 1.20, cacheRead: 0.06, cacheWrite: 0 },
'MiniMax-M2.7-highspeed': { input: 0.60, output: 2.40, cacheRead: 0, cacheWrite: 0 },
'MiniMax-M2.5': { input: 0.30, output: 1.20, cacheRead: 0, cacheWrite: 0 },
'MiniMax-M2.5-highspeed': { input: 0.30, output: 2.40, cacheRead: 0, cacheWrite: 0 },
'MiniMax-M2.1': { input: 0.20, output: 0.80, cacheRead: 0, cacheWrite: 0 },
'MiniMax-M2.1-highspeed': { input: 0.20, output: 1.60, cacheRead: 0, cacheWrite: 0 },
'MiniMax-M2': { input: 0.15, output: 0.60, cacheRead: 0, cacheWrite: 0 }
},
moonshot: {
'kimi-k2.5': { input: 0.60, output: 2.50, cacheRead: 0, cacheWrite: 0 },
'kimi-k2-thinking': { input: 0.60, output: 2.50, cacheRead: 0, cacheWrite: 0 },
'kimi-k2-thinking-turbo': { input: 0.30, output: 1.20, cacheRead: 0, cacheWrite: 0 }
'kimi-k3': { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 0 },
'kimi-k2.7-code': { input: 0.95, output: 4.00, cacheRead: 0.19, cacheWrite: 0 },
'kimi-k2.7-code-highspeed': { input: 1.90, output: 8.00, cacheRead: 0.38, cacheWrite: 0 },
'kimi-k2.6': { input: 0.95, output: 4.00, cacheRead: 0.16, cacheWrite: 0 }
},
zhipu: {
'glm-5': { input: 0.72, output: 2.30, cacheRead: 0, cacheWrite: 0 },
'glm-5-turbo': { input: 0.36, output: 1.15, cacheRead: 0, cacheWrite: 0 },
'glm-5.3': { input: 1.40, output: 4.40, cacheRead: 0.26, cacheWrite: 0 },
'glm-5.3-flash': { input: 0.15, output: 0.50, cacheRead: 0.03, cacheWrite: 0 },
'glm-5.2': { input: 1.40, output: 4.40, cacheRead: 0.26, cacheWrite: 0 },
'glm-5': { input: 1.00, output: 3.20, cacheRead: 0.20, cacheWrite: 0 },
'glm-4.7': { input: 0.40, output: 1.20, cacheRead: 0, cacheWrite: 0 },
'glm-4-plus': { input: 0.30, output: 0.90, cacheRead: 0, cacheWrite: 0 },
'glm-4-air': { input: 0.07, output: 0.07, cacheRead: 0, cacheWrite: 0 },
'glm-4-airx': { input: 0.14, output: 0.14, cacheRead: 0, cacheWrite: 0 },
'glm-4-flash': { input: 0.01, output: 0.01, cacheRead: 0, cacheWrite: 0 }
'glm-4.7-flash': { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
'glm-4-flash': { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
},
deepseek: {
'deepseek-v4-flash': { input: 0.27, output: 1.10, cacheRead: 0.07, cacheWrite: 0 },
Expand Down
30 changes: 0 additions & 30 deletions src/providers/minimax.js

This file was deleted.

30 changes: 0 additions & 30 deletions src/providers/moonshot.js

This file was deleted.

18 changes: 18 additions & 0 deletions src/providers/openai-compatible.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ export function makeOpenAICompatibleProvider(preset) {
return estimateCostWithRegistry(this.type, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
}

// Vendors without a GET /models endpoint declare a `validateModel` in
// their preset; validation then probes a 1-token chat request instead.
async validateKey() {
if (!preset.validateModel) {
return super.validateKey();
}
try {
const response = await this.sendRequest({
model: preset.validateModel,
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 1,
});
return response.status !== 401 && response.status !== 403;
} catch {
return false;
}
}

static get pricing() {
return getDefaultPricing(preset.id);
}
Expand Down
Loading