wulf-pulse/lib/services/llm/openrouter-call.ts
lorentz 1112a06afe feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
  resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
  receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
  xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
  link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
  panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
  admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00

126 lines
3.9 KiB
TypeScript

/**
* OpenRouter chat-completions caller.
*
* Talks to https://openrouter.ai/api/v1/chat/completions in OpenAI-compatible
* format. Used as the OpenRouter side of `callLLMStage` so the existing
* Anthropic call path stays untouched.
*
* JSON adherence: requests `response_format: { type: 'json_object' }`. DeepSeek
* supports this (the API tolerates it as a hint when not natively supported,
* per OpenAI-compat). The retry-on-parse-fail logic in call.ts is the safety
* net for stragglers.
*/
import type { TokenUsage } from './pricing';
import type { ModelId } from './models';
const OPENROUTER_BASE = 'https://openrouter.ai/api/v1';
export interface OpenRouterChatResponse {
text: string;
usage: TokenUsage;
}
interface RawChatResponse {
id: string;
model: string;
choices: Array<{
index: number;
message: { role: 'assistant'; content: string | null };
finish_reason: string;
}>;
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens?: number;
};
error?: { message: string; code?: number };
}
/**
* Send a chat-completions request to OpenRouter and return the assistant's
* text content + token usage. Throws on HTTP error or empty response.
*/
export async function callOpenRouterChat(opts: {
model: ModelId;
system: string;
history: Array<{ role: 'user' | 'assistant'; content: string }>;
maxTokens: number;
}): Promise<OpenRouterChatResponse> {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
throw new Error(
'OPENROUTER_API_KEY is not set. The OpenRouter pipeline cannot run without it.'
);
}
const messages = [
{ role: 'system' as const, content: opts.system },
...opts.history,
];
const body = {
model: opts.model,
messages,
max_tokens: opts.maxTokens,
response_format: { type: 'json_object' as const },
// Provider preferences:
// - data_collection: 'deny' → refuse any inference provider whose
// policy allows storing prompts/completions or training on them.
// - sort: 'throughput' → among compliant providers, prefer the
// fastest one. V4 Pro deep-analysis was ~3.5min without this hint;
// with throughput sort it should land closer to ~1.5-2min.
// - allow_fallbacks: true → still route across compliant providers
// when the primary is down (default, made explicit).
// OpenRouter publishes per-provider data policies; data_collection is
// the documented way to enforce a privacy floor at the API call level.
// The account-level "opt out of training" toggle is the belt; this is
// the braces.
provider: {
data_collection: 'deny' as const,
sort: 'throughput' as const,
allow_fallbacks: true,
},
};
const res = await fetch(`${OPENROUTER_BASE}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
// OpenRouter uses these for analytics + their leaderboard.
'HTTP-Referer':
process.env.BETTER_AUTH_URL || 'https://pulse.wulfconsulting.cloud',
'X-Title': 'Pulse Ticket Analyzer',
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(
`OpenRouter HTTP ${res.status}: ${text.slice(0, 500)}`
);
}
const json = (await res.json()) as RawChatResponse;
if (json.error) {
throw new Error(`OpenRouter error: ${json.error.message}`);
}
const choice = json.choices?.[0];
if (!choice) {
throw new Error('OpenRouter returned no choices');
}
const text = (choice.message.content ?? '').trim();
const usage: TokenUsage = {
input_tokens: json.usage?.prompt_tokens ?? 0,
output_tokens: json.usage?.completion_tokens ?? 0,
};
return { text, usage };
}
export function isOpenRouterConfigured(): boolean {
return !!process.env.OPENROUTER_API_KEY;
}