Skip to content
Closed
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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@

## [v0.3.38](https://github.com/ding113/claude-code-hub/releases/tag/v0.3.38) - 2025-12-29

### 新增

- Session 详情页新增请求/响应头日志展示,支持 Tab 切换查看 (#469)
- 排行榜新增排序和供应商类型筛选功能 (#448) [@YewFence](https://github.com/YewFence)
- 虚拟化表格组件 (use-virtualizer hook) 用于大数据量列表性能优化 (#467) [@NightYuYyy](https://github.com/NightYuYyy)
- 新增 `FETCH_CONNECT_TIMEOUT` 环境变量,统一配置 Undici 连接超时(默认 30 秒)(#479, #480)

### 优化

- 供应商管理页面 UX 改进,优化交互体验 (#446) [@miraserver](https://github.com/miraserver)
- 用户筛选与排序体验优化,移除使用日志用户筛选限制 (#462, #449) [@NightYu](https://github.com/NightYuYyy)
- 缓存 tooltip 显示改进,当 5m/1h breakdown 不可用时提供友好提示 (#445) [@Hwwwww](https://github.com/Hwwwww-dev)
- TagInput 组件和虚拟化表格稳定性增强 (#467) [@NightYuYyy](https://github.com/NightYuYyy)
- SSE 解析工具增强,添加错误处理和测试 (#469)
- Session 消息客户端 SSE 性能和 matchMedia 回退优化 (#469)

### 修复

- 修复计费模型来源配置不生效问题 (#464)
- Codex instructions 一律透传,移除缓存与策略 (#475)
- 修复 Session 详情页中的 tool_use_id 验证问题 (#473, #472)
- 修复日志表格中供应商名称溢出问题 (#478) [@YangQing-Lin](https://github.com/YangQing-Lin)
- 请求过滤器 header 修改追踪修复,确保在 Session 详情中正确显示 (#465)
- 数据导入组件优化,移除重复描述文本 (#458) [@Abner](https://github.com)

### 其他

- 新增多项单元测试:undici 超时、proxy forwarder、session 等 (#469, #479)
- 移除 codex-instructions-cache.ts 模块,简化代码结构 (#475)
Comment on lines +9 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The changelog entries added in this pull request do not seem to correspond to the feature being introduced (feat: add models-list endpoint support). The changelog should accurately reflect the changes made in this PR. Please update this section to describe the new models-list endpoint support.

---

## [v0.3.37](https://github.com/ding113/claude-code-hub/releases/tag/v0.3.37) - 2025-12-24
Expand Down
24 changes: 23 additions & 1 deletion src/app/v1/_lib/proxy/format-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import type { Format } from "../converters/types";
* - "claude": 检测到 Claude Messages API 格式的请求(通过 `system` 或 Claude 特有字段)
* - "gemini": 检测到 Gemini API 直接格式的请求(通过 `contents` 字段)
* - "gemini-cli": 检测到 Gemini CLI 格式的请求(通过 `request` envelope)
* - "models-list": 模型列表请求(格式无关,不限制供应商类型)
*/
export type ClientFormat = "response" | "openai" | "claude" | "gemini" | "gemini-cli";
export type ClientFormat = "response" | "openai" | "claude" | "gemini" | "gemini-cli" | "models-list";

/**
* 根据请求端点检测客户端格式(优先级最高)
Expand Down Expand Up @@ -67,6 +68,13 @@ export function detectFormatByEndpoint(pathname: string): ClientFormat | null {
// OpenAI Chat Completions
{ pattern: /^\/v1\/chat\/completions$/i, format: "openai" },

// ⭐ 模型列表端点(格式无关,不限制供应商类型,仅按用户分组过滤)
// 支持多种客户端:有些请求 /v1/models,有些在 base URL 后加 /models
{ pattern: /^\/v1\/models$/i, format: "models-list" },
{ pattern: /^\/v1\/responses\/models$/i, format: "models-list" },
{ pattern: /^\/v1\/chat\/completions\/models$/i, format: "models-list" },
Comment on lines +73 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better maintainability and conciseness, the three regular expressions for the /v1 model list endpoints can be combined into a single one using a non-capturing group.

Suggested change
{ pattern: /^\/v1\/models$/i, format: "models-list" },
{ pattern: /^\/v1\/responses\/models$/i, format: "models-list" },
{ pattern: /^\/v1\/chat\/completions\/models$/i, format: "models-list" },
{ pattern: /^\/v1(?:\/(?:responses|chat\/completions))?\/models$/i, format: "models-list" },

{ pattern: /^\/v1beta\/models$/i, format: "models-list" },

// Gemini Direct API
{
pattern: /^\/v1beta\/models\/[^/:]+:(?:generateContent|streamGenerateContent|countTokens)$/i,
Expand Down Expand Up @@ -107,6 +115,10 @@ export function mapClientFormatToTransformer(clientFormat: ClientFormat): Format
return "gemini-cli"; // 直接 Gemini 格式内部使用 gemini-cli 转换器
case "gemini-cli":
return "gemini-cli";
case "models-list":
// 模型列表端点不需要格式转换,默认返回 openai-compatible
// 因为大多数客户端期望 OpenAI 格式的模型列表响应
return "openai-compatible";
default: {
// 类型守卫:如果有未处理的格式,TypeScript 会报错
const _exhaustiveCheck: never = clientFormat;
Expand Down Expand Up @@ -156,6 +168,16 @@ export function mapTransformerFormatToClient(transformerFormat: Format): ClientF
}
}

/**
* 检查是否为模型列表端点(格式无关)
*
* @param format - 客户端格式
* @returns 是否为模型列表端点
*/
export function isModelsListFormat(format: ClientFormat): boolean {
return format === "models-list";
}

/**
* 检测请求格式(基于请求体结构)
*
Expand Down
24 changes: 23 additions & 1 deletion src/app/v1/_lib/proxy/forwarder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ const STANDARD_ENDPOINTS = [
"/v1/messages",
"/v1/messages/count_tokens",
"/v1/responses",
"/v1/responses/models",
"/v1/chat/completions",
"/v1/chat/completions/models",
"/v1/models",
"/v1beta/models",
];

const RETRY_LIMITS = PROVIDER_LIMITS.MAX_RETRY_ATTEMPTS;
Expand Down Expand Up @@ -1083,10 +1086,29 @@ export class ProxyForwarder {
);
}

// ⭐ 兼容客户端在 base URL 后加 /models 获取模型列表
// 例如客户端配置 base URL 为 /v1/responses,会请求 /v1/responses/models
// 需要重写为标准的 /v1/models 路径
const MODELS_PATH_REWRITES: Record<string, string> = {
"/v1/responses/models": "/v1/models",
"/v1/chat/completions/models": "/v1/models",
};

let effectiveRequestUrl = session.requestUrl;
const rewrittenPath = MODELS_PATH_REWRITES[requestPath];
if (rewrittenPath) {
effectiveRequestUrl = new URL(session.requestUrl.href);
effectiveRequestUrl.pathname = rewrittenPath;
logger.debug("ProxyForwarder: Rewriting models path", {
originalPath: requestPath,
rewrittenPath,
});
}

// ⭐ 直接使用原始请求路径,让 buildProxyUrl() 智能处理路径拼接
// 移除了强制 /v1/responses 路径重写,解决 Issue #139
// buildProxyUrl() 会检测 base_url 是否已包含完整路径,避免重复拼接
proxyUrl = buildProxyUrl(effectiveBaseUrl, session.requestUrl);
proxyUrl = buildProxyUrl(effectiveBaseUrl, effectiveRequestUrl);

logger.debug("ProxyForwarder: Final proxy URL", {
url: proxyUrl,
Expand Down
16 changes: 14 additions & 2 deletions src/app/v1/_lib/proxy/provider-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getSystemSettings } from "@/repository/system-config";
import type { ProviderChainItem } from "@/types/message";
import type { Provider } from "@/types/provider";
import type { ClientFormat } from "./format-mapper";
import { isModelsListFormat } from "./format-mapper";
import { ProxyResponses } from "./responses";
import type { ProxySession } from "./session";

Expand Down Expand Up @@ -181,6 +182,7 @@ function providerSupportsModel(provider: Provider, requestedModel: string): bool
* - openai → openai-compatible
* - gemini → gemini
* - gemini-cli → gemini-cli
* - models-list → 所有类型(格式无关,不限制供应商类型)
*
* @param format - 客户端请求格式(从 session.originalFormat 获取)
* @param providerType - 供应商类型
Expand All @@ -192,6 +194,11 @@ function checkFormatProviderTypeCompatibility(
format: ClientFormat,
providerType: Provider["providerType"]
): boolean {
// ⭐ 模型列表端点:格式无关,不限制供应商类型
if (isModelsListFormat(format)) {
return true;
}

switch (format) {
case "claude":
return providerType === "claude" || providerType === "claude-auth";
Expand Down Expand Up @@ -713,8 +720,13 @@ export class ProxyProviderResolver {

// 2c. 模型匹配(保留原有逻辑)
if (!requestedModel) {
// 没有模型信息时,只选择 Anthropic 提供商(向后兼容)
return provider.providerType === "claude";
// 没有模型信息时,根据请求格式选择对应类型的供应商
if (session?.originalFormat) {
// 如果有格式信息,已经在 2b 步骤过滤过了,这里直接返回 true
return true;
}
// 没有格式信息时,只选择 Anthropic 提供商(向后兼容)
return provider.providerType === "claude" || provider.providerType === "claude-auth";
}

return providerSupportsModel(provider, requestedModel);
Expand Down
Loading