Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ scripts/claude-proxy.log
.env
.env.local
.env.*.local
.vscode/
21 changes: 0 additions & 21 deletions .vscode/launch.json

This file was deleted.

12 changes: 0 additions & 12 deletions .vscode/tasks.json

This file was deleted.

14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@
- reduce maxInputTokens for GPT-5.6 models to 272000 (b5febfc, SmallMain)
- remove redundant Anthropic Messages variant (aa0d17c, RheagalFire)

## v7.12.5 - 2026-07-23

### Fixes
- return cancelled queued RPM acquisitions immediately instead of waiting behind earlier FIFO entries; cancelled entries still consume no token when they reach the queue head

## v7.12.4 - 2026-07-14

### Fixes
- make provider RPM token buckets FIFO-safe under concurrent chat requests and show available tokens with two decimal places
- clarify per-window logical chat request RPM behavior and preserve explicit per-provider `rpm: 0` overrides
- treat invalid/empty per-provider `rateLimit` (e.g. `{}` or non-integer `rpm`) as unconfigured so the global default applies, while still honoring explicit `rpm: 0`
- make RPM token acquisition cancellation-aware: aborting a chat request while waiting for a token no longer blocks or consumes a token
- log the exact post-acquire token snapshot by peeking the bucket without an extra time-based refill

## v7.12.3 - 2026-07-12

