Skip to content

feat: 添加模型价格同步功能#17

Merged
sxjeru merged 2 commits intosxjeru:pr17from
ZIC143:main
Jan 27, 2026
Merged

feat: 添加模型价格同步功能#17
sxjeru merged 2 commits intosxjeru:pr17from
ZIC143:main

Conversation

@ZIC143
Copy link
Contributor

@ZIC143 ZIC143 commented Jan 26, 2026

  • 新增"模型价格"按钮,从 models.dev 获取最新价格并同步到本地数据库,不会自动更新,只有按钮手动更新
  • 新增 /api/sync-model-prices API 路由,支持精确匹配、前缀匹配和模糊匹配
  • 修复 hydration 错误:将 rangeInit 改为客户端挂载后从 localStorage 恢复

Summary by Sourcery

从 models.dev 手动同步模型价格到本地数据库,并修复日期范围在水合过程中的行为问题。

新功能:

  • 在仪表盘中新增按钮,用于触发模型价格同步,并在模态框中展示详细的同步结果,同时通过 toast 通知反馈状态。
  • 新增 /api/sync-model-prices 接口,从 models.dev 获取价格信息,将其与可用模型进行匹配,并将价格 upsert 到本地数据库中。

错误修复:

  • 调整日期范围状态的初始化逻辑:在首次渲染时使用固定的后备值,并在客户端挂载后从 localStorage 恢复用户选择,以避免水合不匹配。
  • 仅在客户端挂载完成后显示上次同步时间,以避免水合警告。

改进:

  • 追踪并展示每个模型的同步状态(已更新、已跳过、失败),并提供汇总统计信息,以便更清晰地了解价格更新情况。
Original summary in English

Summary by Sourcery

Add manual model price synchronization from models.dev to the local database and fix date range hydration behavior.

New Features:

  • Add a dashboard button to trigger model price synchronization and display detailed sync results in a modal with toast notifications.
  • Introduce a /api/sync-model-prices endpoint that fetches pricing from models.dev, matches it to available models, and upserts prices into the local database.

Bug Fixes:

  • Adjust date range state initialization to use a fixed fallback on first render and restore user selections from localStorage on client mount to prevent hydration mismatches.
  • Show last sync time only after client mount to avoid hydration warnings.

Enhancements:

  • Track and present per-model sync status (updated, skipped, failed) with summary statistics for better visibility into price updates.

- 新增"模型价格"按钮,从 models.dev 获取最新价格并同步到本地数据库
- 新增 /api/sync-model-prices API 路由,支持精确匹配、前缀匹配和模糊匹配
- 修复 hydration 错误:将 rangeInit 改为客户端挂载后从 localStorage 恢复
@vercel
Copy link

vercel bot commented Jan 26, 2026

@Zic-Wang is attempting to deploy a commit to the sxjeru's projects Team on Vercel.

A member of the Team first needs to authorize it.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 26, 2026

审阅者指南

添加了一个手动“模型价格”同步流程:从 models.dev 获取模型价格,使用多种策略与 CLIProxy 模型进行匹配,将结果持久化到本地的 modelPrices 表中,并通过仪表盘按钮、toast 提示和弹窗展示进度与结果;同时修复了日期范围 hydration 问题,并简化了上次同步时间的渲染逻辑。

手动模型价格同步流程的时序图

