CoDEVX / lib /llm-client.ts
CodexMacTiger
feat: live package-scoped chat and thinking logs
837e3ac
import {
isAnthropicCompatibleBaseUrl,
normalizeLlmConfig,
type LlmConfig,
} from "./llm-config";
import { compactTracePreview } from "./chat-trace";
export type LlmCallResult = {
text: string;
endpoint: string;
requestPreview: string;
responsePreview: string;
};
function extractJsonText(value: unknown): string | undefined {
if (typeof value === "string") return value;
if (Array.isArray(value)) {
const joined = value
.map((item) => {
if (typeof item === "string") return item;
if (
item &&
typeof item === "object" &&
"type" in item &&
"text" in item &&
item.type === "text" &&
typeof item.text === "string"
) {
return item.text;
}
return "";
})
.filter(Boolean)
.join("\n");
return joined || undefined;
}
return undefined;
}
function extractOpenAiText(payload: unknown): string | undefined {
const data = payload as
| {
choices?: Array<{
message?: { content?: unknown };
text?: unknown;
}>;
}
| undefined;
const choice = data?.choices?.[0];
return extractJsonText(choice?.message?.content) ?? extractJsonText(choice?.text);
}
function extractAnthropicText(payload: unknown): string | undefined {
const data = payload as
| {
content?: Array<{
type?: string;
text?: string;
}>;
}
| undefined;
return data?.content
?.filter((item) => item.type === "text" && typeof item.text === "string")
.map((item) => item.text)
.join("\n");
}
export async function generateLlmText(args: {
config: LlmConfig;
systemPrompt: string;
userPrompt: string;
temperature?: number;
maxTokens?: number;
responseFormat?: "json_object";
}): Promise<LlmCallResult> {
const { systemPrompt, userPrompt } = args;
const config = normalizeLlmConfig(args.config);
const temperature = args.temperature ?? 0.2;
const maxTokens = args.maxTokens ?? 2200;
const anthropicStyle = isAnthropicCompatibleBaseUrl(config.baseUrl);
const responseFormat = args.responseFormat;
const endpoint = anthropicStyle
? `${config.baseUrl}/v1/messages`
: `${config.baseUrl}/chat/completions`;
const response = await fetch(endpoint, {
method: "POST",
headers: anthropicStyle
? {
"content-type": "application/json",
"x-api-key": config.apiKey,
"anthropic-version": "2023-06-01",
}
: {
"content-type": "application/json",
authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify(
anthropicStyle
? {
model: config.model,
max_tokens: maxTokens,
temperature,
system: systemPrompt,
messages: [
{
role: "user",
content: [
{
type: "text",
text: userPrompt,
},
],
},
],
}
: {
model: config.model,
temperature,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
...(responseFormat
? { response_format: { type: responseFormat } }
: {}),
},
),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`LLM request failed (${response.status}): ${text}`.slice(0, 1200));
}
const rawText = await response.text();
let payload: unknown = null;
if (rawText.trim()) {
try {
payload = JSON.parse(rawText);
} catch {
payload = null;
}
}
const text = anthropicStyle
? extractAnthropicText(payload)
: extractOpenAiText(payload) ?? rawText.trim();
if (!text) {
throw new Error("LLM returned an empty response.");
}
return {
text,
endpoint,
requestPreview: compactTracePreview(
`SYSTEM:\n${systemPrompt}\n\nUSER:\n${userPrompt}`,
),
responsePreview: compactTracePreview(text),
};
}