@oh-my-pi/pi-ai 17.3.8 → 17.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor/interaction-query.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +1 -1
- package/dist/types/providers/cursor.d.ts +3 -3
- package/dist/types/providers/openai-reasoning-fallback.d.ts +10 -2
- package/dist/types/providers/openai-shared.d.ts +14 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/schema/json-schema-validator.d.ts +5 -5
- package/package.json +5 -7
- package/src/auth-gateway/server.ts +29 -3
- package/src/providers/cursor/exec-modern.ts +2 -2
- package/src/providers/cursor/interaction-query.ts +2 -2
- package/src/providers/cursor-pi-args.ts +1 -1
- package/src/providers/cursor.ts +18 -17
- package/src/providers/devin.ts +10 -16
- package/src/providers/openai-completions.ts +5 -0
- package/src/providers/openai-reasoning-fallback.ts +86 -4
- package/src/providers/openai-responses.ts +5 -0
- package/src/providers/openai-shared.ts +23 -0
- package/src/stream.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils/schema/json-schema-validator.ts +54 -17
- package/src/utils/validation.ts +24 -5
- package/src/providers/cursor/proto/buf.gen.yaml +0 -6
- package/src/providers/cursor/proto/buf.yaml +0 -17
- package/src/providers/devin/proto/buf.gen.yaml +0 -33
- package/src/providers/devin/proto/buf.yaml +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.4.0] - 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added model metadata fields (`context_length`, `max_output_tokens`, `input_modalities`, etc.) to auth gateway model listing responses
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed tool-argument repair applying lossy transformations (such as stringifying objects or stripping unrecognized keys) when validating union schemas (`anyOf`/`oneOf`), preventing corrupted tool call and subagent payloads
|
|
14
|
+
- Fixed 400 errors when communicating with local OpenAI-compatible inference servers that reject `chat_template_kwargs.reasoning_effort` by improving reasoning effort parameter fallback and compatibility handling
|
|
15
|
+
- Fixed DeepSeek-family models on hosts like Fireworks losing reasoning whenever tools were offered: a redundant `tool_choice: "auto"` is now omitted so the provider keeps thinking enabled; forced and `"none"` selectors still take priority ([#1207](https://github.com/can1357/oh-my-pi/issues/1207))
|
|
16
|
+
|
|
5
17
|
## [17.3.8] - 2026-08-19
|
|
6
18
|
|
|
7
19
|
### Changed
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `ExecClientMessage` carrying a result with an unset oneof is a fake success
|
|
10
10
|
* the server reads as "the tool ran and produced nothing".
|
|
11
11
|
*/
|
|
12
|
-
import { type ExecuteHookRequest, type ExecuteHookResult, type McpStateExecResult, type McpToolDefinition, type PiBashExecResult, type PiEditExecResult, type PiFindExecResult, type PiGrepExecResult, type PiLsExecResult, type PiReadExecResult, type PiTruncation, type PiWriteExecResult } from "@oh-my-pi/pi-catalog/discovery/cursor-
|
|
12
|
+
import { type ExecuteHookRequest, type ExecuteHookResult, type McpStateExecResult, type McpToolDefinition, type PiBashExecResult, type PiEditExecResult, type PiFindExecResult, type PiGrepExecResult, type PiLsExecResult, type PiReadExecResult, type PiTruncation, type PiWriteExecResult } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
13
13
|
import type { ToolResultMessage } from "../../types.js";
|
|
14
14
|
/**
|
|
15
15
|
* The pure arg translation lives in `../cursor-pi-args` so the legacy pi shim
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type http2 from "node:http2";
|
|
2
|
-
import { type InteractionQuery } from "@oh-my-pi/pi-catalog/discovery/cursor-
|
|
2
|
+
import { type InteractionQuery } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
3
3
|
/**
|
|
4
4
|
* Answer a Cursor `interaction_query` so the Run RPC can continue.
|
|
5
5
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Kept apart from `cursor/exec-modern.ts` on purpose: these are pure
|
|
11
11
|
* string/path functions with no protobuf coupling, while that module pulls in
|
|
12
|
-
*
|
|
12
|
+
* the generated cursor protobuf graph. The legacy shim is
|
|
13
13
|
* compiled into the bundled virtual module registry, so importing it from a
|
|
14
14
|
* nested path would drag the whole exec implementation in with it — and
|
|
15
15
|
* `./providers/*` is a single-segment wildcard export that cannot serve a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http2 from "node:http2";
|
|
2
|
-
import {
|
|
3
|
-
import type
|
|
4
|
-
import { type
|
|
2
|
+
import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
3
|
+
import { type AgentServerMessage, type ConversationStateStructure } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
4
|
+
import { type JsonValue } from "@oh-my-pi/pi-catalog/discovery/protobuf";
|
|
5
5
|
import type { AssistantMessage, Context, CursorExecHandlerResult, CursorExecHandlers, CursorExecPairing, CursorTodoSyncHandler, CursorToolResultHandler, Message, Model, StreamFunction, StreamOptions, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage } from "../types.js";
|
|
6
6
|
import { kCursorExecResolved, kStreamingBlockIndex, kStreamingBlockKind, kStreamingEnvelopeId, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols.js";
|
|
7
7
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { CapturedHttpErrorResponse } from "../utils/http-inspector.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Fallback marker: the server rejected the `chat_template_kwargs.reasoning_effort`
|
|
4
|
+
* spelling itself (strict kwargs whitelists — Ninfer-style servers), not the
|
|
5
|
+
* effort value. Apply strips the kwarg and hoists the value onto the top-level
|
|
6
|
+
* `reasoning_effort` field when that spelling is absent.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare const STRIP_TEMPLATE_KWARG_REASONING_EFFORT: unique symbol;
|
|
10
|
+
/** @internal */
|
|
11
|
+
export type OpenAIReasoningEffortFallback = string | null | typeof STRIP_TEMPLATE_KWARG_REASONING_EFFORT;
|
|
4
12
|
/** @internal */
|
|
5
13
|
export interface OpenAIReasoningEffortFallbackState {
|
|
6
14
|
reasoningEffortFallbacks: Map<string, OpenAIReasoningEffortFallback>;
|
|
@@ -315,6 +315,20 @@ export interface OpenAICompatPolicy {
|
|
|
315
315
|
export declare function mapOpenAIReasoningEffort(model: Pick<Model, "thinking">, compat: {
|
|
316
316
|
reasoningEffortMap?: Partial<Record<Effort, string>>;
|
|
317
317
|
} | undefined, effort: string): string;
|
|
318
|
+
/**
|
|
319
|
+
* Whether a redundant `tool_choice: "auto"` should be dropped to keep
|
|
320
|
+
* reasoning alive. Hosts with `disableReasoningOnToolChoice` (DeepSeek family
|
|
321
|
+
* on e.g. Fireworks) silently turn reasoning off whenever any `tool_choice`
|
|
322
|
+
* is present. "auto" is already the provider default, so omitting it is
|
|
323
|
+
* wire-neutral for tool selection; forced and "none" choices are semantic and
|
|
324
|
+
* still win over reasoning (#1207).
|
|
325
|
+
*/
|
|
326
|
+
export declare function shouldDropAutoToolChoiceForReasoning(model: Pick<Model, "reasoning">, compat: {
|
|
327
|
+
disableReasoningOnToolChoice: boolean;
|
|
328
|
+
}, toolChoice: unknown, options: {
|
|
329
|
+
reasoning?: string;
|
|
330
|
+
disableReasoning?: boolean;
|
|
331
|
+
} | undefined): boolean;
|
|
318
332
|
export declare function resolveOpenAICompatPolicy<TApi extends Api>(model: Model<TApi>, options: ResolveOpenAICompatPolicyOptions): OpenAICompatPolicy;
|
|
319
333
|
export declare function applyChatCompletionsCompatPolicy(params: OpenAICompletionsParams, policy: OpenAICompatPolicy): void;
|
|
320
334
|
export declare function applyChatCompletionsReasoningParams(params: OpenAICompletionsParams, model: Model<"openai-completions">, compat: ResolvedOpenAICompat, options: (ChatCompletionsReasoningOptions & {
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "@oh-my-pi/pi-catalog/effort";
|
|
2
2
|
export * from "@oh-my-pi/pi-catalog/types";
|
|
3
3
|
import type { Type } from "@oh-my-pi/omptype";
|
|
4
|
-
import type { DeleteArgs, DeleteResult, DiagnosticsArgs, DiagnosticsResult, GrepArgs, GrepResult, LsArgs, LsResult, McpResult, PiBashExecArgs, PiBashExecResult, PiEditExecArgs, PiEditExecResult, PiFindExecArgs, PiFindExecResult, PiGrepExecArgs, PiGrepExecResult, PiLsExecArgs, PiLsExecResult, PiReadExecArgs, PiReadExecResult, PiWriteExecArgs, PiWriteExecResult, ReadArgs, ReadResult, ShellArgs, ShellResult, WriteArgs, WriteResult } from "@oh-my-pi/pi-catalog/discovery/cursor-
|
|
4
|
+
import type { DeleteArgs, DeleteResult, DiagnosticsArgs, DiagnosticsResult, GrepArgs, GrepResult, LsArgs, LsResult, McpResult, PiBashExecArgs, PiBashExecResult, PiEditExecArgs, PiEditExecResult, PiFindExecArgs, PiFindExecResult, PiGrepExecArgs, PiGrepExecResult, PiLsExecArgs, PiLsExecResult, PiReadExecArgs, PiReadExecResult, PiWriteExecArgs, PiWriteExecResult, ReadArgs, ReadResult, ShellArgs, ShellResult, WriteArgs, WriteResult } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
5
5
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
6
6
|
import type { Api, FetchImpl, Model, Provider, ThinkingBudgets, Usage } from "@oh-my-pi/pi-catalog/types";
|
|
7
7
|
import type { ApiKey } from "./auth-retry.js";
|
|
@@ -4,11 +4,11 @@ export interface JsonSchemaValidationIssue {
|
|
|
4
4
|
expectedTypes?: string[];
|
|
5
5
|
keyword?: string;
|
|
6
6
|
/**
|
|
7
|
-
* Marks issues
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Marks issues surfaced from a failed `anyOf` / `oneOf` branch (at any
|
|
8
|
+
* depth). Such a diagnosis is one candidate branch's guess, not
|
|
9
|
+
* authoritative: the tool-argument coercion layer keeps lossy repairs
|
|
10
|
+
* (container stringification, unrecognized-key deletion, singleton-array
|
|
11
|
+
* wrapping) off for these while still applying lossless ones.
|
|
12
12
|
*/
|
|
13
13
|
fromUnionBranch?: boolean;
|
|
14
14
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.
|
|
4
|
+
"version": "17.4.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,14 +37,12 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@
|
|
41
|
-
"@oh-my-pi/
|
|
42
|
-
"@oh-my-pi/pi-
|
|
43
|
-
"@oh-my-pi/pi-
|
|
44
|
-
"@oh-my-pi/pi-wire": "17.3.8"
|
|
40
|
+
"@oh-my-pi/omptype": "17.4.0",
|
|
41
|
+
"@oh-my-pi/pi-catalog": "17.4.0",
|
|
42
|
+
"@oh-my-pi/pi-utils": "17.4.0",
|
|
43
|
+
"@oh-my-pi/pi-wire": "17.4.0"
|
|
45
44
|
},
|
|
46
45
|
"devDependencies": {
|
|
47
|
-
"@bufbuild/protoc-gen-es": "^2.12.1",
|
|
48
46
|
"@types/bun": "^1.3.14"
|
|
49
47
|
},
|
|
50
48
|
"engines": {
|
|
@@ -730,19 +730,45 @@ async function handleCredentialsCheck(storage: AuthStorage, signal: AbortSignal)
|
|
|
730
730
|
return json(200, { generatedAt: Date.now(), credentials });
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
+
/**
|
|
734
|
+
* Row shape for `GET /v1/models`. Beyond the OpenAI-standard `id`/`object`/
|
|
735
|
+
* `owned_by`, rows advertise the catalog metadata OpenAI-compatible clients
|
|
736
|
+
* (omp's own proxy discovery, Zed's openai_compatible provider, ...) read to
|
|
737
|
+
* size and capability-gate discovered models: `context_length`,
|
|
738
|
+
* `max_output_tokens`, `input_modalities`, and `supports_tools` (only emitted
|
|
739
|
+
* when the catalog explicitly reports `false`; absent means usable).
|
|
740
|
+
*/
|
|
741
|
+
interface ModelListRow {
|
|
742
|
+
id: string;
|
|
743
|
+
object: "model";
|
|
744
|
+
owned_by: string;
|
|
745
|
+
api: Api;
|
|
746
|
+
display_name: string;
|
|
747
|
+
context_length?: number;
|
|
748
|
+
max_output_tokens?: number;
|
|
749
|
+
input_modalities: ("text" | "image")[];
|
|
750
|
+
supports_tools?: boolean;
|
|
751
|
+
}
|
|
752
|
+
|
|
733
753
|
function handleModelsList(opts: AuthGatewayBootOptions): Response {
|
|
734
754
|
const seen = new Set<string>();
|
|
735
|
-
const data:
|
|
755
|
+
const data: ModelListRow[] = [];
|
|
736
756
|
for (const model of opts.listModels?.() ?? []) {
|
|
737
757
|
const id = `${model.provider}/${model.id}`;
|
|
738
758
|
if (seen.has(id)) continue;
|
|
739
759
|
seen.add(id);
|
|
740
|
-
|
|
760
|
+
const row: ModelListRow = {
|
|
741
761
|
id,
|
|
742
762
|
object: "model",
|
|
743
763
|
owned_by: model.provider,
|
|
744
764
|
api: model.api,
|
|
745
|
-
|
|
765
|
+
display_name: model.name,
|
|
766
|
+
input_modalities: model.input,
|
|
767
|
+
};
|
|
768
|
+
if (model.contextWindow != null) row.context_length = model.contextWindow;
|
|
769
|
+
if (model.maxTokens != null) row.max_output_tokens = model.maxTokens;
|
|
770
|
+
if (model.supportsTools === false) row.supports_tools = false;
|
|
771
|
+
data.push(row);
|
|
746
772
|
}
|
|
747
773
|
return json(200, { object: "list", data });
|
|
748
774
|
}
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* the server reads as "the tool ran and produced nothing".
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { create } from "@bufbuild/protobuf";
|
|
14
13
|
import {
|
|
15
14
|
AfterAgentResponseRequestResponseSchema,
|
|
16
15
|
AfterAgentThoughtRequestResponseSchema,
|
|
@@ -64,7 +63,8 @@ import {
|
|
|
64
63
|
StopRequestResponseSchema,
|
|
65
64
|
SubagentStartRequestResponseSchema,
|
|
66
65
|
SubagentStopRequestResponseSchema,
|
|
67
|
-
} from "@oh-my-pi/pi-catalog/discovery/cursor-
|
|
66
|
+
} from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
67
|
+
import { create } from "@oh-my-pi/pi-catalog/discovery/protobuf";
|
|
68
68
|
import type { ToolResultMessage } from "../../types";
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type http2 from "node:http2";
|
|
2
|
-
import { create, toBinary } from "@bufbuild/protobuf";
|
|
3
2
|
import {
|
|
4
3
|
AgentClientMessageSchema,
|
|
5
4
|
AskQuestionInteractionResponseSchema,
|
|
@@ -21,7 +20,8 @@ import {
|
|
|
21
20
|
WebFetchRequestResponseSchema,
|
|
22
21
|
WebSearchRequestResponse_ApprovedSchema,
|
|
23
22
|
WebSearchRequestResponseSchema,
|
|
24
|
-
} from "@oh-my-pi/pi-catalog/discovery/cursor-
|
|
23
|
+
} from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
24
|
+
import { create, toBinary } from "@oh-my-pi/pi-catalog/discovery/protobuf";
|
|
25
25
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
26
26
|
|
|
27
27
|
const NOT_IMPLEMENTED_SUFFIX = "not implemented by this client";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Kept apart from `cursor/exec-modern.ts` on purpose: these are pure
|
|
11
11
|
* string/path functions with no protobuf coupling, while that module pulls in
|
|
12
|
-
*
|
|
12
|
+
* the generated cursor protobuf graph. The legacy shim is
|
|
13
13
|
* compiled into the bundled virtual module registry, so importing it from a
|
|
14
14
|
* nested path would drag the whole exec implementation in with it — and
|
|
15
15
|
* `./providers/*` is a single-segment wildcard export that cannot serve a
|
package/src/providers/cursor.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import http2 from "node:http2";
|
|
4
|
-
import
|
|
5
|
-
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
6
|
-
import type { ConversationStep, McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
4
|
+
import type { ConversationStep, McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
7
5
|
import {
|
|
8
6
|
AgentClientMessageSchema,
|
|
9
7
|
AgentConversationTurnStructureSchema,
|
|
@@ -139,11 +137,21 @@ import {
|
|
|
139
137
|
WriteShellStdinErrorSchema,
|
|
140
138
|
WriteShellStdinResultSchema,
|
|
141
139
|
WriteSuccessSchema,
|
|
142
|
-
} from "@oh-my-pi/pi-catalog/discovery/cursor-
|
|
140
|
+
} from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
141
|
+
import {
|
|
142
|
+
create,
|
|
143
|
+
decodeJsonValue,
|
|
144
|
+
encodeJsonValue,
|
|
145
|
+
fromBinary,
|
|
146
|
+
type JsonValue,
|
|
147
|
+
toBinary,
|
|
148
|
+
toJson,
|
|
149
|
+
} from "@oh-my-pi/pi-catalog/discovery/protobuf";
|
|
143
150
|
import { isKimiK3ModelId } from "@oh-my-pi/pi-catalog/identity";
|
|
144
151
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
145
152
|
import {
|
|
146
153
|
$env,
|
|
154
|
+
isRecord,
|
|
147
155
|
logger,
|
|
148
156
|
parseJsonWithRepair,
|
|
149
157
|
parseStreamingJson,
|
|
@@ -3099,8 +3107,7 @@ function parseToolArgsJson(text: string): unknown {
|
|
|
3099
3107
|
|
|
3100
3108
|
function decodeMcpArgValue(value: Uint8Array): unknown {
|
|
3101
3109
|
try {
|
|
3102
|
-
const
|
|
3103
|
-
const jsonValue = toJson(ValueSchema, parsedValue) as JsonValue;
|
|
3110
|
+
const jsonValue = decodeJsonValue(value);
|
|
3104
3111
|
if (typeof jsonValue === "string") {
|
|
3105
3112
|
return parseToolArgsJson(jsonValue);
|
|
3106
3113
|
}
|
|
@@ -4212,10 +4219,10 @@ export function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefin
|
|
|
4212
4219
|
return forwarded.map(tool => {
|
|
4213
4220
|
const jsonSchema = toolWireSchema(tool);
|
|
4214
4221
|
const schemaValue: JsonValue =
|
|
4215
|
-
jsonSchema &&
|
|
4216
|
-
?
|
|
4222
|
+
jsonSchema !== null && !Array.isArray(jsonSchema) && isJsonValue(jsonSchema)
|
|
4223
|
+
? jsonSchema
|
|
4217
4224
|
: { type: "object", properties: {}, required: [] };
|
|
4218
|
-
const inputSchema =
|
|
4225
|
+
const inputSchema = encodeJsonValue(schemaValue);
|
|
4219
4226
|
return create(McpToolDefinitionSchema, {
|
|
4220
4227
|
name: tool.name,
|
|
4221
4228
|
description: tool.description || "",
|
|
@@ -4463,17 +4470,11 @@ function buildRootPromptMessagesJson(
|
|
|
4463
4470
|
return entries;
|
|
4464
4471
|
}
|
|
4465
4472
|
|
|
4466
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
4467
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
4468
|
-
const prototype = Object.getPrototypeOf(value);
|
|
4469
|
-
return prototype === Object.prototype || prototype === null;
|
|
4470
|
-
}
|
|
4471
|
-
|
|
4472
4473
|
function isJsonValue(value: unknown): value is JsonValue {
|
|
4473
4474
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
4474
4475
|
if (typeof value === "number") return Number.isFinite(value);
|
|
4475
4476
|
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
4476
|
-
if (!
|
|
4477
|
+
if (!isRecord(value)) return false;
|
|
4477
4478
|
for (const key in value) {
|
|
4478
4479
|
if (!isJsonValue(value[key])) return false;
|
|
4479
4480
|
}
|
|
@@ -4488,7 +4489,7 @@ function encodeCursorMcpArguments(toolCall: ToolCall): Record<string, Uint8Array
|
|
|
4488
4489
|
if (!isJsonValue(value)) {
|
|
4489
4490
|
throw new AIError.ValidationError(`Cursor tool argument ${toolCall.name}.${name} is not JSON-serializable`);
|
|
4490
4491
|
}
|
|
4491
|
-
encoded[name] =
|
|
4492
|
+
encoded[name] = encodeJsonValue(value);
|
|
4492
4493
|
}
|
|
4493
4494
|
return encoded;
|
|
4494
4495
|
}
|
package/src/providers/devin.ts
CHANGED
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
2
|
-
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
3
|
-
import {
|
|
4
|
-
ChatMessageRequestType,
|
|
5
|
-
GetChatMessageRequestSchema,
|
|
6
|
-
GetChatMessageResponseSchema,
|
|
7
|
-
} from "@oh-my-pi/pi-catalog/discovery/devin-gen/exa/api_server_pb/api_server_pb";
|
|
8
|
-
import {
|
|
9
|
-
GetUserJwtRequestSchema,
|
|
10
|
-
GetUserJwtResponseSchema,
|
|
11
|
-
} from "@oh-my-pi/pi-catalog/discovery/devin-gen/exa/auth_pb/auth_pb";
|
|
12
2
|
import {
|
|
13
3
|
CacheControlType,
|
|
14
4
|
type ChatMessagePrompt,
|
|
15
5
|
ChatMessagePromptSchema,
|
|
16
|
-
|
|
17
|
-
ChatToolDefinitionSchema,
|
|
18
|
-
PromptCacheOptionsSchema,
|
|
19
|
-
} from "@oh-my-pi/pi-catalog/discovery/devin-gen/exa/chat_pb/chat_pb";
|
|
20
|
-
import {
|
|
6
|
+
ChatMessageRequestType,
|
|
21
7
|
ChatMessageSource,
|
|
22
8
|
type ChatToolCall,
|
|
23
9
|
ChatToolCallSchema,
|
|
10
|
+
ChatToolChoiceSchema,
|
|
11
|
+
ChatToolDefinitionSchema,
|
|
24
12
|
CompletionConfigurationSchema,
|
|
25
13
|
ConversationalPlannerMode,
|
|
14
|
+
GetChatMessageRequestSchema,
|
|
15
|
+
GetChatMessageResponseSchema,
|
|
16
|
+
GetUserJwtRequestSchema,
|
|
17
|
+
GetUserJwtResponseSchema,
|
|
26
18
|
ImageDataSchema,
|
|
27
19
|
MetadataSchema,
|
|
20
|
+
PromptCacheOptionsSchema,
|
|
28
21
|
StopReason,
|
|
29
|
-
} from "@oh-my-pi/pi-catalog/discovery/devin-
|
|
22
|
+
} from "@oh-my-pi/pi-catalog/discovery/devin-proto";
|
|
23
|
+
import { create, fromBinary, toBinary } from "@oh-my-pi/pi-catalog/discovery/protobuf";
|
|
30
24
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
31
25
|
import { logger, parseStreamingJson, parseStreamingJsonThrottled } from "@oh-my-pi/pi-utils";
|
|
32
26
|
import * as AIError from "../error";
|
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
resolveOpenAICompletionsOutputClamp,
|
|
107
107
|
resolveOpenAIOutputTokenParam,
|
|
108
108
|
resolveOpenAIRequestSetup,
|
|
109
|
+
shouldDropAutoToolChoiceForReasoning,
|
|
109
110
|
shouldRetryWithoutStrictTools,
|
|
110
111
|
} from "./openai-shared";
|
|
111
112
|
import { transformMessages } from "./transform-messages";
|
|
@@ -1692,6 +1693,10 @@ function buildParams(
|
|
|
1692
1693
|
delete params.tool_choice;
|
|
1693
1694
|
}
|
|
1694
1695
|
|
|
1696
|
+
if (shouldDropAutoToolChoiceForReasoning(model, initialCompat, params.tool_choice, options)) {
|
|
1697
|
+
delete params.tool_choice;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1695
1700
|
const finalPolicy = resolveOpenAICompatPolicy(model, {
|
|
1696
1701
|
endpoint: "chat-completions",
|
|
1697
1702
|
reasoning: options?.reasoning,
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { extractHttpStatusFromError } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Fallback marker: the server rejected the `chat_template_kwargs.reasoning_effort`
|
|
6
|
+
* spelling itself (strict kwargs whitelists — Ninfer-style servers), not the
|
|
7
|
+
* effort value. Apply strips the kwarg and hoists the value onto the top-level
|
|
8
|
+
* `reasoning_effort` field when that spelling is absent.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export const STRIP_TEMPLATE_KWARG_REASONING_EFFORT = Symbol("strip-template-kwarg-reasoning-effort");
|
|
12
|
+
|
|
4
13
|
/** @internal */
|
|
5
|
-
export type OpenAIReasoningEffortFallback = string | null;
|
|
14
|
+
export type OpenAIReasoningEffortFallback = string | null | typeof STRIP_TEMPLATE_KWARG_REASONING_EFFORT;
|
|
6
15
|
|
|
7
16
|
/** @internal */
|
|
8
17
|
export interface OpenAIReasoningEffortFallbackState {
|
|
@@ -73,7 +82,23 @@ export function readOpenAIReasoningEffort(params: unknown): string | undefined {
|
|
|
73
82
|
if (!isRecord(params)) return undefined;
|
|
74
83
|
if (typeof params.reasoning_effort === "string") return params.reasoning_effort;
|
|
75
84
|
const reasoning = params.reasoning;
|
|
76
|
-
|
|
85
|
+
if (isRecord(reasoning) && typeof reasoning.effort === "string") return reasoning.effort;
|
|
86
|
+
return readTemplateKwargReasoningEffort(params);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readTemplateKwargReasoningEffort(params: Record<string, unknown>): string | undefined {
|
|
90
|
+
const kwargs = params.chat_template_kwargs;
|
|
91
|
+
return isRecord(kwargs) && typeof kwargs.reasoning_effort === "string" ? kwargs.reasoning_effort : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Remove `chat_template_kwargs.reasoning_effort`, dropping the kwargs object when it becomes empty. */
|
|
95
|
+
function deleteTemplateKwargReasoningEffort(kwargs: Record<string, unknown>, parent: Record<string, unknown>): void {
|
|
96
|
+
delete kwargs.reasoning_effort;
|
|
97
|
+
for (const key in kwargs) {
|
|
98
|
+
void key;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
delete parent.chat_template_kwargs;
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
function deleteReasoningEffort(reasoning: Record<string, unknown>, parent: Record<string, unknown>): boolean {
|
|
@@ -89,6 +114,17 @@ function deleteReasoningEffort(reasoning: Record<string, unknown>, parent: Recor
|
|
|
89
114
|
/** @internal */
|
|
90
115
|
export function applyOpenAIReasoningEffortFallback(params: unknown, fallback: OpenAIReasoningEffortFallback): boolean {
|
|
91
116
|
if (!isRecord(params)) return false;
|
|
117
|
+
if (fallback === STRIP_TEMPLATE_KWARG_REASONING_EFFORT) {
|
|
118
|
+
const kwargs = params.chat_template_kwargs;
|
|
119
|
+
if (!isRecord(kwargs) || typeof kwargs.reasoning_effort !== "string") return false;
|
|
120
|
+
const effort = kwargs.reasoning_effort;
|
|
121
|
+
deleteTemplateKwargReasoningEffort(kwargs, params);
|
|
122
|
+
// The `qwen-chat-template` dialect rides kwargs alone; keep the effort
|
|
123
|
+
// selection alive on the standard OpenAI field (Ninfer-style servers
|
|
124
|
+
// accept it, vLLM-style renderers ignore it).
|
|
125
|
+
if (typeof params.reasoning_effort !== "string") params.reasoning_effort = effort;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
92
128
|
let changed = false;
|
|
93
129
|
if (typeof params.reasoning_effort === "string") {
|
|
94
130
|
if (fallback === null) {
|
|
@@ -107,6 +143,17 @@ export function applyOpenAIReasoningEffortFallback(params: unknown, fallback: Op
|
|
|
107
143
|
changed = true;
|
|
108
144
|
}
|
|
109
145
|
}
|
|
146
|
+
// Keep the Qwen template kwarg twin in lockstep — a value remap or drop
|
|
147
|
+
// must not leave a stale effort for kwargs-reading renderers.
|
|
148
|
+
const kwargs = params.chat_template_kwargs;
|
|
149
|
+
if (isRecord(kwargs) && typeof kwargs.reasoning_effort === "string") {
|
|
150
|
+
if (fallback === null) {
|
|
151
|
+
deleteTemplateKwargReasoningEffort(kwargs, params);
|
|
152
|
+
} else {
|
|
153
|
+
kwargs.reasoning_effort = fallback;
|
|
154
|
+
}
|
|
155
|
+
changed = true;
|
|
156
|
+
}
|
|
110
157
|
return changed;
|
|
111
158
|
}
|
|
112
159
|
|
|
@@ -165,13 +212,17 @@ function isInvalidReasoningEffortError(
|
|
|
165
212
|
if (/reasoning[_ ]content/i.test(message) && !REASONING_EFFORT_FIELD_PATTERN.test(message)) return false;
|
|
166
213
|
if (/invalid[^\n]*(?:reasoning[_. ]effort|reasoning value)/i.test(message)) return true;
|
|
167
214
|
if (
|
|
168
|
-
/(?:reasoning[_. ]effort|reasoning value)[^\n]*(?:invalid|unsupported|not supported|must be|expected)/i.test(
|
|
215
|
+
/(?:reasoning[_. ]effort|reasoning value)[^\n]*(?:invalid|unsupported|not supported|not permitted|must be|expected|unknown|unexpected|unrecognized)/i.test(
|
|
169
216
|
message,
|
|
170
217
|
)
|
|
171
218
|
) {
|
|
172
219
|
return true;
|
|
173
220
|
}
|
|
174
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
/(?:unsupported|not supported|not permitted|unknown|unexpected|unrecognized|extra)[^\n]*(?:reasoning[_. ]effort|reasoning value)/i.test(
|
|
223
|
+
message,
|
|
224
|
+
)
|
|
225
|
+
) {
|
|
175
226
|
return true;
|
|
176
227
|
}
|
|
177
228
|
// Gateways put the rejected value first (`level "none" not supported`), the
|
|
@@ -258,6 +309,35 @@ function nearestEnabledReasoningFallback(currentEffort: string, allowed: Set<str
|
|
|
258
309
|
return best;
|
|
259
310
|
}
|
|
260
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Text that identifies a rejection of the kwargs spelling itself: the server
|
|
314
|
+
* names `chat_template_kwargs` together with `reasoning_effort` (Ninfer-style
|
|
315
|
+
* strict kwargs whitelists: `chat_template_kwargs.reasoning_effort is not
|
|
316
|
+
* supported`).
|
|
317
|
+
*/
|
|
318
|
+
const TEMPLATE_KWARG_EFFORT_PATTERN =
|
|
319
|
+
/chat_template_kwargs[^\n]{0,120}reasoning[_. ]effort|reasoning[_. ]effort[^\n]{0,120}chat_template_kwargs/i;
|
|
320
|
+
const FIELD_REJECTION_PATTERN =
|
|
321
|
+
/invalid|unsupported|not supported|not permitted|unknown|unexpected|unrecognized|rejected|extra input/i;
|
|
322
|
+
|
|
323
|
+
function resolveStripTemplateKwargFallback(
|
|
324
|
+
error: unknown,
|
|
325
|
+
captured: CapturedHttpErrorResponse | undefined,
|
|
326
|
+
params: unknown,
|
|
327
|
+
): typeof STRIP_TEMPLATE_KWARG_REASONING_EFFORT | undefined {
|
|
328
|
+
if (!isRecord(params)) return undefined;
|
|
329
|
+
const effort = readTemplateKwargReasoningEffort(params);
|
|
330
|
+
if (effort === undefined) return undefined;
|
|
331
|
+
const status = extractHttpStatusFromError(error) ?? captured?.status;
|
|
332
|
+
if (status !== 400 && status !== 422) return undefined;
|
|
333
|
+
const message = collectMessageParts(error, captured);
|
|
334
|
+
if (!TEMPLATE_KWARG_EFFORT_PATTERN.test(message) || !FIELD_REJECTION_PATTERN.test(message)) return undefined;
|
|
335
|
+
// A value-level rejection listing allowed levels wants the value remapped
|
|
336
|
+
// (in every spelling) by the ordinary flow, not the kwarg stripped.
|
|
337
|
+
if (parseAllowedReasoningValues(message, effort) !== undefined) return undefined;
|
|
338
|
+
return STRIP_TEMPLATE_KWARG_REASONING_EFFORT;
|
|
339
|
+
}
|
|
340
|
+
|
|
261
341
|
/** @internal */
|
|
262
342
|
export function resolveOpenAIReasoningEffortFallback(
|
|
263
343
|
error: unknown,
|
|
@@ -265,6 +345,8 @@ export function resolveOpenAIReasoningEffortFallback(
|
|
|
265
345
|
params: unknown,
|
|
266
346
|
options?: { explicitDisable?: boolean },
|
|
267
347
|
): OpenAIReasoningEffortFallback | undefined {
|
|
348
|
+
const strip = resolveStripTemplateKwargFallback(error, captured, params);
|
|
349
|
+
if (strip !== undefined) return strip;
|
|
268
350
|
const currentEffort = readOpenAIReasoningEffort(params);
|
|
269
351
|
if (!currentEffort || !KNOWN_REASONING_VALUE[currentEffort.toLowerCase()]) return undefined;
|
|
270
352
|
if (!isInvalidReasoningEffortError(error, captured, currentEffort)) return undefined;
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
resolveOpenAIOutputTokenParam,
|
|
100
100
|
resolveOpenAIRequestSetup,
|
|
101
101
|
resolveOpenAIResponsesOutputClamp,
|
|
102
|
+
shouldDropAutoToolChoiceForReasoning,
|
|
102
103
|
shouldRetryWithoutStrictTools,
|
|
103
104
|
} from "./openai-shared";
|
|
104
105
|
|
|
@@ -1276,6 +1277,10 @@ export function buildParams(
|
|
|
1276
1277
|
}
|
|
1277
1278
|
}
|
|
1278
1279
|
|
|
1280
|
+
if (shouldDropAutoToolChoiceForReasoning(model, model.compat, params.tool_choice, options)) {
|
|
1281
|
+
delete params.tool_choice;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1279
1284
|
const reasoningPolicy = resolveOpenAICompatPolicy(model, {
|
|
1280
1285
|
endpoint: "responses",
|
|
1281
1286
|
reasoning: options?.reasoning,
|
|
@@ -844,6 +844,29 @@ function isImplicitDisableWhenNotRequested(disableMode: OpenAIReasoningDisableMo
|
|
|
844
844
|
);
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
/**
|
|
848
|
+
* Whether a redundant `tool_choice: "auto"` should be dropped to keep
|
|
849
|
+
* reasoning alive. Hosts with `disableReasoningOnToolChoice` (DeepSeek family
|
|
850
|
+
* on e.g. Fireworks) silently turn reasoning off whenever any `tool_choice`
|
|
851
|
+
* is present. "auto" is already the provider default, so omitting it is
|
|
852
|
+
* wire-neutral for tool selection; forced and "none" choices are semantic and
|
|
853
|
+
* still win over reasoning (#1207).
|
|
854
|
+
*/
|
|
855
|
+
export function shouldDropAutoToolChoiceForReasoning(
|
|
856
|
+
model: Pick<Model, "reasoning">,
|
|
857
|
+
compat: { disableReasoningOnToolChoice: boolean },
|
|
858
|
+
toolChoice: unknown,
|
|
859
|
+
options: { reasoning?: string; disableReasoning?: boolean } | undefined,
|
|
860
|
+
): boolean {
|
|
861
|
+
return (
|
|
862
|
+
toolChoice === "auto" &&
|
|
863
|
+
compat.disableReasoningOnToolChoice &&
|
|
864
|
+
Boolean(model.reasoning) &&
|
|
865
|
+
options?.reasoning !== undefined &&
|
|
866
|
+
!options.disableReasoning
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
847
870
|
export function resolveOpenAICompatPolicy<TApi extends Api>(
|
|
848
871
|
model: Model<TApi>,
|
|
849
872
|
options: ResolveOpenAICompatPolicyOptions,
|
package/src/stream.ts
CHANGED
|
@@ -40,7 +40,7 @@ import type { OpenAICompletionsOptions } from "./providers/openai-completions";
|
|
|
40
40
|
import { streamPiNative } from "./providers/pi-native-client";
|
|
41
41
|
// Heavy provider stream functions are imported lazily via register-builtins,
|
|
42
42
|
// which wraps each provider module in a dynamic import. This keeps the
|
|
43
|
-
// AWS SDK, google-auth-library, @google/genai,
|
|
43
|
+
// AWS SDK, google-auth-library, @google/genai, and
|
|
44
44
|
// other provider SDKs out of the CLI startup parse graph. The
|
|
45
45
|
// gitlab-duo / kimi / synthetic providers stay eager because their modules
|
|
46
46
|
// export routing predicates (isGitLabDuoModel, isKimiModel, isSyntheticModel)
|
package/src/types.ts
CHANGED
|
@@ -32,7 +32,7 @@ import type {
|
|
|
32
32
|
ShellResult,
|
|
33
33
|
WriteArgs,
|
|
34
34
|
WriteResult,
|
|
35
|
-
} from "@oh-my-pi/pi-catalog/discovery/cursor-
|
|
35
|
+
} from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
|
|
36
36
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
37
37
|
import { isOpenAIModelId } from "@oh-my-pi/pi-catalog/identity/family";
|
|
38
38
|
import type { Api, FetchImpl, KnownApi, Model, Provider, ThinkingBudgets, Usage } from "@oh-my-pi/pi-catalog/types";
|
|
@@ -21,11 +21,11 @@ export interface JsonSchemaValidationIssue {
|
|
|
21
21
|
expectedTypes?: string[];
|
|
22
22
|
keyword?: string;
|
|
23
23
|
/**
|
|
24
|
-
* Marks issues
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* Marks issues surfaced from a failed `anyOf` / `oneOf` branch (at any
|
|
25
|
+
* depth). Such a diagnosis is one candidate branch's guess, not
|
|
26
|
+
* authoritative: the tool-argument coercion layer keeps lossy repairs
|
|
27
|
+
* (container stringification, unrecognized-key deletion, singleton-array
|
|
28
|
+
* wrapping) off for these while still applying lossless ones.
|
|
29
29
|
*/
|
|
30
30
|
fromUnionBranch?: boolean;
|
|
31
31
|
}
|
|
@@ -68,6 +68,35 @@ function getValueIdentity(ctx: ValidationContext, value: object): number {
|
|
|
68
68
|
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
69
69
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether `value` matches every `const`/`enum` discriminator property the
|
|
73
|
+
* branch declares (with at least one such property present and matching).
|
|
74
|
+
* A uniquely tag-selected branch's validation issues are authoritative — the
|
|
75
|
+
* model named its intended variant — so the coercion layer may apply lossy
|
|
76
|
+
* repairs to them; without a unique tag, branch issues are guesses.
|
|
77
|
+
*/
|
|
78
|
+
function isTagSelectedBranch(branch: unknown, value: unknown): boolean {
|
|
79
|
+
if (!isJsonObject(branch) || !isJsonObject(value)) return false;
|
|
80
|
+
const props = branch.properties;
|
|
81
|
+
if (!isJsonObject(props)) return false;
|
|
82
|
+
let matched = false;
|
|
83
|
+
for (const key in props) {
|
|
84
|
+
const propSchema = props[key];
|
|
85
|
+
if (!isJsonObject(propSchema)) continue;
|
|
86
|
+
const hasConst = Object.hasOwn(propSchema, "const");
|
|
87
|
+
const enumValues = Array.isArray(propSchema.enum) ? propSchema.enum : undefined;
|
|
88
|
+
if (!hasConst && !enumValues) continue;
|
|
89
|
+
if (!Object.hasOwn(value, key)) return false;
|
|
90
|
+
const candidate = value[key];
|
|
91
|
+
if (hasConst) {
|
|
92
|
+
if (!areJsonValuesEqual(candidate, propSchema.const)) return false;
|
|
93
|
+
} else if (enumValues && !enumValues.some(entry => areJsonValuesEqual(entry, candidate))) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
matched = true;
|
|
97
|
+
}
|
|
98
|
+
return matched;
|
|
99
|
+
}
|
|
71
100
|
|
|
72
101
|
function pushIssue(
|
|
73
102
|
issues: JsonSchemaValidationIssue[],
|
|
@@ -239,27 +268,35 @@ function validateSchemaNode(
|
|
|
239
268
|
|
|
240
269
|
let matches = 0;
|
|
241
270
|
let firstIssues: JsonSchemaValidationIssue[] | undefined;
|
|
271
|
+
let selectedIssues: JsonSchemaValidationIssue[] | undefined;
|
|
272
|
+
let selectedCount = 0;
|
|
242
273
|
for (const branch of branches) {
|
|
243
274
|
const branchIssues: JsonSchemaValidationIssue[] = [];
|
|
244
275
|
if (validateSchemaNode(branch, value, path, ctx, branchIssues)) {
|
|
245
276
|
matches += 1;
|
|
246
|
-
|
|
247
|
-
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!firstIssues) firstIssues = branchIssues;
|
|
280
|
+
if (isTagSelectedBranch(branch, value)) {
|
|
281
|
+
selectedCount += 1;
|
|
282
|
+
if (selectedCount === 1) selectedIssues = branchIssues;
|
|
248
283
|
}
|
|
249
284
|
}
|
|
250
285
|
const branchValid = keyword === "anyOf" ? matches > 0 : matches === 1;
|
|
251
286
|
if (!branchValid) {
|
|
252
|
-
if (matches === 0 &&
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
287
|
+
if (matches === 0 && selectedCount === 1 && selectedIssues && selectedIssues.length > 0) {
|
|
288
|
+
// A const/enum discriminator uniquely identifies the intended
|
|
289
|
+
// variant, so its diagnosis is authoritative: surface untagged and
|
|
290
|
+
// keep every repair (including lossy ones) available.
|
|
291
|
+
issues.push(...selectedIssues);
|
|
292
|
+
} else if (matches === 0 && firstIssues && firstIssues.length > 0) {
|
|
293
|
+
// No variant matched and no tag picks one: everything reported is
|
|
294
|
+
// the first failing branch's guess — another variant may accept the
|
|
295
|
+
// value as-is. Surface all issues (deep ones remain individually
|
|
296
|
+
// repairable by lossless coercions) but mark their provenance so
|
|
297
|
+
// lossy repairs (stringify, key deletion, singleton wrap) stay off.
|
|
257
298
|
for (const branchIssue of firstIssues) {
|
|
258
|
-
|
|
259
|
-
issues.push({ ...branchIssue, fromUnionBranch: true });
|
|
260
|
-
} else {
|
|
261
|
-
issues.push(branchIssue);
|
|
262
|
-
}
|
|
299
|
+
issues.push(branchIssue.fromUnionBranch ? branchIssue : { ...branchIssue, fromUnionBranch: true });
|
|
263
300
|
}
|
|
264
301
|
} else {
|
|
265
302
|
pushIssue(
|
package/src/utils/validation.ts
CHANGED
|
@@ -136,12 +136,20 @@ function tryCoerceBooleanToNumber(value: unknown, expectedTypes: string[]): { va
|
|
|
136
136
|
return { value: value ? 1 : 0, changed: true };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function tryCoerceString(
|
|
139
|
+
function tryCoerceString(
|
|
140
|
+
value: unknown,
|
|
141
|
+
expectedTypes: string[],
|
|
142
|
+
allowLossy: boolean,
|
|
143
|
+
): { value: unknown; changed: boolean } {
|
|
140
144
|
if (!expectedTypes.includes("string") || typeof value === "string" || value === null || value === undefined) {
|
|
141
145
|
return { value, changed: false };
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
if (Array.isArray(value) || typeof value === "object") {
|
|
149
|
+
// JSON.stringify is irreversible (downstream consumers receive encoded
|
|
150
|
+
// text where they expected structure), so it requires an authoritative
|
|
151
|
+
// diagnosis — never a union-branch guess.
|
|
152
|
+
if (!allowLossy) return { value, changed: false };
|
|
145
153
|
try {
|
|
146
154
|
const stringified = JSON.stringify(value);
|
|
147
155
|
if (stringified === undefined) return { value, changed: false };
|
|
@@ -158,7 +166,16 @@ function tryCoerceString(value: unknown, expectedTypes: string[]): { value: unkn
|
|
|
158
166
|
return { value: String(value), changed: true };
|
|
159
167
|
}
|
|
160
168
|
|
|
161
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Schema-directed value repair for a single type issue. `allowLossy` gates the
|
|
171
|
+
* irreversible repairs (container→string stringification); lossless repairs
|
|
172
|
+
* (JSON parsing, boolean spellings, scalar stringification) always apply.
|
|
173
|
+
*/
|
|
174
|
+
function tryCoerceForExpectedTypes(
|
|
175
|
+
value: unknown,
|
|
176
|
+
expectedTypes: string[],
|
|
177
|
+
allowLossy: boolean,
|
|
178
|
+
): { value: unknown; changed: boolean } {
|
|
162
179
|
if (typeof value === "string") {
|
|
163
180
|
const parsed = tryParseJsonForTypes(value, expectedTypes);
|
|
164
181
|
if (parsed.changed) return parsed;
|
|
@@ -171,7 +188,7 @@ function tryCoerceForExpectedTypes(value: unknown, expectedTypes: string[]): { v
|
|
|
171
188
|
const numericCoercion = tryCoerceBooleanToNumber(value, expectedTypes);
|
|
172
189
|
if (numericCoercion.changed) return numericCoercion;
|
|
173
190
|
|
|
174
|
-
return tryCoerceString(value, expectedTypes);
|
|
191
|
+
return tryCoerceString(value, expectedTypes, allowLossy);
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
function tryParseLeadingJsonContainer(value: string): unknown | undefined {
|
|
@@ -1576,9 +1593,11 @@ function coerceArgsFromIssues(args: unknown, issues: FlatIssue[]): { value: unkn
|
|
|
1576
1593
|
// a type coercion actually needs to write into a leaf.
|
|
1577
1594
|
let owned = false;
|
|
1578
1595
|
let nextArgs: unknown = args;
|
|
1579
|
-
|
|
1580
1596
|
for (const issue of issues) {
|
|
1597
|
+
// Failed union branches still contribute schema-directed type repairs.
|
|
1598
|
+
// Container-to-string conversion remains enabled for string branches.
|
|
1581
1599
|
if (issue.keyword === "unrecognized") {
|
|
1600
|
+
if (issue.unionBranch) continue;
|
|
1582
1601
|
const previous = nextArgs;
|
|
1583
1602
|
nextArgs = deleteValueAtPointer(nextArgs, issue.instancePath);
|
|
1584
1603
|
if (nextArgs !== previous) changed = true;
|
|
@@ -1588,7 +1607,7 @@ function coerceArgsFromIssues(args: unknown, issues: FlatIssue[]): { value: unkn
|
|
|
1588
1607
|
if (issue.expectedTypes.length === 0) continue;
|
|
1589
1608
|
|
|
1590
1609
|
const currentValue = getValueAtPointer(nextArgs, issue.instancePath);
|
|
1591
|
-
const result = tryCoerceForExpectedTypes(currentValue, issue.expectedTypes);
|
|
1610
|
+
const result = tryCoerceForExpectedTypes(currentValue, issue.expectedTypes, true);
|
|
1592
1611
|
let coercedValue = result.changed ? result.value : undefined;
|
|
1593
1612
|
if (
|
|
1594
1613
|
coercedValue === undefined &&
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
version: v2
|
|
2
|
-
# Codegen is scoped to the api_server.proto transitive closure (the RPCs the
|
|
3
|
-
# devin provider actually uses). The rest of the reverse-engineered Codeium
|
|
4
|
-
# corpus is kept in this directory for reference/maintenance but is NOT
|
|
5
|
-
# generated. Vendored google/* WKTs are intentionally absent — buf bundles them.
|
|
6
|
-
inputs:
|
|
7
|
-
- directory: .
|
|
8
|
-
paths:
|
|
9
|
-
- buf/validate/validate.proto
|
|
10
|
-
- exa/analytics_pb/analytics.proto
|
|
11
|
-
- exa/api_server_pb/api_server.proto
|
|
12
|
-
- exa/auth_pb/auth.proto
|
|
13
|
-
- exa/auto_cascade_common_pb/auto_cascade_common.proto
|
|
14
|
-
- exa/bug_checker_pb/bug_checker.proto
|
|
15
|
-
- exa/cascade_plugins_pb/cascade_plugins.proto
|
|
16
|
-
- exa/chat_pb/chat.proto
|
|
17
|
-
- exa/code_edit/code_edit_pb/code_edit.proto
|
|
18
|
-
- exa/codeium_common_pb/codeium_common.proto
|
|
19
|
-
- exa/context_module_pb/context_module.proto
|
|
20
|
-
- exa/cortex_pb/cortex.proto
|
|
21
|
-
- exa/diff_action_pb/diff_action.proto
|
|
22
|
-
- exa/index_pb/index.proto
|
|
23
|
-
- exa/knowledge_base_pb/knowledge_base.proto
|
|
24
|
-
- exa/language_server_pb/language_server.proto
|
|
25
|
-
- exa/opensearch_clients_pb/opensearch_clients.proto
|
|
26
|
-
- exa/prompt_pb/prompt.proto
|
|
27
|
-
- exa/reactive_component_pb/reactive_component.proto
|
|
28
|
-
- exa/trust_pb/trust.proto
|
|
29
|
-
plugins:
|
|
30
|
-
- local: ../../../../../../node_modules/.bin/protoc-gen-es
|
|
31
|
-
out: ../../../../../catalog/src/discovery/devin-gen
|
|
32
|
-
opt:
|
|
33
|
-
- target=ts
|