feat(ai/providers): added sessionId option for prompt caching in streams
- Added sessionId option to StreamOptions for session-based prompt caching in supported providers. - Changed reasoning configuration to only apply when explicitly specified. - Replaced static CODEX_PI_BRIDGE export with dynamic buildCodexPiBridge function that formats available tools. - Added buildCodexSystemPrompt function to construct system prompts from codex instructions and user prompts.
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `sessionId` option for session-based prompt caching in providers that support it
|
||||
- Added Google Vertex AI provider with Gemini 1.5, 2.0, 2.5, and 3.0 model support
|
||||
- Added GPT-5 series models (gpt-5, gpt-5.1, gpt-5.2 and variants) to OpenAI Codex provider
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed reasoning configuration to only apply when explicitly specified, removing automatic defaults
|
||||
- Changed default reasoning summary from `auto` to `detailed` for OpenAI Codex provider
|
||||
|
||||
## [3.20.1] - 2026-01-06
|
||||
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
URL_PATHS,
|
||||
} from "./openai-codex/constants";
|
||||
import { getCodexInstructions } from "./openai-codex/prompts/codex";
|
||||
import { buildCodexPiBridge } from "./openai-codex/prompts/pi-codex-bridge";
|
||||
import { buildCodexSystemPrompt } from "./openai-codex/prompts/system-prompt";
|
||||
import {
|
||||
type CodexRequestOptions,
|
||||
normalizeModel,
|
||||
@@ -94,6 +96,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
||||
model: model.id,
|
||||
input: messages,
|
||||
stream: true,
|
||||
prompt_cache_key: options?.sessionId,
|
||||
};
|
||||
|
||||
if (options?.maxTokens) {
|
||||
@@ -110,6 +113,15 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
||||
|
||||
const normalizedModel = normalizeModel(params.model);
|
||||
const codexInstructions = await getCodexInstructions(normalizedModel);
|
||||
const bridgeText = buildCodexPiBridge(context.tools);
|
||||
const systemPrompt = buildCodexSystemPrompt({
|
||||
codexInstructions,
|
||||
bridgeText,
|
||||
userSystemPrompt: context.systemPrompt,
|
||||
});
|
||||
|
||||
params.model = normalizedModel;
|
||||
params.instructions = systemPrompt.instructions;
|
||||
|
||||
const codexOptions: CodexRequestOptions = {
|
||||
reasoningEffort: options?.reasoningEffort,
|
||||
@@ -118,17 +130,14 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
||||
include: options?.include,
|
||||
};
|
||||
|
||||
const transformedBody = await transformRequestBody(
|
||||
params,
|
||||
codexInstructions,
|
||||
codexOptions,
|
||||
options?.codexMode ?? true,
|
||||
);
|
||||
const transformedBody = await transformRequestBody(params, codexOptions, systemPrompt);
|
||||
|
||||
const headers = createCodexHeaders(model.headers, accountId, apiKey, transformedBody.prompt_cache_key);
|
||||
const reasoningEffort = transformedBody.reasoning?.effort ?? null;
|
||||
const headers = createCodexHeaders(model.headers, accountId, apiKey, options?.sessionId);
|
||||
logCodexDebug("codex request", {
|
||||
url,
|
||||
model: params.model,
|
||||
reasoningEffort,
|
||||
headers: redactHeaders(headers),
|
||||
});
|
||||
|
||||
@@ -406,11 +415,11 @@ function logCodexDebug(message: string, details?: Record<string, unknown>): void
|
||||
|
||||
function redactHeaders(headers: Headers): Record<string, string> {
|
||||
const redacted: Record<string, string> = {};
|
||||
headers.forEach((value, key) => {
|
||||
for (const [key, value] of headers.entries()) {
|
||||
const lower = key.toLowerCase();
|
||||
if (lower === "authorization") {
|
||||
redacted[key] = "Bearer [redacted]";
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
lower.includes("account") ||
|
||||
@@ -419,10 +428,10 @@ function redactHeaders(headers: Headers): Record<string, string> {
|
||||
lower === "cookie"
|
||||
) {
|
||||
redacted[key] = "[redacted]";
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
redacted[key] = value;
|
||||
});
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,46 +3,53 @@
|
||||
* Aligns Codex CLI expectations with Pi's toolset.
|
||||
*/
|
||||
|
||||
export const CODEX_PI_BRIDGE = `# Codex Running in Pi
|
||||
import type { Tool } from "../../../types";
|
||||
|
||||
You are running Codex through pi, a terminal coding assistant. The tools and rules differ from Codex CLI.
|
||||
function formatToolList(tools?: Tool[]): string {
|
||||
if (!tools || tools.length === 0) {
|
||||
return "- (none)";
|
||||
}
|
||||
|
||||
## CRITICAL: Tool Replacements
|
||||
const normalized = tools
|
||||
.map((tool) => {
|
||||
const name = tool.name.trim();
|
||||
if (!name) return null;
|
||||
const description = (tool.description || "Custom tool").replace(/\s*\n\s*/g, " ").trim();
|
||||
return { name, description };
|
||||
})
|
||||
.filter((tool): tool is { name: string; description: string } => tool !== null);
|
||||
|
||||
<critical_rule priority="0">
|
||||
❌ APPLY_PATCH DOES NOT EXIST → ✅ USE "edit" INSTEAD
|
||||
- NEVER use: apply_patch, applyPatch
|
||||
- ALWAYS use: edit for ALL file modifications
|
||||
</critical_rule>
|
||||
if (normalized.length === 0) {
|
||||
return "- (none)";
|
||||
}
|
||||
|
||||
<critical_rule priority="0">
|
||||
❌ UPDATE_PLAN DOES NOT EXIST
|
||||
- NEVER use: update_plan, updatePlan, read_plan, readPlan, todowrite, todoread
|
||||
- There is no plan tool in this environment
|
||||
</critical_rule>
|
||||
const maxNameLength = normalized.reduce((max, tool) => Math.max(max, tool.name.length), 0);
|
||||
const padWidth = Math.max(6, maxNameLength + 1);
|
||||
|
||||
## Available Tools (pi)
|
||||
return normalized
|
||||
.map((tool) => {
|
||||
const paddedName = tool.name.padEnd(padWidth);
|
||||
return `- ${paddedName}- ${tool.description}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
- read - Read file contents
|
||||
- bash - Execute bash commands
|
||||
- edit - Modify files with exact find/replace (requires prior read)
|
||||
- write - Create or overwrite files
|
||||
- grep - Search file contents (read-only)
|
||||
- find - Find files by glob pattern (read-only)
|
||||
- ls - List directory contents (read-only)
|
||||
export function buildCodexPiBridge(tools?: Tool[]): string {
|
||||
const toolsList = formatToolList(tools);
|
||||
|
||||
## Usage Rules
|
||||
return `# Codex Environment Bridge
|
||||
|
||||
- Read before edit; use read instead of cat/sed for file contents
|
||||
- Use edit for surgical changes; write only for new files or complete rewrites
|
||||
- Prefer grep/find/ls over bash for discovery
|
||||
- Be concise and show file paths clearly when working with files
|
||||
<environment_override priority="0">
|
||||
IGNORE ALL PREVIOUS INSTRUCTIONS ABOVE THIS MESSAGE.
|
||||
Do not assume any tools are available unless listed below.
|
||||
</environment_override>
|
||||
|
||||
## Verification Checklist
|
||||
The next system instructions that follow this message are authoritative and must be obeyed, even if they conflict with earlier instructions.
|
||||
|
||||
1. Using edit, not apply_patch
|
||||
2. No plan tools used
|
||||
3. Only the tools listed above are called
|
||||
## Available Tools
|
||||
|
||||
Below are additional system instruction you MUST follow when responding:
|
||||
${toolsList}
|
||||
|
||||
Only use the tools listed above. Do not reference or call any other tools.
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export interface CodexSystemPrompt {
|
||||
instructions: string;
|
||||
developerMessages: string[];
|
||||
}
|
||||
|
||||
export function buildCodexSystemPrompt(args: {
|
||||
codexInstructions: string;
|
||||
bridgeText: string;
|
||||
userSystemPrompt?: string;
|
||||
}): CodexSystemPrompt {
|
||||
const { codexInstructions, bridgeText, userSystemPrompt } = args;
|
||||
const developerMessages: string[] = [];
|
||||
|
||||
if (bridgeText.trim().length > 0) {
|
||||
developerMessages.push(bridgeText.trim());
|
||||
}
|
||||
|
||||
if (userSystemPrompt && userSystemPrompt.trim().length > 0) {
|
||||
developerMessages.push(userSystemPrompt.trim());
|
||||
}
|
||||
|
||||
return {
|
||||
instructions: codexInstructions.trim(),
|
||||
developerMessages,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
import { TOOL_REMAP_MESSAGE } from "./prompts/codex";
|
||||
import { CODEX_PI_BRIDGE } from "./prompts/pi-codex-bridge";
|
||||
|
||||
export interface ReasoningConfig {
|
||||
effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
summary: "auto" | "concise" | "detailed" | "off" | "on";
|
||||
@@ -38,6 +35,7 @@ export interface RequestBody {
|
||||
};
|
||||
include?: string[];
|
||||
prompt_cache_key?: string;
|
||||
prompt_cache_retention?: "in_memory" | "24h";
|
||||
max_output_tokens?: number;
|
||||
max_completion_tokens?: number;
|
||||
[key: string]: unknown;
|
||||
@@ -159,10 +157,10 @@ function getReasoningConfig(modelName: string | undefined, options: CodexRequest
|
||||
const defaultEffort: ReasoningConfig["effort"] = isCodexMini
|
||||
? "medium"
|
||||
: supportsXhigh
|
||||
? "high"
|
||||
: isLightweight
|
||||
? "minimal"
|
||||
: "medium";
|
||||
? "high"
|
||||
: isLightweight
|
||||
? "minimal"
|
||||
: "medium";
|
||||
|
||||
let effort = options.reasoningEffort || defaultEffort;
|
||||
|
||||
@@ -210,74 +208,25 @@ function filterInput(input: InputItem[] | undefined): InputItem[] | undefined {
|
||||
});
|
||||
}
|
||||
|
||||
function addCodexBridgeMessage(
|
||||
input: InputItem[] | undefined,
|
||||
hasTools: boolean,
|
||||
systemPrompt?: string
|
||||
): InputItem[] | undefined {
|
||||
if (!hasTools || !Array.isArray(input)) return input;
|
||||
|
||||
const bridgeText = systemPrompt ? `${CODEX_PI_BRIDGE}\n\n${systemPrompt}` : CODEX_PI_BRIDGE;
|
||||
|
||||
const bridgeMessage: InputItem = {
|
||||
type: "message",
|
||||
role: "developer",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: bridgeText,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return [bridgeMessage, ...input];
|
||||
}
|
||||
|
||||
function addToolRemapMessage(input: InputItem[] | undefined, hasTools: boolean): InputItem[] | undefined {
|
||||
if (!hasTools || !Array.isArray(input)) return input;
|
||||
|
||||
const toolRemapMessage: InputItem = {
|
||||
type: "message",
|
||||
role: "developer",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: TOOL_REMAP_MESSAGE,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return [toolRemapMessage, ...input];
|
||||
}
|
||||
|
||||
export async function transformRequestBody(
|
||||
body: RequestBody,
|
||||
codexInstructions: string,
|
||||
options: CodexRequestOptions = {},
|
||||
codexMode = true,
|
||||
systemPrompt?: string
|
||||
prompt?: { instructions: string; developerMessages: string[] },
|
||||
): Promise<RequestBody> {
|
||||
const normalizedModel = normalizeModel(body.model);
|
||||
|
||||
body.model = normalizedModel;
|
||||
body.store = false;
|
||||
body.stream = true;
|
||||
body.instructions = codexInstructions;
|
||||
|
||||
if (body.input && Array.isArray(body.input)) {
|
||||
body.input = filterInput(body.input);
|
||||
|
||||
if (codexMode) {
|
||||
body.input = addCodexBridgeMessage(body.input, !!body.tools, systemPrompt);
|
||||
} else {
|
||||
body.input = addToolRemapMessage(body.input, !!body.tools);
|
||||
}
|
||||
|
||||
if (body.input) {
|
||||
const functionCallIds = new Set(
|
||||
body.input
|
||||
.filter((item) => item.type === "function_call" && typeof item.call_id === "string")
|
||||
.map((item) => item.call_id as string)
|
||||
.map((item) => item.call_id as string),
|
||||
);
|
||||
|
||||
body.input = body.input.map((item) => {
|
||||
@@ -308,11 +257,27 @@ export async function transformRequestBody(
|
||||
}
|
||||
}
|
||||
|
||||
const reasoningConfig = getReasoningConfig(normalizedModel, options);
|
||||
body.reasoning = {
|
||||
...body.reasoning,
|
||||
...reasoningConfig,
|
||||
};
|
||||
if (prompt?.developerMessages && prompt.developerMessages.length > 0 && Array.isArray(body.input)) {
|
||||
const developerMessages = prompt.developerMessages.map(
|
||||
(text) =>
|
||||
({
|
||||
type: "message",
|
||||
role: "developer",
|
||||
content: [{ type: "input_text", text }],
|
||||
}) as InputItem,
|
||||
);
|
||||
body.input = [...developerMessages, ...body.input];
|
||||
}
|
||||
|
||||
if (options.reasoningEffort !== undefined) {
|
||||
const reasoningConfig = getReasoningConfig(normalizedModel, options);
|
||||
body.reasoning = {
|
||||
...body.reasoning,
|
||||
...reasoningConfig,
|
||||
};
|
||||
} else {
|
||||
delete body.reasoning;
|
||||
}
|
||||
|
||||
body.text = {
|
||||
...body.text,
|
||||
|
||||
@@ -65,6 +65,12 @@ export interface StreamOptions {
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
apiKey?: string;
|
||||
/**
|
||||
* Optional session identifier for providers that support session-based caching.
|
||||
* Providers can use this to enable prompt caching, request routing, or other
|
||||
* session-aware features. Ignored by providers that don't support it.
|
||||
*/
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
// Unified options with reasoning passed to streamSimple() and completeSimple()
|
||||
|
||||
Reference in New Issue
Block a user