feat(zhipu): support team GLM Coding Plan usage query
This commit is contained in:
Wesley Liddick
2026-08-29 09:08:42 +08:00
committed by GitHub
5 changed files with 136 additions and 0 deletions
@@ -142,6 +142,7 @@ func (s *CNProviderQuotaService) queryUsageForAccount(ctx context.Context, accou
var (
targetURL string
authHeader string
zhipuOrg string
)
switch provider {
case PlatformKimi:
@@ -150,6 +151,13 @@ func (s *CNProviderQuotaService) queryUsageForAccount(ctx context.Context, accou
case PlatformZhipu:
targetURL = zhipuQuotaURL(baseURL)
authHeader = apiKey // 智谱额度端点鉴权不加 Bearer 前缀
// 团队版 GLM Coding Plan:额度端点需 ?type=2 + 组织/项目请求头,
// 否则官方 API 回「当前用户不存在coding plan」。组织 ID 存在即视为
// 团队版(个人版凭据不含该字段,走原个人版查询路径)。
zhipuOrg = strings.TrimSpace(account.GetCredential("zhipu_organization"))
if zhipuOrg != "" {
targetURL += "?type=2"
}
}
// 探测发起前过出站 URL 安全策略(与网关转发/Grok 探测同一套校验):
@@ -172,6 +180,12 @@ func (s *CNProviderQuotaService) queryUsageForAccount(ctx context.Context, accou
if provider == PlatformZhipu {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept-Language", "en-US,en")
if zhipuOrg != "" {
req.Header.Set("bigmodel-organization", zhipuOrg)
if project := strings.TrimSpace(account.GetCredential("zhipu_project")); project != "" {
req.Header.Set("bigmodel-project", project)
}
}
}
// 探测与真实转发保持同一套账号级请求头覆写,避免探测通过但转发失败。
account.ApplyHeaderOverrides(req.Header)
@@ -547,6 +547,36 @@
</div>
</div>
<!-- Zhipu 团队版 Coding Plan:组织/项目 ID(可选,填写后额度探测走团队版端点) -->
<div v-if="form.platform === 'zhipu' && accountMode === 'coding'" class="mt-4">
<div class="flex items-center">
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.title') }}</label>
<HelpTooltip trigger="click" width-class="w-80">
<p class="mb-1 font-medium">{{ t('admin.accounts.cnProviders.zhipuTeam.help.title') }}</p>
<ol class="list-decimal space-y-1 pl-4">
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step1') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step2') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step3') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step4') }}</li>
</ol>
<p class="mt-2 break-all rounded bg-black/20 p-1.5 font-mono text-[11px] leading-relaxed">
{{ t('admin.accounts.cnProviders.zhipuTeam.help.example') }}
</p>
</HelpTooltip>
</div>
<div class="mt-2 grid gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.organization') }}</label>
<input v-model="zhipuOrganization" type="text" class="input" :placeholder="t('admin.accounts.cnProviders.zhipuTeam.organizationPlaceholder')" />
</div>
<div>
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.project') }}</label>
<input v-model="zhipuProject" type="text" class="input" :placeholder="t('admin.accounts.cnProviders.zhipuTeam.projectPlaceholder')" />
</div>
</div>
<p class="input-hint mt-2">{{ t('admin.accounts.cnProviders.zhipuTeam.hint') }}</p>
</div>
<!-- Account Type Selection (Gemini) -->
<div v-if="form.platform === 'gemini'">
<div class="flex items-center justify-between">
@@ -3764,6 +3794,7 @@ import type {
import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
import Icon from '@/components/icons/Icon.vue'
import ProxySelector from '@/components/common/ProxySelector.vue'
@@ -3969,6 +4000,9 @@ const accountMode = ref<CnAccountMode>('payg')
// API 协议决定转发端点与格式:cc=现有转换链,anthropic=原生直通(Claude Code),
// responses=deepseek 原生 Responses 端点(Codex)。与账号类型正交。
const apiProtocol = ref<CnApiProtocol>('adaptive')
// 智谱团队版 Coding Plan:组织/项目 ID,写入 credentials 供额度探测切换团队端点
const zhipuOrganization = ref('')
const zhipuProject = ref('')
const adaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
chat_completions: '',
anthropic: '',
@@ -5544,6 +5578,11 @@ const handleSubmit = async () => {
if (apiProtocol.value !== 'adaptive' && resolvedCNBase) {
credentials.base_url = resolvedCNBase
}
// 智谱团队版 Coding Plan:组织/项目 ID 写入凭据(非空才写)
if (form.platform === 'zhipu' && accountMode.value === 'coding') {
if (zhipuOrganization.value.trim()) credentials.zhipu_organization = zhipuOrganization.value.trim()
if (zhipuProject.value.trim()) credentials.zhipu_project = zhipuProject.value.trim()
}
}
// Add model mapping if configured(OpenAI 开启自动透传时不应用)
@@ -118,6 +118,35 @@
</div>
<p class="input-hint">{{ t(`admin.accounts.cnProviders.apiProtocol.${cnProtocolDescKey}Desc`) }}</p>
</div>
<!-- Zhipu 团队版 Coding Plan:组织/项目 ID(可选,填写后用量查询走团队版端点) -->
<div v-if="account.platform === 'zhipu' && editAccountMode === 'coding'">
<div class="flex items-center">
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.title') }}</label>
<HelpTooltip trigger="click" width-class="w-80">
<p class="mb-1 font-medium">{{ t('admin.accounts.cnProviders.zhipuTeam.help.title') }}</p>
<ol class="list-decimal space-y-1 pl-4">
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step1') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step2') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step3') }}</li>
<li>{{ t('admin.accounts.cnProviders.zhipuTeam.help.step4') }}</li>
</ol>
<p class="mt-2 break-all rounded bg-black/20 p-1.5 font-mono text-[11px] leading-relaxed">
{{ t('admin.accounts.cnProviders.zhipuTeam.help.example') }}
</p>
</HelpTooltip>
</div>
<div class="mt-2 grid gap-4 sm:grid-cols-2">
<div>
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.organization') }}</label>
<input v-model="editZhipuOrganization" type="text" class="input" :placeholder="t('admin.accounts.cnProviders.zhipuTeam.organizationPlaceholder')" />
</div>
<div>
<label class="input-label">{{ t('admin.accounts.cnProviders.zhipuTeam.project') }}</label>
<input v-model="editZhipuProject" type="text" class="input" :placeholder="t('admin.accounts.cnProviders.zhipuTeam.projectPlaceholder')" />
</div>
</div>
<p class="input-hint mt-2">{{ t('admin.accounts.cnProviders.zhipuTeam.hint') }}</p>
</div>
<div>
<label class="input-label">{{ t('admin.accounts.apiKey') }}</label>
<input
@@ -2854,6 +2883,7 @@ import type {
import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
import Toggle from '@/components/common/Toggle.vue'
import Icon from '@/components/icons/Icon.vue'
import ProxySelector from '@/components/common/ProxySelector.vue'
@@ -2987,6 +3017,9 @@ const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
})
const editApiProtocol = ref<CnApiProtocol>('adaptive')
const editAccountMode = ref<CnAccountMode>('payg')
// 智谱团队版 Coding Plan:组织/项目 ID,写入 credentials 供额度探测切换团队端点
const editZhipuOrganization = ref('')
const editZhipuProject = ref('')
const editAdaptiveBaseUrls = ref<Record<CnNativeApiProtocol, string>>({
chat_completions: '',
anthropic: '',
@@ -3939,6 +3972,11 @@ const syncFormFromAccount = (newAccount: Account | null) => {
nextAdaptiveBaseUrls[legacyProtocol] = legacyBaseUrl
}
editAdaptiveBaseUrls.value = nextAdaptiveBaseUrls
// 智谱团队版 Coding Plan:回填组织/项目 ID
if (newAccount.platform === 'zhipu') {
editZhipuOrganization.value = typeof credentials.zhipu_organization === 'string' ? credentials.zhipu_organization : ''
editZhipuProject.value = typeof credentials.zhipu_project === 'string' ? credentials.zhipu_project : ''
}
}
const platformDefaultUrl =
newAccount.platform === 'openai'
@@ -4648,6 +4686,19 @@ const handleSubmit = async () => {
} else {
delete newCredentials.api_base_urls
}
// 智谱团队版 Coding Plan:组织/项目 ID 写入凭据(非空才写,清空即移除回落个人版路径)
if (props.account.platform === 'zhipu') {
const org = editZhipuOrganization.value.trim()
const project = editZhipuProject.value.trim()
if (org) {
newCredentials.zhipu_organization = org
if (project) newCredentials.zhipu_project = project
else delete newCredentials.zhipu_project
} else {
delete newCredentials.zhipu_organization
delete newCredentials.zhipu_project
}
}
}
// Handle API key
@@ -129,6 +129,22 @@ export default {
responses: 'Responses',
responsesDesc: 'Provider’s native Responses endpoint — ideal for Codex.',
},
zhipuTeam: {
title: 'Team Plan Organization / Project ID',
organization: 'Organization ID (team plan, optional)',
organizationPlaceholder: 'Organization ID of the team Coding Plan',
project: 'Project ID (team plan, optional)',
projectPlaceholder: 'Project ID of the team Coding Plan',
hint: 'Only required for the team GLM Coding Plan; when set, usage queries go through the team endpoint. Leave empty for personal plans. Click the question mark for how to obtain the IDs.',
help: {
title: 'How to get the Organization / Project ID',
step1: 'Sign in to the Zhipu open platform (bigmodel.cn) with your team account and open "Coding Plan → Team → My Plan".',
step2: 'Press F12 to open browser DevTools, switch to the Network tab, then reload the page.',
step3: 'Type /api/biz/v1/organization into the Network filter box and click the matched request (e.g. api_keys).',
step4: 'In the request URL, the org-… segment is the Organization ID and the proj_… segment is the Project ID (also visible as the bigmodel-organization / bigmodel-project request headers). Fill them into the fields above.',
example: 'Example: …/organization/org-0610bE2D…/projects/proj_0798F20…/api_keys → org-0610bE2D… goes into "Organization ID", proj_0798F20… into "Project ID"',
},
},
balance: 'Balance --',
window5h: '5-hour window',
windowWeekly: 'Weekly window',
@@ -332,6 +332,22 @@ export default {
responses: 'Responses',
responsesDesc: '供应商原生 Responses 端点,适配 Codex。',
},
zhipuTeam: {
title: '团队版组织 / 项目 ID',
organization: '组织 ID(团队版可选)',
organizationPlaceholder: '团队版 Coding Plan 的组织 ID',
project: '项目 ID(团队版可选)',
projectPlaceholder: '团队版 Coding Plan 的项目 ID',
hint: '仅团队版 GLM Coding Plan 需要填写,填写后用量查询走团队版端点;个人版留空即可。获取方式点击左侧问号查看教程。',
help: {
title: '如何获取组织 / 项目 ID',
step1: '用团队版账号登录智谱开放平台(bigmodel.cn),进入「Coding Plan → 团队版 → 我的套餐」页面。',
step2: '按 F12 打开浏览器开发者工具,切换到「Network / 网络」标签,然后刷新页面。',
step3: '在 Network 的筛选框中输入 /api/biz/v1/organization,点击命中的请求(如 api_keys)。',
step4: '请求 URL 中 org- 开头的一段即组织 ID、proj_ 开头的一段即项目 ID(也可在 Request Headers 中查看 bigmodel-organization / bigmodel-project 的值),分别填入上方输入框。',
example: '示例:…/organization/org-0610bE2D…/projects/proj_0798F20…/api_keys → org-0610bE2D… 填「组织 ID」,proj_0798F20… 填「项目 ID」',
},
},
balance: '余额 --',
window5h: '5 小时窗口',
windowWeekly: '每周窗口',