@oh-my-pi/pi-ai 16.1.7 → 16.1.9
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 +35 -0
- package/README.md +0 -1
- 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/llama-cpp.d.ts +8 -0
- package/dist/types/registry/nvidia.d.ts +1 -0
- package/dist/types/registry/oauth/wafer.d.ts +0 -1
- package/dist/types/registry/registry.d.ts +6 -4
- package/dist/types/registry/types.d.ts +2 -0
- package/dist/types/types.d.ts +6 -4
- package/dist/types/utils/request-debug.d.ts +0 -3
- 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/providers/openai-shared.ts +10 -0
- package/src/registry/llama-cpp.ts +34 -0
- package/src/registry/nvidia.ts +2 -0
- package/src/registry/oauth/wafer.ts +4 -30
- package/src/registry/registry.ts +2 -2
- package/src/registry/types.ts +2 -0
- package/src/stream.ts +29 -1
- package/src/types.ts +13 -6
- package/src/utils/request-debug.ts +1 -34
- package/src/utils/schema/wire.ts +68 -0
- package/src/utils/validation.ts +129 -2
- package/dist/types/registry/wafer-pass.d.ts +0 -6
- package/src/registry/wafer-pass.ts +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.9] - 2026-06-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `llama.cpp` to the interactive `/login` provider list, accepting an optional API key while defaulting to local no-auth mode.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Optimized generated AI tool schemas by collapsing verbose `anyOf` unions into standard `enum` types
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed tool-call argument validation dropping nested keys that were accidentally double-encoded
|
|
18
|
+
- Fixed the `moonshot` provider being locked to the international Kimi host (`api.moonshot.ai`): OpenAI-completions requests now honor a `MOONSHOT_BASE_URL` override so users can reach the Kimi China platform (`api.moonshot.cn`), which rejects keys issued for the international endpoint. ([#2883](https://github.com/can1357/oh-my-pi/issues/2883))
|
|
19
|
+
- Fixed tool-call argument validation dropping fields whose object keys were accidentally JSON-encoded a second time (e.g. `{ "\"op\"": "done" }`), which surfaced as spurious missing-required errors. A schema-agnostic pre-validation pass now recursively unwraps such double-encoded keys — through arrays and nested objects, and again after a JSON-string container is parsed — before the unrecognized-key repair can delete them.
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
|
|
23
|
+
- Removed the `setNextRequestDebugPath`, `clearNextRequestDebugPath`, and `getNextRequestDebugPath` utility functions for request debugging, as request/response recording now relies exclusively on the `PI_REQ_DEBUG` environment variable.
|
|
24
|
+
- Removed Wafer Pass (`wafer-pass`) login support; Wafer Serverless remains available as `wafer-serverless`.
|
|
25
|
+
|
|
26
|
+
## [16.1.8] - 2026-06-20
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- 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.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- 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))
|
|
35
|
+
- 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))
|
|
36
|
+
- 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))
|
|
37
|
+
- 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))
|
|
38
|
+
- 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.
|
|
39
|
+
|
|
5
40
|
## [16.1.4] - 2026-06-19
|
|
6
41
|
|
|
7
42
|
### Added
|
package/README.md
CHANGED
|
@@ -62,7 +62,6 @@ Unified LLM API with automatic model discovery, provider configuration, token an
|
|
|
62
62
|
- **Hugging Face Inference**
|
|
63
63
|
- **xAI**
|
|
64
64
|
- **Venice** (requires `VENICE_API_KEY`)
|
|
65
|
-
- **Wafer Pass** (requires `WAFER_PASS_API_KEY`; flat-rate subscription, includes GLM-5.1 and Qwen3.5-397B-A17B)
|
|
66
65
|
- **Wafer Serverless** (requires `WAFER_SERVERLESS_API_KEY`; pay-as-you-go)
|
|
67
66
|
- **OpenRouter**
|
|
68
67
|
- **Kilo Gateway** (supports OAuth `/login kilo` or `KILO_API_KEY`)
|
|
@@ -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 {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
|
|
2
|
+
export declare function loginLlamaCpp(options: OAuthController): Promise<string>;
|
|
3
|
+
export declare const llamaCppProvider: {
|
|
4
|
+
readonly id: "llama.cpp";
|
|
5
|
+
readonly name: "llama.cpp (Local OpenAI-compatible)";
|
|
6
|
+
readonly envKeys: "LLAMA_CPP_API_KEY";
|
|
7
|
+
readonly login: (cb: OAuthLoginCallbacks) => Promise<string>;
|
|
8
|
+
};
|
|
@@ -112,6 +112,11 @@ declare const ALL: ({
|
|
|
112
112
|
readonly id: "litellm";
|
|
113
113
|
readonly name: "LiteLLM";
|
|
114
114
|
readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
|
|
115
|
+
} | {
|
|
116
|
+
readonly id: "llama.cpp";
|
|
117
|
+
readonly name: "llama.cpp (Local OpenAI-compatible)";
|
|
118
|
+
readonly envKeys: "LLAMA_CPP_API_KEY";
|
|
119
|
+
readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
|
|
115
120
|
} | {
|
|
116
121
|
readonly id: "lm-studio";
|
|
117
122
|
readonly name: "LM Studio (Local OpenAI-compatible)";
|
|
@@ -142,6 +147,7 @@ declare const ALL: ({
|
|
|
142
147
|
readonly id: "nvidia";
|
|
143
148
|
readonly name: "NVIDIA";
|
|
144
149
|
readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
|
|
150
|
+
readonly appendApiKeyLogin: true;
|
|
145
151
|
} | {
|
|
146
152
|
readonly id: "ollama";
|
|
147
153
|
readonly name: "Ollama (Local OpenAI-compatible)";
|
|
@@ -225,10 +231,6 @@ declare const ALL: ({
|
|
|
225
231
|
readonly id: "vllm";
|
|
226
232
|
readonly name: "vLLM (Local OpenAI-compatible)";
|
|
227
233
|
readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
|
|
228
|
-
} | {
|
|
229
|
-
readonly id: "wafer-pass";
|
|
230
|
-
readonly name: "Wafer Pass (flat-rate subscription)";
|
|
231
|
-
readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
|
|
232
234
|
} | {
|
|
233
235
|
readonly id: "wafer-serverless";
|
|
234
236
|
readonly name: "Wafer Serverless (pay-as-you-go)";
|
|
@@ -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
|
/**
|
|
@@ -22,9 +22,6 @@ export interface RequestDebugSession {
|
|
|
22
22
|
wrapResponse(response: Response): Promise<Response>;
|
|
23
23
|
}
|
|
24
24
|
export declare function isRequestDebugEnabled(): boolean;
|
|
25
|
-
export declare function setNextRequestDebugPath(requestPath: string): void;
|
|
26
|
-
export declare function clearNextRequestDebugPath(): void;
|
|
27
|
-
export declare function getNextRequestDebugPath(): string | undefined;
|
|
28
25
|
export declare function wrapFetchForRequestDebug(fetchImpl: FetchImpl): FetchImpl;
|
|
29
26
|
export declare function withRequestDebugFetch<T extends {
|
|
30
27
|
fetch?: FetchImpl;
|
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.9",
|
|
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.9",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.1.9",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.1.9",
|
|
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, {
|
|
@@ -155,6 +155,16 @@ export function resolveOpenAIRequestSetup(
|
|
|
155
155
|
|
|
156
156
|
let copilotPremiumRequests: number | undefined;
|
|
157
157
|
let baseUrl = model.baseUrl;
|
|
158
|
+
if (model.provider === "moonshot") {
|
|
159
|
+
// Bundled `moonshot` catalog models hardcode the international endpoint
|
|
160
|
+
// (`api.moonshot.ai`). MOONSHOT_BASE_URL lets users redirect the provider
|
|
161
|
+
// at the China platform (`api.moonshot.cn`), which only accepts China keys
|
|
162
|
+
// and rejects the international host. (#2883)
|
|
163
|
+
const moonshotBaseUrl = $env.MOONSHOT_BASE_URL?.trim();
|
|
164
|
+
if (moonshotBaseUrl) {
|
|
165
|
+
baseUrl = moonshotBaseUrl;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
158
168
|
if (model.provider === "github-copilot") {
|
|
159
169
|
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
|
160
170
|
const copilot = buildCopilotDynamicHeaders({
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
|
|
2
|
+
import type { ProviderDefinition } from "./types";
|
|
3
|
+
|
|
4
|
+
const PROVIDER_ID = "llama.cpp";
|
|
5
|
+
const AUTH_URL = "https://github.com/ggml-org/llama.cpp#quick-start";
|
|
6
|
+
const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:8080";
|
|
7
|
+
const DEFAULT_LOCAL_TOKEN = "llama-cpp-local";
|
|
8
|
+
|
|
9
|
+
export async function loginLlamaCpp(options: OAuthController): Promise<string> {
|
|
10
|
+
if (!options.onPrompt) {
|
|
11
|
+
throw new Error(`${PROVIDER_ID} login requires onPrompt callback`);
|
|
12
|
+
}
|
|
13
|
+
options.onAuth?.({
|
|
14
|
+
url: AUTH_URL,
|
|
15
|
+
instructions: `Paste your llama.cpp API key if your server requires auth. Leave empty for local no-auth mode (default base URL: ${DEFAULT_LOCAL_BASE_URL}; set LLAMA_CPP_BASE_URL to customize).`,
|
|
16
|
+
});
|
|
17
|
+
const apiKey = await options.onPrompt({
|
|
18
|
+
message: "Paste your llama.cpp API key (optional for local no-auth)",
|
|
19
|
+
placeholder: DEFAULT_LOCAL_TOKEN,
|
|
20
|
+
allowEmpty: true,
|
|
21
|
+
});
|
|
22
|
+
if (options.signal?.aborted) {
|
|
23
|
+
throw new Error("Login cancelled");
|
|
24
|
+
}
|
|
25
|
+
const trimmed = apiKey.trim();
|
|
26
|
+
return trimmed || DEFAULT_LOCAL_TOKEN;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const llamaCppProvider = {
|
|
30
|
+
id: PROVIDER_ID,
|
|
31
|
+
name: "llama.cpp (Local OpenAI-compatible)",
|
|
32
|
+
envKeys: "LLAMA_CPP_API_KEY",
|
|
33
|
+
login: (cb: OAuthLoginCallbacks) => loginLlamaCpp(cb),
|
|
34
|
+
} as const satisfies ProviderDefinition;
|
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;
|
|
@@ -1,41 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Wafer login
|
|
2
|
+
* Wafer Serverless login flow.
|
|
3
3
|
*
|
|
4
|
-
* Wafer (https://wafer.ai) exposes a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - **Wafer Pass** — flat-rate subscription. The key authorizes models whose
|
|
8
|
-
* catalog entries carry `wafer.tier = "pass_included"`.
|
|
9
|
-
* - **Wafer Serverless** — pay-as-you-go. Superset of Pass; the same `/v1/models`
|
|
10
|
-
* endpoint returns the full per-account model list.
|
|
11
|
-
*
|
|
12
|
-
* Both SKUs issue `wfr_…` keys. The key prefix alone does not distinguish
|
|
13
|
-
* tiers — the entitlement is per-account on the server side — so we expose
|
|
14
|
-
* two parallel logins / env vars (`WAFER_PASS_API_KEY`, `WAFER_SERVERLESS_API_KEY`)
|
|
15
|
-
* mirroring the firepass/fireworks split, letting users with both
|
|
16
|
-
* subscriptions switch between them without re-pasting.
|
|
17
|
-
*
|
|
18
|
-
* Validation uses the shared `/v1/models` endpoint, which works for both
|
|
19
|
-
* tiers and is cheap (no token spend).
|
|
4
|
+
* Wafer (https://wafer.ai) exposes a pay-as-you-go OpenAI-compatible SKU at
|
|
5
|
+
* `https://pass.wafer.ai/v1`. Keys use the `wfr_…` prefix and are validated
|
|
6
|
+
* against `/v1/models`, which is cheap (no token spend).
|
|
20
7
|
*/
|
|
21
8
|
import { createApiKeyLogin } from "../api-key-login";
|
|
22
9
|
|
|
23
10
|
const WAFER_AUTH_URL = "https://wafer.ai/dashboard";
|
|
24
11
|
const WAFER_MODELS_URL = "https://pass.wafer.ai/v1/models";
|
|
25
12
|
|
|
26
|
-
export const loginWaferPass = createApiKeyLogin({
|
|
27
|
-
providerLabel: "Wafer Pass",
|
|
28
|
-
authUrl: WAFER_AUTH_URL,
|
|
29
|
-
instructions: "Create or copy your Wafer Pass API key from the Wafer dashboard",
|
|
30
|
-
promptMessage: "Paste your Wafer Pass API key",
|
|
31
|
-
placeholder: "wfr_...",
|
|
32
|
-
validation: {
|
|
33
|
-
kind: "models-endpoint",
|
|
34
|
-
provider: "Wafer Pass",
|
|
35
|
-
modelsUrl: WAFER_MODELS_URL,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
13
|
export const loginWaferServerless = createApiKeyLogin({
|
|
40
14
|
providerLabel: "Wafer Serverless",
|
|
41
15
|
authUrl: WAFER_AUTH_URL,
|
package/src/registry/registry.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { kagiProvider } from "./kagi";
|
|
|
22
22
|
import { kiloProvider } from "./kilo";
|
|
23
23
|
import { kimiCodeProvider } from "./kimi-code";
|
|
24
24
|
import { litellmProvider } from "./litellm";
|
|
25
|
+
import { llamaCppProvider } from "./llama-cpp";
|
|
25
26
|
import { lmStudioProvider } from "./lm-studio";
|
|
26
27
|
import { minimaxProvider } from "./minimax";
|
|
27
28
|
import { minimaxCodeProvider } from "./minimax-code";
|
|
@@ -50,7 +51,6 @@ import { umansProvider } from "./umans";
|
|
|
50
51
|
import { veniceProvider } from "./venice";
|
|
51
52
|
import { vercelAiGatewayProvider } from "./vercel-ai-gateway";
|
|
52
53
|
import { vllmProvider } from "./vllm";
|
|
53
|
-
import { waferPassProvider } from "./wafer-pass";
|
|
54
54
|
import { waferServerlessProvider } from "./wafer-serverless";
|
|
55
55
|
import { xaiProvider } from "./xai";
|
|
56
56
|
import { xaiOauthProvider } from "./xai-oauth";
|
|
@@ -95,7 +95,6 @@ const ALL = [
|
|
|
95
95
|
xiaomiTokenPlanAmsProvider,
|
|
96
96
|
xiaomiTokenPlanCnProvider,
|
|
97
97
|
firepassProvider,
|
|
98
|
-
waferPassProvider,
|
|
99
98
|
deepseekProvider,
|
|
100
99
|
moonshotProvider,
|
|
101
100
|
cerebrasProvider,
|
|
@@ -122,6 +121,7 @@ const ALL = [
|
|
|
122
121
|
ollamaProvider,
|
|
123
122
|
ollamaCloudProvider,
|
|
124
123
|
lmStudioProvider,
|
|
124
|
+
llamaCppProvider,
|
|
125
125
|
vllmProvider,
|
|
126
126
|
openaiProvider,
|
|
127
127
|
googleProvider,
|
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
|
@@ -160,7 +160,6 @@ type KeyResolver = string | (() => string | undefined);
|
|
|
160
160
|
const LEGACY_ENV_KEYS: Record<string, KeyResolver> = {
|
|
161
161
|
// Non-provider / search-tool keys and API-name keys not modeled as registry provider defs.
|
|
162
162
|
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
|
|
163
|
-
"llama.cpp": "LLAMA_CPP_API_KEY",
|
|
164
163
|
exa: "EXA_API_KEY",
|
|
165
164
|
jina: "JINA_API_KEY",
|
|
166
165
|
brave: "BRAVE_API_KEY",
|
|
@@ -673,6 +672,30 @@ function mapOpenAiToolChoice(choice?: ToolChoice): OpenAICompletionsOptions["too
|
|
|
673
672
|
return undefined;
|
|
674
673
|
}
|
|
675
674
|
|
|
675
|
+
type ReasoningEffortMapCompat = {
|
|
676
|
+
reasoningEffortMap?: Partial<Record<Effort, string>>;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
function getCompatReasoningEffortMap<TApi extends Api>(
|
|
680
|
+
model: Model<TApi>,
|
|
681
|
+
): Partial<Record<Effort, string>> | undefined {
|
|
682
|
+
const compat = model.compat;
|
|
683
|
+
if (compat === undefined || typeof compat !== "object" || !("reasoningEffortMap" in compat)) {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
return (compat as ReasoningEffortMapCompat).reasoningEffortMap;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function resolveSupportedMappedReasoningEffort<TApi extends Api>(
|
|
690
|
+
model: Model<TApi>,
|
|
691
|
+
reasoning: Effort,
|
|
692
|
+
): Effort | undefined {
|
|
693
|
+
const mapped = getCompatReasoningEffortMap(model)?.[reasoning];
|
|
694
|
+
if (!mapped) return undefined;
|
|
695
|
+
const mappedEffort = mapped as Effort;
|
|
696
|
+
return model.thinking?.efforts.includes(mappedEffort) ? mappedEffort : undefined;
|
|
697
|
+
}
|
|
698
|
+
|
|
676
699
|
function resolveOpenAiReasoningEffort<TApi extends Api>(
|
|
677
700
|
model: Model<TApi>,
|
|
678
701
|
options?: SimpleStreamOptions,
|
|
@@ -687,6 +710,11 @@ function resolveOpenAiReasoningEffort<TApi extends Api>(
|
|
|
687
710
|
// defeat the gate and surface a confusing "Compaction failed: Thinking effort
|
|
688
711
|
// high is not supported by..." to the user.
|
|
689
712
|
if (!model.thinking) return undefined;
|
|
713
|
+
if (model.thinking.efforts.includes(reasoning)) return reasoning;
|
|
714
|
+
const mappedReasoning = resolveSupportedMappedReasoningEffort(model, reasoning);
|
|
715
|
+
if (mappedReasoning) return mappedReasoning;
|
|
716
|
+
if (getCompatReasoningEffortMap(model)?.[reasoning] !== undefined) return reasoning;
|
|
717
|
+
if (model.thinking.effortMap?.[reasoning] !== undefined) return reasoning;
|
|
690
718
|
return requireSupportedEffort(model, reasoning);
|
|
691
719
|
}
|
|
692
720
|
|
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
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as path from "node:path";
|
|
4
3
|
import type { FetchImpl } from "../types";
|
|
5
4
|
|
|
6
5
|
const REQUEST_DEBUG_ENV = "PI_REQ_DEBUG";
|
|
@@ -9,7 +8,6 @@ const textEncoder = new TextEncoder();
|
|
|
9
8
|
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
10
9
|
|
|
11
10
|
let nextSessionId = 1;
|
|
12
|
-
let nextRequestDebugPath: string | undefined;
|
|
13
11
|
|
|
14
12
|
type DebugFetch = FetchImpl & { [DEBUG_FETCH_MARKER]?: true };
|
|
15
13
|
type RequestBodyInit = NonNullable<RequestInit["body"]>;
|
|
@@ -55,25 +53,7 @@ function isRequestDebugEnvEnabled(): boolean {
|
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
export function isRequestDebugEnabled(): boolean {
|
|
58
|
-
return isRequestDebugEnvEnabled()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function setNextRequestDebugPath(requestPath: string): void {
|
|
62
|
-
nextRequestDebugPath = requestPath;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function clearNextRequestDebugPath(): void {
|
|
66
|
-
nextRequestDebugPath = undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function getNextRequestDebugPath(): string | undefined {
|
|
70
|
-
return nextRequestDebugPath;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function consumeNextRequestDebugPath(): string | undefined {
|
|
74
|
-
const requestPath = nextRequestDebugPath;
|
|
75
|
-
nextRequestDebugPath = undefined;
|
|
76
|
-
return requestPath;
|
|
56
|
+
return isRequestDebugEnvEnabled();
|
|
77
57
|
}
|
|
78
58
|
|
|
79
59
|
export function wrapFetchForRequestDebug(fetchImpl: FetchImpl): FetchImpl {
|
|
@@ -249,19 +229,6 @@ function copyResponseMetadata(target: Response, source: Response): void {
|
|
|
249
229
|
}
|
|
250
230
|
|
|
251
231
|
async function reserveRequestDebugFile(): Promise<ReservedRequestDebugFile> {
|
|
252
|
-
const explicitPath = consumeNextRequestDebugPath();
|
|
253
|
-
if (explicitPath) {
|
|
254
|
-
await fs.mkdir(path.dirname(explicitPath), { recursive: true });
|
|
255
|
-
const handle = await fs.open(explicitPath, "w");
|
|
256
|
-
return {
|
|
257
|
-
id: nextSessionId++,
|
|
258
|
-
requestPath: explicitPath,
|
|
259
|
-
responsePath: `${explicitPath}.res.log`,
|
|
260
|
-
handle,
|
|
261
|
-
overwrite: true,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
232
|
for (;;) {
|
|
266
233
|
const id = nextSessionId++;
|
|
267
234
|
const requestPath = `rr-session-${id}.json`;
|
package/src/utils/schema/wire.ts
CHANGED
|
@@ -388,6 +388,73 @@ function inferBareEnumScalarType(obj: Record<string, unknown>): void {
|
|
|
388
388
|
if (inferred !== undefined) obj.type = inferred;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* ArkType serializes a *described* literal union — `type.enumerated(...).describe(d)`
|
|
393
|
+
* or a `"a" | "b"` union carrying a description — as an `anyOf` of
|
|
394
|
+
* `{ const, description }` branches that repeat the description on every branch
|
|
395
|
+
* *and* the union root. The meta is distributed across the union's constituents
|
|
396
|
+
* at the type level (each `unit` node inherits it), so the duplication is baked
|
|
397
|
+
* in before serialization rather than added by this pipeline.
|
|
398
|
+
*
|
|
399
|
+
* Collapse such a homogeneous all-`const` union into one typed
|
|
400
|
+
* `{ type, enum, description }` node: a shorter wire and a single description in
|
|
401
|
+
* the place providers expect it. The collapse is conservative — applied only
|
|
402
|
+
* when it is lossless:
|
|
403
|
+
* - every branch is a bare `{ const }` (optionally `{ const, description }`),
|
|
404
|
+
* - all branch values share one scalar JSON type (so `enum` gets a `type`,
|
|
405
|
+
* which Gemini/Vertex require),
|
|
406
|
+
* - branch descriptions are either all absent or all identical,
|
|
407
|
+
* so a union whose branches carry *distinct* per-variant descriptions is left
|
|
408
|
+
* untouched (a flat `enum` has nowhere to keep them). The union root's own
|
|
409
|
+
* description wins when present; otherwise the shared branch description is kept.
|
|
410
|
+
*/
|
|
411
|
+
function collapseConstUnionAnyOf(obj: Record<string, unknown>): void {
|
|
412
|
+
// `hasSchemaDefiningSibling` already rejects a sibling `enum`/`const`/etc.; it
|
|
413
|
+
// does not list `type`, so guard it here — collapsing would overwrite a
|
|
414
|
+
// wrapper `type` constraint paired with the `anyOf`.
|
|
415
|
+
if (hasSchemaDefiningSibling(obj) || "type" in obj) return;
|
|
416
|
+
const variants = obj.anyOf;
|
|
417
|
+
if (!Array.isArray(variants) || variants.length < 2) return;
|
|
418
|
+
|
|
419
|
+
const values: unknown[] = [];
|
|
420
|
+
let branchDescription: string | undefined;
|
|
421
|
+
let describedCount = 0;
|
|
422
|
+
for (const variant of variants) {
|
|
423
|
+
if (!isSchemaRecord(variant) || !Object.hasOwn(variant, "const")) return;
|
|
424
|
+
for (const key in variant) {
|
|
425
|
+
if (key !== "const" && key !== "description") return; // extra constraints — not a bare const
|
|
426
|
+
}
|
|
427
|
+
const desc = variant.description;
|
|
428
|
+
if (typeof desc === "string") {
|
|
429
|
+
if (describedCount === 0) branchDescription = desc;
|
|
430
|
+
else if (desc !== branchDescription) return; // distinct per-variant descriptions — preserve them
|
|
431
|
+
describedCount++;
|
|
432
|
+
}
|
|
433
|
+
values.push(variant.const);
|
|
434
|
+
}
|
|
435
|
+
if (describedCount !== 0 && describedCount !== variants.length) return; // mixed described/undescribed
|
|
436
|
+
// A shared branch description that disagrees with the union root's own
|
|
437
|
+
// description would be silently dropped by the collapse — keep the anyOf so
|
|
438
|
+
// neither annotation is lost. (Equal descriptions, the ArkType case, collapse.)
|
|
439
|
+
if (
|
|
440
|
+
describedCount === variants.length &&
|
|
441
|
+
typeof obj.description === "string" &&
|
|
442
|
+
obj.description !== branchDescription
|
|
443
|
+
) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const scalarType = homogeneousEnumScalarType(values);
|
|
448
|
+
if (scalarType === undefined) return; // mixed / non-scalar (incl. null) — leave as anyOf
|
|
449
|
+
|
|
450
|
+
delete obj.anyOf;
|
|
451
|
+
obj.type = scalarType;
|
|
452
|
+
obj.enum = values;
|
|
453
|
+
if (typeof obj.description !== "string" && branchDescription !== undefined) {
|
|
454
|
+
obj.description = branchDescription;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
391
458
|
function walk(node: unknown, zodCleanup: boolean): void {
|
|
392
459
|
if (Array.isArray(node)) {
|
|
393
460
|
for (const child of node) walk(child, zodCleanup);
|
|
@@ -397,6 +464,7 @@ function walk(node: unknown, zodCleanup: boolean): void {
|
|
|
397
464
|
const obj = node as Record<string, unknown>;
|
|
398
465
|
rewriteNullableScalarAnyOf(obj);
|
|
399
466
|
inferBareEnumScalarType(obj);
|
|
467
|
+
collapseConstUnionAnyOf(obj);
|
|
400
468
|
|
|
401
469
|
if (zodCleanup) {
|
|
402
470
|
// Drop noise injected for `z.number().int()`.
|
package/src/utils/validation.ts
CHANGED
|
@@ -834,6 +834,105 @@ function normalizeOptionalNullsForSchema(
|
|
|
834
834
|
return { value: changed ? nextValue : value, changed };
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
+
// ============================================================================
|
|
838
|
+
// Double-encoded object-key normalization (LLM quirk).
|
|
839
|
+
// ============================================================================
|
|
840
|
+
//
|
|
841
|
+
// LLMs occasionally serialize an object key one time too many, so the property
|
|
842
|
+
// NAME arrives as the JSON encoding of the real name — literal quote characters
|
|
843
|
+
// and all (e.g. `{ "\"op\"": "done" }` decodes to the JS key `"op"`). The
|
|
844
|
+
// schema never matches such a key, so it reads as an unrecognized extra and is
|
|
845
|
+
// dropped by the unrecognized-key repair, later surfacing as a spurious
|
|
846
|
+
// missing-required error. We walk the whole value (arrays + nested objects)
|
|
847
|
+
// and rename any key that is itself the JSON encoding of a plain string back to
|
|
848
|
+
// that string.
|
|
849
|
+
// ============================================================================
|
|
850
|
+
|
|
851
|
+
/** Max layers of accidental JSON-encoding to peel off a single object key. */
|
|
852
|
+
const MAX_KEY_DECODE_DEPTH = 3;
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* If `key` is the JSON encoding of a plain string (quote-wrapped and
|
|
856
|
+
* `JSON.parse`s to a string), return the decoded string; otherwise null. Peels
|
|
857
|
+
* up to {@link MAX_KEY_DECODE_DEPTH} nested encodings so multiply-encoded keys
|
|
858
|
+
* collapse in one pass. Conservative: any key that is not a quote-wrapped JSON
|
|
859
|
+
* string literal is left untouched.
|
|
860
|
+
*/
|
|
861
|
+
function decodeDoubleEncodedKey(key: string): string | null {
|
|
862
|
+
let current = key;
|
|
863
|
+
let decoded: string | null = null;
|
|
864
|
+
for (let depth = 0; depth < MAX_KEY_DECODE_DEPTH; depth += 1) {
|
|
865
|
+
if (current.length < 2 || current[0] !== '"' || current[current.length - 1] !== '"') break;
|
|
866
|
+
let parsed: unknown;
|
|
867
|
+
try {
|
|
868
|
+
parsed = JSON.parse(current);
|
|
869
|
+
} catch {
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
if (typeof parsed !== "string") break;
|
|
873
|
+
current = parsed;
|
|
874
|
+
decoded = current;
|
|
875
|
+
}
|
|
876
|
+
return decoded;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Recursively unwrap object keys that were accidentally JSON-encoded an extra
|
|
881
|
+
* time. Schema-agnostic by design: such keys are dropped before any schema pass
|
|
882
|
+
* can map them, so this runs first. A key is only renamed when the decoded name
|
|
883
|
+
* differs and does not already exist on the same object — renaming would
|
|
884
|
+
* otherwise clobber a sibling and silently lose data.
|
|
885
|
+
*/
|
|
886
|
+
function normalizeDoubleEncodedKeys(value: unknown): { value: unknown; changed: boolean } {
|
|
887
|
+
if (Array.isArray(value)) {
|
|
888
|
+
let changed = false;
|
|
889
|
+
let next = value;
|
|
890
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
891
|
+
const normalized = normalizeDoubleEncodedKeys(value[i]);
|
|
892
|
+
if (!normalized.changed) continue;
|
|
893
|
+
if (!changed) {
|
|
894
|
+
next = [...value];
|
|
895
|
+
changed = true;
|
|
896
|
+
}
|
|
897
|
+
next[i] = normalized.value;
|
|
898
|
+
}
|
|
899
|
+
return { value: changed ? next : value, changed };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (value === null || typeof value !== "object") return { value, changed: false };
|
|
903
|
+
|
|
904
|
+
const source = value as Record<string, unknown>;
|
|
905
|
+
let changed = false;
|
|
906
|
+
const out: Record<string, unknown> = {};
|
|
907
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
908
|
+
const normalizedChild = normalizeDoubleEncodedKeys(entry);
|
|
909
|
+
const nextChild = normalizedChild.changed ? normalizedChild.value : entry;
|
|
910
|
+
|
|
911
|
+
const decodedKey = decodeDoubleEncodedKey(key);
|
|
912
|
+
// `Object.hasOwn` (not `in`) so a decoded `constructor`/`toString` is not
|
|
913
|
+
// mistaken for a collision via the prototype chain.
|
|
914
|
+
const targetKey =
|
|
915
|
+
decodedKey !== null &&
|
|
916
|
+
decodedKey !== key &&
|
|
917
|
+
!Object.hasOwn(source, decodedKey) &&
|
|
918
|
+
!Object.hasOwn(out, decodedKey)
|
|
919
|
+
? decodedKey
|
|
920
|
+
: key;
|
|
921
|
+
|
|
922
|
+
if (targetKey !== key || normalizedChild.changed) changed = true;
|
|
923
|
+
// `defineProperty` so a decoded `__proto__` key becomes an own property
|
|
924
|
+
// instead of mutating the result object's prototype.
|
|
925
|
+
Object.defineProperty(out, targetKey, {
|
|
926
|
+
value: nextChild,
|
|
927
|
+
writable: true,
|
|
928
|
+
enumerable: true,
|
|
929
|
+
configurable: true,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return { value: changed ? out : value, changed };
|
|
934
|
+
}
|
|
935
|
+
|
|
837
936
|
// ============================================================================
|
|
838
937
|
// String-encoded array coercion for union(string, array) schemas.
|
|
839
938
|
// ============================================================================
|
|
@@ -922,8 +1021,15 @@ function normalizeStringEncodedArrayUnions(schema: unknown, value: unknown): { v
|
|
|
922
1021
|
if (!trimmed.startsWith("[")) return { value, changed: false };
|
|
923
1022
|
try {
|
|
924
1023
|
const parsed = JSON.parse(trimmed) as unknown;
|
|
925
|
-
if (Array.isArray(parsed)
|
|
926
|
-
|
|
1024
|
+
if (Array.isArray(parsed)) {
|
|
1025
|
+
// Unwrap any double-encoded object keys inside the parsed array
|
|
1026
|
+
// before the branch-match check; otherwise an `array<object>`
|
|
1027
|
+
// branch fails to validate and the value silently stays on the
|
|
1028
|
+
// string branch.
|
|
1029
|
+
const candidate = normalizeDoubleEncodedKeys(parsed).value as unknown[];
|
|
1030
|
+
if (parsedArrayMatchesArrayBranch(schemaObject, candidate)) {
|
|
1031
|
+
return { value: candidate, changed: true };
|
|
1032
|
+
}
|
|
927
1033
|
}
|
|
928
1034
|
} catch {
|
|
929
1035
|
// Not valid JSON — leave the string alone for the validator to handle.
|
|
@@ -1312,6 +1418,18 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1312
1418
|
// placeholders for "no value" even when validation would otherwise pass.
|
|
1313
1419
|
let normalizedArgs: unknown = originalArgs;
|
|
1314
1420
|
let changed = false;
|
|
1421
|
+
|
|
1422
|
+
// Unwrap accidentally double-JSON-encoded object keys before any schema
|
|
1423
|
+
// pass. LLMs sometimes emit `{ "\"op\"": "done" }`, so the property name
|
|
1424
|
+
// arrives quote-wrapped; left alone it reads as an unrecognized key, gets
|
|
1425
|
+
// dropped by the coercion repair, and re-surfaces as a missing-required
|
|
1426
|
+
// error. Running first means every later pass sees the corrected names.
|
|
1427
|
+
const keyNormalization = normalizeDoubleEncodedKeys(normalizedArgs);
|
|
1428
|
+
if (keyNormalization.changed) {
|
|
1429
|
+
normalizedArgs = keyNormalization.value;
|
|
1430
|
+
changed = true;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1315
1433
|
const initialNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
|
|
1316
1434
|
if (initialNormalization.changed) {
|
|
1317
1435
|
normalizedArgs = initialNormalization.value;
|
|
@@ -1338,6 +1456,15 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1338
1456
|
normalizedArgs = coercion.value;
|
|
1339
1457
|
changed = true;
|
|
1340
1458
|
|
|
1459
|
+
// `coerceArgsFromIssues` may have just parsed a JSON-string container at
|
|
1460
|
+
// the root or a nested field, exposing double-encoded keys the initial
|
|
1461
|
+
// pass could not reach. Re-unwrap before the unrecognized-key repair on
|
|
1462
|
+
// the next validation pass would delete them.
|
|
1463
|
+
const keyNormalizationPass = normalizeDoubleEncodedKeys(normalizedArgs);
|
|
1464
|
+
if (keyNormalizationPass.changed) {
|
|
1465
|
+
normalizedArgs = keyNormalizationPass.value;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1341
1468
|
const nullNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
|
|
1342
1469
|
if (nullNormalization.changed) {
|
|
1343
1470
|
normalizedArgs = nullNormalization.value;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
2
|
-
import type { ProviderDefinition } from "./types";
|
|
3
|
-
|
|
4
|
-
export const waferPassProvider = {
|
|
5
|
-
id: "wafer-pass",
|
|
6
|
-
name: "Wafer Pass (flat-rate subscription)",
|
|
7
|
-
login: async (cb: OAuthLoginCallbacks) => {
|
|
8
|
-
// Lazy import: keep heavy OAuth flow modules out of the eager registry graph.
|
|
9
|
-
const { loginWaferPass } = await import("./oauth/wafer");
|
|
10
|
-
return loginWaferPass(cb);
|
|
11
|
-
},
|
|
12
|
-
} as const satisfies ProviderDefinition;
|