mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(github-copilot): preserve catalog thinking efforts in requests (#107834)
* fix(github-copilot): preserve catalog thinking efforts in requests Unify discovered and bundled capability mapping with the provider thinking policy. Preserve supported xhigh/max Responses efforts and map minimal to the supported low minimum, while respecting explicit account opt-outs and transport limits. Fixes #107792 Co-authored-by: Pluviobyte <Pluviobyte@users.noreply.github.com> * fix(github-copilot): resolve nullable thinking policy transport Accept the public policy API context and resolve missing transports before enforcing Claude and Gemini effort restrictions. Cover undefined and null API values without changing explicit Responses routes. * refactor(github-copilot): normalize manifest models as one catalog Use the canonical batch model provider builder after the single-row helper was removed on main. Preserve model transport and compatibility decoration without a legacy API shim. * refactor(github-copilot): decorate owned catalog rows in place Keep the normalized manifest batch as the sole owner of runtime rows and apply transport metadata directly, avoiding redundant row copies. --------- Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Pluviobyte <Pluviobyte@users.noreply.github.com>
This commit is contained in:
@@ -263,6 +263,17 @@ configured default model is never replaced.
|
||||
transport based on the model ref.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Thinking levels">
|
||||
Use `/think xhigh` or `/think max` when the selected model exposes that
|
||||
level. Copilot's live catalog determines the supported efforts for your
|
||||
account, and OpenClaw preserves those efforts in Responses requests.
|
||||
When a Responses model starts its native effort range at `low`, `minimal`
|
||||
maps to `low` instead of sending an unsupported value.
|
||||
Explicit live limits take precedence over the bundled catalog. Gemini's
|
||||
Chat Completions transport does not expose `max`.
|
||||
See [Thinking levels](/tools/thinking) for session and per-message controls.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Request compatibility">
|
||||
OpenClaw sends Copilot-compatible request headers with a Copilot CLI request
|
||||
identity, marks tool-result follow-up turns as agent-initiated, and sets the
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from "./domain.js";
|
||||
import { createGithubCopilotDynamicModelHooks } from "./dynamic-models.js";
|
||||
import { githubCopilotMemoryEmbeddingProviderAdapter } from "./embeddings.js";
|
||||
import { DEFAULT_COPILOT_MODEL, resolveCopilotExtendedThinkingLevels } from "./model-metadata.js";
|
||||
import { DEFAULT_COPILOT_MODEL } from "./model-metadata.js";
|
||||
import { PROVIDER_ID } from "./models.js";
|
||||
import {
|
||||
buildGithubCopilotAuthDoctorHint,
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
parseGithubCopilotApiKey,
|
||||
refreshGithubCopilotOAuth,
|
||||
} from "./oauth.js";
|
||||
import { resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
import {
|
||||
buildGithubCopilotReplayPolicy,
|
||||
sanitizeGithubCopilotReplayHistory,
|
||||
@@ -701,19 +702,7 @@ export default definePluginEntry({
|
||||
wrapStreamFn: wrapCopilotProviderStream,
|
||||
buildReplayPolicy: buildGithubCopilotReplayPolicy,
|
||||
sanitizeReplayHistory: sanitizeGithubCopilotReplayHistory,
|
||||
resolveThinkingProfile: ({ modelId, compat }) => {
|
||||
const extendedLevels = resolveCopilotExtendedThinkingLevels(modelId, compat);
|
||||
return {
|
||||
levels: [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
...extendedLevels.map((id) => ({ id })),
|
||||
],
|
||||
};
|
||||
},
|
||||
resolveThinkingProfile,
|
||||
prepareRuntimeAuth: async (ctx) => {
|
||||
const source = parseGithubCopilotApiKey(ctx.apiKey);
|
||||
const { resolveCopilotRuntimeAuth } = await loadGithubCopilotRuntime();
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// Github Copilot plugin module implements model metadata behavior.
|
||||
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { supportsClaudeAdaptiveThinking } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import manifest from "./openclaw.plugin.json" with { type: "json" };
|
||||
|
||||
type CopilotRuntimeApi = "anthropic-messages" | "openai-completions" | "openai-responses";
|
||||
type CopilotReasoningCompat = {
|
||||
supportsReasoningEffort?: boolean;
|
||||
supportedReasoningEfforts?: readonly string[] | null;
|
||||
};
|
||||
|
||||
@@ -18,16 +21,18 @@ const COPILOT_CHAT_COMPLETIONS_COMPAT: ModelDefinitionConfig["compat"] = {
|
||||
supportsUsageInStreaming: false,
|
||||
maxTokensField: "max_tokens",
|
||||
};
|
||||
const COPILOT_XHIGH_MODEL_IDS = new Set([
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
"gpt-5.6-luna",
|
||||
"gpt-5.5",
|
||||
"gpt-5.4",
|
||||
"gpt-5.3-codex",
|
||||
]);
|
||||
const manifestCatalog = manifest.modelCatalog.providers["github-copilot"];
|
||||
const manifestModels = buildManifestModelProviderConfig({
|
||||
providerId: "github-copilot",
|
||||
catalog: manifestCatalog,
|
||||
}).models;
|
||||
for (const model of manifestModels) {
|
||||
model.api = resolveCopilotTransportApi(model.id);
|
||||
model.compat = { ...resolveCopilotModelCompat(model.id), ...model.compat };
|
||||
}
|
||||
|
||||
const STATIC_MODEL_OVERRIDES = new Map<string, Partial<ModelDefinitionConfig>>([
|
||||
...manifestModels.map((model) => [model.id, model] as const),
|
||||
// These two non-catalog ids preserve metadata for legacy configured refs and
|
||||
// account discovery responses. They are intentionally not picker entries.
|
||||
[
|
||||
@@ -54,44 +59,6 @@ const STATIC_MODEL_OVERRIDES = new Map<string, Partial<ModelDefinitionConfig>>([
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
|
||||
},
|
||||
],
|
||||
[
|
||||
"gpt-5.3-codex",
|
||||
{
|
||||
name: "GPT-5.3-Codex",
|
||||
api: "openai-responses",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
contextTokens: 272_000,
|
||||
maxTokens: 128_000,
|
||||
},
|
||||
],
|
||||
[
|
||||
"gpt-5.4",
|
||||
{
|
||||
name: "GPT-5.4",
|
||||
api: "openai-responses",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
||||
contextWindow: 1_050_000,
|
||||
maxTokens: 128_000,
|
||||
},
|
||||
],
|
||||
[
|
||||
"gpt-5.5",
|
||||
{
|
||||
name: "GPT-5.5",
|
||||
api: "openai-responses",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||
contextWindow: 1_050_000,
|
||||
contextTokens: 272_000,
|
||||
maxTokens: 128_000,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function isCopilotGeminiModelId(modelId: string): boolean {
|
||||
@@ -128,44 +95,37 @@ export function resolveCopilotModelCompat(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function compatSupportsEffort(
|
||||
compat: CopilotReasoningCompat | null | undefined,
|
||||
effort: "xhigh" | "max",
|
||||
): boolean {
|
||||
return (
|
||||
Array.isArray(compat?.supportedReasoningEfforts) &&
|
||||
compat.supportedReasoningEfforts.some(
|
||||
(candidate) => normalizeOptionalLowercaseString(candidate) === effort,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveCopilotExtendedThinkingLevels(
|
||||
export function resolveCopilotThinkingLevelMap(
|
||||
modelId: string,
|
||||
compat?: CopilotReasoningCompat | null,
|
||||
): Array<"xhigh" | "max"> {
|
||||
api?: string | null,
|
||||
): ModelDefinitionConfig["thinkingLevelMap"] | undefined {
|
||||
const normalizedModelId = normalizeOptionalLowercaseString(modelId) ?? "";
|
||||
const runtimeApi = api ?? resolveCopilotTransportApi(normalizedModelId);
|
||||
const staticCompat = resolveStaticCopilotModelOverride(normalizedModelId)?.compat;
|
||||
const isClaudeModel = normalizedModelId.includes("claude");
|
||||
const supportsAdaptiveClaudeEffort =
|
||||
!isClaudeModel || supportsClaudeAdaptiveThinking({ id: normalizedModelId });
|
||||
const levels: Array<"xhigh" | "max"> = [];
|
||||
if (
|
||||
supportsAdaptiveClaudeEffort &&
|
||||
(COPILOT_XHIGH_MODEL_IDS.has(normalizedModelId) ||
|
||||
compatSupportsEffort(compat, "xhigh") ||
|
||||
compatSupportsEffort(staticCompat, "xhigh"))
|
||||
) {
|
||||
levels.push("xhigh");
|
||||
// A declared account catalog is authoritative, including explicit opt-outs;
|
||||
// the manifest only supplies effort metadata when discovery has none.
|
||||
const efforts =
|
||||
compat?.supportsReasoningEffort === false
|
||||
? []
|
||||
: (compat?.supportedReasoningEfforts ?? staticCompat?.supportedReasoningEfforts);
|
||||
if (!Array.isArray(efforts)) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
isClaudeModel &&
|
||||
supportsAdaptiveClaudeEffort &&
|
||||
(compatSupportsEffort(compat, "max") || compatSupportsEffort(staticCompat, "max"))
|
||||
) {
|
||||
levels.push("max");
|
||||
}
|
||||
return levels;
|
||||
const supported = new Set(efforts.map(normalizeOptionalLowercaseString));
|
||||
const supportsEffort =
|
||||
runtimeApi !== "anthropic-messages" ||
|
||||
supportsClaudeAdaptiveThinking({ id: normalizedModelId });
|
||||
return {
|
||||
// Keep the public minimal setting usable when this route starts its native ladder at low.
|
||||
...(runtimeApi === "openai-responses" && !supported.has("minimal") && supported.has("low")
|
||||
? { minimal: "low" }
|
||||
: {}),
|
||||
xhigh: supportsEffort && supported.has("xhigh") ? "xhigh" : null,
|
||||
// Chat Completions currently translates max to xhigh rather than preserving it.
|
||||
max:
|
||||
supportsEffort && runtimeApi !== "openai-completions" && supported.has("max") ? "max" : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveStaticCopilotModelOverride(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { streamSimpleOpenAIResponses } from "@openclaw/ai/internal/openai";
|
||||
// Github Copilot tests cover models plugin behavior.
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { createProviderUsageFetch, makeResponse } from "openclaw/plugin-sdk/test-env";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
import { CopilotRuntimeAuthError } from "./runtime-auth-error.js";
|
||||
import { resolveCopilotRuntimeAuth } from "./runtime-auth.js";
|
||||
import { fetchCopilotUsage } from "./usage.js";
|
||||
@@ -75,6 +77,8 @@ describe("resolveCopilotForwardCompatModel", () => {
|
||||
contextWindow: 400_000,
|
||||
contextTokens: 272_000,
|
||||
maxTokens: 128_000,
|
||||
thinkingLevelMap: { minimal: "low", xhigh: "xhigh", max: null },
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,6 +94,8 @@ describe("resolveCopilotForwardCompatModel", () => {
|
||||
cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
||||
contextWindow: 1_050_000,
|
||||
maxTokens: 128_000,
|
||||
thinkingLevelMap: { minimal: "low", xhigh: "xhigh", max: null },
|
||||
compat: { supportedReasoningEfforts: ["none", "low", "medium", "high", "xhigh"] },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,6 +112,11 @@ describe("resolveCopilotForwardCompatModel", () => {
|
||||
contextWindow: 1_050_000,
|
||||
contextTokens: 272_000,
|
||||
maxTokens: 128_000,
|
||||
thinkingLevelMap: { minimal: "low", xhigh: "xhigh", max: null },
|
||||
compat: {
|
||||
codeMode: "capable",
|
||||
supportedReasoningEfforts: ["none", "low", "medium", "high", "xhigh"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,10 +132,10 @@ describe("resolveCopilotForwardCompatModel", () => {
|
||||
});
|
||||
|
||||
it("creates synthetic model for arbitrary unknown model ID", () => {
|
||||
const ctx = createMockCtx("gpt-5.4-mini");
|
||||
const ctx = createMockCtx("future-model");
|
||||
const result = requireResolvedModel(ctx);
|
||||
expect(result.id).toBe("gpt-5.4-mini");
|
||||
expect(result.name).toBe("gpt-5.4-mini");
|
||||
expect(result.id).toBe("future-model");
|
||||
expect(result.name).toBe("future-model");
|
||||
expect((result as unknown as Record<string, unknown>).api).toBe("openai-responses");
|
||||
expect((result as unknown as Record<string, unknown>).input).toEqual(["text", "image"]);
|
||||
});
|
||||
@@ -163,19 +174,19 @@ describe("resolveCopilotForwardCompatModel", () => {
|
||||
});
|
||||
|
||||
it("sets reasoning=false for non-reasoning model IDs including mid-string o1/o3", () => {
|
||||
for (const id of [
|
||||
"gpt-5.4-mini",
|
||||
"claude-sonnet-4.6",
|
||||
"gpt-4o",
|
||||
"mycodexmodel",
|
||||
"audio-o1-hd",
|
||||
"turbo-o3-voice",
|
||||
]) {
|
||||
for (const id of ["gpt-4o", "mycodexmodel", "audio-o1-hd", "turbo-o3-voice"]) {
|
||||
const ctx = createMockCtx(id);
|
||||
const result = requireResolvedModel(ctx);
|
||||
expect((result as unknown as Record<string, unknown>).reasoning).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["gpt-5.4-mini", "claude-sonnet-5"])(
|
||||
"uses manifest reasoning metadata for %s instead of synthesizing an unknown model",
|
||||
(modelId) => {
|
||||
expect(requireResolvedModel(createMockCtx(modelId)).reasoning).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("fetchCopilotUsage", () => {
|
||||
@@ -650,6 +661,92 @@ describe("fetchCopilotModelCatalog", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it.each([
|
||||
{
|
||||
source: "live",
|
||||
reasoning: "minimal",
|
||||
efforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
||||
expected: "low",
|
||||
},
|
||||
{ source: "static", reasoning: "minimal", efforts: [], expected: "low" },
|
||||
{
|
||||
source: "native minimal live",
|
||||
reasoning: "minimal",
|
||||
efforts: ["minimal", "low", "high"],
|
||||
expected: "minimal",
|
||||
},
|
||||
{
|
||||
source: "live",
|
||||
reasoning: "xhigh",
|
||||
efforts: ["low", "high", "xhigh", "max"],
|
||||
expected: "xhigh",
|
||||
},
|
||||
{ source: "live", reasoning: "max", efforts: ["low", "high", "xhigh", "max"], expected: "max" },
|
||||
{ source: "static", reasoning: "xhigh", efforts: [], expected: "xhigh" },
|
||||
{ source: "static", reasoning: "max", efforts: [], expected: "max" },
|
||||
{
|
||||
source: "limited live",
|
||||
reasoning: "xhigh",
|
||||
efforts: ["low", "medium", "high"],
|
||||
expected: "high",
|
||||
},
|
||||
{
|
||||
source: "limited live",
|
||||
reasoning: "max",
|
||||
efforts: ["low", "medium", "high"],
|
||||
expected: "high",
|
||||
},
|
||||
{ source: "empty live", reasoning: "max", efforts: [], expected: undefined },
|
||||
] as const)(
|
||||
"keeps $source catalog policy and $reasoning Responses effort aligned",
|
||||
async ({ source, reasoning, efforts, expected }) => {
|
||||
const [entry] = await fetchSelectionFixture([
|
||||
{
|
||||
id: "gpt-5.6-luna",
|
||||
vendor: "OpenAI",
|
||||
capabilities: {
|
||||
type: "chat",
|
||||
supports: { reasoning_effort: efforts },
|
||||
},
|
||||
},
|
||||
]);
|
||||
const model = {
|
||||
...(source === "static"
|
||||
? requireResolvedModel(createMockCtx("gpt-5.6-luna"))
|
||||
: expectDefined(entry, "discovered Copilot model")),
|
||||
provider: "github-copilot",
|
||||
api: "openai-responses" as const,
|
||||
baseUrl: "https://api.githubcopilot.com",
|
||||
};
|
||||
const profile = resolveThinkingProfile({
|
||||
provider: model.provider,
|
||||
modelId: model.id,
|
||||
api: model.api,
|
||||
compat: model.compat,
|
||||
});
|
||||
expect(profile?.levels.some(({ id }) => id === reasoning)).toBe(
|
||||
reasoning === "minimal" || expected === reasoning,
|
||||
);
|
||||
const onPayload = vi.fn(() => {
|
||||
throw new Error("captured before sending");
|
||||
});
|
||||
|
||||
const result = await streamSimpleOpenAIResponses(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "Reply OK", timestamp: 1 }] },
|
||||
{ apiKey: "test-token", reasoning, onPayload },
|
||||
).result();
|
||||
|
||||
expect(result.errorMessage).toBe("captured before sending");
|
||||
expect(onPayload).toHaveBeenCalledWith(
|
||||
expected
|
||||
? expect.objectContaining({ reasoning: { effort: expected, summary: "auto" } })
|
||||
: expect.not.objectContaining({ reasoning: expect.anything() }),
|
||||
model,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("selects the preferred model only when the authenticated catalog marks it eligible", async () => {
|
||||
const models = await fetchSelectionFixture([
|
||||
selectableModelEntry({ id: "fallback", contextWindow: 1_000_000 }),
|
||||
@@ -733,6 +830,7 @@ describe("fetchCopilotModelCatalog", () => {
|
||||
contextTokens: 272000,
|
||||
maxTokens: 128000,
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
|
||||
thinkingLevelMap: { minimal: "low", xhigh: null, max: null },
|
||||
});
|
||||
|
||||
const codex = out.find((m) => m.id === "gpt-5.3-codex");
|
||||
|
||||
@@ -6,16 +6,14 @@ import type {
|
||||
import { buildCopilotIdeHeaders } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { readProviderJsonArrayFieldResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
normalizeModelCompat,
|
||||
supportsClaudeAdaptiveThinking,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
asPositiveSafeInteger,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
resolveCopilotModelCompat,
|
||||
resolveCopilotThinkingLevelMap,
|
||||
resolveCopilotTransportApi,
|
||||
resolveStaticCopilotModelOverride,
|
||||
} from "./model-metadata.js";
|
||||
@@ -233,7 +231,7 @@ function mergeCopilotCompat(
|
||||
),
|
||||
]
|
||||
: [];
|
||||
if (supportedReasoningEfforts.length === 0) {
|
||||
if (!Array.isArray(reasoningEfforts)) {
|
||||
return base;
|
||||
}
|
||||
return {
|
||||
@@ -242,22 +240,6 @@ function mergeCopilotCompat(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCopilotThinkingLevelMap(
|
||||
api: ModelDefinitionConfig["api"],
|
||||
modelId: string,
|
||||
compat: ModelDefinitionConfig["compat"] | undefined,
|
||||
): ModelDefinitionConfig["thinkingLevelMap"] | undefined {
|
||||
const efforts = compat?.supportedReasoningEfforts;
|
||||
if (api !== "anthropic-messages" || !Array.isArray(efforts)) {
|
||||
return undefined;
|
||||
}
|
||||
const supportsAdaptiveEffort = supportsClaudeAdaptiveThinking({ id: modelId });
|
||||
return {
|
||||
xhigh: supportsAdaptiveEffort && efforts.includes("xhigh") ? "xhigh" : null,
|
||||
max: supportsAdaptiveEffort && efforts.includes("max") ? "max" : null,
|
||||
};
|
||||
}
|
||||
|
||||
function mapCopilotApiModelToDefinition(
|
||||
entry: CopilotApiModelEntry,
|
||||
): CopilotCatalogModel | undefined {
|
||||
@@ -290,7 +272,7 @@ function mapCopilotApiModelToDefinition(
|
||||
const maxTokens = asPositiveSafeInteger(limits?.max_output_tokens) ?? DEFAULT_MAX_TOKENS;
|
||||
const compat = mergeCopilotCompat(resolveCopilotModelCompat(id), supports?.reasoning_effort);
|
||||
const api = resolveCopilotApiForVendor(entry.vendor, id);
|
||||
const thinkingLevelMap = resolveCopilotThinkingLevelMap(api, id, compat);
|
||||
const thinkingLevelMap = resolveCopilotThinkingLevelMap(id, compat, api);
|
||||
|
||||
const definition: CopilotCatalogModel = {
|
||||
id,
|
||||
|
||||
@@ -134,39 +134,44 @@
|
||||
"id": "gpt-5.6-sol",
|
||||
"name": "GPT-5.6 Sol",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": { "minimal": "low", "xhigh": "xhigh", "max": "max" },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 1050000,
|
||||
"contextTokens": 922000,
|
||||
"maxTokens": 128000,
|
||||
"cost": { "input": 5, "output": 30, "cacheRead": 0.5, "cacheWrite": 0 },
|
||||
"compat": { "codeMode": "capable" }
|
||||
"compat": { "codeMode": "capable", "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"] }
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.6-terra",
|
||||
"name": "GPT-5.6 Terra",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": { "minimal": "low", "xhigh": "xhigh", "max": "max" },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 1050000,
|
||||
"contextTokens": 922000,
|
||||
"maxTokens": 128000,
|
||||
"cost": { "input": 2.5, "output": 15, "cacheRead": 0.25, "cacheWrite": 0 },
|
||||
"compat": { "codeMode": "capable" }
|
||||
"compat": { "codeMode": "capable", "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"] }
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.6-luna",
|
||||
"name": "GPT-5.6 Luna",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": { "minimal": "low", "xhigh": "xhigh", "max": "max" },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 1050000,
|
||||
"contextTokens": 922000,
|
||||
"maxTokens": 128000,
|
||||
"cost": { "input": 1, "output": 6, "cacheRead": 0.1, "cacheWrite": 0 },
|
||||
"compat": { "codeMode": "capable" }
|
||||
"compat": { "codeMode": "capable", "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"] }
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.3-codex",
|
||||
"name": "GPT-5.3-Codex",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": { "minimal": "low", "xhigh": "xhigh", "max": null },
|
||||
"compat": { "supportedReasoningEfforts": ["low", "medium", "high", "xhigh"] },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 400000,
|
||||
"contextTokens": 272000,
|
||||
@@ -179,6 +184,8 @@
|
||||
"status": "deprecated",
|
||||
"replacedBy": "gpt-5.6-terra",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": { "minimal": "low", "xhigh": "xhigh", "max": null },
|
||||
"compat": { "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh"] },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 1050000,
|
||||
"maxTokens": 128000,
|
||||
@@ -190,12 +197,13 @@
|
||||
"status": "deprecated",
|
||||
"replacedBy": "gpt-5.6-sol",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": { "minimal": "low", "xhigh": "xhigh", "max": null },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 1050000,
|
||||
"contextTokens": 272000,
|
||||
"maxTokens": 128000,
|
||||
"cost": { "input": 5, "output": 30, "cacheRead": 0.5, "cacheWrite": 0 },
|
||||
"compat": { "codeMode": "capable" }
|
||||
"compat": { "codeMode": "capable", "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh"] }
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.4-mini",
|
||||
|
||||
@@ -51,26 +51,55 @@ describe("github-copilot provider-policy-api", () => {
|
||||
).toContain("max");
|
||||
});
|
||||
|
||||
it("does not expose max for non-Anthropic Copilot transports", () => {
|
||||
it("appends max when GPT catalog compat advertises it", () => {
|
||||
expect(
|
||||
resolveThinkingProfile({
|
||||
provider: "github-copilot",
|
||||
modelId: "future-copilot-model",
|
||||
modelId: "gpt-5.6-sol",
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high", "max"] },
|
||||
})?.levels.map((level) => level.id),
|
||||
).toContain("max");
|
||||
});
|
||||
|
||||
it.each([undefined, null])("does not expose older Claude adaptive effort with api=%s", (api) => {
|
||||
expect(
|
||||
resolveThinkingProfile({
|
||||
provider: "github-copilot",
|
||||
modelId: "claude-opus-4-5",
|
||||
api,
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high", "max"] },
|
||||
})?.levels.map((level) => level.id),
|
||||
).not.toContain("max");
|
||||
});
|
||||
|
||||
it("does not expose adaptive effort for older Claude models", () => {
|
||||
it.each([
|
||||
{ supportedReasoningEfforts: ["low", "medium", "high"] },
|
||||
{ supportedReasoningEfforts: [] },
|
||||
{ supportsReasoningEffort: false, supportedReasoningEfforts: ["xhigh", "max"] },
|
||||
])("honors explicit catalog limits before static GPT metadata: %j", (compat) => {
|
||||
expect(
|
||||
resolveThinkingProfile({
|
||||
provider: "github-copilot",
|
||||
modelId: "claude-opus-4-5",
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high", "max"] },
|
||||
})?.levels.map((level) => level.id),
|
||||
).not.toContain("max");
|
||||
modelId: "gpt-5.6-luna",
|
||||
compat,
|
||||
})?.levels.map(({ id }) => id),
|
||||
).toEqual(["off", "minimal", "low", "medium", "high"]);
|
||||
});
|
||||
|
||||
it.each(["openai-completions", undefined, null])(
|
||||
"does not expose Gemini max with api=%s",
|
||||
(api) => {
|
||||
expect(
|
||||
resolveThinkingProfile({
|
||||
provider: "github-copilot",
|
||||
modelId: "gemini-3.6-flash",
|
||||
api,
|
||||
compat: { supportedReasoningEfforts: ["low", "medium", "high", "max"] },
|
||||
})?.levels.map(({ id }) => id),
|
||||
).not.toContain("max");
|
||||
},
|
||||
);
|
||||
|
||||
it("appends xhigh for static Copilot metadata overrides", () => {
|
||||
expect(
|
||||
resolveThinkingProfile({
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// Github Copilot API module exposes the plugin public contract.
|
||||
import type { ProviderDefaultThinkingPolicyContext } from "openclaw/plugin-sdk/core";
|
||||
import { resolveCopilotExtendedThinkingLevels } from "./model-metadata.js";
|
||||
import { resolveCopilotThinkingLevelMap } from "./model-metadata.js";
|
||||
|
||||
export function resolveThinkingProfile(context: ProviderDefaultThinkingPolicyContext) {
|
||||
if (context.provider.trim().toLowerCase() !== "github-copilot") {
|
||||
return null;
|
||||
}
|
||||
const extendedLevels = resolveCopilotExtendedThinkingLevels(context.modelId, context.compat);
|
||||
const thinkingLevelMap = resolveCopilotThinkingLevelMap(
|
||||
context.modelId,
|
||||
context.compat,
|
||||
context.api,
|
||||
);
|
||||
const extendedLevels = (["xhigh", "max"] as const).filter((id) => thinkingLevelMap?.[id]);
|
||||
|
||||
return {
|
||||
levels: [
|
||||
|
||||
Reference in New Issue
Block a user