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
6 changes: 2 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,8 @@ node dist/cli.js lang show

## API Endpoints

- 国内线路: `https://openai.qiniu.com`
- 国内备用线路: `https://api.qnaigc.com`
- 海外线路: `https://openai.sufy.com`
- Modelink 线路: `https://api.modelink.ai`
- 国内线路: `https://api.qnaigc.com`
- 海外线路: `https://api.modelink.ai`
- 支持接口: `/v1/chat/completions`、`/v1/messages`、`/v1/models`
- API Key 校验: `/v2/stat/usage`(轻量级,验证 token 有效性)
- 认证: Bearer token
Expand Down
16 changes: 7 additions & 9 deletions README.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
## ✨ Features

- **🎯 Interactive Wizard** — Guided step-by-step setup for language, endpoint, API Key, and model on first run
- **🌐 Multi-Region Support** — Domestic (openai.qiniu.com) / Domestic Backup (api.qnaigc.com) / International (openai.sufy.com) / Modelink (api.modelink.ai) endpoints
- **🌐 Multi-Region Support** — Domestic (api.qnaigc.com) / International (api.modelink.ai) endpoints
- **🔐 API Key Management** — Input, validate, save, and revoke API keys
- **📦 Model Configuration** — Fetch available models from the API, with manual model ID input support
- **⚡ Claude Code Integration** — Automatically writes environment variables to `~/.claude/settings.json`
Expand Down Expand Up @@ -208,10 +208,8 @@ npx qiniu-coding-helper enter hermes

| Region | Base URL | Best For |
|--------|----------|----------|
| 🇨🇳 **Domestic** | `https://openai.qiniu.com` | Users in China |
| 🇨🇳 **Domestic Backup** | `https://api.qnaigc.com` | Users in China |
| 🌍 **International** | `https://openai.sufy.com` | Users outside China |
| 🔁 **Modelink** | `https://api.modelink.ai` | Available in and outside China |
| 🇨🇳 **Domestic** | `https://api.qnaigc.com` | Users in China |
| 🌍 **International** | `https://api.modelink.ai` | Users outside China |

### 🔧 Claude Code Environment Variables

Expand All @@ -220,7 +218,7 @@ When configuration is applied, these environment variables are set in `~/.claude
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://openai.qiniu.com",
"ANTHROPIC_BASE_URL": "https://api.qnaigc.com",
"ANTHROPIC_AUTH_TOKEN": "<your-api-key>",
"API_TIMEOUT_MS": "3000000",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
Expand All @@ -241,7 +239,7 @@ model_provider = "qnaigc"

[model_providers.qnaigc]
name = "Qiniu"
base_url = "https://openai.qiniu.com/bypass/openai/v1"
base_url = "https://api.qnaigc.com/bypass/openai/v1"
requires_openai_auth = true
wire_api = "responses"

Expand All @@ -265,7 +263,7 @@ The configuration flow lets you multi-select models from the Qiniu model market
"id": "anthropic/claude-sonnet-4-5",
"name": "Claude Sonnet 4.5",
"vendor": "Qiniu",
"url": "https://openai.qiniu.com/v1/chat/completions",
"url": "https://api.qnaigc.com/v1/chat/completions",
"apiKey": "<your-api-key>",
"maxInputTokens": 200000,
"maxOutputTokens": 8192,
Expand Down Expand Up @@ -302,7 +300,7 @@ Hermes Agent stores its configuration in `~/.hermes/config.yaml`. The configurat
model:
provider: custom
default: anthropic/claude-sonnet-4-5
base_url: https://openai.qiniu.com/v1
base_url: https://api.qnaigc.com/v1
api_key: <your-api-key>
```

Expand Down
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
## ✨ 功能特性

- **🎯 交互式向导** — 首次运行引导配置语言、线路、API Key 和模型,步步引导
- **🌐 多线路支持** — 国内线路 (openai.qiniu.com) / 国内备用线路 (api.qnaigc.com) / 海外线路 (openai.sufy.com) / Modelink 线路 (api.modelink.ai)
- **🌐 多线路支持** — 国内线路 (api.qnaigc.com) / 海外线路 (api.modelink.ai)
- **🔐 API Key 管理** — 输入、验证、保存、删除,一条龙管理
- **📦 模型配置** — 从 API 获取可用模型列表,也支持手动输入模型 ID
- **⚡ Claude Code 集成** — 自动将环境变量写入 `~/.claude/settings.json`
Expand Down Expand Up @@ -208,10 +208,8 @@ npx qiniu-coding-helper enter hermes

