Skip to content

feat: add response model detection tool#2

Open
ding113 wants to merge 4 commits intomainfrom
feat/model-detection
Open

feat: add response model detection tool#2
ding113 wants to merge 4 commits intomainfrom
feat/model-detection

Conversation

@ding113
Copy link
Copy Markdown
Owner

@ding113 ding113 commented Apr 8, 2026

Summary

  • add a new response model detection page under /docs with a navigation entry
  • implement pure-frontend detection logic with provider-specific request fallbacks and family probability scoring
  • add vitest coverage for navigation, scoring, request builders, and the UI flow

Verification

  • bun run test
  • bun run build
  • bunx biome check package.json src/lib/navigation.ts src/lib/navigation.test.ts src/lib/model-detection src/app/docs/model-detection vitest.config.ts vitest.setup.ts

Notes

  • the site UI does not expose the underlying probe strings
  • full-repo The number of diagnostics exceeds the limit allowed. Use --max-diagnostics to increase it.
    Diagnostics not shown: 14.
    Checked 87 files in 39ms. No fixes applied.
    Found 34 errors. still reports pre-existing issues outside this change set

Greptile Summary

This PR adds a pure-frontend model-family detection page (/docs/model-detection) that probes a user-supplied API endpoint with glitch-token strings and ranks candidate model families by how often the model fails to echo each probe. The concurrency runner, scoring logic, and request-builder fallback chain are all well-structured, and test coverage addresses the key paths (timeout, invalid URL, response extraction, scoring edge cases).

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/improvement suggestions that do not affect correctness.

Prior P1 findings (double extractResponseTextFromAny call, dead Suspense boundary, Gemini URL degenerate case) are flagged in previous review threads. The two new inline comments are both P2: stale-result UX and the endpointType-ordering preference in extractResponseTextFromAny. Core detection logic, concurrency worker pattern, scoring, and test coverage are sound.

src/lib/model-detection/runner.ts and src/app/docs/model-detection/components/ModelDetectionClient.tsx have the open P2 suggestions.

Vulnerabilities

No security concerns identified. The API key is typed directly by the user into a type="password" field, held only in React component state, and sent straight from the browser to the user-supplied endpoint — it never touches the application's own backend. All detection logic runs client-side. No secrets are logged or persisted.

Important Files Changed

Filename Overview
src/lib/model-detection/runner.ts Core detection orchestrator: concurrency worker pattern is correct; open issues flagged (double extractResponseTextFromAny call, endpointType-agnostic text extraction).
src/lib/model-detection/providers.ts Request-builder and response-text extractor; Gemini URL degenerate case flagged in prior review; ensurePath and version-segment stripping logic are otherwise correct.
src/lib/model-detection/scoring.ts Scoring logic is correct; hit semantics (repeatedExactly === false signals glitch token) are intentional and documented.
src/app/docs/model-detection/components/ModelDetectionClient.tsx Client UI component; stale result visible during re-detection (result not cleared on new submit start); otherwise form logic, disabled-state guard, and error display are clean.
src/app/docs/model-detection/page.tsx Server page component; Suspense wrapper is dead code (flagged in prior review) — ModelDetectionClient is imported eagerly without React.lazy.
src/lib/model-detection/families.ts Defines probe families and glitch-token lists; probe strings are intentionally not surfaced in the UI.
src/lib/model-detection/types.ts Type definitions are clean and well-structured; all interfaces match their usage.
src/lib/navigation.ts Navigation entry for model-detection page added correctly.

Sequence Diagram

sequenceDiagram
    participant UI as ModelDetectionClient
    participant Runner as runner.ts
    participant Providers as providers.ts
    participant API as External API

    UI->>Runner: runModelDetection(input)
    Runner->>Runner: buildProbeEntries() — flatten families × probes
    loop mapWithConcurrency (4 workers)
        Runner->>Runner: runProbe(input, familyId, probe)
        Runner->>Providers: buildRequestCandidates(input + probe)
        Providers-->>Runner: RequestCandidate[]
        loop try each candidate in order
            Runner->>API: POST /v1/responses (or /messages, :generateContent)
            alt HTTP 200
                API-->>Runner: payload
                Runner->>Providers: extractResponseTextFromAny(payload)
                Providers-->>Runner: rawText
                Runner-->>Runner: repeatedExactly = rawText.includes(probe)
            else HTTP error or timeout
                Runner->>Runner: push error, try next candidate
            end
        end
        Runner-->>Runner: ProbeObservation
    end
    Runner->>Runner: summarizeDetectionFamilies(families, observations)
    Runner-->>UI: ModelDetectionResult (rankings + summary)
    UI->>UI: render ranked family cards
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/app/docs/model-detection/components/ModelDetectionClient.tsx
Line: 62-85

