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:
alexion committed 2026-10-15 17:58:48 -04:00
1 parent 719370c8f7
commit 6081e3e325
1743 files changed
+205459 -131704

No files matched your search

+103 -45
View File
@@ -5,7 +5,6 @@ import type {
Api,
AssistantMessage,
CacheRetention,
Context,
Model,
OpenAIResponsesCompat,
ProviderEnv,
@@ -13,24 +12,41 @@ import type {
SimpleStreamOptions,
StreamFunction,
StreamOptions,
TranscriptContext,
Usage,
} from "../types.ts";
import { splitDeferredTools } from "../utils/deferred-tools.ts";
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { getPiUserAgent } from "../utils/pi-user-agent.ts";
import { getProviderEnvValue } from "../utils/provider-env.ts";
import { retryProviderRequest } from "../utils/provider-retry.ts";
import { getDeclaredTools, resolveTranscript, resolveTranscriptTools } from "../utils/transcript.ts";
import { createGrammarToolInputProperties } from "./constrained-sampling.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
import { buildBaseOptions, resolveSamplingParams } from "./simple-options.ts";
const OPENAI_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
// OpenAI Responses rejects max_output_tokens below 16: https://github.com/earendil-works/pi/issues/6265
const OPENAI_RESPONSES_MIN_OUTPUT_TOKENS = 16;
const CHATGPT_USAGE_URL = "https://chatgpt.com/settings/usage";
// Fixed reasoning.effort while configuration_update items carry the thinking level.
const CONFIGURATION_UPDATE_REQUEST_EFFORT = "medium";
/**
* OpenAI API keys start with `sk-`; a different credential sent directly to OpenAI
* is a Sign in with ChatGPT access token.
*/
function isChatGPTSignIn(model: Model<"openai-responses">, apiKey: string | undefined): boolean {
return (
model.provider === "openai" &&
model.baseUrl === "https://api.openai.com/v1" &&
apiKey !== undefined &&
!apiKey.startsWith("sk-")
);
}
function hasHeader(headers: ProviderHeaders | undefined, name: string): boolean {
if (!headers) return false;
@@ -68,6 +84,7 @@ function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEn
function getCompat(model: Model<"openai-responses">): Required<OpenAIResponsesCompat> {
return {
supportsDeveloperRole: model.compat?.supportsDeveloperRole ?? true,
supportsMidConvoSystemMessages: model.compat?.supportsMidConvoSystemMessages ?? false,
sessionAffinityFormat: model.compat?.sessionAffinityFormat ?? detectSessionAffinityFormat(model),
supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
supportsStrictMode: model.compat?.supportsStrictMode ?? false,
@@ -76,9 +93,27 @@ function getCompat(model: Model<"openai-responses">): Required<OpenAIResponsesCo
supportsToolSearch: model.compat?.supportsToolSearch ?? false,
supportsExplicitPromptCacheMode: model.compat?.supportsExplicitPromptCacheMode ?? false,
supportsMaxOutputTokens: model.compat?.supportsMaxOutputTokens ?? true,
supportsMidConvoEffort: model.compat?.supportsMidConvoEffort ?? false,
};
}
/** Effort to send in configuration_update items, or undefined to send it as reasoning.effort. */
function getConfigurationUpdateEffort(
model: Model<"openai-responses">,
options: OpenAIResponsesOptions | undefined,
compat: Required<OpenAIResponsesCompat>,
): string | undefined {
if (
!compat.supportsMidConvoEffort ||
!options?.reasoningEffort ||
// Sign in with ChatGPT rejects configuration_update items.
isChatGPTSignIn(model, options.apiKey)
) {
return undefined;
}
return model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
}
function getPromptCacheRetention(
compat: Required<OpenAIResponsesCompat>,
cacheRetention: CacheRetention,
@@ -91,17 +126,13 @@ function getPromptCacheRetention(
function getPromptCacheOptions(
compat: Required<OpenAIResponsesCompat>,
cacheRetention: CacheRetention,
): { mode?: "explicit"; ttl?: "30m" } | undefined {
): ResponseCreateParamsStreaming["prompt_cache_options"] {
if (!compat.supportsExplicitPromptCacheMode) return undefined;
if (cacheRetention === "none") return { mode: "explicit" };
if (cacheRetention === "long" && compat.supportsLongCacheRetention) return { ttl: "30m" };
return undefined;
}
function formatOpenAIResponsesError(error: unknown): string {
return formatProviderError(normalizeProviderError(error), "OpenAI API error");
}
// OpenAI Responses-specific options
export interface OpenAIResponsesOptions extends StreamOptions {
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
@@ -115,19 +146,23 @@ export interface OpenAIResponsesOptions extends StreamOptions {
*/
export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions> = (
model: Model<"openai-responses">,
context: Context,
context: TranscriptContext,
options?: OpenAIResponsesOptions,
): AssistantMessageEventStream => {
const stream = new AssistantMessageEventStream();
const normalizedContext = resolveTranscript(context, getCompat(model).supportsMidConvoSystemMessages);
// Start async processing
(async () => {
const compat = getCompat(model);
const configurationUpdateEffort = getConfigurationUpdateEffort(model, options, compat);
const output: AssistantMessage = {
role: "assistant",
content: [],
api: model.api as Api,
provider: model.provider,
model: model.id,
...(configurationUpdateEffort === undefined ? {} : { providerThinkingLevel: configurationUpdateEffort }),
usage: {
input: 0,
output: 0,
@@ -145,13 +180,26 @@ export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions>
const apiKey = getClientApiKey(model.provider, options?.apiKey, options?.headers);
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
const compat = getCompat(model);
const grammarToolInputProperties = createGrammarToolInputProperties(
context.tools,
getDeclaredTools(normalizedContext.messages),
compat.supportsOpenAIGrammarTools,
);
const client = createClient(model, context, apiKey, options?.headers, options?.fetch, cacheSessionId);
let params = buildParams(model, context, options, compat, grammarToolInputProperties);
const client = createClient(
model,
normalizedContext,
apiKey,
options?.headers,
options?.fetch,
cacheSessionId,
);
let params = buildParams(
model,
normalizedContext,
options,
compat,
grammarToolInputProperties,
configurationUpdateEffort,
);
const nextParams = await options?.onPayload?.(params, model);
if (nextParams !== undefined) {
params = nextParams as ResponseCreateParamsStreaming;
@@ -173,6 +221,7 @@ export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions>
stream.push({ type: "start", partial: output });
await processResponsesStream(openaiStream, output, stream, model, {
onProviderStreamEvent: options?.onProviderStreamEvent,
serviceTier: options?.serviceTier,
grammarToolInputProperties,
applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
@@ -199,7 +248,14 @@ export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions>
delete (block as { customInput?: unknown }).customInput;
}
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = formatOpenAIResponsesError(error);
const errorMessage = formatProviderError(
normalizeProviderError(error),
`${model.provider === "openai" ? "OpenAI" : model.provider} API error`,
);
// Sign in with ChatGPT shares the subscription's usage limit with other apps.
output.errorMessage = errorMessage.includes("subscription_sharing_usage_limit_exceeded")
? `${errorMessage}\nCheck your ChatGPT usage: ${CHATGPT_USAGE_URL}`
: errorMessage;
stream.push({ type: "error", reason: output.stopReason, error: output });
stream.end();
}
@@ -210,7 +266,7 @@ export const stream: StreamFunction<"openai-responses", OpenAIResponsesOptions>
export const streamSimple: StreamFunction<"openai-responses", SimpleStreamOptions> = (
model: Model<"openai-responses">,
context: Context,
context: TranscriptContext,
options?: SimpleStreamOptions,
): AssistantMessageEventStream => {
getClientApiKey(model.provider, options?.apiKey, options?.headers);
@@ -230,7 +286,7 @@ export const streamSimple: StreamFunction<"openai-responses", SimpleStreamOption
function createClient(
model: Model<"openai-responses">,
context: Context,
context: TranscriptContext,
apiKey: string,
optionsHeaders?: ProviderHeaders,
fetch?: typeof globalThis.fetch,
@@ -274,48 +330,46 @@ function createClient(
function buildParams(
model: Model<"openai-responses">,
context: Context,
context: TranscriptContext,
options: OpenAIResponsesOptions | undefined,
compat: Required<OpenAIResponsesCompat> = getCompat(model),
grammarToolInputProperties: ReadonlyMap<string, string> = createGrammarToolInputProperties(
context.tools,
compat.supportsOpenAIGrammarTools,
),
compat: Required<OpenAIResponsesCompat>,
grammarToolInputProperties: ReadonlyMap<string, string>,
configurationUpdateEffort: string | undefined,
) {
const deferredToolsMode = compat.supportsAdditionalTools
? "additional-tools"
: compat.supportsToolSearch
? "tool-search"
: undefined;
const toolPlacement = splitDeferredTools(context, deferredToolsMode !== undefined);
const transcriptTools = resolveTranscriptTools(
context.messages,
compat.supportsAdditionalTools || compat.supportsToolSearch,
);
const messages = convertResponsesMessages(model, context, OPENAI_TOOL_CALL_PROVIDERS, {
grammarToolInputProperties,
deferredTools: toolPlacement.deferred,
deferredToolsMode,
supportsMidConvoSystemMessages: compat.supportsMidConvoSystemMessages,
supportsAdditionalTools: compat.supportsAdditionalTools,
supportsToolSearch: compat.supportsToolSearch,
toolOptions: {
supportsStrictMode: compat.supportsStrictMode,
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools,
},
configurationUpdateEffort,
});
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
const params: ResponseCreateParamsStreaming & {
prompt_cache_options?: { mode?: "explicit"; ttl?: "30m" };
} = {
// Sign in with ChatGPT rejects these request fields.
const omitUnsupportedFields = isChatGPTSignIn(model, options?.apiKey);
const params: ResponseCreateParamsStreaming = {
model: model.id,
input: messages,
stream: true,
prompt_cache_key: cacheRetention === "none" ? undefined : clampOpenAIPromptCacheKey(options?.sessionId),
prompt_cache_retention: getPromptCacheRetention(compat, cacheRetention),
prompt_cache_options: getPromptCacheOptions(compat, cacheRetention),
prompt_cache_retention: omitUnsupportedFields ? undefined : getPromptCacheRetention(compat, cacheRetention),
prompt_cache_options: omitUnsupportedFields ? undefined : getPromptCacheOptions(compat, cacheRetention),
store: false,
};
if (options?.maxTokens && compat.supportsMaxOutputTokens) {
if (options?.maxTokens && compat.supportsMaxOutputTokens && !omitUnsupportedFields) {
params.max_output_tokens = Math.max(options.maxTokens, OPENAI_RESPONSES_MIN_OUTPUT_TOKENS);
}
if (options?.temperature !== undefined) {
if (options?.temperature !== undefined && !omitUnsupportedFields) {
params.temperature = options?.temperature;
}
@@ -323,8 +377,8 @@ function buildParams(
params.service_tier = options.serviceTier;
}
if (toolPlacement.immediate.length > 0) {
params.tools = convertResponsesTools(toolPlacement.immediate, {
if (transcriptTools.requestTools.length > 0) {
params.tools = convertResponsesTools(transcriptTools.requestTools, {
supportsStrictMode: compat.supportsStrictMode,
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools,
});
@@ -334,11 +388,13 @@ function buildParams(
params.tool_choice = options.toolChoice;
}
const reasoningEffort = options?.reasoningEffort ?? (options?.reasoningSummary ? "medium" : undefined);
if (model.reasoning) {
if (options?.reasoningEffort || options?.reasoningSummary) {
const effort = options?.reasoningEffort
if (reasoningEffort) {
const requestedEffort = options?.reasoningEffort
? (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort)
: "medium";
: reasoningEffort;
const effort = configurationUpdateEffort === undefined ? requestedEffort : CONFIGURATION_UPDATE_REQUEST_EFFORT;
params.reasoning = {
effort: effort as NonNullable<typeof params.reasoning>["effort"],
summary: options?.reasoningSummary || "auto",
@@ -352,9 +408,10 @@ function buildParams(
if (model.provider === "xai") params.include = ["reasoning.encrypted_content"];
}
// Last so custom keys override the named request fields.
if (options?.samplingParams) {
Object.assign(params, options.samplingParams);
// Last so model and request sampling parameters override named request fields.
const samplingParams = resolveSamplingParams(model, reasoningEffort ?? "off", options?.samplingParams);
if (samplingParams) {
Object.assign(params, samplingParams);
}
return params;
@@ -368,6 +425,7 @@ function getServiceTierCostMultiplier(
case "flex":
return 0.5;
case "priority":
case "fast":
return model.id === "gpt-5.5" ? 2.5 : 2;
default:
return 1;