@oh-my-pi/pi-ai 15.1.3 → 15.1.5
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 +12 -0
- package/dist/types/providers/aws-credentials.d.ts +11 -0
- package/dist/types/types.d.ts +5 -2
- package/dist/types/utils/schema/index.d.ts +1 -0
- package/dist/types/utils/schema/normalize.d.ts +4 -3
- package/dist/types/utils/schema/wire.d.ts +13 -1
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/package.json +2 -2
- package/src/auth-gateway/server.ts +42 -0
- package/src/providers/amazon-bedrock.ts +2 -1
- package/src/providers/anthropic.ts +1 -0
- package/src/providers/aws-credentials.ts +167 -0
- package/src/providers/azure-openai-responses.ts +2 -1
- package/src/providers/cursor.ts +2 -1
- package/src/providers/google-gemini-cli.ts +2 -1
- package/src/providers/google-shared.ts +2 -1
- package/src/providers/ollama.ts +2 -1
- package/src/providers/openai-codex-responses.ts +13 -3
- package/src/providers/openai-completions.ts +1 -0
- package/src/providers/openai-responses.ts +2 -1
- package/src/providers/transform-messages.ts +79 -1
- package/src/stream.ts +80 -28
- package/src/types.ts +5 -2
- package/src/utils/schema/index.ts +1 -0
- package/src/utils/schema/normalize.ts +117 -32
- package/src/utils/schema/wire.ts +17 -2
- package/src/utils/schema/zod-decontaminate.ts +322 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.1.4] - 2026-05-19
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Updated auth-gateway format and pi-native request handling to invalidate the failed API key and retry the provider request with a replacement key when authentication fails
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed OpenAI Responses and Codex tool schema normalization to emit `properties: {}` for no-argument object schemas without rewriting literal payloads. ([#1147](https://github.com/can1357/oh-my-pi/issues/1147))
|
|
13
|
+
- Fixed Anthropic 400 (`unexpected tool_use_id found in tool_result blocks ... Each tool_result block must have a corresponding tool_use block in the previous message`) when handoff/compaction folds an assistant `tool_use` into the handoff summary string but leaves the matching user-side `tool_result` message in the history. `transformMessages` now indexes every `tool_use` id surviving the first pass and drops orphan `tool_result` messages whose originator was compacted away, preserving the text payload as a user-level `<stale-tool-result>` note so the model still sees what the tool returned. The note is emitted with `role: "user"` rather than `role: "developer"` so providers that elevate developer-role messages (Ollama: `developer` → `system`; OpenAI chat-completions reasoning models: `developer` → `developer`) cannot lift stale tool output to an instruction-priority tier above the surrounding user/developer messages.
|
|
14
|
+
- Fixed streaming authentication retry to trigger when a provider emits a 401 `error` event after a `start` event but before any replay-unsafe content is emitted
|
|
15
|
+
- Added `credential_process` support to the Bedrock provider's AWS credential resolver so profiles delegating to external brokers (`aws-vault`, `granted`, in-house tools) resolve instead of falling through to `Unable to resolve AWS credentials`. Parses the AWS SDK `Version: 1` JSON envelope, honors `Expiration` in the per-profile cache, propagates `AbortSignal` to the spawned helper, routes Windows `.cmd`/`.bat` helpers through `cmd.exe /c`, and ships a POSIX-shell-style tokenizer that preserves backslashes inside double quotes so Windows paths survive ([#1142](https://github.com/can1357/oh-my-pi/issues/1142))
|
|
16
|
+
|
|
5
17
|
## [15.1.3] - 2026-05-17
|
|
6
18
|
### Breaking Changes
|
|
7
19
|
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* - SSO profile referencing a cached token in `~/.aws/sso/cache/*.json`,
|
|
10
10
|
* which we exchange for short-lived role credentials via
|
|
11
11
|
* `https://portal.sso.{region}.amazonaws.com/federation/credentials`.
|
|
12
|
+
* - `credential_process` — an external command emitting the AWS SDK
|
|
13
|
+
* `Version: 1` JSON envelope on stdout. Used by `aws-vault`, `granted`,
|
|
14
|
+
* in-house brokers, etc.
|
|
12
15
|
* 3. EC2 IMDSv2 (only when `AWS_EC2_METADATA_DISABLED` is unset / falsey and
|
|
13
16
|
* `169.254.169.254` is reachable within a 1 s timeout).
|
|
14
17
|
*
|
|
@@ -28,5 +31,13 @@ export interface CredentialResolveOptions {
|
|
|
28
31
|
signal?: AbortSignal;
|
|
29
32
|
}
|
|
30
33
|
export declare function resolveAwsCredentials(opts?: CredentialResolveOptions): Promise<ResolvedCredentials>;
|
|
34
|
+
/** POSIX-shell-style tokenizer used by the AWS CLI for `credential_process`.
|
|
35
|
+
*
|
|
36
|
+
* Outside quotes a backslash escapes the next character. Inside single quotes
|
|
37
|
+
* everything is literal (no escapes, cannot contain `'`). Inside double quotes
|
|
38
|
+
* a backslash only escapes `$`, `` ` ``, `"`, and `\` — every other backslash
|
|
39
|
+
* is preserved verbatim, which is what makes Windows paths like
|
|
40
|
+
* `"C:\Program Files\tool\auth.exe"` survive tokenization. */
|
|
41
|
+
export declare function tokenizeCredentialProcessCommand(cmd: string): string[];
|
|
31
42
|
/** Test/diagnostic helper — drops cached credentials. */
|
|
32
43
|
export declare function clearAwsCredentialCache(): void;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -126,8 +126,9 @@ export interface StreamOptions {
|
|
|
126
126
|
signal?: AbortSignal;
|
|
127
127
|
apiKey?: string;
|
|
128
128
|
/**
|
|
129
|
-
* Called when a provider returns 401 before any assistant
|
|
130
|
-
* emitted. Returning a different key retries the provider
|
|
129
|
+
* Called when a provider returns 401 before any replay-unsafe assistant
|
|
130
|
+
* event has been emitted. Returning a different key retries the provider
|
|
131
|
+
* request once.
|
|
131
132
|
*/
|
|
132
133
|
onAuthError?: (provider: string, apiKey: string, error: unknown) => Promise<string | undefined>;
|
|
133
134
|
cacheRetention?: CacheRetention;
|
|
@@ -368,6 +369,8 @@ export interface AssistantMessage {
|
|
|
368
369
|
usage: Usage;
|
|
369
370
|
stopReason: StopReason;
|
|
370
371
|
errorMessage?: string;
|
|
372
|
+
/** HTTP status surfaced by the provider when the request failed. Populated by every provider's catch block alongside `errorMessage` so consumers (auth retry, telemetry, UI) can branch without regex-scraping the message. */
|
|
373
|
+
errorStatus?: number;
|
|
371
374
|
/** Provider-specific opaque payload used to reconstruct transport-native history. */
|
|
372
375
|
providerPayload?: ProviderPayload;
|
|
373
376
|
timestamp: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DescriptionSpillFormat } from "./spill";
|
|
2
|
-
import type
|
|
2
|
+
import { type JsonObject } from "./types";
|
|
3
3
|
export type ResidualSchemaIncompatibility = "type-array" | "type-null" | "nullable" | "combiners";
|
|
4
4
|
export interface NormalizeSchemaOptions {
|
|
5
5
|
unsupportedFields: (key: string) => boolean;
|
|
@@ -37,8 +37,9 @@ export declare function normalizeSchemaForCCA(value: unknown): unknown;
|
|
|
37
37
|
export declare function normalizeSchemaForMCP(value: unknown): unknown;
|
|
38
38
|
/**
|
|
39
39
|
* OpenAI Responses rejects `oneOf` in tool schemas even when strict mode is
|
|
40
|
-
* disabled
|
|
41
|
-
*
|
|
40
|
+
* disabled, and rejects every schema node with `type: "object"` unless it has
|
|
41
|
+
* a `properties` member. Normalize only schema-valued positions so literal
|
|
42
|
+
* payloads under `enum`, `const`, `default`, and `examples` remain unchanged.
|
|
42
43
|
*
|
|
43
44
|
* Identity-preserving: returns the input reference unchanged when no rewrite
|
|
44
45
|
* occurred so callers can dedupe via reference equality (and the strict-mode
|
|
@@ -12,7 +12,19 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type ZodType } from "zod/v4";
|
|
14
14
|
import type { Tool } from "../../types";
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* True when `value` is a live Zod schema instance.
|
|
17
|
+
*
|
|
18
|
+
* The check is stricter than "has a `_zod` property" because a JSON
|
|
19
|
+
* round-trip preserves the `_zod` key as a plain object and would otherwise
|
|
20
|
+
* fool the predicate — see issue #1101, where MCP servers ship
|
|
21
|
+
* `JSON.stringify(zodSchemaInstance)` as a tool's `inputSchema` and the
|
|
22
|
+
* resulting plain object then explodes `z.toJSONSchema` because the prototype
|
|
23
|
+
* (and every Zod parsing method) is gone.
|
|
24
|
+
*
|
|
25
|
+
* Live Zod instances always carry a `.parse` function on the prototype;
|
|
26
|
+
* impostors do not.
|
|
27
|
+
*/
|
|
16
28
|
export declare function isZodSchema(value: unknown): value is ZodType;
|
|
17
29
|
/** Convert a Zod schema into the JSON Schema shape providers consume. */
|
|
18
30
|
export declare function zodToWireSchema(schema: ZodType): Record<string, unknown>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive rewrite for nodes that look like `JSON.stringify(zodSchemaInstance)`
|
|
3
|
+
* output rather than JSON Schema. MCP servers using Zod 4 sometimes ship a
|
|
4
|
+
* serialised schema instance directly as a tool's `inputSchema`, because the
|
|
5
|
+
* fields Zod surfaces on its instances (`type`, `enum`, `options`, `def`) shadow
|
|
6
|
+
* (and clash with) JSON Schema keywords. The resulting payload is neither valid
|
|
7
|
+
* Zod nor valid JSON Schema 2020-12 and Anthropic's strict validator rejects
|
|
8
|
+
* the whole tool list.
|
|
9
|
+
*
|
|
10
|
+
* Symptoms we've observed (gitnexus_impact.direction):
|
|
11
|
+
* {
|
|
12
|
+
* def: { type: "enum", entries: { upstream: "upstream", ... } },
|
|
13
|
+
* type: "enum", // <- invalid `type` value
|
|
14
|
+
* enum: { upstream: "upstream", ... }, // <- `enum` MUST be an array
|
|
15
|
+
* options: ["upstream", "downstream"],
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* This module recognises the shape (`def.type === node.type` and `def.type` is
|
|
19
|
+
* a known Zod kind) and rewrites it to clean JSON Schema where deterministic.
|
|
20
|
+
* For Zod kinds we don't fully model, we strip the toxic siblings (`def`,
|
|
21
|
+
* `options`, object-shaped `enum`) and drop an invalid `type` so the remainder
|
|
22
|
+
* passes meta-schema validation as a permissive node.
|
|
23
|
+
*
|
|
24
|
+
* Pure / identity-preserving: returns the input reference when nothing changes.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Walks a JSON value and rewrites every Zod-instance-shaped node into clean
|
|
28
|
+
* JSON Schema 2020-12. Identity-preserving when no rewrite fires. Tolerates
|
|
29
|
+
* self-referential graphs — a revisited node returns as-is.
|
|
30
|
+
*/
|
|
31
|
+
export declare function decontaminateZodInstance(value: unknown): unknown;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.1.
|
|
4
|
+
"version": "15.1.5",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@oh-my-pi/pi-utils": "15.1.
|
|
46
|
+
"@oh-my-pi/pi-utils": "15.1.5",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -231,6 +231,26 @@ function classifyGatewayError(err: unknown): { status: number; type: string; mes
|
|
|
231
231
|
return { status: 502, type: "upstream_error", message };
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
async function refreshGatewayApiKeyAfterAuthError(
|
|
235
|
+
storage: AuthStorage,
|
|
236
|
+
model: Model<Api>,
|
|
237
|
+
provider: string,
|
|
238
|
+
oldKey: string,
|
|
239
|
+
error: unknown,
|
|
240
|
+
signal: AbortSignal,
|
|
241
|
+
format: string,
|
|
242
|
+
peer: string,
|
|
243
|
+
): Promise<string | undefined> {
|
|
244
|
+
await storage.invalidateCredentialMatching(provider, oldKey, signal);
|
|
245
|
+
logger.debug("auth-gateway retrying provider request after credential invalidation", {
|
|
246
|
+
format,
|
|
247
|
+
provider,
|
|
248
|
+
peer,
|
|
249
|
+
error: error instanceof Error ? error.message : String(error),
|
|
250
|
+
});
|
|
251
|
+
return storage.getApiKey(provider, undefined, { modelId: model.id, signal });
|
|
252
|
+
}
|
|
253
|
+
|
|
234
254
|
function clientClosedResponse(route: { module: FormatModule }): Response {
|
|
235
255
|
return route.module.formatError(499, "request_aborted", "client closed request");
|
|
236
256
|
}
|
|
@@ -332,6 +352,17 @@ async function handleFormatEndpoint(
|
|
|
332
352
|
|
|
333
353
|
const streamOpts = buildStreamOptions(parsed, model.api, controller.signal);
|
|
334
354
|
streamOpts.apiKey = apiKey;
|
|
355
|
+
streamOpts.onAuthError = (provider, oldKey, error) =>
|
|
356
|
+
refreshGatewayApiKeyAfterAuthError(
|
|
357
|
+
bootOpts.storage,
|
|
358
|
+
model,
|
|
359
|
+
provider,
|
|
360
|
+
oldKey,
|
|
361
|
+
error,
|
|
362
|
+
controller.signal,
|
|
363
|
+
route.label,
|
|
364
|
+
peer,
|
|
365
|
+
);
|
|
335
366
|
|
|
336
367
|
logger.info("auth-gateway request", {
|
|
337
368
|
format: route.label,
|
|
@@ -469,6 +500,17 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
469
500
|
// only inject server-controlled fields. The codex temperature/topP strip
|
|
470
501
|
// matches `buildStreamOptions` — Codex rejects them with a 400.
|
|
471
502
|
const streamOpts: SimpleStreamOptions = { ...parsed.options, apiKey, signal: controller.signal };
|
|
503
|
+
streamOpts.onAuthError = (provider, oldKey, error) =>
|
|
504
|
+
refreshGatewayApiKeyAfterAuthError(
|
|
505
|
+
bootOpts.storage,
|
|
506
|
+
model,
|
|
507
|
+
provider,
|
|
508
|
+
oldKey,
|
|
509
|
+
error,
|
|
510
|
+
controller.signal,
|
|
511
|
+
"pi-native",
|
|
512
|
+
peer,
|
|
513
|
+
);
|
|
472
514
|
if (model.api === "openai-codex-responses") {
|
|
473
515
|
delete streamOpts.temperature;
|
|
474
516
|
delete streamOpts.topP;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Bun's native `HTTPS_PROXY` support.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { $env, $flag, fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { $env, $flag, extractHttpStatusFromError, fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import type { Effort } from "../model-thinking";
|
|
12
12
|
import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "../model-thinking";
|
|
13
13
|
import { calculateCost } from "../models";
|
|
@@ -333,6 +333,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
333
333
|
delete (block as Block).partialJson;
|
|
334
334
|
}
|
|
335
335
|
output.stopReason = options.signal?.aborted ? "aborted" : "error";
|
|
336
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
336
337
|
const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
337
338
|
// Enrich error with thinking block diagnostics for signature-related failures
|
|
338
339
|
let diagnostics = "";
|
|
@@ -1324,6 +1324,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1324
1324
|
}
|
|
1325
1325
|
const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
|
|
1326
1326
|
output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
1327
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
1327
1328
|
output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
|
|
1328
1329
|
output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
|
|
1329
1330
|
output.duration = Date.now() - startTime;
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* - SSO profile referencing a cached token in `~/.aws/sso/cache/*.json`,
|
|
10
10
|
* which we exchange for short-lived role credentials via
|
|
11
11
|
* `https://portal.sso.{region}.amazonaws.com/federation/credentials`.
|
|
12
|
+
* - `credential_process` — an external command emitting the AWS SDK
|
|
13
|
+
* `Version: 1` JSON envelope on stdout. Used by `aws-vault`, `granted`,
|
|
14
|
+
* in-house brokers, etc.
|
|
12
15
|
* 3. EC2 IMDSv2 (only when `AWS_EC2_METADATA_DISABLED` is unset / falsey and
|
|
13
16
|
* `169.254.169.254` is reachable within a 1 s timeout).
|
|
14
17
|
*
|
|
@@ -162,6 +165,10 @@ async function readProfileCredentials(
|
|
|
162
165
|
return readSsoCredentials(merged, configIni, region, signal);
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
if (merged.credential_process) {
|
|
169
|
+
return readCredentialProcess(profile, merged.credential_process, signal);
|
|
170
|
+
}
|
|
171
|
+
|
|
165
172
|
return undefined;
|
|
166
173
|
}
|
|
167
174
|
|
|
@@ -276,6 +283,166 @@ async function sha1Hex(input: string): Promise<string> {
|
|
|
276
283
|
return out;
|
|
277
284
|
}
|
|
278
285
|
|
|
286
|
+
// ---------- credential_process ----------
|
|
287
|
+
|
|
288
|
+
/** JSON envelope emitted by an external credential process. Matches the
|
|
289
|
+
* AWS CLI / SDK contract documented at
|
|
290
|
+
* https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html */
|
|
291
|
+
interface CredentialProcessEnvelope {
|
|
292
|
+
Version?: number;
|
|
293
|
+
AccessKeyId?: string;
|
|
294
|
+
SecretAccessKey?: string;
|
|
295
|
+
SessionToken?: string;
|
|
296
|
+
Expiration?: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function readCredentialProcess(
|
|
300
|
+
profile: string,
|
|
301
|
+
command: string,
|
|
302
|
+
signal: AbortSignal | undefined,
|
|
303
|
+
): Promise<ResolvedCredentials> {
|
|
304
|
+
const argv = buildCredentialProcessArgv(profile, command);
|
|
305
|
+
|
|
306
|
+
const child = Bun.spawn(argv, {
|
|
307
|
+
stdin: "ignore",
|
|
308
|
+
stdout: "pipe",
|
|
309
|
+
stderr: "pipe",
|
|
310
|
+
windowsHide: true,
|
|
311
|
+
signal,
|
|
312
|
+
});
|
|
313
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
314
|
+
new Response(child.stdout).text(),
|
|
315
|
+
new Response(child.stderr).text(),
|
|
316
|
+
child.exited,
|
|
317
|
+
]);
|
|
318
|
+
if (exitCode !== 0) {
|
|
319
|
+
const tail = stderr.trim().slice(-512) || stdout.trim().slice(-512) || "(no output)";
|
|
320
|
+
throw new Error(`AWS credential_process for profile '${profile}' exited ${exitCode}: ${tail}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let parsed: CredentialProcessEnvelope;
|
|
324
|
+
try {
|
|
325
|
+
parsed = JSON.parse(stdout) as CredentialProcessEnvelope;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
throw new Error(`AWS credential_process for profile '${profile}' did not emit valid JSON: ${String(err)}`);
|
|
328
|
+
}
|
|
329
|
+
if (parsed.Version !== 1) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`AWS credential_process for profile '${profile}' returned unsupported Version ${parsed.Version ?? "<missing>"}; expected 1.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (!parsed.AccessKeyId || !parsed.SecretAccessKey) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`AWS credential_process for profile '${profile}' returned envelope without AccessKeyId/SecretAccessKey.`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const out: ResolvedCredentials = {
|
|
341
|
+
accessKeyId: parsed.AccessKeyId,
|
|
342
|
+
secretAccessKey: parsed.SecretAccessKey,
|
|
343
|
+
};
|
|
344
|
+
if (parsed.SessionToken) out.sessionToken = parsed.SessionToken;
|
|
345
|
+
if (parsed.Expiration) {
|
|
346
|
+
const exp = Date.parse(parsed.Expiration);
|
|
347
|
+
if (!Number.isNaN(exp)) out.expiresAt = exp;
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Resolve the argv for `Bun.spawn`. On Windows we route `.cmd`/`.bat` helpers
|
|
353
|
+
* through `cmd.exe /c` because direct execution refuses batch files (mirrors
|
|
354
|
+
* Node's `execFile` policy and avoids surprise no-ops). */
|
|
355
|
+
function buildCredentialProcessArgv(profile: string, command: string): string[] {
|
|
356
|
+
const tokens = tokenizeCredentialProcessCommand(command);
|
|
357
|
+
if (tokens.length === 0) {
|
|
358
|
+
throw new Error(`AWS credential_process for profile '${profile}' is empty.`);
|
|
359
|
+
}
|
|
360
|
+
if (process.platform === "win32" && isBatchScript(tokens[0])) {
|
|
361
|
+
return ["cmd.exe", "/d", "/s", "/c", command];
|
|
362
|
+
}
|
|
363
|
+
return tokens;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isBatchScript(executable: string): boolean {
|
|
367
|
+
const lower = executable.toLowerCase();
|
|
368
|
+
return lower.endsWith(".cmd") || lower.endsWith(".bat");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** POSIX-shell-style tokenizer used by the AWS CLI for `credential_process`.
|
|
372
|
+
*
|
|
373
|
+
* Outside quotes a backslash escapes the next character. Inside single quotes
|
|
374
|
+
* everything is literal (no escapes, cannot contain `'`). Inside double quotes
|
|
375
|
+
* a backslash only escapes `$`, `` ` ``, `"`, and `\` — every other backslash
|
|
376
|
+
* is preserved verbatim, which is what makes Windows paths like
|
|
377
|
+
* `"C:\Program Files\tool\auth.exe"` survive tokenization. */
|
|
378
|
+
export function tokenizeCredentialProcessCommand(cmd: string): string[] {
|
|
379
|
+
const tokens: string[] = [];
|
|
380
|
+
let current = "";
|
|
381
|
+
let hasToken = false;
|
|
382
|
+
let mode: "normal" | "single" | "double" = "normal";
|
|
383
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
384
|
+
const ch = cmd[i];
|
|
385
|
+
if (mode === "normal") {
|
|
386
|
+
if (ch === "'") {
|
|
387
|
+
mode = "single";
|
|
388
|
+
hasToken = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (ch === '"') {
|
|
392
|
+
mode = "double";
|
|
393
|
+
hasToken = true;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (ch === "\\" && i + 1 < cmd.length) {
|
|
397
|
+
current += cmd[++i];
|
|
398
|
+
hasToken = true;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
402
|
+
if (hasToken) {
|
|
403
|
+
tokens.push(current);
|
|
404
|
+
current = "";
|
|
405
|
+
hasToken = false;
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
current += ch;
|
|
410
|
+
hasToken = true;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (mode === "single") {
|
|
414
|
+
if (ch === "'") {
|
|
415
|
+
mode = "normal";
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
current += ch;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
// double-quote
|
|
422
|
+
if (ch === '"') {
|
|
423
|
+
mode = "normal";
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (ch === "\\" && i + 1 < cmd.length) {
|
|
427
|
+
const next = cmd[i + 1];
|
|
428
|
+
if (next === "$" || next === "`" || next === '"' || next === "\\") {
|
|
429
|
+
current += next;
|
|
430
|
+
i++;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
// Preserve literal backslash for Windows paths.
|
|
434
|
+
current += ch;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
current += ch;
|
|
438
|
+
}
|
|
439
|
+
if (mode !== "normal") {
|
|
440
|
+
throw new Error("AWS credential_process command has an unterminated quote.");
|
|
441
|
+
}
|
|
442
|
+
if (hasToken) tokens.push(current);
|
|
443
|
+
return tokens;
|
|
444
|
+
}
|
|
445
|
+
|
|
279
446
|
// ---------- IMDSv2 ----------
|
|
280
447
|
|
|
281
448
|
const IMDS_HOST = "169.254.169.254";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { $env, extractHttpStatusFromError } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { AzureOpenAI } from "openai";
|
|
3
3
|
import type {
|
|
4
4
|
Tool as OpenAITool,
|
|
@@ -173,6 +173,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|
|
173
173
|
for (const block of output.content) delete (block as { index?: number }).index;
|
|
174
174
|
const firstEventTimeoutError = abortTracker.getLocalAbortReason();
|
|
175
175
|
output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
176
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
176
177
|
output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
|
|
177
178
|
output.duration = Date.now() - startTime;
|
|
178
179
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
package/src/providers/cursor.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import http2 from "node:http2";
|
|
4
4
|
import { create, fromBinary, fromJson, type JsonValue, toBinary, toJson } from "@bufbuild/protobuf";
|
|
5
5
|
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
6
|
-
import { $env, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { $env, extractHttpStatusFromError, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { calculateCost } from "../models";
|
|
8
8
|
import type {
|
|
9
9
|
Api,
|
|
@@ -547,6 +547,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
547
547
|
stream.end();
|
|
548
548
|
} catch (error) {
|
|
549
549
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
550
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
550
551
|
output.errorMessage = formatErrorMessageWithRetryAfter(error);
|
|
551
552
|
output.duration = Date.now() - startTime;
|
|
552
553
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { scheduler } from "node:timers/promises";
|
|
8
|
-
import { fetchWithRetry, readSseJson } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { extractHttpStatusFromError, fetchWithRetry, readSseJson } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { calculateCost } from "../models";
|
|
10
10
|
import type {
|
|
11
11
|
Api,
|
|
@@ -596,6 +596,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
596
596
|
}
|
|
597
597
|
}
|
|
598
598
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
599
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
599
600
|
output.errorMessage = await appendRawHttpRequestDumpFor400(
|
|
600
601
|
error instanceof Error ? error.message : JSON.stringify(error),
|
|
601
602
|
error,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared utilities for Google Generative AI and Google Cloud Code Assist providers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { readSseJson } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { extractHttpStatusFromError, readSseJson } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { calculateCost } from "../models";
|
|
7
7
|
import type {
|
|
8
8
|
Api,
|
|
@@ -836,6 +836,7 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
836
836
|
}
|
|
837
837
|
}
|
|
838
838
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
839
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
839
840
|
output.errorMessage = await finalizeErrorMessage(error, rawRequestDump);
|
|
840
841
|
output.duration = Date.now() - startTime;
|
|
841
842
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
package/src/providers/ollama.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { extractHttpStatusFromError, fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { getEnvApiKey } from "../stream";
|
|
3
3
|
import type {
|
|
4
4
|
Api,
|
|
@@ -505,6 +505,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
output.stopReason = options.signal?.aborted ? "aborted" : "error";
|
|
508
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
508
509
|
output.errorMessage = await finalizeErrorMessage(error, rawRequestDump);
|
|
509
510
|
output.duration = Date.now() - startTime;
|
|
510
511
|
if (firstTokenTime) {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import { scheduler } from "node:timers/promises";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
$env,
|
|
5
|
+
$flag,
|
|
6
|
+
asRecord,
|
|
7
|
+
extractHttpStatusFromError,
|
|
8
|
+
fetchWithRetry,
|
|
9
|
+
logger,
|
|
10
|
+
readSseJson,
|
|
11
|
+
structuredCloneJSON,
|
|
12
|
+
} from "@oh-my-pi/pi-utils";
|
|
4
13
|
import type OpenAI from "openai";
|
|
5
14
|
import type {
|
|
6
15
|
ResponseCustomToolCall,
|
|
@@ -40,7 +49,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
40
49
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
41
50
|
import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
42
51
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
43
|
-
import { adaptSchemaForStrict, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
52
|
+
import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
44
53
|
import { compactGrammarDefinition } from "./grammar";
|
|
45
54
|
import { CODEX_BASE_URL, getCodexAccountId, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "./openai-codex/constants";
|
|
46
55
|
import {
|
|
@@ -1505,6 +1514,7 @@ async function handleCodexStreamFailure(
|
|
|
1505
1514
|
resetCodexSessionMetadata(context.requestContext.websocketState);
|
|
1506
1515
|
}
|
|
1507
1516
|
output.stopReason = context.options?.signal?.aborted ? "aborted" : "error";
|
|
1517
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
1508
1518
|
output.errorMessage = await finalizeErrorMessage(error, context.requestContext.rawRequestDump);
|
|
1509
1519
|
output.duration = Date.now() - context.startTime;
|
|
1510
1520
|
if (context.firstTokenTime) {
|
|
@@ -2485,7 +2495,7 @@ export function convertOpenAICodexResponsesTools(
|
|
|
2485
2495
|
};
|
|
2486
2496
|
}
|
|
2487
2497
|
const strict = !!(!NO_STRICT && tool.strict);
|
|
2488
|
-
const baseParameters = toolWireSchema(tool);
|
|
2498
|
+
const baseParameters = sanitizeSchemaForOpenAIResponses(toolWireSchema(tool));
|
|
2489
2499
|
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
|
|
2490
2500
|
return {
|
|
2491
2501
|
type: "function",
|
|
@@ -860,6 +860,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
860
860
|
for (const block of output.content) delete (block as any).index;
|
|
861
861
|
const firstEventTimeoutError = abortTracker.getLocalAbortReason();
|
|
862
862
|
output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
863
|
+
output.errorStatus = extractHttpStatusFromError(error) ?? getCapturedErrorResponse?.()?.status;
|
|
863
864
|
output.errorMessage =
|
|
864
865
|
firstEventTimeoutError?.message ??
|
|
865
866
|
(await finalizeErrorMessage(error, rawRequestDump, getCapturedErrorResponse?.()));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { $env, extractHttpStatusFromError, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import OpenAI from "openai";
|
|
3
3
|
import type {
|
|
4
4
|
Tool as OpenAITool,
|
|
@@ -290,6 +290,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
290
290
|
for (const block of output.content) delete (block as { index?: number }).index;
|
|
291
291
|
const firstEventTimeoutError = abortTracker.getLocalAbortReason();
|
|
292
292
|
output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
293
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
293
294
|
output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
|
|
294
295
|
output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
|
|
295
296
|
output.duration = Date.now() - startTime;
|