From 4f834ca2a44c4a94619849acb34267cb59620118 Mon Sep 17 00:00:00 2001 From: Bryan Font Date: Tue, 17 Feb 2026 22:11:49 -0500 Subject: [PATCH 1/3] perf: reduce request and auth hot-path overhead --- lib/codex-native/acquire-auth.ts | 11 +- lib/codex-native/openai-loader-fetch.ts | 189 +++++---- .../request-transform-pipeline.ts | 27 +- lib/codex-native/request-transform.ts | 391 +++++++++++++++++- lib/codex-status-storage.ts | 3 + lib/storage.ts | 4 + scripts/perf-profile.ts | 341 +++++++++++++++ test/acquire-auth-locking.test.ts | 82 ++++ test/codex-native-request-transform.test.ts | 163 +++++++- test/codex-status-storage.test.ts | 19 + ...enai-loader-fetch.prompt-cache-key.test.ts | 230 ++++++++++- 11 files changed, 1330 insertions(+), 130 deletions(-) create mode 100644 scripts/perf-profile.ts diff --git a/lib/codex-native/acquire-auth.ts b/lib/codex-native/acquire-auth.ts index caab2e1..d95b2de 100644 --- a/lib/codex-native/acquire-auth.ts +++ b/lib/codex-native/acquire-auth.ts @@ -11,6 +11,7 @@ import { extractAccountId, refreshAccessToken, type OAuthTokenRefreshError } fro const AUTH_REFRESH_FAILURE_COOLDOWN_MS = 30_000 const AUTH_REFRESH_LEASE_MS = 30_000 +const LAST_USED_WRITE_INTERVAL_MS = 5_000 function isOAuthTokenRefreshError(value: unknown): value is OAuthTokenRefreshError { return value instanceof Error && ("status" in value || "oauthCode" in value) @@ -210,7 +211,10 @@ export async function acquireOpenAIAuth(input: AcquireOpenAIAuthInput): Promise< plan = selected.plan if (selected.access && selected.expires && selected.expires > now) { - selected.lastUsed = now + const previousLastUsed = typeof selected.lastUsed === "number" ? selected.lastUsed : undefined + if (previousLastUsed === undefined || now - previousLastUsed >= LAST_USED_WRITE_INTERVAL_MS) { + selected.lastUsed = now + } access = selected.access accountId = selected.accountId identityKey = selected.identityKey @@ -283,7 +287,10 @@ export async function acquireOpenAIAuth(input: AcquireOpenAIAuthInput): Promise< if (claims?.email) selected.email = normalizeEmail(claims.email) if (claims?.plan) selected.plan = normalizePlan(claims.plan) ensureIdentityKey(selected) - selected.lastUsed = now + const previousLastUsed = typeof selected.lastUsed === "number" ? selected.lastUsed : undefined + if (previousLastUsed === undefined || now - previousLastUsed >= LAST_USED_WRITE_INTERVAL_MS) { + selected.lastUsed = now + } delete selected.refreshLeaseUntil delete selected.cooldownUntil if (selected.identityKey) domain.activeIdentityKey = selected.identityKey diff --git a/lib/codex-native/openai-loader-fetch.ts b/lib/codex-native/openai-loader-fetch.ts index 91d621e..26f3a5b 100644 --- a/lib/codex-native/openai-loader-fetch.ts +++ b/lib/codex-native/openai-loader-fetch.ts @@ -21,9 +21,8 @@ import { persistRateLimitSnapshotFromResponse } from "./rate-limit-snapshots" import { assertAllowedOutboundUrl, rewriteUrl } from "./request-routing" import { applyRequestTransformPipeline } from "./request-transform-pipeline" import { - applyPromptCacheKeyOverrideToRequest, - sanitizeOutboundRequestIfNeeded, - stripReasoningReplayFromRequest + type OutboundRequestPayloadTransformResult, + transformOutboundRequestPayload, } from "./request-transform" import type { SessionAffinityRuntimeState } from "./session-affinity-state" @@ -60,10 +59,31 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { const internalCollaborationAgentHeader = input.internalCollaborationAgentHeader ?? "x-opencode-collaboration-agent-kind" const quotaTrackerByIdentity = new Map() const quotaRefreshAtByIdentity = new Map() + const QUOTA_STATE_MAX_ENTRIES = 512 const QUOTA_REFRESH_TTL_MS = 60_000 + const QUOTA_REFRESH_FAILURE_RETRY_MS = 10_000 const QUOTA_FETCH_TIMEOUT_MS = 3000 const QUOTA_EXHAUSTED_FALLBACK_COOLDOWN_MS = 5 * 60 * 1000 + const pruneQuotaState = (now: number): void => { + if (quotaRefreshAtByIdentity.size <= QUOTA_STATE_MAX_ENTRIES) return + + for (const [identityKey, nextRefreshAt] of quotaRefreshAtByIdentity) { + if (nextRefreshAt < now - QUOTA_REFRESH_TTL_MS) { + quotaRefreshAtByIdentity.delete(identityKey) + quotaTrackerByIdentity.delete(identityKey) + } + if (quotaRefreshAtByIdentity.size <= QUOTA_STATE_MAX_ENTRIES) return + } + + while (quotaRefreshAtByIdentity.size > QUOTA_STATE_MAX_ENTRIES) { + const oldest = quotaRefreshAtByIdentity.keys().next().value as string | undefined + if (!oldest) break + quotaRefreshAtByIdentity.delete(oldest) + quotaTrackerByIdentity.delete(oldest) + } + } + return async (requestInput: string | URL | Request, init?: RequestInit): Promise => { const baseRequest = new Request(requestInput, init) if (input.headerTransformDebug) { @@ -102,36 +122,51 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { catalogModels: input.getCatalogModels(), behaviorSettings: input.behaviorSettings, fallbackPersonality: input.personality, - preserveOrchestratorInstructions: collaborationAgentKind === "orchestrator" + preserveOrchestratorInstructions: collaborationAgentKind === "orchestrator", + replaceCodexToolCalls: input.spoofMode === "codex" }) outbound = transformed.request const isSubagentRequest = transformed.isSubagentRequest + let selectedIdentityKey: string | undefined + let selectedAuthForQuota: { access: string; accountId?: string; identityKey?: string } | undefined + + const promptCacheKeyStrategy = input.promptCacheKeyStrategy ?? "default" + const promptCacheKeyOverride = + promptCacheKeyStrategy === "project" + ? buildProjectPromptCacheKey({ + projectPath: input.projectPath ?? process.cwd(), + spoofMode: input.spoofMode + }) + : undefined + + const initialPayloadTransform: OutboundRequestPayloadTransformResult = await transformOutboundRequestPayload({ + request: outbound, + stripReasoningReplayEnabled: true, + remapDeveloperMessagesToUserEnabled: input.remapDeveloperMessagesToUserEnabled, + compatInputSanitizerEnabled: input.compatInputSanitizerEnabled, + promptCacheKeyOverrideEnabled: promptCacheKeyStrategy === "project", + promptCacheKeyOverride + }) + outbound = initialPayloadTransform.request + if (input.headerTransformDebug) { await input.requestSnapshots.captureRequest("after-header-transform", outbound, { spoofMode: input.spoofMode, instructionsOverridden: transformed.instructionOverride.changed, instructionOverrideReason: transformed.instructionOverride.reason, - developerMessagesRemapped: transformed.developerRoleRemap.changed, - developerMessageRemapReason: transformed.developerRoleRemap.reason, - developerMessageRemapCount: transformed.developerRoleRemap.remappedCount, - developerMessagePreservedCount: transformed.developerRoleRemap.preservedCount, + developerMessagesRemapped: initialPayloadTransform.developerRoleRemap.changed, + developerMessageRemapReason: initialPayloadTransform.developerRoleRemap.reason, + developerMessageRemapCount: initialPayloadTransform.developerRoleRemap.remappedCount, + developerMessagePreservedCount: initialPayloadTransform.developerRoleRemap.preservedCount, ...(isSubagentRequest ? { subagent: transformed.subagentHeader } : {}) }) } - let selectedIdentityKey: string | undefined - let selectedAuthForQuota: { access: string; accountId?: string; identityKey?: string } | undefined - - const replaySanitized = await stripReasoningReplayFromRequest({ - request: outbound, - enabled: true - }) - outbound = replaySanitized.request - if (replaySanitized.changed) { + if (initialPayloadTransform.replay.changed) { input.log?.debug("reasoning replay stripped", { - removedPartCount: replaySanitized.removedPartCount, - removedFieldCount: replaySanitized.removedFieldCount + removedPartCount: initialPayloadTransform.replay.removedPartCount, + removedFieldCount: initialPayloadTransform.replay.removedFieldCount }) } @@ -207,34 +242,7 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { }, showToast: input.showToast, onAttemptRequest: async ({ attempt, maxAttempts, attemptReasonCode, request, auth, sessionKey }) => { - const transformed = await applyRequestTransformPipeline({ - request, - spoofMode: input.spoofMode, - remapDeveloperMessagesToUserEnabled: input.remapDeveloperMessagesToUserEnabled, - catalogModels: input.getCatalogModels(), - behaviorSettings: input.behaviorSettings, - fallbackPersonality: input.personality, - preserveOrchestratorInstructions: collaborationAgentKind === "orchestrator" - }) - - const promptCacheKeyStrategy = input.promptCacheKeyStrategy ?? "default" - const promptCacheKeyOverride = - promptCacheKeyStrategy === "project" - ? await applyPromptCacheKeyOverrideToRequest({ - request: transformed.request, - enabled: true, - promptCacheKey: buildProjectPromptCacheKey({ - projectPath: input.projectPath ?? process.cwd(), - spoofMode: input.spoofMode - }) - }) - : { - request: transformed.request, - changed: false, - reason: "default_strategy" - } - - await input.requestSnapshots.captureRequest("outbound-attempt", promptCacheKeyOverride.request, { + await input.requestSnapshots.captureRequest("outbound-attempt", request, { attempt: attempt + 1, maxAttempts, attemptReasonCode, @@ -243,12 +251,15 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { accountLabel: auth.accountLabel, instructionsOverridden: transformed.instructionOverride.changed, instructionOverrideReason: transformed.instructionOverride.reason, - developerMessagesRemapped: transformed.developerRoleRemap.changed, - developerMessageRemapReason: transformed.developerRoleRemap.reason, - developerMessageRemapCount: transformed.developerRoleRemap.remappedCount, - developerMessagePreservedCount: transformed.developerRoleRemap.preservedCount, - promptCacheKeyOverridden: promptCacheKeyOverride.changed, - promptCacheKeyOverrideReason: promptCacheKeyOverride.reason, + developerMessagesRemapped: initialPayloadTransform.developerRoleRemap.changed, + developerMessageRemapReason: initialPayloadTransform.developerRoleRemap.reason, + developerMessageRemapCount: initialPayloadTransform.developerRoleRemap.remappedCount, + developerMessagePreservedCount: initialPayloadTransform.developerRoleRemap.preservedCount, + promptCacheKeyOverridden: initialPayloadTransform.promptCacheKey.changed, + promptCacheKeyOverrideReason: + promptCacheKeyStrategy === "project" + ? initialPayloadTransform.promptCacheKey.reason + : "default_strategy", ...(input.headerTransformDebug === true && auth.selectionTrace ? { selectionStrategy: auth.selectionTrace.strategy, @@ -276,7 +287,7 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { : null) }) - return promptCacheKeyOverride.request + return request }, onAttemptResponse: async ({ attempt, maxAttempts, attemptReasonCode, response, auth, sessionKey }) => { await input.requestSnapshots.captureResponse("outbound-response", response, { @@ -290,18 +301,17 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { } }) - const sanitizedOutbound = await sanitizeOutboundRequestIfNeeded(outbound, input.compatInputSanitizerEnabled) - if (sanitizedOutbound.changed) { + if (initialPayloadTransform.compatSanitizer.changed) { input.log?.debug("compat input sanitizer applied", { mode: input.spoofMode }) } - await input.requestSnapshots.captureRequest("after-sanitize", sanitizedOutbound.request, { + await input.requestSnapshots.captureRequest("after-sanitize", initialPayloadTransform.request, { spoofMode: input.spoofMode, - sanitized: sanitizedOutbound.changed + sanitized: initialPayloadTransform.compatSanitizer.changed }) try { - assertAllowedOutboundUrl(new URL(sanitizedOutbound.request.url)) + assertAllowedOutboundUrl(new URL(initialPayloadTransform.request.url)) } catch (error) { if (isPluginFatalError(error)) { return toSyntheticErrorResponse(error) @@ -318,7 +328,7 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { let response: Response try { - response = await orchestrator.execute(sanitizedOutbound.request) + response = await orchestrator.execute(initialPayloadTransform.request) } catch (error) { if (isPluginFatalError(error)) { input.log?.debug("fatal auth/error response", { @@ -345,29 +355,34 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { const identityForQuota = selectedAuthForQuota?.identityKey if (identityForQuota && selectedAuthForQuota?.access) { - try { - const now = Date.now() - const nextRefreshAt = quotaRefreshAtByIdentity.get(identityForQuota) - if (nextRefreshAt === undefined || now >= nextRefreshAt) { - quotaRefreshAtByIdentity.set(identityForQuota, now + QUOTA_REFRESH_TTL_MS) - const quotaSnapshot = await fetchQuotaSnapshotFromBackend({ - accessToken: selectedAuthForQuota.access, - accountId: selectedAuthForQuota.accountId, - now, - modelFamily: "gpt-5.3-codex", - userAgent: resolveRequestUserAgent(input.spoofMode, resolveCodexOriginator(input.spoofMode)), - log: input.log, - timeoutMs: QUOTA_FETCH_TIMEOUT_MS - }) + const now = Date.now() + pruneQuotaState(now) + const nextRefreshAt = quotaRefreshAtByIdentity.get(identityForQuota) + if (nextRefreshAt === undefined || now >= nextRefreshAt) { + quotaRefreshAtByIdentity.set(identityForQuota, now + QUOTA_REFRESH_TTL_MS) + void (async () => { + try { + const quotaSnapshot = await fetchQuotaSnapshotFromBackend({ + accessToken: selectedAuthForQuota!.access, + accountId: selectedAuthForQuota!.accountId, + now, + modelFamily: "gpt-5.3-codex", + userAgent: resolveRequestUserAgent(input.spoofMode, resolveCodexOriginator(input.spoofMode)), + log: input.log, + timeoutMs: QUOTA_FETCH_TIMEOUT_MS + }) + + if (!quotaSnapshot) { + quotaRefreshAtByIdentity.set(identityForQuota, Date.now() + QUOTA_REFRESH_FAILURE_RETRY_MS) + return + } - if (quotaSnapshot) { await saveSnapshots(defaultSnapshotsPath(), (current) => ({ ...current, [identityForQuota]: quotaSnapshot })) - const previousTracker = - quotaTrackerByIdentity.get(identityForQuota) ?? DEFAULT_QUOTA_THRESHOLD_TRACKER_STATE + const previousTracker = quotaTrackerByIdentity.get(identityForQuota) ?? DEFAULT_QUOTA_THRESHOLD_TRACKER_STATE const evaluated = evaluateQuotaThresholds({ snapshot: quotaSnapshot, previousState: previousTracker @@ -379,13 +394,16 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { } if (evaluated.exhaustedCrossings.length > 0) { + const nowForCooldown = Date.now() const cooldownCandidates = evaluated.exhaustedCrossings .map((crossing) => crossing.resetsAt) - .filter((value): value is number => typeof value === "number" && Number.isFinite(value) && value > now) + .filter( + (value): value is number => typeof value === "number" && Number.isFinite(value) && value > nowForCooldown + ) const cooldownUntil = cooldownCandidates.length > 0 ? Math.max(...cooldownCandidates) - : now + QUOTA_EXHAUSTED_FALLBACK_COOLDOWN_MS + : nowForCooldown + QUOTA_EXHAUSTED_FALLBACK_COOLDOWN_MS await input.setCooldown(identityForQuota, cooldownUntil) if (evaluated.exhaustedCrossings.length === 1) { @@ -396,13 +414,14 @@ export function createOpenAIFetchHandler(input: CreateOpenAIFetchHandlerInput) { await input.showToast("Switching account due to 5h and weekly quota limits", "warning", input.quietMode) } } + } catch (error) { + quotaRefreshAtByIdentity.set(identityForQuota, Date.now() + QUOTA_REFRESH_FAILURE_RETRY_MS) + input.log?.debug("quota refresh during request failed", { + identityKey: identityForQuota, + error: error instanceof Error ? error.message : String(error) + }) } - } - } catch (error) { - input.log?.debug("quota refresh during request failed", { - identityKey: identityForQuota, - error: error instanceof Error ? error.message : String(error) - }) + })() } } diff --git a/lib/codex-native/request-transform-pipeline.ts b/lib/codex-native/request-transform-pipeline.ts index 3e1a80b..951575f 100644 --- a/lib/codex-native/request-transform-pipeline.ts +++ b/lib/codex-native/request-transform-pipeline.ts @@ -1,11 +1,17 @@ import type { BehaviorSettings, CodexSpoofMode, PersonalityOption } from "../config" import type { CodexModelInfo } from "../model-catalog" -import { applyCatalogInstructionOverrideToRequest, remapDeveloperMessagesToUserOnRequest } from "./request-transform" +import { applyCatalogInstructionOverrideToRequest } from "./request-transform" export type RequestTransformPipelineResult = { request: Request instructionOverride: Awaited> - developerRoleRemap: Awaited> + developerRoleRemap: { + request: Request + changed: boolean + reason: string + remappedCount: number + preservedCount: number + } subagentHeader?: string isSubagentRequest: boolean } @@ -18,6 +24,7 @@ export async function applyRequestTransformPipeline(input: { behaviorSettings?: BehaviorSettings fallbackPersonality?: PersonalityOption preserveOrchestratorInstructions?: boolean + replaceCodexToolCalls?: boolean }): Promise { const instructionOverride = await applyCatalogInstructionOverrideToRequest({ request: input.request, @@ -25,13 +32,17 @@ export async function applyRequestTransformPipeline(input: { catalogModels: input.catalogModels, behaviorSettings: input.behaviorSettings, fallbackPersonality: input.fallbackPersonality, - preserveOrchestratorInstructions: input.preserveOrchestratorInstructions - }) - const developerRoleRemap = await remapDeveloperMessagesToUserOnRequest({ - request: instructionOverride.request, - enabled: input.remapDeveloperMessagesToUserEnabled + preserveOrchestratorInstructions: input.preserveOrchestratorInstructions, + replaceCodexToolCalls: input.replaceCodexToolCalls }) - const request = developerRoleRemap.request + const request = instructionOverride.request + const developerRoleRemap = { + request, + changed: false, + reason: input.remapDeveloperMessagesToUserEnabled ? "deferred_to_payload_transform" : "disabled", + remappedCount: 0, + preservedCount: 0 + } const subagentHeader = request.headers.get("x-openai-subagent")?.trim() return { diff --git a/lib/codex-native/request-transform.ts b/lib/codex-native/request-transform.ts index 3444c21..586cef3 100644 --- a/lib/codex-native/request-transform.ts +++ b/lib/codex-native/request-transform.ts @@ -3,7 +3,7 @@ import type { CodexModelInfo } from "../model-catalog" import { resolveInstructionsForModel } from "../model-catalog" import { sanitizeRequestPayloadForCompat } from "../compat-sanitizer" import { isRecord } from "../util" -import { ensureOpenCodeToolingCompatibility, isOrchestratorInstructions } from "./collaboration" +import { isOrchestratorInstructions, replaceCodexToolCallsForOpenCode } from "./collaboration" type ChatParamsOutput = { temperature: number @@ -423,6 +423,378 @@ export async function sanitizeOutboundRequestIfNeeded( return { request: sanitizedRequest, changed: true } } +type TransformReason = + | "disabled" + | "non_post" + | "empty_body" + | "invalid_json" + | "non_object_body" + | "missing_input_array" + | "no_reasoning_replay" + | "no_developer_messages" + | "permissions_only" + | "missing_key" + | "already_matches" + | "set" + | "replaced" + | "updated" + +type ReplayTransformResult = { + changed: boolean + reason: TransformReason + removedPartCount: number + removedFieldCount: number +} + +type DeveloperRoleRemapTransformResult = { + changed: boolean + reason: TransformReason + remappedCount: number + preservedCount: number +} + +type PromptCacheKeyTransformResult = { + changed: boolean + reason: TransformReason +} + +type CompatSanitizerTransformResult = { + changed: boolean + reason: TransformReason +} + +type OutboundRequestPayloadTransformInput = { + request: Request + stripReasoningReplayEnabled: boolean + remapDeveloperMessagesToUserEnabled: boolean + compatInputSanitizerEnabled: boolean + promptCacheKeyOverrideEnabled: boolean + promptCacheKeyOverride?: string +} + +export type OutboundRequestPayloadTransformResult = { + request: Request + changed: boolean + replay: ReplayTransformResult + developerRoleRemap: DeveloperRoleRemapTransformResult + promptCacheKey: PromptCacheKeyTransformResult + compatSanitizer: CompatSanitizerTransformResult +} + +function stripReasoningReplayFromPayload(payload: Record): ReplayTransformResult { + if (!Array.isArray(payload.input)) { + return { + changed: false, + reason: "missing_input_array", + removedPartCount: 0, + removedFieldCount: 0 + } + } + + let changed = false + let removedPartCount = 0 + let removedFieldCount = 0 + const nextInput: unknown[] = [] + + for (const item of payload.input) { + if (isReasoningReplayPart(item)) { + changed = true + removedPartCount += 1 + continue + } + + if (!isRecord(item)) { + nextInput.push(item) + continue + } + + const nextItem: Record = { ...item } + const role = asString(nextItem.role)?.toLowerCase() + if (role === "assistant" && Array.isArray(nextItem.content)) { + const contentOut: unknown[] = [] + for (const entry of nextItem.content) { + if (isReasoningReplayPart(entry)) { + changed = true + removedPartCount += 1 + continue + } + const strippedEntry = stripReasoningReplayFields(entry) + if (strippedEntry.removed > 0) { + changed = true + removedFieldCount += strippedEntry.removed + } + contentOut.push(strippedEntry.value) + } + nextItem.content = contentOut + } + + const strippedItem = stripReasoningReplayFields(nextItem) + if (strippedItem.removed > 0) { + changed = true + removedFieldCount += strippedItem.removed + } + nextInput.push(strippedItem.value) + } + + if (!changed) { + return { + changed: false, + reason: "no_reasoning_replay", + removedPartCount, + removedFieldCount + } + } + + payload.input = nextInput + return { + changed: true, + reason: "updated", + removedPartCount, + removedFieldCount + } +} + +function remapDeveloperMessagesToUserOnPayload(payload: Record): DeveloperRoleRemapTransformResult { + if (!Array.isArray(payload.input)) { + return { + changed: false, + reason: "missing_input_array", + remappedCount: 0, + preservedCount: 0 + } + } + + let nextInput: unknown[] | undefined + let remappedCount = 0 + let preservedCount = 0 + let developerCount = 0 + for (let index = 0; index < payload.input.length; index += 1) { + const item = payload.input[index] + if (!isRecord(item)) continue + if (item.role !== "developer") continue + developerCount += 1 + if (shouldPreserveDeveloperRole(item)) { + preservedCount += 1 + continue + } + if (!nextInput) nextInput = payload.input.slice() + nextInput[index] = { + ...item, + role: "user" + } + remappedCount += 1 + } + + if (!nextInput) { + return { + changed: false, + reason: developerCount === 0 ? "no_developer_messages" : "permissions_only", + remappedCount, + preservedCount + } + } + + payload.input = nextInput + return { + changed: true, + reason: "updated", + remappedCount, + preservedCount + } +} + +function applyPromptCacheKeyOverrideToPayload( + payload: Record, + promptCacheKey: string | undefined +): PromptCacheKeyTransformResult { + if (!promptCacheKey) { + return { changed: false, reason: "missing_key" } + } + + const current = asString(payload.prompt_cache_key) + if (current === promptCacheKey) { + return { changed: false, reason: "already_matches" } + } + + payload.prompt_cache_key = promptCacheKey + return { + changed: true, + reason: current ? "replaced" : "set" + } +} + +export async function transformOutboundRequestPayload( + input: OutboundRequestPayloadTransformInput +): Promise { + const disabledReplay: ReplayTransformResult = { + changed: false, + reason: "disabled", + removedPartCount: 0, + removedFieldCount: 0 + } + const disabledRoleRemap: DeveloperRoleRemapTransformResult = { + changed: false, + reason: "disabled", + remappedCount: 0, + preservedCount: 0 + } + const disabledPromptCacheKey: PromptCacheKeyTransformResult = { + changed: false, + reason: "disabled" + } + const disabledCompatSanitizer: CompatSanitizerTransformResult = { + changed: false, + reason: "disabled" + } + + const method = input.request.method.toUpperCase() + if (method !== "POST") { + return { + request: input.request, + changed: false, + replay: input.stripReasoningReplayEnabled + ? { ...disabledReplay, reason: "non_post" } + : disabledReplay, + developerRoleRemap: input.remapDeveloperMessagesToUserEnabled + ? { ...disabledRoleRemap, reason: "non_post" } + : disabledRoleRemap, + promptCacheKey: input.promptCacheKeyOverrideEnabled + ? { ...disabledPromptCacheKey, reason: "non_post" } + : disabledPromptCacheKey, + compatSanitizer: input.compatInputSanitizerEnabled + ? { ...disabledCompatSanitizer, reason: "non_post" } + : disabledCompatSanitizer + } + } + + let raw: string + try { + raw = await input.request.clone().text() + } catch { + return { + request: input.request, + changed: false, + replay: input.stripReasoningReplayEnabled + ? { ...disabledReplay, reason: "invalid_json" } + : disabledReplay, + developerRoleRemap: input.remapDeveloperMessagesToUserEnabled + ? { ...disabledRoleRemap, reason: "invalid_json" } + : disabledRoleRemap, + promptCacheKey: input.promptCacheKeyOverrideEnabled + ? { ...disabledPromptCacheKey, reason: "invalid_json" } + : disabledPromptCacheKey, + compatSanitizer: input.compatInputSanitizerEnabled + ? { ...disabledCompatSanitizer, reason: "invalid_json" } + : disabledCompatSanitizer + } + } + + if (!raw) { + return { + request: input.request, + changed: false, + replay: input.stripReasoningReplayEnabled ? { ...disabledReplay, reason: "empty_body" } : disabledReplay, + developerRoleRemap: input.remapDeveloperMessagesToUserEnabled + ? { ...disabledRoleRemap, reason: "empty_body" } + : disabledRoleRemap, + promptCacheKey: input.promptCacheKeyOverrideEnabled + ? { ...disabledPromptCacheKey, reason: "empty_body" } + : disabledPromptCacheKey, + compatSanitizer: input.compatInputSanitizerEnabled + ? { ...disabledCompatSanitizer, reason: "empty_body" } + : disabledCompatSanitizer + } + } + + let payload: unknown + try { + payload = JSON.parse(raw) + } catch { + return { + request: input.request, + changed: false, + replay: input.stripReasoningReplayEnabled + ? { ...disabledReplay, reason: "invalid_json" } + : disabledReplay, + developerRoleRemap: input.remapDeveloperMessagesToUserEnabled + ? { ...disabledRoleRemap, reason: "invalid_json" } + : disabledRoleRemap, + promptCacheKey: input.promptCacheKeyOverrideEnabled + ? { ...disabledPromptCacheKey, reason: "invalid_json" } + : disabledPromptCacheKey, + compatSanitizer: input.compatInputSanitizerEnabled + ? { ...disabledCompatSanitizer, reason: "invalid_json" } + : disabledCompatSanitizer + } + } + + if (!isRecord(payload)) { + return { + request: input.request, + changed: false, + replay: input.stripReasoningReplayEnabled + ? { ...disabledReplay, reason: "non_object_body" } + : disabledReplay, + developerRoleRemap: input.remapDeveloperMessagesToUserEnabled + ? { ...disabledRoleRemap, reason: "non_object_body" } + : disabledRoleRemap, + promptCacheKey: input.promptCacheKeyOverrideEnabled + ? { ...disabledPromptCacheKey, reason: "non_object_body" } + : disabledPromptCacheKey, + compatSanitizer: input.compatInputSanitizerEnabled + ? { ...disabledCompatSanitizer, reason: "non_object_body" } + : disabledCompatSanitizer + } + } + + let changed = false + const replay = input.stripReasoningReplayEnabled + ? stripReasoningReplayFromPayload(payload) + : disabledReplay + changed = changed || replay.changed + + const developerRoleRemap = input.remapDeveloperMessagesToUserEnabled + ? remapDeveloperMessagesToUserOnPayload(payload) + : disabledRoleRemap + changed = changed || developerRoleRemap.changed + + const promptCacheKey = input.promptCacheKeyOverrideEnabled + ? applyPromptCacheKeyOverrideToPayload(payload, asString(input.promptCacheKeyOverride)) + : disabledPromptCacheKey + changed = changed || promptCacheKey.changed + + const compatSanitizedPayload = input.compatInputSanitizerEnabled ? sanitizeRequestPayloadForCompat(payload) : null + const compatSanitizer: CompatSanitizerTransformResult = input.compatInputSanitizerEnabled + ? { + changed: compatSanitizedPayload?.changed === true, + reason: compatSanitizedPayload?.changed === true ? "updated" : "already_matches" + } + : disabledCompatSanitizer + + const finalPayload = compatSanitizedPayload?.payload ?? payload + changed = changed || compatSanitizer.changed + + if (!changed) { + return { + request: input.request, + changed: false, + replay, + developerRoleRemap, + promptCacheKey, + compatSanitizer + } + } + + return { + request: rebuildRequestWithJsonBody(input.request, finalPayload), + changed: true, + replay, + developerRoleRemap, + promptCacheKey, + compatSanitizer + } +} + export async function applyPromptCacheKeyOverrideToRequest(input: { request: Request enabled: boolean @@ -850,6 +1222,7 @@ export async function applyCatalogInstructionOverrideToRequest(input: { behaviorSettings: BehaviorSettings | undefined fallbackPersonality: PersonalityOption | undefined preserveOrchestratorInstructions?: boolean + replaceCodexToolCalls?: boolean }): Promise<{ request: Request; changed: boolean; reason: string }> { if (!input.enabled) return { request: input.request, changed: false, reason: "disabled" } @@ -891,31 +1264,27 @@ export async function applyCatalogInstructionOverrideToRequest(input: { const rendered = resolveInstructionsForModel(catalogModel, effectivePersonality) if (!rendered) return { request: input.request, changed: false, reason: "rendered_empty_or_unsafe" } + const renderedForRequest = + input.replaceCodexToolCalls === true ? replaceCodexToolCallsForOpenCode(rendered) ?? rendered : rendered const currentInstructions = asString(payload.instructions) const preserveOrchestratorInstructions = input.preserveOrchestratorInstructions !== false if (preserveOrchestratorInstructions && isOrchestratorInstructions(currentInstructions)) { - const compatible = ensureOpenCodeToolingCompatibility(currentInstructions) - if (compatible && compatible.trim() !== (currentInstructions?.trim() ?? "")) { - payload.instructions = compatible - const updatedRequest = rebuildRequestWithJsonBody(input.request, payload) - return { request: updatedRequest, changed: true, reason: "tooling_compatibility_added" } - } return { request: input.request, changed: false, reason: "orchestrator_instructions_preserved" } } - if (currentInstructions === rendered) { + if (currentInstructions === renderedForRequest) { return { request: input.request, changed: false, reason: "already_matches" } } - if (currentInstructions && currentInstructions.includes(rendered)) { + if (currentInstructions && currentInstructions.includes(renderedForRequest)) { return { request: input.request, changed: false, reason: "already_contains_rendered" } } const collaborationTail = currentInstructions ? extractCollaborationInstructionTail(currentInstructions) : undefined - const nextInstructionsBase = collaborationTail ? `${rendered}\n\n${collaborationTail}` : rendered - const nextInstructions = ensureOpenCodeToolingCompatibility(nextInstructionsBase) ?? nextInstructionsBase + const nextInstructionsBase = collaborationTail ? `${renderedForRequest}\n\n${collaborationTail}` : renderedForRequest + const nextInstructions = nextInstructionsBase if (currentInstructions === nextInstructions) { return { request: input.request, changed: false, reason: "already_matches" } diff --git a/lib/codex-status-storage.ts b/lib/codex-status-storage.ts index 60ab725..c7f3b08 100644 --- a/lib/codex-status-storage.ts +++ b/lib/codex-status-storage.ts @@ -27,6 +27,9 @@ export async function saveSnapshots( return withLockedFile(filePath, async () => { const cur = await loadSnapshots(filePath) const next = await update(cur) + if (JSON.stringify(next) === JSON.stringify(cur)) { + return next + } await writeAtomic(filePath, next) return next }) diff --git a/lib/storage.ts b/lib/storage.ts index 4ade667..a6772c3 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -640,9 +640,13 @@ export async function saveAuthStorage( ): Promise { return withFileLock(filePath, async () => { const current = await readAuthUnlocked(filePath) + const before = JSON.stringify(current) const result = await update(current) const nextBase = result === undefined ? current : result const next = sanitizeAuthFile(migrateAuthFile(nextBase)) + if (JSON.stringify(next) === before) { + return next + } await writeAuthUnlocked(filePath, next) return next }) diff --git a/scripts/perf-profile.ts b/scripts/perf-profile.ts new file mode 100644 index 0000000..9520d37 --- /dev/null +++ b/scripts/perf-profile.ts @@ -0,0 +1,341 @@ +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { performance } from "node:perf_hooks" +import { pathToFileURL } from "node:url" + +type RequestTransformModule = typeof import("../lib/codex-native/request-transform") +type LoaderModule = typeof import("../lib/codex-native/openai-loader-fetch") +type OrchestratorModule = typeof import("../lib/fetch-orchestrator") +type RotationModule = typeof import("../lib/rotation") +type AcquireAuthModule = typeof import("../lib/codex-native/acquire-auth") + +function median(values: number[]): number { + const sorted = [...values].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + return sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]! +} + +function p95(values: number[]): number { + const sorted = [...values].sort((a, b) => a - b) + const idx = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95)) + return sorted[idx]! +} + +function fmt(v: number): number { + return Number(v.toFixed(3)) +} + +async function importFromRoot(root: string, relPath: string): Promise { + const fullPath = path.resolve(root, relPath) + return (await import(pathToFileURL(fullPath).href)) as T +} + +function buildPayload() { + return { + model: "gpt-5.3-codex", + prompt_cache_key: "ses_perf", + input: [ + { + type: "message", + role: "assistant", + content: [{ type: "reasoning_summary", text: "remove" }, { type: "output_text", text: "keep" }] + }, + { + type: "message", + role: "developer", + content: [{ type: "input_text", text: "rewrite" }] + }, + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "hello" }] + } + ] + } +} + +function buildRequest(): Request { + return new Request("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "content-type": "application/json", + session_id: "ses_perf" + }, + body: JSON.stringify(buildPayload()) + }) +} + +async function seedAuthStore(xdgConfigHome: string): Promise { + const dir = path.join(xdgConfigHome, "opencode") + await fs.mkdir(dir, { recursive: true }) + const filePath = path.join(dir, "codex-accounts.json") + const identityKey = "acc_1|user@example.com|plus" + await fs.writeFile( + filePath, + `${JSON.stringify( + { + openai: { + type: "oauth", + native: { + strategy: "sticky", + activeIdentityKey: identityKey, + accounts: [ + { + identityKey, + accountId: "acc_1", + email: "user@example.com", + plan: "plus", + enabled: true, + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 3600_000 + } + ] + } + } + }, + null, + 2 + )}\n`, + { mode: 0o600 } + ) + return filePath +} + +async function benchmarkPayloadTransforms(root: string, iterations: number): Promise<{ + legacy: { medianMs: number; p95Ms: number } + singlePass?: { medianMs: number; p95Ms: number } +}> { + const mod = await importFromRoot(root, "lib/codex-native/request-transform.ts") + const legacyDurations: number[] = [] + for (let i = 0; i < iterations; i += 1) { + const t0 = performance.now() + const request = buildRequest() + const replay = await mod.stripReasoningReplayFromRequest({ request, enabled: true }) + const remap = await mod.remapDeveloperMessagesToUserOnRequest({ request: replay.request, enabled: true }) + const prompt = await mod.applyPromptCacheKeyOverrideToRequest({ + request: remap.request, + enabled: true, + promptCacheKey: "pk_project" + }) + await mod.sanitizeOutboundRequestIfNeeded(prompt.request, true) + legacyDurations.push(performance.now() - t0) + } + + const hasSinglePass = typeof (mod as Partial).transformOutboundRequestPayload === "function" + if (!hasSinglePass) { + return { + legacy: { + medianMs: fmt(median(legacyDurations)), + p95Ms: fmt(p95(legacyDurations)) + } + } + } + + const singleDurations: number[] = [] + for (let i = 0; i < iterations; i += 1) { + const t0 = performance.now() + await mod.transformOutboundRequestPayload({ + request: buildRequest(), + stripReasoningReplayEnabled: true, + remapDeveloperMessagesToUserEnabled: true, + compatInputSanitizerEnabled: true, + promptCacheKeyOverrideEnabled: true, + promptCacheKeyOverride: "pk_project" + }) + singleDurations.push(performance.now() - t0) + } + + return { + legacy: { + medianMs: fmt(median(legacyDurations)), + p95Ms: fmt(p95(legacyDurations)) + }, + singlePass: { + medianMs: fmt(median(singleDurations)), + p95Ms: fmt(p95(singleDurations)) + } + } +} + +async function benchmarkAcquireAuthNoop(root: string, iterations: number): Promise<{ + medianMs: number + p95Ms: number + fileWritesDetected: number +}> { + const acquireMod = await importFromRoot(root, "lib/codex-native/acquire-auth.ts") + + const xdgConfigHome = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-perf-xdg-")) + const previousXdgConfigHome = process.env.XDG_CONFIG_HOME + process.env.XDG_CONFIG_HOME = xdgConfigHome + + try { + const authFilePath = await seedAuthStore(xdgConfigHome) + const defaults = acquireMod.createAcquireOpenAIAuthInputDefaults() + const durations: number[] = [] + let writes = 0 + let lastMtimeMs = (await fs.stat(authFilePath)).mtimeMs + + for (let i = 0; i < iterations; i += 1) { + const t0 = performance.now() + await acquireMod.acquireOpenAIAuth({ + authMode: "native", + context: { sessionKey: "ses_perf_auth" }, + isSubagentRequest: false, + stickySessionState: defaults.stickySessionState, + hybridSessionState: defaults.hybridSessionState, + seenSessionKeys: new Map(), + persistSessionAffinityState: () => {}, + pidOffsetEnabled: false + }) + durations.push(performance.now() - t0) + const currentMtimeMs = (await fs.stat(authFilePath)).mtimeMs + if (currentMtimeMs !== lastMtimeMs) { + writes += 1 + lastMtimeMs = currentMtimeMs + } + } + + return { + medianMs: fmt(median(durations)), + p95Ms: fmt(p95(durations)), + fileWritesDetected: writes + } + } finally { + if (previousXdgConfigHome === undefined) { + delete process.env.XDG_CONFIG_HOME + } else { + process.env.XDG_CONFIG_HOME = previousXdgConfigHome + } + } +} + +async function benchmarkQuotaBlocking(root: string): Promise<{ latencyMs: number }> { + const loaderMod = await importFromRoot(root, "lib/codex-native/openai-loader-fetch.ts") + const orchestratorMod = await importFromRoot(root, "lib/fetch-orchestrator.ts") + const rotationMod = await importFromRoot(root, "lib/rotation.ts") + + const xdgConfigHome = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-perf-quota-")) + const previousXdgConfigHome = process.env.XDG_CONFIG_HOME + process.env.XDG_CONFIG_HOME = xdgConfigHome + await seedAuthStore(xdgConfigHome) + + const originalFetch = globalThis.fetch + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url + if (url.includes("/wham/usage")) { + await new Promise((resolve) => setTimeout(resolve, 120)) + return new Response( + JSON.stringify({ + rate_limit: { + primary_window: { used_percent: 20, reset_at: 1_710_000_000 }, + secondary_window: { used_percent: 10, reset_at: 1_711_000_000 } + } + }), + { status: 200 } + ) + } + + return new Response("ok", { status: 200 }) + }) as typeof fetch + + try { + const handler = loaderMod.createOpenAIFetchHandler({ + authMode: "native", + spoofMode: "native", + remapDeveloperMessagesToUserEnabled: false, + quietMode: true, + pidOffsetEnabled: false, + headerTransformDebug: false, + compatInputSanitizerEnabled: false, + internalCollaborationModeHeader: "x-opencode-collaboration-mode-kind", + requestSnapshots: { + captureRequest: async () => {}, + captureResponse: async () => {} + }, + sessionAffinityState: { + orchestratorState: orchestratorMod.createFetchOrchestratorState(), + stickySessionState: rotationMod.createStickySessionState(), + hybridSessionState: rotationMod.createStickySessionState(), + persistSessionAffinityState: () => {} + }, + getCatalogModels: () => undefined, + syncCatalogFromAuth: async () => undefined, + setCooldown: async () => {}, + showToast: async () => {} + }) + + const t0 = performance.now() + await handler("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "content-type": "application/json", + session_id: "ses_quota_profile" + }, + body: JSON.stringify({ model: "gpt-5.3-codex", input: "hello" }) + }) + return { latencyMs: fmt(performance.now() - t0) } + } finally { + globalThis.fetch = originalFetch + if (previousXdgConfigHome === undefined) { + delete process.env.XDG_CONFIG_HOME + } else { + process.env.XDG_CONFIG_HOME = previousXdgConfigHome + } + } +} + +async function run(): Promise { + const iterations = Math.max(1, Number.parseInt(process.argv[2] ?? "300", 10) || 300) + const baselineRoot = path.resolve(process.argv[3] ?? path.join(process.cwd(), "..", "..")) + const optimizedRoot = path.resolve(process.argv[4] ?? process.cwd()) + + const baseline = { + transforms: await benchmarkPayloadTransforms(baselineRoot, iterations), + acquireAuth: await benchmarkAcquireAuthNoop(baselineRoot, Math.max(50, Math.floor(iterations / 2))), + quota: await benchmarkQuotaBlocking(baselineRoot) + } + + const optimized = { + transforms: await benchmarkPayloadTransforms(optimizedRoot, iterations), + acquireAuth: await benchmarkAcquireAuthNoop(optimizedRoot, Math.max(50, Math.floor(iterations / 2))), + quota: await benchmarkQuotaBlocking(optimizedRoot) + } + + const baselineTransformMs = baseline.transforms.legacy.medianMs + const optimizedTransformMs = optimized.transforms.singlePass?.medianMs ?? optimized.transforms.legacy.medianMs + const transformGainPct = + baselineTransformMs > 0 ? fmt(((baselineTransformMs - optimizedTransformMs) / baselineTransformMs) * 100) : 0 + + const acquireGainPct = + baseline.acquireAuth.medianMs > 0 + ? fmt(((baseline.acquireAuth.medianMs - optimized.acquireAuth.medianMs) / baseline.acquireAuth.medianMs) * 100) + : 0 + + const quotaLatencyGainPct = + baseline.quota.latencyMs > 0 + ? fmt(((baseline.quota.latencyMs - optimized.quota.latencyMs) / baseline.quota.latencyMs) * 100) + : 0 + + console.log( + JSON.stringify( + { + iterations, + baseline, + optimized, + gains: { + transformMedianGainPct: transformGainPct, + acquireAuthMedianGainPct: acquireGainPct, + quotaLatencyGainPct, + acquireAuthWriteReduction: + baseline.acquireAuth.fileWritesDetected - optimized.acquireAuth.fileWritesDetected + } + }, + null, + 2 + ) + ) +} + +void run() diff --git a/test/acquire-auth-locking.test.ts b/test/acquire-auth-locking.test.ts index 7062fe0..d3d4fcd 100644 --- a/test/acquire-auth-locking.test.ts +++ b/test/acquire-auth-locking.test.ts @@ -216,6 +216,88 @@ describe("acquire auth lock behavior", () => { expect(saveAuthStorage).toHaveBeenCalled() }) + it("currently writes auth storage when selecting a still-valid access token", async () => { + vi.resetModules() + + let authState: Record = { + openai: { + type: "oauth", + native: { + accounts: [ + { + identityKey: "acc_1|user@example.com|plus", + accountId: "acc_1", + email: "user@example.com", + plan: "plus", + enabled: true, + access: "at_1", + refresh: "rt_1", + expires: Date.now() + 60_000 + } + ], + activeIdentityKey: "acc_1|user@example.com|plus" + } + } + } + + let writes = 0 + const saveAuthStorage = vi.fn( + async ( + _path: string | undefined, + update: ( + auth: Record + ) => Promise | void> | Record | void + ) => { + const before = JSON.stringify(authState) + const current = structuredClone(authState) + const next = await update(current) + authState = structuredClone((next ?? current) as Record) + const after = JSON.stringify(authState) + if (before !== after) writes += 1 + return authState + } + ) + + const ensureOpenAIOAuthDomain = vi.fn((auth: Record, mode: "native" | "codex") => { + const openai = auth.openai as { type?: string; native?: { accounts: unknown[] }; codex?: { accounts: unknown[] } } + if (!openai || openai.type !== "oauth") { + throw new Error("OpenAI OAuth not configured") + } + const existing = mode === "native" ? openai.native : openai.codex + if (existing) return existing + const created = { accounts: [] as unknown[] } + if (mode === "native") openai.native = created + else openai.codex = created + return created + }) + + vi.doMock("../lib/storage", () => ({ + saveAuthStorage, + ensureOpenAIOAuthDomain + })) + + const fetchSpy = vi.fn() + vi.stubGlobal("fetch", fetchSpy) + + const { acquireOpenAIAuth, createAcquireOpenAIAuthInputDefaults } = await import("../lib/codex-native/acquire-auth") + const defaults = createAcquireOpenAIAuthInputDefaults() + + const auth = await acquireOpenAIAuth({ + authMode: "native", + context: { sessionKey: null }, + isSubagentRequest: false, + stickySessionState: defaults.stickySessionState, + hybridSessionState: defaults.hybridSessionState, + seenSessionKeys: new Map(), + persistSessionAffinityState: () => {}, + pidOffsetEnabled: false + }) + + expect(auth.access).toBe("at_1") + expect(fetchSpy).not.toHaveBeenCalled() + expect(writes).toBe(1) + }) + it("fails with missing identity metadata without applying cooldown", async () => { vi.resetModules() diff --git a/test/codex-native-request-transform.test.ts b/test/codex-native-request-transform.test.ts index 3fccbb2..d0b095d 100644 --- a/test/codex-native-request-transform.test.ts +++ b/test/codex-native-request-transform.test.ts @@ -2,7 +2,10 @@ import { describe, expect, it } from "vitest" import { applyCatalogInstructionOverrideToRequest, + applyPromptCacheKeyOverrideToRequest, remapDeveloperMessagesToUserOnRequest, + sanitizeOutboundRequestIfNeeded, + transformOutboundRequestPayload, stripReasoningReplayFromRequest } from "../lib/codex-native/request-transform" @@ -265,14 +268,28 @@ describe("catalog instruction override orchestrator preservation gating", () => fallbackPersonality: undefined, preserveOrchestratorInstructions: true }) - expect(preserved.changed).toBe(true) - expect(preserved.reason).toBe("tooling_compatibility_added") + expect(preserved.changed).toBe(false) + expect(preserved.reason).toBe("orchestrator_instructions_preserved") const preservedBody = JSON.parse(await preserved.request.text()) as { instructions?: string } expect(preservedBody.instructions).toContain("You are Codex, a coding agent based on GPT-5.") - expect(preservedBody.instructions).toContain("Tooling Compatibility (OpenCode)") + expect(preservedBody.instructions).not.toContain("Tooling Compatibility (OpenCode)") + + const replacementRequest = new Request("https://chatgpt.com/backend-api/codex/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "gpt-5.3-codex", + instructions: [ + "You are Codex, a coding agent based on GPT-5.", + "", + "# Sub-agents", + "If `spawn_agent` is unavailable or fails, ignore this section and proceed solo." + ].join("\n") + }) + }) const replaced = await applyCatalogInstructionOverrideToRequest({ - request, + request: replacementRequest, enabled: true, catalogModels, behaviorSettings: undefined, @@ -307,11 +324,11 @@ describe("catalog instruction override orchestrator preservation gating", () => behaviorSettings: undefined, fallbackPersonality: undefined }) - expect(preserved.changed).toBe(true) - expect(preserved.reason).toBe("tooling_compatibility_added") + expect(preserved.changed).toBe(false) + expect(preserved.reason).toBe("orchestrator_instructions_preserved") const preservedBody = JSON.parse(await preserved.request.text()) as { instructions?: string } expect(preservedBody.instructions).toContain("You are Codex, a coding agent based on GPT-5.") - expect(preservedBody.instructions).toContain("Tooling Compatibility (OpenCode)") + expect(preservedBody.instructions).not.toContain("Tooling Compatibility (OpenCode)") }) it("preserves marker-based orchestrator instructions without spawn_agent token", async () => { @@ -337,11 +354,11 @@ describe("catalog instruction override orchestrator preservation gating", () => fallbackPersonality: undefined }) - expect(preserved.changed).toBe(true) - expect(preserved.reason).toBe("tooling_compatibility_added") + expect(preserved.changed).toBe(false) + expect(preserved.reason).toBe("orchestrator_instructions_preserved") const preservedBody = JSON.parse(await preserved.request.text()) as { instructions?: string } expect(preservedBody.instructions).toContain("Coordinate them via wait / send_input.") - expect(preservedBody.instructions).toContain("Tooling Compatibility (OpenCode)") + expect(preservedBody.instructions).not.toContain("Tooling Compatibility (OpenCode)") }) it("does not preserve generic wait/send_input prose under sub-agents header", async () => { @@ -373,7 +390,7 @@ describe("catalog instruction override orchestrator preservation gating", () => expect(body.instructions).toContain("Base Default voice") }) - it("does not report compatibility-added when compatibility block already exists with spacing differences", async () => { + it("preserves orchestrator instructions when compatibility block already exists with spacing differences", async () => { const request = new Request("https://chatgpt.com/backend-api/codex/responses", { method: "POST", headers: { "content-type": "application/json" }, @@ -383,9 +400,9 @@ describe("catalog instruction override orchestrator preservation gating", () => "You are Codex, a coding agent based on GPT-5.", "", "# Sub-agents", - "If `spawn_agent` is unavailable or fails, ignore this section and proceed solo.", + "If `task` is unavailable or fails, ignore this section and proceed solo.", "", - "# Tooling Compatibility (OpenCode)", + "# Notes", "" ].join("\n") }) @@ -431,4 +448,124 @@ describe("catalog instruction override orchestrator preservation gating", () => expect(result.changed).toBe(false) expect(result.reason).toBe("orchestrator_instructions_preserved") }) + + it("replaces codex tool-call names in rendered catalog instructions when enabled", async () => { + const request = new Request("https://chatgpt.com/backend-api/codex/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "gpt-5.3-codex" + }) + }) + + const result = await applyCatalogInstructionOverrideToRequest({ + request, + enabled: true, + catalogModels: [ + { + slug: "gpt-5.3-codex", + model_messages: { + instructions_template: "Use spawn_agent and send_input with write_stdin; close_agent when done" + } + } + ], + behaviorSettings: undefined, + fallbackPersonality: undefined, + replaceCodexToolCalls: true + }) + + expect(result.changed).toBe(true) + expect(result.reason).toBe("updated") + const body = JSON.parse(await result.request.text()) as { instructions?: string } + expect(body.instructions).toContain("task") + expect(body.instructions).toContain("skip_task_reuse") + expect(body.instructions).not.toContain("spawn_agent") + expect(body.instructions).not.toContain("send_input") + expect(body.instructions).not.toContain("write_stdin") + }) +}) + +describe("request transform aggregation", () => { + it("applies replay stripping, remap, prompt key override, and compat sanitization in one parse", async () => { + const request = new Request("https://chatgpt.com/backend-api/codex/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "gpt-5.3-codex", + input: [ + { + type: "message", + role: "assistant", + content: [{ type: "reasoning_summary", text: "remove me" }, { type: "output_text", text: "keep me" }] + }, + { + type: "message", + role: "developer", + content: [{ type: "input_text", text: "rewrite role" }] + } + ] + }) + }) + + const transformed = await transformOutboundRequestPayload({ + request, + stripReasoningReplayEnabled: true, + remapDeveloperMessagesToUserEnabled: true, + compatInputSanitizerEnabled: true, + promptCacheKeyOverrideEnabled: true, + promptCacheKeyOverride: "pk_project" + }) + + expect(transformed.changed).toBe(true) + expect(transformed.replay.reason).toBe("updated") + expect(transformed.replay.removedPartCount).toBe(1) + expect(transformed.developerRoleRemap.reason).toBe("updated") + expect(transformed.developerRoleRemap.remappedCount).toBe(1) + expect(transformed.promptCacheKey.reason).toBe("set") + expect(transformed.compatSanitizer.changed).toBe(false) + + const body = JSON.parse(await transformed.request.text()) as { + input: Array<{ role?: string; content?: Array<{ type?: string; text?: string }> }> + prompt_cache_key?: string + } + expect(body.prompt_cache_key).toBe("pk_project") + expect(body.input).toHaveLength(2) + expect(body.input[0]?.content).toEqual([{ type: "output_text", text: "keep me" }]) + expect(body.input[1]?.role).toBe("user") + }) + + it("matches legacy behavior when no changes are needed", async () => { + const request = new Request("https://chatgpt.com/backend-api/codex/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "gpt-5.3-codex", + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hello" }] }] + }) + }) + + const legacyReplay = await stripReasoningReplayFromRequest({ request, enabled: true }) + const legacyRemap = await remapDeveloperMessagesToUserOnRequest({ request: legacyReplay.request, enabled: true }) + const legacyPrompt = await applyPromptCacheKeyOverrideToRequest({ + request: legacyRemap.request, + enabled: true, + promptCacheKey: "pk_project" + }) + const legacyCompat = await sanitizeOutboundRequestIfNeeded(legacyPrompt.request, true) + + const aggregated = await transformOutboundRequestPayload({ + request, + stripReasoningReplayEnabled: true, + remapDeveloperMessagesToUserEnabled: true, + compatInputSanitizerEnabled: true, + promptCacheKeyOverrideEnabled: true, + promptCacheKeyOverride: "pk_project" + }) + + expect(await aggregated.request.text()).toBe(await legacyCompat.request.text()) + expect(aggregated.replay.reason).toBe("no_reasoning_replay") + expect(aggregated.developerRoleRemap.reason).toBe("no_developer_messages") + expect(aggregated.promptCacheKey.reason).toBe("set") + expect(aggregated.compatSanitizer.changed).toBe(false) + }) }) diff --git a/test/codex-status-storage.test.ts b/test/codex-status-storage.test.ts index d829814..9dfc416 100644 --- a/test/codex-status-storage.test.ts +++ b/test/codex-status-storage.test.ts @@ -101,4 +101,23 @@ describe("codex-status storage", () => { // Cleanup await fs.rm(dir, { recursive: true, force: true }) }) + + it("skips writing when snapshot content is unchanged", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-status-noop-")) + const p = path.join(dir, "snapshots.json") + + await saveSnapshots(p, () => ({ + "acc|u@e.com|plus": { updatedAt: 1, modelFamily: "gpt-5.2", limits: [{ name: "requests", leftPct: 50 }] } + })) + + const firstMtime = (await fs.stat(p)).mtimeMs + await new Promise((r) => setTimeout(r, 15)) + + await saveSnapshots(p, (cur) => ({ ...cur })) + + const secondMtime = (await fs.stat(p)).mtimeMs + expect(secondMtime).toBe(firstMtime) + + await fs.rm(dir, { recursive: true, force: true }) + }) }) diff --git a/test/openai-loader-fetch.prompt-cache-key.test.ts b/test/openai-loader-fetch.prompt-cache-key.test.ts index 382b70f..6daefc5 100644 --- a/test/openai-loader-fetch.prompt-cache-key.test.ts +++ b/test/openai-loader-fetch.prompt-cache-key.test.ts @@ -318,7 +318,7 @@ describe("openai loader fetch prompt cache key", () => { vi.stubGlobal("fetch", fetchMock) const setCooldown = vi.fn(async () => {}) - const showToast = vi.fn(async () => {}) + const showToast = vi.fn(async (_message: string, _variant?: string, _quietMode?: boolean) => {}) const log = { debug: vi.fn(), warn: vi.fn(), info: vi.fn(), error: vi.fn() } const handler = createOpenAIFetchHandler({ authMode: "native", @@ -360,18 +360,226 @@ describe("openai loader fetch prompt cache key", () => { }) expect(apiCallCount).toBe(1) - expect(fetchMock.mock.calls.some((call) => String(call[0]).includes("/wham/usage"))).toBe(true) - expect(log.debug).not.toHaveBeenCalledWith( - "quota refresh during request failed", - expect.objectContaining({ identityKey: auth.identityKey }) + await vi.waitFor(() => { + expect(fetchMock.mock.calls.some((call) => String(call[0]).includes("/wham/usage"))).toBe(true) + expect(log.debug).not.toHaveBeenCalledWith( + "quota refresh during request failed", + expect.objectContaining({ identityKey: auth.identityKey }) + ) + expect(setCooldown).toHaveBeenCalledWith(auth.identityKey, expect.any(Number)) + expect( + showToast.mock.calls.some( + (call) => call[0] === "Switching account due to weekly quota limit" && call[1] === "warning" + ) + ).toBe(true) + }) + }) + + it("does not block response on quota refresh", async () => { + vi.resetModules() + + const auth = { + access: "access-token", + accountId: "acc_123", + identityKey: "acc_123|user@example.com|plus", + email: "user@example.com", + plan: "plus", + accountLabel: "user@example.com (plus)" + } + + const acquireOpenAIAuth = vi.fn(async () => auth) + vi.doMock("../lib/codex-native/acquire-auth", () => ({ + acquireOpenAIAuth + })) + + const { createOpenAIFetchHandler } = await import("../lib/codex-native/openai-loader-fetch") + const { createFetchOrchestratorState } = await import("../lib/fetch-orchestrator") + const { createStickySessionState } = await import("../lib/rotation") + + let resolveQuota: ((response: Response) => void) | undefined + const quotaPending = new Promise((resolve) => { + resolveQuota = resolve + }) + + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url + if (url.includes("/wham/usage")) { + return quotaPending + } + return new Response("ok", { status: 200 }) + }) + vi.stubGlobal("fetch", fetchMock) + + const setCooldown = vi.fn(async () => {}) + const showToast = vi.fn(async (_message: string, _variant?: string, _quietMode?: boolean) => {}) + const handler = createOpenAIFetchHandler({ + authMode: "native", + spoofMode: "native", + remapDeveloperMessagesToUserEnabled: false, + quietMode: false, + pidOffsetEnabled: false, + headerTransformDebug: false, + compatInputSanitizerEnabled: false, + internalCollaborationModeHeader: "x-opencode-collaboration-mode-kind", + requestSnapshots: { + captureRequest: async () => {}, + captureResponse: async () => {} + }, + sessionAffinityState: { + orchestratorState: createFetchOrchestratorState(), + stickySessionState: createStickySessionState(), + hybridSessionState: createStickySessionState(), + persistSessionAffinityState: () => {} + }, + getCatalogModels: () => undefined, + syncCatalogFromAuth: async () => undefined, + setCooldown, + showToast + }) + + const handlerPromise = handler("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "content-type": "application/json", + session_id: "ses_async_quota" + }, + body: JSON.stringify({ + model: "gpt-5.3-codex", + input: "hello" + }) + }) + + const earlyResolution = await Promise.race([ + handlerPromise.then(() => "resolved"), + new Promise((resolve) => setTimeout(() => resolve("pending"), 25)) + ]) + + resolveQuota?.( + new Response( + JSON.stringify({ + rate_limit: { + primary_window: { used_percent: 10, reset_at: 1_710_000_000 }, + secondary_window: { used_percent: 100, reset_at: 1_711_000_000 } + } + }), + { status: 200 } + ) ) - expect(setCooldown).toHaveBeenCalled() - expect(setCooldown).toHaveBeenCalledWith(auth.identityKey, expect.any(Number)) - expect( - showToast.mock.calls.some( - (call) => call[0] === "Switching account due to weekly quota limit" && call[1] === "warning" + + await handlerPromise + + expect(earlyResolution).toBe("resolved") + await vi.waitFor(() => { + expect(setCooldown).toHaveBeenCalled() + expect(showToast).toHaveBeenCalled() + }) + }) + + it("retries quota refresh sooner after failure instead of waiting full ttl", async () => { + vi.resetModules() + + const auth = { + access: "access-token", + accountId: "acc_123", + identityKey: "acc_123|user@example.com|plus", + email: "user@example.com", + plan: "plus", + accountLabel: "user@example.com (plus)" + } + + const acquireOpenAIAuth = vi.fn(async () => auth) + vi.doMock("../lib/codex-native/acquire-auth", () => ({ + acquireOpenAIAuth + })) + + const { createOpenAIFetchHandler } = await import("../lib/codex-native/openai-loader-fetch") + const { createFetchOrchestratorState } = await import("../lib/fetch-orchestrator") + const { createStickySessionState } = await import("../lib/rotation") + + let usageCalls = 0 + let now = 1_700_000_000_000 + const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => now) + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url + if (url.includes("/wham/usage")) { + usageCalls += 1 + throw new Error("quota backend down") + } + return new Response("ok", { status: 200 }) + }) + vi.stubGlobal("fetch", fetchMock) + + const setCooldown = vi.fn(async () => {}) + const showToast = vi.fn(async () => {}) + const log = { debug: vi.fn(), warn: vi.fn(), info: vi.fn(), error: vi.fn() } + const handler = createOpenAIFetchHandler({ + authMode: "native", + spoofMode: "native", + remapDeveloperMessagesToUserEnabled: false, + quietMode: true, + pidOffsetEnabled: false, + headerTransformDebug: false, + compatInputSanitizerEnabled: false, + internalCollaborationModeHeader: "x-opencode-collaboration-mode-kind", + requestSnapshots: { + captureRequest: async () => {}, + captureResponse: async () => {} + }, + sessionAffinityState: { + orchestratorState: createFetchOrchestratorState(), + stickySessionState: createStickySessionState(), + hybridSessionState: createStickySessionState(), + persistSessionAffinityState: () => {} + }, + getCatalogModels: () => undefined, + syncCatalogFromAuth: async () => undefined, + setCooldown, + showToast, + log + }) + + await handler("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "content-type": "application/json", + session_id: "ses_quota_fail_1" + }, + body: JSON.stringify({ model: "gpt-5.3-codex", input: "hello" }) + }) + + await new Promise((resolve) => setTimeout(resolve, 20)) + + await handler("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "content-type": "application/json", + session_id: "ses_quota_fail_2" + }, + body: JSON.stringify({ model: "gpt-5.3-codex", input: "hello again" }) + }) + + expect(usageCalls).toBe(1) + + now += 10_200 + + await handler("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "content-type": "application/json", + session_id: "ses_quota_fail_3" + }, + body: JSON.stringify({ model: "gpt-5.3-codex", input: "hello after retry window" }) + }) + + await vi.waitFor(() => { + expect(usageCalls).toBeGreaterThanOrEqual(2) + expect(log.debug).toHaveBeenCalledWith( + "quota fetch failed", + expect.objectContaining({ endpoint: expect.stringContaining("/wham/usage") }) ) - ).toBe(true) + }) + + nowSpy.mockRestore() }) it("preserves allowed inbound originator in native mode", async () => { From ed2adf96b8974fe93829563438be143b34269b20 Mon Sep 17 00:00:00 2001 From: Bryan Font Date: Tue, 17 Feb 2026 22:11:55 -0500 Subject: [PATCH 2/3] fix(collab): codex-only instruction replacement and agent scoping --- index.ts | 2 - lib/codex-native.ts | 7 +- lib/codex-native/chat-hooks.ts | 41 ++++++------ lib/codex-native/collaboration.ts | 62 +++++++----------- lib/orchestrator-agent.ts | 4 +- test/codex-native-chat-hooks.test.ts | 3 +- test/codex-native-collaboration.test.ts | 35 ++++------ .../codex-native-in-vivo-instructions.test.ts | 36 ++++++----- test/codex-native-spoof-mode.test.ts | 64 +++++++++++++++---- test/helpers/codex-in-vivo.ts | 4 +- test/orchestrator-agent.test.ts | 6 +- 11 files changed, 139 insertions(+), 125 deletions(-) diff --git a/index.ts b/index.ts index 9b3d790..a397b81 100644 --- a/index.ts +++ b/index.ts @@ -13,7 +13,6 @@ import { getCompatInputSanitizerEnabled, getCodexCompactionOverrideEnabled, getBehaviorSettings, - getCollaborationToolProfile, getCollaborationProfileEnabled, getDebugEnabled, getHeaderSnapshotBodiesEnabled, @@ -135,7 +134,6 @@ export const OpenAIMultiAuthPlugin: Plugin = async (input) => { headerTransformDebug: getHeaderTransformDebugEnabled(cfg), collaborationProfileEnabled, orchestratorSubagentsEnabled: getOrchestratorSubagentsEnabled(cfg), - collaborationToolProfile: getCollaborationToolProfile(cfg), behaviorSettings: getBehaviorSettings(cfg) }) diff --git a/lib/codex-native.ts b/lib/codex-native.ts index 099f89b..5a1baf6 100644 --- a/lib/codex-native.ts +++ b/lib/codex-native.ts @@ -11,7 +11,6 @@ import type { PluginRuntimeMode, PromptCacheKeyStrategy } from "./config" -import type { CollaborationToolProfile } from "./codex-native/collaboration" import { formatToastMessage } from "./toast" import type { CodexModelInfo } from "./model-catalog" import { createRequestSnapshots } from "./request-snapshots" @@ -169,7 +168,6 @@ export type CodexAuthPluginOptions = { headerTransformDebug?: boolean collaborationProfileEnabled?: boolean orchestratorSubagentsEnabled?: boolean - collaborationToolProfile?: CollaborationToolProfile } export async function CodexAuthPlugin(input: PluginInput, opts: CodexAuthPluginOptions = {}): Promise { @@ -191,8 +189,6 @@ export async function CodexAuthPlugin(input: PluginInput, opts: CodexAuthPluginO typeof opts.orchestratorSubagentsEnabled === "boolean" ? opts.orchestratorSubagentsEnabled : collaborationProfileEnabled - const collaborationToolProfile: CollaborationToolProfile = - opts.collaborationToolProfile === "codex" ? "codex" : "opencode" void refreshCodexClientVersionFromGitHub(opts.log).catch((error) => { if (error instanceof Error) { // best-effort background refresh @@ -379,8 +375,7 @@ export async function CodexAuthPlugin(input: PluginInput, opts: CodexAuthPluginO fallbackPersonality: opts.personality, spoofMode, collaborationProfileEnabled, - orchestratorSubagentsEnabled, - collaborationToolProfile + orchestratorSubagentsEnabled }) }, "chat.headers": async (hookInput, output) => { diff --git a/lib/codex-native/chat-hooks.ts b/lib/codex-native/chat-hooks.ts index 3e1cc25..58f18b5 100644 --- a/lib/codex-native/chat-hooks.ts +++ b/lib/codex-native/chat-hooks.ts @@ -23,17 +23,13 @@ import { sessionUsesOpenAIProvider } from "./session-messages" import { - CODEX_CODE_MODE_INSTRUCTIONS, - CODEX_ORCHESTRATOR_INSTRUCTIONS, - ensureOpenCodeToolingCompatibility, getCodexPlanModeInstructions, isOrchestratorInstructions, mergeInstructions, - resolveCollaborationInstructions, + replaceCodexToolCallsForOpenCode, + resolveHookAgentName, resolveCollaborationProfile, - resolveSubagentHeaderValue, - resolveToolingInstructions, - type CollaborationToolProfile + resolveSubagentHeaderValue } from "./collaboration" function normalizeVerbositySetting(value: unknown): "default" | "low" | "medium" | "high" | undefined { @@ -83,7 +79,6 @@ export async function handleChatParamsHook(input: { spoofMode: CodexSpoofMode collaborationProfileEnabled: boolean orchestratorSubagentsEnabled: boolean - collaborationToolProfile: CollaborationToolProfile }): Promise { if (input.hookInput.model.providerID !== "openai") return const modelOptions = isRecord(input.hookInput.model.options) ? input.hookInput.model.options : {} @@ -159,25 +154,31 @@ export async function handleChatParamsHook(input: { output: input.output }) - input.output.options.instructions = ensureOpenCodeToolingCompatibility(asString(input.output.options.instructions)) + if (input.spoofMode !== "codex") return + + const normalizedAgentName = resolveHookAgentName(input.hookInput.agent)?.trim().toLowerCase() + if (normalizedAgentName === "build") { + const current = asString(input.output.options.instructions) + const replaced = replaceCodexToolCallsForOpenCode(current) + if (replaced) { + input.output.options.instructions = replaced + } + return + } if (!input.collaborationProfileEnabled) return if (!profile.enabled || !profile.kind) return - const collaborationInstructions = resolveCollaborationInstructions(profile.kind, { - plan: getCodexPlanModeInstructions(), - code: CODEX_CODE_MODE_INSTRUCTIONS - }) - let mergedInstructions = mergeInstructions(asString(input.output.options.instructions), collaborationInstructions) - - if (profile.isOrchestrator && input.orchestratorSubagentsEnabled) { - mergedInstructions = mergeInstructions(mergedInstructions, CODEX_ORCHESTRATOR_INSTRUCTIONS) + if (profile.instructionPreset === "plan") { + const replacedPlan = replaceCodexToolCallsForOpenCode(getCodexPlanModeInstructions()) ?? getCodexPlanModeInstructions() + input.output.options.instructions = mergeInstructions( + asString(input.output.options.instructions), + replacedPlan + ) + return } - mergedInstructions = mergeInstructions(mergedInstructions, resolveToolingInstructions(input.collaborationToolProfile)) - - input.output.options.instructions = mergedInstructions } export async function handleChatHeadersHook(input: { diff --git a/lib/codex-native/collaboration.ts b/lib/codex-native/collaboration.ts index 63b0baa..ef8e32b 100644 --- a/lib/codex-native/collaboration.ts +++ b/lib/codex-native/collaboration.ts @@ -1,11 +1,11 @@ export type CodexCollaborationModeKind = "plan" | "code" -export type CollaborationToolProfile = "opencode" | "codex" export type CodexCollaborationProfile = { enabled: boolean kind?: CodexCollaborationModeKind normalizedAgentName?: string isOrchestrator?: boolean + instructionPreset?: "plan" } export type CollaborationInstructionsByKind = { @@ -142,7 +142,8 @@ export function getCodexPlanModeInstructions(): string { export function setCodexPlanModeInstructions(next: string | undefined): void { const trimmed = next?.trim() - codexPlanModeInstructions = trimmed ? trimmed : CODEX_PLAN_MODE_INSTRUCTIONS_FALLBACK + const source = trimmed ? trimmed : CODEX_PLAN_MODE_INSTRUCTIONS_FALLBACK + codexPlanModeInstructions = replaceCodexToolCallsForOpenCode(source) ?? source } export const CODEX_CODE_MODE_INSTRUCTIONS = "you are now in code mode." @@ -155,31 +156,21 @@ When subagents are available, delegate independent work in parallel, coordinate When subagents are active, your primary role is coordination and synthesis; avoid doing worker implementation in parallel with active workers unless needed for unblock/fallback.` -const OPENCODE_TOOLING_TRANSLATION_INSTRUCTIONS = `# Tooling Compatibility (OpenCode) - -Translate Codex-style tool intent to OpenCode-native tools: - -- exec_command -> bash -- read/search/list -> read, grep, glob -- apply_patch/edit_file -> apply_patch -- write_stdin -> task with existing task_id (or bash fallback) -- spawn_agent -> task (launch a subagent) -- send_input -> task with existing task_id (continue the same subagent) -- wait -> do not return final output until spawned task(s) complete; poll/resume via task tool as needed -- close_agent -> stop reusing task_id (no dedicated close tool in OpenCode) - -Always use the available OpenCode tool names and schemas in this runtime.` - -const CODEX_STYLE_TOOLING_INSTRUCTIONS = `# Tooling Compatibility (Codex-style) - -Prefer Codex-style workflow semantics and naming when reasoning about steps. If an exact Codex tool is unavailable, fall back to the nearest OpenCode equivalent and continue.` - -const OPENCODE_TOOLING_HEADER = "# Tooling Compatibility (OpenCode)" -const OPENCODE_TOOLING_HEADER_REGEX = /^\s{0,3}#{1,6}\s*tooling\s+compatibility\s*\(\s*opencode\s*\)\s*$/im - const CODEX_TOOL_NAME_REGEX = /\b(exec_command|read_file|search_files|list_dir|write_stdin|spawn_agent|send_input|close_agent|edit_file|apply_patch)\b/i +const TOOL_CALL_REPLACEMENTS: Array<{ pattern: RegExp; replacement: string }> = [ + { pattern: /\bexec_command\b/gi, replacement: "bash" }, + { pattern: /\bread_file\b/gi, replacement: "read" }, + { pattern: /\bsearch_files\b/gi, replacement: "grep" }, + { pattern: /\blist_dir\b/gi, replacement: "glob" }, + { pattern: /\bwrite_stdin\b/gi, replacement: "task" }, + { pattern: /\bspawn_agent\b/gi, replacement: "task" }, + { pattern: /\bsend_input\b/gi, replacement: "task" }, + { pattern: /\bclose_agent\b/gi, replacement: "skip_task_reuse" }, + { pattern: /\bedit_file\b/gi, replacement: "apply_patch" } +] + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value) } @@ -237,7 +228,8 @@ export function resolveCollaborationProfile(agent: unknown): CodexCollaborationP enabled: true, normalizedAgentName, kind: "plan", - isOrchestrator: false + isOrchestrator: false, + instructionPreset: "plan" } } @@ -275,27 +267,21 @@ export function resolveCollaborationInstructions( return instructions.code } -export function resolveToolingInstructions(profile: CollaborationToolProfile): string { - return profile === "codex" ? CODEX_STYLE_TOOLING_INSTRUCTIONS : OPENCODE_TOOLING_TRANSLATION_INSTRUCTIONS -} - export function hasCodexToolNameMarkers(instructions: string | undefined): boolean { if (!instructions) return false return CODEX_TOOL_NAME_REGEX.test(instructions) } -export function hasOpenCodeToolingCompatibility(instructions: string | undefined): boolean { - if (!instructions) return false - if (instructions.includes(OPENCODE_TOOLING_HEADER)) return true - return OPENCODE_TOOLING_HEADER_REGEX.test(instructions) -} - -export function ensureOpenCodeToolingCompatibility(instructions: string | undefined): string | undefined { +export function replaceCodexToolCallsForOpenCode(instructions: string | undefined): string | undefined { const normalized = instructions?.trim() if (!normalized) return instructions - if (hasOpenCodeToolingCompatibility(normalized)) return instructions if (!hasCodexToolNameMarkers(normalized)) return instructions - return mergeInstructions(normalized, resolveToolingInstructions("opencode")) + + let out = normalized + for (const replacement of TOOL_CALL_REPLACEMENTS) { + out = out.replace(replacement.pattern, replacement.replacement) + } + return out } export function mergeInstructions(base: string | undefined, extra: string): string { diff --git a/lib/orchestrator-agent.ts b/lib/orchestrator-agent.ts index c67928f..e0946ed 100644 --- a/lib/orchestrator-agent.ts +++ b/lib/orchestrator-agent.ts @@ -3,6 +3,7 @@ import os from "node:os" import path from "node:path" import { refreshCachedCodexPrompts } from "./codex-prompts-cache" +import { replaceCodexToolCallsForOpenCode } from "./codex-native/collaboration" export const CODEX_ORCHESTRATOR_AGENT_FILE = "orchestrator.md" export const CODEX_ORCHESTRATOR_AGENT_FILE_DISABLED = `${CODEX_ORCHESTRATOR_AGENT_FILE}.disabled` @@ -136,7 +137,8 @@ function stripLeadingFrontmatter(content: string): string { function composeTemplateFromPrompt(prompt: string): string { const normalizedPrompt = stripLeadingFrontmatter(prompt) - return `${ORCHESTRATOR_FRONTMATTER}\n${normalizedPrompt}\n` + const replacedPrompt = replaceCodexToolCallsForOpenCode(normalizedPrompt) ?? normalizedPrompt + return `${ORCHESTRATOR_FRONTMATTER}\n${replacedPrompt}\n` } async function resolveOrchestratorAgentTemplate(cacheDir?: string): Promise { diff --git a/test/codex-native-chat-hooks.test.ts b/test/codex-native-chat-hooks.test.ts index 1e3422f..00e07a3 100644 --- a/test/codex-native-chat-hooks.test.ts +++ b/test/codex-native-chat-hooks.test.ts @@ -46,8 +46,7 @@ describe("codex-native chat hooks instruction source order", () => { ], spoofMode: "codex", collaborationProfileEnabled: false, - orchestratorSubagentsEnabled: false, - collaborationToolProfile: "opencode" + orchestratorSubagentsEnabled: false }) expect(output.options.instructions).toBe("Cached template instructions") diff --git a/test/codex-native-collaboration.test.ts b/test/codex-native-collaboration.test.ts index 3b74224..c662446 100644 --- a/test/codex-native-collaboration.test.ts +++ b/test/codex-native-collaboration.test.ts @@ -1,15 +1,13 @@ import { describe, expect, it } from "vitest" import { - ensureOpenCodeToolingCompatibility, hasCodexToolNameMarkers, - hasOpenCodeToolingCompatibility, isOrchestratorInstructions, mergeInstructions, + replaceCodexToolCallsForOpenCode, resolveCollaborationInstructions, resolveCollaborationProfile, - resolveSubagentHeaderValue, - resolveToolingInstructions + resolveSubagentHeaderValue } from "../lib/codex-native/collaboration" describe("codex collaboration profile", () => { @@ -25,9 +23,9 @@ describe("codex collaboration profile", () => { expect(profile.kind).toBe("code") }) - it("does not enable profile for unrelated build agent", () => { + it("does not map build agent to plan preset", () => { const profile = resolveCollaborationProfile("build") - expect(profile.enabled).toBe(false) + expect(profile.instructionPreset).toBeUndefined() }) it("maps codex review helper to review subagent header", () => { @@ -54,33 +52,22 @@ describe("codex collaboration profile", () => { expect(mergeInstructions(merged, "extra")).toBe("base\n\nextra") }) - it("resolves tooling profiles", () => { - expect(resolveToolingInstructions("opencode")).toContain("Tooling Compatibility (OpenCode)") - expect(resolveToolingInstructions("codex")).toContain("Tooling Compatibility (Codex-style)") - }) - - it("detects codex tool names and injects OpenCode compatibility guidance", () => { + it("detects codex tool names and replaces with OpenCode tool names", () => { const codexInstructions = "Use spawn_agent and send_input to coordinate workers." expect(hasCodexToolNameMarkers(codexInstructions)).toBe(true) - expect(ensureOpenCodeToolingCompatibility(codexInstructions)).toContain("Tooling Compatibility (OpenCode)") + expect(replaceCodexToolCallsForOpenCode(codexInstructions)).toContain("task") + expect(replaceCodexToolCallsForOpenCode(codexInstructions)).not.toContain("spawn_agent") - const toolCompat = ensureOpenCodeToolingCompatibility(codexInstructions) - expect(toolCompat).toContain("Tooling Compatibility (OpenCode)") - expect(hasOpenCodeToolingCompatibility(toolCompat)).toBe(true) + const replaced = replaceCodexToolCallsForOpenCode(codexInstructions) + expect(replaced).toContain("task") const writeStdin = "If needed, call write_stdin to continue the worker session." expect(hasCodexToolNameMarkers(writeStdin)).toBe(true) - expect(ensureOpenCodeToolingCompatibility(writeStdin)).toContain("write_stdin -> task with existing task_id") + expect(replaceCodexToolCallsForOpenCode(writeStdin)).toContain("task") const plainInstructions = "Use available tools in this runtime." expect(hasCodexToolNameMarkers(plainInstructions)).toBe(false) - expect(ensureOpenCodeToolingCompatibility(plainInstructions)).toBe(plainInstructions) - - const alreadyCompatible = `${codexInstructions}\n\n# Tooling Compatibility (OpenCode)` - expect(ensureOpenCodeToolingCompatibility(alreadyCompatible)).toBe(alreadyCompatible) - - const alreadyCompatibleCaseVariant = `${codexInstructions}\n\n## tooling compatibility (opencode)` - expect(ensureOpenCodeToolingCompatibility(alreadyCompatibleCaseVariant)).toBe(alreadyCompatibleCaseVariant) + expect(replaceCodexToolCallsForOpenCode(plainInstructions)).toBe(plainInstructions) }) it("detects orchestrator-style upstream instructions", () => { diff --git a/test/codex-native-in-vivo-instructions.test.ts b/test/codex-native-in-vivo-instructions.test.ts index a4f7f76..28e75a4 100644 --- a/test/codex-native-in-vivo-instructions.test.ts +++ b/test/codex-native-in-vivo-instructions.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest" import { runCodexInVivoInstructionProbe } from "./helpers/codex-in-vivo" -describe("codex-native in-vivo instruction injection", () => { +describe("codex-native in-vivo instruction replacement", () => { it("replaces host instructions and sends personality-rendered instructions in codex mode", async () => { const result = await runCodexInVivoInstructionProbe({ hostInstructions: "OpenCode Host Instructions", @@ -33,22 +33,17 @@ describe("codex-native in-vivo instruction injection", () => { expect(result.outboundInstructions).not.toBe("OpenCode Host Instructions") }) - it("injects codex-to-opencode tool call replacements for orchestrator prompts", async () => { + it("keeps catalog instructions for orchestrator requests in runtime transforms", async () => { const result = await runCodexInVivoInstructionProbe({ hostInstructions: "OpenCode Host Instructions", personalityKey: "vivo_persona", personalityText: "Vivo Persona Voice", agent: "orchestrator", collaborationProfileEnabled: true, - orchestratorSubagentsEnabled: true, - collaborationToolProfile: "opencode" + orchestratorSubagentsEnabled: true }) - expect(result.outboundInstructions).toContain("# Sub-agents") - expect(result.outboundInstructions).toContain("spawn_agent -> task") - expect(result.outboundInstructions).toContain("send_input -> task with existing task_id") - expect(result.outboundInstructions).toContain("wait -> do not return final output") - expect(result.outboundInstructions).toContain("close_agent -> stop reusing task_id") + expect(result.outboundInstructions).toContain("Base Vivo Persona Voice") }) it("keeps orchestrator agent instructions instead of replacing them with model base instructions", async () => { @@ -63,8 +58,7 @@ describe("codex-native in-vivo instruction injection", () => { personalityText: "Vivo Persona Voice", agent: "orchestrator", collaborationProfileEnabled: true, - orchestratorSubagentsEnabled: true, - collaborationToolProfile: "opencode" + orchestratorSubagentsEnabled: true }) expect(result.preflightInstructions).toContain("You are Codex, a coding agent based on GPT-5.") @@ -73,14 +67,13 @@ describe("codex-native in-vivo instruction injection", () => { expect(result.outboundInstructions).not.toContain("Base Vivo Persona Voice") }) - it("injects plan mode semantics plus tool replacements for plan agent", async () => { + it("uses plan mode instructions from codex source with tool replacements", async () => { const result = await runCodexInVivoInstructionProbe({ hostInstructions: "OpenCode Host Instructions", personalityKey: "vivo_persona", personalityText: "Vivo Persona Voice", agent: "plan", - collaborationProfileEnabled: true, - collaborationToolProfile: "opencode" + collaborationProfileEnabled: true }) expect(result.outboundInstructions).toContain("# Plan Mode (Conversational)") @@ -88,6 +81,19 @@ describe("codex-native in-vivo instruction injection", () => { expect(result.outboundInstructions).toContain( "Before asking the user any question, perform at least one targeted non-mutating exploration pass" ) - expect(result.outboundInstructions).toContain("spawn_agent -> task") + expect(result.outboundInstructions).toContain("request_user_input") + }) + + it("replaces build agent instructions in codex mode", async () => { + const result = await runCodexInVivoInstructionProbe({ + hostInstructions: "OpenCode Host Instructions", + personalityKey: "vivo_persona", + personalityText: "Vivo Persona Voice", + agent: "build", + collaborationProfileEnabled: true + }) + + expect(result.outboundInstructions).toContain("Base Vivo Persona Voice") + expect(result.outboundInstructions).not.toContain("spawn_agent") }) }) diff --git a/test/codex-native-spoof-mode.test.ts b/test/codex-native-spoof-mode.test.ts index cc36ca6..27a6757 100644 --- a/test/codex-native-spoof-mode.test.ts +++ b/test/codex-native-spoof-mode.test.ts @@ -768,10 +768,11 @@ describe("codex-native spoof + params hooks", () => { await chatParams?.(input, output) expect(output.options.instructions).toContain("Catalog instructions") expect(output.options.instructions).toContain("# Plan Mode (Conversational)") - expect(output.options.instructions).toContain("Tooling Compatibility (OpenCode)") + expect(output.options.instructions).toContain("request_user_input") + expect(output.options.instructions).not.toContain("Tooling Compatibility (OpenCode)") }) - it("does not enable codex collaboration profile for native OpenCode agents", async () => { + it("replaces build agent instructions in codex mode without execute preset", async () => { const hooks = await CodexAuthPlugin({} as never, { spoofMode: "codex" }) const chatParams = hooks["chat.params"] expect(chatParams).toBeTypeOf("function") @@ -801,12 +802,51 @@ describe("codex-native spoof + params hooks", () => { } await chatParams?.(input, output) - expect(output.options.instructions).toBe("Catalog instructions") - expect(output.options.instructions).not.toContain("you are now in code mode.") + expect(output.options.instructions).toContain("Catalog instructions") + expect(output.options.instructions).not.toContain("# Collaboration Style: Execute") expect(output.options.instructions).not.toContain("# Plan Mode") expect(output.options.reasoningEffort).toBe("high") }) + it("keeps build-agent instruction replacement active in codex mode when collaboration profile is disabled", async () => { + const hooks = await CodexAuthPlugin({} as never, { + spoofMode: "codex", + collaborationProfileEnabled: false + }) + const chatParams = hooks["chat.params"] + expect(chatParams).toBeTypeOf("function") + + const input = { + sessionID: "ses_build_no_collab", + agent: "build", + provider: {}, + message: {}, + model: { + providerID: "openai", + capabilities: { toolcall: true }, + options: { + codexInstructions: "Use spawn_agent and send_input with write_stdin", + codexRuntimeDefaults: { + defaultReasoningEffort: "high" + } + } + } + } as unknown as Parameters>[0] + + const output = { + temperature: 0, + topP: 1, + topK: 0, + options: {} + } + + await chatParams?.(input, output) + expect(output.options.instructions).toContain("task") + expect(output.options.instructions).not.toContain("spawn_agent") + expect(output.options.instructions).not.toContain("send_input") + expect(output.options.instructions).not.toContain("write_stdin") + }) + it("uses native headers by default (legacy-plugin style)", async () => { const hooks = await CodexAuthPlugin({} as never) const chatHeaders = hooks["chat.headers"] @@ -964,15 +1004,14 @@ describe("codex-native spoof + params hooks", () => { expect(output.headers["x-opencode-collaboration-mode-kind"]).toBeUndefined() }) - it("allows collaboration injection in native mode when explicitly enabled", async () => { + it("allows collaboration headers in native mode without runtime instruction injection", async () => { const hooks = await CodexAuthPlugin( {} as never, { spoofMode: "native", mode: "native", collaborationProfileEnabled: true, - orchestratorSubagentsEnabled: true, - collaborationToolProfile: "codex" + orchestratorSubagentsEnabled: true } as never ) const chatParams = hooks["chat.params"] @@ -1003,8 +1042,8 @@ describe("codex-native spoof + params hooks", () => { await chatParams?.(paramsInput, paramsOutput) expect(paramsOutput.options.instructions).toContain("Catalog instructions") - expect(paramsOutput.options.instructions).toContain("# Sub-agents") - expect(paramsOutput.options.instructions).toContain("Tooling Compatibility (Codex-style)") + expect(paramsOutput.options.instructions).not.toContain("# Sub-agents") + expect(paramsOutput.options.instructions).not.toContain("# Collaboration Style: Execute") const headersInput = { sessionID: "ses_native_collab_headers", @@ -1075,7 +1114,7 @@ describe("codex-native spoof + params hooks", () => { expect(output.options.instructions).toContain("# Plan Mode (Conversational)") }) - it("injects orchestrator collaboration instructions when experimental collaboration profile is enabled", async () => { + it("does not append orchestrator profile instructions at runtime", async () => { const hooks = await CodexAuthPlugin( {} as never, { @@ -1112,7 +1151,7 @@ describe("codex-native spoof + params hooks", () => { await chatParams?.(input, output) expect(output.options.instructions).toContain("Catalog instructions") - expect(output.options.instructions).toContain("# Sub-agents") + expect(output.options.instructions).toContain("Catalog instructions") }) it("preserves orchestrator instructions instead of replacing with model base instructions", async () => { @@ -1160,7 +1199,8 @@ describe("codex-native spoof + params hooks", () => { expect(output.options.instructions).toContain("You are Codex, a coding agent based on GPT-5.") expect(output.options.instructions).toContain("# Sub-agents") - expect(output.options.instructions).toContain("Tooling Compatibility (OpenCode)") + expect(output.options.instructions).toContain("spawn_agent") + expect(output.options.instructions).not.toContain("Tooling Compatibility (OpenCode)") expect(output.options.instructions).not.toContain("Catalog instructions") }) diff --git a/test/helpers/codex-in-vivo.ts b/test/helpers/codex-in-vivo.ts index 3b0590a..aaa144a 100644 --- a/test/helpers/codex-in-vivo.ts +++ b/test/helpers/codex-in-vivo.ts @@ -14,7 +14,6 @@ type InVivoProbeInput = { agent?: string collaborationProfileEnabled?: boolean orchestratorSubagentsEnabled?: boolean - collaborationToolProfile?: "opencode" | "codex" stripModelOptionsBeforeParams?: boolean modelInstructionsFallback?: string omitModelIdentityBeforeParams?: boolean @@ -165,8 +164,7 @@ export async function runCodexInVivoInstructionProbe(input: InVivoProbeInput): P spoofMode: "codex", behaviorSettings: { global: { personality: input.personalityKey } }, collaborationProfileEnabled: input.collaborationProfileEnabled, - orchestratorSubagentsEnabled: input.orchestratorSubagentsEnabled, - collaborationToolProfile: input.collaborationToolProfile + orchestratorSubagentsEnabled: input.orchestratorSubagentsEnabled }) const provider = { diff --git a/test/orchestrator-agent.test.ts b/test/orchestrator-agent.test.ts index 62be062..787e532 100644 --- a/test/orchestrator-agent.test.ts +++ b/test/orchestrator-agent.test.ts @@ -45,7 +45,9 @@ describe("orchestrator agent installer", () => { ) expect(firstContent).toContain("mode: primary") expect(firstContent).toContain("You are Codex, a coding agent based on GPT-5.") - expect(firstContent).toContain("If `spawn_agent` is unavailable or fails, ignore this section and proceed solo.") + expect(firstContent).toContain("If `task` is unavailable or fails, ignore this section and proceed solo.") + expect(firstContent).not.toContain("spawn_agent") + expect(firstContent).toContain("task") const cacheRaw = await fs.readFile(path.join(cacheDir, CODEX_PROMPTS_CACHE_FILE), "utf8") const cache = JSON.parse(cacheRaw) as { @@ -54,7 +56,7 @@ describe("orchestrator agent installer", () => { expect(cache.prompts?.orchestrator).toContain("You are Codex, a coding agent based on GPT-5.") const metaRaw = await fs.readFile(path.join(cacheDir, CODEX_PROMPTS_CACHE_META_FILE), "utf8") - const meta = JSON.parse(metaRaw) as { urls?: { orchestrator?: string; plan?: string } } + const meta = JSON.parse(metaRaw) as { urls?: { orchestrator?: string; plan?: string; build?: string } } expect(meta.urls?.orchestrator).toContain("templates/agents/orchestrator.md") expect(meta.urls?.plan).toContain("templates/collaboration_mode/plan.md") }) From 0f811c71524e378c64d499012923cc54a7173501 Mon Sep 17 00:00:00 2001 From: Bryan Font Date: Tue, 17 Feb 2026 22:12:01 -0500 Subject: [PATCH 3/3] chore(config): remove unused collaboration tool profile --- CHANGELOG.md | 1 - docs/configuration.md | 5 ----- docs/development/CONFIG_FIELDS.md | 3 --- lib/config.ts | 28 ++-------------------------- schemas/codex-config.schema.json | 4 ---- test/config.test.ts | 12 +----------- 6 files changed, 3 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fedd8f9..ef1ec22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ All notable changes to this project will be documented in this file. - Added experimental Codex collaboration profile gates (`runtime.collaborationProfile`, `runtime.orchestratorSubagents`) for plan/orchestrator parity. - Collaboration features now auto-enable by default in `runtime.mode="codex"` and can be explicitly enabled/disabled in any mode. -- Added `runtime.collaborationToolProfile` (`opencode` | `codex`) to choose OpenCode tool translation guidance vs codex-style tool semantics in injected collaboration instructions. - Added managed `orchestrator` agent template sync under `~/.config/opencode/agents`, with visibility auto-gated by runtime mode. - Synced pinned upstream Codex orchestrator + plan templates into a local prompt cache (ETag/304-aware, TTL refreshed) and used the cached plan prompt to populate plan-mode collaboration instructions. - Added configurable `runtime.promptCacheKeyStrategy` (`default` | `project`) for session-based or project-path-based prompt cache keying. diff --git a/docs/configuration.md b/docs/configuration.md index 6a6f169..c258232 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -106,10 +106,6 @@ Known-field type validation is applied on load. If a known field has an invalid - Experimental: enables Codex-style subagent header hints for helper agents under collaboration profile mode. - If omitted, inherits `runtime.collaborationProfile` effective value. - Explicit `true`/`false` works in any mode. -- `runtime.collaborationToolProfile: "opencode" | "codex"` - - Controls tool-language guidance in injected collaboration instructions. - - `opencode` (default): translates Codex semantics to OpenCode tool names. - - `codex`: prefers Codex tool semantics and falls back to OpenCode equivalents. ### Model behavior @@ -230,7 +226,6 @@ Advanced path: - `OPENCODE_OPENAI_MULTI_HEADER_TRANSFORM_DEBUG`: `1|0|true|false`. - `OPENCODE_OPENAI_MULTI_COLLABORATION_PROFILE`: `1|0|true|false`. - `OPENCODE_OPENAI_MULTI_ORCHESTRATOR_SUBAGENTS`: `1|0|true|false`. -- `OPENCODE_OPENAI_MULTI_COLLABORATION_TOOL_PROFILE`: `opencode|codex`. ### Debug/OAuth controls diff --git a/docs/development/CONFIG_FIELDS.md b/docs/development/CONFIG_FIELDS.md index 9860707..8fc66e8 100644 --- a/docs/development/CONFIG_FIELDS.md +++ b/docs/development/CONFIG_FIELDS.md @@ -29,7 +29,6 @@ Top-level: - `runtime.pidOffset: boolean` - `runtime.collaborationProfile: boolean` - `runtime.orchestratorSubagents: boolean` -- `runtime.collaborationToolProfile: "opencode" | "codex"` - `global.personality: string` - `global.thinkingSummaries: boolean` - `global.verbosityEnabled: boolean` @@ -67,7 +66,6 @@ Default generated values: - `runtime.pidOffset: false` - `runtime.collaborationProfile`: mode-derived when unset (`true` in `codex`, `false` in `native`) - `runtime.orchestratorSubagents`: inherits `runtime.collaborationProfile` effective value when unset -- `runtime.collaborationToolProfile: "opencode"` - `global.personality: "pragmatic"` - `global.verbosityEnabled: true` - `global.verbosity: "default"` @@ -115,7 +113,6 @@ Resolved by `resolveConfig`: - `OPENCODE_OPENAI_MULTI_PROACTIVE_REFRESH_BUFFER_MS` - `OPENCODE_OPENAI_MULTI_COLLABORATION_PROFILE` - `OPENCODE_OPENAI_MULTI_ORCHESTRATOR_SUBAGENTS` -- `OPENCODE_OPENAI_MULTI_COLLABORATION_TOOL_PROFILE` Resolved by auth/runtime code (`lib/codex-native.ts` + helper modules under `lib/codex-native/`): diff --git a/lib/config.ts b/lib/config.ts index 3e0fcac..d90e634 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -10,7 +10,6 @@ export type CodexSpoofMode = "native" | "codex" export type PluginRuntimeMode = "native" | "codex" export type VerbosityOption = "default" | "low" | "medium" | "high" export type PromptCacheKeyStrategy = "default" | "project" -export type CollaborationToolProfile = "opencode" | "codex" export type ModelBehaviorOverride = { personality?: PersonalityOption @@ -47,7 +46,6 @@ export type PluginConfig = { promptCacheKeyStrategy?: PromptCacheKeyStrategy collaborationProfileEnabled?: boolean orchestratorSubagentsEnabled?: boolean - collaborationToolProfile?: CollaborationToolProfile behaviorSettings?: BehaviorSettings } @@ -155,8 +153,7 @@ const DEFAULT_CODEX_CONFIG_TEMPLATE = `{ // Experimental collaboration controls (optional): // "collaborationProfile": true, - // "orchestratorSubagents": true, - // "collaborationToolProfile": "opencode" // "opencode" | "codex" + // "orchestratorSubagents": true }, "global": { @@ -312,8 +309,7 @@ export function validateConfigFileObject(raw: unknown): ConfigValidationResult { const enumChecks: Array<{ field: string; allowed: string[] }> = [ { field: "mode", allowed: ["native", "codex"] }, { field: "rotationStrategy", allowed: ["sticky", "hybrid", "round_robin"] }, - { field: "promptCacheKeyStrategy", allowed: ["default", "project"] }, - { field: "collaborationToolProfile", allowed: ["opencode", "codex"] } + { field: "promptCacheKeyStrategy", allowed: ["default", "project"] } ] for (const check of enumChecks) { const value = runtime[check.field] @@ -509,15 +505,6 @@ function parsePromptCacheKeyStrategy(value: unknown): PromptCacheKeyStrategy | u return undefined } -function parseCollaborationToolProfile(value: unknown): CollaborationToolProfile | undefined { - if (typeof value !== "string") return undefined - const normalized = value.trim().toLowerCase() - if (normalized === "opencode" || normalized === "codex") { - return normalized - } - return undefined -} - function normalizeVerbosityOption(value: unknown): VerbosityOption | undefined { if (typeof value !== "string") return undefined const normalized = value.trim().toLowerCase() @@ -723,9 +710,6 @@ function parseConfigFileObject(raw: unknown): Partial { isRecord(raw.runtime) && typeof raw.runtime.orchestratorSubagents === "boolean" ? raw.runtime.orchestratorSubagents : undefined - const collaborationToolProfile = parseCollaborationToolProfile( - isRecord(raw.runtime) ? raw.runtime.collaborationToolProfile : undefined - ) return { debug, @@ -746,7 +730,6 @@ function parseConfigFileObject(raw: unknown): Partial { headerTransformDebug, collaborationProfileEnabled, orchestratorSubagentsEnabled, - collaborationToolProfile, behaviorSettings } } @@ -912,8 +895,6 @@ export function resolveConfig(input: { parseEnvBoolean(env.OPENCODE_OPENAI_MULTI_COLLABORATION_PROFILE) ?? file.collaborationProfileEnabled const orchestratorSubagentsEnabled = parseEnvBoolean(env.OPENCODE_OPENAI_MULTI_ORCHESTRATOR_SUBAGENTS) ?? file.orchestratorSubagentsEnabled - const collaborationToolProfile = - parseCollaborationToolProfile(env.OPENCODE_OPENAI_MULTI_COLLABORATION_TOOL_PROFILE) ?? file.collaborationToolProfile return { ...file, @@ -935,7 +916,6 @@ export function resolveConfig(input: { headerTransformDebug, collaborationProfileEnabled, orchestratorSubagentsEnabled, - collaborationToolProfile, behaviorSettings: resolvedBehaviorSettings } } @@ -1022,10 +1002,6 @@ export function getOrchestratorSubagentsEnabled(cfg: PluginConfig): boolean { return getCollaborationProfileEnabled(cfg) } -export function getCollaborationToolProfile(cfg: PluginConfig): CollaborationToolProfile { - return cfg.collaborationToolProfile === "codex" ? "codex" : "opencode" -} - export function getBehaviorSettings(cfg: PluginConfig): BehaviorSettings | undefined { return cfg.behaviorSettings } diff --git a/schemas/codex-config.schema.json b/schemas/codex-config.schema.json index 809cac3..f9a415d 100644 --- a/schemas/codex-config.schema.json +++ b/schemas/codex-config.schema.json @@ -70,10 +70,6 @@ }, "orchestratorSubagents": { "type": "boolean" - }, - "collaborationToolProfile": { - "type": "string", - "enum": ["opencode", "codex"] } } }, diff --git a/test/config.test.ts b/test/config.test.ts index f611bbb..ef57283 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -9,7 +9,6 @@ import { ensureDefaultConfigFile, getCompatInputSanitizerEnabled, getCollaborationProfileEnabled, - getCollaborationToolProfile, getCodexCompactionOverrideEnabled, getBehaviorSettings, getDebugEnabled, @@ -232,13 +231,6 @@ describe("config loading", () => { expect(getOrchestratorSubagentsEnabled(disabled)).toBe(false) }) - it("parses collaboration tooling profile from env", () => { - const codex = resolveConfig({ env: { OPENCODE_OPENAI_MULTI_COLLABORATION_TOOL_PROFILE: "codex" } }) - const opencode = resolveConfig({ env: { OPENCODE_OPENAI_MULTI_COLLABORATION_TOOL_PROFILE: "opencode" } }) - expect(getCollaborationToolProfile(codex)).toBe("codex") - expect(getCollaborationToolProfile(opencode)).toBe("opencode") - }) - it("reads personality + behavior settings from file config", () => { const cfg = resolveConfig({ env: {}, @@ -412,8 +404,7 @@ describe("config file loading", () => { headerTransformDebug: true, pidOffset: true, collaborationProfile: true, - orchestratorSubagents: true, - collaborationToolProfile: "codex" + orchestratorSubagents: true }, global: { thinkingSummaries: true, @@ -453,7 +444,6 @@ describe("config file loading", () => { expect(loaded.pidOffsetEnabled).toBe(true) expect(loaded.collaborationProfileEnabled).toBe(true) expect(loaded.orchestratorSubagentsEnabled).toBe(true) - expect(loaded.collaborationToolProfile).toBe("codex") expect(loaded.rotationStrategy).toBe("hybrid") expect(loaded.promptCacheKeyStrategy).toBe("project") expect(loaded.mode).toBe("codex")