/** * LLM client for Pi API (Inflection AI) * Provides streaming and non-streaming completion functions */ const PI_API_URL = "https://api.pi.ai/v1/chat/completions"; const client = { apiKey: process.env.PI_API_KEY || "", }; export interface StreamOptions { onProgress?: (text: string) => void; signal?: AbortSignal; } export interface CompletionOptions { model?: string; maxTokens?: number; temperature?: number; } const DEFAULT_MODEL = "pi"; const DEFAULT_MAX_TOKENS = 4096; /** * Stream a response from Pi */ export async function streamResponse( prompt: string, options: StreamOptions & CompletionOptions = {} ): Promise { const { model = DEFAULT_MODEL, maxTokens = DEFAULT_MAX_TOKENS, temperature = 0, onProgress, signal, } = options; if (!client.apiKey) { throw new Error("PI_API_KEY is not set"); } const response = await fetch(PI_API_URL, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${client.apiKey}`, }, body: JSON.stringify({ model, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens, temperature, stream: true, }), signal, }); if (!response.ok) { const error = await response.text(); throw new Error(`Pi API error: ${response.status} ${error}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error("Failed to get response body reader"); } const decoder = new TextDecoder(); let fullResponse = ""; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split("\n"); for (const line of lines) { if (line.startsWith("data: ") && line !== "data: [DONE]") { try { const data = JSON.parse(line.slice(6)); const content = data.choices?.[0]?.delta?.content; if (content) { fullResponse += content; onProgress?.(content); } } catch { // Skip invalid JSON } } } } return fullResponse; } /** * Get a complete response from Pi (non-streaming) */ export async function complete( prompt: string, options: CompletionOptions = {} ): Promise { const { model = DEFAULT_MODEL, maxTokens = DEFAULT_MAX_TOKENS, temperature = 0, } = options; if (!client.apiKey) { throw new Error("PI_API_KEY is not set"); } const response = await fetch(PI_API_URL, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${client.apiKey}`, }, body: JSON.stringify({ model, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens, temperature, }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Pi API error: ${response.status} ${error}`); } const data = await response.json(); return data.choices?.[0]?.message?.content ?? ""; } /** * Check if API key is configured */ export function isConfigured(): boolean { return !!client.apiKey && client.apiKey.length > 0; } /** * Stream structured JSON response * Useful for agents that need to return structured data */ export async function streamStructuredResponse( prompt: string, schema: Record, options: StreamOptions & CompletionOptions = {} ): Promise { const structuredPrompt = `${prompt} Please respond with a JSON object that follows this structure: ${JSON.stringify(schema, null, 2)} Your response must be valid JSON only, with no additional text or explanation.`; const response = await streamResponse(structuredPrompt, options); // Try to parse as JSON try { return JSON.parse(response) as T; } catch (error) { // If the response isn't valid JSON, try to extract JSON from the response const jsonMatch = response.match(/\{[\s\S]*\}/); if (jsonMatch) { return JSON.parse(jsonMatch[0]) as T; } throw new Error("Failed to parse structured response as JSON"); } } /** * Complete with structured JSON response (non-streaming) */ export async function completeStructured( prompt: string, schema: Record, options: CompletionOptions = {} ): Promise { const structuredPrompt = `${prompt} Please respond with a JSON object that follows this structure: ${JSON.stringify(schema, null, 2)} Your response must be valid JSON only, with no additional text or explanation.`; const response = await complete(structuredPrompt, options); // Try to parse as JSON try { return JSON.parse(response) as T; } catch (error) { // If the response isn't valid JSON, try to extract JSON from the response const jsonMatch = response.match(/\{[\s\S]*\}/); if (jsonMatch) { return JSON.parse(jsonMatch[0]) as T; } throw new Error("Failed to parse structured response as JSON"); } } export default client;