diff --git a/sdk/packages/llms/src/providers/routing/generic-compatible.ts b/sdk/packages/llms/src/providers/routing/generic-compatible.ts index 5041787b03..8b98671fbc 100644 --- a/sdk/packages/llms/src/providers/routing/generic-compatible.ts +++ b/sdk/packages/llms/src/providers/routing/generic-compatible.ts @@ -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, diff --git a/sdk/packages/llms/src/providers/routing/provider-options.test.ts b/sdk/packages/llms/src/providers/routing/provider-options.test.ts index 14e1964bfd..7cd5f90b80 100644 --- a/sdk/packages/llms/src/providers/routing/provider-options.test.ts +++ b/sdk/packages/llms/src/providers/routing/provider-options.test.ts @@ -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: { diff --git a/sdk/packages/llms/src/providers/vendors/cline.test.ts b/sdk/packages/llms/src/providers/vendors/cline.test.ts index 0a916f4f73..a865725637 100644 --- a/sdk/packages/llms/src/providers/vendors/cline.test.ts +++ b/sdk/packages/llms/src/providers/vendors/cline.test.ts @@ -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(); @@ -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; + }; + + // 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)); diff --git a/sdk/packages/llms/src/providers/vendors/cline.ts b/sdk/packages/llms/src/providers/vendors/cline.ts index c83ed0b9de..c51f4e29f1 100644 --- a/sdk/packages/llms/src/providers/vendors/cline.ts +++ b/sdk/packages/llms/src/providers/vendors/cline.ts @@ -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; fetch?: typeof fetch; onResponseError?: (response: Response) => Promise | 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 { 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),