Initial import NDC_1C

This commit is contained in:
2026-03-26 10:38:25 +03:00
commit a162d77ef7
2943 changed files with 3615871 additions and 0 deletions
@@ -0,0 +1,213 @@
import fs from "fs";
import path from "path";
import { DEFAULT_OPENAI_BASE_URL, SCHEMAS_DIR } from "../config";
import { ApiError } from "../utils/http";
export interface OpenAIRequestConfig {
apiKey: string;
model: string;
baseUrl?: string;
temperature?: number;
maxOutputTokens?: number;
}
export interface OpenAIResponseEnvelope {
raw: unknown;
outputText: string;
usage: {
input_tokens: number;
output_tokens: number;
total_tokens: number;
};
}
function extractUsage(raw: Record<string, unknown>): {
input_tokens: number;
output_tokens: number;
total_tokens: number;
} {
const usage = (raw.usage ?? {}) as Record<string, unknown>;
const input = Number(usage.input_tokens ?? usage.prompt_tokens ?? 0);
const output = Number(usage.output_tokens ?? usage.completion_tokens ?? 0);
const total = Number(usage.total_tokens ?? input + output);
return {
input_tokens: Number.isFinite(input) ? input : 0,
output_tokens: Number.isFinite(output) ? output : 0,
total_tokens: Number.isFinite(total) ? total : 0
};
}
function extractOutputText(raw: Record<string, unknown>): string {
if (typeof raw.output_text === "string" && raw.output_text.trim().length > 0) {
return raw.output_text;
}
const output = raw.output;
if (Array.isArray(output)) {
for (const item of output) {
if (!item || typeof item !== "object") {
continue;
}
const content = (item as Record<string, unknown>).content;
if (!Array.isArray(content)) {
continue;
}
for (const c of content) {
if (!c || typeof c !== "object") {
continue;
}
const block = c as Record<string, unknown>;
if (typeof block.text === "string" && block.text.trim()) {
return block.text;
}
}
}
}
const response = raw.response;
if (response && typeof response === "object") {
const nested = response as Record<string, unknown>;
if (typeof nested.output_text === "string" && nested.output_text.trim().length > 0) {
return nested.output_text;
}
}
throw new ApiError("OPENAI_OUTPUT_PARSE_FAILED", "Не удалось извлечь output_text из Responses API ответа.", 502, raw);
}
function loadSchemaForTransport(schemaVersion: "v1" | "v2" | "v2_0_1" | "v2_0_2"): Record<string, unknown> {
const schemaFile =
schemaVersion === "v1"
? "normalized_query_v1.json"
: schemaVersion === "v2_0_1"
? "normalized_query_v2_0_1.json"
: schemaVersion === "v2_0_2"
? "normalized_query_v2_0_2.json"
: "normalized_query_v2.json";
const schemaPath = path.resolve(SCHEMAS_DIR, schemaFile);
return JSON.parse(fs.readFileSync(schemaPath, "utf-8")) as Record<string, unknown>;
}
export class OpenAIResponsesClient {
public async testConnection(config: OpenAIRequestConfig): Promise<{ ok: boolean; model: string }> {
const payload = {
model: config.model,
input: [
{
role: "user",
content: [{ type: "input_text", text: "ping" }]
}
],
max_output_tokens: 16
};
await this.post(config, payload);
return { ok: true, model: config.model };
}
public async normalize(
config: OpenAIRequestConfig,
prompt: {
systemPrompt: string;
developerPrompt: string;
domainPrompt: string;
userQuestion: string;
schemaVersion: "v1" | "v2" | "v2_0_1" | "v2_0_2";
controlledRetryInstruction?: string;
}
): Promise<OpenAIResponseEnvelope> {
const schema = loadSchemaForTransport(prompt.schemaVersion);
const schemaName =
prompt.schemaVersion === "v1"
? "normalized_query_v1"
: prompt.schemaVersion === "v2_0_1"
? "normalized_query_v2_0_1"
: prompt.schemaVersion === "v2_0_2"
? "normalized_query_v2_0_2"
: "normalized_query_v2";
const developerPrompt = prompt.controlledRetryInstruction
? `${prompt.developerPrompt}\n\n${prompt.controlledRetryInstruction}`
: prompt.developerPrompt;
const payload = {
model: config.model,
temperature: config.temperature ?? 0,
max_output_tokens: config.maxOutputTokens ?? 700,
input: [
{
role: "system",
content: [{ type: "input_text", text: prompt.systemPrompt }]
},
{
role: "developer",
content: [{ type: "input_text", text: developerPrompt }]
},
{
role: "user",
content: [
{
type: "input_text",
text: `${prompt.domainPrompt}\n\nПользовательский вопрос:\n${prompt.userQuestion}`
}
]
}
],
text: {
format: {
type: "json_schema",
name: schemaName,
strict: true,
schema
}
}
};
const raw = await this.post(config, payload);
const outputText = extractOutputText(raw);
return {
raw,
outputText,
usage: extractUsage(raw)
};
}
private async post(config: OpenAIRequestConfig, payload: Record<string, unknown>): Promise<Record<string, unknown>> {
if (!config.apiKey || config.apiKey.trim().length < 10) {
throw new ApiError("OPENAI_API_KEY_MISSING", "API ключ OpenAI не задан или слишком короткий.", 400);
}
const url = `${(config.baseUrl ?? DEFAULT_OPENAI_BASE_URL).replace(/\/$/, "")}/responses`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const text = await response.text();
let data: Record<string, unknown>;
try {
data = JSON.parse(text) as Record<string, unknown>;
} catch {
throw new ApiError("OPENAI_NON_JSON_RESPONSE", "OpenAI вернул не-JSON ответ.", 502, { status: response.status, body: text.slice(0, 500) });
}
if (!response.ok) {
const errorObj = (data.error ?? {}) as Record<string, unknown>;
throw new ApiError(
"OPENAI_REQUEST_FAILED",
String(errorObj.message ?? `OpenAI request failed with status ${response.status}`),
response.status,
{
status: response.status,
type: errorObj.type ?? null,
code: errorObj.code ?? null
}
);
}
return data;
}
}