Compare commits

..

3 Commits

Author SHA1 Message Date
Markus Hartung
97fa38799e board #11072 2026-08-21 22:07:14 -03:00
Markus Hartung
b45aa9eb19 merge: reconcile command-code /provider/v1 migration with tip (resolve #10986 overlap)
The new compact executor already carries the #10986 reasoning-only content fallback,
so the old-executor conflict is resolved by taking the new executor version.
2026-08-21 21:02:12 -03:00
Markus Hartung
690f5739ad fix(command-code): use documented /provider/v1 chat endpoint (#10265) 2026-08-21 20:28:45 -03:00
20 changed files with 144 additions and 786 deletions

View File

@@ -1 +0,0 @@
- fix(install): make the ONNX dependency chain optional so Termux/Android installs succeed again (#11095)

View File

@@ -443,7 +443,7 @@
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1250
},
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1080,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1073,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1298,
@@ -459,8 +459,7 @@
"open-sse/executors/commandCode.ts": 1059,
"_rebaseline_2026_08_21_10859_vision_bridge_catalog": "#10859 own growth (Vision Bridge fixes #10808/#10809): src/lib/modelCapabilities.ts 1006->1016 (+10, cmd/gpt-5.3-codex* text-only capability resolution) and open-sse/executors/commandCode.ts 988->1023 (+35, Command Code wire-model normalization for bare ids + reasoning field fallback for opencode-routed gateways). Cohesive bug fixes at the existing capability-resolution / executor chokepoints; not extractable mid-fix. Covered by tests/unit/model-capabilities-command-code-codex-textonly-10703.test.ts, tests/unit/command-code-vision.test.ts, tests/unit/opencode-mimo-reasoning-details-nonstream.test.ts. Pushed directly to release (own-session miss: the original rebaseline was made in a throwaway validation worktree and never landed on the PR branch or the release before merge).",
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts.",
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming).",
"_rebaseline_2026_08_21_11069_m365_har_import": "#11069 own growth: AddApiKeyModal.tsx 1073->1080 (+7 = Import .har file button for the copilot-m365-web credential modal — M365 is the only provider whose credential (access_token+chathubPath) must be extracted from a DevTools HAR WebSocket URL, added as a new modal affordance). Cohesive UI at the existing modal chokepoint; not extractable. Covered by tests/unit/m365-har-import*.test.ts."
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming)."
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",

View File

