import { withSpendGuard, type OpenAIBindingOptions, type SpendGuardConfig } from '../spend-guard'; import type { CapabilityTier, SpendPolicy, SpendScope } from '../types'; import { createFrameworkGuard, preflightFrameworkCall, settleFrameworkCall, type FrameworkAdapterOptions } from './common'; export interface OpenRouterBindingOptions { policy: SpendPolicy; scope: SpendScope; capabilityClaim?: CapabilityTier; config?: Omit; licenseKey?: string; openRouterBaseUrl?: string; } export function withSpendGuardOpenRouter(client: TClient, opts: OpenRouterBindingOptions): TClient { const config = { ...(opts.config ?? {}), openRouterBaseUrl: opts.openRouterBaseUrl ?? opts.config?.openRouterBaseUrl ?? 'https://openrouter.ai/api/v1', providerRoute: opts.config?.providerRoute ?? 'openrouter', } as Omit; return withSpendGuard(client, { policy: opts.policy, scope: opts.scope, capabilityClaim: opts.capabilityClaim, licenseKey: opts.licenseKey, config, } satisfies OpenAIBindingOptions) as TClient; } export function withOpenRouterSpendGuard(client: TClient, opts: OpenRouterBindingOptions): TClient { return withSpendGuardOpenRouter(client, opts); } export interface OpenRouterFetchOptions extends FrameworkAdapterOptions { fetchImpl?: (url: string, init?: Record) => Promise; endpoint?: string; } export function createOpenRouterFetch(opts: OpenRouterFetchOptions): (url: string, init?: Record) => Promise { const guard = createFrameworkGuard({ ...opts, defaultModel: opts.defaultModel ?? 'openrouter/auto', framework: 'openrouter' }); const fetchImpl = opts.fetchImpl ?? (globalThis as unknown as { fetch?: (url: string, init?: Record) => Promise }).fetch; if (!fetchImpl) throw new Error('createOpenRouterFetch: fetch is not available'); const endpoint = opts.endpoint ?? 'https://openrouter.ai/api/v1/chat/completions'; return async (url: string, init: Record = {}) => { const target = String(url); if (!target.startsWith(endpoint)) return fetchImpl(target, init); const body = parseJsonBody(init.body); const preflight = await preflightFrameworkCall(guard, opts, { framework: 'openrouter', model: typeof body.model === 'string' ? body.model : opts.defaultModel ?? 'openrouter/auto', params: body, metadata: metadataRecord(body.metadata), requestShape: { endpoint: '/api/v1/chat/completions', stream: body.stream === true, maxTokens: typeof body.max_tokens === 'number' ? body.max_tokens : undefined, toolCount: Array.isArray(body.tools) ? body.tools.length : 0, }, }); const nextBody = preflight.decision.action === 'downgrade' && preflight.decision.modelResolved !== preflight.decision.modelRequested ? { ...body, model: preflight.decision.modelResolved } : body; const response = await fetchImpl(target, { ...init, body: JSON.stringify(nextBody) }); await settleFrameworkCall(preflight, await responseJson(response)); return response; }; } function parseJsonBody(body: unknown): Record { if (typeof body === 'string') { try { return JSON.parse(body) as Record; } catch { return {}; } } if (body && typeof body === 'object' && !Array.isArray(body)) return body as Record; return {}; } function metadataRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } async function responseJson(response: unknown): Promise { const maybe = response as { clone?: () => { json?: () => Promise }; json?: () => Promise }; try { if (typeof maybe.clone === 'function') return await maybe.clone().json?.(); if (typeof maybe.json === 'function') return await maybe.json(); } catch { return null; } return null; }