@clearcompass-ai/llm-spec
Version:
A Vercel AI SDK provider for the LARS agent-based backend.
233 lines (232 loc) • 10.1 kB
JavaScript
;
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;