structformatter 0.1.2

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +122 -0
  4. package/README.zh-CN.md +122 -0
  5. package/config.example.yaml +37 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +48 -0
  8. package/dist/config/load.d.ts +3 -0
  9. package/dist/config/load.js +62 -0
  10. package/dist/config/schema.d.ts +72 -0
  11. package/dist/config/schema.js +73 -0
  12. package/dist/enforce/engine.d.ts +15 -0
  13. package/dist/enforce/engine.js +149 -0
  14. package/dist/enforce/errors.d.ts +18 -0
  15. package/dist/enforce/errors.js +41 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +8 -0
  18. package/dist/json/extract.d.ts +2 -0
  19. package/dist/json/extract.js +27 -0
  20. package/dist/json/parse.d.ts +10 -0
  21. package/dist/json/parse.js +28 -0
  22. package/dist/patch/patch.d.ts +3 -0
  23. package/dist/patch/patch.js +73 -0
  24. package/dist/prompt/build.d.ts +9 -0
  25. package/dist/prompt/build.js +21 -0
  26. package/dist/prompt/sanitize_schema.d.ts +1 -0
  27. package/dist/prompt/sanitize_schema.js +26 -0
  28. package/dist/prompt/templates.d.ts +6 -0
  29. package/dist/prompt/templates.js +28 -0
  30. package/dist/providers/index.d.ts +12 -0
  31. package/dist/providers/index.js +42 -0
  32. package/dist/providers/openai_compatible.d.ts +22 -0
  33. package/dist/providers/openai_compatible.js +91 -0
  34. package/dist/server.d.ts +4 -0
  35. package/dist/server.js +218 -0
  36. package/dist/stream/sse.d.ts +2 -0
  37. package/dist/stream/sse.js +57 -0
  38. package/dist/types/internal.d.ts +53 -0
  39. package/dist/types/internal.js +15 -0
  40. package/dist/types/openai.d.ts +59 -0
  41. package/dist/types/openai.js +2 -0
  42. package/dist/validate/ajv.d.ts +5 -0
  43. package/dist/validate/ajv.js +18 -0
  44. package/dist/validate/cache.d.ts +11 -0
  45. package/dist/validate/cache.js +62 -0
  46. package/dist/validate/errors.d.ts +8 -0
  47. package/dist/validate/errors.js +13 -0
  48. package/package.json +66 -0
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceJsonSchema = enforceJsonSchema;
4
+ const parse_1 = require("../json/parse");
5
+ const patch_1 = require("../patch/patch");
6
+ const build_1 = require("../prompt/build");
7
+ const errors_1 = require("../validate/errors");
8
+ const errors_2 = require("./errors");
9
+ function extractCandidateText(resp) {
10
+ const c = resp.choices?.[0];
11
+ const content = c?.message?.content;
12
+ return typeof content === 'string' ? content : null;
13
+ }
14
+ function finishReason(resp) {
15
+ const fr = resp.choices?.[0]?.finish_reason;
16
+ return typeof fr === 'string' ? fr : null;
17
+ }
18
+ function usageFrom(resp) {
19
+ const u = resp.usage ?? {};
20
+ return {
21
+ prompt_tokens: typeof u.prompt_tokens === 'number' ? u.prompt_tokens : 0,
22
+ completion_tokens: typeof u.completion_tokens === 'number' ? u.completion_tokens : 0,
23
+ total_tokens: typeof u.total_tokens === 'number' ? u.total_tokens : 0,
24
+ };
25
+ }
26
+ function addUsage(a, b) {
27
+ return {
28
+ prompt_tokens: a.prompt_tokens + b.prompt_tokens,
29
+ completion_tokens: a.completion_tokens + b.completion_tokens,
30
+ total_tokens: a.total_tokens + b.total_tokens,
31
+ };
32
+ }
33
+ async function enforceJsonSchema(args) {
34
+ const validate = args.validatorCache.get(args.schema);
35
+ let lastCandidate;
36
+ let lastErrors;
37
+ let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
38
+ const traces = [];
39
+ for (let attempt = 1; attempt <= args.policy.maxAttempts; attempt++) {
40
+ const messages = (0, build_1.buildMessages)({
41
+ original: args.originalRequest.messages,
42
+ schema: args.schema,
43
+ attempt,
44
+ lastCandidate,
45
+ lastErrors,
46
+ });
47
+ const req = {
48
+ ...args.originalRequest,
49
+ model: args.upstreamModel,
50
+ messages,
51
+ stream: false,
52
+ };
53
+ // Prefer upstream JSON mode to reduce syntax errors.
54
+ if (args.adapter.supports.json_object)
55
+ req.response_format = { type: 'json_object' };
56
+ else
57
+ delete req.response_format;
58
+ const ctx = {
59
+ requestId: args.requestId,
60
+ timeoutMs: args.policy.timeoutMsPerAttempt,
61
+ provider: args.provider,
62
+ baseUrl: args.baseUrl,
63
+ upstreamModel: args.upstreamModel,
64
+ };
65
+ let upstreamResp;
66
+ try {
67
+ upstreamResp = await args.adapter.chatCompletions(req, ctx);
68
+ }
69
+ catch (e) {
70
+ const msg = e instanceof Error ? e.message : String(e);
71
+ traces.push({ attempt, kind: 'upstream_error', message: msg });
72
+ return (0, errors_2.upstreamError)({
73
+ message: msg,
74
+ attempts: attempt,
75
+ attemptTraces: args.debug ? traces : undefined,
76
+ });
77
+ }
78
+ usage = addUsage(usage, usageFrom(upstreamResp));
79
+ const fr = finishReason(upstreamResp);
80
+ if (fr === 'refusal' || fr === 'content_filter') {
81
+ traces.push({ attempt, kind: 'refusal', finish_reason: fr });
82
+ return (0, errors_2.refusalError)({
83
+ finishReason: fr,
84
+ attempts: attempt,
85
+ attemptTraces: args.debug ? traces : undefined,
86
+ });
87
+ }
88
+ const candidateText = extractCandidateText(upstreamResp);
89
+ if (!candidateText) {
90
+ lastCandidate = undefined;
91
+ lastErrors = [{ path: '/', keyword: 'parse', message: 'missing assistant content' }];
92
+ traces.push({ attempt, kind: 'no_content' });
93
+ continue;
94
+ }
95
+ const parsed = (0, parse_1.tryParseJson)(candidateText, { enableRepair: args.policy.enableJsonRepair });
96
+ if (!parsed.ok) {
97
+ lastCandidate = undefined;
98
+ lastErrors = [{ path: '/', keyword: 'parse', message: parsed.error }];
99
+ traces.push({ attempt, kind: 'parse_error', error: parsed.error });
100
+ continue;
101
+ }
102
+ const candidateObj = parsed.value;
103
+ const valid = validate(candidateObj);
104
+ if (valid) {
105
+ const jsonText = JSON.stringify(candidateObj);
106
+ traces.push({ attempt, kind: 'ok' });
107
+ return {
108
+ ok: true,
109
+ json: candidateObj,
110
+ jsonText,
111
+ usage,
112
+ attempts: attempt,
113
+ attemptTraces: args.debug ? traces : undefined,
114
+ };
115
+ }
116
+ let errors = (0, errors_1.summarizeAjvErrors)(validate.errors);
117
+ let finalCandidate = candidateObj;
118
+ if (args.policy.enableDeterministicFix) {
119
+ const patched = (0, patch_1.patchToSchema)(args.schema, candidateObj, {
120
+ enableTypeCoercion: args.policy.enableTypeCoercion,
121
+ });
122
+ const valid2 = validate(patched);
123
+ if (valid2) {
124
+ const jsonText = JSON.stringify(patched);
125
+ traces.push({ attempt, kind: 'patched_ok' });
126
+ return {
127
+ ok: true,
128
+ json: patched,
129
+ jsonText,
130
+ usage,
131
+ attempts: attempt,
132
+ attemptTraces: args.debug ? traces : undefined,
133
+ };
134
+ }
135
+ errors = (0, errors_1.summarizeAjvErrors)(validate.errors);
136
+ finalCandidate = patched;
137
+ }
138
+ lastCandidate = finalCandidate;
139
+ lastErrors = errors;
140
+ traces.push({ attempt, kind: 'schema_invalid', errors });
141
+ }
142
+ const excerpt = lastCandidate ? JSON.stringify(lastCandidate).slice(0, 500) : '';
143
+ return (0, errors_2.structuredOutputFailed)({
144
+ attempts: args.policy.maxAttempts,
145
+ lastCandidateExcerpt: excerpt,
146
+ validationErrors: lastErrors ?? [],
147
+ attemptTraces: args.debug ? traces : undefined,
148
+ });
149
+ }
@@ -0,0 +1,18 @@
1
+ import type { StructuredError } from '../types/internal';
2
+ import type { ValidationErrorSummary } from '../validate/errors';
3
+ export declare function structuredOutputFailed(args: {
4
+ attempts: number;
5
+ lastCandidateExcerpt: string;
6
+ validationErrors: ValidationErrorSummary[];
7
+ attemptTraces?: unknown[];
8
+ }): StructuredError;
9
+ export declare function upstreamError(args: {
10
+ message: string;
11
+ attempts: number;
12
+ attemptTraces?: unknown[];
13
+ }): StructuredError;
14
+ export declare function refusalError(args: {
15
+ finishReason: string;
16
+ attempts: number;
17
+ attemptTraces?: unknown[];
18
+ }): StructuredError;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.structuredOutputFailed = structuredOutputFailed;
4
+ exports.upstreamError = upstreamError;
5
+ exports.refusalError = refusalError;
6
+ function structuredOutputFailed(args) {
7
+ return {
8
+ ok: false,
9
+ attempts: args.attempts,
10
+ attemptTraces: args.attemptTraces,
11
+ error: {
12
+ type: 'structured_output_failed',
13
+ message: `Failed to produce schema-valid JSON after ${args.attempts} attempts`,
14
+ details: {
15
+ attempts: args.attempts,
16
+ last_candidate_excerpt: args.lastCandidateExcerpt,
17
+ validation_errors: args.validationErrors,
18
+ },
19
+ },
20
+ };
21
+ }
22
+ function upstreamError(args) {
23
+ return {
24
+ ok: false,
25
+ attempts: args.attempts,
26
+ attemptTraces: args.attemptTraces,
27
+ error: { type: 'upstream_error', message: args.message, details: { attempts: args.attempts } },
28
+ };
29
+ }
30
+ function refusalError(args) {
31
+ return {
32
+ ok: false,
33
+ attempts: args.attempts,
34
+ attemptTraces: args.attemptTraces,
35
+ error: {
36
+ type: 'refusal',
37
+ message: 'Upstream refused to answer',
38
+ details: { finish_reason: args.finishReason, attempts: args.attempts },
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,3 @@
1
+ export { loadConfig, summarizeConfig } from './config/load';
2
+ export type { AppConfig } from './config/schema';
3
+ export { createServer } from './server';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createServer = exports.summarizeConfig = exports.loadConfig = void 0;
4
+ var load_1 = require("./config/load");
5
+ Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return load_1.loadConfig; } });
6
+ Object.defineProperty(exports, "summarizeConfig", { enumerable: true, get: function () { return load_1.summarizeConfig; } });
7
+ var server_1 = require("./server");
8
+ Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_1.createServer; } });
@@ -0,0 +1,2 @@
1
+ export declare function stripCodeFences(text: string): string;
2
+ export declare function extractJsonCandidates(text: string): string[];
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripCodeFences = stripCodeFences;
4
+ exports.extractJsonCandidates = extractJsonCandidates;
5
+ const FENCE_RE = /```[a-zA-Z0-9_-]*\s*([\s\S]*?)```/gi;
6
+ function stripCodeFences(text) {
7
+ const matches = [...text.matchAll(FENCE_RE)];
8
+ if (!matches.length)
9
+ return text;
10
+ const biggest = matches.reduce((a, b) => ((a[1]?.length ?? 0) >= (b[1]?.length ?? 0) ? a : b));
11
+ return biggest[1] ?? text;
12
+ }
13
+ function extractJsonCandidates(text) {
14
+ const cleaned = stripCodeFences(text).trim();
15
+ const candidates = [];
16
+ for (const [startChar, endChar] of [
17
+ ['{', '}'],
18
+ ['[', ']'],
19
+ ]) {
20
+ const start = cleaned.indexOf(startChar);
21
+ const end = cleaned.lastIndexOf(endChar);
22
+ if (start >= 0 && end > start)
23
+ candidates.push(cleaned.slice(start, end + 1).trim());
24
+ }
25
+ candidates.sort((a, b) => b.length - a.length);
26
+ return candidates;
27
+ }
@@ -0,0 +1,10 @@
1
+ export type ParseResult = {
2
+ ok: true;
3
+ value: unknown;
4
+ } | {
5
+ ok: false;
6
+ error: string;
7
+ };
8
+ export declare function tryParseJson(text: string, opts: {
9
+ enableRepair: boolean;
10
+ }): ParseResult;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tryParseJson = tryParseJson;
4
+ const jsonrepair_1 = require("jsonrepair");
5
+ const extract_1 = require("./extract");
6
+ function tryParseJson(text, opts) {
7
+ const candidates = (0, extract_1.extractJsonCandidates)(text);
8
+ const all = candidates.length ? candidates : [text.trim()];
9
+ let lastError = 'failed to parse JSON';
10
+ for (const c of all) {
11
+ try {
12
+ return { ok: true, value: JSON.parse(c) };
13
+ }
14
+ catch (e) {
15
+ lastError = e instanceof Error ? e.message : String(e);
16
+ }
17
+ if (opts.enableRepair) {
18
+ try {
19
+ const repaired = (0, jsonrepair_1.jsonrepair)(c);
20
+ return { ok: true, value: JSON.parse(repaired) };
21
+ }
22
+ catch (e) {
23
+ lastError = e instanceof Error ? e.message : String(e);
24
+ }
25
+ }
26
+ }
27
+ return { ok: false, error: lastError };
28
+ }
@@ -0,0 +1,3 @@
1
+ export declare function patchToSchema(schema: unknown, value: unknown, opts: {
2
+ enableTypeCoercion: boolean;
3
+ }): unknown;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.patchToSchema = patchToSchema;
4
+ function schemaExpectsType(schema, expected) {
5
+ if (!schema || typeof schema !== 'object')
6
+ return false;
7
+ const t = schema.type;
8
+ if (typeof t === 'string')
9
+ return t === expected;
10
+ if (Array.isArray(t))
11
+ return t.includes(expected);
12
+ return false;
13
+ }
14
+ function coercePrimitive(schema, value) {
15
+ if (value === null || value === undefined)
16
+ return value;
17
+ if (typeof value === 'string' && schemaExpectsType(schema, 'integer')) {
18
+ const s = value.trim();
19
+ if (/^[+-]?\d+$/.test(s)) {
20
+ const n = Number(s);
21
+ if (Number.isSafeInteger(n))
22
+ return n;
23
+ }
24
+ }
25
+ if (typeof value === 'string' && schemaExpectsType(schema, 'number')) {
26
+ const s = value.trim();
27
+ if (/^[+-]?(?:\d+\.?\d*|\d*\.?\d+)(?:[eE][+-]?\d+)?$/.test(s)) {
28
+ const n = Number(s);
29
+ if (Number.isFinite(n))
30
+ return n;
31
+ }
32
+ }
33
+ if (typeof value === 'string' && schemaExpectsType(schema, 'boolean')) {
34
+ const s = value.trim().toLowerCase();
35
+ if (s === 'true')
36
+ return true;
37
+ if (s === 'false')
38
+ return false;
39
+ }
40
+ return value;
41
+ }
42
+ function patchToSchema(schema, value, opts) {
43
+ if (!schema || typeof schema !== 'object')
44
+ return value;
45
+ const patchedValue = opts.enableTypeCoercion ? coercePrimitive(schema, value) : value;
46
+ if (schemaExpectsType(schema, 'object') && patchedValue && typeof patchedValue === 'object') {
47
+ if (Array.isArray(patchedValue))
48
+ return patchedValue;
49
+ const s = schema;
50
+ const props = (s.properties && typeof s.properties === 'object' ? s.properties : {});
51
+ const additionalAllowed = s.additionalProperties !== false;
52
+ const out = {};
53
+ for (const [k, v] of Object.entries(patchedValue)) {
54
+ if (!additionalAllowed && !(k in props))
55
+ continue;
56
+ const childSchema = props[k];
57
+ out[k] =
58
+ childSchema && typeof childSchema === 'object'
59
+ ? patchToSchema(childSchema, v, opts)
60
+ : v;
61
+ }
62
+ return out;
63
+ }
64
+ if (schemaExpectsType(schema, 'array') && Array.isArray(patchedValue)) {
65
+ const s = schema;
66
+ const items = s.items;
67
+ if (items && typeof items === 'object') {
68
+ return patchedValue.map((v) => patchToSchema(items, v, opts));
69
+ }
70
+ return patchedValue;
71
+ }
72
+ return patchedValue;
73
+ }
@@ -0,0 +1,9 @@
1
+ import type { OpenAIChatMessage } from '../types/openai';
2
+ import type { ValidationErrorSummary } from '../validate/errors';
3
+ export declare function buildMessages(args: {
4
+ original: OpenAIChatMessage[];
5
+ schema: Record<string, unknown>;
6
+ attempt: number;
7
+ lastCandidate?: unknown;
8
+ lastErrors?: ValidationErrorSummary[];
9
+ }): OpenAIChatMessage[];
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildMessages = buildMessages;
4
+ const sanitize_schema_1 = require("./sanitize_schema");
5
+ const templates_1 = require("./templates");
6
+ function formatErrors(errors) {
7
+ if (!errors?.length)
8
+ return '(none)';
9
+ return errors.slice(0, 50).map((e) => `- ${e.path}: ${e.message}`).join('\n');
10
+ }
11
+ function buildMessages(args) {
12
+ const schemaJson = JSON.stringify((0, sanitize_schema_1.sanitizeSchemaForPrompt)(args.schema), null, 0);
13
+ const system = args.attempt === 1
14
+ ? (0, templates_1.firstAttemptSystemPrompt)(schemaJson)
15
+ : (0, templates_1.reaskSystemPrompt)({
16
+ schemaJson,
17
+ lastCandidateJson: JSON.stringify(args.lastCandidate ?? null),
18
+ errorsText: formatErrors(args.lastErrors),
19
+ });
20
+ return [{ role: 'system', content: system }, ...args.original];
21
+ }
@@ -0,0 +1 @@
1
+ export declare function sanitizeSchemaForPrompt(value: unknown): unknown;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeSchemaForPrompt = sanitizeSchemaForPrompt;
4
+ const DROP_KEYS = new Set([
5
+ 'title',
6
+ 'description',
7
+ 'examples',
8
+ 'default',
9
+ 'deprecated',
10
+ 'readOnly',
11
+ 'writeOnly',
12
+ ]);
13
+ function sanitizeSchemaForPrompt(value) {
14
+ if (Array.isArray(value))
15
+ return value.map(sanitizeSchemaForPrompt);
16
+ if (!value || typeof value !== 'object')
17
+ return value;
18
+ const obj = value;
19
+ const out = {};
20
+ for (const [k, v] of Object.entries(obj)) {
21
+ if (DROP_KEYS.has(k))
22
+ continue;
23
+ out[k] = sanitizeSchemaForPrompt(v);
24
+ }
25
+ return out;
26
+ }
@@ -0,0 +1,6 @@
1
+ export declare function firstAttemptSystemPrompt(schemaJson: string): string;
2
+ export declare function reaskSystemPrompt(args: {
3
+ schemaJson: string;
4
+ lastCandidateJson: string;
5
+ errorsText: string;
6
+ }): string;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.firstAttemptSystemPrompt = firstAttemptSystemPrompt;
4
+ exports.reaskSystemPrompt = reaskSystemPrompt;
5
+ function firstAttemptSystemPrompt(schemaJson) {
6
+ return [
7
+ 'You MUST output ONLY valid JSON (no markdown, no code fences, no explanation).',
8
+ 'The JSON MUST conform to the following JSON Schema:',
9
+ schemaJson,
10
+ ].join('\n');
11
+ }
12
+ function reaskSystemPrompt(args) {
13
+ return [
14
+ 'You previously returned JSON that failed validation.',
15
+ 'Return ONLY corrected JSON that conforms to the schema.',
16
+ 'Do NOT add any keys not present in schema.',
17
+ 'Do NOT include markdown.',
18
+ '',
19
+ 'Schema:',
20
+ args.schemaJson,
21
+ '',
22
+ 'Previous JSON:',
23
+ args.lastCandidateJson,
24
+ '',
25
+ 'Validation errors:',
26
+ args.errorsText,
27
+ ].join('\n');
28
+ }
@@ -0,0 +1,12 @@
1
+ import type { AppConfig } from '../config/schema';
2
+ import type { ProviderAdapter } from '../types/internal';
3
+ export interface ResolvedModel {
4
+ provider: string;
5
+ upstreamModel: string;
6
+ routedModel: string;
7
+ requestedModel: string;
8
+ }
9
+ export declare function resolveModel(requestedModel: string, config: AppConfig): ResolvedModel;
10
+ export declare function listModelIds(config: AppConfig): string[];
11
+ export declare function createProviderRegistry(config: AppConfig): Map<string, ProviderAdapter>;
12
+ export declare function getAdapter(registry: Map<string, ProviderAdapter>, providerName: string): ProviderAdapter;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveModel = resolveModel;
4
+ exports.listModelIds = listModelIds;
5
+ exports.createProviderRegistry = createProviderRegistry;
6
+ exports.getAdapter = getAdapter;
7
+ const openai_compatible_1 = require("./openai_compatible");
8
+ function resolveModel(requestedModel, config) {
9
+ const routed = config.routing.model_aliases[requestedModel] ?? requestedModel;
10
+ const idx = routed.indexOf('/');
11
+ if (idx <= 0 || idx === routed.length - 1) {
12
+ throw new Error('model must be "provider/model" (or mapped by routing.model_aliases)');
13
+ }
14
+ const provider = routed.slice(0, idx);
15
+ const upstreamModel = routed.slice(idx + 1);
16
+ return { provider, upstreamModel, routedModel: routed, requestedModel };
17
+ }
18
+ function listModelIds(config) {
19
+ const ids = new Set();
20
+ for (const [alias, routed] of Object.entries(config.routing.model_aliases)) {
21
+ ids.add(alias);
22
+ ids.add(routed);
23
+ }
24
+ return [...ids].sort();
25
+ }
26
+ function createProviderRegistry(config) {
27
+ const adapters = new Map();
28
+ for (const [name, providerCfg] of Object.entries(config.providers)) {
29
+ if (providerCfg.type === 'openai_compatible') {
30
+ adapters.set(name, new openai_compatible_1.OpenAICompatibleAdapter(name, providerCfg));
31
+ continue;
32
+ }
33
+ throw new Error(`Unsupported provider type: ${providerCfg.type}`);
34
+ }
35
+ return adapters;
36
+ }
37
+ function getAdapter(registry, providerName) {
38
+ const adapter = registry.get(providerName);
39
+ if (!adapter)
40
+ throw new Error(`Unknown provider: ${providerName}`);
41
+ return adapter;
42
+ }
@@ -0,0 +1,22 @@
1
+ import { fetch } from 'undici';
2
+ import type { ProviderConfig } from '../config/schema';
3
+ import type { OpenAIChatCompletionsRequest, OpenAIChatCompletionsResponse } from '../types/openai';
4
+ import type { ProviderAdapter, RequestContext } from '../types/internal';
5
+ export declare class UpstreamError extends Error {
6
+ readonly statusCode: number;
7
+ readonly bodyText: string;
8
+ constructor(statusCode: number, bodyText: string);
9
+ }
10
+ export declare class OpenAICompatibleAdapter implements ProviderAdapter {
11
+ readonly name: string;
12
+ readonly supports: ProviderConfig['capabilities'];
13
+ private readonly cfg;
14
+ private readonly agent;
15
+ constructor(name: string, cfg: ProviderConfig);
16
+ chatCompletionsRaw(req: OpenAIChatCompletionsRequest, ctx: RequestContext): Promise<{
17
+ response: Awaited<ReturnType<typeof fetch>>;
18
+ abort: () => void;
19
+ cleanup: () => void;
20
+ }>;
21
+ chatCompletions(req: OpenAIChatCompletionsRequest, ctx: RequestContext): Promise<OpenAIChatCompletionsResponse>;
22
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenAICompatibleAdapter = exports.UpstreamError = void 0;
4
+ const undici_1 = require("undici");
5
+ class UpstreamError extends Error {
6
+ statusCode;
7
+ bodyText;
8
+ constructor(statusCode, bodyText) {
9
+ super(`Upstream error ${statusCode}: ${bodyText}`);
10
+ this.statusCode = statusCode;
11
+ this.bodyText = bodyText;
12
+ }
13
+ }
14
+ exports.UpstreamError = UpstreamError;
15
+ function chatCompletionsUrl(baseUrl) {
16
+ const trimmed = baseUrl.replace(/\/+$/, '');
17
+ if (trimmed.endsWith('/v1'))
18
+ return `${trimmed}/chat/completions`;
19
+ return `${trimmed}/v1/chat/completions`;
20
+ }
21
+ function dropParams(req, keys) {
22
+ if (!keys.length)
23
+ return req;
24
+ const out = { ...req };
25
+ for (const k of keys)
26
+ delete out[k];
27
+ return out;
28
+ }
29
+ class OpenAICompatibleAdapter {
30
+ name;
31
+ supports;
32
+ cfg;
33
+ agent;
34
+ constructor(name, cfg) {
35
+ this.name = name;
36
+ this.cfg = cfg;
37
+ this.supports = cfg.capabilities;
38
+ this.agent = new undici_1.Agent({ keepAliveTimeout: 60_000, keepAliveMaxTimeout: 60_000 });
39
+ }
40
+ async chatCompletionsRaw(req, ctx) {
41
+ const url = chatCompletionsUrl(this.cfg.base_url);
42
+ const headers = {
43
+ 'Content-Type': 'application/json',
44
+ ...this.cfg.default_headers,
45
+ };
46
+ if (this.cfg.api_key_env && !headers.Authorization) {
47
+ const apiKey = process.env[this.cfg.api_key_env];
48
+ if (apiKey)
49
+ headers.Authorization = `Bearer ${apiKey}`;
50
+ }
51
+ const upstreamReq = dropParams(req, this.cfg.drop_params);
52
+ const ac = new AbortController();
53
+ const t = setTimeout(() => ac.abort(), ctx.timeoutMs);
54
+ t.unref?.();
55
+ let resp;
56
+ try {
57
+ resp = await (0, undici_1.fetch)(url, {
58
+ dispatcher: this.agent,
59
+ method: 'POST',
60
+ headers,
61
+ body: JSON.stringify(upstreamReq),
62
+ signal: ac.signal,
63
+ });
64
+ }
65
+ catch (e) {
66
+ clearTimeout(t);
67
+ throw e;
68
+ }
69
+ if (!resp.ok) {
70
+ const text = await resp.text();
71
+ clearTimeout(t);
72
+ throw new UpstreamError(resp.status, text);
73
+ }
74
+ return {
75
+ response: resp,
76
+ abort: () => ac.abort(),
77
+ cleanup: () => clearTimeout(t),
78
+ };
79
+ }
80
+ async chatCompletions(req, ctx) {
81
+ const raw = await this.chatCompletionsRaw(req, ctx);
82
+ try {
83
+ const text = await raw.response.text();
84
+ return JSON.parse(text);
85
+ }
86
+ finally {
87
+ raw.cleanup();
88
+ }
89
+ }
90
+ }
91
+ exports.OpenAICompatibleAdapter = OpenAICompatibleAdapter;
@@ -0,0 +1,4 @@
1
+ import type { AppConfig } from './config/schema';
2
+ export declare function createServer(config: AppConfig): import("fastify").FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault> & PromiseLike<import("fastify").FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>> & {
3
+ __linterBrands: "SafePromiseLike";
4
+ };