| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 |
- import OpenAI from "openai";
- export interface LLMClientConfig {
- baseURL?: string;
- apiKey?: string;
- model: string;
- }
- export interface LLMResult {
- content: string;
- // Provider finish reason: "stop" (complete), "length" (truncated by
- // max_tokens — yields incomplete JSON), "content_filter", etc. The parse
- // stage uses this to retry truncated github-trending output.
- finishReason: string | null;
- }
- export class LLMClient {
- private client: OpenAI;
- private model: string;
- constructor(config: LLMClientConfig) {
- this.client = new OpenAI({
- baseURL: config.baseURL || process.env.OPENAI_BASE_URL || "https://api.openai.com/v1",
- apiKey: config.apiKey || process.env.OPENAI_API_KEY,
- });
- this.model = config.model;
- }
- async chat(systemPrompt: string, userMessage: string): Promise<LLMResult> {
- const response = await this.client.chat.completions.create({
- model: this.model,
- messages: [
- { role: "system", content: systemPrompt },
- { role: "user", content: userMessage },
- ],
- temperature: 0.3,
- response_format: { type: "json_object" },
- // github-trending with a large trending list (many repos × verbose
- // intro/narration) can exceed 8k output tokens and get truncated mid-JSON,
- // breaking parsing. 16384 gives headroom. (The gateway clamps rather than
- // errors on an oversized value, and the parse stage additionally retries
- // once with a conciseness nudge if finish_reason="length".)
- max_tokens: 16384,
- });
- const choice = response.choices[0];
- const content = choice?.message?.content;
- if (!content) throw new Error("LLM returned empty response");
- return { content, finishReason: choice?.finish_reason ?? null };
- }
- }
|