diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 0583c0922..a72b0a521 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -30406,6 +30406,8 @@ pub enum ModelPickerCategory { /// Powerful model category optimized for complex tasks. #[serde(rename = "powerful")] Powerful, + #[serde(rename = "experimental")] + Experimental, /// Unknown variant for forward compatibility. #[default] #[serde(other)] diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 69a65a558..eddb483bc 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -14,7 +14,7 @@ use github_copilot_sdk::handler::{ }; use github_copilot_sdk::rpc::{ CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - OpenCanvasInstance, + ModelPickerCategory, ModelsListResult, OpenCanvasInstance, }; use github_copilot_sdk::session_events::{ ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, @@ -4106,6 +4106,77 @@ async fn rpc_namespace_client_models_list_dispatches_correctly() { assert!(result.models.is_empty()); } +#[tokio::test] +async fn raw_client_call_supports_supplemental_model_compatibility() { + let (client, mut server_read, mut server_write) = make_client(); + + let supplemental_call = tokio::spawn({ + let client = client.clone(); + async move { + let value = client.call("models.listSupplemental", None).await.unwrap(); + serde_json::from_value::(value).unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "models.listSupplemental"); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": request["id"], + "result": { + "models": [{ + "id": "experimental-model", + "name": "Experimental Model", + "capabilities": {}, + "modelPickerCategory": "experimental" + }, { + "id": "future-model", + "name": "Future Model", + "capabilities": {}, + "modelPickerCategory": "future-category" + }, { + "id": "legacy-model", + "name": "Legacy Model", + "capabilities": {} + }] + } + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + let result = timeout(TIMEOUT, supplemental_call).await.unwrap().unwrap(); + assert_eq!( + result.models[0].model_picker_category, + Some(ModelPickerCategory::Experimental) + ); + assert_eq!( + result.models[1].model_picker_category, + Some(ModelPickerCategory::Unknown) + ); + assert_eq!(result.models[2].model_picker_category, None); + + let unavailable_call = tokio::spawn(async move { + client + .call("models.listSupplemental", None) + .await + .unwrap_err() + }); + + let request = read_framed(&mut server_read).await; + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": request["id"], + "error": { + "code": -32601, + "message": "Method not found" + } + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + let error = timeout(TIMEOUT, unavailable_call).await.unwrap().unwrap(); + assert_eq!(error.rpc_code(), Some(-32601)); + assert!(!error.is_transport_failure()); +} + #[tokio::test] async fn client_stop_sends_session_destroy_for_each_active_session() { // One client, two registered sessions. Client::stop must send diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index b5235ce1e..8ef01d2aa 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -85,6 +85,25 @@ const STRING_NEWTYPE_OVERRIDES: Record = { requestId: "RequestId", }; +/** + * Keep the Rust model metadata lossless while supplemental models roll out + * ahead of the next published CLI schema. + */ +function addExperimentalModelPickerCategory(schema: ApiSchema): ApiSchema { + const definition = collectDefinitions( + schema as unknown as Record, + ).ModelPickerCategory; + if (!definition || typeof definition !== "object" || !Array.isArray(definition.enum)) { + throw new Error("ModelPickerCategory enum is missing from api.schema.json"); + } + + if (!definition.enum.includes("experimental")) { + definition.enum.push("experimental"); + } + + return schema; +} + // ── Naming helpers ────────────────────────────────────────────────────────── function toPascalCase(s: string): string { @@ -2235,7 +2254,7 @@ async function generate(): Promise { ); const apiSchema = propagateInternalVisibility( postProcessSchema( - stripBooleanLiterals(apiRaw) as JSONSchema7, + stripBooleanLiterals(addExperimentalModelPickerCategory(apiRaw)) as JSONSchema7, ), ) as unknown as ApiSchema;