src / request.ts
import OpenAI from "openai/index.js";
import type { ChatCompletionCreateParamsStreaming, ChatCompletionTool } from "openai/resources/index.js";
import { CONFIG_DEFAULTS, type ShartiConfig } from "./config";
import type { OpenAICompatMessageParam, ShartiCompletionsBody } from "./types";
import { asBool, asNumber, asString } from "./utils/coerce";
import { formatBlock } from "./utils/block";
import { ShartiLogger } from "./utils/logger";
/* -------------------------------------------------------------------------- */
/* Client factory */
/* -------------------------------------------------------------------------- */
/** Construct an OpenAI-compatible client for the configured endpoint. */
export function createOpenAI(baseUrl: string, apiKey: string) {
return new OpenAI({
apiKey: apiKey || "no-key",
baseURL: baseUrl || undefined,
});
}
/**
* Issue a streaming chat-completions request.
*
* Centralizes the single, localized escape hatch at the OpenAI-compatible boundary: the
* (superset) body is asserted against the SDK's streaming-params contract. Because the body
* carries `stream: true`, the SDK resolves its streaming overload, so the awaited result is a
* proper `Stream<ChatCompletionChunk>` — no downstream re-casting required.
*/
export async function executeCompletions(
client: OpenAI,
body: ShartiCompletionsBody,
signal: AbortSignal,
) {
return client.chat.completions.create(body as ChatCompletionCreateParamsStreaming, { signal });
}
/* -------------------------------------------------------------------------- */
/* Payload assembly */
/* */
/* Structural parameters are always sent. Tuning parameters are gated by the */
/* `customSampling` toggle so an untouched profile sends a minimal request. */
/* Coercion fallbacks draw from CONFIG_DEFAULTS so a default is declared once.*/
/* -------------------------------------------------------------------------- */
export function buildRequestPayload(
config: ShartiConfig,
messages: OpenAICompatMessageParam[],
tools: ChatCompletionTool[] | undefined,
): ShartiCompletionsBody {
const payload: ShartiCompletionsBody = {
model: asString(config.get("model")),
messages,
stream: true,
// Ensure the final chunk carries token usage stats.
stream_options: { include_usage: true },
};
// Tools: structural part of the request — attached whenever available.
if (tools && tools.length > 0) {
payload.tools = tools;
payload.tool_choice = asString(config.get("toolChoice"), CONFIG_DEFAULTS.toolChoice) as ShartiCompletionsBody["tool_choice"];
payload.parallel_tool_calls = asBool(config.get("parallelToolCalls"), CONFIG_DEFAULTS.parallelToolCalls);
}
// Behavioural flags — always sent regardless of customSampling.
payload.preserve_thinking = true;
payload.priority = asString(config.get("priority"), CONFIG_DEFAULTS.priority);
const timeout = asNumber(config.get("timeout"), CONFIG_DEFAULTS.timeout);
if (timeout > 0) {
payload.timeout = timeout;
}
// Tuning parameters — omitted wholesale when customSampling is OFF.
if (!asBool(config.get("customSampling"), CONFIG_DEFAULTS.customSampling)) {
return payload;
}
payload.temperature = asNumber(config.get("temperature"), CONFIG_DEFAULTS.temperature);
payload.top_k = asNumber(config.get("topK"), CONFIG_DEFAULTS.topK);
payload.top_p = asNumber(config.get("topP"), CONFIG_DEFAULTS.topP);
payload.min_p = asNumber(config.get("minP"), CONFIG_DEFAULTS.minP);
payload.presence_penalty = asNumber(config.get("presencePenalty"), CONFIG_DEFAULTS.presencePenalty);
payload.frequency_penalty = asNumber(config.get("frequencyPenalty"), CONFIG_DEFAULTS.frequencyPenalty);
payload.repetition_penalty = asNumber(config.get("repetitionPenalty"), CONFIG_DEFAULTS.repetitionPenalty);
payload.seed = asNumber(config.get("seed"), CONFIG_DEFAULTS.seed);
payload.max_tokens = asNumber(config.get("maxTokens"), CONFIG_DEFAULTS.maxTokens);
const reasoningEffort = asString(config.get("reasoningEffort"), CONFIG_DEFAULTS.reasoningEffort);
if (reasoningEffort !== "default" && reasoningEffort.length > 0) {
payload.reasoning_effort = reasoningEffort;
}
const thinkingTokenBudget = asNumber(config.get("thinkingTokenBudget"), CONFIG_DEFAULTS.thinkingTokenBudget);
if (thinkingTokenBudget >= 0) {
payload.thinking_token_budget = thinkingTokenBudget;
}
return payload;
}
/* -------------------------------------------------------------------------- */
/* Diagnostic payload dump */
/* */
/* Derives the debug view directly from the constructed payload, so any knob */
/* added in buildRequestPayload surfaces in diagnostics automatically. */
/* -------------------------------------------------------------------------- */
/** Infrastructure keys that are not user-facing tuning knobs (hidden from the dump). */
const HIDDEN_DEBUG_KEYS = new Set([
"model",
"messages",
"stream",
"stream_options",
"preserve_thinking",
]);
/**
* Display order for the Settings dump, mirroring the field order in config.ts
* (Sampling → Reasoning → Tools → Request). `tools` is not a config field; it is
* slotted at the end of the Tools group.
*/
const SETTINGS_DISPLAY_ORDER: string[] = [
"temperature",
"top_k",
"top_p",
"min_p",
"presence_penalty",
"frequency_penalty",
"repetition_penalty",
"seed",
"max_tokens",
"reasoning_effort",
"thinking_token_budget",
"tools",
"tool_choice",
"parallel_tool_calls",
"priority",
"timeout",
];
const SETTINGS_ORDER_RANK: Record<string, number> = Object.fromEntries(
SETTINGS_DISPLAY_ORDER.map((key, index) => [key, index]),
);
/** Optional per-key presentation tweaks for the debug dump. */
const DISPLAY_TRANSFORMERS: Partial<Record<string, (v: unknown) => unknown>> = {
tools: (v) => (Array.isArray(v) && v.length > 0 ? true : false),
timeout: (v) => `${v}s`,
};
export function logDebugPayload(logger: ShartiLogger, payload: Record<string, unknown>): void {
const rows: Array<[string, unknown]> = Object.entries(payload)
.filter(([k, v]) => !HIDDEN_DEBUG_KEYS.has(k) && v !== undefined && v !== null)
.sort(
([a], [b]) =>
(SETTINGS_ORDER_RANK[a] ?? Number.MAX_SAFE_INTEGER) -
(SETTINGS_ORDER_RANK[b] ?? Number.MAX_SAFE_INTEGER),
)
.map(([k, v]) => [k, DISPLAY_TRANSFORMERS[k] ? DISPLAY_TRANSFORMERS[k]!(v) : v]);
const block = formatBlock("⚙️ Settings:", rows);
if (block) logger.debug(block);
}
src / request.ts
import OpenAI from "openai/index.js";
import type { ChatCompletionCreateParamsStreaming, ChatCompletionTool } from "openai/resources/index.js";
import { CONFIG_DEFAULTS, type ShartiConfig } from "./config";
import type { OpenAICompatMessageParam, ShartiCompletionsBody } from "./types";
import { asBool, asNumber, asString } from "./utils/coerce";
import { formatBlock } from "./utils/block";
import { ShartiLogger } from "./utils/logger";
/* -------------------------------------------------------------------------- */
/* Client factory */
/* -------------------------------------------------------------------------- */
/** Construct an OpenAI-compatible client for the configured endpoint. */
export function createOpenAI(baseUrl: string, apiKey: string) {
return new OpenAI({
apiKey: apiKey || "no-key",
baseURL: baseUrl || undefined,
});
}
/**
* Issue a streaming chat-completions request.
*
* Centralizes the single, localized escape hatch at the OpenAI-compatible boundary: the
* (superset) body is asserted against the SDK's streaming-params contract. Because the body
* carries `stream: true`, the SDK resolves its streaming overload, so the awaited result is a
* proper `Stream<ChatCompletionChunk>` — no downstream re-casting required.
*/
export async function executeCompletions(
client: OpenAI,
body: ShartiCompletionsBody,
signal: AbortSignal,
) {
return client.chat.completions.create(body as ChatCompletionCreateParamsStreaming, { signal });
}
/* -------------------------------------------------------------------------- */
/* Payload assembly */
/* */
/* Structural parameters are always sent. Tuning parameters are gated by the */
/* `customSampling` toggle so an untouched profile sends a minimal request. */
/* Coercion fallbacks draw from CONFIG_DEFAULTS so a default is declared once.*/
/* -------------------------------------------------------------------------- */
export function buildRequestPayload(
config: ShartiConfig,
messages: OpenAICompatMessageParam[],
tools: ChatCompletionTool[] | undefined,
): ShartiCompletionsBody {
const payload: ShartiCompletionsBody = {
model: asString(config.get("model")),
messages,
stream: true,
// Ensure the final chunk carries token usage stats.
stream_options: { include_usage: true },
};
// Tools: structural part of the request — attached whenever available.
if (tools && tools.length > 0) {
payload.tools = tools;
payload.tool_choice = asString(config.get("toolChoice"), CONFIG_DEFAULTS.toolChoice) as ShartiCompletionsBody["tool_choice"];
payload.parallel_tool_calls = asBool(config.get("parallelToolCalls"), CONFIG_DEFAULTS.parallelToolCalls);
}
// Behavioural flags — always sent regardless of customSampling.
payload.preserve_thinking = true;
payload.priority = asString(config.get("priority"), CONFIG_DEFAULTS.priority);
const timeout = asNumber(config.get("timeout"), CONFIG_DEFAULTS.timeout);
if (timeout > 0) {
payload.timeout = timeout;
}
// Tuning parameters — omitted wholesale when customSampling is OFF.
if (!asBool(config.get("customSampling"), CONFIG_DEFAULTS.customSampling)) {
return payload;
}
payload.temperature = asNumber(config.get("temperature"), CONFIG_DEFAULTS.temperature);
payload.top_k = asNumber(config.get("topK"), CONFIG_DEFAULTS.topK);
payload.top_p = asNumber(config.get("topP"), CONFIG_DEFAULTS.topP);
payload.min_p = asNumber(config.get("minP"), CONFIG_DEFAULTS.minP);
payload.presence_penalty = asNumber(config.get("presencePenalty"), CONFIG_DEFAULTS.presencePenalty);
payload.frequency_penalty = asNumber(config.get("frequencyPenalty"), CONFIG_DEFAULTS.frequencyPenalty);
payload.repetition_penalty = asNumber(config.get("repetitionPenalty"), CONFIG_DEFAULTS.repetitionPenalty);
payload.seed = asNumber(config.get("seed"), CONFIG_DEFAULTS.seed);
payload.max_tokens = asNumber(config.get("maxTokens"), CONFIG_DEFAULTS.maxTokens);
const reasoningEffort = asString(config.get("reasoningEffort"), CONFIG_DEFAULTS.reasoningEffort);
if (reasoningEffort !== "default" && reasoningEffort.length > 0) {
payload.reasoning_effort = reasoningEffort;
}
const thinkingTokenBudget = asNumber(config.get("thinkingTokenBudget"), CONFIG_DEFAULTS.thinkingTokenBudget);
if (thinkingTokenBudget >= 0) {
payload.thinking_token_budget = thinkingTokenBudget;
}
return payload;
}
/* -------------------------------------------------------------------------- */
/* Diagnostic payload dump */
/* */
/* Derives the debug view directly from the constructed payload, so any knob */
/* added in buildRequestPayload surfaces in diagnostics automatically. */
/* -------------------------------------------------------------------------- */
/** Infrastructure keys that are not user-facing tuning knobs (hidden from the dump). */
const HIDDEN_DEBUG_KEYS = new Set([
"model",
"messages",
"stream",
"stream_options",
"preserve_thinking",
]);
/**
* Display order for the Settings dump, mirroring the field order in config.ts
* (Sampling → Reasoning → Tools → Request). `tools` is not a config field; it is
* slotted at the end of the Tools group.
*/
const SETTINGS_DISPLAY_ORDER: string[] = [
"temperature",
"top_k",
"top_p",
"min_p",
"presence_penalty",
"frequency_penalty",
"repetition_penalty",
"seed",
"max_tokens",
"reasoning_effort",
"thinking_token_budget",
"tools",
"tool_choice",
"parallel_tool_calls",
"priority",
"timeout",
];
const SETTINGS_ORDER_RANK: Record<string, number> = Object.fromEntries(
SETTINGS_DISPLAY_ORDER.map((key, index) => [key, index]),
);
/** Optional per-key presentation tweaks for the debug dump. */
const DISPLAY_TRANSFORMERS: Partial<Record<string, (v: unknown) => unknown>> = {
tools: (v) => (Array.isArray(v) && v.length > 0 ? true : false),
timeout: (v) => `${v}s`,
};
export function logDebugPayload(logger: ShartiLogger, payload: Record<string, unknown>): void {
const rows: Array<[string, unknown]> = Object.entries(payload)
.filter(([k, v]) => !HIDDEN_DEBUG_KEYS.has(k) && v !== undefined && v !== null)
.sort(
([a], [b]) =>
(SETTINGS_ORDER_RANK[a] ?? Number.MAX_SAFE_INTEGER) -
(SETTINGS_ORDER_RANK[b] ?? Number.MAX_SAFE_INTEGER),
)
.map(([k, v]) => [k, DISPLAY_TRANSFORMERS[k] ? DISPLAY_TRANSFORMERS[k]!(v) : v]);
const block = formatBlock("⚙️ Settings:", rows);
if (block) logger.debug(block);
}