@oh-my-pi/pi-ai 15.1.8 → 15.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/types/auth-broker/remote-store.d.ts +20 -0
- package/dist/types/auth-storage.d.ts +23 -0
- package/package.json +2 -2
- package/src/auth-broker/remote-store.ts +89 -0
- package/src/auth-storage.ts +34 -3
- package/src/providers/ollama.ts +26 -1
- package/src/providers/openai-completions.ts +21 -3
- package/src/utils/http-inspector.ts +5 -1
- package/src/utils/overflow.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.2.0] - 2026-05-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `/login` (and `/logout`, plus any `AuthStorage.set` / `remove` call) against a remote auth-broker throwing `RemoteAuthCredentialStore is read-only on the client. Use 'omp auth-broker login <provider>' to mutate credentials.` Added three optional async write hooks to `AuthCredentialStore` (`upsertAuthCredentialRemote`, `replaceAuthCredentialsRemote`, `deleteAuthCredentialsRemote`); `RemoteAuthCredentialStore` implements them via the broker's `POST /v1/credential` and `POST /v1/credential/:id/disable` endpoints and applies the broker's authoritative post-write entries to the local snapshot. `AuthStorage` routes through the hooks when present, so OAuth and API-key logins (and logouts) initiated from a broker-backed client now persist server-side and surface immediately without waiting for the long-poll snapshot tick.
|
|
10
|
+
|
|
11
|
+
## [15.1.9] - 2026-05-21
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed Ollama named tool forcing to send only the requested tool when the caller passes a named `toolChoice`, preserving `tool_choice: "required"` while preventing local models from selecting a different tool. ([#1236](https://github.com/can1357/oh-my-pi/issues/1236))
|
|
16
|
+
- Fixed `/btw` (and IRC background replies) returning a `BedrockException` 400 (`The toolConfig field must be defined when using toolUse and toolResult content blocks.`) on LiteLLM → Bedrock once the session has tool-call history. Two source fixes in `buildParams`: (1) `if (context.tools)` → `if (context.tools?.length)` so an explicit `context.tools = []` (the /btw opt-out) never routes through `convertTools` and never emits an empty `"tools"` array; (2) `else if (hasToolHistory(...))` → `else if (context.tools === undefined && hasToolHistory(...))` so the Anthropic-proxy sentinel that injects `tools: []` for tool-history turns is suppressed when the caller explicitly opted out, preventing it from re-introducing the empty array. As defence-in-depth, `tool_choice: "none"` is also dropped when the resolved tools list is missing or empty. ([#1227](https://github.com/can1357/oh-my-pi/issues/1227))
|
|
17
|
+
|
|
5
18
|
## [15.1.8] - 2026-05-20
|
|
6
19
|
### Added
|
|
7
20
|
|
|
@@ -40,6 +40,26 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
40
40
|
replaceAuthCredentialsForProvider(_provider: string, _credentials: AuthCredential[]): StoredAuthCredential[];
|
|
41
41
|
upsertAuthCredentialForProvider(_provider: string, _credential: AuthCredential): StoredAuthCredential[];
|
|
42
42
|
deleteAuthCredentialsForProvider(_provider: string, _disabledCause: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Upsert a single credential through the broker. The broker server is the
|
|
45
|
+
* canonical writer — see `POST /v1/credential`. The redacted snapshot
|
|
46
|
+
* entries returned by the server replace the provider's rows in our local
|
|
47
|
+
* snapshot, and the global snapshot is then refreshed in the background so
|
|
48
|
+
* any concurrent peer (refresh, generation bump) stays in sync.
|
|
49
|
+
*/
|
|
50
|
+
upsertAuthCredentialRemote(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Replace-all semantics: disable every active credential for the provider,
|
|
53
|
+
* then upload each of the new credentials. Used by API-key login so a new
|
|
54
|
+
* key clobbers any previously stored key for the same provider.
|
|
55
|
+
*/
|
|
56
|
+
replaceAuthCredentialsRemote(provider: string, credentials: AuthCredential[]): Promise<StoredAuthCredential[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Logout: disable every active credential for the provider on the broker,
|
|
59
|
+
* then drop them from the local snapshot. Refresh fetches the authoritative
|
|
60
|
+
* post-state in the background.
|
|
61
|
+
*/
|
|
62
|
+
deleteAuthCredentialsRemote(provider: string, disabledCause: string): Promise<void>;
|
|
43
63
|
getCache(key: string): string | null;
|
|
44
64
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
45
65
|
cleanExpiredCache(): void;
|
|
@@ -152,6 +152,29 @@ export interface AuthCredentialStore {
|
|
|
152
152
|
markCredentialSuspect?(credentialId: number, opts?: {
|
|
153
153
|
signal?: AbortSignal;
|
|
154
154
|
}): Promise<void>;
|
|
155
|
+
/**
|
|
156
|
+
* Optional async write hook for upserting a single credential. When present,
|
|
157
|
+
* `AuthStorage.#upsertOAuthCredential` routes through this instead of the
|
|
158
|
+
* sync `upsertAuthCredentialForProvider`. `RemoteAuthCredentialStore` uses
|
|
159
|
+
* it to send the upsert to the broker via `POST /v1/credential`.
|
|
160
|
+
*
|
|
161
|
+
* Implementations MUST update the in-memory snapshot before returning so the
|
|
162
|
+
* post-write read path is consistent.
|
|
163
|
+
*/
|
|
164
|
+
upsertAuthCredentialRemote?(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
|
|
165
|
+
/**
|
|
166
|
+
* Optional async write hook for replace-all semantics (e.g. API-key login
|
|
167
|
+
* overwriting any previous keys for the same provider). When present,
|
|
168
|
+
* `AuthStorage.set` routes through this instead of the sync
|
|
169
|
+
* `replaceAuthCredentialsForProvider`.
|
|
170
|
+
*/
|
|
171
|
+
replaceAuthCredentialsRemote?(provider: string, credentials: AuthCredential[]): Promise<StoredAuthCredential[]>;
|
|
172
|
+
/**
|
|
173
|
+
* Optional async write hook for clearing every credential for a provider
|
|
174
|
+
* (logout). When present, `AuthStorage.remove` routes through this instead
|
|
175
|
+
* of the sync `deleteAuthCredentialsForProvider`.
|
|
176
|
+
*/
|
|
177
|
+
deleteAuthCredentialsRemote?(provider: string, disabledCause: string): Promise<void>;
|
|
155
178
|
}
|
|
156
179
|
/**
|
|
157
180
|
* Event payload describing a credential that was just soft-disabled.
|
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.2.1",
|
|
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.2.1",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -11,6 +11,7 @@ import { scheduler } from "node:timers/promises";
|
|
|
11
11
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import {
|
|
13
13
|
type AuthCredential,
|
|
14
|
+
type AuthCredentialSnapshotEntry,
|
|
14
15
|
type AuthCredentialStore,
|
|
15
16
|
type OAuthCredential,
|
|
16
17
|
REMOTE_REFRESH_SENTINEL,
|
|
@@ -212,6 +213,94 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
212
213
|
);
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Upsert a single credential through the broker. The broker server is the
|
|
218
|
+
* canonical writer — see `POST /v1/credential`. The redacted snapshot
|
|
219
|
+
* entries returned by the server replace the provider's rows in our local
|
|
220
|
+
* snapshot, and the global snapshot is then refreshed in the background so
|
|
221
|
+
* any concurrent peer (refresh, generation bump) stays in sync.
|
|
222
|
+
*/
|
|
223
|
+
async upsertAuthCredentialRemote(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]> {
|
|
224
|
+
const { entries } = await this.#client.uploadCredential(provider, credential);
|
|
225
|
+
this.#applyProviderEntries(provider, entries);
|
|
226
|
+
void this.refreshSnapshot().catch(error => {
|
|
227
|
+
logger.debug("auth-broker snapshot refresh after upload failed", { error: String(error) });
|
|
228
|
+
});
|
|
229
|
+
return this.listAuthCredentials(provider);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Replace-all semantics: disable every active credential for the provider,
|
|
234
|
+
* then upload each of the new credentials. Used by API-key login so a new
|
|
235
|
+
* key clobbers any previously stored key for the same provider.
|
|
236
|
+
*/
|
|
237
|
+
async replaceAuthCredentialsRemote(
|
|
238
|
+
provider: string,
|
|
239
|
+
credentials: AuthCredential[],
|
|
240
|
+
): Promise<StoredAuthCredential[]> {
|
|
241
|
+
const existing = this.listAuthCredentials(provider);
|
|
242
|
+
for (const entry of existing) {
|
|
243
|
+
try {
|
|
244
|
+
await this.#client.disableCredential(entry.id, "replaced by newer credential");
|
|
245
|
+
} catch (error) {
|
|
246
|
+
logger.warn("auth-broker disable during replace failed", {
|
|
247
|
+
provider,
|
|
248
|
+
id: entry.id,
|
|
249
|
+
error: String(error),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Snapshot reflects the disables before we add the new rows so a concurrent
|
|
254
|
+
// reader cannot momentarily see old + new together for the same provider.
|
|
255
|
+
this.#removeProviderEntries(provider);
|
|
256
|
+
for (const credential of credentials) {
|
|
257
|
+
const { entries } = await this.#client.uploadCredential(provider, credential);
|
|
258
|
+
this.#applyProviderEntries(provider, entries);
|
|
259
|
+
}
|
|
260
|
+
void this.refreshSnapshot().catch(error => {
|
|
261
|
+
logger.debug("auth-broker snapshot refresh after replace failed", { error: String(error) });
|
|
262
|
+
});
|
|
263
|
+
return this.listAuthCredentials(provider);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Logout: disable every active credential for the provider on the broker,
|
|
268
|
+
* then drop them from the local snapshot. Refresh fetches the authoritative
|
|
269
|
+
* post-state in the background.
|
|
270
|
+
*/
|
|
271
|
+
async deleteAuthCredentialsRemote(provider: string, disabledCause: string): Promise<void> {
|
|
272
|
+
const existing = this.listAuthCredentials(provider);
|
|
273
|
+
for (const entry of existing) {
|
|
274
|
+
try {
|
|
275
|
+
await this.#client.disableCredential(entry.id, disabledCause);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
logger.warn("auth-broker disable during delete failed", {
|
|
278
|
+
provider,
|
|
279
|
+
id: entry.id,
|
|
280
|
+
error: String(error),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this.#removeProviderEntries(provider);
|
|
285
|
+
void this.refreshSnapshot().catch(error => {
|
|
286
|
+
logger.debug("auth-broker snapshot refresh after delete failed", { error: String(error) });
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#applyProviderEntries(provider: string, entries: AuthCredentialSnapshotEntry[]): void {
|
|
291
|
+
// `entries` is the broker's authoritative post-upsert list of rows for
|
|
292
|
+
// `provider`. Drop our existing rows for the same provider and splice in
|
|
293
|
+
// the fresh set — preserving every other provider's rows in place.
|
|
294
|
+
const others = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
|
|
295
|
+
const incoming = entries.map(entry => ({ ...entry, rotatesInMs: null }));
|
|
296
|
+
this.#snapshot = { ...this.#snapshot, credentials: [...others, ...incoming] };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#removeProviderEntries(provider: string): void {
|
|
300
|
+
const next = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
|
|
301
|
+
this.#snapshot = { ...this.#snapshot, credentials: next };
|
|
302
|
+
}
|
|
303
|
+
|
|
215
304
|
getCache(key: string): string | null {
|
|
216
305
|
const entry = this.#cache.get(key);
|
|
217
306
|
if (!entry) return null;
|
package/src/auth-storage.ts
CHANGED
|
@@ -198,6 +198,29 @@ export interface AuthCredentialStore {
|
|
|
198
198
|
* {@link AuthStorage.invalidateCredentialMatching} fall back to `reload()`.
|
|
199
199
|
*/
|
|
200
200
|
markCredentialSuspect?(credentialId: number, opts?: { signal?: AbortSignal }): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* Optional async write hook for upserting a single credential. When present,
|
|
203
|
+
* `AuthStorage.#upsertOAuthCredential` routes through this instead of the
|
|
204
|
+
* sync `upsertAuthCredentialForProvider`. `RemoteAuthCredentialStore` uses
|
|
205
|
+
* it to send the upsert to the broker via `POST /v1/credential`.
|
|
206
|
+
*
|
|
207
|
+
* Implementations MUST update the in-memory snapshot before returning so the
|
|
208
|
+
* post-write read path is consistent.
|
|
209
|
+
*/
|
|
210
|
+
upsertAuthCredentialRemote?(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
|
|
211
|
+
/**
|
|
212
|
+
* Optional async write hook for replace-all semantics (e.g. API-key login
|
|
213
|
+
* overwriting any previous keys for the same provider). When present,
|
|
214
|
+
* `AuthStorage.set` routes through this instead of the sync
|
|
215
|
+
* `replaceAuthCredentialsForProvider`.
|
|
216
|
+
*/
|
|
217
|
+
replaceAuthCredentialsRemote?(provider: string, credentials: AuthCredential[]): Promise<StoredAuthCredential[]>;
|
|
218
|
+
/**
|
|
219
|
+
* Optional async write hook for clearing every credential for a provider
|
|
220
|
+
* (logout). When present, `AuthStorage.remove` routes through this instead
|
|
221
|
+
* of the sync `deleteAuthCredentialsForProvider`.
|
|
222
|
+
*/
|
|
223
|
+
deleteAuthCredentialsRemote?(provider: string, disabledCause: string): Promise<void>;
|
|
201
224
|
}
|
|
202
225
|
|
|
203
226
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1076,7 +1099,9 @@ export class AuthStorage {
|
|
|
1076
1099
|
async set(provider: string, credential: AuthCredentialEntry): Promise<void> {
|
|
1077
1100
|
const normalized = Array.isArray(credential) ? credential : [credential];
|
|
1078
1101
|
const deduped = this.#dedupeOAuthCredentials(provider, normalized);
|
|
1079
|
-
const stored = this.#store.
|
|
1102
|
+
const stored = this.#store.replaceAuthCredentialsRemote
|
|
1103
|
+
? await this.#store.replaceAuthCredentialsRemote(provider, deduped)
|
|
1104
|
+
: this.#store.replaceAuthCredentialsForProvider(provider, deduped);
|
|
1080
1105
|
this.#setStoredCredentials(
|
|
1081
1106
|
provider,
|
|
1082
1107
|
stored.map(record => ({ id: record.id, credential: record.credential })),
|
|
@@ -1085,7 +1110,9 @@ export class AuthStorage {
|
|
|
1085
1110
|
}
|
|
1086
1111
|
|
|
1087
1112
|
async #upsertOAuthCredential(provider: string, credential: OAuthCredential): Promise<void> {
|
|
1088
|
-
const stored = this.#store.
|
|
1113
|
+
const stored = this.#store.upsertAuthCredentialRemote
|
|
1114
|
+
? await this.#store.upsertAuthCredentialRemote(provider, credential)
|
|
1115
|
+
: this.#store.upsertAuthCredentialForProvider(provider, credential);
|
|
1089
1116
|
this.#setStoredCredentials(
|
|
1090
1117
|
provider,
|
|
1091
1118
|
stored.map(record => ({ id: record.id, credential: record.credential })),
|
|
@@ -1097,7 +1124,11 @@ export class AuthStorage {
|
|
|
1097
1124
|
* Remove credential for a provider.
|
|
1098
1125
|
*/
|
|
1099
1126
|
async remove(provider: string): Promise<void> {
|
|
1100
|
-
this.#store.
|
|
1127
|
+
if (this.#store.deleteAuthCredentialsRemote) {
|
|
1128
|
+
await this.#store.deleteAuthCredentialsRemote(provider, "deleted by user");
|
|
1129
|
+
} else {
|
|
1130
|
+
this.#store.deleteAuthCredentialsForProvider(provider, "deleted by user");
|
|
1131
|
+
}
|
|
1101
1132
|
this.#setStoredCredentials(provider, []);
|
|
1102
1133
|
this.#resetProviderAssignments(provider);
|
|
1103
1134
|
}
|
package/src/providers/ollama.ts
CHANGED
|
@@ -116,6 +116,29 @@ function mapToolChoice(toolChoice: ToolChoice | undefined): "auto" | "none" | "r
|
|
|
116
116
|
return undefined;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function getNamedToolChoiceName(toolChoice: ToolChoice | undefined): string | undefined {
|
|
120
|
+
if (!toolChoice || typeof toolChoice === "string") {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
if ("function" in toolChoice) {
|
|
124
|
+
return toolChoice.function.name;
|
|
125
|
+
}
|
|
126
|
+
return toolChoice.name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function selectToolsForToolChoice(tools: Tool[] | undefined, toolChoice: ToolChoice | undefined): Tool[] | undefined {
|
|
130
|
+
const toolName = getNamedToolChoiceName(toolChoice);
|
|
131
|
+
if (!toolName || !tools) {
|
|
132
|
+
return tools;
|
|
133
|
+
}
|
|
134
|
+
for (const tool of tools) {
|
|
135
|
+
if (tool.name === toolName) {
|
|
136
|
+
return [tool];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
function toPlainContent(content: string | Array<{ type: "text" | "image"; text?: string; data?: string }>): {
|
|
120
143
|
content: string;
|
|
121
144
|
images?: string[];
|
|
@@ -231,10 +254,12 @@ function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefin
|
|
|
231
254
|
function createChatBody(model: Model<"ollama-chat">, context: Context, options: OllamaChatOptions | undefined) {
|
|
232
255
|
const think = mapReasoning(options?.reasoning);
|
|
233
256
|
const toolChoice = mapToolChoice(options?.toolChoice);
|
|
257
|
+
const selectedTools = selectToolsForToolChoice(context.tools, options?.toolChoice);
|
|
258
|
+
const tools = convertTools(selectedTools);
|
|
234
259
|
return {
|
|
235
260
|
model: model.id,
|
|
236
261
|
messages: convertMessages(model, context),
|
|
237
|
-
...(
|
|
262
|
+
...(tools ? { tools } : {}),
|
|
238
263
|
...(think !== undefined ? { think } : {}),
|
|
239
264
|
...(toolChoice !== undefined ? { tool_choice: toolChoice } : {}),
|
|
240
265
|
...(options?.maxTokens !== undefined ? { options: { num_predict: options.maxTokens } } : {}),
|
|
@@ -1110,12 +1110,18 @@ function buildParams(
|
|
|
1110
1110
|
}
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
-
if (context.tools) {
|
|
1113
|
+
if (context.tools?.length) {
|
|
1114
1114
|
const builtTools = convertTools(context.tools, compat, toolStrictModeOverride);
|
|
1115
1115
|
params.tools = builtTools.tools;
|
|
1116
1116
|
toolStrictMode = builtTools.toolStrictMode;
|
|
1117
|
-
} else if (hasToolHistory(context.messages)) {
|
|
1118
|
-
// Anthropic (via LiteLLM/proxy) requires tools param when conversation
|
|
1117
|
+
} else if (context.tools === undefined && hasToolHistory(context.messages)) {
|
|
1118
|
+
// Anthropic (via LiteLLM/proxy) requires the `tools` param when the conversation
|
|
1119
|
+
// contains tool_calls/tool_results, even when no tools are offered this turn.
|
|
1120
|
+
// Only inject the sentinel when the caller passed `context.tools = undefined`
|
|
1121
|
+
// (i.e. tools were not specified at all). An explicit `context.tools = []` means
|
|
1122
|
+
// the caller opted out of tools for this turn (as /btw and IRC background replies
|
|
1123
|
+
// do via AgentSession.runEphemeralTurn) — honour that intent and emit nothing,
|
|
1124
|
+
// so LiteLLM → Bedrock never sees an empty `toolConfig` block.
|
|
1119
1125
|
params.tools = [];
|
|
1120
1126
|
}
|
|
1121
1127
|
|
|
@@ -1123,6 +1129,18 @@ function buildParams(
|
|
|
1123
1129
|
params.tool_choice = mapToOpenAICompletionsToolChoice(options.toolChoice);
|
|
1124
1130
|
}
|
|
1125
1131
|
|
|
1132
|
+
if (params.tool_choice === "none" && (!Array.isArray(params.tools) || params.tools.length === 0)) {
|
|
1133
|
+
// `tool_choice: "none"` with no tools to gate is redundant and also
|
|
1134
|
+
// trips LiteLLM → Bedrock: the proxy serializes the directive into a
|
|
1135
|
+
// `toolConfig` block, and Bedrock requires `toolConfig.tools` to be
|
|
1136
|
+
// non-empty whenever the conversation already holds `toolUse`/`toolResult`
|
|
1137
|
+
// content. Drop it whenever the resolved tools list is missing or empty.
|
|
1138
|
+
// Side-channel turns hit this: `/btw` and IRC background replies route
|
|
1139
|
+
// through `AgentSession.runEphemeralTurn`, which sets `context.tools = []`
|
|
1140
|
+
// and `toolChoice: "none"` (see packages/coding-agent/src/session/agent-session.ts).
|
|
1141
|
+
delete params.tool_choice;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1126
1144
|
if (supportsReasoningParams && compat.thinkingFormat === "zai" && model.reasoning) {
|
|
1127
1145
|
// Z.ai uses binary thinking: { type: "enabled" | "disabled" }
|
|
1128
1146
|
// Must explicitly disable since z.ai defaults to thinking enabled.
|
|
@@ -129,7 +129,11 @@ function formatCapturedHttpError(captured: CapturedHttpErrorResponse | undefined
|
|
|
129
129
|
if (!payload) return bodyText;
|
|
130
130
|
|
|
131
131
|
const errorPayload = getObjectProperty(payload, "error") ?? payload;
|
|
132
|
-
|
|
132
|
+
// {"error": "string"} — the error value is a plain string, not a nested object.
|
|
133
|
+
// Fall back to it when the structured fields ("message", etc.) are absent.
|
|
134
|
+
const stringError = errorPayload === payload ? getStringProperty(payload, "error") : undefined;
|
|
135
|
+
const message =
|
|
136
|
+
getStringProperty(errorPayload, "message") ?? getStringProperty(payload, "message") ?? stringError ?? bodyText;
|
|
133
137
|
const extras = [
|
|
134
138
|
getStringProperty(errorPayload, "type") ?? getStringProperty(payload, "type"),
|
|
135
139
|
getStringProperty(errorPayload, "param") ?? getStringProperty(payload, "param"),
|
package/src/utils/overflow.ts
CHANGED
|
@@ -108,9 +108,12 @@ export function isContextOverflow(message: AssistantMessage, contextWindow?: num
|
|
|
108
108
|
return true;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// Cerebras and Mistral return 400/413 with no body for context overflow
|
|
111
|
+
// Cerebras and Mistral return 400/413 with no body for context overflow.
|
|
112
|
+
// Proxy providers (e.g. api.synthetic.new) wrap upstream 400/413 no-body
|
|
113
|
+
// responses in a JSON envelope, so the status code phrase may appear
|
|
114
|
+
// anywhere in the message rather than at its start.
|
|
112
115
|
// Note: 429 is rate limiting (requests/tokens per time), NOT context overflow
|
|
113
|
-
if (
|
|
116
|
+
if (/\b4(00|13)\s*(status code)?\s*\(no body\)/i.test(message.errorMessage)) {
|
|
114
117
|
return true;
|
|
115
118
|
}
|
|
116
119
|
}
|