sequenceDiagram
    actor User
    participant DashboardPage
    participant SyncModelPricesAPI as Api_sync_model_prices
    participant ModelsDev as Models_dev
    participant CLIProxyAPI
    participant Postgres as Postgres_modelPrices

    User->>DashboardPage: click_模型价格_button
    DashboardPage->>DashboardPage: syncModelPrices()
    DashboardPage->>DashboardPage: resolve_apiKey_from_filterRouteInput_or_routeOptions
    alt no_apiKey
        DashboardPage->>DashboardPage: setPricesSyncStatus_and_pricesSyncData_error
        DashboardPage-->>User: show_error_toast_and_modal
    else has_apiKey
        DashboardPage->>DashboardPage: setSyncingPrices_true_and_open_modal
        DashboardPage->>SyncModelPricesAPI: POST /api/sync-model-prices {apiKey}

        Note over SyncModelPricesAPI: validate_config_and_apiKey
        SyncModelPricesAPI->>ModelsDev: GET https://models.dev/api.json
        ModelsDev-->>SyncModelPricesAPI: models_dev_json
        SyncModelPricesAPI->>SyncModelPricesAPI: build_priceMap_from_models

        SyncModelPricesAPI->>CLIProxyAPI: GET {baseUrl}/v1/models
        CLIProxyAPI-->>SyncModelPricesAPI: models_list
        SyncModelPricesAPI->>SyncModelPricesAPI: match_models_exact_prefix_fuzzy

        loop for_each_model
            alt price_found
                SyncModelPricesAPI->>Postgres: upsert_into_modelPrices
                Postgres-->>SyncModelPricesAPI: ok_or_error
            else price_not_found
                SyncModelPricesAPI->>SyncModelPricesAPI: record_skipped_detail
            end
        end

        SyncModelPricesAPI-->>DashboardPage: summary_and_details_json
        DashboardPage->>DashboardPage: setPricesSyncData
        alt res_ok
            DashboardPage->>DashboardPage: setPricesSyncStatus_with_summary
        else res_error
            DashboardPage->>DashboardPage: setPricesSyncStatus_with_error
        end
        DashboardPage->>DashboardPage: setSyncingPrices_false
        DashboardPage-->>User: update_modal_and_toast
    end
Loading

新的 sync-model-prices 类型与仪表盘状态的类图

classDiagram
    class ModelsDevModel {
      +string id
      +number cost_input
      +number cost_output
      +number cost_cache_read
    }

    class ModelsDevProvider {
      +Record_models Record~string,ModelsDevModel~
    }

    class ModelsDevResponse {
      +Record_providers Record~string,ModelsDevProvider~
    }

    class ModelPriceRecord {
      +string model
      +string inputPricePer1M
      +string cachedInputPricePer1M
      +string outputPricePer1M
    }

    class PricesSyncSummary {
      +number total
      +number updated
      +number skipped
      +number failed
    }

    class PricesSyncDetail {
      +string model
      +string status
      +string matchedWith
      +string reason
    }

    class PricesSyncData {
      +PricesSyncSummary summary
      +PricesSyncDetail[] details
      +string error
    }

    class DashboardPageComponent {
      +boolean mounted
      +boolean syncingPrices
      +string pricesSyncStatus
      +boolean pricesSyncModalOpen
      +PricesSyncData pricesSyncData
      +string filterRouteInput
      +string[] routeOptions
      +syncModelPrices()
    }

    class SyncModelPricesRoute {
      +POST(request)
      -validateApiKey()
      -loadModelsDevData()
      -buildPriceMap()
      -loadCliProxyModels()
      -matchAndUpsertPrices()
    }

    ModelsDevProvider --> ModelsDevModel : contains
    ModelsDevResponse --> ModelsDevProvider : providers

    SyncModelPricesRoute --> ModelsDevResponse : uses
    SyncModelPricesRoute --> ModelPriceRecord : upserts

    DashboardPageComponent --> PricesSyncData : holds
    DashboardPageComponent --> SyncModelPricesRoute : calls_POST
Loading

文件级改动

Change Details Files
重构日期范围状态初始化逻辑,通过使用固定的 SSR 默认值并仅在挂载后从 localStorage 恢复,避免 hydration 不匹配。
  • 引入 fallbackRange,从固定的默认开始/结束日期计算得出,在服务端和客户端首次渲染时保持一致。
  • 用直接的 useState 调用(以 fallbackRange 为初始值)替换原先的 rangeInit 惰性状态初始化器。
  • 添加挂载时的 effect,从 localStorage 读取并校验 rangeSelection,再据此更新 rangeModerangeDaysappliedDayscustomStartcustomEnd
  • customDraftStart/customDraftEnd 的初始值从 fallbackRange 读取,而不是使用已移除的 rangeInit
app/page.tsx
在仪表盘 UI 中新增手动模型价格同步工作流,包括触发按钮、状态 toast 和详细结果弹窗。
  • 新增 syncingPricespricesSyncStatuspricesSyncModalOpenpricesSyncDatapricesSyncStatusTimerRef 等 state/ref,用于跟踪同步进度和 UI 状态。
  • 实现 syncModelPrices 回调,负责解析 API key、调用新的 /api/sync-model-prices 接口,并根据响应或错误更新 pricesSyncDatapricesSyncStatus
  • 在头部渲染新的“模型价格”按钮,点击后调用 syncModelPrices,并使用 DollarSign 图标展示加载状态。
  • 展示可关闭的 toast,针对成功/失败进行不同样式,反映 pricesSyncStatus,并通过定时器自动隐藏。
  • 渲染一个 Modal,用于展示本次同步操作的汇总统计、逐模型详情(statusmatchedWithreason),以及错误信息。
