mirror of
https://github.com/cline/cline.git
synced 2026-09-17 17:45:33 +08:00
fix(vscode): revert OpenAI-compatible metadata limit plumbing (#11775)
* fix(vscode): stop deriving output limits from model metadata * fix(vscode): send OpenAI-compatible output token limit (#11776)
This commit is contained in:
@@ -411,7 +411,7 @@ describe("buildSessionConfig", () => {
|
||||
expect(config.providerConfig).not.toHaveProperty("apiKey")
|
||||
})
|
||||
|
||||
it("passes OpenAI Compatible custom model metadata through as SDK knownModels", async () => {
|
||||
it("passes OpenAI Compatible max output tokens as an explicit request limit", async () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "openai",
|
||||
actModeOpenAiModelId: "custom-reasoner",
|
||||
@@ -423,7 +423,6 @@ describe("buildSessionConfig", () => {
|
||||
maxTokens: 4_096,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsReasoning: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
@@ -433,20 +432,10 @@ describe("buildSessionConfig", () => {
|
||||
|
||||
expect(config.providerId).toBe("openai-compatible")
|
||||
expect(config.modelId).toBe("custom-reasoner")
|
||||
expect(config.knownModels?.["custom-reasoner"]).toMatchObject({
|
||||
id: "custom-reasoner",
|
||||
name: "Custom Reasoner",
|
||||
contextWindow: 16_000,
|
||||
maxInputTokens: 16_000,
|
||||
maxTokens: 4_096,
|
||||
capabilities: ["streaming", "tools"],
|
||||
})
|
||||
expect((config.providerConfig as any).knownModels?.["custom-reasoner"]).toMatchObject({
|
||||
contextWindow: 16_000,
|
||||
maxInputTokens: 16_000,
|
||||
maxTokens: 4_096,
|
||||
})
|
||||
expect((config.providerConfig as any).maxOutputTokens).toBe(4_096)
|
||||
expect(config.knownModels).toBeUndefined()
|
||||
expect((config.providerConfig as any).knownModels).toBeUndefined()
|
||||
expect((config.providerConfig as any).maxOutputTokens).toBeUndefined()
|
||||
expect((config as any).maxTokensPerTurn).toBe(4_096)
|
||||
})
|
||||
|
||||
it("builds structured SAP AI Core config from legacy ApiConfiguration fields", async () => {
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
resolveProviderApiKeyFromSettings,
|
||||
type StartSessionResult,
|
||||
} from "@cline/core"
|
||||
import { getGeneratedModelsForProvider, MODEL_COLLECTIONS_BY_PROVIDER_ID, type ModelInfo as SdkModelInfo } from "@cline/llms"
|
||||
import { getGeneratedModelsForProvider, MODEL_COLLECTIONS_BY_PROVIDER_ID } from "@cline/llms"
|
||||
import { buildClineSystemPrompt } from "@cline/shared"
|
||||
import type { ApiConfiguration, ModelInfo as LegacyModelInfo } from "@shared/api"
|
||||
import type { ApiConfiguration } from "@shared/api"
|
||||
import type { HistoryItem } from "@shared/HistoryItem"
|
||||
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, type LanguageDisplay } from "@shared/Languages"
|
||||
import { Logger } from "@shared/services/Logger"
|
||||
@@ -182,52 +182,10 @@ function resolveProviderReasoningConfig(providerId: string): SessionReasoningCon
|
||||
}
|
||||
}
|
||||
|
||||
function positiveNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined
|
||||
}
|
||||
|
||||
function getOpenAiCompatibleModelInfo(config: ApiConfiguration, mode: Mode): LegacyModelInfo | undefined {
|
||||
return mode === "plan" ? config.planModeOpenAiModelInfo : config.actModeOpenAiModelInfo
|
||||
}
|
||||
|
||||
const OPENAI_COMPATIBLE_DEFAULT_CAPABILITIES: NonNullable<SdkModelInfo["capabilities"]> = ["streaming", "tools"]
|
||||
|
||||
function buildOpenAiCompatibleCapabilities(modelInfo: LegacyModelInfo): NonNullable<SdkModelInfo["capabilities"]> {
|
||||
const capabilities: NonNullable<SdkModelInfo["capabilities"]> = [...OPENAI_COMPATIBLE_DEFAULT_CAPABILITIES]
|
||||
if (modelInfo.supportsImages !== false) {
|
||||
capabilities.push("images")
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
// VS Code stores OpenAI-compatible custom model metadata in legacy
|
||||
// ApiConfiguration fields. The SDK runtime enforces context/output limits from
|
||||
// knownModels, so bridge the selected model into the SDK shape during session
|
||||
// creation.
|
||||
function buildOpenAiCompatibleKnownModels(
|
||||
modelId: string | undefined,
|
||||
modelInfo: LegacyModelInfo | undefined,
|
||||
): Record<string, SdkModelInfo> | undefined {
|
||||
const trimmedModelId = modelId?.trim()
|
||||
if (!trimmedModelId || !modelInfo) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contextWindow = positiveNumber(modelInfo.contextWindow)
|
||||
const maxTokens = positiveNumber(modelInfo.maxTokens)
|
||||
|
||||
return {
|
||||
[trimmedModelId]: {
|
||||
id: trimmedModelId,
|
||||
name: modelInfo.name ?? trimmedModelId,
|
||||
description: modelInfo.description,
|
||||
contextWindow,
|
||||
maxInputTokens: contextWindow,
|
||||
maxTokens,
|
||||
capabilities: buildOpenAiCompatibleCapabilities(modelInfo),
|
||||
temperature: modelInfo.temperature,
|
||||
},
|
||||
}
|
||||
function resolveOpenAiCompatibleMaxTokens(config: ApiConfiguration | undefined, mode: Mode): number | undefined {
|
||||
const modelInfo = mode === "plan" ? config?.planModeOpenAiModelInfo : config?.actModeOpenAiModelInfo
|
||||
const maxTokens = modelInfo?.maxTokens
|
||||
return typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 ? maxTokens : undefined
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -546,7 +504,6 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
let modelId: string | undefined
|
||||
let apiKey: string | undefined
|
||||
let baseUrl: string | undefined
|
||||
let knownModels: CoreSessionConfig["knownModels"] | undefined
|
||||
let apiConfig: ApiConfiguration | undefined
|
||||
// Cloud-provider structured options. The core runtime reads these from
|
||||
// CoreSessionConfig.providerConfig; without them the SDK gateway never receives
|
||||
@@ -569,9 +526,6 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
|
||||
// Resolve model ID
|
||||
modelId = resolveModelId(providerId, mode, apiConfig)
|
||||
if (providerId === "openai") {
|
||||
knownModels = buildOpenAiCompatibleKnownModels(modelId, getOpenAiCompatibleModelInfo(apiConfig, mode))
|
||||
}
|
||||
|
||||
// Resolve base URL
|
||||
baseUrl = resolveBaseUrl(providerId, apiConfig)
|
||||
@@ -632,6 +586,7 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
apiKey = resolveApiKey(providerId, apiConfig)
|
||||
}
|
||||
apiKey = apiKey ?? ""
|
||||
const maxTokensPerTurn = providerId === "openai" ? resolveOpenAiCompatibleMaxTokens(apiConfig, mode) : undefined
|
||||
const reasoningConfig = resolveProviderReasoningConfig(providerId)
|
||||
|
||||
// Build the system prompt using the shared prompt builder. Core still
|
||||
@@ -699,10 +654,6 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
modelId,
|
||||
...(apiKey ? { apiKey } : {}),
|
||||
...(baseUrl !== undefined ? { baseUrl } : {}),
|
||||
...(knownModels ? { knownModels } : {}),
|
||||
...(modelId && positiveNumber(knownModels?.[modelId]?.maxTokens)
|
||||
? { maxOutputTokens: positiveNumber(knownModels?.[modelId]?.maxTokens) }
|
||||
: {}),
|
||||
fetch,
|
||||
}
|
||||
|
||||
@@ -711,7 +662,6 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
modelId,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
knownModels,
|
||||
providerConfig,
|
||||
cwd,
|
||||
workspaceRoot,
|
||||
@@ -730,6 +680,7 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
|
||||
disableMcpSettingsTools: true,
|
||||
mode: mode === "plan" ? "plan" : "act",
|
||||
...reasoningConfig,
|
||||
...(maxTokensPerTurn !== undefined ? { maxTokensPerTurn } : {}),
|
||||
maxIterations: undefined,
|
||||
logger: sdkLogger,
|
||||
extensionContext: {
|
||||
|
||||
@@ -27,6 +27,7 @@ export type DelegatedAgentConnectionConfig = Pick<
|
||||
| "providerConfig"
|
||||
| "knownModels"
|
||||
| "thinking"
|
||||
| "maxTokensPerTurn"
|
||||
>;
|
||||
|
||||
export interface DelegatedAgentRuntimeConfig
|
||||
@@ -87,6 +88,7 @@ export function createDelegatedAgentConfigProvider(
|
||||
providerConfig: runtimeConfig.providerConfig,
|
||||
knownModels: runtimeConfig.knownModels,
|
||||
thinking: runtimeConfig.thinking,
|
||||
maxTokensPerTurn: runtimeConfig.maxTokensPerTurn,
|
||||
}),
|
||||
updateConnectionDefaults: (overrides) => {
|
||||
runtimeConfig = {
|
||||
|
||||
@@ -468,6 +468,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
thinking: configWithProvider.thinking,
|
||||
reasoningEffort:
|
||||
configWithProvider.reasoningEffort ?? providerConfig.reasoningEffort,
|
||||
maxTokensPerTurn: configWithProvider.maxTokensPerTurn,
|
||||
systemPrompt: configWithProvider.systemPrompt,
|
||||
maxIterations: configWithProvider.maxIterations,
|
||||
execution: configWithProvider.execution,
|
||||
|
||||
@@ -485,6 +485,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
|
||||
providerConfig: config.providerConfig,
|
||||
knownModels: config.knownModels,
|
||||
thinking: config.thinking,
|
||||
maxTokensPerTurn: config.maxTokensPerTurn,
|
||||
maxIterations: config.maxIterations,
|
||||
hooks,
|
||||
extensions: runtimeExtensions,
|
||||
|
||||
@@ -125,31 +125,6 @@ describe("createAgentModelFromConfig", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses providerConfig maxOutputTokens when no direct per-turn override is set", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
|
||||
createAgentModelFromConfig(
|
||||
{
|
||||
providerId: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
apiKey: "key",
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
providerConfig: {
|
||||
providerId: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
maxOutputTokens: 4_096,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(gatewayMock.createAgentModel).toHaveBeenLastCalledWith(
|
||||
{ providerId: "openai-compatible", modelId: "custom-model" },
|
||||
{ maxTokens: 4_096 },
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves model capabilities and metadata when configuring gateway models", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
|
||||
@@ -225,6 +200,31 @@ describe("createAgentModelFromConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses explicit per-turn max tokens for gateway request limits", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
|
||||
createAgentModelFromConfig(
|
||||
{
|
||||
providerId: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
apiKey: "key",
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
maxTokensPerTurn: 4_096,
|
||||
providerConfig: {
|
||||
providerId: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(gatewayMock.createAgentModel).toHaveBeenLastCalledWith(
|
||||
{ providerId: "openai-compatible", modelId: "custom-model" },
|
||||
{ maxTokens: 4_096 },
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards Bedrock AWS settings as gateway provider options", async () => {
|
||||
const { createAgentModelFromConfig } = await import("./handler-factory");
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ export function createAgentModelFromConfig(
|
||||
baseUrl: config.baseUrl ?? baseProviderConfig?.baseUrl,
|
||||
headers: config.headers ?? baseProviderConfig?.headers,
|
||||
knownModels: resolveKnownModelsFromConfig(config),
|
||||
maxOutputTokens: config.maxTokensPerTurn ?? baseProviderConfig?.maxOutputTokens,
|
||||
maxOutputTokens: config.maxTokensPerTurn,
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
thinkingBudgetTokens: config.thinkingBudgetTokens,
|
||||
thinking: config.thinking,
|
||||
|
||||
@@ -37,6 +37,10 @@ export interface CoreModelConfig {
|
||||
* Explicit reasoning effort override for capable models.
|
||||
*/
|
||||
reasoningEffort?: ProviderConfig["reasoningEffort"];
|
||||
/**
|
||||
* Maximum output tokens per API call.
|
||||
*/
|
||||
maxTokensPerTurn?: number;
|
||||
}
|
||||
|
||||
export interface CoreRuntimeFeatures {
|
||||
|
||||
@@ -394,6 +394,36 @@ describe("createGatewayApiHandler.createMessage", () => {
|
||||
expect(call).not.toHaveProperty("maxOutputTokens");
|
||||
});
|
||||
|
||||
it("sends configured OpenAI-compatible maxOutputTokens to the provider request", async () => {
|
||||
streamTextSpy.mockReturnValue({
|
||||
fullStream: (async function* () {
|
||||
yield { type: "finish", finishReason: "stop" };
|
||||
})(),
|
||||
usage: Promise.resolve({ inputTokens: 1, outputTokens: 1 }),
|
||||
});
|
||||
|
||||
const handler = createGatewayApiHandler({
|
||||
providerId: "openai-compatible",
|
||||
clientType: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
apiKey: "test-key",
|
||||
baseUrl: "https://example.com/v1",
|
||||
maxOutputTokens: 4_096,
|
||||
});
|
||||
|
||||
for await (const _chunk of handler.createMessage("", [
|
||||
{ role: "user", content: "Hello" },
|
||||
])) {
|
||||
// Drain the stream so the provider request is executed.
|
||||
}
|
||||
|
||||
expect(streamTextSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
maxOutputTokens: 4_096,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("caps configured maxOutputTokens with the catalog model output limit", async () => {
|
||||
streamTextSpy.mockReturnValue({
|
||||
fullStream: (async function* () {
|
||||
|
||||
Reference in New Issue
Block a user