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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "thinkwatch-lite",
"private": true,
"description": "Desktop app for a local AI API gateway on macOS, Windows and Linux",
"version": "2026.10.9",
"version": "2026.10.10",
"type": "module",
"packageManager": "pnpm@11.13.0",
"scripts": {
Expand Down
14 changes: 14 additions & 0 deletions release-notes/2026.10.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
**Upgrade notes:**
- The bundled core is now 0.66.0, with control-plane protocol 42. A remote server must be upgraded to core 0.66.0 together with the app: this version does not connect to a server on 0.65.0, and 2026.10.9 and earlier do not connect to 0.66.0.
- Request history is kept.
- Models added by hand are now checked when the configuration loads. An entry with `*` or `?`, spaces around it, a duplicate, or more than 256 characters is refused and the gateway starts in safe mode, naming the upstream and the model; the earlier manual model list accepted such entries. Plain model ids load unchanged.

**Upstreams:**
- Models can be added by hand: **Add models…** in an upstream's model list. A model the upstream serves but leaves out of its list — newer models an account backend hides, a relay that lists only some — is then routed to it like any listed model: routing and failover, aliases, routing rules that name a model, a key's allowed models, the model list clients see, the dry run and the speed test.
- Models added by hand are marked **Manual** and can be removed from the list, with **Undo** in the confirmation. The summary counts them apart from the ones the upstream lists. For an upstream that lists no models, the same dialog replaces the manual model list.

**Menu bar:**
- The **Menu bar** setting has a fourth choice, **Hidden**, which takes the item off the menu bar. Opening ThinkWatch Lite again from Finder or Spotlight shows the main window; the gateway keeps running in the background.

**MCP:**
- Editing a client's configuration, such as adding a line to `~/.codex/config.toml`, no longer reports the remote MCP servers and other findings that were already there as new. Only findings that are actually new are reported.
21 changes: 14 additions & 7 deletions scripts/shots/mock/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,18 +157,23 @@ function inScope(p: ProviderView, model: string) {
return !p.models_only || p.models_only.some((g) => glob(g, model));
}

/** 这个上游能服务的模型:手动清单,或者列出来的里面在启用范围内的 */
/** 这个上游的清单:列出来的,后面接上手动添加的(列了的不重复),和 core 一样 */
function idsOf(p: ProviderView): string[] {
return [...new Set([...(LISTED[p.name] ?? []), ...p.models])];
}

/** 这个上游能服务的模型:清单里在启用范围内的 */
export function catalogOf(p: ProviderView): string[] {
if (p.model_source === "manual" || p.models.length > 0) return p.models;
return (LISTED[p.name] ?? []).filter((m) => inScope(p, m));
return idsOf(p).filter((m) => inScope(p, m));
}

/** 上一次去问模型清单是什么时候 */
const CHECKED = NOW - 2 * HOUR - 17 * MIN;

export function providers(): ProviderView[] {
return FX.overview.providers.map((p) => {
const manual = p.models.length > 0;
// 上游不给清单时,手动添加的就是全部
const manual = !LISTED[p.name] && p.models.length > 0;
return {
...clone(p),
model_source: manual ? "manual" : "discovered",
Expand All @@ -183,17 +188,19 @@ export function providers(): ProviderView[] {
export function providerModels(name: string): ProviderModelsView | null {
const p = providers().find((x) => x.name === name);
if (!p) return null;
const ids = p.model_source === "manual" ? p.models : (LISTED[name] ?? []);
const rows: ModelRow[] = ids.map((id) => {
const listed = LISTED[name] ?? [];
const rows: ModelRow[] = idsOf(p).map((id) => {
const price = priceFor(name, id);
return {
id,
enabled: p.model_source === "manual" || inScope(p, id),
enabled: inScope(p, id),
context_window: FX.prices.find((x) => x.model === id)?.max_input_tokens ?? null,
price,
price_source: price ? priceSource(name) : null,
estimated: false,
aliases: [],
manual: p.models.includes(id),
listed: listed.includes(id),
};
});
return {
Expand Down
1 change: 1 addition & 0 deletions scripts/shots/mock/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const CORE: { [N in WebviewEndpoint]: Handler<N> } = {
DeleteProvider: refuse,
ProviderModels: (_req, [name]) => providerModels(name!) ?? notFound(`Upstream ${name}`),
SetModelSpec: refuse,
SetManualModels: refuse,
RefreshProviderModels: (_req, [name]) => providerModels(name!) ?? notFound(`Upstream ${name}`),
RefreshStaleModels: () => ({ providers: [] }),

Expand Down
30 changes: 15 additions & 15 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "thinkwatch-lite"
version = "2026.10.9"
version = "2026.10.10"
edition = "2024"
# **这个数决定依赖能升到哪一版。**edition 2024 的解析器只挑声明的 Rust
# 版本编得动的依赖:写 1.85 的时候,`cargo update` 一直停在旧的 time 和
Expand All @@ -26,12 +26,12 @@ license = "MIT"
# twcore 二进制必须和这里编译进去的协议镜像来自同一个 core 版本 —— 打包脚本正是
# 从 tw-api 锁到的 tag 去取二进制的(见 scripts/fetch-core.sh)。
[workspace.dependencies]
tw-api = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-types = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-yaml = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-watch = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-link = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.65.0" }
tw-api = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-types = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-yaml = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-guard = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-watch = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }
tw-link = { git = "https://github.com/ThinkWatchProject/ThinkWatch-Core.git", tag = "v0.66.0" }

[lib]
name = "thinkwatch_lite_lib"
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ webview_endpoints![
ProviderModels,
// 手写一个模型的上下文窗口、输出上限
SetModelSpec,
// 手动添加的模型:上游能服务、却没列进清单的
SetManualModels,
RefreshProviderModels,
RefreshStaleModels,
CreateProxy,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ThinkWatch Lite",
"version": "2026.10.9",
"version": "2026.10.10",
"identifier": "app.thinkwatch.lite",
"build": {
"beforeDevCommand": "pnpm dev",
Expand Down
1 change: 1 addition & 0 deletions src/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const WEBVIEW_ENDPOINTS = [
"DeleteProvider",
"ProviderModels",
"SetModelSpec",
"SetManualModels",
"RefreshProviderModels",
"RefreshStaleModels",
"CreateProxy",
Expand Down
43 changes: 36 additions & 7 deletions src/generated/tw-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Generated by tw-api (`tw_api::ts::export_all`). Do not edit by hand.

export const CONTROL_API_VERSION = 41;
export const CONTROL_API_VERSION = 42;

/**
* 一个账号上游登的是哪个账号。
Expand Down Expand Up @@ -2225,6 +2225,19 @@ on_error: OnError, reply_mode: ReplyMode,
*/
settings_schema: Array<SettingSpecView>, hooks: PluginHooks, };

/**
* 设一家上游手动添加的模型(`PUT /provider-manual-models`):上游能服务、却没列进
* 清单的模型。**交的是整份清单**,按给的顺序换掉原来的;空的就是一个都不留。
*
* 每一项去掉首尾空白之后是一个确切的模型 ID:不空、不带 `*` `?`、不超过 256 个字符,
* 而且不重复(`config.manual_model_*`)。上游也列了的模型可以加,不算重复。
*/
export type ManualModelsSave = { provider: string, models: Array<string>,
/**
* 你基于哪一版。**对不上就是 409**
*/
base_version?: string | null, };

/**
* 一条规则按什么认。**给界面说明用**,界面按类型写成自己的话。
*/
Expand Down Expand Up @@ -2365,7 +2378,17 @@ estimated: boolean,
* 列出了这个模型名的别名,按别名表的顺序。**不论这家发不发它**:别名在这家按列表
* 顺序取它有的第一个,排在后面的名字也算列进了这个别名
*/
aliases: Array<string>, };
aliases: Array<string>,
/**
* 手动添加的(配置里这一家的 `models`)。和上游列出的一样算这家提供;删得掉的只有
* 这些(`PUT /provider-manual-models`)
*/
manual: boolean,
/**
* 上游自己的清单里有它。手动添加的模型上游也列了时两项都是 true;上游没给出清单时
* 一律是 false
*/
listed: boolean, };

/**
* 一个上游的模型清单从哪儿来。
Expand Down Expand Up @@ -3070,7 +3093,9 @@ proxy: string,
*/
on_proxy_fail: OnProxyFail,
/**
* 服务不提供模型列表时的手动清单
* 手动添加的模型:和上游列出的一起算这家提供(上游不提供清单时就是全部)。
* **编辑对话框保存时原样交回**,不然手动添加的模型会被清掉;单改这份清单用
* `PUT /provider-manual-models`([`ManualModelsSave`])
*/
models?: Array<string>,
/**
Expand Down Expand Up @@ -3099,7 +3124,8 @@ disabled: boolean, };
*/
export type ProviderModelsView = { provider: string,
/**
* `discovered`(上游列出的)/ `manual`(手动清单)/ `none`
* `discovered`(上游列出的,手动添加的接在后面)/ `manual`(上游没给出清单,只有
* 手动添加的)/ `none`
*/
source: ModelSource,
/**
Expand Down Expand Up @@ -3239,16 +3265,17 @@ proxy: string,
*/
on_proxy_fail: OnProxyFail,
/**
* 服务不提供模型列表时用的手动清单
* 手动添加的模型,按写的顺序:和上游列出的一起算这家提供(上游不提供清单时就是
* 全部)
*/
models: Array<string>,
/**
* 启用范围:只用这些模型(ID 或 glob)。空 = 它提供的全部
*/
models_only?: Array<string> | null,
/**
* 模型清单从哪儿来:`discovered`(上游列出的)/ `manual`(手动清单)/
* `none`(不知道它有什么)
* 模型清单从哪儿来:`discovered`(上游列出的,手动添加的接在后面)/ `manual`(上游
* 没给出清单,只有手动添加的)/ `none`(不知道它有什么)
*/
model_source: ModelSource,
/**
Expand Down Expand Up @@ -4932,6 +4959,7 @@ export const ENDPOINTS = {
DeleteProvider: { method: "DELETE", path: "/providers/{name}", params: ["name"], format: "json" },
ProviderModels: { method: "GET", path: "/providers/{name}/models", params: ["name"], format: "json" },
SetModelSpec: { method: "PUT", path: "/provider-model-spec", params: [], format: "json" },
SetManualModels: { method: "PUT", path: "/provider-manual-models", params: [], format: "json" },
RefreshProviderModels: { method: "POST", path: "/providers/{name}/models/refresh", params: ["name"], format: "json" },
RefreshStaleModels: { method: "POST", path: "/models/refresh", params: [], format: "json" },
CreateProxy: { method: "POST", path: "/proxies", params: [], format: "json" },
Expand Down Expand Up @@ -5054,6 +5082,7 @@ export type Endpoints = {
DeleteProvider: { req: BaseVersion; res: ConfigWritten };
ProviderModels: { req: null; res: ProviderModelsView };
SetModelSpec: { req: ModelSpecSave; res: ConfigWritten };
SetManualModels: { req: ManualModelsSave; res: ConfigWritten };
RefreshProviderModels: { req: null; res: ProviderModelsView };
RefreshStaleModels: { req: null; res: ModelsRefreshing };
CreateProxy: { req: ProxySave; res: ConfigWritten };
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/core.zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,11 @@
"config.alias_blank_model": "别名「{alias}」列出的模型中有空白项。",
"config.alias_chained": "别名「{alias}」列出的 {model} 本身也是别名。别名列出的应是上游使用的模型名称,不能是其他别名。",
"config.alias_only_itself": "别名「{alias}」只列出了它自己,不起任何作用。请列出各上游使用的名称,或删除该别名。",
"config.manual_model_blank": "上游「{upstream}」手动添加的模型(models)中有空白项。",
"config.manual_model_padded": "上游「{upstream}」手动添加的模型「{model}」首尾有空白,请去掉。",
"config.manual_model_wildcard": "上游「{upstream}」手动添加的模型「{model}」含有 * 或 ?。手动添加的模型须为确切的模型 ID;只使用上游的部分模型,请设置启用范围(models_only)。",
"config.manual_model_duplicate": "模型「{model}」在上游「{upstream}」手动添加的模型中重复。",
"config.manual_model_too_long": "上游「{upstream}」手动添加的模型中有一项超过 {max} 个字符。",
"config.model_spec_blank_model": "上游「{upstream}」的模型规格(model_specs)中有一项的模型 ID 为空。",
"config.model_spec_wildcard": "上游「{upstream}」的模型规格「{model}」含有 * 或 ?。模型规格只能对应一个确切的模型 ID。",
"config.model_spec_nothing_set": "上游「{upstream}」的模型规格「{model}」中 context_window、max_output_tokens、reasoning、image_input 均未设置。请至少设置一项,或删除该规格。",
Expand Down
4 changes: 2 additions & 2 deletions src/import/ImportDialog.i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const importDialogText = messages(
auto: "自动识别",
key: "API 密钥",
noKey: "未提供",
models: "手动模型清单",
models: "手动添加的模型",
create: "创建",
created: (name: string) => `已创建上游「${name}」`,
},
Expand All @@ -27,7 +27,7 @@ export const importDialogText = messages(
auto: "Auto-detect",
key: "API key",
noKey: "Not provided",
models: "Manual model list",
models: "Models added by hand",
create: "Create",
created: (name: string) => `Upstream “${name}” created`,
},
Expand Down
39 changes: 39 additions & 0 deletions src/upstreams/ManualModelsDialog.i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { messages } from "@/i18n";

export const manualModelsDialogText = messages(
{
title: "添加模型",
desc: "添加上游能服务、但未列在其模型列表中的模型。",
models: "模型 ID",
placeholder: "例如 gpt-6-luna",
enterToAdd: "回车添加",
hint: "可添加多个,每项为一个确切的模型 ID。",
remove: (id: string) => `移除 ${id}`,
removeShort: "移除",
outOfScope: "不在启用范围内",
blank: "模型 ID 不能为空。",
wildcard: (id: string) => `「${id}」含有 * 或 ?。手动添加的模型须为确切的模型 ID,不支持通配。`,
tooLong: (max: number) => `模型 ID 最多 ${max} 个字符。`,
duplicate: (id: string) => `「${id}」已在待添加的模型中。`,
added: (id: string) => `「${id}」已经手动添加过。`,
listed: (id: string) => `上游的模型列表中已有「${id}」,无需添加。`,
},
{
title: "Add models",
desc: "Add models the upstream serves but leaves out of its model list.",
models: "Model IDs",
placeholder: "e.g. gpt-6-luna",
enterToAdd: "Enter to add",
hint: "Add as many as needed, each an exact model ID.",
remove: (id: string) => `Remove ${id}`,
removeShort: "Remove",
outOfScope: "Not enabled",
blank: "A model ID cannot be empty.",
wildcard: (id: string) =>
`“${id}” contains * or ?. A model added by hand is an exact model ID; wildcards are not supported.`,
tooLong: (max: number) => `A model ID is at most ${max} characters.`,
duplicate: (id: string) => `“${id}” is already in the list to add.`,
added: (id: string) => `“${id}” has already been added by hand.`,
listed: (id: string) => `The upstream's model list already has “${id}”; there is no need to add it.`,
},
);
Loading
Loading