@oh-my-pi/pi-ai 16.1.7 → 16.1.8
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 +14 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +0 -6
- package/dist/types/providers/openai-codex-responses.d.ts +3 -1
- package/dist/types/registry/nvidia.d.ts +1 -0
- package/dist/types/registry/registry.d.ts +1 -0
- package/dist/types/registry/types.d.ts +2 -0
- package/dist/types/types.d.ts +6 -4
- package/package.json +4 -4
- package/src/auth-gateway/server.ts +18 -12
- package/src/auth-storage.ts +14 -5
- package/src/providers/amazon-bedrock.ts +71 -20
- package/src/providers/anthropic.ts +6 -1
- package/src/providers/openai-codex/request-transformer.ts +4 -6
- package/src/providers/openai-codex-responses.ts +8 -30
- package/src/providers/openai-completions.ts +15 -1
- package/src/providers/openai-responses.ts +0 -9
- package/src/registry/nvidia.ts +2 -0
- package/src/registry/types.ts +2 -0
- package/src/stream.ts +29 -0
- package/src/types.ts +13 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.8] - 2026-06-20
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Changed OpenAI Responses and Codex Responses custom grammar tool requests to leave `parallel_tool_calls` unset instead of forcing serial tool calls; Codex `responsesLite` still disables parallel tool calls when tools are present.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed Bedrock `/btw` and other no-tool ephemeral turns failing after prior tool calls by sending the required sentinel `toolConfig` whenever replayed history contains `toolUse`/`toolResult` blocks. ([#3124](https://github.com/can1357/oh-my-pi/issues/3124))
|
|
14
|
+
- Fixed Anthropic Messages pre-content TLS `bad record MAC` server transport errors surfacing before the provider retry loop exhausts its budget. ([#3134](https://github.com/can1357/oh-my-pi/issues/3134))
|
|
15
|
+
- Fixed API-key login flows replacing existing stored keys for the same provider, so providers such as NVIDIA NIM can keep multiple active keys available for session-level rotation. ([#2923](https://github.com/can1357/oh-my-pi/issues/2923))
|
|
16
|
+
- Fixed `openai-codex-responses` forwarding sampling controls (`temperature`, `top_p`, `top_k`, `min_p`, `presence_penalty`, `repetition_penalty`) into the Codex request body — the ChatGPT-subscription Codex backend rejects each of them with a 400 `{"detail":"Unsupported parameter: temperature"}`, so any caller setting non-default `StreamOptions` saw every turn fail. The provider now drops the full sampling set (matching codex-rs), and the auth-gateway's defensive strip on both `buildStreamOptions` and the pi-native path was widened from `{temperature, topP}` to the same set plus `stopSequences`/`frequencyPenalty`. ([#3117](https://github.com/can1357/oh-my-pi/issues/3117))
|
|
17
|
+
- Fixed Anthropic Messages retry classification for transient TLS/server-error failures such as `tls: bad record MAC (type=server_error)`. These pre-content transport blips are now retried inside the provider loop before the session sees an error banner.
|
|
18
|
+
|
|
5
19
|
## [16.1.4] - 2026-06-19
|
|
6
20
|
|
|
7
21
|
### Added
|
|
@@ -34,12 +34,6 @@ export interface RequestBody {
|
|
|
34
34
|
input?: InputItem[];
|
|
35
35
|
tools?: unknown;
|
|
36
36
|
tool_choice?: unknown;
|
|
37
|
-
temperature?: number;
|
|
38
|
-
top_p?: number;
|
|
39
|
-
top_k?: number;
|
|
40
|
-
min_p?: number;
|
|
41
|
-
presence_penalty?: number;
|
|
42
|
-
repetition_penalty?: number;
|
|
43
37
|
reasoning?: Partial<ReasoningConfig>;
|
|
44
38
|
text?: {
|
|
45
39
|
verbosity?: "low" | "medium" | "high";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Context, Model, ProviderSessionState, ServiceTier, StreamFunction, StreamOptions, Tool, ToolChoice } from "../types";
|
|
2
|
-
import { type CodexReasoningContext } from "./openai-codex/request-transformer";
|
|
2
|
+
import { type CodexReasoningContext, type RequestBody } from "./openai-codex/request-transformer";
|
|
3
3
|
import type { ResponseInput } from "./openai-responses-wire";
|
|
4
4
|
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
5
5
|
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
@@ -49,6 +49,8 @@ export interface OpenAICodexWebSocketDebugStats {
|
|
|
49
49
|
}
|
|
50
50
|
/** @internal Exported for tests. */
|
|
51
51
|
export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined, tools?: Tool[], model?: Model<"openai-codex-responses">): string | Record<string, unknown> | undefined;
|
|
52
|
+
/** @internal Exported for tests. */
|
|
53
|
+
export declare function buildTransformedCodexRequestBody(model: Model<"openai-codex-responses">, context: Context, options: OpenAICodexResponsesOptions | undefined, promptCacheKey?: string | undefined): Promise<RequestBody>;
|
|
52
54
|
export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
|
|
53
55
|
export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState" | "responsesLite">): Promise<void>;
|
|
54
56
|
export interface OpenAICodexTransportDetails {
|
|
@@ -142,6 +142,7 @@ declare const ALL: ({
|
|
|
142
142
|
readonly id: "nvidia";
|
|
143
143
|
readonly name: "NVIDIA";
|
|
144
144
|
readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
|
|
145
|
+
readonly appendApiKeyLogin: true;
|
|
145
146
|
} | {
|
|
146
147
|
readonly id: "ollama";
|
|
147
148
|
readonly name: "Ollama (Local OpenAI-compatible)";
|
|
@@ -40,6 +40,8 @@ export interface ProviderDefinition {
|
|
|
40
40
|
readonly showInLoginList?: boolean;
|
|
41
41
|
readonly envKeys?: KeyResolver;
|
|
42
42
|
readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
|
|
43
|
+
/** String-returning login appends API keys instead of replacing the provider's existing key. */
|
|
44
|
+
readonly appendApiKeyLogin?: boolean;
|
|
43
45
|
readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
|
|
44
46
|
readonly getApiKey?: (credentials: OAuthCredentials) => string;
|
|
45
47
|
/** Store OAuth credentials under a different provider id (e.g. `openai-codex-device` ⇒ `openai-codex`). */
|
package/dist/types/types.d.ts
CHANGED
|
@@ -93,10 +93,12 @@ export type ResolvedServiceTier = Exclude<ServiceTier, "openai-only" | "claude-o
|
|
|
93
93
|
*/
|
|
94
94
|
export declare function resolveServiceTier(serviceTier: ServiceTier | null | undefined, provider: Provider | undefined): ResolvedServiceTier | undefined;
|
|
95
95
|
/**
|
|
96
|
-
* True when the (possibly scoped) tier should be sent as
|
|
97
|
-
* `service_tier` request field for the given provider.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
96
|
+
* True when the (possibly scoped) tier should be sent on the wire as the
|
|
97
|
+
* `service_tier` request field for the given provider. OpenAI / OpenAI-Codex
|
|
98
|
+
* accept `flex`/`scale`/`priority`; Fireworks Serverless realizes only its
|
|
99
|
+
* Priority serving path (`service_tier: "priority"`) on the OpenAI-compatible
|
|
100
|
+
* chat-completions endpoint. Unsupported tiers (`"auto"`, `"default"`), other
|
|
101
|
+
* providers, and scope mismatches all return false.
|
|
100
102
|
*/
|
|
101
103
|
export declare function shouldSendServiceTier(serviceTier: ServiceTier | null | undefined, provider: Provider | undefined): boolean;
|
|
102
104
|
/**
|
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.8",
|
|
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.8",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.1.8",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.1.8",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "^4"
|
|
@@ -120,20 +120,20 @@ function deriveSessionId(modelId: string, context: Context): string {
|
|
|
120
120
|
function buildStreamOptions(parsed: ParsedFormatRequest, api: Api, signal: AbortSignal): SimpleStreamOptions {
|
|
121
121
|
const opts: SimpleStreamOptions = { signal };
|
|
122
122
|
const { options } = parsed;
|
|
123
|
-
// Codex backend rejects
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// doesn't honour
|
|
123
|
+
// Codex backend rejects every sampling control with
|
|
124
|
+
// `Unsupported parameter: …` (#3117). Strip the full set for that one
|
|
125
|
+
// provider; everything else is harmless to forward — `streamSimple` ignores
|
|
126
|
+
// what the underlying provider doesn't honour.
|
|
127
127
|
const isCodex = api === "openai-codex-responses";
|
|
128
128
|
if (options.maxOutputTokens !== undefined) opts.maxTokens = options.maxOutputTokens;
|
|
129
129
|
if (options.temperature !== undefined && !isCodex) opts.temperature = options.temperature;
|
|
130
130
|
if (options.topP !== undefined && !isCodex) opts.topP = options.topP;
|
|
131
|
-
if (options.topK !== undefined) opts.topK = options.topK;
|
|
132
|
-
if (options.minP !== undefined) opts.minP = options.minP;
|
|
133
|
-
if (options.stopSequences !== undefined) opts.stopSequences = options.stopSequences;
|
|
134
|
-
if (options.presencePenalty !== undefined) opts.presencePenalty = options.presencePenalty;
|
|
135
|
-
if (options.frequencyPenalty !== undefined) opts.frequencyPenalty = options.frequencyPenalty;
|
|
136
|
-
if (options.repetitionPenalty !== undefined) opts.repetitionPenalty = options.repetitionPenalty;
|
|
131
|
+
if (options.topK !== undefined && !isCodex) opts.topK = options.topK;
|
|
132
|
+
if (options.minP !== undefined && !isCodex) opts.minP = options.minP;
|
|
133
|
+
if (options.stopSequences !== undefined && !isCodex) opts.stopSequences = options.stopSequences;
|
|
134
|
+
if (options.presencePenalty !== undefined && !isCodex) opts.presencePenalty = options.presencePenalty;
|
|
135
|
+
if (options.frequencyPenalty !== undefined && !isCodex) opts.frequencyPenalty = options.frequencyPenalty;
|
|
136
|
+
if (options.repetitionPenalty !== undefined && !isCodex) opts.repetitionPenalty = options.repetitionPenalty;
|
|
137
137
|
if (options.metadata !== undefined) opts.metadata = options.metadata;
|
|
138
138
|
if (options.headers !== undefined) opts.headers = { ...(opts.headers ?? {}), ...options.headers };
|
|
139
139
|
if (options.toolChoice !== undefined) {
|
|
@@ -662,8 +662,8 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
662
662
|
|
|
663
663
|
// Build the SimpleStreamOptions actually handed to `streamSimple`. We
|
|
664
664
|
// trust the client's options (already allow-listed by `parseRequest`) and
|
|
665
|
-
// only inject server-controlled fields. The codex
|
|
666
|
-
//
|
|
665
|
+
// only inject server-controlled fields. The codex sampling strip mirrors
|
|
666
|
+
// `buildStreamOptions` — Codex rejects every one with a 400 (#3117).
|
|
667
667
|
const streamOpts: SimpleStreamOptions = { ...parsed.options, apiKey, signal: controller.signal };
|
|
668
668
|
streamOpts.apiKey = buildGatewayApiKeyResolver(
|
|
669
669
|
bootOpts.storage,
|
|
@@ -677,6 +677,12 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
677
677
|
if (model.api === "openai-codex-responses") {
|
|
678
678
|
delete streamOpts.temperature;
|
|
679
679
|
delete streamOpts.topP;
|
|
680
|
+
delete streamOpts.topK;
|
|
681
|
+
delete streamOpts.minP;
|
|
682
|
+
delete streamOpts.stopSequences;
|
|
683
|
+
delete streamOpts.presencePenalty;
|
|
684
|
+
delete streamOpts.frequencyPenalty;
|
|
685
|
+
delete streamOpts.repetitionPenalty;
|
|
680
686
|
}
|
|
681
687
|
// Merge gateway-captured passthrough headers under the client's own
|
|
682
688
|
// headers — the client's values win when they collide.
|
package/src/auth-storage.ts
CHANGED
|
@@ -1825,10 +1825,6 @@ export class AuthStorage {
|
|
|
1825
1825
|
onPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;
|
|
1826
1826
|
},
|
|
1827
1827
|
): Promise<void> {
|
|
1828
|
-
const saveApiKeyCredential = async (apiKey: string): Promise<void> => {
|
|
1829
|
-
const newCredential: ApiKeyCredential = { type: "api_key", key: apiKey };
|
|
1830
|
-
await this.set(provider, newCredential);
|
|
1831
|
-
};
|
|
1832
1828
|
const manualCodeInput = () => ctrl.onPrompt({ message: "Paste the authorization code (or full redirect URL):" });
|
|
1833
1829
|
// Built-in registry first, then runtime-registered extension providers.
|
|
1834
1830
|
const def = getProviderDefinition(provider) ?? getOAuthProvider(provider);
|
|
@@ -1848,7 +1844,20 @@ export class AuthStorage {
|
|
|
1848
1844
|
if (!result) {
|
|
1849
1845
|
return;
|
|
1850
1846
|
}
|
|
1851
|
-
|
|
1847
|
+
const newCredential: ApiKeyCredential = { type: "api_key", key: result };
|
|
1848
|
+
const appendApiKeyLogin = "appendApiKeyLogin" in def && def.appendApiKeyLogin === true;
|
|
1849
|
+
const stored = appendApiKeyLogin
|
|
1850
|
+
? this.#store.upsertAuthCredentialRemote
|
|
1851
|
+
? await this.#store.upsertAuthCredentialRemote(provider, newCredential)
|
|
1852
|
+
: this.#store.upsertAuthCredentialForProvider(provider, newCredential)
|
|
1853
|
+
: this.#store.replaceAuthCredentialsRemote
|
|
1854
|
+
? await this.#store.replaceAuthCredentialsRemote(provider, [newCredential])
|
|
1855
|
+
: this.#store.replaceAuthCredentialsForProvider(provider, [newCredential]);
|
|
1856
|
+
this.#setStoredCredentials(
|
|
1857
|
+
provider,
|
|
1858
|
+
stored.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
1859
|
+
);
|
|
1860
|
+
this.#resetProviderAssignments(provider);
|
|
1852
1861
|
return;
|
|
1853
1862
|
}
|
|
1854
1863
|
const newCredential: OAuthCredential = { type: "oauth", ...result };
|
|
@@ -142,6 +142,28 @@ interface WireToolConfig {
|
|
|
142
142
|
toolChoice?: WireToolChoice;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Bedrock validates that requests carrying any `toolUse`/`toolResult` history
|
|
147
|
+
* include a `toolConfig`. For no-tool ephemeral turns (`/btw`, IRC auto-replies)
|
|
148
|
+
* we have nothing real to send, so we inject this placeholder. Its presence is
|
|
149
|
+
* tracked by a per-request flag — never the wire name — so callers who happen
|
|
150
|
+
* to register a real tool literally called `__no_tools__` are not affected.
|
|
151
|
+
*/
|
|
152
|
+
const NO_TOOLS_SENTINEL_NAME = "__no_tools__";
|
|
153
|
+
|
|
154
|
+
const NO_TOOLS_SENTINEL: WireToolSpec = {
|
|
155
|
+
toolSpec: {
|
|
156
|
+
name: NO_TOOLS_SENTINEL_NAME,
|
|
157
|
+
description: "Placeholder required by Bedrock validation. Do not call; answer with text.",
|
|
158
|
+
inputSchema: { json: { type: "object", properties: {} } },
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
interface BedrockToolPlan {
|
|
163
|
+
toolConfig: WireToolConfig | undefined;
|
|
164
|
+
sentinelInjected: boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
145
167
|
interface ConverseStreamRequest {
|
|
146
168
|
messages: WireMessage[];
|
|
147
169
|
system?: SystemContent[];
|
|
@@ -222,10 +244,10 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
222
244
|
|
|
223
245
|
try {
|
|
224
246
|
const cacheRetention = resolveCacheRetention(options.cacheRetention);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
247
|
+
const convertedMessages = convertMessages(context, model, cacheRetention);
|
|
248
|
+
const toolPlan = planToolConfig(context.tools, options.toolChoice, convertedMessages);
|
|
249
|
+
const toolConfig = toolPlan.toolConfig;
|
|
250
|
+
const sentinelInjected = toolPlan.sentinelInjected;
|
|
229
251
|
let additionalModelRequestFields = buildAdditionalModelRequestFields(model, options);
|
|
230
252
|
|
|
231
253
|
// Bedrock rejects thinking + forced tool_choice ("any" or specific tool).
|
|
@@ -236,7 +258,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
236
258
|
}
|
|
237
259
|
|
|
238
260
|
const commandInput: ConverseStreamRequest = {
|
|
239
|
-
messages:
|
|
261
|
+
messages: convertedMessages,
|
|
240
262
|
system: buildSystemPrompt(context.systemPrompt, model, cacheRetention),
|
|
241
263
|
inferenceConfig: {
|
|
242
264
|
maxTokens: options.maxTokens,
|
|
@@ -369,7 +391,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
369
391
|
}
|
|
370
392
|
case "contentBlockStart": {
|
|
371
393
|
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
372
|
-
handleContentBlockStart(payload as ContentBlockStartEvent, blocks, output, stream);
|
|
394
|
+
handleContentBlockStart(payload as ContentBlockStartEvent, blocks, output, stream, sentinelInjected);
|
|
373
395
|
break;
|
|
374
396
|
}
|
|
375
397
|
case "contentBlockDelta": {
|
|
@@ -383,7 +405,10 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
383
405
|
}
|
|
384
406
|
case "messageStop": {
|
|
385
407
|
const ev = payload as MessageStopEvent;
|
|
386
|
-
|
|
408
|
+
// A sentinel-only request must never surface a tool-use stop:
|
|
409
|
+
// no real tool exists for the agent to dispatch.
|
|
410
|
+
output.stopReason =
|
|
411
|
+
sentinelInjected && ev.stopReason === "tool_use" ? "stop" : mapStopReason(ev.stopReason);
|
|
387
412
|
if (output.stopReason === "error") {
|
|
388
413
|
output.errorMessage = `Generation failed with stop reason: ${ev.stopReason ?? "unknown"}`;
|
|
389
414
|
}
|
|
@@ -462,10 +487,16 @@ function handleContentBlockStart(
|
|
|
462
487
|
blocks: Block[],
|
|
463
488
|
output: AssistantMessage,
|
|
464
489
|
stream: AssistantMessageEventStream,
|
|
490
|
+
sentinelInjected: boolean,
|
|
465
491
|
): void {
|
|
466
492
|
const index = event.contentBlockIndex;
|
|
467
493
|
const start = event.start;
|
|
468
494
|
|
|
495
|
+
// Drop the sentinel call only when we injected it ourselves. A caller that
|
|
496
|
+
// registers a real tool named `__no_tools__` would otherwise lose its
|
|
497
|
+
// legitimate tool-use events on normal turns.
|
|
498
|
+
if (sentinelInjected && start?.toolUse?.name === NO_TOOLS_SENTINEL_NAME) return;
|
|
499
|
+
|
|
469
500
|
if (start?.toolUse) {
|
|
470
501
|
const block: Block = {
|
|
471
502
|
type: "toolCall",
|
|
@@ -784,28 +815,48 @@ function convertMessages(
|
|
|
784
815
|
return result;
|
|
785
816
|
}
|
|
786
817
|
|
|
787
|
-
function
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
818
|
+
function messagesHaveToolBlocks(messages: WireMessage[]): boolean {
|
|
819
|
+
for (const message of messages) {
|
|
820
|
+
for (const block of message.content) {
|
|
821
|
+
if ("toolUse" in block || "toolResult" in block) return true;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
793
826
|
|
|
794
|
-
|
|
827
|
+
function convertToolSpec(tool: Tool): WireToolSpec {
|
|
828
|
+
return {
|
|
795
829
|
toolSpec: {
|
|
796
830
|
name: tool.name,
|
|
797
831
|
description: tool.description || "",
|
|
798
832
|
inputSchema: { json: toolWireSchema(tool) },
|
|
799
833
|
},
|
|
800
|
-
}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function planToolConfig(
|
|
838
|
+
tools: Tool[] | undefined,
|
|
839
|
+
toolChoice: BedrockOptions["toolChoice"],
|
|
840
|
+
messages: WireMessage[],
|
|
841
|
+
): BedrockToolPlan {
|
|
842
|
+
const activeTools = tools ?? [];
|
|
843
|
+
const hasTools = activeTools.length > 0;
|
|
844
|
+
const historyHasToolBlocks = messagesHaveToolBlocks(messages);
|
|
801
845
|
|
|
802
|
-
// Bedrock rejects requests whose history contains toolUse/toolResult blocks without a
|
|
803
|
-
// toolConfig. With prior tool use we must keep the tool specs and merely omit the choice
|
|
804
|
-
// (there is no "none" choice on Converse); dropping toolConfig entirely would 400.
|
|
805
846
|
if (toolChoice === "none") {
|
|
806
|
-
|
|
847
|
+
if (!historyHasToolBlocks) return { toolConfig: undefined, sentinelInjected: false };
|
|
848
|
+
if (!hasTools) {
|
|
849
|
+
return {
|
|
850
|
+
toolConfig: { tools: [NO_TOOLS_SENTINEL], toolChoice: { auto: {} } },
|
|
851
|
+
sentinelInjected: true,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
return { toolConfig: { tools: activeTools.map(convertToolSpec) }, sentinelInjected: false };
|
|
807
855
|
}
|
|
808
856
|
|
|
857
|
+
if (!hasTools) return { toolConfig: undefined, sentinelInjected: false };
|
|
858
|
+
|
|
859
|
+
const bedrockTools = activeTools.map(convertToolSpec);
|
|
809
860
|
let bedrockToolChoice: WireToolChoice | undefined;
|
|
810
861
|
switch (toolChoice) {
|
|
811
862
|
case "auto":
|
|
@@ -820,7 +871,7 @@ function convertToolConfig(
|
|
|
820
871
|
}
|
|
821
872
|
}
|
|
822
873
|
|
|
823
|
-
return { tools: bedrockTools, toolChoice: bedrockToolChoice };
|
|
874
|
+
return { toolConfig: { tools: bedrockTools, toolChoice: bedrockToolChoice }, sentinelInjected: false };
|
|
824
875
|
}
|
|
825
876
|
|
|
826
877
|
function mapStopReason(reason: string | undefined): StopReason {
|
|
@@ -1485,6 +1485,10 @@ function isProviderRetryableStreamEnvelopeError(error: unknown): boolean {
|
|
|
1485
1485
|
return /stream event order|before message_start/i.test(error.message);
|
|
1486
1486
|
}
|
|
1487
1487
|
|
|
1488
|
+
function isAnthropicTransientTransportMessage(message: string): boolean {
|
|
1489
|
+
return message.includes("tls: bad record mac") || message.includes("type=server_error");
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1488
1492
|
export function isProviderRetryableError(error: unknown, provider?: string): boolean {
|
|
1489
1493
|
if (!(error instanceof Error)) return false;
|
|
1490
1494
|
if (provider === "github-copilot" && isCopilotTransientModelError(error)) return true;
|
|
@@ -1501,7 +1505,8 @@ export function isProviderRetryableError(error: unknown, provider?: string): boo
|
|
|
1501
1505
|
const msg = error.message.toLowerCase();
|
|
1502
1506
|
if (
|
|
1503
1507
|
isUnexpectedSocketCloseMessage(msg) ||
|
|
1504
|
-
|
|
1508
|
+
isAnthropicTransientTransportMessage(msg) ||
|
|
1509
|
+
/rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|server_error|bad record mac|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test(
|
|
1505
1510
|
msg,
|
|
1506
1511
|
) ||
|
|
1507
1512
|
isTransientStreamParseError(error) ||
|
|
@@ -41,12 +41,10 @@ export interface RequestBody {
|
|
|
41
41
|
input?: InputItem[];
|
|
42
42
|
tools?: unknown;
|
|
43
43
|
tool_choice?: unknown;
|
|
44
|
-
temperature
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
presence_penalty?: number;
|
|
49
|
-
repetition_penalty?: number;
|
|
44
|
+
// Sampling controls (temperature/top_p/top_k/min_p/presence_penalty/
|
|
45
|
+
// repetition_penalty/frequency_penalty/stop) are intentionally absent: the
|
|
46
|
+
// Codex backend rejects every one with a 400 `Unsupported parameter`, so
|
|
47
|
+
// the transformer never sets them (#3117).
|
|
50
48
|
reasoning?: Partial<ReasoningConfig>;
|
|
51
49
|
text?: {
|
|
52
50
|
verbosity?: "low" | "medium" | "high";
|
|
@@ -714,7 +714,8 @@ async function buildCodexRequestContext(
|
|
|
714
714
|
};
|
|
715
715
|
}
|
|
716
716
|
|
|
717
|
-
|
|
717
|
+
/** @internal Exported for tests. */
|
|
718
|
+
export async function buildTransformedCodexRequestBody(
|
|
718
719
|
model: Model<"openai-codex-responses">,
|
|
719
720
|
context: Context,
|
|
720
721
|
options: OpenAICodexResponsesOptions | undefined,
|
|
@@ -729,25 +730,12 @@ async function buildTransformedCodexRequestBody(
|
|
|
729
730
|
|
|
730
731
|
// `maxTokens` is intentionally not forwarded: transformRequestBody strips
|
|
731
732
|
// `max_output_tokens`/`max_completion_tokens` (the Codex backend rejects
|
|
732
|
-
// caller-supplied output caps).
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
if (options?.topK !== undefined) {
|
|
740
|
-
params.top_k = options.topK;
|
|
741
|
-
}
|
|
742
|
-
if (options?.minP !== undefined) {
|
|
743
|
-
params.min_p = options.minP;
|
|
744
|
-
}
|
|
745
|
-
if (options?.presencePenalty !== undefined) {
|
|
746
|
-
params.presence_penalty = options.presencePenalty;
|
|
747
|
-
}
|
|
748
|
-
if (options?.repetitionPenalty !== undefined) {
|
|
749
|
-
params.repetition_penalty = options.repetitionPenalty;
|
|
750
|
-
}
|
|
733
|
+
// caller-supplied output caps). Sampling controls (`temperature`, `top_p`,
|
|
734
|
+
// `top_k`, `min_p`, `presence_penalty`, `repetition_penalty`,
|
|
735
|
+
// `frequency_penalty`, `stop`) are likewise refused with
|
|
736
|
+
// `{"detail":"Unsupported parameter: temperature"}` etc., so we drop
|
|
737
|
+
// everything from `StreamOptions` rather than forwarding any of them.
|
|
738
|
+
// (#3117 — codex-rs sends none of these either.)
|
|
751
739
|
applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
|
|
752
740
|
if (context.tools && context.tools.length > 0) {
|
|
753
741
|
params.tools = convertOpenAICodexResponsesTools(context.tools, model);
|
|
@@ -757,16 +745,6 @@ async function buildTransformedCodexRequestBody(
|
|
|
757
745
|
params.tool_choice = toolChoice;
|
|
758
746
|
}
|
|
759
747
|
}
|
|
760
|
-
// When a custom-tool is active, force serial tool-calling. OpenAI's
|
|
761
|
-
// `parallel_tool_calls` is request-scoped — disabling it here affects
|
|
762
|
-
// every tool in the turn, not just the custom one. That's coarser
|
|
763
|
-
// than spec §1's "supports_parallel_tool_calls = false" (which
|
|
764
|
-
// strictly targets `apply_patch`), but the platform API offers no
|
|
765
|
-
// per-tool flag.
|
|
766
|
-
const emittedTools = params.tools as CodexToolPayload[];
|
|
767
|
-
if (emittedTools.some(t => t.type === "custom")) {
|
|
768
|
-
params.parallel_tool_calls = false;
|
|
769
|
-
}
|
|
770
748
|
}
|
|
771
749
|
|
|
772
750
|
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
@@ -175,6 +175,19 @@ function normalizeMistralToolId(id: string, isMistral: boolean): string {
|
|
|
175
175
|
// #1488). The default route forwards `delta.content` (including DSML
|
|
176
176
|
// envelope leaks) which `StreamMarkupHealing` heals into a structured call
|
|
177
177
|
// client-side.
|
|
178
|
+
function resolveOpenAICompletionsRoutingEffort(
|
|
179
|
+
model: Model<"openai-completions">,
|
|
180
|
+
effort: Effort | undefined,
|
|
181
|
+
): Effort | undefined {
|
|
182
|
+
if (!effort) return undefined;
|
|
183
|
+
if (model.thinking?.efforts.includes(effort)) return effort;
|
|
184
|
+
const compatMappedEffort = model.compat.reasoningEffortMap?.[effort] as Effort | undefined;
|
|
185
|
+
if (compatMappedEffort && model.thinking?.efforts.includes(compatMappedEffort)) return compatMappedEffort;
|
|
186
|
+
const thinkingMappedEffort = model.thinking?.effortMap?.[effort] as Effort | undefined;
|
|
187
|
+
if (thinkingMappedEffort && model.thinking?.efforts.includes(thinkingMappedEffort)) return thinkingMappedEffort;
|
|
188
|
+
return effort;
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
function resolveOpenAICompletionsModelId(
|
|
179
192
|
model: Model<"openai-completions">,
|
|
180
193
|
options: OpenAICompletionsOptions | undefined,
|
|
@@ -182,8 +195,9 @@ function resolveOpenAICompletionsModelId(
|
|
|
182
195
|
// Effort-tier variants route per request effort (off → bare id, efforts →
|
|
183
196
|
// the thinking backing id); catalog variants (Copilot long-context `-1m`
|
|
184
197
|
// entries) pin via `requestModelId`; everything else serializes `model.id`.
|
|
185
|
-
const
|
|
198
|
+
const requestedEffort =
|
|
186
199
|
options?.reasoning && !options.disableReasoning && model.reasoning ? (options.reasoning as Effort) : undefined;
|
|
200
|
+
const effort = resolveOpenAICompletionsRoutingEffort(model, requestedEffort);
|
|
187
201
|
const wireId = resolveWireModelId(model, effort);
|
|
188
202
|
return applyWireModelIdTransform(wireId, model.compat.wireModelIdMode, options?.openrouterVariant);
|
|
189
203
|
}
|
|
@@ -862,15 +862,6 @@ export function buildParams(
|
|
|
862
862
|
params.tool_choice = toolChoice;
|
|
863
863
|
}
|
|
864
864
|
}
|
|
865
|
-
// The apply_patch spec §1 marks only `apply_patch` itself as
|
|
866
|
-
// `supports_parallel_tool_calls = false`. OpenAI's Responses API
|
|
867
|
-
// exposes `parallel_tool_calls` as a request-scoped flag, not a
|
|
868
|
-
// per-tool one, so when a custom grammar tool is in the list we
|
|
869
|
-
// disable parallelism for the whole turn. Slightly coarser than
|
|
870
|
-
// the spec requires — but the platform API offers no finer knob.
|
|
871
|
-
if (params.tools.some(t => (t as { type?: string }).type === "custom")) {
|
|
872
|
-
params.parallel_tool_calls = false;
|
|
873
|
-
}
|
|
874
865
|
}
|
|
875
866
|
|
|
876
867
|
const reasoningPolicy = resolveOpenAICompatPolicy(model, {
|
package/src/registry/nvidia.ts
CHANGED
|
@@ -39,6 +39,7 @@ export async function loginNvidia(options: OAuthController): Promise<string> {
|
|
|
39
39
|
baseUrl: API_BASE_URL,
|
|
40
40
|
model: VALIDATION_MODEL,
|
|
41
41
|
signal: options.signal,
|
|
42
|
+
fetch: options.fetch,
|
|
42
43
|
});
|
|
43
44
|
} catch (error) {
|
|
44
45
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -57,4 +58,5 @@ export const nvidiaProvider = {
|
|
|
57
58
|
id: "nvidia",
|
|
58
59
|
name: "NVIDIA",
|
|
59
60
|
login: (cb: OAuthLoginCallbacks) => loginNvidia(cb),
|
|
61
|
+
appendApiKeyLogin: true,
|
|
60
62
|
} as const satisfies ProviderDefinition;
|
package/src/registry/types.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface ProviderDefinition {
|
|
|
44
44
|
readonly envKeys?: KeyResolver;
|
|
45
45
|
// --- interactive login (OAuthProviderInterface-compatible) ---
|
|
46
46
|
readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
|
|
47
|
+
/** String-returning login appends API keys instead of replacing the provider's existing key. */
|
|
48
|
+
readonly appendApiKeyLogin?: boolean;
|
|
47
49
|
readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
|
|
48
50
|
readonly getApiKey?: (credentials: OAuthCredentials) => string;
|
|
49
51
|
/** Store OAuth credentials under a different provider id (e.g. `openai-codex-device` ⇒ `openai-codex`). */
|
package/src/stream.ts
CHANGED
|
@@ -673,6 +673,30 @@ function mapOpenAiToolChoice(choice?: ToolChoice): OpenAICompletionsOptions["too
|
|
|
673
673
|
return undefined;
|
|
674
674
|
}
|
|
675
675
|
|
|
676
|
+
type ReasoningEffortMapCompat = {
|
|
677
|
+
reasoningEffortMap?: Partial<Record<Effort, string>>;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
function getCompatReasoningEffortMap<TApi extends Api>(
|
|
681
|
+
model: Model<TApi>,
|
|
682
|
+
): Partial<Record<Effort, string>> | undefined {
|
|
683
|
+
const compat = model.compat;
|
|
684
|
+
if (compat === undefined || typeof compat !== "object" || !("reasoningEffortMap" in compat)) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
return (compat as ReasoningEffortMapCompat).reasoningEffortMap;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function resolveSupportedMappedReasoningEffort<TApi extends Api>(
|
|
691
|
+
model: Model<TApi>,
|
|
692
|
+
reasoning: Effort,
|
|
693
|
+
): Effort | undefined {
|
|
694
|
+
const mapped = getCompatReasoningEffortMap(model)?.[reasoning];
|
|
695
|
+
if (!mapped) return undefined;
|
|
696
|
+
const mappedEffort = mapped as Effort;
|
|
697
|
+
return model.thinking?.efforts.includes(mappedEffort) ? mappedEffort : undefined;
|
|
698
|
+
}
|
|
699
|
+
|
|
676
700
|
function resolveOpenAiReasoningEffort<TApi extends Api>(
|
|
677
701
|
model: Model<TApi>,
|
|
678
702
|
options?: SimpleStreamOptions,
|
|
@@ -687,6 +711,11 @@ function resolveOpenAiReasoningEffort<TApi extends Api>(
|
|
|
687
711
|
// defeat the gate and surface a confusing "Compaction failed: Thinking effort
|
|
688
712
|
// high is not supported by..." to the user.
|
|
689
713
|
if (!model.thinking) return undefined;
|
|
714
|
+
if (model.thinking.efforts.includes(reasoning)) return reasoning;
|
|
715
|
+
const mappedReasoning = resolveSupportedMappedReasoningEffort(model, reasoning);
|
|
716
|
+
if (mappedReasoning) return mappedReasoning;
|
|
717
|
+
if (getCompatReasoningEffortMap(model)?.[reasoning] !== undefined) return reasoning;
|
|
718
|
+
if (model.thinking.effortMap?.[reasoning] !== undefined) return reasoning;
|
|
690
719
|
return requireSupportedEffort(model, reasoning);
|
|
691
720
|
}
|
|
692
721
|
|
package/src/types.ts
CHANGED
|
@@ -141,18 +141,25 @@ export function resolveServiceTier(
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
* True when the (possibly scoped) tier should be sent as
|
|
145
|
-
* `service_tier` request field for the given provider.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
144
|
+
* True when the (possibly scoped) tier should be sent on the wire as the
|
|
145
|
+
* `service_tier` request field for the given provider. OpenAI / OpenAI-Codex
|
|
146
|
+
* accept `flex`/`scale`/`priority`; Fireworks Serverless realizes only its
|
|
147
|
+
* Priority serving path (`service_tier: "priority"`) on the OpenAI-compatible
|
|
148
|
+
* chat-completions endpoint. Unsupported tiers (`"auto"`, `"default"`), other
|
|
149
|
+
* providers, and scope mismatches all return false.
|
|
148
150
|
*/
|
|
149
151
|
export function shouldSendServiceTier(
|
|
150
152
|
serviceTier: ServiceTier | null | undefined,
|
|
151
153
|
provider: Provider | undefined,
|
|
152
154
|
): boolean {
|
|
153
|
-
if (provider !== "openai" && provider !== "openai-codex") return false;
|
|
154
155
|
const resolved = resolveServiceTier(serviceTier, provider);
|
|
155
|
-
|
|
156
|
+
if (provider === "openai" || provider === "openai-codex") {
|
|
157
|
+
return resolved === "flex" || resolved === "scale" || resolved === "priority";
|
|
158
|
+
}
|
|
159
|
+
if (provider === "fireworks") {
|
|
160
|
+
return resolved === "priority";
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
/**
|