client.ts 1.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
  1. import OpenAI from "openai";
  2. export interface LLMClientConfig {
  3. baseURL?: string;
  4. apiKey?: string;
  5. model: string;
  6. }
  7. export interface LLMResult {
  8. content: string;
  9. // Provider finish reason: "stop" (complete), "length" (truncated by
  10. // max_tokens — yields incomplete JSON), "content_filter", etc. The parse
  11. // stage uses this to retry truncated github-trending output.
  12. finishReason: string | null;
  13. }
  14. export class LLMClient {
  15. private client: OpenAI;
  16. private model: string;
  17. constructor(config: LLMClientConfig) {
  18. this.client = new OpenAI({
  19. baseURL: config.baseURL || process.env.OPENAI_BASE_URL || "https://api.openai.com/v1",
  20. apiKey: config.apiKey || process.env.OPENAI_API_KEY,
  21. });
  22. this.model = config.model;
  23. }
  24. async chat(systemPrompt: string, userMessage: string): Promise<LLMResult> {
  25. const response = await this.client.chat.completions.create({
  26. model: this.model,
  27. messages: [
  28. { role: "system", content: systemPrompt },
  29. { role: "user", content: userMessage },
  30. ],
  31. temperature: 0.3,
  32. response_format: { type: "json_object" },
  33. // github-trending with a large trending list (many repos × verbose
  34. // intro/narration) can exceed 8k output tokens and get truncated mid-JSON,
  35. // breaking parsing. 16384 gives headroom. (The gateway clamps rather than
  36. // errors on an oversized value, and the parse stage additionally retries
  37. // once with a conciseness nudge if finish_reason="length".)
  38. max_tokens: 16384,
  39. });
  40. const choice = response.choices[0];
  41. const content = choice?.message?.content;
  42. if (!content) throw new Error("LLM returned empty response");
  43. return { content, finishReason: choice?.finish_reason ?? null };
  44. }
  45. }