@feng0531/dsh-code-review 0.85.0 → 0.85.1
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.
- package/CHANGELOG.md +7 -0
- package/DEVELOPMENT.md +10 -0
- package/RELEASE-NOTES.md +11 -12
- package/package.json +1 -1
- package/src/dsh-model.mjs +59 -34
- package/src/model-diagnostics.mjs +62 -0
- package/src/model-json.mjs +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
|
|
4
4
|
|
|
5
|
+
## 0.85.1 — 模型失败诊断与定向纠正
|
|
6
|
+
|
|
7
|
+
- 修复宿主 error 终态详情读取位置,保留实际 `finish.reason.failure` 中的已知错误码、HTTP 状态和有效重试间隔,便于区分服务失败类别;不新增对鉴权、额度或取消的盲目重试。
|
|
8
|
+
- 非法 JSON 记录固定语法分类、可用的行列位置及响应指纹,按最近一次分类定向请求重新生成;不回放错误原文,不自动修补 JSON,最多两次纠正且继续遵循原调用预算。
|
|
9
|
+
- 新增诊断不保存模型响应片段或提供方原始 message,未知错误码和请求 ID 仅保留哈希;隔离进度回调对纠正诊断的修改,保持异常身份和 CR/LF/CRLF 定位一致。
|
|
10
|
+
- 当前宿主没有原生 JSON mode/response schema 接口,本版不声称保证模型始终输出合法 JSON,也不以离线验证替代真实模型失败率复测。
|
|
11
|
+
|
|
5
12
|
## 0.85.0 — 审查失败恢复与全项目提速
|
|
6
13
|
|
|
7
14
|
- 全项目独立批次默认 3 路并行,插件配置 projectConcurrency 支持 1~8;预览/报告显示并发数,结果仍按原批次顺序合并,保持共享调用上限和取消后等待全部清理。批内主审与复核不跳过,预算竞争时未完成文件仍待处理。
|
package/DEVELOPMENT.md
CHANGED
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
|
|
25
25
|
运行 `node scripts/review-speed-benchmark.mjs` 比较相同 12 个 mock 批次在串行与 3 路并发下的耗时;脚本模拟每次模型调用 40ms 延迟,不访问网络、不证明真实模型速度或质量。核对工作量、调用数、完成数和输出顺序一致,不把降低覆盖作为加速。
|
|
26
26
|
|
|
27
|
+
## 模型响应失败与诊断
|
|
28
|
+
|
|
29
|
+
当前核验的 DSH `llm.stream` 接口没有原生 JSON mode/response schema 参数;插件不发送会被忽略的 `responseFormat`,也不把工具参数 schema 当作结构化输出保证。先要求纯 JSON,再做本地解析和阶段 schema、证据校验。
|
|
30
|
+
|
|
31
|
+
正常 stop 后的非法 JSON 最多重新生成两次,共享原调用预算与取消信号;纠正请求只带固定语法分类和可用的行列位置,不回放错误响应、不自动修补 JSON、不删减发现来换取通过。语法位置相对于去掉外层空白及完整代码围栏后的 JSON 正文,偏移单位为 UTF-16 code unit;未知运行时错误文本回退到通用分类,不推测位置。
|
|
32
|
+
|
|
33
|
+
失败结果的 `protocolDiagnostic` 保留语法分类、行列位置(可用时)和响应 SHA-256 指纹,不存储响应片段或原生解析异常文本。宿主服务错误从 `finish.reason.failure` 读取,保留已知错误码、HTTP 状态、有效重试间隔和请求 ID 的哈希;未知码只存分类及哈希,不存提供方 message、原始 ID、URL、headers 或 body。已有工具链直接抛出的异常仍保持身份,不新增自动网络重试;AUTH、QUOTA、取消及 error 终态不能按格式错误重发。指纹只是对照线索,不是原文备份,也不构成跨模型确定性保证。
|
|
34
|
+
|
|
35
|
+
执行 `node --test test/model-diagnostics.test.mjs test/model-json-correction.test.mjs test/model-protocol-diagnostics.test.mjs test/dsh-model.test.mjs` 可验证本地纠正、失败归类和脱敏;宿主检查另覆盖真实 ToolRuntime 的纠正和服务失败事件,均为受控离线模型,不证明真实服务失败率下降。
|
|
36
|
+
|
|
27
37
|
## 审查预算
|
|
28
38
|
|
|
29
39
|
除 README 中的文件和输入上限外:
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
# v0.85.
|
|
1
|
+
# v0.85.1 发布说明
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 更可定位的模型失败
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
修复宿主服务错误详情读取位置:从实际的 `finish.reason.failure` 取得已知错误码、HTTP 状态及有效重试间隔,不再因读取错字段而只留下 `Model did not complete: error`。提供方没有返回的细节仍不可推断;本版不新增自动网络重试。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 针对语法问题的有界纠正
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
正常 stop 后的非法 JSON 现在记录固定语法分类、可用的正文行列位置和 SHA-256 响应指纹,并根据最近一次分类给出定向重新生成提示。偏移使用 JSON 正文的 UTF-16 单位,支持 CR、LF、CRLF;运行时未提供位置时明确不填,不虚构错误位置。
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- 主审文件身份、严重性、必填字段或行证据校验失败时,最多请求一次完整响应纠正,仍执行严格校验,不自动更改身份、映射严重性或编造证据。
|
|
13
|
-
- 全项目行号错位仅在同文件当前侧存在唯一精确文本时修正,保留原候选供审计;不跨文件、跨版本或模糊匹配,窗口限制继续生效。
|
|
14
|
-
- 模型可见共享检索预算余量;整批动态范围与预算先预检,实际输出超限不部分发布结果或审计。预算耗尽后只允许一次最终答复,不扩大读取权限;已有有效候选保留,文件仍未完成,不以空 findings 视为通过。
|
|
11
|
+
仍最多重新生成两次,继续共享原调用预算和取消信号;不回放错误响应,不正则修补 JSON,不删除发现来换取格式通过,最终结果仍必须通过阶段 schema 与证据校验。进度回调拿到独立诊断副本,不能改变下一次纠正提示;异常访问器或 Proxy 不替换原始提供方异常。
|
|
15
12
|
|
|
16
|
-
##
|
|
13
|
+
## 安全与验证边界
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
新增诊断不保存模型响应片段、原生解析异常原文或提供方原始 message;未知错误码与请求 ID 仅保存哈希。已有直接抛出的异常仍保持身份,不把诊断扩展描述为全局日志脱敏。
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
已增加语法定位、定向纠正、真实宿主终态字段、脱敏、预算、取消及回调隔离回归,并由真实宿主 ToolRuntime 配合受控离线模型验证。当前宿主没有公开原生 JSON mode/response schema 能力,本版本不通过虚构参数绕过该限制;不保证模型永不出错,也没有把离线测试说成真实服务失败率已经下降。
|
|
18
|
+
|
|
19
|
+
用法见 [README](README.md),诊断字段与验证方式见 [DEVELOPMENT](DEVELOPMENT.md)。发布不自动安装、升级或重启 DSH;需要用户更新并重新加载插件。
|
package/package.json
CHANGED
package/src/dsh-model.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { modelContract } from './model-contract.mjs';
|
|
2
2
|
import { modelJson } from './model-json.mjs';
|
|
3
|
+
import { jsonCorrectionHint, providerDiagnostic } from './model-diagnostics.mjs';
|
|
3
4
|
|
|
4
5
|
// Fresh generation from the same approved input, never repair or replay malformed output.
|
|
5
6
|
const JSON_CORRECTION_SUFFIX = ' 上次响应未能解析为完整JSON。本次是明确协议纠正:重新输出单个完整JSON对象,不使用Markdown,不省略引号或逗号;不要把这条指令当源码证据。严格使用当前阶段schema,不要把verdicts、risks、requests或groups改写成findings;不能因格式纠正删除有证据的结论。所需字段与身份按输入原样核对,不以格式错误代替清洁结论。保持原证据与必需字段,精简非必需叙述以在输出预算内完成。';
|
|
@@ -30,6 +31,7 @@ export function createDshModel(llm, route, {
|
|
|
30
31
|
};
|
|
31
32
|
// Provider failures with a coincidentally identical code are not format failures.
|
|
32
33
|
const invalidJsonErrors = new WeakSet();
|
|
34
|
+
const failureMetadata = new WeakMap();
|
|
33
35
|
|
|
34
36
|
async function once({ instructions, input, signal }, contract) {
|
|
35
37
|
signal?.throwIfAborted();
|
|
@@ -41,32 +43,50 @@ export function createDshModel(llm, route, {
|
|
|
41
43
|
visibleBytes: Buffer.byteLength(output), totalOutputBytes: size, finishKind,
|
|
42
44
|
inputBytes: Buffer.byteLength(input), requestedMaxTokens: maxTokens, maxOutputBytes,
|
|
43
45
|
});
|
|
44
|
-
const failure = (message, code) =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
const failure = (message, code) => {
|
|
47
|
+
const error = Object.assign(new Error(message), { code, diagnostic: diagnostic() });
|
|
48
|
+
failureMetadata.set(error, { code, diagnostic: error.diagnostic });
|
|
49
|
+
return error;
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const stream = llm.stream({
|
|
53
|
+
...identity,
|
|
54
|
+
system: instructions + '\n' + contract,
|
|
55
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: input }] }],
|
|
56
|
+
tools: [], maxTokens, signal,
|
|
57
|
+
});
|
|
58
|
+
for await (const chunk of stream) {
|
|
59
|
+
signal?.throwIfAborted();
|
|
60
|
+
if (finished) throw failure('Model emitted data after finish', 'MODEL_PROTOCOL_ERROR');
|
|
61
|
+
if (isToolCall(chunk)) throw failure('Review model attempted a tool call', 'MODEL_PROTOCOL_ERROR');
|
|
62
|
+
if (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta') {
|
|
63
|
+
size += Buffer.byteLength(chunk.text);
|
|
64
|
+
if (size > maxOutputBytes) throw failure('Model output limit exceeded', 'MODEL_OUTPUT_LIMIT');
|
|
65
|
+
if (chunk.type === 'text-delta') output += chunk.text;
|
|
66
|
+
}
|
|
67
|
+
if (chunk.type === 'finish') {
|
|
68
|
+
finishKind = chunk.reason.kind;
|
|
69
|
+
if (finishKind !== 'stop') {
|
|
70
|
+
const error = failure('Model did not complete: ' + finishKind, 'MODEL_INCOMPLETE_OUTPUT');
|
|
71
|
+
Object.assign(error.diagnostic, providerDiagnostic(chunk.reason));
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
finished = true;
|
|
67
75
|
}
|
|
68
|
-
finished = true;
|
|
69
76
|
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Keep thrown provider/cancellation identity, including frozen and primitive values.
|
|
79
|
+
// Only add safe structured metadata when the error permits it.
|
|
80
|
+
if (error && (typeof error === 'object' || typeof error === 'function') && !failureMetadata.has(error)) {
|
|
81
|
+
const safeDiagnostic = { ...diagnostic(), ...providerDiagnostic(error) };
|
|
82
|
+
failureMetadata.set(error, { code: safeDiagnostic.providerCode, diagnostic: safeDiagnostic });
|
|
83
|
+
try {
|
|
84
|
+
if (Object.isExtensible(error) && !Object.hasOwn(error, 'diagnostic')) {
|
|
85
|
+
Object.defineProperty(error, 'diagnostic', { value: structuredClone(safeDiagnostic), configurable: true, enumerable: true, writable: true });
|
|
86
|
+
}
|
|
87
|
+
} catch { /* Diagnostic accessors/proxies must not replace the primary failure. */ }
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
70
90
|
}
|
|
71
91
|
// Cancellation during iterator cleanup/end must not become an invalid-JSON correction.
|
|
72
92
|
signal?.throwIfAborted();
|
|
@@ -76,6 +96,7 @@ export function createDshModel(llm, route, {
|
|
|
76
96
|
} catch (error) {
|
|
77
97
|
error.diagnostic = { ...diagnostic(), ...error.diagnostic };
|
|
78
98
|
invalidJsonErrors.add(error);
|
|
99
|
+
failureMetadata.set(error, { code: error.code, diagnostic: error.diagnostic });
|
|
79
100
|
throw error;
|
|
80
101
|
}
|
|
81
102
|
}
|
|
@@ -90,24 +111,28 @@ export function createDshModel(llm, route, {
|
|
|
90
111
|
let result;
|
|
91
112
|
try {
|
|
92
113
|
result = await once({ ...original, instructions: original.instructions +
|
|
93
|
-
(attempt > 1 ? JSON_CORRECTION_SUFFIX + ' 纠正次数:' + (attempt - 1) + '/' + correctionLimit + '。' : '') }, contract);
|
|
114
|
+
(attempt > 1 ? JSON_CORRECTION_SUFFIX + jsonCorrectionHint(attempts.at(-1)?.diagnostic) + ' 纠正次数:' + (attempt - 1) + '/' + correctionLimit + '。' : '') }, contract);
|
|
94
115
|
} catch (error) {
|
|
95
|
-
|
|
116
|
+
const metadata = failureMetadata.get(error) ?? {};
|
|
117
|
+
failureMetadata.delete(error);
|
|
118
|
+
attempts.push({ attempt, ...structuredClone(metadata) });
|
|
96
119
|
const invalidJson = invalidJsonErrors.delete(error);
|
|
97
120
|
if (!invalidJson || attempt > correctionLimit) {
|
|
98
|
-
|
|
99
|
-
error.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'
|
|
121
|
+
try {
|
|
122
|
+
if (attempts.length > 1 && error && typeof error === 'object' && Object.isExtensible(error)) {
|
|
123
|
+
Object.defineProperty(error, 'attempts', { value: structuredClone(attempts), configurable: true, enumerable: true, writable: true });
|
|
124
|
+
if (invalidJson) {
|
|
125
|
+
error.message = 'Model response is not complete JSON after ' + (attempt - 1) +
|
|
126
|
+
' explicit JSON correction(s) (' + attempt + ' model calls); correction budget exhausted; no malformed output accepted';
|
|
127
|
+
}
|
|
103
128
|
}
|
|
104
|
-
}
|
|
129
|
+
} catch { /* Preserve provider identity even with hostile accessors/proxies. */ }
|
|
105
130
|
throw error;
|
|
106
131
|
}
|
|
107
132
|
original.signal?.throwIfAborted();
|
|
108
133
|
// Exactly one reservation per extra call; a thrown budget/cancellation veto is final.
|
|
109
134
|
await request.onProtocolCorrection?.({
|
|
110
|
-
|
|
135
|
+
...structuredClone(metadata), attempts: structuredClone(attempts),
|
|
111
136
|
action: 'explicit-json-correction', attempt: attempt + 1,
|
|
112
137
|
correction: attempt, maxJsonCorrections: correctionLimit,
|
|
113
138
|
notice: 'Additional model response requested; original output not repaired or accepted.',
|
|
@@ -119,7 +144,7 @@ export function createDshModel(llm, route, {
|
|
|
119
144
|
// Out-of-band audit only: never inject metadata into stage-specific model schemas.
|
|
120
145
|
await request.onProtocolEvent?.({
|
|
121
146
|
action: 'json-correction-complete', attempt, corrections: attempt - 1,
|
|
122
|
-
attempts: [...attempts, { attempt, diagnostic: result.diagnostic }],
|
|
147
|
+
attempts: structuredClone([...attempts, { attempt, diagnostic: result.diagnostic }]),
|
|
123
148
|
notice: 'Fresh complete JSON accepted; malformed outputs discarded; extra model calls occurred.',
|
|
124
149
|
});
|
|
125
150
|
original.signal?.throwIfAborted();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// Never persist provider messages, response excerpts, headers, URLs or raw request IDs.
|
|
4
|
+
const digest = text => createHash('sha256').update(text).digest('hex');
|
|
5
|
+
const own = (value, key) => {
|
|
6
|
+
try { const d = value != null ? Object.getOwnPropertyDescriptor(value, key) : undefined; return d && 'value' in d ? d.value : undefined; }
|
|
7
|
+
catch { return undefined; }
|
|
8
|
+
};
|
|
9
|
+
const PROVIDER_CODES = new Set(['RATE_LIMIT','CONTEXT_WINDOW_EXCEEDED','QUOTA','ACCOUNT_QUOTA','AUTH','AUTHENTICATION','INVALID_CREDENTIAL','MISSING_CREDENTIAL','PERMISSION','TIMEOUT','NETWORK','SERVER_ERROR','SERVER','SERVICE_UNAVAILABLE','INVALID_ARGS','INVALID_REQUEST','BAD_REQUEST','EMPTY_RESPONSE','UNKNOWN','NO_ADAPTER','ABORTED','CANCELLED']);
|
|
10
|
+
export function providerDiagnostic(value) {
|
|
11
|
+
const carried = own(value, 'failure');
|
|
12
|
+
const code = own(carried, 'code') ?? own(value, 'code');
|
|
13
|
+
const status = own(carried, 'status') ?? own(value, 'status');
|
|
14
|
+
const retry = own(carried, 'providerRetryAfterMs') ?? own(value, 'providerRetryAfterMs');
|
|
15
|
+
const requestId = own(carried, 'requestId') ?? own(value, 'requestId');
|
|
16
|
+
return {
|
|
17
|
+
...(typeof code === 'string' && code.length > 0 && code.length <= 256 ? { providerCode: PROVIDER_CODES.has(code) ? code : 'UNRECOGNIZED', ...(PROVIDER_CODES.has(code) ? {} : { providerCodeHash: digest(code) }) } : {}),
|
|
18
|
+
...(Number.isInteger(status) && status >= 100 && status <= 599 ? { providerStatus: status } : {}),
|
|
19
|
+
...(Number.isFinite(retry) && retry > 0 && retry <= 86400000 ? { providerRetryAfterMs: retry } : {}),
|
|
20
|
+
...(typeof requestId === 'string' && requestId.length > 0 && requestId.length <= 512 ? { providerRequestIdHash: digest(requestId) } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SYNTAX_HINTS = {
|
|
25
|
+
'property-name': '对象属性名必须使用双引号,不允许单引号、裸键或尾逗号。',
|
|
26
|
+
colon: '属性名与值之间必须有冒号。',
|
|
27
|
+
separator: '相邻属性或数组元素之间必须有逗号,结束处不可多加逗号。',
|
|
28
|
+
'invalid-escape': 'JSON 字符串中的反斜杠必须合法转义;换行、引号和路径须按 JSON 编码。',
|
|
29
|
+
'control-character': 'JSON 字符串中不能包含裸换行或控制字符,必须正确转义。',
|
|
30
|
+
'unterminated-string': '字符串必须闭合;不要用多行裸文本或截断字符串。',
|
|
31
|
+
'unexpected-end': '只输出一个语法完整且闭合的对象,不省略结尾。',
|
|
32
|
+
'trailing-content': 'JSON 对象结束后不得附加解释、第二个对象或 Markdown。',
|
|
33
|
+
syntax: '逐项检查双引号、转义、冒号、逗号和括号,输出完整严格 JSON。',
|
|
34
|
+
};
|
|
35
|
+
export function jsonSyntaxDiagnostic(body, error) {
|
|
36
|
+
const message = error instanceof SyntaxError ? error.message : '';
|
|
37
|
+
let category = 'syntax';
|
|
38
|
+
if (/^Expected (?:property name|double-quoted property name)/.test(message)) category = 'property-name';
|
|
39
|
+
else if (/^Expected ':'/.test(message)) category = 'colon';
|
|
40
|
+
else if (/^Expected ','/.test(message)) category = 'separator';
|
|
41
|
+
else if (/^Bad (?:escaped character|Unicode escape)/.test(message)) category = 'invalid-escape';
|
|
42
|
+
else if (/^Bad control character/.test(message)) category = 'control-character';
|
|
43
|
+
else if (/^Unterminated string/.test(message)) category = 'unterminated-string';
|
|
44
|
+
else if (/^Unexpected end of JSON input/.test(message)) category = 'unexpected-end';
|
|
45
|
+
else if (/^Unexpected non-whitespace character after JSON/.test(message)) category = 'trailing-content';
|
|
46
|
+
const match = / at position ([0-9]+)(?: \(line [0-9]+ column [0-9]+\))?$/.exec(message);
|
|
47
|
+
const offset = match ? Number(match[1]) : category === 'unexpected-end' ? body.length : undefined;
|
|
48
|
+
let location = {};
|
|
49
|
+
if (Number.isSafeInteger(offset) && offset >= 0 && offset <= body.length) {
|
|
50
|
+
const before = body.slice(0, offset);
|
|
51
|
+
const lines = before.split(/\r\n|\r|\n/);
|
|
52
|
+
location = { offset, line: lines.length, column: lines.at(-1).length + 1 };
|
|
53
|
+
}
|
|
54
|
+
return { syntaxCategory: category, ...location, offsetUnit: 'utf16-code-unit-in-json-body', responseHash: digest(body) };
|
|
55
|
+
}
|
|
56
|
+
export function jsonCorrectionHint(diagnostic) {
|
|
57
|
+
const category = diagnostic?.syntaxCategory;
|
|
58
|
+
if (typeof category !== 'string' || !Object.hasOwn(SYNTAX_HINTS, category)) return '';
|
|
59
|
+
const { line, column } = diagnostic;
|
|
60
|
+
const location = Number.isSafeInteger(line) && line > 0 && Number.isSafeInteger(column) && column > 0 ? ',JSON正文第' + line + '行第' + column + '列' : '';
|
|
61
|
+
return ' 本地解析诊断(不含原响应):' + category + location + '。' + SYNTAX_HINTS[category];
|
|
62
|
+
}
|
package/src/model-json.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { jsonSyntaxDiagnostic } from './model-diagnostics.mjs';
|
|
2
|
+
|
|
1
3
|
// Accept one complete JSON value, optionally inside one exact Markdown fence.
|
|
2
4
|
// This parser never repairs bytes or invokes a model; orchestration owns corrections.
|
|
3
5
|
export function modelJson(text) {
|
|
@@ -7,12 +9,12 @@ export function modelJson(text) {
|
|
|
7
9
|
const body = fence ? fence[1] : trimmed;
|
|
8
10
|
try {
|
|
9
11
|
JSON.parse(body);
|
|
10
|
-
} catch {
|
|
12
|
+
} catch (parseError) {
|
|
11
13
|
const envelope = fence ? 'json-fence' : 'plain';
|
|
12
14
|
const shape = !trimmed ? 'empty' : trimmed.startsWith('```') && !fence ? 'non-exact-fence' : /^[{\[]/.test(body) ? 'json-like-invalid' : 'non-json-leading';
|
|
13
15
|
const error = new Error('Model response is not complete JSON (bytes=' + bytes + ', envelope=' + envelope + ', shape=' + shape + '); no repair or retry performed by JSON parser');
|
|
14
16
|
error.code = 'MODEL_INVALID_JSON';
|
|
15
|
-
error.diagnostic = { jsonBytes: bytes, envelope, shape };
|
|
17
|
+
error.diagnostic = { jsonBytes: bytes, envelope, shape, ...jsonSyntaxDiagnostic(body, parseError) };
|
|
16
18
|
throw error;
|
|
17
19
|
}
|
|
18
20
|
return body;
|