ruoxi_sun 58657935fc bundle fingerprint tool repos into evalstone for self-containment
Vendor LLMmap / llm-verify / llm-fingerprint-detector under
bash/fingerprint/tools so the three fingerprint benchmarks run with only
/data1/eval mounted (no /data1/xii dependency):
- run.py DEFAULT_TOOLS_ROOT prefers builtin tools/, falls back to /data1/xii
- exclude .git / node_modules / template backups
- detector dist/ (pre-built) retained; node_modules not needed at runtime
2026-09-03 06:45:46 +00:00

216 lines
7.0 KiB
TypeScript

/**
* Minimal HTTP layer over `POST {baseUrl}/chat/completions`:
* timeout + exponential backoff on 429/5xx/timeouts + AbortSignal
* pass-through + error classification. Uses the global `fetch`
* (Node ≥ 18 built-in, or any browser).
*
* The API key only ever appears in the Authorization header of the request
* to the endpoint under test; it is never logged or sent anywhere else.
*/
import { DEFAULT_MAX_RETRIES, DEFAULT_REQUEST_TIMEOUT_MS } from './constants.js'
import type { ResolvedEndpoint, SampleUsage } from './types.js'
export type ProbeErrorKind = 'network' | 'auth' | 'http' | 'timeout' | 'aborted'
export class ProbeRequestError extends Error {
readonly kind: ProbeErrorKind
readonly status: number | null
constructor(kind: ProbeErrorKind, message: string, status: number | null = null) {
super(message)
this.name = 'ProbeRequestError'
this.kind = kind
this.status = status
}
}
export interface ChatCompletionResult {
content: string
usage: SampleUsage | null
latencyMs: number
status: number
}
interface ChatCompletionUsagePayload {
prompt_tokens?: number
completion_tokens?: number
reasoning_tokens?: number
completion_tokens_details?: { reasoning_tokens?: number }
}
function parseUsage(payload: ChatCompletionUsagePayload | undefined | null): SampleUsage | null {
if (!payload) return null
return {
promptTokens: typeof payload.prompt_tokens === 'number' ? payload.prompt_tokens : null,
completionTokens:
typeof payload.completion_tokens === 'number' ? payload.completion_tokens : null,
reasoningTokens:
typeof payload.completion_tokens_details?.reasoning_tokens === 'number'
? payload.completion_tokens_details.reasoning_tokens
: typeof payload.reasoning_tokens === 'number'
? payload.reasoning_tokens
: null,
}
}
/** `content` is a string in the OpenAI schema, but some gateways send part arrays. */
function extractContent(message: unknown): string {
if (!message || typeof message !== 'object') return ''
const content = (message as { content?: unknown }).content
if (typeof content === 'string') return content
if (Array.isArray(content)) {
return content
.map((part) =>
typeof part === 'string'
? part
: typeof (part as { text?: unknown })?.text === 'string'
? (part as { text: string }).text
: '',
)
.join('')
}
return ''
}
export interface ChatCompletionRequest {
endpoint: ResolvedEndpoint
systemPrompt: string
userPrompt: string
temperature: number
maxTokens: number
extraBody: Record<string, unknown>
signal?: AbortSignal
timeoutMs?: number
retries?: number
}
async function delay(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
signal?.removeEventListener('abort', onAbort)
resolve()
}, ms)
const onAbort = () => {
clearTimeout(timer)
reject(new ProbeRequestError('aborted', 'Aborted'))
}
signal?.addEventListener('abort', onAbort, { once: true })
})
}
/**
* One chat/completions call with retries. Retried: 429, 5xx, timeouts.
* Not retried: transport errors and 401/403 — those are classified and
* re-thrown for the caller to decide.
*/
export async function fetchChatCompletion(
request: ChatCompletionRequest,
): Promise<ChatCompletionResult> {
const retries = request.retries ?? DEFAULT_MAX_RETRIES
const timeoutMs = request.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
let lastError: ProbeRequestError | null = null
for (let attempt = 0; attempt <= retries; attempt++) {
if (request.signal?.aborted) throw new ProbeRequestError('aborted', 'Aborted')
const timeoutController = new AbortController()
const timer = setTimeout(() => timeoutController.abort(), timeoutMs)
const onOuterAbort = () => timeoutController.abort()
request.signal?.addEventListener('abort', onOuterAbort, { once: true })
const startedAt = performance.now()
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...request.endpoint.headers,
}
if (request.endpoint.apiKey) {
headers.Authorization = `Bearer ${request.endpoint.apiKey}`
}
const response = await fetch(`${request.endpoint.baseUrl}/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify({
model: request.endpoint.model,
temperature: request.temperature,
max_tokens: request.maxTokens,
stream: false,
messages: [
{ role: 'system', content: request.systemPrompt },
{ role: 'user', content: request.userPrompt },
],
...request.extraBody,
}),
signal: timeoutController.signal,
})
const latencyMs = performance.now() - startedAt
if (response.status === 401 || response.status === 403) {
throw new ProbeRequestError(
'auth',
`HTTP ${response.status} — API key rejected`,
response.status,
)
}
if (response.status === 429 || response.status >= 500) {
const retryAfterHeader = response.headers.get('retry-after')
const retryAfterMs = retryAfterHeader ? Number(retryAfterHeader) * 1000 : NaN
lastError = new ProbeRequestError('http', `HTTP ${response.status}`, response.status)
if (attempt < retries) {
const backoff = Number.isFinite(retryAfterMs)
? retryAfterMs
: 800 * 2 ** attempt + Math.random() * 400
await delay(backoff, request.signal)
continue
}
throw lastError
}
if (!response.ok) {
let detail = ''
try {
detail = (await response.text()).slice(0, 300)
} catch {
// Ignore body read failures.
}
throw new ProbeRequestError(
'http',
`HTTP ${response.status} ${detail}`.trim(),
response.status,
)
}
const payload = (await response.json()) as {
choices?: Array<{ message?: unknown }>
usage?: ChatCompletionUsagePayload
}
return {
content: extractContent(payload?.choices?.[0]?.message),
usage: parseUsage(payload?.usage),
latencyMs,
status: response.status,
}
} catch (error) {
if (error instanceof ProbeRequestError) throw error
if (request.signal?.aborted) throw new ProbeRequestError('aborted', 'Aborted')
if (timeoutController.signal.aborted) {
lastError = new ProbeRequestError('timeout', `Request timed out after ${timeoutMs}ms`)
} else {
lastError = new ProbeRequestError(
'network',
error instanceof Error ? error.message : 'Network error',
)
}
if (lastError.kind === 'timeout' && attempt < retries) {
continue
}
throw lastError
} finally {
clearTimeout(timer)
request.signal?.removeEventListener('abort', onOuterAbort)
}
}
throw lastError ?? new ProbeRequestError('network', 'Unknown error')
}