feat: copilot-m365-web tone-selected model variants (#7872) (#7997)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-22 00:42:47 -03:00
committed by GitHub
parent effddc6a0e
commit 91f4c35e9d
5 changed files with 97 additions and 1 deletions

View File

@@ -0,0 +1 @@
- feat(providers): copilot-m365-web tone-selected model variants (#7872)

View File

@@ -8,5 +8,14 @@ export const copilot_m365_webProvider: RegistryEntry = {
baseUrl: "wss://substrate.office.com/m365Copilot/Chathub",
authType: "apikey",
authHeader: "cookie",
models: [{ id: "copilot-m365", name: "Microsoft 365 Copilot (BizChat)", toolCalling: false }],
models: [
{ id: "copilot-m365", name: "Microsoft 365 Copilot (BizChat)", toolCalling: false },
{ id: "copilot-m365-claude-opus", name: "Microsoft 365 Copilot — Claude Opus", toolCalling: false },
{
id: "copilot-m365-gpt-5-6-reasoning",
name: "Microsoft 365 Copilot — GPT 5.6 Reasoning",
toolCalling: false,
},
{ id: "copilot-m365-gpt-5-5-chat", name: "Microsoft 365 Copilot — GPT 5.5 Chat", toolCalling: false },
],
};

View File

@@ -193,6 +193,29 @@ export function resolveChatInvocationOverrides(tier: string | undefined): {
};
}
/**
* BizChat exposes several models selected by the `tone` field of the `type:4` chat
* invocation (#7872, values confirmed against a real enterprise tenant in #7850). Each
* tone-selected variant is registered as its own model id; the bare `copilot-m365` id is
* intentionally absent here so it keeps the tier default tone (`Magic` on enterprise, `""`
* otherwise) resolved by {@link resolveChatInvocationOverrides}.
*/
export const M365_MODEL_TONE_MAP: Readonly<Record<string, string>> = {
"copilot-m365-claude-opus": "Claude_Opus",
"copilot-m365-gpt-5-6-reasoning": "Gpt_5_6_Reasoning",
"copilot-m365-gpt-5-5-chat": "Gpt_5_5_Chat",
};
/**
* Resolve the `tone` for a requested model id, or `undefined` when the id is the bare
* `copilot-m365` / unknown — callers then fall back to the tier default tone. Model-driven
* tone takes precedence over the tier default (see the executor wiring).
*/
export function resolveToneForModel(model: string | undefined): string | undefined {
if (!model) return undefined;
return M365_MODEL_TONE_MAP[model];
}
/**
* Build the `type:4` chat invocation frame body (not yet `\x1e`-terminated).
* Mirrors the argument shape captured on the individual M365 path in #4042.

View File

@@ -20,6 +20,7 @@ import {
keepaliveFrame,
parseFrame,
resolveChatInvocationOverrides,
resolveToneForModel,
splitFrames,
} from "./copilot-m365-frames.ts";
@@ -167,6 +168,9 @@ export class CopilotM365WebExecutor extends BaseExecutor {
const sendChat = () => {
ws?.send(keepaliveFrame());
const overrides = resolveChatInvocationOverrides(input.tier);
// Model-driven tone (#7872) wins over the tier default; a bare/unknown id
// keeps the tier tone resolved above.
const tone = resolveToneForModel(input.model) ?? overrides.tone;
ws?.send(
encodeFrame(
buildChatInvocation({
@@ -175,6 +179,7 @@ export class CopilotM365WebExecutor extends BaseExecutor {
sessionId,
isStartOfSession: true,
...overrides,
tone,
})
)
);

View File

@@ -0,0 +1,58 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
M365_MODEL_TONE_MAP,
resolveToneForModel,
resolveChatInvocationOverrides,
} from "../../open-sse/executors/copilot-m365-frames.ts";
import { copilot_m365_webProvider } from "../../open-sse/config/providers/registry/copilot-m365-web/index.ts";
// #7872 — tone-selected model variants for copilot-m365-web.
// The wiring (model id → tone → invocation payload) is unit-tested here; whether a tone
// actually selects that model upstream is a live enterprise-tenant check (release-drain).
test("resolveToneForModel maps each variant id to its confirmed tone", () => {
assert.equal(resolveToneForModel("copilot-m365-claude-opus"), "Claude_Opus");
assert.equal(resolveToneForModel("copilot-m365-gpt-5-6-reasoning"), "Gpt_5_6_Reasoning");
assert.equal(resolveToneForModel("copilot-m365-gpt-5-5-chat"), "Gpt_5_5_Chat");
});
test("resolveToneForModel returns undefined for the bare id and unknown ids", () => {
// bare id must fall back to the tier default, not a hard-coded tone
assert.equal(resolveToneForModel("copilot-m365"), undefined);
assert.equal(resolveToneForModel("totally-unknown"), undefined);
assert.equal(resolveToneForModel(undefined), undefined);
assert.equal(resolveToneForModel(""), undefined);
});
test("model-driven tone overrides the tier default; bare id keeps the tier tone", () => {
const enterprise = resolveChatInvocationOverrides("enterprise");
const individual = resolveChatInvocationOverrides(undefined);
// enterprise tier default tone is Magic
assert.equal(enterprise.tone, "Magic");
assert.equal(individual.tone, "");
// precedence: resolveToneForModel(model) ?? overrides.tone (mirrors the executor wiring)
const toneFor = (model: string | undefined, tierTone: string) =>
resolveToneForModel(model) ?? tierTone;
// a variant id wins over BOTH tier defaults
assert.equal(toneFor("copilot-m365-claude-opus", enterprise.tone), "Claude_Opus");
assert.equal(toneFor("copilot-m365-claude-opus", individual.tone), "Claude_Opus");
// the bare id keeps whatever the tier resolved
assert.equal(toneFor("copilot-m365", enterprise.tone), "Magic");
assert.equal(toneFor("copilot-m365", individual.tone), "");
});
test("registry exposes the bare id (first) plus every tone variant", () => {
const ids = copilot_m365_webProvider.models.map((m) => m.id);
assert.equal(ids[0], "copilot-m365", "bare Auto/default id must be first");
for (const variantId of Object.keys(M365_MODEL_TONE_MAP)) {
assert.ok(ids.includes(variantId), `registry missing variant ${variantId}`);
}
// no duplicate ids
assert.equal(ids.length, new Set(ids).size);
});