| 线路 | 地址 | 适用场景 |
|------|------|----------|
| 🇨🇳 **国内线路** | `https://openai.qiniu.com` | 中国大陆用户 |
| 🇨🇳 **国内备用线路** | `https://api.qnaigc.com` | 中国大陆用户 |
| 🌍 **海外线路** | `https://openai.sufy.com` | 海外用户 |
| 🔁 **Modelink 线路** | `https://api.modelink.ai` | 国内外通用 |
| 🇨🇳 **国内线路** | `https://api.qnaigc.com` | 中国大陆用户 |
| 🌍 **海外线路** | `https://api.modelink.ai` | 海外用户 |

### 🔧 Claude Code 环境变量

Expand All @@ -220,7 +218,7 @@ npx qiniu-coding-helper enter hermes
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://openai.qiniu.com",
"ANTHROPIC_BASE_URL": "https://api.qnaigc.com",
"ANTHROPIC_AUTH_TOKEN": "<your-api-key>",
"API_TIMEOUT_MS": "3000000",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
Expand All @@ -241,7 +239,7 @@ model_provider = "qnaigc"

[model_providers.qnaigc]
name = "Qiniu"
base_url = "https://openai.qiniu.com/bypass/openai/v1"
base_url = "https://api.qnaigc.com/bypass/openai/v1"
requires_openai_auth = true
wire_api = "responses"

