Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion rust/src/generated/api_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8484,7 +8484,7 @@ pub struct ModelSetReasoningEffortResult {
pub reasoning_effort: String,
}

/// Optional GitHub token used to list models for a specific user instead of the global auth context.
/// Optional GitHub token and working directory used to resolve available models.
///
/// <div class="warning">
///
Expand All @@ -8495,6 +8495,9 @@ pub struct ModelSetReasoningEffortResult {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelsListRequest {
/// Working directory used to apply repository model policy. When omitted, model availability is account-global.
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
/// GitHub token for per-user model listing. When provided, resolves this token to determine the user's Copilot plan and available models instead of using the global auth.
#[serde(skip_serializing_if = "Option::is_none")]
pub git_hub_token: Option<String>,
Expand Down
20 changes: 19 additions & 1 deletion rust/tests/api_types_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

use github_copilot_sdk::rpc::{
Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, ModelsListRequest,
TasksStartAgentRequest,
};
use github_copilot_sdk::session_events::{PermissionRequest, PermissionRequestedData};

Expand Down Expand Up @@ -104,6 +105,23 @@ fn permission_event_exposes_managed_approval_required() {
assert_eq!(request.managed_approval_required, Some(true));
}

#[test]
fn models_list_request_serializes_repository_cwd() {
let request = ModelsListRequest {
cwd: Some("/workspace/repository".to_string()),
git_hub_token: None,
};

assert_eq!(
serde_json::to_value(request).unwrap(),
serde_json::json!({ "cwd": "/workspace/repository" })
);
assert_eq!(
serde_json::to_value(ModelsListRequest::default()).unwrap(),
serde_json::json!({})
);
}

fn running_extension(id: &str, name: &str) -> Extension {
Extension {
id: id.to_string(),
Expand Down
35 changes: 34 additions & 1 deletion rust/tests/session_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use github_copilot_sdk::handler::{
};
use github_copilot_sdk::rpc::{
CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult,
OpenCanvasInstance,
ModelsListRequest, OpenCanvasInstance,
};
use github_copilot_sdk::session_events::{
ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig,
Expand Down Expand Up @@ -4044,6 +4044,39 @@ async fn rpc_namespace_client_models_list_dispatches_correctly() {
assert!(result.models.is_empty());
}

#[tokio::test]
async fn rpc_namespace_client_models_list_sends_repository_cwd() {
let (session, mut server) = create_session_pair().await;
let session = Arc::new(session);

let client = session.client().clone();
let handle = tokio::spawn(async move {
client
.rpc()
.models()
.list_with_params(ModelsListRequest {
cwd: Some("/workspace/repository".to_string()),
git_hub_token: None,
})
.await
});

let request = server.read_request().await;
assert_eq!(request["method"], "models.list");
assert_eq!(
request["params"],
serde_json::json!({
"cwd": "/workspace/repository"
})
);
server
.respond(&request, serde_json::json!({ "models": [] }))
.await;

let result = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap();
assert!(result.models.is_empty());
}

#[tokio::test]
async fn client_stop_sends_session_destroy_for_each_active_session() {
// One client, two registered sessions. Client::stop must send
Expand Down
3 changes: 2 additions & 1 deletion scripts/codegen/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { fileURLToPath } from "url";
import { promisify } from "util";
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
import {
addCwdToModelsListRequest,
addManagedApprovalRequiredToPermissionRequests,
type ApiSchema,
type DefinitionCollections,
Expand Down Expand Up @@ -2219,7 +2220,7 @@ async function generate(): Promise<void> {
);
const apiSchema = propagateInternalVisibility(
postProcessSchema(
stripBooleanLiterals(apiRaw) as JSONSchema7,
stripBooleanLiterals(addCwdToModelsListRequest(apiRaw)) as JSONSchema7,
),
) as unknown as ApiSchema;

Expand Down
40 changes: 40 additions & 0 deletions scripts/codegen/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,46 @@ export function addManagedApprovalRequiredToPermissionRequests<T extends JSONSch
return cloned;
}

/**
* Add repository scoping to model listing until the pinned CLI schema includes the field.
*/
export function addCwdToModelsListRequest<T extends JSONSchema7>(schema: T): T {
const cloned = cloneSchemaForCodegen(schema);
const property: JSONSchema7 = {
description:
"Working directory used to apply repository model policy. When omitted, model availability is account-global.",
type: ["string", "null"],
};
(property as Record<string, unknown>)["x-copilot-sdk-append-last"] = true;

for (const definitions of [cloned.definitions, cloned.$defs]) {
if (!definitions) continue;
const definition = definitions.ModelsListRequest;
if (!definition || typeof definition !== "object") continue;
const requestDefinition = definition as JSONSchema7;
const objectDefinition = [
requestDefinition,
...(requestDefinition.anyOf ?? []),
...(requestDefinition.oneOf ?? []),
].find(
(candidate): candidate is JSONSchema7 =>
typeof candidate === "object" &&
candidate !== null &&
(candidate.type === "object" || candidate.properties !== undefined),
);
if (!objectDefinition) continue;
if (objectDefinition.properties?.cwd) continue;
requestDefinition.description =
"Optional GitHub token and working directory used to resolve available models.";
Comment on lines +528 to +529
objectDefinition.properties = {
...objectDefinition.properties,
cwd: cloneSchemaForCodegen(property),
};
}

return cloned;
}

export function getEnumValueDescriptions(schema: JSONSchema7 | null | undefined): EnumValueDescriptions | undefined {
if (!schema || typeof schema !== "object") return undefined;

Expand Down
Loading