enhance pi configuration and skills
This commit is contained in:
parent
623512cbf5
commit
ab0a86a707
18 changed files with 2000 additions and 159 deletions
21
pi/extensions/openai-server-compaction/LICENSE.md
Normal file
21
pi/extensions/openai-server-compaction/LICENSE.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Alexis Gallagher
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
81
pi/extensions/openai-server-compaction/README.md
Normal file
81
pi/extensions/openai-server-compaction/README.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# OpenAI server compaction
|
||||
|
||||
A vendored Pi extension that uses OpenAI's Responses compaction protocol for GPT
|
||||
models while preserving Pi's session, tree, and fallback behavior.
|
||||
|
||||
This fork is based on
|
||||
[`algal/pi-openai-server-compaction`](https://github.com/algal/pi-openai-server-compaction)
|
||||
and retains its MIT license. It is adapted for this dotfiles repository and Pi
|
||||
0.82.
|
||||
|
||||
## Scope
|
||||
|
||||
Remote compaction is intentionally limited to GPT models on these native Pi
|
||||
providers:
|
||||
|
||||
- `openai/*` using `openai-responses`
|
||||
- `openai-codex/*` using `openai-codex-responses`
|
||||
|
||||
Other models and providers are untouched and continue using Pi's default
|
||||
compaction.
|
||||
|
||||
## Compaction behavior
|
||||
|
||||
Pi remains responsible for deciding when to compact, selecting the cut point,
|
||||
and writing the compaction entry. On `session_before_compact`, this extension:
|
||||
|
||||
1. asks OpenAI for an opaque `compaction` item through the Responses API;
|
||||
2. generates a portable text summary in parallel;
|
||||
3. stores the opaque replacement history in
|
||||
`CompactionEntry.details.remoteCompaction`; and
|
||||
4. replays that history on later requests to the exact same provider/API/model.
|
||||
|
||||
If remote compaction fails but the portable summary succeeds, Pi uses that text
|
||||
summary. If neither extension path succeeds, the handler returns control to
|
||||
Pi's default compactor.
|
||||
|
||||
The opaque artifact is model-specific. Switching models uses Pi's portable text
|
||||
summary; switching back reconstructs the matching artifact from session JSONL.
|
||||
|
||||
## Pi 0.82 adaptation
|
||||
|
||||
Unlike upstream, this fork does not override Pi's OpenAI provider or install a
|
||||
custom WebSocket transport. It uses Pi 0.82's native HTTP Responses transport.
|
||||
This avoids the upstream WebSocket partial-rendering issue and removes the
|
||||
runtime `ws` dependency.
|
||||
|
||||
Because Pi's full replay payload is not safe to combine with
|
||||
`previous_response_id`, this fork disables that optimization. It also leaves
|
||||
normal pre-compaction requests unchanged instead of enabling OpenAI's automatic
|
||||
`context_management`, whose compaction stream events Pi 0.82 does not natively
|
||||
persist. Pi triggers compaction normally; post-compaction requests replay the
|
||||
opaque artifact explicitly.
|
||||
|
||||
## Data handling
|
||||
|
||||
Conversation context is sent to OpenAI during compaction with `store: false`,
|
||||
and returned encrypted artifacts are stored in Pi's local session JSONL. The
|
||||
artifacts are not human-readable. OpenAI's normal API data-handling and abuse
|
||||
monitoring policies still apply.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is read from:
|
||||
|
||||
- `~/.pi/agent/openai-server-compaction.json`
|
||||
- `.pi/openai-server-compaction.json` (takes precedence)
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"notify": false
|
||||
}
|
||||
```
|
||||
|
||||
Environment overrides:
|
||||
|
||||
- `PI_OPENAI_SERVER_COMPACTION_ENABLED`
|
||||
- `PI_OPENAI_SERVER_COMPACTION_NOTIFY`
|
||||
|
||||
Set `PI_OPENAI_SERVER_COMPACTION_ENABLED=0` for a quick rollback, or start Pi
|
||||
with `--no-extensions` to bypass all extensions.
|
||||
59
pi/extensions/openai-server-compaction/config.ts
Normal file
59
pi/extensions/openai-server-compaction/config.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Configuration loading for the extension.
|
||||
*
|
||||
* Reads global/project JSON config files plus environment overrides and exposes
|
||||
* a normalized, fully-populated runtime config object.
|
||||
*/
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
export type JsonRecord = Record<string, unknown>;
|
||||
|
||||
export type ExtensionConfig = {
|
||||
enabled?: boolean;
|
||||
notify?: boolean;
|
||||
};
|
||||
|
||||
export function isRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readJsonFile(path: string): JsonRecord | undefined {
|
||||
if (!existsSync(path)) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
||||
return isRecord(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function toBoolean(value: unknown): boolean | undefined {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value !== 0;
|
||||
if (typeof value !== "string") return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
||||
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function loadConfig(cwd: string): Required<ExtensionConfig> {
|
||||
const globalPath = join(homedir(), ".pi", "agent", "openai-server-compaction.json");
|
||||
const projectPath = join(cwd, ".pi", "openai-server-compaction.json");
|
||||
const globalCfg = readJsonFile(globalPath) ?? {};
|
||||
const projectCfg = readJsonFile(projectPath) ?? {};
|
||||
const merged = { ...globalCfg, ...projectCfg };
|
||||
|
||||
return {
|
||||
enabled:
|
||||
toBoolean(process.env.PI_OPENAI_SERVER_COMPACTION_ENABLED) ??
|
||||
toBoolean(merged.enabled) ??
|
||||
true,
|
||||
notify:
|
||||
toBoolean(process.env.PI_OPENAI_SERVER_COMPACTION_NOTIFY) ??
|
||||
toBoolean(merged.notify) ??
|
||||
false,
|
||||
};
|
||||
}
|
||||
348
pi/extensions/openai-server-compaction/index.ts
Normal file
348
pi/extensions/openai-server-compaction/index.ts
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
/**
|
||||
* Main extension entrypoint.
|
||||
*
|
||||
* Wires together request patching, remote compaction, runtime state
|
||||
* reconstruction, session lifecycle cleanup, and Pi-native HTTP request patching.
|
||||
*/
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import { isRecord, loadConfig } from "./config.ts";
|
||||
import {
|
||||
applyRemoteHistoryPayloadPatch,
|
||||
extractResponsesReasoningConfig,
|
||||
extractResponsesTextConfig,
|
||||
isOpenAICodexResponsesModel,
|
||||
looksLikeResponsesPayload,
|
||||
messageMatchesModel,
|
||||
modelKey,
|
||||
supportsRemoteCompactionModel,
|
||||
thinkingLevelToResponsesReasoning,
|
||||
} from "./openai.ts";
|
||||
import {
|
||||
buildCompactionSummaryText,
|
||||
buildRemoteCompactionDetails,
|
||||
buildToolsPayload,
|
||||
callRemoteCompactionEndpoint,
|
||||
generateBestEffortLocalSummary,
|
||||
messageToResponseItems,
|
||||
messagesToResponseItems,
|
||||
normalizeResponseItemsForPrompt,
|
||||
reconstructRemoteCompactionStateFromBranch,
|
||||
} from "./remote-compaction.ts";
|
||||
import {
|
||||
clearAllRuntimeState,
|
||||
clearRemoteCompactionState,
|
||||
clearResponsesRequestShapeState,
|
||||
getRemoteCompactionState,
|
||||
getResponsesRequestShapeState,
|
||||
setRemoteCompactionState,
|
||||
setResponsesRequestShapeState,
|
||||
} from "./state.ts";
|
||||
|
||||
type TargetModel = Parameters<typeof modelKey>[0];
|
||||
|
||||
type BranchEntry = {
|
||||
type: string;
|
||||
id: string;
|
||||
details?: unknown;
|
||||
message?: unknown;
|
||||
thinkingLevel?: unknown;
|
||||
};
|
||||
|
||||
type SessionContextLike = {
|
||||
sessionManager: {
|
||||
getSessionId(): string;
|
||||
getBranch(): BranchEntry[];
|
||||
};
|
||||
};
|
||||
|
||||
function getSessionId(ctx: SessionContextLike): string {
|
||||
return ctx.sessionManager.getSessionId();
|
||||
}
|
||||
|
||||
function getBranchMessages(branchEntries: BranchEntry[]): AgentMessage[] {
|
||||
return branchEntries.flatMap((entry) =>
|
||||
entry.type === "message" && entry.message ? [entry.message as AgentMessage] : [],
|
||||
);
|
||||
}
|
||||
|
||||
function getBranchThinkingLevel(branchEntries: BranchEntry[]): string | undefined {
|
||||
for (let index = branchEntries.length - 1; index >= 0; index--) {
|
||||
const entry = branchEntries[index];
|
||||
if (entry?.type !== "thinking_level_change") continue;
|
||||
return typeof entry.thinkingLevel === "string" ? entry.thinkingLevel : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function clearSessionRuntimeState(sessionId: string | undefined): void {
|
||||
clearRemoteCompactionState(sessionId);
|
||||
clearResponsesRequestShapeState(sessionId);
|
||||
}
|
||||
|
||||
function syncRemoteState(ctx: SessionContextLike): void {
|
||||
const sessionId = getSessionId(ctx);
|
||||
const branchEntries = ctx.sessionManager.getBranch() as Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
details?: unknown;
|
||||
message?: AgentMessage;
|
||||
}>;
|
||||
const state = reconstructRemoteCompactionStateFromBranch({ branchEntries });
|
||||
if (state) {
|
||||
setRemoteCompactionState(sessionId, state);
|
||||
} else {
|
||||
clearRemoteCompactionState(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
function getMatchingRemoteState(
|
||||
sessionId: string,
|
||||
model: TargetModel | undefined,
|
||||
): ReturnType<typeof getRemoteCompactionState> {
|
||||
if (!model) return undefined;
|
||||
const remoteState = getRemoteCompactionState(sessionId);
|
||||
return remoteState && remoteState.modelKey === modelKey(model) ? remoteState : undefined;
|
||||
}
|
||||
|
||||
function extendRemoteHistoryIfCompatible(params: {
|
||||
sessionId: string;
|
||||
model: TargetModel | undefined;
|
||||
message: AgentMessage;
|
||||
}): void {
|
||||
const remoteState = getMatchingRemoteState(params.sessionId, params.model);
|
||||
if (!remoteState || !params.model) return;
|
||||
if (params.message.role === "assistant" && !messageMatchesModel(params.message, params.model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = messageToResponseItems(params.message);
|
||||
if (items.length === 0) return;
|
||||
|
||||
setRemoteCompactionState(params.sessionId, {
|
||||
...remoteState,
|
||||
explicitHistory: [...remoteState.explicitHistory, ...items],
|
||||
});
|
||||
}
|
||||
|
||||
function maybeNotifyRequestFeatures(params: {
|
||||
notifiedModels: Set<string>;
|
||||
hasUI: boolean;
|
||||
notify: boolean;
|
||||
ui: { notify(message: string, level: "info" | "warning"): void };
|
||||
model: TargetModel;
|
||||
features: string[];
|
||||
}): void {
|
||||
if (!params.notify || !params.hasUI || params.features.length === 0) return;
|
||||
|
||||
const key = `${String(params.model.provider)}/${String(params.model.id)}`;
|
||||
const noticeKey = `${key}:${params.features.join(",")}`;
|
||||
if (params.notifiedModels.has(noticeKey)) return;
|
||||
|
||||
params.notifiedModels.add(noticeKey);
|
||||
params.ui.notify(`OpenAI compaction active for ${key} (${params.features.join(", ")})`, "info");
|
||||
}
|
||||
|
||||
export default function openaiServerCompactionExtension(pi: ExtensionAPI) {
|
||||
const notifiedModels = new Set<string>();
|
||||
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
const sessionId = getSessionId(ctx);
|
||||
clearResponsesRequestShapeState(sessionId);
|
||||
syncRemoteState(ctx);
|
||||
});
|
||||
|
||||
const clearBeforeSessionChange = (_event: unknown, ctx: SessionContextLike): void => {
|
||||
clearSessionRuntimeState(getSessionId(ctx));
|
||||
};
|
||||
pi.on("session_before_switch", clearBeforeSessionChange);
|
||||
pi.on("session_before_fork", clearBeforeSessionChange);
|
||||
pi.on("session_before_tree", clearBeforeSessionChange);
|
||||
|
||||
const syncAfterSessionChange = (_event: unknown, ctx: SessionContextLike): void => {
|
||||
syncRemoteState(ctx);
|
||||
};
|
||||
pi.on("session_tree", syncAfterSessionChange);
|
||||
pi.on("session_compact", syncAfterSessionChange);
|
||||
|
||||
pi.on("model_select", (_event, ctx) => {
|
||||
clearResponsesRequestShapeState(getSessionId(ctx));
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", () => {
|
||||
clearAllRuntimeState();
|
||||
});
|
||||
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
const cfg = loadConfig(ctx.cwd);
|
||||
const model = ctx.model;
|
||||
if (!cfg.enabled || !model || !supportsRemoteCompactionModel(model)) return undefined;
|
||||
|
||||
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
||||
if (!auth.ok || !auth.apiKey) return undefined;
|
||||
|
||||
const tools = buildToolsPayload(pi.getAllTools(), pi.getActiveTools());
|
||||
const sessionId = getSessionId(ctx);
|
||||
const branchEntries = event.branchEntries as BranchEntry[];
|
||||
const remoteState = getMatchingRemoteState(sessionId, model);
|
||||
const observedRequestShape = getResponsesRequestShapeState(sessionId);
|
||||
const fullBranchMessages = getBranchMessages(branchEntries);
|
||||
const responseItems = remoteState
|
||||
? remoteState.explicitHistory
|
||||
: messagesToResponseItems(fullBranchMessages);
|
||||
const promptResponseItems = normalizeResponseItemsForPrompt(responseItems, model);
|
||||
const thinkingLevel = pi.getThinkingLevel();
|
||||
const fallbackReasoning = model.reasoning
|
||||
? thinkingLevelToResponsesReasoning(thinkingLevel ?? getBranchThinkingLevel(branchEntries))
|
||||
: undefined;
|
||||
const reasoning = observedRequestShape?.reasoning ?? fallbackReasoning;
|
||||
const text = observedRequestShape?.text;
|
||||
|
||||
const [localResult, remoteResult] = await Promise.allSettled([
|
||||
generateBestEffortLocalSummary({
|
||||
preparation: event.preparation,
|
||||
messages: fullBranchMessages,
|
||||
model,
|
||||
apiKey: auth.apiKey,
|
||||
headers: auth.headers,
|
||||
customInstructions: event.customInstructions,
|
||||
signal: event.signal,
|
||||
thinkingLevel,
|
||||
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
||||
tokensBefore: event.preparation.tokensBefore,
|
||||
}),
|
||||
callRemoteCompactionEndpoint({
|
||||
model,
|
||||
apiKey: auth.apiKey,
|
||||
headers: auth.headers,
|
||||
sessionId,
|
||||
input: promptResponseItems,
|
||||
instructions: ctx.getSystemPrompt(),
|
||||
tools,
|
||||
parallelToolCalls: true,
|
||||
reasoning,
|
||||
text,
|
||||
signal: event.signal,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (remoteResult.status !== "fulfilled") {
|
||||
if (localResult.status === "fulfilled") {
|
||||
return { compaction: localResult.value };
|
||||
}
|
||||
if (!event.signal.aborted && ctx.hasUI) {
|
||||
const message =
|
||||
remoteResult.reason instanceof Error
|
||||
? remoteResult.reason.message
|
||||
: String(remoteResult.reason);
|
||||
ctx.ui.notify(
|
||||
`OpenAI remote compaction failed; falling back to default compaction. ${message}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const remoteDetails = buildRemoteCompactionDetails(
|
||||
model,
|
||||
remoteResult.value.output,
|
||||
remoteResult.value.usage,
|
||||
);
|
||||
const localSummary =
|
||||
localResult.status === "fulfilled"
|
||||
? localResult.value
|
||||
: {
|
||||
summary: buildCompactionSummaryText(model),
|
||||
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
||||
tokensBefore: event.preparation.tokensBefore,
|
||||
};
|
||||
|
||||
return {
|
||||
compaction: {
|
||||
summary: localSummary.summary,
|
||||
firstKeptEntryId: localSummary.firstKeptEntryId,
|
||||
tokensBefore: localSummary.tokensBefore,
|
||||
details: {
|
||||
...(localSummary.details !== undefined
|
||||
? { localSummaryDetails: localSummary.details }
|
||||
: {}),
|
||||
remoteCompaction: remoteDetails,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
pi.on("message_end", (event, ctx) => {
|
||||
const sessionId = getSessionId(ctx);
|
||||
const model = ctx.model;
|
||||
|
||||
extendRemoteHistoryIfCompatible({
|
||||
sessionId,
|
||||
model,
|
||||
message: event.message,
|
||||
});
|
||||
});
|
||||
|
||||
pi.on("before_provider_request", (event, ctx) => {
|
||||
const cfg = loadConfig(ctx.cwd);
|
||||
if (!cfg.enabled) return undefined;
|
||||
|
||||
const model = ctx.model;
|
||||
if (
|
||||
!model ||
|
||||
!supportsRemoteCompactionModel(model) ||
|
||||
!isRecord(event.payload) ||
|
||||
!looksLikeResponsesPayload(event.payload)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sessionId = getSessionId(ctx);
|
||||
setResponsesRequestShapeState(sessionId, {
|
||||
updatedAt: Date.now(),
|
||||
reasoning: extractResponsesReasoningConfig(event.payload),
|
||||
text: extractResponsesTextConfig(event.payload),
|
||||
});
|
||||
const remoteState = getMatchingRemoteState(sessionId, model);
|
||||
|
||||
if (isOpenAICodexResponsesModel(model)) {
|
||||
if (!remoteState) return undefined;
|
||||
const payload = applyRemoteHistoryPayloadPatch({
|
||||
payload: event.payload,
|
||||
explicitHistory: normalizeResponseItemsForPrompt(
|
||||
remoteState.explicitHistory,
|
||||
model,
|
||||
) as unknown[],
|
||||
});
|
||||
maybeNotifyRequestFeatures({
|
||||
notifiedModels,
|
||||
hasUI: ctx.hasUI,
|
||||
notify: cfg.notify,
|
||||
ui: ctx.ui,
|
||||
model,
|
||||
features: ["remote_compaction_history"],
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (!remoteState) return undefined;
|
||||
|
||||
const payload = applyRemoteHistoryPayloadPatch({
|
||||
payload: event.payload,
|
||||
explicitHistory: normalizeResponseItemsForPrompt(
|
||||
remoteState.explicitHistory,
|
||||
model,
|
||||
) as unknown[],
|
||||
});
|
||||
maybeNotifyRequestFeatures({
|
||||
notifiedModels,
|
||||
hasUI: ctx.hasUI,
|
||||
notify: cfg.notify,
|
||||
ui: ctx.ui,
|
||||
model,
|
||||
features: ["remote_compaction_history", "native_http"],
|
||||
});
|
||||
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
117
pi/extensions/openai-server-compaction/openai.ts
Normal file
117
pi/extensions/openai-server-compaction/openai.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* OpenAI API and OpenAI Codex model/payload helpers.
|
||||
*
|
||||
* Keeps provider-specific detection, request patching, endpoint classification,
|
||||
* and model-key logic out of the higher-level extension wiring.
|
||||
*/
|
||||
import type { JsonRecord } from "./config.ts";
|
||||
import type { ResponsesReasoningConfig, ResponsesTextConfig } from "./remote-compaction.ts";
|
||||
import { isRecord } from "./config.ts";
|
||||
|
||||
export type ModelLike = {
|
||||
api?: unknown;
|
||||
provider?: unknown;
|
||||
id?: unknown;
|
||||
baseUrl?: unknown;
|
||||
reasoning?: unknown;
|
||||
input?: readonly unknown[];
|
||||
};
|
||||
|
||||
export function hostnameFromBaseUrl(baseUrl: unknown): string | undefined {
|
||||
if (typeof baseUrl !== "string" || !baseUrl.trim()) return undefined;
|
||||
try {
|
||||
return new URL(baseUrl).hostname.toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function isOpenAIResponsesModel(model: unknown): model is ModelLike {
|
||||
return (
|
||||
isRecord(model) &&
|
||||
(
|
||||
model.api === "openai-responses" ||
|
||||
model.api === "openai-codex-responses"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isDirectOpenAIResponsesModel(model: ModelLike): boolean {
|
||||
if (model.api !== "openai-responses") return false;
|
||||
if (model.provider !== "openai") return false;
|
||||
const host = hostnameFromBaseUrl(model.baseUrl);
|
||||
return host === undefined || host === "api.openai.com";
|
||||
}
|
||||
|
||||
export function isOpenAICodexResponsesModel(model: ModelLike): boolean {
|
||||
if (model.api !== "openai-codex-responses") return false;
|
||||
const provider = typeof model.provider === "string" ? model.provider : "";
|
||||
if (provider === "openai-codex") return true;
|
||||
const host = hostnameFromBaseUrl(model.baseUrl);
|
||||
return host === "chatgpt.com";
|
||||
}
|
||||
|
||||
function isGptModel(model: ModelLike): boolean {
|
||||
return typeof model.id === "string" && model.id.startsWith("gpt-");
|
||||
}
|
||||
|
||||
export function supportsRemoteCompactionModel(model: unknown): model is ModelLike {
|
||||
if (!isOpenAIResponsesModel(model) || !isGptModel(model)) return false;
|
||||
return isDirectOpenAIResponsesModel(model) || isOpenAICodexResponsesModel(model);
|
||||
}
|
||||
|
||||
export function looksLikeResponsesPayload(payload: JsonRecord): boolean {
|
||||
return "input" in payload || "model" in payload || "messages" in payload;
|
||||
}
|
||||
|
||||
export function modelKey(model: ModelLike): string {
|
||||
return `${String(model.provider)}:${String(model.api)}:${String(model.id)}`;
|
||||
}
|
||||
|
||||
export function thinkingLevelToResponsesReasoning(
|
||||
thinkingLevel: unknown,
|
||||
): ResponsesReasoningConfig | undefined {
|
||||
if (thinkingLevel === "minimal") return { effort: "minimal", summary: "auto" };
|
||||
if (thinkingLevel === "low") return { effort: "low", summary: "auto" };
|
||||
if (thinkingLevel === "medium") return { effort: "medium", summary: "auto" };
|
||||
if (thinkingLevel === "high") return { effort: "high", summary: "auto" };
|
||||
if (thinkingLevel === "xhigh") return { effort: "xhigh", summary: "auto" };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function applyRemoteHistoryPayloadPatch(params: {
|
||||
payload: JsonRecord;
|
||||
explicitHistory: unknown[];
|
||||
}): JsonRecord {
|
||||
const nextPayload: JsonRecord = {
|
||||
...params.payload,
|
||||
input: params.explicitHistory,
|
||||
};
|
||||
delete nextPayload.messages;
|
||||
delete nextPayload.previous_response_id;
|
||||
return nextPayload;
|
||||
}
|
||||
|
||||
export function extractResponsesReasoningConfig(payload: unknown): ResponsesReasoningConfig | undefined {
|
||||
if (!isRecord(payload) || !isRecord(payload.reasoning)) return undefined;
|
||||
const effort = payload.reasoning.effort;
|
||||
const summary = payload.reasoning.summary;
|
||||
const normalized: ResponsesReasoningConfig = {
|
||||
...(typeof effort === "string" ? { effort: effort as ResponsesReasoningConfig["effort"] } : {}),
|
||||
...(
|
||||
summary === null || typeof summary === "string"
|
||||
? { summary: summary as ResponsesReasoningConfig["summary"] }
|
||||
: {}
|
||||
),
|
||||
};
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function extractResponsesTextConfig(payload: unknown): ResponsesTextConfig | undefined {
|
||||
return isRecord(payload) && isRecord(payload.text) ? payload.text : undefined;
|
||||
}
|
||||
|
||||
export function messageMatchesModel(message: unknown, model: ModelLike): boolean {
|
||||
if (!isRecord(message)) return false;
|
||||
return message.provider === model.provider && message.model === model.id;
|
||||
}
|
||||
1070
pi/extensions/openai-server-compaction/remote-compaction.ts
Normal file
1070
pi/extensions/openai-server-compaction/remote-compaction.ts
Normal file
File diff suppressed because it is too large
Load diff
62
pi/extensions/openai-server-compaction/state.ts
Normal file
62
pi/extensions/openai-server-compaction/state.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* In-memory per-session runtime state.
|
||||
*
|
||||
* This data is intentionally ephemeral. Persisted remote compaction artifacts
|
||||
* live in Pi session entries; this module only caches the currently active
|
||||
* continuation and reconstructed replay state for the running process.
|
||||
*/
|
||||
import type {
|
||||
RemoteCompactionSessionState,
|
||||
ResponsesReasoningConfig,
|
||||
ResponsesTextConfig,
|
||||
} from "./remote-compaction.ts";
|
||||
|
||||
export type ResponsesRequestShapeState = {
|
||||
updatedAt: number;
|
||||
reasoning?: ResponsesReasoningConfig;
|
||||
text?: ResponsesTextConfig;
|
||||
};
|
||||
|
||||
const remoteCompactionBySessionId = new Map<string, RemoteCompactionSessionState>();
|
||||
const requestShapeBySessionId = new Map<string, ResponsesRequestShapeState>();
|
||||
|
||||
export function getRemoteCompactionState(
|
||||
sessionId: string,
|
||||
): RemoteCompactionSessionState | undefined {
|
||||
return remoteCompactionBySessionId.get(sessionId);
|
||||
}
|
||||
|
||||
export function setRemoteCompactionState(
|
||||
sessionId: string,
|
||||
state: RemoteCompactionSessionState,
|
||||
): void {
|
||||
remoteCompactionBySessionId.set(sessionId, state);
|
||||
}
|
||||
|
||||
export function clearRemoteCompactionState(sessionId: string | undefined): void {
|
||||
if (!sessionId) return;
|
||||
remoteCompactionBySessionId.delete(sessionId);
|
||||
}
|
||||
|
||||
export function getResponsesRequestShapeState(
|
||||
sessionId: string,
|
||||
): ResponsesRequestShapeState | undefined {
|
||||
return requestShapeBySessionId.get(sessionId);
|
||||
}
|
||||
|
||||
export function setResponsesRequestShapeState(
|
||||
sessionId: string,
|
||||
state: ResponsesRequestShapeState,
|
||||
): void {
|
||||
requestShapeBySessionId.set(sessionId, state);
|
||||
}
|
||||
|
||||
export function clearResponsesRequestShapeState(sessionId: string | undefined): void {
|
||||
if (!sessionId) return;
|
||||
requestShapeBySessionId.delete(sessionId);
|
||||
}
|
||||
|
||||
export function clearAllRuntimeState(): void {
|
||||
remoteCompactionBySessionId.clear();
|
||||
requestShapeBySessionId.clear();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue