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,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* developer-rejection boundary probe (Hermes exploration) — 三轮合并版(2026-08-29)
|
|
4
|
+
* Usage: node scripts/hermes-probe-dev-role.mjs [--round 1|2|3](默认 1)
|
|
5
|
+
*
|
|
6
|
+
* 轮次考古(臂表/判据/落盘文件名/日志格式均保持各轮原样):
|
|
7
|
+
* --round 1 = 原 hermes-probe-dev-role.mjs (C0–C7,落盘 dev-role-boundary-<date>.jsonl)
|
|
8
|
+
* --round 2 = 原 hermes-probe-dev-role2.mjs (P1–P7,落盘 dev-role-boundary2-<date>.jsonl)
|
|
9
|
+
* --round 3 = 原 hermes-probe-dev-role3.mjs (S1–S6,落盘 dev-role-boundary3-<date>.jsonl)
|
|
10
|
+
*
|
|
11
|
+
* ── ROUND 1 (2026-08-19) ─────────────────────────────────────────────
|
|
12
|
+
* Understanding-only continuation of topic 6 (content-moderation.md).
|
|
13
|
+
* Kimi's probe pinned the gate: L1 gateway pre-routing check, literal
|
|
14
|
+
* role:"developer" → HTTP 500 / 11128.
|
|
15
|
+
*
|
|
16
|
+
* This round maps the BOUNDARY of that literal check, to understand what the
|
|
17
|
+
* validator sees and does NOT see. Emphasis: understanding, not bypass.
|
|
18
|
+
* We never use a variant to actually run a harmful/rejected request end-to-end
|
|
19
|
+
* to completion — rejected arms simply confirm the boundary, passing arms
|
|
20
|
+
* are max_tokens=8 stub completions that answer "OK".
|
|
21
|
+
*
|
|
22
|
+
* Arms (all max_tokens=8, 2.5s spacing, one account, read-only intent):
|
|
23
|
+
* case variants : role:"Developer", "DEVELOPER", "developer " (trailing space)
|
|
24
|
+
* unicode variants : fullwidth developer, "dev\u0065loper" (normalized-form test via JSON escape)
|
|
25
|
+
* structural variants: developer as object with extra keys, array-of-roles (role in content), role:"system" containing "developer" text
|
|
26
|
+
* control : role:"system" (should pass 200)
|
|
27
|
+
*
|
|
28
|
+
* The goal: answer — is the gate an exact byte match on the string
|
|
29
|
+
* "developer", or does it normalize (case/unicode/whitespace) before match?
|
|
30
|
+
* This is a *classification* result (which variants are rejected vs passed),
|
|
31
|
+
* NOT a circumvention — findings feed docs/rules content only.
|
|
32
|
+
*
|
|
33
|
+
* ── ROUND 2 ──────────────────────────────────────────────────────────
|
|
34
|
+
* Round 1 established: gate is exact-literal byte match on lowercase "developer".
|
|
35
|
+
* Round 2 maps POSITION and STRUCTURE sensitivity of that literal check:
|
|
36
|
+
*
|
|
37
|
+
* P1 developer as SECOND message (after user) → still rejected? (position)
|
|
38
|
+
* P2 developer LAST message (after user+assistant) → still rejected?
|
|
39
|
+
* P3 mixed: system + developer + user → affects?
|
|
40
|
+
* P4 two developers in different messages → counted individually?
|
|
41
|
+
* P5 role:"developer " + role:"developer" together → does trailing-space one trigger?
|
|
42
|
+
* P6 role as string "developer" but in a nested object (tool message) → does tool path check?
|
|
43
|
+
* P7 control: all system + user → pass
|
|
44
|
+
*
|
|
45
|
+
* All max_tokens=8, 2.5s spacing, understanding-only (no completion of any
|
|
46
|
+
* rejected path; passing arms only answer "OK").
|
|
47
|
+
*
|
|
48
|
+
* ── ROUND 3 ──────────────────────────────────────────────────────────
|
|
49
|
+
* R1: exact-literal match on lowercase "developer" in role field.
|
|
50
|
+
* R2: position-independent; per-message independent check; role field only.
|
|
51
|
+
*
|
|
52
|
+
* R3 maps the *shape* the validator can see — i.e. HOW it enumerates
|
|
53
|
+
* messages. Questions:
|
|
54
|
+
*
|
|
55
|
+
* S1 duplicate role fields in ONE message object → validator reads first? last? any?
|
|
56
|
+
* S2 role as non-string (number) → parse tolerance?
|
|
57
|
+
* S3 role missing entirely (bare content) → does it 400/accept?
|
|
58
|
+
* S4 role:"developer" with content as array → content shape affects the check? (true content block form)
|
|
59
|
+
* S5 multi-content blocks (one developer, one user) → same object, mixed roles?
|
|
60
|
+
* S6 control again
|
|
61
|
+
*
|
|
62
|
+
* All max_tokens=8, 2.5s, understanding-only.
|
|
63
|
+
*/
|
|
64
|
+
import { readFileSync, existsSync, appendFileSync } from 'node:fs'
|
|
65
|
+
import { homedir } from 'node:os'
|
|
66
|
+
import { join } from 'node:path'
|
|
67
|
+
|
|
68
|
+
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
69
|
+
const GATEWAY = 'https://copilot.tencent.com'
|
|
70
|
+
const SPACING_MS = 2500
|
|
71
|
+
const MODEL = 'glm-5.2'
|
|
72
|
+
|
|
73
|
+
function resolveKey() {
|
|
74
|
+
try {
|
|
75
|
+
const cfg = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin.json'), 'utf8'))
|
|
76
|
+
const active = (cfg.apiKeys ?? []).find((k) => k.name === cfg.activeApiKey)
|
|
77
|
+
if (active?.key) return active.key
|
|
78
|
+
} catch { /* fall through */ }
|
|
79
|
+
if (process.env.CODEBUDDY_API_KEY) return process.env.CODEBUDDY_API_KEY
|
|
80
|
+
const credFile = join(DSH_HOME, '.credentials.yaml')
|
|
81
|
+
if (existsSync(credFile)) {
|
|
82
|
+
const m = readFileSync(credFile, 'utf8').match(/^\s*CODEBUDDY_API_KEY:\s*["']?([^"'\s]+)["']?\s*$/m)
|
|
83
|
+
if (m) return m[1]
|
|
84
|
+
}
|
|
85
|
+
throw new Error('no CodeBuddy credential found')
|
|
86
|
+
}
|
|
87
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
88
|
+
const redact = (v) => typeof v === 'string' ? v.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<redacted>') : v
|
|
89
|
+
const SYS = 'You are a concise test agent. Answer with exactly: OK'
|
|
90
|
+
|
|
91
|
+
// ─────────────────────────── ROUND 1(原 hermes-probe-dev-role.mjs) ───────────────────────────
|
|
92
|
+
|
|
93
|
+
const redactR1 = (v) => typeof v === 'string'
|
|
94
|
+
? 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>')
|
|
95
|
+
: v
|
|
96
|
+
|
|
97
|
+
// Each variant: a function returning [role, content] or null (skip)
|
|
98
|
+
const ARMS_R1 = [
|
|
99
|
+
{ name: 'C0-control-system', role: 'system', note: 'control: system must pass', expectPass: true },
|
|
100
|
+
{ name: 'C1-developer-baseline', role: 'developer', note: 'baseline: literal developer must be rejected', expectPass: false },
|
|
101
|
+
{ name: 'C2-case-Developer', role: 'Developer', note: 'capital D: case-sensitive?', expectPass: null },
|
|
102
|
+
{ name: 'C3-case-DEVELOPER', role: 'DEVELOPER', note: 'all caps', expectPass: null },
|
|
103
|
+
{ name: 'C4-trailing-space', role: 'developer ', note: 'trailing space inside role value', expectPass: null },
|
|
104
|
+
{ name: 'C5-fullwidth', role: 'developer', note: 'fullwidth unicode homoglyph', expectPass: null },
|
|
105
|
+
{ name: 'C6-escaped-e', role: 'dev\u0065loper', note: 'JSON escape \\u0065 = literal e: should equal developer', expectPass: false },
|
|
106
|
+
{ name: 'C7-role-in-content', role: 'user', content: `developer message`, note: 'role user but content mentions developer: gate on content or role?', expectPass: true, useContent: true },
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
async function runRound1() {
|
|
110
|
+
const OUT = join('docs', 'probes', `dev-role-boundary-${new Date().toISOString().slice(0, 10)}.jsonl`)
|
|
111
|
+
const key = resolveKey()
|
|
112
|
+
const results = []
|
|
113
|
+
for (const arm of ARMS_R1) {
|
|
114
|
+
const role = arm.useContent ? 'user' : arm.role
|
|
115
|
+
const content = arm.useContent
|
|
116
|
+
? (arm.content || `developer: ${SYS}`)
|
|
117
|
+
: SYS
|
|
118
|
+
const messages = [{ role, content }]
|
|
119
|
+
const body = {
|
|
120
|
+
model: MODEL,
|
|
121
|
+
stream: true,
|
|
122
|
+
max_tokens: 8,
|
|
123
|
+
messages,
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const started = Date.now()
|
|
127
|
+
const resp = await fetch(`${GATEWAY}/v2/chat/completions`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
'Authorization': `Bearer ${key}`,
|
|
132
|
+
'User-Agent': 'CLI/unknown CodeBuddy/2.136.0',
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify(body),
|
|
135
|
+
})
|
|
136
|
+
const text = await resp.text()
|
|
137
|
+
const ms = Date.now() - started
|
|
138
|
+
let code = null, msg = null
|
|
139
|
+
try { const j = JSON.parse(text); code = j.code; msg = j.msg } catch { /* sse */ }
|
|
140
|
+
const raw = text.slice(0, 200)
|
|
141
|
+
const entry = {
|
|
142
|
+
type: 'probe',
|
|
143
|
+
at: new Date().toISOString(),
|
|
144
|
+
name: arm.name,
|
|
145
|
+
role: role,
|
|
146
|
+
expectPass: arm.expectPass,
|
|
147
|
+
note: arm.note,
|
|
148
|
+
status: resp.status,
|
|
149
|
+
code,
|
|
150
|
+
msg,
|
|
151
|
+
ms,
|
|
152
|
+
raw: redactR1(raw),
|
|
153
|
+
}
|
|
154
|
+
results.push(entry)
|
|
155
|
+
appendFileSync(OUT, JSON.stringify(entry) + '\n')
|
|
156
|
+
console.log(`${arm.name.padEnd(26)} status=${resp.status} code=${code ?? '-'} msg=${(msg ?? '').slice(0, 40)} ${ms}ms`)
|
|
157
|
+
} catch (e) {
|
|
158
|
+
const entry = { type: 'probe', at: new Date().toISOString(), name: arm.name, error: String(e) }
|
|
159
|
+
results.push(entry)
|
|
160
|
+
appendFileSync(OUT, JSON.stringify(entry) + '\n')
|
|
161
|
+
console.log(`${arm.name.padEnd(26)} ERROR ${e}`)
|
|
162
|
+
}
|
|
163
|
+
await sleep(SPACING_MS)
|
|
164
|
+
}
|
|
165
|
+
console.log(`\nDone. ${results.length} arms → ${OUT}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────── ROUND 2(原 hermes-probe-dev-role2.mjs) ───────────────────────────
|
|
169
|
+
|
|
170
|
+
// messages builder per arm
|
|
171
|
+
const ARMS_R2 = [
|
|
172
|
+
{ name: 'P7-control', mk: () => [{ role: 'system', content: SYS }, { role: 'user', content: 'hi' }], note: 'control: no developer at all, expect 200' },
|
|
173
|
+
{ name: 'P1-dev-second', mk: () => [{ role: 'user', content: 'hi' }, { role: 'developer', content: SYS }, { role: 'user', content: 'ok?' }], note: 'developer in middle position' },
|
|
174
|
+
{ name: 'P2-dev-last', mk: () => [{ role: 'user', content: 'hi' }, { role: 'assistant', content: 'hi' }, { role: 'developer', content: SYS }], note: 'developer as last message' },
|
|
175
|
+
{ name: 'P3-sys-dev-user', mk: () => [{ role: 'system', content: SYS }, { role: 'developer', content: SYS }, { role: 'user', content: 'hi' }], note: 'system then developer then user' },
|
|
176
|
+
{ name: 'P4-two-dev', mk: () => [{ role: 'developer', content: SYS }, { role: 'developer', content: SYS }, { role: 'user', content: 'hi' }], note: 'two developer messages' },
|
|
177
|
+
{ name: 'P5-space-then-dev', mk: () => [{ role: 'developer ', content: SYS }, { role: 'developer', content: SYS }, { role: 'user', content: 'hi' }], note: 'trailing-space dev (passes) then real dev (rejects?)' },
|
|
178
|
+
{ name: 'P6-tool-dev', mk: () => [{ role: 'system', content: SYS }, { role: 'user', content: 'hi' }, { role: 'assistant', content: 'call tool', tool_calls: [{ id: 't1', type: 'function', function: { name: 'x', arguments: '{}' } }] }, { role: 'tool', tool_call_id: 't1', content: 'developer deep inside' }], note: 'developer string only inside tool content' },
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
async function runRound2() {
|
|
182
|
+
const OUT = join('docs', 'probes', `dev-role-boundary2-${new Date().toISOString().slice(0, 10)}.jsonl`)
|
|
183
|
+
const key = resolveKey()
|
|
184
|
+
for (const arm of ARMS_R2) {
|
|
185
|
+
const messages = arm.mk()
|
|
186
|
+
const body = { model: MODEL, stream: true, max_tokens: 8, messages }
|
|
187
|
+
try {
|
|
188
|
+
const started = Date.now()
|
|
189
|
+
const resp = await fetch(`${GATEWAY}/v2/chat/completions`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}`, 'User-Agent': 'CLI/unknown CodeBuddy/2.136.0' },
|
|
192
|
+
body: JSON.stringify(body),
|
|
193
|
+
})
|
|
194
|
+
const text = await resp.text()
|
|
195
|
+
const ms = Date.now() - started
|
|
196
|
+
let code = null, msg = null
|
|
197
|
+
try { const j = JSON.parse(text); code = j.code; msg = j.msg } catch { /* sse */ }
|
|
198
|
+
const entry = { type: 'probe', at: new Date().toISOString(), name: arm.name, note: arm.note, status: resp.status, code, msg, ms, raw: redact(text.slice(0, 120)) }
|
|
199
|
+
appendFileSync(OUT, JSON.stringify(entry) + '\n')
|
|
200
|
+
console.log(`${arm.name.padEnd(20)} status=${resp.status} code=${code ?? '-'} msg=${(msg ?? '').slice(0, 44)} ${ms}ms`)
|
|
201
|
+
} catch (e) {
|
|
202
|
+
appendFileSync(OUT, JSON.stringify({ type: 'probe', at: new Date().toISOString(), name: arm.name, error: String(e) }) + '\n')
|
|
203
|
+
console.log(`${arm.name.padEnd(20)} ERROR ${e}`)
|
|
204
|
+
}
|
|
205
|
+
await sleep(SPACING_MS)
|
|
206
|
+
}
|
|
207
|
+
console.log(`\nDone → ${OUT}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─────────────────────────── ROUND 3(原 hermes-probe-dev-role3.mjs) ───────────────────────────
|
|
211
|
+
|
|
212
|
+
// raw body per arm (we need raw object control for structural probes)
|
|
213
|
+
const ARMS_R3 = [
|
|
214
|
+
{ name: 'S6-control', mk: () => ({ messages: [{ role: 'system', content: SYS }, { role: 'user', content: 'hi' }] }), note: 'control' },
|
|
215
|
+
{ name: 'S1-dup-role', mk: () => ({ messages: [{ role: 'developer', role2: 'user', content: SYS }, { role: 'user', content: 'hi' }] }), note: 'two role-ish keys in one object' },
|
|
216
|
+
{ name: 'S2-role-number', mk: () => ({ messages: [{ role: 1, content: SYS }, { role: 'user', content: 'hi' }] }), note: 'role as integer' },
|
|
217
|
+
{ name: 'S3-no-role', mk: () => ({ messages: [{ content: SYS }, { role: 'user', content: 'hi' }] }), note: 'message without any role field' },
|
|
218
|
+
{ name: 'S4-dev-array', mk: () => ({ messages: [{ role: 'developer', content: [{ type: 'text', text: SYS }] }, { role: 'user', content: 'hi' }] }), note: 'developer with content as array of blocks' },
|
|
219
|
+
{ name: 'S5-mixed-blocks', mk: () => ({ messages: [{ role: 'user', content: [{ type: 'text', text: SYS }] }], extra: { role: 'developer' } }), note: 'developer only in top-level extra (not messages array)' },
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
async function runRound3() {
|
|
223
|
+
const OUT = join('docs', 'probes', `dev-role-boundary3-${new Date().toISOString().slice(0, 10)}.jsonl`)
|
|
224
|
+
const key = resolveKey()
|
|
225
|
+
for (const arm of ARMS_R3) {
|
|
226
|
+
const base = arm.mk()
|
|
227
|
+
const body = { model: MODEL, stream: true, max_tokens: 8, ...base }
|
|
228
|
+
try {
|
|
229
|
+
const started = Date.now()
|
|
230
|
+
const resp = await fetch(`${GATEWAY}/v2/chat/completions`, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}`, 'User-Agent': 'CLI/unknown CodeBuddy/2.136.0' },
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
})
|
|
235
|
+
const text = await resp.text()
|
|
236
|
+
const ms = Date.now() - started
|
|
237
|
+
let code = null, msg = null
|
|
238
|
+
try { const j = JSON.parse(text); code = j.code; msg = j.msg } catch { /* sse */ }
|
|
239
|
+
const entry = { type: 'probe', at: new Date().toISOString(), name: arm.name, note: arm.note, status: resp.status, code, msg, ms, raw: redact(text.slice(0, 120)) }
|
|
240
|
+
appendFileSync(OUT, JSON.stringify(entry) + '\n')
|
|
241
|
+
console.log(`${arm.name.padEnd(18)} status=${resp.status} code=${code ?? '-'} msg=${(msg ?? '').slice(0, 44)} ${ms}ms`)
|
|
242
|
+
} catch (e) {
|
|
243
|
+
appendFileSync(OUT, JSON.stringify({ type: 'probe', at: new Date().toISOString(), name: arm.name, error: String(e) }) + '\n')
|
|
244
|
+
console.log(`${arm.name.padEnd(18)} ERROR ${e}`)
|
|
245
|
+
}
|
|
246
|
+
await sleep(SPACING_MS)
|
|
247
|
+
}
|
|
248
|
+
console.log(`\nDone → ${OUT}`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─────────────────────────── 轮次调度 ───────────────────────────
|
|
252
|
+
|
|
253
|
+
function argValue(flag) {
|
|
254
|
+
const i = process.argv.indexOf(flag)
|
|
255
|
+
return i > 0 ? process.argv[i + 1] : null
|
|
256
|
+
}
|
|
257
|
+
const ROUNDS = { 1: runRound1, 2: runRound2, 3: runRound3 }
|
|
258
|
+
const ROUND = argValue('--round') ?? '1'
|
|
259
|
+
if (process.argv.includes('--help') || !ROUNDS[ROUND]) {
|
|
260
|
+
console.log('Usage: node scripts/hermes-probe-dev-role.mjs [--round 1|2|3](默认 1;轮次对应见文件头注释)')
|
|
261
|
+
process.exit(process.argv.includes('--help') ? 0 : 1)
|
|
262
|
+
}
|
|
263
|
+
ROUNDS[ROUND]().catch((e) => { console.error(e); process.exit(1) })
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Latency measurement + error reproduction for 识图 (bridge aggregation) and
|
|
4
|
+
* /agenttool search — item 2 tooling. Repeatable by construction: fixed
|
|
5
|
+
* payloads, fixed iteration count, every iteration appended as JSONL.
|
|
6
|
+
*
|
|
7
|
+
* Paths measured:
|
|
8
|
+
* vision-aggregate : POST {bridge}/v2/chat/completions, stream absent, with
|
|
9
|
+
* image content — the describe-image shape; the caller
|
|
10
|
+
* waits for the FULL upstream stream (bridge aggregates).
|
|
11
|
+
* vision-stream : same payload with stream:true — TTFB/total split
|
|
12
|
+
* separates upstream generation time from bridge work.
|
|
13
|
+
* search : makeSearchProvider().search() — the production
|
|
14
|
+
* /agenttool/v1/search code path (credentials included).
|
|
15
|
+
* search-direct : (mock mode) raw fetch to the gateway, bypassing the
|
|
16
|
+
* provider — isolates provider-side overhead.
|
|
17
|
+
*
|
|
18
|
+
* Modes:
|
|
19
|
+
* --mock (default) mock gateway + the real bridge on ephemeral ports.
|
|
20
|
+
* No credentials needed; quantifies bridge/provider overhead
|
|
21
|
+
* against a controlled upstream delay.
|
|
22
|
+
* --real live gateway: search resolves the key from
|
|
23
|
+
* ~/.dsh/codebuddy-plugin.json; vision goes through the already
|
|
24
|
+
* running dsh bridge (--bridge-port, default 3901).
|
|
25
|
+
*
|
|
26
|
+
* Options: --iters N (20) · --out PATH (/tmp/codebuddy-latency.jsonl)
|
|
27
|
+
* --bridge-port N (3901) · --model ID · --query STR
|
|
28
|
+
* --upstream-delay MS (mock, 300) · --stream-drip MS (mock, 25)
|
|
29
|
+
*
|
|
30
|
+
* Records: {ts, mode, path, iter, ok, ms, ttfbMs?, bytesIn, status, err?}.
|
|
31
|
+
* Summary (min/p50/p95/max per path) is printed and appended to the file.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { createServer } from 'node:http'
|
|
35
|
+
import { appendFileSync, mkdtempSync, readFileSync } from 'node:fs'
|
|
36
|
+
import { homedir, tmpdir } from 'node:os'
|
|
37
|
+
import { join, dirname } from 'node:path'
|
|
38
|
+
import { fileURLToPath } from 'node:url'
|
|
39
|
+
|
|
40
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
41
|
+
|
|
42
|
+
// ------------------------------------------------------------------ args
|
|
43
|
+
const args = process.argv.slice(2)
|
|
44
|
+
const opt = (name, dflt) => {
|
|
45
|
+
const i = args.indexOf(`--${name}`)
|
|
46
|
+
return i >= 0 ? args[i + 1] : dflt
|
|
47
|
+
}
|
|
48
|
+
const MODE = args.includes('--real') ? 'real' : 'mock'
|
|
49
|
+
const ITERS = Number(opt('iters', 20))
|
|
50
|
+
const OUT = opt('out', '/tmp/codebuddy-latency.jsonl')
|
|
51
|
+
const BRIDGE_PORT = Number(opt('bridge-port', 3901))
|
|
52
|
+
const UPSTREAM_DELAY = Number(opt('upstream-delay', 300))
|
|
53
|
+
const STREAM_DRIP = Number(opt('stream-drip', 25))
|
|
54
|
+
const QUERY = opt('query', '腾讯 CodeBuddy 是什么')
|
|
55
|
+
const MODEL = opt('model', MODE === 'real' ? 'deepseek-v4-flash' : 'm1')
|
|
56
|
+
|
|
57
|
+
// 1x1 red PNG — valid image, tiny upload.
|
|
58
|
+
const RED_PNG_DATA_URL =
|
|
59
|
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='
|
|
60
|
+
|
|
61
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
62
|
+
const records = []
|
|
63
|
+
function record(rec) {
|
|
64
|
+
records.push(rec)
|
|
65
|
+
appendFileSync(OUT, JSON.stringify(rec) + '\n')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ------------------------------------------------------------------ mock gateway
|
|
69
|
+
function startMockGateway() {
|
|
70
|
+
const server = createServer((req, res) => {
|
|
71
|
+
let raw = ''
|
|
72
|
+
req.on('data', (c) => (raw += c))
|
|
73
|
+
req.on('end', () => {
|
|
74
|
+
if (req.url.endsWith('/chat/completions')) {
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' })
|
|
77
|
+
const chunks = ['a red ', 'dot on ', 'a white ', 'background.']
|
|
78
|
+
let i = 0
|
|
79
|
+
const drip = () => {
|
|
80
|
+
if (i < chunks.length) {
|
|
81
|
+
res.write(`data: {"id":"mock","choices":[{"index":0,"delta":{"content":${JSON.stringify(chunks[i++])}}}]}\n\n`)
|
|
82
|
+
setTimeout(drip, STREAM_DRIP)
|
|
83
|
+
} else {
|
|
84
|
+
res.end('data: {"id":"mock","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":4,"total_tokens":14}}\n\ndata: [DONE]\n\n')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
drip()
|
|
88
|
+
}, UPSTREAM_DELAY)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
93
|
+
res.end(JSON.stringify({ code: 0, results: [{ url: 'https://example.com', title: 'x', snippet: 'y' }] }))
|
|
94
|
+
}, UPSTREAM_DELAY)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
server.listen(0, '127.0.0.1', () => resolve({ server, port: server.address().port }))
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function freePort() {
|
|
103
|
+
const probe = createServer()
|
|
104
|
+
await new Promise((r) => probe.listen(0, '127.0.0.1', r))
|
|
105
|
+
const { port } = probe.address()
|
|
106
|
+
await new Promise((r) => probe.close(r))
|
|
107
|
+
return port
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ------------------------------------------------------------------ probes
|
|
111
|
+
function visionPayload(stream) {
|
|
112
|
+
return {
|
|
113
|
+
model: MODEL,
|
|
114
|
+
...(stream ? { stream: true } : {}),
|
|
115
|
+
messages: [
|
|
116
|
+
{
|
|
117
|
+
role: 'user',
|
|
118
|
+
content: [
|
|
119
|
+
{ type: 'text', text: 'Describe this image in one short sentence.' },
|
|
120
|
+
{ type: 'image_url', image_url: { url: RED_PNG_DATA_URL } },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function probeVision(bridgeBase, stream, iter) {
|
|
128
|
+
const t0 = Date.now()
|
|
129
|
+
let ttfbMs = null
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(`${bridgeBase}/v2/chat/completions`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify(visionPayload(stream)),
|
|
135
|
+
})
|
|
136
|
+
ttfbMs = Date.now() - t0
|
|
137
|
+
const text = await res.text()
|
|
138
|
+
const ms = Date.now() - t0
|
|
139
|
+
const ok = res.status === 200 && (stream ? text.includes('[DONE]') : text.includes('chat.completion'))
|
|
140
|
+
record({
|
|
141
|
+
ts: t0, mode: MODE, path: stream ? 'vision-stream' : 'vision-aggregate',
|
|
142
|
+
iter, ok, ms, ttfbMs, bytesIn: text.length, status: res.status,
|
|
143
|
+
...(ok ? {} : { err: text.slice(0, 300) }),
|
|
144
|
+
})
|
|
145
|
+
} catch (err) {
|
|
146
|
+
record({ ts: t0, mode: MODE, path: stream ? 'vision-stream' : 'vision-aggregate', iter, ok: false, ms: Date.now() - t0, err: String(err?.message ?? err) })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function probeSearch(searchFn, iter, path = 'search') {
|
|
151
|
+
const t0 = Date.now()
|
|
152
|
+
try {
|
|
153
|
+
const out = await searchFn()
|
|
154
|
+
record({ ts: t0, mode: MODE, path, iter, ok: true, ms: Date.now() - t0, bytesIn: JSON.stringify(out).length, status: 200 })
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// The error text IS the evidence: keep it verbatim (gateway code/msg or
|
|
157
|
+
// the network-layer cause chain).
|
|
158
|
+
record({ ts: t0, mode: MODE, path, iter, ok: false, ms: Date.now() - t0, err: String(err?.message ?? err) })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ------------------------------------------------------------------ stats
|
|
163
|
+
function summarize(pathFilter) {
|
|
164
|
+
const rows = records.filter((r) => r.path === pathFilter)
|
|
165
|
+
const oks = rows.filter((r) => r.ok).map((r) => r.ms).sort((a, b) => a - b)
|
|
166
|
+
const fails = rows.filter((r) => !r.ok)
|
|
167
|
+
const pct = (p) => (oks.length ? oks[Math.min(oks.length - 1, Math.floor((p / 100) * oks.length))] : null)
|
|
168
|
+
const ttfbs = rows.filter((r) => r.ok && r.ttfbMs != null).map((r) => r.ttfbMs).sort((a, b) => a - b)
|
|
169
|
+
const line = {
|
|
170
|
+
kind: 'summary', mode: MODE, path: pathFilter, n: rows.length, failed: fails.length,
|
|
171
|
+
min: oks[0] ?? null, p50: pct(50), p95: pct(95), max: oks[oks.length - 1] ?? null,
|
|
172
|
+
ttfbP50: ttfbs.length ? ttfbs[Math.floor(ttfbs.length / 2)] : null,
|
|
173
|
+
}
|
|
174
|
+
appendFileSync(OUT, JSON.stringify(line) + '\n')
|
|
175
|
+
console.log(` ${pathFilter.padEnd(18)} n=${String(line.n).padStart(3)} fail=${line.failed} min=${line.min} p50=${line.p50} p95=${line.p95} max=${line.max}${line.ttfbP50 != null ? ` ttfbP50=${line.ttfbP50}` : ''}`)
|
|
176
|
+
for (const f of fails.slice(0, 5)) console.log(` fail#${f.iter} ${f.ms}ms :: ${f.err}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ------------------------------------------------------------------ main
|
|
180
|
+
async function main() {
|
|
181
|
+
// Env must be set BEFORE importing index.js — it captures DSH_HOME at
|
|
182
|
+
// module load (verify-bridge.mjs does the same).
|
|
183
|
+
if (MODE === 'mock') {
|
|
184
|
+
process.env.DSH_HOME = mkdtempSync(join(tmpdir(), 'dsh-latency-'))
|
|
185
|
+
process.env.CODEBUDDY_API_KEY = 'ck_latency_test_key'
|
|
186
|
+
}
|
|
187
|
+
const { apply, Config, makeSearchProvider } = await import(new URL('../index.js', import.meta.url).href)
|
|
188
|
+
const AUTH_MODE_OVERRIDE = opt('auth-mode', null) // real mode: e.g. --auth-mode api-key
|
|
189
|
+
|
|
190
|
+
if (MODE === 'mock') {
|
|
191
|
+
const { server, port: gwPort } = await startMockGateway()
|
|
192
|
+
const bridgePort = await freePort()
|
|
193
|
+
const ctx = {
|
|
194
|
+
inject: (_deps, cb) => cb({ webServer: { register() {} } }),
|
|
195
|
+
web: { registerSearchProvider: () => () => {}, registerFetchProvider: () => () => {} },
|
|
196
|
+
on: () => {},
|
|
197
|
+
}
|
|
198
|
+
apply(ctx, { baseURL: `http://127.0.0.1:${gwPort}`, bridgePort })
|
|
199
|
+
await sleep(250)
|
|
200
|
+
const bridgeBase = `http://127.0.0.1:${bridgePort}`
|
|
201
|
+
const settings = () => Config({ baseURL: `http://127.0.0.1:${gwPort}` })
|
|
202
|
+
const provider = makeSearchProvider(settings)
|
|
203
|
+
|
|
204
|
+
console.log(`[mock] upstream delay ${UPSTREAM_DELAY}ms + drip ${STREAM_DRIP}ms/chunk, ${ITERS} iters per path`)
|
|
205
|
+
for (let i = 0; i < ITERS; i++) {
|
|
206
|
+
await probeVision(bridgeBase, false, i)
|
|
207
|
+
await probeVision(bridgeBase, true, i)
|
|
208
|
+
await probeSearch(() => provider.search({ query: QUERY }), i)
|
|
209
|
+
await probeSearch(
|
|
210
|
+
() => fetch(`http://127.0.0.1:${gwPort}/agenttool/v1/search`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ck_latency_test_key' },
|
|
213
|
+
body: JSON.stringify({ query: QUERY, type: 'text2text', max_results: 5 }),
|
|
214
|
+
}).then(async (r) => {
|
|
215
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
|
216
|
+
return r.json()
|
|
217
|
+
}),
|
|
218
|
+
i,
|
|
219
|
+
'search-direct',
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
server.close()
|
|
223
|
+
} else {
|
|
224
|
+
// real: vision through the running dsh bridge, search direct to gateway
|
|
225
|
+
const settingsFile = join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'codebuddy-plugin.json')
|
|
226
|
+
const settings = () => Config({
|
|
227
|
+
...JSON.parse(readFileSync(settingsFile, 'utf8')),
|
|
228
|
+
...(AUTH_MODE_OVERRIDE ? { authMode: AUTH_MODE_OVERRIDE } : {}),
|
|
229
|
+
})
|
|
230
|
+
const provider = makeSearchProvider(settings)
|
|
231
|
+
const bridgeBase = `http://127.0.0.1:${BRIDGE_PORT}`
|
|
232
|
+
console.log(`[real] bridge :${BRIDGE_PORT}, model ${MODEL}, ${ITERS} iters per path`)
|
|
233
|
+
for (let i = 0; i < ITERS; i++) {
|
|
234
|
+
await probeVision(bridgeBase, false, i)
|
|
235
|
+
await probeVision(bridgeBase, true, i)
|
|
236
|
+
await probeSearch(() => provider.search({ query: QUERY }), i)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log('\nsummary (ms):')
|
|
241
|
+
summarize('vision-aggregate')
|
|
242
|
+
summarize('vision-stream')
|
|
243
|
+
summarize('search')
|
|
244
|
+
if (MODE === 'mock') summarize('search-direct')
|
|
245
|
+
const totalFails = records.filter((r) => !r.ok).length
|
|
246
|
+
console.log(`\n${records.length} records → ${OUT} (${totalFails} failures)`)
|
|
247
|
+
process.exit(0)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
main().catch((err) => {
|
|
251
|
+
console.error(`measure-latency crashed: ${err.stack ?? err.message}`)
|
|
252
|
+
process.exit(1)
|
|
253
|
+
})
|