Feat: add model reroute notification (#12001)
### Summary
Builiding off
5c75aa7b89 (diff-058ae8f109a8b84b4b79bbfa45f522c2233b9d9e139696044ae374d50b6196e0),
we have created a `model/rerouted` notification that captures the event
so that consumers can render as expected. Keep the `EventMsg::Warning`
path in core so that this does not affect TUI rendering.
`model/rerouted` is meant to be generic to account for future usage
including capacity planning etc.
This commit is contained in:
parent
a1b8e34938
commit
48018e9eac
28 changed files with 605 additions and 146 deletions
|
|
@ -473,6 +473,35 @@
|
|||
"title": "WarningEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Model routing changed from the requested model to a different model.",
|
||||
"properties": {
|
||||
"from_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ModelRerouteReason"
|
||||
},
|
||||
"to_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"model_reroute"
|
||||
],
|
||||
"title": "ModelRerouteEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from_model",
|
||||
"reason",
|
||||
"to_model",
|
||||
"type"
|
||||
],
|
||||
"title": "ModelRerouteEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Conversation history was compacted (either automatically or manually).",
|
||||
"properties": {
|
||||
|
|
@ -3318,6 +3347,12 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"high_risk_cyber_activity"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"description": "Represents whether outbound network access is available to the agent.",
|
||||
"enum": [
|
||||
|
|
@ -5378,6 +5413,35 @@
|
|||
"title": "WarningEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Model routing changed from the requested model to a different model.",
|
||||
"properties": {
|
||||
"from_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ModelRerouteReason"
|
||||
},
|
||||
"to_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"model_reroute"
|
||||
],
|
||||
"title": "ModelRerouteEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from_model",
|
||||
"reason",
|
||||
"to_model",
|
||||
"type"
|
||||
],
|
||||
"title": "ModelRerouteEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Conversation history was compacted (either automatically or manually).",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -1094,6 +1094,35 @@
|
|||
"title": "WarningEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Model routing changed from the requested model to a different model.",
|
||||
"properties": {
|
||||
"from_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ModelRerouteReason2"
|
||||
},
|
||||
"to_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"model_reroute"
|
||||
],
|
||||
"title": "ModelRerouteEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from_model",
|
||||
"reason",
|
||||
"to_model",
|
||||
"type"
|
||||
],
|
||||
"title": "ModelRerouteEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Conversation history was compacted (either automatically or manually).",
|
||||
"properties": {
|
||||
|
|
@ -4210,6 +4239,45 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"highRiskCyberActivity"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelRerouteReason2": {
|
||||
"enum": [
|
||||
"high_risk_cyber_activity"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelReroutedNotification": {
|
||||
"properties": {
|
||||
"fromModel": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ModelRerouteReason"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"toModel": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fromModel",
|
||||
"reason",
|
||||
"threadId",
|
||||
"toModel",
|
||||
"turnId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"description": "Represents whether outbound network access is available to the agent.",
|
||||
"enum": [
|
||||
|
|
@ -8276,6 +8344,26 @@
|
|||
"title": "Thread/compactedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"model/rerouted"
|
||||
],
|
||||
"title": "Model/reroutedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ModelReroutedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Model/reroutedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
|
|
|||
|
|
@ -2486,6 +2486,35 @@
|
|||
"title": "WarningEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Model routing changed from the requested model to a different model.",
|
||||
"properties": {
|
||||
"from_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/v2/ModelRerouteReason"
|
||||
},
|
||||
"to_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"model_reroute"
|
||||
],
|
||||
"title": "ModelRerouteEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from_model",
|
||||
"reason",
|
||||
"to_model",
|
||||
"type"
|
||||
],
|
||||
"title": "ModelRerouteEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Conversation history was compacted (either automatically or manually).",
|
||||
"properties": {
|
||||
|
|
@ -6266,6 +6295,12 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"high_risk_cyber_activity"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"description": "Represents whether outbound network access is available to the agent.",
|
||||
"enum": [
|
||||
|
|
@ -8491,6 +8526,26 @@
|
|||
"title": "Thread/compactedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"model/rerouted"
|
||||
],
|
||||
"title": "Model/reroutedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ModelReroutedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Model/reroutedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
|
@ -12750,6 +12805,41 @@
|
|||
"title": "ModelListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"highRiskCyberActivity"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelReroutedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"fromModel": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/v2/ModelRerouteReason"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"toModel": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fromModel",
|
||||
"reason",
|
||||
"threadId",
|
||||
"toModel",
|
||||
"turnId"
|
||||
],
|
||||
"title": "ModelReroutedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
"restricted",
|
||||
|
|
|
|||
|
|
@ -473,6 +473,35 @@
|
|||
"title": "WarningEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Model routing changed from the requested model to a different model.",
|
||||
"properties": {
|
||||
"from_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ModelRerouteReason"
|
||||
},
|
||||
"to_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"model_reroute"
|
||||
],
|
||||
"title": "ModelRerouteEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from_model",
|
||||
"reason",
|
||||
"to_model",
|
||||
"type"
|
||||
],
|
||||
"title": "ModelRerouteEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Conversation history was compacted (either automatically or manually).",
|
||||
"properties": {
|
||||
|
|
@ -3318,6 +3347,12 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"high_risk_cyber_activity"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"description": "Represents whether outbound network access is available to the agent.",
|
||||
"enum": [
|
||||
|
|
|
|||
|
|
@ -473,6 +473,35 @@
|
|||
"title": "WarningEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Model routing changed from the requested model to a different model.",
|
||||
"properties": {
|
||||
"from_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ModelRerouteReason"
|
||||
},
|
||||
"to_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"model_reroute"
|
||||
],
|
||||
"title": "ModelRerouteEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from_model",
|
||||
"reason",
|
||||
"to_model",
|
||||
"type"
|
||||
],
|
||||
"title": "ModelRerouteEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Conversation history was compacted (either automatically or manually).",
|
||||
"properties": {
|
||||
|
|
@ -3318,6 +3347,12 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"high_risk_cyber_activity"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"description": "Represents whether outbound network access is available to the agent.",
|
||||
"enum": [
|
||||
|
|
|
|||
|
|
@ -473,6 +473,35 @@
|
|||
"title": "WarningEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Model routing changed from the requested model to a different model.",
|
||||
"properties": {
|
||||
"from_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ModelRerouteReason"
|
||||
},
|
||||
"to_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"model_reroute"
|
||||
],
|
||||
"title": "ModelRerouteEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from_model",
|
||||
"reason",
|
||||
"to_model",
|
||||
"type"
|
||||
],
|
||||
"title": "ModelRerouteEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Conversation history was compacted (either automatically or manually).",
|
||||
"properties": {
|
||||
|
|
@ -3318,6 +3347,12 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"high_risk_cyber_activity"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"description": "Represents whether outbound network access is available to the agent.",
|
||||
"enum": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"highRiskCyberActivity"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"fromModel": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"$ref": "#/definitions/ModelRerouteReason"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"toModel": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fromModel",
|
||||
"reason",
|
||||
"threadId",
|
||||
"toModel",
|
||||
"turnId"
|
||||
],
|
||||
"title": "ModelReroutedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ import type { McpStartupCompleteEvent } from "./McpStartupCompleteEvent";
|
|||
import type { McpStartupUpdateEvent } from "./McpStartupUpdateEvent";
|
||||
import type { McpToolCallBeginEvent } from "./McpToolCallBeginEvent";
|
||||
import type { McpToolCallEndEvent } from "./McpToolCallEndEvent";
|
||||
import type { ModelRerouteEvent } from "./ModelRerouteEvent";
|
||||
import type { PatchApplyBeginEvent } from "./PatchApplyBeginEvent";
|
||||
import type { PatchApplyEndEvent } from "./PatchApplyEndEvent";
|
||||
import type { PlanDeltaEvent } from "./PlanDeltaEvent";
|
||||
|
|
@ -74,4 +75,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent";
|
|||
* Response event from the agent
|
||||
* NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
|
||||
*/
|
||||
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;
|
||||
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
// 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.
|
||||
import type { ModelRerouteReason } from "./ModelRerouteReason";
|
||||
|
||||
export type ModelRerouteEvent = { from_model: string, to_model: string, reason: ModelRerouteReason, };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// 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.
|
||||
|
||||
export type ModelRerouteReason = "high_risk_cyber_activity";
|
||||
|
|
@ -21,6 +21,7 @@ import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification";
|
|||
import type { ItemStartedNotification } from "./v2/ItemStartedNotification";
|
||||
import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification";
|
||||
import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification";
|
||||
import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification";
|
||||
import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification";
|
||||
import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification";
|
||||
import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification";
|
||||
|
|
@ -39,4 +40,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/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification };
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "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": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification };
|
||||
|
|
|
|||
|
|
@ -125,6 +125,8 @@ export type { McpToolCallBeginEvent } from "./McpToolCallBeginEvent";
|
|||
export type { McpToolCallEndEvent } from "./McpToolCallEndEvent";
|
||||
export type { MessagePhase } from "./MessagePhase";
|
||||
export type { ModeKind } from "./ModeKind";
|
||||
export type { ModelRerouteEvent } from "./ModelRerouteEvent";
|
||||
export type { ModelRerouteReason } from "./ModelRerouteReason";
|
||||
export type { NetworkAccess } from "./NetworkAccess";
|
||||
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
|
||||
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
// 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.
|
||||
|
||||
export type ModelRerouteReason = "highRiskCyberActivity";
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// 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.
|
||||
import type { ModelRerouteReason } from "./ModelRerouteReason";
|
||||
|
||||
export type ModelReroutedNotification = { threadId: string, turnId: string, fromModel: string, toModel: string, reason: ModelRerouteReason, };
|
||||
|
|
@ -91,6 +91,8 @@ export type { MergeStrategy } from "./MergeStrategy";
|
|||
export type { Model } from "./Model";
|
||||
export type { ModelListParams } from "./ModelListParams";
|
||||
export type { ModelListResponse } from "./ModelListResponse";
|
||||
export type { ModelRerouteReason } from "./ModelRerouteReason";
|
||||
export type { ModelReroutedNotification } from "./ModelReroutedNotification";
|
||||
export type { NetworkAccess } from "./NetworkAccess";
|
||||
export type { NetworkRequirements } from "./NetworkRequirements";
|
||||
export type { OverriddenMetadata } from "./OverriddenMetadata";
|
||||
|
|
|
|||
|
|
@ -795,6 +795,7 @@ server_notification_definitions! {
|
|||
ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification),
|
||||
/// Deprecated: Use `ContextCompaction` item type instead.
|
||||
ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification),
|
||||
ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification),
|
||||
DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification),
|
||||
ConfigWarning => "configWarning" (v2::ConfigWarningNotification),
|
||||
FuzzyFileSearchSessionUpdated => "fuzzyFileSearch/sessionUpdated" (FuzzyFileSearchSessionUpdatedNotification),
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
|
|||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
|
||||
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
|
||||
use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason;
|
||||
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
|
||||
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
|
||||
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
|
||||
|
|
@ -229,6 +230,12 @@ v2_enum_from_core!(
|
|||
}
|
||||
);
|
||||
|
||||
v2_enum_from_core!(
|
||||
pub enum ModelRerouteReason from CoreModelRerouteReason {
|
||||
HighRiskCyberActivity
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
|
|
@ -3305,6 +3312,17 @@ pub struct AccountLoginCompletedNotification {
|
|||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ModelReroutedNotification {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub from_model: String,
|
||||
pub to_model: String,
|
||||
pub reason: ModelRerouteReason,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
|
|
|||
|
|
@ -550,6 +550,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
|
|||
- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`.
|
||||
- `turn/diff/updated` — `{ threadId, turnId, diff }` represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item. `diff` is the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individual `fileChange` items.
|
||||
- `turn/plan/updated` — `{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`.
|
||||
- `model/rerouted` — `{ threadId, turnId, fromModel, toModel, reason }` when the backend reroutes a request to a different model (for example, due to high-risk cyber safety checks).
|
||||
|
||||
Today both notifications carry an empty `items` array even when item events were streamed; rely on `item/*` notifications for the canonical item list until this is fixed.
|
||||
|
||||
|
|
@ -557,7 +558,7 @@ Today both notifications carry an empty `items` array even when item events were
|
|||
|
||||
`ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items:
|
||||
|
||||
- `userMessage` — `{id, content}` where `content` is a list of user inputs (`text`, `image`, or `localImage`). Cyber model-routing warnings are surfaced as synthetic `userMessage` items with `text` prefixed by `Warning:`.
|
||||
- `userMessage` — `{id, content}` where `content` is a list of user inputs (`text`, `image`, or `localImage`).
|
||||
- `agentMessage` — `{id, text}` containing the accumulated agent reply.
|
||||
- `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental).
|
||||
- `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ use codex_app_server_protocol::JSONRPCErrorError;
|
|||
use codex_app_server_protocol::McpToolCallError;
|
||||
use codex_app_server_protocol::McpToolCallResult;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::ModelReroutedNotification;
|
||||
use codex_app_server_protocol::PatchApplyStatus;
|
||||
use codex_app_server_protocol::PatchChangeKind as V2PatchChangeKind;
|
||||
use codex_app_server_protocol::PlanDeltaNotification;
|
||||
|
|
@ -68,7 +69,6 @@ use codex_app_server_protocol::TurnInterruptResponse;
|
|||
use codex_app_server_protocol::TurnPlanStep;
|
||||
use codex_app_server_protocol::TurnPlanUpdatedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_app_server_protocol::build_turns_from_rollout_items;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::parse_command::shlex_join;
|
||||
|
|
@ -96,8 +96,6 @@ use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUse
|
|||
use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::hash::Hash;
|
||||
use std::hash::Hasher;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
|
@ -125,32 +123,18 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||
EventMsg::TurnComplete(_ev) => {
|
||||
handle_turn_complete(conversation_id, event_turn_id, &outgoing, &thread_state).await;
|
||||
}
|
||||
EventMsg::Warning(warning_event) => {
|
||||
if matches!(api_version, ApiVersion::V2)
|
||||
&& is_safety_check_downgrade_warning(&warning_event.message)
|
||||
{
|
||||
let item = ThreadItem::UserMessage {
|
||||
id: warning_item_id(&event_turn_id, &warning_event.message),
|
||||
content: vec![V2UserInput::Text {
|
||||
text: format!("Warning: {}", warning_event.message),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
};
|
||||
let started = ItemStartedNotification {
|
||||
EventMsg::Warning(_warning_event) => {}
|
||||
EventMsg::ModelReroute(event) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
let notification = ModelReroutedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
item: item.clone(),
|
||||
from_model: event.from_model,
|
||||
to_model: event.to_model,
|
||||
reason: event.reason.into(),
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemStarted(started))
|
||||
.await;
|
||||
let completed = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
item,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(completed))
|
||||
.send_server_notification(ServerNotification::ModelRerouted(notification))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
|
@ -1318,18 +1302,6 @@ async fn complete_command_execution_item(
|
|||
.await;
|
||||
}
|
||||
|
||||
fn is_safety_check_downgrade_warning(message: &str) -> bool {
|
||||
message.contains("Your account was flagged for potentially high-risk cyber activity")
|
||||
&& message.contains("apply for trusted access: https://chatgpt.com/cyber")
|
||||
}
|
||||
|
||||
fn warning_item_id(turn_id: &str, message: &str) -> String {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
message.hash(&mut hasher);
|
||||
let digest = hasher.finish();
|
||||
format!("{turn_id}-warning-{digest:x}")
|
||||
}
|
||||
|
||||
async fn maybe_emit_raw_response_item_completed(
|
||||
api_version: ApiVersion,
|
||||
conversation_id: ThreadId,
|
||||
|
|
@ -2060,18 +2032,6 @@ mod tests {
|
|||
assert_eq!(item, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safety_check_downgrade_warning_detection_matches_expected_message() {
|
||||
let warning = "Your account was flagged for potentially high-risk cyber activity and this request was routed to gpt-5.2 as a fallback. To regain access to gpt-5.3-codex, apply for trusted access: https://chatgpt.com/cyber\nLearn more: https://developers.openai.com/codex/concepts/cyber-safety";
|
||||
assert!(is_safety_check_downgrade_warning(warning));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safety_check_downgrade_warning_detection_ignores_other_warnings() {
|
||||
let warning = "Model metadata for `mock-model` not found. Defaulting to fallback metadata; this can degrade performance and cause issues.";
|
||||
assert!(!is_safety_check_downgrade_warning(warning));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_error_records_message() -> Result<()> {
|
||||
let conversation_id = ThreadId::new();
|
||||
|
|
|
|||
|
|
@ -399,6 +399,8 @@ mod tests {
|
|||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::LoginChatGptCompleteNotification;
|
||||
use codex_app_server_protocol::ModelRerouteReason;
|
||||
use codex_app_server_protocol::ModelReroutedNotification;
|
||||
use codex_app_server_protocol::RateLimitSnapshot;
|
||||
use codex_app_server_protocol::RateLimitWindow;
|
||||
use codex_protocol::ThreadId;
|
||||
|
|
@ -546,6 +548,34 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_model_rerouted_notification_serialization() {
|
||||
let notification = ServerNotification::ModelRerouted(ModelReroutedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
from_model: "gpt-5.3-codex".to_string(),
|
||||
to_model: "gpt-5.2".to_string(),
|
||||
reason: ModelRerouteReason::HighRiskCyberActivity,
|
||||
});
|
||||
|
||||
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "model/rerouted",
|
||||
"params": {
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1",
|
||||
"fromModel": "gpt-5.3-codex",
|
||||
"toModel": "gpt-5.2",
|
||||
"reason": "highRiskCyberActivity",
|
||||
},
|
||||
}),
|
||||
serde_json::to_value(jsonrpc_notification)
|
||||
.expect("ensure the notification serializes correctly"),
|
||||
"ensure the notification serializes correctly"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_response_routes_to_target_connection() {
|
||||
let (tx, mut rx) = mpsc::channel::<OutgoingEnvelope>(4);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ use app_test_support::McpProcess;
|
|||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::ModelRerouteReason;
|
||||
use codex_app_server_protocol::ModelReroutedNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
|
|
@ -23,7 +25,7 @@ const REQUESTED_MODEL: &str = "gpt-5.1-codex-max";
|
|||
const SERVER_MODEL: &str = "gpt-5.2-codex";
|
||||
|
||||
#[tokio::test]
|
||||
async fn openai_model_header_mismatch_emits_warning_item_v2() -> Result<()> {
|
||||
async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
|
|
@ -64,64 +66,30 @@ async fn openai_model_header_mismatch_emits_warning_item_v2() -> Result<()> {
|
|||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let _turn_resp: JSONRPCResponse = timeout(
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let _turn_start: TurnStartResponse = to_response(_turn_resp)?;
|
||||
let turn_start: TurnStartResponse = to_response(turn_resp)?;
|
||||
|
||||
let warning_started = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let notification: JSONRPCNotification = mcp
|
||||
.read_stream_until_notification_message("item/started")
|
||||
.await?;
|
||||
let params = notification.params.expect("item/started params");
|
||||
let started: ItemStartedNotification =
|
||||
serde_json::from_value(params).expect("deserialize item/started");
|
||||
if warning_text_from_item(&started.item).is_some_and(is_cyber_model_warning_text) {
|
||||
return Ok::<ItemStartedNotification, anyhow::Error>(started);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
|
||||
let warning_text =
|
||||
warning_text_from_item(&warning_started.item).expect("expected warning user message item");
|
||||
assert!(warning_text.contains("Warning:"));
|
||||
assert!(warning_text.contains("gpt-5.2 as a fallback"));
|
||||
assert!(warning_text.contains("regain access to gpt-5.3-codex"));
|
||||
|
||||
let warning_completed = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let notification: JSONRPCNotification = mcp
|
||||
.read_stream_until_notification_message("item/completed")
|
||||
.await?;
|
||||
let params = notification.params.expect("item/completed params");
|
||||
let completed: ItemCompletedNotification =
|
||||
serde_json::from_value(params).expect("deserialize item/completed");
|
||||
if warning_text_from_item(&completed.item).is_some_and(is_cyber_model_warning_text) {
|
||||
return Ok::<ItemCompletedNotification, anyhow::Error>(completed);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let rerouted = collect_turn_notifications_and_validate_no_warning_item(&mut mcp).await?;
|
||||
assert_eq!(
|
||||
warning_text_from_item(&warning_completed.item),
|
||||
warning_text_from_item(&warning_started.item)
|
||||
rerouted,
|
||||
ModelReroutedNotification {
|
||||
thread_id: thread.id,
|
||||
turn_id: turn_start.turn.id,
|
||||
from_model: REQUESTED_MODEL.to_string(),
|
||||
to_model: SERVER_MODEL.to_string(),
|
||||
reason: ModelRerouteReason::HighRiskCyberActivity,
|
||||
}
|
||||
);
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn response_model_field_mismatch_emits_warning_item_v2_when_header_matches_requested()
|
||||
async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when_header_matches_requested()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
|
|
@ -174,54 +142,65 @@ async fn response_model_field_mismatch_emits_warning_item_v2_when_header_matches
|
|||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let _turn_start: TurnStartResponse = to_response(turn_resp)?;
|
||||
let turn_start: TurnStartResponse = to_response(turn_resp)?;
|
||||
|
||||
let warning_started = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let notification: JSONRPCNotification = mcp
|
||||
.read_stream_until_notification_message("item/started")
|
||||
.await?;
|
||||
let params = notification.params.expect("item/started params");
|
||||
let started: ItemStartedNotification =
|
||||
serde_json::from_value(params).expect("deserialize item/started");
|
||||
if warning_text_from_item(&started.item).is_some_and(is_cyber_model_warning_text) {
|
||||
return Ok::<ItemStartedNotification, anyhow::Error>(started);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let warning_text =
|
||||
warning_text_from_item(&warning_started.item).expect("expected warning user message item");
|
||||
assert!(warning_text.contains("gpt-5.2 as a fallback"));
|
||||
|
||||
let warning_completed = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let notification: JSONRPCNotification = mcp
|
||||
.read_stream_until_notification_message("item/completed")
|
||||
.await?;
|
||||
let params = notification.params.expect("item/completed params");
|
||||
let completed: ItemCompletedNotification =
|
||||
serde_json::from_value(params).expect("deserialize item/completed");
|
||||
if warning_text_from_item(&completed.item).is_some_and(is_cyber_model_warning_text) {
|
||||
return Ok::<ItemCompletedNotification, anyhow::Error>(completed);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let rerouted = collect_turn_notifications_and_validate_no_warning_item(&mut mcp).await?;
|
||||
assert_eq!(
|
||||
warning_text_from_item(&warning_completed.item),
|
||||
warning_text_from_item(&warning_started.item)
|
||||
rerouted,
|
||||
ModelReroutedNotification {
|
||||
thread_id: thread.id,
|
||||
turn_id: turn_start.turn.id,
|
||||
from_model: REQUESTED_MODEL.to_string(),
|
||||
to_model: SERVER_MODEL.to_string(),
|
||||
reason: ModelRerouteReason::HighRiskCyberActivity,
|
||||
}
|
||||
);
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn collect_turn_notifications_and_validate_no_warning_item(
|
||||
mcp: &mut McpProcess,
|
||||
) -> Result<ModelReroutedNotification> {
|
||||
let mut rerouted = None;
|
||||
|
||||
loop {
|
||||
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notification) = message else {
|
||||
continue;
|
||||
};
|
||||
match notification.method.as_str() {
|
||||
"model/rerouted" => {
|
||||
let params = notification.params.ok_or_else(|| {
|
||||
anyhow::anyhow!("model/rerouted notifications must include params")
|
||||
})?;
|
||||
let payload: ModelReroutedNotification = serde_json::from_value(params)?;
|
||||
rerouted = Some(payload);
|
||||
}
|
||||
"item/started" => {
|
||||
let params = notification.params.ok_or_else(|| {
|
||||
anyhow::anyhow!("item/started notifications must include params")
|
||||
})?;
|
||||
let payload: ItemStartedNotification = serde_json::from_value(params)?;
|
||||
assert!(!is_warning_user_message_item(&payload.item));
|
||||
}
|
||||
"item/completed" => {
|
||||
let params = notification.params.ok_or_else(|| {
|
||||
anyhow::anyhow!("item/completed notifications must include params")
|
||||
})?;
|
||||
let payload: ItemCompletedNotification = serde_json::from_value(params)?;
|
||||
assert!(!is_warning_user_message_item(&payload.item));
|
||||
}
|
||||
"turn/completed" => {
|
||||
return rerouted.ok_or_else(|| {
|
||||
anyhow::anyhow!("expected model/rerouted notification before turn/completed")
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn warning_text_from_item(item: &ThreadItem) -> Option<&str> {
|
||||
let ThreadItem::UserMessage { content, .. } = item else {
|
||||
return None;
|
||||
|
|
@ -233,9 +212,8 @@ fn warning_text_from_item(item: &ThreadItem) -> Option<&str> {
|
|||
})
|
||||
}
|
||||
|
||||
fn is_cyber_model_warning_text(text: &str) -> bool {
|
||||
text.contains("flagged for potentially high-risk cyber activity")
|
||||
&& text.contains("apply for trusted access: https://chatgpt.com/cyber")
|
||||
fn is_warning_user_message_item(item: &ThreadItem) -> bool {
|
||||
warning_text_from_item(item).is_some()
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
|
||||
|
|
|
|||
|
|
@ -172,6 +172,8 @@ use crate::protocol::Event;
|
|||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::ExecApprovalRequestEvent;
|
||||
use crate::protocol::McpServerRefreshConfig;
|
||||
use crate::protocol::ModelRerouteEvent;
|
||||
use crate::protocol::ModelRerouteReason;
|
||||
use crate::protocol::NetworkApprovalContext;
|
||||
use crate::protocol::Op;
|
||||
use crate::protocol::PlanDeltaEvent;
|
||||
|
|
@ -2440,7 +2442,7 @@ impl Session {
|
|||
turn_context: &Arc<TurnContext>,
|
||||
server_model: String,
|
||||
) -> bool {
|
||||
let requested_model = turn_context.model_info.slug.as_str();
|
||||
let requested_model = turn_context.model_info.slug.clone();
|
||||
if server_model == requested_model {
|
||||
info!("server reported model {server_model} (matches requested model)");
|
||||
return false;
|
||||
|
|
@ -2452,6 +2454,16 @@ impl Session {
|
|||
"Your account was flagged for potentially high-risk cyber activity and this request was routed to gpt-5.2 as a fallback. To regain access to gpt-5.3-codex, apply for trusted access: {CYBER_VERIFY_URL} or learn more: {CYBER_SAFETY_URL}"
|
||||
);
|
||||
|
||||
self.send_event(
|
||||
turn_context,
|
||||
EventMsg::ModelReroute(ModelRerouteEvent {
|
||||
from_model: requested_model.clone(),
|
||||
to_model: server_model.clone(),
|
||||
reason: ModelRerouteReason::HighRiskCyberActivity,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
self.send_event(
|
||||
turn_context,
|
||||
EventMsg::Warning(WarningEvent {
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
|
|||
| EventMsg::CollabCloseEnd(_)
|
||||
| EventMsg::CollabResumeEnd(_) => Some(EventPersistenceMode::Extended),
|
||||
EventMsg::Warning(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
| EventMsg::AgentMessageDelta(_)
|
||||
| EventMsg::AgentReasoningDelta(_)
|
||||
| EventMsg::AgentReasoningRawContentDelta(_)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::ModelRerouteReason;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
|
|
@ -54,6 +55,17 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
|
|||
})
|
||||
.await?;
|
||||
|
||||
let reroute = wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::ModelReroute(_))
|
||||
})
|
||||
.await;
|
||||
let EventMsg::ModelReroute(reroute) = reroute else {
|
||||
panic!("expected model reroute event");
|
||||
};
|
||||
assert_eq!(reroute.from_model, REQUESTED_MODEL);
|
||||
assert_eq!(reroute.to_model, SERVER_MODEL);
|
||||
assert_eq!(reroute.reason, ModelRerouteReason::HighRiskCyberActivity);
|
||||
|
||||
let warning = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Warning(_))).await;
|
||||
let EventMsg::Warning(warning) = warning else {
|
||||
panic!("expected warning event");
|
||||
|
|
@ -138,6 +150,17 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
|
|||
})
|
||||
.await?;
|
||||
|
||||
let reroute = wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::ModelReroute(_))
|
||||
})
|
||||
.await;
|
||||
let EventMsg::ModelReroute(reroute) = reroute else {
|
||||
panic!("expected model reroute event");
|
||||
};
|
||||
assert_eq!(reroute.from_model, REQUESTED_MODEL);
|
||||
assert_eq!(reroute.to_model, SERVER_MODEL);
|
||||
assert_eq!(reroute.reason, ModelRerouteReason::HighRiskCyberActivity);
|
||||
|
||||
let warning = wait_for_event(&test.codex, |event| {
|
||||
matches!(
|
||||
event,
|
||||
|
|
@ -151,7 +174,8 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
|
|||
let EventMsg::Warning(warning) = warning else {
|
||||
panic!("expected warning event");
|
||||
};
|
||||
assert!(warning.message.contains("gpt-5.2 as a fallback"));
|
||||
assert!(warning.message.contains(REQUESTED_MODEL));
|
||||
assert!(warning.message.contains(SERVER_MODEL));
|
||||
|
||||
let _ = wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
|||
"warning:".style(self.yellow).style(self.bold)
|
||||
);
|
||||
}
|
||||
EventMsg::ModelReroute(_) => {}
|
||||
EventMsg::DeprecationNotice(DeprecationNoticeEvent { summary, details }) => {
|
||||
ts_msg!(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -254,6 +254,9 @@ async fn run_codex_tool_session_inner(
|
|||
EventMsg::Warning(_) => {
|
||||
continue;
|
||||
}
|
||||
EventMsg::ModelReroute(_) => {
|
||||
continue;
|
||||
}
|
||||
EventMsg::ElicitationRequest(_) => {
|
||||
// TODO: forward elicitation requests to the client?
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -883,6 +883,9 @@ pub enum EventMsg {
|
|||
/// indicates the turn continued but the user should still be notified.
|
||||
Warning(WarningEvent),
|
||||
|
||||
/// Model routing changed from the requested model to a different model.
|
||||
ModelReroute(ModelRerouteEvent),
|
||||
|
||||
/// Conversation history was compacted (either automatically or manually).
|
||||
ContextCompacted(ContextCompactedEvent),
|
||||
|
||||
|
|
@ -1342,6 +1345,20 @@ pub struct WarningEvent {
|
|||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
pub enum ModelRerouteReason {
|
||||
HighRiskCyberActivity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct ModelRerouteEvent {
|
||||
pub from_model: String,
|
||||
pub to_model: String,
|
||||
pub reason: ModelRerouteReason,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ContextCompactedEvent;
|
||||
|
||||
|
|
|
|||
|
|
@ -4021,6 +4021,7 @@ impl ChatWidget {
|
|||
self.on_rate_limit_snapshot(ev.rate_limits);
|
||||
}
|
||||
EventMsg::Warning(WarningEvent { message }) => self.on_warning(message),
|
||||
EventMsg::ModelReroute(_) => {}
|
||||
EventMsg::Error(ErrorEvent {
|
||||
message,
|
||||
codex_error_info,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue