mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(github-copilot): honor live model selection in guided setup
This commit is contained in:
@@ -31,7 +31,7 @@ provider or agent runtime in three different ways.
|
||||
</Step>
|
||||
<Step title="Set a default model">
|
||||
```bash
|
||||
openclaw models set github-copilot/claude-sonnet-4.6
|
||||
openclaw models set github-copilot/claude-sonnet-5
|
||||
```
|
||||
|
||||
Or in config:
|
||||
@@ -39,7 +39,7 @@ provider or agent runtime in three different ways.
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: { model: { primary: "github-copilot/claude-sonnet-4.6" } },
|
||||
defaults: { model: { primary: "github-copilot/claude-sonnet-5" } },
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -204,7 +204,7 @@ back to `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, then `GITHUB_TOKEN`. Use
|
||||
|
||||
Fresh non-interactive setup validates the token before saving it. When setup
|
||||
must choose a default, it also checks the live Copilot model catalog. OpenClaw
|
||||
prefers the documented general-purpose Copilot CLI model when that model is
|
||||
prefers the provider's current general-purpose model when that model is
|
||||
enabled for the account; otherwise it chooses a deterministic eligible fallback.
|
||||
Setup fails without writing a new auth profile if the account has no
|
||||
picker-visible model that supports streaming and tool calls. An explicitly
|
||||
|
||||
@@ -29,7 +29,7 @@ const mocks = vi.hoisted(() => ({
|
||||
release: vi.fn(async () => {}),
|
||||
})),
|
||||
resolveCopilotRuntimeAuth: vi.fn(),
|
||||
resolveCopilotStarterModel: vi.fn(async () => "github-copilot/claude-sonnet-4.6"),
|
||||
resolveCopilotStarterModel: vi.fn(async () => "github-copilot/claude-sonnet-5"),
|
||||
}));
|
||||
|
||||
function requireAuthMethod<T>(methods: readonly T[], index: number): T {
|
||||
@@ -569,7 +569,7 @@ describe("github-copilot plugin", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultModel: "github-copilot/claude-sonnet-4.6",
|
||||
defaultModel: "github-copilot/claude-sonnet-5",
|
||||
});
|
||||
expect(mocks.resolveCopilotStarterModel).toHaveBeenCalledWith({
|
||||
githubToken: "existing-token",
|
||||
@@ -1366,11 +1366,9 @@ describe("github-copilot plugin", () => {
|
||||
mode: "token",
|
||||
});
|
||||
expect(result?.agents?.defaults?.model).toEqual({
|
||||
primary: "github-copilot/claude-sonnet-4.6",
|
||||
primary: "github-copilot/claude-sonnet-5",
|
||||
});
|
||||
expect(result?.agents?.defaults?.models?.["github-copilot/claude-sonnet-4.6"]).toStrictEqual(
|
||||
{},
|
||||
);
|
||||
expect(result?.agents?.defaults?.models?.["github-copilot/claude-sonnet-5"]).toStrictEqual({});
|
||||
|
||||
const profile = ensureAuthProfileStore(agentDir).profiles["github-copilot:github"];
|
||||
expect(profile).toEqual({
|
||||
@@ -1494,7 +1492,7 @@ describe("github-copilot plugin", () => {
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(result?.agents?.defaults?.model).toEqual({
|
||||
fallbacks: ["openai/gpt-5.4"],
|
||||
primary: "github-copilot/claude-sonnet-4.6",
|
||||
primary: "github-copilot/claude-sonnet-5",
|
||||
});
|
||||
|
||||
const profile = ensureAuthProfileStore(agentDir).profiles["github-copilot:github"];
|
||||
|
||||
@@ -8,9 +8,9 @@ type CopilotReasoningCompat = {
|
||||
supportedReasoningEfforts?: readonly string[] | null;
|
||||
};
|
||||
|
||||
// GitHub Copilot CLI's documented general-purpose default. Setup treats this
|
||||
// as a preference only and verifies it against the authenticated live catalog.
|
||||
export const DEFAULT_COPILOT_MODEL = "github-copilot/claude-sonnet-4.6";
|
||||
// Provider-owned general-purpose preference. Setup verifies it against the
|
||||
// authenticated live catalog instead of assuming every account can use it.
|
||||
export const DEFAULT_COPILOT_MODEL = "github-copilot/claude-sonnet-5";
|
||||
|
||||
const COPILOT_CHAT_COMPLETIONS_COMPAT: ModelDefinitionConfig["compat"] = {
|
||||
supportsStore: false,
|
||||
|
||||
@@ -617,7 +617,7 @@ describe("fetchCopilotModelCatalog", () => {
|
||||
pickerEnabled?: boolean;
|
||||
policyState?: string;
|
||||
preview?: boolean;
|
||||
streaming?: boolean;
|
||||
streaming?: boolean | "omit";
|
||||
toolCalls?: boolean;
|
||||
}) {
|
||||
return {
|
||||
@@ -635,7 +635,7 @@ describe("fetchCopilotModelCatalog", () => {
|
||||
max_output_tokens: params.maxTokens ?? 64_000,
|
||||
},
|
||||
supports: {
|
||||
streaming: params.streaming ?? true,
|
||||
...(params.streaming === "omit" ? {} : { streaming: params.streaming ?? true }),
|
||||
tool_calls: params.toolCalls ?? true,
|
||||
},
|
||||
},
|
||||
@@ -685,6 +685,14 @@ describe("fetchCopilotModelCatalog", () => {
|
||||
expect(selectCopilotStarterModel(models, "hidden")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not treat omitted streaming metadata as an explicit lack of support", async () => {
|
||||
const models = await fetchSelectionFixture([
|
||||
selectableModelEntry({ id: "omitted-streaming", streaming: "omit" }),
|
||||
]);
|
||||
|
||||
expect(selectCopilotStarterModel(models, "omitted-streaming")?.id).toBe("omitted-streaming");
|
||||
});
|
||||
|
||||
it("maps Copilot /models entries to ModelDefinitionConfig with real context windows", async () => {
|
||||
const fetchImpl = vi.fn().mockResolvedValue(makeResponse(200, sampleApiResponse));
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ type CopilotModelSelectionMetadata = {
|
||||
pickerEnabled: boolean;
|
||||
policyState?: string;
|
||||
preview: boolean;
|
||||
streaming: boolean;
|
||||
streaming?: boolean;
|
||||
toolCalls: boolean;
|
||||
};
|
||||
|
||||
@@ -151,7 +151,9 @@ export function isCopilotCatalogModelVisible(model: CopilotCatalogModel): boolea
|
||||
|
||||
function isCopilotCatalogModelSelectable(model: CopilotCatalogModel): boolean {
|
||||
const metadata = readCopilotModelSelectionMetadata(model);
|
||||
return Boolean(isCopilotCatalogModelVisible(model) && metadata?.streaming && metadata.toolCalls);
|
||||
return Boolean(
|
||||
isCopilotCatalogModelVisible(model) && metadata?.streaming !== false && metadata?.toolCalls,
|
||||
);
|
||||
}
|
||||
|
||||
const COPILOT_STARTER_CATEGORY_RANK = new Map<string, number>([
|
||||
@@ -306,7 +308,7 @@ function mapCopilotApiModelToDefinition(
|
||||
pickerEnabled: entry.model_picker_enabled === true,
|
||||
policyState: normalizeOptionalLowercaseString(entry.policy?.state),
|
||||
preview: entry.preview === true,
|
||||
streaming: supports?.streaming === true,
|
||||
streaming: supports?.streaming,
|
||||
toolCalls: supports?.tool_calls === true,
|
||||
});
|
||||
return definition;
|
||||
|
||||
@@ -591,9 +591,41 @@ async function runProviderManualSecretMethod(params: {
|
||||
throw new Error(methodError || `Provider setup exited with code ${code}.`);
|
||||
},
|
||||
};
|
||||
const existingPrimary = resolveAgentModelPrimaryValue(params.config.agents?.defaults?.model);
|
||||
const existingProvider = existingPrimary ? parseRef(existingPrimary).provider : undefined;
|
||||
let providerSetupConfig = params.config;
|
||||
if (
|
||||
existingProvider &&
|
||||
normalizeProviderId(existingProvider) !== normalizeProviderId(params.choice.providerId)
|
||||
) {
|
||||
const agents = params.config.agents;
|
||||
const defaults = agents?.defaults;
|
||||
const model = defaults?.model;
|
||||
if (defaults && model !== undefined) {
|
||||
const { model: _model, ...defaultsWithoutModel } = defaults;
|
||||
let modelWithoutPrimary: Exclude<typeof model, string> | undefined;
|
||||
if (typeof model === "object" && model !== null) {
|
||||
const { primary: _primary, ...remainingModelConfig } = model;
|
||||
modelWithoutPrimary = remainingModelConfig;
|
||||
}
|
||||
// App-guided setup needs this provider's verified candidate, not an
|
||||
// unrelated current default. The original config remains the merge base.
|
||||
providerSetupConfig = {
|
||||
...params.config,
|
||||
agents: {
|
||||
...agents,
|
||||
defaults:
|
||||
modelWithoutPrimary && Object.keys(modelWithoutPrimary).length > 0
|
||||
? { ...defaultsWithoutModel, model: modelWithoutPrimary }
|
||||
: defaultsWithoutModel,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const configured = await runNonInteractive({
|
||||
authChoice: params.choice.choiceId,
|
||||
config: params.config,
|
||||
config: providerSetupConfig,
|
||||
baseConfig: params.baseConfig,
|
||||
opts: { [optionKey]: params.apiKey, secretInputMode: "plaintext" },
|
||||
runtime: isolatedRuntime,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type AgentExecutionAuthBinding,
|
||||
} from "../agents/execution-auth-binding.js";
|
||||
import { detectInferenceBackends } from "../commands/onboard-inference.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { withoutPluginInstallRecords } from "../plugins/installed-plugin-index-records.js";
|
||||
@@ -3719,16 +3720,19 @@ describe("activateSetupInference", () => {
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "uses a provider starter model instead of an unrelated existing default",
|
||||
name: "requests a dynamic provider model instead of using a static starter",
|
||||
existingModel: "openai/gpt-5.2",
|
||||
starterModel: "github-copilot/claude-sonnet-4.5",
|
||||
starterModel: "github-copilot/static-should-not-win",
|
||||
expectedSetupInputModel: undefined,
|
||||
},
|
||||
{
|
||||
name: "accepts an unchanged provider-owned dynamic model",
|
||||
existingModel: "github-copilot/claude-sonnet-4.5",
|
||||
starterModel: undefined,
|
||||
expectedSetupInputModel: "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
])("$name without starting interactive login", async ({ existingModel, starterModel }) => {
|
||||
])("$name without starting interactive login", async (testCase) => {
|
||||
const { existingModel, starterModel, expectedSetupInputModel } = testCase;
|
||||
const stateDir = await makeTempDir();
|
||||
const agentDir = path.join(stateDir, "agent");
|
||||
const runInteractive = vi.fn();
|
||||
@@ -3838,6 +3842,10 @@ describe("activateSetupInference", () => {
|
||||
opts: expect.objectContaining({ githubCopilotToken: "github-token" }),
|
||||
}),
|
||||
);
|
||||
const setupInputConfig = runNonInteractive.mock.calls[0]?.[0].config;
|
||||
expect(resolveAgentModelPrimaryValue(setupInputConfig?.agents?.defaults?.model)).toBe(
|
||||
expectedSetupInputModel,
|
||||
);
|
||||
const activatedProfileId = runEmbeddedAgent.mock.calls[0]?.[0].authProfileId;
|
||||
if (!activatedProfileId) {
|
||||
throw new Error("expected setup auth profile");
|
||||
|
||||
Reference in New Issue
Block a user