Skip to content

M365 Copilot ​

Talks to Microsoft 365 Copilot's chat service - a SignalR-over-WebSocket API, not a REST endpoint. kdeps runs a small local OpenAI-compatible server in front of it (m365 backend), so it plugs into the same chat: resource and agent-loop paths as any other provider. See LLM Provider Reference for the other backends.

yaml
# ~/.kdeps/config.yaml
llm:
  backend: m365

There is no api_key field - authentication is a signed-in Microsoft 365 account, not an API key.

Agent mode (recommended): sign in through a real browser window. The first time you pick an m365 model (kdeps --model m365-copilot --backend m365, or /model m365-copilot in the REPL) with no cached session yet, kdeps opens a visible Chrome window at the Microsoft login page and waits for you to complete sign-in yourself - password, MFA app, passkey, SSO tile, whatever your tenant requires. Nothing is read from or written to a credentials file for this path; kdeps never sees your password. Once you are in, the resulting session is cached at ~/.config/kdeps/m365/token-cache.json and refreshed silently from then on

  • every later launch goes straight to the model, no browser window. Run /login in the REPL any time to force a fresh sign-in (switch accounts, recover a revoked session).

The Chromium driver itself is installed automatically on first use if it is missing - no manual playwright install chromium step required. Override which browser launches with CHROMIUM_PATH, and the persistent browser profile location with KDEPS_M365_BROWSER_PROFILE.

Linux only: downloading Chromium is not enough - it also needs a handful of shared libraries the OS does not ship by default. On Debian/Ubuntu:

bash
sudo apt-get install -y libnss3 libnspr4 libasound2t64

(On older Debian/Ubuntu releases the package is libasound2 instead of libasound2t64.) If these are missing, kdeps' launch error names the exact package manager command for your distro - Playwright detects it directly from the host, so trust that command over this list if they differ.

Headless hosts (CI, servers with no display): scripted secrets.json fallback. Write ~/.config/kdeps/m365/secrets.json yourself before running kdeps:

json
// ~/.config/kdeps/m365/secrets.json
{
  "email": "you@yourtenant.com",
  "password": "your-password",
  "mfaSecret": "your-TOTP-seed"
}

mfaSecret is the TOTP seed (the same secret you would scan into an authenticator app), not a one-time code - kdeps generates codes from it itself, so your account needs authenticator-app MFA enrolled (not push/SMS-only). When this file is present, kdeps drives a headless Chromium browser through the Azure AD login form with those credentials instead of opening a visible window, caching the resulting refresh token the same way. Workflow mode's chat: resource has no terminal or display to prompt on, so it always requires this file to already exist.

Override paths with KDEPS_M365_CACHE_FILE, KDEPS_M365_SECRETS_FILE, and KDEPS_M365_BROWSER_PROFILE. Tool calling routes through an auto-provisioned Copilot Studio agent for most tones. Claude tones stay agent-less to preserve the tone (attaching an agent forces GPT-5), with one exception: Claude_Opus keeps the agent because it tool-calls reliably through it. gpt-6-think-deeper is agent-less because its agent route does not serve at all; the Sol tones (gpt-6-sol, gpt-6.1-sol) default agent-less because their agent route serves only on a premium account, which a token does not identify. Force either way with KDEPS_M365_FORCE_AGENT=1 (always attach) or KDEPS_M365_FORCE_AGENT=0 (never attach).

Each chat request also carries a scenario and licenseType query pair that decides which tones the service will serve. Entitlement-gated tones (Claude_Opus, claude-sonnet-5, claude-sonnet-5.5, gpt-6-think-deeper) are requested under the paid scenario (OfficeWebPaidCopilot, Premium); every other model uses the included scenario (OfficeWebIncludedCopilot, Starter). On the included scenario a gated tone answers with the canned BotConnection apology instead of a reply, so kdeps never sends one there. claude-opus-4.8 and claude-opus-4.5 are the exception inside the Opus family - they serve on the included scenario, so they stay there. Override either field with KDEPS_M365_SCENARIO and KDEPS_M365_LICENSE_TYPE.

