mirror of
https://github.com/cline/cline.git
synced 2026-08-29 03:52:41 +08:00
fix(llms): keep ClinePass provider options on the wire in the shared Cline provider
The shared Cline provider hardcoded the AI SDK provider name to "cline", but the openai-compatible model reads request-body passthrough options from providerOptions[<name>]. Option routing emits ClinePass options under the "cline-pass"/"clinePass" buckets, so gateway reasoning (extended thinking budgets) silently stopped reaching the wire for cline-pass after it moved off the generic openai-compatible module. Thread the gateway provider id through as the provider name, and restore strictJsonSchema: false for the new "cline" provider-options target so the wire format matches the previous openai-compatible behavior. Add cline-pass coverage at both the option-routing and request-body levels.
This commit is contained in:
@@ -58,7 +58,12 @@ export function buildCompatibleProviderOptions(options: {
|
||||
const promptCache = hasPromptCacheRoute ? createEphemeralCacheControl() : {};
|
||||
|
||||
return {
|
||||
...(target === "openai-compatible" ? { strictJsonSchema: false } : {}),
|
||||
// The "cline" target is the OpenAI-compatible Cline provider; keep the
|
||||
// same relaxed JSON schema behavior it had before it split off from the
|
||||
// generic openai-compatible target.
|
||||
...(target === "openai-compatible" || target === "cline"
|
||||
? { strictJsonSchema: false }
|
||||
: {}),
|
||||
...buildCompatibleThinkingOptions({ request, context, suppressions }),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...promptCache,
|
||||
|
||||
@@ -593,6 +593,27 @@ describe("composeAiSdkProviderOptions: Anthropic thinking precedence", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "ClinePass-routed Sonnet 4.5 -> gateway reasoning under provider-id and alias buckets",
|
||||
request: {
|
||||
providerId: "cline-pass",
|
||||
modelId: "anthropic/claude-sonnet-4-5",
|
||||
reasoning: { enabled: true, effort: "low" },
|
||||
},
|
||||
context: { family: "claude-sonnet" },
|
||||
expect: [
|
||||
{
|
||||
bucket: "cline-pass",
|
||||
has: { reasoning: { enabled: true, max_tokens: 1024 } },
|
||||
lacks: ["thinking"],
|
||||
},
|
||||
{
|
||||
bucket: "clinePass",
|
||||
has: { reasoning: { enabled: true, max_tokens: 1024 } },
|
||||
lacks: ["thinking"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "legacy custom Claude with promptCacheStrategy -> Anthropic reasoning",
|
||||
request: {
|
||||
|
||||
+38
-1
@@ -1,5 +1,6 @@
|
||||
import type { GatewayResolvedProviderConfig } from "@cline/shared";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createCline } from "./cline";
|
||||
import { createCline, createClineProviderModule } from "./cline";
|
||||
|
||||
describe("createCline", () => {
|
||||
const fetchMock = vi.fn<typeof fetch>();
|
||||
@@ -112,6 +113,42 @@ describe("createCline", () => {
|
||||
expect(body).not.toHaveProperty("max_tokens");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"cline",
|
||||
"cline-pass",
|
||||
])("applies %s provider-options buckets to the request body", async (providerId) => {
|
||||
const modelId = "anthropic/claude-sonnet-4.6";
|
||||
fetchMock.mockResolvedValue(jsonCompletionResponse(modelId));
|
||||
const module = await createClineProviderModule(
|
||||
{
|
||||
providerId,
|
||||
apiKey: "test-key",
|
||||
baseUrl: "https://api.cline.bot/api/v1",
|
||||
fetch: fetchMock,
|
||||
} as unknown as GatewayResolvedProviderConfig,
|
||||
{ provider: { id: providerId } } as never,
|
||||
);
|
||||
const model = module.model(modelId) as {
|
||||
doGenerate: (options: unknown) => Promise<unknown>;
|
||||
};
|
||||
|
||||
// The gateway routes Cline options under the concrete provider id
|
||||
// and its camelCase alias (see clineGatewayReasoningRule +
|
||||
// buildProviderAndAliasPatch). The provider name must match the
|
||||
// provider id for these to reach the wire.
|
||||
const bucket = { reasoning: { enabled: true, max_tokens: 1024 } };
|
||||
await model.doGenerate({
|
||||
prompt: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
||||
providerOptions:
|
||||
providerId === "cline"
|
||||
? { cline: bucket }
|
||||
: { "cline-pass": bucket, clinePass: bucket },
|
||||
});
|
||||
|
||||
const body = capturedRequestBody(fetchMock);
|
||||
expect(body.reasoning).toEqual({ enabled: true, max_tokens: 1024 });
|
||||
});
|
||||
|
||||
it("keeps max_tokens for non-reasoning models", async () => {
|
||||
const modelId = "anthropic/claude-sonnet-4.6";
|
||||
fetchMock.mockResolvedValue(jsonCompletionResponse(modelId));
|
||||
|
||||
+13
-2
@@ -34,6 +34,13 @@ export interface ClineWebSearchOptions {
|
||||
export interface ClineProviderOptions {
|
||||
apiKey?: string;
|
||||
baseURL: string;
|
||||
/**
|
||||
* AI SDK provider name, used as the `providerOptions` lookup key for
|
||||
* request-body passthrough. Must match the gateway provider id ("cline" or
|
||||
* "cline-pass") so options routed under that id (e.g. gateway reasoning)
|
||||
* reach the wire. Defaults to "cline".
|
||||
*/
|
||||
name?: string;
|
||||
headers?: Record<string, string>;
|
||||
fetch?: typeof fetch;
|
||||
onResponseError?: (response: Response) => Promise<void> | void;
|
||||
@@ -181,7 +188,7 @@ export interface ClineProvider {
|
||||
export function createCline(options: ClineProviderOptions): ClineProvider {
|
||||
const providerFetch = createClineFetch(options);
|
||||
const compatible = createOpenAICompatible({
|
||||
name: "cline",
|
||||
name: options.name ?? "cline",
|
||||
baseURL: withoutTrailingSlash(options.baseURL),
|
||||
apiKey: options.apiKey,
|
||||
headers: options.headers,
|
||||
@@ -220,11 +227,15 @@ function readResponseErrorHandler(
|
||||
|
||||
export async function createClineProviderModule(
|
||||
config: GatewayResolvedProviderConfig,
|
||||
_context: GatewayProviderContext,
|
||||
context: GatewayProviderContext,
|
||||
): Promise<ProviderFactoryResult> {
|
||||
const cline = createCline({
|
||||
apiKey: await resolveApiKey(config),
|
||||
baseURL: config.baseUrl ?? "https://api.cline.bot/api/v1",
|
||||
// Keep the provider name aligned with the gateway provider id so
|
||||
// provider options emitted under "cline-pass"/"clinePass" buckets are
|
||||
// still applied when this module serves ClinePass.
|
||||
name: context.provider.id,
|
||||
headers: config.headers,
|
||||
fetch: config.fetch,
|
||||
onResponseError: readResponseErrorHandler(config),
|
||||
|
||||
Reference in New Issue
Block a user