diff --git a/CHANGELOG.md b/CHANGELOG.md index af6054f..f9933a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] +### Added (v0.1.32 sync) +- `:agent` parameter to `create-session` and `resume-session` config for pre-selecting a custom agent when the session starts. Must match the `:name` of one of the agents in `:custom-agents`. (upstream PR #722) +- `:on-list-models` client option: a zero-arg function returning a seq of model info maps. When provided, `list-models` calls this handler instead of querying the CLI server. Useful in BYOK mode to return models from your custom provider. Connection is not required when this handler is set. (upstream PR #730) + ## [0.1.30.1] - 2026-03-07 ### Added - `disconnect!` function as the preferred API for closing sessions, matching upstream SDK's `disconnect()` (upstream PR #599). `destroy!` is deprecated but still works as an alias. diff --git a/doc/auth/byok.md b/doc/auth/byok.md index ca55f2e..1925028 100644 --- a/doc/auth/byok.md +++ b/doc/auth/byok.md @@ -183,6 +183,36 @@ You must provide and manage the API key or bearer token that BYOK uses. - **Usage tracking** — Tracked by your provider, not GitHub Copilot - **Premium requests** — Do not count against Copilot premium request quotas +## Custom Model Listing + +When using BYOK, the CLI server may not know which models your provider supports. Use `:on-list-models` in your client options to supply a custom model list: + +```clojure +(require '[github.copilot-sdk :as copilot]) + +(def my-models + [{:id "my-gpt-4o" + :name "My GPT-4o" + :vendor "openai" + :family "gpt-4o" + :version "" + :max-input-tokens 128000 + :max-output-tokens 16384 + :preview? false + :default-temperature 1 + :model-picker-priority 1 + :model-capabilities {:model-supports {} :model-limits {}}}]) + +(def client + (copilot/client {:on-list-models (fn [] my-models)})) + +;; list-models now returns my-models (no CLI connection required) +(copilot/list-models client) +;; => [{:id "my-gpt-4o" ...}] +``` + +The handler is a zero-arg function returning a seq of model info maps in the same format that `list-models` returns. Results are cached after the first call. + ## Troubleshooting ### "Model not specified" Error diff --git a/doc/reference/API.md b/doc/reference/API.md index dba73c2..035edf1 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -240,6 +240,7 @@ Create a client and session together, ensuring both are cleaned up on exit. | `:reasoning-effort` | string | Reasoning effort level: `"low"`, `"medium"`, `"high"`, or `"xhigh"` | | `:on-user-input-request` | fn | Handler for `ask_user` requests (see below) | | `:hooks` | map | Lifecycle hooks (see below) | +| `:agent` | string | Name of the custom agent to activate when the session starts. Must match the `:name` of one of the agents in `:custom-agents`. | #### `resume-session` @@ -340,7 +341,12 @@ Get current authentication status. Returns: ``` List available models with their metadata. Results are cached per client connection. -Requires authentication. Returns a vector of model info maps: +Requires authentication (unless `:on-list-models` was provided in client options). + +When `:on-list-models` is set in client options, calls that handler instead of querying the CLI +server — no connection required. Useful in BYOK mode to return models from your custom provider. + +Returns a vector of model info maps: ```clojure [{:id "gpt-5.2" :name "GPT-5.2" diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index c805ccc..4afa3ca 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -119,7 +119,10 @@ - :tool-timeout-ms - Timeout for tool calls that return a channel (default: 120000) - :env - Environment variables map - :github-token - GitHub token for authentication (sets COPILOT_SDK_AUTH_TOKEN env var) - - :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided)" + - :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided) + - :on-list-models - Custom handler fn (no args) returning a seq of model info maps. + When provided, list-models calls this instead of querying the CLI. + Useful in BYOK mode to return models from your custom provider." ([] (client {})) ([opts] @@ -924,7 +927,8 @@ "List available models with their metadata. Results are cached per client connection to prevent rate limiting under concurrency. Cache is cleared on stop!/force-stop!. - Requires authentication. + When :on-list-models was provided in the client options, calls that handler + instead of querying the CLI server (connection not required in that case). Returns a vector of model info maps with keys: :id :name :vendor :family :version :max-input-tokens :max-output-tokens :preview? :default-temperature :model-picker-priority @@ -939,33 +943,37 @@ :supports-reasoning-effort (legacy flat key) :vision-limits {:supported-media-types :max-prompt-images :max-prompt-image-size} (legacy)" [client] - (ensure-connected! client) - (let [p (promise) - entry (swap! (:state client) update :models-cache #(or % p)) - cached (:models-cache entry)] - (cond - ;; Already cached result (immutable, no need to copy) - (vector? cached) - cached - - ;; We won the race and must fetch - (identical? cached p) - (try - (let [models (fetch-models! client)] - (deliver p models) - (swap! (:state client) assoc :models-cache models) - models) - (catch Exception e - (deliver p e) - (swap! (:state client) assoc :models-cache nil) - (throw e))) - - ;; Another thread is fetching, wait on promise - :else - (let [result @cached] - (if (instance? Exception result) - (throw result) - result))))) + (let [on-list-models (get-in @(:state client) [:options :on-list-models])] + (when-not on-list-models + (ensure-connected! client)) + (let [p (promise) + entry (swap! (:state client) update :models-cache #(or % p)) + cached (:models-cache entry)] + (cond + ;; Already cached result (immutable, no need to copy) + (vector? cached) + cached + + ;; We won the race and must fetch + (identical? cached p) + (try + (let [models (if on-list-models + (vec (on-list-models)) + (fetch-models! client))] + (deliver p models) + (swap! (:state client) assoc :models-cache models) + models) + (catch Exception e + (deliver p e) + (swap! (:state client) assoc :models-cache nil) + (throw e))) + + ;; Another thread is fetching, wait on promise + :else + (let [result @cached] + (if (instance? Exception result) + (throw result) + result)))))) (defn list-tools "List available tools with their metadata. @@ -1079,6 +1087,7 @@ (:working-directory config) (assoc :working-directory (:working-directory config)) wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions) (:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config)) + (:agent config) (assoc :agent (:agent config)) true (assoc :request-user-input (boolean (:on-user-input-request config))) true (assoc :hooks (boolean (:hooks config))) true (assoc :env-value-mode "direct")))) @@ -1127,6 +1136,7 @@ (:disabled-skills config) (assoc :disabled-skills (:disabled-skills config)) wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions) (:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config)) + (:agent config) (assoc :agent (:agent config)) true (assoc :request-user-input (boolean (:on-user-input-request config))) true (assoc :hooks (boolean (:hooks config))) (:working-directory config) (assoc :working-directory (:working-directory config)) @@ -1177,6 +1187,8 @@ - :hooks - Lifecycle hooks map (PR #269): {:on-pre-tool-use, :on-post-tool-use, :on-user-prompt-submitted, :on-session-start, :on-session-end, :on-error-occurred} + - :agent - Name of the custom agent to activate when the session starts. + Must match the :name of one of the agents in :custom-agents. Returns a CopilotSession." [client config] @@ -1212,6 +1224,8 @@ - :reasoning-effort - Reasoning effort level: \"low\", \"medium\", \"high\", or \"xhigh\" - :on-user-input-request - Handler for ask_user requests - :hooks - Lifecycle hooks map + - :agent - Name of the custom agent to activate for the resumed session. + Must match the :name of one of the agents in :custom-agents. Returns a CopilotSession." [client session-id config] diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index 77deb8b..31c15d0 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -48,18 +48,23 @@ (s/def ::github-token ::non-blank-string) (s/def ::use-logged-in-user? boolean?) +;; Custom model listing handler (upstream PR #730) +(s/def ::on-list-models fn?) + (def client-options-keys #{:cli-path :cli-args :cli-url :cwd :port :use-stdio? :log-level :auto-start? :auto-restart? :notification-queue-size :router-queue-size - :tool-timeout-ms :env :github-token :use-logged-in-user?}) + :tool-timeout-ms :env :github-token :use-logged-in-user? + :on-list-models}) (s/def ::client-options (closed-keys (s/keys :opt-un [::cli-path ::cli-args ::cli-url ::cwd ::port ::use-stdio? ::log-level ::auto-start? ::auto-restart? ::notification-queue-size ::router-queue-size - ::tool-timeout-ms ::env ::github-token ::use-logged-in-user?]) + ::tool-timeout-ms ::env ::github-token ::use-logged-in-user? + ::on-list-models]) client-options-keys)) ;; ----------------------------------------------------------------------------- @@ -198,6 +203,9 @@ (s/def ::client-name ::non-blank-string) +;; Pre-selected custom agent name (upstream PR #722) +(s/def ::agent ::non-blank-string) + (def session-config-keys #{:session-id :client-name :model :tools :system-message :available-tools :excluded-tools :provider @@ -205,7 +213,7 @@ :custom-agents :config-dir :skill-directories :disabled-skills :large-output :infinite-sessions :reasoning-effort :on-user-input-request :hooks - :working-directory}) + :working-directory :agent}) (s/def ::session-config (closed-keys @@ -216,7 +224,7 @@ ::custom-agents ::config-dir ::skill-directories ::disabled-skills ::large-output ::infinite-sessions ::reasoning-effort ::on-user-input-request ::hooks - ::working-directory]) + ::working-directory ::agent]) session-config-keys)) (def ^:private resume-session-config-keys @@ -224,7 +232,7 @@ :provider :streaming? :on-permission-request :mcp-servers :custom-agents :config-dir :skill-directories :disabled-skills :infinite-sessions :reasoning-effort - :on-user-input-request :hooks :working-directory :disable-resume?}) + :on-user-input-request :hooks :working-directory :disable-resume? :agent}) (s/def ::resume-session-config (closed-keys @@ -233,7 +241,8 @@ ::provider ::streaming? ::mcp-servers ::custom-agents ::config-dir ::skill-directories ::disabled-skills ::infinite-sessions ::reasoning-effort - ::on-user-input-request ::hooks ::working-directory ::disable-resume?]) + ::on-user-input-request ::hooks ::working-directory ::disable-resume? + ::agent]) resume-session-config-keys)) ;; -----------------------------------------------------------------------------