# copilot


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

``` python
from fastcore.test import test_eq, test_fail
```

## The wire

Copilot serves OpenAI chat completions at `{endpoint}/chat/completions`.
The token exchange returns the endpoint for the account’s plan.
`COPILOT_API` is the fallback when the response omits it.

Copilot requires headers from a recognised editor.
`copilot-integration-id` identifies the editor. The version headers
match the VS Code chat extension. `x-initiator` distinguishes user turns
from tool-loop turns. `copilot-vision-request` enables image input.

Editors also send `x-request-id`, but Rishi omits it. fastllm caches
HTTP clients by constructor arguments. A changing request id would
create a new client for every request.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L38"
target="_blank" style="float:right; font-size:smaller">source</a>

### copilot_hdrs

``` python
def copilot_hdrs(
    auth:NoneType=None, *, integration_id:str='vscode-chat', vision:bool=False, initiator:NoneType=None,
    intent:str='conversation-panel'
):
```

*Build the headers Copilot requires on every request, with `auth` as the
complete `Authorization` value when given.*

``` python
h = copilot_hdrs()
test_eq(h['copilot-integration-id'], 'vscode-chat')
assert 'Authorization' not in h                      # fastllm sets it from the api key
assert 'x-request-id' not in h                       # would defeat fastllm's client cache
test_eq(copilot_hdrs('Bearer t')['Authorization'], 'Bearer t')
test_eq(copilot_hdrs(vision=True)['copilot-vision-request'], 'true')
test_eq(copilot_hdrs(initiator='agent')['x-initiator'], 'agent')
```

## The GitHub token

