@oh-my-pi/pi-ai 18.12.0 → 18.12.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
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.12.1] - 2026-10-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed every request failing behind a TLS-inspecting proxy that refuses compressed uploads: a 400/403/415 whose body is not JSON, or the Anthropic API's "request body is not valid JSON" after a proxy drops `Content-Encoding`, now resends the body uncompressed and stops compressing for that host ([#16108](https://github.com/can1357/oh-my-pi/pull/16108) by [@alphastorm](https://github.com/alphastorm))
|
|
10
|
+
|
|
5
11
|
## [18.12.0] - 2026-10-17
|
|
6
12
|
|
|
7
13
|
### Breaking Changes
|
|
@@ -2336,25 +2342,4 @@
|
|
|
2336
2342
|
|
|
2337
2343
|
- Removed the `partial-json` dependency; streaming JSON parsing now uses the in-house `RelaxedJson` parser.
|
|
2338
2344
|
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
### Added
|
|
2342
|
-
|
|
2343
|
-
- Added `llama.cpp` to the interactive `/login` provider list, accepting an optional API key while defaulting to local no-auth mode.
|
|
2344
|
-
|
|
2345
|
-
### Changed
|
|
2346
|
-
|
|
2347
|
-
- Optimized generated AI tool schemas by collapsing verbose `anyOf` unions into standard `enum` types
|
|
2348
|
-
|
|
2349
|
-
### Fixed
|
|
2350
|
-
|
|
2351
|
-
- Fixed tool-call argument validation dropping nested keys that were accidentally double-encoded
|
|
2352
|
-
- Fixed the `moonshot` provider being locked to the international Kimi host (`api.moonshot.ai`): OpenAI-completions requests now honor a `MOONSHOT_BASE_URL` override so users can reach the Kimi China platform (`api.moonshot.cn`), which rejects keys issued for the international endpoint. ([#2883](https://github.com/can1357/oh-my-pi/issues/2883))
|
|
2353
|
-
- Fixed tool-call argument validation dropping fields whose object keys were accidentally JSON-encoded a second time (e.g. `{ "\"op\"": "done" }`), which surfaced as spurious missing-required errors. A schema-agnostic pre-validation pass now recursively unwraps such double-encoded keys — through arrays and nested objects, and again after a JSON-string container is parsed — before the unrecognized-key repair can delete them.
|
|
2354
|
-
|
|
2355
|
-
### Removed
|
|
2356
|
-
|
|
2357
|
-
- Removed the `setNextRequestDebugPath`, `clearNextRequestDebugPath`, and `getNextRequestDebugPath` utility functions for request debugging, as request/response recording now relies exclusively on the `PI_REQ_DEBUG` environment variable.
|
|
2358
|
-
- Removed Wafer Pass (`wafer-pass`) login support; Wafer Serverless remains available as `wafer-serverless`.
|
|
2359
|
-
|
|
2360
|
-
Older entries are archived in [packages/ai/CHANGELOG.md@50ec66f75d57](https://github.com/can1357/oh-my-pi/blob/50ec66f75d57ad55838a9fe4da5e66f29af4d73c/packages/ai/CHANGELOG.md).
|
|
2345
|
+
Older entries are archived in [packages/ai/CHANGELOG.md@bb602d67c9b6](https://github.com/can1357/oh-my-pi/blob/bb602d67c9b685535bbf8ef1a1cc9de3c944ede4/packages/ai/CHANGELOG.md).
|
|
@@ -9,10 +9,13 @@ import type { FetchImpl, Model } from "../types.js";
|
|
|
9
9
|
* Install it beneath any body rewriter or signer (e.g. the cch attestation
|
|
10
10
|
* patch): those must see the uncompressed bytes. Other hosts, small bodies,
|
|
11
11
|
* bodies that already carry `Content-Encoding`, and `PI_REQUEST_COMPRESSION=0`
|
|
12
|
-
* pass through untouched. A possible rejection
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* pass through untouched. A possible rejection — the host's decode error, or a
|
|
13
|
+
* non-JSON 400/403/415 from a proxy in front of it — is resent uncompressed
|
|
14
|
+
* once; only when that resend succeeds is the encoding latched off for the
|
|
15
|
+
* host, so a genuine 400 never disables compression. When a non-JSON refusal
|
|
16
|
+
* recurs uncompressed with the same status and a non-JSON body, that status
|
|
17
|
+
* stops being re-checked for the host.
|
|
15
18
|
*/
|
|
16
19
|
export declare function wrapFetchForRequestCompression(base: FetchImpl, model: Model): FetchImpl;
|
|
17
|
-
/** Test hook: clears every per-host
|
|
20
|
+
/** Test hook: clears every per-host latch. */
|
|
18
21
|
export declare function resetRequestCompressionForTesting(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.12.
|
|
3
|
+
"version": "18.12.1",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.12.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.12.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.12.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.12.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.12.
|
|
158
|
+
"@oh-my-pi/omptype": "18.12.1",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.12.1",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.12.1",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.12.1",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.12.1"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
|
@@ -32,6 +32,17 @@ const ZSTD_LEVEL = 3;
|
|
|
32
32
|
*/
|
|
33
33
|
const rejectedEncodings = new Set<string>();
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `<origin> <status>` pairs where a non-JSON refusal of a compressed body
|
|
37
|
+
* recurred on the uncompressed resend: something in front of the host refuses
|
|
38
|
+
* every request (a blocking proxy, an edge challenge), not the encoding. Later
|
|
39
|
+
* refusals with that status skip the resend instead of doubling each attempt.
|
|
40
|
+
*/
|
|
41
|
+
const encodingUnrelatedRefusals = new Set<string>();
|
|
42
|
+
|
|
43
|
+
/** Why a failed compressed request may be an encoding rejection. */
|
|
44
|
+
type RejectionSuspect = "host" | "proxy";
|
|
45
|
+
|
|
35
46
|
type RequestHeaders = RequestInit["headers"];
|
|
36
47
|
|
|
37
48
|
function requestUrl(input: string | URL | Request): string | undefined {
|
|
@@ -67,16 +78,43 @@ function compress(body: string | ArrayBufferView, encoding: RequestBodyEncoding)
|
|
|
67
78
|
return encoding === "zstd" ? Bun.zstdCompress(bytes, { level: ZSTD_LEVEL }) : gzip(bytes, { level: GZIP_LEVEL });
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!policy.rejection) return true;
|
|
81
|
+
/** Reads a clone of {@link response}'s body; `undefined` when it cannot be read. */
|
|
82
|
+
async function bodyText(response: Response): Promise<string | undefined> {
|
|
73
83
|
try {
|
|
74
|
-
return
|
|
84
|
+
return await response.clone().text();
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isJson(text: string): boolean {
|
|
91
|
+
try {
|
|
92
|
+
JSON.parse(text);
|
|
93
|
+
return true;
|
|
75
94
|
} catch {
|
|
76
95
|
return false;
|
|
77
96
|
}
|
|
78
97
|
}
|
|
79
98
|
|
|
99
|
+
/**
|
|
100
|
+
* `host`: a JSON 400/415 matching the host's rejection pattern (or any JSON
|
|
101
|
+
* 400/415 when none is assigned). `proxy`: a 400/403/415 whose body is not JSON,
|
|
102
|
+
* empty included — provider APIs answer errors in JSON, so the body is presumed
|
|
103
|
+
* to come from a proxy or TLS-inspecting gateway that may refuse a compressed
|
|
104
|
+
* upload the host itself decodes.
|
|
105
|
+
*/
|
|
106
|
+
async function suspectEncodingRejection(
|
|
107
|
+
response: Response,
|
|
108
|
+
policy: RequestCompressionPolicy,
|
|
109
|
+
): Promise<RejectionSuspect | undefined> {
|
|
110
|
+
if (response.status !== 400 && response.status !== 403 && response.status !== 415) return undefined;
|
|
111
|
+
const body = await bodyText(response);
|
|
112
|
+
if (body === undefined) return undefined;
|
|
113
|
+
if (!isJson(body)) return "proxy";
|
|
114
|
+
if (response.status === 403) return undefined;
|
|
115
|
+
return (policy.rejection?.test(body) ?? true) ? "host" : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
/**
|
|
81
119
|
* Compresses request bodies from {@link model} to its provider's own endpoint
|
|
82
120
|
* when its catalog rules declare encodings the endpoint decodes
|
|
@@ -87,9 +125,12 @@ async function mayBeEncodingRejection(response: Response, policy: RequestCompres
|
|
|
87
125
|
* Install it beneath any body rewriter or signer (e.g. the cch attestation
|
|
88
126
|
* patch): those must see the uncompressed bytes. Other hosts, small bodies,
|
|
89
127
|
* bodies that already carry `Content-Encoding`, and `PI_REQUEST_COMPRESSION=0`
|
|
90
|
-
* pass through untouched. A possible rejection
|
|
91
|
-
*
|
|
92
|
-
*
|
|
128
|
+
* pass through untouched. A possible rejection — the host's decode error, or a
|
|
129
|
+
* non-JSON 400/403/415 from a proxy in front of it — is resent uncompressed
|
|
130
|
+
* once; only when that resend succeeds is the encoding latched off for the
|
|
131
|
+
* host, so a genuine 400 never disables compression. When a non-JSON refusal
|
|
132
|
+
* recurs uncompressed with the same status and a non-JSON body, that status
|
|
133
|
+
* stops being re-checked for the host.
|
|
93
134
|
*/
|
|
94
135
|
export function wrapFetchForRequestCompression(base: FetchImpl, model: Model): FetchImpl {
|
|
95
136
|
const wrapped: FetchImpl = async (input, init) => {
|
|
@@ -128,7 +169,10 @@ export function wrapFetchForRequestCompression(base: FetchImpl, model: Model): F
|
|
|
128
169
|
body: compressed,
|
|
129
170
|
headers: withContentEncoding(init?.headers, encoding),
|
|
130
171
|
});
|
|
131
|
-
|
|
172
|
+
const suspect = await suspectEncodingRejection(response, policy);
|
|
173
|
+
if (!suspect) return response;
|
|
174
|
+
const refusal = `${origin} ${response.status}`;
|
|
175
|
+
if (suspect === "proxy" && encodingUnrelatedRefusals.has(refusal)) return response;
|
|
132
176
|
|
|
133
177
|
await response.body?.cancel().catch(() => {});
|
|
134
178
|
const plain = await base(input, init);
|
|
@@ -139,6 +183,11 @@ export function wrapFetchForRequestCompression(base: FetchImpl, model: Model): F
|
|
|
139
183
|
encoding,
|
|
140
184
|
status: response.status,
|
|
141
185
|
});
|
|
186
|
+
} else if (suspect === "proxy" && plain.status === response.status) {
|
|
187
|
+
// A JSON body means the resend reached the provider; its own error says
|
|
188
|
+
// nothing about whether the proxy refuses every request.
|
|
189
|
+
const plainBody = await bodyText(plain);
|
|
190
|
+
if (plainBody !== undefined && !isJson(plainBody)) encodingUnrelatedRefusals.add(refusal);
|
|
142
191
|
}
|
|
143
192
|
return plain;
|
|
144
193
|
};
|
|
@@ -146,7 +195,8 @@ export function wrapFetchForRequestCompression(base: FetchImpl, model: Model): F
|
|
|
146
195
|
return wrapped;
|
|
147
196
|
}
|
|
148
197
|
|
|
149
|
-
/** Test hook: clears every per-host
|
|
198
|
+
/** Test hook: clears every per-host latch. */
|
|
150
199
|
export function resetRequestCompressionForTesting(): void {
|
|
151
200
|
rejectedEncodings.clear();
|
|
201
|
+
encodingUnrelatedRefusals.clear();
|
|
152
202
|
}
|