@@ -30,45 +30,17 @@ export const HANDSHAKE_REQUEST = { protocol: "json", version: 1 } as const;
export const KEEPALIVE_PING = { type: 6 } as const;
/**
* Allowed message types observed in a 2026-08-21 live capture of a working
* `m365.cloud.microsoft/chat` session (issue: "Stream ended before producing a
* non-ping SSE event" on every individual/consumer M365 Copilot call). The
* #10718 6-entry shape above no longer produces a `type:1 target:"update"`
* frame at all — the socket only replies with SignalR keepalive pings and then
* closes, which is exactly what surfaces client-side as that generic stream
* error. 30 entries, up from 6.
* Allowed message types observed in the 2026-08 recapture of the working
* `m365.cloud.microsoft/chat` client (#10718). The old 11-entry list is no longer
* seen on the wire — the stale shape gets closed immediately after the type:4.
*/
export const ALLOWED_MESSAGE_TYPES = [
"Chat",
"Suggestion",
"InternalSearchQuery",
"Disengaged",
"InternalLoaderMessage",
"Progress",
"GeneratedCode",
"RenderCardRequest",
"AdsQuery",
"SemanticSerp",
"GenerateContentQuery",
"GenerateGraphicArt",
"SearchQuery",
"ConfirmationCard",
"AuthError",
"DeveloperLogs",
"TriggerPlugin",
"HintInvocation",
"MemoryUpdate",
"EndOfRequest",
"TriggerConfirmation",
"ResumeInvokeAction",
"ResumeUserInputRequest",
"TriggerUserInputRequest",
"EscapeHatch",
"TriggerPluginAuth",
"ResumePluginAuth",
"SideBySide",
"ReferencesListComplete",
"SwitchRespondingEndpoint",
"InternalLoaderMessage",
] as const;
/**
@@ -106,26 +78,19 @@ export const M365_ENTERPRISE_EXTRA_MESSAGE_TYPES = [
] as const;
/**
* Individual / EDU option sets from a 2026-08-21 live capture34 entries, up
* from the #10718 14-entry shape (which itself superseded an earlier 25-entry
* shape). Each recapture so far has been additive/reshuffled rather than a
* wholesale replacement — treat this as the protocol continuing to drift, not
* a one-time fix; a future capture may again need to update this list.
* Individual / EDU option sets from the 2026-08 recapture (#10718)14 entries.
* The previous 25-entry consumer/MSA set (enable_msa_user, pdnascan, cwc_code_*,
* …) is no longer observed on the wire and belongs to the shape the substrate
* now drops silently.
*/
export const M365_DEFAULT_OPTION_SETS = [
"search_result_progress_messages_with_search_queries",
"update_textdoc_response_after_streaming",
"deepleo_networking_timeout_10minutes_canmore",
"cwc_flux_image",
"cwc_code_interpreter",
"cwc_code_interpreter_amsfix",
"cwcfluxgptv",
"flux_v3_gptv_enable_upload_multi_image_in_turn_wo_ch",
"gptvnorm2048",
"cwc_code_interpreter_citation_fix",
"code_interpreter_interactive_charts",
"cwc_code_interpreter_interactive_charts_inline_image",
"code_interpreter_matplotlib_patching",
"cwc_fileupload_odb",
"update_memory_plugin",
"add_custom_instructions",
@@ -133,20 +98,6 @@ export const M365_DEFAULT_OPTION_SETS = [
"flux_v3_progress_messages",
"enable_batch_token_processing",
"enable_gg_gpt",
"async_client_interaction",
"flux_v3_references",
"flux_v3_references_entities",
"flux_v3_references_ci",
"add_filestore_filetype",
"cwc_code_interpreter_citation_sourceannotations",
"cdxcwc_code_interpreter_hallucinated_url_filter",
"flux_v3_image_gen_enable_dimensions",
"flux_v3_image_gen_enable_non_watermarked_storage",
"flux_v3_image_gen_enable_icon_dimensions",
"flux_v3_image_gen_enable_system_text_with_params",
"flux_v3_image_gen_enable_designer_dimensions_meta_prompting_in_system_prompts",
"flux_v3_image_gen_enable_story",
"rich_responses",
] as const;
/** Append the record separator to a JSON-serializable frame. */
@@ -482,14 +433,12 @@ export function resolveChatInvocationOverrides(tier: string | undefined): {
}
return {
optionsSets: [...M365_DEFAULT_OPTION_SETS],
// 2026-08-21 capture — the individual/consumer surface now sends "Magic"
// (capitalized), matching the enterprise tone literal. The #10718
// lowercase "magic" is part of the shape that gets silently dropped.
tone: "Magic",
// #10718 — the 2026-08 recapture sends tone:"magic" (lowercase) on the
// individual/EDU surface; the old "" default is part of the dropped shape.
tone: "magic",
allowedMessageTypes: ALLOWED_MESSAGE_TYPES,
// 2026-08-21 capture — disconnectBehavior:"continue" is now present on the
// individual/consumer wire too, not just enterprise (see ChatInvocationOptions).
disconnectBehavior: "continue",
// Omitted entirely on the individual/EDU wire (see ChatInvocationOptions).
disconnectBehavior: undefined,
};
}
@@ -518,33 +467,16 @@ export function resolveToneForModel(model: string | undefined): string | undefin
/**
* Build the `type:4` chat invocation frame body (not yet `\x1e`-terminated).
* Base shape from the #10718 recapture (populated `clientInfo` +
* `productThreadType:"Office"`, a `conversationId` matching the WS URL query, a
* rich `message` object), extended per a 2026-08-21 live capture that found the
* #10718 shape alone no longer produces a `type:1 target:"update"` frame — the
* socket only replies with keepalive pings and closes. The additions below
* (richer `clientInfo`, non-empty `plugins`, `extraExtensionParameters`,
* `isSbsSupported`, `renderReferencesBehindEOS`,
* `message.connectedFederatedConnections`, and `disconnectBehavior` on every
* tier) are exactly the fields the 2026-08-21 capture had that this shape was
* missing; the #10718 fields (`conversationId`, `productThreadType`,
* `toolChoice`, `message.attachments`) are kept as-is since removing them was
* not verified against a live socket.
* Mirrors the argument shape recaptured from a working `m365.cloud.microsoft/chat`
* client in 2026-08 (#10718). Notable differences from the pre-#10718 shape: a
* populated `clientInfo` + `productThreadType:"Office"`, a `conversationId`
* matching the WS URL query, a rich `message` object, and no
* `spokenTextMode` / `extraExtensionParameters` / `isSbsSupported` /
* `renderReferencesBehindEOS` / `disconnectBehavior` — none of those are still
* observed on the wire, and the stale shape gets closed immediately after the
* invocation.
*/
export function buildChatInvocation(opts: ChatInvocationOptions): Record<string, unknown> {
const clientInfo = {
clientAppName: "Office",
clientPlatform: "mcmcopilot-web",
clientEntrypoint: "mcmcopilot-officeweb",
clientSessionId: opts.sessionId,
ProductCategory: "Chat",
clientAppType: "Web",
productEntryPoint: "ChatPanel",
deviceOS: "Windows",
deviceType: "Desktop",
clientPlatformVersion: "10",
};
return {
type: 4,
target: "chat",
@@ -555,17 +487,17 @@ export function buildChatInvocation(opts: ChatInvocationOptions): Record<string,
? [...opts.allowedMessageTypes]
: [...ALLOWED_MESSAGE_TYPES],
clientCorrelationId: opts.clientCorrelationId ?? opts.traceId,
clientInfo,
clientInfo: {
clientAppName: "Office",
clientPlatform: "mcmcopilot-web",
},
conversationId: opts.conversationId,
extraExtensionParameters: {},
isStartOfSession: opts.isStartOfSession ?? true,
message: {
adaptiveCards: [],
attachments: null,
author: "user",
clientInfo,
clientPreferences: {},
connectedFederatedConnections: ["dummyId"],
entityAnnotationTypes: ["People", "File", "Event", "Email", "TeamsMessage"],
experienceType: "Default",
inputMethod: "Keyboard",
@@ -578,27 +510,22 @@ export function buildChatInvocation(opts: ChatInvocationOptions): Record<string,
requestId: opts.requestId,
text: opts.text,
},
isSbsSupported: true,
options: {},
optionsSets: opts.optionsSets ?? [...M365_DEFAULT_OPTION_SETS],
// 2026-08-21 capture (#11069): BingWebSearch is now the universal
// BuiltIn plugin on individual/consumer tier; keep an opt-out override.
plugins: opts.plugins ?? [{ Id: "BingWebSearch", Source: "BuiltIn" }],
plugins: opts.plugins ?? [],
...(opts.customInstructions ? { customInstructions: opts.customInstructions } : {}),
productThreadType: "Office",
renderReferencesBehindEOS: true,
sessionId: opts.sessionId,
sliceIds: [],
source: "officeweb",
streamingMode: "ConciseWithPadding",
threadLevelGptId: {},
// 2026-08-21 capture (#11069): tone is now capitalized "Magic" on both tiers.
tone: opts.tone ?? "Magic",
tone: opts.tone ?? "magic",
toolChoice: opts.toolChoice ?? null,
traceId: opts.traceId,
// 2026-08-21 capture — disconnectBehavior:"continue" is sent on every
// tier now, not gated to enterprise as the #8971 comment described.
disconnectBehavior: opts.disconnectBehavior ?? "continue",
// #8971 keeps "continue" for the enterprise tier; the individual/EDU wire
// omits the key, so only include it when actually set (#10718).
...(opts.disconnectBehavior ? { disconnectBehavior: opts.disconnectBehavior } : {}),
},
],
};

