UNPKG

@clearcompass-ai/llm-spec

Version:

A Vercel AI SDK provider for the LARS agent-based backend.

233 lines (232 loc) 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LarsLanguageModel = void 0; const provider_1 = require("@ai-sdk/provider"); const provider_utils_1 = require("@ai-sdk/provider-utils"); const zod_1 = require("zod"); // Zod schema for robust runtime validation of backend events. const larsBackendStreamEventSchema = zod_1.z.object({ type: zod_1.z.enum([ 'start', 'metadata', 'reasoning-start', 'reasoning-delta', 'reasoning-end', 'text-start', 'text-delta', 'text-end', 'error', 'finish', 'done', ]), messageId: zod_1.z.string().optional(), id: zod_1.z.string().optional(), delta: zod_1.z.string().optional(), errorText: zod_1.z.string().optional(), finishReason: zod_1.z.string().optional(), usage: zod_1.z .object({ inputTokens: zod_1.z.number(), outputTokens: zod_1.z.number(), reasoningTokens: zod_1.z.number().optional(), totalTokens: zod_1.z.number(), }) .optional(), answer: zod_1.z.unknown().optional(), data: zod_1.z.record(zod_1.z.unknown()).optional(), }); class LarsLanguageModel { get provider() { return 'lars.chat'; } get supportedUrls() { return {}; } constructor(config) { this.specificationVersion = 'v2'; this.modelId = config.modelId; this.baseURL = config.baseURL ?? (typeof process !== 'undefined' ? process.env.LARS_API_BASE_URL : undefined) ?? 'https://api.clearcompass.so'; this.debug = config.debug ?? false; } log(...args) { if (this.debug) { console.log(`[LARS Provider Debug]:`, ...args); } } async doGenerate(options) { throw new provider_1.UnsupportedFunctionalityError({ functionality: 'doGenerate', }); } async doStream({ prompt, headers, abortSignal, providerOptions, }) { if (!headers?.['Authorization']) { throw new Error('Authentication error: Authorization header is missing. Please provide a valid bearer token.'); } for (const message of prompt) { if (Array.isArray(message.content)) { for (const part of message.content) { if (part.type !== 'text') { throw new provider_1.UnsupportedFunctionalityError({ functionality: `Input type '${part.type}' is not supported by this model.`, }); } } } } const url = `${this.baseURL}/api/v1/chat`; const threadId = providerOptions?.threadId; const initialContext = providerOptions?.initialContext; const requestBody = { model_id: this.modelId, messages: prompt.map(message => ({ role: message.role, content: Array.isArray(message.content) ? message.content .map(part => (part.type === 'text' ? part.text : '')) .join('') : message.content, })), thread_id: threadId, initial_context: initialContext, }; const response = await fetch(url, { method: 'POST', headers: (0, provider_utils_1.combineHeaders)({ 'Content-Type': 'application/json', Accept: 'text/event-stream', }, headers), body: JSON.stringify(requestBody), signal: abortSignal, }); if (!response.ok) { const errorText = await response.text(); try { const errorJson = JSON.parse(errorText); throw new Error(`API call failed with status ${response.status}: ${errorJson.detail || errorText}`); } catch (e) { throw new Error(`API call failed with status ${response.status} ${response.statusText}: ${errorText}`); } } if (response.body == null) { throw new Error('API response body is null.'); } const self = this; let buffer = ''; const stream = response.body .pipeThrough(new TextDecoderStream()) .pipeThrough(new TransformStream({ transform(chunk, controller) { self.log('Received raw chunk:', JSON.stringify(chunk)); buffer += chunk; while (true) { const separatorIndex = buffer.indexOf('\n\n'); if (separatorIndex === -1) { break; } const message = buffer.slice(0, separatorIndex); buffer = buffer.slice(separatorIndex + 2); if (message.trim() === '' || message.startsWith('data: [DONE]')) { continue; } const jsonString = message.replace(/^data: /, ''); try { const json = JSON.parse(jsonString); const validationResult = larsBackendStreamEventSchema.safeParse(json); if (!validationResult.success) { self.log('Skipping invalid backend event:', validationResult.error); continue; } const event = validationResult.data; self.log('Parsed and validated event:', event); switch (event.type) { case 'start': controller.enqueue({ type: 'stream-start', warnings: [] }); break; case 'metadata': // The generic 'data' part is not standard in the latest spec. // We log it but do not enqueue it to maintain compatibility. self.log('Received metadata event (will be available to UI via custom hooks):', event.data); break; case 'reasoning-start': controller.enqueue({ type: 'reasoning-start', id: event.id ?? 'reasoning-block', }); break; case 'reasoning-delta': if (event.delta) { controller.enqueue({ type: 'reasoning-delta', id: event.id ?? 'reasoning-block', delta: event.delta, }); } break; case 'reasoning-end': controller.enqueue({ type: 'reasoning-end', id: event.id ?? 'reasoning-block', }); break; case 'text-start': controller.enqueue({ type: 'text-start', id: event.id ?? 'text-block', }); break; case 'text-delta': if (event.delta) { controller.enqueue({ type: 'text-delta', id: event.id ?? 'text-block', delta: event.delta, }); } break; case 'text-end': controller.enqueue({ type: 'text-end', id: event.id ?? 'text-block', }); break; case 'error': controller.enqueue({ type: 'error', error: new Error(event.errorText ?? 'An unknown backend error occurred.'), }); break; case 'finish': controller.enqueue({ type: 'finish', finishReason: event.finishReason ?? 'stop', usage: { inputTokens: event.usage?.inputTokens ?? 0, outputTokens: event.usage?.outputTokens ?? 0, totalTokens: event.usage?.totalTokens ?? 0, reasoningTokens: event.usage?.reasoningTokens, }, }); break; case 'done': break; default: self.log(`Unknown stream event type received`); break; } } catch (error) { self.log('Failed to parse SSE message JSON:', jsonString, error); } } }, })); return { stream }; } } exports.LarsLanguageModel = LarsLanguageModel;