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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ OPENAI_COMPATIBLE_API_KEY=
OPENAI_COMPATIBLE_BASE_URL=
OPENAI_COMPATIBLE_MODEL=

# 默认模型 (可选,如 deepseek:deepseek-v4-flash)
# 默认模型 (可选,如 deepseek:deepseek-flash)
X_CODE_MODEL=

# Tavily 搜索 (可选,免费 1000 次/月,https://tavily.com)
Expand Down
12 changes: 0 additions & 12 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ jobs:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v7
with:
Expand All @@ -43,8 +41,6 @@ jobs:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v7
with:
Expand All @@ -67,8 +63,6 @@ jobs:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v7
with:
Expand All @@ -85,8 +79,6 @@ jobs:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v7
with:
Expand Down Expand Up @@ -121,8 +113,6 @@ jobs:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v7
with:
Expand All @@ -141,8 +131,6 @@ jobs:
fetch-depth: 0

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v7
with:
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ jobs:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v7
with:
Expand Down Expand Up @@ -101,8 +99,6 @@ jobs:
fetch-depth: 0

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v7
with:
Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ The current release stores ChatGPT tokens in a plaintext credential file under `
Alternatively, configure at least one provider API key:

> **Recommended: [DeepSeek](https://platform.deepseek.com/)** — affordable and capable enough for everyday coding. Promotional credits and prices can change; check the official console for current terms.
>
> The default DeepSeek model is V4.1 Flash (`deepseek-flash`). It receives supported image attachments and tool screenshots natively; X-Code does not send them to a separate vision model.

| Variable | Provider | Sign up |
| ------------------------------ | ------------------ | --------------------------------------------------------------------------- |
Expand All @@ -72,6 +74,21 @@ Alternatively, configure at least one provider API key:
| `ZHIPU_API_KEY` | Zhipu (GLM) | [open.bigmodel.cn](https://open.bigmodel.cn/usercenter/apikeys) |
| `MOONSHOT_API_KEY` | Moonshot (Kimi) | [Choose a service](#moonshot-kimi-endpoints) |

The curated `/model` catalog tracks the current API families below. Older supported entries remain selectable.

| Provider | Current catalog highlights | Smart default |
| --------- | --------------------------------------------------------------- | ------------------- |
| Anthropic | Claude Fable 5.1, Opus 5, Sonnet 5, Haiku 4.5 | Claude Sonnet 5 |
| OpenAI | GPT-6 Astra, GPT-5.6 Sol / Terra / Luna | GPT-5.6 Sol |
| DeepSeek | DeepSeek V4.1 Flash (native vision), V4 Pro (text only) | DeepSeek V4.1 Flash |
| Alibaba | Qwen3.8 Max / Flash (native vision), Qwen3.7 and Qwen3 variants | Qwen3.8 Max |
| Google | Gemini 3.8 / 3.7 / 3.6 / 3.5 Flash, Gemini 2.5 | Gemini 3.8 Flash |
| xAI | Grok 4.6, Grok 4.20 reasoning / non-reasoning | Grok 4.6 |
| Zhipu | GLM-5.3, GLM-5.3 Flash (native vision), earlier GLM models | GLM-5.3 |
| Moonshot | Kimi K3, K2.7 Code / Highspeed, K2.6 | Kimi K3 |

GPT-6 Astra is intentionally not the OpenAI smart default: GPT-5.6 Sol remains the safer default for ChatGPT subscription compatibility and lower accidental API cost. Models whose reasoning cannot be disabled automatically map `/thinking off` to their lowest supported effort.

**OpenAI-compatible escape hatch** (vLLM / OpenRouter / internal gateways): set both `OPENAI_COMPATIBLE_API_KEY` and `OPENAI_COMPATIBLE_BASE_URL`, then address models as `custom:<your-model-id>`.

<details>
Expand Down Expand Up @@ -134,7 +151,7 @@ To enable the `webSearch` tool, configure any one of the following:

> Tavily is recommended for first-time setup: simpler signup, LLM-optimized responses. When several keys are set, the first in the table order above wins. Set `X_CODE_WEB_SEARCH_PROVIDER` to `tavily`, `brave`, `exa`, `perplexity`, `firecrawl`, or `deepseek` to force a specific provider.
>
> **DeepSeek users need no extra key**: when the active model is a DeepSeek model and `DEEPSEEK_API_KEY` is set, `webSearch` automatically uses DeepSeek's built-in server-side web search. Note that each search is billed as a model turn (default `deepseek-v4-flash`), not as a flat search request.
> **DeepSeek users need no extra key**: when the active model is a DeepSeek model and `DEEPSEEK_API_KEY` is set, `webSearch` automatically uses DeepSeek's built-in server-side web search. Note that each search is billed as a model turn (default `deepseek-flash`), not as a flat search request.

</details>

Expand All @@ -147,7 +164,7 @@ Moonshot/Kimi credentials come from three separate services. A key only works wi
- China Open Platform: [platform.kimi.com](https://platform.kimi.com/console/api-keys) → `https://api.moonshot.cn/v1`
- International Open Platform: [platform.kimi.ai](https://platform.kimi.ai/console/api-keys) → `https://api.moonshot.ai/v1`

After selecting a Kimi model via `/model`, an endpoint picker appears automatically.
After selecting a Kimi model via `/model`, an endpoint picker appears automatically. On Coding Plan, the stable `kimi-for-coding` wire id currently auto-routes to K2.8 Preview; the public Moonshot API model ids remain unchanged.

</details>

Expand Down Expand Up @@ -312,6 +329,8 @@ Log path: `~/.x-code/logs/debug.log` (Windows: `%USERPROFILE%\.x-code\logs\debug

Requires Node.js 22+ and pnpm 10.x.

The repository pins pnpm 10.28.2 through `packageManager`. If pnpm 11 is installed globally, run `corepack enable` and use the repository-local `pnpm` (or `corepack pnpm`); running pnpm 11 directly is intentionally rejected by `engines.pnpm`.

```bash
git clone https://github.com/woai3c/x-code-cli.git
cd x-code-cli
Expand Down
23 changes: 21 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ xc logout # 退出并删除本地 ChatGPT 凭据
也可以配置至少一个厂商的 API Key:

> **推荐 [DeepSeek](https://platform.deepseek.com/)**:价格低、国内访问稳定,适合首次试用。赠送额度与价格可能变化,请以官方控制台为准。
>
> DeepSeek 默认模型为 V4.1 Flash(`deepseek-flash`)。它会原生接收支持的图片附件和工具截图,X-Code 不会将它们发送给其他视觉模型。

| 环境变量 | 厂商 | 注册地址 |
| ------------------------------ | ------------------- | --------------------------------------------------------------------------- |
Expand All @@ -72,6 +74,21 @@ xc logout # 退出并删除本地 ChatGPT 凭据
| `ZHIPU_API_KEY` | 智谱(GLM) | [open.bigmodel.cn](https://open.bigmodel.cn/usercenter/apikeys) |
| `MOONSHOT_API_KEY` | Moonshot(Kimi) | [按服务选择](#moonshot-kimi-endpoints) |

内置 `/model` 目录已跟进以下当前 API 型号;仍可用的旧型号继续保留。

| 厂商 | 当前目录重点型号 | 智能默认值 |
| --------- | ------------------------------------------------------ | ------------------- |
| Anthropic | Claude Fable 5.1、Opus 5、Sonnet 5、Haiku 4.5 | Claude Sonnet 5 |
| OpenAI | GPT-6 Astra、GPT-5.6 Sol / Terra / Luna | GPT-5.6 Sol |
| DeepSeek | DeepSeek V4.1 Flash(原生视觉)、V4 Pro(仅文本) | DeepSeek V4.1 Flash |
| 阿里通义 | Qwen3.8 Max / Flash(原生视觉)、Qwen3.7 与 Qwen3 系列 | Qwen3.8 Max |
| Google | Gemini 3.8 / 3.7 / 3.6 / 3.5 Flash、Gemini 2.5 | Gemini 3.8 Flash |
| xAI | Grok 4.6、Grok 4.20 推理版 / 非推理版 | Grok 4.6 |
| 智谱 | GLM-5.3、GLM-5.3 Flash(原生视觉)及较早 GLM 型号 | GLM-5.3 |
| Moonshot | Kimi K3、K2.7 Code / Highspeed、K2.6 | Kimi K3 |

GPT-6 Astra 不会自动成为 OpenAI 智能默认值:保留 GPT-5.6 Sol 可以兼顾 ChatGPT 订阅兼容性,并降低误用高价 API 的风险。对于不能关闭推理的型号,`/thinking off` 会自动降到该型号支持的最低推理档位。

**OpenAI 兼容接入**(vLLM / OpenRouter / 代理网关等):同时设置 `OPENAI_COMPATIBLE_API_KEY` 与 `OPENAI_COMPATIBLE_BASE_URL`,模型 ID 写成 `custom:<your-model-id>`。

<details>
Expand Down Expand Up @@ -134,7 +151,7 @@ setx DEEPSEEK_API_KEY "sk-..."

> 推荐首次配 Tavily:注册简便,返回格式针对 LLM 优化。配置多个 key 时按上表顺序取第一个;也可通过 `X_CODE_WEB_SEARCH_PROVIDER` 显式指定(可选值:`tavily`、`brave`、`exa`、`perplexity`、`firecrawl`、`deepseek`)。
>
> **DeepSeek 用户无需额外 key**:当前模型为 DeepSeek 且已配置 `DEEPSEEK_API_KEY` 时,`webSearch` 自动使用 DeepSeek 内置的服务端联网搜索。注意每次搜索按一次模型调用计费(默认 `deepseek-v4-flash`),而非按搜索次数计费。
> **DeepSeek 用户无需额外 key**:当前模型为 DeepSeek 且已配置 `DEEPSEEK_API_KEY` 时,`webSearch` 自动使用 DeepSeek 内置的服务端联网搜索。注意每次搜索按一次模型调用计费(默认 `deepseek-flash`),而非按搜索次数计费。

</details>

Expand All @@ -147,7 +164,7 @@ Moonshot/Kimi 提供三套独立的凭证与端点,API Key 只能用于签发
- 国内开放平台:[platform.kimi.com](https://platform.kimi.com/console/api-keys) → `https://api.moonshot.cn/v1`
- 国际开放平台:[platform.kimi.ai](https://platform.kimi.ai/console/api-keys) → `https://api.moonshot.ai/v1`

通过 `/model` 选择 Kimi 模型后,X-Code CLI 会自动显示端点选择器。
通过 `/model` 选择 Kimi 模型后,X-Code CLI 会自动显示端点选择器。Coding Plan 的稳定 wire id `kimi-for-coding` 当前会自动路由到 K2.8 Preview;Moonshot 开放平台的模型 ID 不变。

</details>

Expand Down Expand Up @@ -312,6 +329,8 @@ set DEBUG_STDOUT=1 && xc

需要 Node.js 22+ 和 pnpm 10.x。

仓库通过 `packageManager` 固定使用 pnpm 10.28.2。如果全局安装的是 pnpm 11,请先运行 `corepack enable`,再在仓库内使用 `pnpm`(或直接使用 `corepack pnpm`);`engines.pnpm` 会有意拒绝 pnpm 11 直接执行。

```bash
git clone https://github.com/woai3c/x-code-cli.git
cd x-code-cli
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"private": true,
"packageManager": "pnpm@10.28.2",
"type": "module",
"engines": {
"node": ">=22.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2039,7 +2039,7 @@ export function App({
// automatically on /model switch since switchModel updates
// state.modelId. When the model has a reasoning-effort tier configured
// (via the /model tier picker), it appends next to the name — e.g.
// "deepseek-v4-flash · High".
// "deepseek-flash · High".
modelLabel={
reasoningTierLabel
? `${renderModelLabel(state.modelId)} · ${reasoningTierLabel}`
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/ui/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ const SHELL_LABELS: Record<string, string> = {
export function getToolLabel(toolName: string): string {
const n = normalizeToolName(toolName)
if (n === 'shell' || n === 'bash') return SHELL_LABELS[getShellProvider().type] ?? 'Shell'
if (n === 'readfile' || n === 'read' || n === 'fileingest') return 'Read'
if (n === 'fileingest') return 'Attach'
if (n === 'readfile' || n === 'read') return 'Read'
if (n === 'writefile' || n === 'write') return 'Write'
if (n === 'edit' || n === 'update') return 'Update'
if (n === 'glob') return 'Glob'
Expand Down Expand Up @@ -192,6 +193,7 @@ export interface ReadGroupSummary {
const TABLE_OUTPUT_MAX_LINES = 30

export function formatReadGroupSummary(tools: readonly DisplayToolCall[]): ReadGroupSummary {
let attachCount = 0
let readCount = 0
let grepCount = 0
let globCount = 0
Expand All @@ -200,7 +202,11 @@ export function formatReadGroupSummary(tools: readonly DisplayToolCall[]): ReadG

for (const tc of tools) {
const n = normalizeToolName(tc.toolName)
if (n === 'read' || n === 'readfile' || n === 'fileingest') {
if (n === 'fileingest') {
attachCount++
const p = (tc.input.filePath as string) || (tc.input.file_path as string) || (tc.input.path as string) || ''
if (p) readPaths.push(basename(p))
} else if (n === 'read' || n === 'readfile') {
readCount++
const p = (tc.input.filePath as string) || (tc.input.file_path as string) || (tc.input.path as string) || ''
if (p) readPaths.push(basename(p))
Expand All @@ -214,6 +220,7 @@ export function formatReadGroupSummary(tools: readonly DisplayToolCall[]): ReadG
}

const clauses: string[] = []
if (attachCount > 0) clauses.push(`attached ${attachCount} file${attachCount === 1 ? '' : 's'}`)
if (readCount > 0) clauses.push(`read ${readCount} file${readCount === 1 ? '' : 's'}`)
if (grepCount > 0) clauses.push(`searched for ${grepCount} pattern${grepCount === 1 ? '' : 's'}`)
if (globCount > 0) clauses.push(`globbed ${globCount} pattern${globCount === 1 ? '' : 's'}`)
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/tests/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pnpm test:e2e
```

The runner detects which `*_API_KEY` you have set, lists the matching models,
and asks you to pick one. Default is `deepseek:deepseek-v4-flash` (cheap, fast).
and asks you to pick one. Default is `deepseek:deepseek-flash` (V4.1 Flash, cheap, fast, and vision-capable).

## Flags

Expand All @@ -34,8 +34,8 @@ pnpm test:e2e --max-turns 8 # cap agent loop turns

## Cost

`deepseek-v4-flash` runs the whole suite (26 scenarios, ~50-100K tokens total)
for roughly **$0.10–0.18 per full run**. Each scenario takes 5–30 seconds.
`deepseek-flash` runs the whole suite (26 scenarios, ~50-100K tokens total).
Cost varies with input/output mix, cache hits, and DeepSeek peak/off-peak pricing. Each scenario takes 5–30 seconds.
Full suite: 4–8 minutes typically.

If you only want to verify changes near a specific area, use `--filter` and
Expand Down Expand Up @@ -85,7 +85,7 @@ export default scenario
| Method | What it does |
| ----------------------------- | ---------------------------------------------------------------- |
| `ctx.tmpDir` | Absolute path to this scenario's temp directory. CLI runs there. |
| `ctx.modelId` | Resolved model id (e.g. `deepseek:deepseek-v4-flash`). |
| `ctx.modelId` | Resolved model id (e.g. `deepseek:deepseek-flash`). |
| `ctx.writeFile(rel, content)` | Write a file inside tmpDir. |
| `ctx.readFile(rel)` | Read a file. |
| `ctx.fileExists(rel)` | Returns `Promise<boolean>`. |
Expand Down
59 changes: 45 additions & 14 deletions packages/cli/tests/e2e/framework/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,70 @@ import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..', '..')

export const DEFAULT_MODEL = 'deepseek:deepseek-v4-flash'
export const DEFAULT_MODEL = 'deepseek:deepseek-flash'

/** Provider env-var → list of model ids that can be selected when that key is set.
* Keep aligned with `packages/core/src/types/index.ts::PROVIDER_DETECTION_ORDER`. */
const PROVIDER_MODELS: Record<string, string[]> = {
DEEPSEEK_API_KEY: ['deepseek:deepseek-v4-flash', 'deepseek:deepseek-v4-pro'],
DEEPSEEK_API_KEY: ['deepseek:deepseek-flash', 'deepseek:deepseek-v4-pro'],
ANTHROPIC_API_KEY: [
'anthropic:claude-fable-5-1',
'anthropic:claude-opus-5',
'anthropic:claude-fable-5',
'anthropic:claude-opus-4-8',
'anthropic:claude-sonnet-5',
'anthropic:claude-haiku-4-5',
],
OPENAI_API_KEY: ['openai:gpt-5.6-sol', 'openai:gpt-5.6-terra', 'openai:gpt-5.6-luna', 'openai:gpt-5.4-mini'],
GOOGLE_GENERATIVE_AI_API_KEY: ['google:gemini-3.5-flash', 'google:gemini-2.5-pro', 'google:gemini-2.5-flash'],
XAI_API_KEY: ['xai:grok-4.3', 'xai:grok-4.5'],
ALIBABA_API_KEY: ['alibaba:qwen3.7-max', 'alibaba:qwen3.7-plus', 'alibaba:qwen3-coder-plus', 'alibaba:qwq-plus'],
ZHIPU_API_KEY: ['zhipu:glm-5.2', 'zhipu:glm-5', 'zhipu:glm-4.7'],
MOONSHOT_API_KEY: ['moonshotai:kimi-k2.6', 'moonshotai:kimi-k3'],
OPENAI_API_KEY: [
'openai:gpt-6-astra',
'openai:gpt-5.6-sol',
'openai:gpt-5.6-terra',
'openai:gpt-5.6-luna',
'openai:gpt-5.4-mini',
],
GOOGLE_GENERATIVE_AI_API_KEY: [
'google:gemini-3.8-flash',
'google:gemini-3.7-flash',
'google:gemini-3.6-flash',
'google:gemini-3.5-flash',
'google:gemini-2.5-pro',
'google:gemini-2.5-flash',
],
XAI_API_KEY: ['xai:grok-4.6', 'xai:grok-4.20', 'xai:grok-4.20-non-reasoning', 'xai:grok-4.5'],
ALIBABA_API_KEY: [
'alibaba:qwen3.8-max',
'alibaba:qwen3.8-flash',
'alibaba:qwen3.7-max',
'alibaba:qwen3.7-plus',
'alibaba:qwen3.7-flash',
'alibaba:qwen3-coder-plus',
'alibaba:qwq-plus',
],
ZHIPU_API_KEY: ['zhipu:glm-5.3', 'zhipu:glm-5.3-flash', 'zhipu:glm-5.2', 'zhipu:glm-5', 'zhipu:glm-4.7'],
MOONSHOT_API_KEY: [
'moonshotai:kimi-k3',
'moonshotai:kimi-k2.7-code',
'moonshotai:kimi-k2.7-code-highspeed',
'moonshotai:kimi-k2.6',
],
}

/** Short aliases — accepted on CLI `--model` flag. Aligns with product's
* `MODEL_ALIASES` table; keep them in sync. */
export const ALIASES: Record<string, string> = {
fable: 'anthropic:claude-fable-5',
fable: 'anthropic:claude-fable-5-1',
sonnet: 'anthropic:claude-sonnet-5',
opus: 'anthropic:claude-opus-4-8',
opus: 'anthropic:claude-opus-5',
haiku: 'anthropic:claude-haiku-4-5',
gpt6: 'openai:gpt-6-astra',
astra: 'openai:gpt-6-astra',
gpt5: 'openai:gpt-5.6-sol',
gemini: 'google:gemini-3.5-flash',
deepseek: 'deepseek:deepseek-v4-flash',
gemini: 'google:gemini-3.8-flash',
deepseek: 'deepseek:deepseek-flash',
'deepseek-pro': 'deepseek:deepseek-v4-pro',
qwen: 'alibaba:qwen3.7-max',
glm: 'zhipu:glm-5.2',
qwen: 'alibaba:qwen3.8-max',
grok: 'xai:grok-4.6',
glm: 'zhipu:glm-5.3',
kimi: 'moonshotai:kimi-k3',
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/e2e/scenarios/14-large-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const scenario: Scenario = {
lines.push(`line ${i}: lorem ipsum dolor sit amet`)
}
// 关键标记放在尾部。前 4999 行模式完全一致 — 一个偷懒的模型只看 head
// 就会"按模式外推"答 `lorem ipsum dolor sit amet`(deepseek-v4-flash 实测会);
// 就会"按模式外推"答 `lorem ipsum dolor sit amet`(deepseek-flash 实测会);
// 只有真的二次调 readFile(offset≈5000) 拿到尾部才能引用出这个 token。
lines[4999] = 'line 5000: FINAL_SENTINEL_TOKEN_XYZ'
await ctx.writeFile('big.txt', lines.join('\n'))
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/openai-auth-transition-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('OpenAI auth transition state', () => {

it('falls back or disables deterministically when the new account has no models', () => {
expect(planOpenAIModelReconciliation('openai:old', [], ['deepseek'], 'blocked', (id) => id)).toMatchObject({
modelId: 'deepseek:deepseek-v4-flash',
modelId: 'deepseek:deepseek-flash',
})
expect(planOpenAIModelReconciliation('openai:old', [], [], 'blocked', (id) => id)).toEqual({
blockedMessage: 'blocked',
Expand Down
Loading
Loading