mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Route GLM thinking via provider metadata ENG-2019 (#10692)
* Route GLM thinking via provider metadata * Address GLM provider routing review feedback
This commit is contained in:
@@ -15,6 +15,11 @@ alwaysApply: true
|
||||
quirks, default behavior, or wire-format details.
|
||||
- Stable, reliable known-model facts belong in typed `ModelInfo.metadata`
|
||||
helpers or `src/providers/model-facts.ts`.
|
||||
- Stable provider routing facts belong in `GatewayProviderMetadata.routing`
|
||||
and the relevant builtin provider manifest. For example, if a native
|
||||
provider uses a known reasoning wire format for a model route, add a typed
|
||||
`GatewayReasoningFormat` value and route metadata instead of matching that
|
||||
provider id directly in a rule predicate.
|
||||
- Provider wire-format encoding belongs in `PROVIDER_OPTION_RULES` and codec
|
||||
helpers under `src/providers/routing`.
|
||||
- Local or dynamic provider fallbacks, such as Ollama or routed model-id
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
## Next Release
|
||||
|
||||
- Supports Bedrock bearer API keys, direct IAM credentials, AWS profiles, and the default AWS SDK credential chain
|
||||
- Supports Bedrock bearer API keys, direct IAM credentials, AWS profiles, and the default AWS SDK credential chain
|
||||
- Routes Z.AI GLM thinking through provider metadata while preserving generic thinking suppression for non-GLM Z.AI custom models
|
||||
|
||||
@@ -103,4 +103,24 @@ describe("built-in provider metadata", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes native Z.AI providers through GLM thinking metadata", async () => {
|
||||
for (const providerId of ["zai", "zai-coding-plan"] as const) {
|
||||
await expect(getProvider(providerId)).resolves.toMatchObject({
|
||||
metadata: {
|
||||
routing: {
|
||||
reasoning: {
|
||||
format: "glm-thinking",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const models = Object.values(await getModelsForProvider(providerId));
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
for (const model of models) {
|
||||
expect(model.family?.startsWith("glm")).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ANTHROPIC_ROUTING_METADATA,
|
||||
QWEN_CACHE_ROUTING_METADATA,
|
||||
} from "./routing/anthropic-compatible";
|
||||
import { GLM_THINKING_ROUTING_METADATA } from "./routing/glm-thinking";
|
||||
|
||||
export const DEFAULT_INTERNAL_OCA_BASE_URL =
|
||||
"https://code-internal.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm";
|
||||
@@ -507,6 +508,7 @@ const OPENAI_COMPATIBLE_SPECS: BuiltinSpec[] = [
|
||||
apiKeyEnv: ["ZHIPU_API_KEY"],
|
||||
modelsProviderId: "zai",
|
||||
defaults: { baseUrl: "https://api.z.ai/api/paas/v4" },
|
||||
metadata: GLM_THINKING_ROUTING_METADATA,
|
||||
},
|
||||
{
|
||||
id: "zai-coding-plan",
|
||||
@@ -518,6 +520,7 @@ const OPENAI_COMPATIBLE_SPECS: BuiltinSpec[] = [
|
||||
apiKeyEnv: ["ZHIPU_API_KEY"],
|
||||
modelsProviderId: "zai-coding-plan",
|
||||
defaults: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
||||
metadata: GLM_THINKING_ROUTING_METADATA,
|
||||
},
|
||||
{
|
||||
id: "moonshot",
|
||||
|
||||
@@ -2712,7 +2712,7 @@ describe("sdk-gateway", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not apply Z.AI GLM thinking controls to non-GLM native Z.AI models", async () => {
|
||||
it("does not apply generic thinking to non-GLM native Z.AI custom models", async () => {
|
||||
streamTextSpy.mockReturnValue({
|
||||
fullStream: makeStreamParts([
|
||||
{ type: "finish", usage: { inputTokens: 1, outputTokens: 1 } },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
GatewayModelRoute,
|
||||
GatewayReasoningFormat,
|
||||
GatewayProviderContext,
|
||||
GatewayStreamRequest,
|
||||
} from "@cline/shared";
|
||||
@@ -128,6 +129,25 @@ export function modelRouteMatches(
|
||||
}
|
||||
}
|
||||
|
||||
export function providerReasoningRouteMatches(
|
||||
format: GatewayReasoningFormat,
|
||||
request: Pick<GatewayStreamRequest, "modelId">,
|
||||
context: GatewayProviderContext,
|
||||
): boolean {
|
||||
const reasoning = context.provider.metadata?.routing?.reasoning;
|
||||
if (reasoning?.format !== format) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return reasoning.routes.some((route) =>
|
||||
modelRouteMatches(route, {
|
||||
modelId: request.modelId,
|
||||
family: resolveModelFamily(context),
|
||||
capabilities: context.model.capabilities,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function isGlmModel(
|
||||
request: Pick<GatewayStreamRequest, "modelId">,
|
||||
context: GatewayProviderContext,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
GatewayProviderContext,
|
||||
GatewayProviderMetadata,
|
||||
GatewayStreamRequest,
|
||||
} from "@cline/shared";
|
||||
import { isGlmModel } from "../model-facts";
|
||||
@@ -14,9 +15,18 @@ import type { ProviderOptionsPatch } from "./utils";
|
||||
* composer can rely on merge order instead of out-of-band flags.
|
||||
*/
|
||||
|
||||
export function isNativeZaiProvider(providerId: string): boolean {
|
||||
return providerId === "zai" || providerId === "zai-coding-plan";
|
||||
}
|
||||
export const GLM_THINKING_ROUTING_METADATA: GatewayProviderMetadata = {
|
||||
routing: {
|
||||
reasoning: {
|
||||
format: "glm-thinking",
|
||||
routes: [
|
||||
{ matcher: "model-family", family: "glm" },
|
||||
{ matcher: "model-family", family: "glm-air" },
|
||||
{ matcher: "model-family", family: "glm-flash" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function buildNativeZaiThinkingOptions(request: GatewayStreamRequest) {
|
||||
if (request.reasoning?.enabled === undefined) {
|
||||
@@ -47,28 +57,32 @@ function buildRoutedGlmReasoningOptions(request: GatewayStreamRequest) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function buildGlmThinkingProviderOptionsPatch(
|
||||
export function buildNativeGlmThinkingProviderOptionsPatch(
|
||||
request: GatewayStreamRequest,
|
||||
providerOptionsKey: string,
|
||||
): ProviderOptionsPatch | undefined {
|
||||
// Native Z.AI GLM endpoints expect `thinking.type`; they do not accept the
|
||||
// routed `reasoning.enabled` / `reasoning.exclude` shape.
|
||||
const nativeThinking = buildNativeZaiThinkingOptions(request);
|
||||
return nativeThinking
|
||||
? {
|
||||
openaiCompatible: nativeThinking,
|
||||
[request.providerId]: nativeThinking,
|
||||
...(providerOptionsKey !== request.providerId
|
||||
? { [providerOptionsKey]: nativeThinking }
|
||||
: {}),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function buildRoutedGlmReasoningProviderOptionsPatch(
|
||||
request: GatewayStreamRequest,
|
||||
context: GatewayProviderContext,
|
||||
providerOptionsKey: string,
|
||||
options?: { includeProviderBuckets?: boolean },
|
||||
): ProviderOptionsPatch | undefined {
|
||||
if (isNativeZaiProvider(request.providerId)) {
|
||||
if (!isGlmModel(request, context)) {
|
||||
return undefined;
|
||||
}
|
||||
const nativeThinking = buildNativeZaiThinkingOptions(request);
|
||||
return nativeThinking
|
||||
? {
|
||||
openaiCompatible: nativeThinking,
|
||||
[request.providerId]: nativeThinking,
|
||||
...(providerOptionsKey !== request.providerId
|
||||
? { [providerOptionsKey]: nativeThinking }
|
||||
: {}),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// Routed GLM endpoints stay OpenAI-compatible and use the generic
|
||||
// `reasoning` include/exclude shape instead of native Z.AI `thinking.type`.
|
||||
if (!isGlmModel(request, context)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { buildGatewayReasoningOptions } from "./anthropic-compatible";
|
||||
import { buildOpenAINativeProviderOptions } from "./generic-compatible";
|
||||
import {
|
||||
buildGlmThinkingProviderOptionsPatch,
|
||||
isNativeZaiProvider,
|
||||
} from "./glm-thinking";
|
||||
import {
|
||||
isDeepSeekFamily,
|
||||
isGlmModel,
|
||||
isKimiK26Family as isKimiK26FamilyFact,
|
||||
isMoonshotKimiModelIdFallback,
|
||||
modelReasoningDefaultsOn,
|
||||
providerReasoningRouteMatches,
|
||||
} from "../model-facts";
|
||||
import { buildGatewayReasoningOptions } from "./anthropic-compatible";
|
||||
import { buildOpenAINativeProviderOptions } from "./generic-compatible";
|
||||
import {
|
||||
buildNativeGlmThinkingProviderOptionsPatch,
|
||||
buildRoutedGlmReasoningProviderOptionsPatch,
|
||||
} from "./glm-thinking";
|
||||
import type {
|
||||
MatchedProviderOptionRule,
|
||||
ProviderOptionBuildInput,
|
||||
@@ -54,6 +55,25 @@ function isOllamaReasoningDefaultOnDisable(
|
||||
);
|
||||
}
|
||||
|
||||
function usesGlmThinkingProviderRouting(
|
||||
input: ProviderOptionMatchInput,
|
||||
): boolean {
|
||||
return providerReasoningRouteMatches(
|
||||
"glm-thinking",
|
||||
input.request,
|
||||
input.context,
|
||||
);
|
||||
}
|
||||
|
||||
function hasGlmThinkingProviderRouting(
|
||||
input: ProviderOptionMatchInput,
|
||||
): boolean {
|
||||
return (
|
||||
input.context.provider.metadata?.routing?.reasoning?.format ===
|
||||
"glm-thinking"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveFamilyThinkingType(
|
||||
input: ProviderOptionMatchInput,
|
||||
defaultWhenUnset: "enabled" | "disabled" | undefined,
|
||||
@@ -285,31 +305,28 @@ const ollamaReasoningDefaultOnDisableRule: ProviderOptionRule = {
|
||||
},
|
||||
};
|
||||
|
||||
const nativeZaiNonGlmSuppressionRule: ProviderOptionRule = {
|
||||
id: "provider.zai.non-glm.suppress-generic-thinking",
|
||||
const nonGlmProviderRoutingSuppressionRule: ProviderOptionRule = {
|
||||
id: "provider.routing.glm-thinking.non-glm.suppress-generic-thinking",
|
||||
phase: "provider",
|
||||
description:
|
||||
"Native Z.AI non-GLM models should not inherit adaptive OpenAI-compatible thinking.",
|
||||
"Providers with GLM thinking routing should not apply generic adaptive thinking to non-GLM models.",
|
||||
applies: (input) =>
|
||||
isNativeZaiProvider(input.request.providerId) &&
|
||||
hasGlmThinkingProviderRouting(input) &&
|
||||
input.request.reasoning?.enabled !== undefined &&
|
||||
!isGlmModel(input.request, input.context),
|
||||
!usesGlmThinkingProviderRouting(input),
|
||||
suppresses: { genericThinking: true },
|
||||
build: () => undefined,
|
||||
};
|
||||
|
||||
const nativeZaiGlmThinkingRule: ProviderOptionRule = {
|
||||
id: "family.glm.native-zai-thinking",
|
||||
id: "provider.routing.glm-thinking",
|
||||
phase: "model-overlay",
|
||||
description: "Native Z.AI GLM models use thinking.type.",
|
||||
applies: (input) =>
|
||||
isNativeZaiProvider(input.request.providerId) &&
|
||||
isGlmModel(input.request, input.context),
|
||||
description: "Providers routed to the GLM thinking format use thinking.type.",
|
||||
applies: usesGlmThinkingProviderRouting,
|
||||
suppresses: { genericThinking: true },
|
||||
build: (input) =>
|
||||
buildGlmThinkingProviderOptionsPatch(
|
||||
buildNativeGlmThinkingProviderOptionsPatch(
|
||||
input.request,
|
||||
input.context,
|
||||
input.providerOptionsKey,
|
||||
),
|
||||
};
|
||||
@@ -320,11 +337,11 @@ const routedGlmReasoningRule: ProviderOptionRule = {
|
||||
description:
|
||||
"Routed GLM models use the generic reasoning include/exclude shape, not thinking.type.",
|
||||
applies: (input) =>
|
||||
!isNativeZaiProvider(input.request.providerId) &&
|
||||
!usesGlmThinkingProviderRouting(input) &&
|
||||
isGlmModel(input.request, input.context),
|
||||
suppresses: { genericThinking: true },
|
||||
build: (input) =>
|
||||
buildGlmThinkingProviderOptionsPatch(
|
||||
buildRoutedGlmReasoningProviderOptionsPatch(
|
||||
input.request,
|
||||
input.context,
|
||||
input.providerOptionsKey,
|
||||
@@ -353,7 +370,7 @@ export const PROVIDER_OPTION_RULES: ReadonlyArray<ProviderOptionRule> = [
|
||||
kimiK26ThinkingRule,
|
||||
deepSeekThinkingRule,
|
||||
ollamaReasoningDefaultOnDisableRule,
|
||||
nativeZaiNonGlmSuppressionRule,
|
||||
nonGlmProviderRoutingSuppressionRule,
|
||||
nativeZaiGlmThinkingRule,
|
||||
routedGlmReasoningRule,
|
||||
];
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
GatewayStreamRequest,
|
||||
} from "@cline/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { GLM_THINKING_ROUTING_METADATA } from "./glm-thinking";
|
||||
import {
|
||||
composeAiSdkProviderOptions,
|
||||
mergeProviderOptionPatches,
|
||||
@@ -770,6 +771,7 @@ describe("composeAiSdkProviderOptions: family/provider thinking patches", () =>
|
||||
modelId: "glm-4.7",
|
||||
reasoning: { enabled: true },
|
||||
},
|
||||
context: { family: "glm", metadata: GLM_THINKING_ROUTING_METADATA },
|
||||
expect: [
|
||||
{ bucket: "zai", has: { thinking: { type: "enabled" } } },
|
||||
{
|
||||
@@ -779,6 +781,19 @@ describe("composeAiSdkProviderOptions: family/provider thinking patches", () =>
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "native zai custom non-GLM -> no generic adaptive thinking",
|
||||
request: {
|
||||
providerId: "zai",
|
||||
modelId: "zai-other-model",
|
||||
reasoning: { enabled: true },
|
||||
},
|
||||
context: { family: "other", metadata: GLM_THINKING_ROUTING_METADATA },
|
||||
expect: [
|
||||
{ bucket: "zai", lacks: ["thinking", "reasoning"] },
|
||||
{ bucket: "openaiCompatible", lacks: ["thinking", "reasoning"] },
|
||||
],
|
||||
},
|
||||
// Kimi K2.6 family: explicit enabled/disabled and unset defaults to enabled
|
||||
{
|
||||
name: "cline Kimi K2.6 family reasoning.enabled=false -> thinking.type=disabled",
|
||||
|
||||
@@ -35,7 +35,7 @@ export type GatewayModelCapability =
|
||||
export type GatewayPromptCacheStrategy = "anthropic-automatic";
|
||||
export type GatewayUsageCostDisplay = "show" | "hide";
|
||||
export type GatewayPromptCacheFormat = "anthropic-cache-control";
|
||||
export type GatewayReasoningFormat = "anthropic-thinking";
|
||||
export type GatewayReasoningFormat = "anthropic-thinking" | "glm-thinking";
|
||||
export type GatewayModelRoute =
|
||||
| { matcher: "anthropic-compatible" }
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user