dsh-llm-verifier 0.1.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/src/session.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
2
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
3
+ import type { Agent } from '@deepseek-ai/dsh-agent'
4
+ import type { VerifierImage } from './caller.ts'
5
+
6
+ export interface SessionExtractOptions {
7
+ fromSeq?: number
8
+ toSeq?: number
9
+ includeAssistantText?: boolean
10
+ redactPatterns?: readonly string[]
11
+ maxChars?: number
12
+ }
13
+
14
+ export interface SessionExtraction {
15
+ problem: string
16
+ trace: string
17
+ images: VerifierImage[]
18
+ sessionId: string
19
+ fromSeq: number
20
+ toSeq: number
21
+ omittedCharacters: number
22
+ }
23
+
24
+ function textOf(blocks: readonly ContentBlock[]): string {
25
+ const parts: string[] = []
26
+ for (const block of blocks) {
27
+ if (block.type === 'text') parts.push(block.text)
28
+ else if (block.type === 'reasoning') parts.push('[Reasoning] ' + block.text)
29
+ else if (block.type === 'tool-call') parts.push('[Tool Call] ' + block.name + ' ' + block.arguments)
30
+ else if (block.type === 'tool-result') parts.push('[Tool Result] ' + textOf(block.content))
31
+ }
32
+ return parts.join('\n')
33
+ }
34
+
35
+ function redact(text: string, patterns: readonly string[]): string {
36
+ let result = text
37
+ for (const pattern of patterns) {
38
+ let regex: RegExp
39
+ try { regex = new RegExp(pattern, 'giu') } catch { throw new Error('llm-verifier: invalid redact pattern: ' + pattern) }
40
+ result = result.replace(regex, '[REDACTED]')
41
+ }
42
+ return result
43
+ }
44
+
45
+ export async function extractSession(agent: Agent, loadImage: (ref: Extract<ContentBlock, { type: 'image' }>['attachment']) => Promise<VerifierImage>, options: SessionExtractOptions = {}): Promise<SessionExtraction> {
46
+ const all = agent.session.events as readonly SessionEvent[]
47
+ const from = options.fromSeq ?? 0
48
+ const to = options.toSeq ?? Number.MAX_SAFE_INTEGER
49
+ const events = all.filter(event => event.seq >= from && event.seq <= to)
50
+ const defaultPatterns = ['Bearer\s+[A-Za-z0-9._~+\/=-]+', '(?:api[_-]?key|token|password|secret)\s*[:=]\s*[^\s,;]+']
51
+ const patterns = [...defaultPatterns, ...(options.redactPatterns ?? [])]
52
+ let problem = ''
53
+ const trace: string[] = []
54
+ const images: VerifierImage[] = []
55
+ for (const event of events) {
56
+ if (event.type === 'user/message') {
57
+ if (event.data.source.kind !== 'user') continue
58
+ const text = textOf(event.data.content)
59
+ if (!problem && text.trim()) problem = text.trim()
60
+ for (const block of event.data.content) if (block.type === 'image') images.push(await loadImage(block.attachment))
61
+ trace.push('--- User seq ' + event.seq + ' ---\n' + text)
62
+ } else if (event.type === 'assistant/message' && options.includeAssistantText !== false) {
63
+ trace.push('--- Assistant turn ' + event.data.turn + ' step ' + event.data.step + ' ---\n' + textOf(event.data.message.content))
64
+ } else if (event.type === 'tool/call') {
65
+ trace.push('--- Tool Call turn ' + event.data.turn + ' step ' + event.data.step + ' ---\n[Command] ' + event.data.name + ' ' + event.data.arguments)
66
+ } else if (event.type === 'tool/result') {
67
+ trace.push('--- Tool Result turn ' + event.data.turn + ' step ' + event.data.step + ' ---\n[Output] ' + textOf(event.data.message.content))
68
+ }
69
+ }
70
+ const raw = redact(trace.join('\n\n'), patterns)
71
+ const maxChars = options.maxChars ?? 200000
72
+ const omittedCharacters = Math.max(0, raw.length - maxChars)
73
+ const bounded = omittedCharacters ? '[Earlier trace truncated: ' + omittedCharacters + ' characters omitted]\n' + raw.slice(-maxChars) : raw
74
+ return { problem: redact(problem, patterns), trace: bounded, images, sessionId: String(agent.id), fromSeq: events[0]?.seq ?? from, toSeq: events.at(-1)?.seq ?? from, omittedCharacters }
75
+ }
@@ -0,0 +1,97 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import { credentialRef } from '@deepseek-ai/dsh-credentials'
3
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
4
+ import type { CompletionLogprobs, TokenAlternative } from './core.ts'
5
+ import type { UsageStats, VerifierImage } from './caller.ts'
6
+
7
+ export interface TopLogprobRoute {
8
+ baseURL: string
9
+ apiKey?: string
10
+ headers?: Record<string, string>
11
+ deepSeekThinking: boolean
12
+ }
13
+ export interface TopLogprobCompletion extends CompletionLogprobs { usage: UsageStats; scoringMode: 'top-logprobs' }
14
+ export class TopLogprobsUnsupportedError extends Error { constructor(message: string) { super(message); this.name = 'TopLogprobsUnsupportedError' } }
15
+
16
+ function object(value: unknown): Record<string, unknown> | undefined { return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : undefined }
17
+ function text(value: unknown): string | undefined { return typeof value === 'string' && value.trim() ? value.trim() : undefined }
18
+ function endpoint(baseURL: string): string { return baseURL.replace(/\/+$/, '') + '/chat/completions' }
19
+ function dataUrl(image: VerifierImage): string { return 'data:' + image.mediaType + ';base64,' + Buffer.from(image.data.buffer, image.data.byteOffset, image.data.byteLength).toString('base64') }
20
+
21
+ async function credential(ctx: Context, name: string | undefined): Promise<string | undefined> {
22
+ if (!name) return undefined
23
+ const provider = ctx.get('credentials')
24
+ return (await provider?.resolve(credentialRef(name)))?.value
25
+ }
26
+
27
+ export async function resolveTopLogprobRoute(ctx: Context, provider: string): Promise<TopLogprobRoute | undefined> {
28
+ const settings = ctx.get('settings')
29
+ if (provider === 'deepseek-official') {
30
+ const value = settings ? object(settings.get(settingsNamespace('llm-deepseek'))) ?? {} : {}
31
+ const apiKeyEnv = text(value.apiKeyEnv) ?? 'DEEPSEEK_API_KEY'
32
+ const apiKey = await credential(ctx, apiKeyEnv)
33
+ if (!apiKey) return undefined
34
+ return { baseURL: text(value.baseURL) ?? 'https://api.deepseek.com', apiKey, deepSeekThinking: true }
35
+ }
36
+ if (!settings) return undefined
37
+ const root = object(settings.get(settingsNamespace('llm-pi-ai')))
38
+ const profiles = object(root?.providers)
39
+ const profile = object(profiles?.[provider])
40
+ // Only explicitly OpenAI-compatible profiles are safe to serialize directly.
41
+ // Other DSH adapters keep their private protocol and use the explicit-tag fallback.
42
+ if (!profile || profile.api !== 'openai-completions') return undefined
43
+ const baseURL = text(profile.baseURL)
44
+ if (!baseURL || !/^https:\/\//i.test(baseURL)) return undefined
45
+ const apiKey = await credential(ctx, text(profile.apiKeyEnv))
46
+ const rawHeaders = object(profile.headers)
47
+ const headers = rawHeaders === undefined ? undefined : Object.fromEntries(Object.entries(rawHeaders).filter((entry): entry is [string, string] => typeof entry[1] === 'string'))
48
+ return { baseURL, ...(apiKey ? { apiKey } : {}), ...(headers ? { headers } : {}), deepSeekThinking: false }
49
+ }
50
+
51
+ export async function callTopLogprobs(route: TopLogprobRoute, model: string, prompt: string, maxTokens: number, reasoningEffort: string | undefined, signal?: AbortSignal, images?: readonly VerifierImage[]): Promise<TopLogprobCompletion> {
52
+ const content: string | Record<string, unknown>[] = images?.length ? [{ type: 'text', text: prompt }, ...images.map(image => ({ type: 'image_url', image_url: { url: dataUrl(image) } }))] : prompt
53
+ const thinking = route.deepSeekThinking && reasoningEffort ? reasoningEffort === 'off' ? { thinking: { type: 'disabled' } } : { thinking: { type: 'enabled' }, reasoning_effort: reasoningEffort } : {}
54
+ const response = await fetch(endpoint(route.baseURL), {
55
+ method: 'POST', redirect: 'error', signal,
56
+ headers: { 'content-type': 'application/json', ...(route.apiKey ? { authorization: 'Bearer ' + route.apiKey } : {}), ...route.headers },
57
+ body: JSON.stringify({ model, messages: [{ role: 'user', content }], max_tokens: maxTokens, temperature: 1, logprobs: true, top_logprobs: 20, ...thinking }),
58
+ })
59
+ const raw = await response.text()
60
+ if (!response.ok) {
61
+ const excerpt = raw.slice(0, 1000)
62
+ if ([400, 404, 405, 415, 422].includes(response.status) && /logprob|top_logprobs|unsupported|unknown (?:field|parameter)|unrecognized (?:field|parameter)|not support/i.test(excerpt)) throw new TopLogprobsUnsupportedError('provider rejected top_logprobs: HTTP ' + response.status + ' ' + excerpt)
63
+ throw new Error('llm-verifier: top_logprobs request failed with HTTP ' + response.status + ': ' + excerpt)
64
+ }
65
+ let body: Record<string, unknown>
66
+ try { body = object(JSON.parse(raw)) ?? {} } catch { throw new Error('llm-verifier: top_logprobs endpoint returned invalid JSON') }
67
+ const choices = Array.isArray(body.choices) ? body.choices : []
68
+ const choice = object(choices[0])
69
+ const message = object(choice?.message)
70
+ const answer = typeof message?.content === 'string' ? message.content : ''
71
+ const logprobs = object(choice?.logprobs)
72
+ const rows = Array.isArray(logprobs?.content) ? logprobs.content : []
73
+ if (!rows.length) throw new TopLogprobsUnsupportedError('provider returned no token logprobs')
74
+ const tokens: string[] = []
75
+ const positions: TokenAlternative[][] = []
76
+ for (const rawRow of rows) {
77
+ const row = object(rawRow) ?? {}
78
+ const token = typeof row.token === 'string' ? row.token : ''
79
+ tokens.push(token)
80
+ const top = Array.isArray(row.top_logprobs) ? row.top_logprobs : []
81
+ const alternatives = top.flatMap(value => { const item = object(value); return item && typeof item.token === 'string' && typeof item.logprob === 'number' ? [{ token: item.token, logprob: item.logprob }] : [] })
82
+ if (!alternatives.length && typeof row.logprob === 'number') alternatives.push({ token, logprob: row.logprob })
83
+ positions.push(alternatives)
84
+ }
85
+ const rawUsage = object(body.usage) ?? {}
86
+ const promptDetails = object(rawUsage.prompt_tokens_details) ?? {}
87
+ const completionDetails = object(rawUsage.completion_tokens_details) ?? {}
88
+ const cached = Number(rawUsage.prompt_cache_hit_tokens ?? promptDetails.cached_tokens ?? 0) || 0
89
+ const input = Number(rawUsage.prompt_tokens ?? 0) || 0
90
+ return { text: answer, tokens, positions, scoringMode: 'top-logprobs', usage: { calls: 1, attempts: 1, retries: 0, inputTokens: Math.max(0, input - cached), cachedInputTokens: cached, outputTokens: Number(rawUsage.completion_tokens ?? 0) || 0, reasoningTokens: Number(completionDetails.reasoning_tokens ?? 0) || 0 } }
91
+ }
92
+
93
+ export class TopLogprobCapabilityCache {
94
+ private readonly unsupported = new Set<string>()
95
+ isUnsupported(provider: string, model: string): boolean { return this.unsupported.has(provider + '\0' + model) }
96
+ markUnsupported(provider: string, model: string): void { this.unsupported.add(provider + '\0' + model) }
97
+ }