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
Original file line number Diff line number Diff line change
Expand Up @@ -5491,6 +5491,12 @@ mod tests {
"openai",
ApiProvider::Openai,
),
(
"glm-5.2",
"https://api.lkeap.cloud.tencent.com/plan/v3/chat/completions",
"openai",
ApiProvider::Openai,
),
];

for (model, base_url, expected_provider, expected_api_provider) in cases {
Expand Down
79 changes: 79 additions & 0 deletions pinvou3-app/src-tauri/src/platform/prefs/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ pub(super) fn migrated_minimax_base_url(value: &str) -> Option<String> {
})
}

/// Identifies known Coding Plan/Token Plan endpoints as (vendor, canonical
/// base_url). The two Tencent Cloud subscription tiers (checked against the
/// official docs, 2026-08):
/// - Coding Plan /coding/v3: overview 1823/130092 (2026-08-21),
/// OpenAI-compatible; the same page also offers an Anthropic-compatible
/// /coding/anthropic endpoint (not used by this repo);
/// - Token Plan /plan/v3: access guide 1823/130075 and plan doc 1823/130119,
/// OpenAI-compatible; a different subscription from Coding Plan with a
/// different model lineup.
/// Both tiers go through the generic OpenAI route: identify only backfills
/// vendor/coding_plan metadata and does not take part in wire routing;
/// dropping either arm makes stored configs lose separate reasoning-field
/// parsing.
pub(super) fn identify_coding_plan_endpoint(
base_url: &str,
) -> Option<(&'static str, &'static str)> {
Expand Down Expand Up @@ -266,4 +279,70 @@ mod tests {
);
}
}

/// Coding-plan endpoint identification regression: every catalog endpoint
/// (including both Tencent tiers) must resolve to (vendor, canonical
/// base_url); off-catalog compatible endpoints must return None. Losing an
/// identification makes stored configs lose coding_plan metadata and
/// separate reasoning-field parsing in normalize_provider_metadata
/// (see the PR #155 review).
#[test]
fn identify_coding_plan_endpoint_matches_catalog() {
let cases: &[(&str, &str, &str)] = &[
(
"https://open.bigmodel.cn/api/coding/paas/v4",
"glm",
"https://open.bigmodel.cn/api/coding/paas/v4",
),
(
"https://api.z.ai/api/coding/paas/v4",
"glm",
"https://api.z.ai/api/coding/paas/v4",
),
(
"https://api.kimi.com/coding/v1",
"kimi",
"https://api.kimi.com/coding/v1",
),
(
"https://api.lkeap.cloud.tencent.com/coding/v3",
"tencent",
"https://api.lkeap.cloud.tencent.com/coding/v3",
),
(
"https://api.lkeap.cloud.tencent.com/plan/v3",
"tencent",
"https://api.lkeap.cloud.tencent.com/plan/v3",
),
];
for (input, expected_vendor, expected_base) in cases {
let (vendor, base) = identify_coding_plan_endpoint(input)
.unwrap_or_else(|| panic!("{input} must be identified as a coding-plan endpoint"));
assert_eq!(vendor, *expected_vendor, "{input}");
assert_eq!(base, *expected_base, "{input}");
}
assert_eq!(
identify_coding_plan_endpoint("https://api.deepseek.com"),
None
);
assert_eq!(
identify_coding_plan_endpoint("https://api.lkeap.cloud.tencent.com/other/v3"),
None,
"unlisted lkeap path must not be identified"
);
}

/// When a user pastes a full chat/completions URL or variants with a
/// trailing slash/uppercase/outer whitespace, normalization must run
/// before identification; the canonical base_url is what gets persisted
/// and read back.
#[test]
fn identify_coding_plan_endpoint_normalizes_user_input() {
let (vendor, base) = identify_coding_plan_endpoint(
" HTTPS://API.LKEAP.CLOUD.TENCENT.COM/coding/v3/chat/completions/ ",
)
.expect("normalized tencent coding-plan endpoint must be identified");
assert_eq!(vendor, "tencent");
assert_eq!(base, "https://api.lkeap.cloud.tencent.com/coding/v3");
}
}
4 changes: 2 additions & 2 deletions pinvou3-app/src/features/settings/SettingsView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ const SCard = React.forwardRef( // eslint-disable-line react/display-name -- for
<span className="min-w-0 flex-1">
<span className={`block text-[14px] leading-5 truncate ${active ? ('text-[#007AFF] dark:text-[#64B5F6]') : ('text-[#1C1C1E] dark:text-[#F2F2F7]')}`}>{item.custom ? ((activeProvider && settingsCopy.customModelTitles[activeProvider.key]) || settingsCopy.customModelTitle(selectedProvider)) : (item.title || item.model || `${settingsCopy.customModel} ID`)}</span>
{item.desc && <span className={`block mt-0.5 text-[12px] leading-[16px] truncate text-[#8A8A8E] dark:text-[#8E8E93]`}>{item.custom
? (activeProvider && activeProvider.providerKind === PROVIDER_KIND_CODING_PLAN ? settingsCopy.customCodingPlanDesc : (activeProvider.preset === 'local_vllm' ? settingsCopy.customLocalDesc : (activeProvider.preset === 'openai_compatible' ? settingsCopy.customCompatibleDesc : settingsCopy.customModelDesc)))
? (activeProvider && activeProvider.key === 'tencent_token_plan' ? settingsCopy.customTokenPlanDesc : (activeProvider && activeProvider.providerKind === PROVIDER_KIND_CODING_PLAN ? settingsCopy.customCodingPlanDesc : (activeProvider.preset === 'local_vllm' ? settingsCopy.customLocalDesc : (activeProvider.preset === 'openai_compatible' ? settingsCopy.customCompatibleDesc : settingsCopy.customModelDesc))))
: (settingsCopy.modelDescriptions[item.desc] || item.desc)}</span>}
</span>
{active && <Check size={17} strokeWidth={2.4} className={'text-[#007AFF] dark:text-[#64B5F6]'} />}
Expand Down Expand Up @@ -1217,7 +1217,7 @@ const SCard = React.forwardRef( // eslint-disable-line react/display-name -- for
const active = preset === group.preset && !item.custom && catalogItemMatchesModel(item, model);
const itemTitle = item.custom ? (settingsCopy.customModelTitles[group.key] || settingsCopy.customModelTitle(presetProviderLabel(group.preset, t))) : item.title;
const itemDescription = item.custom
? (group.providerKind === PROVIDER_KIND_CODING_PLAN ? settingsCopy.customCodingPlanDesc : (group.preset === 'local_vllm' ? settingsCopy.customLocalDesc : (group.preset === 'openai_compatible' ? settingsCopy.customCompatibleDesc : settingsCopy.customModelDesc)))
? (group.key === 'tencent_token_plan' ? settingsCopy.customTokenPlanDesc : (group.providerKind === PROVIDER_KIND_CODING_PLAN ? settingsCopy.customCodingPlanDesc : (group.preset === 'local_vllm' ? settingsCopy.customLocalDesc : (group.preset === 'openai_compatible' ? settingsCopy.customCompatibleDesc : settingsCopy.customModelDesc))))
: (settingsCopy.modelDescriptions[item.desc] || item.desc);
return (
<button
Expand Down
48 changes: 48 additions & 0 deletions pinvou3-app/src/features/settings/model-catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,25 @@ const MODEL_CATALOG = {
{ model: '', title: '自定义 GLM Coding Plan 模型', desc: '手动填写 Coding Plan 模型 ID', custom: true },
],
},
// The two Tencent Cloud subscription tiers are modeled separately per the
// official docs (TokenHub product 1823):
// - Coding Plan: https://cloud.tencent.com/document/product/1823/130092
// (checked 2026-08-21) OpenAI-compatible base URL /coding/v3; the catalog
// lists all three of its model rows. The same page also offers an
// Anthropic-compatible /coding/anthropic endpoint for Claude Code-style
// tools; this repo's OpenAI route does not use it.
// - Token Plan: https://cloud.tencent.com/document/product/1823/130119
// (checked 2026-08-27) OpenAI-compatible base URL /plan/v3 (access guide
// in 130075); its general and Hy tiers have different lineups, and it is
// a different subscription from Coding Plan.
// Both endpoints are identified as vendor=tencent coding_plan by
// identify_coding_plan_endpoint on the Rust side, so both groups must keep
// providerKind=CODING_PLAN to stay consistent with the metadata read back
// after saving. Model names pass through the generic OpenAI-compatible
// route verbatim and must use the official lowercase wire ids; the other
// parallel official spellings on the same page (e.g. kimi-k-2-5 and the
// deepseek/deepseek-v4-* forms) are registered in legacyAliases so stored
// configs match with any of them.
{
key: 'tencent_coding_plan',
section: 'coding_plan',
Expand All @@ -171,9 +190,38 @@ const MODEL_CATALOG = {
endpointAliases: ['https://api.lkeap.cloud.tencent.com/coding/v3/chat/completions'],
items: [
{ model: 'tc-code-latest', title: 'tc-code-latest', desc: 'Coding Plan 自动模型' },
{ model: 'glm-5', legacyAliases: ['glm-5-0'], title: 'glm-5', desc: '旗舰编码模型' },
{ model: 'kimi-k2.5', legacyAliases: ['kimi-k-2-5'], title: 'kimi-k2.5', desc: '官方将于 2026-08-31 下线' },
{ model: '', title: '自定义腾讯云 Coding Plan 模型', desc: '手动填写 Coding Plan 模型 ID', custom: true },
],
},
{
key: 'tencent_token_plan',
section: 'coding_plan',
title: '腾讯云 Token Plan / Tencent Cloud Token Plan',
configTitle: '腾讯云 Token Plan',
desc: '腾讯云 TokenHub Token 订阅接口',
preset: 'openai_compatible',
providerKind: PROVIDER_KIND_CODING_PLAN,
vendor: 'tencent',
baseUrl: 'https://api.lkeap.cloud.tencent.com/plan/v3',
endpointAliases: ['https://api.lkeap.cloud.tencent.com/plan/v3/chat/completions'],
// kimi-k2.5 is still listed in the general tier (2026-08-27 revision,
// retiring 2026-08-31, now agreeing with the Coding Plan page); drop the
// row once the retirement takes effect.
items: [
{ model: 'tc-code-latest', title: 'tc-code-latest', desc: '自动模型,智能路由' },
{ model: 'glm-5.2', legacyAliases: ['glm-5-2'], title: 'glm-5.2', desc: '旗舰推理与编码' },
{ model: 'glm-5.1', legacyAliases: ['glm-5-1'], title: 'glm-5.1', desc: '均衡智能与成本' },
{ model: 'glm-5', legacyAliases: ['glm-5-0'], title: 'glm-5', desc: '通用推理,默认推荐' },
{ model: 'deepseek-v4-pro-202606', legacyAliases: ['deepseek/deepseek-v4-pro-0813', 'deepseek/deepseek-v4-pro'], title: 'deepseek-v4-pro-202606', desc: '高能力模型' },
{ model: 'deepseek-v4-flash-202605', legacyAliases: ['deepseek/deepseek-v4-flash-0731', 'deepseek/deepseek-v4-flash'], title: 'deepseek-v4-flash-202605', desc: '快速响应' },
{ model: 'minimax-m2.7', legacyAliases: ['minimax-m-2-7'], title: 'minimax-m2.7', desc: '最新推荐' },
{ model: 'kimi-k2.5', legacyAliases: ['kimi-k-2-5'], title: 'kimi-k2.5', desc: '官方将于 2026-08-31 下线' },
{ model: 'hy3', legacyAliases: ['hy3-preview'], title: 'hy3', desc: 'Hy 套餐专属模型' },
{ model: '', title: '自定义腾讯云 Token Plan 模型', desc: '手动填写 Token Plan 模型 ID', custom: true },
],
},
{
key: 'kimi_coding_plan',
section: 'coding_plan',
Expand Down
8 changes: 7 additions & 1 deletion pinvou3-app/src/shared/i18n/en.js

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

8 changes: 7 additions & 1 deletion pinvou3-app/src/shared/i18n/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,7 @@ Object.assign(dictJa.uiSettingsDetail, {
providerCatalog:{
local:{title:'ローカルモデル',desc:'ローカルサービスのデフォルトモデル'}, glm_coding_plan:{title:'Zhipu Coding Plan / GLM Coding Plan',configTitle:'Zhipu Coding Plan',desc:'コーディングと Agent 向けの専用エンドポイント'},
tencent_coding_plan:{title:'Tencent Cloud Coding Plan',configTitle:'Tencent Cloud Coding Plan',desc:'Tencent Cloud のコーディングプラン用エンドポイント'},
tencent_token_plan:{title:'Tencent Cloud Token Plan',configTitle:'Tencent Cloud Token Plan',desc:'Tencent Cloud TokenHub Token Plan サブスクリプション専用ゲートウェイ'},
kimi_coding_plan:{title:'Kimi Coding Plan',configTitle:'Kimi Coding Plan',desc:'Kimi のコーディング向け専用エンドポイント'},
deepseek:{title:'DeepSeek',configTitle:'DeepSeek',desc:'DeepSeek 公式 API'}, kimi:{title:'Kimi China',configTitle:'Kimi',desc:'Moonshot 公式 API'},
glm:{title:'Zhipu Open Platform / GLM API',configTitle:'GLM API',desc:'Zhipu Open Platform の標準 API'},
Expand Down Expand Up @@ -924,6 +925,10 @@ Object.assign(dictJa.uiSettingsDetail.modelDescriptions, {
'本地服务默认模型':'ローカルサービスのデフォルトモデル',
'旗舰编码模型':'フラッグシップコーディングモデル', '高性能编码模型':'高性能コーディングモデル',
'日常编码模型':'日常向けコーディングモデル', 'Coding Plan 自动模型':'Coding Plan 自動モデル',
'自动模型,智能路由':'自動モデル(スマートルーティング)',
'官方将于 2026-08-31 下线':'2026-08-31 に提供終了予定',
'Hy 套餐专属模型':'Hy プラン専用モデル',
'手动填写 Token Plan 模型 ID':'Token Plan モデル ID を手動入力',
'标准编码模型':'標準コーディングモデル', 'K3 256K 上下文模型':'K3 256K コンテキストモデル',
'K3 长上下文模型':'K3 長コンテキストモデル', '高速编码模型':'高速コーディングモデル',
'旗舰推理':'フラッグシップ推論モデル',
Expand All @@ -946,11 +951,12 @@ Object.assign(dictJa.uiSettingsDetail.modelDescriptions, {
});

dictJa.uiSettingsDetail.customCodingPlanDesc = 'Coding Plan モデル ID を手動入力';
dictJa.uiSettingsDetail.customTokenPlanDesc = 'Token Plan モデル ID を手動入力';
dictJa.uiSettingsDetail.modelAlias = '別名';
dictJa.uiSettingsDetail.modelAliasPlaceholder = '任意(例:日常アシスタント)';

dictJa.uiSettingsDetail.customModelTitles = {
glm:'カスタム GLM モデル', qwen:'カスタム Qwen モデル',
openai_compatible:'カスタム互換モデル', glm_coding_plan:'カスタム GLM Coding Plan モデル',
tencent_coding_plan:'カスタム Tencent Cloud Coding Plan モデル', kimi_coding_plan:'カスタム Kimi Coding Plan モデル',
tencent_coding_plan:'カスタム Tencent Cloud Coding Plan モデル', tencent_token_plan:'カスタム Tencent Cloud Token Plan モデル', kimi_coding_plan:'カスタム Kimi Coding Plan モデル',
};
3 changes: 2 additions & 1 deletion pinvou3-app/src/shared/i18n/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -936,9 +936,10 @@ Object.assign(dictZh.uiAttachments, {

// 自定义项标题/描述的逐供应商口径(与 i18n 化前硬编码目录一致);未列出的供应商回退 customModelTitle。
dictZh.uiSettingsDetail.customCodingPlanDesc = '手动填写 Coding Plan 模型 ID';
dictZh.uiSettingsDetail.customTokenPlanDesc = '手动填写 Token Plan 模型 ID';

dictZh.uiSettingsDetail.customModelTitles = {
glm:'自定义 GLM 模型', qwen:'自定义通义模型',
openai_compatible:'自定义兼容模型', glm_coding_plan:'自定义 GLM Coding Plan 模型',
tencent_coding_plan:'自定义腾讯云 Coding Plan 模型', kimi_coding_plan:'自定义 Kimi Coding Plan 模型',
tencent_coding_plan:'自定义腾讯云 Coding Plan 模型', tencent_token_plan:'自定义腾讯云 Token Plan 模型', kimi_coding_plan:'自定义 Kimi Coding Plan 模型',
};
Loading
Loading