UNPKG

straico-custom-provider

Version:

Straico custom provider for Vercel AI SDK

301 lines (256 loc) 8.25 kB
/** * Simplified implementation of a Straico provider for the Vercel AI SDK * This doesn't rely on the @ai-sdk/provider package and is intended as a starting point */ // Define minimal environment interface declare const process: { env: Record<string, string | undefined>; }; export interface ContentPart { type: string; [key: string]: any; } export interface TextContentPart extends ContentPart { type: 'text'; text: string; } export function isTextContentPart(part: ContentPart): part is TextContentPart { return part.type === 'text' && typeof part.text === 'string'; } export type StraicoChatModelId = string; export interface StraicoChatSettings { maxTokens?: number; temperature?: number; fileUrls?: string[]; youtubeUrls?: string[]; } interface StraicoChatRequestArgs { models: string[]; message: string; file_urls?: string[]; youtube_urls?: string[]; max_tokens?: number; temperature?: number; } export interface StraicoChatModelOptions { provider: string; baseURL: string; headers: () => Record<string, string>; generateId: () => string; } export class StraicoChatLanguageModel { readonly specificationVersion = 'v1'; readonly provider: string; readonly modelId: string; readonly defaultObjectGenerationMode = 'json'; private readonly baseURL: string; private readonly getHeaders: () => Record<string, string>; private readonly generateId: () => string; private readonly defaultSettings: StraicoChatSettings; constructor( modelId: StraicoChatModelId, settings: StraicoChatSettings, options: StraicoChatModelOptions ) { this.modelId = modelId; this.provider = options.provider; this.baseURL = options.baseURL; this.getHeaders = options.headers; this.generateId = options.generateId; this.defaultSettings = settings; } async doGenerate( parts: ContentPart[], options: any = {} ): Promise<ContentPart[]> { const { signal } = options; const args = this.getArgs(parts, options); try { const response = await fetch( `${this.baseURL}/prompt/completion`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(args), signal, } ); if (!response.ok) { throw new Error(`Straico API returned an error: ${response.statusText}`); } const data = await response.json(); if (!data.success) { throw new Error('Straico API returned an unsuccessful response'); } // Extract the model completion for the specified modelId const modelResult = data.data.completions[this.modelId]; if (!modelResult) { throw new Error(`No completion found for model ${this.modelId}`); } // Extract the content from the completion const content = modelResult.completion.choices[0].message.content; return [{ type: 'text', text: content }]; } catch (error) { if (error instanceof Error) { throw error; } throw new Error(`Error calling Straico API: ${String(error)}`); } } async doStream( parts: ContentPart[], options: any = {} ): Promise<AsyncIterable<ContentPart>> { // Straico API doesn't support streaming natively // We'll simulate streaming by breaking up the complete response const completion = await this.doGenerate(parts, options); return this.simulateStream(completion); } private async *simulateStream( completionParts: ContentPart[] ): AsyncIterable<ContentPart> { for (const part of completionParts) { if (isTextContentPart(part)) { // Simulate streaming by yielding character by character // In a real implementation, you might want to yield chunks instead const text = part.text; let currentText = ''; for (const char of text) { currentText += char; yield { type: 'text', text: currentText }; // Small delay to simulate streaming await new Promise(resolve => setTimeout(resolve, 10)); } } else { yield part; } } } private getArgs(parts: ContentPart[], options: any): StraicoChatRequestArgs { // Convert Vercel AI SDK format to Straico API format const mergedSettings = { ...this.defaultSettings, ...options }; // Extract the message from content parts const message = parts .filter(isTextContentPart) .map(part => part.text) .join('\n'); // Basic Straico API request structure const args: StraicoChatRequestArgs = { models: [this.modelId], message: message }; // Add optional parameters if they exist if (mergedSettings.fileUrls && mergedSettings.fileUrls.length > 0) { args.file_urls = mergedSettings.fileUrls; } if (mergedSettings.youtubeUrls && mergedSettings.youtubeUrls.length > 0) { args.youtube_urls = mergedSettings.youtubeUrls; } if (mergedSettings.maxTokens) { args.max_tokens = mergedSettings.maxTokens; } if (mergedSettings.temperature !== undefined) { args.temperature = mergedSettings.temperature; } return args; } } // Helper function to load API key from options or environment export function loadApiKey(options: { apiKey?: string; environmentVariableName: string; description: string; }): string { const { apiKey, environmentVariableName, description } = options; if (apiKey) { return apiKey; } // For browser environments, we only use provided apiKey if (typeof window !== 'undefined') { throw new Error( `API key for ${description} not found. Please provide an apiKey option.` ); } // For Node.js environments try { // @ts-ignore - Accessing process.env const envApiKey = process.env[environmentVariableName]; if (!envApiKey) { throw new Error(); } return envApiKey; } catch (e) { throw new Error( `API key for ${description} not found. Either provide an apiKey option or set the ${environmentVariableName} environment variable.` ); } } // Helper function to generate a unique ID export function generateId(): string { return `gen-${Math.random().toString(36).substring(2, 12)}`; } // Helper function to ensure URLs don't have trailing slashes export function withoutTrailingSlash(url?: string): string | undefined { if (!url) return undefined; return url.endsWith('/') ? url.slice(0, -1) : url; } // Model factory function with additional methods and properties export interface StraicoProvider { ( modelId: StraicoChatModelId, settings?: StraicoChatSettings, ): StraicoChatLanguageModel; // Explicit method for targeting specific API chat( modelId: StraicoChatModelId, settings?: StraicoChatSettings, ): StraicoChatLanguageModel; } // Optional settings for the provider export interface StraicoProviderSettings { baseURL?: string; apiKey?: string; headers?: Record<string, string>; generateId?: () => string; } // Provider factory function export function createStraicioProvider( options: StraicoProviderSettings = {}, ): StraicoProvider { const createModel = ( modelId: StraicoChatModelId, settings: StraicoChatSettings = {}, ) => new StraicoChatLanguageModel(modelId, settings, { provider: 'straico.chat', baseURL: withoutTrailingSlash(options.baseURL) ?? 'https://api.straico.com/v1', headers: () => ({ Authorization: `Bearer ${loadApiKey({ apiKey: options.apiKey, environmentVariableName: 'STRAICO_API_KEY', description: 'Straico Provider', })}`, 'Content-Type': 'application/json', ...options.headers, }), generateId: options.generateId ?? generateId, }); const provider = function ( modelId: StraicoChatModelId, settings?: StraicoChatSettings, ) { if (new.target) { throw new Error( 'The model factory function cannot be called with the new keyword.', ); } return createModel(modelId, settings); }; provider.chat = createModel; return provider; } /** * Default Straico provider instance. */ export const straico = createStraicioProvider();