app/page.tsx
调整上次同步时间的显示方式,仅在客户端挂载后再渲染,避免 hydration 警告。
  • 使用 mounted && lastSyncTime 来控制 lastSyncTime 标签渲染,而不是依赖 suppressHydrationWarning
  • mounted 为 true 时,直接通过 lastSyncTime.toLocaleTimeString 渲染本地化时间。
app/page.tsx
新增一个服务端 API 路由,从 models.dev 获取模型价格,与 CLIProxy 模型进行匹配,并将结果 upsert 到 modelPrices 表。
  • 添加运行于 nodejs runtime 的 /api/sync-model-prices POST 处理器,要求请求体中必须提供 apiKey,并校验 CLIPROXY_API_BASE_URLDATABASE_URL 配置。
  • 获取并解析 https://models.dev/api.json 为 provider/models 结构,并基于此构建从模型 id 到每百万 token 成本(输入、输出、cache_read)的映射。
  • 使用提供的 apiKey 调用 CLIProxy /v1/models 接口以获取当前模型列表。
  • 对于每个 CLIProxy 模型 id,依次尝试:精确匹配、去前缀匹配(使用最后一个路径片段)、以及基于 baseModelName 的模糊匹配,以在价格映射中找到对应项。
  • 通过 db.insert(...).onConflictDoUpdate 将匹配到的价格 upsert 到 modelPrices 表中,同时跟踪 updatedskippedfailed 的计数,并构建逐模型的详情数组。
  • 返回 JSON 响应,其中包含 success 标记、汇总统计(totalupdatedskippedfailed)以及逐模型详情;在配置错误或模型获取失败时返回相应的错误响应,并在顶层提供 500 错误处理。
app/api/sync-model-prices/route.ts

技巧与命令

与 Sourcery 交互

  • 触发新的代码审阅: 在 Pull Request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 从审阅评论生成 GitHub Issue: 在审阅评论下回复,请求 Sourcery 从该评论创建 issue。你也可以直接回复 @sourcery-ai issue,从该评论创建 issue。
  • 生成 Pull Request 标题: 在 PR 标题中任意位置写上 @sourcery-ai,即可随时生成标题。也可以在 PR 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 PR 正文任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。也可以在 PR 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅者指南: 在 Pull Request 中评论 @sourcery-ai guide,即可(重新)生成审阅者指南。
  • 一次性标记所有 Sourcery 评论为已解决: 在 Pull Request 中评论 @sourcery-ai resolve,将所有 Sourcery 评论标记为已解决。适用于你已经处理了所有评论且不希望继续看到它们的情况。
  • 隐藏所有 Sourcery 审阅: 在 Pull Request 中评论 @sourcery-ai dismiss,即可隐藏所有现有的 Sourcery 审阅。尤其适用于你希望用一次全新的审阅重新开始——别忘了再评论 @sourcery-ai review 来触发新的审阅!

自定义你的体验

访问你的 dashboard 来:

  • 启用或禁用审阅功能,例如 Sourcery 生成的 PR 摘要、审阅者指南等。
  • 修改审阅语言。
  • 添加、移除或编辑自定义审阅指令。
  • 调整其他审阅设置。

获取帮助

Original review guide in English

Reviewer's Guide

Adds a manual "模型价格" sync flow that fetches model prices from models.dev, matches them against CLIProxy models with multiple strategies, persists them to the local modelPrices table, and surfaces progress and results via a dashboard button, toast, and modal; also fixes a date range hydration issue and simplifies last-sync rendering.

Sequence diagram for manual model price sync flow