M365 models (Claude tones especially) have a built-in code interpreter and will sometimes use it instead of the fenced kdeps tools - its sandbox is empty, so every result comes back blank and the model reports "NO CONTENT AVAILABLE" or claims it cannot read files. The system preamble tells the model to use the kdeps tools only; if it still slips, reply use the kdeps tools, not your own and it recovers.

The provisioned Copilot agent is told that kdeps is a parser and an interpreter: it parses one matched <invoke> block out of the reply at runtime and interprets it. Not the model's code interpreter. An <invoke> block inside a markdown fence is not a call, on a short reply or a long one. The tags have to sit outside the fence. Because tool calls are recovered from <invoke> blocks in free text, a written answer that quotes that syntax (or just names a tool, like "read the plan file, limit 100 lines") could be mis-read as an action. kdeps keeps such a reply as text when the recovered call is missing a required parameter, repeats a call already run this turn, or sits inside a long prose answer - so a final summary that mentions tool names renders in full instead of leaving a few dangling fragments before the prompt.

Reasoning-tone models (think-deeper, *-think-deeper) stream their chain-of-thought summary as live reasoning feedback, same as native extended-thinking models - visible in the agent-loop REPL automatically (thinking is on by default), or via reasoning_content in the raw OpenAI-compatible response for direct API use.

When the Copilot upstream fails mid-stream, the local server can only report it to a plain OpenAI-compatible client as a content delta: [m365 error: ...]. kdeps itself treats that marker as an upstream failure - a dropped connection is retried by the agent loop, and once retries are exhausted the error is reported instead of being saved as the assistant's answer.

Service throttling arrives the same way, as the plain-text apology "We're temporarily unable to respond to this volume of requests." kdeps treats that message as a rate-limit error, so the agent loop backs off and retries instead of saving the apology as the reply.

ModelDescription
m365-copilotDefault - service picks the model
quickFast, lower-latency responses (GPT-5.5 chat tone)
think-deeperExtended reasoning (GPT-5.5 reasoning tone)
claudeAlias for claude-sonnet
claude-sonnetClaude Sonnet via M365 (-4.5, -4.6 variants)
claude-sonnet-5Claude Sonnet 5 - paid scenario
claude-sonnet-5.5Claude Sonnet 5.5 - paid scenario
claude-sonnet-think-deeperSonnet with extended reasoning
claude-opusClaude Opus via M365 - paid scenario
claude-opus-4.5, claude-opus-4.8Opus on the included scenario
claude-opus-5, claude-opus-5.5Opus 5 family - paid scenario
gpt-6-think-deeperGPT-6 reasoning - agent-less, paid scenario
gpt-6-sol, gpt-6.1-solGPT-6 Sol reasoning - agent-less by default
gpt-5.6GPT-5.6, -quick/-think-deeper variants
gpt-5.5, gpt-5.4, gpt-5.3, gpt-5.2GPT-5.x family, -quick/-think-deeper variants

An unrecognized claude-* name keeps its model family instead of falling back to GPT: anything containing opus resolves to the Opus tone, other claude* names to Sonnet (or Sonnet 5 for a sonnet-5 name that is not 5.5).

Standalone proxy ​

kdeps m365 proxy runs the OpenAI-compatible server on a fixed port with CORS open, so any OpenAI client (Cursor, LiteLLM, curl, another kdeps host) can call it:

bash
kdeps m365 proxy               # http://127.0.0.1:11435/v1
kdeps m365 proxy --port 8080 --host 0.0.0.0

The first request opens a browser to sign in (or pre-seed ~/.config/kdeps/m365/secrets.json). Point the client at http://<host>:<port>/v1.

See also ​

Released under the Apache 2.0 License.