Skip to content
Closed
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
8 changes: 4 additions & 4 deletions apps/desktop/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,8 @@ const APP_MESSAGES = {
brave: "Brave Search API key",
firecrawl: "Firecrawl API key",
tavily: "Tavily API key",
exa: "Exa API key (optional)",
anysearch: "AnySearch API key (optional)",
exa: "Exa API key",
anysearch: "AnySearch API key",
zhihu: "Zhihu Access Secret",
},
envPrefix: "Values starting with ",
Expand Down Expand Up @@ -1262,8 +1262,8 @@ const APP_MESSAGES = {
brave: "Brave Search API Key",
firecrawl: "Firecrawl API Key",
tavily: "Tavily API Key",
exa: "Exa API Key(可选)",
anysearch: "AnySearch API Key(可选)",
exa: "Exa API Key",
anysearch: "AnySearch API Key",
zhihu: "知乎 Access Secret",
},
envPrefix: "以 ",
Expand Down
78 changes: 61 additions & 17 deletions packages/runtime/src/models/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,68 @@ import { DEEPSEEK_MODELS } from "@earendil-works/pi-ai/providers/deepseek.models

type DeepSeekApi = "openai-completions" | "openai-responses";

/**
* DeepSeek V4.1 Flash intermediate-version preview (opened 2026-09-08,
* expires 2026-09-10 — the id suffix is the deadline). New architecture with
* native multimodal (text + image) input; pricing matches deepseek-v4-flash
* and each account is rate-limited to 20 concurrent requests. Announced after
* the upstream pi-ai catalog shipped, so it is declared here on the
* OpenAI-compatible completions endpoint (the preview has not announced
* Responses-API support).
*/
const DEEPSEEK_V41_FLASH_PREVIEW: Model<"openai-completions"> = {
id: "deepseek-v4.1-flash-expires-on-0910",
name: "DeepSeek V4.1 Flash Preview",
api: "openai-completions",
baseUrl: "https://api.deepseek.com",
provider: "deepseek",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.14,
output: 0.28,
cacheRead: 0.0028,
cacheWrite: 0,
},
contextWindow: 1_000_000,
maxTokens: 384_000,
compat: {
supportsStore: false,
supportsDeveloperRole: false,
maxTokensField: "max_tokens",
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "deepseek",
},
thinkingLevelMap: {
minimal: null,
low: "low",
medium: null,
high: "high",
max: "max",
},
};

