fix(hermes): restore z-ai pricing; require owner/repo; reject UNC

Extra High held 91754c5. z-ai/glm-5.2 went unpriced (Cline's real
vendor spelling). Forward-slash UNC became a workspace on POSIX.
PR matching used basename, so evil/codeburn collided. Tilde fences
were still scanned.

Add z-ai to known namespaces. Reject // UNC on POSIX. Attribute
PRs only when origin yields owner/repo. Strip ``` and ~~~ fences.
Bump Hermes parse version to v4.
This commit is contained in:
Aditya Vikram Singh 2026-08-19 20:32:51 +05:30
parent 91754c5f54
commit fae4db0008
5 changed files with 97 additions and 14 deletions

View file

@ -617,7 +617,7 @@ const KNOWN_NAMESPACES = new Set([
'gemini', 'vertex_ai', 'bedrock', 'xiaomi', 'deepseek', 'moonshot',
'minimax', 'mistral', 'meta-llama', 'meta', 'cohere', 'together_ai',
'groq', 'fireworks_ai', 'xai', 'databricks', 'snowflake', 'novita',
'nvidia', 'cerebras', 'kimi', 'alibaba', 'dashscope',
'nvidia', 'cerebras', 'kimi', 'alibaba', 'dashscope', 'z-ai',
'cp', 'cline-pass', 'cline-free', 'cmd', 'antigravity',
])

View file

@ -1,4 +1,5 @@
import { readdir, stat } from 'fs/promises'
import { existsSync, readFileSync, statSync } from 'fs'
import { basename, dirname, join } from 'path'
import { homedir } from 'os'
@ -92,23 +93,72 @@ function displayProjectForProfile(profile: string): string {
return profile === 'default' ? 'hermes' : sanitizeProject(profile)
}
function stripCodeRegions(text: string): string {
return text
.replace(/^[ \t]{4,}.*$/gm, ' ')
.replace(/(`{3,}|~{3,})[^\n]*\n[\s\S]*?\1/g, ' ')
.replace(/`[^`]*`/g, ' ')
}
function githubOwnerRepoFromRoot(repoRoot: string): { owner: string; repo: string } | null {
try {
let gitDir = join(repoRoot, '.git')
if (!existsSync(gitDir)) return null
const st = statSyncSafe(gitDir)
if (st === 'file') {
const body = readFileSync(gitDir, 'utf8')
const match = body.match(/^gitdir:\s*(.+?)\s*$/m)
if (!match?.[1]) return null
gitDir = match[1].startsWith('/') || /^[a-zA-Z]:[\\/]/.test(match[1])
? match[1]
: join(repoRoot, match[1])
} else if (st !== 'dir') {
return null
}
const config = readFileSync(join(gitDir, 'config'), 'utf8')
const urlMatch = config.match(/url\s*=\s*(.+)/i)
if (!urlMatch?.[1]) return null
const url = urlMatch[1].trim()
const gh = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/i)
if (!gh) return null
return { owner: gh[1].toLowerCase(), repo: gh[2].replace(/\.git$/i, '').toLowerCase() }
} catch {
return null
}
}
function statSyncSafe(path: string): 'file' | 'dir' | null {
try {
const st = statSync(path)
if (st.isFile()) return 'file'
if (st.isDirectory()) return 'dir'
return null
} catch {
return null
}
}
function extractGithubPullUrls(
texts: Array<string | null | undefined>,
repoName?: string,
identity?: { owner: string; repo: string } | 'none',
): string[] {
if (identity === 'none') return []
const found = new Set<string>()
const re = /https:\/\/github\.com\/[^/\s"'<>]+\/[^/\s"'<>]+\/pull\/\d+/gi
const wantedRepo = repoName?.trim().toLowerCase()
for (const text of texts) {
if (!text) continue
const searchable = text.replace(/```[\s\S]*?```/g, ' ').replace(/`[^`]*`/g, ' ')
const searchable = stripCodeRegions(text)
for (const match of searchable.matchAll(re)) {
try {
const url = new URL(match[0])
if (url.protocol !== 'https:') continue
const pathMatch = url.pathname.match(/^\/[^/]+\/([^/]+)\/pull\/\d+$/)
const pathMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/\d+$/)
if (!pathMatch) continue
if (wantedRepo && pathMatch[1].toLowerCase() !== wantedRepo) continue
if (identity) {
const owner = pathMatch[1].toLowerCase()
const repo = pathMatch[2].toLowerCase()
if (owner !== identity.owner || repo !== identity.repo) continue
}
found.add(`${url.origin}${url.pathname}`)
} catch {
// skip malformed
@ -283,8 +333,8 @@ function isRealWorkspace(cwd: string | null | undefined): cwd is string {
if (!cwd?.trim()) return false
const trimmed = cwd.trim()
const isAbsolute = process.platform === 'win32'
? /^[a-zA-Z]:[\\/]/.test(trimmed)
: trimmed.startsWith('/')
? /^[a-zA-Z]:[\\/]/.test(trimmed) || /^\\\\[^\\/]+\\/.test(trimmed)
: trimmed.startsWith('/') && !trimmed.startsWith('//')
if (!isAbsolute) return false
const normalized = trimmed.replace(/\\/g, '/').replace(/\/+$/, '')
if (normalized === '/' || normalized === homedir() || normalized === homedir().replace(/\\/g, '/')) return false
@ -448,12 +498,14 @@ function createParser(source: SessionSource, seenKeys: Set<string>, hermesHome:
if (seenKeys.has(dedupKey)) return
seenKeys.add(dedupKey)
const repoName = workspace.projectPath ? basename(workspace.projectPath) : undefined
const identity = workspace.projectPath
? (githubOwnerRepoFromRoot(workspace.projectPath) ?? 'none')
: undefined
const prLinks = extractGithubPullUrls(
messages
.filter(msg => msg.role === 'assistant' || msg.role === 'user')
.map(msg => msg.content),
repoName,
identity,
)
// Hermes bills reasoning tokens at the output rate (same as Gemini).

View file

@ -292,7 +292,7 @@ export const PROVIDER_PARSE_VERSIONS: Record<string, string> = {
// replays (double-counted before), takes the model from the reporting
// assistant/message, and keeps agent-injected context out of the preview.
dsh: 'seed-aware-v1',
hermes: 'reasoning-output-accounting-v1-est-cost-routed-ids-workspace-pr-v3',
hermes: 'reasoning-output-accounting-v1-est-cost-routed-ids-workspace-pr-v4',
'lingtai-tui': 'token-ledger-registry-activity-v3',
'ibm-bob': 'worktree-project-grouping-v1',
// project-path-v1: the parser now records the session's full working

View file

@ -78,6 +78,8 @@ describe('getModelCosts', () => {
expect(getModelCosts('cp/provider/glm-5.3')).toBeNull()
expect(getModelCosts('omniroute:provider/glm-5.3')).toBeNull()
expect(getModelCosts('unknown/deepseek-v4-flash')).toBeNull()
expect(getModelCosts('z-ai/glm-5.2')).not.toBeNull()
expect(getModelCosts('z-ai/glm-5.3')!.inputCostPerToken).toBe(sibling!.inputCostPerToken)
})
})

View file

@ -636,11 +636,14 @@ skipUnlessSqlite('hermes provider', () => {
})
it('ignores fenced and unrelated-repo pull URLs when a git root is known', async () => {
const repo = join(tmpDir, 'codeburn-src')
await mkdir(join(repo, '.git'), { recursive: true })
await writeFile(join(repo, '.git', 'config'), '[remote "origin"]\n\turl = https://github.com/getagentseal/codeburn.git\n')
const dbPath = createHermesDb(tmpDir)
withTestDb(dbPath, (db) => {
insertSession(db, {
id: 'pr-filter',
gitRepoRoot: '/Users/a/Documents/Codeburn',
gitRepoRoot: repo,
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 0,
@ -654,10 +657,13 @@ skipUnlessSqlite('hermes provider', () => {
'assistant',
[
'Opened https://github.com/getagentseal/codeburn/pull/1037',
'Also see https://github.com/other/haystack/pull/1',
'Also see https://github.com/evil/codeburn/pull/2',
'```',
'https://github.com/getagentseal/codeburn/pull/677',
'```',
'~~~',
'https://github.com/getagentseal/codeburn/pull/4',
'~~~',
].join('\n'),
1779549201,
)
@ -665,7 +671,30 @@ skipUnlessSqlite('hermes provider', () => {
const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=pr-filter`)
expect(calls[0]?.prLinks).toEqual(['https://github.com/getagentseal/codeburn/pull/1037'])
expect(calls[0]?.project).toBe('Codeburn')
expect(calls[0]?.project).toBe('codeburn-src')
})
it('rejects a slash-UNC path as a workspace on POSIX', async () => {
const dbPath = createHermesDb(tmpDir)
withTestDb(dbPath, (db) => {
insertSession(db, {
id: 'unc-cwd',
source: 'desktop',
cwd: '//server/share/repo',
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
startedAt: 1779549200,
})
db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)')
.run('unc-cwd', 'user', 'hi', 1779549201)
})
const calls = await collectCalls(tmpDir, `${dbPath}#hermes-session=unc-cwd`)
expect(calls[0]?.project).toBe('hermes')
expect(calls[0]?.projectPath).toBeUndefined()
})
it('infers projects from Windows current working directory messages', async () => {