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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user