Expand All @@ -265,7 +263,7 @@ CodeBuddy 是一个 CLI 工具,配置保存在 `~/.codebuddy/models.json`。
"id": "anthropic/claude-sonnet-4-5",
"name": "Claude Sonnet 4.5",
"vendor": "Qiniu",
"url": "https://openai.qiniu.com/v1/chat/completions",
"url": "https://api.qnaigc.com/v1/chat/completions",
"apiKey": "<your-api-key>",
"maxInputTokens": 200000,
"maxOutputTokens": 8192,
Expand Down Expand Up @@ -300,7 +298,7 @@ Hermes Agent 配置保存在 `~/.hermes/config.yaml`。配置流程会从七牛
model:
provider: custom
default: anthropic/claude-sonnet-4-5
base_url: https://openai.qiniu.com/v1
base_url: https://api.qnaigc.com/v1
api_key: <your-api-key>
```

Expand Down
5 changes: 3 additions & 2 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import yaml from 'js-yaml';
import { DEFAULT_ENDPOINT, getBaseUrl } from './endpoints.js';
import { DEFAULT_ENDPOINT, getBaseUrl, normalizeEndpointId } from './endpoints.js';

// 配置文件路径
const CONFIG_DIR = path.join(os.homedir(), '.coding-helper');
Expand Down Expand Up @@ -96,7 +96,8 @@ class ConfigManager {
// 线路选择
getEndpoint(): string {
this.load();
return this.config.endpoint || DEFAULT_ENDPOINT;
// 规范化旧 id,兼容历史配置(如 modelink → international)
return normalizeEndpointId(this.config.endpoint || DEFAULT_ENDPOINT);
}

setEndpoint(endpoint: string): void {
Expand Down
26 changes: 14 additions & 12 deletions src/lib/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ export interface EndpointDefinition {
}

export const ENDPOINT_DEFINITIONS: Record<string, EndpointDefinition> = {
qiniu: {
id: 'qiniu',
i18nKey: 'endpoint_qiniu',
baseUrl: 'https://openai.qiniu.com',
},
china: {
id: 'china',
i18nKey: 'endpoint_china',
Expand All @@ -20,20 +15,27 @@ export const ENDPOINT_DEFINITIONS: Record<string, EndpointDefinition> = {
international: {
id: 'international',
i18nKey: 'endpoint_international',
baseUrl: 'https://openai.sufy.com',
},
modelink: {
id: 'modelink',
i18nKey: 'endpoint_modelink',
baseUrl: 'https://api.modelink.ai',
},
};

export const DEFAULT_ENDPOINT = 'qiniu';
export const DEFAULT_ENDPOINT = 'china';

// 旧线路 id 到新线路 id 的兼容映射(历史配置迁移)
// 老用户 config.yaml 里可能存有已移除的 id,读取时规范化到对应新线路
const LEGACY_ENDPOINT_ALIASES: Record<string, string> = {
qiniu: 'china', // 旧 openai.qiniu.com → 国内线路 api.qnaigc.com
modelink: 'international', // 旧 api.modelink.ai → 海外线路(地址不变)
};

// 规范化端点 id:已知旧 id 映射到新 id,其余原样返回
export function normalizeEndpointId(endpointId: string): string {
return LEGACY_ENDPOINT_ALIASES[endpointId] ?? endpointId;
}

// 根据端点 ID 获取 base URL
export function getBaseUrl(endpointId: string): string {
const endpoint = ENDPOINT_DEFINITIONS[endpointId];
const endpoint = ENDPOINT_DEFINITIONS[normalizeEndpointId(endpointId)];
if (!endpoint) {
return ENDPOINT_DEFINITIONS[DEFAULT_ENDPOINT].baseUrl;
}
Expand Down
6 changes: 2 additions & 4 deletions src/locales/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
"language_set": "Language set to: {{lang}}",
"language_current": "Current language: {{lang}}",
"endpoint_prompt": "Select API endpoint",
"endpoint_qiniu": "Domestic (openai.qiniu.com)",
"endpoint_china": "Domestic Backup (api.qnaigc.com)",
"endpoint_international": "International (openai.sufy.com)",
"endpoint_modelink": "Modelink (api.modelink.ai)",
"endpoint_china": "China (api.qnaigc.com)",
"endpoint_international": "International (api.modelink.ai)",
"endpoint_set": "Endpoint set to: {{endpoint}}",
"apikey_hint": "To get an API Key, visit: https://portal.qiniu.com/ai-inference/api-key",
"apikey_prompt": "Enter your API Key",
Expand Down
6 changes: 2 additions & 4 deletions src/locales/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
"language_current": "当前语言: {{lang}}",

"endpoint_prompt": "请选择 API 线路",
"endpoint_qiniu": "国内线路 (openai.qiniu.com)",
"endpoint_china": "国内备用线路 (api.qnaigc.com)",
"endpoint_international": "海外线路 (openai.sufy.com)",
"endpoint_modelink": "Modelink 线路 (api.modelink.ai)",
"endpoint_china": "国内线路 (api.qnaigc.com)",
"endpoint_international": "海外线路 (api.modelink.ai)",
"endpoint_set": "线路已设置为: {{endpoint}}",

"apikey_hint": "如需获取 API Key,请访问: https://portal.qiniu.com/ai-inference/api-key",
Expand Down
34 changes: 34 additions & 0 deletions tests/endpoints.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import {
DEFAULT_ENDPOINT,
ENDPOINT_DEFINITIONS,
getBaseUrl,
normalizeEndpointId,
} from '../dist/lib/endpoints.js';

test('normalizeEndpointId maps legacy modelink id to international', () => {
// 老用户配置里的 modelink 应规范化到新的海外线路 id
assert.equal(normalizeEndpointId('modelink'), 'international');
});

test('getBaseUrl keeps legacy modelink users on api.modelink.ai', () => {
// 关键回归:老 Modelink 用户升级后地址不应漂移到国内线路
assert.equal(getBaseUrl('modelink'), 'https://api.modelink.ai');
});

test('normalizeEndpointId maps legacy qiniu id to china', () => {
assert.equal(normalizeEndpointId('qiniu'), 'china');
assert.equal(getBaseUrl('qiniu'), 'https://api.qnaigc.com');
});

test('normalizeEndpointId returns current ids unchanged', () => {
assert.equal(normalizeEndpointId('china'), 'china');
assert.equal(normalizeEndpointId('international'), 'international');
});

test('getBaseUrl falls back to default endpoint for unknown ids', () => {
const expected = ENDPOINT_DEFINITIONS[DEFAULT_ENDPOINT].baseUrl;
assert.equal(getBaseUrl('unknown-endpoint'), expected);
});
6 changes: 3 additions & 3 deletions tests/hermes-tool.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ test('buildHermesConfig writes Qiniu custom endpoint and preserves unrelated con
});

test('buildHermesConfig escapes scalar values that require YAML quoting', () => {
const next = buildHermesConfig('', 'https://openai.sufy.com/', 'key:with#chars', 'vendor/model:latest');
const next = buildHermesConfig('', 'https://api.modelink.ai/', 'key:with#chars', 'vendor/model:latest');
const parsed = yaml.load(next);

assert.match(next, /base_url: https:\/\/openai\.sufy\.com\/v1/);
assert.match(next, /base_url: https:\/\/api\.modelink\.ai\/v1/);
assert.deepEqual(parsed.model, {
provider: 'custom',
default: 'vendor/model:latest',
base_url: 'https://openai.sufy.com/v1',
base_url: 'https://api.modelink.ai/v1',
api_key: 'key:with#chars',
});
});
Expand Down
Loading