dsh-tap 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +99 -0
- package/CHANGELOG.md +751 -0
- package/LICENSE +21 -0
- package/README.en.md +59 -0
- package/README.md +256 -0
- package/cordis.patch.yml +385 -0
- package/core/bridge.js +698 -0
- package/core/json-store.js +91 -0
- package/core/rotation.js +108 -0
- package/core/usage-meter.js +176 -0
- package/docs/diagnosis-cache-quota.md +181 -0
- package/docs/diagnosis-qoder-flash.md +67 -0
- package/docs/diagnosis-trae-3003.md +302 -0
- package/docs/goals/bridge-port-host-split.md +91 -0
- package/docs/goals/desktop-adaptation.md +106 -0
- package/docs/goals/qoder-cn-provider-design.md +308 -0
- package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
- package/docs/goals/settings-card-ux-redesign.md +162 -0
- package/docs/goals/trae-agent-v3.md +41 -0
- package/docs/goals/trae-work-cn-repair.md +44 -0
- package/docs/goals/v0.8-/351/242/235/345/272/246/345/217/257/350/247/201-/346/250/241/345/236/213/345/212/250/346/200/201/345/214/226-/345/244/232/346/234/215/345/212/241/345/225/206.md +85 -0
- package/docs/pitfalls.md +105 -0
- package/docs/reverse/trae-cloud-api.md +218 -0
- package/docs/reverse/trae-model-catalog.md +129 -0
- package/docs/reverse/traework-cn.md +520 -0
- package/docs/rules/STATE.md +198 -0
- package/docs/rules/content-moderation.md +54 -0
- package/docs/rules/dev-role-boundary.md +94 -0
- package/docs/rules/extra-providers.md +42 -0
- package/docs/rules/gateway-facts.md +91 -0
- package/docs/rules/oauth-handshake.md +76 -0
- package/docs/rules/prompt-cache.md +93 -0
- package/docs/rules/quota-signals.md +125 -0
- package/docs/rules/routing.md +84 -0
- package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
- package/docs/rules/trae-surface.md +242 -0
- package/docs/rules/ua-validation.md +81 -0
- package/host-config.js +282 -0
- package/index.js +2306 -0
- package/lib/client.js +2893 -0
- package/local-scan.js +104 -0
- package/package.json +82 -0
- package/providers/ark/index.js +11 -0
- package/providers/bailian/index.js +10 -0
- package/providers/bigmodel/index.js +11 -0
- package/providers/codebuddy/agenttool.js +122 -0
- package/providers/codebuddy/catalog.js +227 -0
- package/providers/codebuddy/errors.js +44 -0
- package/providers/codebuddy/headers.js +36 -0
- package/providers/codebuddy/images.js +125 -0
- package/providers/codebuddy/index.js +123 -0
- package/providers/codebuddy/oauth.js +279 -0
- package/providers/deepseek/index.js +11 -0
- package/providers/moonshot/index.js +11 -0
- package/providers/openai-compat.js +177 -0
- package/providers/openrouter/index.js +27 -0
- package/providers/qoder/catalog.js +145 -0
- package/providers/qoder/cosy.js +419 -0
- package/providers/qoder/gateway.js +563 -0
- package/providers/qoder/index.js +116 -0
- package/providers/qoder/oauth.js +364 -0
- package/providers/qoder/qoder_auth.wasm +0 -0
- package/providers/qoder/quota.js +56 -0
- package/providers/qwen/index.js +15 -0
- package/providers/tool-pairing.js +129 -0
- package/providers/trae/catalog.js +103 -0
- package/providers/trae/errors.js +85 -0
- package/providers/trae/gateway.js +853 -0
- package/providers/trae/index.js +126 -0
- package/providers/trae/oauth.js +443 -0
- package/providers/trae/quota.js +75 -0
- package/providers/trae/remote.js +365 -0
- package/scripts/capture-cache.mjs +65 -0
- package/scripts/capture-traffic.mjs +83 -0
- package/scripts/hermes-probe-dev-role.mjs +263 -0
- package/scripts/measure-latency.mjs +253 -0
- package/scripts/probe-ark-thinking.mjs +298 -0
- package/scripts/probe-cache-decline.mjs +292 -0
- package/scripts/probe-cache-ttl.mjs +221 -0
- package/scripts/probe-cache.mjs +156 -0
- package/scripts/probe-codebuddy-efforts.mjs +355 -0
- package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
- package/scripts/probe-effort-gaps.mjs +133 -0
- package/scripts/probe-media.mjs +115 -0
- package/scripts/probe-moderation.mjs +159 -0
- package/scripts/probe-oauth.mjs +617 -0
- package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
- package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
- package/scripts/probe-qoder-attribution.mjs +266 -0
- package/scripts/probe-qoder-flash-confirm.mjs +94 -0
- package/scripts/probe-qoder-live.mjs +344 -0
- package/scripts/probe-qoder-matrix.mjs +274 -0
- package/scripts/probe-qoder-null-content.mjs +153 -0
- package/scripts/probe-qoder-pairing.mjs +308 -0
- package/scripts/probe-qoder-quota.mjs +162 -0
- package/scripts/probe-qoder-thinking-config.mjs +52 -0
- package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
- package/scripts/probe-quota.mjs +136 -0
- package/scripts/probe-quota2.mjs +144 -0
- package/scripts/probe-routing.mjs +226 -0
- package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
- package/scripts/probe-trae-agent-v3.mjs +399 -0
- package/scripts/probe-trae-efforts.mjs +149 -0
- package/scripts/probe-trae-live.mjs +147 -0
- package/scripts/probe-trae-max-effort.mjs +179 -0
- package/scripts/probe-trae-model-routing.mjs +381 -0
- package/scripts/probe-trae-thinking-scene.mjs +238 -0
- package/scripts/probe-trae-transport-outage.mjs +176 -0
- package/scripts/probe-ua.mjs +508 -0
- package/scripts/trae-model-catalog.mjs +632 -0
- package/scripts/verify-agents-md.mjs +45 -0
- package/scripts/verify-bridge.mjs +1041 -0
- package/scripts/verify-core-generic.mjs +302 -0
- package/scripts/verify-desktop-acceptance.mjs +184 -0
- package/scripts/verify-host-config.mjs +265 -0
- package/scripts/verify-models.mjs +390 -0
- package/scripts/verify-providers.mjs +255 -0
- package/scripts/verify-qoder-provider.mjs +1020 -0
- package/scripts/verify-rotation.mjs +308 -0
- package/scripts/verify-trae-model-catalog.mjs +284 -0
- package/scripts/verify-trae-provider.mjs +1451 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// probe-qoder-thinking-config.mjs — 取 Qoder 目录里逐模型的思考能力声明
|
|
2
|
+
// (thinking_config)并落证据,同时打印投影出的 reasoningEfforts 档位表。
|
|
3
|
+
//
|
|
4
|
+
// 只读:一次签名 GET(/algo/api/v2/model/list?Encode=1),不发聊天请求、不动配置。
|
|
5
|
+
// 用法:node scripts/probe-qoder-thinking-config.mjs
|
|
6
|
+
// 证据 → docs/probes/qoder-thinking-config-<ts>.json
|
|
7
|
+
import { readJson } from '../core/json-store.js'
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
12
|
+
|
|
13
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
14
|
+
const store = readJson(join(homedir(), '.dsh', 'qoder-plugin-auth.json'))
|
|
15
|
+
const { createCosyRuntime } = await import('../providers/qoder/cosy.js')
|
|
16
|
+
const { fetchQoderCatalog, qoderReasoningEfforts } = await import('../providers/qoder/catalog.js')
|
|
17
|
+
|
|
18
|
+
const cosy = createCosyRuntime({ wasmPath: join(ROOT, 'providers', 'qoder', 'qoder_auth.wasm') })
|
|
19
|
+
const cred = { accessToken: store.auth.accessToken, machineId: store.machine.machineId, uid: store.account.uid }
|
|
20
|
+
const infer = process.argv[2] ?? 'https://gateway.qoder.com.cn'
|
|
21
|
+
const cat = await fetchQoderCatalog(cosy, cred, infer)
|
|
22
|
+
|
|
23
|
+
const entries = {}
|
|
24
|
+
for (const [id, entry] of Object.entries(cat.entries)) {
|
|
25
|
+
entries[id] = {
|
|
26
|
+
is_reasoning: entry.is_reasoning === true,
|
|
27
|
+
thinking_config: entry.thinking_config ?? null,
|
|
28
|
+
effortTable: qoderReasoningEfforts(entry),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('model is_reasoning effortTable')
|
|
33
|
+
for (const [id, e] of Object.entries(entries)) {
|
|
34
|
+
const table = e.effortTable
|
|
35
|
+
const shape = table === null
|
|
36
|
+
? '—'
|
|
37
|
+
: Object.entries(table).map(([k, v]) => `${k}${v === null ? ':null' : `:${v}`}`).join('/')
|
|
38
|
+
console.log(`${id.padEnd(24)} ${String(e.is_reasoning).padEnd(13)} ${shape}`)
|
|
39
|
+
}
|
|
40
|
+
const withTable = Object.values(entries).filter((e) => e.effortTable !== null).length
|
|
41
|
+
console.log(`\n${withTable}/${Object.keys(entries).length} 个模型出档位表(宿主 Model/Effort 选择器据此出档)`)
|
|
42
|
+
|
|
43
|
+
mkdirSync(join(ROOT, 'docs', 'probes'), { recursive: true })
|
|
44
|
+
const out = join(ROOT, 'docs', 'probes', `qoder-thinking-config-${Date.now()}.json`)
|
|
45
|
+
writeFileSync(out, JSON.stringify({
|
|
46
|
+
at: new Date().toISOString(),
|
|
47
|
+
source: `GET ${infer}/algo/api/v2/model/list?Encode=1(providers/qoder/catalog.js,签名只读)`,
|
|
48
|
+
models: Object.keys(entries).length,
|
|
49
|
+
withEffortTable: withTable,
|
|
50
|
+
entries,
|
|
51
|
+
}, null, 2))
|
|
52
|
+
console.log('证据 →', out)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* probe-qoder-thinking-efforts.mjs — Qoder 本地翻译网关(OpenAI 兼容 /v1)
|
|
4
|
+
* reasoning_effort 档位单调性实测:显式 effort 各档 × N 采样,聚合 reasoning
|
|
5
|
+
* 字符数 / completion tokens,按档位语义序打单调性判定。
|
|
6
|
+
*
|
|
7
|
+
* 为什么需要:0.18.0/0.19.0 记录在案的未定论——6 臂单采样非单调、disabled 臂仍
|
|
8
|
+
* 出 619 字推理 ⇒ 按踩坑 #42(能力声明 ≠ 线值)没敢下结论。本探针把那次取证
|
|
9
|
+
* 升级为「显式档位 × 多采样聚合」,一条命令出 MONOTONIC / INCONCLUSIVE。
|
|
10
|
+
*
|
|
11
|
+
* 臂设计(变量干净优先):
|
|
12
|
+
* - 臂 = **显式 reasoning_effort**。网关对客户端已带的 effort 透传不覆盖
|
|
13
|
+
* (providers/qoder/gateway.js:仅 payload 未带时才注入该模型 prefs 的
|
|
14
|
+
* effort),显式带参即绕开 prefs 注入,baseline 不被存量 max 污染。
|
|
15
|
+
* - off 臂跳过:openai 方言 off = 省略参数,省略会触发网关注入 prefs effort,
|
|
16
|
+
* 变量不干净。off 档是否真能关思考不在本探针范围(历史 disabled 臂实测
|
|
17
|
+
* 仍出推理),报告按「off 未测」处理。
|
|
18
|
+
* - 各模型跑哪些档 = 目录声明快照(DECLARED_TIERS,真源 providers/qoder/
|
|
19
|
+
* catalog.js qoderReasoningEfforts;2026-10-04 全量目录实测,见
|
|
20
|
+
* docs/probes/qoder-thinking-config-*.json,可用
|
|
21
|
+
* `node scripts/probe-qoder-thinking-config.mjs` 重取)减 off,按语义序
|
|
22
|
+
* off<low<medium<high<xhigh<max 排序后做相邻对单调性检查。
|
|
23
|
+
*
|
|
24
|
+
* 判据(踩坑 #42 纪律:多采样、非单调不下结论):
|
|
25
|
+
* 每臂 --repeat(默认 3)次同题同 prompt → 聚合 reasoningChars min/mean/max
|
|
26
|
+
* 与 completionTokens min/mean/max;按档位语义序检查**均值单调不减**——
|
|
27
|
+
* 任一相邻对下降 → INCONCLUSIVE(#42:非单调,不下结论)
|
|
28
|
+
* 任一臂有样本报错 → INCONCLUSIVE(错误原样落证据)
|
|
29
|
+
* 全部不减 → MONOTONIC(若各档均值全等,额外标注 flat)
|
|
30
|
+
*
|
|
31
|
+
* 用法:
|
|
32
|
+
* node scripts/probe-qoder-thinking-efforts.mjs # 默认 qmodel_38max+dmodel ×3 repeat(15 次)
|
|
33
|
+
* node scripts/probe-qoder-thinking-efforts.mjs --models dmodel # 只跑一个模型
|
|
34
|
+
* node scripts/probe-qoder-thinking-efforts.mjs --repeat 5 # 每臂 5 次采样
|
|
35
|
+
* node scripts/probe-qoder-thinking-efforts.mjs --endpoint http://127.0.0.1:3903/v1
|
|
36
|
+
* node scripts/probe-qoder-thinking-efforts.mjs --help
|
|
37
|
+
*
|
|
38
|
+
* 额度账:总调用 = Σ(每模型档位臂数) × --repeat;默认 2 模型(3+2 臂)× 3 = 15
|
|
39
|
+
* 次,硬闸 ≤ 30 次超出即拒跑。max_tokens 钳 1024。走的是 Qoder 订阅额度
|
|
40
|
+
* (免费输入面),每臂样本真实入账。
|
|
41
|
+
*
|
|
42
|
+
* 凭据:无需配置——鉴权走本地网关固定 token(Authorization: Bearer dsh-qoder-bridge,
|
|
43
|
+
* 网关仅监听 127.0.0.1 回环),Qoder 侧凭据由桌面端 OAuth 会话提供。
|
|
44
|
+
* 证据 → docs/probes/qoder-efforts-<ts>.json(含 endpoint/模型/臂/逐样本原始 usage)。
|
|
45
|
+
*/
|
|
46
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
47
|
+
import { dirname, join } from 'node:path'
|
|
48
|
+
import { fileURLToPath } from 'node:url'
|
|
49
|
+
|
|
50
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
51
|
+
const ENDPOINT_DFLT = process.env.QODER_BRIDGE_ENDPOINT ?? 'http://127.0.0.1:3913/v1'
|
|
52
|
+
const BRIDGE_TOKEN = 'dsh-qoder-bridge'
|
|
53
|
+
const MAX_TOKENS = 1024
|
|
54
|
+
const MAX_TOTAL_CALLS = 30
|
|
55
|
+
const CALL_TIMEOUT_MS = 180_000
|
|
56
|
+
const PROMPT = '一个农夫有 17 只羊,除了 9 只以外都跑了,还剩几只?只给最终数字。'
|
|
57
|
+
|
|
58
|
+
/** 目录声明档位静态快照(DECLARED_TIERS,来源见头注释)。目录改版后重取。 */
|
|
59
|
+
const DECLARED_TIERS = {
|
|
60
|
+
qmodel_38max: ['off', 'low', 'medium', 'xhigh'],
|
|
61
|
+
qfmodel: ['off', 'low', 'medium', 'xhigh'],
|
|
62
|
+
dmodel: ['off', 'high', 'max'],
|
|
63
|
+
gm51model: ['off', 'high', 'max'],
|
|
64
|
+
gmodel: ['high', 'low', 'max'],
|
|
65
|
+
kmodel: ['high', 'low', 'max'],
|
|
66
|
+
kmodel_latest: ['high', 'low', 'max'],
|
|
67
|
+
}
|
|
68
|
+
/** 档位语义序(只用于排序做单调性检查;各家族内部顺序均成立)。 */
|
|
69
|
+
const TIER_RANK = { off: 0, low: 1, medium: 2, high: 3, xhigh: 4, max: 5 }
|
|
70
|
+
|
|
71
|
+
const args = process.argv.slice(2)
|
|
72
|
+
const argOf = (name, dflt) => {
|
|
73
|
+
const i = args.indexOf(name)
|
|
74
|
+
return i >= 0 && args[i + 1] !== undefined && !args[i + 1].startsWith('--') ? args[i + 1] : dflt
|
|
75
|
+
}
|
|
76
|
+
const ENDPOINT = String(argOf('--endpoint', ENDPOINT_DFLT)).replace(/\/+$/, '')
|
|
77
|
+
const MODELS = String(argOf('--models', 'qmodel_38max,dmodel')).split(',').map((s) => s.trim()).filter(Boolean)
|
|
78
|
+
const REPEAT = Number(argOf('--repeat', '3'))
|
|
79
|
+
|
|
80
|
+
const USAGE = `probe-qoder-thinking-efforts.mjs — Qoder 本地网关 reasoning_effort 档位单调性实测
|
|
81
|
+
|
|
82
|
+
用法:node scripts/probe-qoder-thinking-efforts.mjs [选项]
|
|
83
|
+
|
|
84
|
+
臂 = 显式 reasoning_effort(透传不覆盖,绕开 prefs 注入);off 臂跳过(openai 方言
|
|
85
|
+
off = 省略参数,会触发 prefs 注入,变量不干净)。各模型档位取目录声明快照减 off。
|
|
86
|
+
判据:每臂多采样聚合,档位语义序上检查 reasoning 字符数均值单调不减——
|
|
87
|
+
非单调 = INCONCLUSIVE(踩坑 #42);单调 = MONOTONIC。
|
|
88
|
+
|
|
89
|
+
--models a,b 指定模型(默认 qmodel_38max,dmodel;可选 qfmodel/gm51model/gmodel/kmodel/kmodel_latest)
|
|
90
|
+
--repeat N 每臂采样 N 次并聚合 min/mean/max(默认 3)
|
|
91
|
+
--endpoint URL 本地 Qoder 网关(默认 http://127.0.0.1:3913/v1,或环境变量 QODER_BRIDGE_ENDPOINT)
|
|
92
|
+
--help 本帮助
|
|
93
|
+
|
|
94
|
+
总额度硬闸 30 次上游调用;max_tokens 钳 1024;同题同 prompt 顺序采样。
|
|
95
|
+
证据 → docs/probes/qoder-efforts-<ts>.json。`
|
|
96
|
+
|
|
97
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
98
|
+
console.log(USAGE)
|
|
99
|
+
process.exit(0)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!Number.isInteger(REPEAT) || REPEAT < 1) {
|
|
103
|
+
console.error(`--repeat 必须是 ≥1 的整数,收到:${argOf('--repeat', '3')}`)
|
|
104
|
+
process.exit(2)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 每模型的臂 = 目录声明减 off,按语义序排(含 off 时注明跳过原因)。
|
|
108
|
+
function armsOf(model) {
|
|
109
|
+
const tiers = DECLARED_TIERS[model]
|
|
110
|
+
if (!tiers) return { error: `DECLARED_TIERS 无该模型的声明快照(已知:${Object.keys(DECLARED_TIERS).join(', ')})` }
|
|
111
|
+
const run = tiers.filter((t) => t !== 'off')
|
|
112
|
+
const unsortable = run.filter((t) => !(t in TIER_RANK))
|
|
113
|
+
if (unsortable.length) return { error: `档位拼写无法排序:${unsortable.join(',')}(TIER_RANK 需扩充)` }
|
|
114
|
+
run.sort((a, b) => TIER_RANK[a] - TIER_RANK[b])
|
|
115
|
+
return { arms: run, skippedOff: tiers.includes('off') }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const plan = MODELS.map((model) => ({ model, ...armsOf(model) }))
|
|
119
|
+
const badPlan = plan.filter((p) => p.error)
|
|
120
|
+
if (badPlan.length) {
|
|
121
|
+
for (const p of badPlan) console.error(`${p.model}: ${p.error}`)
|
|
122
|
+
process.exit(2)
|
|
123
|
+
}
|
|
124
|
+
const totalCalls = plan.reduce((n, p) => n + p.arms.length * REPEAT, 0)
|
|
125
|
+
console.log('额度账:')
|
|
126
|
+
for (const p of plan) console.log(` ${p.model}: ${p.arms.join(' < ')} 共 ${p.arms.length} 臂 × ${REPEAT} repeat = ${p.arms.length * REPEAT} 次`)
|
|
127
|
+
console.log(`合计 ${totalCalls} 次(硬闸 ${MAX_TOTAL_CALLS})${plan.some((p) => p.skippedOff) ? ';off 臂跳过(省略参数会触发 prefs 注入,变量不干净)' : ''}`)
|
|
128
|
+
if (totalCalls > MAX_TOTAL_CALLS) {
|
|
129
|
+
console.error(`超出硬闸:${totalCalls} > ${MAX_TOTAL_CALLS} 次上游调用,拒跑。请减模型/臂或调低 --repeat。`)
|
|
130
|
+
process.exit(2)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 预检:本地 GET /v1/models 免费且不烧上游额度(网关本地路由),验证可达 + 目录在位。
|
|
134
|
+
async function preflight() {
|
|
135
|
+
let res
|
|
136
|
+
try {
|
|
137
|
+
res = await fetch(`${ENDPOINT}/models`, { headers: { authorization: `Bearer ${BRIDGE_TOKEN}` }, signal: AbortSignal.timeout(10_000) })
|
|
138
|
+
} catch (err) {
|
|
139
|
+
throw new Error(`网关不可达 ${ENDPOINT}/models:${err?.cause?.code ?? err?.message ?? err}`)
|
|
140
|
+
}
|
|
141
|
+
const text = await res.text()
|
|
142
|
+
let json = null
|
|
143
|
+
try { json = JSON.parse(text) } catch { /* keep text */ }
|
|
144
|
+
if (!res.ok || !Array.isArray(json?.data)) {
|
|
145
|
+
throw new Error(`预检失败 HTTP ${res.status}:${text.slice(0, 300)}`)
|
|
146
|
+
}
|
|
147
|
+
return json.data.map((m) => m?.id).filter(Boolean)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let catalogIds
|
|
151
|
+
try {
|
|
152
|
+
catalogIds = await preflight()
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(String(err?.message ?? err))
|
|
155
|
+
process.exit(2)
|
|
156
|
+
}
|
|
157
|
+
console.log(`预检 OK:${ENDPOINT}(目录 ${catalogIds.length} 个模型)`)
|
|
158
|
+
const missing = plan.filter((p) => !catalogIds.includes(p.model))
|
|
159
|
+
if (missing.length) {
|
|
160
|
+
console.error(`模型不在网关目录里:${missing.map((p) => p.model).join(', ')}(目录有:${catalogIds.join(', ')})`)
|
|
161
|
+
process.exit(2)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function call(model, effort) {
|
|
165
|
+
let res
|
|
166
|
+
try {
|
|
167
|
+
res = await fetch(`${ENDPOINT}/chat/completions`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: { authorization: `Bearer ${BRIDGE_TOKEN}`, 'content-type': 'application/json' },
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
model,
|
|
172
|
+
max_tokens: MAX_TOKENS,
|
|
173
|
+
reasoning_effort: effort,
|
|
174
|
+
messages: [{ role: 'user', content: PROMPT }],
|
|
175
|
+
}),
|
|
176
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
177
|
+
})
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return { status: 0, reasoningChars: 0, textChars: 0, completionTokens: null, usage: null, error: { code: 'fetch_failed', message: String(err?.cause?.code ?? err?.message ?? err) } }
|
|
180
|
+
}
|
|
181
|
+
const text = await res.text()
|
|
182
|
+
let json = null
|
|
183
|
+
try { json = JSON.parse(text) } catch { /* keep text */ }
|
|
184
|
+
const msg = json?.choices?.[0]?.message ?? null
|
|
185
|
+
return {
|
|
186
|
+
status: res.status,
|
|
187
|
+
reasoningChars: typeof msg?.reasoning_content === 'string' ? msg.reasoning_content.length : 0,
|
|
188
|
+
textChars: typeof msg?.content === 'string' ? msg.content.length : 0,
|
|
189
|
+
completionTokens: Number.isFinite(json?.usage?.completion_tokens) ? json.usage.completion_tokens : null,
|
|
190
|
+
finishReason: json?.choices?.[0]?.finish_reason ?? null,
|
|
191
|
+
usage: json?.usage ?? null,
|
|
192
|
+
error: json?.error
|
|
193
|
+
? { code: json.error.code ?? null, message: String(json.error.message ?? '').slice(0, 200) }
|
|
194
|
+
: (res.ok ? null : { code: `http_${res.status}`, message: text.slice(0, 200) }),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 每臂聚合:保留全部逐样本原始数据,标量归并(min/mean/max,同 probe-ark-thinking
|
|
199
|
+
// 的 aggregateArm 思路)。completionTokens 逐样本可能为 null,只对数值聚合。
|
|
200
|
+
function aggregateArm(samples) {
|
|
201
|
+
const stat = (pick) => {
|
|
202
|
+
const vs = samples.map(pick).filter((v) => Number.isFinite(v))
|
|
203
|
+
if (!vs.length) return null
|
|
204
|
+
return { min: Math.min(...vs), mean: Math.round((vs.reduce((a, b) => a + b, 0) / vs.length) * 10) / 10, max: Math.max(...vs) }
|
|
205
|
+
}
|
|
206
|
+
const statuses = [...new Set(samples.map((s) => s.status))]
|
|
207
|
+
const errors = [...new Set(samples.filter((s) => s.error).map((s) => JSON.stringify(s.error)))].map((s) => JSON.parse(s))
|
|
208
|
+
return {
|
|
209
|
+
runs: samples.length,
|
|
210
|
+
status: statuses.length === 1 ? statuses[0] : statuses,
|
|
211
|
+
reasoningChars: stat((s) => s.reasoningChars),
|
|
212
|
+
completionTokens: stat((s) => s.completionTokens),
|
|
213
|
+
error: errors.length ? errors : null,
|
|
214
|
+
samples,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const fmt = (a) => `http=${JSON.stringify(a.status)} ×${a.runs} reasoning=min ${a.reasoningChars?.min ?? '—'}/mean ${a.reasoningChars?.mean ?? '—'}/max ${a.reasoningChars?.max ?? '—'} completion=min ${a.completionTokens?.min ?? '—'}/mean ${a.completionTokens?.mean ?? '—'}/max ${a.completionTokens?.max ?? '—'}${a.error ? ` error=${JSON.stringify(a.error)}` : ''}`
|
|
219
|
+
|
|
220
|
+
// 单调性判定(#42):先看臂级错误,再看档位语义序上均值是否单调不减。
|
|
221
|
+
function judge(armNames, arms) {
|
|
222
|
+
const bad = armNames.filter((a) => arms[a].error)
|
|
223
|
+
if (bad.length) return { verdict: 'INCONCLUSIVE', reason: `臂报错:${bad.join(',')}(错误原文见证据 samples)` }
|
|
224
|
+
const means = armNames.map((a) => arms[a].reasoningChars?.mean)
|
|
225
|
+
if (means.some((v) => !Number.isFinite(v))) return { verdict: 'INCONCLUSIVE', reason: 'usage/reasoning 缺失,无法聚合' }
|
|
226
|
+
for (let i = 1; i < means.length; i++) {
|
|
227
|
+
if (means[i] < means[i - 1]) return { verdict: 'INCONCLUSIVE', reason: `非单调(#42):${armNames[i - 1]} mean ${means[i - 1]} → ${armNames[i]} mean ${means[i]} 下降,不下结论` }
|
|
228
|
+
}
|
|
229
|
+
const flat = new Set(means).size === 1
|
|
230
|
+
return { verdict: 'MONOTONIC', reason: flat ? '均值随档位单调不减(全平:各档观测相等,未见档位差异)' : '均值随档位单调不减' }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const report = []
|
|
234
|
+
for (const { model, arms: armNames, skippedOff } of plan) {
|
|
235
|
+
const arms = {}
|
|
236
|
+
for (const arm of armNames) {
|
|
237
|
+
const samples = []
|
|
238
|
+
for (let i = 0; i < REPEAT; i++) samples.push(await call(model, arm))
|
|
239
|
+
arms[arm] = aggregateArm(samples)
|
|
240
|
+
}
|
|
241
|
+
const { verdict, reason } = judge(armNames, arms)
|
|
242
|
+
report.push({
|
|
243
|
+
model,
|
|
244
|
+
declaredTiers: DECLARED_TIERS[model],
|
|
245
|
+
armsRun: armNames,
|
|
246
|
+
offSkipped: skippedOff || undefined,
|
|
247
|
+
verdict,
|
|
248
|
+
verdictReason: reason,
|
|
249
|
+
arms,
|
|
250
|
+
})
|
|
251
|
+
console.log(`\n### ${model} → ${verdict}(${reason})`)
|
|
252
|
+
for (const arm of armNames) console.log(` ${arm.padEnd(7)}: ${fmt(arms[arm])}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
mkdirSync(join(ROOT, 'docs', 'probes'), { recursive: true })
|
|
256
|
+
const out = join(ROOT, 'docs', 'probes', `qoder-efforts-${Date.now()}.json`)
|
|
257
|
+
writeFileSync(out, JSON.stringify({
|
|
258
|
+
at: new Date().toISOString(),
|
|
259
|
+
endpoint: ENDPOINT,
|
|
260
|
+
purpose: 'reasoning_effort 档位单调性实测(0.18.0/0.19.0 未定论的复核:显式档位 × 多采样)',
|
|
261
|
+
prompt: PROMPT,
|
|
262
|
+
maxTokens: MAX_TOKENS,
|
|
263
|
+
repeat: REPEAT,
|
|
264
|
+
armDesign: '臂 = 显式 reasoning_effort(网关透传不覆盖,绕开 prefs 注入);off 臂跳过(省略参数会触发 prefs 注入)',
|
|
265
|
+
declaredTiersSource: 'providers/qoder/catalog.js qoderReasoningEfforts 的 2026-10-04 目录快照(docs/probes/qoder-thinking-config-*.json)',
|
|
266
|
+
verdictRule: '档位语义序(off<low<medium<high<xhigh<max)上 reasoningChars 均值单调不减 → MONOTONIC;任一下降或臂报错 → INCONCLUSIVE(踩坑 #42)',
|
|
267
|
+
models: report,
|
|
268
|
+
}, null, 2))
|
|
269
|
+
console.log('\n证据 →', out)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Quota-signal probe: what can the plugin's credentials actually learn about
|
|
4
|
+
* the account's remaining quota? Probes every reachable route and records
|
|
5
|
+
* the evidence (docs/probes/). Non-chat probes are free; the chat-header
|
|
6
|
+
* check costs one minimal completion (max_tokens=1, deepseek-v3 ≈0.001 credit).
|
|
7
|
+
*
|
|
8
|
+
* GET /v2/accounts → account metadata (plan type…)
|
|
9
|
+
* POST /v2/billing/meter/get-dosage-notify → low-quota banner (the official
|
|
10
|
+
* CLI's BillingService source)
|
|
11
|
+
* POST /v2/chat/completions (max_tokens=1) → response headers scanned for
|
|
12
|
+
* quota/rate-limit fields
|
|
13
|
+
*
|
|
14
|
+
* Background (2026-08-18): quota is account-level and shared with WorkBuddy
|
|
15
|
+
* (same Tencent-Cloud.coding-copilot auth realm; workbuddy.cn serves the
|
|
16
|
+
* isomorphic device flow at /v2/plugin/auth/state). The web console's numeric
|
|
17
|
+
* plan API (codebuddy.cn/profile/plan) is cookie-authed — not reachable with
|
|
18
|
+
* CLI OAuth tokens or ck_ keys.
|
|
19
|
+
*
|
|
20
|
+
* Usage: node scripts/probe-quota.mjs [--out docs/probes/quota-YYYY-MM-DD.json]
|
|
21
|
+
* [--model deepseek-v3] [--skip-chat]
|
|
22
|
+
* Credentials resolve exactly like the plugin's: active key from
|
|
23
|
+
* ~/.dsh/codebuddy-plugin.json (fallback: CODEBUDDY_API_KEY / credentials file);
|
|
24
|
+
* in OAuth mode the stored access token is used.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
|
|
28
|
+
import { homedir } from 'node:os'
|
|
29
|
+
import { join, dirname } from 'node:path'
|
|
30
|
+
|
|
31
|
+
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
32
|
+
const GATEWAY = 'https://copilot.tencent.com'
|
|
33
|
+
const UA = 'CLI/unknown CodeBuddy/2.136.0'
|
|
34
|
+
const argValue = (flag) => {
|
|
35
|
+
const i = process.argv.indexOf(flag)
|
|
36
|
+
return i > 0 ? process.argv[i + 1] : null
|
|
37
|
+
}
|
|
38
|
+
const MODEL = argValue('--model') ?? 'deepseek-v3'
|
|
39
|
+
const SKIP_CHAT = process.argv.includes('--skip-chat')
|
|
40
|
+
|
|
41
|
+
function resolveCredential() {
|
|
42
|
+
try {
|
|
43
|
+
const cfg = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin.json'), 'utf8'))
|
|
44
|
+
if (cfg.authMode === 'oauth') {
|
|
45
|
+
const auth = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin-auth.json'), 'utf8'))
|
|
46
|
+
if (auth?.auth?.accessToken) return { authorization: `Bearer ${auth.auth.accessToken}`, apiKey: null }
|
|
47
|
+
}
|
|
48
|
+
const active = (cfg.apiKeys ?? []).find((k) => k.name === cfg.activeApiKey)
|
|
49
|
+
if (active?.key) return { authorization: `Bearer ${active.key}`, apiKey: active.key }
|
|
50
|
+
} catch { /* fall through */ }
|
|
51
|
+
if (process.env.CODEBUDDY_API_KEY) {
|
|
52
|
+
return { authorization: `Bearer ${process.env.CODEBUDDY_API_KEY}`, apiKey: process.env.CODEBUDDY_API_KEY }
|
|
53
|
+
}
|
|
54
|
+
const credFile = join(DSH_HOME, '.credentials.yaml')
|
|
55
|
+
if (existsSync(credFile)) {
|
|
56
|
+
const m = readFileSync(credFile, 'utf8').match(/^\s*CODEBUDDY_API_KEY:\s*["']?([^"'\s]+)["']?\s*$/m)
|
|
57
|
+
if (m) return { authorization: `Bearer ${m[1]}`, apiKey: m[1] }
|
|
58
|
+
}
|
|
59
|
+
throw new Error('no CodeBuddy credential found')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function probe(name, { method = 'GET', path, body = null, headers = {} }) {
|
|
63
|
+
const t0 = Date.now()
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${GATEWAY}${path}`, {
|
|
66
|
+
method,
|
|
67
|
+
headers: { Accept: 'application/json', 'User-Agent': UA, ...headers },
|
|
68
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
69
|
+
})
|
|
70
|
+
const text = await res.text()
|
|
71
|
+
let json = null
|
|
72
|
+
try { json = JSON.parse(text) } catch { /* HTML error page */ }
|
|
73
|
+
return { name, method, path, status: res.status, ms: Date.now() - t0, body: json ?? text.slice(0, 200) }
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return { name, method, path, ms: Date.now() - t0, error: String(err?.message ?? err) }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cred = resolveCredential()
|
|
80
|
+
const authHeaders = { Authorization: cred.authorization, ...(cred.apiKey ? { 'x-api-key': cred.apiKey } : {}) }
|
|
81
|
+
|
|
82
|
+
const out = { at: new Date().toISOString(), gateway: GATEWAY, results: [] }
|
|
83
|
+
|
|
84
|
+
console.log('quota probe: /v2/accounts + get-dosage-notify' + (SKIP_CHAT ? '' : ` + chat headers (${MODEL})`))
|
|
85
|
+
|
|
86
|
+
out.results.push(await probe('accounts', { path: '/v2/accounts', headers: authHeaders }))
|
|
87
|
+
out.results.push(await probe('dosage-notify', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
path: '/v2/billing/meter/get-dosage-notify',
|
|
90
|
+
headers: { ...authHeaders, 'Content-Type': 'application/json' },
|
|
91
|
+
body: {},
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
if (!SKIP_CHAT) {
|
|
95
|
+
// Minimal streamed chat: capture the RESPONSE HEADERS only and look for
|
|
96
|
+
// quota/rate-limit fields (the usage body is covered by the bridge meter).
|
|
97
|
+
const t0 = Date.now()
|
|
98
|
+
const res = await fetch(`${GATEWAY}/v2/chat/completions`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { ...authHeaders, 'Content-Type': 'application/json', 'User-Agent': UA },
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
model: MODEL, stream: true, stream_options: { include_usage: true }, max_tokens: 1,
|
|
103
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
const headers = {}
|
|
107
|
+
res.headers.forEach((v, k) => { headers[k] = v })
|
|
108
|
+
await res.text()
|
|
109
|
+
const quotaish = Object.keys(headers).filter((k) => /quota|credit|limit|remain|balance|dosage/i.test(k))
|
|
110
|
+
out.results.push({
|
|
111
|
+
name: 'chat-response-headers',
|
|
112
|
+
method: 'POST',
|
|
113
|
+
path: '/v2/chat/completions',
|
|
114
|
+
status: res.status,
|
|
115
|
+
ms: Date.now() - t0,
|
|
116
|
+
quotaRelatedHeaders: quotaish,
|
|
117
|
+
quotaRelatedHeaderCount: quotaish.length,
|
|
118
|
+
headers,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const r of out.results) {
|
|
123
|
+
const summary = r.error
|
|
124
|
+
? `error=${r.error}`
|
|
125
|
+
: `http=${r.status} ${JSON.stringify(r.quotaRelatedHeaderCount !== undefined ? { quotaHeaders: r.quotaRelatedHeaderCount } : r.body).slice(0, 300)}`
|
|
126
|
+
console.log(`${r.name.padEnd(22)} ${summary}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const outPath = argValue('--out')
|
|
130
|
+
if (outPath) {
|
|
131
|
+
mkdirSync(dirname(outPath), { recursive: true })
|
|
132
|
+
if (existsSync(outPath)) appendFileSync(outPath, '\n')
|
|
133
|
+
writeFileSync(outPath, JSON.stringify(out, null, 2) + '\n', { flag: 'a' })
|
|
134
|
+
console.log(`evidence appended → ${outPath}`)
|
|
135
|
+
}
|
|
136
|
+
console.log('probe done.')
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Quota-signal map probe (topic 4 of docs/rules/): WHICH endpoints and WHICH
|
|
4
|
+
* response headers carry quota information?
|
|
5
|
+
*
|
|
6
|
+
* Prior inventory (AGENTS.md:26, docs/probes/quota-2026-08-18.json):
|
|
7
|
+
* - GET /v2/accounts → account metadata (plan type) in body
|
|
8
|
+
* - POST /v2/billing/meter/get-dosage-notify → low-quota banner source;
|
|
9
|
+
* healthy shape {dosageNotifyCode:0, dosageNotifyZh:"", ...}
|
|
10
|
+
* - chat response headers: 12 enumerated, ZERO quota-ish
|
|
11
|
+
* - numeric remaining quota: NO CLI/api-key reachable API
|
|
12
|
+
* - per-request usage.credit in chat SSE body
|
|
13
|
+
*
|
|
14
|
+
* What the inventory lacks (this probe's target): a FULL scan across every
|
|
15
|
+
* reachable endpoint of BOTH response headers AND body fields for quota
|
|
16
|
+
* signals, incl. agenttool bodies, error envelopes, and dosage-notify
|
|
17
|
+
* parameter sensitivity.
|
|
18
|
+
*
|
|
19
|
+
* Pre-registered predictions (2026-08-19):
|
|
20
|
+
* P-Q1 accounts, /v3/config, dosage-notify, and a 12403 error envelope
|
|
21
|
+
* all carry ZERO quota-ish RESPONSE HEADERS (the chat scan
|
|
22
|
+
* generalizes: this gateway puts no quota in headers at all)
|
|
23
|
+
* P-Q2a agenttool search body has NO usage/credit field (the UA-probe
|
|
24
|
+
* snippet showed {query,type,provider,results} only)
|
|
25
|
+
* P-Q2b agenttool webfetch body HAS a usage field (index.js:653 records
|
|
26
|
+
* data?.usage conditionally — predicts it exists)
|
|
27
|
+
* P-Q3 dosage-notify ignores parameters: {threshold:100} returns the
|
|
28
|
+
* same healthy shape as {} (dosageNotifyCode 0, empty strings)
|
|
29
|
+
* P-Q4 the 12403 error response (v3/config with a gate-failing UA)
|
|
30
|
+
* carries zero quota-ish headers — errors don't leak quota either
|
|
31
|
+
* Exploratory: GET /v2/report existence (CLI endpoint list mentions it).
|
|
32
|
+
*
|
|
33
|
+
* Low rate: 2s spacing, single account, read-only (search max_results=1,
|
|
34
|
+
* webfetch example.com, chat max_tokens=1).
|
|
35
|
+
*
|
|
36
|
+
* Usage: node scripts/probe-quota2.mjs [--out docs/probes/quota-map-YYYY-MM-DD.jsonl]
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs'
|
|
40
|
+
import { homedir } from 'node:os'
|
|
41
|
+
import { join, dirname } from 'node:path'
|
|
42
|
+
|
|
43
|
+
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
44
|
+
const GATEWAY = 'https://copilot.tencent.com'
|
|
45
|
+
const UA = 'CLI/unknown CodeBuddy/2.136.0'
|
|
46
|
+
const SPACING_MS = 2000
|
|
47
|
+
const OUT = process.argv.includes('--out')
|
|
48
|
+
? process.argv[process.argv.indexOf('--out') + 1]
|
|
49
|
+
: join('docs', 'probes', `quota-map-${new Date().toISOString().slice(0, 10)}.jsonl`)
|
|
50
|
+
|
|
51
|
+
const QUOTA_RE = /quota|credit|limit|remain|balance|dosage|usage|rate|plan/i
|
|
52
|
+
|
|
53
|
+
function resolveCredential() {
|
|
54
|
+
try {
|
|
55
|
+
const cfg = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin.json'), 'utf8'))
|
|
56
|
+
if (cfg.authMode === 'oauth') {
|
|
57
|
+
const auth = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin-auth.json'), 'utf8'))
|
|
58
|
+
if (auth?.auth?.accessToken) return { authorization: `Bearer ${auth.auth.accessToken}`, apiKey: null }
|
|
59
|
+
}
|
|
60
|
+
const active = (cfg.apiKeys ?? []).find((k) => k.name === cfg.activeApiKey)
|
|
61
|
+
if (active?.key) return { authorization: `Bearer ${active.key}`, apiKey: active.key }
|
|
62
|
+
} catch { /* fall through */ }
|
|
63
|
+
if (process.env.CODEBUDDY_API_KEY) return { authorization: `Bearer ${process.env.CODEBUDDY_API_KEY}`, apiKey: process.env.CODEBUDDY_API_KEY }
|
|
64
|
+
const credFile = join(DSH_HOME, '.credentials.yaml')
|
|
65
|
+
if (existsSync(credFile)) {
|
|
66
|
+
const m = readFileSync(credFile, 'utf8').match(/^\s*CODEBUDDY_API_KEY:\s*["']?([^"'\s]+)["']?\s*$/m)
|
|
67
|
+
if (m) return { authorization: `Bearer ${m[1]}`, apiKey: m[1] }
|
|
68
|
+
}
|
|
69
|
+
throw new Error('no CodeBuddy credential found')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
73
|
+
|
|
74
|
+
/** Deep-scan a parsed body for quota-ish KEY NAMES; returns paths. */
|
|
75
|
+
function bodyHits(value, path = '', hits = []) {
|
|
76
|
+
if (Array.isArray(value)) value.forEach((v, i) => bodyHits(v, `${path}[${i}]`, hits))
|
|
77
|
+
else if (value && typeof value === 'object') {
|
|
78
|
+
for (const [k, v] of Object.entries(value)) {
|
|
79
|
+
const p = path ? `${path}.${k}` : k
|
|
80
|
+
if (QUOTA_RE.test(k)) hits.push(p)
|
|
81
|
+
bodyHits(v, p, hits)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return hits
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const redact = (v) => typeof v === 'string'
|
|
88
|
+
? v.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<redacted>').replace(/\b\d{6,}\b/g, '<redacted>')
|
|
89
|
+
: v
|
|
90
|
+
|
|
91
|
+
async function probe(cred, arm) {
|
|
92
|
+
const headers = { Accept: 'application/json', Authorization: cred.authorization, 'User-Agent': arm.ua ?? UA }
|
|
93
|
+
if (cred.apiKey) headers['x-api-key'] = cred.apiKey
|
|
94
|
+
if (arm.body) headers['Content-Type'] = 'application/json'
|
|
95
|
+
const t0 = Date.now()
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(`${GATEWAY}${arm.path}`, {
|
|
98
|
+
method: arm.method ?? (arm.body ? 'POST' : 'GET'),
|
|
99
|
+
headers,
|
|
100
|
+
body: arm.body ? JSON.stringify(arm.body) : undefined,
|
|
101
|
+
})
|
|
102
|
+
const resHeaders = {}
|
|
103
|
+
res.headers.forEach((v, k) => { resHeaders[k] = v.length > 80 ? v.slice(0, 80) + '…' : v })
|
|
104
|
+
const text = await res.text()
|
|
105
|
+
let json = null
|
|
106
|
+
try { json = JSON.parse(text) } catch { /* SSE or plain text */ }
|
|
107
|
+
return {
|
|
108
|
+
name: arm.name, path: arm.path, pred: arm.pred, note: arm.note,
|
|
109
|
+
status: res.status,
|
|
110
|
+
headerNames: Object.keys(resHeaders),
|
|
111
|
+
quotaHeaders: Object.keys(resHeaders).filter((k) => QUOTA_RE.test(k)),
|
|
112
|
+
code: json?.code ?? null,
|
|
113
|
+
bodyQuotaPaths: json ? bodyHits(json) : [],
|
|
114
|
+
bodySample: redact(json ? JSON.stringify(json).slice(0, 300) : text.slice(0, 200)),
|
|
115
|
+
ms: Date.now() - t0,
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return { name: arm.name, path: arm.path, pred: arm.pred, note: arm.note, error: String(err?.message ?? err), ms: Date.now() - t0 }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ARMS = [
|
|
123
|
+
{ name: 'P-Q1a-accounts', path: '/v2/accounts', pred: 'P-Q1', note: 'zero quota-ish response headers' },
|
|
124
|
+
{ name: 'P-Q1b-config', path: '/v3/config', pred: 'P-Q1', note: 'zero quota-ish response headers' },
|
|
125
|
+
{ name: 'P-Q1c-dosage', path: '/v2/billing/meter/get-dosage-notify', body: {}, pred: 'P-Q1', note: 'zero quota-ish response headers' },
|
|
126
|
+
{ name: 'P-Q4-12403-headers', path: '/v3/config', ua: 'Garbage/0.0', pred: 'P-Q4', note: 'error envelope: zero quota headers' },
|
|
127
|
+
{ name: 'P-Q2a-search-body', path: '/agenttool/v1/search', body: { query: 'kimi', type: 'text2text', max_results: 1 }, pred: 'P-Q2a', note: 'search body has NO usage/credit field' },
|
|
128
|
+
{ name: 'P-Q2b-webfetch-body', path: '/agenttool/v1/webfetch', body: { url: 'https://example.com/' }, pred: 'P-Q2b', note: 'webfetch body HAS usage field' },
|
|
129
|
+
{ name: 'P-Q3-dosage-params', path: '/v2/billing/meter/get-dosage-notify', body: { threshold: 100, level: 'high' }, pred: 'P-Q3', note: 'params ignored: same healthy shape as {}' },
|
|
130
|
+
{ name: 'X-report-exists', path: '/v2/report', pred: null, note: 'exploratory: CLI endpoint existence' },
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
const cred = resolveCredential()
|
|
134
|
+
mkdirSync(dirname(OUT), { recursive: true })
|
|
135
|
+
appendFileSync(OUT, JSON.stringify({ type: 'run', at: new Date().toISOString(), gateway: GATEWAY }) + '\n')
|
|
136
|
+
console.log(`evidence → ${OUT}`)
|
|
137
|
+
|
|
138
|
+
for (const arm of ARMS) {
|
|
139
|
+
const rec = { type: 'probe', ...await probe(cred, arm) }
|
|
140
|
+
appendFileSync(OUT, JSON.stringify(rec) + '\n')
|
|
141
|
+
console.log(`${arm.name.padEnd(22)} status=${rec.status ?? '-'} code=${rec.code ?? '-'} quotaHeaders=${(rec.quotaHeaders ?? []).length} bodyQuotaPaths=${JSON.stringify(rec.bodyQuotaPaths ?? [])} ${(rec.bodySample ?? rec.error ?? '').slice(0, 100)}`)
|
|
142
|
+
await sleep(SPACING_MS)
|
|
143
|
+
}
|
|
144
|
+
console.log('done.')
|