72
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@huggingface/transformers": "^4.2.0",
"@lobehub/icons": "^5.16.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@monaco-editor/react": "^4.7.0",
@@ -60,6 +61,7 @@
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
"omniglyph": "^1.4.0",
"onnxruntime-node": "1.24.3",
"open": "^11.0.1",
"ora": "^9.4.1",
"parse5": "^8.0.1",
@@ -154,11 +156,9 @@
},
"optionalDependencies": {
"@atjsh/llmlingua-2": "3.0.0",
"@huggingface/transformers": "^4.2.0",
"better-sqlite3": "^13.0.2",
"js-tiktoken": "^1.0.20",
"keytar": "^7.9.0",
"onnxruntime-node": "1.24.3",
"sqlite-vec": "^0.1.9",
"tls-client-node": "^0.2.0",
"wreq-js": "^3.0.0"
@@ -4510,7 +4510,6 @@
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz",
"integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
}
@@ -4519,15 +4518,13 @@
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz",
"integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==",
"license": "Apache-2.0",
"optional": true
"license": "Apache-2.0"
},
"node_modules/@huggingface/transformers": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.2.0.tgz",
"integrity": "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@huggingface/jinja": "^0.5.6",
"@huggingface/tokenizers": "^0.1.3",
@@ -9486,35 +9483,30 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
"devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1"
@@ -9524,28 +9516,24 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@radix-ui/number": {
@@ -12749,7 +12737,6 @@
"version": "26.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
@@ -14011,7 +13998,6 @@
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
"integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14.0"
}
@@ -14985,8 +14971,7 @@
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/bottleneck": {
"version": "2.19.5",
@@ -17950,7 +17935,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@@ -17980,7 +17964,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
@@ -18081,8 +18064,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/detect-node-es": {
"version": "1.1.0",
@@ -18926,8 +18908,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/es6-promisify": {
"version": "7.0.0",
@@ -20419,8 +20400,7 @@
"version": "25.9.23",
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
"integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
"license": "Apache-2.0",
"optional": true
"license": "Apache-2.0"
},
"node_modules/flatted": {
"version": "3.4.2",
@@ -21246,7 +21226,6 @@
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"es6-error": "^4.1.1",
@@ -21264,7 +21243,6 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -21313,7 +21291,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"define-properties": "^1.2.1",
@@ -21668,8 +21645,7 @@
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
"license": "ISC",
"optional": true
"license": "ISC"
},
"node_modules/hachure-fill": {
"version": "0.5.2",
@@ -21703,7 +21679,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@@ -24815,8 +24790,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC",
"optional": true
"license": "ISC"
},
"node_modules/json5": {
"version": "2.2.3",
@@ -26680,7 +26654,6 @@
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"license": "MIT",
"optional": true,
"dependencies": {
"escape-string-regexp": "^4.0.0"
},
@@ -29452,7 +29425,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -29660,8 +29632,7 @@
"version": "1.24.3",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz",
"integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/onnxruntime-node": {
"version": "1.24.3",
@@ -29669,7 +29640,6 @@
"integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"win32",
"darwin",
@@ -29686,7 +29656,6 @@
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0-dev.20260416-b7804b056c.tgz",
"integrity": "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==",
"license": "MIT",
"optional": true,
"dependencies": {
"flatbuffers": "^25.1.24",
"guid-typescript": "^1.0.9",
@@ -29700,15 +29669,13 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0",
"optional": true
"license": "Apache-2.0"
},
"node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
"version": "1.24.0-dev.20251116-b39e144322",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz",
"integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/open": {
"version": "11.0.1",
@@ -30939,8 +30906,7 @@
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/playwright": {
"version": "1.62.1",
@@ -31895,7 +31861,6 @@
"version": "7.6.5",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
"integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
"devOptional": true,
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -31919,7 +31884,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/proxy-addr": {
@@ -33316,7 +33280,6 @@
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"detect-node": "^2.0.4",
@@ -33692,8 +33655,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/send": {
"version": "1.2.1",
@@ -33726,7 +33688,6 @@
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"license": "MIT",
"optional": true,
"dependencies": {
"type-fest": "^0.13.1"
},
@@ -33742,7 +33703,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"license": "(MIT OR CC0-1.0)",
"optional": true,
"engines": {
"node": ">=10"
},
@@ -34514,8 +34474,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause",
"optional": true
"license": "BSD-3-Clause"
},
"node_modules/sql.js": {
"version": "1.14.2",
@@ -36228,7 +36187,6 @@
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/unicode-emoji-modifier-base": {

View File

@@ -265,6 +265,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@huggingface/transformers": "^4.2.0",
"@lobehub/icons": "^5.16.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@monaco-editor/react": "^4.7.0",
@@ -307,6 +308,7 @@
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
"omniglyph": "^1.4.0",
"onnxruntime-node": "1.24.3",
"open": "^11.0.1",
"ora": "^9.4.1",
"parse5": "^8.0.1",
@@ -341,11 +343,9 @@
},
"optionalDependencies": {
"@atjsh/llmlingua-2": "3.0.0",
"@huggingface/transformers": "^4.2.0",
"better-sqlite3": "^13.0.2",
"js-tiktoken": "^1.0.20",
"keytar": "^7.9.0",
"onnxruntime-node": "1.24.3",
"sqlite-vec": "^0.1.9",
"tls-client-node": "^0.2.0",
"wreq-js": "^3.0.0"

View File

@@ -1,12 +1,12 @@
---
name: omni-webhooks
description: Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries.
description: Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries.
---
<!-- generated by src/lib/agentSkills/generator.ts; manual edits will be overwritten -->
## Overview
Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries.
Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries.
## Authentication

View File

@@ -12,13 +12,8 @@ import {
} from "@/lib/combos/intelligentRouting";
import { AI_PROVIDERS } from "@/shared/constants/providers";
function getI18nOrFallback(
t: any,
key: string,
fallback: string,
values?: Record<string, unknown>
) {
if (typeof t?.has === "function" && t.has(key)) return t(key, values);
function getI18nOrFallback(t: any, key: string, fallback: string) {
if (typeof t?.has === "function" && t.has(key)) return t(key);
return fallback;
}
@@ -99,9 +94,10 @@ export default function IntelligentComboPanel({
const updatedCombo = await response.json();
onComboUpdated?.(updatedCombo);
notify.success(
getI18nOrFallback(t, "modePackUpdated", "Mode pack updated to {pack}.", {
pack: modePackId,
}).replace("{pack}", modePackId)
getI18nOrFallback(t, "modePackUpdated", "Mode pack updated to {pack}.").replace(
"{pack}",
modePackId
)
);
} catch (error: any) {
notify.error(error?.message || "Failed to update mode pack.");
@@ -188,9 +184,10 @@ export default function IntelligentComboPanel({
</div>
{savingModePack && (
<span className="text-[11px] text-text-muted">
{getI18nOrFallback(t, "savingModePack", "Saving {pack}…", {
pack: savingModePack,
}).replace("{pack}", savingModePack)}
{getI18nOrFallback(t, "savingModePack", "Saving {pack}…").replace(
"{pack}",
savingModePack
)}
</span>
)}
</div>

View File

@@ -533,9 +533,9 @@ function getStrategyBadgeClass(strategy) {
return "bg-blue-500/15 text-blue-600 dark:text-blue-400";
}
function getI18nOrFallback(t, key, fallback, values) {
function getI18nOrFallback(t, key, fallback) {
try {
if (typeof t.has === "function" && t.has(key)) return t(key, values);
if (typeof t.has === "function" && t.has(key)) return t(key);
} catch {}
return fallback;
}
@@ -1565,8 +1565,7 @@ function StrategyRecommendationsPanel({ strategy, onApply, showNudge }) {
{getI18nOrFallback(
t,
"recommendationsUpdated",
"Recommendations updated for {strategy}.",
{ strategy: strategyLabel }
"Recommendations updated for {strategy}."
).replace("{strategy}", strategyLabel)}
</div>
)}

View File

@@ -1,165 +0,0 @@
"use client";
import { useRef, useState } from "react";
import { useTranslations } from "next-intl";
import {
extractM365CredentialFromHar,
describeHarImportExpiry,
type M365HarImportResult,
} from "@/shared/utils/m365HarImport";
import { providerText, type ProviderMessageTranslator } from "../providerPageHelpers";
type HarImporter = (text: string) => M365HarImportResult;
// One entry per web-session provider that can offer HAR import. Add a new
// key here (and its own extractor in src/shared/utils/) to support another
// provider — the button renders nothing for any provider not listed.
const HAR_IMPORTERS: Record<string, HarImporter> = {
"copilot-m365-web": extractM365CredentialFromHar,
};
const ERROR_MESSAGE_KEYS: Record<string, [string, string]> = {
notJson: ["harImportErrorNotJson", "That file isn't valid JSON — is it really a .har export?"],
noEntries: ["harImportErrorNoEntries", "This HAR has no network entries recorded."],
noChathubUrl: [
"harImportErrorNoChathubUrl",
"No Copilot chat connection found in this HAR. Send at least one chat message in m365.cloud.microsoft before exporting.",
],
unparsableUrl: [
"harImportErrorUnparsableUrl",
"Found the chat connection, but couldn't read its URL.",
],
missingFields: [
"harImportErrorMissingFields",
"Found the chat connection, but the token was missing from it.",
],
};
export interface HarImportButtonProps {
provider: string;
onImport: (apiKey: string) => void;
}
export default function HarImportButton({ provider, onImport }: HarImportButtonProps) {
const t = useTranslations("providers") as ProviderMessageTranslator;
const importer = HAR_IMPORTERS[provider];
const fileInputRef = useRef<HTMLInputElement>(null);
const [state, setState] = useState<
| { phase: "idle" }
| { phase: "reading" }
| { phase: "error"; message: string }
| { phase: "success"; expiresAt: number | null }
>({ phase: "idle" });
if (!importer) return null;
async function handleFile(file: File | undefined) {
if (!file) return;
setState({ phase: "reading" });
let text: string;
try {
text = await file.text();
} catch {
setState({
phase: "error",
message: providerText(t, "harImportErrorReadFailed", "Couldn't read that file."),
});
return;
}
const result = importer(text);
if (!result.ok) {
const [key, fallback] = ERROR_MESSAGE_KEYS[result.error] ?? [
"harImportErrorUnknown",
"Couldn't extract a credential from that HAR file.",
];
setState({ phase: "error", message: providerText(t, key, fallback) });
return;
}
onImport(result.apiKey);
setState({ phase: "success", expiresAt: result.expiresAt });
}
const expiry = state.phase === "success" ? describeHarImportExpiry(state.expiresAt) : null;
const expiryText =
expiry?.tone === "unknown"
? providerText(t, "harImportStatusUnknownExpiry", "Imported. Couldn't read its expiry.")
: expiry?.tone === "bad"
? providerText(
t,
"harImportStatusExpired",
"Imported, but this token already expired ({minutes}m ago) — export a fresh HAR.",
{ minutes: Math.abs(expiry.minutesRemaining ?? 0) }
)
: expiry?.tone === "warn"
? providerText(
t,
"harImportStatusExpiringSoon",
"Imported — valid for only ~{minutes}m more.",
{ minutes: expiry.minutesRemaining ?? 0 }
)
: expiry?.tone === "ok"
? providerText(t, "harImportStatusValid", "Imported — valid for ~{minutes}m.", {
minutes: expiry.minutesRemaining ?? 0,
})
: null;
return (
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={state.phase === "reading"}
data-testid="har-import-button"
className="inline-flex items-center gap-1.5 rounded border border-border px-2.5 py-1.5 text-xs font-medium text-text-main hover:bg-surface-hover disabled:opacity-50"
>
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
upload_file
</span>
{state.phase === "reading"
? providerText(t, "harImportButtonBusy", "Importing…")
: providerText(t, "harImportButtonLabel", "Import .har file")}
</button>
<span className="text-xs text-text-muted">
{providerText(
t,
"harImportButtonHint",
"Export from DevTools Network tab after sending at least one chat message."
)}
</span>
<input
ref={fileInputRef}
type="file"
accept=".har,application/json"
data-testid="har-import-input"
className="hidden"
onChange={(event) => {
void handleFile(event.target.files?.[0]);
event.target.value = "";
}}
/>
</div>
{state.phase === "error" && (
<p className="text-xs text-red-600 dark:text-red-400" data-testid="har-import-error">
{state.message}
</p>
)}
{state.phase === "success" && expiryText && (
<p
className={
expiry?.tone === "bad"
? "text-xs text-red-600 dark:text-red-400"
: expiry?.tone === "warn"
? "text-xs text-amber-700 dark:text-amber-300"
: "text-xs text-emerald-700 dark:text-emerald-300"
}
data-testid="har-import-status"
>
{expiryText}
</p>
)}
</div>
);
}

View File

@@ -31,7 +31,6 @@ import {
import { getWebSessionCredentialRequirement } from "../../webSessionCredentials";
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import HarImportButton from "../HarImportButton";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
import { buildAddProviderSpecificData } from "./connectionProviderSpecificData";
import { getCommandCodeAuthPhaseLabel } from "./commandCodeAuthPhase";
@@ -210,13 +209,13 @@ export default function AddApiKeyModal({
? "Freebuff uses an authentic CLI auth token obtained via codebuff CLI login or automated harvester."
: isWebSessionCredential
? getWebSessionCredentialHint(t, webSessionCredential, providerDisplayName, false)
: isLocalSelfHostedProvider
? t("localProviderApiKeyOptionalHint", {
provider: localProviderMetadata?.name || providerName || provider || "",
})
: apiKeyOptional
? t("apiKeyOptionalHint")
: undefined;
: isLocalSelfHostedProvider
? t("localProviderApiKeyOptionalHint", {
provider: localProviderMetadata?.name || providerName || provider || "",
})
: apiKeyOptional
? t("apiKeyOptionalHint")
: undefined;
const credentialValidationFailedMessage = isWebSessionCredential
? providerText(
t,
@@ -751,12 +750,6 @@ export default function AddApiKeyModal({
t={t}
/>
)}
{provider && (
<HarImportButton
provider={provider}
onImport={(apiKey) => setFormData({ ...formData, apiKey })}
/>
)}
{!isNoAuthWebSessionCredential && (
<div className="flex gap-2">
<Input

View File

@@ -49,7 +49,6 @@ import {
import { getWebSessionCredentialRequirement } from "../../webSessionCredentials";
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import HarImportButton from "../HarImportButton";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
import { CodexConnectionFields } from "./CodexFingerprintFields";
import { assignEditApiKeyProviderSpecificData } from "./connectionProviderSpecificData";
@@ -910,12 +909,6 @@ export default function EditConnectionModal({
t={t}
/>
)}
{provider && (
<HarImportButton
provider={provider}
onImport={(apiKey) => setFormData({ ...formData, apiKey })}
/>
)}
{!isNoAuthWebSessionCredential && (
<div className="flex gap-2">
<Input

View File

@@ -6269,20 +6269,6 @@
"webSessionGuideStep3": "Copy the required credential from the provider's own domain. For cookies, copy only the Cookie header value and omit Cookie:.",
"webSessionGuideStep3Manual": "Manual path: open the browser developer tools (F12 → Network), refresh the page, open an authenticated request, and copy the Cookie header value from Request Headers — omit the Cookie: prefix.",
"webSessionGuideStep4": "Paste it here and check the connection. If it stops working, sign in again and replace it with a fresh value.",
"harImportButtonLabel": "Import .har file",
"harImportButtonBusy": "Importing…",
"harImportButtonHint": "Export from DevTools Network tab after sending at least one chat message.",
"harImportStatusValid": "Imported — valid for ~{minutes}m.",
"harImportStatusExpiringSoon": "Imported — valid for only ~{minutes}m more.",
"harImportStatusExpired": "Imported, but this token already expired ({minutes}m ago) — export a fresh HAR.",
"harImportStatusUnknownExpiry": "Imported. Couldn't read its expiry.",
"harImportErrorNotJson": "That file isn't valid JSON — is it really a .har export?",
"harImportErrorNoEntries": "This HAR has no network entries recorded.",
"harImportErrorNoChathubUrl": "No Copilot chat connection found in this HAR. Send at least one chat message in m365.cloud.microsoft before exporting.",
"harImportErrorUnparsableUrl": "Found the chat connection, but couldn't read its URL.",
"harImportErrorMissingFields": "Found the chat connection, but the token was missing from it.",
"harImportErrorReadFailed": "Couldn't read that file.",
"harImportErrorUnknown": "Couldn't extract a credential from that HAR file.",
"webSessionSecurityHint": "Treat this like a password: it may access your signed-in web account until it expires or is revoked.",
"webNoAuthGuideTitle": "No credential required",
"webNoAuthGuideBody": "{provider} does not need an API key or cookie. Save the connection to use its free web endpoint.",

View File

@@ -1,116 +0,0 @@
/**
* Pure, transport-free helpers that extract a `copilot-m365-web` credential
* (`access_token=...; chathubPath=...`) directly from a DevTools HAR export,
* so the "Add connection" / "Edit connection" dialogs can offer a one-click
* "Import .har file" button instead of the user hand-extracting the token
* from the WebSocket URL (see `HarImportButton.tsx`).
*
* The token rides in the query string of the BizChat ChatHub WebSocket URL
* (`wss://substrate.office.com/m365Copilot/Chathub/<oid>@<tenant>?...
* access_token=...`) — the same URL the executor itself connects to (see
* `open-sse/executors/copilot-m365-connection.ts`). No network calls here;
* everything operates on the HAR text already in the browser.
*/
/** Prefix identifying the BizChat ChatHub WebSocket request in a HAR entry. */
export const M365_CHATHUB_WS_PREFIX = "wss://substrate.office.com/m365Copilot/Chathub/";
export type M365HarImportResult =
| { ok: true; apiKey: string; chathubPath: string; expiresAt: number | null }
| { ok: false; error: string };
interface HarEntryLike {
request?: { url?: unknown };
}
interface HarLike {
log?: { entries?: unknown };
}
/** Decode a JWT payload WITHOUT verification — exp is a display hint only. */
function decodeJwtExpiry(token: string): number | null {
const parts = token.split(".");
if (parts.length !== 3) return null;
try {
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
const json = JSON.parse(atob(padded)) as { exp?: unknown };
return typeof json.exp === "number" ? json.exp * 1000 : null;
} catch {
return null;
}
}
/**
* Extract the `copilot-m365-web` credential from raw HAR file text.
*
* Scans every request in `log.entries` for the ChatHub WebSocket URL and
* uses the LAST match (a HAR may contain several turns of the same session;
* the most recent one carries the freshest token). Returns a structured
* error — never throws — so callers can render it directly.
*/
export function extractM365CredentialFromHar(text: string): M365HarImportResult {
let har: HarLike;
try {
har = JSON.parse(text) as HarLike;
} catch {
return { ok: false, error: "notJson" };
}
const entries = har.log?.entries;
if (!Array.isArray(entries)) {
return { ok: false, error: "noEntries" };
}
let matchedUrl: string | null = null;
for (const entry of entries as HarEntryLike[]) {
const url = entry?.request?.url;
if (typeof url === "string" && url.startsWith(M365_CHATHUB_WS_PREFIX)) {
matchedUrl = url;
}
}
if (!matchedUrl) {
return { ok: false, error: "noChathubUrl" };
}
let parsed: URL;
try {
parsed = new URL(matchedUrl);
} catch {
return { ok: false, error: "unparsableUrl" };
}
const token = parsed.searchParams.get("access_token");
const chathubPath = decodeURIComponent(parsed.pathname.split("/m365Copilot/Chathub/")[1] || "");
if (!token || !chathubPath) {
return { ok: false, error: "missingFields" };
}
return {
ok: true,
apiKey: `access_token=${token}; chathubPath=${chathubPath}`,
chathubPath,
expiresAt: decodeJwtExpiry(token),
};
}
export type HarImportExpiryTone = "ok" | "warn" | "bad" | "unknown";
export interface HarImportExpiryStatus {
tone: HarImportExpiryTone;
minutesRemaining: number | null;
}
/** Classify a decoded token expiry for the inline status hint. */
export function describeHarImportExpiry(
expiresAt: number | null,
now: number = Date.now()
): HarImportExpiryStatus {
if (expiresAt === null) return { tone: "unknown", minutesRemaining: null };
const minutesRemaining = Math.round((expiresAt - now) / 60000);
if (minutesRemaining <= 0) return { tone: "bad", minutesRemaining };
if (minutesRemaining < 15) return { tone: "warn", minutesRemaining };
return { tone: "ok", minutesRemaining };
}

View File

@@ -9,52 +9,40 @@ function readJson<T = Record<string, unknown>>(relPath: string): T {
return JSON.parse(readFileSync(join(repoRoot, relPath), "utf8")) as T;
}
test("ONNX chain (@huggingface/transformers + onnxruntime-node) stays optional so Termux/Android installs succeed", () => {
// #11095: onnxruntime-node declares os ["win32","darwin","linux"], so while
// these lived in `dependencies` every npm install on Android/Termux aborted
// with a fatal EBADPLATFORM. As optionalDependencies npm skips only the
// unsupported-platform subtree (with a warning) and installs normally
// everywhere else. This deliberately reverses the MECHANISM of #9962 while
// keeping its goal: #9962's skip happened because the old onnxruntime-node@
// 1.21.0 pin built from source (NAN) and failed to compile on Node 24/26;
// the current 1.24.3 pin ships napi prebuilds, so on supported platforms the
// chain always installs and `npm ci`/`next build` keep resolving it. On
// platforms where it IS skipped, both consumers degrade gracefully via lazy/
// dynamic imports (asserted below).
test("@huggingface/transformers is a regular dependency so npm ci never skips it", () => {
// #9962 deliberately moved @huggingface/transformers out of optionalDependencies:
// as an optional dep, npm silently skipped the whole subtree on Node 24/26 (old
// pin dragged onnxruntime-node@1.21.0 whose NAN build no longer compiles), which
// broke `npm ci`/`next build` with "Can't resolve @huggingface/transformers"
// (lazy import in src/lib/memory/embedding/transformersLocal.ts). As a regular
// dep with onnxruntime-node@~1.24.3 (napi prebuilds, no node-gyp) it stays
// installable and the memory embedding path requires() cleanly.
const pkg = readJson<{
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
overrides?: Record<string, string>;
}>("package.json");
assert.equal(
pkg.dependencies?.["@huggingface/transformers"],
undefined,
"transformers must NOT be a hard dependency (fatal EBADPLATFORM on Android)"
);
assert.equal(
pkg.optionalDependencies?.["@huggingface/transformers"],
"^4.2.0",
"transformers must be an optionalDependency"
);
assert.equal(
pkg.dependencies?.["onnxruntime-node"],
undefined,
"onnxruntime-node must NOT be a hard dependency (fatal EBADPLATFORM on Android)"
);
assert.equal(
pkg.optionalDependencies?.["onnxruntime-node"],
"1.24.3",
"onnxruntime-node must be an optionalDependency pinned in lockstep with the overrides pin"
);
assert.equal(
pkg.overrides?.["onnxruntime-node"],
"1.24.3",
"the overrides pin must stay aligned with @huggingface/transformers' own pin (single-copy invariant)"
"transformers must be a regular dependency (never optional) so npm ci cannot skip it"
);
assert.equal(pkg.optionalDependencies?.["@huggingface/transformers"], undefined);
});
test("lockfile marks the whole ONNX chain optional", () => {
test("transformers + onnxruntime-node are regular dependencies (not optional)", () => {
const pkg = readJson<{
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
}>("package.json");
assert.equal(
pkg.dependencies?.["onnxruntime-node"],
"1.24.3",
"onnxruntime-node is a regular dep (napi prebuilds, installable on Node 24/26)"
);
assert.equal(pkg.optionalDependencies?.["onnxruntime-node"], undefined);
const lock = readJson<{
packages: Record<
string,
@@ -63,61 +51,26 @@ test("lockfile marks the whole ONNX chain optional", () => {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
}
>;
>;
}>("package-lock.json");
assert.equal(
lock.packages[""]?.optionalDependencies?.["@huggingface/transformers"],
lock.packages[""]?.dependencies?.["@huggingface/transformers"],
"^4.2.0",
"root lock optionalDependencies must hold transformers"
"root lock dependencies must hold transformers as a regular (non-optional) dep"
);
assert.equal(
lock.packages[""]?.optionalDependencies?.["onnxruntime-node"],
"1.24.3",
"root lock optionalDependencies must hold onnxruntime-node"
// Optional flag is only written `true` for genuinely optional packages;
// regular deps leave it absent/null. Assert each is NOT optional.
assert.ok(
!lock.packages["node_modules/@huggingface/transformers"]?.optional,
"transformers must not be marked optional in the lockfile"
);
assert.ok(
lock.packages["node_modules/@huggingface/transformers"]?.optional,
"transformers must be marked optional in the lockfile"
!lock.packages["node_modules/onnxruntime-node"]?.optional,
"onnxruntime-node must not be marked optional in the lockfile"
);
assert.ok(
lock.packages["node_modules/onnxruntime-node"]?.optional,
"onnxruntime-node must be marked optional in the lockfile"
);
assert.ok(
lock.packages["node_modules/onnxruntime-common"]?.optional,
"onnxruntime-common must be marked optional in the lockfile"
);
});
test("every @huggingface/transformers consumer loads it lazily so absent installs degrade gracefully", () => {
// If any module ever switches to a STATIC import of the optional chain,
// startup crashes on platforms where npm skipped it (Android/Termux).
// transformersLocal.ts must keep its lazy await import() (D8/D25);
// onnxWorker.ts must keep its runtime-variable dynamicImport indirection.
const embeddingSrc = readFileSync(
join(repoRoot, "src/lib/memory/embedding/transformersLocal.ts"),
"utf8"
);
assert.doesNotMatch(
embeddingSrc,
/^\s*import\s+(?:[^'"]*?\s+from\s+)?["']@huggingface\/transformers["']/m,
"transformersLocal.ts must not statically import @huggingface/transformers"
);
assert.match(
embeddingSrc,
/await import\(["']@huggingface\/transformers["']\)/,
"transformersLocal.ts must load @huggingface/transformers via await import()"
);
const workerSrc = readFileSync(
join(repoRoot, "open-sse/services/compression/engines/llmlingua/onnxWorker.ts"),
"utf8"
);
assert.doesNotMatch(
workerSrc,
/^\s*import\s+(?:[^'"]*?\s+from\s+)?["']@huggingface\/transformers["']/m,
"onnxWorker.ts must not statically import @huggingface/transformers"
!lock.packages["node_modules/onnxruntime-common"]?.optional,
"onnxruntime-common must not be marked optional in the lockfile"
);
});

View File

@@ -15,10 +15,7 @@ class MockM365WebSocket {
closed = false;
listeners = new Map<string, Listener[]>();
constructor(
public url: string,
public options: unknown
) {
constructor(public url: string, public options: unknown) {
MockM365WebSocket.instances.push(this);
queueMicrotask(() => this.emit("open"));
}
@@ -147,55 +144,30 @@ test("#7870: enterprise-tier chat invocation defaults tone to Magic", async () =
assert.equal(invocationArgs.tone, "Magic");
});
test("2026-08-21: individual (no tier) chat invocation carries the recaptured shape", async () => {
test("#10718: individual (no tier) chat invocation carries the recaptured 2026-08 shape", async () => {
const invocationArgs = await sendChatInvocation(undefined);
const optionsSets = invocationArgs.optionsSets as string[];
// The pre-#10718 25-entry consumer/MSA set (enable_msa_user, pdnascan, …) is
// still gone from the wire — only the 2026-08-21 34-entry set is current.
// The 25-entry consumer/MSA set (enable_msa_user, pdnascan, …) is gone from
// the wire — the stale set was part of the silently-dropped shape.
assert.ok(!optionsSets.includes("enable_msa_user"));
assert.ok(optionsSets.includes("enable_gg_gpt"));
// The individual/consumer surface now sends tone:"Magic" (capitalized) —
// the #10718 lowercase "magic" is stale.
assert.equal(invocationArgs.tone, "Magic");
// The browser sends tone:"magic" (lowercase) on the individual/EDU surface.
assert.equal(invocationArgs.tone, "magic");
assert.deepEqual(invocationArgs.allowedMessageTypes, [
"Chat",
"Suggestion",
"InternalSearchQuery",
"Disengaged",
"InternalLoaderMessage",
"Progress",
"GeneratedCode",
"RenderCardRequest",
"AdsQuery",
"SemanticSerp",
"GenerateContentQuery",
"GenerateGraphicArt",
"SearchQuery",
"ConfirmationCard",
"AuthError",
"DeveloperLogs",
"TriggerPlugin",
"HintInvocation",
"MemoryUpdate",
"EndOfRequest",
"TriggerConfirmation",
"ResumeInvokeAction",
"ResumeUserInputRequest",
"TriggerUserInputRequest",
"EscapeHatch",
"TriggerPluginAuth",
"ResumePluginAuth",
"SideBySide",
"ReferencesListComplete",
"SwitchRespondingEndpoint",
"InternalLoaderMessage",
]);
});
test("2026-08-21: EDU-tier chat invocation carries the same recaptured shape", async () => {
test("#10718: EDU-tier chat invocation carries the same recaptured shape", async () => {
const invocationArgs = await sendChatInvocation("edu");
const optionsSets = invocationArgs.optionsSets as string[];
assert.ok(!optionsSets.includes("enable_msa_user"));
assert.equal(invocationArgs.tone, "Magic");
assert.equal(invocationArgs.tone, "magic");
});
test("#8971: enterprise-tier chat invocation must send disconnectBehavior=continue", async () => {
@@ -207,11 +179,11 @@ test("#8971: enterprise-tier chat invocation must send disconnectBehavior=contin
);
});
test("2026-08-21: individual (no tier) chat invocation also sends disconnectBehavior=continue (now on every tier)", async () => {
test("#8971/#10718: individual (no tier) chat invocation omits disconnectBehavior (not on the 2026-08 wire)", async () => {
const invocationArgs = await sendChatInvocation(undefined);
assert.equal(
invocationArgs.disconnectBehavior,
"continue",
`individual-tier invocation must carry disconnectBehavior="continue"; got ${JSON.stringify(invocationArgs.disconnectBehavior)}`
undefined,
`individual-tier invocation must omit disconnectBehavior; got ${JSON.stringify(invocationArgs.disconnectBehavior)}`
);
});

View File

@@ -41,7 +41,7 @@ test("#10718: metricsFrame emits the exact bytes observed in the browser capture
// ── Recaptured invocation shape ────────────────────────────────────────────
test("2026-08-21: buildChatInvocation matches the recaptured arguments[0] key set", () => {
test("#10718: buildChatInvocation matches the recaptured arguments[0] key set", () => {
const arg = buildChatInvocation({
text: "Say OK in one word.",
traceId: "11111111-1111-1111-1111-111111111111",
@@ -50,25 +50,19 @@ test("2026-08-21: buildChatInvocation matches the recaptured arguments[0] key se
conversationId: "44444444-4444-4444-4444-444444444444",
}).arguments[0] as Record<string, unknown>;
// Exact key set from the 2026-08-21 capture (issue: individual/consumer M365
// Copilot calls got only SignalR keepalive pings and no type:1 update at all
// — "Stream ended before producing a non-ping SSE event" client-side). The
// #10718 shape below is missing exactly the keys this capture added.
// Exact key set from the capture — additions AND omissions are both pinned,
// because the stale keys are exactly what got the shape dropped.
assert.deepEqual(Object.keys(arg).sort(), [
"allowedMessageTypes",
"clientCorrelationId",
"clientInfo",
"conversationId",
"disconnectBehavior",
"extraExtensionParameters",
"isSbsSupported",
"isStartOfSession",
"message",
"options",
"optionsSets",
"plugins",
"productThreadType",
"renderReferencesBehindEOS",
"sessionId",
"sliceIds",
"source",
@@ -79,32 +73,13 @@ test("2026-08-21: buildChatInvocation matches the recaptured arguments[0] key se
"traceId",
]);
assert.equal(arg.productThreadType, "Office");
assert.deepEqual(arg.clientInfo, {
clientAppName: "Office",
clientPlatform: "mcmcopilot-web",
clientEntrypoint: "mcmcopilot-officeweb",
clientSessionId: "22222222-2222-2222-2222-222222222222",
ProductCategory: "Chat",
clientAppType: "Web",
productEntryPoint: "ChatPanel",
deviceOS: "Windows",
deviceType: "Desktop",
clientPlatformVersion: "10",
});
assert.deepEqual(arg.clientInfo, { clientAppName: "Office", clientPlatform: "mcmcopilot-web" });
assert.equal(arg.conversationId, "44444444-4444-4444-4444-444444444444");
assert.equal(arg.toolChoice, null);
// The individual/consumer surface now sends "Magic" (capitalized), matching
// the enterprise tone literal — the #10718 lowercase "magic" is stale.
assert.equal(arg.tone, "Magic");
assert.equal(arg.isSbsSupported, true);
assert.equal(arg.renderReferencesBehindEOS, true);
assert.deepEqual(arg.extraExtensionParameters, {});
assert.deepEqual(arg.plugins, [{ Id: "BingWebSearch", Source: "BuiltIn" }]);
// Sent on every tier now, not gated to enterprise as #8971 described.
assert.equal(arg.disconnectBehavior, "continue");
assert.equal(arg.tone, "magic");
});
test("2026-08-21: the message object carries the recaptured rich shape", () => {
test("#10718: the message object carries the recaptured rich shape", () => {
const arg = buildChatInvocation({
text: "Say OK in one word.",
traceId: "t",
@@ -118,9 +93,7 @@ test("2026-08-21: the message object carries the recaptured rich shape", () => {
"adaptiveCards",
"attachments",
"author",
"clientInfo",
"clientPreferences",
"connectedFederatedConnections",
"entityAnnotationTypes",
"experienceType",
"inputMethod",
@@ -134,41 +107,26 @@ test("2026-08-21: the message object carries the recaptured rich shape", () => {
assert.equal(message.messageType, "Chat");
assert.equal(message.requestId, "r");
assert.equal(message.experienceType, "Default");
assert.deepEqual(message.entityAnnotationTypes, [
"People",
"File",
"Event",
"Email",
"TeamsMessage",
]);
assert.deepEqual(message.entityAnnotationTypes, ["People", "File", "Event", "Email", "TeamsMessage"]);
assert.equal(message.attachments, null);
assert.deepEqual(message.locationInfo, { timeZone: "UTC", timeZoneOffset: 0 });
assert.deepEqual(message.connectedFederatedConnections, ["dummyId"]);
// Same clientInfo object echoed inside message, per the capture.
assert.deepEqual(message.clientInfo, arg.clientInfo);
});
test("2026-08-21: default tier lists are the recaptured 34-entry optionsSets / 30-entry allowedMessageTypes", () => {
test("#10718: default tier lists are the recaptured 14-entry optionsSets / 6-entry allowedMessageTypes", () => {
const overrides = resolveChatInvocationOverrides(undefined);
assert.equal(overrides.optionsSets.length, 34);
assert.equal(overrides.allowedMessageTypes.length, 30);
assert.equal(overrides.tone, "Magic");
assert.equal(overrides.disconnectBehavior, "continue");
assert.equal(overrides.optionsSets.length, 14);
assert.equal(overrides.allowedMessageTypes.length, 6);
assert.equal(overrides.tone, "magic");
// The pre-#10718 consumer/MSA flags are gone from the wire.
const optionSets = M365_DEFAULT_OPTION_SETS as readonly string[];
const messageTypes = ALLOWED_MESSAGE_TYPES as readonly string[];
// Entries the 2026-08-21 capture showed that the #10718 lists lacked.
for (const present of [
"cwc_code_interpreter",
"rich_responses",
"async_client_interaction",
"flux_v3_references",
]) {
assert.ok(optionSets.includes(present), `${present} must be in the default option sets`);
for (const stale of ["enable_msa_user", "pdnascan", "cwc_code_interpreter", "rich_responses"]) {
assert.ok(!optionSets.includes(stale), `${stale} must not be in the default option sets`);
}
for (const present of ["InternalSearchQuery", "GeneratedCode", "AuthError", "TriggerPlugin"]) {
assert.ok(messageTypes.includes(present), `${present} must be in allowedMessageTypes`);
for (const stale of ["InternalSearchQuery", "GeneratedCode", "RenderCardRequest", "AdsQuery", "SemanticSerp", "GenerateContentQuery"]) {
assert.ok(!messageTypes.includes(stale), `${stale} must not be in allowedMessageTypes`);
}
// Entries the #10718 capture showed and this capture still confirms.
// Entries the capture showed and the old lists lacked.
assert.ok(optionSets.includes("cwcfluxgptv"));
assert.ok(messageTypes.includes("EndOfRequest"));
});
@@ -176,7 +134,8 @@ test("2026-08-21: default tier lists are the recaptured 34-entry optionsSets / 3
// ── refresh_token helpers ──────────────────────────────────────────────────
function fakeJwt(claims: Record<string, unknown>): string {
const b64 = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
const b64 = (value: unknown) =>
Buffer.from(JSON.stringify(value)).toString("base64url");
return `${b64({ alg: "none" })}.${b64(claims)}.sig`;
}

View File

@@ -182,9 +182,7 @@ test("buildChatInvocation defaults stay backward compatible", () => {
requestId: "r",
conversationId: "c",
}).arguments[0] as Record<string, unknown>;
// 2026-08-21 capture (#11069): BingWebSearch BuiltIn plugin is now the
// default on all tiers (was [] in the #10718 shape); toolChoice stays null.
assert.deepEqual(arg.plugins, [{ Id: "BingWebSearch", Source: "BuiltIn" }]);
assert.deepEqual(arg.plugins, []);
assert.equal(arg.toolChoice, null);
assert.equal(arg.customInstructions, undefined);
});

View File

@@ -1,78 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
extractM365CredentialFromHar,
describeHarImportExpiry,
M365_CHATHUB_WS_PREFIX,
} from "../../../src/shared/utils/m365HarImport.ts";
function fakeJwt(exp: number): string {
const b64 = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
return `${b64({ alg: "none" })}.${b64({ exp })}.sig`;
}
function harWithUrl(url: string | null): string {
return JSON.stringify({
log: { entries: url ? [{ request: { url } }] : [] },
});
}
test("extracts access_token + chathubPath from a matching ChatHub WS entry", () => {
const exp = Math.floor(Date.now() / 1000) + 3600;
const token = fakeJwt(exp);
const url = `${M365_CHATHUB_WS_PREFIX}oid-123%40tenant-456?chatsessionid=abc&access_token=${token}`;
const result = extractM365CredentialFromHar(harWithUrl(url));
assert.equal(result.ok, true);
if (!result.ok) return;
assert.equal(result.chathubPath, "oid-123@tenant-456");
assert.equal(result.apiKey, `access_token=${token}; chathubPath=oid-123@tenant-456`);
assert.ok(result.expiresAt !== null && Math.abs(result.expiresAt - exp * 1000) < 1000);
});
test("picks the LAST matching entry when a HAR has multiple turns", () => {
const oldToken = fakeJwt(1000);
const freshToken = fakeJwt(9999999999);
const har = JSON.stringify({
log: {
entries: [
{ request: { url: `${M365_CHATHUB_WS_PREFIX}u%40t?access_token=${oldToken}` } },
{ request: { url: "https://unrelated.example/" } },
{ request: { url: `${M365_CHATHUB_WS_PREFIX}u%40t?access_token=${freshToken}` } },
],
},
});
const result = extractM365CredentialFromHar(har);
assert.equal(result.ok, true);
if (!result.ok) return;
assert.ok(result.apiKey.includes(freshToken));
});
test("returns notJson for malformed input", () => {
const result = extractM365CredentialFromHar("not json{{{");
assert.deepEqual(result, { ok: false, error: "notJson" });
});
test("returns noEntries when log.entries is missing/not an array", () => {
const result = extractM365CredentialFromHar(JSON.stringify({ log: {} }));
assert.deepEqual(result, { ok: false, error: "noEntries" });
});
test("returns noChathubUrl when no request matches the ChatHub prefix", () => {
const result = extractM365CredentialFromHar(harWithUrl("https://example.com/"));
assert.deepEqual(result, { ok: false, error: "noChathubUrl" });
});
test("returns missingFields when the URL matches but lacks access_token or chathubPath", () => {
const result = extractM365CredentialFromHar(harWithUrl(`${M365_CHATHUB_WS_PREFIX}`));
assert.equal(result.ok, false);
});
test("describeHarImportExpiry classifies ok/warn/bad/unknown", () => {
const now = Date.now();
assert.equal(describeHarImportExpiry(now + 30 * 60000, now).tone, "ok");
assert.equal(describeHarImportExpiry(now + 5 * 60000, now).tone, "warn");
assert.equal(describeHarImportExpiry(now - 60000, now).tone, "bad");
assert.equal(describeHarImportExpiry(null, now).tone, "unknown");
});

View File

@@ -131,22 +131,7 @@ test("buildChatInvocation produces a type:4 chat invocation carrying the user te
assert.equal(arg.sessionId, "session-id");
assert.equal(arg.conversationId, "conversation-id");
assert.equal(arg.productThreadType, "Office");
// 2026-08-21 capture (#11069): clientInfo gained 8 keys (clientEntrypoint,
// clientSessionId, ProductCategory, clientAppType, productEntryPoint,
// deviceOS, deviceType, clientPlatformVersion). The #10718 base shape only
// carried clientAppName + clientPlatform.
assert.deepEqual(arg.clientInfo, {
clientAppName: "Office",
clientPlatform: "mcmcopilot-web",
clientEntrypoint: "mcmcopilot-officeweb",
clientSessionId: "session-id",
ProductCategory: "Chat",
clientAppType: "Web",
productEntryPoint: "ChatPanel",
deviceOS: "Windows",
deviceType: "Desktop",
clientPlatformVersion: "10",
});
assert.deepEqual(arg.clientInfo, { clientAppName: "Office", clientPlatform: "mcmcopilot-web" });
assert.equal(arg.isStartOfSession, true);
assert.ok(Array.isArray(arg.optionsSets));
assert.ok((arg.optionsSets as string[]).includes("enable_gg_gpt"));

View File

@@ -30,11 +30,10 @@ 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; individual/EDU now also sends
// "Magic" (capitalized) — the 2026-08-21 capture (#11069) showed lowercase
// "magic" is part of the shape that gets silently dropped (ping-only socket).
// enterprise tier default tone is Magic; individual/EDU sends "magic" (#10718
// recapture — the old "" default was part of the silently-dropped shape)
assert.equal(enterprise.tone, "Magic");
assert.equal(individual.tone, "Magic");
assert.equal(individual.tone, "magic");
// precedence: resolveToneForModel(model) ?? overrides.tone (mirrors the executor wiring)
const toneFor = (model: string | undefined, tierTone: string) =>
@@ -44,9 +43,9 @@ test("model-driven tone overrides the tier default; bare id keeps the tier tone"
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 (both "Magic" post-#11069)
// the bare id keeps whatever the tier resolved
assert.equal(toneFor("copilot-m365", enterprise.tone), "Magic");
assert.equal(toneFor("copilot-m365", individual.tone), "Magic");
assert.equal(toneFor("copilot-m365", individual.tone), "magic");
});
test("registry exposes the bare id (first) plus every tone variant", () => {