Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,20 @@ const importConverter = async (): Promise<{
}> => import('./converter.js');

describe('AnthropicContentGenerator', () => {
const MAX_OUTPUT_TOKENS_ENV = 'QWEN_CODE_MAX_OUTPUT_TOKENS';
let mockConfig: Config;
let anthropicState: {
constructorOptions?: Record<string, unknown>;
lastCreateArgs?: AnthropicCreateArgs;
createImpl: ReturnType<typeof vi.fn>;
};
let savedMaxOutputTokensEnv: string | undefined;

beforeEach(async () => {
vi.clearAllMocks();
vi.resetModules();
savedMaxOutputTokensEnv = process.env[MAX_OUTPUT_TOKENS_ENV];
delete process.env[MAX_OUTPUT_TOKENS_ENV];

mockTokenizer.calculateTokens.mockResolvedValue({
totalTokens: 50,
Expand All @@ -106,6 +110,11 @@ describe('AnthropicContentGenerator', () => {
});

afterEach(() => {
if (savedMaxOutputTokensEnv === undefined) {
delete process.env[MAX_OUTPUT_TOKENS_ENV];
} else {
process.env[MAX_OUTPUT_TOKENS_ENV] = savedMaxOutputTokensEnv;
}
vi.restoreAllMocks();
});

Expand Down Expand Up @@ -1620,6 +1629,75 @@ describe('AnthropicContentGenerator', () => {
);
});

it('ignores malformed QWEN_CODE_MAX_OUTPUT_TOKENS values', async () => {
const { AnthropicContentGenerator } = await importGenerator();

for (const envValue of ['1.5', '2k', 'abc']) {
process.env[MAX_OUTPUT_TOKENS_ENV] = envValue;
anthropicState.createImpl.mockResolvedValueOnce({
id: `anthropic-${envValue}`,
model: 'claude-sonnet-4',
content: [{ type: 'text', text: 'hi' }],
});

const generator = new AnthropicContentGenerator(
{
model: 'claude-sonnet-4',
apiKey: 'test-key',
timeout: 10_000,
maxRetries: 2,
samplingParams: {},
schemaCompliance: 'auto',
},
mockConfig,
);

await generator.generateContent({
model: 'models/ignored',
contents: 'Hello',
} as unknown as GenerateContentParameters);

const [anthropicRequest] =
anthropicState.lastCreateArgs as AnthropicCreateArgs;
expect(anthropicRequest).toEqual(
expect.objectContaining({ max_tokens: 8000 }),
);
}
});

it('respects a valid QWEN_CODE_MAX_OUTPUT_TOKENS value', async () => {
const { AnthropicContentGenerator } = await importGenerator();
process.env[MAX_OUTPUT_TOKENS_ENV] = '9000';
anthropicState.createImpl.mockResolvedValue({
id: 'anthropic-1',
model: 'claude-sonnet-4',
content: [{ type: 'text', text: 'hi' }],
});

const generator = new AnthropicContentGenerator(
{
model: 'claude-sonnet-4',
apiKey: 'test-key',
timeout: 10_000,
maxRetries: 2,
samplingParams: {},
schemaCompliance: 'auto',
},
mockConfig,
);

await generator.generateContent({
model: 'models/ignored',
contents: 'Hello',
} as unknown as GenerateContentParameters);

const [anthropicRequest] =
anthropicState.lastCreateArgs as AnthropicCreateArgs;
expect(anthropicRequest).toEqual(
expect.objectContaining({ max_tokens: 9000 }),
);
});

it('respects configured max_tokens for unknown models', async () => {
const { AnthropicContentGenerator } = await importGenerator();
anthropicState.createImpl.mockResolvedValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
tokenLimit,
CAPPED_DEFAULT_MAX_TOKENS,
hasExplicitOutputLimit,
parsePositiveIntegerEnvValue,
} from '../tokenLimits.js';

const debugLogger = createDebugLogger('ANTHROPIC');
Expand Down Expand Up @@ -594,9 +595,10 @@ export class AnthropicContentGenerator implements ContentGenerator {
: userMaxTokens;
} else {
// No explicit user config — check env var, then use capped default.
const envVal = process.env['QWEN_CODE_MAX_OUTPUT_TOKENS'];
const envMaxTokens = envVal ? parseInt(envVal, 10) : NaN;
if (!isNaN(envMaxTokens) && envMaxTokens > 0) {
const envMaxTokens = parsePositiveIntegerEnvValue(
process.env['QWEN_CODE_MAX_OUTPUT_TOKENS'],
);
if (envMaxTokens !== undefined) {
maxTokens = isKnownModel
? Math.min(envMaxTokens, modelLimit)
: envMaxTokens;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
expect,
vi,
beforeEach,
afterEach,
type MockedFunction,
} from 'vitest';
import OpenAI from 'openai';
Expand Down Expand Up @@ -37,12 +38,16 @@ vi.mock('../../../utils/runtimeFetchOptions.js', () => ({
}));

describe('DefaultOpenAICompatibleProvider', () => {
const MAX_OUTPUT_TOKENS_ENV = 'QWEN_CODE_MAX_OUTPUT_TOKENS';
let provider: DefaultOpenAICompatibleProvider;
let mockContentGeneratorConfig: ContentGeneratorConfig;
let mockCliConfig: Config;
let savedMaxOutputTokensEnv: string | undefined;

beforeEach(() => {
vi.clearAllMocks();
savedMaxOutputTokensEnv = process.env[MAX_OUTPUT_TOKENS_ENV];
delete process.env[MAX_OUTPUT_TOKENS_ENV];
const mockedBuildRuntimeFetchOptions =
buildRuntimeFetchOptions as unknown as MockedFunction<
(sdkType: 'openai', proxyUrl?: string) => OpenAIRuntimeFetchOptions
Expand Down Expand Up @@ -70,6 +75,14 @@ describe('DefaultOpenAICompatibleProvider', () => {
);
});

afterEach(() => {
if (savedMaxOutputTokensEnv === undefined) {
delete process.env[MAX_OUTPUT_TOKENS_ENV];
} else {
process.env[MAX_OUTPUT_TOKENS_ENV] = savedMaxOutputTokensEnv;
}
});

describe('constructor', () => {
it('should initialize with provided configs', () => {
expect(provider).toBeInstanceOf(DefaultOpenAICompatibleProvider);
Expand Down Expand Up @@ -209,6 +222,33 @@ describe('DefaultOpenAICompatibleProvider', () => {
expect(result.max_tokens).toBe(8000);
});

it('should ignore malformed QWEN_CODE_MAX_OUTPUT_TOKENS values', () => {
const request: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }],
};

for (const envValue of ['1.5', '2k', 'abc']) {
process.env[MAX_OUTPUT_TOKENS_ENV] = envValue;

const result = provider.buildRequest(request, 'prompt-id');

expect(result.max_tokens).toBe(8000);
}
});

it('should respect a valid QWEN_CODE_MAX_OUTPUT_TOKENS value', () => {
process.env[MAX_OUTPUT_TOKENS_ENV] = '9000';
const request: OpenAI.Chat.ChatCompletionCreateParams = {
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }],
};

const result = provider.buildRequest(request, 'prompt-id');

expect(result.max_tokens).toBe(9000);
});

it('should respect user max_tokens for unknown models (deployment aliases, self-hosted)', () => {
// Unknown models: user config is respected entirely (backend may support larger limits)
const request: OpenAI.Chat.ChatCompletionCreateParams = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
tokenLimit,
CAPPED_DEFAULT_MAX_TOKENS,
hasExplicitOutputLimit,
parsePositiveIntegerEnvValue,
} from '../../tokenLimits.js';

type AssistantMessageWithReasoningFields =
Expand Down Expand Up @@ -187,9 +188,10 @@ export class DefaultOpenAICompatibleProvider
// No explicit user config — check env var, then use capped default.
// Capped default (8K) reduces GPU slot over-reservation by ~4×.
// Requests hitting the cap get one clean retry at 64K (geminiChat.ts).
const envVal = process.env['QWEN_CODE_MAX_OUTPUT_TOKENS'];
const envMaxTokens = envVal ? parseInt(envVal, 10) : NaN;
if (!isNaN(envMaxTokens) && envMaxTokens > 0) {
const envMaxTokens = parsePositiveIntegerEnvValue(
process.env['QWEN_CODE_MAX_OUTPUT_TOKENS'],
);
if (envMaxTokens !== undefined) {
effectiveMaxTokens = isKnownModel
? Math.min(envMaxTokens, modelLimit)
: envMaxTokens;
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/core/tokenLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ export const DEFAULT_OUTPUT_TOKEN_LIMIT: TokenCount = 32_000; // 32K tokens
export const CAPPED_DEFAULT_MAX_TOKENS: TokenCount = 8_000;
export const ESCALATED_MAX_TOKENS: TokenCount = 64_000;

export function parsePositiveIntegerEnvValue(
raw: string | undefined,
): number | undefined {
if (raw === undefined) return undefined;

const trimmed = raw.trim();
if (!/^\d+$/.test(trimmed)) return undefined;

const parsed = Number(trimmed);
if (!Number.isSafeInteger(parsed) || parsed <= 0) return undefined;

return parsed;
}

/**
* Accurate numeric limits:
* - power-of-two approximations (128K -> 131072, 256K -> 262144, etc.)
Expand Down
Loading