mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d662b7e7e4 | |||
| c1d1f1099c | |||
| 29b4c42f35 | |||
| 53ba93512f | |||
| 949e176e17 | |||
| 497f4580af | |||
| 15dbdf762e |
@@ -49,6 +49,7 @@ export async function initialize(storageContext: StorageContext): Promise<Webvie
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to initialize storage. Please check logs for details or try restarting the client.",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
// Register host-only SDK provider handlers (e.g. VS Code Language Model API),
|
||||
|
||||
@@ -55,7 +55,7 @@ async function makeAuthenticatedRequest<T>(endpoint: string, organizationId: str
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ async function makeAuthenticatedRequest<T>(endpoint: string, organizationId: str
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ async function performCommitMsgGeneration(controller: Controller, gitDiff: strin
|
||||
// transform that doesn't need extended thinking; disabling reasoning also
|
||||
// avoids sending both reasoning.effort and reasoning.max_tokens, which
|
||||
// some providers (e.g. OpenRouter) reject.
|
||||
const apiHandler = buildApiHandler(apiConfiguration, currentMode, { disableReasoning: true })
|
||||
const apiHandler = await buildApiHandler(apiConfiguration, currentMode, { disableReasoning: true })
|
||||
|
||||
// Create a system prompt
|
||||
const systemPrompt = PROMPT.system
|
||||
|
||||
@@ -95,5 +95,10 @@ export const HostRegistryInfo = {
|
||||
const ide = host.clineType || "unknown"
|
||||
hostInfo = { hostVersion, extensionVersion, platform, os, ide, distinctId }
|
||||
},
|
||||
get: () => hostInfo,
|
||||
get: () => {
|
||||
if (!hostInfo) {
|
||||
throw new Error("HostRegistryInfo has not been initialized. Call initialize() before using host metadata.")
|
||||
}
|
||||
return hostInfo
|
||||
},
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export class ClineAccountService {
|
||||
headers: {
|
||||
Authorization: `Bearer ${clineAccountAuthToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
...config.headers,
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
|
||||
@@ -77,7 +77,7 @@ vi.mock("@/shared/net", () => ({
|
||||
|
||||
// Mock buildBasicClineHeaders
|
||||
vi.mock("@/services/EnvUtils", () => ({
|
||||
buildBasicClineHeaders: async () => ({}),
|
||||
buildBasicClineHeaders: () => ({}),
|
||||
}))
|
||||
|
||||
// Mock feature flags
|
||||
|
||||
@@ -255,7 +255,7 @@ export class AuthService {
|
||||
headers: {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
})
|
||||
@@ -483,7 +483,7 @@ export class AuthService {
|
||||
apiBaseUrl,
|
||||
// Use WorkOS device auth so the browser confirmation code can be surfaced in the extension.
|
||||
useWorkOSDeviceAuth: true,
|
||||
headers: await buildBasicClineHeaders(),
|
||||
headers: buildBasicClineHeaders(),
|
||||
callbacks: createOAuthClientCallbacks({
|
||||
onOutput: (message) => {
|
||||
resolveAuthMessage(message)
|
||||
@@ -544,7 +544,7 @@ export class AuthService {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: "test-personal-token",
|
||||
@@ -755,7 +755,7 @@ export class AuthService {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: "authorization_code",
|
||||
|
||||
@@ -24,6 +24,15 @@ const mocks = vi.hoisted(() => {
|
||||
}
|
||||
|
||||
return {
|
||||
buildClineRequestMetadata: vi.fn(async () => ({
|
||||
userAgent: "Cline/test",
|
||||
clientName: "VSCode Extension",
|
||||
clientVersion: "1.2.3",
|
||||
platform: "Visual Studio Code",
|
||||
platformVersion: "1.90.0",
|
||||
coreVersion: "1.2.3",
|
||||
isMultiRoot: false,
|
||||
})),
|
||||
getDistinctId: vi.fn(() => "test-distinct-id"),
|
||||
getProviderSettingsManager: vi.fn(() => providerSettingsManager),
|
||||
providerSettingsManager,
|
||||
@@ -53,6 +62,52 @@ vi.mock("@/services/logging/distinctId", () => ({
|
||||
getDistinctId: mocks.getDistinctId,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/EnvUtils", () => ({
|
||||
buildClineRequestMetadata: mocks.buildClineRequestMetadata,
|
||||
}))
|
||||
|
||||
vi.mock("@cline/llms", () => {
|
||||
const generatedModels = {
|
||||
cline: {
|
||||
"anthropic/claude-sonnet-4.6": { id: "anthropic/claude-sonnet-4.6" },
|
||||
},
|
||||
"cline-pass": {
|
||||
"cline-pass/glm-5.1": { id: "cline-pass/glm-5.1" },
|
||||
},
|
||||
gemini: {
|
||||
"gemini-3.5-flash": { id: "gemini-3.5-flash" },
|
||||
},
|
||||
"openai-compatible": {
|
||||
"gpt-4o": { id: "gpt-4o" },
|
||||
},
|
||||
}
|
||||
return {
|
||||
getGeneratedModelsForProvider: (providerId: keyof typeof generatedModels) => generatedModels[providerId] ?? {},
|
||||
MODEL_COLLECTIONS_BY_PROVIDER_ID: {
|
||||
cline: {
|
||||
provider: { name: "Cline", defaultModelId: "anthropic/claude-sonnet-4.6" },
|
||||
models: {},
|
||||
},
|
||||
"cline-pass": {
|
||||
provider: { name: "ClinePass", defaultModelId: "cline-pass/glm-5.1" },
|
||||
models: {},
|
||||
},
|
||||
gemini: {
|
||||
provider: { name: "Gemini", defaultModelId: "gemini-3.5-pro" },
|
||||
models: {},
|
||||
},
|
||||
"openai-compatible": {
|
||||
provider: { name: "OpenAI Compatible", defaultModelId: "gpt-4o", baseUrl: "https://api.openai.com/v1" },
|
||||
models: {},
|
||||
},
|
||||
ollama: {
|
||||
provider: { name: "Ollama", baseUrl: "http://localhost:11434/v1" },
|
||||
models: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("./provider-migration", () => ({
|
||||
getProviderSettingsManager: mocks.getProviderSettingsManager,
|
||||
}))
|
||||
@@ -88,6 +143,15 @@ beforeEach(() => {
|
||||
})
|
||||
mocks.providerSettingsManager.getLastUsedProviderSettings.mockReturnValue(undefined)
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(undefined)
|
||||
mocks.buildClineRequestMetadata.mockResolvedValue({
|
||||
userAgent: "Cline/test",
|
||||
clientName: "VSCode Extension",
|
||||
clientVersion: "1.2.3",
|
||||
platform: "Visual Studio Code",
|
||||
platformVersion: "1.90.0",
|
||||
coreVersion: "1.2.3",
|
||||
isMultiRoot: false,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -193,6 +257,32 @@ describe("buildStartSessionInput", () => {
|
||||
|
||||
expect(result.prompt).toBeUndefined()
|
||||
})
|
||||
|
||||
it("does not rewrite provider headers before start", () => {
|
||||
const headers = {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
"X-Task-ID": "",
|
||||
"X-CLIENT-TYPE": "VSCode Extension",
|
||||
}
|
||||
const config = makeBaseConfig({
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
sessionId: "session-123",
|
||||
headers,
|
||||
providerConfig: {
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
headers,
|
||||
},
|
||||
} as Partial<CoreSessionConfig>)
|
||||
|
||||
const result = buildStartSessionInput(config, { cwd: "/tmp/workspace" })
|
||||
|
||||
expect(result.config).toBe(config)
|
||||
expect(config.headers?.["X-Task-ID"]).toBe("")
|
||||
expect((config.providerConfig as any).headers["X-Task-ID"]).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -289,6 +379,40 @@ describe("buildSessionConfig", () => {
|
||||
expect(config.apiKey).toBe("workos:test-access-token")
|
||||
})
|
||||
|
||||
it("passes VS Code client request metadata without owning Cline provider headers", async () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "cline",
|
||||
actModeClineModelId: "anthropic/claude-sonnet-4.6",
|
||||
clineApiKey: "workos:test-access-token",
|
||||
} as any)
|
||||
|
||||
const config = await buildSessionConfig({ cwd: "/tmp/workspace" })
|
||||
|
||||
expect(mocks.buildClineRequestMetadata).toHaveBeenCalledTimes(1)
|
||||
expect(config.headers).toBeUndefined()
|
||||
expect((config.providerConfig as any).headers).toBeUndefined()
|
||||
expect(config.extensionContext).toMatchObject({
|
||||
client: {
|
||||
name: "cline-vscode",
|
||||
version: "1.2.3",
|
||||
},
|
||||
workspace: {
|
||||
cwd: "/tmp/workspace",
|
||||
ide: "VS Code",
|
||||
platform: process.platform,
|
||||
},
|
||||
requestMetadata: {
|
||||
clientType: "VSCode Extension",
|
||||
clientVersion: "1.2.3",
|
||||
userAgent: "Cline/test",
|
||||
platform: "Visual Studio Code",
|
||||
platformVersion: "1.90.0",
|
||||
coreVersion: "1.2.3",
|
||||
isMultiRoot: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("resolves ClinePass from the shared Cline OAuth credentials", async () => {
|
||||
mocks.providerSettingsManager.getProviderSettings.mockImplementation((providerId?: string) => {
|
||||
if (providerId !== "cline") {
|
||||
|
||||
@@ -26,7 +26,7 @@ import type { Settings } from "@shared/storage/state-keys"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { buildClineRequestMetadata } from "@/services/EnvUtils"
|
||||
import { getFeatureFlagsService } from "@/services/feature-flags"
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import { fetch } from "@/shared/net"
|
||||
@@ -662,6 +662,7 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
// own provider id spelling (e.g. "openai-compatible" rather than the
|
||||
// extension's "openai"). Convert before handing the id to core.
|
||||
const sdkProviderId = toSdkProviderId(providerId)
|
||||
const clineRequestMetadata = await buildClineRequestMetadata()
|
||||
|
||||
// Always pass a providerConfig so the proxy/CA-aware fetch reaches the SDK
|
||||
// gateway; without it the agent loop uses bare global fetch and corporate
|
||||
@@ -713,7 +714,7 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
user: distinctId ? { distinctId } : undefined,
|
||||
client: {
|
||||
name: "cline-vscode",
|
||||
version: ExtensionRegistryInfo.version,
|
||||
version: clineRequestMetadata.clientVersion,
|
||||
},
|
||||
workspace: {
|
||||
rootPath: workspaceRoot,
|
||||
@@ -723,6 +724,15 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
platform: process.platform,
|
||||
mode: mode === "plan" ? "plan" : "act",
|
||||
},
|
||||
requestMetadata: {
|
||||
clientType: clineRequestMetadata.clientName,
|
||||
clientVersion: clineRequestMetadata.clientVersion,
|
||||
userAgent: clineRequestMetadata.userAgent,
|
||||
platform: clineRequestMetadata.platform,
|
||||
platformVersion: clineRequestMetadata.platformVersion,
|
||||
coreVersion: clineRequestMetadata.coreVersion,
|
||||
isMultiRoot: clineRequestMetadata.isMultiRoot,
|
||||
},
|
||||
logger: sdkLogger,
|
||||
},
|
||||
hooks: buildAgentHooks(StateManager.get()),
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { buildSdkProviderConfig } from "./sdk-api-handler"
|
||||
import { buildApiHandler, buildSdkProviderConfig } from "./sdk-api-handler"
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const providerSettingsManager = {
|
||||
getProviderSettings: vi.fn(),
|
||||
}
|
||||
return {
|
||||
buildClineRequestMetadata: vi.fn(async () => ({
|
||||
userAgent: "Cline/test",
|
||||
clientName: "VSCode Extension",
|
||||
clientVersion: "1.2.3",
|
||||
platform: "Visual Studio Code",
|
||||
platformVersion: "1.90.0",
|
||||
coreVersion: "1.2.3",
|
||||
isMultiRoot: false,
|
||||
})),
|
||||
createHandler: vi.fn((config: { modelId?: string }) => ({
|
||||
createMessage: vi.fn(),
|
||||
getModel: vi.fn(() => ({
|
||||
id: config.modelId ?? "",
|
||||
info: {},
|
||||
})),
|
||||
})),
|
||||
getProviderSettingsManager: vi.fn(() => providerSettingsManager),
|
||||
providerSettingsManager,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("@/services/EnvUtils", () => ({
|
||||
buildClineRequestMetadata: mocks.buildClineRequestMetadata,
|
||||
}))
|
||||
|
||||
vi.mock("@cline/llms", async () => {
|
||||
const actual = await vi.importActual<typeof import("@cline/llms")>("@cline/llms")
|
||||
return {
|
||||
...actual,
|
||||
createHandler: mocks.createHandler,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("./provider-migration", () => ({
|
||||
getProviderSettingsManager: mocks.getProviderSettingsManager,
|
||||
}))
|
||||
@@ -82,4 +110,39 @@ describe("buildSdkProviderConfig", () => {
|
||||
})
|
||||
expect(mocks.providerSettingsManager.getProviderSettings).toHaveBeenCalledWith("v0")
|
||||
})
|
||||
|
||||
it("threads VS Code request metadata into standalone Cline handlers", async () => {
|
||||
const handler = await buildApiHandler(
|
||||
{
|
||||
actModeApiProvider: "cline",
|
||||
actModeClineModelId: "anthropic/claude-sonnet-4.6",
|
||||
clineApiKey: "test-key",
|
||||
},
|
||||
"act",
|
||||
{ disableReasoning: true },
|
||||
)
|
||||
|
||||
expect(mocks.buildClineRequestMetadata).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.createHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
providerId: "cline",
|
||||
extensionContext: expect.objectContaining({
|
||||
client: {
|
||||
name: "cline-vscode",
|
||||
version: "1.2.3",
|
||||
},
|
||||
requestMetadata: {
|
||||
clientType: "VSCode Extension",
|
||||
clientVersion: "1.2.3",
|
||||
userAgent: "Cline/test",
|
||||
platform: "Visual Studio Code",
|
||||
platformVersion: "1.90.0",
|
||||
coreVersion: "1.2.3",
|
||||
isMultiRoot: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect((handler.getModel() as { providerId?: string }).providerId).toBe("cline")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { type ApiHandler, createHandler, type ProviderConfig } from "@cline/llms"
|
||||
import type { ApiConfiguration } from "@shared/api"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { buildClineRequestMetadata } from "@/services/EnvUtils"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { buildBedrockProviderConfig } from "./bedrock-config"
|
||||
import { resolveApiKey, resolveBaseUrl, resolveModelId, resolveVertexProviderConfig } from "./cline-session-factory"
|
||||
@@ -24,6 +25,27 @@ export interface BuildApiHandlerOptions {
|
||||
* OpenRouter don't receive a reasoning config at all.
|
||||
*/
|
||||
disableReasoning?: boolean
|
||||
extensionContext?: ProviderConfig["extensionContext"]
|
||||
}
|
||||
|
||||
function buildStandaloneExtensionContext(
|
||||
clineRequestMetadata: Awaited<ReturnType<typeof buildClineRequestMetadata>>,
|
||||
): ProviderConfig["extensionContext"] {
|
||||
return {
|
||||
client: {
|
||||
name: "cline-vscode",
|
||||
version: clineRequestMetadata.clientVersion,
|
||||
},
|
||||
requestMetadata: {
|
||||
clientType: clineRequestMetadata.clientName,
|
||||
clientVersion: clineRequestMetadata.clientVersion,
|
||||
userAgent: clineRequestMetadata.userAgent,
|
||||
platform: clineRequestMetadata.platform,
|
||||
platformVersion: clineRequestMetadata.platformVersion,
|
||||
coreVersion: clineRequestMetadata.coreVersion,
|
||||
isMultiRoot: clineRequestMetadata.isMultiRoot,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,6 +92,7 @@ export function buildSdkProviderConfig(
|
||||
// Bedrock needs its region + structured AWS auth options forwarded to the
|
||||
// SDK gateway. Without these, a pasted Bedrock API key / region is dropped.
|
||||
...(providerId === "bedrock" ? buildBedrockProviderConfig(configuration, mode) : {}),
|
||||
...(options?.extensionContext ? { extensionContext: options.extensionContext } : {}),
|
||||
}
|
||||
|
||||
if (options?.disableReasoning) {
|
||||
@@ -95,8 +118,16 @@ export function buildSdkProviderConfig(
|
||||
* returned handler implements the same `createMessage`/`getModel` surface, so
|
||||
* existing callers continue to work unchanged.
|
||||
*/
|
||||
export function buildApiHandler(configuration: ApiConfiguration, mode: Mode, options?: BuildApiHandlerOptions): ApiHandler {
|
||||
const providerConfig = buildSdkProviderConfig(configuration, mode, options)
|
||||
export async function buildApiHandler(
|
||||
configuration: ApiConfiguration,
|
||||
mode: Mode,
|
||||
options?: BuildApiHandlerOptions,
|
||||
): Promise<ApiHandler> {
|
||||
const extensionContext = options?.extensionContext ?? buildStandaloneExtensionContext(await buildClineRequestMetadata())
|
||||
const providerConfig = buildSdkProviderConfig(configuration, mode, {
|
||||
...options,
|
||||
extensionContext,
|
||||
})
|
||||
const handler = createHandler(providerConfig)
|
||||
const getModel = handler.getModel.bind(handler)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ vi.mock("@/core/storage/remote-config/utils", () => ({
|
||||
isRemoteConfigEnabled,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/EnvUtils", () => ({ buildBasicClineHeaders: async () => ({}) }))
|
||||
vi.mock("@/services/EnvUtils", () => ({ buildBasicClineHeaders: () => ({}) }))
|
||||
vi.mock("@/shared/net", () => ({ getAxiosSettings: () => ({}) }))
|
||||
vi.mock("axios", () => ({ default: { request: vi.fn() } }))
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { EmptyRequest } from "@/shared/proto/cline/common"
|
||||
import { ExtensionRegistryInfo, HostRegistryInfo } from "@/registry"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// Canonical header names for extra client/host context
|
||||
@@ -13,27 +12,61 @@ const ClineHeaders = {
|
||||
IS_MULTIROOT: "X-IS-MULTIROOT",
|
||||
} as const
|
||||
|
||||
export interface ClientIdentityContext {
|
||||
platform: string
|
||||
platformVersion: string
|
||||
clientName: string
|
||||
clientVersion: string
|
||||
userAgent: string
|
||||
coreVersion: string
|
||||
}
|
||||
|
||||
export interface ClineRequestMetadataContext extends ClientIdentityContext {
|
||||
isMultiRoot: boolean
|
||||
}
|
||||
|
||||
export function buildExternalBasicHeaders(): Record<string, string> {
|
||||
return {
|
||||
"User-Agent": `Cline/${ExtensionRegistryInfo.version}`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildBasicClineHeaders(): Promise<Record<string, string>> {
|
||||
const headers: Record<string, string> = buildExternalBasicHeaders()
|
||||
try {
|
||||
const host = await HostProvider.env.getHostVersion(EmptyRequest.create({}))
|
||||
headers[ClineHeaders.PLATFORM] = host.platform || "unknown"
|
||||
headers[ClineHeaders.PLATFORM_VERSION] = host.version || "unknown"
|
||||
headers[ClineHeaders.CLIENT_TYPE] = host.clineType || "unknown"
|
||||
headers[ClineHeaders.CLIENT_VERSION] = host.clineVersion || "unknown"
|
||||
} catch (error) {
|
||||
Logger.log("Failed to get IDE/platform info via HostBridge EnvService.getHostVersion", error)
|
||||
headers[ClineHeaders.PLATFORM] = "unknown"
|
||||
headers[ClineHeaders.PLATFORM_VERSION] = "unknown"
|
||||
headers[ClineHeaders.CLIENT_TYPE] = "unknown"
|
||||
headers[ClineHeaders.CLIENT_VERSION] = "unknown"
|
||||
export function buildClientIdentityContext(): ClientIdentityContext {
|
||||
const host = HostRegistryInfo.get()
|
||||
return {
|
||||
platform: host.platform || "unknown",
|
||||
platformVersion: host.hostVersion || "unknown",
|
||||
clientName: host.ide || "unknown",
|
||||
clientVersion: host.extensionVersion || ExtensionRegistryInfo.version,
|
||||
userAgent: buildExternalBasicHeaders()["User-Agent"],
|
||||
coreVersion: ExtensionRegistryInfo.version,
|
||||
}
|
||||
}
|
||||
|
||||
export async function isMultiRootWorkspace(): Promise<boolean> {
|
||||
try {
|
||||
const { paths } = await HostProvider.workspace.getWorkspacePaths({})
|
||||
return paths.length > 1
|
||||
} catch (error) {
|
||||
Logger.log("Failed to detect multi-root workspace", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildClineRequestMetadata(): Promise<ClineRequestMetadataContext> {
|
||||
return {
|
||||
...buildClientIdentityContext(),
|
||||
isMultiRoot: await isMultiRootWorkspace(),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBasicClineHeaders(): Record<string, string> {
|
||||
const host = buildClientIdentityContext()
|
||||
const headers: Record<string, string> = buildExternalBasicHeaders()
|
||||
headers[ClineHeaders.PLATFORM] = host.platform
|
||||
headers[ClineHeaders.PLATFORM_VERSION] = host.platformVersion
|
||||
headers[ClineHeaders.CLIENT_TYPE] = host.clientName
|
||||
headers[ClineHeaders.CLIENT_VERSION] = host.clientVersion
|
||||
headers[ClineHeaders.CORE_VERSION] = ExtensionRegistryInfo.version
|
||||
|
||||
return headers
|
||||
|
||||
@@ -69,7 +69,7 @@ export class ClineAccountService {
|
||||
headers: {
|
||||
Authorization: `Bearer ${clineAccountAuthToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
...config.headers,
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
|
||||
@@ -71,9 +71,6 @@ export class BannerService {
|
||||
return BannerService.instance
|
||||
}
|
||||
const hostInfo = HostRegistryInfo.get()
|
||||
if (!hostInfo) {
|
||||
throw new Error("[BannerService] Ensure HostRegistryInfo is initialized before BannerService.")
|
||||
}
|
||||
BannerService.instance = new BannerService(controller, hostInfo)
|
||||
return BannerService.instance
|
||||
}
|
||||
@@ -257,7 +254,7 @@ export class BannerService {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
banner_id: bannerId,
|
||||
@@ -299,7 +296,7 @@ export class BannerService {
|
||||
const url = this.buildFetchUrl()
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...buildBasicClineHeaders(),
|
||||
}
|
||||
const authToken = await AuthService.getInstance().getAuthToken()
|
||||
if (authToken) {
|
||||
|
||||
@@ -109,6 +109,34 @@ describe("AgentRuntime (provider-form config + Agent alias)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes provider headers through to the llms gateway", () => {
|
||||
const model = new ScriptedModel([]);
|
||||
createAgentModel.mockReturnValue(model);
|
||||
|
||||
new Agent({
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
apiKey: "test-key",
|
||||
headers: {
|
||||
"x-custom": "kept",
|
||||
},
|
||||
});
|
||||
|
||||
expect(createGateway).toHaveBeenCalledWith({
|
||||
providerConfigs: [
|
||||
{
|
||||
providerId: "cline",
|
||||
apiKey: "test-key",
|
||||
baseUrl: undefined,
|
||||
headers: {
|
||||
"x-custom": "kept",
|
||||
},
|
||||
options: undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards abort() to the active AgentRuntime", async () => {
|
||||
let abortReason: unknown;
|
||||
const model = new ScriptedModel([
|
||||
|
||||
@@ -101,7 +101,15 @@ function resolveRuntimeConfig(
|
||||
const { providerId, modelId, apiKey, baseUrl, headers, options, ...rest } =
|
||||
config;
|
||||
const gateway = createGateway({
|
||||
providerConfigs: [{ providerId, apiKey, baseUrl, headers, options }],
|
||||
providerConfigs: [
|
||||
{
|
||||
providerId,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
headers,
|
||||
options,
|
||||
},
|
||||
],
|
||||
telemetry: rest.telemetry,
|
||||
});
|
||||
const model = gateway.createAgentModel({ providerId, modelId });
|
||||
|
||||
@@ -21,6 +21,18 @@ vi.mock("@cline/llms", () => ({
|
||||
normalizeProviderId: (id: string) => id,
|
||||
}));
|
||||
|
||||
type GatewayConfigCall = {
|
||||
providerConfigs: Array<{ headers?: Record<string, string> }>;
|
||||
};
|
||||
|
||||
function getLastGatewayConfig(): GatewayConfigCall | undefined {
|
||||
return (
|
||||
gatewayMock.createGateway.mock.calls as unknown as Array<
|
||||
[GatewayConfigCall]
|
||||
>
|
||||
).at(-1)?.[0];
|
||||
}
|
||||
|
||||
describe("createAgentModelFromConfig", () => {
|
||||
beforeEach(() => {
|
||||
gatewayMock.createAgentModel.mockReset();
|
||||
@@ -125,6 +137,145 @@ describe("createAgentModelFromConfig", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("adds SDK client headers for Cline provider requests", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
|
||||
createAgentModelFromConfig(
|
||||
{
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
apiKey: "key",
|
||||
sessionId: "sess-vscode",
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
extensionContext: {
|
||||
client: { name: "cline-vscode", version: "1.2.3" },
|
||||
workspace: {
|
||||
rootPath: "/tmp/project",
|
||||
platform: "darwin",
|
||||
},
|
||||
requestMetadata: {
|
||||
clientType: "VSCode Extension",
|
||||
clientVersion: "1.2.3",
|
||||
userAgent: "Cline/1.2.3",
|
||||
platform: "Visual Studio Code",
|
||||
platformVersion: "1.90.0",
|
||||
coreVersion: "1.2.3",
|
||||
isMultiRoot: true,
|
||||
},
|
||||
},
|
||||
providerConfig: {
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
headers: {
|
||||
"x-provider": "kept",
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
const gatewayConfig = getLastGatewayConfig();
|
||||
expect(gatewayConfig?.providerConfigs[0].headers).toMatchObject({
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
"User-Agent": "Cline/1.2.3",
|
||||
"X-IS-MULTIROOT": "true",
|
||||
"X-CLIENT-TYPE": "VSCode Extension",
|
||||
"X-CLIENT-VERSION": "1.2.3",
|
||||
"X-PLATFORM": "Visual Studio Code",
|
||||
"X-PLATFORM-VERSION": "1.90.0",
|
||||
"X-CORE-VERSION": "1.2.3",
|
||||
"X-Task-ID": "sess-vscode",
|
||||
"x-provider": "kept",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps custom Cline headers while derived SDK client headers win", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
|
||||
createAgentModelFromConfig(
|
||||
{
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
apiKey: "key",
|
||||
sessionId: "sess-vscode",
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
headers: {
|
||||
"X-CLIENT-TYPE": "",
|
||||
"X-IS-MULTIROOT": "",
|
||||
"x-top": "wins",
|
||||
},
|
||||
extensionContext: {
|
||||
client: { name: "cline-vscode", version: "1.2.3" },
|
||||
workspace: {
|
||||
rootPath: "/tmp/project",
|
||||
platform: "darwin",
|
||||
},
|
||||
requestMetadata: {
|
||||
clientType: "VSCode Extension",
|
||||
clientVersion: "1.2.3",
|
||||
isMultiRoot: true,
|
||||
},
|
||||
},
|
||||
providerConfig: {
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
headers: {
|
||||
"X-CLIENT-TYPE": "provider-client",
|
||||
"X-CLIENT-VERSION": "stale-provider-version",
|
||||
"x-provider": "kept",
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
const gatewayConfig = getLastGatewayConfig();
|
||||
expect(gatewayConfig?.providerConfigs[0].headers).toMatchObject({
|
||||
"X-CLIENT-TYPE": "VSCode Extension",
|
||||
"X-CLIENT-VERSION": "1.2.3",
|
||||
"X-IS-MULTIROOT": "true",
|
||||
"X-Task-ID": "sess-vscode",
|
||||
"x-provider": "kept",
|
||||
"x-top": "wins",
|
||||
});
|
||||
expect(gatewayConfig?.providerConfigs[0].headers).not.toHaveProperty(
|
||||
"X-PLATFORM",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves existing header precedence for non-Cline providers", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
|
||||
createAgentModelFromConfig(
|
||||
{
|
||||
providerId: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
apiKey: "key",
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
headers: {
|
||||
"x-top": "wins",
|
||||
},
|
||||
providerConfig: {
|
||||
providerId: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
headers: {
|
||||
"x-provider": "not-merged",
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
const gatewayConfig = getLastGatewayConfig();
|
||||
expect(gatewayConfig?.providerConfigs[0].headers).toEqual({
|
||||
"x-top": "wins",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves model capabilities and metadata when configuring gateway models", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
|
||||
@@ -448,6 +599,9 @@ describe("createAgentModelFromConfig", () => {
|
||||
|
||||
it("uses a registered handler (adapter) instead of the gateway, building it lazily", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
const extensionContext = {
|
||||
client: { name: "cline-vscode", version: "1.2.3" },
|
||||
} satisfies NonNullable<AgentConfig["extensionContext"]>;
|
||||
|
||||
// Pretend a host handler is registered for this provider.
|
||||
gatewayMock.hasRegisteredHandler.mockReturnValue(true);
|
||||
@@ -469,6 +623,7 @@ describe("createAgentModelFromConfig", () => {
|
||||
apiKey: "",
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
extensionContext,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
@@ -487,5 +642,8 @@ describe("createAgentModelFromConfig", () => {
|
||||
// drain
|
||||
}
|
||||
expect(gatewayMock.createHandlerAsync).toHaveBeenCalledTimes(1);
|
||||
expect(gatewayMock.createHandlerAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ extensionContext }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,16 @@ import {
|
||||
MODEL_COLLECTIONS_BY_PROVIDER_ID,
|
||||
normalizeProviderId,
|
||||
} from "@cline/llms";
|
||||
import type {
|
||||
AgentConfig,
|
||||
AgentModel,
|
||||
BasicLogger,
|
||||
GatewayModelDefinition,
|
||||
ITelemetryService,
|
||||
ModelInfo,
|
||||
import {
|
||||
type AgentConfig,
|
||||
type AgentModel,
|
||||
type BasicLogger,
|
||||
buildClineRequestHeaders,
|
||||
type ExtensionContext,
|
||||
type GatewayModelDefinition,
|
||||
type ITelemetryService,
|
||||
isClineProvider,
|
||||
type ModelInfo,
|
||||
} from "@cline/shared";
|
||||
import { createAgentModelFromApiHandler } from "./apihandler-agent-model-adapter";
|
||||
import type { ProviderConfig } from "./provider-settings";
|
||||
@@ -96,6 +99,39 @@ export function resolveKnownModelsFromConfig(
|
||||
);
|
||||
}
|
||||
|
||||
function mergeHeaders(
|
||||
baseHeaders: Record<string, string> | undefined,
|
||||
overrideHeaders: Record<string, string> | undefined,
|
||||
): Record<string, string> | undefined {
|
||||
if (!baseHeaders && !overrideHeaders) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(baseHeaders ?? {}),
|
||||
...(overrideHeaders ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveHeadersFromConfig(
|
||||
config: AgentConfig,
|
||||
baseProviderConfig: ProviderConfig | undefined,
|
||||
extensionContext: ExtensionContext | undefined,
|
||||
): Record<string, string> | undefined {
|
||||
const providerId = normalizeProviderId(config.providerId);
|
||||
if (!isClineProvider(providerId)) {
|
||||
return config.headers ?? baseProviderConfig?.headers;
|
||||
}
|
||||
|
||||
const headers = mergeHeaders(baseProviderConfig?.headers, config.headers);
|
||||
|
||||
return buildClineRequestHeaders({
|
||||
providerId,
|
||||
headers,
|
||||
extensionContext,
|
||||
taskId: config.sessionId ?? baseProviderConfig?.taskId,
|
||||
});
|
||||
}
|
||||
|
||||
function toGatewayCapabilities(
|
||||
capabilities: ModelInfo["capabilities"],
|
||||
): GatewayModelDefinition["capabilities"] {
|
||||
@@ -147,6 +183,13 @@ function toGatewayConfiguredModel(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveExtensionContext(
|
||||
config: AgentConfig,
|
||||
baseProviderConfig: ProviderConfig | undefined,
|
||||
): ExtensionContext | undefined {
|
||||
return config.extensionContext ?? baseProviderConfig?.extensionContext;
|
||||
}
|
||||
|
||||
export function createAgentModelFromConfig(
|
||||
config: AgentConfig,
|
||||
logger: BasicLogger | undefined,
|
||||
@@ -155,20 +198,24 @@ export function createAgentModelFromConfig(
|
||||
const pc = config.providerConfig as ProviderConfig | undefined;
|
||||
const baseProviderConfig =
|
||||
pc?.providerId === config.providerId ? pc : undefined;
|
||||
const extensionContext = resolveExtensionContext(config, baseProviderConfig);
|
||||
const normalizedProviderConfig: ProviderConfig = {
|
||||
...(baseProviderConfig ?? {}),
|
||||
providerId: config.providerId,
|
||||
modelId: config.modelId,
|
||||
apiKey: config.apiKey ?? baseProviderConfig?.apiKey,
|
||||
baseUrl: config.baseUrl ?? baseProviderConfig?.baseUrl,
|
||||
headers: config.headers ?? baseProviderConfig?.headers,
|
||||
headers: resolveHeadersFromConfig(
|
||||
config,
|
||||
baseProviderConfig,
|
||||
extensionContext,
|
||||
),
|
||||
knownModels: resolveKnownModelsFromConfig(config),
|
||||
maxOutputTokens: config.maxTokensPerTurn,
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
thinkingBudgetTokens: config.thinkingBudgetTokens,
|
||||
thinking: config.thinking,
|
||||
logger,
|
||||
extensionContext: config.extensionContext,
|
||||
};
|
||||
|
||||
// Host-registered custom handlers (e.g. VS Code LM, which needs the host's
|
||||
@@ -183,7 +230,10 @@ export function createAgentModelFromConfig(
|
||||
)
|
||||
) {
|
||||
return createAgentModelFromApiHandler(() =>
|
||||
createHandlerAsync(normalizedProviderConfig),
|
||||
createHandlerAsync({
|
||||
...normalizedProviderConfig,
|
||||
...(extensionContext ? { extensionContext } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,8 +259,7 @@ export function createAgentModelFromConfig(
|
||||
},
|
||||
],
|
||||
logger,
|
||||
telemetry:
|
||||
telemetry ?? config.telemetry ?? config.extensionContext?.telemetry,
|
||||
telemetry: telemetry ?? config.telemetry ?? extensionContext?.telemetry,
|
||||
}).createAgentModel(
|
||||
{
|
||||
providerId: normalizedProviderConfig.providerId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionContext } from "@cline/shared";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderSettings } from "../types/provider-settings";
|
||||
|
||||
@@ -394,6 +395,57 @@ describe("prepareLocalRuntimeBootstrap", () => {
|
||||
expect(bootstrap.providerConfig.fetch).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves stored Cline headers when session config only supplies request metadata", async () => {
|
||||
const { prepareLocalRuntimeBootstrap } = await import(
|
||||
"./local-runtime-bootstrap"
|
||||
);
|
||||
|
||||
const input = createStartInput();
|
||||
const config = input.config as typeof input.config & {
|
||||
extensionContext: ExtensionContext;
|
||||
};
|
||||
config.extensionContext = {
|
||||
client: { name: "cline-vscode", version: "1.2.3" },
|
||||
requestMetadata: {
|
||||
clientType: "VSCode Extension",
|
||||
clientVersion: "1.2.3",
|
||||
platform: "Visual Studio Code",
|
||||
platformVersion: "1.90.0",
|
||||
coreVersion: "1.2.3",
|
||||
isMultiRoot: false,
|
||||
},
|
||||
};
|
||||
|
||||
const bootstrap = await prepareLocalRuntimeBootstrap({
|
||||
input,
|
||||
sessionId: "sess-cline",
|
||||
providerSettingsManager: createProviderSettingsManager({
|
||||
provider: "cline",
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
headers: {
|
||||
"x-stored": "stored",
|
||||
},
|
||||
}) as never,
|
||||
defaultTelemetry: undefined,
|
||||
defaultToolPolicies: undefined,
|
||||
onPluginEvent: () => {},
|
||||
onTeamEvent: () => {},
|
||||
createSpawnTool,
|
||||
readSessionMetadata: async () => undefined,
|
||||
writeSessionMetadata: async () => {},
|
||||
});
|
||||
|
||||
expect(bootstrap.config.headers).toBeUndefined();
|
||||
expect(bootstrap.providerConfig.headers).toEqual({
|
||||
"x-stored": "stored",
|
||||
});
|
||||
expect(bootstrap.providerConfig.extensionContext).toBeUndefined();
|
||||
expect(bootstrap.config.extensionContext?.requestMetadata).toMatchObject({
|
||||
clientType: "VSCode Extension",
|
||||
platform: "Visual Studio Code",
|
||||
});
|
||||
});
|
||||
|
||||
it("adds Codex backend headers for openai-codex from stored OAuth settings", async () => {
|
||||
const { prepareLocalRuntimeBootstrap } = await import(
|
||||
"./local-runtime-bootstrap"
|
||||
|
||||
@@ -179,6 +179,51 @@ function deriveOpenAICodexAccountId(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function mergeExtensionContext(
|
||||
base: ExtensionContext | undefined,
|
||||
override: ExtensionContext | undefined,
|
||||
): ExtensionContext | undefined {
|
||||
if (!base && !override) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(base ?? {}),
|
||||
...(override ?? {}),
|
||||
user:
|
||||
base?.user || override?.user
|
||||
? { ...(base?.user ?? {}), ...(override?.user ?? {}) }
|
||||
: undefined,
|
||||
client:
|
||||
base?.client || override?.client
|
||||
? ({
|
||||
...(base?.client ?? {}),
|
||||
...(override?.client ?? {}),
|
||||
} as ExtensionContext["client"])
|
||||
: undefined,
|
||||
workspace:
|
||||
base?.workspace || override?.workspace
|
||||
? ({
|
||||
...(base?.workspace ?? {}),
|
||||
...(override?.workspace ?? {}),
|
||||
} as ExtensionContext["workspace"])
|
||||
: undefined,
|
||||
requestMetadata:
|
||||
base?.requestMetadata || override?.requestMetadata
|
||||
? {
|
||||
...(base?.requestMetadata ?? {}),
|
||||
...(override?.requestMetadata ?? {}),
|
||||
}
|
||||
: undefined,
|
||||
session:
|
||||
base?.session || override?.session
|
||||
? { ...(base?.session ?? {}), ...(override?.session ?? {}) }
|
||||
: undefined,
|
||||
automation: override?.automation ?? base?.automation,
|
||||
logger: override?.logger ?? base?.logger,
|
||||
telemetry: override?.telemetry ?? base?.telemetry,
|
||||
};
|
||||
}
|
||||
|
||||
function buildProviderConfig(
|
||||
config: CoreSessionConfig,
|
||||
sessionId: string,
|
||||
@@ -225,9 +270,6 @@ function buildProviderConfig(
|
||||
if (config.knownModels) {
|
||||
providerConfig.knownModels = config.knownModels;
|
||||
}
|
||||
if (config.extensionContext) {
|
||||
providerConfig.extensionContext = config.extensionContext;
|
||||
}
|
||||
// Thread a host-provided custom fetch through to the AI gateway providers.
|
||||
// Precedence: explicit per-session config > stored provider settings > host default.
|
||||
const sessionFetch = (config as { fetch?: typeof fetch }).fetch;
|
||||
@@ -319,7 +361,10 @@ export async function prepareLocalRuntimeBootstrap(
|
||||
// as workspaceMetadata; the structured object is kept as workspaceInfo.
|
||||
const { workspaceInfo, workspaceMetadata, durationMs, vcsType, initError } =
|
||||
await buildWorkspaceMetadataWithInfo(workspacePath);
|
||||
const configuredExtensionContext = localConfig?.extensionContext;
|
||||
const configuredExtensionContext = mergeExtensionContext(
|
||||
(input.config as { extensionContext?: ExtensionContext }).extensionContext,
|
||||
localConfig?.extensionContext,
|
||||
);
|
||||
const extensionContext: ExtensionContext = {
|
||||
...(configuredExtensionContext ?? {}),
|
||||
workspace: {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { CLINE_ENVIRONMENT_ENV, CLINE_ENVIRONMENTS } from "@cline/shared";
|
||||
import {
|
||||
CLINE_ENVIRONMENT_ENV,
|
||||
CLINE_ENVIRONMENTS,
|
||||
DEFAULT_REQUEST_HEADERS,
|
||||
} from "@cline/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { BUILTIN_SPECS } from "./builtins";
|
||||
import { getModelsForProvider, getProvider } from "./model-registry";
|
||||
@@ -11,6 +15,14 @@ function findClineSpec() {
|
||||
return spec;
|
||||
}
|
||||
|
||||
function findClinePassSpec() {
|
||||
const spec = BUILTIN_SPECS.find((s) => s.id === "cline-pass");
|
||||
if (!spec) {
|
||||
throw new Error("cline-pass builtin spec not found");
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
describe("cline builtin spec defaults.baseUrl", () => {
|
||||
const originalEnvironment = process.env[CLINE_ENVIRONMENT_ENV];
|
||||
|
||||
@@ -50,6 +62,15 @@ describe("cline builtin spec defaults.baseUrl", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cline builtin spec request headers", () => {
|
||||
it("uses the shared default request headers for Cline-compatible providers", () => {
|
||||
expect(findClineSpec().defaults?.headers).toEqual(DEFAULT_REQUEST_HEADERS);
|
||||
expect(findClinePassSpec().defaults?.headers).toEqual(
|
||||
DEFAULT_REQUEST_HEADERS,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cline builtin models", () => {
|
||||
it("prefers Vercel-style Z.ai model ids over equivalent OpenRouter ids", async () => {
|
||||
const models = await getModelsForProvider("cline");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
DEFAULT_REQUEST_HEADERS,
|
||||
type GatewayModelCapability,
|
||||
type GatewayModelDefinition,
|
||||
type GatewayProviderManifest,
|
||||
@@ -492,6 +493,8 @@ function createClineLikeSpec(
|
||||
>
|
||||
>,
|
||||
): BuiltinSpec {
|
||||
const { headers, ...defaults } = input.defaults ?? {};
|
||||
|
||||
return {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
@@ -507,7 +510,11 @@ function createClineLikeSpec(
|
||||
get baseUrl(): string {
|
||||
return `${getClineEnvironmentConfig().apiBaseUrl}/api/v1`;
|
||||
},
|
||||
...input.defaults,
|
||||
headers: {
|
||||
...DEFAULT_REQUEST_HEADERS,
|
||||
...(headers ?? {}),
|
||||
},
|
||||
...defaults,
|
||||
},
|
||||
metadata: {
|
||||
...ANTHROPIC_AND_QWEN_CACHE_ROUTING_METADATA,
|
||||
|
||||
@@ -357,6 +357,63 @@ describe("createGatewayApiHandler.createMessage", () => {
|
||||
openaiCompatibleSpy.mockClear();
|
||||
});
|
||||
|
||||
it("adds SDK client headers for Cline provider requests", async () => {
|
||||
streamTextSpy.mockReturnValue({
|
||||
fullStream: (async function* () {
|
||||
yield { type: "finish", finishReason: "stop" };
|
||||
})(),
|
||||
usage: Promise.resolve({ inputTokens: 1, outputTokens: 1 }),
|
||||
});
|
||||
|
||||
const handler = createGatewayApiHandler({
|
||||
providerId: "cline",
|
||||
clientType: "openai-compatible",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
apiKey: "test-key",
|
||||
taskId: "task-jetbrains",
|
||||
extensionContext: {
|
||||
client: { name: "cline-jetbrains", version: "2.3.4" },
|
||||
workspace: {
|
||||
rootPath: "/tmp/project",
|
||||
platform: "darwin",
|
||||
},
|
||||
requestMetadata: {
|
||||
userAgent: "Cline/2.3.4",
|
||||
platform: "JetBrains",
|
||||
platformVersion: "2026.1",
|
||||
coreVersion: "2.3.4",
|
||||
isMultiRoot: true,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
"x-custom": "kept",
|
||||
},
|
||||
});
|
||||
|
||||
for await (const _chunk of handler.createMessage("", [
|
||||
{ role: "user", content: "Hello" },
|
||||
])) {
|
||||
// Drain the stream so the provider is constructed.
|
||||
}
|
||||
|
||||
const factoryConfig = openaiCompatibleFactorySpy.mock.calls.at(-1)?.[0] as
|
||||
| { headers?: Record<string, string> }
|
||||
| undefined;
|
||||
expect(factoryConfig?.headers).toMatchObject({
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
"User-Agent": "Cline/2.3.4",
|
||||
"X-IS-MULTIROOT": "true",
|
||||
"X-CLIENT-TYPE": "cline-jetbrains",
|
||||
"X-CLIENT-VERSION": "2.3.4",
|
||||
"X-PLATFORM": "JetBrains",
|
||||
"X-PLATFORM-VERSION": "2026.1",
|
||||
"X-CORE-VERSION": "2.3.4",
|
||||
"X-Task-ID": "task-jetbrains",
|
||||
"x-custom": "kept",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not convert catalog maxTokens into request maxOutputTokens", async () => {
|
||||
streamTextSpy.mockReturnValue({
|
||||
fullStream: (async function* () {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type {
|
||||
AgentModelEvent,
|
||||
GatewayModelDefinition,
|
||||
GatewayProviderFactory,
|
||||
GatewayProviderRegistration,
|
||||
GatewayStreamRequest,
|
||||
import {
|
||||
type AgentModelEvent,
|
||||
buildClineRequestHeaders,
|
||||
type GatewayModelDefinition,
|
||||
type GatewayProviderFactory,
|
||||
type GatewayProviderRegistration,
|
||||
type GatewayStreamRequest,
|
||||
} from "@cline/shared";
|
||||
import { nanoid } from "nanoid";
|
||||
import type {
|
||||
@@ -449,7 +450,12 @@ function buildGatewayConfig(config: ProviderConfig) {
|
||||
providerId,
|
||||
apiKey: config.apiKey ?? config.accessToken,
|
||||
baseUrl: config.baseUrl,
|
||||
headers: config.headers,
|
||||
headers: buildClineRequestHeaders({
|
||||
providerId,
|
||||
headers: config.headers,
|
||||
extensionContext: config.extensionContext,
|
||||
taskId: config.taskId,
|
||||
}),
|
||||
timeoutMs: config.timeoutMs,
|
||||
fetch: config.fetch,
|
||||
defaultModelId: config.modelId,
|
||||
|
||||
@@ -815,7 +815,8 @@ export interface AgentConfig {
|
||||
telemetry?: ITelemetryService;
|
||||
/**
|
||||
* Ambient runtime context: user identity, client surface, workspace, logger,
|
||||
* and telemetry. Threaded through to ProviderConfig so handlers can access it.
|
||||
* telemetry, and host request metadata. Used by runtime extension setup and
|
||||
* forwarded to registered provider handlers that need host-local context.
|
||||
*/
|
||||
extensionContext?: ExtensionContext;
|
||||
|
||||
|
||||
@@ -29,6 +29,29 @@ export interface ClientContext {
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client/runtime metadata used when constructing provider request metadata.
|
||||
*
|
||||
* This is intentionally separate from WorkspaceContext so request headers can
|
||||
* preserve legacy host metadata without changing prompt/workspace semantics.
|
||||
*/
|
||||
export interface RequestMetadataContext {
|
||||
/** Legacy client type value for provider request headers. */
|
||||
clientType?: string;
|
||||
/** Client version value for provider request headers. */
|
||||
clientVersion?: string;
|
||||
/** HTTP User-Agent value for provider requests. */
|
||||
userAgent?: string;
|
||||
/** Host/application platform label, e.g. "Visual Studio Code". */
|
||||
platform?: string;
|
||||
/** Host/application version, e.g. the VS Code version. */
|
||||
platformVersion?: string;
|
||||
/** Cline core/runtime version. */
|
||||
coreVersion?: string;
|
||||
/** Whether the host is currently operating on multiple workspace roots. */
|
||||
isMultiRoot?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity of the authenticated user.
|
||||
*/
|
||||
@@ -80,6 +103,7 @@ export interface ExtensionContext {
|
||||
user?: UserContext;
|
||||
client?: ClientContext;
|
||||
workspace?: WorkspaceContext;
|
||||
requestMetadata?: RequestMetadataContext;
|
||||
/** Core session metadata forwarded into plugin setup context. */
|
||||
session?: AgentExtensionSessionContext;
|
||||
/**
|
||||
|
||||
@@ -41,6 +41,7 @@ export type {
|
||||
ClientContext,
|
||||
ClientName,
|
||||
ExtensionContext,
|
||||
RequestMetadataContext,
|
||||
UserContext,
|
||||
WorkspaceContext,
|
||||
} from "./extensions/context";
|
||||
@@ -193,7 +194,12 @@ export {
|
||||
resolveReasoningBudgetFromRatio,
|
||||
resolveReasoningEffortRatio,
|
||||
} from "./llms/reasoning-effort";
|
||||
export { DEFAULT_REQUEST_HEADERS, serializeAbortReason } from "./llms/requests";
|
||||
export {
|
||||
type BuildClineRequestHeadersInput,
|
||||
buildClineRequestHeaders,
|
||||
DEFAULT_REQUEST_HEADERS,
|
||||
serializeAbortReason,
|
||||
} from "./llms/requests";
|
||||
export { CHARS_PER_TOKEN, estimateTokens } from "./llms/tokens";
|
||||
export type {
|
||||
ToolApprovalRequest,
|
||||
|
||||
@@ -55,6 +55,7 @@ export type {
|
||||
ClientContext,
|
||||
ClientName,
|
||||
ExtensionContext,
|
||||
RequestMetadataContext,
|
||||
UserContext,
|
||||
WorkspaceContext,
|
||||
} from "./extensions/context";
|
||||
@@ -207,7 +208,12 @@ export {
|
||||
resolveReasoningBudgetFromRatio,
|
||||
resolveReasoningEffortRatio,
|
||||
} from "./llms/reasoning-effort";
|
||||
export { DEFAULT_REQUEST_HEADERS, serializeAbortReason } from "./llms/requests";
|
||||
export {
|
||||
type BuildClineRequestHeadersInput,
|
||||
buildClineRequestHeaders,
|
||||
DEFAULT_REQUEST_HEADERS,
|
||||
serializeAbortReason,
|
||||
} from "./llms/requests";
|
||||
export { CHARS_PER_TOKEN, estimateTokens } from "./llms/tokens";
|
||||
export type {
|
||||
ToolApprovalRequest,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { ExtensionContext } from "../extensions/context";
|
||||
import { isClineProvider } from "../providers/utils";
|
||||
|
||||
export const DEFAULT_REQUEST_HEADERS: Record<string, string> = {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
@@ -5,6 +8,85 @@ export const DEFAULT_REQUEST_HEADERS: Record<string, string> = {
|
||||
"X-CLIENT-TYPE": "cline-sdk",
|
||||
};
|
||||
|
||||
export interface BuildClineRequestHeadersInput {
|
||||
providerId: string;
|
||||
headers?: Record<string, string>;
|
||||
extensionContext?: ExtensionContext;
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
userAgent?: string;
|
||||
platform?: string;
|
||||
platformVersion?: string;
|
||||
coreVersion?: string;
|
||||
isMultiRoot?: boolean;
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
function cleanHeaderValue(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed && trimmed.toLowerCase() !== "unknown" ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function cleanHeaderOverrides(
|
||||
headers: Record<string, string> | undefined,
|
||||
): Record<string, string> | undefined {
|
||||
if (!headers) {
|
||||
return undefined;
|
||||
}
|
||||
const cleaned = Object.fromEntries(
|
||||
Object.entries(headers).filter(([, value]) => cleanHeaderValue(value)),
|
||||
);
|
||||
return Object.keys(cleaned).length > 0 ? cleaned : undefined;
|
||||
}
|
||||
|
||||
export function buildClineRequestHeaders(
|
||||
input: BuildClineRequestHeadersInput,
|
||||
): Record<string, string> | undefined {
|
||||
if (!isClineProvider(input.providerId)) {
|
||||
return input.headers;
|
||||
}
|
||||
|
||||
const requestMetadata = input.extensionContext?.requestMetadata;
|
||||
const clientName =
|
||||
cleanHeaderValue(input.clientName) ??
|
||||
cleanHeaderValue(requestMetadata?.clientType) ??
|
||||
cleanHeaderValue(input.extensionContext?.client?.name);
|
||||
const clientVersion =
|
||||
cleanHeaderValue(input.clientVersion) ??
|
||||
cleanHeaderValue(requestMetadata?.clientVersion) ??
|
||||
cleanHeaderValue(input.extensionContext?.client?.version);
|
||||
const userAgent =
|
||||
cleanHeaderValue(input.userAgent) ??
|
||||
cleanHeaderValue(requestMetadata?.userAgent);
|
||||
const platform =
|
||||
cleanHeaderValue(input.platform) ??
|
||||
cleanHeaderValue(requestMetadata?.platform);
|
||||
const platformVersion =
|
||||
cleanHeaderValue(input.platformVersion) ??
|
||||
cleanHeaderValue(requestMetadata?.platformVersion);
|
||||
const coreVersion =
|
||||
cleanHeaderValue(input.coreVersion) ??
|
||||
cleanHeaderValue(requestMetadata?.coreVersion);
|
||||
const isMultiRoot = input.isMultiRoot ?? requestMetadata?.isMultiRoot;
|
||||
const taskId = cleanHeaderValue(input.taskId);
|
||||
const headerOverrides = cleanHeaderOverrides(input.headers);
|
||||
|
||||
return {
|
||||
...DEFAULT_REQUEST_HEADERS,
|
||||
...(headerOverrides ?? {}),
|
||||
...(userAgent ? { "User-Agent": userAgent } : {}),
|
||||
...(clientName ? { "X-CLIENT-TYPE": clientName } : {}),
|
||||
...(clientVersion ? { "X-CLIENT-VERSION": clientVersion } : {}),
|
||||
...(platform ? { "X-PLATFORM": platform } : {}),
|
||||
...(platformVersion ? { "X-PLATFORM-VERSION": platformVersion } : {}),
|
||||
...(coreVersion ? { "X-CORE-VERSION": coreVersion } : {}),
|
||||
...(isMultiRoot !== undefined
|
||||
? { "X-IS-MULTIROOT": isMultiRoot ? "true" : "false" }
|
||||
: {}),
|
||||
...(taskId ? { "X-Task-ID": taskId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeAbortReason(reason: unknown): unknown {
|
||||
return reason instanceof Error
|
||||
? { name: reason.name, message: reason.message }
|
||||
|
||||
Reference in New Issue
Block a user