Comment:
**Stale result visible during re-detection**

When the user submits the form a second time, the previous `result` is never cleared before the new detection starts. `setError('')` is cleared, `isRunning` is set, but `result` keeps its old value — so the previous rankings stay visible under "检测中..." for the entire run duration. On success the state is replaced; on failure `setResult(null)` clears it. Adding a clear at the top of the try block keeps the UI consistent:

```suggestion
    setIsRunning(true)
    setResult(null)
    setError('')
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/lib/model-detection/runner.ts
Line: 109-118

Comment:
**`extractResponseTextFromAny` ignores configured `endpointType`**

The function always tries parsers in the fixed order `openai → anthropic → gemini` regardless of which endpoint type was configured for this detection run. In practice the three response shapes use distinct top-level keys (`choices`/`output` vs `content` vs `candidates`), so cross-parser false extraction is unlikely today. However, if a proxy or new API version ever surfaces a response that satisfies both the OpenAI and Anthropic matchers (e.g. a body with a top-level `content` array alongside `choices`), the wrong text would be silently extracted.

The `endpointType` is available in `runProbe` via `input.endpointType` and could be threaded through to `runCandidateRequest` so `extractResponseText` is called with the correct type first:

```ts
async function runCandidateRequest(
  candidate: RequestCandidate,
  probe: string,
  endpointType: DetectionEndpointType,
): Promise<{ repeatedExactly: boolean; rawText: string }> {
  ...
  const rawText = extractResponseTextFromAny(payload, endpointType)
  ...
}
```

where `extractResponseTextFromAny` tries the configured type first before falling back to the others.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (4): Last reviewed commit: "fix: harden detection request handling" | Re-trigger Greptile

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

新增完整的模型检测子系统:TypeScript 类型与探针数据、请求候选与响应解析、检测运行器与评分逻辑、前端客户端页面与测试、Vitest 测试配置及导航链接更新。

Changes

Cohort / File(s) Summary
测试配置与依赖
package.json, vitest.config.ts, vitest.setup.ts
添加 Vitest 与 React Testing Library 相关 devDependencies、test/test:watch 脚本,新增 Vitest 配置(jsdom、globals、setup 文件)并加载 @testing-library/jest-dom
类型与探针数据
src/lib/model-detection/types.ts, src/lib/model-detection/families.ts
新增模型检测共享类型(输入/结果/候选/观测等)和 MODEL_PROBE_FAMILIES 探针家族定义。
请求候选与响应解析
src/lib/model-detection/providers.ts, src/lib/model-detection/providers.test.ts
新增 provider-specific 请求候选生成(OpenAI/Anthropic/Gemini)、URL/头/body 规范化,以及按端点类型提取归一化响应文本;附带对应单元测试覆盖多种响应形态与候选生成规则。
检测执行与重试/并发
src/lib/model-detection/runner.ts
新增 runModelDetection:构建探针观测、并发执行(上限 4)、按候选顺序重试、解析/提取响应并产出 observations、summary 与 family rankings;包含错误聚合行为。
评分汇总逻辑与测试
src/lib/model-detection/scoring.ts, src/lib/model-detection/scoring.test.ts
新增 summarizeDetectionFamilies:按 family 聚合 observations,计算 hits/tested/total/probability,过滤零概率并排序;附带边界与错误处理测试。
前端页面与组件
src/app/docs/model-detection/page.tsx, src/app/docs/model-detection/components/ModelDetectionClient.tsx, src/app/docs/model-detection/components/ModelDetectionClient.test.tsx
新增客户端页面与可复用组件:表单输入、运行检测调用(可注入 runDetection)、结果渲染(排名、进度条、摘要),及相应的 Vitest + React Testing Library 测试。
导航更新与导航测试
src/lib/navigation.ts, src/lib/navigation.test.ts
在文档导航中新增 "响应模型检测" 链接 /docs/model-detection,并新增测试验证该链接存在。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 分钟

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add response model detection tool' accurately and concisely summarizes the main feature addition: a new response model detection tool with a complete page implementation.
Description check ✅ Passed PR描述清楚地说明了变更内容:添加响应模型检测页面、实现纯前端检测逻辑、添加测试覆盖。描述与代码变更相符。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/model-detection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a model detection tool that identifies AI model families by analyzing API response signatures. The implementation includes a React client component, provider-specific request builders for OpenAI, Anthropic, and Gemini, and a scoring system to rank results. Comprehensive tests and a Vitest configuration are also added. Feedback identifies a bug in the Anthropic request structure, suggests performance optimizations in the runner logic, and recommends improvements for component testability and code documentation.

model,
max_tokens: 128,
stream: false,
system: [{ type: 'text', text: ANTHROPIC_SYSTEM }],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The structure for the system parameter in this Anthropic request candidate appears to be incorrect. The Anthropic Messages API expects the system parameter to be a string, but here it's provided as an array of content blocks. This will likely cause the request to fail. It should be a simple string, as used in the second candidate.

Suggested change
system: [{ type: 'text', text: ANTHROPIC_SYSTEM }],
system: ANTHROPIC_SYSTEM,

Comment on lines +13 to +15
interface ModelDetectionClientProps {
runDetection?: (input: ModelDetectionInput) => Promise<ModelDetectionResult>
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The runDetection prop is a good way to inject dependencies for testing. As an alternative that could simplify the component's props interface, you might consider using vi.mock within your test file to mock the runModelDetection function directly. This would remove the need for the optional prop on the component itself.

Example of mocking in ModelDetectionClient.test.tsx:

import { render, screen } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';

import { runModelDetection } from '@/lib/model-detection/runner';
import { ModelDetectionClient } from '@/app/docs/model-detection/components/ModelDetectionClient';

// Mock the runner module
vi.mock('@/lib/model-detection/runner');

describe('ModelDetectionClient', () => {
  test('...', async () => {
    // Use the mocked function
    vi.mocked(runModelDetection).mockResolvedValue(...);

    // Render the component without the prop
    render(<ModelDetectionClient />);

    // ... assertions
  });
});

Comment on lines +84 to +85
repeatedExactly: extractResponseTextFromAny(payload).includes(probe),
rawText: extractResponseTextFromAny(payload),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The extractResponseTextFromAny function is called twice here. To improve efficiency, you could call it once, store the result in a variable, and then reuse it.

    const rawText = extractResponseTextFromAny(payload)
    return {
      repeatedExactly: rawText.includes(probe),
      rawText,
    }

Comment on lines +21 to +23
const hits = familyObservations.filter(
(observation) => observation.repeatedExactly === false,
).length
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The variable hits is used to count instances where repeatedExactly === false. This implies a "hit" is when the model refuses to repeat the probe string, which is a valid fingerprinting technique. However, the name hits can be confusing for other developers. To improve code clarity and maintainability, consider adding a comment explaining that a "hit" in this context refers to a signature match via refusal.

Suggested change
const hits = familyObservations.filter(
(observation) => observation.repeatedExactly === false,
).length
// A "hit" signifies a signature match, occurring when the model refuses to repeat the probe string.
const hits = familyObservations.filter(
(observation) => observation.repeatedExactly === false,
).length

@@ -0,0 +1,67 @@
'use client'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Stray 'use client' directive in test file

The 'use client' string at line 1 is a Next.js boundary directive that is meaningless in a Vitest test file. Vitest evaluates it as a no-op expression, but it makes the file look like it is a Next.js client module and may confuse tooling or future contributors. Remove it.

Suggested change
'use client'
import { render, screen } from '@testing-library/react'
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/docs/model-detection/components/ModelDetectionClient.test.tsx
Line: 1

Comment:
**Stray `'use client'` directive in test file**

The `'use client'` string at line 1 is a Next.js boundary directive that is meaningless in a Vitest test file. Vitest evaluates it as a no-op expression, but it makes the file look like it is a Next.js client module and may confuse tooling or future contributors. Remove it.

```suggestion
import { render, screen } from '@testing-library/react'
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1 to +19
import { Suspense } from 'react'

