mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 06:24:32 +00:00
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:
parent
91754c5f54
commit
fae4db0008
5 changed files with 97 additions and 14 deletions
|
|
@ -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',
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue