@oh-my-pi/pi-ai 16.1.0 → 16.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.
- package/CHANGELOG.md +15 -1
- package/dist/types/auth-broker/discover.d.ts +35 -0
- package/dist/types/auth-broker/index.d.ts +1 -0
- package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
- package/dist/types/providers/openai-responses.d.ts +2 -1
- package/dist/types/types.d.ts +2 -2
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +217 -0
- package/src/auth-broker/index.ts +1 -0
- package/src/dialect/examples.ts +1 -1
- package/src/dialect/pi.md +1 -1
- package/src/providers/anthropic.ts +14 -3
- package/src/providers/azure-openai-responses.ts +50 -21
- package/src/providers/openai-completions.ts +52 -2
- package/src/providers/openai-reasoning-fallback.ts +269 -0
- package/src/providers/openai-responses.ts +81 -7
- package/src/types.ts +2 -2
- package/src/utils/validation.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.2] - 2026-06-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added improved JSON repair capabilities for Anthropic tool arguments
|
|
10
|
+
- Added authentication broker discovery to sync credentials between local SQLite and remote state
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Improved error feedback and transparency for malformed Anthropic tool call arguments
|
|
15
|
+
- Added automatic fallback for unsupported OpenAI reasoning effort levels
|
|
16
|
+
- Improved reliability when handling invalid reasoning parameter errors across OpenAI-compatible APIs
|
|
17
|
+
- Fixed OpenAI-compatible Chat Completions, Responses, and Azure Responses requests to retry once with the nearest provider-supported reasoning effort when an endpoint rejects `xhigh`/`minimal`-style effort values.
|
|
18
|
+
|
|
5
19
|
## [16.1.0] - 2026-06-19
|
|
6
20
|
|
|
7
21
|
### Added
|
|
@@ -3920,4 +3934,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
|
|
|
3920
3934
|
|
|
3921
3935
|
## [0.9.4] - 2025-11-26
|
|
3922
3936
|
|
|
3923
|
-
Initial release with multi-provider LLM support.
|
|
3937
|
+
Initial release with multi-provider LLM support.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AuthStorage } from "../auth-storage";
|
|
2
|
+
export interface AuthBrokerClientConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ResolveAuthBrokerConfigOptions {
|
|
7
|
+
agentDir?: string;
|
|
8
|
+
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
9
|
+
}
|
|
10
|
+
export interface DiscoverAuthStorageOptions {
|
|
11
|
+
agentDir?: string;
|
|
12
|
+
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
13
|
+
cachePath?: string;
|
|
14
|
+
sourceLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Path to the local bearer token file. Created by `omp auth-broker token`. */
|
|
17
|
+
export declare function getAuthBrokerTokenFilePath(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve broker connection configuration using the same precedence as the TUI:
|
|
20
|
+
*
|
|
21
|
+
* 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
|
|
22
|
+
* 2. `auth.broker.url` / `auth.broker.token` in `<agentDir>/config.yml`.
|
|
23
|
+
* 3. `<config-root>/auth-broker.token` file (paired with a URL from env/config).
|
|
24
|
+
*
|
|
25
|
+
* Returns `null` when no broker URL is configured — callers should fall back to
|
|
26
|
+
* the local SQLite store. Throws when a URL is configured but no token is
|
|
27
|
+
* available, matching the TUI behavior.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveAuthBrokerConfig(options?: ResolveAuthBrokerConfigOptions): Promise<AuthBrokerClientConfig | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Create an AuthStorage instance, using the broker when configured and falling
|
|
32
|
+
* back to the local SQLite store otherwise. This is the single source of truth
|
|
33
|
+
* for the TUI and the catalog generator.
|
|
34
|
+
*/
|
|
35
|
+
export declare function discoverAuthStorage(options?: DiscoverAuthStorageOptions): Promise<AuthStorage>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
|
|
2
|
+
/** @internal */
|
|
3
|
+
export type OpenAIReasoningEffortFallback = string | null;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export interface OpenAIReasoningEffortFallbackState {
|
|
6
|
+
reasoningEffortFallbacks: Map<string, OpenAIReasoningEffortFallback>;
|
|
7
|
+
}
|
|
8
|
+
/** @internal */
|
|
9
|
+
export declare function createOpenAIReasoningEffortFallbackState(): OpenAIReasoningEffortFallbackState;
|
|
10
|
+
/** @internal */
|
|
11
|
+
export declare function clearOpenAIReasoningEffortFallbackState(state: OpenAIReasoningEffortFallbackState): void;
|
|
12
|
+
/** @internal */
|
|
13
|
+
export declare function getOpenAIReasoningEffortFallback(state: OpenAIReasoningEffortFallbackState | undefined, key: string): OpenAIReasoningEffortFallback | undefined;
|
|
14
|
+
/** @internal */
|
|
15
|
+
export declare function rememberOpenAIReasoningEffortFallback(state: OpenAIReasoningEffortFallbackState | undefined, key: string, fallback: OpenAIReasoningEffortFallback): void;
|
|
16
|
+
/** @internal */
|
|
17
|
+
export declare function createOpenAIReasoningEffortFallbackKey(endpoint: "chat-completions" | "responses" | "azure-responses", baseUrl: string | undefined, wireModelId: string | undefined): string;
|
|
18
|
+
/** @internal */
|
|
19
|
+
export declare function readOpenAIReasoningEffort(params: unknown): string | undefined;
|
|
20
|
+
/** @internal */
|
|
21
|
+
export declare function applyOpenAIReasoningEffortFallback(params: unknown, fallback: OpenAIReasoningEffortFallback): boolean;
|
|
22
|
+
/** @internal */
|
|
23
|
+
export declare function resolveOpenAIReasoningEffortFallback(error: unknown, captured: CapturedHttpErrorResponse | undefined, params: unknown, options?: {
|
|
24
|
+
explicitDisable?: boolean;
|
|
25
|
+
}): OpenAIReasoningEffortFallback | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context, Model, OpenAICompat, ProviderSessionState, ServiceTier, StreamFunction, StreamOptions, Tool, ToolChoice } from "../types";
|
|
2
2
|
import { type OpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
3
|
+
import { type OpenAIReasoningEffortFallbackState } from "./openai-reasoning-fallback";
|
|
3
4
|
import type { Tool as OpenAITool, ResponseCreateParamsStreaming, ResponseInput } from "./openai-responses-wire";
|
|
4
5
|
import { type OpenAIStrictToolsScope, type OpenAIStrictToolsState } from "./openai-shared";
|
|
5
6
|
export interface OpenAIResponsesOptions extends StreamOptions {
|
|
@@ -55,7 +56,7 @@ export interface OpenAIResponsesOptions extends StreamOptions {
|
|
|
55
56
|
*/
|
|
56
57
|
extraBody?: Record<string, unknown>;
|
|
57
58
|
}
|
|
58
|
-
interface OpenAIResponsesProviderSessionState extends ProviderSessionState, OpenAIStrictToolsState {
|
|
59
|
+
interface OpenAIResponsesProviderSessionState extends ProviderSessionState, OpenAIStrictToolsState, OpenAIReasoningEffortFallbackState {
|
|
59
60
|
nativeHistoryReplayWarmed: boolean;
|
|
60
61
|
/** Stateful `previous_response_id` chain baselines, keyed by baseUrl/model/session. */
|
|
61
62
|
chains: Map<string, OpenAIResponsesChainState>;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -549,8 +549,8 @@ export interface Tool<TParameters extends TSchema = TSchema> {
|
|
|
549
549
|
* Illustrative calls/notes; the AI layer renders them into an `<examples>`
|
|
550
550
|
* block in the model's native tool-call syntax and appends to the wire
|
|
551
551
|
* description. Author `call`/`bad`/`good` as plain argument objects WITHOUT
|
|
552
|
-
* `
|
|
553
|
-
* a placeholder `
|
|
552
|
+
* `i` — when intent tracing injects `i` into the schema, the renderer adds
|
|
553
|
+
* a placeholder `i` automatically. Type each tool's `examples` against its
|
|
554
554
|
* own schema (e.g. `readonly ToolExample<typeof schema["type"]>[]`).
|
|
555
555
|
*/
|
|
556
556
|
examples?: readonly ToolExample[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.1.2",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.1.2",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.1.2",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "^4"
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broker-aware auth-storage discovery used by both the coding-agent runtime and
|
|
3
|
+
* the catalog model generator. Keeps the precedence logic (env → config.yml →
|
|
4
|
+
* token file → local SQLite) in one place so build-time tooling sees the same
|
|
5
|
+
* credentials as the TUI.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
getAgentDbPath,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
getAuthBrokerSnapshotCachePath,
|
|
12
|
+
getConfigRootDir,
|
|
13
|
+
isEnoent,
|
|
14
|
+
logger,
|
|
15
|
+
} from "@oh-my-pi/pi-utils";
|
|
16
|
+
import { YAML } from "bun";
|
|
17
|
+
import { AuthStorage } from "../auth-storage";
|
|
18
|
+
import { AuthBrokerClient } from "./client";
|
|
19
|
+
import { RemoteAuthCredentialStore } from "./remote-store";
|
|
20
|
+
import { readAuthBrokerSnapshotCache, writeAuthBrokerSnapshotCache } from "./snapshot-cache";
|
|
21
|
+
import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "./types";
|
|
22
|
+
|
|
23
|
+
export interface AuthBrokerClientConfig {
|
|
24
|
+
url: string;
|
|
25
|
+
token: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ResolveAuthBrokerConfigOptions {
|
|
29
|
+
agentDir?: string;
|
|
30
|
+
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DiscoverAuthStorageOptions {
|
|
34
|
+
agentDir?: string;
|
|
35
|
+
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
36
|
+
cachePath?: string;
|
|
37
|
+
sourceLabel?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Path to the local bearer token file. Created by `omp auth-broker token`. */
|
|
41
|
+
export function getAuthBrokerTokenFilePath(): string {
|
|
42
|
+
return path.join(getConfigRootDir(), "auth-broker.token");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default resolver for config values: checks `process.env` first, then treats
|
|
47
|
+
* the value as a literal. Does NOT execute `!command` syntax; such values are
|
|
48
|
+
* left unresolved so the caller can fall back to the token file.
|
|
49
|
+
*/
|
|
50
|
+
async function defaultResolveConfigValue(config: string): Promise<string | undefined> {
|
|
51
|
+
if (config.startsWith("!")) return undefined;
|
|
52
|
+
const envValue = process.env[config];
|
|
53
|
+
return envValue || config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readTokenFile(): Promise<string | null> {
|
|
57
|
+
try {
|
|
58
|
+
const raw = await Bun.file(getAuthBrokerTokenFilePath()).text();
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (isEnoent(err)) return null;
|
|
63
|
+
logger.warn("auth-broker token file unreadable", { error: String(err) });
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ConfigSnapshot {
|
|
69
|
+
url?: string;
|
|
70
|
+
token?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readConfigYaml(agentDir: string): Promise<ConfigSnapshot> {
|
|
74
|
+
const configPath = path.join(agentDir, "config.yml");
|
|
75
|
+
try {
|
|
76
|
+
const raw = await Bun.file(configPath).text();
|
|
77
|
+
const parsed = YAML.parse(raw);
|
|
78
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
79
|
+
const record = parsed as Record<string, unknown>;
|
|
80
|
+
const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined;
|
|
81
|
+
const token =
|
|
82
|
+
typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined;
|
|
83
|
+
return { url, token };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (isEnoent(err)) return {};
|
|
86
|
+
logger.warn("auth-broker config.yml unreadable", { error: String(err) });
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveSnapshotTtlMs(): number {
|
|
92
|
+
const raw = process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS;
|
|
93
|
+
if (raw === undefined) return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
94
|
+
const value = raw.trim();
|
|
95
|
+
if (value === "") return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
96
|
+
const ttlMs = Number(value);
|
|
97
|
+
if (Number.isFinite(ttlMs) && ttlMs >= 0) return ttlMs;
|
|
98
|
+
logger.warn("Invalid OMP_AUTH_BROKER_SNAPSHOT_TTL_MS; using default", { value: raw });
|
|
99
|
+
return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve broker connection configuration using the same precedence as the TUI:
|
|
104
|
+
*
|
|
105
|
+
* 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
|
|
106
|
+
* 2. `auth.broker.url` / `auth.broker.token` in `<agentDir>/config.yml`.
|
|
107
|
+
* 3. `<config-root>/auth-broker.token` file (paired with a URL from env/config).
|
|
108
|
+
*
|
|
109
|
+
* Returns `null` when no broker URL is configured — callers should fall back to
|
|
110
|
+
* the local SQLite store. Throws when a URL is configured but no token is
|
|
111
|
+
* available, matching the TUI behavior.
|
|
112
|
+
*/
|
|
113
|
+
export async function resolveAuthBrokerConfig(
|
|
114
|
+
options: ResolveAuthBrokerConfigOptions = {},
|
|
115
|
+
): Promise<AuthBrokerClientConfig | null> {
|
|
116
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
117
|
+
const resolveConfig = options.configValueResolver ?? defaultResolveConfigValue;
|
|
118
|
+
|
|
119
|
+
const envUrl = process.env.OMP_AUTH_BROKER_URL;
|
|
120
|
+
const envToken = process.env.OMP_AUTH_BROKER_TOKEN;
|
|
121
|
+
|
|
122
|
+
let url = envUrl && envUrl.length > 0 ? envUrl : undefined;
|
|
123
|
+
let configToken: string | undefined;
|
|
124
|
+
if (!url || !envToken) {
|
|
125
|
+
const fromConfig = await readConfigYaml(agentDir);
|
|
126
|
+
if (!url && fromConfig.url) {
|
|
127
|
+
const resolved = await resolveConfig(fromConfig.url);
|
|
128
|
+
if (resolved && resolved.length > 0) url = resolved;
|
|
129
|
+
}
|
|
130
|
+
if (fromConfig.token) {
|
|
131
|
+
const resolved = await resolveConfig(fromConfig.token);
|
|
132
|
+
if (resolved && resolved.length > 0) configToken = resolved;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!url) return null;
|
|
136
|
+
|
|
137
|
+
const token =
|
|
138
|
+
(envToken && envToken.length > 0 ? envToken : undefined) ?? configToken ?? (await readTokenFile()) ?? undefined;
|
|
139
|
+
if (!token) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`OMP_AUTH_BROKER_URL is set (${url}) but no bearer token is available. ` +
|
|
142
|
+
`Set OMP_AUTH_BROKER_TOKEN, the \`auth.broker.token\` config entry, or place one at ${getAuthBrokerTokenFilePath()}.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return { url, token };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create an AuthStorage instance, using the broker when configured and falling
|
|
150
|
+
* back to the local SQLite store otherwise. This is the single source of truth
|
|
151
|
+
* for the TUI and the catalog generator.
|
|
152
|
+
*/
|
|
153
|
+
export async function discoverAuthStorage(options: DiscoverAuthStorageOptions = {}): Promise<AuthStorage> {
|
|
154
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
155
|
+
const brokerConfig = await resolveAuthBrokerConfig({
|
|
156
|
+
agentDir,
|
|
157
|
+
configValueResolver: options.configValueResolver,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (brokerConfig) {
|
|
161
|
+
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
162
|
+
const cachePath = options.cachePath ?? getAuthBrokerSnapshotCachePath();
|
|
163
|
+
const ttlMs = resolveSnapshotTtlMs();
|
|
164
|
+
const persist =
|
|
165
|
+
ttlMs > 0
|
|
166
|
+
? (snapshot: SnapshotResponse): void => {
|
|
167
|
+
void writeAuthBrokerSnapshotCache({
|
|
168
|
+
path: cachePath,
|
|
169
|
+
token: brokerConfig.token,
|
|
170
|
+
url: brokerConfig.url,
|
|
171
|
+
snapshot,
|
|
172
|
+
}).catch(error => {
|
|
173
|
+
logger.debug("auth-broker snapshot cache write failed", { error: String(error) });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
: undefined;
|
|
177
|
+
|
|
178
|
+
let initialSnapshot: SnapshotResponse | undefined;
|
|
179
|
+
if (ttlMs > 0) {
|
|
180
|
+
initialSnapshot =
|
|
181
|
+
(await readAuthBrokerSnapshotCache({
|
|
182
|
+
path: cachePath,
|
|
183
|
+
token: brokerConfig.token,
|
|
184
|
+
url: brokerConfig.url,
|
|
185
|
+
ttlMs,
|
|
186
|
+
}).catch(error => {
|
|
187
|
+
logger.debug("auth-broker snapshot cache read failed", { error: String(error) });
|
|
188
|
+
return null;
|
|
189
|
+
})) ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
if (!initialSnapshot) {
|
|
192
|
+
const initialResult = await client.fetchSnapshot();
|
|
193
|
+
if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot");
|
|
194
|
+
initialSnapshot = initialResult.snapshot;
|
|
195
|
+
persist?.(initialSnapshot);
|
|
196
|
+
}
|
|
197
|
+
const store = new RemoteAuthCredentialStore({
|
|
198
|
+
client,
|
|
199
|
+
initialSnapshot,
|
|
200
|
+
onSnapshot: persist,
|
|
201
|
+
});
|
|
202
|
+
const storage = new AuthStorage(store, {
|
|
203
|
+
configValueResolver: options.configValueResolver,
|
|
204
|
+
sourceLabel: options.sourceLabel ?? `broker ${brokerConfig.url}`,
|
|
205
|
+
});
|
|
206
|
+
await storage.reload();
|
|
207
|
+
return storage;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const dbPath = getAgentDbPath(agentDir);
|
|
211
|
+
const storage = await AuthStorage.create(dbPath, {
|
|
212
|
+
configValueResolver: options.configValueResolver,
|
|
213
|
+
sourceLabel: options.sourceLabel ?? `local ${dbPath}`,
|
|
214
|
+
});
|
|
215
|
+
await storage.reload();
|
|
216
|
+
return storage;
|
|
217
|
+
}
|
package/src/auth-broker/index.ts
CHANGED
package/src/dialect/examples.ts
CHANGED
|
@@ -9,7 +9,7 @@ export function renderToolExamples(tool: InbandTool, dialect: Dialect, intentFie
|
|
|
9
9
|
if (!examples?.length) return "";
|
|
10
10
|
const definition = getDialectDefinition(dialect);
|
|
11
11
|
const renderCall = (args: Record<string, unknown>): string => {
|
|
12
|
-
// When intent tracing injects `
|
|
12
|
+
// When intent tracing injects `i` into the schema, examples must show a
|
|
13
13
|
// placeholder so the model learns to emit it. Keep it first, matching the
|
|
14
14
|
// schema injection order.
|
|
15
15
|
const finalArgs = intentField ? { [intentField]: INTENT_PLACEHOLDER, ...args } : args;
|
package/src/dialect/pi.md
CHANGED
|
@@ -23,7 +23,7 @@ Call with a verbatim body — everything between `«` and `»` is taken literall
|
|
|
23
23
|
|
|
24
24
|
Argument values:
|
|
25
25
|
|
|
26
|
-
- Strings are written bare and verbatim (`path=src/a.ts`). Quote with `"…"` only when the value contains spaces or starts with `"`, `[`, or `{` (`
|
|
26
|
+
- Strings are written bare and verbatim (`path=src/a.ts`). Quote with `"…"` only when the value contains spaces or starts with `"`, `[`, or `{` (`i="run the tests"`).
|
|
27
27
|
- Numbers, booleans, and `null` are JSON literals (`offset=50`, `force=true`).
|
|
28
28
|
- Arrays and objects are inline JSON (`paths=["src","test"]`).
|
|
29
29
|
- The body fence holds the call's first long/multi-line string parameter; its key is implied, never written.
|
|
@@ -50,7 +50,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
50
50
|
import { isFoundryEnabled } from "../utils/foundry";
|
|
51
51
|
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
|
|
52
52
|
import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
53
|
-
import { parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
53
|
+
import { parseJsonWithRepair, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
54
54
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
55
55
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
56
56
|
import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
@@ -1752,14 +1752,25 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1752
1752
|
const finalJson =
|
|
1753
1753
|
block.partialJson.length > 0 ? block.partialJson : JSON.stringify(block.arguments ?? {});
|
|
1754
1754
|
try {
|
|
1755
|
-
block.arguments =
|
|
1755
|
+
block.arguments = parseJsonWithRepair(finalJson) as ToolCall["arguments"];
|
|
1756
1756
|
} catch (parseError) {
|
|
1757
1757
|
// Non-fatal: keep the best-effort arguments recovered by the throttled streaming
|
|
1758
1758
|
// parser instead of failing the turn on malformed/truncated tool-argument JSON.
|
|
1759
1759
|
reportAnthropicEnvelopeAnomaly(
|
|
1760
1760
|
`tool_use ${block.id} arguments are not valid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
1761
1761
|
);
|
|
1762
|
-
|
|
1762
|
+
const recoveredKeys = Object.keys(block.arguments ?? {});
|
|
1763
|
+
if (recoveredKeys.length === 0) {
|
|
1764
|
+
const maxLen = 512;
|
|
1765
|
+
const truncatedJson =
|
|
1766
|
+
finalJson.length <= maxLen
|
|
1767
|
+
? finalJson
|
|
1768
|
+
: `${finalJson.slice(0, maxLen)}… [truncated ${finalJson.length - maxLen} chars]`;
|
|
1769
|
+
block.arguments = {
|
|
1770
|
+
__parseError: parseError instanceof Error ? parseError.message : String(parseError),
|
|
1771
|
+
__rawJson: truncatedJson,
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1763
1774
|
}
|
|
1764
1775
|
delete (block as { partialJson?: string }).partialJson;
|
|
1765
1776
|
delete (block as { lastParseLen?: number }).lastParseLen;
|
|
@@ -18,9 +18,15 @@ import {
|
|
|
18
18
|
getOpenAIStreamIdleTimeoutMs,
|
|
19
19
|
iterateWithIdleTimeout,
|
|
20
20
|
} from "../utils/idle-iterator";
|
|
21
|
-
import { postOpenAIStream } from "../utils/openai-http";
|
|
21
|
+
import { OpenAIHttpError, postOpenAIStream } from "../utils/openai-http";
|
|
22
22
|
import { sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
23
23
|
import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
24
|
+
import {
|
|
25
|
+
applyOpenAIReasoningEffortFallback,
|
|
26
|
+
createOpenAIReasoningEffortFallbackKey,
|
|
27
|
+
type OpenAIReasoningEffortFallback,
|
|
28
|
+
resolveOpenAIReasoningEffortFallback,
|
|
29
|
+
} from "./openai-reasoning-fallback";
|
|
24
30
|
import type { ResponseCreateParamsStreaming, ResponseStreamEvent } from "./openai-responses-wire";
|
|
25
31
|
import {
|
|
26
32
|
applyCommonResponsesSamplingParams,
|
|
@@ -135,29 +141,52 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|
|
135
141
|
url,
|
|
136
142
|
body: params,
|
|
137
143
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
const reasoningEffortFallbackKey = createOpenAIReasoningEffortFallbackKey(
|
|
145
|
+
"azure-responses",
|
|
146
|
+
url,
|
|
147
|
+
typeof params.model === "string" ? params.model : model.id,
|
|
148
|
+
);
|
|
149
|
+
const attemptedReasoningEffortFallbacks = new Set<string>();
|
|
142
150
|
let openaiStream: AsyncIterable<ResponseStreamEvent>;
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
while (true) {
|
|
152
|
+
let requestTimeout: NodeJS.Timeout | undefined;
|
|
145
153
|
if (requestTimeoutMs !== undefined) {
|
|
146
|
-
|
|
154
|
+
requestTimeout = setTimeout(
|
|
155
|
+
() => abortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
156
|
+
requestTimeoutMs,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const headersWithTimeout = { ...headers };
|
|
161
|
+
if (requestTimeoutMs !== undefined) {
|
|
162
|
+
headersWithTimeout["X-Stainless-Timeout"] = Math.floor(requestTimeoutMs / 1000).toString();
|
|
163
|
+
}
|
|
164
|
+
const handle = await postOpenAIStream<ResponseStreamEvent>({
|
|
165
|
+
url,
|
|
166
|
+
headers: headersWithTimeout,
|
|
167
|
+
body: params,
|
|
168
|
+
signal: requestSignal,
|
|
169
|
+
fetch: options?.fetch,
|
|
170
|
+
// Watchdog armed → no retries, so they cannot silently extend the deadline.
|
|
171
|
+
maxAttempts: requestTimeoutMs !== undefined ? 1 : undefined,
|
|
172
|
+
onSseEvent: rawSseObserver,
|
|
173
|
+
});
|
|
174
|
+
openaiStream = handle.events;
|
|
175
|
+
break;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
|
|
178
|
+
const reasoningEffortFallback: OpenAIReasoningEffortFallback | undefined = !requestSignal.aborted
|
|
179
|
+
? resolveOpenAIReasoningEffortFallback(error, capturedErrorResponse, params)
|
|
180
|
+
: undefined;
|
|
181
|
+
if (reasoningEffortFallback === undefined) throw error;
|
|
182
|
+
const retryMarker = `${reasoningEffortFallbackKey}:${String(reasoningEffortFallback)}`;
|
|
183
|
+
if (attemptedReasoningEffortFallbacks.has(retryMarker)) throw error;
|
|
184
|
+
attemptedReasoningEffortFallbacks.add(retryMarker);
|
|
185
|
+
applyOpenAIReasoningEffortFallback(params, reasoningEffortFallback);
|
|
186
|
+
rawRequestDump.body = params;
|
|
187
|
+
} finally {
|
|
188
|
+
if (requestTimeout !== undefined) clearTimeout(requestTimeout);
|
|
147
189
|
}
|
|
148
|
-
const handle = await postOpenAIStream<ResponseStreamEvent>({
|
|
149
|
-
url,
|
|
150
|
-
headers: headersWithTimeout,
|
|
151
|
-
body: params,
|
|
152
|
-
signal: requestSignal,
|
|
153
|
-
fetch: options?.fetch,
|
|
154
|
-
// Watchdog armed → no retries, so they cannot silently extend the deadline.
|
|
155
|
-
maxAttempts: requestTimeoutMs !== undefined ? 1 : undefined,
|
|
156
|
-
onSseEvent: rawSseObserver,
|
|
157
|
-
});
|
|
158
|
-
openaiStream = handle.events;
|
|
159
|
-
} finally {
|
|
160
|
-
if (requestTimeout !== undefined) clearTimeout(requestTimeout);
|
|
161
190
|
}
|
|
162
191
|
stream.push({ type: "start", partial: output });
|
|
163
192
|
|
|
@@ -56,6 +56,17 @@ import type {
|
|
|
56
56
|
ChatCompletionTool,
|
|
57
57
|
ChatCompletionToolMessageParam,
|
|
58
58
|
} from "./openai-chat-wire";
|
|
59
|
+
import {
|
|
60
|
+
applyOpenAIReasoningEffortFallback,
|
|
61
|
+
clearOpenAIReasoningEffortFallbackState,
|
|
62
|
+
createOpenAIReasoningEffortFallbackKey,
|
|
63
|
+
createOpenAIReasoningEffortFallbackState,
|
|
64
|
+
getOpenAIReasoningEffortFallback,
|
|
65
|
+
type OpenAIReasoningEffortFallback,
|
|
66
|
+
type OpenAIReasoningEffortFallbackState,
|
|
67
|
+
rememberOpenAIReasoningEffortFallback,
|
|
68
|
+
resolveOpenAIReasoningEffortFallback,
|
|
69
|
+
} from "./openai-reasoning-fallback";
|
|
59
70
|
import {
|
|
60
71
|
applyChatCompletionsCompatPolicy,
|
|
61
72
|
applyChatCompletionsToolStream,
|
|
@@ -444,14 +455,19 @@ type BuiltOpenAICompletionTools = {
|
|
|
444
455
|
|
|
445
456
|
const OPENAI_COMPLETIONS_PROVIDER_SESSION_STATE_PREFIX = "openai-completions:";
|
|
446
457
|
|
|
447
|
-
type OpenAICompletionsProviderSessionState = ProviderSessionState &
|
|
458
|
+
type OpenAICompletionsProviderSessionState = ProviderSessionState &
|
|
459
|
+
OpenAIStrictToolsState &
|
|
460
|
+
OpenAIReasoningEffortFallbackState;
|
|
448
461
|
|
|
449
462
|
function createOpenAICompletionsProviderSessionState(): OpenAICompletionsProviderSessionState {
|
|
450
463
|
const strictToolsState = createOpenAIStrictToolsState();
|
|
464
|
+
const reasoningEffortFallbackState = createOpenAIReasoningEffortFallbackState();
|
|
451
465
|
const state: OpenAICompletionsProviderSessionState = {
|
|
452
466
|
...strictToolsState,
|
|
467
|
+
...reasoningEffortFallbackState,
|
|
453
468
|
close: () => {
|
|
454
469
|
clearOpenAIStrictToolsState(state);
|
|
470
|
+
clearOpenAIReasoningEffortFallbackState(state);
|
|
455
471
|
},
|
|
456
472
|
};
|
|
457
473
|
return state;
|
|
@@ -581,6 +597,10 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
581
597
|
);
|
|
582
598
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
583
599
|
let appliedStrictTools = false;
|
|
600
|
+
const requestReasoningEffortFallbacks = new Map<string, OpenAIReasoningEffortFallback>();
|
|
601
|
+
const attemptedReasoningEffortFallbacks = new Set<string>();
|
|
602
|
+
let activeReasoningEffortFallbackKey: string | undefined;
|
|
603
|
+
let activeRequestParams: OpenAICompletionsParams | undefined;
|
|
584
604
|
const providerSessionState = getOpenAICompletionsProviderSessionState(
|
|
585
605
|
model,
|
|
586
606
|
baseUrl,
|
|
@@ -601,6 +621,19 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
601
621
|
effectiveToolStrictModeOverride,
|
|
602
622
|
);
|
|
603
623
|
appliedStrictTools = strictToolsApplied;
|
|
624
|
+
const reasoningEffortFallbackKey = createOpenAIReasoningEffortFallbackKey(
|
|
625
|
+
"chat-completions",
|
|
626
|
+
trimmedBaseUrl,
|
|
627
|
+
params.model,
|
|
628
|
+
);
|
|
629
|
+
const requestReasoningEffortFallback = requestReasoningEffortFallbacks.has(reasoningEffortFallbackKey)
|
|
630
|
+
? requestReasoningEffortFallbacks.get(reasoningEffortFallbackKey)
|
|
631
|
+
: getOpenAIReasoningEffortFallback(providerSessionState, reasoningEffortFallbackKey);
|
|
632
|
+
if (requestReasoningEffortFallback !== undefined) {
|
|
633
|
+
applyOpenAIReasoningEffortFallback(params, requestReasoningEffortFallback);
|
|
634
|
+
}
|
|
635
|
+
activeReasoningEffortFallbackKey = reasoningEffortFallbackKey;
|
|
636
|
+
activeRequestParams = params;
|
|
604
637
|
options?.onPayload?.(params);
|
|
605
638
|
rawRequestDump = {
|
|
606
639
|
provider: model.provider,
|
|
@@ -650,7 +683,24 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
650
683
|
});
|
|
651
684
|
} catch (error) {
|
|
652
685
|
const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
|
|
653
|
-
|
|
686
|
+
const reasoningEffortFallback =
|
|
687
|
+
activeReasoningEffortFallbackKey && activeRequestParams && !requestSignal.aborted
|
|
688
|
+
? resolveOpenAIReasoningEffortFallback(error, capturedErrorResponse, activeRequestParams, {
|
|
689
|
+
explicitDisable: options?.disableReasoning === true && options.reasoning === undefined,
|
|
690
|
+
})
|
|
691
|
+
: undefined;
|
|
692
|
+
if (reasoningEffortFallback !== undefined && activeReasoningEffortFallbackKey) {
|
|
693
|
+
const retryMarker = `${activeReasoningEffortFallbackKey}:${String(reasoningEffortFallback)}`;
|
|
694
|
+
if (attemptedReasoningEffortFallbacks.has(retryMarker)) throw error;
|
|
695
|
+
attemptedReasoningEffortFallbacks.add(retryMarker);
|
|
696
|
+
requestReasoningEffortFallbacks.set(activeReasoningEffortFallbackKey, reasoningEffortFallback);
|
|
697
|
+
openaiStream = await createCompletionsStream();
|
|
698
|
+
rememberOpenAIReasoningEffortFallback(
|
|
699
|
+
providerSessionState,
|
|
700
|
+
activeReasoningEffortFallbackKey,
|
|
701
|
+
reasoningEffortFallback,
|
|
702
|
+
);
|
|
703
|
+
} else if (
|
|
654
704
|
isOpenRouterAnthropicModel(model) &&
|
|
655
705
|
!disableStrictTools &&
|
|
656
706
|
isCompiledGrammarTooLargeStrictError(error, capturedErrorResponse)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { extractHttpStatusFromError } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
|
|
3
|
+
|
|
4
|
+
/** @internal */
|
|
5
|
+
export type OpenAIReasoningEffortFallback = string | null;
|
|
6
|
+
|
|
7
|
+
/** @internal */
|
|
8
|
+
export interface OpenAIReasoningEffortFallbackState {
|
|
9
|
+
reasoningEffortFallbacks: Map<string, OpenAIReasoningEffortFallback>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ENABLED_REASONING_VALUES = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
13
|
+
const KNOWN_REASONING_VALUE: Readonly<Record<string, true>> = {
|
|
14
|
+
none: true,
|
|
15
|
+
minimal: true,
|
|
16
|
+
low: true,
|
|
17
|
+
medium: true,
|
|
18
|
+
high: true,
|
|
19
|
+
xhigh: true,
|
|
20
|
+
max: true,
|
|
21
|
+
};
|
|
22
|
+
const REASONING_VALUE_RANK: Readonly<Record<string, number>> = {
|
|
23
|
+
minimal: 0,
|
|
24
|
+
low: 1,
|
|
25
|
+
medium: 2,
|
|
26
|
+
high: 3,
|
|
27
|
+
xhigh: 4,
|
|
28
|
+
max: 5,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
export function createOpenAIReasoningEffortFallbackState(): OpenAIReasoningEffortFallbackState {
|
|
33
|
+
return { reasoningEffortFallbacks: new Map() };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @internal */
|
|
37
|
+
export function clearOpenAIReasoningEffortFallbackState(state: OpenAIReasoningEffortFallbackState): void {
|
|
38
|
+
state.reasoningEffortFallbacks.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @internal */
|
|
42
|
+
export function getOpenAIReasoningEffortFallback(
|
|
43
|
+
state: OpenAIReasoningEffortFallbackState | undefined,
|
|
44
|
+
key: string,
|
|
45
|
+
): OpenAIReasoningEffortFallback | undefined {
|
|
46
|
+
return state?.reasoningEffortFallbacks.get(key);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
export function rememberOpenAIReasoningEffortFallback(
|
|
51
|
+
state: OpenAIReasoningEffortFallbackState | undefined,
|
|
52
|
+
key: string,
|
|
53
|
+
fallback: OpenAIReasoningEffortFallback,
|
|
54
|
+
): void {
|
|
55
|
+
state?.reasoningEffortFallbacks.set(key, fallback);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @internal */
|
|
59
|
+
export function createOpenAIReasoningEffortFallbackKey(
|
|
60
|
+
endpoint: "chat-completions" | "responses" | "azure-responses",
|
|
61
|
+
baseUrl: string | undefined,
|
|
62
|
+
wireModelId: string | undefined,
|
|
63
|
+
): string {
|
|
64
|
+
return `${endpoint}:${baseUrl ?? ""}:${wireModelId ?? ""}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
68
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @internal */
|
|
72
|
+
export function readOpenAIReasoningEffort(params: unknown): string | undefined {
|
|
73
|
+
if (!isRecord(params)) return undefined;
|
|
74
|
+
if (typeof params.reasoning_effort === "string") return params.reasoning_effort;
|
|
75
|
+
const reasoning = params.reasoning;
|
|
76
|
+
return isRecord(reasoning) && typeof reasoning.effort === "string" ? reasoning.effort : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function deleteReasoningEffort(reasoning: Record<string, unknown>, parent: Record<string, unknown>): boolean {
|
|
80
|
+
if (typeof reasoning.effort !== "string") return false;
|
|
81
|
+
delete reasoning.effort;
|
|
82
|
+
for (const key in reasoning) {
|
|
83
|
+
if (key !== "effort") return true;
|
|
84
|
+
}
|
|
85
|
+
delete parent.reasoning;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @internal */
|
|
90
|
+
export function applyOpenAIReasoningEffortFallback(params: unknown, fallback: OpenAIReasoningEffortFallback): boolean {
|
|
91
|
+
if (!isRecord(params)) return false;
|
|
92
|
+
let changed = false;
|
|
93
|
+
if (typeof params.reasoning_effort === "string") {
|
|
94
|
+
if (fallback === null) {
|
|
95
|
+
delete params.reasoning_effort;
|
|
96
|
+
} else {
|
|
97
|
+
params.reasoning_effort = fallback;
|
|
98
|
+
}
|
|
99
|
+
changed = true;
|
|
100
|
+
}
|
|
101
|
+
const reasoning = params.reasoning;
|
|
102
|
+
if (isRecord(reasoning) && typeof reasoning.effort === "string") {
|
|
103
|
+
if (fallback === null) {
|
|
104
|
+
changed = deleteReasoningEffort(reasoning, params) || changed;
|
|
105
|
+
} else {
|
|
106
|
+
reasoning.effort = fallback;
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return changed;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function capturedStringField(
|
|
114
|
+
captured: CapturedHttpErrorResponse | undefined,
|
|
115
|
+
field: "code" | "message" | "param" | "type",
|
|
116
|
+
) {
|
|
117
|
+
const body = isRecord(captured?.bodyJson) ? captured.bodyJson : undefined;
|
|
118
|
+
const error = isRecord(body?.error) ? body.error : undefined;
|
|
119
|
+
if (typeof error?.[field] === "string") return error[field];
|
|
120
|
+
return typeof body?.[field] === "string" ? body[field] : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function collectMessageParts(error: unknown, captured: CapturedHttpErrorResponse | undefined): string {
|
|
124
|
+
const parts = [
|
|
125
|
+
error instanceof Error ? error.message : undefined,
|
|
126
|
+
capturedStringField(captured, "message"),
|
|
127
|
+
capturedStringField(captured, "param"),
|
|
128
|
+
capturedStringField(captured, "code"),
|
|
129
|
+
capturedStringField(captured, "type"),
|
|
130
|
+
captured?.bodyText,
|
|
131
|
+
].filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
|
132
|
+
return parts.join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const REASONING_EFFORT_FIELD_PATTERN = /reasoning[_. ]effort|reasoning value/i;
|
|
136
|
+
|
|
137
|
+
function mentionsReasoningEffort(error: unknown, captured: CapturedHttpErrorResponse | undefined): boolean {
|
|
138
|
+
const param = capturedStringField(captured, "param");
|
|
139
|
+
const code = capturedStringField(captured, "code");
|
|
140
|
+
const type = capturedStringField(captured, "type");
|
|
141
|
+
const message = collectMessageParts(error, captured);
|
|
142
|
+
return (
|
|
143
|
+
REASONING_EFFORT_FIELD_PATTERN.test(param ?? "") ||
|
|
144
|
+
REASONING_EFFORT_FIELD_PATTERN.test(code ?? "") ||
|
|
145
|
+
REASONING_EFFORT_FIELD_PATTERN.test(type ?? "") ||
|
|
146
|
+
REASONING_EFFORT_FIELD_PATTERN.test(message)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isInvalidReasoningEffortError(
|
|
151
|
+
error: unknown,
|
|
152
|
+
captured: CapturedHttpErrorResponse | undefined,
|
|
153
|
+
currentEffort: string,
|
|
154
|
+
): boolean {
|
|
155
|
+
const status = extractHttpStatusFromError(error) ?? captured?.status;
|
|
156
|
+
if (status !== 400 && status !== 422) return false;
|
|
157
|
+
if (!mentionsReasoningEffort(error, captured)) return false;
|
|
158
|
+
const message = collectMessageParts(error, captured);
|
|
159
|
+
if (/reasoning[_ ]content/i.test(message) && !REASONING_EFFORT_FIELD_PATTERN.test(message)) return false;
|
|
160
|
+
if (/invalid[^\n]*(?:reasoning[_. ]effort|reasoning value)/i.test(message)) return true;
|
|
161
|
+
if (
|
|
162
|
+
/(?:reasoning[_. ]effort|reasoning value)[^\n]*(?:invalid|unsupported|not supported|must be|expected)/i.test(
|
|
163
|
+
message,
|
|
164
|
+
)
|
|
165
|
+
) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (/(?:unsupported|not supported)[^\n]*(?:reasoning[_. ]effort|reasoning value)/i.test(message)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return new RegExp(
|
|
172
|
+
`(?:invalid|unsupported|not supported)[^\\n]*["'\`]${escapeRegExp(currentEffort)}["'\`]`,
|
|
173
|
+
"i",
|
|
174
|
+
).test(message);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function escapeRegExp(value: string): string {
|
|
178
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseKnownReasoningValues(text: string): Set<string> {
|
|
182
|
+
const values = new Set<string>();
|
|
183
|
+
const quotedPattern = /["'`](none|minimal|low|medium|high|xhigh|max)["'`]/gi;
|
|
184
|
+
let quotedMatch = quotedPattern.exec(text);
|
|
185
|
+
while (quotedMatch !== null) {
|
|
186
|
+
values.add(quotedMatch[1]!.toLowerCase());
|
|
187
|
+
quotedMatch = quotedPattern.exec(text);
|
|
188
|
+
}
|
|
189
|
+
const allowedMatch = /(?:must be|one of|allowed values?|supported values?(?: are)?|expected)([^.\n]+)/i.exec(text);
|
|
190
|
+
if (allowedMatch) {
|
|
191
|
+
const allowedText = allowedMatch[1]!;
|
|
192
|
+
const barePattern = /\b(none|minimal|low|medium|high|xhigh|max)\b/gi;
|
|
193
|
+
let bareMatch = barePattern.exec(allowedText);
|
|
194
|
+
while (bareMatch !== null) {
|
|
195
|
+
values.add(bareMatch[1]!.toLowerCase());
|
|
196
|
+
bareMatch = barePattern.exec(allowedText);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return values;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseAllowedReasoningValues(message: string, currentEffort: string): Set<string> | undefined {
|
|
203
|
+
const values = parseKnownReasoningValues(message);
|
|
204
|
+
const hasAllowedCue = /must be|one of|allowed values?|supported values?|expected/i.test(message);
|
|
205
|
+
values.delete(currentEffort.toLowerCase());
|
|
206
|
+
if (!hasAllowedCue && values.size === 0) return undefined;
|
|
207
|
+
return values;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function orderedEnabledAllowedValues(allowed: Set<string>): string[] {
|
|
211
|
+
return ENABLED_REASONING_VALUES.filter(value => allowed.has(value));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function lowestEnabledAllowedValue(allowed: Set<string>): string | undefined {
|
|
215
|
+
for (const value of ENABLED_REASONING_VALUES) {
|
|
216
|
+
if (allowed.has(value)) return value;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function nearestEnabledReasoningFallback(currentEffort: string, allowed: Set<string>): string | undefined {
|
|
222
|
+
const current = currentEffort.toLowerCase();
|
|
223
|
+
const allowedEnabled = orderedEnabledAllowedValues(allowed);
|
|
224
|
+
if (allowedEnabled.length === 0) return undefined;
|
|
225
|
+
if (current === "minimal" && allowedEnabled.includes("low")) return "low";
|
|
226
|
+
if (current === "xhigh" && allowedEnabled.includes("max")) return "max";
|
|
227
|
+
if (current === "xhigh" && allowedEnabled.includes("high")) return "high";
|
|
228
|
+
if (current === "max" && allowedEnabled.includes("xhigh")) return "xhigh";
|
|
229
|
+
const currentRank = REASONING_VALUE_RANK[current];
|
|
230
|
+
if (currentRank === undefined) return undefined;
|
|
231
|
+
let best: string | undefined;
|
|
232
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
233
|
+
let bestRank = Number.NEGATIVE_INFINITY;
|
|
234
|
+
for (const candidate of allowedEnabled) {
|
|
235
|
+
if (candidate === current) continue;
|
|
236
|
+
const candidateRank = REASONING_VALUE_RANK[candidate];
|
|
237
|
+
if (candidateRank === undefined) continue;
|
|
238
|
+
const distance = Math.abs(candidateRank - currentRank);
|
|
239
|
+
if (distance < bestDistance || (distance === bestDistance && candidateRank > bestRank)) {
|
|
240
|
+
best = candidate;
|
|
241
|
+
bestDistance = distance;
|
|
242
|
+
bestRank = candidateRank;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return best;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @internal */
|
|
249
|
+
export function resolveOpenAIReasoningEffortFallback(
|
|
250
|
+
error: unknown,
|
|
251
|
+
captured: CapturedHttpErrorResponse | undefined,
|
|
252
|
+
params: unknown,
|
|
253
|
+
options?: { explicitDisable?: boolean },
|
|
254
|
+
): OpenAIReasoningEffortFallback | undefined {
|
|
255
|
+
const currentEffort = readOpenAIReasoningEffort(params);
|
|
256
|
+
if (!currentEffort || !KNOWN_REASONING_VALUE[currentEffort.toLowerCase()]) return undefined;
|
|
257
|
+
if (!isInvalidReasoningEffortError(error, captured, currentEffort)) return undefined;
|
|
258
|
+
const message = collectMessageParts(error, captured);
|
|
259
|
+
const allowed = parseAllowedReasoningValues(message, currentEffort);
|
|
260
|
+
const normalizedCurrent = currentEffort.toLowerCase();
|
|
261
|
+
if (allowed === undefined) return null;
|
|
262
|
+
if (options?.explicitDisable) {
|
|
263
|
+
if (normalizedCurrent !== "none" && allowed.has("none")) return "none";
|
|
264
|
+
const fallback = lowestEnabledAllowedValue(allowed);
|
|
265
|
+
return fallback && fallback !== normalizedCurrent ? fallback : null;
|
|
266
|
+
}
|
|
267
|
+
if (normalizedCurrent === "none") return null;
|
|
268
|
+
return nearestEnabledReasoningFallback(normalizedCurrent, allowed) ?? null;
|
|
269
|
+
}
|
|
@@ -44,6 +44,17 @@ import {
|
|
|
44
44
|
type OpenAIResponsesToolChoice,
|
|
45
45
|
} from "../utils/tool-choice";
|
|
46
46
|
import { compactGrammarDefinition } from "./grammar";
|
|
47
|
+
import {
|
|
48
|
+
applyOpenAIReasoningEffortFallback,
|
|
49
|
+
clearOpenAIReasoningEffortFallbackState,
|
|
50
|
+
createOpenAIReasoningEffortFallbackKey,
|
|
51
|
+
createOpenAIReasoningEffortFallbackState,
|
|
52
|
+
getOpenAIReasoningEffortFallback,
|
|
53
|
+
type OpenAIReasoningEffortFallback,
|
|
54
|
+
type OpenAIReasoningEffortFallbackState,
|
|
55
|
+
rememberOpenAIReasoningEffortFallback,
|
|
56
|
+
resolveOpenAIReasoningEffortFallback,
|
|
57
|
+
} from "./openai-reasoning-fallback";
|
|
47
58
|
import type {
|
|
48
59
|
Tool as OpenAITool,
|
|
49
60
|
ResponseCreateParamsStreaming,
|
|
@@ -140,7 +151,10 @@ const OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE =
|
|
|
140
151
|
/** Consecutive stale-previous-response failures before chaining is disabled for the session. */
|
|
141
152
|
const OPENAI_RESPONSES_CHAIN_STALE_FAILURE_LIMIT = 3;
|
|
142
153
|
|
|
143
|
-
interface OpenAIResponsesProviderSessionState
|
|
154
|
+
interface OpenAIResponsesProviderSessionState
|
|
155
|
+
extends ProviderSessionState,
|
|
156
|
+
OpenAIStrictToolsState,
|
|
157
|
+
OpenAIReasoningEffortFallbackState {
|
|
144
158
|
nativeHistoryReplayWarmed: boolean;
|
|
145
159
|
/** Stateful `previous_response_id` chain baselines, keyed by baseUrl/model/session. */
|
|
146
160
|
chains: Map<string, OpenAIResponsesChainState>;
|
|
@@ -164,14 +178,17 @@ interface OpenAIResponsesChainState {
|
|
|
164
178
|
|
|
165
179
|
function createOpenAIResponsesProviderSessionState(): OpenAIResponsesProviderSessionState {
|
|
166
180
|
const strictToolsState = createOpenAIStrictToolsState();
|
|
181
|
+
const reasoningEffortFallbackState = createOpenAIReasoningEffortFallbackState();
|
|
167
182
|
const state: OpenAIResponsesProviderSessionState = {
|
|
168
183
|
...strictToolsState,
|
|
184
|
+
...reasoningEffortFallbackState,
|
|
169
185
|
nativeHistoryReplayWarmed: false,
|
|
170
186
|
chains: new Map(),
|
|
171
187
|
close: () => {
|
|
172
188
|
state.nativeHistoryReplayWarmed = false;
|
|
173
189
|
state.chains.clear();
|
|
174
190
|
clearOpenAIStrictToolsState(state);
|
|
191
|
+
clearOpenAIReasoningEffortFallbackState(state);
|
|
175
192
|
},
|
|
176
193
|
};
|
|
177
194
|
return state;
|
|
@@ -385,6 +402,26 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
385
402
|
const { trailingScaffoldingItems } = builtParams;
|
|
386
403
|
let activeParams = params;
|
|
387
404
|
let activeTrailingScaffoldingItems = trailingScaffoldingItems;
|
|
405
|
+
const resolvedBaseUrl = (baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
406
|
+
const requestReasoningEffortFallbacks = new Map<string, OpenAIReasoningEffortFallback>();
|
|
407
|
+
const attemptedReasoningEffortFallbacks = new Set<string>();
|
|
408
|
+
let pendingReasoningEffortFallback: { key: string; fallback: OpenAIReasoningEffortFallback } | undefined;
|
|
409
|
+
let activeReasoningEffortFallbackKey: string | undefined;
|
|
410
|
+
let activeRequestParams: OpenAIResponsesSamplingParams | undefined;
|
|
411
|
+
const applyReasoningEffortFallbackForRequest = (requestParams: OpenAIResponsesSamplingParams): string => {
|
|
412
|
+
const fallbackKey = createOpenAIReasoningEffortFallbackKey(
|
|
413
|
+
"responses",
|
|
414
|
+
resolvedBaseUrl,
|
|
415
|
+
typeof requestParams.model === "string" ? requestParams.model : model.id,
|
|
416
|
+
);
|
|
417
|
+
const requestReasoningEffortFallback = requestReasoningEffortFallbacks.has(fallbackKey)
|
|
418
|
+
? requestReasoningEffortFallbacks.get(fallbackKey)
|
|
419
|
+
: getOpenAIReasoningEffortFallback(providerSessionState, fallbackKey);
|
|
420
|
+
if (requestReasoningEffortFallback !== undefined) {
|
|
421
|
+
applyOpenAIReasoningEffortFallback(requestParams, requestReasoningEffortFallback);
|
|
422
|
+
}
|
|
423
|
+
return fallbackKey;
|
|
424
|
+
};
|
|
388
425
|
if (isOpenAIResponsesStatefulEnabled(options, baseUrl) && routingSessionId && providerSessionState) {
|
|
389
426
|
chainState = getOpenAIResponsesChainState(providerSessionState, model, baseUrl, routingSessionId);
|
|
390
427
|
if (!chainState.disabled) {
|
|
@@ -392,6 +429,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
392
429
|
params.store = true;
|
|
393
430
|
}
|
|
394
431
|
}
|
|
432
|
+
applyReasoningEffortFallbackForRequest(params);
|
|
395
433
|
let chained: OpenAIResponsesChainedParams =
|
|
396
434
|
chainState && !chainState.disabled
|
|
397
435
|
? buildOpenAIResponsesChainedParams(params, trailingScaffoldingItems, chainState)
|
|
@@ -402,12 +440,13 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
402
440
|
options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
403
441
|
const requestTimeoutMs =
|
|
404
442
|
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
405
|
-
const requestUrl = `${
|
|
443
|
+
const requestUrl = `${resolvedBaseUrl}/responses`;
|
|
406
444
|
const applyPayloadReplacement = async (requestParams: OpenAIResponsesSamplingParams) => {
|
|
407
445
|
const replacementPayload = await options?.onPayload?.(requestParams, model);
|
|
408
|
-
|
|
409
|
-
? (replacementPayload as OpenAIResponsesSamplingParams)
|
|
410
|
-
|
|
446
|
+
const payload =
|
|
447
|
+
replacementPayload !== undefined ? (replacementPayload as OpenAIResponsesSamplingParams) : requestParams;
|
|
448
|
+
applyReasoningEffortFallbackForRequest(payload);
|
|
449
|
+
return payload;
|
|
411
450
|
};
|
|
412
451
|
chained = { ...chained, params: await applyPayloadReplacement(chained.params) };
|
|
413
452
|
rawRequestDump = {
|
|
@@ -418,8 +457,14 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
418
457
|
url: requestUrl,
|
|
419
458
|
body: chained.params,
|
|
420
459
|
};
|
|
421
|
-
const openResponsesStream = (requestParams: OpenAIResponsesSamplingParams) =>
|
|
422
|
-
|
|
460
|
+
const openResponsesStream = (requestParams: OpenAIResponsesSamplingParams) => {
|
|
461
|
+
activeReasoningEffortFallbackKey = createOpenAIReasoningEffortFallbackKey(
|
|
462
|
+
"responses",
|
|
463
|
+
resolvedBaseUrl,
|
|
464
|
+
typeof requestParams.model === "string" ? requestParams.model : model.id,
|
|
465
|
+
);
|
|
466
|
+
activeRequestParams = requestParams;
|
|
467
|
+
return callWithCopilotModelRetry(
|
|
423
468
|
async () => {
|
|
424
469
|
let requestTimeout: NodeJS.Timeout | undefined;
|
|
425
470
|
if (requestTimeoutMs !== undefined) {
|
|
@@ -458,6 +503,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
458
503
|
},
|
|
459
504
|
{ provider: model.provider, signal: requestSignal },
|
|
460
505
|
);
|
|
506
|
+
};
|
|
461
507
|
let openaiStream: AsyncIterable<ResponseStreamEvent>;
|
|
462
508
|
let strictRetryAvailable = true;
|
|
463
509
|
let activeStrictToolsApplied = builtParams.strictToolsApplied;
|
|
@@ -465,9 +511,37 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
465
511
|
while (true) {
|
|
466
512
|
try {
|
|
467
513
|
openaiStream = await openResponsesStream(chained.params);
|
|
514
|
+
if (pendingReasoningEffortFallback) {
|
|
515
|
+
rememberOpenAIReasoningEffortFallback(
|
|
516
|
+
providerSessionState,
|
|
517
|
+
pendingReasoningEffortFallback.key,
|
|
518
|
+
pendingReasoningEffortFallback.fallback,
|
|
519
|
+
);
|
|
520
|
+
pendingReasoningEffortFallback = undefined;
|
|
521
|
+
}
|
|
468
522
|
break;
|
|
469
523
|
} catch (error) {
|
|
470
524
|
const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
|
|
525
|
+
const reasoningEffortFallback =
|
|
526
|
+
activeReasoningEffortFallbackKey && activeRequestParams && !requestSignal.aborted
|
|
527
|
+
? resolveOpenAIReasoningEffortFallback(error, capturedErrorResponse, activeRequestParams, {
|
|
528
|
+
explicitDisable: options?.disableReasoning === true && options.reasoning === undefined,
|
|
529
|
+
})
|
|
530
|
+
: undefined;
|
|
531
|
+
if (reasoningEffortFallback !== undefined && activeReasoningEffortFallbackKey) {
|
|
532
|
+
const retryMarker = `${activeReasoningEffortFallbackKey}:${String(reasoningEffortFallback)}`;
|
|
533
|
+
if (attemptedReasoningEffortFallbacks.has(retryMarker)) throw error;
|
|
534
|
+
attemptedReasoningEffortFallbacks.add(retryMarker);
|
|
535
|
+
requestReasoningEffortFallbacks.set(activeReasoningEffortFallbackKey, reasoningEffortFallback);
|
|
536
|
+
applyOpenAIReasoningEffortFallback(chained.params, reasoningEffortFallback);
|
|
537
|
+
applyOpenAIReasoningEffortFallback(activeParams, reasoningEffortFallback);
|
|
538
|
+
rawRequestDump.body = chained.params;
|
|
539
|
+
pendingReasoningEffortFallback = {
|
|
540
|
+
key: activeReasoningEffortFallbackKey,
|
|
541
|
+
fallback: reasoningEffortFallback,
|
|
542
|
+
};
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
471
545
|
const compiledGrammarTooLarge =
|
|
472
546
|
isOpenRouterAnthropicModel(model) &&
|
|
473
547
|
isCompiledGrammarTooLargeStrictError(error, capturedErrorResponse);
|
package/src/types.ts
CHANGED
|
@@ -656,8 +656,8 @@ export interface Tool<TParameters extends TSchema = TSchema> {
|
|
|
656
656
|
* Illustrative calls/notes; the AI layer renders them into an `<examples>`
|
|
657
657
|
* block in the model's native tool-call syntax and appends to the wire
|
|
658
658
|
* description. Author `call`/`bad`/`good` as plain argument objects WITHOUT
|
|
659
|
-
* `
|
|
660
|
-
* a placeholder `
|
|
659
|
+
* `i` — when intent tracing injects `i` into the schema, the renderer adds
|
|
660
|
+
* a placeholder `i` automatically. Type each tool's `examples` against its
|
|
661
661
|
* own schema (e.g. `readonly ToolExample<typeof schema["type"]>[]`).
|
|
662
662
|
*/
|
|
663
663
|
examples?: readonly ToolExample[];
|
package/src/utils/validation.ts
CHANGED
|
@@ -1291,6 +1291,18 @@ function truncateArgsForError(value: unknown): unknown {
|
|
|
1291
1291
|
*/
|
|
1292
1292
|
export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall["arguments"] {
|
|
1293
1293
|
const originalArgs = toolCall.arguments;
|
|
1294
|
+
if (originalArgs && typeof originalArgs === "object" && "__parseError" in originalArgs) {
|
|
1295
|
+
const parseError = originalArgs.__parseError;
|
|
1296
|
+
const rawJson = String(originalArgs.__rawJson ?? "");
|
|
1297
|
+
const maxLen = 512;
|
|
1298
|
+
const truncatedRawJson =
|
|
1299
|
+
rawJson.length <= maxLen
|
|
1300
|
+
? rawJson
|
|
1301
|
+
: `${rawJson.slice(0, maxLen)}… [truncated ${rawJson.length - maxLen} chars]`;
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`Validation failed for tool "${toolCall.name}": Tool call arguments are not valid JSON.\nParse Error: ${parseError}\nRaw JSON:\n${truncatedRawJson}`,
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1294
1306
|
const ctx = getValidationContext(tool);
|
|
1295
1307
|
const { json } = ctx;
|
|
1296
1308
|
|