llm-cli-gateway 2.11.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,43 +0,0 @@
1
- import type { Logger } from "./logger.js";
2
- export type XaiResponsesRole = "system" | "user" | "assistant";
3
- export type XaiReasoningEffort = "none" | "low" | "medium" | "high";
4
- export interface XaiResponsesInputMessage {
5
- role: XaiResponsesRole;
6
- content: string;
7
- }
8
- export interface XaiResponsesRequest {
9
- baseUrl: string;
10
- apiKey: string;
11
- model: string;
12
- input: string | XaiResponsesInputMessage[];
13
- instructions?: string;
14
- previousResponseId?: string;
15
- maxOutputTokens?: number;
16
- temperature?: number;
17
- topP?: number;
18
- reasoningEffort?: XaiReasoningEffort;
19
- timeoutMs?: number;
20
- }
21
- export interface XaiResponsesUsage {
22
- inputTokens?: number;
23
- outputTokens?: number;
24
- cacheReadTokens?: number;
25
- costUsd?: number;
26
- raw?: unknown;
27
- }
28
- export interface XaiResponsesResult {
29
- responseId: string | null;
30
- model: string;
31
- status: string | null;
32
- text: string;
33
- usage: XaiResponsesUsage;
34
- raw: unknown;
35
- httpStatus: number;
36
- }
37
- export declare class XaiApiError extends Error {
38
- readonly status: number | null;
39
- readonly responseText: string;
40
- readonly code?: string | undefined;
41
- constructor(message: string, status?: number | null, responseText?: string, code?: string | undefined);
42
- }
43
- export declare function createXaiResponse(params: XaiResponsesRequest, logger?: Logger): Promise<XaiResponsesResult>;
@@ -1,191 +0,0 @@
1
- import { request as httpRequest } from "node:http";
2
- import { request as httpsRequest } from "node:https";
3
- import { URL } from "node:url";
4
- import { createCircuitBreaker, withRetry } from "./retry.js";
5
- import { logWarn, noopLogger } from "./logger.js";
6
- const MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
7
- const DEFAULT_TIMEOUT_MS = 600_000;
8
- export class XaiApiError extends Error {
9
- status;
10
- responseText;
11
- code;
12
- constructor(message, status = null, responseText = "", code) {
13
- super(message);
14
- this.status = status;
15
- this.responseText = responseText;
16
- this.code = code;
17
- this.name = "XaiApiError";
18
- }
19
- }
20
- let xaiCircuitBreaker = null;
21
- function getXaiCircuitBreaker(logger) {
22
- xaiCircuitBreaker ??= createCircuitBreaker({
23
- failureThreshold: 3,
24
- resetTimeout: 60_000,
25
- onStateChange: state => logWarn(logger, `[xai-api] circuit breaker state changed to ${state}`),
26
- });
27
- return xaiCircuitBreaker;
28
- }
29
- function isHttpTransient(error) {
30
- const status = typeof error?.status === "number" ? error.status : null;
31
- if (status === 429 || (status !== null && status >= 500))
32
- return true;
33
- return ["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "EPIPE"].includes(String(error?.code ?? ""));
34
- }
35
- function responsesUrl(baseUrl) {
36
- const trimmed = baseUrl.replace(/\/+$/, "");
37
- const url = new URL(`${trimmed}/responses`);
38
- if (url.protocol !== "https:" &&
39
- !(url.protocol === "http:" && ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname))) {
40
- throw new XaiApiError("xAI API baseUrl must use https unless it targets localhost/loopback");
41
- }
42
- return url;
43
- }
44
- function extractErrorMessage(status, body) {
45
- if (!body)
46
- return `xAI API request failed with HTTP ${status}`;
47
- try {
48
- const parsed = JSON.parse(body);
49
- const message = parsed?.error?.message ?? parsed?.message ?? parsed?.error;
50
- if (typeof message === "string" && message.length > 0) {
51
- return `xAI API request failed with HTTP ${status}: ${message}`;
52
- }
53
- }
54
- catch {
55
- }
56
- return `xAI API request failed with HTTP ${status}: ${body.slice(0, 1000)}`;
57
- }
58
- function normalizeCostUsd(usage) {
59
- const ticks = usage?.cost_in_usd_ticks;
60
- if (typeof ticks === "number" && Number.isFinite(ticks))
61
- return ticks / 10_000_000_000;
62
- const nanos = usage?.cost_in_nano_usd;
63
- if (typeof nanos === "number" && Number.isFinite(nanos))
64
- return nanos / 1_000_000_000;
65
- return undefined;
66
- }
67
- function extractResponseText(parsed) {
68
- const output = Array.isArray(parsed?.output) ? parsed.output : [];
69
- const chunks = [];
70
- for (const item of output) {
71
- if (item?.type !== "message" || !Array.isArray(item.content))
72
- continue;
73
- for (const content of item.content) {
74
- if ((content?.type === "output_text" || content?.type === "text") &&
75
- typeof content.text === "string") {
76
- chunks.push(content.text);
77
- }
78
- }
79
- }
80
- if (chunks.length > 0)
81
- return chunks.join("");
82
- if (typeof parsed?.output_text === "string")
83
- return parsed.output_text;
84
- return "";
85
- }
86
- function parseResponsesResult(status, body) {
87
- const parsed = JSON.parse(body);
88
- const usage = parsed?.usage ?? {};
89
- return {
90
- responseId: typeof parsed?.id === "string" ? parsed.id : null,
91
- model: typeof parsed?.model === "string" ? parsed.model : "unknown",
92
- status: typeof parsed?.status === "string" ? parsed.status : null,
93
- text: extractResponseText(parsed),
94
- usage: {
95
- inputTokens: typeof usage.input_tokens === "number"
96
- ? usage.input_tokens
97
- : typeof usage.prompt_tokens === "number"
98
- ? usage.prompt_tokens
99
- : undefined,
100
- outputTokens: typeof usage.output_tokens === "number"
101
- ? usage.output_tokens
102
- : typeof usage.completion_tokens === "number"
103
- ? usage.completion_tokens
104
- : undefined,
105
- cacheReadTokens: typeof usage?.input_tokens_details?.cached_tokens === "number"
106
- ? usage.input_tokens_details.cached_tokens
107
- : typeof usage?.prompt_tokens_details?.cached_tokens === "number"
108
- ? usage.prompt_tokens_details.cached_tokens
109
- : undefined,
110
- costUsd: normalizeCostUsd(usage),
111
- raw: usage,
112
- },
113
- raw: parsed,
114
- httpStatus: status,
115
- };
116
- }
117
- function postJson(url, body, apiKey, timeoutMs) {
118
- const payload = JSON.stringify(body);
119
- const requester = url.protocol === "https:" ? httpsRequest : httpRequest;
120
- return new Promise((resolve, reject) => {
121
- const req = requester(url, {
122
- method: "POST",
123
- timeout: timeoutMs,
124
- headers: {
125
- authorization: `Bearer ${apiKey}`,
126
- "content-type": "application/json",
127
- accept: "application/json",
128
- "content-length": Buffer.byteLength(payload),
129
- },
130
- }, res => {
131
- const chunks = [];
132
- let bytes = 0;
133
- res.on("data", chunk => {
134
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
135
- bytes += buf.length;
136
- if (bytes > MAX_RESPONSE_BYTES) {
137
- req.destroy(new XaiApiError("xAI API response exceeded the 50MB limit", null));
138
- return;
139
- }
140
- chunks.push(buf);
141
- });
142
- res.on("end", () => {
143
- const text = Buffer.concat(chunks).toString("utf8");
144
- const status = res.statusCode ?? 0;
145
- if (status < 200 || status >= 300) {
146
- const err = new XaiApiError(extractErrorMessage(status, text), status, text);
147
- reject(err);
148
- return;
149
- }
150
- resolve(text);
151
- });
152
- });
153
- req.on("timeout", () => {
154
- req.destroy(new XaiApiError("xAI API request timed out", null, "", "ETIMEDOUT"));
155
- });
156
- req.on("error", reject);
157
- req.end(payload);
158
- });
159
- }
160
- export async function createXaiResponse(params, logger = noopLogger) {
161
- const requestBody = {
162
- model: params.model,
163
- input: params.input,
164
- store: true,
165
- };
166
- if (params.instructions)
167
- requestBody.instructions = params.instructions;
168
- if (params.previousResponseId)
169
- requestBody.previous_response_id = params.previousResponseId;
170
- if (params.maxOutputTokens !== undefined)
171
- requestBody.max_output_tokens = params.maxOutputTokens;
172
- if (params.temperature !== undefined)
173
- requestBody.temperature = params.temperature;
174
- if (params.topP !== undefined)
175
- requestBody.top_p = params.topP;
176
- if (params.reasoningEffort !== undefined) {
177
- requestBody.reasoning = { effort: params.reasoningEffort };
178
- }
179
- const url = responsesUrl(params.baseUrl);
180
- const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
181
- const body = await withRetry(() => postJson(url, requestBody, params.apiKey, timeoutMs), getXaiCircuitBreaker(logger), {
182
- initialDelay: 1_000,
183
- maxDelay: 30_000,
184
- factor: 2,
185
- isTransient: isHttpTransient,
186
- onRetry: (error, attempt, delay) => {
187
- logWarn(logger, `[xai-api] transient request failure on attempt ${attempt}; retrying in ${delay}ms: ${error.message}`);
188
- },
189
- }, logger);
190
- return parseResponsesResult(200, body);
191
- }