sequenceDiagram
    actor User
    participant DashboardPage
    participant SyncModelPricesAPI as Api_sync_model_prices
    participant ModelsDev as Models_dev
    participant CLIProxyAPI
    participant Postgres as Postgres_modelPrices

    User->>DashboardPage: click_模型价格_button
    DashboardPage->>DashboardPage: syncModelPrices()
    DashboardPage->>DashboardPage: resolve_apiKey_from_filterRouteInput_or_routeOptions
    alt no_apiKey
        DashboardPage->>DashboardPage: setPricesSyncStatus_and_pricesSyncData_error
        DashboardPage-->>User: show_error_toast_and_modal
    else has_apiKey
        DashboardPage->>DashboardPage: setSyncingPrices_true_and_open_modal
        DashboardPage->>SyncModelPricesAPI: POST /api/sync-model-prices {apiKey}

        Note over SyncModelPricesAPI: validate_config_and_apiKey
        SyncModelPricesAPI->>ModelsDev: GET https://models.dev/api.json
        ModelsDev-->>SyncModelPricesAPI: models_dev_json
        SyncModelPricesAPI->>SyncModelPricesAPI: build_priceMap_from_models

        SyncModelPricesAPI->>CLIProxyAPI: GET {baseUrl}/v1/models
        CLIProxyAPI-->>SyncModelPricesAPI: models_list
        SyncModelPricesAPI->>SyncModelPricesAPI: match_models_exact_prefix_fuzzy

        loop for_each_model
            alt price_found
                SyncModelPricesAPI->>Postgres: upsert_into_modelPrices
                Postgres-->>SyncModelPricesAPI: ok_or_error
            else price_not_found
                SyncModelPricesAPI->>SyncModelPricesAPI: record_skipped_detail
            end
        end

        SyncModelPricesAPI-->>DashboardPage: summary_and_details_json
        DashboardPage->>DashboardPage: setPricesSyncData
        alt res_ok
            DashboardPage->>DashboardPage: setPricesSyncStatus_with_summary
        else res_error
            DashboardPage->>DashboardPage: setPricesSyncStatus_with_error
        end
        DashboardPage->>DashboardPage: setSyncingPrices_false
        DashboardPage-->>User: update_modal_and_toast
    end
Loading

Class diagram for new sync-model-prices types and dashboard state

classDiagram
    class ModelsDevModel {
      +string id
      +number cost_input
      +number cost_output
      +number cost_cache_read
    }

    class ModelsDevProvider {
      +Record_models Record~string,ModelsDevModel~
    }

    class ModelsDevResponse {
      +Record_providers Record~string,ModelsDevProvider~
    }

    class ModelPriceRecord {
      +string model
      +string inputPricePer1M
      +string cachedInputPricePer1M
      +string outputPricePer1M
    }

    class PricesSyncSummary {
      +number total
      +number updated
      +number skipped
      +number failed
    }

    class PricesSyncDetail {
      +string model
      +string status
      +string matchedWith
      +string reason
    }

    class PricesSyncData {
      +PricesSyncSummary summary
      +PricesSyncDetail[] details
      +string error
    }

    class DashboardPageComponent {
      +boolean mounted
      +boolean syncingPrices
      +string pricesSyncStatus
      +boolean pricesSyncModalOpen
      +PricesSyncData pricesSyncData
      +string filterRouteInput
      +string[] routeOptions
      +syncModelPrices()
    }

    class SyncModelPricesRoute {
      +POST(request)
      -validateApiKey()
      -loadModelsDevData()
      -buildPriceMap()
      -loadCliProxyModels()
      -matchAndUpsertPrices()
    }

    ModelsDevProvider --> ModelsDevModel : contains
    ModelsDevResponse --> ModelsDevProvider : providers

    SyncModelPricesRoute --> ModelsDevResponse : uses
    SyncModelPricesRoute --> ModelPriceRecord : upserts

    DashboardPageComponent --> PricesSyncData : holds
    DashboardPageComponent --> SyncModelPricesRoute : calls_POST
Loading

File-Level Changes

Change Details Files
Refactor date-range state initialization to avoid hydration mismatch by using fixed SSR defaults and restoring from localStorage only after mount.
  • Introduce fallbackRange computed from a fixed default start/end date used for both server and first client render.
  • Replace rangeInit lazy state initializer with direct useState hooks seeded from fallbackRange.
  • Add a mount-time effect that reads and validates rangeSelection from localStorage and updates rangeMode, rangeDays, appliedDays, customStart, and customEnd accordingly.
  • Initialize customDraftStart/customDraftEnd from fallbackRange instead of the removed rangeInit.