import { ModelDetectionClient } from '@/app/docs/model-detection/components/ModelDetectionClient'

export default function ModelDetectionPage() {
return (
<Suspense
fallback={
<div className="flex min-w-0 flex-auto flex-col px-4 py-16 lg:pl-8 xl:px-16">
<div className="rounded-2xl border border-[var(--claude-smoke)]/30 bg-[var(--claude-paper)] p-4 text-sm text-[var(--claude-walnut)]/70 dark:bg-[var(--claude-sand)]">
正在加载检测页面...
</div>
</div>
}
>
<ModelDetectionClient />
</Suspense>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Suspense boundary will never activate

ModelDetectionClient is imported eagerly (not via React.lazy) and does not call use() or any other Suspense-triggering API. The Suspense wrapper and its fallback are dead code. If the intent is to lazy-load the client component to defer its JS bundle, wrap it with React.lazy and a dynamic import; otherwise remove the Suspense boundary.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/docs/model-detection/page.tsx
Line: 1-19

Comment:
**`Suspense` boundary will never activate**

`ModelDetectionClient` is imported eagerly (not via `React.lazy`) and does not call `use()` or any other Suspense-triggering API. The `Suspense` wrapper and its `fallback` are dead code. If the intent is to lazy-load the client component to defer its JS bundle, wrap it with `React.lazy` and a dynamic import; otherwise remove the `Suspense` boundary.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +83 to +86
return {
repeatedExactly: extractResponseTextFromAny(payload).includes(probe),
rawText: extractResponseTextFromAny(payload),
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 extractResponseTextFromAny invoked twice on the same payload

extractResponseTextFromAny(payload) is called once to compute repeatedExactly and again to store rawText. Extracting to a local variable avoids the redundant work.

Suggested change
return {
repeatedExactly: extractResponseTextFromAny(payload).includes(probe),
rawText: extractResponseTextFromAny(payload),
}
const rawText = extractResponseTextFromAny(payload)
return {
repeatedExactly: rawText.includes(probe),
rawText,
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/model-detection/runner.ts
Line: 83-86

Comment:
**`extractResponseTextFromAny` invoked twice on the same payload**

`extractResponseTextFromAny(payload)` is called once to compute `repeatedExactly` and again to store `rawText`. Extracting to a local variable avoids the redundant work.

```suggestion
  const rawText = extractResponseTextFromAny(payload)
  return {
    repeatedExactly: rawText.includes(probe),
    rawText,
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +40 to +49
if (normalizedBaseUrl.includes(':generateContent')) {
return normalizedBaseUrl
}

const normalizedModel = normalizeGeminiModel(model)
if (normalizedBaseUrl.endsWith(`/${version}`)) {
return `${normalizedBaseUrl}/models/${normalizedModel}:generateContent`
}

return `${normalizedBaseUrl}/${version}/models/${normalizedModel}:generateContent`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Both Gemini candidates become identical when the base URL already contains :generateContent

When baseUrl already includes :generateContent (e.g. https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent), resolveGeminiUrl returns normalizedBaseUrl unchanged for both calls — the version and model arguments are ignored entirely. The two RequestCandidate entries produced by buildGeminiCandidates therefore share the same URL but different labels. The runner will hit the same endpoint twice with no fallback benefit, and if the first attempt returns an empty body it will retry the identical URL rather than a different API version.

Consider returning null (and filtering) when the URL is already fully qualified, or at minimum add a comment acknowledging the degenerate case so future readers understand why both candidates can be the same URL.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/model-detection/providers.ts
Line: 40-49

Comment:
**Both Gemini candidates become identical when the base URL already contains `:generateContent`**

When `baseUrl` already includes `:generateContent` (e.g. `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent`), `resolveGeminiUrl` returns `normalizedBaseUrl` unchanged for **both** calls — the `version` and `model` arguments are ignored entirely. The two `RequestCandidate` entries produced by `buildGeminiCandidates` therefore share the same URL but different labels. The runner will hit the same endpoint twice with no fallback benefit, and if the first attempt returns an empty body it will retry the identical URL rather than a different API version.

Consider returning `null` (and filtering) when the URL is already fully qualified, or at minimum add a comment acknowledging the degenerate case so future readers understand why both candidates can be the same URL.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2013d2fd53

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

return {
repeatedExactly: extractResponseTextFromAny(payload).includes(probe),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use exact match for probe repeat classification

runCandidateRequest marks repeatedExactly using includes(probe), so outputs like "Here is the text: <probe>" are treated as exact repeats even though they violate the prompt contract. Because scoring counts repeatedExactly === false as a hit, this substring check suppresses true hits whenever providers add wrappers/prefixes/suffixes, producing materially inaccurate family probabilities.

Useful? React with 👍 / 👎.

Comment on lines +23 to +27
if (normalizedBaseUrl.endsWith(path)) {
return normalizedBaseUrl
}

return `${normalizedBaseUrl}${path}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid duplicating API version segments in request URLs

ensurePath blindly appends the target path unless the base URL already ends with that full path, so a common base like https://api.openai.com/v1 becomes .../v1/v1/responses (and similarly for other endpoints). In that input shape every probe request fails with 404, making the detector unusable despite otherwise valid credentials and model settings.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
src/lib/navigation.test.ts (1)

8-15: 建议把断言范围收敛到“参考文档”分组。

当前只验证“全局存在”,不能防止链接被错误移动到其它分组后仍通过。建议先定位 title === '参考文档',再断言其 links

♻️ 建议修改
 describe('navigation', () => {
   test('包含响应模型检测入口', () => {
-    expect(
-      navigation.some((section) =>
-        section.links.some(
-          (link) =>
-            link.title === '响应模型检测' &&
-            link.href === '/docs/model-detection',
-        ),
-      ),
-    ).toBe(true)
+    const referenceSection = navigation.find(
+      (section) => section.title === '参考文档',
+    )
+    expect(referenceSection).toBeDefined()
+    expect(
+      referenceSection?.links.some(
+        (link) =>
+          link.title === '响应模型检测' &&
+          link.href === '/docs/model-detection',
+      ),
+    ).toBe(true)
   })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/navigation.test.ts` around lines 8 - 15, The test currently asserts
globally via navigation.some(...) which allows the target link to pass if placed
in any group; change the assertion to first find the specific section by
matching section.title === '参考文档' (use the same navigation and section variables
from the test), then assert that that section.links contains a link object where
link.title === '响应模型检测' and link.href === '/docs/model-detection' so the check
is scoped to the “参考文档” group only.
src/app/docs/model-detection/components/ModelDetectionClient.tsx (1)

170-172: 类型断言可考虑添加运行时验证

event.target.value 直接断言为 DetectionEndpointType 虽然在当前场景下是安全的(因为 <option> 值是硬编码的),但如果未来选项变更,可能引入不一致。

可选的防御性改进:

♻️ 可选:添加运行时类型守卫
+const ENDPOINT_TYPES = ['openai', 'anthropic', 'gemini'] as const
+
+function isDetectionEndpointType(value: string): value is DetectionEndpointType {
+  return (ENDPOINT_TYPES as readonly string[]).includes(value)
+}
+
 onChange={(event) =>
-  setEndpointType(event.target.value as DetectionEndpointType)
+  {
+    const value = event.target.value
+    if (isDetectionEndpointType(value)) {
+      setEndpointType(value)
+    }
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/docs/model-detection/components/ModelDetectionClient.tsx` around
lines 170 - 172, The onChange handler for the endpoint selector directly casts
event.target.value to DetectionEndpointType via
setEndpointType(event.target.value as DetectionEndpointType); add a runtime
guard to avoid unsafe casts: implement an isDetectionEndpointType(value:
string): value is DetectionEndpointType type-guard (or a Set/array of allowed
values) and call it inside the onChange handler (or a small helper like
handleEndpointChange) to validate the incoming value before calling
setEndpointType; if the value is invalid, either ignore it or log/handle the
fallback case.
src/lib/model-detection/runner.ts (2)

83-86: 重复调用 extractResponseTextFromAny

extractResponseTextFromAny(payload) 在同一个 payload 上被调用了两次。虽然函数是纯函数且开销不大,但可以缓存结果。

♻️ 可选:缓存提取结果
+  const rawText = extractResponseTextFromAny(payload)
   return {
-    repeatedExactly: extractResponseTextFromAny(payload).includes(probe),
-    rawText: extractResponseTextFromAny(payload),
+    repeatedExactly: rawText.includes(probe),
+    rawText,
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/model-detection/runner.ts` around lines 83 - 86, The code calls
extractResponseTextFromAny(payload) twice when building the returned object;
cache its result in a local const (e.g., const text =
extractResponseTextFromAny(payload)) and then set repeatedExactly:
text.includes(probe) and rawText: text to avoid duplicate calls; update the
function/closure that returns repeatedExactly/rawText accordingly.

145-150: 并发工作器中的游标访问存在潜在风险

虽然 JavaScript 是单线程的,但在 await mapper(...) 暂停期间,多个 worker 可能交错执行。当前实现在 await 之前读取并递增 cursor,这在 JS 中是安全的,因为 while 检查到赋值之间没有 await

但这个模式容易在重构时引入问题。建议添加注释说明或使用更明确的索引分配方式。

♻️ 可选:使用更明确的模式
 async function worker() {
   while (cursor < items.length) {
+    // 注意:cursor 读取和递增必须在同一同步代码块内,
+    // 在 await 之前完成,以确保多个 worker 不会处理同一索引
     const currentIndex = cursor
     cursor += 1
     results[currentIndex] = await mapper(items[currentIndex])
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/model-detection/runner.ts` around lines 145 - 150, The worker() loop
risks future race conditions if refactored because cursor is read and
incremented around an await; update worker to perform an explicit atomic index
assignment before any await (e.g., capture an index via a single
fetch-and-increment operation on cursor and use that index for items/results)
and/or add a clear comment in worker explaining that the index must be reserved
before awaiting; reference the worker function and variables cursor, items,
mapper, and results so reviewers can find and verify the change.
src/lib/model-detection/providers.ts (1)

159-159: 常量命名可能引起混淆

ANTHROPIC_SYSTEM 被 Gemini 候选构建器复用。虽然内容通用,但命名暗示它是 Anthropic 专用的。

♻️ 可选:重命名为更通用的名称
-const ANTHROPIC_SYSTEM =
+const SIMPLE_COPY_SYSTEM =
   'Copy the target text exactly once. Return only the original text.'

然后在 Anthropic 和 Gemini 构建器中都使用 SIMPLE_COPY_SYSTEM

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/model-detection/providers.ts` at line 159, Rename the constant
ANTHROPIC_SYSTEM to a more generic name (e.g. SIMPLE_COPY_SYSTEM) and update all
usages such as the line constructing prompt in providers.ts (`const prompt =
\`${ANTHROPIC_SYSTEM}\n\n${buildProbePrompt(probe)}\``) and any Anthropic/Gemini
builder modules to reference the new SIMPLE_COPY_SYSTEM identifier so both
builders share the same generic system prompt; ensure imports/exports and any
tests are updated to the new name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/docs/model-detection/components/ModelDetectionClient.tsx`:
- Around line 170-172: The onChange handler for the endpoint selector directly
casts event.target.value to DetectionEndpointType via
setEndpointType(event.target.value as DetectionEndpointType); add a runtime
guard to avoid unsafe casts: implement an isDetectionEndpointType(value:
string): value is DetectionEndpointType type-guard (or a Set/array of allowed
values) and call it inside the onChange handler (or a small helper like
handleEndpointChange) to validate the incoming value before calling
setEndpointType; if the value is invalid, either ignore it or log/handle the
fallback case.

In `@src/lib/model-detection/providers.ts`:
- Line 159: Rename the constant ANTHROPIC_SYSTEM to a more generic name (e.g.
SIMPLE_COPY_SYSTEM) and update all usages such as the line constructing prompt
in providers.ts (`const prompt =
\`${ANTHROPIC_SYSTEM}\n\n${buildProbePrompt(probe)}\``) and any Anthropic/Gemini
builder modules to reference the new SIMPLE_COPY_SYSTEM identifier so both
builders share the same generic system prompt; ensure imports/exports and any
tests are updated to the new name.

In `@src/lib/model-detection/runner.ts`:
- Around line 83-86: The code calls extractResponseTextFromAny(payload) twice
when building the returned object; cache its result in a local const (e.g.,
const text = extractResponseTextFromAny(payload)) and then set repeatedExactly:
text.includes(probe) and rawText: text to avoid duplicate calls; update the
function/closure that returns repeatedExactly/rawText accordingly.
- Around line 145-150: The worker() loop risks future race conditions if
refactored because cursor is read and incremented around an await; update worker
to perform an explicit atomic index assignment before any await (e.g., capture
an index via a single fetch-and-increment operation on cursor and use that index
for items/results) and/or add a clear comment in worker explaining that the
index must be reserved before awaiting; reference the worker function and
variables cursor, items, mapper, and results so reviewers can find and verify
the change.

In `@src/lib/navigation.test.ts`:
- Around line 8-15: The test currently asserts globally via navigation.some(...)
which allows the target link to pass if placed in any group; change the
assertion to first find the specific section by matching section.title ===
'参考文档' (use the same navigation and section variables from the test), then
assert that that section.links contains a link object where link.title ===
'响应模型检测' and link.href === '/docs/model-detection' so the check is scoped to the
“参考文档” group only.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9a261c1b-2a69-4d09-93d7-945f80a98846

📥 Commits

Reviewing files that changed from the base of the PR and between efdb6e4 and 2013d2f.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • package.json
  • src/app/docs/model-detection/components/ModelDetectionClient.test.tsx
  • src/app/docs/model-detection/components/ModelDetectionClient.tsx
  • src/app/docs/model-detection/page.tsx
  • src/lib/model-detection/families.ts
  • src/lib/model-detection/providers.test.ts
  • src/lib/model-detection/providers.ts
  • src/lib/model-detection/runner.ts
  • src/lib/model-detection/scoring.test.ts
  • src/lib/model-detection/scoring.ts
  • src/lib/model-detection/types.ts
  • src/lib/navigation.test.ts
  • src/lib/navigation.ts
  • vitest.config.ts
  • vitest.setup.ts

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 8, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
claude-code-hub-docs 8828bde Apr 08 2026, 06:01 PM

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a6524aaee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return `${normalizedBaseUrl}/models/${normalizedModel}:generateContent`
}

return `${normalizedBaseUrl}/${version}/models/${normalizedModel}:generateContent`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Support Gemini model-scoped base URLs without path duplication

resolveGeminiUrl only handles two Gemini input shapes (root .../v1/.../v1beta and fully-qualified ...:generateContent), so a common model-scoped base URL like https://.../v1beta/models/gemini-2.0-flash falls into the final return path and becomes .../v1beta/models/gemini-2.0-flash/v1beta/models/<model>:generateContent. In that case every probe request hits a malformed endpoint and detection fails even though the user provided a valid Gemini model URL and key.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 8, 2026

Tip:

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.

Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/model-detection/providers.ts`:
- Around line 21-46: ensurePath currently lets callers paste a full endpoint
like "/v1/chat/completions" which then gets appended to other candidates and
corrupts fallbacks; update ensurePath so before computing targetSegments you
normalize the input (use trimTrailingSlash) and if the first target segment
matches a version like /^v\d+$/ then truncate targetSegments to only that
version segment (i.e., treat "/v1/..." as just "/v1"), then continue the
overlap/merge logic with baseSegments and targetSegments; reference ensurePath,
trimTrailingSlash, targetSegments and baseSegments when making the change.

In `@src/lib/model-detection/runner.ts`:
- Around line 68-88: The runCandidateRequest function lacks a timeout for fetch;
wrap the fetch call with an AbortController and a timer so requests are aborted
after a short configurable timeout, clear the timer on success, and pass
controller.signal into fetch; ensure the abort error is propagated the same way
as other failures so readPayload/extractResponseTextFromAny callers and the
existing error aggregation see the failure (i.e., throw an Error when the
controller aborts or when fetch rejects due to the timeout). Update references
in runCandidateRequest to use the controller.signal and to clear the timeout
before returning.
- Around line 102-107: buildRequestCandidates(...) can throw (e.g., new
URL(input.baseUrl)) and currently that rejection will short-circuit
mapWithConcurrency; wrap the call to buildRequestCandidates in a try/catch
inside runProbe and on error return a ProbeObservation representing the failure
(include probe, an empty/failed candidates list and the error message) instead
of letting the exception propagate; ensure runProbe always resolves to a
ProbeObservation so mapWithConcurrency / runModelDetection can continue
aggregating per-probe results.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c13cb20f-04cc-4e6e-b953-0ffae2e8381c

📥 Commits

Reviewing files that changed from the base of the PR and between 2013d2f and 4a6524a.

📒 Files selected for processing (6)
  • src/app/docs/model-detection/components/ModelDetectionClient.tsx
  • src/lib/model-detection/providers.test.ts
  • src/lib/model-detection/providers.ts
  • src/lib/model-detection/runner.ts
  • src/lib/model-detection/scoring.ts
  • src/lib/navigation.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/navigation.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/model-detection/scoring.ts
  • src/app/docs/model-detection/components/ModelDetectionClient.tsx

Comment on lines +21 to +46
function ensurePath(baseUrl: string, path: string) {
const normalizedBaseUrl = trimTrailingSlash(baseUrl)
if (normalizedBaseUrl.endsWith(path)) {
return normalizedBaseUrl
}

const url = new URL(normalizedBaseUrl)
const baseSegments = url.pathname.split('/').filter(Boolean)
const targetSegments = path.split('/').filter(Boolean)

let overlap = 0
for (
let size = Math.min(baseSegments.length, targetSegments.length);
size > 0;
size -= 1
) {
const baseTail = baseSegments.slice(-size).join('/')
const targetHead = targetSegments.slice(0, size).join('/')
if (baseTail === targetHead) {
overlap = size
break
}
}

url.pathname = `/${[...baseSegments, ...targetSegments.slice(overlap)].join('/')}`
return trimTrailingSlash(url.toString())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

完整 endpoint 输入会把回退 URL 拼坏。

如果这里允许用户直接粘贴完整的 /v1/... endpoint,而不只是根路径,ensurePath() 在切到其他候选路径时会把已有终端路径继续拼在后面。比如传入 /v1/chat/completions 时,另一个候选会变成 .../v1/chat/completions/v1/responses。这样首选候选一旦失败,fallback 就失效了。建议在拼接前先剥离已知终端路径,或者在入口把该字段明确限制为真正的 base URL。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/model-detection/providers.ts` around lines 21 - 46, ensurePath
currently lets callers paste a full endpoint like "/v1/chat/completions" which
then gets appended to other candidates and corrupts fallbacks; update ensurePath
so before computing targetSegments you normalize the input (use
trimTrailingSlash) and if the first target segment matches a version like
/^v\d+$/ then truncate targetSegments to only that version segment (i.e., treat
"/v1/..." as just "/v1"), then continue the overlap/merge logic with
baseSegments and targetSegments; reference ensurePath, trimTrailingSlash,
targetSegments and baseSegments when making the change.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3dccf07bc3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return `${normalizedBaseUrl}/models/${normalizedModel}:generateContent`
}

return `${normalizedBaseUrl}/${version}/models/${normalizedModel}:generateContent`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip incompatible Gemini API version candidates

When baseUrl already includes a fixed Gemini version (for example https://relay.example.com/v1), resolveGeminiUrl still constructs a candidate for the other version via the fallback return path, producing URLs like /v1/v1beta/models/.... In runProbe this malformed candidate is tried first for /v1 inputs, so every probe incurs a guaranteed failure before the valid candidate, which materially increases request volume/latency and can trigger avoidable rate-limit failures on large probe runs.

Useful? React with 👍 / 👎.

familyId: string,
probe: string,
): Promise<ProbeObservation> {
const candidates = buildRequestCandidates({ ...input, probe })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Catch candidate-construction errors inside runProbe

buildRequestCandidates is invoked before entering the per-candidate try/catch, so invalid baseUrl values (for example missing protocol) throw from new URL(...) and reject the mapper directly. Because mapWithConcurrency uses Promise.all, that single throw aborts the whole detection batch instead of being recorded as probe-level failures, preventing the structured error path from running.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8828bdef8f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +84 to +88
if (normalizedBaseUrl.endsWith(`/${version}`)) {
return `${normalizedBaseUrl}/models/${normalizedModel}:generateContent`
}

return `${normalizedBaseUrl}/${version}/models/${normalizedModel}:generateContent`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Build Gemini URLs with URL parsing, not string concat

When baseUrl includes query parameters (for example a copied Gemini endpoint like .../v1beta?key=... or a signed relay URL), this branch appends /${version}/models/... directly to the raw string and produces malformed URLs such as .../v1beta?key=abc/v1beta/models/.... In that input shape every Gemini probe request targets an invalid endpoint and the detector reports total failure even though the base host/model are valid.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant