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:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
+103
-45
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user