chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -1,6 +1,7 @@
import type OpenAI from "openai";
import type {
Tool as OpenAITool,
ResponseConfigurationUpdateItemParam,
ResponseCreateParamsStreaming,
ResponseInput,
ResponseInputContent,
@@ -17,21 +18,25 @@ import { calculateCost } from "../models.ts";
import type {
Api,
AssistantMessage,
Context,
ImageContent,
Model,
StopReason,
StreamOptions,
SystemMessage,
TextContent,
TextSignatureV1,
ThinkingContent,
Tool,
ToolCall,
TranscriptContext,
Usage,
} from "../types.ts";
import type { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { shortHash } from "../utils/hash.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { parseStreamingJson, parseToolCallArguments } from "../utils/json-parse.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.ts";
import { resolveTranscript, resolveTranscriptTools } from "../utils/transcript.ts";
import {
appendGrammarToolInputJsonDelta,
type GrammarToolInputJsonBuffer,
@@ -104,6 +109,7 @@ function convertToolResultOutput<TApi extends Api>(
}
export interface OpenAIResponsesStreamOptions {
onProviderStreamEvent?: StreamOptions["onProviderStreamEvent"];
serviceTier?: ResponseCreateParamsStreaming["service_tier"];
grammarToolInputProperties?: ReadonlyMap<string, string>;
resolveServiceTier?: (
@@ -119,16 +125,28 @@ export interface OpenAIResponsesStreamOptions {
export interface ConvertResponsesMessagesOptions {
includeSystemPrompt?: boolean;
grammarToolInputProperties?: ReadonlyMap<string, string>;
deferredTools?: ReadonlyMap<string, Tool>;
deferredToolsMode?: "additional-tools" | "tool-search";
/** Whether later system messages are sent in place; otherwise they are folded into the leading prompt. */
supportsMidConvoSystemMessages?: boolean;
supportsAdditionalTools?: boolean;
supportsToolSearch?: boolean;
toolOptions?: ConvertResponsesToolsOptions;
/**
* Effort for the reply being generated. When set, the thinking level is sent as
* configuration_update items instead of a changing reasoning.effort, which would invalidate the
* prompt cache. Each item sets the effort from that point on: one goes before every earlier
* reply with the level recorded on it (`providerThinkingLevel`), and one with this effort goes
* at the end. Earlier input therefore never changes. OpenAI documents the item before user
* messages only. After tool results it was verified on gpt-6-luna (October 2026) to be accepted
* and to apply to the next reply.
*/
configurationUpdateEffort?: string;
}
export interface ConvertResponsesToolsOptions {
strict?: boolean | null;
supportsStrictMode?: boolean;
supportsOpenAIGrammarTools?: boolean;
deferLoading?: boolean;
toolSearchResult?: boolean;
}
// =============================================================================
@@ -137,12 +155,12 @@ export interface ConvertResponsesToolsOptions {
export function convertResponsesMessages<TApi extends Api>(
model: Model<TApi>,
context: Context,
context: TranscriptContext,
allowedToolCallProviders: ReadonlySet<string>,
options?: ConvertResponsesMessagesOptions,
): ResponseInput {
const normalizedContext = resolveTranscript(context, options?.supportsMidConvoSystemMessages);
const messages: ResponseInput = [];
const loadedToolNames = new Set<string>();
const normalizeIdPart = (part: string): string => {
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -169,21 +187,63 @@ export function convertResponsesMessages<TApi extends Api>(
return `${normalizedCallId}|${normalizedItemId}`;
};
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
const includeSystemPrompt = options?.includeSystemPrompt ?? true;
if (includeSystemPrompt && context.systemPrompt) {
const compat = model.compat as { supportsDeveloperRole?: boolean } | undefined;
const role = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system";
const transformedMessages = transformMessages(normalizedContext.messages, model, normalizeToolCallId);
const transcriptTools = resolveTranscriptTools(
normalizedContext.messages,
(options?.supportsAdditionalTools ?? false) || (options?.supportsToolSearch ?? false),
);
const appendSystemToolAdditions = (message: SystemMessage, seed: string): void => {
const tools = transcriptTools.anchorsAdditions ? (message.toolsAdded ?? []) : [];
if (tools.length === 0) return;
if (options?.supportsAdditionalTools) {
messages.push({
type: "additional_tools",
role: "developer",
tools: convertResponsesTools(tools, options.toolOptions),
} satisfies ResponseInputItem);
return;
}
if (!options?.supportsToolSearch) return;
const names = tools.map((tool) => tool.name);
const callId = `pi_tool_load_${shortHash(`${seed}:${names.join(",")}`)}`;
messages.push({
role,
content: sanitizeSurrogates(context.systemPrompt),
});
}
type: "tool_search_call",
call_id: callId,
execution: "client",
status: "completed",
arguments: { query: names.join(" "), limit: names.length },
} satisfies ResponseInputItem);
messages.push({
type: "tool_search_output",
call_id: callId,
execution: "client",
status: "completed",
tools: convertResponsesTools(tools, { ...options.toolOptions, toolSearchResult: true }),
} satisfies ResponseToolSearchOutputItemParam);
};
const includeInitialSystemMessage = options?.includeSystemPrompt ?? true;
const pushConfigurationUpdate = (effort: string): void => {
messages.push({
type: "configuration_update",
reasoning: { effort: effort as NonNullable<ResponseConfigurationUpdateItemParam.Reasoning["effort"]> },
} satisfies ResponseConfigurationUpdateItemParam);
};
const compat = model.compat as { supportsDeveloperRole?: boolean } | undefined;
const instructionRole = model.reasoning && compat?.supportsDeveloperRole !== false ? "developer" : "system";
let msgIndex = 0;
let sourceIndex = 0;
for (const msg of transformedMessages) {
if (msg.role === "user") {
const isLeadingSystemMessage = sourceIndex++ === 0 && msg.role === "system";
if (msg.role === "system") {
if (!isLeadingSystemMessage) appendSystemToolAdditions(msg, `system:${msgIndex}`);
if (!isLeadingSystemMessage || includeInitialSystemMessage) {
const text = isLeadingSystemMessage ? getSystemMessageText(msg) : renderSystemMessageUpdate(msg);
if (text.length > 0) {
messages.push({ role: instructionRole, content: sanitizeSurrogates(text) });
}
}
} else if (msg.role === "user") {
if (typeof msg.content === "string") {
messages.push({
role: "user",
@@ -251,19 +311,16 @@ export function convertResponsesMessages<TApi extends Api>(
let itemId: string | undefined = itemIdRaw;
// For different-model messages, set id to undefined to avoid pairing validation.
// OpenAI tracks which fc_xxx IDs were paired with rs_xxx reasoning items.
// OpenAI tracks which item IDs were paired with rs_xxx reasoning items.
// By omitting the id, we avoid triggering that validation (like cross-provider does).
// When replaying custom-tool calls as a function_call, also drop non-fc_* ids such as
// ctc_* custom-tool ids because function_call item ids must be fc_*.
if (
(isDifferentModel && itemId?.startsWith("fc_")) ||
(customInputProperty === undefined && !itemId?.startsWith("fc_"))
) {
// Also drop ids that do not match the replayed item type: function_call ids must be fc_*
// and custom_tool_call ids must be ctc_*. Foreign tool call ids are normalized to fc_*, and
// a call can switch between the two types when grammar tool support differs.
const itemIdPrefix = customInputProperty === undefined ? "fc_" : "ctc_";
if (isDifferentModel || !itemId?.startsWith(itemIdPrefix)) {
itemId = undefined;
}
const canReplayNamespace = isSameModel || options?.deferredTools?.has(toolCall.name) === true;
if (customInputProperty !== undefined) {
output.push({
type: "custom_tool_call",
@@ -273,9 +330,7 @@ export function convertResponsesMessages<TApi extends Api>(
input: sanitizeSurrogates(
getGrammarToolInput(toolCall.name, toolCall.arguments, customInputProperty),
),
...(canReplayNamespace && toolCall.namespace !== undefined
? { namespace: toolCall.namespace }
: {}),
...(isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}),
} satisfies ResponseOutputItem);
} else {
output.push({
@@ -284,14 +339,19 @@ export function convertResponsesMessages<TApi extends Api>(
call_id: callId,
name: toolCall.name,
arguments: JSON.stringify(toolCall.arguments),
...(canReplayNamespace && toolCall.namespace !== undefined
? { namespace: toolCall.namespace }
: {}),
...(isSameModel && toolCall.namespace !== undefined ? { namespace: toolCall.namespace } : {}),
});
}
}
}
if (output.length === 0) continue;
if (
options?.configurationUpdateEffort !== undefined &&
isSameModel &&
msg.providerThinkingLevel !== undefined
) {
pushConfigurationUpdate(msg.providerThinkingLevel);
}
messages.push(...output);
} else if (msg.role === "toolResult") {
const [callId] = msg.toolCallId.split("|");
@@ -310,44 +370,10 @@ export function convertResponsesMessages<TApi extends Api>(
output,
});
}
const deferredTools: Tool[] = [];
for (const name of msg.addedToolNames ?? []) {
const tool = options?.deferredTools?.get(name);
if (!tool || loadedToolNames.has(name)) continue;
loadedToolNames.add(name);
deferredTools.push(tool);
}
if (deferredTools.length > 0 && options?.deferredToolsMode === "additional-tools") {
messages.push({
type: "additional_tools",
role: "developer",
tools: convertResponsesTools(deferredTools, options.toolOptions),
} satisfies ResponseInputItem);
} else if (deferredTools.length > 0 && options?.deferredToolsMode === "tool-search") {
const names = deferredTools.map((tool) => tool.name);
const searchCallId = `pi_tool_load_${shortHash(`${msg.toolCallId}:${names.join(",")}`)}`;
messages.push({
type: "tool_search_call",
call_id: searchCallId,
execution: "client",
status: "completed",
arguments: { query: names.join(" "), limit: names.length },
} satisfies ResponseInputItem);
messages.push({
type: "tool_search_output",
call_id: searchCallId,
execution: "client",
status: "completed",
tools: convertResponsesTools(deferredTools, {
...options.toolOptions,
deferLoading: true,
}),
} satisfies ResponseToolSearchOutputItemParam);
}
}
msgIndex++;
if (!isLeadingSystemMessage) msgIndex++;
}
if (options?.configurationUpdateEffort !== undefined) pushConfigurationUpdate(options.configurationUpdateEffort);
return messages;
}
@@ -373,7 +399,7 @@ export function convertResponsesTools(tools: readonly Tool[], options?: ConvertR
syntax: grammar.format,
definition: grammar.definition,
},
...(options?.deferLoading ? { defer_loading: true } : {}),
...(options?.toolSearchResult ? { defer_loading: true } : {}),
} satisfies OpenAITool;
}
@@ -386,7 +412,7 @@ export function convertResponsesTools(tools: readonly Tool[], options?: ConvertR
name: tool.name,
description: tool.description,
parameters: getJsonSchemaToolParameters(tool, strict === true) as Record<string, unknown>,
...(options?.deferLoading ? { defer_loading: true } : {}),
...(options?.toolSearchResult ? { defer_loading: true } : {}),
};
if (supportsStrictMode) {
functionTool.strict = strict;
@@ -596,6 +622,7 @@ export async function processResponsesStream<TApi extends Api>(
};
for await (const event of openaiStream) {
await options?.onProviderStreamEvent?.(event, model);
if (event.type === "response.created") {
output.responseId = event.response.id;
} else if (event.type === "response.output_item.added") {
@@ -711,7 +738,7 @@ export async function processResponsesStream<TApi extends Api>(
slot?.type === "toolCall" &&
slot.block.partialJson !== undefined
) {
slot.block.arguments = parseStreamingJson(item.arguments || slot.block.partialJson || "{}");
Object.assign(slot.block, parseToolCallArguments(item.arguments || slot.block.partialJson));
if (item.namespace !== undefined) slot.block.namespace = item.namespace;
// Finalize in-place and strip the scratch buffer so replay only
// carries parsed arguments.
@@ -758,6 +785,20 @@ export async function processResponsesStream<TApi extends Api>(
if (!sawTerminalResponseEvent) {
throw new Error("OpenAI Responses stream ended before a terminal response event");
}
// The agent runs every tool call in the final message. Refuse to hand over calls whose
// output_item.done never arrived: their arguments may be cut off or mixed up, e.g. when a
// non-compliant server omits output_index. Finished calls have their scratch buffers removed.
if (output.stopReason === "toolUse") {
for (const block of output.content) {
if (block.type !== "toolCall") continue;
const toolCall = block as StreamingToolCall;
if (toolCall.partialJson !== undefined || toolCall.customInput !== undefined) {
throw new Error(
`OpenAI Responses stream completed with an unfinished tool call: ${toolCall.name} (${toolCall.id})`,
);
}
}
}
}
function mapStopReason(