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
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.2] - 2025-12-26
9
+
10
+ ### Changed
11
+
12
+ - Rename npm package + CLI from `structuredformatter` to `structformatter`.
13
+ - Env vars: prefer `STRUCTFORMATTER_*` (old `STRUCTUREDFORMATTER_*` still accepted).
14
+ - Docs/specs updated to the new name.
15
+
16
+ ## [0.1.1] - 2025-12-26
17
+
18
+ ### Added
19
+
20
+ - `CHANGELOG.md`.
21
+
22
+ ### Changed
23
+
24
+ - Packaging: run build via `prepack` so `npm pack`/`npm publish` don’t require `pnpm`.
25
+ - Publish docs: note npm’s session-based auth and CI/CD token guidance.
26
+
27
+ ## [0.1.0] - 2025-12-26
28
+
29
+ ### Added
30
+
31
+ - OpenAI-compatible proxy server that enforces JSON Schema structured outputs via an extract/repair/validate/retry loop.
32
+ - CLI `structformatter` for running the server with a config file.
33
+ - Config schema + validation, plus a `config.example.yaml`.
34
+ - Vitest test suite for core behaviors.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 StructFormatter contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # StructFormatter
2
+
3
+ <p align="center">
4
+ <a href="README.md"><img src="https://img.shields.io/badge/lang-English-blue.svg" alt="English"></a>
5
+ <a href="README.zh-CN.md"><img src="https://img.shields.io/badge/lang-简体中文-red.svg" alt="简体中文"></a>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://github.com/FrankQDWang/StructFormatter/actions/workflows/ci.yml"><img src="https://github.com/FrankQDWang/StructFormatter/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ <a href="https://www.npmjs.com/package/structformatter"><img src="https://img.shields.io/npm/v/structformatter" alt="npm"></a>
11
+ </p>
12
+
13
+ StructFormatter is an **OpenAI-compatible proxy (sidecar)** that makes **non-native** LLM APIs behave like they support **`response_format: { type: "json_schema" }`**.
14
+
15
+ It enforces schema validity via a robust “B strategy” loop:
16
+ prompting → JSON extraction → JSON repair → JSON Schema validation (Ajv) → deterministic fixes → re-ask retries.
17
+
18
+ The `specs/` directory is the source of truth (architecture, API contract, algorithm, testing plan, and checklist).
19
+
20
+ ## Why you may want this
21
+
22
+ If you use agents that rely on structured outputs, many providers only offer “JSON mode” (valid JSON) but **not** JSON Schema constrained decoding.
23
+
24
+ This service lets your agent keep calling an OpenAI-shaped endpoint, while StructFormatter enforces schema validity behind the scenes.
25
+
26
+ ## Quickstart (from source)
27
+
28
+ ```bash
29
+ pnpm install
30
+ cp config.example.yaml config.yaml
31
+ export STRUCTFORMATTER_CONFIG=./config.yaml
32
+ pnpm dev
33
+ ```
34
+
35
+ Endpoints:
36
+ - `GET /healthz`
37
+ - `GET /v1/models`
38
+ - `POST /v1/chat/completions`
39
+
40
+ ## Quickstart (npm)
41
+
42
+ After publishing:
43
+
44
+ ```bash
45
+ pnpm dlx structformatter --config ./config.yaml
46
+ ```
47
+
48
+ or:
49
+
50
+ ```bash
51
+ npx structformatter --config ./config.yaml
52
+ ```
53
+
54
+ ## Agent integration (drop-in)
55
+
56
+ Point your OpenAI SDK `base_url` to this server:
57
+
58
+ - Base URL: `http://localhost:8080/v1`
59
+ - Model naming: `provider/model` (e.g. `deepseek/deepseek-chat`)
60
+ - or use `routing.model_aliases` in `config.yaml` (e.g. `glm` → `zai/glm-4.5`)
61
+
62
+ When your request contains:
63
+
64
+ ```json
65
+ {
66
+ "response_format": {
67
+ "type": "json_schema",
68
+ "json_schema": { "name": "X", "strict": true, "schema": { "...": "..." } }
69
+ }
70
+ }
71
+ ```
72
+
73
+ StructFormatter will:
74
+ - return `choices[0].message.content` as a JSON string that **validates against your schema**, or
75
+ - return HTTP **422** with a typed error payload (see `specs/api.md`).
76
+
77
+ ## Config
78
+
79
+ Copy `config.example.yaml` and edit providers + routing:
80
+
81
+ - `providers.<name>.base_url`
82
+ - `providers.<name>.api_key_env`
83
+ - `providers.<name>.capabilities.json_object` (recommended when upstream supports JSON mode)
84
+ - `routing.model_aliases` (optional)
85
+
86
+ ## Debugging
87
+
88
+ Optional request headers:
89
+ - `X-SF-Debug: 1` → include `__debug` field in the response
90
+ - `X-SF-Max-Attempts: 5` → override attempt budget for this request (1–10)
91
+
92
+ ## Streaming
93
+
94
+ Per `specs/api.md` (v1):
95
+ - `stream=true` + `json_schema` → **400** (not supported)
96
+ - `stream=true` without `json_schema` → **SSE pass-through** to upstream
97
+
98
+ ## Examples
99
+
100
+ ```bash
101
+ bash examples/curl_schema_enforced.sh
102
+ python examples/python_openai_sdk_dropin.py
103
+ ```
104
+
105
+ ## Publishing to npm
106
+
107
+ Notes (npm auth changes as of Dec 2025):
108
+ - `npm login` is session-based (short-lived), so publish shortly after logging in.
109
+ - Publishing typically requires 2FA; for CI/CD use a granular token (not classic) or npm trusted publishing (OIDC).
110
+
111
+ 1) Log in:
112
+
113
+ ```bash
114
+ npm login
115
+ npm whoami
116
+ ```
117
+
118
+ 2) Publish (runs `npm run build` via `prepack`):
119
+
120
+ ```bash
121
+ npm publish
122
+ ```
@@ -0,0 +1,122 @@
1
+ # StructFormatter
2
+
3
+ <p align="center">
4
+ <a href="README.md"><img src="https://img.shields.io/badge/lang-English-blue.svg" alt="English"></a>
5
+ <a href="README.zh-CN.md"><img src="https://img.shields.io/badge/lang-简体中文-red.svg" alt="简体中文"></a>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://github.com/FrankQDWang/StructFormatter/actions/workflows/ci.yml"><img src="https://github.com/FrankQDWang/StructFormatter/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ <a href="https://www.npmjs.com/package/structformatter"><img src="https://img.shields.io/npm/v/structformatter" alt="npm"></a>
11
+ </p>
12
+
13
+ StructFormatter 是一个 **OpenAI 兼容的代理服务(sidecar/proxy)**:让**不支持原生 JSON Schema 约束解码(A)** 的上游模型,也能“像支持 A 一样”返回结构化 JSON。
14
+
15
+ 它用一套稳健的 **B 策略闭环** 来强制输出满足 schema:
16
+ 提示词 → JSON 抽取 → JSON 修复(jsonrepair)→ JSON Schema 校验(Ajv)→ 机械修补 → 带错误信息 re-ask 重试。
17
+
18
+ `specs/` 目录是项目的设计与验收来源(架构、API 合约、算法、测试计划、任务清单)。
19
+
20
+ ## 适用场景
21
+
22
+ 很多厂商只提供 “JSON mode”(保证输出是合法 JSON),但**不保证**符合你提供的 JSON Schema。
23
+
24
+ 如果你的 agent 流程强依赖结构化输出,StructFormatter 可以作为一个“外置桥接服务”,让你的 agent **基本不用改逻辑**,只改 `base_url` 即可。
25
+
26
+ ## 快速开始(源码运行)
27
+
28
+ ```bash
29
+ pnpm install
30
+ cp config.example.yaml config.yaml
31
+ export STRUCTFORMATTER_CONFIG=./config.yaml
32
+ pnpm dev
33
+ ```
34
+
35
+ 对外接口:
36
+ - `GET /healthz`
37
+ - `GET /v1/models`
38
+ - `POST /v1/chat/completions`
39
+
40
+ ## 快速开始(npm)
41
+
42
+ 发布到 npm 后,可直接运行:
43
+
44
+ ```bash
45
+ pnpm dlx structformatter --config ./config.yaml
46
+ ```
47
+
48
+ 或:
49
+
50
+ ```bash
51
+ npx structformatter --config ./config.yaml
52
+ ```
53
+
54
+ ## 如何嵌入现有 agent(Drop-in)
55
+
56
+ 把 OpenAI SDK 的 `base_url` 指向本服务:
57
+
58
+ - Base URL:`http://localhost:8080/v1`
59
+ - `model` 命名:`provider/model`(例如 `deepseek/deepseek-chat`)
60
+ - 或者在 `config.yaml` 里用 `routing.model_aliases` 做别名映射
61
+
62
+ 当你的请求包含:
63
+
64
+ ```json
65
+ {
66
+ "response_format": {
67
+ "type": "json_schema",
68
+ "json_schema": { "name": "X", "strict": true, "schema": { "...": "..." } }
69
+ }
70
+ }
71
+ ```
72
+
73
+ StructFormatter 会承诺:
74
+ - 成功:`choices[0].message.content` 是**可解析且符合 schema 的 JSON 字符串**
75
+ - 失败:HTTP **422** + 结构化错误(见 `specs/api.md`)
76
+
77
+ ## 配置说明
78
+
79
+ 建议从 `config.example.yaml` 复制一份:
80
+
81
+ - `providers.<name>.base_url`:上游 OpenAI-compatible 地址
82
+ - `providers.<name>.api_key_env`:从环境变量读取 key(避免写死在文件里)
83
+ - `providers.<name>.capabilities.json_object`:上游支持 JSON mode 时建议开启
84
+ - `routing.model_aliases`:可选别名映射(如 `glm` → `zai/glm-4.5`)
85
+
86
+ ## 调试与可观测性
87
+
88
+ 可选请求头:
89
+ - `X-SF-Debug: 1` → 响应中包含 `__debug`
90
+ - `X-SF-Max-Attempts: 5` → 覆盖本次请求的最大重试次数(1–10)
91
+
92
+ ## Streaming(v1 规则)
93
+
94
+ 严格遵循 `specs/api.md`:
95
+ - `stream=true` 且 `json_schema` → **400**(v1 不支持)
96
+ - `stream=true` 且非 `json_schema` → **SSE 透传上游**
97
+
98
+ ## 示例
99
+
100
+ ```bash
101
+ bash examples/curl_schema_enforced.sh
102
+ python examples/python_openai_sdk_dropin.py
103
+ ```
104
+
105
+ ## 发布到 npm
106
+
107
+ 备注(npm 在 2025-12 之后的鉴权变化):
108
+ - `npm login` 改为基于 session 的短时登录态,建议登录后尽快发布。
109
+ - 发布通常需要 2FA;如果走 CI/CD,请使用 granular token(不要用 classic token)或配置 npm trusted publishing(OIDC)。
110
+
111
+ 1) 先登录 npm:
112
+
113
+ ```bash
114
+ npm login
115
+ npm whoami
116
+ ```
117
+
118
+ 2) 发布(`prepack` 会自动执行 `npm run build`):
119
+
120
+ ```bash
121
+ npm publish
122
+ ```
@@ -0,0 +1,37 @@
1
+ server:
2
+ host: 0.0.0.0
3
+ port: 8080
4
+ request_body_limit_mb: 2
5
+
6
+ enforcement:
7
+ max_attempts: 3
8
+ timeout_ms_per_attempt: 20000
9
+ enable_jsonrepair: true
10
+ enable_deterministic_fix: true
11
+ enable_type_coercion: true
12
+ schema_max_bytes: 200000
13
+ validator_cache_size: 128
14
+
15
+ routing:
16
+ mode: provider_prefix
17
+ model_aliases:
18
+ glm: "zai/glm-4.5"
19
+ ds: "deepseek/deepseek-chat"
20
+
21
+ providers:
22
+ deepseek:
23
+ type: openai_compatible
24
+ base_url: "https://api.deepseek.com"
25
+ api_key_env: "DEEPSEEK_API_KEY"
26
+ capabilities:
27
+ json_object: true
28
+ drop_params: []
29
+
30
+ zai:
31
+ type: openai_compatible
32
+ base_url: "https://api.z.ai/api/paas/v4"
33
+ api_key_env: "ZAI_API_KEY"
34
+ capabilities:
35
+ json_object: true
36
+ drop_params: []
37
+
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const load_1 = require("./config/load");
5
+ const server_1 = require("./server");
6
+ function printHelp() {
7
+ console.log([
8
+ 'StructFormatter (OpenAI-compatible structured output proxy)',
9
+ '',
10
+ 'Usage:',
11
+ ' structformatter [--config <path>]',
12
+ '',
13
+ 'Options:',
14
+ ' --config, -c Path to config.yaml/config.json (defaults to STRUCTFORMATTER_CONFIG or ./config.{yaml,yml,json})',
15
+ ' --help, -h Show this help',
16
+ '',
17
+ ].join('\n'));
18
+ }
19
+ function parseArgs(argv) {
20
+ let configPath;
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const a = argv[i];
23
+ if (a === '--help' || a === '-h')
24
+ return { help: true };
25
+ if (a === '--config' || a === '-c') {
26
+ configPath = argv[i + 1];
27
+ i += 1;
28
+ continue;
29
+ }
30
+ if (a.startsWith('--config=')) {
31
+ configPath = a.slice('--config='.length);
32
+ continue;
33
+ }
34
+ }
35
+ return { help: false, configPath };
36
+ }
37
+ async function main() {
38
+ const args = parseArgs(process.argv.slice(2));
39
+ if (args.help) {
40
+ printHelp();
41
+ return;
42
+ }
43
+ const config = (0, load_1.loadConfig)(args.configPath);
44
+ console.log((0, load_1.summarizeConfig)(config));
45
+ const app = (0, server_1.createServer)(config);
46
+ await app.listen({ port: config.server.port, host: config.server.host });
47
+ }
48
+ void main();
@@ -0,0 +1,3 @@
1
+ import { type AppConfig } from './schema';
2
+ export declare function loadConfig(explicitPath?: string): AppConfig;
3
+ export declare function summarizeConfig(config: AppConfig): string;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadConfig = loadConfig;
7
+ exports.summarizeConfig = summarizeConfig;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const yaml_1 = __importDefault(require("yaml"));
11
+ const schema_1 = require("./schema");
12
+ function readConfigFile(filePath) {
13
+ const text = node_fs_1.default.readFileSync(filePath, 'utf8');
14
+ const ext = node_path_1.default.extname(filePath).toLowerCase();
15
+ if (ext === '.yaml' || ext === '.yml')
16
+ return yaml_1.default.parse(text);
17
+ if (ext === '.json')
18
+ return JSON.parse(text);
19
+ throw new Error(`Unsupported config extension: ${ext}`);
20
+ }
21
+ function candidatePaths(explicitPath) {
22
+ const fromEnv = process.env.STRUCTFORMATTER_CONFIG ?? process.env.STRUCTUREDFORMATTER_CONFIG;
23
+ const candidates = [
24
+ explicitPath,
25
+ fromEnv && fromEnv !== explicitPath ? fromEnv : undefined,
26
+ 'config.yaml',
27
+ 'config.yml',
28
+ 'config.json',
29
+ ].filter(Boolean);
30
+ return [...new Set(candidates)];
31
+ }
32
+ function loadConfig(explicitPath) {
33
+ let raw = {};
34
+ for (const p of candidatePaths(explicitPath)) {
35
+ if (!node_fs_1.default.existsSync(p))
36
+ continue;
37
+ raw = readConfigFile(p);
38
+ break;
39
+ }
40
+ const config = schema_1.AppConfigSchema.parse(raw);
41
+ const hostEnv = process.env.STRUCTFORMATTER_HOST ?? process.env.STRUCTUREDFORMATTER_HOST;
42
+ if (hostEnv)
43
+ config.server.host = hostEnv;
44
+ const portEnv = process.env.STRUCTFORMATTER_PORT ?? process.env.STRUCTUREDFORMATTER_PORT;
45
+ if (portEnv) {
46
+ const port = Number(portEnv);
47
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
48
+ throw new Error('STRUCTFORMATTER_PORT must be an integer in [1, 65535].');
49
+ }
50
+ config.server.port = port;
51
+ }
52
+ return config;
53
+ }
54
+ function summarizeConfig(config) {
55
+ const providers = Object.keys(config.providers).sort().join(', ') || '(none)';
56
+ return [
57
+ 'StructFormatter config:',
58
+ `- server: ${config.server.host}:${config.server.port}`,
59
+ `- enforcement: max_attempts=${config.enforcement.max_attempts}, timeout_ms_per_attempt=${config.enforcement.timeout_ms_per_attempt}`,
60
+ `- providers: ${providers}`,
61
+ ].join('\n');
62
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ export declare const ServerConfigSchema: z.ZodDefault<z.ZodObject<{
3
+ host: z.ZodDefault<z.ZodString>;
4
+ port: z.ZodDefault<z.ZodNumber>;
5
+ request_body_limit_mb: z.ZodDefault<z.ZodNumber>;
6
+ }, z.core.$strip>>;
7
+ export declare const EnforcementConfigSchema: z.ZodDefault<z.ZodObject<{
8
+ max_attempts: z.ZodDefault<z.ZodNumber>;
9
+ timeout_ms_per_attempt: z.ZodDefault<z.ZodNumber>;
10
+ enable_jsonrepair: z.ZodDefault<z.ZodBoolean>;
11
+ enable_deterministic_fix: z.ZodDefault<z.ZodBoolean>;
12
+ enable_type_coercion: z.ZodDefault<z.ZodBoolean>;
13
+ schema_max_bytes: z.ZodDefault<z.ZodNumber>;
14
+ validator_cache_size: z.ZodDefault<z.ZodNumber>;
15
+ }, z.core.$strip>>;
16
+ export declare const RoutingConfigSchema: z.ZodDefault<z.ZodObject<{
17
+ mode: z.ZodDefault<z.ZodLiteral<"provider_prefix">>;
18
+ model_aliases: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
19
+ }, z.core.$strip>>;
20
+ export declare const ProviderCapabilitiesSchema: z.ZodDefault<z.ZodObject<{
21
+ json_object: z.ZodDefault<z.ZodBoolean>;
22
+ tools: z.ZodDefault<z.ZodBoolean>;
23
+ strict_tools: z.ZodDefault<z.ZodBoolean>;
24
+ }, z.core.$strip>>;
25
+ export declare const ProviderConfigSchema: z.ZodObject<{
26
+ type: z.ZodDefault<z.ZodLiteral<"openai_compatible">>;
27
+ base_url: z.ZodString;
28
+ api_key_env: z.ZodOptional<z.ZodString>;
29
+ beta_base_url: z.ZodOptional<z.ZodString>;
30
+ default_headers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
31
+ drop_params: z.ZodDefault<z.ZodArray<z.ZodString>>;
32
+ capabilities: z.ZodDefault<z.ZodObject<{
33
+ json_object: z.ZodDefault<z.ZodBoolean>;
34
+ tools: z.ZodDefault<z.ZodBoolean>;
35
+ strict_tools: z.ZodDefault<z.ZodBoolean>;
36
+ }, z.core.$strip>>;
37
+ }, z.core.$strip>;
38
+ export declare const AppConfigSchema: z.ZodDefault<z.ZodObject<{
39
+ server: z.ZodDefault<z.ZodObject<{
40
+ host: z.ZodDefault<z.ZodString>;
41
+ port: z.ZodDefault<z.ZodNumber>;
42
+ request_body_limit_mb: z.ZodDefault<z.ZodNumber>;
43
+ }, z.core.$strip>>;
44
+ enforcement: z.ZodDefault<z.ZodObject<{
45
+ max_attempts: z.ZodDefault<z.ZodNumber>;
46
+ timeout_ms_per_attempt: z.ZodDefault<z.ZodNumber>;
47
+ enable_jsonrepair: z.ZodDefault<z.ZodBoolean>;
48
+ enable_deterministic_fix: z.ZodDefault<z.ZodBoolean>;
49
+ enable_type_coercion: z.ZodDefault<z.ZodBoolean>;
50
+ schema_max_bytes: z.ZodDefault<z.ZodNumber>;
51
+ validator_cache_size: z.ZodDefault<z.ZodNumber>;
52
+ }, z.core.$strip>>;
53
+ routing: z.ZodDefault<z.ZodObject<{
54
+ mode: z.ZodDefault<z.ZodLiteral<"provider_prefix">>;
55
+ model_aliases: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
56
+ }, z.core.$strip>>;
57
+ providers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
58
+ type: z.ZodDefault<z.ZodLiteral<"openai_compatible">>;
59
+ base_url: z.ZodString;
60
+ api_key_env: z.ZodOptional<z.ZodString>;
61
+ beta_base_url: z.ZodOptional<z.ZodString>;
62
+ default_headers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
63
+ drop_params: z.ZodDefault<z.ZodArray<z.ZodString>>;
64
+ capabilities: z.ZodDefault<z.ZodObject<{
65
+ json_object: z.ZodDefault<z.ZodBoolean>;
66
+ tools: z.ZodDefault<z.ZodBoolean>;
67
+ strict_tools: z.ZodDefault<z.ZodBoolean>;
68
+ }, z.core.$strip>>;
69
+ }, z.core.$strip>>>;
70
+ }, z.core.$strip>>;
71
+ export type AppConfig = z.infer<typeof AppConfigSchema>;
72
+ export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppConfigSchema = exports.ProviderConfigSchema = exports.ProviderCapabilitiesSchema = exports.RoutingConfigSchema = exports.EnforcementConfigSchema = exports.ServerConfigSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.ServerConfigSchema = zod_1.z
6
+ .object({
7
+ host: zod_1.z.string().default('0.0.0.0'),
8
+ port: zod_1.z.number().int().min(1).max(65535).default(8080),
9
+ request_body_limit_mb: zod_1.z.number().int().min(1).max(100).default(2),
10
+ })
11
+ .default({ host: '0.0.0.0', port: 8080, request_body_limit_mb: 2 });
12
+ exports.EnforcementConfigSchema = zod_1.z
13
+ .object({
14
+ max_attempts: zod_1.z.number().int().min(1).max(10).default(3),
15
+ timeout_ms_per_attempt: zod_1.z.number().int().min(100).default(20_000),
16
+ enable_jsonrepair: zod_1.z.boolean().default(true),
17
+ enable_deterministic_fix: zod_1.z.boolean().default(true),
18
+ enable_type_coercion: zod_1.z.boolean().default(true),
19
+ schema_max_bytes: zod_1.z.number().int().min(1_000).default(200_000),
20
+ validator_cache_size: zod_1.z.number().int().min(1).max(10_000).default(128),
21
+ })
22
+ .default({
23
+ max_attempts: 3,
24
+ timeout_ms_per_attempt: 20_000,
25
+ enable_jsonrepair: true,
26
+ enable_deterministic_fix: true,
27
+ enable_type_coercion: true,
28
+ schema_max_bytes: 200_000,
29
+ validator_cache_size: 128,
30
+ });
31
+ exports.RoutingConfigSchema = zod_1.z
32
+ .object({
33
+ mode: zod_1.z.literal('provider_prefix').default('provider_prefix'),
34
+ model_aliases: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).default({}),
35
+ })
36
+ .default({ mode: 'provider_prefix', model_aliases: {} });
37
+ exports.ProviderCapabilitiesSchema = zod_1.z
38
+ .object({
39
+ json_object: zod_1.z.boolean().default(false),
40
+ tools: zod_1.z.boolean().default(false),
41
+ strict_tools: zod_1.z.boolean().default(false),
42
+ })
43
+ .default({ json_object: false, tools: false, strict_tools: false });
44
+ exports.ProviderConfigSchema = zod_1.z.object({
45
+ type: zod_1.z.literal('openai_compatible').default('openai_compatible'),
46
+ base_url: zod_1.z.string().min(1),
47
+ api_key_env: zod_1.z.string().min(1).optional(),
48
+ beta_base_url: zod_1.z.string().min(1).optional(),
49
+ default_headers: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).default({}),
50
+ drop_params: zod_1.z.array(zod_1.z.string()).default([]),
51
+ capabilities: exports.ProviderCapabilitiesSchema,
52
+ });
53
+ exports.AppConfigSchema = zod_1.z
54
+ .object({
55
+ server: exports.ServerConfigSchema,
56
+ enforcement: exports.EnforcementConfigSchema,
57
+ routing: exports.RoutingConfigSchema,
58
+ providers: zod_1.z.record(zod_1.z.string(), exports.ProviderConfigSchema).default({}),
59
+ })
60
+ .default({
61
+ server: { host: '0.0.0.0', port: 8080, request_body_limit_mb: 2 },
62
+ enforcement: {
63
+ max_attempts: 3,
64
+ timeout_ms_per_attempt: 20_000,
65
+ enable_jsonrepair: true,
66
+ enable_deterministic_fix: true,
67
+ enable_type_coercion: true,
68
+ schema_max_bytes: 200_000,
69
+ validator_cache_size: 128,
70
+ },
71
+ routing: { mode: 'provider_prefix', model_aliases: {} },
72
+ providers: {},
73
+ });
@@ -0,0 +1,15 @@
1
+ import type { EnforcementPolicy, ProviderAdapter, StructuredError, StructuredResult } from '../types/internal';
2
+ import type { OpenAIChatCompletionsRequest } from '../types/openai';
3
+ import type { ValidatorCache } from '../validate/cache';
4
+ export declare function enforceJsonSchema(args: {
5
+ requestId: string;
6
+ adapter: ProviderAdapter;
7
+ provider: string;
8
+ baseUrl: string;
9
+ upstreamModel: string;
10
+ originalRequest: OpenAIChatCompletionsRequest;
11
+ schema: Record<string, unknown>;
12
+ policy: EnforcementPolicy;
13
+ validatorCache: ValidatorCache;
14
+ debug: boolean;
15
+ }): Promise<StructuredResult | StructuredError>;