mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Merge remote-tracking branch 'origin/main' into pakrym/core-cwd-absolutepathbuf
# Conflicts: # codex-rs/app-server/src/codex_message_processor.rs
This commit is contained in:
@@ -2857,6 +2857,48 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeStartTransport": {
|
||||
"description": "EXPERIMENTAL - transport used by thread realtime.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"websocket"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"description": "SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webrtc"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"type"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ThreadResumeParams": {
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
"properties": {
|
||||
|
||||
@@ -3304,6 +3304,22 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeSdpNotification": {
|
||||
"description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.",
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeStartedNotification": {
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
"properties": {
|
||||
@@ -4927,6 +4943,26 @@
|
||||
"title": "Thread/realtime/outputAudio/deltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/realtime/sdp"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadRealtimeSdpNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
@@ -4335,6 +4335,26 @@
|
||||
"title": "Thread/realtime/outputAudio/deltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/realtime/sdp"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadRealtimeSdpNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -9288,6 +9308,13 @@
|
||||
},
|
||||
"Model": {
|
||||
"properties": {
|
||||
"additionalSpeedTiers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"availabilityNux": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -13655,6 +13682,66 @@
|
||||
"title": "ThreadRealtimeOutputAudioDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeSdpNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.",
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadRealtimeSdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeStartTransport": {
|
||||
"description": "EXPERIMENTAL - transport used by thread realtime.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"websocket"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"description": "SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webrtc"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"type"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ThreadRealtimeStartedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
|
||||
@@ -6111,6 +6111,13 @@
|
||||
},
|
||||
"Model": {
|
||||
"properties": {
|
||||
"additionalSpeedTiers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"availabilityNux": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -9436,6 +9443,26 @@
|
||||
"title": "Thread/realtime/outputAudio/deltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/realtime/sdp"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadRealtimeSdpNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/realtime/sdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -11510,6 +11537,66 @@
|
||||
"title": "ThreadRealtimeOutputAudioDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeSdpNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.",
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadRealtimeSdpNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeStartTransport": {
|
||||
"description": "EXPERIMENTAL - transport used by thread realtime.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"websocket"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "WebsocketThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"description": "SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webrtc"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransportType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"type"
|
||||
],
|
||||
"title": "WebrtcThreadRealtimeStartTransport",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ThreadRealtimeStartedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.",
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
},
|
||||
"Model": {
|
||||
"properties": {
|
||||
"additionalSpeedTiers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"availabilityNux": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.",
|
||||
"properties": {
|
||||
"sdp": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sdp",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadRealtimeSdpNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import type { ThreadRealtimeClosedNotification } from "./v2/ThreadRealtimeClosed
|
||||
import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNotification";
|
||||
import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification";
|
||||
import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification";
|
||||
import type { ThreadRealtimeSdpNotification } from "./v2/ThreadRealtimeSdpNotification";
|
||||
import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification";
|
||||
import type { ThreadRealtimeTranscriptUpdatedNotification } from "./v2/ThreadRealtimeTranscriptUpdatedNotification";
|
||||
import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification";
|
||||
@@ -57,4 +58,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
|
||||
/**
|
||||
* Notification sent from the server to the client.
|
||||
*/
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
|
||||
|
||||
@@ -7,4 +7,4 @@ import type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
|
||||
import type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
|
||||
import type { ReasoningEffortOption } from "./ReasoningEffortOption";
|
||||
|
||||
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, isDefault: boolean, };
|
||||
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, additionalSpeedTiers: Array<string>, isDefault: boolean, };
|
||||
|
||||
@@ -27,7 +27,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null | null, developerInstructions?: string | null | null, ephemeral?: boolean, /**
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /**
|
||||
* If true, persist additional rollout EventMsg variants required to
|
||||
* reconstruct a richer thread history on subsequent resume/fork/read.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.
|
||||
*/
|
||||
export type ThreadRealtimeSdpNotification = { threadId: string, sdp: string, };
|
||||
@@ -0,0 +1,13 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL - transport used by thread realtime.
|
||||
*/
|
||||
export type ThreadRealtimeStartTransport = { "type": "websocket" } | { "type": "webrtc",
|
||||
/**
|
||||
* SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the
|
||||
* realtime events data channel.
|
||||
*/
|
||||
sdp: string, };
|
||||
@@ -36,7 +36,7 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null | null, developerInstructions?: string | null | null, personality?: Personality | null, /**
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
|
||||
* If true, persist additional rollout EventMsg variants required to
|
||||
* reconstruct a richer thread history on subsequent resume/fork/read.
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string |
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null | null, developerInstructions?: string | null | null, personality?: Personality | null, ephemeral?: boolean | null, /**
|
||||
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
|
||||
* If true, opt into emitting raw Responses API items on the event stream.
|
||||
* This is for internal use only (e.g. Codex Cloud).
|
||||
*/
|
||||
|
||||
@@ -294,6 +294,8 @@ export type { ThreadRealtimeClosedNotification } from "./ThreadRealtimeClosedNot
|
||||
export type { ThreadRealtimeErrorNotification } from "./ThreadRealtimeErrorNotification";
|
||||
export type { ThreadRealtimeItemAddedNotification } from "./ThreadRealtimeItemAddedNotification";
|
||||
export type { ThreadRealtimeOutputAudioDeltaNotification } from "./ThreadRealtimeOutputAudioDeltaNotification";
|
||||
export type { ThreadRealtimeSdpNotification } from "./ThreadRealtimeSdpNotification";
|
||||
export type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTransport";
|
||||
export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification";
|
||||
export type { ThreadRealtimeTranscriptUpdatedNotification } from "./ThreadRealtimeTranscriptUpdatedNotification";
|
||||
export type { ThreadResumeParams } from "./ThreadResumeParams";
|
||||
|
||||
@@ -620,6 +620,41 @@ macro_rules! server_request_definitions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed response from the client to the server.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "method", rename_all = "camelCase")]
|
||||
pub enum ServerResponse {
|
||||
$(
|
||||
$(#[$variant_meta])*
|
||||
$(#[serde(rename = $wire)])?
|
||||
$variant {
|
||||
#[serde(rename = "id")]
|
||||
request_id: RequestId,
|
||||
response: $response,
|
||||
},
|
||||
)*
|
||||
}
|
||||
|
||||
impl ServerResponse {
|
||||
pub fn id(&self) -> &RequestId {
|
||||
match self {
|
||||
$(Self::$variant { request_id, .. } => request_id,)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method(&self) -> String {
|
||||
serde_json::to_value(self)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("method")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.unwrap_or_else(|| "<unknown>".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, JsonSchema)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ServerRequestPayload {
|
||||
@@ -970,6 +1005,8 @@ server_notification_definitions! {
|
||||
ThreadRealtimeTranscriptUpdated => "thread/realtime/transcriptUpdated" (v2::ThreadRealtimeTranscriptUpdatedNotification),
|
||||
#[experimental("thread/realtime/outputAudio/delta")]
|
||||
ThreadRealtimeOutputAudioDelta => "thread/realtime/outputAudio/delta" (v2::ThreadRealtimeOutputAudioDeltaNotification),
|
||||
#[experimental("thread/realtime/sdp")]
|
||||
ThreadRealtimeSdp => "thread/realtime/sdp" (v2::ThreadRealtimeSdpNotification),
|
||||
#[experimental("thread/realtime/error")]
|
||||
ThreadRealtimeError => "thread/realtime/error" (v2::ThreadRealtimeErrorNotification),
|
||||
#[experimental("thread/realtime/closed")]
|
||||
@@ -1230,6 +1267,30 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_server_response() -> Result<()> {
|
||||
let response = ServerResponse::CommandExecutionRequestApproval {
|
||||
request_id: RequestId::Integer(8),
|
||||
response: v2::CommandExecutionRequestApprovalResponse {
|
||||
decision: v2::CommandExecutionApprovalDecision::AcceptForSession,
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(response.id(), &RequestId::Integer(8));
|
||||
assert_eq!(response.method(), "item/commandExecution/requestApproval");
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "item/commandExecution/requestApproval",
|
||||
"id": 8,
|
||||
"response": {
|
||||
"decision": "acceptForSession"
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&response)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_mcp_server_elicitation_request() -> Result<()> {
|
||||
let requested_schema: v2::McpElicitationSchema = serde_json::from_value(json!({
|
||||
@@ -1702,6 +1763,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
prompt: "You are on a call".to_string(),
|
||||
session_id: Some("sess_456".to_string()),
|
||||
transport: None,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1711,7 +1773,8 @@ mod tests {
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"prompt": "You are on a call",
|
||||
"sessionId": "sess_456"
|
||||
"sessionId": "sess_456",
|
||||
"transport": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
@@ -1791,6 +1854,7 @@ mod tests {
|
||||
thread_id: "thr_123".to_string(),
|
||||
prompt: "You are on a call".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
},
|
||||
};
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
|
||||
|
||||
@@ -1791,6 +1791,8 @@ pub struct Model {
|
||||
pub input_modalities: Vec<InputModality>,
|
||||
#[serde(default)]
|
||||
pub supports_personality: bool,
|
||||
#[serde(default)]
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
// Only one model should be marked as default.
|
||||
pub is_default: bool,
|
||||
}
|
||||
@@ -2603,22 +2605,10 @@ pub struct ThreadStartParams {
|
||||
pub config: Option<HashMap<String, JsonValue>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub service_name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub base_instructions: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub base_instructions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub developer_instructions: Option<Option<String>>,
|
||||
pub developer_instructions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub personality: Option<Personality>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -2733,22 +2723,10 @@ pub struct ThreadResumeParams {
|
||||
pub sandbox: Option<SandboxMode>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub base_instructions: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub base_instructions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub developer_instructions: Option<Option<String>>,
|
||||
pub developer_instructions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub personality: Option<Personality>,
|
||||
/// If true, persist additional rollout EventMsg variants required to
|
||||
@@ -2822,22 +2800,10 @@ pub struct ThreadForkParams {
|
||||
pub sandbox: Option<SandboxMode>,
|
||||
#[ts(optional = nullable)]
|
||||
pub config: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub base_instructions: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub base_instructions: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub developer_instructions: Option<Option<String>>,
|
||||
pub developer_instructions: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub ephemeral: bool,
|
||||
/// If true, persist additional rollout EventMsg variants required to
|
||||
@@ -3890,6 +3856,21 @@ pub struct ThreadRealtimeStartParams {
|
||||
pub prompt: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub session_id: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub transport: Option<ThreadRealtimeStartTransport>,
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL - transport used by thread realtime.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/", tag = "type")]
|
||||
pub enum ThreadRealtimeStartTransport {
|
||||
Websocket,
|
||||
Webrtc {
|
||||
/// SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the
|
||||
/// realtime events data channel.
|
||||
sdp: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL - response for starting thread realtime.
|
||||
@@ -3981,6 +3962,15 @@ pub struct ThreadRealtimeOutputAudioDeltaNotification {
|
||||
pub audio: ThreadRealtimeAudioChunk,
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadRealtimeSdpNotification {
|
||||
pub thread_id: String,
|
||||
pub sdp: String,
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL - emitted when thread realtime encounters an error.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -8364,35 +8354,6 @@ mod tests {
|
||||
assert_eq!(serialized_without_override.get("serviceTier"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_start_params_preserve_explicit_null_instructions() {
|
||||
let params: ThreadStartParams = serde_json::from_value(json!({
|
||||
"baseInstructions": null,
|
||||
"developerInstructions": null,
|
||||
}))
|
||||
.expect("params should deserialize");
|
||||
assert_eq!(params.base_instructions, Some(None));
|
||||
assert_eq!(params.developer_instructions, Some(None));
|
||||
|
||||
let serialized = serde_json::to_value(¶ms).expect("params should serialize");
|
||||
assert_eq!(
|
||||
serialized.get("baseInstructions"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
assert_eq!(
|
||||
serialized.get("developerInstructions"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
|
||||
let serialized_without_override =
|
||||
serde_json::to_value(ThreadStartParams::default()).expect("params should serialize");
|
||||
assert_eq!(serialized_without_override.get("baseInstructions"), None);
|
||||
assert_eq!(
|
||||
serialized_without_override.get("developerInstructions"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
let params: TurnStartParams = serde_json::from_value(json!({
|
||||
|
||||
@@ -152,7 +152,7 @@ Example with notification opt-out:
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`.
|
||||
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
|
||||
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); returns `{}` and streams `thread/realtime/*` notifications.
|
||||
- `thread/realtime/start` — start a thread-scoped realtime session (experimental); returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`.
|
||||
- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`.
|
||||
- `thread/realtime/appendText` — append text input to the active realtime session (experimental); returns `{}`.
|
||||
- `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`.
|
||||
@@ -172,7 +172,7 @@ Example with notification opt-out:
|
||||
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`.
|
||||
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
|
||||
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `plugins`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
|
||||
@@ -563,6 +563,51 @@ Invoke a plugin by including a UI mention token such as `@sample` in the text in
|
||||
} } }
|
||||
```
|
||||
|
||||
### Example: Start realtime with WebRTC
|
||||
|
||||
Use `thread/realtime/start` with `transport.type: "webrtc"` when a browser or webview owns the `RTCPeerConnection` and app-server should create the server-side realtime session. The transport `sdp` must be the offer SDP produced by `RTCPeerConnection.createOffer()`, not a hand-written or minimal SDP string.
|
||||
|
||||
The offer should include the media sections the client wants to negotiate. For the standard realtime UI flow, create the audio track/transceiver and the `oai-events` data channel before calling `createOffer()`:
|
||||
|
||||
```javascript
|
||||
const pc = new RTCPeerConnection();
|
||||
|
||||
audioElement.autoplay = true;
|
||||
pc.ontrack = (event) => {
|
||||
audioElement.srcObject = event.streams[0];
|
||||
};
|
||||
|
||||
const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
pc.addTrack(mediaStream.getAudioTracks()[0], mediaStream);
|
||||
pc.createDataChannel("oai-events");
|
||||
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
```
|
||||
|
||||
Then send `offer.sdp` to app-server. Core uses `experimental_realtime_ws_backend_prompt` for the backend instructions and the thread conversation id for the realtime session id. The start response is `{}`; the remote answer SDP arrives later as `thread/realtime/sdp` and should be passed to `setRemoteDescription()`:
|
||||
|
||||
```json
|
||||
{ "method": "thread/realtime/start", "id": 40, "params": {
|
||||
"threadId": "thr_123",
|
||||
"prompt": "You are on a call.",
|
||||
"sessionId": null,
|
||||
"transport": { "type": "webrtc", "sdp": "v=0\r\no=..." }
|
||||
} }
|
||||
{ "id": 40, "result": {} }
|
||||
{ "method": "thread/realtime/sdp", "params": {
|
||||
"threadId": "thr_123",
|
||||
"sdp": "v=0\r\no=..."
|
||||
} }
|
||||
```
|
||||
|
||||
```javascript
|
||||
await pc.setRemoteDescription({
|
||||
type: "answer",
|
||||
sdp: notification.params.sdp,
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Interrupt an active turn
|
||||
|
||||
You can cancel a running Turn with `turn/interrupt`.
|
||||
|
||||
@@ -80,6 +80,7 @@ use codex_app_server_protocol::ThreadRealtimeClosedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeErrorNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeItemAddedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeOutputAudioDeltaNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeSdpNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeTranscriptUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
@@ -357,6 +358,17 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::RealtimeConversationSdp(event) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
let notification = ThreadRealtimeSdpNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
sdp: event.sdp,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ThreadRealtimeSdp(notification))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::RealtimeConversationRealtime(event) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
match event.payload {
|
||||
@@ -1343,7 +1355,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
|
||||
let message = ev.message.clone();
|
||||
let codex_error_info = ev.codex_error_info.clone();
|
||||
|
||||
// If this error belongs to an in-flight `thread/rollback` request, fail that request
|
||||
// (and clear pending state) so subsequent rollbacks are unblocked.
|
||||
//
|
||||
|
||||
@@ -146,6 +146,7 @@ use codex_app_server_protocol::ThreadRealtimeAppendTextParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeAppendTextResponse;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartResponse;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartTransport;
|
||||
use codex_app_server_protocol::ThreadRealtimeStopParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeStopResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
@@ -269,6 +270,7 @@ use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::ConversationAudioParams;
|
||||
use codex_protocol::protocol::ConversationStartParams;
|
||||
use codex_protocol::protocol::ConversationStartTransport;
|
||||
use codex_protocol::protocol::ConversationTextParams;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::GitInfo as CoreGitInfo;
|
||||
@@ -2501,8 +2503,8 @@ impl CodexMessageProcessor {
|
||||
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
|
||||
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
|
||||
sandbox: Option<SandboxMode>,
|
||||
base_instructions: Option<Option<String>>,
|
||||
developer_instructions: Option<Option<String>>,
|
||||
base_instructions: Option<String>,
|
||||
developer_instructions: Option<String>,
|
||||
personality: Option<Personality>,
|
||||
) -> Result<ConfigOverrides, JSONRPCErrorError> {
|
||||
let cwd = cwd
|
||||
@@ -4411,13 +4413,6 @@ impl CodexMessageProcessor {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if typesafe_overrides.base_instructions.is_none()
|
||||
&& let Ok(history) = RolloutRecorder::get_rollout_history(&rollout_path).await
|
||||
&& let Some(base_instructions) = history.get_base_instructions()
|
||||
{
|
||||
typesafe_overrides.base_instructions =
|
||||
Some(base_instructions.map(|base_instructions| base_instructions.text));
|
||||
}
|
||||
typesafe_overrides.ephemeral = ephemeral.then_some(true);
|
||||
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
|
||||
let cloud_requirements = self.current_cloud_requirements();
|
||||
@@ -6907,6 +6902,14 @@ impl CodexMessageProcessor {
|
||||
Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: params.prompt,
|
||||
session_id: params.session_id,
|
||||
transport: params.transport.map(|transport| match transport {
|
||||
ThreadRealtimeStartTransport::Websocket => {
|
||||
ConversationStartTransport::Websocket
|
||||
}
|
||||
ThreadRealtimeStartTransport::Webrtc { sdp } => {
|
||||
ConversationStartTransport::Webrtc { sdp }
|
||||
}
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -42,6 +42,7 @@ fn model_from_preset(preset: ModelPreset) -> Model {
|
||||
default_reasoning_effort: preset.default_reasoning_effort,
|
||||
input_modalities: preset.input_modalities,
|
||||
supports_personality: preset.supports_personality,
|
||||
additional_speed_tiers: preset.additional_speed_tiers,
|
||||
is_default: preset.is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
},
|
||||
supported_in_api: preset.supported_in_api,
|
||||
priority,
|
||||
additional_speed_tiers: preset.additional_speed_tiers.clone(),
|
||||
upgrade: preset.upgrade.as_ref().map(Into::into),
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -84,7 +84,6 @@ pub fn create_fake_rollout_with_source(
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
@@ -168,7 +167,6 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::MockExperimentalMethodParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartTransport;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -75,6 +76,44 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
|
||||
thread_id: "thr_123".to_string(),
|
||||
prompt: "hello".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
})
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_experimental_capability_error(error, "thread/realtime/start");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
opt_out_notification_methods: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
anyhow::bail!("expected initialize response, got {init:?}");
|
||||
};
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_realtime_start_request(ThreadRealtimeStartParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
prompt: "hello".to_string(),
|
||||
session_id: None,
|
||||
transport: Some(ThreadRealtimeStartTransport::Webrtc {
|
||||
sdp: "v=offer\r\n".to_string(),
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
let error = timeout(
|
||||
|
||||
@@ -50,6 +50,7 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
|
||||
// cache report `supports_personality = false`.
|
||||
// todo(sayan): fix, maybe make roundtrip use ModelInfo only
|
||||
supports_personality: false,
|
||||
additional_speed_tiers: preset.additional_speed_tiers.clone(),
|
||||
is_default: preset.is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ use codex_app_server_protocol::ThreadRealtimeClosedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeErrorNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeItemAddedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeOutputAudioDeltaNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeSdpNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartResponse;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartTransport;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeStopParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeStopResponse;
|
||||
@@ -33,13 +35,59 @@ use pretty_assertions::assert_eq;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::Match;
|
||||
use wiremock::Mock;
|
||||
use wiremock::Request as WiremockRequest;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const STARTUP_CONTEXT_HEADER: &str = "Startup context from Codex.";
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum StartupContextConfig<'a> {
|
||||
Generated,
|
||||
Override(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RealtimeCallRequestCapture {
|
||||
requests: Arc<Mutex<Vec<WiremockRequest>>>,
|
||||
}
|
||||
|
||||
impl RealtimeCallRequestCapture {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
requests: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn single_request(&self) -> WiremockRequest {
|
||||
let requests = self
|
||||
.requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(requests.len(), 1, "expected one realtime call request");
|
||||
requests[0].clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Match for RealtimeCallRequestCapture {
|
||||
fn matches(&self, request: &WiremockRequest) -> bool {
|
||||
self.requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(request.clone());
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -100,6 +148,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
&responses_server.uri(),
|
||||
realtime_server.uri(),
|
||||
/*realtime_enabled*/ true,
|
||||
StartupContextConfig::Generated,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
@@ -121,6 +170,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
thread_id: thread_start.thread.id.clone(),
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
})
|
||||
.await?;
|
||||
let start_response: JSONRPCResponse = timeout(
|
||||
@@ -309,6 +359,7 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> {
|
||||
&responses_server.uri(),
|
||||
realtime_server.uri(),
|
||||
/*realtime_enabled*/ true,
|
||||
StartupContextConfig::Generated,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
@@ -330,6 +381,7 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> {
|
||||
thread_id: thread_start.thread.id.clone(),
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
})
|
||||
.await?;
|
||||
let start_response: JSONRPCResponse = timeout(
|
||||
@@ -368,6 +420,169 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let responses_server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let call_capture = RealtimeCallRequestCapture::new();
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/realtime/calls"))
|
||||
.and(call_capture.clone())
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("v=answer\r\n"))
|
||||
.mount(&responses_server)
|
||||
.await;
|
||||
let realtime_server = start_websocket_server(vec![vec![]]).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&responses_server.uri(),
|
||||
realtime_server.uri(),
|
||||
/*realtime_enabled*/ true,
|
||||
StartupContextConfig::Override("startup context"),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
mcp.initialize().await?;
|
||||
login_with_api_key(&mut mcp, "sk-test-key").await?;
|
||||
|
||||
let thread_start_request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
let thread_start_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_start_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let thread_start: ThreadStartResponse = to_response(thread_start_response)?;
|
||||
|
||||
let thread_id = thread_start.thread.id;
|
||||
let start_request_id = mcp
|
||||
.send_thread_realtime_start_request(ThreadRealtimeStartParams {
|
||||
thread_id: thread_id.clone(),
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: Some(ThreadRealtimeStartTransport::Webrtc {
|
||||
sdp: "v=offer\r\n".to_string(),
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
let start_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadRealtimeStartResponse = to_response(start_response)?;
|
||||
|
||||
let sdp_notification =
|
||||
read_notification::<ThreadRealtimeSdpNotification>(&mut mcp, "thread/realtime/sdp").await?;
|
||||
assert_eq!(
|
||||
sdp_notification,
|
||||
ThreadRealtimeSdpNotification {
|
||||
thread_id: thread_id.clone(),
|
||||
sdp: "v=answer\r\n".to_string()
|
||||
}
|
||||
);
|
||||
let closed_notification =
|
||||
read_notification::<ThreadRealtimeClosedNotification>(&mut mcp, "thread/realtime/closed")
|
||||
.await?;
|
||||
assert_eq!(
|
||||
closed_notification,
|
||||
ThreadRealtimeClosedNotification {
|
||||
thread_id: thread_id.clone(),
|
||||
reason: Some("transport_closed".to_string())
|
||||
}
|
||||
);
|
||||
|
||||
let request = call_capture.single_request();
|
||||
assert_eq!(request.url.path(), "/v1/realtime/calls");
|
||||
assert_eq!(request.url.query(), None);
|
||||
let body = String::from_utf8(request.body).context("multipart body should be utf-8")?;
|
||||
let session = r#"{"tool_choice":"auto","type":"realtime","instructions":"backend prompt\n\nstartup context","output_modalities":["audio"],"audio":{"input":{"format":{"type":"audio/pcm","rate":24000},"noise_reduction":{"type":"near_field"},"turn_detection":{"type":"server_vad","interrupt_response":true,"create_response":true}},"output":{"format":{"type":"audio/pcm","rate":24000},"voice":"marin"}},"tools":[{"type":"function","name":"codex","description":"Delegate a request to Codex and return the final result to the user. Use this as the default action. If the user asks to do something next, later, after this, or once current work finishes, call this tool so the work is actually queued instead of merely promising to do it later.","parameters":{"type":"object","properties":{"prompt":{"type":"string","description":"The user request to delegate to Codex."}},"required":["prompt"],"additionalProperties":false}}]}"#;
|
||||
assert_eq!(
|
||||
body,
|
||||
format!(
|
||||
"--codex-realtime-call-boundary\r\n\
|
||||
Content-Disposition: form-data; name=\"sdp\"\r\n\
|
||||
Content-Type: application/sdp\r\n\
|
||||
\r\n\
|
||||
v=offer\r\n\
|
||||
\r\n\
|
||||
--codex-realtime-call-boundary\r\n\
|
||||
Content-Disposition: form-data; name=\"session\"\r\n\
|
||||
Content-Type: application/json\r\n\
|
||||
\r\n\
|
||||
{session}\r\n\
|
||||
--codex-realtime-call-boundary--\r\n"
|
||||
)
|
||||
);
|
||||
|
||||
realtime_server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn realtime_webrtc_start_surfaces_backend_error() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let responses_server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/realtime/calls"))
|
||||
.respond_with(ResponseTemplate::new(500).set_body_string("boom"))
|
||||
.mount(&responses_server)
|
||||
.await;
|
||||
let realtime_server = start_websocket_server(vec![vec![]]).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&responses_server.uri(),
|
||||
realtime_server.uri(),
|
||||
/*realtime_enabled*/ true,
|
||||
StartupContextConfig::Override("startup context"),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
mcp.initialize().await?;
|
||||
login_with_api_key(&mut mcp, "sk-test-key").await?;
|
||||
|
||||
let thread_start_request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
let thread_start_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_start_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let thread_start: ThreadStartResponse = to_response(thread_start_response)?;
|
||||
|
||||
let start_request_id = mcp
|
||||
.send_thread_realtime_start_request(ThreadRealtimeStartParams {
|
||||
thread_id: thread_start.thread.id,
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: Some(ThreadRealtimeStartTransport::Webrtc {
|
||||
sdp: "v=offer\r\n".to_string(),
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
let start_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadRealtimeStartResponse = to_response(start_response)?;
|
||||
|
||||
let error =
|
||||
read_notification::<ThreadRealtimeErrorNotification>(&mut mcp, "thread/realtime/error")
|
||||
.await?;
|
||||
assert!(error.message.contains("currently experiencing high demand"));
|
||||
|
||||
realtime_server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn realtime_conversation_requires_feature_flag() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -381,6 +596,7 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> {
|
||||
&responses_server.uri(),
|
||||
realtime_server.uri(),
|
||||
/*realtime_enabled*/ false,
|
||||
StartupContextConfig::Generated,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
@@ -401,6 +617,7 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> {
|
||||
thread_id: thread_start.thread.id.clone(),
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
})
|
||||
.await?;
|
||||
let error = timeout(
|
||||
@@ -450,12 +667,19 @@ fn create_config_toml(
|
||||
responses_server_uri: &str,
|
||||
realtime_server_uri: &str,
|
||||
realtime_enabled: bool,
|
||||
startup_context: StartupContextConfig<'_>,
|
||||
) -> std::io::Result<()> {
|
||||
let realtime_feature_key = FEATURES
|
||||
.iter()
|
||||
.find(|spec| spec.id == Feature::RealtimeConversation)
|
||||
.map(|spec| spec.key)
|
||||
.unwrap_or("realtime_conversation");
|
||||
let startup_context = match startup_context {
|
||||
StartupContextConfig::Generated => String::new(),
|
||||
StartupContextConfig::Override(context) => {
|
||||
format!("experimental_realtime_ws_startup_context = {context:?}\n")
|
||||
}
|
||||
};
|
||||
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
@@ -466,6 +690,8 @@ approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
model_provider = "mock_provider"
|
||||
experimental_realtime_ws_base_url = "{realtime_server_uri}"
|
||||
experimental_realtime_ws_backend_prompt = "backend prompt"
|
||||
{startup_context}
|
||||
|
||||
[realtime]
|
||||
version = "v2"
|
||||
|
||||
@@ -26,8 +26,6 @@ use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
@@ -185,170 +183,6 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response_mock =
|
||||
responses::mount_sse_sequence(&server, vec![body.clone(), body.clone(), body]).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let mut config_toml = std::fs::read_to_string(&config_path)?;
|
||||
let first_table_index = config_toml
|
||||
.find("\n[")
|
||||
.expect("test config must include a table header");
|
||||
config_toml.insert_str(
|
||||
first_table_index,
|
||||
"\ndeveloper_instructions = \"Config developer instructions sentinel\"\n",
|
||||
);
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let conversation_id = create_fake_rollout(
|
||||
codex_home.path(),
|
||||
"2025-01-05T12-00-00",
|
||||
"2025-01-05T12:00:00Z",
|
||||
"Saved user message",
|
||||
Some("mock_provider"),
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let disabled_instruction_config = json!({
|
||||
"include_permissions_instructions": false,
|
||||
"include_apps_instructions": false,
|
||||
"include_environment_context": false,
|
||||
"features.apps": false,
|
||||
"features.plugins": false,
|
||||
"features.codex_hooks": false,
|
||||
"skills.bundled.enabled": false,
|
||||
});
|
||||
|
||||
let fork_params = [
|
||||
(
|
||||
json!({
|
||||
"threadId": conversation_id.clone(),
|
||||
"config": disabled_instruction_config.clone(),
|
||||
}),
|
||||
/*expect_instructions*/ true,
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"threadId": conversation_id.clone(),
|
||||
"config": disabled_instruction_config.clone(),
|
||||
"baseInstructions": null,
|
||||
"developerInstructions": null,
|
||||
}),
|
||||
/*expect_instructions*/ false,
|
||||
),
|
||||
];
|
||||
|
||||
let mut forked_thread_ids = Vec::new();
|
||||
for (params, _expect_instructions) in fork_params {
|
||||
let fork_id = mcp.send_raw_request("thread/fork", Some(params)).await?;
|
||||
let fork_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(fork_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadForkResponse { thread, .. } = to_response::<ThreadForkResponse>(fork_resp)?;
|
||||
forked_thread_ids.push(thread.id.clone());
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![UserInput::Text {
|
||||
text: "continue".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
}
|
||||
|
||||
let refork_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/fork",
|
||||
Some(json!({
|
||||
"threadId": forked_thread_ids[1].clone(),
|
||||
"config": disabled_instruction_config.clone(),
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let refork_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(refork_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadForkResponse { thread, .. } = to_response::<ThreadForkResponse>(refork_resp)?;
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![UserInput::Text {
|
||||
text: "continue again".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
for (request, expect_instructions) in requests.into_iter().zip([true, false, false]) {
|
||||
let payload = request.body_json();
|
||||
assert_eq!(
|
||||
payload.get("instructions").is_some(),
|
||||
expect_instructions,
|
||||
"unexpected instructions field in payload: {payload:?}"
|
||||
);
|
||||
let developer_texts = request.message_input_texts("developer");
|
||||
assert_eq!(
|
||||
developer_texts
|
||||
.iter()
|
||||
.any(|text| { text.contains("Config developer instructions sentinel") }),
|
||||
expect_instructions,
|
||||
"unexpected config developer instruction presence: {developer_texts:?}"
|
||||
);
|
||||
assert!(
|
||||
developer_texts.iter().all(|text| !text.is_empty()),
|
||||
"did not expect empty developer instruction messages: {developer_texts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
@@ -377,7 +377,6 @@ stream_max_retries = 0
|
||||
agent_role: None,
|
||||
model_provider: Some("mock_provider".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
|
||||
@@ -152,120 +152,6 @@ async fn turn_start_sends_originator_header() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response_mock = responses::mount_sse_sequence(&server, vec![body.clone(), body]).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never", &BTreeMap::new())?;
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let mut config_toml = std::fs::read_to_string(&config_path)?;
|
||||
let first_table_index = config_toml
|
||||
.find("\n[")
|
||||
.expect("test config must include a table header");
|
||||
config_toml.insert_str(
|
||||
first_table_index,
|
||||
"\ndeveloper_instructions = \"Config developer instructions sentinel\"\n",
|
||||
);
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let disabled_instruction_config = json!({
|
||||
"include_permissions_instructions": false,
|
||||
"include_apps_instructions": false,
|
||||
"include_environment_context": false,
|
||||
"features.apps": false,
|
||||
"features.plugins": false,
|
||||
"features.codex_hooks": false,
|
||||
"skills.bundled.enabled": false,
|
||||
});
|
||||
|
||||
let thread_start_params = [
|
||||
(
|
||||
json!({
|
||||
"model": "mock-model",
|
||||
"config": disabled_instruction_config.clone(),
|
||||
}),
|
||||
/*expect_instructions*/ true,
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"model": "mock-model",
|
||||
"config": disabled_instruction_config.clone(),
|
||||
"baseInstructions": null,
|
||||
"developerInstructions": null,
|
||||
}),
|
||||
/*expect_instructions*/ false,
|
||||
),
|
||||
];
|
||||
|
||||
for (params, _expect_instructions) in thread_start_params {
|
||||
let thread_req = mcp.send_raw_request("thread/start", Some(params)).await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
}
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
for (request, expect_instructions) in requests.into_iter().zip([true, false]) {
|
||||
let payload = request.body_json();
|
||||
assert_eq!(
|
||||
payload.get("instructions").is_some(),
|
||||
expect_instructions,
|
||||
"unexpected instructions field in payload: {payload:?}"
|
||||
);
|
||||
let developer_texts = request.message_input_texts("developer");
|
||||
assert_eq!(
|
||||
developer_texts
|
||||
.iter()
|
||||
.any(|text| { text.contains("Config developer instructions sentinel") }),
|
||||
expect_instructions,
|
||||
"unexpected config developer instruction presence: {developer_texts:?}"
|
||||
);
|
||||
assert!(
|
||||
developer_texts.iter().all(|text| !text.is_empty()),
|
||||
"did not expect empty developer instruction messages: {developer_texts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
|
||||
@@ -153,8 +153,7 @@ impl From<VerbosityConfig> for OpenAiVerbosity {
|
||||
#[derive(Debug, Serialize, Clone, PartialEq)]
|
||||
pub struct ResponsesApiRequest {
|
||||
pub model: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub instructions: Option<String>,
|
||||
pub instructions: String,
|
||||
pub input: Vec<ResponseItem>,
|
||||
pub tools: Vec<serde_json::Value>,
|
||||
pub tool_choice: String,
|
||||
@@ -199,8 +198,7 @@ impl From<&ResponsesApiRequest> for ResponseCreateWsRequest {
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResponseCreateWsRequest {
|
||||
pub model: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub instructions: Option<String>,
|
||||
pub instructions: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub previous_response_id: Option<String>,
|
||||
pub input: Vec<ResponseItem>,
|
||||
|
||||
@@ -72,6 +72,7 @@ mod tests {
|
||||
use crate::provider::RetryConfig;
|
||||
use async_trait::async_trait;
|
||||
use codex_client::Request;
|
||||
use codex_client::RequestBody;
|
||||
use codex_client::Response;
|
||||
use codex_client::StreamResponse;
|
||||
use codex_client::TransportError;
|
||||
@@ -213,7 +214,11 @@ mod tests {
|
||||
request.url,
|
||||
"https://example.com/api/codex/memories/trace_summarize"
|
||||
);
|
||||
let body = request.body.expect("request body should be present");
|
||||
let body = request
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(RequestBody::json)
|
||||
.expect("request body should be JSON");
|
||||
assert_eq!(body["model"], "gpt-test");
|
||||
assert_eq!(body["traces"][0]["id"], "trace-1");
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub(crate) mod compact;
|
||||
pub(crate) mod memories;
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod realtime_call;
|
||||
pub(crate) mod realtime_websocket;
|
||||
pub(crate) mod responses;
|
||||
pub(crate) mod responses_websocket;
|
||||
@@ -9,6 +10,8 @@ mod session;
|
||||
pub use compact::CompactClient;
|
||||
pub use memories::MemoriesClient;
|
||||
pub use models::ModelsClient;
|
||||
pub use realtime_call::RealtimeCallClient;
|
||||
pub use realtime_call::RealtimeCallResponse;
|
||||
pub use realtime_websocket::RealtimeEventParser;
|
||||
pub use realtime_websocket::RealtimeSessionConfig;
|
||||
pub use realtime_websocket::RealtimeSessionMode;
|
||||
@@ -16,6 +19,7 @@ pub use realtime_websocket::RealtimeWebsocketClient;
|
||||
pub use realtime_websocket::RealtimeWebsocketConnection;
|
||||
pub use realtime_websocket::RealtimeWebsocketEvents;
|
||||
pub use realtime_websocket::RealtimeWebsocketWriter;
|
||||
pub use realtime_websocket::session_update_session_json;
|
||||
pub use responses::ResponsesClient;
|
||||
pub use responses::ResponsesOptions;
|
||||
pub use responses_websocket::ResponsesWebsocketClient;
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
use crate::auth::AuthProvider;
|
||||
use crate::endpoint::realtime_websocket::RealtimeSessionConfig;
|
||||
use crate::endpoint::realtime_websocket::session_update_session_json;
|
||||
use crate::endpoint::session::EndpointSession;
|
||||
use crate::error::ApiError;
|
||||
use crate::provider::Provider;
|
||||
use bytes::Bytes;
|
||||
use codex_client::HttpTransport;
|
||||
use codex_client::RequestBody;
|
||||
use codex_client::RequestTelemetry;
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use http::Method;
|
||||
use http::header::CONTENT_TYPE;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use serde_json::to_string;
|
||||
use serde_json::to_value;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
|
||||
const MULTIPART_BOUNDARY: &str = "codex-realtime-call-boundary";
|
||||
const MULTIPART_CONTENT_TYPE: &str = "multipart/form-data; boundary=codex-realtime-call-boundary";
|
||||
|
||||
pub struct RealtimeCallClient<T: HttpTransport, A: AuthProvider> {
|
||||
session: EndpointSession<T, A>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RealtimeCallResponse {
|
||||
pub sdp: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct BackendRealtimeCallRequest<'a> {
|
||||
sdp: &'a str,
|
||||
session: &'a Value,
|
||||
}
|
||||
|
||||
impl<T: HttpTransport, A: AuthProvider> RealtimeCallClient<T, A> {
|
||||
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
|
||||
Self {
|
||||
session: EndpointSession::new(transport, provider, auth),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_telemetry(self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
|
||||
Self {
|
||||
session: self.session.with_request_telemetry(request),
|
||||
}
|
||||
}
|
||||
|
||||
fn path() -> &'static str {
|
||||
"realtime/calls"
|
||||
}
|
||||
|
||||
fn uses_backend_request_shape(&self) -> bool {
|
||||
self.session.provider().base_url.contains("/backend-api")
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "realtime_call.create",
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(
|
||||
http.method = "POST",
|
||||
api.path = "realtime/calls"
|
||||
)
|
||||
)]
|
||||
pub async fn create(&self, sdp: String) -> Result<RealtimeCallResponse, ApiError> {
|
||||
self.create_with_headers(sdp, HeaderMap::new()).await
|
||||
}
|
||||
|
||||
pub async fn create_with_session(
|
||||
&self,
|
||||
sdp: String,
|
||||
session_config: RealtimeSessionConfig,
|
||||
) -> Result<RealtimeCallResponse, ApiError> {
|
||||
self.create_with_session_and_headers(sdp, session_config, HeaderMap::new())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_with_headers(
|
||||
&self,
|
||||
sdp: String,
|
||||
extra_headers: HeaderMap,
|
||||
) -> Result<RealtimeCallResponse, ApiError> {
|
||||
let resp = self
|
||||
.session
|
||||
.execute_with(
|
||||
Method::POST,
|
||||
Self::path(),
|
||||
extra_headers,
|
||||
/*body*/ None,
|
||||
|req| {
|
||||
req.headers
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("application/sdp"));
|
||||
req.body = Some(RequestBody::Raw(Bytes::from(sdp.clone())));
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sdp = decode_sdp_response(resp.body.as_ref())?;
|
||||
|
||||
Ok(RealtimeCallResponse { sdp })
|
||||
}
|
||||
|
||||
pub async fn create_with_session_and_headers(
|
||||
&self,
|
||||
sdp: String,
|
||||
session_config: RealtimeSessionConfig,
|
||||
extra_headers: HeaderMap,
|
||||
) -> Result<RealtimeCallResponse, ApiError> {
|
||||
let mut session = realtime_session_json(session_config)?;
|
||||
if let Some(session) = session.as_object_mut() {
|
||||
session.remove("id");
|
||||
}
|
||||
// TODO(aibrahim): Align the SIWC route with the API multipart shape and remove this branch.
|
||||
if self.uses_backend_request_shape() {
|
||||
let body = to_value(BackendRealtimeCallRequest {
|
||||
sdp: &sdp,
|
||||
session: &session,
|
||||
})
|
||||
.map_err(|err| ApiError::Stream(format!("failed to encode realtime call: {err}")))?;
|
||||
let resp = self
|
||||
.session
|
||||
.execute(Method::POST, Self::path(), extra_headers, Some(body))
|
||||
.await?;
|
||||
let sdp = decode_sdp_response(resp.body.as_ref())?;
|
||||
return Ok(RealtimeCallResponse { sdp });
|
||||
}
|
||||
|
||||
let session = to_string(&session).map_err(|err| ApiError::InvalidRequest {
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(format!("--{MULTIPART_BOUNDARY}\r\n").as_bytes());
|
||||
body.extend_from_slice(b"Content-Disposition: form-data; name=\"sdp\"\r\n");
|
||||
body.extend_from_slice(b"Content-Type: application/sdp\r\n\r\n");
|
||||
body.extend_from_slice(sdp.as_bytes());
|
||||
body.extend_from_slice(b"\r\n");
|
||||
body.extend_from_slice(format!("--{MULTIPART_BOUNDARY}\r\n").as_bytes());
|
||||
body.extend_from_slice(b"Content-Disposition: form-data; name=\"session\"\r\n");
|
||||
body.extend_from_slice(b"Content-Type: application/json\r\n\r\n");
|
||||
body.extend_from_slice(session.as_bytes());
|
||||
body.extend_from_slice(b"\r\n");
|
||||
body.extend_from_slice(format!("--{MULTIPART_BOUNDARY}--\r\n").as_bytes());
|
||||
|
||||
let resp = self
|
||||
.session
|
||||
.execute_with(
|
||||
Method::POST,
|
||||
Self::path(),
|
||||
extra_headers,
|
||||
/*body*/ None,
|
||||
|req| {
|
||||
req.headers.insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_static(MULTIPART_CONTENT_TYPE),
|
||||
);
|
||||
req.body = Some(RequestBody::Raw(Bytes::from(body.clone())));
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sdp = decode_sdp_response(resp.body.as_ref())?;
|
||||
|
||||
Ok(RealtimeCallResponse { sdp })
|
||||
}
|
||||
}
|
||||
|
||||
fn realtime_session_json(session_config: RealtimeSessionConfig) -> Result<Value, ApiError> {
|
||||
session_update_session_json(session_config)
|
||||
.map_err(|err| ApiError::Stream(format!("failed to encode realtime call session: {err}")))
|
||||
}
|
||||
|
||||
fn decode_sdp_response(body: &[u8]) -> Result<String, ApiError> {
|
||||
String::from_utf8(body.to_vec()).map_err(|err| {
|
||||
ApiError::Stream(format!(
|
||||
"failed to decode realtime call SDP response: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::endpoint::realtime_websocket::RealtimeEventParser;
|
||||
use crate::endpoint::realtime_websocket::RealtimeSessionMode;
|
||||
use crate::provider::RetryConfig;
|
||||
use async_trait::async_trait;
|
||||
use codex_client::Request;
|
||||
use codex_client::Response;
|
||||
use codex_client::StreamResponse;
|
||||
use codex_client::TransportError;
|
||||
use http::StatusCode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CapturingTransport {
|
||||
last_request: Arc<Mutex<Option<Request>>>,
|
||||
}
|
||||
|
||||
impl CapturingTransport {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
last_request: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HttpTransport for CapturingTransport {
|
||||
async fn execute(&self, req: Request) -> Result<Response, TransportError> {
|
||||
*self.last_request.lock().unwrap() = Some(req);
|
||||
Ok(Response {
|
||||
status: StatusCode::OK,
|
||||
headers: HeaderMap::new(),
|
||||
body: Bytes::from_static(b"v=0\r\n"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn stream(&self, _req: Request) -> Result<StreamResponse, TransportError> {
|
||||
Err(TransportError::Build("stream should not run".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct DummyAuth;
|
||||
|
||||
impl AuthProvider for DummyAuth {
|
||||
fn bearer_token(&self) -> Option<String> {
|
||||
Some("test-token".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn provider(base_url: &str) -> Provider {
|
||||
Provider {
|
||||
name: "test".to_string(),
|
||||
base_url: base_url.to_string(),
|
||||
query_params: None,
|
||||
headers: HeaderMap::new(),
|
||||
retry: RetryConfig {
|
||||
max_attempts: 1,
|
||||
base_delay: Duration::from_millis(1),
|
||||
retry_429: false,
|
||||
retry_5xx: true,
|
||||
retry_transport: true,
|
||||
},
|
||||
stream_idle_timeout: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn realtime_session_config(session_id: &str) -> RealtimeSessionConfig {
|
||||
RealtimeSessionConfig {
|
||||
instructions: "hi".to_string(),
|
||||
model: Some("gpt-realtime".to_string()),
|
||||
session_id: Some(session_id.to_string()),
|
||||
event_parser: RealtimeEventParser::RealtimeV2,
|
||||
session_mode: RealtimeSessionMode::Conversational,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sends_sdp_offer_as_raw_body() {
|
||||
let transport = CapturingTransport::new();
|
||||
let client = RealtimeCallClient::new(
|
||||
transport.clone(),
|
||||
provider("https://api.openai.com/v1"),
|
||||
DummyAuth,
|
||||
);
|
||||
|
||||
let response = client
|
||||
.create("v=offer\r\n".to_string())
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
RealtimeCallResponse {
|
||||
sdp: "v=0\r\n".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let request = transport.last_request.lock().unwrap().clone().unwrap();
|
||||
assert_eq!(request.method, Method::POST);
|
||||
assert_eq!(request.url, "https://api.openai.com/v1/realtime/calls");
|
||||
assert_eq!(
|
||||
request.headers.get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("application/sdp")
|
||||
);
|
||||
assert_eq!(
|
||||
request
|
||||
.headers
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("Bearer test-token")
|
||||
);
|
||||
assert_eq!(
|
||||
request.body,
|
||||
Some(RequestBody::Raw(Bytes::from_static(b"v=offer\r\n")))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sends_api_session_call_as_multipart_body() {
|
||||
let transport = CapturingTransport::new();
|
||||
let client = RealtimeCallClient::new(
|
||||
transport.clone(),
|
||||
provider("https://api.openai.com/v1"),
|
||||
DummyAuth,
|
||||
);
|
||||
|
||||
let response = client
|
||||
.create_with_session(
|
||||
"v=offer\r\n".to_string(),
|
||||
realtime_session_config("sess-api"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
RealtimeCallResponse {
|
||||
sdp: "v=0\r\n".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let request = transport.last_request.lock().unwrap().clone().unwrap();
|
||||
assert_eq!(request.method, Method::POST);
|
||||
assert_eq!(request.url, "https://api.openai.com/v1/realtime/calls");
|
||||
assert_eq!(
|
||||
request.headers.get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static(MULTIPART_CONTENT_TYPE)
|
||||
);
|
||||
let Some(RequestBody::Raw(body)) = request.body else {
|
||||
panic!("multipart body should be raw");
|
||||
};
|
||||
let body = std::str::from_utf8(&body).expect("multipart body should be utf-8");
|
||||
let mut session = realtime_session_json(realtime_session_config("sess-api"))
|
||||
.expect("session should encode");
|
||||
session
|
||||
.as_object_mut()
|
||||
.expect("session should be an object")
|
||||
.remove("id");
|
||||
let session = to_string(&session).expect("session should serialize");
|
||||
assert_eq!(
|
||||
body,
|
||||
format!(
|
||||
"--codex-realtime-call-boundary\r\n\
|
||||
Content-Disposition: form-data; name=\"sdp\"\r\n\
|
||||
Content-Type: application/sdp\r\n\
|
||||
\r\n\
|
||||
v=offer\r\n\
|
||||
\r\n\
|
||||
--codex-realtime-call-boundary\r\n\
|
||||
Content-Disposition: form-data; name=\"session\"\r\n\
|
||||
Content-Type: application/json\r\n\
|
||||
\r\n\
|
||||
{session}\r\n\
|
||||
--codex-realtime-call-boundary--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sends_backend_session_call_as_json_body() {
|
||||
let transport = CapturingTransport::new();
|
||||
let client = RealtimeCallClient::new(
|
||||
transport.clone(),
|
||||
provider("https://chatgpt.com/backend-api/codex"),
|
||||
DummyAuth,
|
||||
);
|
||||
|
||||
let response = client
|
||||
.create_with_session(
|
||||
"v=offer\r\n".to_string(),
|
||||
realtime_session_config("sess-backend"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
RealtimeCallResponse {
|
||||
sdp: "v=0\r\n".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let request = transport.last_request.lock().unwrap().clone().unwrap();
|
||||
assert_eq!(request.method, Method::POST);
|
||||
assert_eq!(
|
||||
request.url,
|
||||
"https://chatgpt.com/backend-api/codex/realtime/calls"
|
||||
);
|
||||
let mut expected_session = realtime_session_json(realtime_session_config("sess-backend"))
|
||||
.expect("session should encode");
|
||||
expected_session
|
||||
.as_object_mut()
|
||||
.expect("session should be an object")
|
||||
.remove("id");
|
||||
assert_eq!(
|
||||
request.body,
|
||||
Some(RequestBody::Json(
|
||||
to_value(BackendRealtimeCallRequest {
|
||||
sdp: "v=offer\r\n",
|
||||
session: &expected_session,
|
||||
})
|
||||
.expect("request should encode")
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,12 @@ use crate::endpoint::realtime_websocket::methods_v2::session_update_session as v
|
||||
use crate::endpoint::realtime_websocket::methods_v2::websocket_intent as v2_websocket_intent;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeEventParser;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeOutboundMessage;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeSessionConfig;
|
||||
use crate::endpoint::realtime_websocket::protocol::RealtimeSessionMode;
|
||||
use crate::endpoint::realtime_websocket::protocol::SessionUpdateSession;
|
||||
use serde_json::Result as JsonResult;
|
||||
use serde_json::Value;
|
||||
use serde_json::to_value;
|
||||
|
||||
pub(super) const REALTIME_AUDIO_SAMPLE_RATE: u32 = 24_000;
|
||||
const AGENT_FINAL_MESSAGE_PREFIX: &str = "\"Agent Final Message\":\n\n";
|
||||
@@ -60,6 +64,17 @@ pub(super) fn session_update_session(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_update_session_json(config: RealtimeSessionConfig) -> JsonResult<Value> {
|
||||
let mut session = session_update_session(
|
||||
config.event_parser,
|
||||
config.instructions,
|
||||
config.session_mode,
|
||||
);
|
||||
session.id = config.session_id;
|
||||
session.model = config.model;
|
||||
to_value(session)
|
||||
}
|
||||
|
||||
pub(super) fn websocket_intent(event_parser: RealtimeEventParser) -> Option<&'static str> {
|
||||
match event_parser {
|
||||
RealtimeEventParser::V1 => v1_websocket_intent(),
|
||||
|
||||
@@ -40,7 +40,9 @@ pub(super) fn conversation_handoff_append_message(
|
||||
|
||||
pub(super) fn session_update_session(instructions: String) -> SessionUpdateSession {
|
||||
SessionUpdateSession {
|
||||
id: None,
|
||||
r#type: SessionType::Quicksilver,
|
||||
model: None,
|
||||
instructions: Some(instructions),
|
||||
output_modalities: None,
|
||||
audio: SessionAudio {
|
||||
|
||||
@@ -62,7 +62,9 @@ pub(super) fn session_update_session(
|
||||
) -> SessionUpdateSession {
|
||||
match session_mode {
|
||||
RealtimeSessionMode::Conversational => SessionUpdateSession {
|
||||
id: None,
|
||||
r#type: SessionType::Realtime,
|
||||
model: None,
|
||||
instructions: Some(instructions),
|
||||
output_modalities: Some(vec![REALTIME_V2_OUTPUT_MODALITY_AUDIO.to_string()]),
|
||||
audio: SessionAudio {
|
||||
@@ -107,7 +109,9 @@ pub(super) fn session_update_session(
|
||||
tool_choice: Some(REALTIME_V2_TOOL_CHOICE.to_string()),
|
||||
},
|
||||
RealtimeSessionMode::Transcription => SessionUpdateSession {
|
||||
id: None,
|
||||
r#type: SessionType::Transcription,
|
||||
model: None,
|
||||
instructions: None,
|
||||
output_modalities: None,
|
||||
audio: SessionAudio {
|
||||
|
||||
@@ -11,6 +11,7 @@ pub use methods::RealtimeWebsocketClient;
|
||||
pub use methods::RealtimeWebsocketConnection;
|
||||
pub use methods::RealtimeWebsocketEvents;
|
||||
pub use methods::RealtimeWebsocketWriter;
|
||||
pub use methods_common::session_update_session_json;
|
||||
pub use protocol::RealtimeEventParser;
|
||||
pub use protocol::RealtimeSessionConfig;
|
||||
pub use protocol::RealtimeSessionMode;
|
||||
|
||||
@@ -48,9 +48,13 @@ pub(super) enum RealtimeOutboundMessage {
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(super) struct SessionUpdateSession {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(super) id: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub(super) r#type: SessionType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(super) model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(super) instructions: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(super) output_modalities: Option<Vec<String>>,
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::provider::Provider;
|
||||
use crate::telemetry::run_with_request_telemetry;
|
||||
use codex_client::HttpTransport;
|
||||
use codex_client::Request;
|
||||
use codex_client::RequestBody;
|
||||
use codex_client::RequestTelemetry;
|
||||
use codex_client::Response;
|
||||
use codex_client::StreamResponse;
|
||||
@@ -53,7 +54,7 @@ impl<T: HttpTransport, A: AuthProvider> EndpointSession<T, A> {
|
||||
let mut req = self.provider.build_request(method.clone(), path);
|
||||
req.headers.extend(extra_headers.clone());
|
||||
if let Some(body) = body {
|
||||
req.body = Some(body.clone());
|
||||
req.body = Some(RequestBody::Json(body.clone()));
|
||||
}
|
||||
add_auth_headers(&self.auth, req)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ pub use crate::common::response_create_client_metadata;
|
||||
pub use crate::endpoint::CompactClient;
|
||||
pub use crate::endpoint::MemoriesClient;
|
||||
pub use crate::endpoint::ModelsClient;
|
||||
pub use crate::endpoint::RealtimeCallClient;
|
||||
pub use crate::endpoint::RealtimeCallResponse;
|
||||
pub use crate::endpoint::RealtimeEventParser;
|
||||
pub use crate::endpoint::RealtimeSessionConfig;
|
||||
pub use crate::endpoint::RealtimeSessionMode;
|
||||
@@ -48,6 +50,7 @@ pub use crate::endpoint::ResponsesClient;
|
||||
pub use crate::endpoint::ResponsesOptions;
|
||||
pub use crate::endpoint::ResponsesWebsocketClient;
|
||||
pub use crate::endpoint::ResponsesWebsocketConnection;
|
||||
pub use crate::endpoint::session_update_session_json;
|
||||
pub use crate::error::ApiError;
|
||||
pub use crate::provider::Provider;
|
||||
pub use crate::provider::RetryConfig;
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_api::ResponsesClient;
|
||||
use codex_api::ResponsesOptions;
|
||||
use codex_client::HttpTransport;
|
||||
use codex_client::Request;
|
||||
use codex_client::RequestBody;
|
||||
use codex_client::Response;
|
||||
use codex_client::StreamResponse;
|
||||
use codex_client::TransportError;
|
||||
@@ -266,7 +267,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
|
||||
|
||||
let request = ResponsesApiRequest {
|
||||
model: "gpt-test".into(),
|
||||
instructions: Some("Say hi".into()),
|
||||
instructions: "Say hi".into(),
|
||||
input: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
tool_choice: "auto".into(),
|
||||
@@ -303,7 +304,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
|
||||
|
||||
let request = ResponsesApiRequest {
|
||||
model: "gpt-test".into(),
|
||||
instructions: Some("Say hi".into()),
|
||||
instructions: "Say hi".into(),
|
||||
input: vec![ResponseItem::Message {
|
||||
id: Some("msg_1".into()),
|
||||
role: "user".into(),
|
||||
@@ -363,6 +364,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
|
||||
let input_id = req
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(RequestBody::json)
|
||||
.and_then(|body| body.get("input"))
|
||||
.and_then(|input| input.get(0))
|
||||
.and_then(|item| item.get("id"))
|
||||
|
||||
@@ -75,6 +75,7 @@ async fn models_client_hits_models_endpoint() {
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -22,6 +22,7 @@ pub use crate::default_client::CodexRequestBuilder;
|
||||
pub use crate::error::StreamError;
|
||||
pub use crate::error::TransportError;
|
||||
pub use crate::request::Request;
|
||||
pub use crate::request::RequestBody;
|
||||
pub use crate::request::RequestCompression;
|
||||
pub use crate::request::Response;
|
||||
pub use crate::retry::RetryOn;
|
||||
|
||||
@@ -12,12 +12,27 @@ pub enum RequestCompression {
|
||||
Zstd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RequestBody {
|
||||
Json(Value),
|
||||
Raw(Bytes),
|
||||
}
|
||||
|
||||
impl RequestBody {
|
||||
pub fn json(&self) -> Option<&Value> {
|
||||
match self {
|
||||
Self::Json(value) => Some(value),
|
||||
Self::Raw(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Request {
|
||||
pub method: Method,
|
||||
pub url: String,
|
||||
pub headers: HeaderMap,
|
||||
pub body: Option<Value>,
|
||||
pub body: Option<RequestBody>,
|
||||
pub compression: RequestCompression,
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
@@ -35,7 +50,12 @@ impl Request {
|
||||
}
|
||||
|
||||
pub fn with_json<T: Serialize>(mut self, body: &T) -> Self {
|
||||
self.body = serde_json::to_value(body).ok();
|
||||
self.body = serde_json::to_value(body).ok().map(RequestBody::Json);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_raw_body(mut self, body: impl Into<Bytes>) -> Self {
|
||||
self.body = Some(RequestBody::Raw(body.into()));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::default_client::CodexHttpClient;
|
||||
use crate::default_client::CodexRequestBuilder;
|
||||
use crate::error::TransportError;
|
||||
use crate::request::Request;
|
||||
use crate::request::RequestBody;
|
||||
use crate::request::RequestCompression;
|
||||
use crate::request::Response;
|
||||
use async_trait::async_trait;
|
||||
@@ -60,52 +61,63 @@ impl ReqwestTransport {
|
||||
builder = builder.timeout(timeout);
|
||||
}
|
||||
|
||||
if let Some(body) = body {
|
||||
if compression != RequestCompression::None {
|
||||
if headers.contains_key(http::header::CONTENT_ENCODING) {
|
||||
match body {
|
||||
Some(RequestBody::Raw(raw_body)) => {
|
||||
if compression != RequestCompression::None {
|
||||
return Err(TransportError::Build(
|
||||
"request compression was requested but content-encoding is already set"
|
||||
.to_string(),
|
||||
"request compression cannot be used with raw bodies".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let json = serde_json::to_vec(&body)
|
||||
.map_err(|err| TransportError::Build(err.to_string()))?;
|
||||
let pre_compression_bytes = json.len();
|
||||
let compression_start = std::time::Instant::now();
|
||||
let (compressed, content_encoding) = match compression {
|
||||
RequestCompression::None => unreachable!("guarded by compression != None"),
|
||||
RequestCompression::Zstd => (
|
||||
zstd::stream::encode_all(std::io::Cursor::new(json), 3)
|
||||
.map_err(|err| TransportError::Build(err.to_string()))?,
|
||||
http::HeaderValue::from_static("zstd"),
|
||||
),
|
||||
};
|
||||
let post_compression_bytes = compressed.len();
|
||||
let compression_duration = compression_start.elapsed();
|
||||
|
||||
// Ensure the server knows to unpack the request body.
|
||||
headers.insert(http::header::CONTENT_ENCODING, content_encoding);
|
||||
if !headers.contains_key(http::header::CONTENT_TYPE) {
|
||||
headers.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
http::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
pre_compression_bytes,
|
||||
post_compression_bytes,
|
||||
compression_duration_ms = compression_duration.as_millis(),
|
||||
"Compressed request body with zstd"
|
||||
);
|
||||
|
||||
builder = builder.headers(headers).body(compressed);
|
||||
} else {
|
||||
builder = builder.headers(headers).json(&body);
|
||||
builder = builder.headers(headers).body(raw_body);
|
||||
}
|
||||
Some(RequestBody::Json(body)) => {
|
||||
if compression != RequestCompression::None {
|
||||
if headers.contains_key(http::header::CONTENT_ENCODING) {
|
||||
return Err(TransportError::Build(
|
||||
"request compression was requested but content-encoding is already set"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let json = serde_json::to_vec(&body)
|
||||
.map_err(|err| TransportError::Build(err.to_string()))?;
|
||||
let pre_compression_bytes = json.len();
|
||||
let compression_start = std::time::Instant::now();
|
||||
let (compressed, content_encoding) = match compression {
|
||||
RequestCompression::None => unreachable!("guarded by compression != None"),
|
||||
RequestCompression::Zstd => (
|
||||
zstd::stream::encode_all(std::io::Cursor::new(json), 3)
|
||||
.map_err(|err| TransportError::Build(err.to_string()))?,
|
||||
http::HeaderValue::from_static("zstd"),
|
||||
),
|
||||
};
|
||||
let post_compression_bytes = compressed.len();
|
||||
let compression_duration = compression_start.elapsed();
|
||||
|
||||
// Ensure the server knows to unpack the request body.
|
||||
headers.insert(http::header::CONTENT_ENCODING, content_encoding);
|
||||
if !headers.contains_key(http::header::CONTENT_TYPE) {
|
||||
headers.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
http::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
pre_compression_bytes,
|
||||
post_compression_bytes,
|
||||
compression_duration_ms = compression_duration.as_millis(),
|
||||
"Compressed request body with zstd"
|
||||
);
|
||||
|
||||
builder = builder.headers(headers).body(compressed);
|
||||
} else {
|
||||
builder = builder.headers(headers).json(&body);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
builder = builder.headers(headers);
|
||||
}
|
||||
} else {
|
||||
builder = builder.headers(headers);
|
||||
}
|
||||
Ok(builder)
|
||||
}
|
||||
@@ -119,6 +131,14 @@ impl ReqwestTransport {
|
||||
}
|
||||
}
|
||||
|
||||
fn request_body_for_trace(req: &Request) -> String {
|
||||
match req.body.as_ref() {
|
||||
Some(RequestBody::Json(body)) => body.to_string(),
|
||||
Some(RequestBody::Raw(body)) => format!("<raw body: {} bytes>", body.len()),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HttpTransport for ReqwestTransport {
|
||||
async fn execute(&self, req: Request) -> Result<Response, TransportError> {
|
||||
@@ -127,7 +147,7 @@ impl HttpTransport for ReqwestTransport {
|
||||
"{} to {}: {}",
|
||||
req.method,
|
||||
req.url,
|
||||
req.body.as_ref().unwrap_or_default()
|
||||
request_body_for_trace(&req)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,7 +179,7 @@ impl HttpTransport for ReqwestTransport {
|
||||
"{} to {}: {}",
|
||||
req.method,
|
||||
req.url,
|
||||
req.body.as_ref().unwrap_or_default()
|
||||
request_body_for_trace(&req)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+29
-10
@@ -39,6 +39,8 @@ use codex_api::MemoriesClient as ApiMemoriesClient;
|
||||
use codex_api::MemorySummarizeInput as ApiMemorySummarizeInput;
|
||||
use codex_api::MemorySummarizeOutput as ApiMemorySummarizeOutput;
|
||||
use codex_api::RawMemory as ApiRawMemory;
|
||||
use codex_api::RealtimeCallClient as ApiRealtimeCallClient;
|
||||
use codex_api::RealtimeSessionConfig;
|
||||
use codex_api::Reasoning;
|
||||
use codex_api::RequestTelemetry;
|
||||
use codex_api::ReqwestTransport;
|
||||
@@ -403,11 +405,7 @@ impl ModelClient {
|
||||
ApiCompactClient::new(transport, client_setup.api_provider, client_setup.api_auth)
|
||||
.with_telemetry(Some(request_telemetry));
|
||||
|
||||
let instructions = prompt
|
||||
.base_instructions
|
||||
.as_ref()
|
||||
.map(|base_instructions| base_instructions.text.clone())
|
||||
.unwrap_or_default();
|
||||
let instructions = prompt.base_instructions.text.clone();
|
||||
let input = prompt.get_formatted_input();
|
||||
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
|
||||
let reasoning = Self::build_reasoning(model_info, effort, summary);
|
||||
@@ -447,6 +445,30 @@ impl ModelClient {
|
||||
.map_err(map_api_error)
|
||||
}
|
||||
|
||||
pub async fn create_realtime_call(
|
||||
&self,
|
||||
sdp: String,
|
||||
session_config: RealtimeSessionConfig,
|
||||
) -> Result<String> {
|
||||
self.create_realtime_call_with_headers(sdp, session_config, ApiHeaderMap::new())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_realtime_call_with_headers(
|
||||
&self,
|
||||
sdp: String,
|
||||
session_config: RealtimeSessionConfig,
|
||||
extra_headers: ApiHeaderMap,
|
||||
) -> Result<String> {
|
||||
let client_setup = self.current_client_setup().await?;
|
||||
let transport = ReqwestTransport::new(build_reqwest_client());
|
||||
ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth)
|
||||
.create_with_session_and_headers(sdp, session_config, extra_headers)
|
||||
.await
|
||||
.map(|response| response.sdp)
|
||||
.map_err(map_api_error)
|
||||
}
|
||||
|
||||
/// Builds memory summaries for each provided normalized raw memory.
|
||||
///
|
||||
/// This is a unary call (no streaming) to `/v1/memories/trace_summarize`.
|
||||
@@ -771,10 +793,7 @@ impl ModelClientSession {
|
||||
summary: ReasoningSummaryConfig,
|
||||
service_tier: Option<ServiceTier>,
|
||||
) -> Result<ResponsesApiRequest> {
|
||||
let instructions = prompt
|
||||
.base_instructions
|
||||
.as_ref()
|
||||
.map(|base_instructions| base_instructions.text.clone());
|
||||
let instructions = &prompt.base_instructions.text;
|
||||
let input = prompt.get_formatted_input();
|
||||
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
|
||||
let default_reasoning_effort = model_info.default_reasoning_level;
|
||||
@@ -813,7 +832,7 @@ impl ModelClientSession {
|
||||
let prompt_cache_key = Some(self.client.state.conversation_id.to_string());
|
||||
let request = ResponsesApiRequest {
|
||||
model: model_info.slug.clone(),
|
||||
instructions,
|
||||
instructions: instructions.clone(),
|
||||
input,
|
||||
tools,
|
||||
tool_choice: "auto".to_string(),
|
||||
|
||||
@@ -23,7 +23,7 @@ pub const REVIEW_EXIT_INTERRUPTED_TMPL: &str =
|
||||
include_str!("../templates/review/exit_interrupted.xml");
|
||||
|
||||
/// API request payload for a single model turn
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Prompt {
|
||||
/// Conversation context input items.
|
||||
pub input: Vec<ResponseItem>,
|
||||
@@ -35,7 +35,7 @@ pub struct Prompt {
|
||||
/// Whether parallel tool calls are permitted for this prompt.
|
||||
pub(crate) parallel_tool_calls: bool,
|
||||
|
||||
pub base_instructions: Option<BaseInstructions>,
|
||||
pub base_instructions: BaseInstructions,
|
||||
|
||||
/// Optionally specify the personality of the model.
|
||||
pub personality: Option<Personality>,
|
||||
@@ -44,19 +44,6 @@ pub struct Prompt {
|
||||
pub output_schema: Option<Value>,
|
||||
}
|
||||
|
||||
impl Default for Prompt {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
parallel_tool_calls: false,
|
||||
base_instructions: Some(BaseInstructions::default()),
|
||||
personality: None,
|
||||
output_schema: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
|
||||
let mut input = self.input.clone();
|
||||
|
||||
@@ -14,7 +14,7 @@ fn serializes_text_verbosity_when_set() {
|
||||
let tools: Vec<serde_json::Value> = vec![];
|
||||
let req = ResponsesApiRequest {
|
||||
model: "gpt-5.1".to_string(),
|
||||
instructions: Some("i".to_string()),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
tool_choice: "auto".to_string(),
|
||||
@@ -58,7 +58,7 @@ fn serializes_text_schema_with_strict_format() {
|
||||
|
||||
let req = ResponsesApiRequest {
|
||||
model: "gpt-5.1".to_string(),
|
||||
instructions: Some("i".to_string()),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
tool_choice: "auto".to_string(),
|
||||
@@ -96,7 +96,7 @@ fn omits_text_when_not_set() {
|
||||
let tools: Vec<serde_json::Value> = vec![];
|
||||
let req = ResponsesApiRequest {
|
||||
model: "gpt-5.1".to_string(),
|
||||
instructions: Some("i".to_string()),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
tool_choice: "auto".to_string(),
|
||||
@@ -119,7 +119,7 @@ fn omits_text_when_not_set() {
|
||||
fn serializes_flex_service_tier_when_set() {
|
||||
let req = ResponsesApiRequest {
|
||||
model: "gpt-5.1".to_string(),
|
||||
instructions: Some("i".to_string()),
|
||||
instructions: "i".to_string(),
|
||||
input: vec![],
|
||||
tools: vec![],
|
||||
tool_choice: "auto".to_string(),
|
||||
|
||||
+20
-53
@@ -579,15 +579,11 @@ impl Codex {
|
||||
let model_info = models_manager
|
||||
.get_model_info(model.as_str(), &config.to_models_manager_config())
|
||||
.await;
|
||||
let base_instructions = match config.base_instructions.clone() {
|
||||
Some(base_instructions) => base_instructions,
|
||||
None => conversation_history
|
||||
.get_base_instructions()
|
||||
.map(|base_instructions| {
|
||||
base_instructions.map(|base_instructions| base_instructions.text)
|
||||
})
|
||||
.unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))),
|
||||
};
|
||||
let base_instructions = config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.or_else(|| conversation_history.get_base_instructions().map(|s| s.text))
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality));
|
||||
|
||||
// Respect thread-start tools. When missing (resumed/forked threads), read from the db
|
||||
// first, then fall back to rollout-file tools.
|
||||
@@ -616,14 +612,6 @@ impl Codex {
|
||||
dynamic_tools
|
||||
};
|
||||
|
||||
let developer_instructions_override = config
|
||||
.developer_instructions_override
|
||||
.clone()
|
||||
.or_else(|| conversation_history.get_developer_instructions());
|
||||
let developer_instructions = developer_instructions_override
|
||||
.clone()
|
||||
.unwrap_or_else(|| config.developer_instructions.clone());
|
||||
|
||||
// TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode
|
||||
// to avoid extracting these fields separately and constructing CollaborationMode here.
|
||||
let collaboration_mode = CollaborationMode {
|
||||
@@ -639,8 +627,7 @@ impl Codex {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
service_tier: config.service_tier,
|
||||
developer_instructions,
|
||||
developer_instructions_override,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions,
|
||||
personality: config.personality,
|
||||
base_instructions,
|
||||
@@ -1106,10 +1093,6 @@ pub(crate) struct SessionConfiguration {
|
||||
/// Developer instructions that supplement the base instructions.
|
||||
developer_instructions: Option<String>,
|
||||
|
||||
/// Explicit developer instructions override, preserving `null` as distinct
|
||||
/// from a missing override.
|
||||
developer_instructions_override: Option<Option<String>>,
|
||||
|
||||
/// Model instructions that are appended to the base instructions.
|
||||
user_instructions: Option<String>,
|
||||
|
||||
@@ -1117,7 +1100,7 @@ pub(crate) struct SessionConfiguration {
|
||||
personality: Option<Personality>,
|
||||
|
||||
/// Base instructions for the session.
|
||||
base_instructions: Option<String>,
|
||||
base_instructions: String,
|
||||
|
||||
/// Compact prompt override.
|
||||
compact_prompt: Option<String>,
|
||||
@@ -1556,13 +1539,9 @@ impl Session {
|
||||
conversation_id,
|
||||
forked_from_id,
|
||||
session_source,
|
||||
session_configuration
|
||||
.base_instructions
|
||||
.clone()
|
||||
.map(|text| BaseInstructions { text }),
|
||||
session_configuration
|
||||
.developer_instructions_override
|
||||
.clone(),
|
||||
BaseInstructions {
|
||||
text: session_configuration.base_instructions.clone(),
|
||||
},
|
||||
session_configuration.dynamic_tools.clone(),
|
||||
if session_configuration.persist_extended_history {
|
||||
EventPersistenceMode::Extended
|
||||
@@ -2126,9 +2105,8 @@ impl Session {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(base_instructions) = session_configuration.base_instructions.clone() {
|
||||
sess.schedule_startup_prewarm(base_instructions).await;
|
||||
}
|
||||
sess.schedule_startup_prewarm(session_configuration.base_instructions.clone())
|
||||
.await;
|
||||
let session_start_source = match &initial_history {
|
||||
InitialHistory::Resumed(_) => codex_hooks::SessionStartSource::Resume,
|
||||
InitialHistory::New | InitialHistory::Forked(_) => {
|
||||
@@ -2230,13 +2208,11 @@ impl Session {
|
||||
state.history.estimate_token_count(turn_context)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_base_instructions(&self) -> Option<BaseInstructions> {
|
||||
pub(crate) async fn get_base_instructions(&self) -> BaseInstructions {
|
||||
let state = self.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.base_instructions
|
||||
.clone()
|
||||
.map(|text| BaseInstructions { text })
|
||||
BaseInstructions {
|
||||
text: state.session_configuration.base_instructions.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// Merges connector IDs into the session-level explicit connector selection.
|
||||
@@ -3640,11 +3616,7 @@ impl Session {
|
||||
state.reference_context_item(),
|
||||
state.previous_turn_settings(),
|
||||
state.session_configuration.collaboration_mode.clone(),
|
||||
state
|
||||
.session_configuration
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
state.session_configuration.base_instructions.clone(),
|
||||
state.session_configuration.session_source.clone(),
|
||||
)
|
||||
};
|
||||
@@ -3885,13 +3857,7 @@ impl Session {
|
||||
|
||||
pub(crate) async fn recompute_token_usage(&self, turn_context: &TurnContext) {
|
||||
let history = self.clone_history().await;
|
||||
let empty_base_instructions = BaseInstructions {
|
||||
text: String::new(),
|
||||
};
|
||||
let base_instructions = self
|
||||
.get_base_instructions()
|
||||
.await
|
||||
.unwrap_or(empty_base_instructions);
|
||||
let base_instructions = self.get_base_instructions().await;
|
||||
let Some(estimated_total_tokens) =
|
||||
history.estimate_token_count_with_base_instructions(&base_instructions)
|
||||
else {
|
||||
@@ -6601,7 +6567,7 @@ pub(crate) fn build_prompt(
|
||||
input: Vec<ResponseItem>,
|
||||
router: &ToolRouter,
|
||||
turn_context: &TurnContext,
|
||||
base_instructions: Option<BaseInstructions>,
|
||||
base_instructions: BaseInstructions,
|
||||
) -> Prompt {
|
||||
let deferred_dynamic_tools = turn_context
|
||||
.dynamic_tools
|
||||
@@ -7088,6 +7054,7 @@ fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
|
||||
EventMsg::Error(_)
|
||||
| EventMsg::Warning(_)
|
||||
| EventMsg::RealtimeConversationStarted(_)
|
||||
| EventMsg::RealtimeConversationSdp(_)
|
||||
| EventMsg::RealtimeConversationRealtime(_)
|
||||
| EventMsg::RealtimeConversationClosed(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
|
||||
@@ -592,15 +592,11 @@ async fn get_base_instructions_no_user_content() {
|
||||
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.base_instructions =
|
||||
Some(model_info.base_instructions.clone());
|
||||
state.session_configuration.base_instructions = model_info.base_instructions.clone();
|
||||
}
|
||||
|
||||
let base_instructions = session.get_base_instructions().await;
|
||||
assert_eq!(
|
||||
base_instructions.expect("base instructions").text,
|
||||
model_info.base_instructions
|
||||
);
|
||||
assert_eq!(base_instructions.text, model_info.base_instructions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1096,7 +1092,7 @@ async fn recompute_token_usage_uses_session_base_instructions() {
|
||||
let override_instructions = "SESSION_OVERRIDE_INSTRUCTIONS_ONLY".repeat(120);
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.base_instructions = Some(override_instructions.clone());
|
||||
state.session_configuration.base_instructions = override_instructions.clone();
|
||||
}
|
||||
|
||||
let item = user_message("hello");
|
||||
@@ -1854,14 +1850,13 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -1957,14 +1952,13 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -2229,8 +2223,7 @@ async fn attach_rollout_recorder(session: &Arc<Session>) -> PathBuf {
|
||||
ThreadId::default(),
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
@@ -2308,14 +2301,13 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -2572,14 +2564,13 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -2676,14 +2667,13 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -3518,14 +3508,13 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
collaboration_mode,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
developer_instructions_override: config.developer_instructions_override.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| Some(model_info.get_model_instructions(config.personality))),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -4270,8 +4259,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
|
||||
ThreadId::default(),
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
@@ -4368,8 +4356,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
|
||||
ThreadId::default(),
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
|
||||
@@ -76,16 +76,10 @@ async fn run_remote_compact_task_inner_impl(
|
||||
.await;
|
||||
let mut history = sess.clone_history().await;
|
||||
let base_instructions = sess.get_base_instructions().await;
|
||||
let token_count_base_instructions =
|
||||
base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| BaseInstructions {
|
||||
text: String::new(),
|
||||
});
|
||||
let deleted_items = trim_function_call_history_to_fit_context_window(
|
||||
&mut history,
|
||||
turn_context.as_ref(),
|
||||
&token_count_base_instructions,
|
||||
&base_instructions,
|
||||
);
|
||||
if deleted_items > 0 {
|
||||
info!(
|
||||
@@ -133,13 +127,8 @@ async fn run_remote_compact_task_inner_impl(
|
||||
)
|
||||
.or_else(|err| async {
|
||||
let total_usage_breakdown = sess.get_total_token_usage_breakdown().await;
|
||||
let base_instruction_text = prompt
|
||||
.base_instructions
|
||||
.as_ref()
|
||||
.map(|base_instructions| base_instructions.text.as_str())
|
||||
.unwrap_or("");
|
||||
let compact_request_log_data =
|
||||
build_compact_request_log_data(&prompt.input, base_instruction_text);
|
||||
build_compact_request_log_data(&prompt.input, &prompt.base_instructions.text);
|
||||
log_remote_compact_failure(
|
||||
turn_context,
|
||||
&compact_request_log_data,
|
||||
|
||||
@@ -4506,7 +4506,6 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
developer_instructions_override: None,
|
||||
guardian_developer_instructions: None,
|
||||
include_permissions_instructions: true,
|
||||
include_apps_instructions: true,
|
||||
@@ -4652,7 +4651,6 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
developer_instructions_override: None,
|
||||
guardian_developer_instructions: None,
|
||||
include_permissions_instructions: true,
|
||||
include_apps_instructions: true,
|
||||
@@ -4796,7 +4794,6 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
developer_instructions_override: None,
|
||||
guardian_developer_instructions: None,
|
||||
include_permissions_instructions: true,
|
||||
include_apps_instructions: true,
|
||||
@@ -4926,7 +4923,6 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
experimental_realtime_ws_startup_context: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
developer_instructions_override: None,
|
||||
guardian_developer_instructions: None,
|
||||
include_permissions_instructions: true,
|
||||
include_apps_instructions: true,
|
||||
|
||||
@@ -243,15 +243,11 @@ pub struct Config {
|
||||
pub user_instructions: Option<String>,
|
||||
|
||||
/// Base instructions override.
|
||||
pub base_instructions: Option<Option<String>>,
|
||||
pub base_instructions: Option<String>,
|
||||
|
||||
/// Developer instructions override injected as a separate message.
|
||||
pub developer_instructions: Option<String>,
|
||||
|
||||
/// Explicit developer instructions override, preserving `null` as distinct
|
||||
/// from a missing override.
|
||||
pub developer_instructions_override: Option<Option<String>>,
|
||||
|
||||
/// Guardian-specific developer instructions override from requirements.toml.
|
||||
pub guardian_developer_instructions: Option<String>,
|
||||
|
||||
@@ -691,7 +687,7 @@ impl Config {
|
||||
model_context_window: self.model_context_window,
|
||||
model_auto_compact_token_limit: self.model_auto_compact_token_limit,
|
||||
tool_output_token_limit: self.tool_output_token_limit,
|
||||
base_instructions: self.base_instructions.clone().flatten(),
|
||||
base_instructions: self.base_instructions.clone(),
|
||||
personality_enabled: self.features.enabled(Feature::Personality),
|
||||
model_supports_reasoning_summaries: self.model_supports_reasoning_summaries,
|
||||
model_catalog: self.model_catalog.clone(),
|
||||
@@ -1204,8 +1200,8 @@ pub struct ConfigOverrides {
|
||||
pub js_repl_node_path: Option<PathBuf>,
|
||||
pub js_repl_node_module_dirs: Option<Vec<PathBuf>>,
|
||||
pub zsh_path: Option<PathBuf>,
|
||||
pub base_instructions: Option<Option<String>>,
|
||||
pub developer_instructions: Option<Option<String>>,
|
||||
pub base_instructions: Option<String>,
|
||||
pub developer_instructions: Option<String>,
|
||||
pub personality: Option<Personality>,
|
||||
pub compact_prompt: Option<String>,
|
||||
pub include_apply_patch_tool: Option<bool>,
|
||||
@@ -1753,10 +1749,8 @@ impl Config {
|
||||
.or(cfg.model_instructions_file.as_ref());
|
||||
let file_base_instructions =
|
||||
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?;
|
||||
let base_instructions = base_instructions.or_else(|| file_base_instructions.map(Some));
|
||||
let developer_instructions_override = developer_instructions.clone();
|
||||
let developer_instructions =
|
||||
developer_instructions.unwrap_or_else(|| cfg.developer_instructions.clone());
|
||||
let base_instructions = base_instructions.or(file_base_instructions);
|
||||
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
|
||||
let include_permissions_instructions = config_profile
|
||||
.include_permissions_instructions
|
||||
.or(cfg.include_permissions_instructions)
|
||||
@@ -1939,7 +1933,6 @@ impl Config {
|
||||
base_instructions,
|
||||
personality,
|
||||
developer_instructions,
|
||||
developer_instructions_override,
|
||||
compact_prompt,
|
||||
commit_attribution,
|
||||
include_permissions_instructions,
|
||||
|
||||
@@ -906,7 +906,7 @@ model_instructions_file = "child.txt"
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.base_instructions.as_ref().and_then(Option::as_deref),
|
||||
config.base_instructions.as_deref(),
|
||||
Some("child instructions")
|
||||
);
|
||||
|
||||
@@ -942,7 +942,7 @@ async fn cli_override_model_instructions_file_sets_base_instructions() -> std::i
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.base_instructions.as_ref().and_then(Option::as_deref),
|
||||
config.base_instructions.as_deref(),
|
||||
Some("cli override instructions")
|
||||
);
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ impl GuardianReviewSessionReuseKey {
|
||||
model_reasoning_summary: spawn_config.model_reasoning_summary,
|
||||
permissions: spawn_config.permissions.clone(),
|
||||
developer_instructions: spawn_config.developer_instructions.clone(),
|
||||
base_instructions: spawn_config.base_instructions.clone().flatten(),
|
||||
base_instructions: spawn_config.base_instructions.clone(),
|
||||
user_instructions: spawn_config.user_instructions.clone(),
|
||||
compact_prompt: spawn_config.compact_prompt.clone(),
|
||||
cwd: spawn_config.cwd.clone(),
|
||||
|
||||
@@ -336,9 +336,9 @@ mod job {
|
||||
}],
|
||||
tools: Vec::new(),
|
||||
parallel_tool_calls: false,
|
||||
base_instructions: Some(BaseInstructions {
|
||||
base_instructions: BaseInstructions {
|
||||
text: phase_one::PROMPT.to_string(),
|
||||
}),
|
||||
},
|
||||
personality: None,
|
||||
output_schema: Some(output_schema()),
|
||||
};
|
||||
|
||||
@@ -43,7 +43,6 @@ async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> {
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::client::ModelClient;
|
||||
use crate::codex::Session;
|
||||
use crate::realtime_context::build_realtime_startup_context;
|
||||
use async_channel::Receiver;
|
||||
@@ -27,12 +28,14 @@ use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::ConversationAudioParams;
|
||||
use codex_protocol::protocol::ConversationStartParams;
|
||||
use codex_protocol::protocol::ConversationStartTransport;
|
||||
use codex_protocol::protocol::ConversationTextParams;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RealtimeConversationClosedEvent;
|
||||
use codex_protocol::protocol::RealtimeConversationRealtimeEvent;
|
||||
use codex_protocol::protocol::RealtimeConversationSdpEvent;
|
||||
use codex_protocol::protocol::RealtimeConversationStartedEvent;
|
||||
use codex_protocol::protocol::RealtimeHandoffRequested;
|
||||
use http::HeaderMap;
|
||||
@@ -139,6 +142,24 @@ struct ConversationState {
|
||||
realtime_active: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
struct RealtimeStart {
|
||||
api_provider: ApiProvider,
|
||||
extra_headers: Option<HeaderMap>,
|
||||
session_config: RealtimeSessionConfig,
|
||||
model_client: ModelClient,
|
||||
sdp: Option<String>,
|
||||
}
|
||||
|
||||
struct RealtimeStartOutput {
|
||||
realtime_active: Arc<AtomicBool>,
|
||||
connection: RealtimeStartConnection,
|
||||
}
|
||||
|
||||
enum RealtimeStartConnection {
|
||||
Websocket { events_rx: Receiver<RealtimeEvent> },
|
||||
Webrtc { sdp: String },
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl RealtimeConversationManager {
|
||||
pub(crate) fn new() -> Self {
|
||||
@@ -154,12 +175,7 @@ impl RealtimeConversationManager {
|
||||
.and_then(|state| state.realtime_active.load(Ordering::Relaxed).then_some(()))
|
||||
}
|
||||
|
||||
pub(crate) async fn start(
|
||||
&self,
|
||||
api_provider: ApiProvider,
|
||||
extra_headers: Option<HeaderMap>,
|
||||
session_config: RealtimeSessionConfig,
|
||||
) -> CodexResult<(Receiver<RealtimeEvent>, Arc<AtomicBool>)> {
|
||||
async fn start(&self, start: RealtimeStart) -> CodexResult<RealtimeStartOutput> {
|
||||
let previous_state = {
|
||||
let mut guard = self.state.lock().await;
|
||||
guard.take()
|
||||
@@ -167,10 +183,37 @@ impl RealtimeConversationManager {
|
||||
if let Some(state) = previous_state {
|
||||
stop_conversation_state(state, RealtimeFanoutTaskStop::Abort).await;
|
||||
}
|
||||
|
||||
self.start_inner(start).await
|
||||
}
|
||||
|
||||
async fn start_inner(&self, start: RealtimeStart) -> CodexResult<RealtimeStartOutput> {
|
||||
let RealtimeStart {
|
||||
api_provider,
|
||||
extra_headers,
|
||||
session_config,
|
||||
model_client,
|
||||
sdp,
|
||||
} = start;
|
||||
let session_kind = match session_config.event_parser {
|
||||
RealtimeEventParser::V1 => RealtimeSessionKind::V1,
|
||||
RealtimeEventParser::RealtimeV2 => RealtimeSessionKind::V2,
|
||||
};
|
||||
let realtime_active = Arc::new(AtomicBool::new(true));
|
||||
|
||||
if let Some(sdp) = sdp {
|
||||
let sdp = model_client
|
||||
.create_realtime_call_with_headers(
|
||||
sdp,
|
||||
session_config,
|
||||
extra_headers.unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
return Ok(RealtimeStartOutput {
|
||||
realtime_active,
|
||||
connection: RealtimeStartConnection::Webrtc { sdp },
|
||||
});
|
||||
}
|
||||
|
||||
let client = RealtimeWebsocketClient::new(api_provider);
|
||||
let connection = client
|
||||
@@ -216,7 +259,10 @@ impl RealtimeConversationManager {
|
||||
fanout_task: None,
|
||||
realtime_active: Arc::clone(&realtime_active),
|
||||
});
|
||||
Ok((events_rx, realtime_active))
|
||||
Ok(RealtimeStartOutput {
|
||||
realtime_active,
|
||||
connection: RealtimeStartConnection::Websocket { events_rx },
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn register_fanout_task(
|
||||
@@ -447,6 +493,7 @@ struct PreparedRealtimeConversationStart {
|
||||
requested_session_id: Option<String>,
|
||||
version: RealtimeWsVersion,
|
||||
session_config: RealtimeSessionConfig,
|
||||
transport: ConversationStartTransport,
|
||||
}
|
||||
|
||||
async fn prepare_realtime_start(
|
||||
@@ -460,16 +507,54 @@ async fn prepare_realtime_start(
|
||||
.auth_manager()
|
||||
.unwrap_or_else(|| Arc::clone(&sess.services.auth_manager));
|
||||
let auth = auth_manager.auth().await;
|
||||
let realtime_api_key = realtime_api_key(auth.as_ref(), &provider)?;
|
||||
let mut api_provider = provider.to_api_provider(Some(AuthMode::ApiKey))?;
|
||||
let config = sess.get_config().await;
|
||||
let transport = params
|
||||
.transport
|
||||
.unwrap_or(ConversationStartTransport::Websocket);
|
||||
let mut api_provider = if matches!(transport, ConversationStartTransport::Websocket) {
|
||||
provider.to_api_provider(Some(AuthMode::ApiKey))?
|
||||
} else {
|
||||
provider.to_api_provider(auth.as_ref().map(CodexAuth::auth_mode))?
|
||||
};
|
||||
if let Some(realtime_ws_base_url) = &config.experimental_realtime_ws_base_url {
|
||||
api_provider.base_url = realtime_ws_base_url.clone();
|
||||
}
|
||||
let version = config.realtime.version;
|
||||
let session_config =
|
||||
build_realtime_session_config(sess, params.prompt, params.session_id).await?;
|
||||
let requested_session_id = session_config.session_id.clone();
|
||||
let extra_headers = match transport {
|
||||
ConversationStartTransport::Websocket => {
|
||||
let realtime_api_key = realtime_api_key(auth.as_ref(), &provider)?;
|
||||
realtime_request_headers(
|
||||
requested_session_id.as_deref(),
|
||||
Some(realtime_api_key.as_str()),
|
||||
)?
|
||||
}
|
||||
ConversationStartTransport::Webrtc { .. } => {
|
||||
realtime_request_headers(requested_session_id.as_deref(), /*api_key*/ None)?
|
||||
}
|
||||
};
|
||||
Ok(PreparedRealtimeConversationStart {
|
||||
api_provider,
|
||||
extra_headers,
|
||||
requested_session_id,
|
||||
version,
|
||||
session_config,
|
||||
transport,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn build_realtime_session_config(
|
||||
sess: &Arc<Session>,
|
||||
prompt: String,
|
||||
session_id: Option<String>,
|
||||
) -> CodexResult<RealtimeSessionConfig> {
|
||||
let config = sess.get_config().await;
|
||||
let prompt = config
|
||||
.experimental_realtime_ws_backend_prompt
|
||||
.clone()
|
||||
.unwrap_or(params.prompt);
|
||||
.unwrap_or(prompt);
|
||||
let startup_context = match config.experimental_realtime_ws_startup_context.clone() {
|
||||
Some(startup_context) => startup_context,
|
||||
None => {
|
||||
@@ -484,8 +569,7 @@ async fn prepare_realtime_start(
|
||||
format!("{prompt}\n\n{startup_context}")
|
||||
};
|
||||
let model = config.experimental_realtime_ws_model.clone();
|
||||
let version = config.realtime.version;
|
||||
let event_parser = match version {
|
||||
let event_parser = match config.realtime.version {
|
||||
RealtimeWsVersion::V1 => RealtimeEventParser::V1,
|
||||
RealtimeWsVersion::V2 => RealtimeEventParser::RealtimeV2,
|
||||
};
|
||||
@@ -493,22 +577,12 @@ async fn prepare_realtime_start(
|
||||
RealtimeWsMode::Conversational => RealtimeSessionMode::Conversational,
|
||||
RealtimeWsMode::Transcription => RealtimeSessionMode::Transcription,
|
||||
};
|
||||
let requested_session_id = params.session_id.or(Some(sess.conversation_id.to_string()));
|
||||
let session_config = RealtimeSessionConfig {
|
||||
Ok(RealtimeSessionConfig {
|
||||
instructions: prompt,
|
||||
model,
|
||||
session_id: requested_session_id.clone(),
|
||||
session_id: Some(session_id.unwrap_or_else(|| sess.conversation_id.to_string())),
|
||||
event_parser,
|
||||
session_mode,
|
||||
};
|
||||
let extra_headers =
|
||||
realtime_request_headers(requested_session_id.as_deref(), realtime_api_key.as_str())?;
|
||||
Ok(PreparedRealtimeConversationStart {
|
||||
api_provider,
|
||||
extra_headers,
|
||||
requested_session_id,
|
||||
version,
|
||||
session_config,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -523,12 +597,21 @@ async fn handle_start_inner(
|
||||
requested_session_id,
|
||||
version,
|
||||
session_config,
|
||||
transport,
|
||||
} = prepared_start;
|
||||
info!("starting realtime conversation");
|
||||
let (events_rx, realtime_active) = sess
|
||||
.conversation
|
||||
.start(api_provider, extra_headers, session_config)
|
||||
.await?;
|
||||
let sdp = match transport {
|
||||
ConversationStartTransport::Websocket => None,
|
||||
ConversationStartTransport::Webrtc { sdp } => Some(sdp),
|
||||
};
|
||||
let start = RealtimeStart {
|
||||
api_provider,
|
||||
extra_headers,
|
||||
session_config,
|
||||
model_client: sess.services.model_client.clone(),
|
||||
sdp,
|
||||
};
|
||||
let start_output = sess.conversation.start(start).await?;
|
||||
|
||||
info!("realtime conversation started");
|
||||
|
||||
@@ -541,6 +624,29 @@ async fn handle_start_inner(
|
||||
})
|
||||
.await;
|
||||
|
||||
let RealtimeStartOutput {
|
||||
realtime_active,
|
||||
connection,
|
||||
} = start_output;
|
||||
let events_rx = match connection {
|
||||
RealtimeStartConnection::Websocket { events_rx } => events_rx,
|
||||
RealtimeStartConnection::Webrtc { sdp } => {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id.to_string(),
|
||||
msg: EventMsg::RealtimeConversationSdp(RealtimeConversationSdpEvent { sdp }),
|
||||
})
|
||||
.await;
|
||||
sess.conversation.finish_if_active(&realtime_active).await;
|
||||
send_realtime_conversation_closed(
|
||||
sess,
|
||||
sub_id.to_string(),
|
||||
RealtimeConversationEnd::TransportClosed,
|
||||
)
|
||||
.await;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let sess_clone = Arc::clone(sess);
|
||||
let sub_id = sub_id.to_string();
|
||||
let fanout_realtime_active = Arc::clone(&realtime_active);
|
||||
@@ -660,7 +766,7 @@ fn realtime_api_key(auth: Option<&CodexAuth>, provider: &ModelProviderInfo) -> C
|
||||
|
||||
fn realtime_request_headers(
|
||||
session_id: Option<&str>,
|
||||
api_key: &str,
|
||||
api_key: Option<&str>,
|
||||
) -> CodexResult<Option<HeaderMap>> {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
@@ -670,10 +776,12 @@ fn realtime_request_headers(
|
||||
headers.insert("x-session-id", session_id);
|
||||
}
|
||||
|
||||
let auth_value = HeaderValue::from_str(&format!("Bearer {api_key}")).map_err(|err| {
|
||||
CodexErr::InvalidRequest(format!("invalid realtime api key header: {err}"))
|
||||
})?;
|
||||
headers.insert(AUTHORIZATION, auth_value);
|
||||
if let Some(api_key) = api_key {
|
||||
let auth_value = HeaderValue::from_str(&format!("Bearer {api_key}")).map_err(|err| {
|
||||
CodexErr::InvalidRequest(format!("invalid realtime api key header: {err}"))
|
||||
})?;
|
||||
headers.insert(AUTHORIZATION, auth_value);
|
||||
}
|
||||
|
||||
Ok(Some(headers))
|
||||
}
|
||||
|
||||
@@ -217,9 +217,9 @@ async fn schedule_startup_prewarm_inner(
|
||||
Vec::new(),
|
||||
startup_router.as_ref(),
|
||||
startup_turn_context.as_ref(),
|
||||
Some(BaseInstructions {
|
||||
BaseInstructions {
|
||||
text: base_instructions,
|
||||
}),
|
||||
},
|
||||
);
|
||||
let startup_turn_metadata_header = startup_turn_context
|
||||
.turn_metadata_state
|
||||
|
||||
@@ -112,7 +112,7 @@ async fn start_review_conversation(
|
||||
let _ = sub_agent_config.features.disable(Feature::Collab);
|
||||
|
||||
// Set explicit review rubric for the sub-agent
|
||||
sub_agent_config.base_instructions = Some(Some(crate::REVIEW_PROMPT.to_string()));
|
||||
sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string());
|
||||
sub_agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||
|
||||
let model = config
|
||||
|
||||
@@ -535,7 +535,7 @@ async fn build_runner_options(
|
||||
let max_concurrency =
|
||||
normalize_concurrency(requested_concurrency, turn.config.agent_max_threads);
|
||||
let base_instructions = session.get_base_instructions().await;
|
||||
let spawn_config = build_agent_spawn_config(base_instructions.as_ref(), turn.as_ref())?;
|
||||
let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref())?;
|
||||
Ok(JobRunnerOptions {
|
||||
max_concurrency,
|
||||
spawn_config,
|
||||
|
||||
@@ -59,10 +59,8 @@ impl ToolHandler for Handler {
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
let mut config = build_agent_spawn_config(
|
||||
session.get_base_instructions().await.as_ref(),
|
||||
turn.as_ref(),
|
||||
)?;
|
||||
let mut config =
|
||||
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
|
||||
@@ -201,12 +201,11 @@ pub(crate) fn parse_collab_input(
|
||||
/// skipping this helper and cloning stale config state directly can send the child agent out with
|
||||
/// the wrong provider or runtime policy.
|
||||
pub(crate) fn build_agent_spawn_config(
|
||||
base_instructions: Option<&BaseInstructions>,
|
||||
base_instructions: &BaseInstructions,
|
||||
turn: &TurnContext,
|
||||
) -> Result<Config, FunctionCallError> {
|
||||
let mut config = build_agent_shared_config(turn)?;
|
||||
config.base_instructions =
|
||||
Some(base_instructions.map(|base_instructions| base_instructions.text.clone()));
|
||||
config.base_instructions = Some(base_instructions.text.clone());
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
|
||||
@@ -3255,9 +3255,9 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("approval policy set");
|
||||
|
||||
let config = build_agent_spawn_config(Some(&base_instructions), &turn).expect("spawn config");
|
||||
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
|
||||
let mut expected = (*turn.config).clone();
|
||||
expected.base_instructions = Some(Some(base_instructions.text));
|
||||
expected.base_instructions = Some(base_instructions.text);
|
||||
expected.model = Some(turn.model_info.slug.clone());
|
||||
expected.model_provider = turn.provider.clone();
|
||||
expected.model_reasoning_effort = turn.reasoning_effort;
|
||||
@@ -3293,7 +3293,7 @@ async fn build_agent_spawn_config_preserves_base_user_instructions() {
|
||||
text: "base".to_string(),
|
||||
};
|
||||
|
||||
let config = build_agent_spawn_config(Some(&base_instructions), &turn).expect("spawn config");
|
||||
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
|
||||
|
||||
assert_eq!(config.user_instructions, base_config.user_instructions);
|
||||
}
|
||||
@@ -3302,7 +3302,7 @@ async fn build_agent_spawn_config_preserves_base_user_instructions() {
|
||||
async fn build_agent_resume_config_clears_base_instructions() {
|
||||
let (_session, mut turn) = make_session_and_context().await;
|
||||
let mut base_config = (*turn.config).clone();
|
||||
base_config.base_instructions = Some(Some("caller-base".to_string()));
|
||||
base_config.base_instructions = Some("caller-base".to_string());
|
||||
turn.config = Arc::new(base_config);
|
||||
turn.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
|
||||
@@ -69,10 +69,8 @@ impl ToolHandler for Handler {
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
let mut config = build_agent_spawn_config(
|
||||
session.get_base_instructions().await.as_ref(),
|
||||
turn.as_ref(),
|
||||
)?;
|
||||
let mut config =
|
||||
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! Apply Patch runtime: executes verified patches under the orchestrator.
|
||||
//!
|
||||
//! Assumes `apply_patch` verification/approval happened upstream. Reuses that
|
||||
//! decision to avoid re-prompting, builds the self-invocation command for
|
||||
//! `codex --codex-run-as-apply-patch`, and runs under the current
|
||||
//! `SandboxAttempt` with a minimal environment.
|
||||
//! decision to avoid re-prompting, applies through the remote filesystem when
|
||||
//! the turn uses a remote environment, or builds the self-invocation command
|
||||
//! for `codex --codex-run-as-apply-patch` and runs it under the current
|
||||
//! `SandboxAttempt` with a minimal environment for local turns.
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
@@ -22,6 +23,7 @@ use crate::tools::sandboxing::with_cached_approval;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::exec_output::StreamOutput;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
@@ -32,6 +34,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use futures::future::BoxFuture;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyPatchRequest {
|
||||
@@ -211,6 +214,32 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
|
||||
attempt: &SandboxAttempt<'_>,
|
||||
ctx: &ToolCtx,
|
||||
) -> Result<ExecToolCallOutput, ToolError> {
|
||||
if let Some(environment) = ctx.turn.environment.as_ref().filter(|env| env.is_remote()) {
|
||||
let started_at = Instant::now();
|
||||
let fs = environment.get_filesystem();
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let result = codex_apply_patch::apply_patch(
|
||||
&req.action.patch,
|
||||
&req.action.cwd,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
fs.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let stdout = String::from_utf8_lossy(&stdout).into_owned();
|
||||
let stderr = String::from_utf8_lossy(&stderr).into_owned();
|
||||
let exit_code = if result.is_ok() { 0 } else { 1 };
|
||||
return Ok(ExecToolCallOutput {
|
||||
exit_code,
|
||||
stdout: StreamOutput::new(stdout.clone()),
|
||||
stderr: StreamOutput::new(stderr.clone()),
|
||||
aggregated_output: StreamOutput::new(format!("{stdout}{stderr}")),
|
||||
duration: started_at.elapsed(),
|
||||
timed_out: false,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let command = Self::build_sandbox_command(req, &ctx.turn.config.codex_home)?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
|
||||
@@ -303,6 +303,10 @@ pub fn sandbox_network_env_var() -> &'static str {
|
||||
|
||||
const REMOTE_ENV_ENV_VAR: &str = "CODEX_TEST_REMOTE_ENV";
|
||||
|
||||
pub fn remote_env_env_var() -> &'static str {
|
||||
REMOTE_ENV_ENV_VAR
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RemoteEnvConfig {
|
||||
pub container_name: String,
|
||||
@@ -537,6 +541,30 @@ macro_rules! skip_if_no_network {
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! skip_if_remote {
|
||||
($reason:expr $(,)?) => {{
|
||||
if ::std::env::var_os($crate::remote_env_env_var()).is_some() {
|
||||
eprintln!(
|
||||
"Skipping test under {}: {}",
|
||||
$crate::remote_env_env_var(),
|
||||
$reason
|
||||
);
|
||||
return;
|
||||
}
|
||||
}};
|
||||
($return_value:expr, $reason:expr $(,)?) => {{
|
||||
if ::std::env::var_os($crate::remote_env_env_var()).is_some() {
|
||||
eprintln!(
|
||||
"Skipping test under {}: {}",
|
||||
$crate::remote_env_env_var(),
|
||||
$reason
|
||||
);
|
||||
return $return_value;
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! codex_linux_sandbox_exe_or_skip {
|
||||
() => {{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::future::Future;
|
||||
use std::io::ErrorKind;
|
||||
use std::mem::swap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -19,6 +20,7 @@ use codex_core::shell::Shell;
|
||||
use codex_core::shell::get_shell_by_model_provided_path;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::RemoveOptions;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
@@ -796,6 +798,12 @@ impl TestCodexHarness {
|
||||
Ok(Self { server, test })
|
||||
}
|
||||
|
||||
pub async fn with_remote_aware_builder(mut builder: TestCodexBuilder) -> Result<Self> {
|
||||
let server = start_mock_server().await;
|
||||
let test = builder.build_remote_aware(&server).await?;
|
||||
Ok(Self { server, test })
|
||||
}
|
||||
|
||||
pub fn server(&self) -> &MockServer {
|
||||
&self.server
|
||||
}
|
||||
@@ -805,11 +813,75 @@ impl TestCodexHarness {
|
||||
}
|
||||
|
||||
pub fn cwd(&self) -> &Path {
|
||||
self.test.cwd_path()
|
||||
self.test.config.cwd.as_path()
|
||||
}
|
||||
|
||||
pub fn path(&self, rel: impl AsRef<Path>) -> PathBuf {
|
||||
self.test.workspace_path(rel)
|
||||
self.path_abs(rel).into_path_buf()
|
||||
}
|
||||
|
||||
pub fn path_abs(&self, rel: impl AsRef<Path>) -> AbsolutePathBuf {
|
||||
self.test.config.cwd.join(rel)
|
||||
}
|
||||
|
||||
pub async fn write_file(
|
||||
&self,
|
||||
rel: impl AsRef<Path>,
|
||||
contents: impl AsRef<[u8]>,
|
||||
) -> Result<()> {
|
||||
let abs_path = self.path_abs(rel);
|
||||
if let Some(parent) = abs_path.parent() {
|
||||
self.test
|
||||
.fs()
|
||||
.create_directory(&parent, CreateDirectoryOptions { recursive: true })
|
||||
.await?;
|
||||
}
|
||||
self.test
|
||||
.fs()
|
||||
.write_file(&abs_path, contents.as_ref().to_vec())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_file_text(&self, rel: impl AsRef<Path>) -> Result<String> {
|
||||
Ok(self.test.fs().read_file_text(&self.path_abs(rel)).await?)
|
||||
}
|
||||
|
||||
pub async fn create_dir_all(&self, rel: impl AsRef<Path>) -> Result<()> {
|
||||
self.test
|
||||
.fs()
|
||||
.create_directory(
|
||||
&self.path_abs(rel),
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn path_exists(&self, rel: impl AsRef<Path>) -> Result<bool> {
|
||||
self.abs_path_exists(&self.path_abs(rel)).await
|
||||
}
|
||||
|
||||
pub async fn remove_abs_path(&self, path: &AbsolutePathBuf) -> Result<()> {
|
||||
self.test
|
||||
.fs()
|
||||
.remove(
|
||||
path,
|
||||
RemoveOptions {
|
||||
recursive: false,
|
||||
force: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn abs_path_exists(&self, path: &AbsolutePathBuf) -> Result<bool> {
|
||||
match self.test.fs().get_metadata(path).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn submit(&self, prompt: &str) -> Result<()> {
|
||||
|
||||
@@ -8,7 +8,6 @@ use core_test_support::responses::ev_apply_patch_custom_tool_call;
|
||||
use core_test_support::responses::ev_shell_command_call;
|
||||
use core_test_support::test_codex::ApplyPatchModelOutput;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
@@ -30,6 +29,7 @@ use core_test_support::responses::ev_shell_command_call_with_args;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_remote;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::TestCodexHarness;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
@@ -54,7 +54,7 @@ async fn apply_patch_harness_with(
|
||||
});
|
||||
// Box harness construction so apply_patch_cli tests do not inline the
|
||||
// full test-thread startup path into each test future.
|
||||
Box::pin(TestCodexHarness::with_builder(builder)).await
|
||||
Box::pin(TestCodexHarness::with_remote_aware_builder(builder)).await
|
||||
}
|
||||
|
||||
pub async fn mount_apply_patch(
|
||||
@@ -129,10 +129,7 @@ async fn apply_patch_cli_uses_codex_self_exe_with_linux_sandbox_helper_alias() -
|
||||
r"(?s)^Exit code: 0.*Success\. Updated the following files:\nA helper-alias\.txt\n?$",
|
||||
&out,
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(harness.path("helper-alias.txt"))?,
|
||||
"hello\n"
|
||||
);
|
||||
assert_eq!(harness.read_file_text("helper-alias.txt").await?, "hello\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -150,10 +147,8 @@ async fn apply_patch_cli_multiple_operations_integration(
|
||||
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.1")).await?;
|
||||
|
||||
// Seed workspace state
|
||||
let modify_path = harness.path("modify.txt");
|
||||
let delete_path = harness.path("delete.txt");
|
||||
fs::write(&modify_path, "line1\nline2\n")?;
|
||||
fs::write(&delete_path, "obsolete\n")?;
|
||||
harness.write_file("modify.txt", "line1\nline2\n").await?;
|
||||
harness.write_file("delete.txt", "obsolete\n").await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Delete File: delete.txt\n*** Update File: modify.txt\n@@\n-line2\n+changed\n*** End Patch";
|
||||
|
||||
@@ -174,12 +169,12 @@ D delete.txt
|
||||
?$";
|
||||
assert_regex_match(expected, &out);
|
||||
|
||||
assert_eq!(harness.read_file_text("nested/new.txt").await?, "created\n");
|
||||
assert_eq!(
|
||||
fs::read_to_string(harness.path("nested/new.txt"))?,
|
||||
"created\n"
|
||||
harness.read_file_text("modify.txt").await?,
|
||||
"line1\nchanged\n"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&modify_path)?, "line1\nchanged\n");
|
||||
assert!(!delete_path.exists());
|
||||
assert!(!harness.path_exists("delete.txt").await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -195,8 +190,9 @@ async fn apply_patch_cli_multiple_chunks(model_output: ApplyPatchModelOutput) ->
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let target = harness.path("multi.txt");
|
||||
fs::write(&target, "line1\nline2\nline3\nline4\n")?;
|
||||
harness
|
||||
.write_file("multi.txt", "line1\nline2\nline3\nline4\n")
|
||||
.await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch";
|
||||
let call_id = "apply-multi-chunks";
|
||||
@@ -205,7 +201,7 @@ async fn apply_patch_cli_multiple_chunks(model_output: ApplyPatchModelOutput) ->
|
||||
harness.submit("apply multi-chunk patch").await?;
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(&target)?,
|
||||
harness.read_file_text("multi.txt").await?,
|
||||
"line1\nchanged2\nline3\nchanged4\n"
|
||||
);
|
||||
Ok(())
|
||||
@@ -224,10 +220,7 @@ async fn apply_patch_cli_moves_file_to_new_directory(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let original = harness.path("old/name.txt");
|
||||
let new_path = harness.path("renamed/dir/name.txt");
|
||||
fs::create_dir_all(original.parent().expect("parent"))?;
|
||||
fs::write(&original, "old content\n")?;
|
||||
harness.write_file("old/name.txt", "old content\n").await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch";
|
||||
let call_id = "apply-move";
|
||||
@@ -235,8 +228,11 @@ async fn apply_patch_cli_moves_file_to_new_directory(
|
||||
|
||||
harness.submit("apply move patch").await?;
|
||||
|
||||
assert!(!original.exists());
|
||||
assert_eq!(fs::read_to_string(&new_path)?, "new content\n");
|
||||
assert!(!harness.path_exists("old/name.txt").await?);
|
||||
assert_eq!(
|
||||
harness.read_file_text("renamed/dir/name.txt").await?,
|
||||
"new content\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -253,8 +249,9 @@ async fn apply_patch_cli_updates_file_appends_trailing_newline(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let target = harness.path("no_newline.txt");
|
||||
fs::write(&target, "no newline at end")?;
|
||||
harness
|
||||
.write_file("no_newline.txt", "no newline at end")
|
||||
.await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch";
|
||||
let call_id = "apply-append-nl";
|
||||
@@ -262,7 +259,7 @@ async fn apply_patch_cli_updates_file_appends_trailing_newline(
|
||||
|
||||
harness.submit("apply newline patch").await?;
|
||||
|
||||
let contents = fs::read_to_string(&target)?;
|
||||
let contents = harness.read_file_text("no_newline.txt").await?;
|
||||
assert!(contents.ends_with('\n'));
|
||||
assert_eq!(contents, "first line\nsecond line\n");
|
||||
Ok(())
|
||||
@@ -281,8 +278,9 @@ async fn apply_patch_cli_insert_only_hunk_modifies_file(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let target = harness.path("insert_only.txt");
|
||||
fs::write(&target, "alpha\nomega\n")?;
|
||||
harness
|
||||
.write_file("insert_only.txt", "alpha\nomega\n")
|
||||
.await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: insert_only.txt\n@@\n alpha\n+beta\n omega\n*** End Patch";
|
||||
let call_id = "apply-insert-only";
|
||||
@@ -290,7 +288,10 @@ async fn apply_patch_cli_insert_only_hunk_modifies_file(
|
||||
|
||||
harness.submit("insert lines via apply_patch").await?;
|
||||
|
||||
assert_eq!(fs::read_to_string(&target)?, "alpha\nbeta\nomega\n");
|
||||
assert_eq!(
|
||||
harness.read_file_text("insert_only.txt").await?,
|
||||
"alpha\nbeta\nomega\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -307,12 +308,10 @@ async fn apply_patch_cli_move_overwrites_existing_destination(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let original = harness.path("old/name.txt");
|
||||
let destination = harness.path("renamed/dir/name.txt");
|
||||
fs::create_dir_all(original.parent().expect("parent"))?;
|
||||
fs::create_dir_all(destination.parent().expect("parent"))?;
|
||||
fs::write(&original, "from\n")?;
|
||||
fs::write(&destination, "existing\n")?;
|
||||
harness.write_file("old/name.txt", "from\n").await?;
|
||||
harness
|
||||
.write_file("renamed/dir/name.txt", "existing\n")
|
||||
.await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch";
|
||||
let call_id = "apply-move-overwrite";
|
||||
@@ -320,8 +319,11 @@ async fn apply_patch_cli_move_overwrites_existing_destination(
|
||||
|
||||
harness.submit("apply move overwrite patch").await?;
|
||||
|
||||
assert!(!original.exists());
|
||||
assert_eq!(fs::read_to_string(&destination)?, "new\n");
|
||||
assert!(!harness.path_exists("old/name.txt").await?);
|
||||
assert_eq!(
|
||||
harness.read_file_text("renamed/dir/name.txt").await?,
|
||||
"new\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -335,16 +337,16 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
|
||||
model_output: ApplyPatchModelOutput,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
let codex = test.codex.clone();
|
||||
let cwd = test.cwd.clone();
|
||||
|
||||
let original = harness.path("old/name.txt");
|
||||
let destination = harness.path("renamed/name.txt");
|
||||
fs::create_dir_all(original.parent().expect("parent should exist"))?;
|
||||
fs::write(&original, "same\n")?;
|
||||
harness.write_file("old/name.txt", "same\n").await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/name.txt\n@@\n same\n*** End Patch";
|
||||
let call_id = "apply-move-no-change";
|
||||
@@ -358,7 +360,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
cwd: harness.cwd().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
@@ -383,8 +385,8 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
|
||||
.await;
|
||||
|
||||
assert!(!saw_turn_diff, "pure rename should not emit a turn diff");
|
||||
assert!(!original.exists());
|
||||
assert_eq!(fs::read_to_string(&destination)?, "same\n");
|
||||
assert!(!harness.path_exists("old/name.txt").await?);
|
||||
assert_eq!(harness.read_file_text("renamed/name.txt").await?, "same\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -401,8 +403,7 @@ async fn apply_patch_cli_add_overwrites_existing_file(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let path = harness.path("duplicate.txt");
|
||||
fs::write(&path, "old content\n")?;
|
||||
harness.write_file("duplicate.txt", "old content\n").await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Add File: duplicate.txt\n+new content\n*** End Patch";
|
||||
let call_id = "apply-add-overwrite";
|
||||
@@ -410,7 +411,10 @@ async fn apply_patch_cli_add_overwrites_existing_file(
|
||||
|
||||
harness.submit("apply add overwrite patch").await?;
|
||||
|
||||
assert_eq!(fs::read_to_string(&path)?, "new content\n");
|
||||
assert_eq!(
|
||||
harness.read_file_text("duplicate.txt").await?,
|
||||
"new content\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -459,8 +463,7 @@ async fn apply_patch_cli_reports_missing_context(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let target = harness.path("modify.txt");
|
||||
fs::write(&target, "line1\nline2\n")?;
|
||||
harness.write_file("modify.txt", "line1\nline2\n").await?;
|
||||
|
||||
let patch =
|
||||
"*** Begin Patch\n*** Update File: modify.txt\n@@\n-missing\n+changed\n*** End Patch";
|
||||
@@ -476,7 +479,10 @@ async fn apply_patch_cli_reports_missing_context(
|
||||
"expected verification failure message"
|
||||
);
|
||||
assert!(out.contains("Failed to find expected lines in"));
|
||||
assert_eq!(fs::read_to_string(&target)?, "line1\nline2\n");
|
||||
assert_eq!(
|
||||
harness.read_file_text("modify.txt").await?,
|
||||
"line1\nline2\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -512,7 +518,7 @@ async fn apply_patch_cli_reports_missing_target_file(
|
||||
out.contains("missing.txt"),
|
||||
"expected missing file path in diagnostics: {out}"
|
||||
);
|
||||
assert!(!harness.path("missing.txt").exists());
|
||||
assert!(!harness.path_exists("missing.txt").await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -549,7 +555,7 @@ async fn apply_patch_cli_delete_missing_file_reports_error(
|
||||
out.contains("missing.txt"),
|
||||
"missing delete diagnostics should surface target path: {out}"
|
||||
);
|
||||
assert!(!harness.path("missing.txt").exists());
|
||||
assert!(!harness.path_exists("missing.txt").await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -591,7 +597,7 @@ async fn apply_patch_cli_delete_directory_reports_verification_error(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
fs::create_dir(harness.path("dir"))?;
|
||||
harness.create_dir_all("dir").await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Delete File: dir\n*** End Patch";
|
||||
let call_id = "apply-delete-dir";
|
||||
@@ -620,12 +626,12 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace(
|
||||
|
||||
let escape_path = harness
|
||||
.test()
|
||||
.config
|
||||
.cwd
|
||||
.path()
|
||||
.parent()
|
||||
.expect("cwd should have parent")
|
||||
.join("escape.txt");
|
||||
let _ = fs::remove_file(&escape_path);
|
||||
harness.remove_abs_path(&escape_path).await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Add File: ../escape.txt\n+outside\n*** End Patch";
|
||||
let call_id = "apply-path-traversal";
|
||||
@@ -653,7 +659,7 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace(
|
||||
"expected rejection message for path traversal: {out}"
|
||||
);
|
||||
assert!(
|
||||
!escape_path.exists(),
|
||||
!harness.abs_path_exists(&escape_path).await?,
|
||||
"path traversal should be rejected; tool output: {out}"
|
||||
);
|
||||
Ok(())
|
||||
@@ -674,15 +680,14 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace(
|
||||
|
||||
let escape_path = harness
|
||||
.test()
|
||||
.config
|
||||
.cwd
|
||||
.path()
|
||||
.parent()
|
||||
.expect("cwd should have parent")
|
||||
.join("escape-move.txt");
|
||||
let _ = fs::remove_file(&escape_path);
|
||||
harness.remove_abs_path(&escape_path).await?;
|
||||
|
||||
let source = harness.path("stay.txt");
|
||||
fs::write(&source, "from\n")?;
|
||||
harness.write_file("stay.txt", "from\n").await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: stay.txt\n*** Move to: ../escape-move.txt\n@@\n-from\n+to\n*** End Patch";
|
||||
let call_id = "apply-move-traversal";
|
||||
@@ -707,10 +712,10 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace(
|
||||
"expected rejection message for path traversal: {out}"
|
||||
);
|
||||
assert!(
|
||||
!escape_path.exists(),
|
||||
!harness.abs_path_exists(&escape_path).await?,
|
||||
"move path traversal should be rejected; tool output: {out}"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&source)?, "from\n");
|
||||
assert_eq!(harness.read_file_text("stay.txt").await?, "from\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -743,9 +748,8 @@ async fn apply_patch_cli_verification_failure_has_no_side_effects(
|
||||
|
||||
harness.submit("attempt partial apply patch").await?;
|
||||
|
||||
let created = harness.path("created.txt");
|
||||
assert!(
|
||||
!created.exists(),
|
||||
!harness.path_exists("created.txt").await?,
|
||||
"verification failure should prevent any filesystem changes"
|
||||
);
|
||||
Ok(())
|
||||
@@ -758,10 +762,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_updates_relative_workdir() ->
|
||||
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.1")).await?;
|
||||
|
||||
// Prepare a file inside a subdir; update it via cd && apply_patch heredoc form.
|
||||
let sub = harness.path("sub");
|
||||
fs::create_dir_all(&sub)?;
|
||||
let target = sub.join("in_sub.txt");
|
||||
fs::write(&target, "before\n")?;
|
||||
harness.write_file("sub/in_sub.txt", "before\n").await?;
|
||||
|
||||
let script = "cd sub && apply_patch <<'EOF'\n*** Begin Patch\n*** Update File: in_sub.txt\n@@\n-before\n+after\n*** End Patch\nEOF\n";
|
||||
let call_id = "shell-heredoc-cd";
|
||||
@@ -785,21 +786,24 @@ async fn apply_patch_shell_command_heredoc_with_cd_updates_relative_workdir() ->
|
||||
out.contains("Success."),
|
||||
"expected successful apply_patch invocation via shell_command: {out}"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&target)?, "after\n");
|
||||
assert_eq!(harness.read_file_text("sub/in_sub.txt").await?, "after\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn apply_patch_cli_can_use_shell_command_output_as_patch_input() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"shell_command output producer runs in the test runner, not in the remote apply_patch workspace",
|
||||
);
|
||||
|
||||
let harness =
|
||||
apply_patch_harness_with(|builder| builder.with_model("gpt-5.1").with_windows_cmd_shell())
|
||||
.await?;
|
||||
|
||||
let source_contents = "line1\nnaïve café\nline3\n";
|
||||
let source_path = harness.path("source.txt");
|
||||
fs::write(&source_path, source_contents)?;
|
||||
harness.write_file("source.txt", source_contents).await?;
|
||||
|
||||
let read_call_id = "read-source";
|
||||
let apply_call_id = "apply-from-read";
|
||||
@@ -923,7 +927,7 @@ async fn apply_patch_cli_can_use_shell_command_output_as_patch_input() -> Result
|
||||
.submit("read source.txt, then apply it to target.txt")
|
||||
.await?;
|
||||
|
||||
let target_contents = fs::read_to_string(harness.path("target.txt"))?;
|
||||
let target_contents = harness.read_file_text("target.txt").await?;
|
||||
assert_eq!(target_contents, source_contents);
|
||||
|
||||
Ok(())
|
||||
@@ -932,17 +936,17 @@ async fn apply_patch_cli_can_use_shell_command_output_as_patch_input() -> Result
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.1")).await?;
|
||||
let test = harness.test();
|
||||
let codex = test.codex.clone();
|
||||
let cwd = test.cwd.clone();
|
||||
|
||||
// Prepare a file inside a subdir; update it via cd && apply_patch heredoc form.
|
||||
let sub = test.workspace_path("sub");
|
||||
fs::create_dir_all(&sub)?;
|
||||
let target = sub.join("in_sub.txt");
|
||||
fs::write(&target, "before\n")?;
|
||||
harness.write_file("sub/in_sub.txt", "before\n").await?;
|
||||
|
||||
let script = "cd sub && apply_patch <<'EOF'\n*** Begin Patch\n*** Update File: in_sub.txt\n@@\n-before\n+after\n*** End Patch\nEOF\n";
|
||||
let call_id = "shell-heredoc-cd";
|
||||
@@ -968,7 +972,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
cwd: harness.cwd().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
@@ -1017,14 +1021,16 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.1")).await?;
|
||||
let test = harness.test();
|
||||
let codex = test.codex.clone();
|
||||
let cwd = test.cwd.clone();
|
||||
|
||||
let target = cwd.path().join("invalid.txt");
|
||||
fs::write(&target, "ok\n")?;
|
||||
harness.write_file("invalid.txt", "ok\n").await?;
|
||||
|
||||
let script = "apply_patch <<'EOF'\n*** Begin Patch\n*** Update File: invalid.txt\n@@\n-nope\n+changed\n*** End Patch\nEOF\n";
|
||||
let call_id = "shell-apply-failure";
|
||||
@@ -1050,7 +1056,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() ->
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
cwd: harness.cwd().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
@@ -1088,7 +1094,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() ->
|
||||
out.contains("invalid.txt"),
|
||||
"expected file path in output: {out}"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&target)?, "ok\n");
|
||||
assert_eq!(harness.read_file_text("invalid.txt").await?, "ok\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1110,8 +1116,7 @@ async fn apply_patch_function_accepts_lenient_heredoc_wrapped_patch(
|
||||
|
||||
harness.submit("apply lenient heredoc patch").await?;
|
||||
|
||||
let new_file = harness.path(file_name);
|
||||
assert_eq!(fs::read_to_string(new_file)?, "lenient\n");
|
||||
assert_eq!(harness.read_file_text(file_name).await?, "lenient\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1126,15 +1131,14 @@ async fn apply_patch_cli_end_of_file_anchor(model_output: ApplyPatchModelOutput)
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let target = harness.path("tail.txt");
|
||||
fs::write(&target, "alpha\nlast\n")?;
|
||||
harness.write_file("tail.txt", "alpha\nlast\n").await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch";
|
||||
let call_id = "apply-eof";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
harness.submit("apply EOF-anchored patch").await?;
|
||||
assert_eq!(fs::read_to_string(&target)?, "alpha\nend\n");
|
||||
assert_eq!(harness.read_file_text("tail.txt").await?, "alpha\nend\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1151,8 +1155,7 @@ async fn apply_patch_cli_missing_second_chunk_context_rejected(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let target = harness.path("two_chunks.txt");
|
||||
fs::write(&target, "a\nb\nc\nd\n")?;
|
||||
harness.write_file("two_chunks.txt", "a\nb\nc\nd\n").await?;
|
||||
|
||||
// First chunk has @@, second chunk intentionally omits @@ to trigger parse error.
|
||||
let patch =
|
||||
@@ -1169,7 +1172,10 @@ async fn apply_patch_cli_missing_second_chunk_context_rejected(
|
||||
"expected hunk context diagnostics: {out}"
|
||||
);
|
||||
// Original file unchanged on failure
|
||||
assert_eq!(fs::read_to_string(&target)?, "a\nb\nc\nd\n");
|
||||
assert_eq!(
|
||||
harness.read_file_text("two_chunks.txt").await?,
|
||||
"a\nb\nc\nd\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1183,11 +1189,14 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
|
||||
model_output: ApplyPatchModelOutput,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
let codex = test.codex.clone();
|
||||
let cwd = test.cwd.clone();
|
||||
|
||||
let call_id = "apply-diff-event";
|
||||
let file = "udiff.txt";
|
||||
@@ -1202,7 +1211,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
cwd: harness.cwd().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
@@ -1244,15 +1253,17 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
|
||||
model_output: ApplyPatchModelOutput,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
let codex = test.codex.clone();
|
||||
let cwd = test.cwd.clone();
|
||||
|
||||
// Seed original file
|
||||
let old = cwd.path().join("old.txt");
|
||||
fs::write(&old, "old\n")?;
|
||||
harness.write_file("old.txt", "old\n").await?;
|
||||
|
||||
// Patch: update + move
|
||||
let call_id = "apply-rename-change";
|
||||
@@ -1267,7 +1278,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
cwd: harness.cwd().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
@@ -1305,11 +1316,14 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
let codex = test.codex.clone();
|
||||
let cwd = test.cwd.clone();
|
||||
|
||||
let call1 = "agg-1";
|
||||
let call2 = "agg-2";
|
||||
@@ -1340,7 +1354,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
cwd: harness.cwd().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
@@ -1375,11 +1389,14 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
let codex = test.codex.clone();
|
||||
let cwd = test.cwd.clone();
|
||||
|
||||
let call_success = "agg-success";
|
||||
let call_failure = "agg-failure";
|
||||
@@ -1413,7 +1430,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
cwd: harness.cwd().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
@@ -1457,10 +1474,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
|
||||
"expected missing context diagnostics: {failure_out}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(cwd.path().join("partial/success.txt"))?,
|
||||
"ok\n"
|
||||
);
|
||||
assert_eq!(harness.read_file_text("partial/success.txt").await?, "ok\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1477,8 +1491,9 @@ async fn apply_patch_change_context_disambiguates_target(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let target = harness.path("multi_ctx.txt");
|
||||
fs::write(&target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n")?;
|
||||
harness
|
||||
.write_file("multi_ctx.txt", "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n")
|
||||
.await?;
|
||||
|
||||
let patch =
|
||||
"*** Begin Patch\n*** Update File: multi_ctx.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch";
|
||||
@@ -1487,7 +1502,7 @@ async fn apply_patch_change_context_disambiguates_target(
|
||||
|
||||
harness.submit("apply with change_context").await?;
|
||||
|
||||
let contents = fs::read_to_string(&target)?;
|
||||
let contents = harness.read_file_text("multi_ctx.txt").await?;
|
||||
assert_eq!(contents, "fn a\nx=10\ny=2\nfn b\nx=11\ny=20\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -932,7 +932,7 @@ async fn includes_base_instructions_override_in_request() {
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(|config| {
|
||||
config.base_instructions = Some(Some("test instructions".to_string()));
|
||||
config.base_instructions = Some("test instructions".to_string());
|
||||
});
|
||||
let codex = builder
|
||||
.build(&server)
|
||||
@@ -964,47 +964,6 @@ async fn includes_base_instructions_override_in_request() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn omits_explicit_null_base_instructions_from_request() {
|
||||
skip_if_no_network!();
|
||||
// Mock server
|
||||
let server = MockServer::start().await;
|
||||
let resp_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(|config| {
|
||||
config.base_instructions = Some(None);
|
||||
});
|
||||
let codex = builder
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("create new conversation")
|
||||
.codex;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
let request_body = request.body_json();
|
||||
|
||||
assert_eq!(request_body.get("instructions"), None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn chatgpt_auth_sends_correct_request() {
|
||||
skip_if_no_network!();
|
||||
|
||||
@@ -1659,9 +1659,9 @@ fn prompt_with_input(input: Vec<ResponseItem>) -> Prompt {
|
||||
|
||||
fn prompt_with_input_and_instructions(input: Vec<ResponseItem>, instructions: &str) -> Prompt {
|
||||
let mut prompt = prompt_with_input(input);
|
||||
prompt.base_instructions = Some(BaseInstructions {
|
||||
prompt.base_instructions = BaseInstructions {
|
||||
text: instructions.to_string(),
|
||||
});
|
||||
};
|
||||
prompt
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ async fn start_realtime_conversation(codex: &codex_core::CodexThread) -> Result<
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -869,7 +870,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
let override_base_instructions = override_base_instructions.clone();
|
||||
move |config| {
|
||||
config.model_context_window = Some(override_context_window);
|
||||
config.base_instructions = Some(Some(override_base_instructions));
|
||||
config.base_instructions = Some(override_base_instructions);
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -82,6 +82,7 @@ fn test_model_info(
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
@@ -898,6 +899,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -335,6 +335,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -66,7 +66,7 @@ async fn base_instructions_override_disables_personality_template() {
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Friendly);
|
||||
config.base_instructions = Some(Some("override instructions".to_string()));
|
||||
config.base_instructions = Some("override instructions".to_string());
|
||||
|
||||
let model_info =
|
||||
codex_core::test_support::construct_model_info_offline("gpt-5.2-codex", &config);
|
||||
@@ -644,6 +644,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
@@ -760,6 +761,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
|
||||
@@ -71,7 +71,6 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
@@ -117,7 +116,6 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::ConversationAudioParams;
|
||||
use codex_protocol::protocol::ConversationStartParams;
|
||||
use codex_protocol::protocol::ConversationStartTransport;
|
||||
use codex_protocol::protocol::ConversationTextParams;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -32,15 +33,56 @@ use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::Match;
|
||||
use wiremock::Mock;
|
||||
use wiremock::Request as WiremockRequest;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
const STARTUP_CONTEXT_HEADER: &str = "Startup context from Codex.";
|
||||
const MEMORY_PROMPT_PHRASE: &str =
|
||||
"You have access to a memory folder with guidance from prior runs.";
|
||||
const REALTIME_CONVERSATION_TEST_SUBPROCESS_ENV_VAR: &str =
|
||||
"CODEX_REALTIME_CONVERSATION_TEST_SUBPROCESS";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RealtimeCallRequestCapture {
|
||||
requests: Arc<Mutex<Vec<WiremockRequest>>>,
|
||||
}
|
||||
|
||||
impl RealtimeCallRequestCapture {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
requests: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn single_request(&self) -> WiremockRequest {
|
||||
let requests = self
|
||||
.requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(requests.len(), 1, "expected one realtime call request");
|
||||
requests[0].clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Match for RealtimeCallRequestCapture {
|
||||
fn matches(&self, request: &WiremockRequest) -> bool {
|
||||
self.requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(request.clone());
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn websocket_request_text(
|
||||
request: &core_test_support::responses::WebSocketRequest,
|
||||
) -> Option<String> {
|
||||
@@ -182,6 +224,7 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> {
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -292,6 +335,89 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn conversation_webrtc_start_posts_generated_session() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let capture = RealtimeCallRequestCapture::new();
|
||||
Mock::given(method("POST"))
|
||||
.and(path_regex(".*/realtime/calls$"))
|
||||
.and(capture.clone())
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("v=answer\r\n"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.experimental_realtime_ws_backend_prompt = Some("backend prompt".to_string());
|
||||
config.experimental_realtime_ws_model = Some("realtime-test-model".to_string());
|
||||
config.experimental_realtime_ws_startup_context = Some("startup context".to_string());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: Some(ConversationStartTransport::Webrtc {
|
||||
sdp: "v=offer\r\n".to_string(),
|
||||
}),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let created = wait_for_event_match(&test.codex, |msg| match msg {
|
||||
EventMsg::RealtimeConversationSdp(created) => Some(Ok(created.clone())),
|
||||
EventMsg::Error(err) => Some(Err(err.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|err: ErrorEvent| panic!("conversation call create failed: {err:?}"));
|
||||
assert_eq!(created.sdp, "v=answer\r\n");
|
||||
let closed = wait_for_event_match(&test.codex, |msg| match msg {
|
||||
EventMsg::RealtimeConversationClosed(closed) => Some(closed.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(closed.reason.as_deref(), Some("transport_closed"));
|
||||
|
||||
let request = capture.single_request();
|
||||
assert_eq!(request.url.path(), "/v1/realtime/calls");
|
||||
assert_eq!(request.url.query(), None);
|
||||
assert_eq!(
|
||||
request
|
||||
.headers
|
||||
.get("authorization")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("Bearer dummy")
|
||||
);
|
||||
assert_eq!(
|
||||
request
|
||||
.headers
|
||||
.get("content-type")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("multipart/form-data; boundary=codex-realtime-call-boundary")
|
||||
);
|
||||
let body = String::from_utf8(request.body).context("multipart body should be utf-8")?;
|
||||
let session = r#"{"audio":{"input":{"format":{"type":"audio/pcm","rate":24000}},"output":{"voice":"fathom"}},"type":"quicksilver","model":"realtime-test-model","instructions":"backend prompt\n\nstartup context"}"#;
|
||||
assert_eq!(
|
||||
body,
|
||||
format!(
|
||||
"--codex-realtime-call-boundary\r\n\
|
||||
Content-Disposition: form-data; name=\"sdp\"\r\n\
|
||||
Content-Type: application/sdp\r\n\
|
||||
\r\n\
|
||||
v=offer\r\n\
|
||||
\r\n\
|
||||
--codex-realtime-call-boundary\r\n\
|
||||
Content-Disposition: form-data; name=\"session\"\r\n\
|
||||
Content-Type: application/json\r\n\
|
||||
\r\n\
|
||||
{session}\r\n\
|
||||
--codex-realtime-call-boundary--\r\n"
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn conversation_start_uses_openai_env_key_fallback_with_chatgpt_auth() -> Result<()> {
|
||||
if std::env::var_os(REALTIME_CONVERSATION_TEST_SUBPROCESS_ENV_VAR).is_none() {
|
||||
@@ -324,6 +450,7 @@ async fn conversation_start_uses_openai_env_key_fallback_with_chatgpt_auth() ->
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -383,6 +510,7 @@ async fn conversation_transport_close_emits_closed_event() -> Result<()> {
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -466,6 +594,7 @@ async fn conversation_start_preflight_failure_emits_realtime_error_only() -> Res
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -506,6 +635,7 @@ async fn conversation_start_connect_failure_emits_realtime_error_only() -> Resul
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -594,6 +724,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> {
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "old".to_string(),
|
||||
session_id: Some("conv_old".to_string()),
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |msg| match msg {
|
||||
@@ -610,6 +741,7 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> {
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "new".to_string(),
|
||||
session_id: Some("conv_new".to_string()),
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
wait_for_event_match(&test.codex, |msg| match msg {
|
||||
@@ -696,6 +828,7 @@ async fn conversation_uses_experimental_realtime_ws_base_url_override() -> Resul
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -750,6 +883,7 @@ async fn conversation_uses_experimental_realtime_ws_backend_prompt_override() ->
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "prompt from op".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -812,6 +946,7 @@ async fn conversation_uses_experimental_realtime_ws_startup_context_override() -
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "prompt from op".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -872,6 +1007,7 @@ async fn conversation_disables_realtime_startup_context_with_empty_override() ->
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "prompt from op".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -925,6 +1061,7 @@ async fn conversation_start_injects_startup_context_from_thread_history() -> Res
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -978,6 +1115,7 @@ async fn conversation_startup_context_falls_back_to_workspace_map() -> Result<()
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1029,6 +1167,7 @@ async fn conversation_startup_context_is_truncated_and_sent_once_per_start() ->
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1113,6 +1252,7 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1239,6 +1379,7 @@ async fn conversation_handoff_persists_across_item_done_until_turn_complete() ->
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1380,6 +1521,7 @@ async fn inbound_handoff_request_starts_turn() -> Result<()> {
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1474,6 +1616,7 @@ async fn inbound_handoff_request_uses_active_transcript() -> Result<()> {
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1566,6 +1709,7 @@ async fn inbound_handoff_request_clears_active_transcript_after_each_handoff() -
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1665,6 +1809,7 @@ async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio(
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1777,6 +1922,7 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -1919,6 +2065,7 @@ async fn inbound_handoff_request_does_not_block_realtime_event_forwarding() -> R
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -2045,6 +2192,7 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> {
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
let _ = wait_for_event_match(&test.codex, |msg| match msg {
|
||||
@@ -2186,6 +2334,7 @@ async fn inbound_handoff_request_starts_turn_and_does_not_block_realtime_audio()
|
||||
.submit(Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "backend prompt".to_string(),
|
||||
session_id: None,
|
||||
transport: None,
|
||||
}))
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -297,6 +297,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
@@ -545,6 +546,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: remote_base.to_string(),
|
||||
model_messages: None,
|
||||
@@ -1027,6 +1029,7 @@ fn test_remote_model_with_policy(
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -405,6 +405,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -173,8 +173,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()>
|
||||
thread_id,
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
|
||||
@@ -555,8 +555,7 @@ A {file_name}
|
||||
);
|
||||
assert_regex_match(&expected_pattern, output.as_str());
|
||||
|
||||
let new_file_path = harness.path(file_name);
|
||||
let created_contents = fs::read_to_string(&new_file_path)?;
|
||||
let created_contents = harness.read_file_text(file_name).await?;
|
||||
assert_eq!(
|
||||
created_contents, "custom tool content\n",
|
||||
"expected file contents for {file_name}"
|
||||
@@ -579,8 +578,7 @@ async fn apply_patch_custom_tool_call_updates_existing_file(
|
||||
|
||||
let call_id = "apply-patch-update-file";
|
||||
let file_name = "custom_tool_apply_patch_existing.txt";
|
||||
let file_path = harness.path(file_name);
|
||||
fs::write(&file_path, "before\n")?;
|
||||
harness.write_file(file_name, "before\n").await?;
|
||||
let patch = format!(
|
||||
"*** Begin Patch\n*** Update File: {file_name}\n@@\n-before\n+after\n*** End Patch\n"
|
||||
);
|
||||
@@ -613,7 +611,7 @@ M {file_name}
|
||||
);
|
||||
assert_regex_match(&expected_pattern, output.as_str());
|
||||
|
||||
let updated_contents = fs::read_to_string(file_path)?;
|
||||
let updated_contents = harness.read_file_text(file_name).await?;
|
||||
assert_eq!(updated_contents, "after\n", "expected updated file content");
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -67,6 +67,7 @@ fn test_model_info(
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -146,7 +146,6 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: Some(dynamic_tools_for_hook),
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -84,7 +84,7 @@ async fn continue_after_stream_error() {
|
||||
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_config(move |config| {
|
||||
config.base_instructions = Some(Some("You are a helpful assistant".to_string()));
|
||||
config.base_instructions = Some("You are a helpful assistant".to_string());
|
||||
config.model_provider = provider;
|
||||
})
|
||||
.build(&server)
|
||||
|
||||
@@ -1349,6 +1349,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -181,8 +181,8 @@ impl CodexToolCallParam {
|
||||
codex_self_exe: arg0_paths.codex_self_exe.clone(),
|
||||
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
|
||||
base_instructions: base_instructions.map(Some),
|
||||
developer_instructions: developer_instructions.map(Some),
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
compact_prompt,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -387,6 +387,7 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::CollabResumeBegin(_)
|
||||
| EventMsg::CollabResumeEnd(_)
|
||||
| EventMsg::RealtimeConversationStarted(_)
|
||||
| EventMsg::RealtimeConversationSdp(_)
|
||||
| EventMsg::RealtimeConversationRealtime(_)
|
||||
| EventMsg::RealtimeConversationClosed(_)
|
||||
| EventMsg::DeprecationNotice(_) => {
|
||||
|
||||
@@ -116,6 +116,9 @@
|
||||
"visibility": "list",
|
||||
"minimal_client_version": "0.98.0",
|
||||
"supported_in_api": true,
|
||||
"additional_speed_tiers": [
|
||||
"fast"
|
||||
],
|
||||
"availability_nux": null,
|
||||
"upgrade": null,
|
||||
"priority": 0,
|
||||
|
||||
@@ -69,6 +69,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
|
||||
visibility: ModelVisibility::None,
|
||||
supported_in_api: true,
|
||||
priority: 99,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
|
||||
@@ -23,5 +23,4 @@ pub mod plan_tool;
|
||||
pub mod protocol;
|
||||
pub mod request_permissions;
|
||||
pub mod request_user_input;
|
||||
mod serde_helpers;
|
||||
pub mod user_input;
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::config_types::ReasoningSummary;
|
||||
use crate::config_types::Verbosity;
|
||||
|
||||
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
|
||||
pub const SPEED_TIER_FAST: &str = "fast";
|
||||
|
||||
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
#[derive(
|
||||
@@ -132,6 +133,9 @@ pub struct ModelPreset {
|
||||
/// Whether this model supports personality-specific instructions.
|
||||
#[serde(default)]
|
||||
pub supports_personality: bool,
|
||||
/// Additional speed tiers this model can run with beyond the standard path.
|
||||
#[serde(default)]
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
/// Whether this is the default model for new users.
|
||||
pub is_default: bool,
|
||||
/// recommended upgrade model
|
||||
@@ -252,6 +256,8 @@ pub struct ModelInfo {
|
||||
pub visibility: ModelVisibility,
|
||||
pub supported_in_api: bool,
|
||||
pub priority: i32,
|
||||
#[serde(default)]
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
pub availability_nux: Option<ModelAvailabilityNux>,
|
||||
pub upgrade: Option<ModelInfoUpgrade>,
|
||||
pub base_instructions: String,
|
||||
@@ -428,6 +434,7 @@ impl From<ModelInfo> for ModelPreset {
|
||||
.unwrap_or(ReasoningEffort::None),
|
||||
supported_reasoning_efforts: info.supported_reasoning_levels.clone(),
|
||||
supports_personality,
|
||||
additional_speed_tiers: info.additional_speed_tiers,
|
||||
is_default: false, // default is the highest priority available model
|
||||
upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade {
|
||||
id: upgrade.model.clone(),
|
||||
@@ -449,6 +456,12 @@ impl From<ModelInfo> for ModelPreset {
|
||||
}
|
||||
|
||||
impl ModelPreset {
|
||||
pub fn supports_fast_mode(&self) -> bool {
|
||||
self.additional_speed_tiers
|
||||
.iter()
|
||||
.any(|tier| tier == SPEED_TIER_FAST)
|
||||
}
|
||||
|
||||
/// Filter models based on authentication mode.
|
||||
///
|
||||
/// In ChatGPT mode, all models are visible. Otherwise, only API-supported models are shown.
|
||||
@@ -527,6 +540,7 @@ mod tests {
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base".to_string(),
|
||||
@@ -772,6 +786,7 @@ mod tests {
|
||||
availability_nux: Some(ModelAvailabilityNux {
|
||||
message: "Try Spark.".to_string(),
|
||||
}),
|
||||
additional_speed_tiers: vec![SPEED_TIER_FAST.to_string()],
|
||||
..test_model(/*spec*/ None)
|
||||
});
|
||||
|
||||
@@ -781,5 +796,6 @@ mod tests {
|
||||
message: "Try Spark.".to_string(),
|
||||
})
|
||||
);
|
||||
assert!(preset.supports_fast_mode());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ impl FileSystemSandboxPolicy {
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(cwd_root) = AbsolutePathBuf::relative_to_current_dir(cwd) {
|
||||
if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) {
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
&cwd_root, /*protect_missing_dot_codex*/ true,
|
||||
) {
|
||||
@@ -476,7 +476,7 @@ impl FileSystemSandboxPolicy {
|
||||
.iter()
|
||||
.filter(|path| normalize_effective_absolute_path((*path).clone()) == root)
|
||||
.collect();
|
||||
let protect_missing_dot_codex = AbsolutePathBuf::relative_to_current_dir(cwd)
|
||||
let protect_missing_dot_codex = AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.ok()
|
||||
.is_some_and(|cwd| normalize_effective_absolute_path(cwd) == root);
|
||||
let mut read_only_subpaths: Vec<AbsolutePathBuf> =
|
||||
@@ -562,7 +562,7 @@ impl FileSystemSandboxPolicy {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let root = AbsolutePathBuf::relative_to_current_dir(cwd)
|
||||
let root = AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.ok()
|
||||
.map(|cwd| absolute_root_path_for_cwd(&cwd));
|
||||
|
||||
@@ -604,7 +604,7 @@ impl FileSystemSandboxPolicy {
|
||||
}
|
||||
}
|
||||
FileSystemSandboxKind::Restricted => {
|
||||
let cwd_absolute = AbsolutePathBuf::relative_to_current_dir(cwd).ok();
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let mut include_platform_defaults = false;
|
||||
let mut has_full_disk_read_access = false;
|
||||
let mut has_full_disk_write_access = false;
|
||||
@@ -744,7 +744,7 @@ impl FileSystemSandboxPolicy {
|
||||
}
|
||||
|
||||
fn resolved_entries_with_cwd(&self, cwd: &Path) -> Vec<ResolvedFileSystemEntry> {
|
||||
let cwd_absolute = AbsolutePathBuf::relative_to_current_dir(cwd).ok();
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
|
||||
@@ -48,8 +48,6 @@ use crate::plan_tool::UpdatePlanArgs;
|
||||
use crate::request_permissions::RequestPermissionsEvent;
|
||||
use crate::request_permissions::RequestPermissionsResponse;
|
||||
use crate::request_user_input::RequestUserInputResponse;
|
||||
use crate::serde_helpers::deserialize_double_option;
|
||||
use crate::serde_helpers::serialize_double_option;
|
||||
use crate::user_input::UserInput;
|
||||
use codex_git_utils::GitSha;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -137,6 +135,16 @@ pub struct ConversationStartParams {
|
||||
pub prompt: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transport: Option<ConversationStartTransport>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum ConversationStartTransport {
|
||||
Websocket,
|
||||
Webrtc { sdp: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -772,7 +780,7 @@ impl ReadOnlyAccess {
|
||||
ReadOnlyAccess::FullAccess => return Vec::new(),
|
||||
ReadOnlyAccess::Restricted { readable_roots, .. } => {
|
||||
let mut roots = readable_roots.clone();
|
||||
match AbsolutePathBuf::relative_to_current_dir(cwd) {
|
||||
match AbsolutePathBuf::from_absolute_path(cwd) {
|
||||
Ok(cwd_root) => roots.push(cwd_root),
|
||||
Err(err) => {
|
||||
error!("Ignoring invalid cwd {cwd:?} for sandbox readable root: {err}");
|
||||
@@ -1023,7 +1031,7 @@ impl SandboxPolicy {
|
||||
// Always include defaults: cwd, /tmp (if present on Unix), and
|
||||
// on macOS, the per-user TMPDIR unless explicitly excluded.
|
||||
// TODO(mbolin): cwd param should be AbsolutePathBuf.
|
||||
let cwd_absolute = AbsolutePathBuf::relative_to_current_dir(cwd);
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd);
|
||||
match cwd_absolute {
|
||||
Ok(cwd) => {
|
||||
roots.push(cwd);
|
||||
@@ -1071,7 +1079,7 @@ impl SandboxPolicy {
|
||||
}
|
||||
|
||||
// For each root, compute subpaths that should remain read-only.
|
||||
let cwd_root = AbsolutePathBuf::relative_to_current_dir(cwd).ok();
|
||||
let cwd_root = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
roots
|
||||
.into_iter()
|
||||
.map(|writable_root| {
|
||||
@@ -1225,6 +1233,9 @@ pub enum EventMsg {
|
||||
/// Realtime conversation lifecycle close event.
|
||||
RealtimeConversationClosed(RealtimeConversationClosedEvent),
|
||||
|
||||
/// Realtime session description protocol payload.
|
||||
RealtimeConversationSdp(RealtimeConversationSdpEvent),
|
||||
|
||||
/// Model routing changed from the requested model to a different model.
|
||||
ModelReroute(ModelRerouteEvent),
|
||||
|
||||
@@ -1527,6 +1538,11 @@ pub struct RealtimeConversationClosedEvent {
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
pub struct RealtimeConversationSdpEvent {
|
||||
pub sdp: String,
|
||||
}
|
||||
|
||||
impl From<CollabAgentSpawnBeginEvent> for EventMsg {
|
||||
fn from(event: CollabAgentSpawnBeginEvent) -> Self {
|
||||
EventMsg::CollabAgentSpawnBegin(event)
|
||||
@@ -2320,7 +2336,7 @@ impl InitialHistory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_base_instructions(&self) -> Option<Option<BaseInstructions>> {
|
||||
pub fn get_base_instructions(&self) -> Option<BaseInstructions> {
|
||||
// TODO: SessionMeta should (in theory) always be first in the history, so we can probably only check the first item?
|
||||
match self {
|
||||
InitialHistory::New => None,
|
||||
@@ -2337,26 +2353,6 @@ impl InitialHistory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_developer_instructions(&self) -> Option<Option<String>> {
|
||||
match self {
|
||||
InitialHistory::New => None,
|
||||
InitialHistory::Resumed(resumed) => {
|
||||
resumed.history.iter().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => {
|
||||
meta_line.meta.developer_instructions.clone()
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => {
|
||||
meta_line.meta.developer_instructions.clone()
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {
|
||||
match self {
|
||||
InitialHistory::New => None,
|
||||
@@ -2548,20 +2544,7 @@ pub struct SessionMeta {
|
||||
/// base_instructions for the session. This *should* always be present when creating a new session,
|
||||
/// but may be missing for older sessions. If not present, fall back to rendering the base_instructions
|
||||
/// from ModelsManager.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_double_option",
|
||||
serialize_with = "serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub base_instructions: Option<Option<BaseInstructions>>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_double_option",
|
||||
serialize_with = "serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub developer_instructions: Option<Option<String>>,
|
||||
pub base_instructions: Option<BaseInstructions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -2583,7 +2566,6 @@ impl Default for SessionMeta {
|
||||
agent_path: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
}
|
||||
@@ -4429,6 +4411,14 @@ mod tests {
|
||||
let start = Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "be helpful".to_string(),
|
||||
session_id: Some("conv_1".to_string()),
|
||||
transport: None,
|
||||
});
|
||||
let webrtc_start = Op::RealtimeConversationStart(ConversationStartParams {
|
||||
prompt: "be helpful".to_string(),
|
||||
session_id: Some("conv_1".to_string()),
|
||||
transport: Some(ConversationStartTransport::Webrtc {
|
||||
sdp: "v=offer\r\n".to_string(),
|
||||
}),
|
||||
});
|
||||
let text = Op::RealtimeConversationText(ConversationTextParams {
|
||||
text: "hello".to_string(),
|
||||
@@ -4469,6 +4459,18 @@ mod tests {
|
||||
serde_json::from_value::<Op>(serde_json::to_value(&close).unwrap()).unwrap(),
|
||||
close
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&webrtc_start).unwrap(),
|
||||
json!({
|
||||
"type": "realtime_conversation_start",
|
||||
"prompt": "be helpful",
|
||||
"session_id": "conv_1",
|
||||
"transport": {
|
||||
"type": "webrtc",
|
||||
"sdp": "v=offer\r\n"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
|
||||
pub fn deserialize_double_option<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
serde_with::rust::double_option::deserialize(deserializer)
|
||||
}
|
||||
|
||||
pub fn serialize_double_option<T, S>(
|
||||
value: &Option<Option<T>>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: Serialize,
|
||||
S: Serializer,
|
||||
{
|
||||
serde_with::rust::double_option::serialize(value, serializer)
|
||||
}
|
||||
@@ -56,7 +56,6 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
@@ -108,7 +107,6 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() {
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
@@ -371,7 +369,6 @@ fn write_rollout_in_sessions_with_cwd(
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
|
||||
@@ -132,6 +132,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
|
||||
| EventMsg::DynamicToolCallResponse(_) => Some(EventPersistenceMode::Extended),
|
||||
EventMsg::Warning(_)
|
||||
| EventMsg::RealtimeConversationStarted(_)
|
||||
| EventMsg::RealtimeConversationSdp(_)
|
||||
| EventMsg::RealtimeConversationRealtime(_)
|
||||
| EventMsg::RealtimeConversationClosed(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
|
||||
@@ -81,8 +81,7 @@ pub enum RolloutRecorderParams {
|
||||
conversation_id: ThreadId,
|
||||
forked_from_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
base_instructions: Option<BaseInstructions>,
|
||||
developer_instructions: Option<Option<String>>,
|
||||
base_instructions: BaseInstructions,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
event_persistence_mode: EventPersistenceMode,
|
||||
},
|
||||
@@ -111,8 +110,7 @@ impl RolloutRecorderParams {
|
||||
conversation_id: ThreadId,
|
||||
forked_from_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
base_instructions: Option<BaseInstructions>,
|
||||
developer_instructions: Option<Option<String>>,
|
||||
base_instructions: BaseInstructions,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
event_persistence_mode: EventPersistenceMode,
|
||||
) -> Self {
|
||||
@@ -121,7 +119,6 @@ impl RolloutRecorderParams {
|
||||
forked_from_id,
|
||||
source,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
dynamic_tools,
|
||||
event_persistence_mode,
|
||||
}
|
||||
@@ -383,7 +380,6 @@ impl RolloutRecorder {
|
||||
forked_from_id,
|
||||
source,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
dynamic_tools,
|
||||
event_persistence_mode,
|
||||
} => {
|
||||
@@ -413,7 +409,6 @@ impl RolloutRecorder {
|
||||
source,
|
||||
model_provider: Some(config.model_provider_id().to_string()),
|
||||
base_instructions: Some(base_instructions),
|
||||
developer_instructions,
|
||||
dynamic_tools: if dynamic_tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -73,8 +73,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
|
||||
thread_id,
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
@@ -167,8 +166,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu
|
||||
thread_id,
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Cli,
|
||||
Some(BaseInstructions::default()),
|
||||
/*developer_instructions*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
|
||||
@@ -977,7 +977,7 @@ async fn test_get_thread_contents() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_base_instructions_missing_in_meta_stays_missing() {
|
||||
async fn test_base_instructions_missing_in_meta_defaults_to_null() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let home = temp.path();
|
||||
|
||||
@@ -1011,7 +1011,10 @@ async fn test_base_instructions_missing_in_meta_stays_missing() {
|
||||
.await
|
||||
.expect("session meta head");
|
||||
let first = head.first().expect("first head entry");
|
||||
assert_eq!(first.get("base_instructions"), None);
|
||||
assert_eq!(
|
||||
first.get("base_instructions"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1140,7 +1143,6 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".into()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -257,7 +257,6 @@ mod tests {
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
@@ -385,7 +384,6 @@ mod tests {
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
@@ -1030,7 +1030,6 @@ mod tests {
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: Some("polluted".to_string()),
|
||||
},
|
||||
@@ -1089,7 +1088,6 @@ mod tests {
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user