- 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>
126 lines
3.9 KiB
TypeScript
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;
|
|
}
|