Copilot requires a GitHub OAuth token issued to a recognised
application. Personal access tokens do not work. Editor sign-ins store a
usable token in a configuration file.
[`copilot_login()`](https://vedicreader.github.io/rishi/copilot.html#copilot_login)
stores the same token shape under `~/.config/rishi`.

[`copilot_oauth()`](https://vedicreader.github.io/rishi/copilot.html#copilot_oauth)
checks Copilot-specific environment variables first, then editor files.
It checks `GH_TOKEN` and `GITHUB_TOKEN` last because they often contain
personal access tokens. The file reader finds a nested `oauth_token`
key.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L78"
target="_blank" style="float:right; font-size:smaller">source</a>

### copilot_oauth

``` python
def copilot_oauth(
    token:NoneType=None
):
```

*A GitHub OAuth token: what you passed, else `COPILOT_ENVS`, else an
editor sign-in, else `GITHUB_ENVS`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L69"
target="_blank" style="float:right; font-size:smaller">source</a>

### read_oauth

``` python
def read_oauth(
    p
):
```

*The GitHub OAuth token in config file `p`, or `None` if it has none or
is not there.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L63"
target="_blank" style="float:right; font-size:smaller">source</a>

### find_oauth

``` python
def find_oauth(
    o, depth:int=0
):
```

*The `oauth_token` in a decoded config file, however deep the editor
nested it.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L55"
target="_blank" style="float:right; font-size:smaller">source</a>

### oauth_paths

``` python
def oauth_paths():
```

*Files a Copilot sign-in may have left a GitHub OAuth token in, best
first.*

``` python
# every shape an editor writes, and one that holds nothing
test_eq(find_oauth({'oauth_token': 'a'}), 'a')
test_eq(find_oauth({'github.com:Iv1.b507a08c87ecfe98': {'oauth_token': 'b', 'user': 'me'}}), 'b')
test_eq(find_oauth({'hosts': {'github.com': {'oauth_token': 'c'}}}), 'c')
test_eq(find_oauth({'user': 'me'}), None)
test_eq(read_oauth('/nonexistent/apps.json'), None)

test_eq(copilot_oauth('explicit'), 'explicit')
test_eq(oauth_paths()[0].name, 'copilot.json')          # rishi's own file is read before any editor's
assert any('github-copilot' in str(p) for p in oauth_paths())
```

``` python
# an editor sign-in beats a general-purpose GitHub variable, because that one holds a personal
# access token, which Copilot refuses. Only a Copilot-specific variable beats the sign-in.
import tempfile

with tempfile.TemporaryDirectory() as d:
    f = Path(d)/'apps.json'
    f.write_text(json.dumps({'github.com:Iv1.b507a08c87ecfe98': {'oauth_token': 'gho_editor'}}))
    saved = {k: os.environ.pop(k, None) for k in OAUTH_ENVS}
    orig_paths, oauth_paths = oauth_paths, lambda: [f]
    try:
        os.environ['GITHUB_TOKEN'] = 'ghp_pat'
        test_eq(copilot_oauth(), 'gho_editor')          # the sign-in, not the PAT
        os.environ['GH_COPILOT_TOKEN'] = 'gho_said_so'
        test_eq(copilot_oauth(), 'gho_said_so')         # names Copilot outright, so it wins
        del os.environ['GH_COPILOT_TOKEN']
        oauth_paths = lambda: []
        test_eq(copilot_oauth(), 'ghp_pat')             # with no sign-in to hide, it is all there is
        del os.environ['GITHUB_TOKEN']
        test_fail(copilot_oauth, contains='No GitHub OAuth token')
    finally:
        oauth_paths = orig_paths
        for k, v in saved.items():
            os.environ.pop(k, None)
            if v is not None: os.environ[k] = v
```

## The Copilot token

The GitHub OAuth token is exchanged for a Copilot token that lasts about
30 minutes.
[`CopilotAuth`](https://vedicreader.github.io/rishi/copilot.html#copilotauth)
holds that token and renews it before it expires, and chats that share
one
[`CopilotAuth`](https://vedicreader.github.io/rishi/copilot.html#copilotauth)
share the exchange. `api_key=` accepts a Copilot token you already have,
but rishi cannot renew it without the GitHub OAuth token.

The same token lets rishi ask Copilot which models the account can
reach.
[`copilot_catalog`](https://vedicreader.github.io/rishi/copilot.html#copilot_catalog)
returns Copilot’s own description of each,
[`copilot_models`](https://vedicreader.github.io/rishi/copilot.html#copilot_models)
lists the ids (chat models only, unless you pass `kind=None`), and
[`copilot_ctx`](https://vedicreader.github.io/rishi/copilot.html#copilot_ctx)
reads a model’s prompt window from its catalogue entry.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L146"
target="_blank" style="float:right; font-size:smaller">source</a>

### copilot_ctx

``` python
def copilot_ctx(
    entry
):
```

*The prompt window Copilot reports for one catalogue entry, or `None`
where it reports none.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L141"
target="_blank" style="float:right; font-size:smaller">source</a>

### copilot_models

``` python
def copilot_models(
    auth:NoneType=None, timeout:int=30, kind:str='chat'
):
```

*List the model ids this account can reach, as Copilot reports them,
keeping embedding and completion models too when `kind=None`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L134"
target="_blank" style="float:right; font-size:smaller">source</a>

### copilot_catalog

``` python
def copilot_catalog(
    auth:NoneType=None, timeout:int=30
):
```

*Every model this account can reach, as `{id: entry}` the way Copilot
describes them.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L116"
target="_blank" style="float:right; font-size:smaller">source</a>

### CopilotAuth

``` python
def CopilotAuth(
    oauth:NoneType=None, *, key:NoneType=None, base_url:NoneType=None, skew:int=300
):
```

*Hold a Copilot token and mint a new one before it expires, so chats
sharing one instance stay signed in.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L101"
target="_blank" style="float:right; font-size:smaller">source</a>

### copilot_exchange

``` python
def copilot_exchange(
    oauth:NoneType=None, *, url:str='https://api.github.com/copilot_internal/v2/token', timeout:int=30
):
```

*Trade a GitHub OAuth token for a
[`CopilotToken`](https://vedicreader.github.io/rishi/copilot.html#copilottoken).*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L91"
target="_blank" style="float:right; font-size:smaller">source</a>

### CopilotToken

``` python
def CopilotToken(
    key:str, base_url:str='https://api.githubcopilot.com', expires_at:float=0.0
)->None:
```

*One minted Copilot token: the bearer, the endpoint it is good for, and
when it dies.*

``` python
# the catalogue carries more than ids: what each model is for, and how big its window is. An
# embedding model answering a chat request is a 400 the caller cannot read, so `kind` filters.
_payload = {'data': [
    {'id': 'gpt-5.5', 'capabilities': {'type': 'chat', 'limits': {'max_prompt_tokens': 128000}}},
    {'id': 'claude-opus-4.7', 'capabilities': {'type': 'chat', 'limits': {'max_context_window_tokens': 264000}}},
    {'id': 'gpt-41-copilot', 'capabilities': {'type': 'completion'}},
    {'id': 'text-embedding-3-small', 'capabilities': {'type': 'embeddings'}},
    {'no_id': 'skipped'}]}

class _ModelsResp:
    status_code = 200
    def raise_for_status(self): pass
    def json(self): return _payload

orig, httpx.get = httpx.get, lambda *a, **kw: _ModelsResp()
try:
    auth = CopilotAuth(key='tid=abc')                 # a key of its own, so nothing is exchanged
    cat = copilot_catalog(auth)
    test_eq(list(cat), ['gpt-5.5', 'claude-opus-4.7', 'gpt-41-copilot', 'text-embedding-3-small'])
    test_eq(copilot_models(auth), ['gpt-5.5', 'claude-opus-4.7'])          # chat only, by default
    test_eq(len(copilot_models(auth, kind=None)), 4)                       # everything it named
    test_eq(copilot_models(auth, kind='embeddings'), ['text-embedding-3-small'])
    test_eq(copilot_ctx(cat['gpt-5.5']), 128000)                           # max_prompt_tokens
    test_eq(copilot_ctx(cat['claude-opus-4.7']), 264000)                   # else the whole window
    test_eq(copilot_ctx(cat['gpt-41-copilot']), None)                      # it reports no limits
    test_eq(copilot_ctx(None), None)
finally: httpx.get = orig
```

``` python
now = time.time()
test_eq(CopilotToken('k').stale(), False)                     # untimed: never stale
test_eq(CopilotToken('k', expires_at=now+3600).stale(), False)
test_eq(CopilotToken('k', expires_at=now+60).stale(), True)    # inside the 5 minute skew
test_eq(CopilotToken('k', expires_at=now-1).stale(), True)

# a key you brought yourself is used as-is, and never exchanged
test_eq(CopilotAuth(key='tid=abc').token().key, 'tid=abc')
test_eq(CopilotAuth(key='k').token().base_url, COPILOT_API)
test_eq(CopilotAuth(key='k', base_url='https://api.business.githubcopilot.com').token().base_url,
        'https://api.business.githubcopilot.com')

# minting runs once, then not again until the token is nearly out of time
mints = []
def _fake_exchange(oauth=None, **kw):
    mints.append(oauth)
    return CopilotToken('tid=minted', 'https://api.individual.githubcopilot.com', time.time()+1800)

orig, copilot_exchange = copilot_exchange, _fake_exchange
try:
    a = CopilotAuth('gho_x')
    test_eq(a.token().key, 'tid=minted')
    test_eq(a.token().base_url, 'https://api.individual.githubcopilot.com')
    test_eq(len(mints), 1)                                     # the second call reused it
    a.tok = replace(a.tok, expires_at=time.time()+10)
    a.token()
    test_eq(len(mints), 2)                                     # nearly expired, so re-minted
finally: copilot_exchange = orig
```

``` python
# a personal access token gets a 404 out of the exchange, and that is a refusal like any other:
# what the caller needs told is that Copilot takes no PAT, not that some URL was not found
class _FakeResp:
    def __init__(self, code): self.status_code = code
    def raise_for_status(self): raise AssertionError('a refusal must not reach raise_for_status')

def _refused(code):
    orig, httpx.get = httpx.get, lambda *a, **kw: _FakeResp(code)
    try: copilot_exchange('gho_x')
    except PermissionError as e: return str(e)
    finally: httpx.get = orig
    assert False, f'{code} raised no PermissionError'

for code in (401, 403, 404): assert str(code) in _refused(code)
```

## Signing in

[`copilot_login()`](https://vedicreader.github.io/rishi/copilot.html#copilot_login)
runs GitHub’s device flow with the editor client id. It prints a code
and waits for browser authorization. It saves the token where
[`copilot_oauth()`](https://vedicreader.github.io/rishi/copilot.html#copilot_oauth)
looks first. An existing editor sign-in makes this step unnecessary.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L163"
target="_blank" style="float:right; font-size:smaller">source</a>

### copilot_login

``` python
def copilot_login(
    client_id:str='Iv1.b507a08c87ecfe98', *, scope:str='read:user', save:bool=True,
    printer:builtin_function_or_method=print, timeout:int=30
):
```

*Run GitHub’s device flow: print a code, wait while you enter it, return
the OAuth token.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L155"
target="_blank" style="float:right; font-size:smaller">source</a>

### save_oauth

``` python
def save_oauth(
    token, path:NoneType=None
):
```

*Write a GitHub OAuth token where
[`copilot_oauth`](https://vedicreader.github.io/rishi/copilot.html#copilot_oauth)
looks first, readable only by you.*

## CopilotChat

[`CopilotChat`](https://vedicreader.github.io/rishi/copilot.html#copilotchat)
is a
[`RemoteChat`](https://vedicreader.github.io/rishi/remote.html#remotechat)
whose `_kw` adds Copilot’s endpoint, a live token and the required
headers to each request. Tools, streaming, approval and the tool loop
are unchanged. Use `Chat('copilot/gpt-4.1')` or `runtime='copilot'`.
With no model named, the chat uses `copilot_default`, `gpt-4.1`.

Only the `copilot/` prefix routes here automatically. Bare `gpt-...` and
`claude-...` ids still go to the hosted APIs through `remote`.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/rishi/blob/main/rishi/copilot.py#L189"
target="_blank" style="float:right; font-size:smaller">source</a>

### CopilotChat

``` python
def CopilotChat(
    model:NoneType=None, *, oauth_token:NoneType=None, # a GitHub OAuth token, instead of the one on disk
    auth:NoneType=None, # a `CopilotAuth` to share, instead of building one
    integration_id:str='vscode-chat', hdrs:NoneType=None, # extra headers, merged over Copilot's own
    **kw
):
```

*A
[`RemoteChat`](https://vedicreader.github.io/rishi/remote.html#remotechat)
pointed at GitHub Copilot, adding Copilot’s headers and its short-lived
token to each request.*

``` python
# nothing here reaches the network: the chat is handed a token, and what is asserted is the request
c = CopilotChat('copilot/gpt-4.1', api_key='tid=abc', sp='Be terse.', messages=['hello'])
test_eq(c.runtime, 'copilot')
test_eq(c.model_id, 'gpt-4.1')          # the `copilot/` prefix is stripped, the id is not
test_eq(c.local, False)

kw = c._kw()
test_eq(kw['api_key'], 'tid=abc')
test_eq(kw['base_url'], COPILOT_API)
test_eq(kw['api_name'], 'openai_chat')  # fastllm has no Copilot vendor, so the api is named outright
test_eq(kw['vendor_name'], None)
test_eq(kw['system'], 'Be terse.')
test_eq(kw['xtra_hdrs']['copilot-integration-id'], 'vscode-chat')
test_eq(kw['xtra_hdrs']['x-initiator'], 'user')             # only a user message so far
assert 'copilot-vision-request' not in kw['xtra_hdrs']
assert 'Authorization' not in kw['xtra_hdrs']               # fastllm sets it, and once

# a tool loop is an agent to Copilot, and an image needs the vision header
c.hist += [{'role': 'assistant', 'content': '', 'tool_calls': [ToolCall('ls', {})]},
           {'role': 'tool', 'tool_call_id': 'x', 'name': 'ls', 'content': 'a.py'}]
test_eq(c._kw()['xtra_hdrs']['x-initiator'], 'agent')
c.hist.append({'role': 'user', 'content': [{'type': 'image_url', 'image_url': {'url': 'data:image/png;base64,aa'}}]})
test_eq(c._kw()['xtra_hdrs']['copilot-vision-request'], 'true')

# your headers win over rishi's, so an integration id or an editor version can be swapped out
test_eq(CopilotChat(api_key='k', integration_id='copilot-cli', hdrs={'editor-version': 'vim/9.1'}
                    )._kw()['xtra_hdrs']['editor-version'], 'vim/9.1')
test_eq(CopilotChat(api_key='k', integration_id='copilot-cli')._kw()['xtra_hdrs']['copilot-integration-id'],
        'copilot-cli')

# routing, without building anything
test_eq(resolve_runtime('copilot/claude-sonnet-4.5'), ('copilot', 'claude-sonnet-4.5'))
test_eq(resolve_runtime('claude-sonnet-4.5'), ('remote', 'claude-sonnet-4.5'))   # bare ids stay remote
test_eq(get_runtime('copilot').__name__, 'CopilotChat')   # the exported class, not this cell's
```

``` python
# and what fastllm builds from those arguments is the request Copilot actually wants
from fastllm.acomplete import api_registry, mk_client

kw = CopilotChat('copilot/gpt-4.1', api_key='tid=abc', sp='Be terse.', messages=['hi'])._kw()
cli, api_name, vendor = mk_client(model='gpt-4.1', vendor_name=kw['vendor_name'], api_name=kw['api_name'],
                                  api_key=kw['api_key'], base_url=kw['base_url'], xtra_hdrs=kw['xtra_hdrs'])
test_eq(cli.base_url + api_registry[api_name].endpoint, 'https://api.githubcopilot.com/chat/completions')

hdrs = dict(cli.transport.base_headers)
test_eq([k for k in hdrs if k.lower() == 'authorization'], ['Authorization'])   # one, not two
test_eq(hdrs['Authorization'], 'Bearer tid=abc')
test_eq(hdrs['copilot-integration-id'], 'vscode-chat')
test_eq(hdrs['editor-version'], EDITOR_VERSION)
```

## Against the real endpoint

These examples need a Copilot subscription, so they are marked
`eval: false` and the test run skips them.

``` python
from rishi.copilot import copilot_models, copilot_login

# copilot_login()          # only on a machine with no editor signed in
print(copilot_models()[:10])
```

``` python
chat = Chat('copilot/gpt-4.1', sp='You are concise.')
r = chat('Reply with exactly: pong')
assert 'pong' in resp_text(r).lower()
print(chat.use)
```

``` python
display_stream(Chat('copilot/claude-sonnet-4.5')('Write two sentences about the monsoon.', stream=True))
```

``` python
def word_count(text: str) -> int:
    "Count the words in `text`."
    return len(text.split())

chat = Chat('copilot/gpt-4.1', tools=[word_count])   # no approve=, so every call runs
print(resp_text(chat('How many words are in "the quick brown fox"? Use the tool.')))
```

``` python
# one exchange, several chats: hand them all the same auth
from rishi.copilot import CopilotAuth, CopilotChat

auth = CopilotAuth()
a, b = CopilotChat('gpt-4.1', auth=auth), CopilotChat('claude-sonnet-4.5', auth=auth)
print(auth)
```