app/page.tsx
Add a manual model price synchronization workflow in the dashboard UI, including trigger button, status toast, and detailed results modal.
  • Add syncingPrices, pricesSyncStatus, pricesSyncModalOpen, pricesSyncData, and pricesSyncStatusTimerRef state/refs to track sync progress and UI state.
  • Implement syncModelPrices callback that resolves an API key, calls the new /api/sync-model-prices endpoint, and updates pricesSyncData and pricesSyncStatus based on the response or errors.
  • Render a new "模型价格" button in the header that calls syncModelPrices and shows loading state using the DollarSign icon.
  • Display a dismissible toast styled for success/failure that reflects pricesSyncStatus and auto-hides with a timer.
  • Render a Modal showing summary stats, per-model details (status, matchedWith, reason), and error message for the last sync operation.
app/page.tsx
Adjust last-sync time display to only render on the client after mount, avoiding hydration warnings.
  • Guard the lastSyncTime label rendering with mounted && lastSyncTime instead of using suppressHydrationWarning.
  • Render the localized time directly from lastSyncTime.toLocaleTimeString when mounted is true.
app/page.tsx
Introduce a server-side API route that fetches model prices from models.dev, matches them to CLIProxy models, and upserts them into the modelPrices table.
  • Add /api/sync-model-prices POST handler running in nodejs runtime that requires apiKey in the request body and validates CLIPROXY_API_BASE_URL and DATABASE_URL configuration.
  • Fetch and parse https://models.dev/api.json into a provider/models structure, building a map from model id to per-1M token cost (input, output, cache_read).
  • Call CLIProxy /v1/models with the provided apiKey to retrieve the current model list.
  • For each CLIProxy model id, attempt exact match, prefix-stripped match (using last path segment), then fuzzy match based on baseModelName against the price map.
  • Upsert matching prices into the modelPrices table via db.insert(...).onConflictDoUpdate, tracking updated, skipped, and failed counts and building a per-model details array.
  • Return a JSON payload containing success flag, summary counts (total, updated, skipped, failed), and per-model details, with appropriate error responses for configuration/model-fetch failures and a top-level 500 handler.
app/api/sync-model-prices/route.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 1 个问题,并留下了一些总体反馈:

  • syncModelPrices 中,toast 的样式和表情符号的选择是通过对状态消息做子串匹配来驱动的(例如 includes("失败") / includes("请先"))。建议改为维护一个单独的结构化状态类型(例如 'success' | 'error'),这样 UI 逻辑就不会和本地化的文案耦合在一起。
  • /api/sync-model-prices 的 POST 处理函数在对每个模型进行处理时,会在一个顺序执行的 for...of 循环里调用 await db.insert(...)。如果模型列表比较大,这可能会比较慢。建议考虑批量更新(例如带并发上限的 Promise.all,或单次批量 upsert/事务),以减少对数据库的往返请求次数。
给 AI Agent 的提示词
Please address the comments from this code review:

## Overall Comments
- In `syncModelPrices`, the toast style and emoji selection are driven by substring checks on the status message (e.g. `includes("失败")` / `includes("请先")`); consider tracking a separate structured status type (e.g. `'success' | 'error'`) so the UI logic isn’t coupled to localized message text.
- The `/api/sync-model-prices` POST handler performs `await db.insert(...)` inside a sequential `for...of` loop for every model; if the model list is large this could be slow, so consider batching updates (e.g. `Promise.all` with a concurrency limit or a single bulk upsert/transaction) to reduce round-trips to the database.

## Individual Comments

