Skip to content
Closed
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 30 additions & 0 deletions doc/auth/byok.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion doc/reference/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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"
Expand Down
72 changes: 43 additions & 29 deletions src/github/copilot_sdk/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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"))))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
21 changes: 15 additions & 6 deletions src/github/copilot_sdk/specs.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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))

;; -----------------------------------------------------------------------------
Expand Down Expand Up @@ -198,14 +203,17 @@

(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
:on-permission-request :streaming? :mcp-servers
: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
Expand All @@ -216,15 +224,15 @@
::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
#{:client-name :model :tools :system-message :available-tools :excluded-tools
: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
Expand All @@ -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))

;; -----------------------------------------------------------------------------
Expand Down