@oh-my-pi/pi-ai 16.3.0 → 16.3.3
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 +28 -0
- package/dist/types/auth-broker/remote-store.d.ts +3 -2
- package/dist/types/auth-storage.d.ts +7 -0
- package/dist/types/dialect/demotion.d.ts +15 -10
- package/dist/types/providers/anthropic.d.ts +10 -0
- package/dist/types/providers/cursor.d.ts +23 -2
- package/dist/types/providers/openai-responses.d.ts +2 -3
- package/dist/types/providers/openai-shared.d.ts +2 -2
- package/dist/types/usage/claude.d.ts +1 -1
- package/dist/types/utils/block-symbols.d.ts +16 -0
- package/dist/types/utils/proxy.d.ts +7 -1
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +109 -9
- package/src/auth-storage.ts +24 -10
- package/src/dialect/demotion.ts +18 -13
- package/src/providers/anthropic.ts +86 -2
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/cursor.ts +108 -2
- package/src/providers/devin.ts +15 -0
- package/src/providers/gitlab-duo-workflow.ts +87 -14
- package/src/providers/openai-completions.ts +10 -7
- package/src/providers/openai-responses.ts +12 -40
- package/src/providers/openai-shared.ts +8 -30
- package/src/providers/transform-messages.ts +16 -11
- package/src/usage/claude.ts +213 -20
- package/src/utils/block-symbols.ts +16 -0
- package/src/utils/proxy.ts +104 -44
- package/src/utils/validation.ts +64 -0
- package/src/providers/openai-responses-reasoning-suppression.md +0 -1
package/src/utils/proxy.ts
CHANGED
|
@@ -162,11 +162,26 @@ export function wrapFetchForProxy(fetchImpl: FetchImpl, provider: string): Fetch
|
|
|
162
162
|
return wrapped;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
export interface ConnectProxiedSocketOptions {
|
|
166
|
+
/** Caller cancellation for the proxy TCP/TLS handshake and CONNECT tunnel. */
|
|
167
|
+
signal?: AbortSignal;
|
|
168
|
+
/** Maximum wall-clock time to establish the final TLS tunnel. Disabled when absent or non-positive. */
|
|
169
|
+
timeoutMs?: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
165
172
|
/**
|
|
166
173
|
* Tunnel a socket connection through an HTTP CONNECT proxy.
|
|
167
174
|
* This is used specifically to wrap Node's `http2.connect(baseUrl, { createConnection })` for Cursor.
|
|
168
175
|
*/
|
|
169
|
-
export async function connectProxiedSocket(
|
|
176
|
+
export async function connectProxiedSocket(
|
|
177
|
+
proxyUrlStr: string,
|
|
178
|
+
targetUrlStr: string,
|
|
179
|
+
options?: ConnectProxiedSocketOptions,
|
|
180
|
+
): Promise<tls.TLSSocket> {
|
|
181
|
+
if (options?.signal?.aborted) {
|
|
182
|
+
throw new AIError.AbortError("Proxy tunnel aborted");
|
|
183
|
+
}
|
|
184
|
+
|
|
170
185
|
const proxyUrl = new URL(proxyUrlStr);
|
|
171
186
|
const targetUrl = new URL(targetUrlStr);
|
|
172
187
|
|
|
@@ -179,23 +194,73 @@ export async function connectProxiedSocket(proxyUrlStr: string, targetUrlStr: st
|
|
|
179
194
|
|
|
180
195
|
const { promise, resolve, reject } = Promise.withResolvers<tls.TLSSocket>();
|
|
181
196
|
|
|
182
|
-
let rawSocket: net.Socket;
|
|
183
|
-
if (useProxySsl) {
|
|
184
|
-
rawSocket = tls.connect({
|
|
185
|
-
host: proxyHost,
|
|
186
|
-
port: proxyPort,
|
|
187
|
-
});
|
|
188
|
-
} else {
|
|
189
|
-
rawSocket = net.connect({
|
|
190
|
-
host: proxyHost,
|
|
191
|
-
port: proxyPort,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
rawSocket.once("error", reject);
|
|
196
|
-
|
|
197
197
|
const readyEvent = useProxySsl ? "secureConnect" : "connect";
|
|
198
|
-
rawSocket.
|
|
198
|
+
let rawSocket: net.Socket | undefined;
|
|
199
|
+
let tunnelSocket: tls.TLSSocket | undefined;
|
|
200
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
201
|
+
let responseData = "";
|
|
202
|
+
let settled = false;
|
|
203
|
+
|
|
204
|
+
const cleanup = (): void => {
|
|
205
|
+
if (timeout) {
|
|
206
|
+
clearTimeout(timeout);
|
|
207
|
+
timeout = undefined;
|
|
208
|
+
}
|
|
209
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
210
|
+
rawSocket?.off("error", onRawError);
|
|
211
|
+
rawSocket?.off(readyEvent, onProxyReady);
|
|
212
|
+
rawSocket?.off("data", onProxyData);
|
|
213
|
+
tunnelSocket?.off("secureConnect", onTunnelReady);
|
|
214
|
+
tunnelSocket?.off("error", onTunnelError);
|
|
215
|
+
};
|
|
216
|
+
const destroyInProgress = (): void => {
|
|
217
|
+
tunnelSocket?.destroy();
|
|
218
|
+
rawSocket?.destroy();
|
|
219
|
+
};
|
|
220
|
+
const rejectOnce = (error: Error): void => {
|
|
221
|
+
if (settled) return;
|
|
222
|
+
settled = true;
|
|
223
|
+
cleanup();
|
|
224
|
+
destroyInProgress();
|
|
225
|
+
reject(error);
|
|
226
|
+
};
|
|
227
|
+
const resolveOnce = (socket: tls.TLSSocket): void => {
|
|
228
|
+
if (settled) return;
|
|
229
|
+
settled = true;
|
|
230
|
+
cleanup();
|
|
231
|
+
resolve(socket);
|
|
232
|
+
};
|
|
233
|
+
const onAbort = (): void => rejectOnce(new AIError.AbortError("Proxy tunnel aborted"));
|
|
234
|
+
const onRawError = (error: Error): void => rejectOnce(error);
|
|
235
|
+
const onTunnelError = (error: Error): void => rejectOnce(error);
|
|
236
|
+
const onTunnelReady = (): void => {
|
|
237
|
+
if (!tunnelSocket) return;
|
|
238
|
+
resolveOnce(tunnelSocket);
|
|
239
|
+
};
|
|
240
|
+
const onProxyData = (chunk: Buffer): void => {
|
|
241
|
+
if (!rawSocket) return;
|
|
242
|
+
responseData += chunk.toString("binary");
|
|
243
|
+
if (!responseData.includes("\r\n\r\n")) return;
|
|
244
|
+
|
|
245
|
+
rawSocket.off("data", onProxyData);
|
|
246
|
+
rawSocket.off("error", onRawError);
|
|
247
|
+
|
|
248
|
+
const firstLine = responseData.split("\r\n")[0];
|
|
249
|
+
if (!firstLine.includes(" 200 ")) {
|
|
250
|
+
rejectOnce(new AIError.ValidationError(`Proxy tunnel failed: ${firstLine}`));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
tunnelSocket = tls.connect({
|
|
255
|
+
socket: rawSocket,
|
|
256
|
+
servername: targetHost,
|
|
257
|
+
ALPNProtocols: ["h2"],
|
|
258
|
+
});
|
|
259
|
+
tunnelSocket.once("secureConnect", onTunnelReady);
|
|
260
|
+
tunnelSocket.once("error", onTunnelError);
|
|
261
|
+
};
|
|
262
|
+
const onProxyReady = (): void => {
|
|
263
|
+
if (!rawSocket) return;
|
|
199
264
|
let connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\n` + `Host: ${targetHost}:${targetPort}\r\n`;
|
|
200
265
|
|
|
201
266
|
if (proxyUrl.username || proxyUrl.password) {
|
|
@@ -207,34 +272,29 @@ export async function connectProxiedSocket(proxyUrlStr: string, targetUrlStr: st
|
|
|
207
272
|
connectReq += "\r\n";
|
|
208
273
|
|
|
209
274
|
rawSocket.write(connectReq);
|
|
275
|
+
rawSocket.on("data", onProxyData);
|
|
276
|
+
};
|
|
210
277
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
rawSocket.destroy();
|
|
232
|
-
reject(new AIError.ValidationError(`Proxy tunnel failed: ${firstLine}`));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
rawSocket.on("data", onData);
|
|
237
|
-
});
|
|
278
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
279
|
+
if (options?.timeoutMs !== undefined && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0) {
|
|
280
|
+
const timeoutMs = Math.trunc(options.timeoutMs);
|
|
281
|
+
timeout = setTimeout(() => {
|
|
282
|
+
rejectOnce(new AIError.StreamTimeoutError(`Proxy tunnel timed out after ${timeoutMs}ms`));
|
|
283
|
+
}, timeoutMs);
|
|
284
|
+
timeout.unref?.();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
rawSocket = useProxySsl
|
|
288
|
+
? tls.connect({
|
|
289
|
+
host: proxyHost,
|
|
290
|
+
port: proxyPort,
|
|
291
|
+
})
|
|
292
|
+
: net.connect({
|
|
293
|
+
host: proxyHost,
|
|
294
|
+
port: proxyPort,
|
|
295
|
+
});
|
|
296
|
+
rawSocket.once("error", onRawError);
|
|
297
|
+
rawSocket.once(readyEvent, onProxyReady);
|
|
238
298
|
|
|
239
299
|
return promise;
|
|
240
300
|
}
|
package/src/utils/validation.ts
CHANGED
|
@@ -1081,6 +1081,54 @@ function normalizeStringEncodedArrayUnions(schema: unknown, value: unknown): { v
|
|
|
1081
1081
|
return { value: changed ? nextValue : valueObject, changed };
|
|
1082
1082
|
}
|
|
1083
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* Name of the sole property when a schema declares exactly one required string
|
|
1086
|
+
* field, else `undefined`. Recognizes the closed single-argument tool shape
|
|
1087
|
+
* (`{ type: "object", properties: { X: { type: "string" } }, required: ["X"] }`).
|
|
1088
|
+
*/
|
|
1089
|
+
function singleRequiredStringKey(schema: unknown): string | undefined {
|
|
1090
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return undefined;
|
|
1091
|
+
const obj = schema as Record<string, unknown>;
|
|
1092
|
+
if (obj.type !== "object") return undefined;
|
|
1093
|
+
const properties = obj.properties;
|
|
1094
|
+
if (!properties || typeof properties !== "object") return undefined;
|
|
1095
|
+
const keys = Object.keys(properties as Record<string, unknown>);
|
|
1096
|
+
if (keys.length !== 1) return undefined;
|
|
1097
|
+
const key = keys[0];
|
|
1098
|
+
const required = obj.required;
|
|
1099
|
+
if (!Array.isArray(required) || required.length !== 1 || required[0] !== key) return undefined;
|
|
1100
|
+
const propertySchema = (properties as Record<string, unknown>)[key];
|
|
1101
|
+
if (!propertySchema || typeof propertySchema !== "object") return undefined;
|
|
1102
|
+
return (propertySchema as Record<string, unknown>).type === "string" ? key : undefined;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* LLM-quirk repair for single-argument tools. When a tool declares exactly one
|
|
1107
|
+
* property — a required string — some providers deliver the payload under a
|
|
1108
|
+
* different key (e.g. the `edit` tool's patch arriving as `input`/`_input`, or
|
|
1109
|
+
* any single-string tool whose argument the model mislabels). When the declared
|
|
1110
|
+
* key is absent but another field holds a string, adopt the first such string
|
|
1111
|
+
* as the declared key so the call validates instead of failing with "<key> was
|
|
1112
|
+
* missing". A present-but-wrong-type value is left alone so its real type error
|
|
1113
|
+
* still surfaces.
|
|
1114
|
+
*/
|
|
1115
|
+
function normalizeSingleStringField(schema: unknown, value: unknown): { value: unknown; changed: boolean } {
|
|
1116
|
+
const key = singleRequiredStringKey(schema);
|
|
1117
|
+
if (key === undefined) return { value, changed: false };
|
|
1118
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return { value, changed: false };
|
|
1119
|
+
const record = value as Record<string, unknown>;
|
|
1120
|
+
if (record[key] !== undefined) return { value, changed: false };
|
|
1121
|
+
for (const candidate in record) {
|
|
1122
|
+
if (candidate === key || !Object.hasOwn(record, candidate)) continue;
|
|
1123
|
+
const candidateValue = record[candidate];
|
|
1124
|
+
if (typeof candidateValue !== "string") continue;
|
|
1125
|
+
const next = { ...record, [key]: candidateValue };
|
|
1126
|
+
delete next[candidate];
|
|
1127
|
+
return { value: next, changed: true };
|
|
1128
|
+
}
|
|
1129
|
+
return { value, changed: false };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1084
1132
|
// ============================================================================
|
|
1085
1133
|
// Zod issue → coercion bridge
|
|
1086
1134
|
// ============================================================================
|
|
@@ -1447,6 +1495,14 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1447
1495
|
changed = true;
|
|
1448
1496
|
}
|
|
1449
1497
|
|
|
1498
|
+
// Single-argument tools (e.g. `edit`): if the model put the lone required
|
|
1499
|
+
// string under a different key, adopt the first string field as that key.
|
|
1500
|
+
const singleStringNorm = normalizeSingleStringField(json, normalizedArgs);
|
|
1501
|
+
if (singleStringNorm.changed) {
|
|
1502
|
+
normalizedArgs = singleStringNorm.value;
|
|
1503
|
+
changed = true;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1450
1506
|
let result = validateContext(ctx, normalizedArgs);
|
|
1451
1507
|
if (result.success) return result.value as ToolCall["arguments"];
|
|
1452
1508
|
|
|
@@ -1480,6 +1536,14 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1480
1536
|
normalizedArgs = stringEncodedArrayNormPass.value;
|
|
1481
1537
|
}
|
|
1482
1538
|
|
|
1539
|
+
// Re-run single-string remap: `coerceArgsFromIssues` may have just
|
|
1540
|
+
// unwrapped a JSON-stringified root object, exposing a mislabelled lone
|
|
1541
|
+
// string field the initial pre-pass could not see.
|
|
1542
|
+
const singleStringNormPass = normalizeSingleStringField(json, normalizedArgs);
|
|
1543
|
+
if (singleStringNormPass.changed) {
|
|
1544
|
+
normalizedArgs = singleStringNormPass.value;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1483
1547
|
result = validateContext(ctx, normalizedArgs);
|
|
1484
1548
|
if (result.success) return result.value as ToolCall["arguments"];
|
|
1485
1549
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Keep internal reasoning brief. Continue following the task and use tools normally.
|