### Comment 1
<location> `app/api/sync-model-prices/route.ts:19-21` </location>
<code_context>
+
+type ModelsDevResponse = Record<string, ModelsDevProvider>;
+
+export async function POST(request: Request) {
+  try {
+    const { apiKey } = await request.json();
+
+    if (!apiKey) {
</code_context>

<issue_to_address>
**🚨 issue (security):** 同步端点信任由客户端提供的 `apiKey`,并且看起来允许在未认证的情况下更新 `modelPrices`,这存在风险。

任何能够访问 `/api/sync-model-prices` 并发送任意 `apiKey` 值的调用方,都可以更新 `modelPrices`,因为这里没有进行身份认证/会话检查,也没有在服务端验证该操作是否为内部/可信操作。这实际上是在公共路由上暴露了一个管理员级别的操作。请通过以下方式之一来加固安全性:要求适当的授权(例如使用你现有的认证/会话)、使用来自配置/环境变量的服务端凭证而不是客户端提供的 `apiKey`,或者以其他方式将该路由限制为可信调用方(例如仅限内部调用,或者通过共享密钥 header)。
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得我们的代码评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的代码评审。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • In syncModelPrices, the toast style and emoji selection are driven by substring checks on the status message (e.g. includes("失败") / includes("请先")); consider tracking a separate structured status type (e.g. 'success' | 'error') so the UI logic isn’t coupled to localized message text.
  • The /api/sync-model-prices POST handler performs await db.insert(...) inside a sequential for...of loop for every model; if the model list is large this could be slow, so consider batching updates (e.g. Promise.all with a concurrency limit or a single bulk upsert/transaction) to reduce round-trips to the database.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `syncModelPrices`, the toast style and emoji selection are driven by substring checks on the status message (e.g. `includes("失败")` / `includes("请先")`); consider tracking a separate structured status type (e.g. `'success' | 'error'`) so the UI logic isn’t coupled to localized message text.
- The `/api/sync-model-prices` POST handler performs `await db.insert(...)` inside a sequential `for...of` loop for every model; if the model list is large this could be slow, so consider batching updates (e.g. `Promise.all` with a concurrency limit or a single bulk upsert/transaction) to reduce round-trips to the database.

## Individual Comments

### Comment 1
<location> `app/api/sync-model-prices/route.ts:19-21` </location>
<code_context>
+
+type ModelsDevResponse = Record<string, ModelsDevProvider>;
+
+export async function POST(request: Request) {
+  try {
+    const { apiKey } = await request.json();
+
+    if (!apiKey) {
</code_context>

<issue_to_address>
**🚨 issue (security):** The sync endpoint trusts a client-provided `apiKey` and appears to allow unauthenticated updates to `modelPrices`, which is risky.

Any caller that can reach `/api/sync-model-prices` and send any `apiKey` value can update `modelPrices`, since there’s no auth/session check or server-side verification that this is an internal/trusted operation. This effectively exposes an admin operation on a public route. Please secure this by requiring proper authorization (e.g., your existing auth/session), using a server-side credential from config/env instead of a client-provided `apiKey`, or otherwise restricting the route to trusted callers only (e.g., internal-only or via a shared secret header).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +19 to +21
export async function POST(request: Request) {
try {
const { apiKey } = await request.json();
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 issue (security): 同步端点信任由客户端提供的 apiKey,并且看起来允许在未认证的情况下更新 modelPrices,这存在风险。

任何能够访问 /api/sync-model-prices 并发送任意 apiKey 值的调用方,都可以更新 modelPrices,因为这里没有进行身份认证/会话检查,也没有在服务端验证该操作是否为内部/可信操作。这实际上是在公共路由上暴露了一个管理员级别的操作。请通过以下方式之一来加固安全性:要求适当的授权(例如使用你现有的认证/会话)、使用来自配置/环境变量的服务端凭证而不是客户端提供的 apiKey,或者以其他方式将该路由限制为可信调用方(例如仅限内部调用,或者通过共享密钥 header)。

Original comment in English

🚨 issue (security): The sync endpoint trusts a client-provided apiKey and appears to allow unauthenticated updates to modelPrices, which is risky.

Any caller that can reach /api/sync-model-prices and send any apiKey value can update modelPrices, since there’s no auth/session check or server-side verification that this is an internal/trusted operation. This effectively exposes an admin operation on a public route. Please secure this by requiring proper authorization (e.g., your existing auth/session), using a server-side credential from config/env instead of a client-provided apiKey, or otherwise restricting the route to trusted callers only (e.g., internal-only or via a shared secret header).

@sxjeru sxjeru changed the base branch from main to pr17 January 27, 2026 07:00
@sxjeru
Copy link
Owner

sxjeru commented Jan 27, 2026

感谢 pr,不需要手动填价格还是很不错的。

@sxjeru sxjeru merged commit 97f3e0a into sxjeru:pr17 Jan 27, 2026
1 of 2 checks passed
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.

3 participants