Explorar o código

feat(llm): support custom HTTP headers via INKOS_LLM_HEADERS env var

Some API providers (e.g. openclaudecode.cn) require a specific
User-Agent header for external clients.  Without it requests get 403.

Adds INKOS_LLM_HEADERS (JSON object) which is injected as
defaultHeaders into the OpenAI SDK client.  No effect when unset.
Also adds an optional `headers` field to LLMConfigSchema.
Ma hai 7 meses
pai
achega
aeb64b658c
Modificáronse 2 ficheiros con 25 adicións e 1 borrados
  1. 24 1
      packages/core/src/llm/provider.ts
  2. 1 0
      packages/core/src/models/project.ts

+ 24 - 1
packages/core/src/llm/provider.ts

@@ -135,15 +135,38 @@ export function createLLMClient(config: LLMConfig): LLMClient {
     };
   }
   // openai or custom — both use OpenAI SDK
+  const extraHeaders = config.headers ?? parseEnvHeaders();
   return {
     provider: "openai",
     apiFormat,
     stream,
-    _openai: new OpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl }),
+    _openai: new OpenAI({
+      apiKey: config.apiKey,
+      baseURL: config.baseUrl,
+      ...(extraHeaders ? { defaultHeaders: extraHeaders } : {}),
+    }),
     defaults,
   };
 }
 
+function parseEnvHeaders(): Record<string, string> | undefined {
+  const raw = process.env.INKOS_LLM_HEADERS;
+  if (!raw) return undefined;
+  try {
+    const parsed: unknown = JSON.parse(raw);
+    if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
+      return parsed as Record<string, string>;
+    }
+  } catch {
+    // not JSON — treat as single "Key: Value" pair
+    const idx = raw.indexOf(":");
+    if (idx > 0) {
+      return { [raw.slice(0, idx).trim()]: raw.slice(idx + 1).trim() };
+    }
+  }
+  return undefined;
+}
+
 // === Partial Response (stream interrupted but usable content received) ===
 
 export class PartialResponseError extends Error {

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

@@ -9,6 +9,7 @@ export const LLMConfigSchema = z.object({
   maxTokens: z.number().int().min(1).default(8192),
   thinkingBudget: z.number().int().min(0).default(0),
   extra: z.record(z.unknown()).optional(),
+  headers: z.record(z.string()).optional(),
   apiFormat: z.enum(["chat", "responses"]).default("chat"),
   stream: z.boolean().default(true),
 });