Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions apps/server/src/provider/Layers/CopilotProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";

import * as Schema from "effect/Schema";
import { describe, it } from "vitest";

import { CopilotSettings } from "../Drivers/CopilotSettings.ts";
import { makePendingCopilotProvider } from "./CopilotProvider.ts";

const decodeCopilotSettings = Schema.decodeSync(CopilotSettings);

/**
* Regression guard: the Copilot adapter has always read, validated and
* forwarded a `reasoningEffort` selection, but the capability descriptor was
* an empty array, so the trait never surfaced in the UI. This test pins the
* descriptor's presence and its intentionally opt-in shape (no default), so a
* future refactor cannot silently re-hide it or introduce a forced default
* that would be auto-dispatched and rejected on models lacking that effort.
*/
describe("CopilotProvider reasoning effort", () => {
it("exposes an opt-in reasoningEffort select on copilot models", () => {
const settings = decodeCopilotSettings({ enabled: true, customModels: ["gpt-5"] });
const draft = makePendingCopilotProvider(settings);

const model = draft.models[0];
assert.ok(model, "expected at least one copilot model");

const descriptors = model.capabilities?.optionDescriptors ?? [];
const effort = descriptors.find((descriptor) => descriptor.id === "reasoningEffort");
if (!effort || effort.type !== "select") {
assert.fail("reasoningEffort select descriptor must be present");
}

assert.deepEqual(
effort.options.map((option) => option.id),
["low", "medium", "high", "xhigh"],
);

// Opt-in semantics: no currentValue and no isDefault, so an untouched
// selector dispatches nothing and the adapter's per-model validation is
// skipped — preserving the prior "no effort" behavior on models that do
// not advertise the picked effort in supportedReasoningEfforts.
assert.equal(effort.currentValue, undefined);
assert.ok(
effort.options.every((option) => option.isDefault !== true),
"no reasoningEffort option may be marked isDefault (opt-in)",
);
});
});
27 changes: 26 additions & 1 deletion apps/server/src/provider/Layers/CopilotProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,33 @@ const COPILOT_PRESENTATION = {
showInteractionModeToggle: true,
} as const;

/**
* GitHub Copilot exposes a per-model reasoning effort (low/medium/high/xhigh).
* The adapter already reads, validates (against the live model's
* `supportedReasoningEfforts`) and forwards a selected effort end-to-end; the
* only missing link was this descriptor, without which the trait never
* rendered. The descriptor is intentionally opt-in — no `isDefault` /
* `currentValue` — because these capabilities are global (stamped onto every
* Copilot model) while effort support is decided per-model at session start.
* Forcing a default would auto-dispatch it and get rejected by
* `validateSessionConfiguration` on any model that does not list that effort.
* Leaving it unset keeps the selection `undefined` until the user picks one,
* which the adapter treats as "no effort" (its prior behavior).
*/
const DEFAULT_COPILOT_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({
optionDescriptors: [],
optionDescriptors: [
{
id: "reasoningEffort",
label: "Reasoning",
type: "select" as const,
options: [
{ id: "low", label: "Low" },
{ id: "medium", label: "Medium" },
{ id: "high", label: "High" },
{ id: "xhigh", label: "Extra High" },
],
},
],
});

/**
Expand Down
Loading