Просмотр исходного кода

feat: support OpenAI reasoning_effort parameter (#57)

Ma 7 месяцев назад
Родитель
Сommit
2ea499fed8

+ 1 - 1
packages/cli/src/commands/config.ts

@@ -23,7 +23,7 @@ configCommand
 
       const KNOWN_KEYS = new Set([
         "llm.provider", "llm.baseUrl", "llm.model", "llm.temperature",
-        "llm.maxTokens", "llm.thinkingBudget", "llm.apiFormat", "llm.stream",
+        "llm.maxTokens", "llm.thinkingBudget", "llm.reasoningEffort", "llm.apiFormat", "llm.stream",
         "daemon.schedule.radarCron", "daemon.schedule.writeCron",
         "daemon.maxConcurrentBooks", "daemon.chaptersPerCycle",
         "daemon.retryDelayMs", "daemon.cooldownAfterChapterMs",

+ 17 - 10
packages/core/src/llm/provider.ts

@@ -79,6 +79,7 @@ export interface LLMClient {
     readonly temperature: number;
     readonly maxTokens: number;
     readonly thinkingBudget: number;
+    readonly reasoningEffort?: "low" | "medium" | "high";
   };
 }
 
@@ -114,6 +115,7 @@ export function createLLMClient(config: LLMConfig): LLMClient {
     temperature: config.temperature ?? 0.7,
     maxTokens: config.maxTokens ?? 8192,
     thinkingBudget: config.thinkingBudget ?? 0,
+    reasoningEffort: config.reasoningEffort,
   };
 
   const apiFormat = config.apiFormat ?? "chat";
@@ -218,6 +220,7 @@ export async function chatCompletion(
   const resolved = {
     temperature: options?.temperature ?? client.defaults.temperature,
     maxTokens: options?.maxTokens ?? client.defaults.maxTokens,
+    reasoningEffort: client.defaults.reasoningEffort,
   };
   const onStreamProgress = options?.onStreamProgress;
   const errorCtx = { baseUrl: client._openai?.baseURL ?? "(anthropic)", model };
@@ -321,21 +324,22 @@ async function chatCompletionOpenAIChat(
   client: OpenAI,
   model: string,
   messages: ReadonlyArray<LLMMessage>,
-  options: { readonly temperature: number; readonly maxTokens: number },
+  options: { readonly temperature: number; readonly maxTokens: number; readonly reasoningEffort?: "low" | "medium" | "high" },
   webSearch?: boolean,
   onStreamProgress?: OnStreamProgress,
 ): Promise<LLMResponse> {
-  const stream = await client.chat.completions.create({
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const createParams: any = {
     model,
-    messages: messages.map((m) => ({
-      role: m.role,
-      content: m.content,
-    })),
+    messages: messages.map((m) => ({ role: m.role, content: m.content })),
     temperature: options.temperature,
     max_tokens: options.maxTokens,
     stream: true,
     ...(webSearch ? { web_search_options: { search_context_size: "medium" as const } } : {}),
-  });
+    ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
+  };
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const stream = await client.chat.completions.create(createParams) as any;
 
   const chunks: string[] = [];
   let inputTokens = 0;
@@ -382,16 +386,19 @@ async function chatCompletionOpenAIChatSync(
   client: OpenAI,
   model: string,
   messages: ReadonlyArray<LLMMessage>,
-  options: { readonly temperature: number; readonly maxTokens: number },
+  options: { readonly temperature: number; readonly maxTokens: number; readonly reasoningEffort?: "low" | "medium" | "high" },
   _webSearch?: boolean,
 ): Promise<LLMResponse> {
-  const response = await client.chat.completions.create({
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const syncParams: any = {
     model,
     messages: messages.map((m) => ({ role: m.role, content: m.content })),
     temperature: options.temperature,
     max_tokens: options.maxTokens,
     stream: false,
-  });
+    ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
+  };
+  const response = await client.chat.completions.create(syncParams);
 
   const content = response.choices[0]?.message?.content ?? "";
   if (!content) throw new Error("LLM returned empty response");

+ 1 - 0
packages/core/src/models/project.ts

@@ -8,6 +8,7 @@ export const LLMConfigSchema = z.object({
   temperature: z.number().min(0).max(2).default(0.7),
   maxTokens: z.number().int().min(1).default(8192),
   thinkingBudget: z.number().int().min(0).default(0),
+  reasoningEffort: z.enum(["low", "medium", "high"]).optional(),
   apiFormat: z.enum(["chat", "responses"]).default("chat"),
   stream: z.boolean().default(true),
 });

+ 1 - 0
packages/core/src/utils/config-loader.ts

@@ -44,6 +44,7 @@ export async function loadProjectConfig(root: string): Promise<ProjectConfig> {
   if (env.INKOS_LLM_TEMPERATURE) llm.temperature = parseFloat(env.INKOS_LLM_TEMPERATURE);
   if (env.INKOS_LLM_MAX_TOKENS) llm.maxTokens = parseInt(env.INKOS_LLM_MAX_TOKENS, 10);
   if (env.INKOS_LLM_THINKING_BUDGET) llm.thinkingBudget = parseInt(env.INKOS_LLM_THINKING_BUDGET, 10);
+  if (env.INKOS_LLM_REASONING_EFFORT) llm.reasoningEffort = env.INKOS_LLM_REASONING_EFFORT;
   if (env.INKOS_LLM_API_FORMAT) llm.apiFormat = env.INKOS_LLM_API_FORMAT;
   config.llm = llm;