### Features
Expand Down
4 changes: 4 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@
"Extra body parameters must be configured in VS Code settings (JSON).": "Extra body parameters must be configured in VS Code settings (JSON).",
"{0} properties": "{0} properties",
"Network Settings": "Network Settings",
"Rate Limit (RPM)": "Rate Limit (RPM)",
"Maximum requests per minute (0 = disabled)": "Maximum requests per minute (0 = disabled)",
"Enter maximum requests per minute (0 = disabled)": "Enter maximum requests per minute (0 = disabled)",
"rpm: {0}": "rpm: {0}",
"default": "default",
"conn: {0}ms": "conn: {0}ms",
"resp: {0}ms": "resp: {0}ms",
Expand Down
4 changes: 4 additions & 0 deletions l10n/bundle.l10n.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@
"Extra body parameters must be configured in VS Code settings (JSON).": "额外 Body 参数必须在 VS Code 设置(JSON)中配置。",
"{0} properties": "{0} 个属性",
"Network Settings": "网络设置",
"Rate Limit (RPM)": "RPM 限流",
"Maximum requests per minute (0 = disabled)": "每分钟最大请求数(0 = 禁用)",
"Enter maximum requests per minute (0 = disabled)": "输入每分钟最大请求数(0 = 禁用)",
"rpm: {0}": "RPM: {0}",
"default": "默认",
"conn: {0}ms": "连接:{0}ms",
"resp: {0}ms": "响应:{0}ms",
Expand Down
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,18 @@
"scope": "application",
"description": "%configuration.networkSettings.description%",
"properties": {
"rateLimit": {
"type": "object",
"description": "%configuration.networkSettings.rateLimit.description%",
"properties": {
"rpm": {
"type": "integer",
"description": "%configuration.networkSettings.rateLimit.rpm.description%",
"default": 0,
"minimum": 0
}
}
},
"timeout": {
"type": "object",
"description": "%configuration.networkSettings.timeout.description%",
Expand Down Expand Up @@ -1003,6 +1015,18 @@
"description": "%configuration.endpoints.extraBody.description%",
"additionalProperties": true
},
"rateLimit": {
"type": "object",
"description": "%configuration.endpoints.rateLimit.description%",
"properties": {
"rpm": {
"type": "integer",
"description": "%configuration.endpoints.rateLimit.rpm.description%",
"default": 0,
"minimum": 0
}
}
},
"timeout": {
"type": "object",
"description": "%configuration.endpoints.timeout.description%",
Expand Down
6 changes: 5 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"configuration.balanceWarning.timeThresholdDays.description": "Time threshold in days for expiration warnings (supports decimals). Default: 1.",
"configuration.balanceWarning.amountThreshold.description": "Balance amount threshold for warnings (unitless; currency ignored). Default: 1.",
"configuration.balanceWarning.tokenThresholdMillions.description": "Token remaining threshold in millions for warnings. Default: 1.",
"configuration.networkSettings.description": "Global network settings. Timeout and retry affect chat requests; proxy affects provider HTTP requests.",
"configuration.networkSettings.description": "Global network settings. Rate limits, timeouts, and retries affect logical chat requests; proxy affects provider HTTP requests.",
"configuration.networkSettings.timeout.description": "Timeout settings for chat requests (milliseconds).",
"configuration.networkSettings.timeout.connection.description": "TCP connection timeout in milliseconds (chat requests).",
"configuration.networkSettings.timeout.response.description": "Maximum time between SSE data chunks in milliseconds (chat requests).",
Expand All @@ -56,6 +56,8 @@
"configuration.networkSettings.retry.jitterFactor.description": "Jitter factor (0-1) to randomize retry delay (chat requests).",
"configuration.networkSettings.retry.statusCodes.description": "HTTP status codes that should trigger retries for chat requests. When set, this overrides the default retryable status codes.",
"configuration.networkSettings.proxy.description": "Global proxy settings for provider requests.",
"configuration.networkSettings.rateLimit.description": "Default rate-limit settings for each provider in this VS Code window. Provider-level values override these settings.",
"configuration.networkSettings.rateLimit.rpm.description": "Maximum logical chat requests per minute for each provider in this VS Code window. Set to 0 (default) to disable rate limiting.",
"configuration.proxy.type.description": "Proxy mode.",
"configuration.proxy.type.enumDescriptions.0": "Use VS Code HTTP proxy settings.",
"configuration.proxy.type.enumDescriptions.1": "Connect directly without using a proxy.",
Expand Down Expand Up @@ -150,6 +152,8 @@
"configuration.endpoints.retry.jitterFactor.description": "Jitter factor (0-1) to randomize retry delay (chat requests).",
"configuration.endpoints.proxy.description": "Proxy settings for this provider. Overrides global proxy settings.",
"configuration.endpoints.autoFetchOfficialModels.description": "Automatically fetch and sync official models from the provider API.",
"configuration.endpoints.rateLimit.description": "Rate-limit settings for logical chat requests in this VS Code window.",
"configuration.endpoints.rateLimit.rpm.description": "Maximum logical chat requests per minute for this provider in this VS Code window. Set to 0 (default) to disable rate limiting.",
"configuration.endpoints.models.description": "List of available models.",
"configuration.endpoints.models.string.description": "Model ID",
"configuration.endpoints.models.id.description": "Model ID (e.g., claude-sonnet-4-20250514).",
Expand Down
6 changes: 5 additions & 1 deletion package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"configuration.balanceWarning.timeThresholdDays.description": "时间阈值(天),用于到期提醒(支持小数)。默认值:1。",
"configuration.balanceWarning.amountThreshold.description": "余额阈值(无单位,忽略货币)。默认值:1。",
"configuration.balanceWarning.tokenThresholdMillions.description": "令牌数阈值(单位:百万),用于剩余令牌提醒。默认值:1。",
"configuration.networkSettings.description": "全局网络设置。超时与重试影响聊天请求;代理影响供应商 HTTP 请求。",
"configuration.networkSettings.description": "全局网络设置。限流、超时与重试影响逻辑聊天请求;代理影响供应商 HTTP 请求。",
"configuration.networkSettings.timeout.description": "聊天请求的超时设置(毫秒)。",
"configuration.networkSettings.timeout.connection.description": "TCP 连接超时(毫秒)(聊天请求)。",
"configuration.networkSettings.timeout.response.description": "SSE 流式传输期间,两次数据分片之间的最大等待时间(毫秒)(聊天请求)。",
Expand All @@ -56,6 +56,8 @@
"configuration.networkSettings.retry.jitterFactor.description": "聊天请求的抖动因子(0-1),用于随机化重试延迟。",
"configuration.networkSettings.retry.statusCodes.description": "聊天请求中触发重试的 HTTP 状态码。设置后会覆盖默认可重试状态码。",
"configuration.networkSettings.proxy.description": "供应商请求的全局代理设置。",
"configuration.networkSettings.rateLimit.description": "此 VS Code 窗口内每个供应商的默认主动限流设置;供应商级设置可覆盖。",
"configuration.networkSettings.rateLimit.rpm.description": "此 VS Code 窗口内每个供应商的逻辑聊天请求每分钟最大数量。设为 0(默认)表示不限流。",
"configuration.proxy.type.description": "代理模式。",
"configuration.proxy.type.enumDescriptions.0": "使用 VS Code HTTP 代理设置。",
"configuration.proxy.type.enumDescriptions.1": "直连,不使用代理。",
Expand Down Expand Up @@ -150,6 +152,8 @@
"configuration.endpoints.retry.jitterFactor.description": "聊天请求的抖动因子(0-1),用于随机化重试延迟。",
"configuration.endpoints.proxy.description": "此供应商的代理设置。会覆盖全局代理设置。",
"configuration.endpoints.autoFetchOfficialModels.description": "自动从供应商 API 拉取并同步官方模型。",
"configuration.endpoints.rateLimit.description": "此 VS Code 窗口内逻辑聊天请求的主动限流设置。",
"configuration.endpoints.rateLimit.rpm.description": "此 VS Code 窗口内该供应商的逻辑聊天请求每分钟最大数量。设为 0(默认)表示不限流。",
"configuration.endpoints.models.description": "可用模型列表。",
"configuration.endpoints.models.string.description": "模型 ID",
"configuration.endpoints.models.id.description": "模型 ID(例如:claude-sonnet-4-20250514)。",
Expand Down
17 changes: 9 additions & 8 deletions scripts/chat-lib-diff-patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,29 +395,30 @@ export function patchChatLibDiffSource(
source: string,
): string {
const resolved = resolve(filePath);
const normalizedSource = source.replace(/\r\n?/g, '\n');
if (resolved === arraysPath) {
return patchArrays(filePath, source);
return patchArrays(filePath, normalizedSource);
}
if (resolved === assertPath) {
return patchAssert(filePath, source);
return patchAssert(filePath, normalizedSource);
}
if (resolved === abstractTextPath) {
return patchAbstractText(filePath, source);
return patchAbstractText(filePath, normalizedSource);
}
if (resolved === defaultLinesDiffComputerPath) {
return patchDefaultLinesDiffComputer(filePath, source);
return patchDefaultLinesDiffComputer(filePath, normalizedSource);
}
if (resolved === heuristicSequenceOptimizationsPath) {
return patchHeuristicSequenceOptimizations(filePath, source);
return patchHeuristicSequenceOptimizations(filePath, normalizedSource);
}
if (resolved === positionToOffsetPath) {
return patchPositionToOffset(filePath, source);
return patchPositionToOffset(filePath, normalizedSource);
}
if (resolved === positionToOffsetImplPath) {
return patchPositionToOffsetImpl(filePath, source);
return patchPositionToOffsetImpl(filePath, normalizedSource);
}
if (resolved === rangeMappingPath) {
return patchRangeMapping(filePath, source);
return patchRangeMapping(filePath, normalizedSource);
}
return source;
}
5 changes: 3 additions & 2 deletions scripts/chat-lib-parser-patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ export function patchChatLibParserSource(
source: string,
): string {
const resolved = resolve(filePath);
const normalizedSource = source.replace(/\r\n?/g, '\n');
if (resolved === blockTrimmerPath) {
return replaceOnce(
source,
normalizedSource,
"import { IPosition, TextDocumentContents } from '../textDocument';",
[
'interface IPosition {',
Expand All @@ -51,7 +52,7 @@ export function patchChatLibParserSource(
}
if (resolved === parseBlockPath) {
return replaceOnce(
source,
normalizedSource,
[
'\t\tif (endIndex < solution.length) {',
'\t\t\t// descendant block is finished, stop at end of block',
Expand Down
11 changes: 11 additions & 0 deletions src/client/anthropic/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
setUserAgentHeader,
} from '../utils';
import type { AuthTokenInfo, AuthTokenRefresh } from '../../auth/types';
import { createRateLimiter } from '../../rate-limit';

type AnthropicMarkerData = {
raw: BetaMessage;
Expand Down Expand Up @@ -116,8 +117,10 @@ const ANTHROPIC_ABNORMAL_STOP_REASONS: ReadonlySet<string> = new Set([

export class AnthropicProvider implements ApiProvider {
private readonly baseUrl: string;
private readonly rateLimiter: ReturnType<typeof createRateLimiter>;

constructor(protected readonly config: ProviderConfig) {
this.rateLimiter = createRateLimiter(config.rateLimit);
this.baseUrl = buildBaseUrl(config.baseUrl, {
stripPattern: /\/v1$/i,
useRawBaseUrl: isRawBaseUrlEnabled(config),
Expand All @@ -136,6 +139,14 @@ export class AnthropicProvider implements ApiProvider {
* Create an Anthropic client with custom fetch for retry support.
* A new client is created per request to enable per-request logging.
*/
getRateLimitStatus(): { available: number; capacity: number } | undefined {
return this.rateLimiter?.getAvailableTokens();
}

async acquireRateLimitToken(signal?: AbortSignal): Promise<void> {
await this.rateLimiter?.acquire(signal);
}

protected createClient(
logger: ProviderHttpLogger | undefined,
stream: boolean,
Expand Down
11 changes: 11 additions & 0 deletions src/client/github-copilot/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '../../utils';
import type { ApiProvider } from '../interface';
import { buildBaseUrl, createCustomFetch, getToken } from '../utils';
import { createRateLimiter } from '../../rate-limit';
import { OpenAIChatCompletionProvider } from '../openai/chat-completion-client';
import { OpenAIResponsesProvider } from '../openai/responses-client';
import type { ChatCompletionChunk } from 'openai/resources/chat/completions';
Expand Down Expand Up @@ -669,6 +670,7 @@ export class GitHubCopilotProvider implements ApiProvider {
private readonly chatProvider: GitHubCopilotChatCompletionProvider;
private readonly responsesProvider: GitHubCopilotResponsesProvider;
private readonly messagesProvider: GitHubCopilotMessagesProvider;
private readonly rateLimiter: ReturnType<typeof createRateLimiter>;

private assertCopilotAuth(): void {
if (this.providerConfig.auth?.method !== 'github-copilot') {
Expand All @@ -679,6 +681,7 @@ export class GitHubCopilotProvider implements ApiProvider {
}

constructor(config: ProviderConfig) {
this.rateLimiter = createRateLimiter(config.rateLimit);
const extraBody = config.extraBody ?? {};
const hasStore = Object.prototype.hasOwnProperty.call(extraBody, 'store');
const configWithDefaults: ProviderConfig = hasStore
Expand All @@ -699,6 +702,14 @@ export class GitHubCopilotProvider implements ApiProvider {
});
}

getRateLimitStatus(): { available: number; capacity: number } | undefined {
return this.rateLimiter?.getAvailableTokens();
}

async acquireRateLimitToken(signal?: AbortSignal): Promise<void> {
await this.rateLimiter?.acquire(signal);
}

async *streamChat(
encodedModelId: string,
model: ModelConfig,
Expand Down
11 changes: 11 additions & 0 deletions src/client/google/ai-studio-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
type RetryConfig,
withIdleTimeout,
} from '../../utils';
import { createRateLimiter } from '../../rate-limit';
import { getBaseModelId } from '../../model-id-utils';
import { ThinkingBlockMetadata } from '../types';
import {
Expand Down Expand Up @@ -157,8 +158,18 @@ function getThoughtSignature(part: Part): string | undefined {
export class GoogleAIStudioProvider implements ApiProvider {
protected readonly baseUrl: string;
protected readonly apiVersion: string;
protected readonly rateLimiter: ReturnType<typeof createRateLimiter>;

getRateLimitStatus(): { available: number; capacity: number } | undefined {
return this.rateLimiter?.getAvailableTokens();
}

async acquireRateLimitToken(signal?: AbortSignal): Promise<void> {
await this.rateLimiter?.acquire(signal);
}

constructor(protected readonly config: ProviderConfig) {
this.rateLimiter = createRateLimiter(config.rateLimit);
if (isRawBaseUrlEnabled(config)) {
this.baseUrl = normalizeRawBaseUrlInput(config.baseUrl);
this.apiVersion = 'v1beta';
Expand Down
16 changes: 16 additions & 0 deletions src/client/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,20 @@ export interface ApiProvider {
refreshCredential?: AuthTokenRefresh,
signal?: AbortSignal,
): Promise<ModelConfig[]>;

/**
* Get the current rate-limit status (token bucket snapshot).
* Returns undefined when no rate limiting is configured for this provider.
*/
getRateLimitStatus?(): { available: number; capacity: number } | undefined;

/**
* Acquire a token for one logical chat request, waiting if necessary.
* Called by the service before transport selection so HTTP, SSE, and
* WebSocket requests share the same limiter.
*
* If `signal` is aborted while waiting, rejects (without consuming a token)
* so a cancelled chat request doesn't waste a rate-limit slot.
*/
acquireRateLimitToken?(signal?: AbortSignal): Promise<void>;
}
11 changes: 11 additions & 0 deletions src/client/ollama/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
normalizeToolInputSchema,
processUsage as sharedProcessUsage,
} from '../utils';
import { createRateLimiter } from '../../rate-limit';

const TOOL_CALL_ID_PREFIX = 'ollama-tool:';

Expand Down Expand Up @@ -104,8 +105,18 @@ const OLLAMA_ABNORMAL_DONE_REASONS: ReadonlySet<string> = new Set([

export class OllamaProvider implements ApiProvider {
private readonly baseUrl: string;
private readonly rateLimiter: ReturnType<typeof createRateLimiter>;

getRateLimitStatus(): { available: number; capacity: number } | undefined {
return this.rateLimiter?.getAvailableTokens();
}

async acquireRateLimitToken(signal?: AbortSignal): Promise<void> {
await this.rateLimiter?.acquire(signal);
}

constructor(private readonly config: ProviderConfig) {
this.rateLimiter = createRateLimiter(config.rateLimit);
this.baseUrl = buildBaseUrl(config.baseUrl, {
stripPattern: /\/api$/i,
useRawBaseUrl: isRawBaseUrlEnabled(config),
Expand Down
Loading