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:
Saoud Rizwan
2026-06-23 22:04:13 -07:00
committed by GitHub
parent 6175896d65
commit 33bd6005a5
9 changed files with 77 additions and 99 deletions
@@ -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 () => {
+8 -57
View File
@@ -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,
+4
View File
@@ -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* () {