fix(catalog): gate copilot claude reasoning inference on the id classifier

Addresses Codex review on #6666: marking a reference-less Copilot Claude
reasoning-capable solely because it uses the anthropic-messages proxy would
fabricate a thinking effort dial for a pre-thinking generation (e.g.
claude-sonnet-3.5 from a lagging enterprise catalog), which rejects thinking
parameters.

Add the centralized `anthropicModelSupportsThinking` predicate (Sonnet/Opus
3.7+, all 4.x/5+, Fable/Mythos; pre-thinking 3.5 and older excluded) next to
the other classifier-based Anthropic capability gates, and require it in
addition to the transport before defaulting reasoning to true.

Fixes #6664
This commit is contained in:
roboomp
2026-07-26 10:54:53 +00:00
parent 29cf093808
commit db3ace6352
3 changed files with 46 additions and 6 deletions
+13
View File
@@ -261,6 +261,19 @@ export const modelFamilyToken = memo((modelId: string): string => {
return "";
});
/**
* True for Claude generations that support extended thinking: Sonnet/Opus 3.7+,
* every 4.x/5+ Opus/Sonnet, and the Fable/Mythos generation. Pre-thinking
* models (Claude 3.5 and older) are excluded so no thinking effort dial is
* fabricated for a model that rejects thinking parameters. Classifier-based, so
* dotted and dashed version forms both match; ids the classifier does not parse
* (e.g. Haiku, bare dated ids) return false.
*/
export const anthropicModelSupportsThinking = memo((modelId: string): boolean => {
const parsed = parseAnthropicModel(bareModelId(modelId));
return parsed !== null && semverGte(parsed.version, "3.7");
});
/**
* Adaptive thinking `display` is supported starting with Claude Opus 4.7+,
* Sonnet 5+, and the Claude Fable/Mythos 5 generation. Older adaptive-thinking
@@ -7,6 +7,7 @@ import {
import { Effort, THINKING_EFFORTS } from "../effort";
import { FIREWORKS_FAST_SUFFIX, toFireworksPublicModelId } from "../fireworks-model-id";
import {
anthropicModelSupportsThinking,
isGlmVisionModelId,
isGrokReasoningEffortCapable,
isKimiK3ModelId,
@@ -4385,12 +4386,16 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
maxTokens,
headers: { ...COPILOT_API_HEADERS },
// Copilot's `/models` advertises no reasoning bit, so a
// Claude model without a bundled reference would fall back
// to `reasoning: false` and lose its thinking dial. Every
// Claude on Copilot's anthropic-messages proxy reasons;
// mark it so `buildModel` derives the adaptive effort
// ladder from the id (e.g. a newly served claude-opus-5).
...(api === "anthropic-messages" ? { reasoning: true } : {}),
// thinking-capable Claude with no bundled reference would
// fall back to `reasoning: false` and lose its effort dial.
// Gate on the id classifier (not the transport alone) so a
// lagging enterprise catalog serving a pre-thinking Claude
// (<= 3.5) over the Messages proxy is not handed a fabricated
// dial it would reject; a modern reference-less model (e.g.
// claude-opus-5) is marked so `buildModel` derives the ladder.
...(api === "anthropic-messages" && anthropicModelSupportsThinking(defaults.id)
? { reasoning: true }
: {}),
...(api === "openai-completions"
? {
compat: {
@@ -94,4 +94,26 @@ describe("#6664 github-copilot reference-less Claude model", () => {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it("does not fabricate a thinking dial for a pre-thinking reference-less Claude", async () => {
// A lagging enterprise catalog could surface an old kind-first Claude on
// the Messages proxy. It must stay non-reasoning so no effort dial is
// offered for thinking parameters the model would reject.
const fetchMock = vi.fn(
async () =>
new Response(JSON.stringify({ data: [tieredEntry("claude-sonnet-3.5", "Claude Sonnet 3.5")] }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
const options = githubCopilotModelManagerOptions({ apiKey: "copilot-test-key", fetch: fetchMock });
const specs = (await options.fetchDynamicModels?.()) ?? [];
const base = specs.find(m => m.id === "claude-sonnet-3.5");
expect(base).toBeDefined();
expect(base?.api).toBe("anthropic-messages");
expect(base?.reasoning).toBe(false);
if (!base) throw new Error("missing base spec");
expect(buildModel(base).thinking).toBeUndefined();
});
});