export function deepseekProvider(): Provider<DeepSeekApi> {
const models = Object.values(DEEPSEEK_MODELS).map((model) => {
if (
model.id !== "deepseek-v4-flash" &&
model.id !== "deepseek-v4-pro"
) {
return model;
}
return {
...model,
api: "openai-responses",
compat: {
supportsDeveloperRole: false,
supportsLongCacheRetention: false,
sessionAffinityFormat: "openai-nosession",
},
} satisfies Model<"openai-responses">;
});
const models = [
...Object.values(DEEPSEEK_MODELS).map((model) => {
if (
model.id !== "deepseek-v4-flash" &&
model.id !== "deepseek-v4-pro"
) {
return model;
}
return {
...model,
api: "openai-responses",
compat: {
supportsDeveloperRole: false,
supportsLongCacheRetention: false,
sessionAffinityFormat: "openai-nosession",
},
} satisfies Model<"openai-responses">;
}),
DEEPSEEK_V41_FLASH_PREVIEW,
];

return createProvider<DeepSeekApi>({
id: "deepseek",
Expand Down
55 changes: 55 additions & 0 deletions packages/runtime/tests/models/providers/deepseek/deepseek.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, test } from "bun:test";

import { deepseekProvider } from "../../../../src/models/providers/deepseek";

describe("deepseekProvider", () => {
test("exposes the DeepSeek endpoint with the upstream models plus the V4.1 Flash preview", () => {
const provider = deepseekProvider();
const models = provider.getModels();

expect(provider.id).toBe("deepseek");
expect(provider.baseUrl).toBe("https://api.deepseek.com");

const ids = models.map((model) => model.id);
expect(ids).toContain("deepseek-v4-flash");
expect(ids).toContain("deepseek-v4-pro");
expect(ids).toContain("deepseek-v4.1-flash-expires-on-0910");
});

test("the V4.1 Flash preview is multimodal, 1M-context, and priced like V4 Flash", () => {
const provider = deepseekProvider();
const preview = provider
.getModels()
.find((model) => model.id === "deepseek-v4.1-flash-expires-on-0910");
expect(preview).toBeDefined();
expect(preview?.api).toBe("openai-completions");
expect(preview?.baseUrl).toBe("https://api.deepseek.com");
expect(preview?.provider).toBe("deepseek");
// Native multimodal input (new V4.1 architecture).
expect(preview?.input.map(String)).toEqual(["text", "image"]);
expect(preview?.reasoning).toBe(true);
expect(preview?.contextWindow).toBe(1_000_000);
// Same billing as deepseek-v4-flash.
expect(preview?.cost).toEqual({
input: 0.14,
output: 0.28,
cacheRead: 0.0028,
cacheWrite: 0,
});
expect(preview?.compat).toMatchObject({
maxTokensField: "max_tokens",
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "deepseek",
});
});

test("keeps the Responses-API override for V4 Flash/Pro", () => {
const provider = deepseekProvider();
const models = provider.getModels();
const flash = models.find((model) => model.id === "deepseek-v4-flash");
const pro = models.find((model) => model.id === "deepseek-v4-pro");
expect(flash?.api).toBe("openai-responses");
expect(pro?.api).toBe("openai-responses");
expect(flash?.contextWindow).toBe(1_000_000);
});
});
66 changes: 66 additions & 0 deletions specs/pr-plans/0005-remove-optional-suffix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# PR 计划 0005:删掉 Exa / AnySearch API key 标签里的 "optional"
> 状态:**已实现(2026-09-08 核对)**:`messages.ts` 中 `Exa API key (optional)` / `AnySearch API key (optional)` 的 `(optional)` 已删(8 棵语言树同步)。**与 DeepSeek V4.1 Flash 内测模型(见下)合并为一个 PR 交付。** 改动在工作树,未提交。
>
> **追加:DeepSeek V4.1 Flash 内测模型(并入本 PR)**:`packages/runtime/src/models/providers/deepseek.ts` 新增 `deepseek-v4.1-flash-expires-on-0910`(id 后缀即截止日:2026-09-08 开、09-10 止)。原生多模态(text+image)、走 openai-completions(内测未宣布 Responses 支持)、上下文 1M / maxTokens 384k、定价同 v4-flash(in 0.14 / out 0.28 / cacheRead 0.0028)、每账号 20 并发、`thinkingFormat: "deepseek"` + thinkingLevelMap。测试 `deepseek.test.ts` 通过。

> 状态:计划(未写代码)
> 体量:极小(1 个文件,4 行文案)。适合作为独立小 PR 快速合入。

## 1. 背景与目标

搜索设置页里,Exa 与 AnySearch 的 API key 输入框标签写作「Exa API key (optional)」/「AnySearch API key(可选)」。两个 key 确实可选(无 key 也能匿名调用),但其他 provider(Brave / Firecrawl / Tavily / Zhihu)其实也都是"填了才能用",标签风格却不统一——带括号后缀既不美观,也容易让人误以为只有这两个是可选的。

**目标**:去掉后缀,标签统一为「Exa API key」/「AnySearch API key」。

## 2. 现状(代码事实)

"optional" **不是组件 prop**,而是硬编码在 i18n 消息树 `search.keys` 里:

- 英文 `apps/desktop/src/i18n/messages.ts`
- 654:`exa: "Exa API key (optional)"`
- 655:`anysearch: "AnySearch API key (optional)"`
- 中文 `apps/desktop/src/i18n/messages.ts`
- 1265:`exa: "Exa API Key(可选)"`
- 1266:`anysearch: "AnySearch API Key(可选)"`

`apps/desktop/src/components/settings/search-page.tsx` 的 `ApiKeyField` 没有 `optional` 之类的 prop(label 取自 `t.search.keys[provider]`,126),所以**改文案即可,组件不用动**。

## 3. 改动

```diff
// en
- exa: "Exa API key (optional)",
- anysearch: "AnySearch API key (optional)",
+ exa: "Exa API key",
+ anysearch: "AnySearch API key",

// zh
- exa: "Exa API Key(可选)",
- anysearch: "AnySearch API Key(可选)",
+ exa: "Exa API Key",
+ anysearch: "AnySearch API Key",
```

**en / zh 必须同时改**,否则中英文案不一致(现有测试会强制两棵树结构一致,但不会检查文案是否同步,所以这一步靠手工核对)。

## 4. 影响面

- **无功能影响**:`ApiKeyField` 没有必填校验,搜索 key 全部可选;页面下方的 `keyNotes`(en 662 / zh 1273)已经说明了 Exa / AnySearch 可以无 key 使用,信息不会丢失。
- **无结构变化**:消息树 leaf 数量不变,`messages.test.ts` 的结构一致性测试自动通过。

## 5. 涉及文件

- `apps/desktop/src/i18n/messages.ts`(4 行)

## 6. 验证

- `bun test apps/desktop/src/i18n/messages.test.ts`
- 手动打开 Settings → Search,确认两个标签显示正常、说明文案仍在。

## 7. 工作量

极小(< 30 分钟,含验证)。

## 8. 备注

这是 PR #163(Exa / AnySearch / Zhihu provider)的收尾清理。可以在 #163 的 review 里顺带提一句,也可以单独开 PR——**单独开更干净**(reviewer 一眼能看完,合入快)。